From d4900df401c7e0ad06f909d6cd885e6194df6d47 Mon Sep 17 00:00:00 2001 From: Greg Stoll Date: Tue, 31 Oct 2023 16:38:55 +0000 Subject: [PATCH] Bug 1811076: Part 4 - Add gtests for correctly-behaving and misbehaving DLP agents Unit tests for mozilla::contentanalysis::ContentAnalysis class. ContentAnalysis must remain responsive, regardless of the behavior of the agent. Differential Revision: https://phabricator.services.mozilla.com/D189570 --- .../mozbuild/mozbuild/action/test_archive.py | 2 + python/mozbuild/mozbuild/artifacts.py | 1 + .../agent/src/event_win.h | 6 + .../content_analysis_sdk/demo/agent.cc | 36 +- .../content_analysis_sdk/demo/handler.h | 20 +- .../demo/handler_misbehaving.h | 495 ++++++++++++++++++ third_party/content_analysis_sdk/demo/modes.h | 25 + toolkit/components/contentanalysis/moz.build | 2 + .../tests/gtest/TestContentAnalysis.cpp | 132 +++++ .../tests/gtest/TestContentAnalysis.h | 24 + .../gtest/TestContentAnalysisMisbehaving.cpp | 416 +++++++++++++++ .../tests/gtest/TestContentAnalysisUtils.cpp | 75 +++ .../tests/gtest/agent/moz.build | 40 ++ .../tests/gtest/allowedFile.txt | 1 + .../tests/gtest/blockedFile.txt | 1 + .../contentanalysis/tests/gtest/moz.build | 23 + toolkit/components/protobuf/moz.build | 2 + 17 files changed, 1288 insertions(+), 13 deletions(-) create mode 100644 third_party/content_analysis_sdk/demo/handler_misbehaving.h create mode 100644 third_party/content_analysis_sdk/demo/modes.h create mode 100644 toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp create mode 100644 toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.h create mode 100644 toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp create mode 100644 toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp create mode 100644 toolkit/components/contentanalysis/tests/gtest/agent/moz.build create mode 100644 toolkit/components/contentanalysis/tests/gtest/allowedFile.txt create mode 100644 toolkit/components/contentanalysis/tests/gtest/blockedFile.txt create mode 100644 toolkit/components/contentanalysis/tests/gtest/moz.build diff --git a/python/mozbuild/mozbuild/action/test_archive.py b/python/mozbuild/mozbuild/action/test_archive.py index 6cd9ecc5a9f5..cf6ac08c0804 100644 --- a/python/mozbuild/mozbuild/action/test_archive.py +++ b/python/mozbuild/mozbuild/action/test_archive.py @@ -41,6 +41,7 @@ TEST_HARNESS_BINS = [ "crashinject", "geckodriver", "http3server", + "content_analysis_sdk_agent", "minidumpwriter", "pk12util", "screenshot", @@ -459,6 +460,7 @@ ARCHIVE_FILES = { "chrome/**", "chrome.manifest", "components/**", + "content_analysis_sdk_agent", "http3server", "*.ini", "localization/**", diff --git a/python/mozbuild/mozbuild/artifacts.py b/python/mozbuild/mozbuild/artifacts.py index d837516cb7c9..5e4e4758d7b3 100644 --- a/python/mozbuild/mozbuild/artifacts.py +++ b/python/mozbuild/mozbuild/artifacts.py @@ -795,6 +795,7 @@ class WinArtifactJob(ArtifactJob): ("bin/ssltunnel.exe", ("bin", "bin")), ("bin/xpcshell.exe", ("bin", "bin")), ("bin/http3server.exe", ("bin", "bin")), + ("bin/content_analysis_sdk_agent.exe", ("bin", "bin")), ("bin/plugins/gmp-*/*/*", ("bin/plugins", "bin")), ("bin/plugins/*", ("bin/plugins", "plugins")), ("bin/components/*", ("bin/components", "bin/components")), diff --git a/third_party/content_analysis_sdk/agent/src/event_win.h b/third_party/content_analysis_sdk/agent/src/event_win.h index 9f8b6903566f..f631f693dcd9 100644 --- a/third_party/content_analysis_sdk/agent/src/event_win.h +++ b/third_party/content_analysis_sdk/agent/src/event_win.h @@ -28,6 +28,12 @@ class ContentAnalysisEventWin : public ContentAnalysisEventBase { ResultCode Close() override; ResultCode Send() override; std::string DebugString() const override; + std::string SerializeStringToSendToBrowser() { + return agent_to_chrome()->SerializeAsString(); + } + void SetResponseSent() { response_sent_ = true; } + + HANDLE Pipe() const { return hPipe_; } private: void Shutdown(); diff --git a/third_party/content_analysis_sdk/demo/agent.cc b/third_party/content_analysis_sdk/demo/agent.cc index 93f75727385a..9246c197528a 100644 --- a/third_party/content_analysis_sdk/demo/agent.cc +++ b/third_party/content_analysis_sdk/demo/agent.cc @@ -10,6 +10,9 @@ #include "content_analysis/sdk/analysis_agent.h" #include "demo/handler.h" +#include "demo/handler_misbehaving.h" + +using namespace content_analysis::sdk; // Different paths are used depending on whether this agent should run as a // use specific agent or not. These values are chosen to match the test @@ -25,6 +28,8 @@ unsigned long delay = 0; // In seconds. unsigned long num_threads = 8u; std::string save_print_data_path = ""; RegexArray toBlock, toWarn, toReport; +static bool useMisbehavingHandler = false; +static std::string modeStr; // Command line parameters. constexpr const char* kArgDelaySpecific = "--delay="; @@ -35,9 +40,22 @@ constexpr const char* kArgUserSpecific = "--user"; constexpr const char* kArgToBlock = "--toblock="; constexpr const char* kArgToWarn = "--towarn="; constexpr const char* kArgToReport = "--toreport="; +constexpr const char* kArgMisbehave = "--misbehave="; constexpr const char* kArgHelp = "--help"; constexpr const char* kArgSavePrintRequestDataTo = "--save-print-request-data-to="; +std::map sStringToMode = { +#define AGENT_MODE(name) {#name, Mode::Mode_##name}, +#include "modes.h" +#undef AGENT_MODE +}; + +std::map sModeToString = { +#define AGENT_MODE(name) {Mode::Mode_##name, #name}, +#include "modes.h" +#undef AGENT_MODE +}; + std::vector> ParseRegex(const std::string str) { std::vector> ret; @@ -78,6 +96,9 @@ bool ParseCommandLine(int argc, char* argv[]) { toWarn = ParseRegex(arg.substr(strlen(kArgToWarn))); } else if (arg.find(kArgToReport) == 0) { toReport = ParseRegex(arg.substr(strlen(kArgToReport))); + } else if (arg.find(kArgMisbehave) == 0) { + modeStr = arg.substr(strlen(kArgMisbehave)); + useMisbehavingHandler = true; } else if (arg.find(kArgHelp) == 0) { return false; } else if (arg.find(kArgSavePrintRequestDataTo) == 0) { @@ -105,6 +126,7 @@ void PrintHelp() { << kArgToBlock << " : Regular expression matching file and text content to block." << std::endl << kArgToWarn << " : Regular expression matching file and text content to warn about." << std::endl << kArgToReport << " : Regular expression matching file and text content to report." << std::endl + << kArgMisbehave << " : Use 'misbehaving' agent in given mode for testing purposes." << std::endl << kArgHelp << " : prints this help message" << std::endl; } @@ -115,9 +137,17 @@ int main(int argc, char* argv[]) { } // TODO: Add toBlock, toWarn, toReport to QueueingHandler - auto handler = use_queue - ? std::make_unique(num_threads, delay, save_print_data_path) - : std::make_unique(delay, save_print_data_path, std::move(toBlock), std::move(toWarn), std::move(toReport)); + auto handler = + useMisbehavingHandler + ? MisbehavingHandler::Create(delay, modeStr) + : use_queue + ? std::make_unique(num_threads, delay, save_print_data_path) + : std::make_unique(delay, save_print_data_path, std::move(toBlock), std::move(toWarn), std::move(toReport)); + + if (!handler) { + std::cout << "[Demo] Failed to construct handler." << std::endl; + return 1; + } // Each agent uses a unique name to identify itself with Google Chrome. content_analysis::sdk::ResultCode rc; diff --git a/third_party/content_analysis_sdk/demo/handler.h b/third_party/content_analysis_sdk/demo/handler.h index 1eb94e9c7d60..71c0fec0e11a 100644 --- a/third_party/content_analysis_sdk/demo/handler.h +++ b/third_party/content_analysis_sdk/demo/handler.h @@ -51,8 +51,8 @@ class Handler : public content_analysis::sdk::AgentEventHandler { DumpEvent(stream, event.get()); bool success = true; - std::optional caResponse = - ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK; + std::optional caResponse = + content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK; if (event->GetRequest().has_text_content()) { caResponse = DecideCAResponse( @@ -80,16 +80,16 @@ class Handler : public content_analysis::sdk::AgentEventHandler { stream << " Verdict: "; if (caResponse) { switch (caResponse.value()) { - case ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK: + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK: stream << "BLOCK"; break; - case ContentAnalysisResponse_Result_TriggeredRule_Action_WARN: + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_WARN: stream << "WARN"; break; - case ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY: + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY: stream << "REPORT_ONLY"; break; - case ContentAnalysisResponse_Result_TriggeredRule_Action_ACTION_UNSPECIFIED: + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_ACTION_UNSPECIFIED: stream << "ACTION_UNSPECIFIED"; break; default: @@ -332,27 +332,27 @@ class Handler : public content_analysis::sdk::AgentEventHandler { return true; } - std::optional + std::optional DecideCAResponse(const std::string& content, std::stringstream& stream) { for (auto& r : toBlock_) { if (std::regex_search(content, r.second)) { stream << "'" << content << "' matches BLOCK regex '" << r.first << "'" << std::endl; - return ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK; + return content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK; } } for (auto& r : toWarn_) { if (std::regex_search(content, r.second)) { stream << "'" << content << "' matches WARN regex '" << r.first << "'" << std::endl; - return ContentAnalysisResponse_Result_TriggeredRule_Action_WARN; + return content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_WARN; } } for (auto& r : toReport_) { if (std::regex_search(content, r.second)) { stream << "'" << content << "' matches REPORT_ONLY regex '" << r.first << "'" << std::endl; - return ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY; + return content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY; } } stream << "'" << content << "' was ALLOWed\n"; diff --git a/third_party/content_analysis_sdk/demo/handler_misbehaving.h b/third_party/content_analysis_sdk/demo/handler_misbehaving.h new file mode 100644 index 000000000000..d303049d98fb --- /dev/null +++ b/third_party/content_analysis_sdk/demo/handler_misbehaving.h @@ -0,0 +1,495 @@ +/* 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/. */ + +#ifndef CONTENT_ANALYSIS_DEMO_HANDLER_MISBEHAVING_H_ +#define CONTENT_ANALYSIS_DEMO_HANDLER_MISBEHAVING_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "content_analysis/sdk/analysis.pb.h" +#include "content_analysis/sdk/analysis_agent.h" +#include "agent/src/event_win.h" + +enum class Mode { +// Have to use a "Mode_" prefix to avoid preprocessing problems in StringToMode +#define AGENT_MODE(name) Mode_##name, +#include "modes.h" +#undef AGENT_MODE +}; + +extern std::map sStringToMode; +extern std::map sModeToString; + +// Writes a string to the pipe. Returns ERROR_SUCCESS if successful, else +// returns GetLastError() of the write. This function does not return until +// the entire message has been sent (or an error occurs). +static DWORD WriteBigMessageToPipe(HANDLE pipe, const std::string& message) { + std::cout << "[demo] WriteBigMessageToPipe top, message size is " + << message.size() << std::endl; + if (message.empty()) { + return ERROR_SUCCESS; + } + + OVERLAPPED overlapped; + memset(&overlapped, 0, sizeof(overlapped)); + overlapped.hEvent = CreateEvent(/*securityAttr=*/nullptr, + /*manualReset=*/TRUE, + /*initialState=*/FALSE, + /*name=*/nullptr); + if (overlapped.hEvent == nullptr) { + return GetLastError(); + } + + DWORD err = ERROR_SUCCESS; + const char* cursor = message.data(); + for (DWORD size = message.length(); size > 0;) { + std::cout << "[demo] WriteBigMessageToPipe top of loop, remaining size " + << size << std::endl; + if (WriteFile(pipe, cursor, size, /*written=*/nullptr, &overlapped)) { + std::cout << "[demo] WriteBigMessageToPipe: success" << std::endl; + err = ERROR_SUCCESS; + break; + } + + // If an I/O is not pending, return the error. + err = GetLastError(); + if (err != ERROR_IO_PENDING) { + std::cout + << "[demo] WriteBigMessageToPipe: returning error from WriteFile " + << err << std::endl; + break; + } + + DWORD written; + if (!GetOverlappedResult(pipe, &overlapped, &written, /*wait=*/TRUE)) { + err = GetLastError(); + std::cout << "[demo] WriteBigMessageToPipe: returning error from " + "GetOverlappedREsult " + << err << std::endl; + break; + } + + // reset err for the next loop iteration + err = ERROR_SUCCESS; + std::cout << "[demo] WriteBigMessageToPipe: bottom of loop, wrote " + << written << std::endl; + cursor += written; + size -= written; + } + + CloseHandle(overlapped.hEvent); + return err; +} + +// An AgentEventHandler that does various misbehaving things +class MisbehavingHandler final : public content_analysis::sdk::AgentEventHandler { + public: + using Event = content_analysis::sdk::ContentAnalysisEvent; + + static + std::unique_ptr Create(unsigned long delay, + const std::string& modeStr) { + auto it = sStringToMode.find(modeStr); + if (it == sStringToMode.end()) { + std::cout << "\"" << modeStr << "\"" + << " is not a valid mode!" << std::endl; + return nullptr; + } + + return std::unique_ptr(new MisbehavingHandler(delay, it->second)); + } + + private: + MisbehavingHandler(unsigned long delay, Mode mode) : delay_(delay), mode_(mode) {} + + template + DWORD SendBytesOverPipe(const unsigned char (&bytes)[N], + const std::unique_ptr& event) { + content_analysis::sdk::ContentAnalysisEventWin* eventWin = + static_cast( + event.get()); + HANDLE pipe = eventWin->Pipe(); + std::string s(reinterpret_cast(bytes), N); + return WriteBigMessageToPipe(pipe, s); + } + + // Analyzes one request from Google Chrome and responds back to the browser + // with either an allow or block verdict. + void AnalyzeContent(std::unique_ptr event) { + // An event represents one content analysis request and response triggered + // by a user action in Google Chrome. The agent determines whether the + // user is allowed to perform the action by examining event->GetRequest(). + // The verdict, which can be "allow" or "block" is written into + // event->GetResponse(). + + std::cout << std::endl << "----------" << std::endl << std::endl; + + DumpRequest(event->GetRequest()); + std::cout << "Mode is " << sModeToString[mode_] << std::endl; + + if (mode_ == Mode::Mode_largeResponse) { + for (size_t i = 0; i < 1000; ++i) { + content_analysis::sdk::ContentAnalysisResponse_Result* result = + event->GetResponse().add_results(); + result->set_tag("someTag"); + content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule* + triggeredRule = result->add_triggered_rules(); + triggeredRule->set_rule_id("some_id"); + triggeredRule->set_rule_name("some_name"); + } + } else if (mode_ == + Mode::Mode_invalidUtf8StringStartByteIsContinuationByte) { + // protobuf docs say + // "A string must always contain UTF-8 encoded text." + // So let's try something invalid + // Anything with bits 10xxxxxx is only a continuation code point + event->GetResponse().set_request_token("\x80\x41\x41\x41"); + } else if (mode_ == + Mode::Mode_invalidUtf8StringEndsInMiddleOfMultibyteSequence) { + // f0 byte indicates there should be 3 bytes following it, but here + // there are only 2 + event->GetResponse().set_request_token("\x41\xf0\x90\x8d"); + } else if (mode_ == Mode::Mode_invalidUtf8StringOverlongEncoding) { + // codepoint U+20AC, should be encoded in 3 bytes (E2 82 AC) + // instead of 4 + event->GetResponse().set_request_token("\xf0\x82\x82\xac"); + } else if (mode_ == Mode::Mode_invalidUtf8StringMultibyteSequenceTooShort) { + // f0 byte indicates there should be 3 bytes following it, but here + // there are only 2 (\x41 is not a continuation byte) + event->GetResponse().set_request_token("\xf0\x90\x8d\x41"); + } else if (mode_ == Mode::Mode_invalidUtf8StringDecodesToInvalidCodePoint) { + // decodes to U+1FFFFF, but only up to U+10FFFF is a valid code point + event->GetResponse().set_request_token("\xf7\xbf\xbf\xbf"); + } else if (mode_ == Mode::Mode_stringWithEmbeddedNull) { + event->GetResponse().set_request_token("\x41\x00\x41"); + } else if (mode_ == Mode::Mode_zeroResults) { + event->GetResponse().clear_results(); + } else if (mode_ == Mode::Mode_resultWithInvalidStatus) { + // This causes an assertion failure and the process exits + // So we just serialize this ourselves below + /*content_analysis::sdk::ContentAnalysisResponse_Result* result = + event->GetResponse().mutable_results(0); + result->set_status( + static_cast< + ::content_analysis::sdk::ContentAnalysisResponse_Result_Status>( + 100));*/ + } else { + bool block = false; + + if (event->GetRequest().has_text_content()) { + block = ShouldBlockRequest(event->GetRequest().text_content()); + } else if (event->GetRequest().has_file_path()) { + block = ShouldBlockRequest(event->GetRequest().file_path()); + } + + if (block) { + auto rc = content_analysis::sdk::SetEventVerdictToBlock(event.get()); + std::cout << " Verdict: block"; + if (rc != content_analysis::sdk::ResultCode::OK) { + std::cout << " error: " + << content_analysis::sdk::ResultCodeToString(rc) + << std::endl; + std::cout << " " << event->DebugString() << std::endl; + } + std::cout << std::endl; + } else { + std::cout << " Verdict: allow" << std::endl; + } + } + + std::cout << std::endl; + + // If a delay is specified, wait that much. + if (delay_ > 0) { + std::cout << "[Demo] delaying request processing for " << delay_ << "s" + << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(delay_)); + } + + if (mode_ == Mode::Mode_largeResponse) { + content_analysis::sdk::ContentAnalysisEventWin* eventWin = + static_cast( + event.get()); + HANDLE pipe = eventWin->Pipe(); + std::cout << "largeResponse about to write" << std::endl; + DWORD result = WriteBigMessageToPipe( + pipe, eventWin->SerializeStringToSendToBrowser()); + std::cout << "largeResponse done writing with error " << result + << std::endl; + eventWin->SetResponseSent(); + } else if (mode_ == Mode::Mode_resultWithInvalidStatus) { + content_analysis::sdk::ContentAnalysisEventWin* eventWin = + static_cast( + event.get()); + HANDLE pipe = eventWin->Pipe(); + std::string serializedString = eventWin->SerializeStringToSendToBrowser(); + // The last byte is the status value. Set it to 100 + serializedString[serializedString.length() - 1] = 100; + WriteBigMessageToPipe(pipe, serializedString); + } else if (mode_ == Mode::Mode_messageTruncatedInMiddleOfString) { + unsigned char bytes[5]; + bytes[0] = 10; // field 1 (request_token), LEN encoding + bytes[1] = 13; // length 13 + bytes[2] = 65; // "A" + bytes[3] = 66; // "B" + bytes[4] = 67; // "C" + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageWithInvalidWireType) { + unsigned char bytes[5]; + bytes[0] = 15; // field 1 (request_token), "7" encoding (invalid value) + bytes[1] = 3; // length 3 + bytes[2] = 65; // "A" + bytes[3] = 66; // "B" + bytes[4] = 67; // "C" + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageWithUnusedFieldNumber) { + unsigned char bytes[5]; + bytes[0] = 82; // field 10 (this is invalid), LEN encoding + bytes[1] = 3; // length 3 + bytes[2] = 65; // "A" + bytes[3] = 66; // "B" + bytes[4] = 67; // "C" + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageWithWrongStringWireType) { + unsigned char bytes[2]; + bytes[0] = 10; // field 1 (request_token), VARINT encoding (but should be + // a string/LEN) + bytes[1] = 42; // value 42 + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageWithZeroTag) { + unsigned char bytes[1]; + // The protobuf deserialization code seems to handle this + // in a special case. + bytes[0] = 0; + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageWithZeroFieldButNonzeroWireType) { + // The protobuf deserialization code seems to handle this + // in a special case. + unsigned char bytes[5]; + bytes[0] = 2; // field 0 (invalid), LEN encoding + bytes[1] = 3; // length 13 + bytes[2] = 65; // "A" + bytes[3] = 66; // "B" + bytes[4] = 67; // "C" + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageWithGroupEnd) { + // GROUP_ENDs are obsolete and the deserialization code + // handles them in a special case. + unsigned char bytes[1]; + bytes[0] = 12; // field 1 (request_token), GROUP_END encoding + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageTruncatedInMiddleOfVarint) { + unsigned char bytes[2]; + bytes[0] = 16; // field 2 (status), VARINT encoding + bytes[1] = 128; // high bit is set, indicating there + // should be a byte after this + SendBytesOverPipe(bytes, event); + } else if (mode_ == Mode::Mode_messageTruncatedInMiddleOfTag) { + unsigned char bytes[1]; + bytes[0] = 128; // tag is actually encoded as a VARINT, so set the high + // bit, indicating there should be a byte after this + SendBytesOverPipe(bytes, event); + } else { + std::cout << "(misbehaving) Handler::AnalyzeContent() about to call " + "event->Send(), mode is " + << sModeToString[mode_] << std::endl; + // Send the response back to Google Chrome. + auto rc = event->Send(); + if (rc != content_analysis::sdk::ResultCode::OK) { + std::cout << "[Demo] Error sending response: " + << content_analysis::sdk::ResultCodeToString(rc) << std::endl; + std::cout << event->DebugString() << std::endl; + } + } + } + + private: + void OnBrowserConnected( + const content_analysis::sdk::BrowserInfo& info) override { + std::cout << std::endl << "==========" << std::endl; + std::cout << "Browser connected pid=" << info.pid << std::endl; + } + + void OnBrowserDisconnected( + const content_analysis::sdk::BrowserInfo& info) override { + std::cout << std::endl + << "Browser disconnected pid=" << info.pid << std::endl; + std::cout << "==========" << std::endl; + } + + void OnAnalysisRequested(std::unique_ptr event) override { + // If the agent is capable of analyzing content in the background, the + // events may be handled in background threads. Having said that, a + // event should not be assumed to be thread safe, that is, it should not + // be accessed by more than one thread concurrently. + // + // In this example code, the event is handled synchronously. + AnalyzeContent(std::move(event)); + } + void OnResponseAcknowledged( + const content_analysis::sdk::ContentAnalysisAcknowledgement& ack) + override { + const char* final_action = ""; + if (ack.has_final_action()) { + switch (ack.final_action()) { + case content_analysis::sdk::ContentAnalysisAcknowledgement:: + ACTION_UNSPECIFIED: + final_action = ""; + break; + case content_analysis::sdk::ContentAnalysisAcknowledgement::ALLOW: + final_action = "Allow"; + break; + case content_analysis::sdk::ContentAnalysisAcknowledgement::REPORT_ONLY: + final_action = "Report only"; + break; + case content_analysis::sdk::ContentAnalysisAcknowledgement::WARN: + final_action = "Warn"; + break; + case content_analysis::sdk::ContentAnalysisAcknowledgement::BLOCK: + final_action = "Block"; + break; + } + } + + std::cout << "Ack: " << ack.request_token() << std::endl; + std::cout << " Final action: " << final_action << std::endl; + } + void OnCancelRequests( + const content_analysis::sdk::ContentAnalysisCancelRequests& cancel) + override { + std::cout << "Cancel: " << std::endl; + std::cout << " User action ID: " << cancel.user_action_id() << std::endl; + } + + void OnInternalError(const char* context, + content_analysis::sdk::ResultCode error) override { + std::cout << std::endl + << "*ERROR*: context=\"" << context << "\" " + << content_analysis::sdk::ResultCodeToString(error) << std::endl; + } + + void DumpRequest( + const content_analysis::sdk::ContentAnalysisRequest& request) { + std::string connector = ""; + if (request.has_analysis_connector()) { + switch (request.analysis_connector()) { + case content_analysis::sdk::FILE_DOWNLOADED: + connector = "download"; + break; + case content_analysis::sdk::FILE_ATTACHED: + connector = "attach"; + break; + case content_analysis::sdk::BULK_DATA_ENTRY: + connector = "bulk-data-entry"; + break; + case content_analysis::sdk::PRINT: + connector = "print"; + break; + case content_analysis::sdk::FILE_TRANSFER: + connector = "file-transfer"; + break; + default: + break; + } + } + + std::string url = + request.has_request_data() && request.request_data().has_url() + ? request.request_data().url() + : ""; + + std::string tab_title = + request.has_request_data() && request.request_data().has_tab_title() + ? request.request_data().tab_title() + : ""; + + std::string filename = + request.has_request_data() && request.request_data().has_filename() + ? request.request_data().filename() + : ""; + + std::string digest = + request.has_request_data() && request.request_data().has_digest() + ? request.request_data().digest() + : ""; + + std::string file_path = + request.has_file_path() ? request.file_path() : ""; + + std::string text_content = + request.has_text_content() ? request.text_content() : ""; + + std::string machine_user = + request.has_client_metadata() && + request.client_metadata().has_browser() && + request.client_metadata().browser().has_machine_user() + ? request.client_metadata().browser().machine_user() + : ""; + + std::string email = + request.has_request_data() && request.request_data().has_email() + ? request.request_data().email() + : ""; + + time_t t = request.expires_at(); + + std::string user_action_id = request.has_user_action_id() + ? request.user_action_id() + : ""; + + std::cout << "Request: " << request.request_token() << std::endl; + std::cout << " User action ID: " << user_action_id << std::endl; + std::cout << " Expires at: " << ctime(&t); // Returned string includes \n. + std::cout << " Connector: " << connector << std::endl; + std::cout << " URL: " << url << std::endl; + std::cout << " Tab title: " << tab_title << std::endl; + std::cout << " Filename: " << filename << std::endl; + std::cout << " Digest: " << digest << std::endl; + std::cout << " Filepath: " << file_path << std::endl; + std::cout << " Text content: '" << text_content << "'" << std::endl; + std::cout << " Machine user: " << machine_user << std::endl; + std::cout << " Email: " << email << std::endl; + } + + bool ReadContentFromFile(const std::string& file_path, std::string* content) { + std::ifstream file(file_path, + std::ios::in | std::ios::binary | std::ios::ate); + if (!file.is_open()) return false; + + // Get file size. This example does not handle files larger than 1MB. + // Make sure content string can hold the contents of the file. + int size = file.tellg(); + if (size > 1024 * 1024) return false; + + content->resize(size + 1); + + // Read file into string. + file.seekg(0, std::ios::beg); + file.read(&(*content)[0], size); + content->at(size) = 0; + return true; + } + + bool ShouldBlockRequest(const std::string& content) { + // Determines if the request should be blocked. (not needed for the + // misbehaving agent) + std::cout << "'" << content << "' was not blocked\n"; + return false; + } + + unsigned long delay_; + Mode mode_; +}; + +#endif // CONTENT_ANALYSIS_DEMO_HANDLER_MISBEHAVING_H_ diff --git a/third_party/content_analysis_sdk/demo/modes.h b/third_party/content_analysis_sdk/demo/modes.h new file mode 100644 index 000000000000..debefc9d1a66 --- /dev/null +++ b/third_party/content_analysis_sdk/demo/modes.h @@ -0,0 +1,25 @@ +/* -*- 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/. */ + +// #define AGENT_MODE(name) to do what you want and then #include this file + +AGENT_MODE(largeResponse) +AGENT_MODE(invalidUtf8StringStartByteIsContinuationByte) +AGENT_MODE(invalidUtf8StringEndsInMiddleOfMultibyteSequence) +AGENT_MODE(invalidUtf8StringOverlongEncoding) +AGENT_MODE(invalidUtf8StringMultibyteSequenceTooShort) +AGENT_MODE(invalidUtf8StringDecodesToInvalidCodePoint) +AGENT_MODE(stringWithEmbeddedNull) +AGENT_MODE(zeroResults) +AGENT_MODE(resultWithInvalidStatus) +AGENT_MODE(messageTruncatedInMiddleOfString) +AGENT_MODE(messageWithInvalidWireType) +AGENT_MODE(messageWithUnusedFieldNumber) +AGENT_MODE(messageWithWrongStringWireType) +AGENT_MODE(messageWithZeroTag) +AGENT_MODE(messageWithZeroFieldButNonzeroWireType) +AGENT_MODE(messageWithGroupEnd) +AGENT_MODE(messageTruncatedInMiddleOfVarint) +AGENT_MODE(messageTruncatedInMiddleOfTag) diff --git a/toolkit/components/contentanalysis/moz.build b/toolkit/components/contentanalysis/moz.build index 02a461d9d2a5..e602d30302cd 100644 --- a/toolkit/components/contentanalysis/moz.build +++ b/toolkit/components/contentanalysis/moz.build @@ -58,3 +58,5 @@ DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True FINAL_LIBRARY = "xul" + +TEST_DIRS += ["tests/gtest"] diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp new file mode 100644 index 000000000000..1cf6d8fc226d --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.cpp @@ -0,0 +1,132 @@ +/* -*- 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/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "content_analysis/sdk/analysis_client.h" +#include "TestContentAnalysis.h" +#include +#include + +using namespace content_analysis::sdk; + +MozAgentInfo LaunchAgentNormal(const wchar_t* aToBlock) { + nsString cmdLineArguments; + if (aToBlock && aToBlock[0] != 0) { + cmdLineArguments.Append(L" --toblock=.*"); + cmdLineArguments.Append(aToBlock); + cmdLineArguments.Append(L".*"); + } + cmdLineArguments.Append(L" --user"); + cmdLineArguments.Append(L" --path="); + nsString pipeName; + GeneratePipeName(L"contentanalysissdk-gtest-", pipeName); + cmdLineArguments.Append(pipeName); + MozAgentInfo agentInfo; + LaunchAgentWithCommandLineArguments(cmdLineArguments, pipeName, agentInfo); + return agentInfo; +} + +TEST(ContentAnalysis, TextShouldNotBeBlocked) +{ + auto MozAgentInfo = LaunchAgentNormal(L"block"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("should succeed"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_STREQ("request token", response.request_token().c_str()); + ASSERT_EQ(1, response.results().size()); + ASSERT_EQ(ContentAnalysisResponse_Result_Status_SUCCESS, + response.results().Get(0).status()); + ASSERT_EQ(0, response.results().Get(0).triggered_rules_size()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysis, TextShouldBeBlocked) +{ + auto MozAgentInfo = LaunchAgentNormal(L"block"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("should be blocked"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_STREQ("request token", response.request_token().c_str()); + ASSERT_EQ(1, response.results().size()); + ASSERT_EQ(ContentAnalysisResponse_Result_Status_SUCCESS, + response.results().Get(0).status()); + ASSERT_EQ(1, response.results().Get(0).triggered_rules_size()); + ASSERT_EQ(ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK, + response.results().Get(0).triggered_rules(0).action()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysis, FileShouldNotBeBlocked) +{ + auto MozAgentInfo = LaunchAgentNormal(L"block"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_file_path("..\\..\\_tests\\gtest\\allowedFile.txt"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_STREQ("request token", response.request_token().c_str()); + ASSERT_EQ(1, response.results().size()); + ASSERT_EQ(ContentAnalysisResponse_Result_Status_SUCCESS, + response.results().Get(0).status()); + ASSERT_EQ(0, response.results().Get(0).triggered_rules_size()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysis, FileShouldBeBlocked) +{ + auto MozAgentInfo = LaunchAgentNormal(L"block"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_file_path("..\\..\\_tests\\gtest\\blockedFile.txt"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_STREQ("request token", response.request_token().c_str()); + ASSERT_EQ(1, response.results().size()); + ASSERT_EQ(ContentAnalysisResponse_Result_Status_SUCCESS, + response.results().Get(0).status()); + ASSERT_EQ(1, response.results().Get(0).triggered_rules_size()); + ASSERT_EQ(ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK, + response.results().Get(0).triggered_rules(0).action()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.h b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.h new file mode 100644 index 000000000000..9e310362625a --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysis.h @@ -0,0 +1,24 @@ +/* -*- 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/. */ +#ifndef mozilla_testcontentanalysis_h +#define mozilla_testcontentanalysis_h + +#include + +#include "content_analysis/sdk/analysis_client.h" +#include "gtest/gtest.h" +#include "nsString.h" + +struct MozAgentInfo { + PROCESS_INFORMATION processInfo; + std::unique_ptr client; +}; + +void GeneratePipeName(const wchar_t* prefix, nsString& pipeName); +void LaunchAgentWithCommandLineArguments(const nsString& cmdLineArguments, + const nsString& pipeName, + MozAgentInfo& agentInfo); +#endif diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp new file mode 100644 index 000000000000..0b005e1f6c48 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisMisbehaving.cpp @@ -0,0 +1,416 @@ +/* -*- 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/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "content_analysis/sdk/analysis_client.h" +#include "TestContentAnalysis.h" +#include +#include +#include + +using namespace content_analysis::sdk; + +namespace { +MozAgentInfo LaunchAgentMisbehaving(const wchar_t* mode) { + nsString cmdLineArguments; + cmdLineArguments.Append(L" --misbehave="); + cmdLineArguments.Append(mode); + cmdLineArguments.Append(L" --user"); + cmdLineArguments.Append(L" --path="); + nsString pipeName; + GeneratePipeName(L"contentanalysissdk-gtest-", pipeName); + cmdLineArguments.Append(pipeName); + MozAgentInfo agentInfo; + LaunchAgentWithCommandLineArguments(cmdLineArguments, pipeName, agentInfo); + return agentInfo; +} +} // namespace + +// Disabled for now +/*TEST(ContentAnalysisMisbehaving, LargeResponse) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"largeResponse"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_STREQ("request token", response.request_token().c_str()); + ASSERT_EQ(1001, response.results().size()); + + BOOL terminateResult = ::TerminateProcess(MozAgentInfo.processInfo.hProcess, +0); ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +}*/ + +TEST(ContentAnalysisMisbehaving, InvalidUtf8StringStartByteIsContinuationByte) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"invalidUtf8StringStartByteIsContinuationByte"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + // The protobuf spec says that strings must be valid UTF-8. So it's OK if + // this gets mangled, just want to make sure it doesn't cause a crash + // or invalid memory access or something. + ASSERT_STREQ("\x80\x41\x41\x41", response.request_token().c_str()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, + InvalidUtf8StringEndsInMiddleOfMultibyteSequence) +{ + auto MozAgentInfo = LaunchAgentMisbehaving( + L"invalidUtf8StringEndsInMiddleOfMultibyteSequence"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + // The protobuf spec says that strings must be valid UTF-8. So it's OK if + // this gets mangled, just want to make sure it doesn't cause a crash + // or invalid memory access or something. + ASSERT_STREQ("\x41\xf0\x90\x8d", response.request_token().c_str()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, InvalidUtf8StringMultibyteSequenceTooShort) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"invalidUtf8StringMultibyteSequenceTooShort"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + // The protobuf spec says that strings must be valid UTF-8. So it's OK if + // this gets mangled, just want to make sure it doesn't cause a crash + // or invalid memory access or something. + ASSERT_STREQ("\xf0\x90\x8d\x41", response.request_token().c_str()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, InvalidUtf8StringDecodesToInvalidCodePoint) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"invalidUtf8StringDecodesToInvalidCodePoint"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + // The protobuf spec says that strings must be valid UTF-8. So it's OK if + // this gets mangled, just want to make sure it doesn't cause a crash + // or invalid memory access or something. + ASSERT_STREQ("\xf7\xbf\xbf\xbf", response.request_token().c_str()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, InvalidUtf8StringOverlongEncoding) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"invalidUtf8StringOverlongEncoding"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + // The protobuf spec says that strings must be valid UTF-8. So it's OK if + // this gets mangled, just want to make sure it doesn't cause a crash + // or invalid memory access or something. + ASSERT_STREQ("\xf0\x82\x82\xac", response.request_token().c_str()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, StringWithEmbeddedNull) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"stringWithEmbeddedNull"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + std::string expected("\x41\x00\x41"); + ASSERT_EQ(expected, response.request_token()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, ZeroResults) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"zeroResults"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_EQ(0, response.results().size()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, ResultWithInvalidStatus) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"resultWithInvalidStatus"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + request.set_text_content("unused"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + ASSERT_EQ(1, response.results().size()); + // protobuf will fail to read this because it's an invalid value. + // (and leave status at its default value of 0) + // just make sure we can get the value without throwing + ASSERT_GE(static_cast(response.results(0).status()), 0); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfString) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"messageTruncatedInMiddleOfString"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageWithInvalidWireType) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"messageWithInvalidWireType"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageWithUnusedFieldNumber) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"messageWithUnusedFieldNumber"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + ASSERT_EQ(0, MozAgentInfo.client->Send(request, &response)); + // protobuf will read the value and store it in an unused section + // just make sure we can get a value without throwing + ASSERT_STREQ("", response.request_token().c_str()); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageWithWrongStringWireType) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"messageWithWrongStringWireType"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageWithZeroTag) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"messageWithZeroTag"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageWithZeroFieldButNonzeroWireType) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"messageWithZeroFieldButNonzeroWireType"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageWithGroupEnd) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"messageWithZeroFieldButNonzeroWireType"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfVarint) +{ + auto MozAgentInfo = + LaunchAgentMisbehaving(L"messageTruncatedInMiddleOfVarint"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} + +TEST(ContentAnalysisMisbehaving, MessageTruncatedInMiddleOfTag) +{ + auto MozAgentInfo = LaunchAgentMisbehaving(L"messageTruncatedInMiddleOfTag"); + // Exit the test early if the process failed to launch + ASSERT_NE(MozAgentInfo.processInfo.dwProcessId, 0UL); + ASSERT_NE(nullptr, MozAgentInfo.client.get()); + + ContentAnalysisRequest request; + request.set_request_token("request token"); + ContentAnalysisResponse response; + // The response is an invalid serialization of protobuf, so this should fail + ASSERT_EQ(-1, MozAgentInfo.client->Send(request, &response)); + + BOOL terminateResult = + ::TerminateProcess(MozAgentInfo.processInfo.hProcess, 0); + ASSERT_NE(FALSE, terminateResult) + << "Failed to terminate content_analysis_sdk_agent process"; +} diff --git a/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp new file mode 100644 index 000000000000..fc0cca5acd1a --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/TestContentAnalysisUtils.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "TestContentAnalysis.h" +#include +#include +#include +#include +#include + +void GeneratePipeName(const wchar_t* prefix, nsString& pipeName) { + pipeName = u""_ns; + pipeName.Append(prefix); + UUID uuid; + ASSERT_EQ(RPC_S_OK, UuidCreate(&uuid)); + // 39 == length of a UUID string including braces and NUL. + wchar_t guidBuf[39] = {}; + ASSERT_EQ(39, StringFromGUID2(uuid, guidBuf, 39)); + // omit opening and closing braces (and trailing null) + pipeName.Append(&guidBuf[1], 36); +} + +void LaunchAgentWithCommandLineArguments(const nsString& cmdLineArguments, + const nsString& pipeName, + MozAgentInfo& agentInfo) { + wchar_t progName[MAX_PATH] = {}; + // content_analysis_sdk_agent.exe is either next to firefox.exe (for local + // builds), or in ../../tests/bin/ (for try/treeherder builds) + DWORD nameSize = ::GetModuleFileNameW(nullptr, progName, MAX_PATH); + ASSERT_NE(DWORD{0}, nameSize); + ASSERT_EQ(S_OK, PathCchRemoveFileSpec(progName, nameSize)); + wchar_t normalizedPath[MAX_PATH] = {}; + nsString test1 = nsString(progName) + u"\\content_analysis_sdk_agent.exe"_ns; + ASSERT_EQ(S_OK, PathCchCanonicalize(normalizedPath, MAX_PATH, test1.get())); + nsString agentPath; + if (::PathFileExistsW(normalizedPath)) { + agentPath = nsString(normalizedPath); + } + if (agentPath.IsEmpty()) { + nsString unNormalizedPath = + nsString(progName) + + u"\\..\\..\\tests\\bin\\content_analysis_sdk_agent.exe"_ns; + ASSERT_EQ(S_OK, PathCchCanonicalize(normalizedPath, MAX_PATH, + unNormalizedPath.get())); + if (::PathFileExistsW(normalizedPath)) { + agentPath = nsString(normalizedPath); + } + } + ASSERT_FALSE(agentPath.IsEmpty()); + nsString localCmdLine = nsString(agentPath) + u" "_ns + cmdLineArguments; + STARTUPINFOW startupInfo = {sizeof(startupInfo)}; + PROCESS_INFORMATION processInfo; + BOOL ok = + ::CreateProcessW(nullptr, localCmdLine.get(), nullptr, nullptr, FALSE, 0, + nullptr, nullptr, &startupInfo, &processInfo); + // The documentation for CreateProcessW() says that any non-zero value is a + // success + if (!ok) { + // Show the last error + ASSERT_EQ(0UL, GetLastError()) + << "Failed to launch content_analysis_sdk_agent"; + } + // Allow time for the agent to set up the pipe + ::Sleep(2000); + content_analysis::sdk::Client::Config config; + config.name = NS_ConvertUTF16toUTF8(pipeName); + config.user_specific = true; + auto clientPtr = content_analysis::sdk::Client::Create(config); + ASSERT_NE(nullptr, clientPtr.get()); + + agentInfo.processInfo = processInfo; + agentInfo.client = std::move(clientPtr); +} diff --git a/toolkit/components/contentanalysis/tests/gtest/agent/moz.build b/toolkit/components/contentanalysis/tests/gtest/agent/moz.build new file mode 100644 index 000000000000..71a3a029ed42 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/agent/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Program("content_analysis_sdk_agent") + +DEFINES["UNICODE"] = True +DEFINES["GOOGLE_PROTOBUF_NO_RTTI"] = True +DEFINES["GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER"] = True + +SOURCES += [ + "../../../../../../third_party/content_analysis_sdk/agent/src/agent_base.cc", + "../../../../../../third_party/content_analysis_sdk/agent/src/agent_utils_win.cc", + "../../../../../../third_party/content_analysis_sdk/agent/src/agent_win.cc", + "../../../../../../third_party/content_analysis_sdk/agent/src/event_base.cc", + "../../../../../../third_party/content_analysis_sdk/agent/src/event_win.cc", + "../../../../../../third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.cc", + "../../../../../../third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.cc", + "../../../../../../third_party/content_analysis_sdk/common/utils_win.cc", + "../../../../../../third_party/content_analysis_sdk/demo/agent.cc", + "../../../content_analysis/sdk/analysis.pb.cc", +] + +LOCAL_INCLUDES += [ + "../../../", + "../../../../../../third_party/content_analysis_sdk", + "../../../../../../third_party/content_analysis_sdk/agent/include/", +] + +USE_LIBS += [ + "mozglue", + "protobuf", + "zlib", +] + +OS_LIBS += [ + "advapi32", +] diff --git a/toolkit/components/contentanalysis/tests/gtest/allowedFile.txt b/toolkit/components/contentanalysis/tests/gtest/allowedFile.txt new file mode 100644 index 000000000000..1564d2d1bc6a --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/allowedFile.txt @@ -0,0 +1 @@ +allow me \ No newline at end of file diff --git a/toolkit/components/contentanalysis/tests/gtest/blockedFile.txt b/toolkit/components/contentanalysis/tests/gtest/blockedFile.txt new file mode 100644 index 000000000000..58d958775d6e --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/blockedFile.txt @@ -0,0 +1 @@ +block me \ No newline at end of file diff --git a/toolkit/components/contentanalysis/tests/gtest/moz.build b/toolkit/components/contentanalysis/tests/gtest/moz.build new file mode 100644 index 000000000000..ce701987a495 --- /dev/null +++ b/toolkit/components/contentanalysis/tests/gtest/moz.build @@ -0,0 +1,23 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +LOCAL_INCLUDES += [ + "../..", + "../../../../../third_party/content_analysis_sdk/browser/include/", +] + +if CONFIG["OS_TARGET"] == "WINNT": + UNIFIED_SOURCES += [ + "TestContentAnalysis.cpp", + "TestContentAnalysisMisbehaving.cpp", + "TestContentAnalysisUtils.cpp", + ] + DIRS += ["agent"] + OS_LIBS += ["pathcch"] + +TEST_HARNESS_FILES.gtest += ["allowedFile.txt", "blockedFile.txt"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/protobuf/moz.build b/toolkit/components/protobuf/moz.build index 15514d069e45..8ab1705a7409 100644 --- a/toolkit/components/protobuf/moz.build +++ b/toolkit/components/protobuf/moz.build @@ -4,6 +4,8 @@ # 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/. +Library('protobuf') + with Files('**'): BUG_COMPONENT = ('Core', 'General')