/* 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 "ExternalAgentBackend.h" #include "ContentAnalysis.h" #include "ContentAnalysisShared.h" #include "content_analysis/sdk/analysis_client.h" #include "GMPUtils.h" // ToHexString #include "mozilla/Assertions.h" #include "mozilla/CheckedInt.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/DataTransfer.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/glean/ContentanalysisMetrics.h" #include "nsIObserverService.h" #include "nsITransferable.h" #include "nsString.h" #include "nsThreadPool.h" #include #ifdef XP_WIN # define SECURITY_WIN32 1 # include # include "mozilla/WinDllServices.h" #endif namespace mozilla::contentanalysis { // Defined in ContentAnalysis.cpp. extern LazyLogModule gContentAnalysisLog; #define LOGD(...) \ MOZ_LOG(mozilla::contentanalysis::gContentAnalysisLog, \ mozilla::LogLevel::Debug, (__VA_ARGS__)) #define LOGE(...) \ MOZ_LOG(mozilla::contentanalysis::gContentAnalysisLog, \ mozilla::LogLevel::Error, (__VA_ARGS__)) } // namespace mozilla::contentanalysis namespace { const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; const char* kClientSignature = "browser.contentanalysis.client_signature"; const char* kAgentNamePref = "browser.contentanalysis.agent_name"; const char* kInterceptionPointPrefNames[] = { "browser.contentanalysis.interception_point.clipboard.enabled", "browser.contentanalysis.interception_point.download.enabled", "browser.contentanalysis.interception_point.drag_and_drop.enabled", "browser.contentanalysis.interception_point.file_upload.enabled", "browser.contentanalysis.interception_point.print.enabled", }; // Allow up to this many threads to be concurrently engaged in synchronous // communcations with the agent. That limit is set by // browser.contentanalysis.max_connections but is clamped to not exceed // this value. const unsigned long kMaxContentAnalysisAgentThreads = 256; // Max number of threads that we keep even if they have no tasks to run. const unsigned long kMaxIdleContentAnalysisAgentThreads = 2; // Time (ms) we wait before declaring a thread idle. 100ms is the // threadpool default. const unsigned long kIdleContentAnalysisAgentTimeoutMs = 100; // Time we wait before destroying the kMaxIdleContentAnalysisAgentThreads // threads. Content Analysis never does this, which is what UINT32_MAX // means. const unsigned long kMaxIdleContentAnalysisAgentTimeoutMs = UINT32_MAX; // How long the threadpool will wait at shutdown for the agent to complete any // in-progress operations before it abandons the threads (they will keep // running). const uint32_t kShutdownThreadpoolTimeoutMs = 2 * 1000; } // namespace namespace mozilla::contentanalysis { static nsresult ConvertToProtobuf( nsIClientDownloadResource* aIn, content_analysis::sdk::ClientDownloadRequest_Resource* aOut) { nsString url; nsresult rv = aIn->GetUrl(url); NS_ENSURE_SUCCESS(rv, rv); aOut->set_url(NS_ConvertUTF16toUTF8(url).get()); uint32_t resourceType; rv = aIn->GetType(&resourceType); NS_ENSURE_SUCCESS(rv, rv); aOut->set_type( static_cast( resourceType)); return NS_OK; } #if defined(DEBUG) static bool IsRequestReadyForAgent(nsIContentAnalysisRequest* aRequest) { NS_ENSURE_TRUE(aRequest, false); // The windowGlobal is allowed to be null at this point in gtests (only). // The URL must be set in that case. We check that below. RefPtr windowGlobal; NS_ENSURE_SUCCESS( aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)), false); // Any DataTransfer should have been expanded into individual requests. nsCOMPtr dataTransfer; NS_ENSURE_SUCCESS(aRequest->GetDataTransfer(getter_AddRefs(dataTransfer)), false); NS_ENSURE_TRUE(!dataTransfer, false); // Any nsITransferable should have been expanded into individual requests. nsCOMPtr transferable; NS_ENSURE_SUCCESS(aRequest->GetTransferable(getter_AddRefs(transferable)), false); NS_ENSURE_TRUE(!transferable, false); nsCString userActionId; NS_ENSURE_SUCCESS(aRequest->GetUserActionId(userActionId), false); NS_ENSURE_TRUE(!userActionId.IsEmpty(), false); int64_t userActionRequestsCount; NS_ENSURE_SUCCESS( aRequest->GetUserActionRequestsCount(&userActionRequestsCount), false); NS_ENSURE_TRUE(userActionRequestsCount, false); nsCOMPtr url; NS_ENSURE_SUCCESS(aRequest->GetUrl(getter_AddRefs(url)), false); if (!url) { // If no URL is given then we use the one for the window. NS_ENSURE_TRUE(windowGlobal, false); url = ContentAnalysis::GetURIForBrowsingContext( windowGlobal->Canonical()->GetBrowsingContext()); NS_ENSURE_TRUE(url, false); } return true; } #endif // defined(DEBUG) static nsresult ConvertToProtobuf( nsIContentAnalysisRequest* aIn, content_analysis::sdk::ContentAnalysisRequest* aOut) { MOZ_ASSERT(IsRequestReadyForAgent(aIn)); nsIContentAnalysisRequest::AnalysisType analysisType; nsresult rv = aIn->GetAnalysisType(&analysisType); NS_ENSURE_SUCCESS(rv, rv); auto connector = static_cast(analysisType); aOut->set_analysis_connector(connector); nsIContentAnalysisRequest::Reason reason; rv = aIn->GetReason(&reason); NS_ENSURE_SUCCESS(rv, rv); auto sdkReason = static_cast( reason); aOut->set_reason(sdkReason); nsCString requestToken; rv = aIn->GetRequestToken(requestToken); NS_ENSURE_SUCCESS(rv, rv); aOut->set_request_token(requestToken.get(), requestToken.Length()); nsCString userActionId; rv = aIn->GetUserActionId(userActionId); NS_ENSURE_SUCCESS(rv, rv); aOut->set_user_action_id(userActionId.get(), userActionId.Length()); int64_t userActionRequestsCount; rv = aIn->GetUserActionRequestsCount(&userActionRequestsCount); NS_ENSURE_SUCCESS(rv, rv); aOut->set_user_action_requests_count(userActionRequestsCount); int32_t timeout = StaticPrefs::browser_contentanalysis_agent_timeout(); // Non-positive timeout values indicate testing, and the test agent does not // care about this value. timeout = std::max(timeout, 1); uint32_t timeoutMultiplier; rv = aIn->GetTimeoutMultiplier(&timeoutMultiplier); NS_ENSURE_SUCCESS(rv, rv); timeoutMultiplier = std::max(timeoutMultiplier, static_cast(1)); auto checkedTimeout = CheckedInt64(time(nullptr)) + timeout * userActionRequestsCount * timeoutMultiplier; if (!checkedTimeout.isValid()) { return NS_ERROR_FAILURE; } aOut->set_expires_at(checkedTimeout.value()); const std::string tag = "dlp"; // TODO: *aOut->add_tags() = tag; auto* requestData = aOut->mutable_request_data(); RefPtr windowGlobal; rv = aIn->GetWindowGlobalParent(getter_AddRefs(windowGlobal)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr url; rv = aIn->GetUrl(getter_AddRefs(url)); NS_ENSURE_SUCCESS(rv, rv); if (!url) { // We already checked that this exists. MOZ_ASSERT(windowGlobal); // If no URL is given then we use the one for the window. url = ContentAnalysis::GetURIForBrowsingContext( windowGlobal->Canonical()->GetBrowsingContext()); // We also already checked for this. MOZ_ASSERT(url); } nsCString urlString; rv = url->GetSpec(urlString); NS_ENSURE_SUCCESS(rv, rv); if (!urlString.IsEmpty()) { requestData->set_url(urlString.get()); } if (windowGlobal) { nsString title; windowGlobal->GetDocumentTitle(title); requestData->set_tab_title(NS_ConvertUTF16toUTF8(title).get()); } nsString email; rv = aIn->GetEmail(email); NS_ENSURE_SUCCESS(rv, rv); if (!email.IsEmpty()) { requestData->set_email(NS_ConvertUTF16toUTF8(email).get()); } nsCString sha256Digest; rv = aIn->GetSha256Digest(sha256Digest); NS_ENSURE_SUCCESS(rv, rv); if (!sha256Digest.IsEmpty()) { requestData->set_digest(sha256Digest.get()); } if (analysisType == nsIContentAnalysisRequest::AnalysisType::ePrint) { #if XP_WIN uint64_t printDataHandle; MOZ_TRY(aIn->GetPrintDataHandle(&printDataHandle)); if (!printDataHandle) { return NS_ERROR_OUT_OF_MEMORY; } aOut->mutable_print_data()->set_handle(printDataHandle); uint64_t printDataSize; MOZ_TRY(aIn->GetPrintDataSize(&printDataSize)); aOut->mutable_print_data()->set_size(printDataSize); nsString printerName; MOZ_TRY(aIn->GetPrinterName(printerName)); requestData->mutable_print_metadata()->set_printer_name( NS_ConvertUTF16toUTF8(printerName).get()); #else return NS_ERROR_NOT_IMPLEMENTED; #endif } else { nsString filePath; rv = aIn->GetFilePath(filePath); NS_ENSURE_SUCCESS(rv, rv); if (!filePath.IsEmpty()) { std::string filePathStr = NS_ConvertUTF16toUTF8(filePath).get(); aOut->set_file_path(filePathStr); auto filename = filePathStr.substr(filePathStr.find_last_of("/\\") + 1); if (!filename.empty()) { requestData->set_filename(filename); } } else { nsString textContent; rv = aIn->GetTextContent(textContent); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(!textContent.IsEmpty()); aOut->set_text_content(NS_ConvertUTF16toUTF8(textContent).get()); } } #ifdef XP_WIN ULONG userLen = 0; GetUserNameExW(NameSamCompatible, nullptr, &userLen); if (GetLastError() == ERROR_MORE_DATA && userLen > 0) { auto user = mozilla::MakeUnique(userLen); if (GetUserNameExW(NameSamCompatible, user.get(), &userLen)) { auto* clientMetadata = aOut->mutable_client_metadata(); auto* browser = clientMetadata->mutable_browser(); browser->set_machine_user(NS_ConvertUTF16toUTF8(user.get()).get()); } } #endif nsTArray> resources; rv = aIn->GetResources(resources); NS_ENSURE_SUCCESS(rv, rv); if (!resources.IsEmpty()) { auto* pbClientDownloadRequest = requestData->mutable_csd(); for (auto& nsResource : resources) { rv = ConvertToProtobuf(nsResource.get(), pbClientDownloadRequest->add_resources()); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } namespace { // We don't want this overload to be called for string parameters, so // use std::enable_if template typename std::enable_if_t>, void> LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) { ss << value; } // 0 indicates no max length template typename std::enable_if_t>, void> LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) { if (!maxLength || value.length() < maxLength) { ss << value; } else { ss << value.substr(0, maxLength) << " (truncated)"; } } } // namespace static void LogRequest( const content_analysis::sdk::ContentAnalysisRequest* aPbRequest) { // We cannot use Protocol Buffer's DebugString() because we optimize for // lite runtime. if (!static_cast(gContentAnalysisLog) ->ShouldLog(LogLevel::Debug)) { return; } std::stringstream ss; ss << "ContentAnalysisRequest:" << "\n"; #define ADD_FIELD_WITH_VALFUNC(PBUF, NAME, FUNC, VALFUNC) \ ss << " " << (NAME) << ": "; \ if ((PBUF)->has_##FUNC()) { \ LogWithMaxLength(ss, VALFUNC(), 500); \ ss << "\n"; \ } else \ ss << "" \ << "\n"; #define ADD_FIELD(PBUF, NAME, FUNC) \ ADD_FIELD_WITH_VALFUNC(PBUF, NAME, FUNC, (PBUF)->FUNC) #define ADD_EXISTS(PBUF, NAME, FUNC) \ ss << " " << (NAME) << ": " \ << ((PBUF)->has_##FUNC() ? "" : "") << "\n"; #define ADD_NONEMPTY(PBUF, NAME, FUNC) \ ss << " " << (NAME) << ": " \ << (((PBUF)->has_##FUNC() && (!(PBUF)->FUNC().empty())) ? "" \ : "") \ << "\n"; ADD_FIELD(aPbRequest, "Expires", expires_at); ADD_FIELD(aPbRequest, "Analysis Type", analysis_connector); ADD_FIELD(aPbRequest, "Request Token", request_token); ADD_FIELD(aPbRequest, "User Action ID", user_action_id); ADD_FIELD(aPbRequest, "User Action Requests Count", user_action_requests_count); ADD_FIELD(aPbRequest, "File Path", file_path); ADD_NONEMPTY(aPbRequest, "Text Content", text_content); // TODO: Tags ADD_EXISTS(aPbRequest, "Request Data Struct", request_data); const auto* requestData = aPbRequest->has_request_data() ? &aPbRequest->request_data() : nullptr; if (requestData) { ADD_FIELD(requestData, " Url", url); ADD_FIELD(requestData, " Email", email); auto hexDigestFunc = [&requestData]() { return ToHexString( reinterpret_cast(requestData->digest().c_str()), requestData->digest().length()); }; ADD_FIELD_WITH_VALFUNC(requestData, " SHA-256 Digest", digest, hexDigestFunc); ADD_FIELD(requestData, " Filename", filename); ADD_EXISTS(requestData, " Client Download Request struct", csd); const auto* csd = requestData->has_csd() ? &requestData->csd() : nullptr; if (csd) { uint32_t i = 0; for (const auto& resource : csd->resources()) { ss << " Resource " << i << ":" << "\n"; ADD_FIELD(&resource, " Url", url); ADD_FIELD(&resource, " Type", type); ++i; } } } ADD_EXISTS(aPbRequest, "Client Metadata Struct", client_metadata); const auto* clientMetadata = aPbRequest->has_client_metadata() ? &aPbRequest->client_metadata() : nullptr; if (clientMetadata) { ADD_EXISTS(clientMetadata, " Browser Struct", browser); const auto* browser = clientMetadata->has_browser() ? &clientMetadata->browser() : nullptr; if (browser) { ADD_FIELD(browser, " Machine User", machine_user); } } #undef ADD_EXISTS #undef ADD_FIELD LOGD("%s", ss.str().c_str()); } /* static */ already_AddRefed ExternalAgentBackend::ConvertResponseFromProtobuf( content_analysis::sdk::ContentAnalysisResponse&& aResponse, const nsCString& aUserActionId) { ContentAnalysisResponse::Action action = ContentAnalysisResponse::Action::eUnspecified; for (const auto& result : aResponse.results()) { if (!result.has_status() || result.status() != content_analysis::sdk::ContentAnalysisResponse::Result::SUCCESS) { return nullptr; } // The action values increase with severity, so the max is the most severe. for (const auto& rule : result.triggered_rules()) { action = static_cast(std::max( static_cast(action), static_cast(rule.action()))); } } // If no rules blocked then we should allow. if (action == ContentAnalysisResponse::Action::eUnspecified) { action = ContentAnalysisResponse::Action::eAllow; } const auto& requestToken = aResponse.request_token(); nsCString requestTokenStr; requestTokenStr.Assign(requestToken.data(), requestToken.size()); return MakeRefPtr(action, requestTokenStr, aUserActionId) .forget(); } static void LogResponse( content_analysis::sdk::ContentAnalysisResponse* aPbResponse) { if (!static_cast(gContentAnalysisLog) ->ShouldLog(LogLevel::Debug)) { return; } std::stringstream ss; ss << "ContentAnalysisResponse:" << "\n"; #define ADD_FIELD(PBUF, NAME, FUNC) \ ss << " " << (NAME) << ": "; \ if ((PBUF)->has_##FUNC()) \ ss << (PBUF)->FUNC() << "\n"; \ else \ ss << "" \ << "\n"; ADD_FIELD(aPbResponse, "Request Token", request_token); uint32_t i = 0; for (const auto& result : aPbResponse->results()) { ss << " Result " << i << ":" << "\n"; ADD_FIELD(&result, " Status", status); uint32_t j = 0; for (const auto& rule : result.triggered_rules()) { ss << " Rule " << j << ":" << "\n"; ADD_FIELD(&rule, " action", action); ++j; } ++i; } #undef ADD_FIELD LOGD("%s", ss.str().c_str()); } static nsresult ConvertToProtobuf( nsIContentAnalysisAcknowledgement* aIn, const nsACString& aRequestToken, content_analysis::sdk::ContentAnalysisAcknowledgement* aOut) { aOut->set_request_token(aRequestToken.Data(), aRequestToken.Length()); nsIContentAnalysisAcknowledgement::Result result; nsresult rv = aIn->GetResult(&result); NS_ENSURE_SUCCESS(rv, rv); aOut->set_status( static_cast( result)); nsIContentAnalysisAcknowledgement::FinalAction finalAction; rv = aIn->GetFinalAction(&finalAction); NS_ENSURE_SUCCESS(rv, rv); aOut->set_final_action( static_cast< content_analysis::sdk::ContentAnalysisAcknowledgement_FinalAction>( finalAction)); return NS_OK; } static void LogAcknowledgement( content_analysis::sdk::ContentAnalysisAcknowledgement* aPbAck) { if (!static_cast(gContentAnalysisLog) ->ShouldLog(LogLevel::Debug)) { return; } std::stringstream ss; ss << "ContentAnalysisAcknowledgement:" << "\n"; #define ADD_FIELD(PBUF, NAME, FUNC) \ ss << " " << (NAME) << ": "; \ if ((PBUF)->has_##FUNC()) \ ss << (PBUF)->FUNC() << "\n"; \ else \ ss << "" \ << "\n"; ADD_FIELD(aPbAck, "Request Token", request_token); ADD_FIELD(aPbAck, "Status", status); ADD_FIELD(aPbAck, "Final Action", final_action); #undef ADD_FIELD LOGD("%s", ss.str().c_str()); } nsresult ExternalAgentBackend::CreateContentAnalysisClient( nsCString&& aPipePathName, nsString&& aClientSignatureSetting, bool aIsPerUser) { MOZ_ASSERT(!NS_IsMainThread()); std::shared_ptr client; bool isShutDown = IsContentAnalysisShutDown(); if (!isShutDown) { client.reset( content_analysis::sdk::Client::Create({aPipePathName.get(), aIsPerUser}) .release()); LOGD("Content analysis is %s", client ? "connected" : "not available"); } else { LOGD("ContentAnalysis::IsShutDown is true"); } #ifdef XP_WIN if (client && !aClientSignatureSetting.IsEmpty()) { std::string agentPath = client->GetAgentInfo().binary_path; nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath); UniquePtr orgName = mozilla::DllServices::Get()->GetBinaryOrgName(agentWidePath.get()); bool signatureMatches = false; if (orgName) { auto dependentOrgName = nsDependentString(orgName.get()); LOGD("Content analysis client signed with organization name \"%S\"", dependentOrgName.getW()); signatureMatches = aClientSignatureSetting.Equals(dependentOrgName); } else { LOGD("Content analysis client has no signature"); } if (!signatureMatches) { LOGE( "Got mismatched content analysis client signature! All content " "analysis operations will fail."); nsresult rv = NS_ERROR_INVALID_SIGNATURE; glean::content_analysis::connection_failure .Get(nsCString{SafeGetStaticErrorName(rv)}) .Add(); NS_DispatchToMainThread( NS_NewRunnableFunction(__func__, [self = RefPtr{this}, rv]() { AssertIsOnMainThread(); self->mClientPromise->Reject(rv, __func__); self->mCreatingClient = false; })); return NS_OK; } } #endif // XP_WIN NS_DispatchToMainThread(NS_NewRunnableFunction( __func__, [self = RefPtr{this}, isShutDown, client = std::move(client)]() { AssertIsOnMainThread(); // Note that if mClientPromise has been resolved or rejected, // calling Resolve() or Reject() is a no-op. if (client) { self->mHaveResolvedClientPromise = true; self->mClientPromise->Resolve(client, __func__); } else { nsresult promiseResult = isShutDown ? NS_ERROR_ILLEGAL_DURING_SHUTDOWN : NS_ERROR_CONNECTION_REFUSED; glean::content_analysis::connection_failure .Get(nsCString{SafeGetStaticErrorName(promiseResult)}) .Add(); self->mClientPromise->Reject(promiseResult, __func__); } self->mCreatingClient = false; })); return NS_OK; } ExternalAgentBackend::ExternalAgentBackend() : mRequestTokenToBasicRequestInfoMap( "ExternalAgentBackend::mRequestTokenToBasicRequestInfoMap") { mClientPromise = MakeRefPtr( "ExternalAgentBackend::ExternalAgentBackend"); mThreadPool = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( mThreadPool->SetName(nsAutoCString("ContentAnalysisAgentIO"))); unsigned long threadLimit = std::min(static_cast( StaticPrefs::browser_contentanalysis_max_connections()), kMaxContentAnalysisAgentThreads); MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetThreadLimit(threadLimit)); MOZ_ALWAYS_SUCCEEDS( mThreadPool->SetIdleThreadLimit(kMaxIdleContentAnalysisAgentThreads)); MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetIdleThreadGraceTimeout( kIdleContentAnalysisAgentTimeoutMs)); MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetIdleThreadMaximumTimeout( kMaxIdleContentAnalysisAgentTimeoutMs)); } void ExternalAgentBackend::OnMaxConnectionsPrefChanged() { AssertIsOnMainThread(); if (!mThreadPool) { return; } unsigned long threadLimit = std::min(static_cast( StaticPrefs::browser_contentanalysis_max_connections()), kMaxContentAnalysisAgentThreads); MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetThreadLimit(threadLimit)); } ExternalAgentBackend::~ExternalAgentBackend() { // Shutdown() should have been called via ContentAnalysis::Close(). MOZ_ASSERT(!mThreadPool); } void ExternalAgentBackend::Shutdown() { AssertIsOnMainThread(); // Reject the promise to avoid assertions when it gets destroyed // No-op if the promise has already been resolved or rejected mClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); // In case the promise _was_ resolved before, create a new one and reject // that. mClientPromise = MakeRefPtr("ExternalAgentBackend:Shutdown"); mClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); if (mThreadPool) { mThreadPool->ShutdownWithTimeout(kShutdownThreadpoolTimeoutMs); mThreadPool = nullptr; } } /* static */ bool ExternalAgentBackend::IsContentAnalysisShutDown() { RefPtr ca = ContentAnalysis::GetContentAnalysisFromService(); return !ca || ca->IsShutDown(); } nsresult ExternalAgentBackend::ForceReinitializeForTest() { AssertIsOnMainThread(); return CreateClientIfNecessary(/* aForceCreate */ true); } bool ExternalAgentBackend::IsCreatingClientForTest() const { AssertIsOnMainThread(); return mCreatingClient; } nsresult ExternalAgentBackend::CreateClientIfNecessary( bool aForceCreate /* = false */) { AssertIsOnMainThread(); if (IsContentAnalysisShutDown()) { return NS_OK; } nsCString pipePathName; nsresult rv = Preferences::GetCString(kPipePathNamePref, pipePathName); if (NS_WARN_IF(NS_FAILED(rv))) { mClientPromise->Reject(rv, __func__); return rv; } if (mHaveResolvedClientPromise && !aForceCreate) { return NS_OK; } // mCreatingClient is only accessed on the main thread if (mCreatingClient) { return NS_OK; } mCreatingClient = true; mHaveResolvedClientPromise = false; // Reject the promise to avoid assertions when it gets destroyed // No-op if the promise has already been resolved or rejected mClientPromise->Reject(NS_ERROR_FAILURE, __func__); mClientPromise = MakeRefPtr( "ExternalAgentBackend:CreateClientIfNecessary"); bool isPerUser = StaticPrefs::browser_contentanalysis_is_per_user(); nsString clientSignature; // It's OK if this fails, we will default to the empty string. Preferences::GetString(kClientSignature, clientSignature); RecordConnectionSettingsTelemetry(clientSignature); LOGD("Dispatching background task to create Content Analysis client"); glean::content_analysis::connection_attempt.Add(); if (aForceCreate) { // indicates this is a retry attempt glean::content_analysis::connection_attempt_retry.Add(); } rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction( "ExternalAgentBackend::CreateContentAnalysisClient", [owner = RefPtr{this}, pipePathName = std::move(pipePathName), clientSignature = std::move(clientSignature), isPerUser]() mutable { owner->CreateContentAnalysisClient( std::move(pipePathName), std::move(clientSignature), isPerUser); })); if (NS_WARN_IF(NS_FAILED(rv))) { glean::content_analysis::connection_failure .Get(nsCString{SafeGetStaticErrorName(rv)}) .Add(); mClientPromise->Reject(rv, __func__); return rv; } return NS_OK; } void ExternalAgentBackend::RecordConnectionSettingsTelemetry( const nsString& aClientSignature) { AssertIsOnMainThread(); { nsCString agentName; Preferences::GetCString(kAgentNamePref, agentName); glean::content_analysis::agent_name.Set(agentName); } AutoTArray interceptionPointsOff; for (const char* interceptionPointPrefName : kInterceptionPointPrefNames) { bool interceptionPointPrefValue = false; Preferences::GetBool(interceptionPointPrefName, &interceptionPointPrefValue); if (!interceptionPointPrefValue) { interceptionPointsOff.AppendElement(interceptionPointPrefName); } } if (!interceptionPointsOff.IsEmpty()) { glean::content_analysis::interception_points_turned_off.Set( interceptionPointsOff); } glean::content_analysis::show_blocked_result.Set( StaticPrefs::browser_contentanalysis_show_blocked_result()); glean::content_analysis::default_result.Set( StaticPrefs::browser_contentanalysis_default_result()); glean::content_analysis::timeout_result.Set( StaticPrefs::browser_contentanalysis_timeout_result()); if (!aClientSignature.IsEmpty()) { glean::content_analysis::client_signature.Set( NS_ConvertUTF16toUTF8(aClientSignature)); } glean::content_analysis::bypass_for_same_tab_operations.Set( StaticPrefs::browser_contentanalysis_bypass_for_same_tab_operations()); { nsCString allowUrlRegexList; Preferences::GetCString(kAllowUrlPref, allowUrlRegexList); // Unfortunately because of the way enterprise policies set and lock prefs, // we can't check if the value is different than the default in // StaticPrefList.yaml, and instead we have to duplicate that value here. At // least we have a test around this so we can update this value if the // default changes. const char* defaultAllowUrlRegexList = "^about:(?!blank|srcdoc).*"; glean::content_analysis::allow_url_regex_list_set.Set( !allowUrlRegexList.Equals(defaultAllowUrlRegexList)); } { nsCString denyUrlRegexList; Preferences::GetCString(kDenyUrlPref, denyUrlRegexList); glean::content_analysis::deny_url_regex_list_set.Set( !denyUrlRegexList.IsEmpty()); } } nsresult ExternalAgentBackend::EnsureReady() { AssertIsOnMainThread(); return CreateClientIfNecessary(); } void ExternalAgentBackend::CancelUserAction(const nsACString& aUserActionId) { CallClientWithRetry( __func__, [userActionId = nsCString(aUserActionId)]( std::shared_ptr client) mutable -> Result { MOZ_ASSERT(!NS_IsMainThread()); auto owner = ContentAnalysis::GetContentAnalysisFromService(); if (!owner) { // May be shutting down return nullptr; } content_analysis::sdk::ContentAnalysisCancelRequests cancelRequest; cancelRequest.set_user_action_id(userActionId.get(), userActionId.Length()); int err = client->CancelRequests(cancelRequest); if (err != 0) { LOGE( "SendCancelToAgent got error %d for " "user_action_id: %s", err, userActionId.get()); return Err(NS_ERROR_FAILURE); } LOGD( "SendCancelToAgent successfully sent CancelRequests to " "agent for user_action_id: %s", userActionId.get()); return nullptr; }) ->Then( GetCurrentSerialEventTarget(), __func__, []() { /* nothing to do */ }, [](nsresult rv) { LOGE("SendCancelToAgent failed to get the client with error %s", SafeGetStaticErrorName(rv)); }); } template RefPtr> ExternalAgentBackend::CallClientWithRetry( StaticString aMethodName, U&& aClientCallFunc) { AssertIsOnMainThread(); auto promise = MakeRefPtr::Private>(aMethodName); // Make a copy of aClientCallFunc so the retry path can re-invoke it. auto reconnectAndRetry = [clientCallFunc = aClientCallFunc, aMethodName, promise, self = RefPtr{this}](nsresult rv) mutable { AssertIsOnMainThread(); LOGD("Failed to get client - trying to reconnect: %s", SafeGetStaticErrorName(rv)); rv = self->CreateClientIfNecessary(/* aForceCreate */ true); if (NS_FAILED(rv)) { LOGD("Failed to reconnect to client: %s", SafeGetStaticErrorName(rv)); self->mClientPromise->Reject(rv, aMethodName); promise->Reject(rv, aMethodName); return; } self->mClientPromise->Then( GetCurrentSerialEventTarget(), aMethodName, [aMethodName, promise, self, clientCallFunc = std::move(clientCallFunc)]( std::shared_ptr client) mutable { nsresult rv = self->mThreadPool->Dispatch(NS_NewCancelableRunnableFunction( aMethodName, [aMethodName, promise, clientCallFunc = std::move(clientCallFunc), client = std::move(client)]() mutable { auto result = clientCallFunc(client); if (result.isOk()) { promise->Resolve(result.unwrap(), aMethodName); } else { promise->Reject(result.unwrapErr(), aMethodName); } })); if (NS_FAILED(rv)) { LOGE( "Failed to launch background task in second call for %s, " "error=%s", aMethodName.get(), SafeGetStaticErrorName(rv)); promise->Reject(rv, aMethodName); } }, [aMethodName, promise](nsresult rv) { LOGE("Failed to get client again for %s, error=%s", aMethodName.get(), SafeGetStaticErrorName(rv)); promise->Reject(rv, aMethodName); }); }; mClientPromise->Then( GetCurrentSerialEventTarget(), aMethodName, [aMethodName, promise, self = RefPtr{this}, clientCallFunc = std::forward(aClientCallFunc), reconnectAndRetry]( std::shared_ptr client) mutable { nsresult rv = self->mThreadPool->Dispatch(NS_NewCancelableRunnableFunction( aMethodName, [aMethodName, promise, clientCallFunc = std::move(clientCallFunc), reconnectAndRetry = std::move(reconnectAndRetry), client = std::move(client)]() mutable { auto result = clientCallFunc(client); if (result.isOk()) { promise->Resolve(result.unwrap(), aMethodName); return; } nsresult rv = result.unwrapErr(); NS_DispatchToMainThread(NS_NewCancelableRunnableFunction( "reconnect to Content Analysis client", [rv, reconnectAndRetry = std::move(reconnectAndRetry)]() mutable { reconnectAndRetry(rv); })); })); if (NS_FAILED(rv)) { LOGE( "Failed to launch background task in first call for %s, " "error=%s", aMethodName.get(), SafeGetStaticErrorName(rv)); promise->Reject(rv, aMethodName); } }, [reconnectAndRetry](nsresult rv) mutable { reconnectAndRetry(rv); }); return promise.forget(); } nsresult ExternalAgentBackend::Analyze( nsCOMPtr aRequest, bool aAutoAcknowledge) { AssertIsOnMainThread(); ++mRequestCount; nsCString requestToken; MOZ_ALWAYS_SUCCEEDS(aRequest->GetRequestToken(requestToken)); nsCString userActionId; MOZ_ALWAYS_SUCCEEDS(aRequest->GetUserActionId(userActionId)); // We will need to submit the request to the agent. content_analysis::sdk::ContentAnalysisRequest pbRequest; nsresult rv = ConvertToProtobuf(aRequest, &pbRequest); NS_ENSURE_SUCCESS(rv, rv); LOGD("Issuing ContentAnalysisRequest for token %s", requestToken.get()); LogRequest(&pbRequest); nsCOMPtr obsServ = mozilla::services::GetObserverService(); // Avoid serializing the string here if no one is observing this message if (obsServ && obsServ->HasObservers("dlp-request-sent-raw")) { std::string requestString = pbRequest.SerializeAsString(); nsTArray requestArray; requestArray.SetLength(requestString.size() + 1); for (size_t i = 0; i < requestString.size(); ++i) { // Since NotifyObservers() expects a null-terminated string, // make sure none of these values are 0. requestArray[i] = requestString[i] + 0xFF00; } requestArray[requestString.size()] = 0; if (auto ca = ContentAnalysis::GetContentAnalysisFromService()) { obsServ->NotifyObservers(static_cast(ca.get()), "dlp-request-sent-raw", requestArray.Elements()); } } bool ignoreCanceled; MOZ_ALWAYS_SUCCEEDS(aRequest->GetTestOnlyIgnoreCanceledAndAlwaysSubmitToAgent( &ignoreCanceled)); { nsDependentCString analysisTypeStr( content_analysis::sdk::AnalysisConnector_Name( pbRequest.analysis_connector()) .c_str()); glean::content_analysis::request_sent_by_analysis_type.Get(analysisTypeStr) .Add(); } { nsDependentCString reasonStr( content_analysis::sdk::ContentAnalysisRequest_Reason_Name( pbRequest.reason()) .c_str()); glean::content_analysis::request_sent_by_reason.Get(reasonStr).Add(); } CallClientWithRetry( __func__, [self = RefPtr{this}, userActionId = userActionId, pbRequest = std::move(pbRequest), aAutoAcknowledge, ignoreCanceled]( std::shared_ptr client) mutable { MOZ_ASSERT(!NS_IsMainThread()); return self->DoAnalyzeRequest(std::move(userActionId), std::move(pbRequest), aAutoAcknowledge, client, ignoreCanceled); }) ->Then( GetMainThreadSerialEventTarget(), __func__, []() { /* do nothing */ }, [userActionId = std::move(userActionId), requestToken = std::move(requestToken)](nsresult rv) mutable { LOGE( "Analyze failed to get client a second time for " "requestToken=%s, userActionId=%s", requestToken.get(), userActionId.get()); RefPtr owner = ContentAnalysis::GetContentAnalysisFromService(); if (!owner) { // May be shutting down return; } owner->CancelWithError(std::move(userActionId), rv); }); return NS_OK; } Result ExternalAgentBackend::DoAnalyzeRequest( nsCString&& aUserActionId, content_analysis::sdk::ContentAnalysisRequest&& aRequest, bool aAutoAcknowledge, const std::shared_ptr& aClient, bool aTestOnlyIgnoreCanceled) { MOZ_ASSERT(!NS_IsMainThread()); RefPtr owner = ContentAnalysis::GetContentAnalysisFromService(); if (!owner) { // May be shutting down // Don't return an error because we don't want to retry return nullptr; } if (aRequest.has_file_path() && !aRequest.file_path().empty() && (!aRequest.request_data().has_digest() || aRequest.request_data().digest().empty())) { // Calculate the digest nsCString digest; nsCString fileCPath(aRequest.file_path().data(), aRequest.file_path().length()); nsString filePath = NS_ConvertUTF8toUTF16(fileCPath); nsresult rv = ContentAnalysisRequest::GetFileDigest(filePath, digest); if (NS_FAILED(rv)) { owner->CancelWithError(std::move(aUserActionId), rv); // Don't return an error because we don't want to retry return nullptr; } if (!digest.IsEmpty()) { aRequest.mutable_request_data()->set_digest(digest.get()); } } bool actionWasCanceled = !aTestOnlyIgnoreCanceled && owner->WasUserActionCanceled(aUserActionId); if (actionWasCanceled) { LOGD( "DoAnalyzeRequest | userAction: %s | requestToken: %s | was already " "canceled", aUserActionId.get(), aRequest.request_token().c_str()); return Err(NS_ERROR_WONT_HANDLE_CONTENT); } // Run request, then dispatch back to main thread to resolve // aCallback content_analysis::sdk::ContentAnalysisResponse pbResponse; nsDependentCString analysisConnectorName( content_analysis::sdk::AnalysisConnector_Name( aRequest.analysis_connector()) .c_str()); auto timerId = glean::content_analysis::response_duration_by_analysis_type .Get(analysisConnectorName) .Start(); { // Insert this into the map before calling Send() because another thread // calling Send() may get a response before our Send() call finishes. auto map = mRequestTokenToBasicRequestInfoMap.Lock(); map->InsertOrUpdate( nsCString(aRequest.request_token()), ExternalAgentBackend::BasicRequestInfo{aUserActionId, timerId, std::move(analysisConnectorName), aAutoAcknowledge}); } LOGD( "DoAnalyzeRequest | userAction: %s | requestToken: %s | sending request " "to agent", aUserActionId.get(), aRequest.request_token().c_str()); int err = aClient->Send(aRequest, &pbResponse); if (err != 0) { LOGE("DoAnalyzeRequest got err=%d for request_token=%s, user_action_id=%s", err, aRequest.request_token().c_str(), aUserActionId.get()); Maybe entry; { auto map = mRequestTokenToBasicRequestInfoMap.Lock(); entry = map->Extract(nsCString(aRequest.request_token())); } if (entry.isSome()) { glean::content_analysis::response_duration_by_analysis_type .Get(entry->mAnalysisTypeStr) .Cancel(std::move(entry->mTimerId)); } return Err(NS_ERROR_FAILURE); } HandleResponseFromAgent(std::move(pbResponse)); return nullptr; } void ExternalAgentBackend::HandleResponseFromAgent( content_analysis::sdk::ContentAnalysisResponse&& aResponse) { MOZ_ASSERT(!NS_IsMainThread()); NS_DispatchToMainThread(NS_NewRunnableFunction( __func__, [self = RefPtr{this}, aResponse = std::move(aResponse)]() mutable { LOGD("HandleResponseFromAgent on main thread"); LogResponse(&aResponse); RefPtr owner = ContentAnalysis::GetContentAnalysisFromService(); if (!owner) { // May be shutting down return; } nsCOMPtr obsServ = mozilla::services::GetObserverService(); // This message is only used for testing purposes, so avoid // serializing the string here if no one is observing this message. // This message is only really useful if we're in a timeout // situation, otherwise dlp-response is fine. if (obsServ && obsServ->HasObservers("dlp-response-received-raw")) { std::string responseString = aResponse.SerializeAsString(); nsTArray responseArray; responseArray.SetLength(responseString.size() + 1); for (size_t i = 0; i < responseString.size(); ++i) { // Since NotifyObservers() expects a null-terminated string, // make sure none of these values are 0. responseArray[i] = responseString[i] + 0xFF00; } responseArray[responseString.size()] = 0; obsServ->NotifyObservers(static_cast(owner), "dlp-response-received-raw", responseArray.Elements()); } Maybe maybeBasicRequestInfo; { auto map = self->mRequestTokenToBasicRequestInfoMap.Lock(); maybeBasicRequestInfo = map->Extract(nsCString(aResponse.request_token())); } if (maybeBasicRequestInfo.isNothing()) { LOGE( "HandleResponseFromAgent could not find userActionId for " "request token %s", aResponse.request_token().c_str()); // We have no hope of doing anything useful, so just early return. return; } glean::content_analysis::response_duration_by_analysis_type .Get(maybeBasicRequestInfo->mAnalysisTypeStr) .StopAndAccumulate(std::move(maybeBasicRequestInfo->mTimerId)); nsCString userActionId = maybeBasicRequestInfo->mUserActionId; RefPtr response = ConvertResponseFromProtobuf(std::move(aResponse), userActionId); if (!response) { LOGE("Content analysis got invalid response!"); return; } owner->HandleResponseFromAgent(response, maybeBasicRequestInfo->mAutoAcknowledge); })); } bool ExternalAgentBackend::IsResponsePendingForRequest( const nsACString& aRequestToken) { auto map = mRequestTokenToBasicRequestInfoMap.Lock(); return map->Contains(aRequestToken); } nsresult ExternalAgentBackend::Acknowledge( nsCOMPtr aAcknowledgement, const nsACString& aRequestToken) { AssertIsOnMainThread(); content_analysis::sdk::ContentAnalysisAcknowledgement pbAck; nsresult rv = ConvertToProtobuf(aAcknowledgement, aRequestToken, &pbAck); NS_ENSURE_SUCCESS(rv, rv); LOGD("Issuing ContentAnalysisAcknowledgement"); LogAcknowledgement(&pbAck); nsCOMPtr obsServ = mozilla::services::GetObserverService(); // Do an early check here to avoid an extra dispatch to the main // thread if no one is observing the message bool rawMessageHasObserver = false; if (obsServ) { rawMessageHasObserver = obsServ->HasObservers("dlp-acknowledgement-sent-raw"); } // The content analysis connection is synchronous so run in the background. LOGD("RunAcknowledgeTask dispatching acknowledge task"); CallClientWithRetry( __func__, [pbAck = std::move(pbAck), rawMessageHasObserver]( std::shared_ptr client) mutable -> Result { MOZ_ASSERT(!NS_IsMainThread()); RefPtr owner = ContentAnalysis::GetContentAnalysisFromService(); if (!owner) { // May be shutting down return nullptr; } int err = client->Acknowledge(pbAck); LOGD( "RunAcknowledgeTask sent transaction acknowledgement, " "err=%d", err); // Wait until the acknowledgement is sent before sending // the dlp-acknowledgement-sent-raw notification to make tests // more reliable. if (rawMessageHasObserver) { NS_DispatchToMainThread(NS_NewRunnableFunction( __func__, [owner, pbAck = std::move(pbAck)]() { nsCOMPtr obsServ = mozilla::services::GetObserverService(); if (!obsServ) { // Shutting down. We don't have a connection to the agent // anymore so sending acknowledgement would fail anyway. return; } std::string acknowledgementString = pbAck.SerializeAsString(); nsTArray acknowledgementArray; acknowledgementArray.SetLength(acknowledgementString.size() + 1); for (size_t i = 0; i < acknowledgementString.size(); ++i) { // Since NotifyObservers() expects a null-terminated string, // make sure none of these values are 0. acknowledgementArray[i] = acknowledgementString[i] + 0xFF00; } acknowledgementArray[acknowledgementString.size()] = 0; obsServ->NotifyObservers( static_cast(owner.get()), "dlp-acknowledgement-sent-raw", acknowledgementArray.Elements()); })); } if (err != 0) { return Err(NS_ERROR_FAILURE); } return nullptr; }) ->Then( GetMainThreadSerialEventTarget(), __func__, []() { /* do nothing */ }, [](nsresult rv) { LOGE("RunAcknowledgeTask failed to get the client"); }); return NS_OK; } RefPtr ExternalAgentBackend::GetDiagnosticInfo() { AssertIsOnMainThread(); auto diagnosticInfoPromise = MakeRefPtr(__func__); CallClientWithRetry( __func__, [self = RefPtr{this}, diagnosticInfoPromise]( std::shared_ptr client) mutable -> Result { MOZ_ASSERT(!NS_IsMainThread()); // I don't think this will be slow, but do it on the background thread // just to be safe std::string agentPath = client->GetAgentInfo().binary_path; // Need to switch back to main thread to create the // ContentAnalysisDiagnosticInfo and resolve the promise NS_DispatchToMainThread(NS_NewRunnableFunction( __func__, [self, diagnosticInfoPromise = std::move(diagnosticInfoPromise), agentPath = std::move(agentPath)]() { AssertIsOnMainThread(); if (IsContentAnalysisShutDown()) { // may be quitting diagnosticInfoPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); return; } nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath); // Note that if we made it here, we have successfully connected to // the agent. auto info = MakeRefPtr( /* mConnectedToAgent */ true, std::move(agentWidePath), false, self->mRequestCount); diagnosticInfoPromise->Resolve(info, __func__); })); return nullptr; }) ->Then( GetMainThreadSerialEventTarget(), __func__, []() {}, [self = RefPtr{this}, diagnosticInfoPromise](nsresult rv) { AssertIsOnMainThread(); auto info = MakeRefPtr( false, EmptyString(), rv == NS_ERROR_INVALID_SIGNATURE, self->mRequestCount); diagnosticInfoPromise->Resolve(info, __func__); }); return diagnosticInfoPromise.forget(); } #undef LOGD #undef LOGE } // namespace mozilla::contentanalysis