summaryrefslogtreecommitdiffstats
path: root/toolkit/components/url-classifier/ProtocolParser.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/url-classifier/ProtocolParser.cpp')
-rw-r--r--toolkit/components/url-classifier/ProtocolParser.cpp1108
1 files changed, 1108 insertions, 0 deletions
diff --git a/toolkit/components/url-classifier/ProtocolParser.cpp b/toolkit/components/url-classifier/ProtocolParser.cpp
new file mode 100644
index 000000000..5da7787be
--- /dev/null
+++ b/toolkit/components/url-classifier/ProtocolParser.cpp
@@ -0,0 +1,1108 @@
+//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ProtocolParser.h"
+#include "LookupCache.h"
+#include "nsNetCID.h"
+#include "mozilla/Logging.h"
+#include "prnetdb.h"
+#include "prprf.h"
+
+#include "nsUrlClassifierDBService.h"
+#include "nsUrlClassifierUtils.h"
+#include "nsPrintfCString.h"
+#include "mozilla/Base64.h"
+#include "RiceDeltaDecoder.h"
+#include "mozilla/EndianUtils.h"
+
+// MOZ_LOG=UrlClassifierProtocolParser:5
+mozilla::LazyLogModule gUrlClassifierProtocolParserLog("UrlClassifierProtocolParser");
+#define PARSER_LOG(args) MOZ_LOG(gUrlClassifierProtocolParserLog, mozilla::LogLevel::Debug, args)
+
+namespace mozilla {
+namespace safebrowsing {
+
+// Updates will fail if fed chunks larger than this
+const uint32_t MAX_CHUNK_SIZE = (1024 * 1024);
+// Updates will fail if the total number of tocuhed chunks is larger than this
+const uint32_t MAX_CHUNK_RANGE = 1000000;
+
+const uint32_t DOMAIN_SIZE = 4;
+
+// Parse one stringified range of chunks of the form "n" or "n-m" from a
+// comma-separated list of chunks. Upon return, 'begin' will point to the
+// next range of chunks in the list of chunks.
+static bool
+ParseChunkRange(nsACString::const_iterator& aBegin,
+ const nsACString::const_iterator& aEnd,
+ uint32_t* aFirst, uint32_t* aLast)
+{
+ nsACString::const_iterator iter = aBegin;
+ FindCharInReadable(',', iter, aEnd);
+
+ nsAutoCString element(Substring(aBegin, iter));
+ aBegin = iter;
+ if (aBegin != aEnd)
+ aBegin++;
+
+ uint32_t numRead = PR_sscanf(element.get(), "%u-%u", aFirst, aLast);
+ if (numRead == 2) {
+ if (*aFirst > *aLast) {
+ uint32_t tmp = *aFirst;
+ *aFirst = *aLast;
+ *aLast = tmp;
+ }
+ return true;
+ }
+
+ if (numRead == 1) {
+ *aLast = *aFirst;
+ return true;
+ }
+
+ return false;
+}
+
+///////////////////////////////////////////////////////////////
+// ProtocolParser implementation
+
+ProtocolParser::ProtocolParser()
+ : mUpdateStatus(NS_OK)
+ , mUpdateWaitSec(0)
+{
+}
+
+ProtocolParser::~ProtocolParser()
+{
+ CleanupUpdates();
+}
+
+nsresult
+ProtocolParser::Init(nsICryptoHash* aHasher)
+{
+ mCryptoHash = aHasher;
+ return NS_OK;
+}
+
+void
+ProtocolParser::CleanupUpdates()
+{
+ for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
+ delete mTableUpdates[i];
+ }
+ mTableUpdates.Clear();
+}
+
+TableUpdate *
+ProtocolParser::GetTableUpdate(const nsACString& aTable)
+{
+ for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
+ if (aTable.Equals(mTableUpdates[i]->TableName())) {
+ return mTableUpdates[i];
+ }
+ }
+
+ // We free automatically on destruction, ownership of these
+ // updates can be transferred to DBServiceWorker, which passes
+ // them back to Classifier when doing the updates, and that
+ // will free them.
+ TableUpdate *update = CreateTableUpdate(aTable);
+ mTableUpdates.AppendElement(update);
+ return update;
+}
+
+///////////////////////////////////////////////////////////////////////
+// ProtocolParserV2
+
+ProtocolParserV2::ProtocolParserV2()
+ : mState(PROTOCOL_STATE_CONTROL)
+ , mResetRequested(false)
+ , mTableUpdate(nullptr)
+{
+}
+
+ProtocolParserV2::~ProtocolParserV2()
+{
+}
+
+void
+ProtocolParserV2::SetCurrentTable(const nsACString& aTable)
+{
+ auto update = GetTableUpdate(aTable);
+ mTableUpdate = TableUpdate::Cast<TableUpdateV2>(update);
+}
+
+nsresult
+ProtocolParserV2::AppendStream(const nsACString& aData)
+{
+ if (NS_FAILED(mUpdateStatus))
+ return mUpdateStatus;
+
+ nsresult rv;
+ mPending.Append(aData);
+
+ bool done = false;
+ while (!done) {
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (mState == PROTOCOL_STATE_CONTROL) {
+ rv = ProcessControl(&done);
+ } else if (mState == PROTOCOL_STATE_CHUNK) {
+ rv = ProcessChunk(&done);
+ } else {
+ NS_ERROR("Unexpected protocol state");
+ rv = NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mUpdateStatus = rv;
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+void
+ProtocolParserV2::End()
+{
+ // Inbound data has already been processed in every AppendStream() call.
+}
+
+nsresult
+ProtocolParserV2::ProcessControl(bool* aDone)
+{
+ nsresult rv;
+
+ nsAutoCString line;
+ *aDone = true;
+ while (NextLine(line)) {
+ PARSER_LOG(("Processing %s\n", line.get()));
+
+ if (StringBeginsWith(line, NS_LITERAL_CSTRING("i:"))) {
+ // Set the table name from the table header line.
+ SetCurrentTable(Substring(line, 2));
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("n:"))) {
+ if (PR_sscanf(line.get(), "n:%d", &mUpdateWaitSec) != 1) {
+ PARSER_LOG(("Error parsing n: '%s' (%d)", line.get(), mUpdateWaitSec));
+ return NS_ERROR_FAILURE;
+ }
+ } else if (line.EqualsLiteral("r:pleasereset")) {
+ mResetRequested = true;
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("u:"))) {
+ rv = ProcessForward(line);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("a:")) ||
+ StringBeginsWith(line, NS_LITERAL_CSTRING("s:"))) {
+ rv = ProcessChunkControl(line);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aDone = false;
+ return NS_OK;
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("ad:")) ||
+ StringBeginsWith(line, NS_LITERAL_CSTRING("sd:"))) {
+ rv = ProcessExpirations(line);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ *aDone = true;
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessExpirations(const nsCString& aLine)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Got an expiration without a table.");
+ return NS_ERROR_FAILURE;
+ }
+ const nsCSubstring &list = Substring(aLine, 3);
+ nsACString::const_iterator begin, end;
+ list.BeginReading(begin);
+ list.EndReading(end);
+ while (begin != end) {
+ uint32_t first, last;
+ if (ParseChunkRange(begin, end, &first, &last)) {
+ if (last < first) return NS_ERROR_FAILURE;
+ if (last - first > MAX_CHUNK_RANGE) return NS_ERROR_FAILURE;
+ for (uint32_t num = first; num <= last; num++) {
+ if (aLine[0] == 'a') {
+ nsresult rv = mTableUpdate->NewAddExpiration(num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ nsresult rv = mTableUpdate->NewSubExpiration(num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ }
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessChunkControl(const nsCString& aLine)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Got a chunk before getting a table.");
+ return NS_ERROR_FAILURE;
+ }
+
+ mState = PROTOCOL_STATE_CHUNK;
+ char command;
+
+ mChunkState.Clear();
+
+ if (PR_sscanf(aLine.get(),
+ "%c:%d:%d:%d",
+ &command,
+ &mChunkState.num, &mChunkState.hashSize, &mChunkState.length)
+ != 4)
+ {
+ NS_WARNING(("PR_sscanf failed"));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mChunkState.length > MAX_CHUNK_SIZE) {
+ NS_WARNING("Invalid length specified in update.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!(mChunkState.hashSize == PREFIX_SIZE || mChunkState.hashSize == COMPLETE_SIZE)) {
+ NS_WARNING("Invalid hash size specified in update.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-shavar")) ||
+ StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-simple"))) {
+ // Accommodate test tables ending in -simple for now.
+ mChunkState.type = (command == 'a') ? CHUNK_ADD : CHUNK_SUB;
+ } else if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-digest256"))) {
+ mChunkState.type = (command == 'a') ? CHUNK_ADD_DIGEST : CHUNK_SUB_DIGEST;
+ }
+ nsresult rv;
+ switch (mChunkState.type) {
+ case CHUNK_ADD:
+ rv = mTableUpdate->NewAddChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ case CHUNK_SUB:
+ rv = mTableUpdate->NewSubChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ case CHUNK_ADD_DIGEST:
+ rv = mTableUpdate->NewAddChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ case CHUNK_SUB_DIGEST:
+ rv = mTableUpdate->NewSubChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessForward(const nsCString& aLine)
+{
+ const nsCSubstring &forward = Substring(aLine, 2);
+ return AddForward(forward);
+}
+
+nsresult
+ProtocolParserV2::AddForward(const nsACString& aUrl)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Forward without a table name.");
+ return NS_ERROR_FAILURE;
+ }
+
+ ForwardedUpdate *forward = mForwards.AppendElement();
+ forward->table = mTableUpdate->TableName();
+ forward->url.Assign(aUrl);
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessChunk(bool* aDone)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Processing chunk without an active table.");
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ASSERTION(mChunkState.num != 0, "Must have a chunk number.");
+
+ if (mPending.Length() < mChunkState.length) {
+ *aDone = true;
+ return NS_OK;
+ }
+
+ // Pull the chunk out of the pending stream data.
+ nsAutoCString chunk;
+ chunk.Assign(Substring(mPending, 0, mChunkState.length));
+ mPending.Cut(0, mChunkState.length);
+
+ *aDone = false;
+ mState = PROTOCOL_STATE_CONTROL;
+
+ if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-shavar"))) {
+ return ProcessShaChunk(chunk);
+ }
+ if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-digest256"))) {
+ return ProcessDigestChunk(chunk);
+ }
+ return ProcessPlaintextChunk(chunk);
+}
+
+/**
+ * Process a plaintext chunk (currently only used in unit tests).
+ */
+nsresult
+ProtocolParserV2::ProcessPlaintextChunk(const nsACString& aChunk)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Chunk received with no table.");
+ return NS_ERROR_FAILURE;
+ }
+
+ PARSER_LOG(("Handling a %d-byte simple chunk", aChunk.Length()));
+
+ nsTArray<nsCString> lines;
+ ParseString(PromiseFlatCString(aChunk), '\n', lines);
+
+ // non-hashed tables need to be hashed
+ for (uint32_t i = 0; i < lines.Length(); i++) {
+ nsCString& line = lines[i];
+
+ if (mChunkState.type == CHUNK_ADD) {
+ if (mChunkState.hashSize == COMPLETE_SIZE) {
+ Completion hash;
+ hash.FromPlaintext(line, mCryptoHash);
+ nsresult rv = mTableUpdate->NewAddComplete(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ NS_ASSERTION(mChunkState.hashSize == 4, "Only 32- or 4-byte hashes can be used for add chunks.");
+ Prefix hash;
+ hash.FromPlaintext(line, mCryptoHash);
+ nsresult rv = mTableUpdate->NewAddPrefix(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ } else {
+ nsCString::const_iterator begin, iter, end;
+ line.BeginReading(begin);
+ line.EndReading(end);
+ iter = begin;
+ uint32_t addChunk;
+ if (!FindCharInReadable(':', iter, end) ||
+ PR_sscanf(lines[i].get(), "%d:", &addChunk) != 1) {
+ NS_WARNING("Received sub chunk without associated add chunk.");
+ return NS_ERROR_FAILURE;
+ }
+ iter++;
+
+ if (mChunkState.hashSize == COMPLETE_SIZE) {
+ Completion hash;
+ hash.FromPlaintext(Substring(iter, end), mCryptoHash);
+ nsresult rv = mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ NS_ASSERTION(mChunkState.hashSize == 4, "Only 32- or 4-byte hashes can be used for add chunks.");
+ Prefix hash;
+ hash.FromPlaintext(Substring(iter, end), mCryptoHash);
+ nsresult rv = mTableUpdate->NewSubPrefix(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessShaChunk(const nsACString& aChunk)
+{
+ uint32_t start = 0;
+ while (start < aChunk.Length()) {
+ // First four bytes are the domain key.
+ Prefix domain;
+ domain.Assign(Substring(aChunk, start, DOMAIN_SIZE));
+ start += DOMAIN_SIZE;
+
+ // Then a count of entries.
+ uint8_t numEntries = static_cast<uint8_t>(aChunk[start]);
+ start++;
+
+ PARSER_LOG(("Handling a %d-byte shavar chunk containing %u entries"
+ " for domain %X", aChunk.Length(), numEntries,
+ domain.ToUint32()));
+
+ nsresult rv;
+ if (mChunkState.type == CHUNK_ADD && mChunkState.hashSize == PREFIX_SIZE) {
+ rv = ProcessHostAdd(domain, numEntries, aChunk, &start);
+ } else if (mChunkState.type == CHUNK_ADD && mChunkState.hashSize == COMPLETE_SIZE) {
+ rv = ProcessHostAddComplete(numEntries, aChunk, &start);
+ } else if (mChunkState.type == CHUNK_SUB && mChunkState.hashSize == PREFIX_SIZE) {
+ rv = ProcessHostSub(domain, numEntries, aChunk, &start);
+ } else if (mChunkState.type == CHUNK_SUB && mChunkState.hashSize == COMPLETE_SIZE) {
+ rv = ProcessHostSubComplete(numEntries, aChunk, &start);
+ } else {
+ NS_WARNING("Unexpected chunk type/hash size!");
+ PARSER_LOG(("Got an unexpected chunk type/hash size: %s:%d",
+ mChunkState.type == CHUNK_ADD ? "add" : "sub",
+ mChunkState.hashSize));
+ return NS_ERROR_FAILURE;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessDigestChunk(const nsACString& aChunk)
+{
+ PARSER_LOG(("Handling a %d-byte digest256 chunk", aChunk.Length()));
+
+ if (mChunkState.type == CHUNK_ADD_DIGEST) {
+ return ProcessDigestAdd(aChunk);
+ }
+ if (mChunkState.type == CHUNK_SUB_DIGEST) {
+ return ProcessDigestSub(aChunk);
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult
+ProtocolParserV2::ProcessDigestAdd(const nsACString& aChunk)
+{
+ // The ABNF format for add chunks is (HASH)+, where HASH is 32 bytes.
+ MOZ_ASSERT(aChunk.Length() % 32 == 0,
+ "Chunk length in bytes must be divisible by 4");
+ uint32_t start = 0;
+ while (start < aChunk.Length()) {
+ Completion hash;
+ hash.Assign(Substring(aChunk, start, COMPLETE_SIZE));
+ start += COMPLETE_SIZE;
+ nsresult rv = mTableUpdate->NewAddComplete(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessDigestSub(const nsACString& aChunk)
+{
+ // The ABNF format for sub chunks is (ADDCHUNKNUM HASH)+, where ADDCHUNKNUM
+ // is a 4 byte chunk number, and HASH is 32 bytes.
+ MOZ_ASSERT(aChunk.Length() % 36 == 0,
+ "Chunk length in bytes must be divisible by 36");
+ uint32_t start = 0;
+ while (start < aChunk.Length()) {
+ // Read ADDCHUNKNUM
+ const nsCSubstring& addChunkStr = Substring(aChunk, start, 4);
+ start += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ // Read the hash
+ Completion hash;
+ hash.Assign(Substring(aChunk, start, COMPLETE_SIZE));
+ start += COMPLETE_SIZE;
+
+ nsresult rv = mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostAdd(const Prefix& aDomain, uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == PREFIX_SIZE,
+ "ProcessHostAdd should only be called for prefix hashes.");
+
+ if (aNumEntries == 0) {
+ nsresult rv = mTableUpdate->NewAddPrefix(mChunkState.num, aDomain);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ if (*aStart + (PREFIX_SIZE * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ Prefix hash;
+ hash.Assign(Substring(aChunk, *aStart, PREFIX_SIZE));
+ PARSER_LOG(("Add prefix %X", hash.ToUint32()));
+ nsresult rv = mTableUpdate->NewAddPrefix(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *aStart += PREFIX_SIZE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostSub(const Prefix& aDomain, uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t *aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == PREFIX_SIZE,
+ "ProcessHostSub should only be called for prefix hashes.");
+
+ if (aNumEntries == 0) {
+ if ((*aStart) + 4 > aChunk.Length()) {
+ NS_WARNING("Received a zero-entry sub chunk without an associated add.");
+ return NS_ERROR_FAILURE;
+ }
+
+ const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
+ *aStart += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ PARSER_LOG(("Sub prefix (addchunk=%u)", addChunk));
+ nsresult rv = mTableUpdate->NewSubPrefix(addChunk, aDomain, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ if (*aStart + ((PREFIX_SIZE + 4) * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
+ *aStart += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ Prefix prefix;
+ prefix.Assign(Substring(aChunk, *aStart, PREFIX_SIZE));
+ *aStart += PREFIX_SIZE;
+
+ PARSER_LOG(("Sub prefix %X (addchunk=%u)", prefix.ToUint32(), addChunk));
+ nsresult rv = mTableUpdate->NewSubPrefix(addChunk, prefix, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostAddComplete(uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == COMPLETE_SIZE,
+ "ProcessHostAddComplete should only be called for complete hashes.");
+
+ if (aNumEntries == 0) {
+ // this is totally comprehensible.
+ // My sarcasm detector is going off!
+ NS_WARNING("Expected > 0 entries for a 32-byte hash add.");
+ return NS_OK;
+ }
+
+ if (*aStart + (COMPLETE_SIZE * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ Completion hash;
+ hash.Assign(Substring(aChunk, *aStart, COMPLETE_SIZE));
+ nsresult rv = mTableUpdate->NewAddComplete(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *aStart += COMPLETE_SIZE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostSubComplete(uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == COMPLETE_SIZE,
+ "ProcessHostSubComplete should only be called for complete hashes.");
+
+ if (aNumEntries == 0) {
+ // this is totally comprehensible.
+ NS_WARNING("Expected > 0 entries for a 32-byte hash sub.");
+ return NS_OK;
+ }
+
+ if (*aStart + ((COMPLETE_SIZE + 4) * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ Completion hash;
+ hash.Assign(Substring(aChunk, *aStart, COMPLETE_SIZE));
+ *aStart += COMPLETE_SIZE;
+
+ const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
+ *aStart += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ nsresult rv = mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+ProtocolParserV2::NextLine(nsACString& aLine)
+{
+ int32_t newline = mPending.FindChar('\n');
+ if (newline == kNotFound) {
+ return false;
+ }
+ aLine.Assign(Substring(mPending, 0, newline));
+ mPending.Cut(0, newline + 1);
+ return true;
+}
+
+TableUpdate*
+ProtocolParserV2::CreateTableUpdate(const nsACString& aTableName) const
+{
+ return new TableUpdateV2(aTableName);
+}
+
+///////////////////////////////////////////////////////////////////////
+// ProtocolParserProtobuf
+
+ProtocolParserProtobuf::ProtocolParserProtobuf()
+{
+}
+
+ProtocolParserProtobuf::~ProtocolParserProtobuf()
+{
+}
+
+void
+ProtocolParserProtobuf::SetCurrentTable(const nsACString& aTable)
+{
+ // Should never occur.
+ MOZ_ASSERT_UNREACHABLE("SetCurrentTable shouldn't be called");
+}
+
+
+TableUpdate*
+ProtocolParserProtobuf::CreateTableUpdate(const nsACString& aTableName) const
+{
+ return new TableUpdateV4(aTableName);
+}
+
+nsresult
+ProtocolParserProtobuf::AppendStream(const nsACString& aData)
+{
+ // Protobuf data cannot be parsed progressively. Just save the incoming data.
+ mPending.Append(aData);
+ return NS_OK;
+}
+
+void
+ProtocolParserProtobuf::End()
+{
+ // mUpdateStatus will be updated to success as long as not all
+ // the responses are invalid.
+ mUpdateStatus = NS_ERROR_FAILURE;
+
+ FetchThreatListUpdatesResponse response;
+ if (!response.ParseFromArray(mPending.get(), mPending.Length())) {
+ NS_WARNING("ProtocolParserProtobuf failed parsing data.");
+ return;
+ }
+
+ auto minWaitDuration = response.minimum_wait_duration();
+ mUpdateWaitSec = minWaitDuration.seconds() +
+ minWaitDuration.nanos() / 1000000000;
+
+ for (int i = 0; i < response.list_update_responses_size(); i++) {
+ auto r = response.list_update_responses(i);
+ nsresult rv = ProcessOneResponse(r);
+ if (NS_SUCCEEDED(rv)) {
+ mUpdateStatus = rv;
+ } else {
+ NS_WARNING("Failed to process one response.");
+ }
+ }
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessOneResponse(const ListUpdateResponse& aResponse)
+{
+ // A response must have a threat type.
+ if (!aResponse.has_threat_type()) {
+ NS_WARNING("Threat type not initialized. This seems to be an invalid response.");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Convert threat type to list name.
+ nsCOMPtr<nsIUrlClassifierUtils> urlUtil =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID);
+ nsCString possibleListNames;
+ nsresult rv = urlUtil->ConvertThreatTypeToListNames(aResponse.threat_type(),
+ possibleListNames);
+ if (NS_FAILED(rv)) {
+ PARSER_LOG((nsPrintfCString("Threat type to list name conversion error: %d",
+ aResponse.threat_type())).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ // Match the table name we received with one of the ones we requested.
+ // We ignore the case where a threat type matches more than one list
+ // per provider and return the first one. See bug 1287059."
+ nsCString listName;
+ nsTArray<nsCString> possibleListNameArray;
+ Classifier::SplitTables(possibleListNames, possibleListNameArray);
+ for (auto possibleName : possibleListNameArray) {
+ if (mRequestedTables.Contains(possibleName)) {
+ listName = possibleName;
+ break;
+ }
+ }
+
+ if (listName.IsEmpty()) {
+ PARSER_LOG(("We received an update for a list we didn't ask for. Ignoring it."));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Test if this is a full update.
+ bool isFullUpdate = false;
+ if (aResponse.has_response_type()) {
+ isFullUpdate =
+ aResponse.response_type() == ListUpdateResponse::FULL_UPDATE;
+ } else {
+ NS_WARNING("Response type not initialized.");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Warn if there's no new state.
+ if (!aResponse.has_new_client_state()) {
+ NS_WARNING("New state not initialized.");
+ return NS_ERROR_FAILURE;
+ }
+
+ auto tu = GetTableUpdate(nsCString(listName.get()));
+ auto tuV4 = TableUpdate::Cast<TableUpdateV4>(tu);
+ NS_ENSURE_TRUE(tuV4, NS_ERROR_FAILURE);
+
+ nsCString state(aResponse.new_client_state().c_str(),
+ aResponse.new_client_state().size());
+ tuV4->SetNewClientState(state);
+
+ if (aResponse.has_checksum()) {
+ tuV4->NewChecksum(aResponse.checksum().sha256());
+ }
+
+ PARSER_LOG(("==== Update for threat type '%d' ====", aResponse.threat_type()));
+ PARSER_LOG(("* listName: %s\n", listName.get()));
+ PARSER_LOG(("* newState: %s\n", aResponse.new_client_state().c_str()));
+ PARSER_LOG(("* isFullUpdate: %s\n", (isFullUpdate ? "yes" : "no")));
+ PARSER_LOG(("* hasChecksum: %s\n", (aResponse.has_checksum() ? "yes" : "no")));
+
+ tuV4->SetFullUpdate(isFullUpdate);
+
+ rv = ProcessAdditionOrRemoval(*tuV4, aResponse.additions(), true /*aIsAddition*/);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = ProcessAdditionOrRemoval(*tuV4, aResponse.removals(), false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PARSER_LOG(("\n\n"));
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessAdditionOrRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySetList& aUpdate,
+ bool aIsAddition)
+{
+ nsresult ret = NS_OK;
+
+ for (int i = 0; i < aUpdate.size(); i++) {
+ auto update = aUpdate.Get(i);
+ if (!update.has_compression_type()) {
+ NS_WARNING(nsPrintfCString("%s with no compression type.",
+ aIsAddition ? "Addition" : "Removal").get());
+ continue;
+ }
+
+ switch (update.compression_type()) {
+ case COMPRESSION_TYPE_UNSPECIFIED:
+ NS_WARNING("Unspecified compression type.");
+ break;
+
+ case RAW:
+ ret = (aIsAddition ? ProcessRawAddition(aTableUpdate, update)
+ : ProcessRawRemoval(aTableUpdate, update));
+ break;
+
+ case RICE:
+ ret = (aIsAddition ? ProcessEncodedAddition(aTableUpdate, update)
+ : ProcessEncodedRemoval(aTableUpdate, update));
+ break;
+ }
+ }
+
+ return ret;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessRawAddition(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aAddition)
+{
+ if (!aAddition.has_raw_hashes()) {
+ PARSER_LOG(("* No raw addition."));
+ return NS_OK;
+ }
+
+ auto rawHashes = aAddition.raw_hashes();
+ if (!rawHashes.has_prefix_size()) {
+ NS_WARNING("Raw hash has no prefix size");
+ return NS_OK;
+ }
+
+ auto prefixes = rawHashes.raw_hashes();
+ if (4 == rawHashes.prefix_size()) {
+ // Process fixed length prefixes separately.
+ uint32_t* fixedLengthPrefixes = (uint32_t*)prefixes.c_str();
+ size_t numOfFixedLengthPrefixes = prefixes.size() / 4;
+ PARSER_LOG(("* Raw addition (4 bytes)"));
+ PARSER_LOG((" - # of prefixes: %d", numOfFixedLengthPrefixes));
+ PARSER_LOG((" - Memory address: 0x%p", fixedLengthPrefixes));
+ } else {
+ // TODO: Process variable length prefixes including full hashes.
+ // See Bug 1283009.
+ PARSER_LOG((" Raw addition (%d bytes)", rawHashes.prefix_size()));
+ }
+
+ if (!rawHashes.mutable_raw_hashes()) {
+ PARSER_LOG(("Unable to get mutable raw hashes. Can't perform a string move."));
+ return NS_ERROR_FAILURE;
+ }
+
+ aTableUpdate.NewPrefixes(rawHashes.prefix_size(),
+ *rawHashes.mutable_raw_hashes());
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessRawRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aRemoval)
+{
+ if (!aRemoval.has_raw_indices()) {
+ NS_WARNING("A removal has no indices.");
+ return NS_OK;
+ }
+
+ // indices is an array of int32.
+ auto indices = aRemoval.raw_indices().indices();
+ PARSER_LOG(("* Raw removal"));
+ PARSER_LOG((" - # of removal: %d", indices.size()));
+
+ aTableUpdate.NewRemovalIndices((const uint32_t*)indices.data(),
+ indices.size());
+
+ return NS_OK;
+}
+
+static nsresult
+DoRiceDeltaDecode(const RiceDeltaEncoding& aEncoding,
+ nsTArray<uint32_t>& aDecoded)
+{
+ if (!aEncoding.has_first_value()) {
+ PARSER_LOG(("The encoding info is incomplete."));
+ return NS_ERROR_FAILURE;
+ }
+ if (aEncoding.num_entries() > 0 &&
+ (!aEncoding.has_rice_parameter() || !aEncoding.has_encoded_data())) {
+ PARSER_LOG(("Rice parameter or encoded data is missing."));
+ return NS_ERROR_FAILURE;
+ }
+
+ PARSER_LOG(("* Encoding info:"));
+ PARSER_LOG((" - First value: %d", aEncoding.first_value()));
+ PARSER_LOG((" - Num of entries: %d", aEncoding.num_entries()));
+ PARSER_LOG((" - Rice parameter: %d", aEncoding.rice_parameter()));
+
+ // Set up the input buffer. Note that the bits should be read
+ // from LSB to MSB so that we in-place reverse the bits before
+ // feeding to the decoder.
+ auto encoded = const_cast<RiceDeltaEncoding&>(aEncoding).mutable_encoded_data();
+ RiceDeltaDecoder decoder((uint8_t*)encoded->c_str(), encoded->size());
+
+ // Setup the output buffer. The "first value" is included in
+ // the output buffer.
+ aDecoded.SetLength(aEncoding.num_entries() + 1);
+
+ // Decode!
+ bool rv = decoder.Decode(aEncoding.rice_parameter(),
+ aEncoding.first_value(), // first value.
+ aEncoding.num_entries(), // # of entries (first value not included).
+ &aDecoded[0]);
+
+ NS_ENSURE_TRUE(rv, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessEncodedAddition(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aAddition)
+{
+ if (!aAddition.has_rice_hashes()) {
+ PARSER_LOG(("* No rice encoded addition."));
+ return NS_OK;
+ }
+
+ nsTArray<uint32_t> decoded;
+ nsresult rv = DoRiceDeltaDecode(aAddition.rice_hashes(), decoded);
+ if (NS_FAILED(rv)) {
+ PARSER_LOG(("Failed to parse encoded prefixes."));
+ return rv;
+ }
+
+ // Say we have the following raw prefixes
+ // BE LE
+ // 00 00 00 01 1 16777216
+ // 00 00 02 00 512 131072
+ // 00 03 00 00 196608 768
+ // 04 00 00 00 67108864 4
+ //
+ // which can be treated as uint32 (big-endian) sorted in increasing order:
+ //
+ // [1, 512, 196608, 67108864]
+ //
+ // According to https://developers.google.com/safe-browsing/v4/compression,
+ // the following should be done prior to compression:
+ //
+ // 1) re-interpret in little-endian ==> [16777216, 131072, 768, 4]
+ // 2) sort in increasing order ==> [4, 768, 131072, 16777216]
+ //
+ // In order to get the original byte stream from |decoded|
+ // ([4, 768, 131072, 16777216] in this case), we have to:
+ //
+ // 1) sort in big-endian order ==> [16777216, 131072, 768, 4]
+ // 2) copy each uint32 in little-endian to the result string
+ //
+
+ // The 4-byte prefixes have to be re-sorted in Big-endian increasing order.
+ struct CompareBigEndian
+ {
+ bool Equals(const uint32_t& aA, const uint32_t& aB) const
+ {
+ return aA == aB;
+ }
+
+ bool LessThan(const uint32_t& aA, const uint32_t& aB) const
+ {
+ return NativeEndian::swapToBigEndian(aA) <
+ NativeEndian::swapToBigEndian(aB);
+ }
+ };
+ decoded.Sort(CompareBigEndian());
+
+ // The encoded prefixes are always 4 bytes.
+ std::string prefixes;
+ for (size_t i = 0; i < decoded.Length(); i++) {
+ // Note that the third argument is the number of elements we want
+ // to copy (and swap) but not the number of bytes we want to copy.
+ char p[4];
+ NativeEndian::copyAndSwapToLittleEndian(p, &decoded[i], 1);
+ prefixes.append(p, 4);
+ }
+
+ aTableUpdate.NewPrefixes(4, prefixes);
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessEncodedRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aRemoval)
+{
+ if (!aRemoval.has_rice_indices()) {
+ PARSER_LOG(("* No rice encoded removal."));
+ return NS_OK;
+ }
+
+ nsTArray<uint32_t> decoded;
+ nsresult rv = DoRiceDeltaDecode(aRemoval.rice_indices(), decoded);
+ if (NS_FAILED(rv)) {
+ PARSER_LOG(("Failed to decode encoded removal indices."));
+ return rv;
+ }
+
+ // The encoded prefixes are always 4 bytes.
+ aTableUpdate.NewRemovalIndices(&decoded[0], decoded.Length());
+
+ return NS_OK;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla