зеркало из https://github.com/mozilla/gecko-dev.git
490 строки
14 KiB
C++
490 строки
14 KiB
C++
/* -*- 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/StaticPrefs.h"
|
|
#include "mozilla/Telemetry.h"
|
|
#include "mozilla/dom/ContentChild.h"
|
|
#include "mozilla/dom/HTMLInputElement.h"
|
|
#include "mozilla/ipc/URIUtils.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::whitelist, 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;
|
|
}
|
|
|
|
URIParams uri;
|
|
SerializeURI(documentURI, uri);
|
|
|
|
if (!content->SendPLoginReputationConstructor(uri)) {
|
|
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(
|
|
GetCurrentThreadSerialEventTarget(), __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");
|
|
}
|
|
}
|