/* -*- 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 mFeature; nsTArray> mTables; }; static already_AddRefed Create( nsIURI* aURI, const nsTArray>& aFeatures, nsIUrlClassifierFeature::listType aListType) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aURI); RefPtr holder = new FeatureHolder(aURI); for (nsIUrlClassifierFeature* feature : aFeatures) { FeatureData* featureData = holder->mFeatureData.AppendElement(); MOZ_ASSERT(featureData); featureData->mFeature = feature; nsTArray 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 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>& 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 result = new mozilla::net::UrlClassifierFeatureResult( mURI, featureData.mFeature, list); aResults.AppendElement(result); } } mozilla::UniquePtr GetTableResults() const { mozilla::UniquePtr results = mozilla::MakeUnique(); 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 = new TableData(aTable); mTableData.AppendElement(tableData); return tableData; } nsCOMPtr mURI; nsTArray mFeatureData; nsTArray> 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 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 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& 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 results = aFeatureHolder->GetTableResults(); if (NS_WARN_IF(!results)) { return NS_ERROR_OUT_OF_MEMORY; } LOG(("Found %zu results.", results->Length())); for (const RefPtr 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(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 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& 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 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 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(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 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 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 pParse; if (result->Ver() == CacheResult::V2) { pParse.reset(new ProtocolParserV2()); } else { pParse.reset(new ProtocolParserProtobuf()); } RefPtr 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 aCacheResult, RefPtr aUpdate) { RefPtr tuV2 = TableUpdate::Cast(aUpdate); if (tuV2) { RefPtr result = CacheResult::Cast(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 tuV4 = TableUpdate::Cast(aUpdate); if (tuV4) { RefPtr result = CacheResult::Cast(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 = 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 lhs = mLastResults[i]; RefPtr rhs = aResult[i]; if (lhs->Ver() != rhs->Ver()) { return false; } if (lhs->Ver() == CacheResult::V2) { equal = *(CacheResult::Cast(lhs)) == *(CacheResult::Cast(rhs)); } else if (lhs->Ver() == CacheResult::V4) { equal = *(CacheResult::Cast(lhs)) == *(CacheResult::Cast(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 aCacheResult); nsresult CacheMisses(); RefPtr mDBService; UniquePtr mResults; // Completed results to send back to the worker for caching. ConstCacheResultArray mCacheResults; uint32_t mPendingCompletions; nsCOMPtr mCallback; }; NS_IMPL_ISUPPORTS(nsUrlClassifierLookupCallback, nsIUrlClassifierLookupCallback, nsIUrlClassifierHashCompleterCallback) nsUrlClassifierLookupCallback::~nsUrlClassifierLookupCallback() { if (mCallback) { NS_ReleaseOnMainThread("nsUrlClassifierLookupCallback::mCallback", mCallback.forget()); } } NS_IMETHODIMP nsUrlClassifierLookupCallback::LookupComplete( UniquePtr 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 completer; nsCString gethashUrl; nsresult rv; nsCOMPtr 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 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 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 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 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 classifyCallback = do_QueryInterface(mCallback); nsTArray 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::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 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 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 mCallback; nsTArray 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 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::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 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 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( 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 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> features; mozilla::net::UrlClassifierFeatureFactory::GetPhishingProtectionFeatures( features); if (features.IsEmpty()) { *aResult = false; return NS_OK; } nsCOMPtr 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 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 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 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 uri; rv = httpChannel->GetURI(getter_AddRefs(uri)); if (NS_SUCCEEDED(rv) && uri) { uri->GetAsciiSpec(spec); } nsCOMPtr 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 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 uri; rv = httpChannel->GetURI(getter_AddRefs(uri)); if (NS_SUCCEEDED(rv) && uri) { uri->GetAsciiSpec(spec); } nsCOMPtr 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 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 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 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 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 loadInfo = reportChannel->LoadInfo(); mozilla::OriginAttributes attrs; attrs.mFirstPartyDomain.AssignLiteral(NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN); loadInfo->SetOriginAttributes(attrs); nsCOMPtr 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 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 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 tableArray; Classifier::SplitTables(aTables, tableArray); nsCOMPtr feature; nsresult rv = CreateFeatureWithTables( "lookup"_ns, tableArray, nsTArray(), getter_AddRefs(feature)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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> features; features.AppendElement(feature.get()); // Let's keep the features alive and release them on the correct thread. RefPtr 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 callback = new nsUrlClassifierLookupCallback(this, aCallback); nsCOMPtr 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 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 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 timer; mCompleters.Clear(); nsCOMPtr 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 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 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 service = nsUrlClassifierDBService::GetInstance(&rv); if (!service) { return nullptr; } return service->mWorker; } NS_IMETHODIMP nsUrlClassifierDBService::AsyncClassifyLocalWithFeatures( nsIURI* aURI, const nsTArray>& aFeatures, nsIUrlClassifierFeature::listType aListType, nsIUrlClassifierFeatureCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); if (gShuttingDownThread) { return NS_ERROR_ABORT; } nsCOMPtr 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 ipcFeatures; for (nsIUrlClassifierFeature* feature : aFeatures) { nsAutoCString name; rv = feature->GetName(name); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } nsTArray 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 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 callback( new nsMainThreadPtrHolder( "nsIURIClassifierFeatureCallback", aCallback)); nsCOMPtr r = NS_NewRunnableFunction( "nsUrlClassifierDBService::AsyncClassifyLocalWithFeatures", [worker, key, holder, callback, startTime]() -> void { holder->DoLocalLookup(key, worker); nsCOMPtr 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> results; holder->GetResults(results); // |callback| is captured as const value so ... auto cb = const_cast(callback.get()); cb->OnClassifyComplete(results); }); NS_DispatchToMainThread(cbRunnable); }); return gDbBackgroundThread->Dispatch(r, NS_DISPATCH_NORMAL); } bool nsUrlClassifierDBService::AsyncClassifyLocalWithFeaturesUsingPreferences( nsIURI* aURI, const nsTArray>& 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> 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 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 callback(aCallback); nsCOMPtr 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 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& aArray) { mozilla::net::UrlClassifierFeatureFactory::GetFeatureNames(aArray); return NS_OK; } NS_IMETHODIMP nsUrlClassifierDBService::CreateFeatureWithTables( const nsACString& aName, const nsTArray& aBlocklistTables, const nsTArray& aEntitylistTables, nsIUrlClassifierFeature** aFeature) { NS_ENSURE_ARG_POINTER(aFeature); nsCOMPtr feature = mozilla::net::UrlClassifierFeatureFactory::CreateFeatureWithTables( aName, aBlocklistTables, aEntitylistTables); if (NS_WARN_IF(!feature)) { return NS_ERROR_FAILURE; } feature.forget(aFeature); return NS_OK; }