/* -*- Mode: C++; tab-width: 4; 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 "LoginReputation.h" #include "nsThreadUtils.h" #include "mozilla/Components.h" #include "mozilla/ErrorNames.h" #include "mozilla/Logging.h" #include "mozilla/net/UrlClassifierFeatureFactory.h" #include "mozilla/Preferences.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/Telemetry.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/ipc/URIUtils.h" #include "nsIURIClassifier.h" #include "nsIUrlClassifierFeature.h" using namespace mozilla; using namespace mozilla::dom; #define PREF_PP_ENABLED "browser.safebrowsing.passwords.enabled" // MOZ_LOG=LoginReputation:5 LazyLogModule gLoginReputationLogModule("LoginReputation"); #define LR_LOG(args) \ MOZ_LOG(gLoginReputationLogModule, mozilla::LogLevel::Debug, args) #define LR_LOG_ENABLED() \ MOZ_LOG_TEST(gLoginReputationLogModule, mozilla::LogLevel::Debug) static Atomic gShuttingDown(false); // ------------------------------------------------------------------------- // ReputationQueryParam // // Concrete class for nsILoginReputationQuery to hold query parameters // class ReputationQueryParam final : public nsILoginReputationQuery { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSILOGINREPUTATIONQUERY explicit ReputationQueryParam(nsIURI* aURI) : mURI(aURI){}; private: ~ReputationQueryParam() = default; nsCOMPtr mURI; }; NS_IMPL_ISUPPORTS(ReputationQueryParam, nsILoginReputationQuery) NS_IMETHODIMP ReputationQueryParam::GetFormURI(nsIURI** aURI) { NS_IF_ADDREF(*aURI = mURI); return NS_OK; } // ------------------------------------------------------------------------- // LoginWhitelist // // This class is a wrapper that encapsulate asynchronous callback API provided // by DBService into a MozPromise callback. // class LoginWhitelist final : public nsIUrlClassifierFeatureCallback { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIURLCLASSIFIERFEATURECALLBACK RefPtr QueryLoginWhitelist( nsILoginReputationQuery* aParam); LoginWhitelist() = default; nsresult Shutdown(); private: ~LoginWhitelist() = default; // Queries that are waiting for callback from // ::AsyncClassifyLocalWithFeatures. nsTArray>> mQueryPromises; }; NS_IMPL_ISUPPORTS(LoginWhitelist, nsIUrlClassifierFeatureCallback) nsresult LoginWhitelist::Shutdown() { // Reject all query promise before releasing. for (uint8_t i = 0; i < mQueryPromises.Length(); i++) { mQueryPromises[i]->Reject(NS_ERROR_ABORT, __func__); } mQueryPromises.Clear(); return NS_OK; } RefPtr LoginWhitelist::QueryLoginWhitelist( nsILoginReputationQuery* aParam) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv; UniquePtr> holder = MakeUnique>(); RefPtr p = holder->Ensure(__func__); // Return rejected promise while there is an error. auto fail = MakeScopeExit([&]() { holder->Reject(rv, __func__); }); nsCOMPtr uri; rv = aParam->GetFormURI(getter_AddRefs(uri)); if (NS_WARN_IF(NS_FAILED(rv) || !uri)) { return p; } nsCOMPtr uriClassifier = mozilla::components::UrlClassifierDB::Service(&rv); if (NS_WARN_IF(NS_FAILED(rv))) { return p; } // AsyncClassifyLocalWithTables API won't trigger a gethash request on // a full-length match, so this API call should only include local operation. // We don't support prefs overwrite for this classification. nsCOMPtr feature = mozilla::net::UrlClassifierFeatureFactory::GetFeatureLoginReputation(); if (NS_WARN_IF(!feature)) { return p; } nsTArray> features; features.AppendElement(feature); rv = uriClassifier->AsyncClassifyLocalWithFeatures( uri, features, nsIUrlClassifierFeature::entitylist, this); if (NS_FAILED(rv)) { return p; } fail.release(); mQueryPromises.AppendElement(std::move(holder)); return p; } nsresult LoginWhitelist::OnClassifyComplete( const nsTArray>& aResults) { MOZ_ASSERT(NS_IsMainThread()); if (gShuttingDown) { return NS_OK; } LR_LOG(("OnClassifyComplete : %s", aResults.IsEmpty() ? "blacklisted" : "whitelisted")); UniquePtr> holder = std::move(mQueryPromises.ElementAt(0)); mQueryPromises.RemoveElementAt(0); if (aResults.IsEmpty()) { // Reject if we can not find url in white list. holder->Reject(NS_OK, __func__); } else { holder->Resolve(nsILoginReputationVerdictType::SAFE, __func__); } return NS_OK; } // ------------------------------------------------------------------------- // LoginReputationService // NS_IMPL_ISUPPORTS(LoginReputationService, nsILoginReputationService, nsIObserver) LoginReputationService* LoginReputationService::gLoginReputationService = nullptr; // static already_AddRefed LoginReputationService::GetSingleton() { if (!gLoginReputationService) { gLoginReputationService = new LoginReputationService(); } return do_AddRef(gLoginReputationService); } LoginReputationService::LoginReputationService() { LR_LOG(("Login reputation service starting up")); } LoginReputationService::~LoginReputationService() { LR_LOG(("Login reputation service shutting down")); MOZ_ASSERT(gLoginReputationService == this); gLoginReputationService = nullptr; } NS_IMETHODIMP LoginReputationService::Init() { MOZ_ASSERT(NS_IsMainThread()); switch (XRE_GetProcessType()) { case GeckoProcessType_Default: LR_LOG(("Init login reputation service in parent")); break; case GeckoProcessType_Content: LR_LOG(("Init login reputation service in child")); // Login reputation service in child process will only forward request to // parent, return here to skip unnecessary initialization. return NS_OK; default: // No other process type is supported! return NS_ERROR_NOT_AVAILABLE; } // The initialization below only happens in parent process. Preferences::AddStrongObserver(this, PREF_PP_ENABLED); // Init should only be called once. MOZ_ASSERT(!mLoginWhitelist); mLoginWhitelist = new LoginWhitelist(); if (StaticPrefs::browser_safebrowsing_passwords_enabled()) { Enable(); } return NS_OK; } nsresult LoginReputationService::Enable() { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(StaticPrefs::browser_safebrowsing_passwords_enabled()); LR_LOG(("Enable login reputation service")); return NS_OK; } nsresult LoginReputationService::Disable() { MOZ_ASSERT(XRE_IsParentProcess()); LR_LOG(("Disable login reputation service")); nsresult rv = mLoginWhitelist->Shutdown(); Unused << NS_WARN_IF(NS_FAILED(rv)); mQueryRequests.Clear(); return NS_OK; } nsresult LoginReputationService::Shutdown() { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(gShuttingDown); // Disable will wait until worker threads are shutdown. Disable(); // Disable will only destroy worker thread, it won't null out these classes. // So we will null these classes in shutdown. mLoginWhitelist = nullptr; return NS_OK; } // static already_AddRefed LoginReputationService::ConstructQueryParam(nsIURI* aURI) { RefPtr param = new ReputationQueryParam(aURI); return param.forget(); } NS_IMETHODIMP LoginReputationService::QueryReputationAsync( HTMLInputElement* aInput, nsILoginReputationQueryCallback* aCallback) { NS_ENSURE_ARG_POINTER(aInput); LR_LOG(("QueryReputationAsync() [this=%p]", this)); if (!StaticPrefs::browser_safebrowsing_passwords_enabled()) { return NS_ERROR_FAILURE; } nsIURI* documentURI = aInput->OwnerDoc()->GetDocumentURI(); NS_ENSURE_STATE(documentURI); if (XRE_IsContentProcess()) { using namespace mozilla::ipc; ContentChild* content = ContentChild::GetSingleton(); if (content->IsShuttingDown()) { return NS_ERROR_FAILURE; } if (!content->SendPLoginReputationConstructor(documentURI)) { return NS_ERROR_FAILURE; } } else { nsCOMPtr query = LoginReputationService::ConstructQueryParam(documentURI); nsresult rv = QueryReputation(query, aCallback); return rv; } return NS_OK; } NS_IMETHODIMP LoginReputationService::QueryReputation( nsILoginReputationQuery* aQuery, nsILoginReputationQueryCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_ARG_POINTER(aQuery); NS_ENSURE_ARG_POINTER(aCallback); LR_LOG(("QueryReputation() [this=%p]", this)); if (gShuttingDown || !StaticPrefs::browser_safebrowsing_passwords_enabled()) { LR_LOG(("QueryReputation() abort [this=%p]", this)); aCallback->OnComplete(NS_ERROR_ABORT, nsILoginReputationVerdictType::UNSPECIFIED); return NS_OK; } // mQueryRequests is an array used to maintain the ownership of // |QueryRequest|. We ensure that |QueryRequest| is always valid until // Finish() is called or LoginReputationService is shutdown. auto request = mQueryRequests.AppendElement(MakeUnique(aQuery, aCallback)); return QueryLoginWhitelist(request->get()); } nsresult LoginReputationService::QueryLoginWhitelist(QueryRequest* aRequest) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_ARG_POINTER(aRequest); if (gShuttingDown) { return NS_ERROR_ABORT; } using namespace mozilla::Telemetry; TimeStamp startTimeMs = TimeStamp::Now(); RefPtr self = this; mLoginWhitelist->QueryLoginWhitelist(aRequest->mParam) ->Then( GetCurrentSerialEventTarget(), __func__, [self, aRequest, startTimeMs](VerdictType aResolveValue) -> void { // Promise is resolved if url is found in google-provided whitelist. MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aResolveValue == nsILoginReputationVerdictType::SAFE); LR_LOG(("Query login whitelist [request = %p, result = SAFE]", aRequest)); AccumulateTimeDelta(LOGIN_REPUTATION_LOGIN_WHITELIST_LOOKUP_TIME, startTimeMs); Accumulate(LOGIN_REPUTATION_LOGIN_WHITELIST_RESULT, nsILoginReputationVerdictType::SAFE); self->Finish(aRequest, NS_OK, nsILoginReputationVerdictType::SAFE); }, [self, aRequest, startTimeMs](nsresult rv) -> void { // Promise is rejected if url cannot be found in google-provided // whitelist. or there is an error. if (NS_FAILED(rv)) { if (LR_LOG_ENABLED()) { nsAutoCString errorName; mozilla::GetErrorName(rv, errorName); LR_LOG( ("Error in QueryLoginWhitelist() [request = %p, rv = %s]", aRequest, errorName.get())); } // Don't record the lookup time when there is an error, only // record the result here. Accumulate(LOGIN_REPUTATION_LOGIN_WHITELIST_RESULT, 2); // 2 is error } else { AccumulateTimeDelta(LOGIN_REPUTATION_LOGIN_WHITELIST_LOOKUP_TIME, startTimeMs); Accumulate(LOGIN_REPUTATION_LOGIN_WHITELIST_RESULT, nsILoginReputationVerdictType::UNSPECIFIED); LR_LOG( ("Query login whitelist cannot find the URL [request = %p]", aRequest)); } // Check trust-based whitelisting if we can't find the url in login // whitelist self->Finish(aRequest, rv, nsILoginReputationVerdictType::UNSPECIFIED); }); return NS_OK; } nsresult LoginReputationService::Finish(const QueryRequest* aRequest, nsresult aStatus, VerdictType aVerdict) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_ARG_POINTER(aRequest); LR_LOG(("Query login reputation end [request = %p, result = %s]", aRequest, VerdictTypeToString(aVerdict).get())); // Since we are shutting down, don't bother call back to child process. if (gShuttingDown) { return NS_OK; } aRequest->mCallback->OnComplete(aStatus, aVerdict); // QueryRequest may not follow the same order when we queued it in // ::QueryReputation because one query request may be finished earlier than // the other. uint32_t idx = 0; for (; idx < mQueryRequests.Length(); idx++) { if (mQueryRequests[idx].get() == aRequest) { break; } } if (NS_WARN_IF(idx >= mQueryRequests.Length())) { return NS_ERROR_FAILURE; } mQueryRequests.RemoveElementAt(idx); return NS_OK; } NS_IMETHODIMP LoginReputationService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { nsDependentString data(aData); if (data.EqualsLiteral(PREF_PP_ENABLED)) { nsresult rv = StaticPrefs::browser_safebrowsing_passwords_enabled() ? Enable() : Disable(); Unused << NS_WARN_IF(NS_FAILED(rv)); } } else if (!strcmp(aTopic, "quit-application")) { // Prepare to shutdown, won't allow any query request after 'gShuttingDown' // is set. gShuttingDown = true; } else if (!strcmp(aTopic, "profile-before-change")) { gShuttingDown = true; Shutdown(); } else { return NS_ERROR_UNEXPECTED; } return NS_OK; } // static nsCString LoginReputationService::VerdictTypeToString(VerdictType aVerdict) { switch (aVerdict) { case nsILoginReputationVerdictType::UNSPECIFIED: return nsCString("Unspecified"); case nsILoginReputationVerdictType::LOW_REPUTATION: return nsCString("Low Reputation"); case nsILoginReputationVerdictType::SAFE: return nsCString("Safe"); case nsILoginReputationVerdictType::PHISHING: return nsCString("Phishing"); default: return nsCString("Invalid"); } }