Files
Greg Stoll cae692fed2 Bug 2044490 part 3 - Wire the in-process WASM backend into content analysis r=jmendez,dlp-reviewers,mkaply,handyman
Add WasmModuleBackend, a ContentAnalysisBackend implementation that
serializes requests to the content_analysis SDK protobuf, and hands them
to the in-process WASM DLP module via nsIContentAnalysisWasmRunner.

Gated behind a pref that is off (and will eventually be set by policy)

Differential Revision: https://phabricator.services.mozilla.com/D311326
2026-07-17 19:47:52 +00:00

1150 lines
44 KiB
C++

/* 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/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/glean/ContentanalysisMetrics.h"
#include "nsIObserverService.h"
#include "nsString.h"
#include "nsThreadPool.h"
#include <sstream>
#ifdef XP_WIN
# 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 {
// Only ExternalAgentBackend needs the print data / file path inline in the
// protobuf request: it has no other channel to ship this content to the
// agent process. The WASM backend instead passes file/print content bytes
// directly to the runner alongside the (shared) request, so it doesn't
// override this, and, unlike the external agent, isn't limited to Windows for
// print requests.
nsresult ExternalAgentBackend::ConvertRequestToProtobuf(
nsIContentAnalysisRequest* aRequest,
content_analysis::sdk::ContentAnalysisRequest* aOut) {
nsresult rv =
ContentAnalysisBackend::ConvertRequestToProtobuf(aRequest, aOut);
NS_ENSURE_SUCCESS(rv, rv);
nsIContentAnalysisRequest::AnalysisType analysisType;
rv = aRequest->GetAnalysisType(&analysisType);
NS_ENSURE_SUCCESS(rv, rv);
auto* requestData = aOut->mutable_request_data();
if (analysisType == nsIContentAnalysisRequest::AnalysisType::ePrint) {
#if XP_WIN
nsTArray<uint8_t> printData;
MOZ_TRY(aRequest->GetPrintData(printData));
// Copy the PDF bytes into a file-mapping section so the agent can map them
// into its own process. The agent duplicates this handle out of our process
// while servicing the request, so the caller must keep the handle open
// until this request's response arrives (see
// ExternalAgentBackend::Analyze).
LARGE_INTEGER dataContentLength;
dataContentLength.QuadPart = static_cast<LONGLONG>(printData.Length());
HANDLE printDataHandle = ::CreateFileMappingW(
INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
dataContentLength.HighPart, dataContentLength.LowPart, nullptr);
if (!printDataHandle) {
return NS_ERROR_OUT_OF_MEMORY;
}
{
mozilla::nt::AutoMappedView view(printDataHandle, FILE_MAP_ALL_ACCESS);
memcpy(view.as<uint8_t>(), printData.Elements(), printData.Length());
}
aOut->mutable_print_data()->set_handle(
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(printDataHandle)));
aOut->mutable_print_data()->set_size(printData.Length());
nsString printerName;
MOZ_TRY(aRequest->GetPrinterName(printerName));
requestData->mutable_print_metadata()->set_printer_name(
NS_ConvertUTF16toUTF8(printerName).get());
return NS_OK;
#else
return NS_ERROR_NOT_IMPLEMENTED;
#endif
}
nsString filePath;
rv = aRequest->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);
}
}
return NS_OK;
}
namespace {
// We don't want this overload to be called for string parameters, so
// use std::enable_if
template <typename T>
typename std::enable_if_t<!std::is_same_v<std::string, std::decay_t<T>>, void>
LogWithMaxLength(std::stringstream& ss, T value, size_t maxLength) {
ss << value;
}
// 0 indicates no max length
template <typename T>
typename std::enable_if_t<std::is_same_v<std::string, std::decay_t<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<LogModule*>(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 << "<none>" \
<< "\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() ? "<exists>" : "<none>") << "\n";
#define ADD_NONEMPTY(PBUF, NAME, FUNC) \
ss << " " << (NAME) << ": " \
<< (((PBUF)->has_##FUNC() && (!(PBUF)->FUNC().empty())) ? "<nonempty>" \
: "<none>") \
<< "\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<const uint8_t*>(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 void LogResponse(
content_analysis::sdk::ContentAnalysisResponse* aPbResponse) {
if (!static_cast<LogModule*>(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 << "<none>" \
<< "\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<content_analysis::sdk::ContentAnalysisAcknowledgement_Status>(
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<LogModule*>(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 << "<none>" \
<< "\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<content_analysis::sdk::Client> 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<wchar_t[]> 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<ClientPromise::Private>(
"ExternalAgentBackend::ExternalAgentBackend");
mThreadPool = MakeRefPtr<nsThreadPool>();
MOZ_ALWAYS_SUCCEEDS(
mThreadPool->SetName(nsAutoCString("ContentAnalysisAgentIO")));
unsigned long threadLimit =
std::min(static_cast<unsigned long>(
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<unsigned long>(
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<ClientPromise::Private>("ExternalAgentBackend:Shutdown");
mClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
if (mThreadPool) {
mThreadPool->ShutdownWithTimeout(kShutdownThreadpoolTimeoutMs);
mThreadPool = nullptr;
}
}
/* static */ bool ExternalAgentBackend::IsContentAnalysisShutDown() {
RefPtr<ContentAnalysis> 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<ClientPromise::Private>(
"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<nsCString, 1> 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<std::nullptr_t>(
__func__,
[userActionId = nsCString(aUserActionId)](
std::shared_ptr<content_analysis::sdk::Client> client) mutable
-> Result<std::nullptr_t, nsresult> {
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 <typename T, typename U>
RefPtr<MozPromise<T, nsresult, true>> ExternalAgentBackend::CallClientWithRetry(
StaticString aMethodName, U&& aClientCallFunc) {
AssertIsOnMainThread();
auto promise =
MakeRefPtr<typename MozPromise<T, nsresult, true>::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<content_analysis::sdk::Client> 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<U>(aClientCallFunc), reconnectAndRetry](
std::shared_ptr<content_analysis::sdk::Client> 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<nsIContentAnalysisRequest> 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 = ConvertRequestToProtobuf(aRequest, &pbRequest);
NS_ENSURE_SUCCESS(rv, rv);
// For print requests on Windows, ConvertRequestToProtobuf stored the
// PDF bytes in a file-mapping section whose handle now lives in pbRequest.
// The agent duplicates that handle out of our process while servicing the
// request, and responses can arrive out of order, so the handle must stay
// open until this request's response arrives -- not merely until Send()
// returns. We hand it to DoAnalyzeRequest, which stores it in the per-request
// map entry that is only removed once the matching response is handled. The
// callable below also keeps a reference so the handle survives reconnect
// retries, where that map entry is briefly removed and re-inserted. Empty on
// platforms that do not transport print data via a handle.
std::shared_ptr<void> printDataHandleGuard;
#ifdef XP_WIN
if (pbRequest.has_print_data() && pbRequest.print_data().handle()) {
printDataHandleGuard = std::shared_ptr<void>(
reinterpret_cast<HANDLE>(
static_cast<uintptr_t>(pbRequest.print_data().handle())),
[](void* aHandle) { ::CloseHandle(aHandle); });
}
#endif
LOGD("Issuing ContentAnalysisRequest for token %s", requestToken.get());
LogRequest(&pbRequest);
nsCOMPtr<nsIObserverService> 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<char16_t> 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<nsIContentAnalysis*>(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<std::nullptr_t>(
__func__,
[self = RefPtr{this}, userActionId = userActionId,
pbRequest = std::move(pbRequest), aAutoAcknowledge, ignoreCanceled,
printDataHandleGuard](
std::shared_ptr<content_analysis::sdk::Client> client) mutable {
MOZ_ASSERT(!NS_IsMainThread());
return self->DoAnalyzeRequest(
std::move(userActionId), std::move(pbRequest), aAutoAcknowledge,
client, ignoreCanceled, printDataHandleGuard);
})
->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<ContentAnalysis> owner =
ContentAnalysis::GetContentAnalysisFromService();
if (!owner) {
// May be shutting down
return;
}
owner->CancelWithError(std::move(userActionId), rv);
});
return NS_OK;
}
Result<std::nullptr_t, nsresult> ExternalAgentBackend::DoAnalyzeRequest(
nsCString&& aUserActionId,
content_analysis::sdk::ContentAnalysisRequest&& aRequest,
bool aAutoAcknowledge,
const std::shared_ptr<content_analysis::sdk::Client>& aClient,
bool aTestOnlyIgnoreCanceled, std::shared_ptr<void> aPrintDataHandle) {
MOZ_ASSERT(!NS_IsMainThread());
RefPtr<ContentAnalysis> 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.
// The print-data handle (if any) is owned by this entry so it stays alive
// until this request's response arrives, even though responses may come
// back out of order.
auto map = mRequestTokenToBasicRequestInfoMap.Lock();
map->InsertOrUpdate(
nsCString(aRequest.request_token()),
MakeUnique<ExternalAgentBackend::BasicRequestInfo>(
ExternalAgentBackend::BasicRequestInfo{
aUserActionId, timerId, std::move(analysisConnectorName),
aAutoAcknowledge, std::move(aPrintDataHandle)}));
}
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<UniquePtr<ExternalAgentBackend::BasicRequestInfo>> 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<ContentAnalysis> owner =
ContentAnalysis::GetContentAnalysisFromService();
if (!owner) {
// May be shutting down
return;
}
nsCOMPtr<nsIObserverService> 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<char16_t> 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<nsIContentAnalysis*>(owner),
"dlp-response-received-raw",
responseArray.Elements());
}
Maybe<UniquePtr<ExternalAgentBackend::BasicRequestInfo>>
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<ContentAnalysisResponse> 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<nsIContentAnalysisAcknowledgement> 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<nsIObserverService> 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<std::nullptr_t>(
__func__,
[pbAck = std::move(pbAck), rawMessageHasObserver](
std::shared_ptr<content_analysis::sdk::Client> client) mutable
-> Result<std::nullptr_t, nsresult> {
MOZ_ASSERT(!NS_IsMainThread());
RefPtr<ContentAnalysis> 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<nsIObserverService> 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<char16_t> 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<nsIContentAnalysis*>(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<ContentAnalysisBackend::DiagnosticInfoPromise>
ExternalAgentBackend::GetDiagnosticInfo() {
AssertIsOnMainThread();
auto diagnosticInfoPromise =
MakeRefPtr<DiagnosticInfoPromise::Private>(__func__);
CallClientWithRetry<std::nullptr_t>(
__func__,
[self = RefPtr{this}, diagnosticInfoPromise](
std::shared_ptr<content_analysis::sdk::Client> client) mutable
-> Result<std::nullptr_t, nsresult> {
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<ContentAnalysisDiagnosticInfo>(
/* 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<ContentAnalysisDiagnosticInfo>(
false, EmptyString(), rv == NS_ERROR_INVALID_SIGNATURE,
self->mRequestCount);
diagnosticInfoPromise->Resolve(info, __func__);
});
return diagnosticInfoPromise.forget();
}
#undef LOGD
#undef LOGE
} // namespace mozilla::contentanalysis