/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "mozilla/Assertions.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/dom/Promise-inl.h" #include "mozilla/media/MediaUtils.h" #include "js/Object.h" #include "js/PropertyAndElement.h" #include "nsNetUtil.h" #include "nsIFile.h" #include "nsIObserverService.h" #include "nsIURI.h" #include "nsIURIMutator.h" #include "nsJSUtils.h" #include "ContentAnalysis.h" #include "SpecialSystemDirectory.h" #include "TestContentAnalysisUtils.h" #include #include #include const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list"; const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list"; const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; const char* kIsDLPEnabledPref = "browser.contentanalysis.enabled"; const char* kTimeoutPref = "browser.contentanalysis.agent_timeout"; const char* kClientSignaturePref = "browser.contentanalysis.client_signature"; using namespace mozilla; using namespace mozilla::contentanalysis; class ContentAnalysisTest : public testing::Test { protected: ContentAnalysisTest() { auto* logmodule = LogModule::Get("contentanalysis"); logmodule->SetLevel(LogLevel::Verbose); MOZ_ALWAYS_SUCCEEDS( Preferences::SetString(kPipePathNamePref, mPipeName.get())); MOZ_ALWAYS_SUCCEEDS(Preferences::SetBool(kIsDLPEnabledPref, true)); nsCOMPtr caSvc = do_GetService("@mozilla.org/contentanalysis;1"); MOZ_ASSERT(caSvc); mContentAnalysis = static_cast(caSvc.get()); // Tests run earlier could have altered these values mContentAnalysis->mParsedUrlLists = false; mContentAnalysis->mAllowUrlList = {}; mContentAnalysis->mDenyUrlList = {}; MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->TestOnlySetCACmdLineArg(true)); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kAllowUrlPref, "")); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kDenyUrlPref, "")); bool isActive = false; MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->GetIsActive(&isActive)); EXPECT_TRUE(isActive); } // Note that the constructor (and SetUp() method) get called once per test, // not once for the whole fixture. Because Firefox does not currently // reconnect to an agent after the DLP pipe is closed (bug 1888293), we only // want to create the agent once and make sure the same process stays alive // through all of these tests. static void SetUpTestSuite() { GeneratePipeName(L"contentanalysissdk-gtest-", mPipeName); StartAgent(); } static void TearDownTestSuite() { mAgentInfo.TerminateProcess(); } static void StartAgent() { mAgentInfo = LaunchAgentNormal(L"block", mPipeName); } void TearDown() override { mContentAnalysis->mParsedUrlLists = false; mContentAnalysis->mAllowUrlList = {}; mContentAnalysis->mDenyUrlList = {}; MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->TestOnlySetCACmdLineArg(false)); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kAllowUrlPref, "")); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kDenyUrlPref, "")); MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kPipePathNamePref)); MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kIsDLPEnabledPref)); } already_AddRefed CreateRequest(const char* aUrl) { nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aUrl)); // We will only use the URL and, implicitly, the analysisType // (behavior differs for download vs other types). return RefPtr(new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eFileTransfer, nsIContentAnalysisRequest::Reason::eFilePickerDialog, EmptyString(), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eDroppedText, nullptr)) .forget(); } enum class CancelMechanism { // Wait for the service to assign our request tokens, then cancel using // that (deprecated) eCancelByRequestToken, // Wait for the service to assign our requests a user action ID, then cancel // using that. eCancelByUserActionId, }; nsresult SendRequestsCancelAndExpectResponse( RefPtr contentAnalysis, const nsTArray>& requests, CancelMechanism aCancelMechanism, bool aExpectFailure); RefPtr GetDiagnosticInfo( RefPtr contentAnalysis); RefPtr mContentAnalysis; static nsString mPipeName; static MozAgentInfo mAgentInfo; // Proxies for private members of ContentAnalysis. TEST_F // creates new subclasses -- they do not inherit `friend`s. // (FRIEND_TEST is another more verbose solution.) using UrlFilterResult = ContentAnalysis::UrlFilterResult; UrlFilterResult FilterByUrlLists(nsIContentAnalysisRequest* aReq) { // For testing, just pull the URI from the request. nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS(aReq->GetUrl(getter_AddRefs(uri))); MOZ_ASSERT(uri); return mContentAnalysis->FilterByUrlLists(aReq, uri); } }; MOZ_RUNINIT nsString ContentAnalysisTest::mPipeName; MOZ_RUNINIT MozAgentInfo ContentAnalysisTest::mAgentInfo; TEST_F(ContentAnalysisTest, AllowUrlList) { MOZ_ALWAYS_SUCCEEDS( Preferences::SetCString(kAllowUrlPref, ".*\\.org/match.*")); RefPtr car = CreateRequest("https://example.org/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eAllow); car = CreateRequest("https://example.com/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); } TEST_F(ContentAnalysisTest, DefaultAllowUrlList) { MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kAllowUrlPref)); RefPtr car = CreateRequest("about:home"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eAllow); car = CreateRequest("about:blank"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); car = CreateRequest("about:srcdoc"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); car = CreateRequest("https://example.com/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); } TEST_F(ContentAnalysisTest, MultipleAllowUrlList) { MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString( kAllowUrlPref, ".*\\.org/match.* .*\\.net/match.*")); RefPtr car = CreateRequest("https://example.org/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eAllow); car = CreateRequest("https://example.net/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eAllow); car = CreateRequest("https://example.com/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); } TEST_F(ContentAnalysisTest, DenyUrlList) { MOZ_ALWAYS_SUCCEEDS( Preferences::SetCString(kDenyUrlPref, ".*\\.com/match.*")); RefPtr car = CreateRequest("https://example.org/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); car = CreateRequest("https://example.com/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eDeny); } TEST_F(ContentAnalysisTest, MultipleDenyUrlList) { MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString( kDenyUrlPref, ".*\\.com/match.* .*\\.biz/match.*")); RefPtr car = CreateRequest("https://example.org/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eCheck); car = CreateRequest("https://example.com/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eDeny); car = CreateRequest("https://example.biz/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eDeny); } TEST_F(ContentAnalysisTest, DenyOverridesAllowUrlList) { MOZ_ALWAYS_SUCCEEDS( Preferences::SetCString(kAllowUrlPref, ".*\\.org/match.*")); MOZ_ALWAYS_SUCCEEDS(Preferences::SetCString(kDenyUrlPref, ".*.org/match.*")); RefPtr car = CreateRequest("https://example.org/matchme/"); ASSERT_EQ(FilterByUrlLists(car), UrlFilterResult::eDeny); } nsCOMPtr GetExampleDotComURI() { nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "https://example.com")); return uri; } nsCOMPtr GetExampleDotComWithPathURI() { nsCOMPtr uri; MOZ_ALWAYS_SUCCEEDS( NS_NewURI(getter_AddRefs(uri), "https://example.com/path")); return uri; } struct BoolStruct { bool mValue = false; }; RefPtr ContentAnalysisTest::GetDiagnosticInfo( RefPtr contentAnalysis) { dom::AutoJSAPI jsapi; // We're using this context to deserialize, stringify, and print a message // manager message here. Since the messages are always sent from and to system // scopes, we need to do this in a system scope, or attempting to deserialize // certain privileged objects will fail. MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); JSContext* cx = jsapi.cx(); bool gotResponse = false; RefPtr timedOut = MakeRefPtr>(); dom::Promise* promise = nullptr; RefPtr diagnosticInfo = nullptr; MOZ_ALWAYS_SUCCEEDS(mContentAnalysis->GetDiagnosticInfo(cx, &promise)); auto result = promise->ThenWithCycleCollectedArgs( [&, timedOut](JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) -> already_AddRefed { if (timedOut->mValue) { return nullptr; } EXPECT_TRUE(aValue.isObject()); JS::Rooted obj(aCx, &aValue.toObject()); JS::Rooted value(aCx); EXPECT_TRUE(JS_GetProperty(aCx, obj, "connectedToAgent", &value)); bool connectedToAgent = JS::ToBoolean(value); EXPECT_TRUE(JS_GetProperty(aCx, obj, "agentPath", &value)); nsAutoJSString agentPath; EXPECT_TRUE(agentPath.init(aCx, value)); EXPECT_TRUE( JS_GetProperty(aCx, obj, "failedSignatureVerification", &value)); bool failedSignatureVerification = JS::ToBoolean(value); EXPECT_TRUE(JS_GetProperty(aCx, obj, "requestCount", &value)); int64_t requestCount; EXPECT_TRUE(JS::ToInt64(aCx, value, &requestCount)); diagnosticInfo = MakeRefPtr( connectedToAgent, agentPath, failedSignatureVerification, requestCount); gotResponse = true; return nullptr; }); RefPtr timer = NS_NewCancelableRunnableFunction("GetDiagnosticInfo timeout", [&] { if (!gotResponse) { timedOut->mValue = true; } }); constexpr uint32_t kDiagnosticTimeout = 10000; NS_DelayedDispatchToCurrentThread(do_AddRef(timer), kDiagnosticTimeout); mozilla::SpinEventLoopUntil( "Waiting for GetDiagnosticInfo result"_ns, [&, timedOut]() { return gotResponse || timedOut->mValue; }); timer->Cancel(); EXPECT_TRUE(gotResponse); EXPECT_FALSE(timedOut->mValue); return diagnosticInfo; } class RawAcknowledgementObserver final : public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER const std::vector& GetAcknowledgements() { return mAcknowledgements; } private: ~RawAcknowledgementObserver() = default; std::vector mAcknowledgements; }; NS_IMPL_ISUPPORTS(RawAcknowledgementObserver, nsIObserver); NS_IMETHODIMP RawAcknowledgementObserver::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { std::wstring dataWideString(reinterpret_cast(aData)); std::vector dataVector(dataWideString.size()); for (size_t i = 0; i < dataWideString.size(); ++i) { // Since this data is really bytes and not a null-terminated string, the // calling code adds 0xFF00 to every member to ensure there are no 0 values. dataVector[i] = static_cast(dataWideString[i] - 0xFF00); } content_analysis::sdk::ContentAnalysisAcknowledgement request; EXPECT_TRUE(request.ParseFromArray(dataVector.data(), dataVector.size())); mAcknowledgements.push_back(std::move(request)); return NS_OK; } nsresult ContentAnalysisTest::SendRequestsCancelAndExpectResponse( RefPtr contentAnalysis, const nsTArray>& requests, CancelMechanism aCancelMechanism, bool aExpectFailure) { bool gotResponse = false; // Make timedOut a RefPtr so if we get a response from content analysis // after this function has finished we can safely check that (and don't // start accessing stack values that don't exist anymore) RefPtr timedOut = MakeRefPtr>(); auto callback = MakeRefPtr( [&, timedOut, aExpectFailure](nsIContentAnalysisResult* result) { if (timedOut->mValue) { return; } bool shouldAllow; MOZ_ALWAYS_SUCCEEDS(result->GetShouldAllowContent(&shouldAllow)); EXPECT_EQ(false, shouldAllow); EXPECT_EQ(false, aExpectFailure); gotResponse = true; }, [&gotResponse, timedOut, aExpectFailure](nsresult error) { if (timedOut->mValue) { return; } const char* errorName = mozilla::GetStaticErrorName(error); errorName = errorName ? errorName : ""; printf("Got error response code %s(%x)\n", errorName, error); // Errors should not have errorCode NS_OK EXPECT_NE(NS_OK, error); gotResponse = true; EXPECT_EQ(true, aExpectFailure); }); nsresult rv = contentAnalysis->AnalyzeContentRequestsCallback( requests, true /* autoAcknowledge */, callback); if (NS_FAILED(rv)) { return rv; } RefPtr timer = NS_NewCancelableRunnableFunction( "SendRequestsCancelAndExpectResponse timeout", [&] { if (!gotResponse) { timedOut->mValue = true; } }); #if defined(MOZ_ASAN) // This can be pretty slow on ASAN builds (bug 1895256) constexpr uint32_t kCATimeout = 25000; #else constexpr uint32_t kCATimeout = 10000; #endif NS_DelayedDispatchToCurrentThread(do_AddRef(timer), kCATimeout); // The user action ID should be set by now, whether we set it or not. nsAutoCString userActionId; MOZ_ALWAYS_SUCCEEDS(requests[0]->GetUserActionId(userActionId)); EXPECT_TRUE(!userActionId.IsEmpty()); bool hasCanceledRequest = false; if (aCancelMechanism == CancelMechanism::eCancelByUserActionId) { MOZ_ALWAYS_SUCCEEDS( contentAnalysis->CancelRequestsByUserAction(userActionId)); hasCanceledRequest = true; } mozilla::SpinEventLoopUntil( "Waiting for ContentAnalysis result"_ns, [&, timedOut]() { if (!hasCanceledRequest) { // Internally, GetFinalRequests expands the request list // asynchronously. We need to wait for that. // (In the case of this test, nothing actually needs to be expanded.) if (aCancelMechanism == CancelMechanism::eCancelByRequestToken) { nsAutoCString requestToken; MOZ_ALWAYS_SUCCEEDS(requests[0]->GetRequestToken(requestToken)); if (!requestToken.IsEmpty()) { MOZ_ALWAYS_SUCCEEDS( contentAnalysis->CancelRequestsByRequestToken(requestToken)); hasCanceledRequest = true; } } } return gotResponse || timedOut->mValue; }); timer->Cancel(); EXPECT_TRUE(gotResponse); EXPECT_FALSE(timedOut->mValue); return NS_OK; } void SendRequestAndExpectResponse( RefPtr contentAnalysis, const nsCOMPtr& request, Maybe expectedShouldAllow, Maybe expectedAction, Maybe expectedIsCached) { std::atomic gotResponse = false; // Make timedOut a RefPtr so if we get a response from content analysis // after this function has finished we can safely check that (and don't // start accessing stack values that don't exist anymore) RefPtr timedOut = MakeRefPtr>(); auto callback = MakeRefPtr( [&, timedOut](nsIContentAnalysisResult* result) { if (timedOut->mValue) { return; } nsCOMPtr response = do_QueryInterface(result); EXPECT_TRUE(response); if (expectedShouldAllow.isSome()) { bool shouldAllow = false; MOZ_ALWAYS_SUCCEEDS(response->GetShouldAllowContent(&shouldAllow)); EXPECT_EQ(*expectedShouldAllow, shouldAllow); } if (expectedAction.isSome()) { nsIContentAnalysisResponse::Action action; MOZ_ALWAYS_SUCCEEDS(response->GetAction(&action)); EXPECT_EQ(*expectedAction, action); } if (expectedIsCached.isSome()) { bool isCached; MOZ_ALWAYS_SUCCEEDS(response->GetIsCachedResponse(&isCached)); EXPECT_EQ(*expectedIsCached, isCached); } nsCString requestToken, originalRequestToken; MOZ_ALWAYS_SUCCEEDS(response->GetRequestToken(requestToken)); MOZ_ALWAYS_SUCCEEDS(request->GetRequestToken(originalRequestToken)); EXPECT_EQ(originalRequestToken, requestToken); gotResponse = true; }, [&gotResponse, timedOut](nsresult error) { if (timedOut->mValue) { return; } const char* errorName = mozilla::GetStaticErrorName(error); errorName = errorName ? errorName : ""; printf("Got error response code %s(%x)\n", errorName, error); // Errors should not have errorCode NS_OK EXPECT_NE(NS_OK, error); gotResponse = true; FAIL() << "Got error response"; }); AutoTArray, 1> requests{request.get()}; MOZ_ALWAYS_SUCCEEDS(contentAnalysis->AnalyzeContentRequestsCallback( requests, false, callback)); RefPtr timer = NS_NewCancelableRunnableFunction( "SendRequestAndExpectResponse timeout", [&] { if (!gotResponse.load()) { timedOut->mValue = true; } }); #if defined(MOZ_ASAN) // This can be pretty slow on ASAN builds (bug 1895256) constexpr uint32_t kCATimeout = 25000; #else constexpr uint32_t kCATimeout = 10000; #endif NS_DelayedDispatchToCurrentThread(do_AddRef(timer), kCATimeout); mozilla::SpinEventLoopUntil( "Waiting for ContentAnalysis result"_ns, [&, timedOut]() { return gotResponse.load() || timedOut->mValue; }); timer->Cancel(); EXPECT_TRUE(gotResponse); EXPECT_FALSE(timedOut->mValue); } void SendRequestAndExpectNoAgentResponse( RefPtr contentAnalysis, const nsCOMPtr& request, nsIContentAnalysisResponse::CancelError expectedCancelError = nsIContentAnalysisResponse::CancelError::eNoAgent) { std::atomic gotResponse = false; // Make timedOut a RefPtr so if we get a response from content analysis // after this function has finished we can safely check that (and don't // start accessing stack values that don't exist anymore) RefPtr timedOut = MakeRefPtr>(); auto callback = MakeRefPtr( [&, timedOut](nsIContentAnalysisResult* result) { if (timedOut->mValue) { return; } nsCOMPtr response = do_QueryInterface(result); EXPECT_TRUE(response); EXPECT_EQ(expectedCancelError, response->GetCancelError()); // We're just doing default deny here EXPECT_EQ(false, response->GetShouldAllowContent()); gotResponse = true; }, [&gotResponse, timedOut](nsresult error) { if (timedOut->mValue) { return; } const char* errorName = mozilla::GetStaticErrorName(error); errorName = errorName ? errorName : ""; printf("Got error response code %s(%x)\n", errorName, error); // Errors should not have errorCode NS_OK EXPECT_NE(NS_OK, error); gotResponse = true; FAIL() << "Got error response"; }); AutoTArray, 1> requests{request.get()}; MOZ_ALWAYS_SUCCEEDS(contentAnalysis->AnalyzeContentRequestsCallback( requests, false, callback)); RefPtr timer = NS_NewCancelableRunnableFunction("Content Analysis timeout", [&] { if (!gotResponse.load()) { timedOut->mValue = true; } }); #if defined(MOZ_ASAN) // This can be pretty slow on ASAN builds (bug 1895256) constexpr uint32_t kCATimeout = 25000; #else constexpr uint32_t kCATimeout = 10000; #endif NS_DelayedDispatchToCurrentThread(do_AddRef(timer), kCATimeout); mozilla::SpinEventLoopUntil( "Waiting for ContentAnalysis result"_ns, [&, timedOut]() { return gotResponse.load() || timedOut->mValue; }); timer->Cancel(); EXPECT_TRUE(gotResponse); EXPECT_FALSE(timedOut->mValue); } void YieldMainThread(uint32_t timeInMs) { std::atomic timeExpired = false; // The timer gets cleared on the main thread, so we need to yield the main // thread for this to work RefPtr timer = NS_NewCancelableRunnableFunction( "Content Analysis yielding", [&] { timeExpired = true; }); // Wait for longer than the cache timeout NS_DelayedDispatchToCurrentThread(do_AddRef(timer), timeInMs); mozilla::SpinEventLoopUntil("Waiting for Content Analysis yielding"_ns, [&]() { return timeExpired.load(); }); timer->Cancel(); } TEST_F(ContentAnalysisTest, SendAllowedTextToAgent_GetAllowedResponse) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectResponse(mContentAnalysis, request, Some(true), Some(nsIContentAnalysisResponse::eAllow), Some(false)); } TEST_F(ContentAnalysisTest, SendBlockedTextToAgent_GetBlockResponse) { nsCOMPtr uri = GetExampleDotComURI(); nsString block(L"block"); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(block), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectResponse(mContentAnalysis, request, Some(false), Some(nsIContentAnalysisResponse::eBlock), Some(false)); } TEST_F(ContentAnalysisTest, RestartAgent_SendAllowedTextToAgent_GetAllowedResponse) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); mAgentInfo.TerminateProcess(); StartAgent(); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectResponse(mContentAnalysis, request, Some(true), Some(nsIContentAnalysisResponse::eAllow), Some(false)); } TEST_F(ContentAnalysisTest, TerminateAgent_SendAllowedTextToAgent_GetError) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); mAgentInfo.TerminateProcess(); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectNoAgentResponse(mContentAnalysis, request); StartAgent(); SendRequestAndExpectResponse(mContentAnalysis, request, Some(true), Some(nsIContentAnalysisResponse::eAllow), Some(false)); } class RawRequestObserver final : public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER RawRequestObserver() {} const std::vector& GetRequests() { return mRequests; } private: ~RawRequestObserver() = default; std::vector mRequests; }; NS_IMPL_ISUPPORTS(RawRequestObserver, nsIObserver); NS_IMETHODIMP RawRequestObserver::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { std::wstring dataWideString(reinterpret_cast(aData)); std::vector dataVector(dataWideString.size()); for (size_t i = 0; i < dataWideString.size(); ++i) { // Since this data is really bytes and not a null-terminated string, the // calling code adds 0xFF00 to every member to ensure there are no 0 values. dataVector[i] = static_cast(dataWideString[i] - 0xFF00); } content_analysis::sdk::ContentAnalysisRequest request; EXPECT_TRUE(request.ParseFromArray(dataVector.data(), dataVector.size())); mRequests.push_back(std::move(request)); return NS_OK; } TEST_F(ContentAnalysisTest, CheckRawRequestWithText) { MOZ_ALWAYS_SUCCEEDS(Preferences::SetInt(kTimeoutPref, 65)); nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawRequestObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); time_t now = time(nullptr); SendRequestAndExpectResponse(mContentAnalysis, request, Nothing(), Nothing(), Some(false)); auto requests = rawRequestObserver->GetRequests(); EXPECT_EQ(static_cast(1), requests.size()); time_t t = requests[0].expires_at(); time_t secs_remaining = t - now; // There should be around 65 seconds remaining EXPECT_LE(abs(secs_remaining - 65), 2); const auto& request_url = requests[0].request_data().url(); EXPECT_EQ(uri->GetSpecOrDefault(), nsCString(request_url.data(), request_url.size())); const auto& request_text = requests[0].text_content(); EXPECT_EQ(nsCString("allow"), nsCString(request_text.data(), request_text.size())); MOZ_ALWAYS_SUCCEEDS( obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kTimeoutPref)); } TEST_F(ContentAnalysisTest, CheckRawRequestWithFile) { nsCOMPtr uri = GetExampleDotComURI(); nsCOMPtr file; MOZ_ALWAYS_SUCCEEDS(GetSpecialSystemDirectory(OS_CurrentWorkingDirectory, getter_AddRefs(file))); nsString allowRelativePath(L"allowedFile.txt"); MOZ_ALWAYS_SUCCEEDS(file->AppendRelativePath(allowRelativePath)); nsString allowPath; MOZ_ALWAYS_SUCCEEDS(file->GetPath(allowPath)); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, allowPath, true, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawRequestObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); SendRequestAndExpectResponse(mContentAnalysis, request, Nothing(), Nothing(), Some(false)); auto requests = rawRequestObserver->GetRequests(); EXPECT_EQ(static_cast(1), requests.size()); const auto& request_url = requests[0].request_data().url(); EXPECT_EQ(uri->GetSpecOrDefault(), nsCString(request_url.data(), request_url.size())); const auto& request_file_path = requests[0].file_path(); EXPECT_EQ(NS_ConvertUTF16toUTF8(allowPath), nsCString(request_file_path.data(), request_file_path.size())); MOZ_ALWAYS_SUCCEEDS( obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); } TEST_F(ContentAnalysisTest, CheckTwoRequestsHaveDifferentUserActionId) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow1(L"allowMe"); nsCOMPtr request1 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow1), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); // Use different text so the request doesn't match the cache nsString allow2(L"allowMeAgain"); nsCOMPtr request2 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow2), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawRequestObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); SendRequestAndExpectResponse(mContentAnalysis, request1, Nothing(), Nothing(), Some(false)); SendRequestAndExpectResponse(mContentAnalysis, request2, Nothing(), Nothing(), Some(false)); auto requests = rawRequestObserver->GetRequests(); EXPECT_EQ(static_cast(2), requests.size()); EXPECT_NE(requests[0].user_action_id(), requests[1].user_action_id()); MOZ_ALWAYS_SUCCEEDS( obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); } TEST_F(ContentAnalysisTest, CheckRequestTokensCanCancelAndHaveSameUserActionId) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow1(L"allowMe"); RefPtr request1 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow1), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); // Use different text so the request doesn't match the cache nsString allow2(L"allowMeAgain"); RefPtr request2 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow2), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); nsTArray> requests{request1, request2}; nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawRequestObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); nsresult rv = SendRequestsCancelAndExpectResponse( mContentAnalysis, requests, CancelMechanism::eCancelByRequestToken, false /* aExpectFailure */); EXPECT_EQ(rv, NS_OK); auto rawRequests = rawRequestObserver->GetRequests(); EXPECT_EQ(static_cast(2), rawRequests.size()); EXPECT_EQ(rawRequests[0].user_action_id(), rawRequests[1].user_action_id()); MOZ_ALWAYS_SUCCEEDS( obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); } TEST_F(ContentAnalysisTest, CheckAssignedUserActionIdCanCancelAndHaveSameUserActionId) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow1(L"allowMe"); RefPtr request1 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow1), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); // Use different text so the request doesn't match the cache nsString allow2(L"allowMeAgain"); RefPtr request2 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow2), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); nsTArray> requests{request1, request2}; nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawRequestObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); nsresult rv = SendRequestsCancelAndExpectResponse( mContentAnalysis, requests, CancelMechanism::eCancelByUserActionId, false /* aExpectFailure */); EXPECT_EQ(rv, NS_OK); auto rawRequests = rawRequestObserver->GetRequests(); EXPECT_EQ(static_cast(2), rawRequests.size()); EXPECT_EQ(rawRequests[0].user_action_id(), rawRequests[1].user_action_id()); MOZ_ALWAYS_SUCCEEDS( obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); } static nsCString GenerateUUID() { nsID id = nsID::GenerateUUID(); return nsCString(id.ToString().get()); } TEST_F(ContentAnalysisTest, CheckGivenUserActionIdCanCancelAndHaveSameUserActionId) { nsCString userActionId = GenerateUUID(); nsCOMPtr uri = GetExampleDotComURI(); nsString allow1(L"allowMe"); RefPtr request1 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow1), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr, nullptr, nsCString(userActionId)); // Use different text so the request doesn't match the cache nsString allow2(L"allowMeAgain"); RefPtr request2 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow2), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr, nullptr, nsCString(userActionId)); nsTArray> requests{request1, request2}; nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawRequestObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS( obsServ->AddObserver(rawRequestObserver, "dlp-request-sent-raw", false)); nsresult rv = SendRequestsCancelAndExpectResponse( mContentAnalysis, requests, CancelMechanism::eCancelByUserActionId, false /* aExpectFailure */); EXPECT_EQ(rv, NS_OK); auto rawRequests = rawRequestObserver->GetRequests(); EXPECT_EQ(static_cast(2), rawRequests.size()); EXPECT_EQ(rawRequests[0].user_action_id(), rawRequests[1].user_action_id()); EXPECT_EQ(rawRequests[0].user_action_id(), userActionId.get()); MOZ_ALWAYS_SUCCEEDS( obsServ->RemoveObserver(rawRequestObserver, "dlp-request-sent-raw")); } TEST_F(ContentAnalysisTest, CheckGivenUserActionIdsMustMatch) { nsCString userActionId1 = GenerateUUID(); nsCString userActionId2 = GenerateUUID(); nsCOMPtr uri = GetExampleDotComURI(); nsString allow1(L"allowMe"); RefPtr request1 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow1), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr, nullptr, nsCString(userActionId1)); // Use different text so the request doesn't match the cache nsString allow2(L"allowMeAgain"); RefPtr request2 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow2), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr, nullptr, nsCString(userActionId2)); nsTArray> requests{request1, request2}; nsresult rv = SendRequestsCancelAndExpectResponse( mContentAnalysis, requests, CancelMechanism::eCancelByUserActionId, true /* aExpectFailure */); EXPECT_EQ(rv, NS_ERROR_INVALID_ARG); } TEST_F(ContentAnalysisTest, CheckBrowserReportsTimeout) { // Submit a request to the agent and then timeout before we get a response. // When we do get a response later, check that we acknowledge as TOO_LATE. // A negative timeout tells Firefox to timeout after 25ms. The agent // always takes 100ms for requests in tests. TODO: can we further reduce // these? MOZ_ALWAYS_SUCCEEDS(Preferences::SetInt(kTimeoutPref, -1)); nsCOMPtr uri = GetExampleDotComURI(); nsString allow1(L"allowMe"); nsCOMPtr request1 = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow1), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); nsCOMPtr obsServ = mozilla::services::GetObserverService(); auto rawAcknowledgementObserver = MakeRefPtr(); MOZ_ALWAYS_SUCCEEDS(obsServ->AddObserver( rawAcknowledgementObserver, "dlp-acknowledgement-sent-raw", false)); SendRequestAndExpectResponse( mContentAnalysis, request1, Some(false) /* expectedShouldAllow */, Some(nsIContentAnalysisResponse::Action::eCanceled), Some(false) /* expectIsCached */); // The request returns before the ack is sent. Give it some time to catch up. bool hitTimeout = false; RefPtr timer = NS_NewCancelableRunnableFunction( "SendRequestsCancelAndExpectResponse timeout", [&] { hitTimeout = true; }); #if defined(MOZ_ASAN) // This can be pretty slow on ASAN builds (bug 1895256) constexpr uint32_t kCATimeoutMs = 25000; #else constexpr uint32_t kCATimeoutMs = 10000; #endif NS_DelayedDispatchToCurrentThread(do_AddRef(timer), kCATimeoutMs); mozilla::SpinEventLoopUntil("Waiting for ContentAnalysis result"_ns, [&]() { auto acknowledgements = rawAcknowledgementObserver->GetAcknowledgements(); if (acknowledgements.empty()) { return hitTimeout; } EXPECT_EQ(static_cast(1), acknowledgements.size()); EXPECT_EQ( ::content_analysis::sdk::ContentAnalysisAcknowledgement_FinalAction:: ContentAnalysisAcknowledgement_FinalAction_BLOCK, acknowledgements[0].final_action()); EXPECT_EQ(::content_analysis::sdk::ContentAnalysisAcknowledgement_Status:: ContentAnalysisAcknowledgement_Status_TOO_LATE, acknowledgements[0].status()); return true; }); timer->Cancel(); EXPECT_FALSE(hitTimeout); MOZ_ALWAYS_SUCCEEDS(obsServ->RemoveObserver(rawAcknowledgementObserver, "dlp-acknowledgement-sent-raw")); MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kTimeoutPref)); } TEST_F(ContentAnalysisTest, GetDiagnosticInfo_Initial) { RefPtr info = GetDiagnosticInfo(mContentAnalysis); EXPECT_TRUE(info->GetConnectedToAgent()); EXPECT_FALSE(info->GetFailedSignatureVerification()); nsString agentPath; MOZ_ALWAYS_SUCCEEDS(info->GetAgentPath(agentPath)); int32_t index = agentPath.Find(u"content_analysis_sdk_agent.exe"); EXPECT_EQ(agentPath.Length() - (sizeof("content_analysis_sdk_agent.exe") - 1), static_cast(index)); EXPECT_GE(info->GetRequestCount(), 0); } TEST_F(ContentAnalysisTest, GetDiagnosticInfo_AfterAgentTerminateAndOneRequest) { mAgentInfo.TerminateProcess(); nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectNoAgentResponse(mContentAnalysis, request); RefPtr info = GetDiagnosticInfo(mContentAnalysis); EXPECT_FALSE(info->GetConnectedToAgent()); EXPECT_FALSE(info->GetFailedSignatureVerification()); nsString agentPath; MOZ_ALWAYS_SUCCEEDS(info->GetAgentPath(agentPath)); EXPECT_TRUE(agentPath.IsEmpty()); EXPECT_GE(info->GetRequestCount(), 0); StartAgent(); } TEST_F(ContentAnalysisTest, GetDiagnosticInfo_AfterAgentTerminateAndReconnect) { mAgentInfo.TerminateProcess(); StartAgent(); nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectResponse(mContentAnalysis, request, Some(true), Some(nsIContentAnalysisResponse::eAllow), Nothing()); RefPtr info = GetDiagnosticInfo(mContentAnalysis); EXPECT_TRUE(info->GetConnectedToAgent()); EXPECT_FALSE(info->GetFailedSignatureVerification()); nsString agentPath; MOZ_ALWAYS_SUCCEEDS(info->GetAgentPath(agentPath)); int32_t index = agentPath.Find(u"content_analysis_sdk_agent.exe"); EXPECT_EQ(agentPath.Length() - (sizeof("content_analysis_sdk_agent.exe") - 1), static_cast(index)); EXPECT_GE(info->GetRequestCount(), 0); } TEST_F(ContentAnalysisTest, GetDiagnosticInfo_RequestCountIncreases) { nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); RefPtr info = GetDiagnosticInfo(mContentAnalysis); int64_t firstRequestCount = info->GetRequestCount(); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectResponse(mContentAnalysis, request, Some(true), Some(nsIContentAnalysisResponse::eAllow), Nothing()); info = GetDiagnosticInfo(mContentAnalysis); EXPECT_EQ(firstRequestCount + 1, info->GetRequestCount()); } TEST_F(ContentAnalysisTest, GetDiagnosticInfo_FailedSignatureVerification) { MOZ_ALWAYS_SUCCEEDS( Preferences::SetCString(kClientSignaturePref, "anInvalidSignature")); mAgentInfo.TerminateProcess(); StartAgent(); nsCOMPtr uri = GetExampleDotComURI(); nsString allow(L"allow"); nsCOMPtr request = new ContentAnalysisRequest( nsIContentAnalysisRequest::AnalysisType::eBulkDataEntry, nsIContentAnalysisRequest::Reason::eClipboardPaste, std::move(allow), false, EmptyCString(), uri, nsIContentAnalysisRequest::OperationType::eClipboard, nullptr); SendRequestAndExpectNoAgentResponse( mContentAnalysis, request, nsIContentAnalysisResponse::CancelError::eInvalidAgentSignature); RefPtr info = GetDiagnosticInfo(mContentAnalysis); EXPECT_FALSE(info->GetConnectedToAgent()); EXPECT_TRUE(info->GetFailedSignatureVerification()); MOZ_ALWAYS_SUCCEEDS(Preferences::ClearUser(kClientSignaturePref)); // Reset the agent so it's working for future tests mAgentInfo.TerminateProcess(); StartAgent(); }