Move the Windows-specific parts (i.e. converting printed data to a HANDLE) to the ExternalAgentBackend. Also abstract out which backend we're using by calling into ContentAnalysisBackendFactory. Differential Revision: https://phabricator.services.mozilla.com/D308894
1361 lines
51 KiB
C++
1361 lines
51 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/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 <sstream>
|
|
|
|
#ifdef XP_WIN
|
|
# define SECURITY_WIN32 1
|
|
# include <security.h>
|
|
# 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<content_analysis::sdk::ClientDownloadRequest_ResourceType>(
|
|
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<dom::WindowGlobalParent> windowGlobal;
|
|
NS_ENSURE_SUCCESS(
|
|
aRequest->GetWindowGlobalParent(getter_AddRefs(windowGlobal)), false);
|
|
|
|
// Any DataTransfer should have been expanded into individual requests.
|
|
nsCOMPtr<dom::DataTransfer> 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<nsITransferable> 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<nsIURI> 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<content_analysis::sdk::AnalysisConnector>(analysisType);
|
|
aOut->set_analysis_connector(connector);
|
|
|
|
nsIContentAnalysisRequest::Reason reason;
|
|
rv = aIn->GetReason(&reason);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
auto sdkReason =
|
|
static_cast<content_analysis::sdk::ContentAnalysisRequest::Reason>(
|
|
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<uint32_t>(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<dom::WindowGlobalParent> windowGlobal;
|
|
rv = aIn->GetWindowGlobalParent(getter_AddRefs(windowGlobal));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
nsCOMPtr<nsIURI> 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
|
|
nsTArray<uint8_t> printData;
|
|
MOZ_TRY(aIn->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(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<wchar_t[]>(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<RefPtr<nsIClientDownloadResource>> 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 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 */
|
|
already_AddRefed<ContentAnalysisResponse>
|
|
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<ContentAnalysisResponse::Action>(std::max(
|
|
static_cast<uint32_t>(action), static_cast<uint32_t>(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<ContentAnalysisResponse>(action, requestTokenStr,
|
|
aUserActionId)
|
|
.forget();
|
|
}
|
|
|
|
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 = ConvertToProtobuf(aRequest, &pbRequest);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// For print requests on Windows, ConvertToProtobuf 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
|