diff options
Diffstat (limited to 'toolkit/components/url-classifier/nsUrlClassifierDBService.cpp')
-rw-r--r-- | toolkit/components/url-classifier/nsUrlClassifierDBService.cpp | 2613 |
1 files changed, 2613 insertions, 0 deletions
diff --git a/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp b/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp new file mode 100644 index 0000000000..929517aa6e --- /dev/null +++ b/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp @@ -0,0 +1,2613 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsArrayUtils.h" +#include "nsCRT.h" +#include "nsIObserverService.h" +#include "nsIPermissionManager.h" +#include "nsIPrefBranch.h" +#include "nsIXULRuntime.h" +#include "nsToolkitCompsCID.h" +#include "nsUrlClassifierDBService.h" +#include "nsUrlClassifierUtils.h" +#include "nsUrlClassifierProxies.h" +#include "nsURILoader.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsTArray.h" +#include "nsNetCID.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "nsString.h" +#include "mozilla/Atomics.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Components.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Mutex.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/Logging.h" +#include "prnetdb.h" +#include "Entries.h" +#include "Classifier.h" +#include "ProtocolParser.h" +#include "mozilla/Attributes.h" +#include "nsIHttpChannel.h" +#include "nsIPrincipal.h" +#include "nsIUrlListManager.h" +#include "Classifier.h" +#include "ProtocolParser.h" +#include "nsContentUtils.h" +#include "mozilla/Components.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/URLClassifierChild.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "mozilla/net/UrlClassifierFeatureResult.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/SyncRunnable.h" +#include "UrlClassifierTelemetryUtils.h" +#include "nsIURLFormatter.h" +#include "nsIUploadChannel.h" +#include "nsStringStream.h" +#include "nsNetUtil.h" +#include "nsToolkitCompsCID.h" + +namespace mozilla { +namespace safebrowsing { + +nsresult TablesToResponse(const nsACString& tables) { + if (tables.IsEmpty()) { + return NS_OK; + } + + // We don't check mCheckMalware and friends because disabled tables are + // never included + if (FindInReadable("-malware-"_ns, tables)) { + return NS_ERROR_MALWARE_URI; + } + if (FindInReadable("-harmful-"_ns, tables)) { + return NS_ERROR_HARMFUL_URI; + } + if (FindInReadable("-phish-"_ns, tables)) { + return NS_ERROR_PHISHING_URI; + } + if (FindInReadable("-unwanted-"_ns, tables)) { + return NS_ERROR_UNWANTED_URI; + } + if (FindInReadable("-track-"_ns, tables)) { + return NS_ERROR_TRACKING_URI; + } + if (FindInReadable("-block-"_ns, tables)) { + return NS_ERROR_BLOCKED_URI; + } + return NS_OK; +} + +} // namespace safebrowsing +} // namespace mozilla + +// This class holds a list of features, their tables, and it stores the lookup +// results. +class nsUrlClassifierDBService::FeatureHolder final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FeatureHolder); + + // In order to avoid multiple lookup for the same table, we have a special + // array for tables and their results. The Features are stored in a separate + // array together with the references to their tables. + + class TableData { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING( + nsUrlClassifierDBService::FeatureHolder::TableData); + + explicit TableData(const nsACString& aTable) : mTable(aTable) {} + + nsCString mTable; + LookupResultArray mResults; + + private: + ~TableData() = default; + }; + + struct FeatureData { + RefPtr<nsIUrlClassifierFeature> mFeature; + nsTArray<RefPtr<TableData>> mTables; + }; + + static already_AddRefed<FeatureHolder> Create( + nsIURI* aURI, const nsTArray<RefPtr<nsIUrlClassifierFeature>>& aFeatures, + nsIUrlClassifierFeature::listType aListType) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aURI); + + RefPtr<FeatureHolder> holder = new FeatureHolder(aURI); + + for (nsIUrlClassifierFeature* feature : aFeatures) { + FeatureData* featureData = holder->mFeatureData.AppendElement(); + MOZ_ASSERT(featureData); + + featureData->mFeature = feature; + nsTArray<nsCString> tables; + nsresult rv = feature->GetTables(aListType, tables); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + for (const nsCString& table : tables) { + TableData* tableData = holder->GetOrCreateTableData(table); + MOZ_ASSERT(tableData); + + featureData->mTables.AppendElement(tableData); + } + } + + return holder.forget(); + } + + nsresult DoLocalLookup(const nsACString& aSpec, + nsUrlClassifierDBServiceWorker* aWorker) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aWorker); + + mozilla::Telemetry::AutoTimer< + mozilla::Telemetry::URLCLASSIFIER_CL_CHECK_TIME> + timer; + + // Get the set of fragments based on the url. This is necessary because we + // only look up at most 5 URLs per aSpec, even if aSpec has more than 5 + // components. + nsTArray<nsCString> fragments; + nsresult rv = LookupCache::GetLookupFragments(aSpec, &fragments); + NS_ENSURE_SUCCESS(rv, rv); + + for (TableData* tableData : mTableData) { + rv = aWorker->DoSingleLocalLookupWithURIFragments( + fragments, tableData->mTable, tableData->mResults); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; + } + + // This method is used to convert the LookupResultArray from + // ::DoSingleLocalLookupWithURIFragments to nsIUrlClassifierFeatureResult + void GetResults(nsTArray<RefPtr<nsIUrlClassifierFeatureResult>>& aResults) { + MOZ_ASSERT(NS_IsMainThread()); + + // For each table, we must concatenate the results of the corresponding + // tables. + + for (FeatureData& featureData : mFeatureData) { + nsAutoCString list; + for (TableData* tableData : featureData.mTables) { + for (uint32_t i = 0; i < tableData->mResults.Length(); ++i) { + if (!list.IsEmpty()) { + list.AppendLiteral(","); + } + list.Append(tableData->mResults[i]->mTableName); + } + } + + if (list.IsEmpty()) { + continue; + } + + RefPtr<mozilla::net::UrlClassifierFeatureResult> result = + new mozilla::net::UrlClassifierFeatureResult( + mURI, featureData.mFeature, list); + aResults.AppendElement(result); + } + } + + mozilla::UniquePtr<LookupResultArray> GetTableResults() const { + mozilla::UniquePtr<LookupResultArray> results = + mozilla::MakeUnique<LookupResultArray>(); + if (NS_WARN_IF(!results)) { + return nullptr; + } + + for (TableData* tableData : mTableData) { + results->AppendElements(tableData->mResults); + } + + return results; + } + + private: + explicit FeatureHolder(nsIURI* aURI) : mURI(aURI) { + MOZ_ASSERT(NS_IsMainThread()); + } + + ~FeatureHolder() { + for (FeatureData& featureData : mFeatureData) { + NS_ReleaseOnMainThread("FeatureHolder:mFeatureData", + featureData.mFeature.forget()); + } + + NS_ReleaseOnMainThread("FeatureHolder:mURI", mURI.forget()); + } + + TableData* GetOrCreateTableData(const nsACString& aTable) { + for (TableData* tableData : mTableData) { + if (tableData->mTable == aTable) { + return tableData; + } + } + + RefPtr<TableData> tableData = new TableData(aTable); + mTableData.AppendElement(tableData); + return tableData; + } + + nsCOMPtr<nsIURI> mURI; + nsTArray<FeatureData> mFeatureData; + nsTArray<RefPtr<TableData>> mTableData; +}; + +using namespace mozilla; +using namespace mozilla::safebrowsing; + +// MOZ_LOG=UrlClassifierDbService:5 +LazyLogModule gUrlClassifierDbServiceLog("UrlClassifierDbService"); +#define LOG(args) \ + MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() \ + MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug) + +#define GETHASH_NOISE_PREF "urlclassifier.gethashnoise" +#define GETHASH_NOISE_DEFAULT 4 + +// 30 minutes as the maximum negative cache duration. +#define MAXIMUM_NEGATIVE_CACHE_DURATION_SEC (30 * 60 * 1000) + +class nsUrlClassifierDBServiceWorker; + +// Singleton instance. +static nsUrlClassifierDBService* sUrlClassifierDBService; + +nsIThread* nsUrlClassifierDBService::gDbBackgroundThread = nullptr; + +// Once we've committed to shutting down, don't do work in the background +// thread. +static Atomic<bool> gShuttingDownThread(false); + +NS_IMPL_ISUPPORTS(nsUrlClassifierDBServiceWorker, nsIUrlClassifierDBService) + +nsUrlClassifierDBServiceWorker::nsUrlClassifierDBServiceWorker() + : mUpdateObserverLock("nsUrlClassifierDBServerWorker.mUpdateObserverLock"), + mInStream(false), + mGethashNoise(0), + mPendingLookupLock("nsUrlClassifierDBServerWorker.mPendingLookupLock") {} + +nsUrlClassifierDBServiceWorker::~nsUrlClassifierDBServiceWorker() { + NS_ASSERTION(!mClassifier, + "Db connection not closed, leaking memory! Call CloseDb " + "to close the connection."); +} + +nsresult nsUrlClassifierDBServiceWorker::Init( + uint32_t aGethashNoise, nsCOMPtr<nsIFile> aCacheDir, + nsUrlClassifierDBService* aDBService) { + mGethashNoise = aGethashNoise; + mCacheDir = aCacheDir; + mDBService = aDBService; + + ResetUpdate(); + + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::QueueLookup( + const nsACString& aKey, + nsUrlClassifierDBService::FeatureHolder* aFeatureHolder, + nsIUrlClassifierLookupCallback* aCallback) { + MOZ_ASSERT(aFeatureHolder); + MOZ_ASSERT(aCallback); + + MutexAutoLock lock(mPendingLookupLock); + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + PendingLookup* lookup = mPendingLookups.AppendElement(fallible); + if (NS_WARN_IF(!lookup)) return NS_ERROR_OUT_OF_MEMORY; + + lookup->mStartTime = TimeStamp::Now(); + lookup->mKey = aKey; + lookup->mCallback = aCallback; + lookup->mFeatureHolder = aFeatureHolder; + + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::DoSingleLocalLookupWithURIFragments( + const nsTArray<nsCString>& aSpecFragments, const nsACString& aTable, + LookupResultArray& aResults) { + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + MOZ_ASSERT( + !NS_IsMainThread(), + "DoSingleLocalLookupWithURIFragments must be on background thread"); + + // Bail if we haven't been initialized on the background thread. + if (!mClassifier) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = + mClassifier->CheckURIFragments(aSpecFragments, aTable, aResults); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LOG(("Found %zu results.", aResults.Length())); + return NS_OK; +} + +/** + * Lookup up a key in the database is a two step process: + * + * a) First we look for any Entries in the database that might apply to this + * url. For each URL there are one or two possible domain names to check: + * the two-part domain name (example.com) and the three-part name + * (www.example.com). We check the database for both of these. + * b) If we find any entries, we check the list of fragments for that entry + * against the possible subfragments of the URL as described in the + * "Simplified Regular Expression Lookup" section of the protocol doc. + */ +nsresult nsUrlClassifierDBServiceWorker::DoLookup( + const nsACString& spec, + nsUrlClassifierDBService::FeatureHolder* aFeatureHolder, + nsIUrlClassifierLookupCallback* c) { + // Make sure the callback is invoked when a failure occurs, + // otherwise we will not be able to load any url. + auto scopeExit = MakeScopeExit([&c]() { c->LookupComplete(nullptr); }); + + if (gShuttingDownThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + PRIntervalTime clockStart = 0; + if (LOG_ENABLED()) { + clockStart = PR_IntervalNow(); + } + + nsresult rv = aFeatureHolder->DoLocalLookup(spec, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (LOG_ENABLED()) { + PRIntervalTime clockEnd = PR_IntervalNow(); + LOG(("query took %dms\n", + PR_IntervalToMilliseconds(clockEnd - clockStart))); + } + + UniquePtr<LookupResultArray> results = aFeatureHolder->GetTableResults(); + if (NS_WARN_IF(!results)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + LOG(("Found %zu results.", results->Length())); + + for (const RefPtr<const LookupResult> lookupResult : *results) { + if (!lookupResult->Confirmed() && + mDBService->CanComplete(lookupResult->mTableName)) { + // We're going to be doing a gethash request, add some extra entries. + // Note that we cannot pass the first two by reference, because we + // add to completes, which can cause completes to reallocate and move. + AddNoise(lookupResult->hash.fixedLengthPrefix, lookupResult->mTableName, + mGethashNoise, *results); + break; + } + } + + // At this point ownership of 'results' is handed to the callback. + scopeExit.release(); + c->LookupComplete(std::move(results)); + + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::HandlePendingLookups() { + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + MutexAutoLock lock(mPendingLookupLock); + while (mPendingLookups.Length() > 0) { + PendingLookup lookup = mPendingLookups[0]; + mPendingLookups.RemoveElementAt(0); + { + MutexAutoUnlock unlock(mPendingLookupLock); + DoLookup(lookup.mKey, lookup.mFeatureHolder, lookup.mCallback); + } + double lookupTime = (TimeStamp::Now() - lookup.mStartTime).ToMilliseconds(); + Telemetry::Accumulate(Telemetry::URLCLASSIFIER_LOOKUP_TIME_2, + static_cast<uint32_t>(lookupTime)); + } + + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::AddNoise(const Prefix aPrefix, + const nsCString tableName, + uint32_t aCount, + LookupResultArray& results) { + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + if (aCount < 1) { + return NS_OK; + } + + PrefixArray noiseEntries; + nsresult rv = + mClassifier->ReadNoiseEntries(aPrefix, tableName, aCount, noiseEntries); + NS_ENSURE_SUCCESS(rv, rv); + + for (const auto noiseEntry : noiseEntries) { + RefPtr<LookupResult> result = new LookupResult; + results.AppendElement(result); + + result->hash.fixedLengthPrefix = noiseEntry; + result->mNoise = true; + result->mPartialHashLength = PREFIX_SIZE; // Noise is always 4-byte, + result->mTableName.Assign(tableName); + } + + return NS_OK; +} + +// Lookup a key in the db. +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::Lookup(nsIPrincipal* aPrincipal, + const nsACString& aTables, + nsIUrlClassifierCallback* c) { + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + return HandlePendingLookups(); +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::GetTables(nsIUrlClassifierCallback* c) { + if (gShuttingDownThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = OpenDb(); + if (NS_FAILED(rv)) { + NS_ERROR("Unable to open SafeBrowsing database"); + return NS_ERROR_FAILURE; + } + + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString response; + mClassifier->TableRequest(response); + LOG(("GetTables: %s", response.get())); + c->HandleEvent(response); + + return rv; +} + +void nsUrlClassifierDBServiceWorker::ResetStream() { + LOG(("ResetStream")); + mInStream = false; + mProtocolParser = nullptr; +} + +void nsUrlClassifierDBServiceWorker::ResetUpdate() { + LOG(("ResetUpdate")); + mUpdateWaitSec = 0; + mUpdateStatus = NS_OK; + { + MutexAutoLock lock(mUpdateObserverLock); + mUpdateObserver = nullptr; + } +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::SetHashCompleter( + const nsACString& tableName, nsIUrlClassifierHashCompleter* completer) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::BeginUpdate( + nsIUrlClassifierUpdateObserver* observer, const nsACString& tables) { + LOG(("nsUrlClassifierDBServiceWorker::BeginUpdate [%s]", + PromiseFlatCString(tables).get())); + + if (gShuttingDownThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + { + MutexAutoLock lock(mUpdateObserverLock); + NS_ENSURE_STATE(!mUpdateObserver); + + nsresult rv = OpenDb(); + if (NS_FAILED(rv)) { + NS_ERROR("Unable to open SafeBrowsing database"); + return NS_ERROR_FAILURE; + } + + mUpdateStatus = NS_OK; + MOZ_ASSERT(mTableUpdates.IsEmpty(), + "mTableUpdates should have been cleared in FinishUpdate()"); + mUpdateObserver = observer; + } + Classifier::SplitTables(tables, mUpdateTables); + + return NS_OK; +} + +// Called from the stream updater. +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::BeginStream(const nsACString& table) { + LOG(("nsUrlClassifierDBServiceWorker::BeginStream")); + MOZ_ASSERT(!NS_IsMainThread(), "Streaming must be on the background thread"); + + if (gShuttingDownThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + { + MutexAutoLock lock(mUpdateObserverLock); + NS_ENSURE_STATE(mUpdateObserver); + } + NS_ENSURE_STATE(!mInStream); + + mInStream = true; + + NS_ASSERTION(!mProtocolParser, "Should not have a protocol parser."); + + // Check if we should use protobuf to parse the update. + bool useProtobuf = false; + for (size_t i = 0; i < mUpdateTables.Length(); i++) { + bool isCurProtobuf = StringEndsWith(mUpdateTables[i], "-proto"_ns); + + if (0 == i) { + // Use the first table name to decice if all the subsequent tables + // should be '-proto'. + useProtobuf = isCurProtobuf; + continue; + } + + if (useProtobuf != isCurProtobuf) { + NS_WARNING( + "Cannot mix 'proto' tables with other types " + "within the same provider."); + break; + } + } + + if (useProtobuf) { + mProtocolParser.reset(new (fallible) ProtocolParserProtobuf()); + } else { + mProtocolParser.reset(new (fallible) ProtocolParserV2()); + } + if (!mProtocolParser) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return mProtocolParser->Begin(table, mUpdateTables); +} + +/** + * Updating the database: + * + * The Update() method takes a series of chunks separated with control data, + * as described in + * http://code.google.com/p/google-safe-browsing/wiki/Protocolv2Spec + * + * It will iterate through the control data until it reaches a chunk. By + * the time it reaches a chunk, it should have received + * a) the table to which this chunk applies + * b) the type of chunk (add, delete, expire add, expire delete). + * c) the chunk ID + * d) the length of the chunk. + * + * For add and subtract chunks, it needs to read the chunk data (expires + * don't have any data). Chunk data is a list of URI fragments whose + * encoding depends on the type of table (which is indicated by the end + * of the table name): + * a) tables ending with -exp are a zlib-compressed list of URI fragments + * separated by newlines. + * b) tables ending with -sha128 have the form + * [domain][N][frag0]...[fragN] + * 16 1 16 16 + * If N is 0, the domain is reused as a fragment. + * c) any other tables are assumed to be a plaintext list of URI fragments + * separated by newlines. + * + * Update() can be fed partial data; It will accumulate data until there is + * enough to act on. Finish() should be called when there will be no more + * data. + */ +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::UpdateStream(const nsACString& chunk) { + if (gShuttingDownThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + MOZ_ASSERT(mProtocolParser); + + NS_ENSURE_STATE(mInStream); + + HandlePendingLookups(); + + // Feed the chunk to the parser. + return mProtocolParser->AppendStream(chunk); +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::FinishStream() { + if (gShuttingDownThread) { + LOG(("shutting down")); + return NS_ERROR_NOT_INITIALIZED; + } + + MutexAutoLock lock(mUpdateObserverLock); + + MOZ_ASSERT(mProtocolParser); + + NS_ENSURE_STATE(mInStream); + NS_ENSURE_STATE(mUpdateObserver); + + mInStream = false; + + mProtocolParser->End(); + + if (NS_SUCCEEDED(mProtocolParser->Status())) { + if (mProtocolParser->UpdateWaitSec()) { + mUpdateWaitSec = mProtocolParser->UpdateWaitSec(); + } + // XXX: Only allow forwards from the initial update? + const nsTArray<ProtocolParser::ForwardedUpdate>& forwards = + mProtocolParser->Forwards(); + for (uint32_t i = 0; i < forwards.Length(); i++) { + const ProtocolParser::ForwardedUpdate& forward = forwards[i]; + mUpdateObserver->UpdateUrlRequested(forward.url, forward.table); + } + // Hold on to any TableUpdate objects that were created by the + // parser. + mTableUpdates.AppendElements(mProtocolParser->GetTableUpdates()); + mProtocolParser->ForgetTableUpdates(); + +#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES + // The assignment involves no string copy since the source string is + // sharable. + mRawTableUpdates = mProtocolParser->GetRawTableUpdates(); +#endif + } else { + LOG( + ("nsUrlClassifierDBService::FinishStream Failed to parse the stream " + "using mProtocolParser.")); + mUpdateStatus = mProtocolParser->Status(); + } + mUpdateObserver->StreamFinished(mProtocolParser->Status(), 0); + + if (NS_SUCCEEDED(mUpdateStatus)) { + if (mProtocolParser->ResetRequested()) { + mClassifier->ResetTables(Classifier::Clear_All, + mProtocolParser->TablesToReset()); + } + } + + mProtocolParser = nullptr; + + return mUpdateStatus; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::FinishUpdate() { + LOG(("nsUrlClassifierDBServiceWorker::FinishUpdate")); + + MOZ_ASSERT(!NS_IsMainThread(), + "nsUrlClassifierDBServiceWorker::FinishUpdate " + "NUST NOT be on the main thread."); + + if (gShuttingDownThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + MOZ_ASSERT(!mProtocolParser, + "Should have been nulled out in FinishStream() " + "or never created."); + + { + MutexAutoLock lock(mUpdateObserverLock); + NS_ENSURE_STATE(mUpdateObserver); + } + + if (NS_FAILED(mUpdateStatus)) { + LOG( + ("nsUrlClassifierDBServiceWorker::FinishUpdate() Not running " + "ApplyUpdate() since the update has already failed.")); + mTableUpdates.Clear(); + return NotifyUpdateObserver(mUpdateStatus); + } + + if (mTableUpdates.IsEmpty()) { + LOG(("Nothing to update. Just notify update observer.")); + return NotifyUpdateObserver(NS_OK); + } + + RefPtr<nsUrlClassifierDBServiceWorker> self = this; + nsresult rv = mClassifier->AsyncApplyUpdates( + mTableUpdates, [self](nsresult aRv) -> void { +#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES + if (NS_FAILED(aRv) && NS_ERROR_OUT_OF_MEMORY != aRv && + NS_ERROR_UC_UPDATE_SHUTDOWNING != aRv) { + self->mClassifier->DumpRawTableUpdates(self->mRawTableUpdates); + } + // Invalidate the raw table updates. + self->mRawTableUpdates.Truncate(); +#endif + + self->NotifyUpdateObserver(aRv); + }); + mTableUpdates.Clear(); // Classifier is working on its copy. + + if (NS_FAILED(rv)) { + LOG(("Failed to start async update. Notify immediately.")); + NotifyUpdateObserver(rv); + } + + return rv; +} + +nsresult nsUrlClassifierDBServiceWorker::NotifyUpdateObserver( + nsresult aUpdateStatus) { + MOZ_ASSERT(!NS_IsMainThread(), + "nsUrlClassifierDBServiceWorker::NotifyUpdateObserver " + "NUST NOT be on the main thread."); + + LOG(("nsUrlClassifierDBServiceWorker::NotifyUpdateObserver")); + + // We've either + // 1) failed starting a download stream + // 2) succeeded in starting a download stream but failed to obtain + // table updates + // 3) succeeded in obtaining table updates but failed to build new + // tables. + // 4) succeeded in building new tables but failed to take them. + // 5) succeeded in taking new tables. + + mUpdateStatus = aUpdateStatus; + + nsUrlClassifierUtils* urlUtil = nsUrlClassifierUtils::GetInstance(); + if (NS_WARN_IF(!urlUtil)) { + return NS_ERROR_FAILURE; + } + + nsCString provider; + // Assume that all the tables in update should have the same provider. + urlUtil->GetTelemetryProvider(mUpdateTables.SafeElementAt(0, ""_ns), + provider); + + nsresult updateStatus = mUpdateStatus; + if (NS_FAILED(mUpdateStatus)) { + updateStatus = + NS_ERROR_GET_MODULE(mUpdateStatus) == NS_ERROR_MODULE_URL_CLASSIFIER + ? mUpdateStatus + : NS_ERROR_UC_UPDATE_UNKNOWN; + } + + // Do not record telemetry for testing tables. + if (!provider.EqualsLiteral(TESTING_TABLE_PROVIDER_NAME)) { + Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR, provider, + NS_ERROR_GET_CODE(updateStatus)); + } + + MutexAutoLock lock(mUpdateObserverLock); + + if (!mUpdateObserver) { + // In the normal shutdown process, CancelUpdate() would NOT be + // called prior to NotifyUpdateObserver(). However, CancelUpdate() + // is a public API which can be called in the test case at any point. + // If the call sequence is FinishUpdate() then CancelUpdate(), the later + // might be executed before NotifyUpdateObserver() which is triggered + // by the update thread. In this case, we will get null mUpdateObserver. + NS_WARNING( + "CancelUpdate() is called before we asynchronously call " + "NotifyUpdateObserver() in FinishUpdate()."); + + // The DB cleanup will be done in CancelUpdate() so we can just return. + return NS_OK; + } + + // Null out mUpdateObserver before notifying so that BeginUpdate() + // becomes available prior to callback. + nsCOMPtr<nsIUrlClassifierUpdateObserver> updateObserver = nullptr; + updateObserver.swap(mUpdateObserver); + + if (NS_SUCCEEDED(mUpdateStatus)) { + LOG(("Notifying success: %d", mUpdateWaitSec)); + updateObserver->UpdateSuccess(mUpdateWaitSec); + } else { + if (LOG_ENABLED()) { + nsAutoCString errorName; + mozilla::GetErrorName(mUpdateStatus, errorName); + LOG(("Notifying error: %s (%" PRIu32 ")", errorName.get(), + static_cast<uint32_t>(mUpdateStatus))); + } + + updateObserver->UpdateError(mUpdateStatus); + /* + * mark the tables as spoiled(clear cache in LookupCache), we don't want to + * block hosts longer than normal because our update failed + */ + mClassifier->ResetTables(Classifier::Clear_Cache, mUpdateTables); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::ResetDatabase() { + nsresult rv = OpenDb(); + + if (NS_SUCCEEDED(rv)) { + mClassifier->Reset(); + } + + rv = CloseDb(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::ReloadDatabase() { + // This will null out mClassifier + nsresult rv = CloseDb(); + NS_ENSURE_SUCCESS(rv, rv); + + // Create new mClassifier and load prefixset and completions from disk. + rv = OpenDb(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::ClearCache() { + nsTArray<nsCString> tables; + nsresult rv = mClassifier->ActiveTables(tables); + NS_ENSURE_SUCCESS(rv, rv); + + mClassifier->ResetTables(Classifier::Clear_Cache, tables); + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::CancelUpdate() { + LOG(("nsUrlClassifierDBServiceWorker::CancelUpdate")); + + { + MutexAutoLock lock(mUpdateObserverLock); + if (!mUpdateObserver) { + LOG(("No UpdateObserver, nothing to cancel")); + + return NS_OK; + } + + LOG(("UpdateObserver exists, cancelling")); + + mUpdateStatus = NS_BINDING_ABORTED; + + mUpdateObserver->UpdateError(mUpdateStatus); + } + + /* + * mark the tables as spoiled(clear cache in LookupCache), we don't want to + * block hosts longer than normal because our update failed + */ + mClassifier->ResetTables(Classifier::Clear_Cache, mUpdateTables); + + ResetStream(); + ResetUpdate(); + + return NS_OK; +} + +void nsUrlClassifierDBServiceWorker::FlushAndDisableAsyncUpdate() { + LOG(("nsUrlClassifierDBServiceWorker::FlushAndDisableAsyncUpdate()")); + + if (mClassifier) { + mClassifier->FlushAndDisableAsyncUpdate(); + } +} + +// Allows the main thread to delete the connection which may be in +// a background thread. +// XXX This could be turned into a single shutdown event so the logic +// is simpler in nsUrlClassifierDBService::Shutdown. +nsresult nsUrlClassifierDBServiceWorker::CloseDb() { + if (mClassifier) { + mClassifier->Close(); + mClassifier = nullptr; + } + + // Clear last completion result when close db so we will still cache + // completion result next time we re-open it. + mLastResults.Clear(); + + LOG(("urlclassifier db closed\n")); + + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::PreShutdown() { + if (mClassifier) { + // Classifier close will release all lookup caches which may be a + // time-consuming job. See Bug 1408631. + mClassifier->Close(); + } + + // WARNING: nothing we put here should affect an ongoing update thread. When + // in doubt, put things in Shutdown() instead. + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::CacheCompletions( + const ConstCacheResultArray& aResults) { + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + LOG(("nsUrlClassifierDBServiceWorker::CacheCompletions [%p]", this)); + if (!mClassifier) { + return NS_OK; + } + + if (aResults.Length() == 0) { + return NS_OK; + } + + if (IsSameAsLastResults(aResults)) { + LOG(("Skipping completions that have just been cached already.")); + return NS_OK; + } + + // Only cache results for tables that we have, don't take + // in tables we might accidentally have hit during a completion. + // This happens due to goog vs googpub lists existing. + nsTArray<nsCString> tables; + nsresult rv = mClassifier->ActiveTables(tables); + NS_ENSURE_SUCCESS(rv, rv); + if (LOG_ENABLED()) { + nsCString s; + for (size_t i = 0; i < tables.Length(); i++) { + if (!s.IsEmpty()) { + s += ","; + } + s += tables[i]; + } + LOG(("Active tables: %s", s.get())); + } + + ConstTableUpdateArray updates; + + for (const auto& result : aResults) { + bool activeTable = false; + + for (uint32_t table = 0; table < tables.Length(); table++) { + if (tables[table].Equals(result->table)) { + activeTable = true; + break; + } + } + if (activeTable) { + UniquePtr<ProtocolParser> pParse; + if (result->Ver() == CacheResult::V2) { + pParse.reset(new ProtocolParserV2()); + } else { + pParse.reset(new ProtocolParserProtobuf()); + } + + RefPtr<TableUpdate> tu = pParse->GetTableUpdate(result->table); + + rv = CacheResultToTableUpdate(result, tu); + if (NS_FAILED(rv)) { + // We can bail without leaking here because ForgetTableUpdates + // hasn't been called yet. + return rv; + } + updates.AppendElement(tu); + pParse->ForgetTableUpdates(); + } else { + LOG(("Completion received, but table %s is not active, so not caching.", + result->table.get())); + } + } + + rv = mClassifier->ApplyFullHashes(updates); + if (NS_SUCCEEDED(rv)) { + mLastResults = aResults.Clone(); + } + return rv; +} + +nsresult nsUrlClassifierDBServiceWorker::CacheResultToTableUpdate( + RefPtr<const CacheResult> aCacheResult, RefPtr<TableUpdate> aUpdate) { + RefPtr<TableUpdateV2> tuV2 = TableUpdate::Cast<TableUpdateV2>(aUpdate); + if (tuV2) { + RefPtr<const CacheResultV2> result = + CacheResult::Cast<const CacheResultV2>(aCacheResult); + MOZ_ASSERT(result); + + if (result->miss) { + return tuV2->NewMissPrefix(result->prefix); + } else { + LOG(("CacheCompletion hash %X, Addchunk %d", + result->completion.ToUint32(), result->addChunk)); + + nsresult rv = tuV2->NewAddComplete(result->addChunk, result->completion); + if (NS_FAILED(rv)) { + return rv; + } + return tuV2->NewAddChunk(result->addChunk); + } + } + + RefPtr<TableUpdateV4> tuV4 = TableUpdate::Cast<TableUpdateV4>(aUpdate); + if (tuV4) { + RefPtr<const CacheResultV4> result = + CacheResult::Cast<const CacheResultV4>(aCacheResult); + MOZ_ASSERT(result); + + if (LOG_ENABLED()) { + const FullHashExpiryCache& fullHashes = result->response.fullHashes; + for (const auto& entry : fullHashes) { + Completion completion; + completion.Assign(entry.GetKey()); + LOG(("CacheCompletion(v4) hash %X, CacheExpireTime %" PRId64, + completion.ToUint32(), entry.GetData())); + } + } + + tuV4->NewFullHashResponse(result->prefix, result->response); + return NS_OK; + } + + // tableUpdate object should be either V2 or V4. + return NS_ERROR_FAILURE; +} + +nsresult nsUrlClassifierDBServiceWorker::OpenDb() { + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + MOZ_ASSERT(!NS_IsMainThread(), "Must initialize DB on background thread"); + // Connection already open, don't do anything. + if (mClassifier) { + return NS_OK; + } + + nsresult rv; + RefPtr<Classifier> classifier = new (fallible) Classifier(); + if (!classifier) { + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = classifier->Open(*mCacheDir); + NS_ENSURE_SUCCESS(rv, rv); + + mClassifier = classifier; + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBServiceWorker::ClearLastResults() { + MOZ_ASSERT(!NS_IsMainThread(), "Must be on the background thread"); + mLastResults.Clear(); + return NS_OK; +} + +nsresult nsUrlClassifierDBServiceWorker::GetCacheInfo( + const nsACString& aTable, nsIUrlClassifierCacheInfo** aCache) { + MOZ_ASSERT(!NS_IsMainThread(), "Must be on the background thread"); + if (!mClassifier) { + return NS_ERROR_NOT_AVAILABLE; + } + + mClassifier->GetCacheInfo(aTable, aCache); + return NS_OK; +} + +bool nsUrlClassifierDBServiceWorker::IsSameAsLastResults( + const ConstCacheResultArray& aResult) const { + if (mLastResults.Length() != aResult.Length()) { + return false; + } + + bool equal = true; + for (uint32_t i = 0; i < mLastResults.Length() && equal; i++) { + RefPtr<const CacheResult> lhs = mLastResults[i]; + RefPtr<const CacheResult> rhs = aResult[i]; + + if (lhs->Ver() != rhs->Ver()) { + return false; + } + + if (lhs->Ver() == CacheResult::V2) { + equal = *(CacheResult::Cast<const CacheResultV2>(lhs)) == + *(CacheResult::Cast<const CacheResultV2>(rhs)); + } else if (lhs->Ver() == CacheResult::V4) { + equal = *(CacheResult::Cast<const CacheResultV4>(lhs)) == + *(CacheResult::Cast<const CacheResultV4>(rhs)); + } + } + + return equal; +} + +// ------------------------------------------------------------------------- +// nsUrlClassifierLookupCallback +// +// This class takes the results of a lookup found on the worker thread +// and handles any necessary partial hash expansions before calling +// the client callback. + +class nsUrlClassifierLookupCallback final + : public nsIUrlClassifierLookupCallback, + public nsIUrlClassifierHashCompleterCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURLCLASSIFIERLOOKUPCALLBACK + NS_DECL_NSIURLCLASSIFIERHASHCOMPLETERCALLBACK + + nsUrlClassifierLookupCallback(nsUrlClassifierDBService* dbservice, + nsIUrlClassifierCallback* c) + : mDBService(dbservice), + mResults(nullptr), + mPendingCompletions(0), + mCallback(c) {} + + private: + ~nsUrlClassifierLookupCallback(); + + nsresult HandleResults(); + nsresult ProcessComplete(RefPtr<CacheResult> aCacheResult); + nsresult CacheMisses(); + + RefPtr<nsUrlClassifierDBService> mDBService; + UniquePtr<LookupResultArray> mResults; + + // Completed results to send back to the worker for caching. + ConstCacheResultArray mCacheResults; + + uint32_t mPendingCompletions; + nsCOMPtr<nsIUrlClassifierCallback> mCallback; +}; + +NS_IMPL_ISUPPORTS(nsUrlClassifierLookupCallback, nsIUrlClassifierLookupCallback, + nsIUrlClassifierHashCompleterCallback) + +nsUrlClassifierLookupCallback::~nsUrlClassifierLookupCallback() { + if (mCallback) { + NS_ReleaseOnMainThread("nsUrlClassifierLookupCallback::mCallback", + mCallback.forget()); + } +} + +NS_IMETHODIMP +nsUrlClassifierLookupCallback::LookupComplete( + UniquePtr<LookupResultArray> results) { + NS_ASSERTION( + mResults == nullptr, + "Should only get one set of results per nsUrlClassifierLookupCallback!"); + + if (!results) { + HandleResults(); + return NS_OK; + } + + mResults = std::move(results); + + // Check the results entries that need to be completed. + for (const auto& result : *mResults) { + // We will complete partial matches and matches that are stale. + if (!result->Confirmed()) { + nsCOMPtr<nsIUrlClassifierHashCompleter> completer; + nsCString gethashUrl; + nsresult rv; + nsCOMPtr<nsIUrlListManager> listManager = + do_GetService("@mozilla.org/url-classifier/listmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = listManager->GetGethashUrl(result->mTableName, gethashUrl); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("The match from %s needs to be completed at %s", + result->mTableName.get(), gethashUrl.get())); + // gethashUrls may be empty in 2 cases: test tables, and on startup where + // we may have found a prefix in an existing table before the listmanager + // has registered the table. In the second case we should not call + // complete. + if ((!gethashUrl.IsEmpty() || + nsUrlClassifierUtils::IsTestTable(result->mTableName)) && + mDBService->GetCompleter(result->mTableName, + getter_AddRefs(completer))) { + // Bug 1323953 - Send the first 4 bytes for completion no matter how + // long we matched the prefix. + nsresult rv = completer->Complete(result->PartialHash(), gethashUrl, + result->mTableName, this); + if (NS_SUCCEEDED(rv)) { + mPendingCompletions++; + } + } else { + // For tables with no hash completer, a complete hash match is + // good enough, we'll consider it is valid. + if (result->Complete()) { + result->mConfirmed = true; + LOG(("Skipping completion in a table without a valid completer (%s).", + result->mTableName.get())); + } else { + NS_WARNING( + "Partial match in a table without a valid completer, ignoring " + "partial match."); + } + } + } + } + + LOG( + ("nsUrlClassifierLookupCallback::LookupComplete [%p] " + "%u pending completions", + this, mPendingCompletions)); + if (mPendingCompletions == 0) { + // All results were complete, we're ready! + HandleResults(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierLookupCallback::CompletionFinished(nsresult status) { + if (LOG_ENABLED()) { + nsAutoCString errorName; + mozilla::GetErrorName(status, errorName); + LOG(("nsUrlClassifierLookupCallback::CompletionFinished [%p, %s]", this, + errorName.get())); + } + + mPendingCompletions--; + if (mPendingCompletions == 0) { + HandleResults(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierLookupCallback::CompletionV2(const nsACString& aCompleteHash, + const nsACString& aTableName, + uint32_t aChunkId) { + LOG(("nsUrlClassifierLookupCallback::Completion [%p, %s, %d]", this, + PromiseFlatCString(aTableName).get(), aChunkId)); + + MOZ_ASSERT(!StringEndsWith(aTableName, "-proto"_ns)); + + RefPtr<CacheResultV2> result = new CacheResultV2(); + + result->table = aTableName; + result->prefix.Assign(aCompleteHash); + result->completion.Assign(aCompleteHash); + result->addChunk = aChunkId; + + return ProcessComplete(result); +} + +NS_IMETHODIMP +nsUrlClassifierLookupCallback::CompletionV4(const nsACString& aPartialHash, + const nsACString& aTableName, + uint32_t aNegativeCacheDuration, + nsIArray* aFullHashes) { + LOG(("nsUrlClassifierLookupCallback::CompletionV4 [%p, %s, %d]", this, + PromiseFlatCString(aTableName).get(), aNegativeCacheDuration)); + + MOZ_ASSERT(StringEndsWith(aTableName, "-proto"_ns)); + + if (!aFullHashes) { + return NS_ERROR_INVALID_ARG; + } + + if (aNegativeCacheDuration > MAXIMUM_NEGATIVE_CACHE_DURATION_SEC) { + LOG( + ("Negative cache duration too large, clamping it down to" + "a reasonable value.")); + aNegativeCacheDuration = MAXIMUM_NEGATIVE_CACHE_DURATION_SEC; + } + + RefPtr<CacheResultV4> result = new CacheResultV4(); + + int64_t nowSec = PR_Now() / PR_USEC_PER_SEC; + + result->table = aTableName; + result->prefix.Assign(aPartialHash); + result->response.negativeCacheExpirySec = nowSec + aNegativeCacheDuration; + + // Fill in positive cache entries. + uint32_t fullHashCount = 0; + nsresult rv = aFullHashes->GetLength(&fullHashCount); + if (NS_FAILED(rv)) { + return rv; + } + + for (uint32_t i = 0; i < fullHashCount; i++) { + nsCOMPtr<nsIFullHashMatch> match = do_QueryElementAt(aFullHashes, i); + + nsCString fullHash; + match->GetFullHash(fullHash); + + uint32_t duration; + match->GetCacheDuration(&duration); + + result->response.fullHashes.InsertOrUpdate(fullHash, nowSec + duration); + } + + return ProcessComplete(result); +} + +nsresult nsUrlClassifierLookupCallback::ProcessComplete( + RefPtr<CacheResult> aCacheResult) { + NS_ENSURE_ARG_POINTER(mResults); + + if (!mCacheResults.AppendElement(aCacheResult, fallible)) { + // OK if this failed, we just won't cache the item. + } + + // Check if this matched any of our results. + for (const auto& result : *mResults) { + // Now, see if it verifies a lookup + if (!result->mNoise && result->mTableName.Equals(aCacheResult->table) && + aCacheResult->findCompletion(result->CompleteHash())) { + result->mProtocolConfirmed = true; + } + } + + return NS_OK; +} + +nsresult nsUrlClassifierLookupCallback::HandleResults() { + if (!mResults) { + // No results, this URI is clean. + LOG(("nsUrlClassifierLookupCallback::HandleResults [%p, no results]", + this)); + return mCallback->HandleEvent(""_ns); + } + MOZ_ASSERT(mPendingCompletions == 0, + "HandleResults() should never be " + "called while there are pending completions"); + + LOG(("nsUrlClassifierLookupCallback::HandleResults [%p, %zu results]", this, + mResults->Length())); + + nsCOMPtr<nsIUrlClassifierClassifyCallback> classifyCallback = + do_QueryInterface(mCallback); + + nsTArray<nsCString> tables; + // Build a stringified list of result tables. + for (const auto& result : *mResults) { + // Leave out results that weren't confirmed, as their existence on + // the list can't be verified. Also leave out randomly-generated + // noise. + if (result->mNoise) { + LOG(("Skipping result %s from table %s (noise)", + result->PartialHashHex().get(), result->mTableName.get())); + continue; + } + + if (!result->Confirmed()) { + LOG(("Skipping result %s from table %s (not confirmed)", + result->PartialHashHex().get(), result->mTableName.get())); + continue; + } + + LOG(("Confirmed result %s from table %s", result->PartialHashHex().get(), + result->mTableName.get())); + + if (tables.IndexOf(result->mTableName) == nsTArray<nsCString>::NoIndex) { + tables.AppendElement(result->mTableName); + } + + if (classifyCallback) { + nsCString fullHashString; + result->hash.complete.ToString(fullHashString); + classifyCallback->HandleResult(result->mTableName, fullHashString); + } + } + + // Some parts of this gethash request generated no hits at all. + // Save the prefixes we checked to prevent repeated requests. + CacheMisses(); + + // This hands ownership of the cache results array back to the worker + // thread. + mDBService->CacheCompletions(mCacheResults); + mCacheResults.Clear(); + + return mCallback->HandleEvent(StringJoin(","_ns, tables)); +} + +nsresult nsUrlClassifierLookupCallback::CacheMisses() { + MOZ_ASSERT(mResults); + + for (const RefPtr<const LookupResult> result : *mResults) { + // Skip V4 because cache information is already included in the + // fullhash response so we don't need to manually add it here. + if (!result->mProtocolV2 || result->Confirmed() || result->mNoise) { + continue; + } + + RefPtr<CacheResultV2> cacheResult = new CacheResultV2(); + + cacheResult->table = result->mTableName; + cacheResult->prefix = result->hash.fixedLengthPrefix; + cacheResult->miss = true; + if (!mCacheResults.AppendElement(cacheResult, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + return NS_OK; +} + +struct Provider { + nsCString name; + uint8_t priority; +}; + +// Order matters +// Provider which is not included in this table has the lowest priority 0 +static const Provider kBuiltInProviders[] = { + {"mozilla"_ns, 1}, + {"google4"_ns, 2}, + {"google"_ns, 3}, +}; + +// ------------------------------------------------------------------------- +// Helper class for nsIURIClassifier implementation, handle classify result and +// send back to nsIURIClassifier + +class nsUrlClassifierClassifyCallback final + : public nsIUrlClassifierCallback, + public nsIUrlClassifierClassifyCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURLCLASSIFIERCALLBACK + NS_DECL_NSIURLCLASSIFIERCLASSIFYCALLBACK + + explicit nsUrlClassifierClassifyCallback(nsIURIClassifierCallback* c) + : mCallback(c) {} + + private: + struct ClassifyMatchedInfo { + nsCString table; + nsCString fullhash; + Provider provider; + nsresult errorCode; + }; + + ~nsUrlClassifierClassifyCallback() = default; + + nsCOMPtr<nsIURIClassifierCallback> mCallback; + nsTArray<ClassifyMatchedInfo> mMatchedArray; +}; + +NS_IMPL_ISUPPORTS(nsUrlClassifierClassifyCallback, nsIUrlClassifierCallback, + nsIUrlClassifierClassifyCallback) + +NS_IMETHODIMP +nsUrlClassifierClassifyCallback::HandleEvent(const nsACString& tables) { + nsresult response = TablesToResponse(tables); + ClassifyMatchedInfo* matchedInfo = nullptr; + + if (NS_FAILED(response)) { + // Filter all matched info which has correct response + // In the case multiple tables found, use the higher priority provider + nsTArray<ClassifyMatchedInfo> matches; + for (uint32_t i = 0; i < mMatchedArray.Length(); i++) { + if (mMatchedArray[i].errorCode == response && + (!matchedInfo || matchedInfo->provider.priority < + mMatchedArray[i].provider.priority)) { + matchedInfo = &mMatchedArray[i]; + } + } + } + + nsCString provider = matchedInfo ? matchedInfo->provider.name : ""_ns; + nsCString fullhash = matchedInfo ? matchedInfo->fullhash : ""_ns; + nsCString table = matchedInfo ? matchedInfo->table : ""_ns; + + mCallback->OnClassifyComplete(response, table, provider, fullhash); + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierClassifyCallback::HandleResult(const nsACString& aTable, + const nsACString& aFullHash) { + LOG( + ("nsUrlClassifierClassifyCallback::HandleResult [%p, table %s full hash " + "%s]", + this, PromiseFlatCString(aTable).get(), + PromiseFlatCString(aFullHash).get())); + + if (NS_WARN_IF(aTable.IsEmpty()) || NS_WARN_IF(aFullHash.IsEmpty())) { + return NS_ERROR_INVALID_ARG; + } + + ClassifyMatchedInfo* matchedInfo = mMatchedArray.AppendElement(); + matchedInfo->table = aTable; + matchedInfo->fullhash = aFullHash; + + nsUrlClassifierUtils* urlUtil = nsUrlClassifierUtils::GetInstance(); + if (NS_WARN_IF(!urlUtil)) { + return NS_ERROR_FAILURE; + } + + nsCString provider; + nsresult rv = urlUtil->GetProvider(aTable, provider); + + matchedInfo->provider.name = NS_SUCCEEDED(rv) ? provider : ""_ns; + matchedInfo->provider.priority = 0; + for (uint8_t i = 0; i < ArrayLength(kBuiltInProviders); i++) { + if (kBuiltInProviders[i].name.Equals(matchedInfo->provider.name)) { + matchedInfo->provider.priority = kBuiltInProviders[i].priority; + } + } + matchedInfo->errorCode = TablesToResponse(aTable); + + return NS_OK; +} + +// ------------------------------------------------------------------------- +// Proxy class implementation + +NS_IMPL_ADDREF(nsUrlClassifierDBService) +NS_IMPL_RELEASE(nsUrlClassifierDBService) +NS_INTERFACE_MAP_BEGIN(nsUrlClassifierDBService) + // Only nsIURIClassifier is supported in the content process! + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIUrlClassifierDBService, + XRE_IsParentProcess()) + NS_INTERFACE_MAP_ENTRY(nsIURIClassifier) + NS_INTERFACE_MAP_ENTRY(nsIUrlClassifierInfo) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIObserver, XRE_IsParentProcess()) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURIClassifier) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<nsUrlClassifierDBService> +nsUrlClassifierDBService::GetInstance(nsresult* result) { + *result = NS_OK; + if (!sUrlClassifierDBService) { + sUrlClassifierDBService = new (fallible) nsUrlClassifierDBService(); + if (!sUrlClassifierDBService) { + *result = NS_ERROR_OUT_OF_MEMORY; + return nullptr; + } + + *result = sUrlClassifierDBService->Init(); + if (NS_FAILED(*result)) { + return nullptr; + } + } + return do_AddRef(sUrlClassifierDBService); +} + +nsUrlClassifierDBService::nsUrlClassifierDBService() : mInUpdate(false) {} + +nsUrlClassifierDBService::~nsUrlClassifierDBService() { + sUrlClassifierDBService = nullptr; +} + +nsresult nsUrlClassifierDBService::ReadDisallowCompletionsTablesFromPrefs() { + nsAutoCString tables; + + Preferences::GetCString(DISALLOW_COMPLETION_TABLE_PREF, tables); + Classifier::SplitTables(tables, mDisallowCompletionsTables); + + return NS_OK; +} + +nsresult nsUrlClassifierDBService::Init() { + MOZ_ASSERT(NS_IsMainThread(), "Must initialize DB service on main thread"); + + switch (XRE_GetProcessType()) { + case GeckoProcessType_Default: + // The parent process is supported. + break; + case GeckoProcessType_Content: + // In a content process, we simply forward all requests to the parent + // process, so we can skip the initialization steps here. Note that since + // we never register an observer, Shutdown() will also never be called in + // the content process. + return NS_OK; + default: + // No other process type is supported! + return NS_ERROR_NOT_AVAILABLE; + } + + uint32_t hashNoise = + Preferences::GetUint(GETHASH_NOISE_PREF, GETHASH_NOISE_DEFAULT); + ReadDisallowCompletionsTablesFromPrefs(); + + // Force nsUrlClassifierUtils loading on main thread. + if (NS_WARN_IF(!nsUrlClassifierUtils::GetInstance())) { + return NS_ERROR_FAILURE; + } + + // Directory providers must also be accessed on the main thread. + nsresult rv; + nsCOMPtr<nsIFile> cacheDir; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR, + getter_AddRefs(cacheDir)); + if (NS_FAILED(rv)) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(cacheDir)); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Start the background thread. + rv = NS_NewNamedThread("URL Classifier", &gDbBackgroundThread); + if (NS_FAILED(rv)) return rv; + + mWorker = new (fallible) nsUrlClassifierDBServiceWorker(); + if (!mWorker) { + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = mWorker->Init(hashNoise, cacheDir, this); + if (NS_FAILED(rv)) { + mWorker = nullptr; + return rv; + } + + // Proxy for calling the worker on the background thread + mWorkerProxy = new UrlClassifierDBServiceWorkerProxy(mWorker); + rv = mWorkerProxy->OpenDb(); + if (NS_FAILED(rv)) { + return rv; + } + + // Add an observer for shutdown + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return NS_ERROR_FAILURE; + + // The application is about to quit + observerService->AddObserver(this, "quit-application", false); + observerService->AddObserver(this, "profile-before-change", false); + + Preferences::AddStrongObserver(this, DISALLOW_COMPLETION_TABLE_PREF); + + return NS_OK; +} + +// nsChannelClassifier is the only consumer of this interface. +NS_IMETHODIMP +nsUrlClassifierDBService::Classify(nsIPrincipal* aPrincipal, + nsIURIClassifierCallback* c, bool* aResult) { + NS_ENSURE_ARG(aPrincipal); + MOZ_ASSERT(c); + NS_ENSURE_ARG(aResult); + + if (aPrincipal->IsSystemPrincipal()) { + *aResult = false; + return NS_OK; + } + + if (XRE_IsContentProcess()) { + using namespace mozilla::dom; + + ContentChild* content = ContentChild::GetSingleton(); + MOZ_ASSERT(content); + + auto actor = static_cast<URLClassifierChild*>( + content->AllocPURLClassifierChild(aPrincipal, aResult)); + MOZ_ASSERT(actor); + + if (!content->SendPURLClassifierConstructor(actor, aPrincipal, aResult)) { + *aResult = false; + return NS_ERROR_FAILURE; + } + + actor->SetCallback(c); + return NS_OK; + } + + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsIPermissionManager> permissionManager = + components::PermissionManager::Service(); + if (NS_WARN_IF(!permissionManager)) { + return NS_ERROR_FAILURE; + } + + uint32_t perm; + nsresult rv = permissionManager->TestPermissionFromPrincipal( + aPrincipal, "safe-browsing"_ns, &perm); + NS_ENSURE_SUCCESS(rv, rv); + + if (perm == nsIPermissionManager::ALLOW_ACTION) { + *aResult = false; + return NS_OK; + } + + nsTArray<RefPtr<nsIUrlClassifierFeature>> features; + mozilla::net::UrlClassifierFeatureFactory::GetPhishingProtectionFeatures( + features); + if (features.IsEmpty()) { + *aResult = false; + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + // Casting to BasePrincipal, as we can't get InnerMost URI otherwise + auto* basePrincipal = BasePrincipal::Cast(aPrincipal); + rv = basePrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); + + // Let's keep the features alive and release them on the correct thread. + RefPtr<FeatureHolder> holder = + FeatureHolder::Create(uri, features, nsIUrlClassifierFeature::blocklist); + if (NS_WARN_IF(!holder)) { + return NS_ERROR_FAILURE; + } + + uri = NS_GetInnermostURI(uri); + NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); + + nsUrlClassifierUtils* utilsService = nsUrlClassifierUtils::GetInstance(); + if (NS_WARN_IF(!utilsService)) { + return NS_ERROR_FAILURE; + } + + // Canonicalize the url + nsAutoCString key; + rv = utilsService->GetKeyForURI(uri, key); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsUrlClassifierClassifyCallback> callback = + new (fallible) nsUrlClassifierClassifyCallback(c); + if (NS_WARN_IF(!callback)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The rest is done async. + rv = LookupURI(key, holder, callback); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = true; + return NS_OK; +} + +class ThreatHitReportListener final : public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + ThreatHitReportListener() = default; + + private: + ~ThreatHitReportListener() = default; +}; + +NS_IMPL_ISUPPORTS(ThreatHitReportListener, nsIStreamListener, + nsIRequestObserver) + +NS_IMETHODIMP +ThreatHitReportListener::OnStartRequest(nsIRequest* aRequest) { + if (!LOG_ENABLED()) { + return NS_OK; // Nothing to do! + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + NS_ENSURE_TRUE(httpChannel, NS_OK); + + nsresult rv, status; + rv = httpChannel->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, NS_OK); + nsAutoCString errorName; + mozilla::GetErrorName(status, errorName); + + uint32_t requestStatus; + rv = httpChannel->GetResponseStatus(&requestStatus); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsAutoCString spec; + nsCOMPtr<nsIURI> uri; + rv = httpChannel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv) && uri) { + uri->GetAsciiSpec(spec); + } + nsCOMPtr<nsIURLFormatter> urlFormatter = + do_GetService("@mozilla.org/toolkit/URLFormatterService;1"); + nsAutoString trimmed; + rv = urlFormatter->TrimSensitiveURLs(NS_ConvertUTF8toUTF16(spec), trimmed); + NS_ENSURE_SUCCESS(rv, NS_OK); + + LOG( + ("ThreatHitReportListener::OnStartRequest " + "(status=%s, code=%d, uri=%s, this=%p)", + errorName.get(), requestStatus, NS_ConvertUTF16toUTF8(trimmed).get(), + this)); + + return NS_OK; +} + +NS_IMETHODIMP +ThreatHitReportListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + return NS_OK; +} + +NS_IMETHODIMP +ThreatHitReportListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + NS_ENSURE_TRUE(httpChannel, aStatus); + + uint8_t netErrCode = + NS_FAILED(aStatus) ? mozilla::safebrowsing::NetworkErrorToBucket(aStatus) + : 0; + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::URLCLASSIFIER_THREATHIT_NETWORK_ERROR, netErrCode); + + uint32_t requestStatus; + nsresult rv = httpChannel->GetResponseStatus(&requestStatus); + NS_ENSURE_SUCCESS(rv, aStatus); + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::URLCLASSIFIER_THREATHIT_REMOTE_STATUS, + mozilla::safebrowsing::HTTPStatusToBucket(requestStatus)); + + if (LOG_ENABLED()) { + nsAutoCString errorName; + mozilla::GetErrorName(aStatus, errorName); + + nsAutoCString spec; + nsCOMPtr<nsIURI> uri; + rv = httpChannel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv) && uri) { + uri->GetAsciiSpec(spec); + } + nsCOMPtr<nsIURLFormatter> urlFormatter = + do_GetService("@mozilla.org/toolkit/URLFormatterService;1"); + nsString trimmed; + rv = urlFormatter->TrimSensitiveURLs(NS_ConvertUTF8toUTF16(spec), trimmed); + NS_ENSURE_SUCCESS(rv, aStatus); + + LOG( + ("ThreatHitReportListener::OnStopRequest " + "(status=%s, code=%d, uri=%s, this=%p)", + errorName.get(), requestStatus, NS_ConvertUTF16toUTF8(trimmed).get(), + this)); + } + + return aStatus; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::SendThreatHitReport(nsIChannel* aChannel, + const nsACString& aProvider, + const nsACString& aList, + const nsACString& aFullHash) { + NS_ENSURE_ARG_POINTER(aChannel); + + if (aProvider.IsEmpty()) { + LOG(("nsUrlClassifierDBService::SendThreatHitReport missing provider")); + return NS_ERROR_FAILURE; + } + if (aList.IsEmpty()) { + LOG(("nsUrlClassifierDBService::SendThreatHitReport missing list")); + return NS_ERROR_FAILURE; + } + if (aFullHash.IsEmpty()) { + LOG(("nsUrlClassifierDBService::SendThreatHitReport missing fullhash")); + return NS_ERROR_FAILURE; + } + + nsPrintfCString reportUrlPref( + "browser.safebrowsing.provider.%s.dataSharingURL", + PromiseFlatCString(aProvider).get()); + + nsCOMPtr<nsIURLFormatter> formatter( + do_GetService("@mozilla.org/toolkit/URLFormatterService;1")); + if (!formatter) { + return NS_ERROR_UNEXPECTED; + } + + nsString urlStr; + nsresult rv = + formatter->FormatURLPref(NS_ConvertUTF8toUTF16(reportUrlPref), urlStr); + NS_ENSURE_SUCCESS(rv, rv); + + if (urlStr.IsEmpty() || u"about:blank"_ns.Equals(urlStr)) { + LOG(("%s is missing a ThreatHit data reporting URL.", + PromiseFlatCString(aProvider).get())); + return NS_OK; + } + + nsUrlClassifierUtils* utilsService = nsUrlClassifierUtils::GetInstance(); + if (NS_WARN_IF(!utilsService)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString reportBody; + rv = + utilsService->MakeThreatHitReport(aChannel, aList, aFullHash, reportBody); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIStringInputStream> sis( + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID)); + rv = sis->SetData(reportBody.get(), reportBody.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("Sending the following ThreatHit report to %s about %s: %s", + PromiseFlatCString(aProvider).get(), PromiseFlatCString(aList).get(), + reportBody.get())); + + nsCOMPtr<nsIURI> reportURI; + rv = NS_NewURI(getter_AddRefs(reportURI), urlStr); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t loadFlags = nsIRequest::LOAD_ANONYMOUS | // no cookies + nsIChannel::INHIBIT_CACHING | + nsIChannel::LOAD_BYPASS_CACHE; + + nsCOMPtr<nsIChannel> reportChannel; + rv = NS_NewChannel(getter_AddRefs(reportChannel), reportURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // nsICookieJarSettings + nullptr, // aPerformanceStorage + nullptr, // aLoadGroup + nullptr, loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = reportChannel->LoadInfo(); + mozilla::OriginAttributes attrs; + attrs.mFirstPartyDomain.AssignLiteral(NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN); + loadInfo->SetOriginAttributes(attrs); + + nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(reportChannel)); + NS_ENSURE_TRUE(uploadChannel, NS_ERROR_FAILURE); + rv = uploadChannel->SetUploadStream(sis, "application/x-protobuf"_ns, -1); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(reportChannel)); + NS_ENSURE_TRUE(httpChannel, NS_ERROR_FAILURE); + rv = httpChannel->SetRequestMethod("POST"_ns); + NS_ENSURE_SUCCESS(rv, rv); + // Disable keepalive. + rv = httpChannel->SetRequestHeader("Connection"_ns, "close"_ns, false); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<ThreatHitReportListener> listener = new ThreatHitReportListener(); + rv = reportChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + LOG(("Failure to send Safe Browsing ThreatHit report")); + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::Lookup(nsIPrincipal* aPrincipal, + const nsACString& aTables, + nsIUrlClassifierCallback* aCallback) { + // We don't expect someone with SystemPrincipal calls this API(See Bug + // 813897). + MOZ_ASSERT(!aPrincipal->IsSystemPrincipal()); + + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + nsTArray<nsCString> tableArray; + Classifier::SplitTables(aTables, tableArray); + + nsCOMPtr<nsIUrlClassifierFeature> feature; + nsresult rv = CreateFeatureWithTables( + "lookup"_ns, tableArray, nsTArray<nsCString>(), getter_AddRefs(feature)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> uri; + // Casting to BasePrincipal, as we can't get InnerMost URI otherwise + auto* basePrincipal = BasePrincipal::Cast(aPrincipal); + rv = basePrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); + + nsTArray<RefPtr<nsIUrlClassifierFeature>> features; + features.AppendElement(feature.get()); + + // Let's keep the features alive and release them on the correct thread. + RefPtr<FeatureHolder> holder = + FeatureHolder::Create(uri, features, nsIUrlClassifierFeature::blocklist); + if (NS_WARN_IF(!holder)) { + return NS_ERROR_FAILURE; + } + + uri = NS_GetInnermostURI(uri); + NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); + + nsUrlClassifierUtils* utilsService = nsUrlClassifierUtils::GetInstance(); + if (NS_WARN_IF(!utilsService)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString key; + // Canonicalize the url + rv = utilsService->GetKeyForURI(uri, key); + NS_ENSURE_SUCCESS(rv, rv); + + return LookupURI(key, holder, aCallback); +} + +nsresult nsUrlClassifierDBService::LookupURI( + const nsACString& aKey, FeatureHolder* aHolder, + nsIUrlClassifierCallback* aCallback) { + MOZ_ASSERT(aHolder); + MOZ_ASSERT(aCallback); + + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + // Create an nsUrlClassifierLookupCallback object. This object will + // take care of confirming partial hash matches if necessary before + // calling the client's callback. + nsCOMPtr<nsIUrlClassifierLookupCallback> callback = + new nsUrlClassifierLookupCallback(this, aCallback); + + nsCOMPtr<nsIUrlClassifierLookupCallback> proxyCallback = + new UrlClassifierLookupCallbackProxy(callback); + + // Queue this lookup and call the lookup function to flush the queue if + // necessary. + nsresult rv = mWorker->QueueLookup(aKey, aHolder, proxyCallback); + NS_ENSURE_SUCCESS(rv, rv); + + // This seems to just call HandlePendingLookups. + nsAutoCString dummy; + return mWorkerProxy->Lookup(nullptr, dummy, nullptr); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::GetTables(nsIUrlClassifierCallback* c) { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + // The proxy callback uses the current thread. + nsCOMPtr<nsIUrlClassifierCallback> proxyCallback = + new UrlClassifierCallbackProxy(c); + + return mWorkerProxy->GetTables(proxyCallback); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::SetHashCompleter( + const nsACString& tableName, nsIUrlClassifierHashCompleter* completer) { + if (completer) { + mCompleters.InsertOrUpdate(tableName, completer); + } else { + mCompleters.Remove(tableName); + } + ClearLastResults(); + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::ClearLastResults() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->ClearLastResults(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::BeginUpdate(nsIUrlClassifierUpdateObserver* observer, + const nsACString& updateTables) { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + if (mInUpdate) { + LOG(("Already updating, not available")); + return NS_ERROR_NOT_AVAILABLE; + } + + if (mWorker->IsBusyUpdating()) { + // |mInUpdate| used to work well because "notifying update observer" + // is synchronously done in Worker::FinishUpdate(). Even if the + // update observer hasn't been notified at this point, we can still + // dispatch BeginUpdate() since it will NOT be run until the + // previous Worker::FinishUpdate() returns. + // + // However, some tasks in Worker::FinishUpdate() have been moved to + // another thread. The update observer will NOT be notified when + // Worker::FinishUpdate() returns. If we only check |mInUpdate|, + // the following sequence might happen on worker thread: + // + // Worker::FinishUpdate() // for update 1 + // Worker::BeginUpdate() // for update 2 + // Worker::NotifyUpdateObserver() // for update 1 + // + // So, we have to find out a way to reject BeginUpdate() right here + // if the previous update observer hasn't been notified. + // + // Directly probing the worker's state is the most lightweight solution. + // No lock is required since Worker::BeginUpdate() and + // Worker::NotifyUpdateObserver() are by nature mutual exclusive. + // (both run on worker thread.) + LOG(("The previous update observer hasn't been notified.")); + return NS_ERROR_NOT_AVAILABLE; + } + + mInUpdate = true; + + // The proxy observer uses the current thread + nsCOMPtr<nsIUrlClassifierUpdateObserver> proxyObserver = + new UrlClassifierUpdateObserverProxy(observer); + + return mWorkerProxy->BeginUpdate(proxyObserver, updateTables); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::BeginStream(const nsACString& table) { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->BeginStream(table); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::UpdateStream(const nsACString& aUpdateChunk) { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->UpdateStream(aUpdateChunk); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::FinishStream() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->FinishStream(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::FinishUpdate() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + mInUpdate = false; + + return mWorkerProxy->FinishUpdate(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::CancelUpdate() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + mInUpdate = false; + + return mWorkerProxy->CancelUpdate(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::ResetDatabase() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + if (mWorker->IsBusyUpdating()) { + LOG(("Failed to ResetDatabase because of the unfinished update.")); + return NS_ERROR_FAILURE; + } + + return mWorkerProxy->ResetDatabase(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::ReloadDatabase() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + if (mWorker->IsBusyUpdating()) { + LOG(("Failed to ReloadDatabase because of the unfinished update.")); + return NS_ERROR_FAILURE; + } + + return mWorkerProxy->ReloadDatabase(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::ClearCache() { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->ClearCache(); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::GetCacheInfo( + const nsACString& aTable, nsIUrlClassifierGetCacheCallback* aCallback) { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->GetCacheInfo(aTable, aCallback); +} + +nsresult nsUrlClassifierDBService::CacheCompletions( + const ConstCacheResultArray& results) { + NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED); + + return mWorkerProxy->CacheCompletions(results); +} + +bool nsUrlClassifierDBService::CanComplete(const nsACString& aTableName) { + return !mDisallowCompletionsTables.Contains(aTableName); +} + +bool nsUrlClassifierDBService::GetCompleter( + const nsACString& tableName, nsIUrlClassifierHashCompleter** completer) { + // If we have specified a completer, go ahead and query it. This is only + // used by tests. + if (mCompleters.Get(tableName, completer)) { + return true; + } + + if (!CanComplete(tableName)) { + return false; + } + + // Otherwise, call gethash to find the hash completions. + return NS_SUCCEEDED( + CallGetService(NS_URLCLASSIFIERHASHCOMPLETER_CONTRACTID, completer)); +} + +NS_IMETHODIMP +nsUrlClassifierDBService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + ReadDisallowCompletionsTablesFromPrefs(); + } else if (!strcmp(aTopic, "quit-application")) { + // Tell the update thread to finish as soon as possible. + gShuttingDownThread = true; + + // The code in ::Shutdown() is run on a 'profile-before-change' event and + // ensures that objects are freed by blocking on this freeing. + // We can however speed up the shutdown time by using the worker thread to + // release, in an earlier event, any objects that cannot affect an ongoing + // update on the update thread. + PreShutdown(); + } else if (!strcmp(aTopic, "profile-before-change")) { + gShuttingDownThread = true; + Shutdown(); + } else { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +// Post a PreShutdown task to worker thread to release objects without blocking +// main-thread. Notice that shutdown process may still be blocked by PreShutdown +// task when ::Shutdown() is executed and synchronously waits for worker thread +// to finish PreShutdown event. +nsresult nsUrlClassifierDBService::PreShutdown() { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mWorkerProxy) { + mWorkerProxy->PreShutdown(); + } + + return NS_OK; +} + +// Join the background thread if it exists. +nsresult nsUrlClassifierDBService::Shutdown() { + LOG(("shutting down db service\n")); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!gDbBackgroundThread) { + return NS_OK; + } + + Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_SHUTDOWN_TIME> timer; + + mCompleters.Clear(); + + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + prefs->RemoveObserver(DISALLOW_COMPLETION_TABLE_PREF, this); + } + + // 1. Synchronize with worker thread and update thread by + // *synchronously* dispatching an event to worker thread + // for shutting down the update thread. The reason not + // shutting down update thread directly from main thread + // is to avoid racing for Classifier::mUpdateThread + // between main thread and the worker thread. (Both threads + // would access Classifier::mUpdateThread.) + // This event is dispatched unconditionally to avoid + // accessing mWorker->mClassifier on the main thread, which + // would be a race. + using Worker = nsUrlClassifierDBServiceWorker; + RefPtr<nsIRunnable> r = NewRunnableMethod( + "nsUrlClassifierDBServiceWorker::FlushAndDisableAsyncUpdate", mWorker, + &Worker::FlushAndDisableAsyncUpdate); + SyncRunnable::DispatchToThread(gDbBackgroundThread, r); + // At this point the update thread has been shut down and + // the worker thread should only have at most one event, + // which is the callback event. + + // 2. Send CancelUpdate() event to notify the dangling update. + // (i.e. BeginUpdate is called but FinishUpdate is not.) + // and CloseDb() to clear mClassifier. They will be the last two + // events on the worker thread in the shutdown process. + DebugOnly<nsresult> rv; + rv = mWorkerProxy->CancelUpdate(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "failed to post 'cancel update' event"); + rv = mWorkerProxy->CloseDb(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "failed to post 'close db' event"); + mWorkerProxy = nullptr; + + // 3. Invalidate XPCOM APIs by nulling out gDbBackgroundThread + // since every API checks gDbBackgroundThread first. This has + // to be done before calling nsIThread.shutdown because it + // will cause the pending events on the joining thread to + // be processed. + nsIThread* backgroundThread = nullptr; + std::swap(backgroundThread, gDbBackgroundThread); + + // 4. Wait until the worker thread is down. + if (backgroundThread) { + backgroundThread->Shutdown(); + NS_RELEASE(backgroundThread); + } + + mWorker = nullptr; + return NS_OK; +} + +nsIThread* nsUrlClassifierDBService::BackgroundThread() { + return gDbBackgroundThread; +} + +// static +bool nsUrlClassifierDBService::ShutdownHasStarted() { + return gShuttingDownThread; +} + +// static +nsUrlClassifierDBServiceWorker* nsUrlClassifierDBService::GetWorker() { + nsresult rv; + RefPtr<nsUrlClassifierDBService> service = + nsUrlClassifierDBService::GetInstance(&rv); + if (!service) { + return nullptr; + } + + return service->mWorker; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::AsyncClassifyLocalWithFeatures( + nsIURI* aURI, const nsTArray<RefPtr<nsIUrlClassifierFeature>>& aFeatures, + nsIUrlClassifierFeature::listType aListType, + nsIUrlClassifierFeatureCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + + if (gShuttingDownThread) { + return NS_ERROR_ABORT; + } + + nsCOMPtr<nsIURI> uri = NS_GetInnermostURI(aURI); + NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); + + // Let's try to use the preferences. + if (AsyncClassifyLocalWithFeaturesUsingPreferences(uri, aFeatures, aListType, + aCallback)) { + return NS_OK; + } + + nsUrlClassifierUtils* utilsService = nsUrlClassifierUtils::GetInstance(); + if (NS_WARN_IF(!utilsService)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString key; + // Canonicalize the url + nsresult rv = utilsService->GetKeyForURI(uri, key); + NS_ENSURE_SUCCESS(rv, rv); + + if (XRE_IsContentProcess()) { + using namespace mozilla::dom; + using namespace mozilla::ipc; + + ContentChild* content = ContentChild::GetSingleton(); + if (NS_WARN_IF(!content || content->IsShuttingDown())) { + return NS_ERROR_FAILURE; + } + + auto actor = new URLClassifierLocalChild(); + + nsTArray<IPCURLClassifierFeature> ipcFeatures; + for (nsIUrlClassifierFeature* feature : aFeatures) { + nsAutoCString name; + rv = feature->GetName(name); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsTArray<nsCString> tables; + rv = feature->GetTables(aListType, tables); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString exceptionHostList; + if (aListType == nsIUrlClassifierFeature::blocklist) { + rv = feature->GetExceptionHostList(exceptionHostList); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + } + + ipcFeatures.AppendElement( + IPCURLClassifierFeature(name, tables, exceptionHostList)); + } + + if (!content->SendPURLClassifierLocalConstructor(actor, aURI, + ipcFeatures)) { + return NS_ERROR_FAILURE; + } + + actor->SetFeaturesAndCallback(aFeatures, aCallback); + return NS_OK; + } + + using namespace mozilla::Telemetry; + auto startTime = TimeStamp::Now(); // For telemetry. + + // Let's keep the features alive and release them on the correct thread. + RefPtr<FeatureHolder> holder = + FeatureHolder::Create(aURI, aFeatures, aListType); + if (NS_WARN_IF(!holder)) { + return NS_ERROR_FAILURE; + } + + auto worker = mWorker; + + // Since aCallback will be passed around threads... + nsMainThreadPtrHandle<nsIUrlClassifierFeatureCallback> callback( + new nsMainThreadPtrHolder<nsIUrlClassifierFeatureCallback>( + "nsIURIClassifierFeatureCallback", aCallback)); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "nsUrlClassifierDBService::AsyncClassifyLocalWithFeatures", + [worker, key, holder, callback, startTime]() -> void { + holder->DoLocalLookup(key, worker); + + nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction( + "nsUrlClassifierDBService::AsyncClassifyLocalWithFeatures", + [callback, holder, startTime]() -> void { + // Measure the time diff between calling and callback. + AccumulateTimeDelta( + Telemetry::URLCLASSIFIER_ASYNC_CLASSIFYLOCAL_TIME, startTime); + + nsTArray<RefPtr<nsIUrlClassifierFeatureResult>> results; + holder->GetResults(results); + + // |callback| is captured as const value so ... + auto cb = + const_cast<nsIUrlClassifierFeatureCallback*>(callback.get()); + cb->OnClassifyComplete(results); + }); + + NS_DispatchToMainThread(cbRunnable); + }); + + return gDbBackgroundThread->Dispatch(r, NS_DISPATCH_NORMAL); +} + +bool nsUrlClassifierDBService::AsyncClassifyLocalWithFeaturesUsingPreferences( + nsIURI* aURI, const nsTArray<RefPtr<nsIUrlClassifierFeature>>& aFeatures, + nsIUrlClassifierFeature::listType aListType, + nsIUrlClassifierFeatureCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString host; + nsresult rv = aURI->GetHost(host); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsTArray<RefPtr<nsIUrlClassifierFeatureResult>> results; + + // Let's see if we have special entries set by prefs. + for (nsIUrlClassifierFeature* feature : aFeatures) { + bool found = false; + + nsAutoCString tableName; + rv = feature->HasHostInPreferences(host, aListType, tableName, &found); + NS_ENSURE_SUCCESS(rv, false); + + if (found) { + MOZ_ASSERT(!tableName.IsEmpty()); + LOG(("URI found in preferences. Table: %s", tableName.get())); + + RefPtr<mozilla::net::UrlClassifierFeatureResult> result = + new mozilla::net::UrlClassifierFeatureResult(aURI, feature, + tableName); + results.AppendElement(result); + } + } + + if (results.IsEmpty()) { + return false; + } + + // If we have some match using the preferences, we don't need to continue. + nsCOMPtr<nsIUrlClassifierFeatureCallback> callback(aCallback); + nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction( + "nsUrlClassifierDBService::AsyncClassifyLocalWithFeatures", + [callback, results = std::move(results)]() { + callback->OnClassifyComplete(results); + }); + + NS_DispatchToMainThread(cbRunnable); + return true; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::GetFeatureByName(const nsACString& aFeatureName, + nsIUrlClassifierFeature** aFeature) { + NS_ENSURE_ARG_POINTER(aFeature); + nsCOMPtr<nsIUrlClassifierFeature> feature = + mozilla::net::UrlClassifierFeatureFactory::GetFeatureByName(aFeatureName); + if (NS_WARN_IF(!feature)) { + return NS_ERROR_FAILURE; + } + + feature.forget(aFeature); + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::GetFeatureNames(nsTArray<nsCString>& aArray) { + mozilla::net::UrlClassifierFeatureFactory::GetFeatureNames(aArray); + return NS_OK; +} + +NS_IMETHODIMP +nsUrlClassifierDBService::CreateFeatureWithTables( + const nsACString& aName, const nsTArray<nsCString>& aBlocklistTables, + const nsTArray<nsCString>& aEntitylistTables, + nsIUrlClassifierFeature** aFeature) { + NS_ENSURE_ARG_POINTER(aFeature); + nsCOMPtr<nsIUrlClassifierFeature> feature = + mozilla::net::UrlClassifierFeatureFactory::CreateFeatureWithTables( + aName, aBlocklistTables, aEntitylistTables); + if (NS_WARN_IF(!feature)) { + return NS_ERROR_FAILURE; + } + + feature.forget(aFeature); + return NS_OK; +} |