Instead of caching the fact that the DLP Agent returned WARN, cache whether the user chose to ALLOW or BLOCK. This is more helpful, and apparently was causing hangs. Differential Revision: https://phabricator.services.mozilla.com/D220464
2438 lines
86 KiB
C++
2438 lines
86 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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 "ContentAnalysis.h"
|
|
#include "ContentAnalysisIPCTypes.h"
|
|
#include "content_analysis/sdk/analysis_client.h"
|
|
|
|
#include "base/process_util.h"
|
|
#include "GMPUtils.h" // ToHexString
|
|
#include "mozilla/Components.h"
|
|
#include "mozilla/dom/CanonicalBrowsingContext.h"
|
|
#include "mozilla/dom/DataTransfer.h"
|
|
#include "mozilla/dom/Promise.h"
|
|
#include "mozilla/dom/WindowGlobalParent.h"
|
|
#include "mozilla/Logging.h"
|
|
#include "mozilla/ScopeExit.h"
|
|
#include "mozilla/Services.h"
|
|
#include "mozilla/SpinEventLoopUntil.h"
|
|
#include "mozilla/StaticMutex.h"
|
|
#include "mozilla/StaticPrefs_browser.h"
|
|
#include "nsAppRunner.h"
|
|
#include "nsBaseClipboard.h"
|
|
#include "nsComponentManagerUtils.h"
|
|
#include "nsIClassInfoImpl.h"
|
|
#include "nsIFile.h"
|
|
#include "nsIGlobalObject.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsIOutputStream.h"
|
|
#include "nsIPrintSettings.h"
|
|
#include "nsIStorageStream.h"
|
|
#include "nsISupportsPrimitives.h"
|
|
#include "nsITransferable.h"
|
|
#include "ScopedNSSTypes.h"
|
|
#include "xpcpublic.h"
|
|
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
#ifdef XP_WIN
|
|
# include <windows.h>
|
|
# define SECURITY_WIN32 1
|
|
# include <security.h>
|
|
# include "mozilla/NativeNt.h"
|
|
# include "mozilla/WinDllServices.h"
|
|
#endif // XP_WIN
|
|
|
|
namespace mozilla::contentanalysis {
|
|
|
|
LazyLogModule gContentAnalysisLog("contentanalysis");
|
|
#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* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list";
|
|
const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list";
|
|
|
|
nsresult MakePromise(JSContext* aCx, RefPtr<mozilla::dom::Promise>* aPromise) {
|
|
nsIGlobalObject* go = xpc::CurrentNativeGlobal(aCx);
|
|
if (NS_WARN_IF(!go)) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
mozilla::ErrorResult result;
|
|
*aPromise = mozilla::dom::Promise::Create(go, result);
|
|
if (NS_WARN_IF(result.Failed())) {
|
|
return result.StealNSResult();
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCString GenerateRequestToken() {
|
|
nsID id = nsID::GenerateUUID();
|
|
return nsCString(id.ToString().get());
|
|
}
|
|
|
|
static nsresult GetFileDisplayName(const nsString& aFilePath,
|
|
nsString& aFileDisplayName) {
|
|
nsresult rv;
|
|
nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1", &rv);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = file->InitWithPath(aFilePath);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return file->GetDisplayName(aFileDisplayName);
|
|
}
|
|
|
|
nsIContentAnalysisAcknowledgement::FinalAction ConvertResult(
|
|
nsIContentAnalysisResponse::Action aResponseResult) {
|
|
switch (aResponseResult) {
|
|
case nsIContentAnalysisResponse::Action::eReportOnly:
|
|
return nsIContentAnalysisAcknowledgement::FinalAction::eReportOnly;
|
|
case nsIContentAnalysisResponse::Action::eWarn:
|
|
return nsIContentAnalysisAcknowledgement::FinalAction::eWarn;
|
|
case nsIContentAnalysisResponse::Action::eBlock:
|
|
return nsIContentAnalysisAcknowledgement::FinalAction::eBlock;
|
|
case nsIContentAnalysisResponse::Action::eAllow:
|
|
return nsIContentAnalysisAcknowledgement::FinalAction::eAllow;
|
|
case nsIContentAnalysisResponse::Action::eUnspecified:
|
|
return nsIContentAnalysisAcknowledgement::FinalAction::eUnspecified;
|
|
default:
|
|
LOGE(
|
|
"ConvertResult got unexpected responseResult "
|
|
"%d",
|
|
static_cast<uint32_t>(aResponseResult));
|
|
return nsIContentAnalysisAcknowledgement::FinalAction::eUnspecified;
|
|
}
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
namespace mozilla::contentanalysis {
|
|
ContentAnalysisRequest::~ContentAnalysisRequest() {
|
|
#ifdef XP_WIN
|
|
CloseHandle(mPrintDataHandle);
|
|
#endif
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetAnalysisType(AnalysisType* aAnalysisType) {
|
|
*aAnalysisType = mAnalysisType;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetTextContent(nsAString& aTextContent) {
|
|
aTextContent = mTextContent;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetFilePath(nsAString& aFilePath) {
|
|
aFilePath = mFilePath;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetPrintDataHandle(uint64_t* aPrintDataHandle) {
|
|
#ifdef XP_WIN
|
|
uintptr_t printDataHandle = reinterpret_cast<uintptr_t>(mPrintDataHandle);
|
|
uint64_t printDataValue = static_cast<uint64_t>(printDataHandle);
|
|
*aPrintDataHandle = printDataValue;
|
|
return NS_OK;
|
|
#else
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
#endif
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetPrinterName(nsAString& aPrinterName) {
|
|
aPrinterName = mPrinterName;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetPrintDataSize(uint64_t* aPrintDataSize) {
|
|
#ifdef XP_WIN
|
|
*aPrintDataSize = mPrintDataSize;
|
|
return NS_OK;
|
|
#else
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
#endif
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetUrl(nsIURI** aUrl) {
|
|
NS_ENSURE_ARG_POINTER(aUrl);
|
|
NS_IF_ADDREF(*aUrl = mUrl);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetEmail(nsAString& aEmail) {
|
|
aEmail = mEmail;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetSha256Digest(nsACString& aSha256Digest) {
|
|
aSha256Digest = mSha256Digest;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetResources(
|
|
nsTArray<RefPtr<nsIClientDownloadResource>>& aResources) {
|
|
aResources = mResources.Clone();
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetRequestToken(nsACString& aRequestToken) {
|
|
aRequestToken = mRequestToken;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetOperationTypeForDisplay(
|
|
OperationType* aOperationType) {
|
|
*aOperationType = mOperationTypeForDisplay;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetOperationDisplayString(
|
|
nsAString& aOperationDisplayString) {
|
|
aOperationDisplayString = mOperationDisplayString;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisRequest::GetWindowGlobalParent(
|
|
dom::WindowGlobalParent** aWindowGlobalParent) {
|
|
NS_IF_ADDREF(*aWindowGlobalParent = mWindowGlobalParent);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult ContentAnalysis::CreateContentAnalysisClient(
|
|
nsCString&& aPipePathName, nsString&& aClientSignatureSetting,
|
|
bool aIsPerUser) {
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
// This method should only be called once
|
|
MOZ_ASSERT(!mCaClientPromise->IsResolved());
|
|
|
|
std::shared_ptr<content_analysis::sdk::Client> client(
|
|
content_analysis::sdk::Client::Create({aPipePathName.Data(), aIsPerUser})
|
|
.release());
|
|
LOGD("Content analysis is %s", client ? "connected" : "not available");
|
|
#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.Data());
|
|
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.");
|
|
mCaClientPromise->Reject(NS_ERROR_INVALID_SIGNATURE, __func__);
|
|
return NS_OK;
|
|
}
|
|
}
|
|
#endif // XP_WIN
|
|
mCaClientPromise->Resolve(client, __func__);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
ContentAnalysisRequest::ContentAnalysisRequest(
|
|
AnalysisType aAnalysisType, nsString aString, bool aStringIsFilePath,
|
|
nsCString aSha256Digest, nsCOMPtr<nsIURI> aUrl,
|
|
OperationType aOperationType, dom::WindowGlobalParent* aWindowGlobalParent)
|
|
: mAnalysisType(aAnalysisType),
|
|
mUrl(std::move(aUrl)),
|
|
mSha256Digest(std::move(aSha256Digest)),
|
|
mWindowGlobalParent(aWindowGlobalParent) {
|
|
MOZ_ASSERT(aAnalysisType != AnalysisType::ePrint,
|
|
"Print should use other ContentAnalysisRequest constructor!");
|
|
if (aStringIsFilePath) {
|
|
mFilePath = std::move(aString);
|
|
} else {
|
|
mTextContent = std::move(aString);
|
|
}
|
|
mOperationTypeForDisplay = aOperationType;
|
|
if (mOperationTypeForDisplay == OperationType::eCustomDisplayString) {
|
|
MOZ_ASSERT(aStringIsFilePath);
|
|
nsresult rv = GetFileDisplayName(mFilePath, mOperationDisplayString);
|
|
if (NS_FAILED(rv)) {
|
|
mOperationDisplayString = u"file";
|
|
}
|
|
}
|
|
|
|
mRequestToken = GenerateRequestToken();
|
|
}
|
|
|
|
ContentAnalysisRequest::ContentAnalysisRequest(
|
|
const nsTArray<uint8_t> aPrintData, nsCOMPtr<nsIURI> aUrl,
|
|
nsString aPrinterName, dom::WindowGlobalParent* aWindowGlobalParent)
|
|
: mAnalysisType(AnalysisType::ePrint),
|
|
mUrl(std::move(aUrl)),
|
|
mPrinterName(std::move(aPrinterName)),
|
|
mWindowGlobalParent(aWindowGlobalParent) {
|
|
#ifdef XP_WIN
|
|
LARGE_INTEGER dataContentLength;
|
|
dataContentLength.QuadPart = static_cast<LONGLONG>(aPrintData.Length());
|
|
mPrintDataHandle = ::CreateFileMappingW(
|
|
INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, dataContentLength.HighPart,
|
|
dataContentLength.LowPart, nullptr);
|
|
if (mPrintDataHandle) {
|
|
mozilla::nt::AutoMappedView view(mPrintDataHandle, FILE_MAP_ALL_ACCESS);
|
|
memcpy(view.as<uint8_t>(), aPrintData.Elements(), aPrintData.Length());
|
|
mPrintDataSize = aPrintData.Length();
|
|
}
|
|
#else
|
|
MOZ_ASSERT_UNREACHABLE(
|
|
"Content Analysis is not supported on non-Windows platforms");
|
|
#endif
|
|
mOperationTypeForDisplay = OperationType::eOperationPrint;
|
|
mRequestToken = GenerateRequestToken();
|
|
}
|
|
|
|
nsresult ContentAnalysisRequest::GetFileDigest(const nsAString& aFilePath,
|
|
nsCString& aDigestString) {
|
|
MOZ_DIAGNOSTIC_ASSERT(
|
|
!NS_IsMainThread(),
|
|
"ContentAnalysisRequest::GetFileDigest does file IO and should "
|
|
"not run on the main thread");
|
|
nsresult rv;
|
|
mozilla::Digest digest;
|
|
digest.Begin(SEC_OID_SHA256);
|
|
PRFileDesc* fd = nullptr;
|
|
nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1", &rv);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = file->InitWithPath(aFilePath);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = file->OpenNSPRFileDesc(PR_RDONLY | nsIFile::OS_READAHEAD, 0, &fd);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
auto closeFile = MakeScopeExit([fd]() { PR_Close(fd); });
|
|
constexpr uint32_t kBufferSize = 1024 * 1024;
|
|
auto buffer = mozilla::MakeUnique<uint8_t[]>(kBufferSize);
|
|
if (!buffer) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
PRInt32 bytesRead = PR_Read(fd, buffer.get(), kBufferSize);
|
|
while (bytesRead != 0) {
|
|
if (bytesRead == -1) {
|
|
return NS_ErrorAccordingToNSPR();
|
|
}
|
|
digest.Update(mozilla::Span<const uint8_t>(buffer.get(), bytesRead));
|
|
bytesRead = PR_Read(fd, buffer.get(), kBufferSize);
|
|
}
|
|
nsTArray<uint8_t> digestResults;
|
|
rv = digest.End(digestResults);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
aDigestString = mozilla::ToHexString(digestResults);
|
|
return NS_OK;
|
|
}
|
|
|
|
// Generate an ID that will be shared by all DLP requests.
|
|
// Used to cancel all requests on Firefox shutdown.
|
|
void ContentAnalysis::GenerateUserActionId() {
|
|
nsID id = nsID::GenerateUUID();
|
|
mUserActionId = nsPrintfCString("Firefox %s", id.ToString().get());
|
|
}
|
|
|
|
nsCString ContentAnalysis::GetUserActionId() { return mUserActionId; }
|
|
|
|
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;
|
|
}
|
|
|
|
static nsresult ConvertToProtobuf(
|
|
nsIContentAnalysisRequest* aIn, nsCString&& aUserActionId,
|
|
int64_t aRequestCount,
|
|
content_analysis::sdk::ContentAnalysisRequest* aOut) {
|
|
uint32_t timeout = StaticPrefs::browser_contentanalysis_agent_timeout();
|
|
aOut->set_expires_at(time(nullptr) + timeout);
|
|
|
|
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);
|
|
|
|
nsCString requestToken;
|
|
rv = aIn->GetRequestToken(requestToken);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
aOut->set_request_token(requestToken.get(), requestToken.Length());
|
|
|
|
aOut->set_user_action_id(aUserActionId.get());
|
|
aOut->set_user_action_requests_count(aRequestCount);
|
|
|
|
const std::string tag = "dlp"; // TODO:
|
|
*aOut->add_tags() = tag;
|
|
|
|
auto* requestData = aOut->mutable_request_data();
|
|
|
|
nsCOMPtr<nsIURI> url;
|
|
rv = aIn->GetUrl(getter_AddRefs(url));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
nsCString urlString;
|
|
rv = url->GetSpec(urlString);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!urlString.IsEmpty()) {
|
|
requestData->set_url(urlString.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<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<std::string, std::decay_t<T>>::value,
|
|
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<std::string, std::decay_t<T>>::value,
|
|
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(PBUF, NAME, FUNC) \
|
|
ss << " " << (NAME) << ": "; \
|
|
if ((PBUF)->has_##FUNC()) { \
|
|
LogWithMaxLength(ss, (PBUF)->FUNC(), 500); \
|
|
ss << "\n"; \
|
|
} else \
|
|
ss << "<none>" \
|
|
<< "\n";
|
|
|
|
#define ADD_EXISTS(PBUF, NAME, FUNC) \
|
|
ss << " " << (NAME) << ": " \
|
|
<< ((PBUF)->has_##FUNC() ? "<exists>" : "<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, "File Path", file_path);
|
|
ADD_FIELD(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);
|
|
ADD_FIELD(requestData, " SHA-256 Digest", digest);
|
|
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());
|
|
}
|
|
|
|
ContentAnalysisResponse::ContentAnalysisResponse(
|
|
content_analysis::sdk::ContentAnalysisResponse&& aResponse) {
|
|
mAction = Action::eUnspecified;
|
|
for (const auto& result : aResponse.results()) {
|
|
if (!result.has_status() ||
|
|
result.status() !=
|
|
content_analysis::sdk::ContentAnalysisResponse::Result::SUCCESS) {
|
|
mAction = Action::eUnspecified;
|
|
return;
|
|
}
|
|
// The action values increase with severity, so the max is the most severe.
|
|
for (const auto& rule : result.triggered_rules()) {
|
|
mAction =
|
|
static_cast<Action>(std::max(static_cast<uint32_t>(mAction),
|
|
static_cast<uint32_t>(rule.action())));
|
|
}
|
|
}
|
|
|
|
// If no rules blocked then we should allow.
|
|
if (mAction == Action::eUnspecified) {
|
|
mAction = Action::eAllow;
|
|
}
|
|
|
|
const auto& requestToken = aResponse.request_token();
|
|
mRequestToken.Assign(requestToken.data(), requestToken.size());
|
|
}
|
|
|
|
ContentAnalysisResponse::ContentAnalysisResponse(
|
|
Action aAction, const nsACString& aRequestToken)
|
|
: mAction(aAction), mRequestToken(aRequestToken) {}
|
|
|
|
/* static */
|
|
already_AddRefed<ContentAnalysisResponse> ContentAnalysisResponse::FromProtobuf(
|
|
content_analysis::sdk::ContentAnalysisResponse&& aResponse) {
|
|
auto ret = RefPtr<ContentAnalysisResponse>(
|
|
new ContentAnalysisResponse(std::move(aResponse)));
|
|
|
|
if (ret->mAction == Action::eUnspecified) {
|
|
return nullptr;
|
|
}
|
|
|
|
return ret.forget();
|
|
}
|
|
|
|
/* static */
|
|
RefPtr<ContentAnalysisResponse> ContentAnalysisResponse::FromAction(
|
|
Action aAction, const nsACString& aRequestToken) {
|
|
if (aAction == Action::eUnspecified) {
|
|
return nullptr;
|
|
}
|
|
return RefPtr<ContentAnalysisResponse>(
|
|
new ContentAnalysisResponse(aAction, aRequestToken));
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisResponse::GetRequestToken(nsACString& aRequestToken) {
|
|
aRequestToken = mRequestToken;
|
|
return NS_OK;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisResponse::GetAction(Action* aAction) {
|
|
*aAction = mAction;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisResponse::GetCancelError(CancelError* aCancelError) {
|
|
*aCancelError = mCancelError;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisResponse::GetIsCachedResponse(bool* aIsCachedResponse) {
|
|
*aIsCachedResponse = mIsCachedResponse;
|
|
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, "Status", status);
|
|
ADD_FIELD(aPbAck, "Final Action", final_action);
|
|
|
|
#undef ADD_FIELD
|
|
|
|
LOGD("%s", ss.str().c_str());
|
|
}
|
|
|
|
void ContentAnalysisResponse::SetOwner(RefPtr<ContentAnalysis> aOwner) {
|
|
mOwner = std::move(aOwner);
|
|
}
|
|
|
|
void ContentAnalysisResponse::SetCancelError(CancelError aCancelError) {
|
|
mCancelError = aCancelError;
|
|
}
|
|
|
|
void ContentAnalysisResponse::ResolveWarnAction(bool aAllowContent) {
|
|
MOZ_ASSERT(mAction == Action::eWarn);
|
|
mAction = aAllowContent ? Action::eAllow : Action::eBlock;
|
|
}
|
|
|
|
ContentAnalysisAcknowledgement::ContentAnalysisAcknowledgement(
|
|
Result aResult, FinalAction aFinalAction)
|
|
: mResult(aResult), mFinalAction(aFinalAction) {}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisAcknowledgement::GetResult(Result* aResult) {
|
|
*aResult = mResult;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisAcknowledgement::GetFinalAction(FinalAction* aFinalAction) {
|
|
*aFinalAction = mFinalAction;
|
|
return NS_OK;
|
|
}
|
|
|
|
namespace {
|
|
static bool ShouldAllowAction(
|
|
nsIContentAnalysisResponse::Action aResponseCode) {
|
|
return aResponseCode == nsIContentAnalysisResponse::Action::eAllow ||
|
|
aResponseCode == nsIContentAnalysisResponse::Action::eReportOnly ||
|
|
aResponseCode == nsIContentAnalysisResponse::Action::eWarn;
|
|
}
|
|
|
|
static DefaultResult GetDefaultResultFromPref() {
|
|
uint32_t value = StaticPrefs::browser_contentanalysis_default_result();
|
|
if (value > static_cast<uint32_t>(DefaultResult::eLastValue)) {
|
|
LOGE(
|
|
"Invalid value for browser.contentanalysis.default_result pref "
|
|
"value");
|
|
return DefaultResult::eBlock;
|
|
}
|
|
return static_cast<DefaultResult>(value);
|
|
}
|
|
} // namespace
|
|
|
|
NS_IMETHODIMP ContentAnalysisResponse::GetShouldAllowContent(
|
|
bool* aShouldAllowContent) {
|
|
*aShouldAllowContent = ShouldAllowAction(mAction);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP ContentAnalysisResult::GetShouldAllowContent(
|
|
bool* aShouldAllowContent) {
|
|
if (mValue.is<NoContentAnalysisResult>()) {
|
|
NoContentAnalysisResult result = mValue.as<NoContentAnalysisResult>();
|
|
if (GetDefaultResultFromPref() == DefaultResult::eAllow) {
|
|
*aShouldAllowContent =
|
|
result != NoContentAnalysisResult::DENY_DUE_TO_CANCELED;
|
|
} else {
|
|
// Note that we allow content if we're unable to get it (for example, if
|
|
// there's clipboard content that is not text or file)
|
|
*aShouldAllowContent =
|
|
result == NoContentAnalysisResult::
|
|
ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE ||
|
|
result == NoContentAnalysisResult::
|
|
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS ||
|
|
result == NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE ||
|
|
result == NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA;
|
|
}
|
|
} else {
|
|
*aShouldAllowContent =
|
|
ShouldAllowAction(mValue.as<nsIContentAnalysisResponse::Action>());
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
void ContentAnalysis::EnsureParsedUrlFilters() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (mParsedUrlLists) {
|
|
return;
|
|
}
|
|
|
|
mParsedUrlLists = true;
|
|
nsAutoCString allowList;
|
|
MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kAllowUrlPref, allowList));
|
|
for (const nsACString& regexSubstr : allowList.Split(u' ')) {
|
|
if (!regexSubstr.IsEmpty()) {
|
|
auto flatStr = PromiseFlatCString(regexSubstr);
|
|
const char* regex = flatStr.get();
|
|
LOGD("CA will allow URLs that match %s", regex);
|
|
mAllowUrlList.push_back(std::regex(regex));
|
|
}
|
|
}
|
|
|
|
nsAutoCString denyList;
|
|
MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kDenyUrlPref, denyList));
|
|
for (const nsACString& regexSubstr : denyList.Split(u' ')) {
|
|
if (!regexSubstr.IsEmpty()) {
|
|
auto flatStr = PromiseFlatCString(regexSubstr);
|
|
const char* regex = flatStr.get();
|
|
LOGD("CA will block URLs that match %s", regex);
|
|
mDenyUrlList.push_back(std::regex(regex));
|
|
}
|
|
}
|
|
}
|
|
|
|
ContentAnalysis::UrlFilterResult ContentAnalysis::FilterByUrlLists(
|
|
nsIContentAnalysisRequest* aRequest) {
|
|
EnsureParsedUrlFilters();
|
|
|
|
nsCOMPtr<nsIURI> nsiUrl;
|
|
MOZ_ALWAYS_SUCCEEDS(aRequest->GetUrl(getter_AddRefs(nsiUrl)));
|
|
nsCString urlString;
|
|
nsresult rv = nsiUrl->GetSpec(urlString);
|
|
NS_ENSURE_SUCCESS(rv, UrlFilterResult::eDeny);
|
|
MOZ_ASSERT(!urlString.IsEmpty());
|
|
std::string url = urlString.BeginReading();
|
|
size_t count = 0;
|
|
for (const auto& denyFilter : mDenyUrlList) {
|
|
if (std::regex_match(url, denyFilter)) {
|
|
LOGD("Denying CA request : Deny filter %zu matched url %s", count,
|
|
url.c_str());
|
|
return UrlFilterResult::eDeny;
|
|
}
|
|
++count;
|
|
}
|
|
|
|
count = 0;
|
|
UrlFilterResult result = UrlFilterResult::eCheck;
|
|
for (const auto& allowFilter : mAllowUrlList) {
|
|
if (std::regex_match(url, allowFilter)) {
|
|
LOGD("CA request : Allow filter %zu matched %s", count, url.c_str());
|
|
result = UrlFilterResult::eAllow;
|
|
break;
|
|
}
|
|
++count;
|
|
}
|
|
|
|
// The rest only applies to download resources.
|
|
nsIContentAnalysisRequest::AnalysisType analysisType;
|
|
MOZ_ALWAYS_SUCCEEDS(aRequest->GetAnalysisType(&analysisType));
|
|
if (analysisType != ContentAnalysisRequest::AnalysisType::eFileDownloaded) {
|
|
MOZ_ASSERT(result == UrlFilterResult::eCheck ||
|
|
result == UrlFilterResult::eAllow);
|
|
LOGD("CA request filter result: %s",
|
|
result == UrlFilterResult::eCheck ? "check" : "allow");
|
|
return result;
|
|
}
|
|
|
|
nsTArray<RefPtr<nsIClientDownloadResource>> resources;
|
|
MOZ_ALWAYS_SUCCEEDS(aRequest->GetResources(resources));
|
|
for (size_t resourceIdx = 0; resourceIdx < resources.Length();
|
|
/* noop */) {
|
|
auto& resource = resources[resourceIdx];
|
|
nsAutoString nsUrl;
|
|
MOZ_ALWAYS_SUCCEEDS(resource->GetUrl(nsUrl));
|
|
std::string url = NS_ConvertUTF16toUTF8(nsUrl).get();
|
|
count = 0;
|
|
for (auto& denyFilter : mDenyUrlList) {
|
|
if (std::regex_match(url, denyFilter)) {
|
|
LOGD(
|
|
"Denying CA request : Deny filter %zu matched download resource "
|
|
"at url %s",
|
|
count, url.c_str());
|
|
return UrlFilterResult::eDeny;
|
|
}
|
|
++count;
|
|
}
|
|
|
|
count = 0;
|
|
bool removed = false;
|
|
for (auto& allowFilter : mAllowUrlList) {
|
|
if (std::regex_match(url, allowFilter)) {
|
|
LOGD(
|
|
"CA request : Allow filter %zu matched download resource "
|
|
"at url %s",
|
|
count, url.c_str());
|
|
resources.RemoveElementAt(resourceIdx);
|
|
removed = true;
|
|
break;
|
|
}
|
|
++count;
|
|
}
|
|
if (!removed) {
|
|
++resourceIdx;
|
|
}
|
|
}
|
|
|
|
// Check unless all were allowed.
|
|
return resources.Length() ? UrlFilterResult::eCheck : UrlFilterResult::eAllow;
|
|
}
|
|
|
|
NS_IMPL_CLASSINFO(ContentAnalysisRequest, nullptr, 0, {0});
|
|
NS_IMPL_ISUPPORTS_CI(ContentAnalysisRequest, nsIContentAnalysisRequest);
|
|
NS_IMPL_CLASSINFO(ContentAnalysisResponse, nullptr, 0, {0});
|
|
NS_IMPL_ISUPPORTS_CI(ContentAnalysisResponse, nsIContentAnalysisResponse);
|
|
NS_IMPL_ISUPPORTS(ContentAnalysisAcknowledgement,
|
|
nsIContentAnalysisAcknowledgement);
|
|
NS_IMPL_ISUPPORTS(ContentAnalysisCallback, nsIContentAnalysisCallback);
|
|
NS_IMPL_ISUPPORTS(ContentAnalysisResult, nsIContentAnalysisResult);
|
|
NS_IMPL_ISUPPORTS(ContentAnalysisDiagnosticInfo,
|
|
nsIContentAnalysisDiagnosticInfo);
|
|
NS_IMPL_ISUPPORTS(ContentAnalysis, nsIContentAnalysis, ContentAnalysis);
|
|
|
|
ContentAnalysis::ContentAnalysis()
|
|
: mCaClientPromise(
|
|
new ClientPromise::Private("ContentAnalysis::ContentAnalysis")),
|
|
mClientCreationAttempted(false),
|
|
mSetByEnterprise(false),
|
|
mCallbackMap("ContentAnalysis::mCallbackMap"),
|
|
mWarnResponseDataMap("ContentAnalysis::mWarnResponseDataMap") {
|
|
GenerateUserActionId();
|
|
}
|
|
|
|
ContentAnalysis::~ContentAnalysis() {
|
|
// Accessing mClientCreationAttempted so need to be on the main thread
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (!mClientCreationAttempted) {
|
|
// Reject the promise to avoid assertions when it gets destroyed
|
|
mCaClientPromise->Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::GetIsActive(bool* aIsActive) {
|
|
*aIsActive = false;
|
|
if (!StaticPrefs::browser_contentanalysis_enabled()) {
|
|
LOGD("Local DLP Content Analysis is not active");
|
|
return NS_OK;
|
|
}
|
|
// Accessing mClientCreationAttempted, mSetByEnterprise and non-static prefs
|
|
// so need to be on the main thread
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
// gAllowContentAnalysisArgPresent is only set in the parent process
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
if (!gAllowContentAnalysisArgPresent && !mSetByEnterprise) {
|
|
LOGE(
|
|
"The content analysis pref is enabled but not by an enterprise "
|
|
"policy and -allow-content-analysis was not present on the "
|
|
"command-line. Content Analysis will not be active.");
|
|
return NS_OK;
|
|
}
|
|
|
|
*aIsActive = true;
|
|
LOGD("Local DLP Content Analysis is active");
|
|
// On the main thread so no need for synchronization here.
|
|
if (!mClientCreationAttempted) {
|
|
mClientCreationAttempted = true;
|
|
LOGD("Dispatching background task to create Content Analysis client");
|
|
|
|
nsCString pipePathName;
|
|
nsresult rv = Preferences::GetCString(kPipePathNamePref, pipePathName);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
mCaClientPromise->Reject(rv, __func__);
|
|
return rv;
|
|
}
|
|
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);
|
|
rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction(
|
|
"ContentAnalysis::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))) {
|
|
mCaClientPromise->Reject(rv, __func__);
|
|
return rv;
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::GetMightBeActive(bool* aMightBeActive) {
|
|
// A DLP connection is not permitted to be added/removed while the
|
|
// browser is running, so we can cache this.
|
|
static bool sIsEnabled = StaticPrefs::browser_contentanalysis_enabled();
|
|
// Note that we can't check gAllowContentAnalysis here because it
|
|
// only gets set in the parent process.
|
|
*aMightBeActive = sIsEnabled;
|
|
return NS_OK;
|
|
}
|
|
|
|
/* static */ bool ContentAnalysis::MightBeActive() {
|
|
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
|
|
mozilla::components::nsIContentAnalysis::Service();
|
|
NS_ENSURE_TRUE(contentAnalysis, false);
|
|
|
|
bool maybeActive = false;
|
|
return NS_SUCCEEDED(contentAnalysis->GetMightBeActive(&maybeActive)) &&
|
|
maybeActive;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::GetIsSetByEnterprisePolicy(bool* aSetByEnterprise) {
|
|
*aSetByEnterprise = mSetByEnterprise;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::SetIsSetByEnterprisePolicy(bool aSetByEnterprise) {
|
|
mSetByEnterprise = aSetByEnterprise;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::TestOnlySetCACmdLineArg(bool aVal) {
|
|
#ifdef ENABLE_TESTS
|
|
gAllowContentAnalysisArgPresent = aVal;
|
|
return NS_OK;
|
|
#else
|
|
LOGE("ContentAnalysis::TestOnlySetCACmdLineArg is test-only");
|
|
return NS_ERROR_UNEXPECTED;
|
|
#endif
|
|
}
|
|
|
|
nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken,
|
|
nsresult aResult) {
|
|
return NS_DispatchToMainThread(NS_NewCancelableRunnableFunction(
|
|
"ContentAnalysis::CancelWithError",
|
|
[aResult, aRequestToken = std::move(aRequestToken)]() mutable {
|
|
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
owner->SetLastResult(aResult);
|
|
nsCOMPtr<nsIObserverService> obsServ =
|
|
mozilla::services::GetObserverService();
|
|
nsIContentAnalysisResponse::Action action =
|
|
nsIContentAnalysisResponse::Action::eCanceled;
|
|
// If we're shutting down, ignore the default result and just leave the
|
|
// action as canceled. This fixes a hang if the default result is warn
|
|
// and we shutdown during a request (bug 1912245)
|
|
if (aResult != NS_ERROR_ILLEGAL_DURING_SHUTDOWN) {
|
|
DefaultResult defaultResponse = GetDefaultResultFromPref();
|
|
switch (defaultResponse) {
|
|
case DefaultResult::eAllow:
|
|
action = nsIContentAnalysisResponse::Action::eAllow;
|
|
break;
|
|
case DefaultResult::eWarn:
|
|
action = nsIContentAnalysisResponse::Action::eWarn;
|
|
break;
|
|
case DefaultResult::eBlock:
|
|
action = nsIContentAnalysisResponse::Action::eCanceled;
|
|
break;
|
|
default:
|
|
MOZ_ASSERT(false);
|
|
action = nsIContentAnalysisResponse::Action::eCanceled;
|
|
}
|
|
}
|
|
RefPtr<ContentAnalysisResponse> response =
|
|
ContentAnalysisResponse::FromAction(action, aRequestToken);
|
|
response->SetOwner(owner);
|
|
nsIContentAnalysisResponse::CancelError cancelError;
|
|
switch (aResult) {
|
|
case NS_ERROR_NOT_AVAILABLE:
|
|
cancelError = nsIContentAnalysisResponse::CancelError::eNoAgent;
|
|
break;
|
|
case NS_ERROR_INVALID_SIGNATURE:
|
|
cancelError =
|
|
nsIContentAnalysisResponse::CancelError::eInvalidAgentSignature;
|
|
break;
|
|
default:
|
|
cancelError = nsIContentAnalysisResponse::CancelError::eErrorOther;
|
|
break;
|
|
}
|
|
response->SetCancelError(cancelError);
|
|
Maybe<CallbackData> maybeCallbackData;
|
|
{
|
|
auto lock = owner->mCallbackMap.Lock();
|
|
maybeCallbackData = lock->Extract(aRequestToken);
|
|
if (maybeCallbackData.isNothing()) {
|
|
LOGD("Content analysis did not find callback for token %s",
|
|
aRequestToken.get());
|
|
return;
|
|
}
|
|
}
|
|
if (action == nsIContentAnalysisResponse::Action::eWarn) {
|
|
owner->SendWarnResponse(std::move(aRequestToken),
|
|
std::move(*maybeCallbackData), response);
|
|
return;
|
|
}
|
|
nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder =
|
|
maybeCallbackData->TakeCallbackHolder();
|
|
obsServ->NotifyObservers(response, "dlp-response", nullptr);
|
|
if (callbackHolder) {
|
|
if (action == nsIContentAnalysisResponse::Action::eCanceled) {
|
|
callbackHolder->Error(aResult);
|
|
} else {
|
|
callbackHolder->ContentResult(response);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
RefPtr<ContentAnalysis> ContentAnalysis::GetContentAnalysisFromService() {
|
|
RefPtr<ContentAnalysis> contentAnalysisService =
|
|
mozilla::components::nsIContentAnalysis::Service();
|
|
if (!contentAnalysisService) {
|
|
// May be shutting down
|
|
return nullptr;
|
|
}
|
|
|
|
return contentAnalysisService;
|
|
}
|
|
|
|
nsresult ContentAnalysis::RunAnalyzeRequestTask(
|
|
const RefPtr<nsIContentAnalysisRequest>& aRequest, bool aAutoAcknowledge,
|
|
int64_t aRequestCount,
|
|
const RefPtr<nsIContentAnalysisCallback>& aCallback) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
nsresult rv = NS_ERROR_FAILURE;
|
|
auto callbackCopy = aCallback;
|
|
auto se = MakeScopeExit([&] {
|
|
if (!NS_SUCCEEDED(rv)) {
|
|
LOGE("RunAnalyzeRequestTask failed");
|
|
callbackCopy->Error(rv);
|
|
}
|
|
});
|
|
|
|
nsCString requestToken;
|
|
rv = aRequest->GetRequestToken(requestToken);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolderCopy(
|
|
new nsMainThreadPtrHolder<nsIContentAnalysisCallback>(
|
|
"content analysis callback", aCallback));
|
|
CallbackData callbackData(std::move(callbackHolderCopy), aAutoAcknowledge);
|
|
{
|
|
auto lock = mCallbackMap.Lock();
|
|
lock->InsertOrUpdate(requestToken, std::move(callbackData));
|
|
}
|
|
|
|
// Check URLs of requested info against
|
|
// browser.contentanalysis.allow_url_regex_list/deny_url_regex_list.
|
|
// Build the list once since creating regexs is slow.
|
|
// URLs that match the allow list are removed from the check. There is
|
|
// only one URL in all cases except downloads. If all contents are removed
|
|
// or the page URL is allowed (for downloads) then the operation is allowed.
|
|
// URLs that match the deny list block the entire operation.
|
|
// If the request is completely covered by this filter then flag it as
|
|
// not needing to send an Acknowledge.
|
|
auto filterResult = FilterByUrlLists(aRequest);
|
|
if (filterResult == ContentAnalysis::UrlFilterResult::eDeny) {
|
|
LOGD("Blocking request due to deny URL filter.");
|
|
auto response = ContentAnalysisResponse::FromAction(
|
|
nsIContentAnalysisResponse::Action::eBlock, requestToken);
|
|
response->DoNotAcknowledge();
|
|
IssueResponse(response);
|
|
return NS_OK;
|
|
}
|
|
if (filterResult == ContentAnalysis::UrlFilterResult::eAllow) {
|
|
LOGD("Allowing request -- all operations match allow URL filter.");
|
|
auto response = ContentAnalysisResponse::FromAction(
|
|
nsIContentAnalysisResponse::Action::eAllow, requestToken);
|
|
response->DoNotAcknowledge();
|
|
IssueResponse(response);
|
|
return NS_OK;
|
|
}
|
|
|
|
content_analysis::sdk::ContentAnalysisRequest pbRequest;
|
|
rv =
|
|
ConvertToProtobuf(aRequest, GetUserActionId(), aRequestCount, &pbRequest);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// This is a very simple cache to avoid the case of making multiple
|
|
// consecutive DLP requests to the agent for the same text data. This has
|
|
// been an issue on Google Docs and OneDrive (bug 1912384)
|
|
nsCOMPtr<nsIContentAnalysisRequest> requestToCache;
|
|
CachedData::CacheResult cacheMatchResult =
|
|
mCachedData.CompareWithRequest(aRequest);
|
|
if (cacheMatchResult == CachedData::CacheResult::Matches) {
|
|
auto action = mCachedData.ResultAction();
|
|
MOZ_ASSERT(action.isSome());
|
|
LOGD("Found existing request in cache for token %s with action %d",
|
|
requestToken.get(), *action);
|
|
mCachedData.SetExpirationTimer();
|
|
auto response = ContentAnalysisResponse::FromAction(*action, requestToken);
|
|
response->DoNotAcknowledge();
|
|
response->SetIsCachedResponse();
|
|
IssueResponse(response);
|
|
return NS_OK;
|
|
}
|
|
if (cacheMatchResult != CachedData::CacheResult::CannotBeCached) {
|
|
// We will use the cache
|
|
requestToCache = aRequest;
|
|
}
|
|
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->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;
|
|
obsServ->NotifyObservers(this, "dlp-request-sent-raw",
|
|
requestArray.Elements());
|
|
}
|
|
|
|
mCaClientPromise->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[requestToken, pbRequest = std::move(pbRequest),
|
|
requestToCache = std::move(requestToCache)](
|
|
std::shared_ptr<content_analysis::sdk::Client> client) mutable {
|
|
// The content analysis call is synchronous so run in the background.
|
|
NS_DispatchBackgroundTask(
|
|
NS_NewCancelableRunnableFunction(
|
|
__func__,
|
|
[requestToken, pbRequest = std::move(pbRequest),
|
|
requestToCache = std::move(requestToCache),
|
|
client = std::move(client)]() mutable {
|
|
DoAnalyzeRequest(requestToken, std::move(pbRequest),
|
|
std::move(requestToCache), client);
|
|
}),
|
|
NS_DISPATCH_EVENT_MAY_BLOCK);
|
|
},
|
|
[requestToken](nsresult rv) mutable {
|
|
LOGD("RunAnalyzeRequestTask failed to get client");
|
|
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
owner->CancelWithError(std::move(requestToken), rv);
|
|
});
|
|
|
|
return rv;
|
|
}
|
|
|
|
void ContentAnalysis::DoAnalyzeRequest(
|
|
nsCString aRequestToken,
|
|
content_analysis::sdk::ContentAnalysisRequest&& aRequest,
|
|
nsCOMPtr<nsIContentAnalysisRequest> aRequestToCache,
|
|
const std::shared_ptr<content_analysis::sdk::Client>& aClient) {
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
RefPtr<ContentAnalysis> owner =
|
|
ContentAnalysis::GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
|
|
if (!aClient) {
|
|
owner->CancelWithError(std::move(aRequestToken), NS_ERROR_NOT_AVAILABLE);
|
|
return;
|
|
}
|
|
|
|
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(aRequestToken), rv);
|
|
return;
|
|
}
|
|
if (!digest.IsEmpty()) {
|
|
aRequest.mutable_request_data()->set_digest(digest.get());
|
|
}
|
|
}
|
|
|
|
{
|
|
auto callbackMap = owner->mCallbackMap.Lock();
|
|
if (!callbackMap->Contains(aRequestToken)) {
|
|
LOGD(
|
|
"RunAnalyzeRequestTask token %s has already been "
|
|
"cancelled - not issuing request",
|
|
aRequestToken.get());
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Run request, then dispatch back to main thread to resolve
|
|
// aCallback
|
|
content_analysis::sdk::ContentAnalysisResponse pbResponse;
|
|
int err = aClient->Send(aRequest, &pbResponse);
|
|
if (err != 0) {
|
|
LOGE("RunAnalyzeRequestTask client transaction failed");
|
|
owner->CancelWithError(std::move(aRequestToken), NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
LOGD("Content analysis client transaction succeeded");
|
|
LogResponse(&pbResponse);
|
|
NS_DispatchToMainThread(NS_NewCancelableRunnableFunction(
|
|
"ContentAnalysis::RunAnalyzeRequestTask::HandleResponse",
|
|
[pbResponse = std::move(pbResponse),
|
|
aRequestToCache = std::move(aRequestToCache)]() mutable {
|
|
RefPtr<ContentAnalysis> owner = GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
|
|
RefPtr<ContentAnalysisResponse> response =
|
|
ContentAnalysisResponse::FromProtobuf(std::move(pbResponse));
|
|
if (!response) {
|
|
LOGE("Content analysis got invalid response!");
|
|
return;
|
|
}
|
|
if (aRequestToCache) {
|
|
nsIContentAnalysisResponse::Action action;
|
|
if (NS_SUCCEEDED(response->GetAction(&action))) {
|
|
owner->mCachedData.SetData(std::move(aRequestToCache), action);
|
|
}
|
|
}
|
|
owner->IssueResponse(response);
|
|
}));
|
|
}
|
|
|
|
void ContentAnalysis::SendWarnResponse(
|
|
nsCString&& aResponseRequestToken, CallbackData aCallbackData,
|
|
RefPtr<ContentAnalysisResponse>& aResponse) {
|
|
nsCOMPtr<nsIObserverService> obsServ =
|
|
mozilla::services::GetObserverService();
|
|
{
|
|
auto warnResponseDataMap = mWarnResponseDataMap.Lock();
|
|
warnResponseDataMap->InsertOrUpdate(
|
|
aResponseRequestToken,
|
|
WarnResponseData(std::move(aCallbackData), aResponse));
|
|
}
|
|
obsServ->NotifyObservers(aResponse, "dlp-response", nullptr);
|
|
}
|
|
|
|
void ContentAnalysis::IssueResponse(RefPtr<ContentAnalysisResponse>& response) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
nsCString responseRequestToken;
|
|
nsresult requestRv = response->GetRequestToken(responseRequestToken);
|
|
if (NS_FAILED(requestRv)) {
|
|
LOGE("Content analysis couldn't get request token from response!");
|
|
return;
|
|
}
|
|
// Successfully made a request to the agent, so mark that we succeeded
|
|
mLastResult = NS_OK;
|
|
|
|
Maybe<CallbackData> maybeCallbackData;
|
|
{
|
|
auto callbackMap = mCallbackMap.Lock();
|
|
maybeCallbackData = callbackMap->Extract(responseRequestToken);
|
|
}
|
|
if (maybeCallbackData.isNothing()) {
|
|
LOGD("Content analysis did not find callback for token %s",
|
|
responseRequestToken.get());
|
|
return;
|
|
}
|
|
response->SetOwner(this);
|
|
if (maybeCallbackData->Canceled()) {
|
|
// request has already been cancelled, so there's
|
|
// nothing to do
|
|
LOGD(
|
|
"Content analysis got response but ignoring "
|
|
"because it was already cancelled for token %s",
|
|
responseRequestToken.get());
|
|
// Note that we always acknowledge here, even if
|
|
// autoAcknowledge isn't set, since we raise an exception
|
|
// at the caller on cancellation.
|
|
auto acknowledgement = MakeRefPtr<ContentAnalysisAcknowledgement>(
|
|
nsIContentAnalysisAcknowledgement::Result::eTooLate,
|
|
nsIContentAnalysisAcknowledgement::FinalAction::eBlock);
|
|
response->Acknowledge(acknowledgement);
|
|
return;
|
|
}
|
|
|
|
LOGD("Content analysis resolving response promise for token %s",
|
|
responseRequestToken.get());
|
|
nsIContentAnalysisResponse::Action action;
|
|
DebugOnly<nsresult> rv = response->GetAction(&action);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
nsCOMPtr<nsIObserverService> obsServ =
|
|
mozilla::services::GetObserverService();
|
|
if (action == nsIContentAnalysisResponse::Action::eWarn) {
|
|
SendWarnResponse(std::move(responseRequestToken),
|
|
std::move(*maybeCallbackData), response);
|
|
return;
|
|
}
|
|
|
|
obsServ->NotifyObservers(response, "dlp-response", nullptr);
|
|
if (maybeCallbackData->AutoAcknowledge()) {
|
|
auto acknowledgement = MakeRefPtr<ContentAnalysisAcknowledgement>(
|
|
nsIContentAnalysisAcknowledgement::Result::eSuccess,
|
|
ConvertResult(action));
|
|
response->Acknowledge(acknowledgement);
|
|
}
|
|
|
|
nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder =
|
|
maybeCallbackData->TakeCallbackHolder();
|
|
callbackHolder->ContentResult(response);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::AnalyzeContentRequest(nsIContentAnalysisRequest* aRequest,
|
|
bool aAutoAcknowledge, JSContext* aCx,
|
|
mozilla::dom::Promise** aPromise) {
|
|
RefPtr<mozilla::dom::Promise> promise;
|
|
nsresult rv = MakePromise(aCx, &promise);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
RefPtr<ContentAnalysisCallback> callbackPtr =
|
|
new ContentAnalysisCallback(promise);
|
|
promise.forget(aPromise);
|
|
return AnalyzeContentRequestCallback(aRequest, aAutoAcknowledge,
|
|
callbackPtr.get());
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::AnalyzeContentRequestCallback(
|
|
nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
|
|
nsIContentAnalysisCallback* aCallback) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
NS_ENSURE_ARG(aRequest);
|
|
NS_ENSURE_ARG(aCallback);
|
|
nsresult rv = AnalyzeContentRequestCallbackPrivate(aRequest, aAutoAcknowledge,
|
|
aCallback);
|
|
if (NS_FAILED(rv)) {
|
|
nsCString requestToken;
|
|
nsresult requestTokenRv = aRequest->GetRequestToken(requestToken);
|
|
NS_ENSURE_SUCCESS(requestTokenRv, requestTokenRv);
|
|
CancelWithError(requestToken, rv);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
nsresult ContentAnalysis::AnalyzeContentRequestCallbackPrivate(
|
|
nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
|
|
nsIContentAnalysisCallback* aCallback) {
|
|
// Make sure we send the notification first, so if we later return
|
|
// an error the JS will handle it correctly.
|
|
nsCOMPtr<nsIObserverService> obsServ =
|
|
mozilla::services::GetObserverService();
|
|
obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
|
|
|
|
bool isActive;
|
|
nsresult rv = GetIsActive(&isActive);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!isActive) {
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
// since we're on the main thread, don't need to synchronize this
|
|
int64_t requestCount = ++mRequestCount;
|
|
return RunAnalyzeRequestTask(aRequest, aAutoAcknowledge, requestCount,
|
|
aCallback);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::CancelContentAnalysisRequest(const nsACString& aRequestToken) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
nsCString requestToken(aRequestToken);
|
|
|
|
auto callbackMap = mCallbackMap.Lock();
|
|
auto entry = callbackMap->Lookup(requestToken);
|
|
LOGD("Content analysis cancelling request %s", requestToken.get());
|
|
// Make sure the entry hasn't been cancelled already
|
|
if (entry && !entry->Canceled()) {
|
|
nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder =
|
|
entry->TakeCallbackHolder();
|
|
entry->SetCanceled();
|
|
// Should only be called once
|
|
MOZ_ASSERT(callbackHolder);
|
|
if (callbackHolder) {
|
|
callbackHolder->Error(NS_ERROR_ABORT);
|
|
}
|
|
} else {
|
|
LOGD("Content analysis request not found when trying to cancel %s",
|
|
requestToken.get());
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::CancelAllRequests() {
|
|
LOGD("CancelAllRequests running");
|
|
mCaClientPromise->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[&](std::shared_ptr<content_analysis::sdk::Client> client) {
|
|
auto owner = GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
NS_DispatchToMainThread(NS_NewCancelableRunnableFunction(
|
|
"ContentAnalysis::CancelAllRequests", []() {
|
|
auto owner = GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
{
|
|
auto callbackMap = owner->mCallbackMap.Lock();
|
|
auto keys = callbackMap->Keys();
|
|
for (const auto& key : keys) {
|
|
owner->CancelWithError(nsCString(key),
|
|
NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
|
|
}
|
|
}
|
|
}));
|
|
{
|
|
auto warnResponseDataMap = owner->mWarnResponseDataMap.Lock();
|
|
auto keys = warnResponseDataMap->Keys();
|
|
for (const auto& key : keys) {
|
|
LOGD(
|
|
"Responding to warn dialog (from CancelAllRequests) for "
|
|
"request %s",
|
|
nsCString(key).get());
|
|
owner->RespondToWarnDialog(key, false);
|
|
}
|
|
}
|
|
if (!client) {
|
|
LOGE("CancelAllRequests got a null client");
|
|
return;
|
|
}
|
|
content_analysis::sdk::ContentAnalysisCancelRequests requests;
|
|
requests.set_user_action_id(owner->GetUserActionId().get());
|
|
int err = client->CancelRequests(requests);
|
|
if (err != 0) {
|
|
LOGE("CancelAllRequests got error %d", err);
|
|
} else {
|
|
LOGD("CancelAllRequests did cancelling of requests");
|
|
}
|
|
},
|
|
[&](nsresult rv) { LOGE("CancelAllRequests failed to get the client"); });
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::RespondToWarnDialog(const nsACString& aRequestToken,
|
|
bool aAllowContent) {
|
|
nsCString requestToken(aRequestToken);
|
|
NS_DispatchToMainThread(NS_NewCancelableRunnableFunction(
|
|
"RespondToWarnDialog",
|
|
[aAllowContent, requestToken = std::move(requestToken)]() {
|
|
RefPtr<ContentAnalysis> self = GetContentAnalysisFromService();
|
|
if (!self) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
|
|
LOGD("Content analysis getting warn response %d for request %s",
|
|
aAllowContent ? 1 : 0, requestToken.get());
|
|
Maybe<WarnResponseData> entry;
|
|
{
|
|
auto warnResponseDataMap = self->mWarnResponseDataMap.Lock();
|
|
entry = warnResponseDataMap->Extract(requestToken);
|
|
}
|
|
if (!entry) {
|
|
LOGD(
|
|
"Content analysis request not found when trying to send warn "
|
|
"response for request %s",
|
|
requestToken.get());
|
|
return;
|
|
}
|
|
entry->mResponse->ResolveWarnAction(aAllowContent);
|
|
nsIContentAnalysisResponse::Action action;
|
|
DebugOnly<nsresult> rv = entry->mResponse->GetAction(&action);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
{
|
|
auto request = self->mCachedData.Request();
|
|
if (request) {
|
|
nsCString cachedRequestToken;
|
|
DebugOnly<nsresult> tokenRv =
|
|
request->GetRequestToken(cachedRequestToken);
|
|
MOZ_ASSERT(NS_SUCCEEDED(tokenRv));
|
|
if (cachedRequestToken.Equals(requestToken)) {
|
|
self->mCachedData.UpdateWarnAction(action);
|
|
}
|
|
}
|
|
}
|
|
if (entry->mCallbackData.AutoAcknowledge()) {
|
|
RefPtr<ContentAnalysisAcknowledgement> acknowledgement =
|
|
new ContentAnalysisAcknowledgement(
|
|
nsIContentAnalysisAcknowledgement::Result::eSuccess,
|
|
ConvertResult(action));
|
|
entry->mResponse->Acknowledge(acknowledgement);
|
|
}
|
|
nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder =
|
|
entry->mCallbackData.TakeCallbackHolder();
|
|
if (callbackHolder) {
|
|
RefPtr<ContentAnalysisResponse> response =
|
|
ContentAnalysisResponse::FromAction(action, requestToken);
|
|
response->SetOwner(self);
|
|
callbackHolder.get()->ContentResult(response.get());
|
|
} else {
|
|
LOGD(
|
|
"Content analysis had no callback to send warn final response "
|
|
"to for request %s",
|
|
requestToken.get());
|
|
}
|
|
}));
|
|
return NS_OK;
|
|
}
|
|
|
|
#if defined(XP_WIN)
|
|
RefPtr<ContentAnalysis::PrintAllowedPromise>
|
|
ContentAnalysis::PrintToPDFToDetermineIfPrintAllowed(
|
|
dom::CanonicalBrowsingContext* aBrowsingContext,
|
|
nsIPrintSettings* aPrintSettings) {
|
|
// Note that the IsChrome() check here excludes a few
|
|
// common about pages like about:config, about:preferences,
|
|
// and about:support, but other about: pages may still
|
|
// go through content analysis.
|
|
if (aBrowsingContext->IsChrome()) {
|
|
return PrintAllowedPromise::CreateAndResolve(PrintAllowedResult(true),
|
|
__func__);
|
|
}
|
|
nsCOMPtr<nsIPrintSettings> contentAnalysisPrintSettings;
|
|
if (NS_WARN_IF(NS_FAILED(aPrintSettings->Clone(
|
|
getter_AddRefs(contentAnalysisPrintSettings)))) ||
|
|
NS_WARN_IF(!aBrowsingContext->GetCurrentWindowGlobal())) {
|
|
return PrintAllowedPromise::CreateAndReject(
|
|
PrintAllowedError(NS_ERROR_FAILURE), __func__);
|
|
}
|
|
contentAnalysisPrintSettings->SetOutputDestination(
|
|
nsIPrintSettings::OutputDestinationType::kOutputDestinationStream);
|
|
contentAnalysisPrintSettings->SetOutputFormat(
|
|
nsIPrintSettings::kOutputFormatPDF);
|
|
nsCOMPtr<nsIStorageStream> storageStream =
|
|
do_CreateInstance("@mozilla.org/storagestream;1");
|
|
if (!storageStream) {
|
|
return PrintAllowedPromise::CreateAndReject(
|
|
PrintAllowedError(NS_ERROR_FAILURE), __func__);
|
|
}
|
|
// Use segment size of 512K
|
|
nsresult rv = storageStream->Init(0x80000, UINT32_MAX);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return PrintAllowedPromise::CreateAndReject(PrintAllowedError(rv),
|
|
__func__);
|
|
}
|
|
|
|
nsCOMPtr<nsIOutputStream> outputStream;
|
|
storageStream->QueryInterface(NS_GET_IID(nsIOutputStream),
|
|
getter_AddRefs(outputStream));
|
|
MOZ_ASSERT(outputStream);
|
|
|
|
contentAnalysisPrintSettings->SetOutputStream(outputStream.get());
|
|
RefPtr<dom::CanonicalBrowsingContext> browsingContext = aBrowsingContext;
|
|
auto promise = MakeRefPtr<PrintAllowedPromise::Private>(__func__);
|
|
nsCOMPtr<nsIPrintSettings> finalPrintSettings(aPrintSettings);
|
|
aBrowsingContext
|
|
->PrintWithNoContentAnalysis(contentAnalysisPrintSettings, true, nullptr)
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[browsingContext, contentAnalysisPrintSettings, finalPrintSettings,
|
|
promise](
|
|
dom::MaybeDiscardedBrowsingContext cachedStaticBrowsingContext)
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable {
|
|
nsCOMPtr<nsIOutputStream> outputStream;
|
|
contentAnalysisPrintSettings->GetOutputStream(
|
|
getter_AddRefs(outputStream));
|
|
nsCOMPtr<nsIStorageStream> storageStream =
|
|
do_QueryInterface(outputStream);
|
|
MOZ_ASSERT(storageStream);
|
|
nsTArray<uint8_t> printData;
|
|
uint32_t length = 0;
|
|
storageStream->GetLength(&length);
|
|
if (!printData.SetLength(length, fallible)) {
|
|
promise->Reject(
|
|
PrintAllowedError(NS_ERROR_OUT_OF_MEMORY,
|
|
cachedStaticBrowsingContext),
|
|
__func__);
|
|
return;
|
|
}
|
|
nsCOMPtr<nsIInputStream> inputStream;
|
|
nsresult rv = storageStream->NewInputStream(
|
|
0, getter_AddRefs(inputStream));
|
|
if (NS_FAILED(rv)) {
|
|
promise->Reject(
|
|
PrintAllowedError(rv, cachedStaticBrowsingContext),
|
|
__func__);
|
|
return;
|
|
}
|
|
uint32_t currentPosition = 0;
|
|
while (currentPosition < length) {
|
|
uint32_t elementsRead = 0;
|
|
// Make sure the reinterpret_cast<> below is safe
|
|
static_assert(std::is_trivially_assignable_v<
|
|
decltype(*printData.Elements()), char>);
|
|
rv = inputStream->Read(
|
|
reinterpret_cast<char*>(printData.Elements()) +
|
|
currentPosition,
|
|
length - currentPosition, &elementsRead);
|
|
if (NS_WARN_IF(NS_FAILED(rv) || !elementsRead)) {
|
|
promise->Reject(
|
|
PrintAllowedError(NS_FAILED(rv) ? rv : NS_ERROR_FAILURE,
|
|
cachedStaticBrowsingContext),
|
|
__func__);
|
|
return;
|
|
}
|
|
currentPosition += elementsRead;
|
|
}
|
|
|
|
nsString printerName;
|
|
rv = contentAnalysisPrintSettings->GetPrinterName(printerName);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
promise->Reject(
|
|
PrintAllowedError(rv, cachedStaticBrowsingContext),
|
|
__func__);
|
|
return;
|
|
}
|
|
|
|
auto* windowParent = browsingContext->GetCurrentWindowGlobal();
|
|
if (!windowParent) {
|
|
// The print window may have been closed by the user by now.
|
|
// Cancel the print.
|
|
promise->Reject(
|
|
PrintAllowedError(NS_ERROR_ABORT,
|
|
cachedStaticBrowsingContext),
|
|
__func__);
|
|
return;
|
|
}
|
|
nsCOMPtr<nsIURI> uri = GetURIForBrowsingContext(
|
|
windowParent->Canonical()->GetBrowsingContext());
|
|
if (!uri) {
|
|
promise->Reject(
|
|
PrintAllowedError(NS_ERROR_FAILURE,
|
|
cachedStaticBrowsingContext),
|
|
__func__);
|
|
return;
|
|
}
|
|
nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
|
|
new contentanalysis::ContentAnalysisRequest(
|
|
std::move(printData), std::move(uri),
|
|
std::move(printerName), windowParent);
|
|
auto callback =
|
|
MakeRefPtr<contentanalysis::ContentAnalysisCallback>(
|
|
[browsingContext, cachedStaticBrowsingContext, promise,
|
|
finalPrintSettings = std::move(finalPrintSettings)](
|
|
nsIContentAnalysisResponse* aResponse)
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA mutable {
|
|
bool shouldAllow = false;
|
|
DebugOnly<nsresult> rv =
|
|
aResponse->GetShouldAllowContent(
|
|
&shouldAllow);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
promise->Resolve(
|
|
PrintAllowedResult(
|
|
shouldAllow, cachedStaticBrowsingContext),
|
|
__func__);
|
|
},
|
|
[promise,
|
|
cachedStaticBrowsingContext](nsresult aError) {
|
|
promise->Reject(
|
|
PrintAllowedError(aError,
|
|
cachedStaticBrowsingContext),
|
|
__func__);
|
|
});
|
|
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
|
|
mozilla::components::nsIContentAnalysis::Service();
|
|
if (NS_WARN_IF(!contentAnalysis)) {
|
|
promise->Reject(
|
|
PrintAllowedError(rv, cachedStaticBrowsingContext),
|
|
__func__);
|
|
} else {
|
|
bool isActive = false;
|
|
nsresult rv = contentAnalysis->GetIsActive(&isActive);
|
|
// Should not be called if content analysis is not active
|
|
MOZ_ASSERT(isActive);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
rv = contentAnalysis->AnalyzeContentRequestCallback(
|
|
contentAnalysisRequest, /* aAutoAcknowledge */ true,
|
|
callback);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
promise->Reject(
|
|
PrintAllowedError(rv, cachedStaticBrowsingContext),
|
|
__func__);
|
|
}
|
|
}
|
|
},
|
|
[promise](nsresult aError) {
|
|
promise->Reject(PrintAllowedError(aError), __func__);
|
|
});
|
|
return promise;
|
|
}
|
|
#endif
|
|
|
|
NS_IMPL_ISUPPORTS(ContentAnalysis::SafeContentAnalysisResultCallback,
|
|
nsIContentAnalysisCallback);
|
|
|
|
// - true means a content analysis request was fired
|
|
// - false means there is no text data in the transferable
|
|
// - NoContentAnalysisResult means there was an error
|
|
using ClipboardContentAnalysisResult =
|
|
mozilla::Result<bool, mozilla::contentanalysis::NoContentAnalysisResult>;
|
|
|
|
NS_IMETHODIMP ContentAnalysis::SafeContentAnalysisResultCallback::ContentResult(
|
|
nsIContentAnalysisResponse* aResponse) {
|
|
RefPtr<ContentAnalysisResult> result =
|
|
ContentAnalysisResult::FromContentAnalysisResponse(aResponse);
|
|
Callback(result);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP ContentAnalysis::SafeContentAnalysisResultCallback::Error(
|
|
nsresult aError) {
|
|
Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR));
|
|
return NS_OK;
|
|
}
|
|
|
|
ClipboardContentAnalysisResult AnalyzeText(
|
|
uint64_t aInnerWindowId,
|
|
ContentAnalysis::SafeContentAnalysisResultCallback* aResolver,
|
|
nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis,
|
|
nsString aText) {
|
|
RefPtr<mozilla::dom::WindowGlobalParent> window =
|
|
mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId);
|
|
if (!window) {
|
|
// The window has gone away in the meantime
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
|
|
new ContentAnalysisRequest(
|
|
nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry,
|
|
std::move(aText), false, EmptyCString(), aDocumentURI,
|
|
nsIContentAnalysisRequest::OperationType::eClipboard, window);
|
|
nsresult rv = aContentAnalysis->AnalyzeContentRequestCallback(
|
|
contentAnalysisRequest, /* aAutoAcknowledge */ true, aResolver);
|
|
if (NS_FAILED(rv)) {
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ClipboardContentAnalysisResult CheckClipboardContentAnalysisAsCustomData(
|
|
uint64_t aInnerWindowId,
|
|
ContentAnalysis::SafeContentAnalysisResultCallback* aResolver,
|
|
nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis,
|
|
nsITransferable* aTrans) {
|
|
nsCOMPtr<nsISupports> transferData;
|
|
if (NS_FAILED(aTrans->GetTransferData(kCustomTypesMime,
|
|
getter_AddRefs(transferData)))) {
|
|
return false;
|
|
}
|
|
nsCOMPtr<nsISupportsCString> cStringData = do_QueryInterface(transferData);
|
|
if (!cStringData) {
|
|
return false;
|
|
}
|
|
nsCString str;
|
|
nsresult rv = cStringData->GetData(str);
|
|
if (NS_FAILED(rv)) {
|
|
return false;
|
|
}
|
|
nsString text;
|
|
dom::DataTransfer::ParseExternalCustomTypesString(
|
|
mozilla::Span(str.Data(), str.Length()),
|
|
[&](dom::DataTransfer::ParseExternalCustomTypesStringData&& aData) {
|
|
text = std::move(std::move(aData).second);
|
|
});
|
|
if (text.IsEmpty()) {
|
|
return false;
|
|
}
|
|
return AnalyzeText(aInnerWindowId, aResolver, aDocumentURI, aContentAnalysis,
|
|
std::move(text));
|
|
}
|
|
|
|
ClipboardContentAnalysisResult CheckClipboardContentAnalysisAsText(
|
|
uint64_t aInnerWindowId,
|
|
ContentAnalysis::SafeContentAnalysisResultCallback* aResolver,
|
|
nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis,
|
|
nsITransferable* aTextTrans, const char* aFlavor) {
|
|
nsCOMPtr<nsISupports> transferData;
|
|
if (NS_FAILED(
|
|
aTextTrans->GetTransferData(aFlavor, getter_AddRefs(transferData)))) {
|
|
return false;
|
|
}
|
|
nsString text;
|
|
nsCOMPtr<nsISupportsString> textData = do_QueryInterface(transferData);
|
|
if (MOZ_LIKELY(textData)) {
|
|
if (NS_FAILED(textData->GetData(text))) {
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
}
|
|
if (text.IsEmpty()) {
|
|
nsCOMPtr<nsISupportsCString> cStringData = do_QueryInterface(transferData);
|
|
if (cStringData) {
|
|
nsCString cText;
|
|
if (NS_FAILED(cStringData->GetData(cText))) {
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
text = NS_ConvertUTF8toUTF16(cText);
|
|
}
|
|
}
|
|
if (text.IsEmpty()) {
|
|
// Content Analysis doesn't expect to analyze an empty string.
|
|
// Just approve it.
|
|
return mozilla::Err(NoContentAnalysisResult::
|
|
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS);
|
|
}
|
|
return AnalyzeText(aInnerWindowId, aResolver, aDocumentURI, aContentAnalysis,
|
|
std::move(text));
|
|
}
|
|
|
|
ClipboardContentAnalysisResult CheckClipboardContentAnalysisAsFile(
|
|
uint64_t aInnerWindowId,
|
|
ContentAnalysis::SafeContentAnalysisResultCallback* aResolver,
|
|
nsIURI* aDocumentURI, nsIContentAnalysis* aContentAnalysis,
|
|
nsITransferable* aFileTrans) {
|
|
nsCOMPtr<nsISupports> transferData;
|
|
nsresult rv =
|
|
aFileTrans->GetTransferData(kFileMime, getter_AddRefs(transferData));
|
|
nsString filePath;
|
|
if (NS_SUCCEEDED(rv)) {
|
|
if (nsCOMPtr<nsIFile> file = do_QueryInterface(transferData)) {
|
|
rv = file->GetPath(filePath);
|
|
} else {
|
|
MOZ_ASSERT_UNREACHABLE("clipboard data had kFileMime but no nsIFile!");
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
}
|
|
if (NS_FAILED(rv) || filePath.IsEmpty()) {
|
|
return false;
|
|
}
|
|
RefPtr<mozilla::dom::WindowGlobalParent> window =
|
|
mozilla::dom::WindowGlobalParent::GetByInnerWindowId(aInnerWindowId);
|
|
if (!window) {
|
|
// The window has gone away in the meantime
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
// Let the content analysis code calculate the digest
|
|
nsCOMPtr<nsIContentAnalysisRequest> contentAnalysisRequest =
|
|
new ContentAnalysisRequest(
|
|
nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry,
|
|
std::move(filePath), true, EmptyCString(), aDocumentURI,
|
|
nsIContentAnalysisRequest::OperationType::eCustomDisplayString,
|
|
window);
|
|
rv = aContentAnalysis->AnalyzeContentRequestCallback(
|
|
contentAnalysisRequest,
|
|
/* aAutoAcknowledge */ true, aResolver);
|
|
if (NS_FAILED(rv)) {
|
|
return mozilla::Err(NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ContentAnalysis::CheckClipboardContentAnalysis(
|
|
nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow,
|
|
nsITransferable* aTransferable, nsIClipboard::ClipboardType aClipboardType,
|
|
SafeContentAnalysisResultCallback* aResolver) {
|
|
using namespace mozilla::contentanalysis;
|
|
|
|
// Content analysis is only needed if an outside webpage has access to
|
|
// the data. So, skip content analysis if there is:
|
|
// - no associated window (for example, scripted clipboard read by system
|
|
// code)
|
|
// - the window is a chrome docshell
|
|
// - the window is being rendered in the parent process (for example,
|
|
// about:support and the like)
|
|
if (!aWindow || aWindow->GetBrowsingContext()->IsChrome() ||
|
|
aWindow->IsInProcess()) {
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::
|
|
ALLOW_DUE_TO_CONTEXT_EXEMPT_FROM_CONTENT_ANALYSIS));
|
|
return;
|
|
}
|
|
nsCOMPtr<nsIContentAnalysis> contentAnalysis =
|
|
mozilla::components::nsIContentAnalysis::Service();
|
|
if (!contentAnalysis) {
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR));
|
|
return;
|
|
}
|
|
|
|
bool contentAnalysisIsActive;
|
|
nsresult rv = contentAnalysis->GetIsActive(&contentAnalysisIsActive);
|
|
if (MOZ_LIKELY(NS_FAILED(rv) || !contentAnalysisIsActive)) {
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::ALLOW_DUE_TO_CONTENT_ANALYSIS_NOT_ACTIVE));
|
|
return;
|
|
}
|
|
|
|
uint64_t innerWindowId = aWindow->InnerWindowId();
|
|
if (mozilla::StaticPrefs::
|
|
browser_contentanalysis_bypass_for_same_tab_operations()) {
|
|
mozilla::Maybe<uint64_t> cacheInnerWindowId =
|
|
aClipboard->GetClipboardCacheInnerWindowId(aClipboardType);
|
|
if (cacheInnerWindowId.isSome() && *cacheInnerWindowId == innerWindowId) {
|
|
// If the same page copied this data to the clipboard (and the above
|
|
// preference is set) we can skip content analysis and immediately allow
|
|
// this.
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::ALLOW_DUE_TO_SAME_TAB_SOURCE));
|
|
return;
|
|
}
|
|
}
|
|
|
|
nsCOMPtr<nsIURI> currentURI =
|
|
GetURIForBrowsingContext(aWindow->Canonical()->GetBrowsingContext());
|
|
if (!currentURI) {
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR));
|
|
return;
|
|
}
|
|
nsTArray<nsCString> flavors;
|
|
rv = aTransferable->FlavorsTransferableCanExport(flavors);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::DENY_DUE_TO_OTHER_ERROR));
|
|
return;
|
|
}
|
|
bool keepChecking = true;
|
|
if (flavors.Contains(kFileMime)) {
|
|
auto fileResult = CheckClipboardContentAnalysisAsFile(
|
|
innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable);
|
|
|
|
if (fileResult.isErr()) {
|
|
aResolver->Callback(
|
|
ContentAnalysisResult::FromNoResult(fileResult.unwrapErr()));
|
|
return;
|
|
}
|
|
keepChecking = !fileResult.unwrap();
|
|
}
|
|
if (!keepChecking) {
|
|
return;
|
|
}
|
|
|
|
auto customResult = CheckClipboardContentAnalysisAsCustomData(
|
|
innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable);
|
|
if (customResult.isErr()) {
|
|
aResolver->Callback(
|
|
ContentAnalysisResult::FromNoResult(customResult.unwrapErr()));
|
|
return;
|
|
}
|
|
keepChecking = !customResult.unwrap();
|
|
if (!keepChecking) {
|
|
return;
|
|
}
|
|
|
|
// Note that on Windows, kNativeHTMLMime will return the text in the native
|
|
// Windows clipboard CF_HTML format - see
|
|
// https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
|
|
auto textFormats = {kTextMime, kHTMLMime, kNativeHTMLMime};
|
|
for (const auto& textFormat : textFormats) {
|
|
auto textResult = CheckClipboardContentAnalysisAsText(
|
|
innerWindowId, aResolver, currentURI, contentAnalysis, aTransferable,
|
|
textFormat);
|
|
if (textResult.isErr()) {
|
|
aResolver->Callback(
|
|
ContentAnalysisResult::FromNoResult(textResult.unwrapErr()));
|
|
return;
|
|
}
|
|
keepChecking = !textResult.unwrap();
|
|
if (!keepChecking) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (keepChecking) {
|
|
// Couldn't get any data from this
|
|
aResolver->Callback(ContentAnalysisResult::FromNoResult(
|
|
NoContentAnalysisResult::ALLOW_DUE_TO_COULD_NOT_GET_DATA));
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool ContentAnalysis::CheckClipboardContentAnalysisSync(
|
|
nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow,
|
|
const nsCOMPtr<nsITransferable>& trans,
|
|
nsIClipboard::ClipboardType aClipboardType) {
|
|
bool requestDone = false;
|
|
RefPtr<nsIContentAnalysisResult> result;
|
|
auto callback = mozilla::MakeRefPtr<SafeContentAnalysisResultCallback>(
|
|
[&requestDone, &result](RefPtr<nsIContentAnalysisResult>&& aResult) {
|
|
result = std::move(aResult);
|
|
requestDone = true;
|
|
});
|
|
CheckClipboardContentAnalysis(aClipboard, aWindow, trans, aClipboardType,
|
|
callback);
|
|
mozilla::SpinEventLoopUntil("CheckClipboardContentAnalysisSync"_ns,
|
|
[&requestDone]() -> bool { return requestDone; });
|
|
return result->GetShouldAllowContent();
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysisResponse::Acknowledge(
|
|
nsIContentAnalysisAcknowledgement* aAcknowledgement) {
|
|
MOZ_ASSERT(mOwner);
|
|
if (mHasAcknowledged) {
|
|
MOZ_ASSERT(false, "Already acknowledged this ContentAnalysisResponse!");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
mHasAcknowledged = true;
|
|
|
|
if (mDoNotAcknowledge) {
|
|
return NS_OK;
|
|
}
|
|
return mOwner->RunAcknowledgeTask(aAcknowledgement, mRequestToken);
|
|
};
|
|
|
|
nsresult ContentAnalysis::RunAcknowledgeTask(
|
|
nsIContentAnalysisAcknowledgement* aAcknowledgement,
|
|
const nsACString& aRequestToken) {
|
|
bool isActive;
|
|
nsresult rv = GetIsActive(&isActive);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!isActive) {
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
content_analysis::sdk::ContentAnalysisAcknowledgement pbAck;
|
|
rv = ConvertToProtobuf(aAcknowledgement, aRequestToken, &pbAck);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
LOGD("Issuing ContentAnalysisAcknowledgement");
|
|
LogAcknowledgement(&pbAck);
|
|
|
|
// The content analysis connection is synchronous so run in the background.
|
|
LOGD("RunAcknowledgeTask dispatching acknowledge task");
|
|
mCaClientPromise->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[pbAck = std::move(pbAck)](
|
|
std::shared_ptr<content_analysis::sdk::Client> client) mutable {
|
|
NS_DispatchBackgroundTask(
|
|
NS_NewCancelableRunnableFunction(
|
|
__func__,
|
|
[pbAck = std::move(pbAck),
|
|
client = std::move(client)]() mutable {
|
|
RefPtr<ContentAnalysis> owner =
|
|
GetContentAnalysisFromService();
|
|
if (!owner) {
|
|
// May be shutting down
|
|
return;
|
|
}
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
int err = client->Acknowledge(pbAck);
|
|
MOZ_ASSERT(err == 0);
|
|
LOGD(
|
|
"RunAcknowledgeTask sent transaction acknowledgement, "
|
|
"err=%d",
|
|
err);
|
|
}),
|
|
NS_DISPATCH_EVENT_MAY_BLOCK);
|
|
},
|
|
[](nsresult rv) { LOGD("RunAcknowledgeTask failed to get the client"); });
|
|
return rv;
|
|
}
|
|
|
|
bool ContentAnalysis::LastRequestSucceeded() {
|
|
return mLastResult != NS_ERROR_NOT_AVAILABLE &&
|
|
mLastResult != NS_ERROR_INVALID_SIGNATURE &&
|
|
mLastResult != NS_ERROR_FAILURE;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ContentAnalysis::GetDiagnosticInfo(JSContext* aCx,
|
|
mozilla::dom::Promise** aPromise) {
|
|
RefPtr<mozilla::dom::Promise> promise;
|
|
nsresult rv = MakePromise(aCx, &promise);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
mCaClientPromise->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[promise](std::shared_ptr<content_analysis::sdk::Client> client) mutable {
|
|
if (!client) {
|
|
auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>(
|
|
false, EmptyString(), false, 0);
|
|
promise->MaybeResolve(info);
|
|
return;
|
|
}
|
|
RefPtr<ContentAnalysis> self = GetContentAnalysisFromService();
|
|
std::string agentPath = client->GetAgentInfo().binary_path;
|
|
nsString agentWidePath = NS_ConvertUTF8toUTF16(agentPath);
|
|
auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>(
|
|
self->LastRequestSucceeded(), std::move(agentWidePath), false,
|
|
self ? self->mRequestCount : 0);
|
|
promise->MaybeResolve(info);
|
|
},
|
|
[promise](nsresult rv) {
|
|
RefPtr<ContentAnalysis> self = GetContentAnalysisFromService();
|
|
auto info = MakeRefPtr<ContentAnalysisDiagnosticInfo>(
|
|
false, EmptyString(), rv == NS_ERROR_INVALID_SIGNATURE,
|
|
self ? self->mRequestCount : 0);
|
|
promise->MaybeResolve(info);
|
|
});
|
|
promise.forget(aPromise);
|
|
return NS_OK;
|
|
}
|
|
|
|
/* static */ nsCOMPtr<nsIURI> ContentAnalysis::GetURIForBrowsingContext(
|
|
dom::CanonicalBrowsingContext* aBrowsingContext) {
|
|
dom::WindowGlobalParent* windowGlobal =
|
|
aBrowsingContext->GetCurrentWindowGlobal();
|
|
if (!windowGlobal) {
|
|
return nullptr;
|
|
}
|
|
nsIPrincipal* principal = windowGlobal->DocumentPrincipal();
|
|
dom::CanonicalBrowsingContext* curBrowsingContext =
|
|
aBrowsingContext->GetParent();
|
|
while (curBrowsingContext) {
|
|
dom::WindowGlobalParent* newWindowGlobal =
|
|
curBrowsingContext->GetCurrentWindowGlobal();
|
|
if (!newWindowGlobal) {
|
|
break;
|
|
}
|
|
nsIPrincipal* newPrincipal = newWindowGlobal->DocumentPrincipal();
|
|
if (!(newPrincipal->Subsumes(principal))) {
|
|
break;
|
|
}
|
|
principal = newPrincipal;
|
|
curBrowsingContext = curBrowsingContext->GetParent();
|
|
}
|
|
return principal->GetURI();
|
|
}
|
|
|
|
// IDL implementation
|
|
NS_IMETHODIMP ContentAnalysis::GetURIForBrowsingContext(
|
|
dom::BrowsingContext* aBrowsingContext, nsIURI** aURI) {
|
|
NS_ENSURE_ARG_POINTER(aBrowsingContext);
|
|
NS_ENSURE_ARG_POINTER(aURI);
|
|
nsCOMPtr<nsIURI> uri =
|
|
GetURIForBrowsingContext(aBrowsingContext->Canonical());
|
|
if (!uri) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
uri.forget(aURI);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP ContentAnalysisCallback::ContentResult(
|
|
nsIContentAnalysisResponse* aResponse) {
|
|
if (mPromise.isSome()) {
|
|
mPromise->get()->MaybeResolve(aResponse);
|
|
} else {
|
|
mContentResponseCallback(aResponse);
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP ContentAnalysisCallback::Error(nsresult aError) {
|
|
if (mPromise.isSome()) {
|
|
mPromise->get()->MaybeReject(aError);
|
|
} else {
|
|
mErrorCallback(aError);
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
ContentAnalysisCallback::ContentAnalysisCallback(RefPtr<dom::Promise> aPromise)
|
|
: mPromise(Some(new nsMainThreadPtrHolder<dom::Promise>(
|
|
"content analysis promise", aPromise))) {}
|
|
|
|
NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetConnectedToAgent(
|
|
bool* aConnectedToAgent) {
|
|
*aConnectedToAgent = mConnectedToAgent;
|
|
return NS_OK;
|
|
}
|
|
NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetAgentPath(
|
|
nsAString& aAgentPath) {
|
|
aAgentPath = mAgentPath;
|
|
return NS_OK;
|
|
}
|
|
NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetFailedSignatureVerification(
|
|
bool* aFailedSignatureVerification) {
|
|
*aFailedSignatureVerification = mFailedSignatureVerification;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP ContentAnalysisDiagnosticInfo::GetRequestCount(
|
|
int64_t* aRequestCount) {
|
|
*aRequestCount = mRequestCount;
|
|
return NS_OK;
|
|
}
|
|
|
|
ContentAnalysis::CachedData::CacheResult
|
|
ContentAnalysis::CachedData::CompareWithRequest(
|
|
const RefPtr<nsIContentAnalysisRequest>& aRequest) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
nsIContentAnalysisRequest::AnalysisType analysisType;
|
|
if (NS_FAILED(aRequest->GetAnalysisType(&analysisType)) ||
|
|
analysisType != nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry) {
|
|
return CacheResult::CannotBeCached;
|
|
}
|
|
nsString requestTextContent;
|
|
if (NS_FAILED(aRequest->GetTextContent(requestTextContent)) ||
|
|
requestTextContent.IsEmpty()) {
|
|
return CacheResult::CannotBeCached;
|
|
}
|
|
nsCOMPtr<nsIURI> requestUri;
|
|
if (NS_FAILED(aRequest->GetUrl(getter_AddRefs(requestUri)))) {
|
|
return CacheResult::CannotBeCached;
|
|
}
|
|
RefPtr<dom::WindowGlobalParent> windowGlobalParent;
|
|
if (NS_FAILED(aRequest->GetWindowGlobalParent(
|
|
getter_AddRefs(windowGlobalParent)))) {
|
|
return CacheResult::CannotBeCached;
|
|
}
|
|
|
|
nsCOMPtr<nsIContentAnalysisRequest> cachedRequest = Request();
|
|
if (!cachedRequest) {
|
|
return CacheResult::DoesNotMatchExisting;
|
|
}
|
|
nsCOMPtr<nsIURI> cachedUri;
|
|
bool uriEquals = false;
|
|
if (NS_FAILED(cachedRequest->GetUrl(getter_AddRefs(cachedUri))) ||
|
|
NS_FAILED(cachedUri->Equals(requestUri, &uriEquals)) || !uriEquals) {
|
|
return CacheResult::DoesNotMatchExisting;
|
|
}
|
|
nsString cachedTextContent;
|
|
if (NS_FAILED(cachedRequest->GetTextContent(cachedTextContent)) ||
|
|
!cachedTextContent.Equals(requestTextContent)) {
|
|
return CacheResult::DoesNotMatchExisting;
|
|
}
|
|
RefPtr<dom::WindowGlobalParent> cachedWindowGlobalParent;
|
|
if (NS_FAILED(cachedRequest->GetWindowGlobalParent(
|
|
getter_AddRefs(cachedWindowGlobalParent)))) {
|
|
return CacheResult::DoesNotMatchExisting;
|
|
}
|
|
if (cachedWindowGlobalParent && windowGlobalParent &&
|
|
cachedWindowGlobalParent->InnerWindowId() !=
|
|
windowGlobalParent->InnerWindowId()) {
|
|
return CacheResult::DoesNotMatchExisting;
|
|
}
|
|
return CacheResult::Matches;
|
|
}
|
|
|
|
void ContentAnalysis::CachedData::SetExpirationTimer() {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (mExpirationTimer) {
|
|
mExpirationTimer->Cancel();
|
|
} else {
|
|
mExpirationTimer = NS_NewTimer();
|
|
}
|
|
mExpirationTimer->InitWithNamedFuncCallback(
|
|
[](nsITimer* func, void* closure) {
|
|
LOGD("Clearing content analysis cache (dispatching to main thread)");
|
|
NS_DispatchToMainThread(
|
|
NS_NewCancelableRunnableFunction("Clear ContentAnalysis cache", [] {
|
|
LOGD("Clearing content analysis cache");
|
|
RefPtr<ContentAnalysis> contentAnalysis =
|
|
ContentAnalysis::GetContentAnalysisFromService();
|
|
if (contentAnalysis) {
|
|
contentAnalysis->mCachedData.Clear();
|
|
}
|
|
}));
|
|
},
|
|
nullptr, mClearTimeout, nsITimer::TYPE_ONE_SHOT,
|
|
"ContentAnalysis::CachedData::SetExpirationTimer");
|
|
LOGD("Set content analysis cached data clear timer with timeout %d",
|
|
mClearTimeout);
|
|
}
|
|
|
|
void ContentAnalysis::SetCachedDataTimeoutForTesting(uint32_t aNewTimeout) {
|
|
mCachedData.mClearTimeout = aNewTimeout;
|
|
}
|
|
void ContentAnalysis::ResetCachedDataTimeoutForTesting() {
|
|
mCachedData.mClearTimeout = kDefaultCachedDataTimeoutInMs;
|
|
}
|
|
|
|
#undef LOGD
|
|
#undef LOGE
|
|
} // namespace mozilla::contentanalysis
|