summaryrefslogtreecommitdiffstats
path: root/toolkit/components/reputationservice/LoginReputation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/reputationservice/LoginReputation.cpp')
-rw-r--r--toolkit/components/reputationservice/LoginReputation.cpp488
1 files changed, 488 insertions, 0 deletions
diff --git a/toolkit/components/reputationservice/LoginReputation.cpp b/toolkit/components/reputationservice/LoginReputation.cpp
new file mode 100644
index 0000000000..6e092b795e
--- /dev/null
+++ b/toolkit/components/reputationservice/LoginReputation.cpp
@@ -0,0 +1,488 @@
+/* -*- 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<bool> 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<nsIURI> 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<ReputationPromise> QueryLoginWhitelist(
+ nsILoginReputationQuery* aParam);
+
+ LoginWhitelist() = default;
+
+ nsresult Shutdown();
+
+ private:
+ ~LoginWhitelist() = default;
+
+ // Queries that are waiting for callback from
+ // ::AsyncClassifyLocalWithFeatures.
+ nsTArray<UniquePtr<MozPromiseHolder<ReputationPromise>>> 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<ReputationPromise> LoginWhitelist::QueryLoginWhitelist(
+ nsILoginReputationQuery* aParam) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv;
+ UniquePtr<MozPromiseHolder<ReputationPromise>> holder =
+ MakeUnique<MozPromiseHolder<ReputationPromise>>();
+ RefPtr<ReputationPromise> p = holder->Ensure(__func__);
+
+ // Return rejected promise while there is an error.
+ auto fail = MakeScopeExit([&]() { holder->Reject(rv, __func__); });
+
+ nsCOMPtr<nsIURI> uri;
+ rv = aParam->GetFormURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv) || !uri)) {
+ return p;
+ }
+
+ nsCOMPtr<nsIURIClassifier> 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<nsIUrlClassifierFeature> feature =
+ mozilla::net::UrlClassifierFeatureFactory::GetFeatureLoginReputation();
+ if (NS_WARN_IF(!feature)) {
+ return p;
+ }
+
+ nsTArray<RefPtr<nsIUrlClassifierFeature>> 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<RefPtr<nsIUrlClassifierFeatureResult>>& aResults) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (gShuttingDown) {
+ return NS_OK;
+ }
+
+ LR_LOG(("OnClassifyComplete : %s",
+ aResults.IsEmpty() ? "blacklisted" : "whitelisted"));
+
+ UniquePtr<MozPromiseHolder<ReputationPromise>> 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>
+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<nsILoginReputationQuery>
+LoginReputationService::ConstructQueryParam(nsIURI* aURI) {
+ RefPtr<ReputationQueryParam> 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<nsILoginReputationQuery> 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<QueryRequest>(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<LoginReputationService> 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");
+ }
+}