diff options
Diffstat (limited to 'third_party/content_analysis_sdk')
62 files changed, 6264 insertions, 0 deletions
diff --git a/third_party/content_analysis_sdk/.gitignore b/third_party/content_analysis_sdk/.gitignore new file mode 100644 index 0000000000..0ab461830e --- /dev/null +++ b/third_party/content_analysis_sdk/.gitignore @@ -0,0 +1,6 @@ +.vscode/ +.ccls-cache/ +.cache/ +build/ +*.bak +*.swp diff --git a/third_party/content_analysis_sdk/CMakeLists.txt b/third_party/content_analysis_sdk/CMakeLists.txt new file mode 100644 index 0000000000..5dacc81031 --- /dev/null +++ b/third_party/content_analysis_sdk/CMakeLists.txt @@ -0,0 +1,214 @@ +# Copyright 2022 The Chromium Authors. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +cmake_minimum_required(VERSION 3.22) + +project(chrome_enterprise_connector_local_analysis) + +# Ensure a C++14 compiler is used. +set(CMAKE_CXX_STANDARD 14) + +# Determine the operating system being targeted. +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(WIN TRUE) + set(MAC FALSE) + set(LINUX FALSE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(WIN FALSE) + set(MAC TRUE) + set(LINUX FALSE) +else() + set(WIN FALSE) + set(MAC FALSE) + set(LINUX TRUE) +endif() + +# Set the path to the protoc protobuf compiler. +if(WIN) + set(PROTOC ${PROJECT_BINARY_DIR}/vcpkg/installed/x64-windows/tools/protobuf/protoc.exe) +elseif(MAC) + set(PROTOC ${PROJECT_BINARY_DIR}/vcpkg/installed/x64-osx/tools/protobuf/protoc) +elseif(LINUX) + set(PROTOC ${PROJECT_BINARY_DIR}/vcpkg/installed/x64-linux/tools/protobuf/protoc) +endif() + +# Calls the protoc compiler using the arguments specific to this project. +# protobuf_generate_cpp is not flexible enough for our needs. +add_custom_command( + OUTPUT ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc + COMMAND + ${PROTOC} + --cpp_out=${PROJECT_BINARY_DIR}/gen + --proto_path=${PROJECT_SOURCE_DIR}/proto + ${PROJECT_SOURCE_DIR}/proto/content_analysis/sdk/analysis.proto + DEPENDS ./proto/content_analysis/sdk/analysis.proto + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} +) +# Define proto target. Compile this target exclusively by calling: +# `cmake --build <build_dir> --target proto` +add_custom_target(proto + ALL + DEPENDS + ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc +) + +# The include directory contains the header files needed by the demo code. +# The gen directory contains generated protobuf headers describing the request +# and response objects used to communicate with Google Chrome. +set(AGENT_INCLUDES + ./agent/include + . + ${PROJECT_BINARY_DIR}/gen +) +set(BROWSER_INCLUDES + ./browser/include + . + ${PROJECT_BINARY_DIR}/gen +) + +# The SDK contains platform specific code for each of the supported platforms. +# ${PLATFORM_AGENT_CODE} holds the list of source files needed for the current +# platform being built. +if(WIN) + set(PLATFORM_AGENT_CODE + ./agent/src/agent_utils_win.cc + ./agent/src/agent_utils_win.h + ./agent/src/agent_win.cc + ./agent/src/agent_win.h + ./agent/src/event_win.cc + ./agent/src/event_win.h + ./agent/src/scoped_print_handle_win.cc + ./agent/src/scoped_print_handle_win.h + ./common/utils_win.cc + ./common/utils_win.h + ) + set(PLATFORM_TEST_CODE + ./agent/src/agent_win_unittest.cc + ./agent/src/event_win_unittest.cc + ) +elseif(MAC) + set(PLATFORM_AGENT_CODE + ./agent/src/agent_mac.cc + ./agent/src/agent_mac.h + ./agent/src/event_mac.cc + ./agent/src/event_mac.h + ./agent/src/scoped_print_handle_mac.cc + ./agent/src/scoped_print_handle_mac.h + ) + set(PLATFORM_TEST_CODE + ./agent/src/event_mac_unittest.cc + ) +elseif(LINUX) + set(PLATFORM_AGENT_CODE + ./agent/src/agent_posix.cc + ./agent/src/agent_posix.h + ./agent/src/event_posix.cc + ./agent/src/event_posix.h + ./agent/src/scoped_print_handle_posix.cc + ./agent/src/scoped_print_handle_posix.h + ) + set(PLATFORM_TEST_CODE + ./agent/src/event_posix_unittest.cc + ) +endif() + +# The SDK contains platform specific code for each of the supported platforms. +# ${PLATFORM_BROWSER_CODE} holds the list of source files needed for the current +# platform being built. +if(WIN) + set(PLATFORM_BROWSER_CODE + ./browser/src/client_win.cc + ./browser/src/client_win.h + ./common/utils_win.cc + ./common/utils_win.h + ) +elseif(MAC) + set(PLATFORM_BROWSER_CODE + ./browser/src/client_mac.cc + ./browser/src/client_mac.h + ) +elseif(LINUX) + set(PLATFORM_BROWSER_CODE + ./browser/src/client_posix.cc + ./browser/src/client_posix.h + ) +endif() + +# Makes available the package definitions in vcpkg. +include("${PROJECT_BINARY_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake") +find_package(Protobuf CONFIG REQUIRED) +# Unit tests +enable_testing() +find_package(GTest CONFIG REQUIRED) +include(GoogleTest) + +add_executable(unit_tests + ${PLATFORM_TEST_CODE} +) +set_property(TARGET unit_tests PROPERTY CXX_STANDARD 20) +target_include_directories(unit_tests + PRIVATE + ${AGENT_INCLUDES} + ${BROWSER_INCLUDES} +) +target_link_libraries(unit_tests + PUBLIC + cac_agent + cac_browser + GTest::gtest GTest::gtest_main +) + +gtest_discover_tests(unit_tests) + +# Builds the content analysis connector agent linker library. This library +# is linked into the agent in order to listen for and process content analysis +# requests from Google Chrome. +add_library(cac_agent + ./agent/include/content_analysis/sdk/analysis_agent.h + ./agent/include/content_analysis/sdk/result_codes.h + ./agent/src/agent_base.cc + ./agent/src/agent_base.h + ./agent/src/event_base.cc + ./agent/src/event_base.h + ./agent/src/scoped_print_handle_base.cc + ./agent/src/scoped_print_handle_base.h + ${PLATFORM_AGENT_CODE} + ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc +) +target_link_libraries(cac_agent + PUBLIC + protobuf::libprotoc + protobuf::libprotobuf + protobuf::libprotobuf-lite) +target_include_directories(cac_agent PRIVATE ${AGENT_INCLUDES}) +# Builds the content analysis connector browser linker library. This library +# is linked into the client in order to send content analysis requests to the +# agent. +add_library(cac_browser + ./browser/include/content_analysis/sdk/analysis_client.h + ./browser/src/client_base.cc + ./browser/src/client_base.h + ${PLATFORM_BROWSER_CODE} + ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc +) +target_include_directories(cac_browser PRIVATE ${BROWSER_INCLUDES}) +target_link_libraries(cac_browser + PUBLIC + protobuf::libprotoc + protobuf::libprotobuf + protobuf::libprotobuf-lite) + +# The demo agent executable. +add_executable(agent + ./demo/agent.cc + ./demo/handler.h +) +target_compile_features(agent PRIVATE cxx_std_17) +target_include_directories(agent PRIVATE ${AGENT_INCLUDES}) +target_link_libraries(agent PRIVATE cac_agent) + +# The demo client executable. +add_executable(browser ./demo/client.cc) +target_include_directories(browser PRIVATE ${BROWSER_INCLUDES}) +target_link_libraries(browser PRIVATE cac_browser) + diff --git a/third_party/content_analysis_sdk/LICENSE b/third_party/content_analysis_sdk/LICENSE new file mode 100644 index 0000000000..1ef68ff09e --- /dev/null +++ b/third_party/content_analysis_sdk/LICENSE @@ -0,0 +1,28 @@ +// Copyright 2022 The Chromium Authors. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google LLC nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/third_party/content_analysis_sdk/README.md b/third_party/content_analysis_sdk/README.md new file mode 100644 index 0000000000..f65a728b49 --- /dev/null +++ b/third_party/content_analysis_sdk/README.md @@ -0,0 +1,70 @@ +# Google Chrome Content Analysis Connector Agent SDK + +The Google Chrome Content Analysis Connector provides an official mechanism +allowing Data Loss Prevention (DLP) agents to more deeply integrate their +services with Google Chrome. + +DLP agents are background processes on managed computers that allow enterprises +to monitor locally running applications for data exfiltration events. They can +allow/block these activities based on customer defined DLP policies. + +This repository contains the SDK that DLP agents may use to become service +providers for the Google Chrome Content Analysis Connector. The code that must +be compiled and linked into the content analysis agent is located in the `agent` +subdirectory. + +A demo implementation of a service provider is located in the `demo` subdirectory. + +The code that must be compiled and linked into Google Chrome is located in +the `browser` subdirectory. + +The Protocol Buffer serialization format is used to serialize messages between the +browser and the agent. The protobuf definitions used can be found in the `proto` +subdirectory. + +## Google Protocol Buffers + +This SDK depends on Google Protocol Buffers version 3.18 or later. It may be +installed from Google's [download page](https://developers.google.com/protocol-buffers/docs/downloads#release-packages) +for your developement platform. It may also be installed using a package +manager. + +The included prepare_build scripts use the Microsoft [vcpkg](https://github.com/microsoft/vcpkg) +package manager to install protobuf. vcpkg is available on all supported +platforms. + +## Build + +### Pre-requisites + +The following must be installed on the computer before building the demo: + +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) version 2.33 or later. +- [cmake](https://cmake.org/install/) version 3.23 or later. +- A C++ compiler toolchain for your platform. +- On linux, the `realpath` shell tool, available in the `coreutils` package. + In Debian-based distributions use `sudo apt intall coreutils`. + In Red Hat distributions use `sudo yum install coreutils`. +- On Mac, use `brew install cmake coreutils pkg-config googletest` or an equivalent setup + +### Running prepare_build + +First get things ready by installing required dependencies: +``` +$SDK_DIR/prepare_build <build-dir> +``` +where `<build-dir>` is the path to a directory where the demo will be built. +By default, if no argument is provided, a directory named `build` will be +created in the project root. Any output within the `build/` directory will +be ignored by version control. + +`prepare_build` performs the following steps: +1. Downloads the vcpkg package manager. +2. Downloads and builds the Google Protocol Buffers library. +3. Creates build files for your specific platform. + +### Cmake Targets + +To build the demo run the command `cmake --build <build-dir>`. + +To build the protocol buffer targets run the command `cmake --build <build-dir> --target proto` diff --git a/third_party/content_analysis_sdk/agent/README.md b/third_party/content_analysis_sdk/agent/README.md new file mode 100644 index 0000000000..0df623b7b5 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/README.md @@ -0,0 +1,38 @@ +# Google Chrome Content Analysis Connector Agent SDK + +This directory holds the Google Chrome Content Analysis Connector Agent SDK. +An Agent is an OS process running on the same computer as Google Chrome +that listens for and processes content analysis requests from the browser. +Supported OSes are Windows, Mac, and Linux. + +## Google Protocol Buffers + +This SDK depends on Google Protocol Buffers version 3.18 or later. It may be +installed from Google's [download page](https://developers.google.com/protocol-buffers/docs/downloads#release-packages) +for your developement platform. It may also be installed using a package +manager. + +The included demo uses the Microsoft [vcpkg](https://github.com/microsoft/vcpkg) +package manager to install protobuf. vcpkg is available on all supported +platforms. See the demo for more details. + +## Adding the SDK into an agent + +Add the SDK to a content analysis agent as follows: + +1. Clone the SDK from [Github](https://github.com/chromium/content_analysis_sdk). +This document assumes that the SDK is downloaded and extracted into the +directory $SDK_DIR. + +2. Add the directory $SDK_DIR/include to the include path of the agent +code base. + +3. Add all the source files in the directory $SDK_DIR/src to the agent build. +Note that files ending in _win.cc or _win.h are meant only for Windows, files +ending in _posix.cc or _posix.h are meant only for Linux, and files ending in +_mac.cc or _mac.h are meant only for Mac. + +4. Reference the SDK in agent code using: +``` +#include "content_analysis/sdk/local_analysis.h" +```
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/analysis_agent.h b/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/analysis_agent.h new file mode 100644 index 0000000000..6f7617120a --- /dev/null +++ b/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/analysis_agent.h @@ -0,0 +1,288 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_AGENT_H_ +#define CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_AGENT_H_ + +#include <memory> +#include <string> + +#include "content_analysis/sdk/analysis.pb.h" +#include "content_analysis/sdk/result_codes.h" + +// This is the main include file for code using Content Analysis Connector +// Agent SDK. No other include is needed. +// +// An agent begins by creating an instance of Agent using the factory +// function Agent::Create(). This instance should live as long as the agent +// intends to receive content analysis requests from Google Chrome. +// +// Agent::Create() must be passed an object that implements the +// AgentEventHandler interface. Methods on this interface will be called +// at the approriate time to handle specific events. The events are: +// +// - A Google Chrome browser has started or stopped. This events contains +// information about the browser such as process id and executable path. +// - A request to analyze content. The agent reads and analyses the +// request in to determine a verdict: allow or block. When the verdict is +// known the response is sent back to Google Chrome. +// - An acknowledgement that Google Chrome has properly received the agent's +// verdict. +// +// The agent is not required to serialize event handling. That is, content +// analysis events can be analyze in the background and the response does +// not need to be sent before OnAnalysisRequested() returns. +// +// Google Chrome thottles the number of requests sent to the agent to 5 +// current requests at a time but this is subject to change. + +namespace content_analysis { +namespace sdk { + +// Represents information about one instance of a Google Chrome browser +// process that is connected to the agent. +struct BrowserInfo { + unsigned long pid = 0; // Process ID of Google Chrome browser process. + std::string binary_path; // The full path to the process's main binary. +}; + +// Represents one content analysis request as generated by a given user action +// in Google Chrome. +// +// The agent should retrieve information about the content analysis request +// using the GetRequest() method. The agent should analyze the request and +// update the response, returned by GetResponse(), with a verdict (allow or +// block). Once the verdict is set the response can be sent back to Google +// Chrome by calling Send(). +// +// The default verdict is to allow the requested user action. If the final +// verdict is to allow then the agent does not need to update the response and +// can simply call Send(). +// +// If the final verdict should be to block, the agent should first update the +// response by calling SetEventVerdictToBlock() before calling Send(). +// +// This class is not thread safe. However, it may be passed to another thread +// as long as the agent properly serializses access to the event. +// +// See the demo directory for an example of how to use this class. +class ContentAnalysisEvent { + public: + virtual ~ContentAnalysisEvent() = default; + + // Prepares the event for graceful shutdown. Upon return calls to all + // other methods of this class will fail. + virtual ResultCode Close() = 0; + + // Retrives information about the browser that generated this content + // analysis event. + virtual const BrowserInfo& GetBrowserInfo() const = 0; + + // Retrieves a read-only reference to the content analysis request received + // from Google Chrome. + virtual const ContentAnalysisRequest& GetRequest() const = 0; + + // Retrieves a writable reference to the content analysis response that will + // be sent to Google Chrome as the verdict for the request of this event. + // The agent may modify this response in place before calling Send(). + virtual ContentAnalysisResponse& GetResponse() = 0; + + // Send the verdict to Google Chrome. Once this method is called further + // changes to the response are ignored. + virtual ResultCode Send() = 0; + + // Returns a string containing internal state of the object that is useful + // for debugging. + virtual std::string DebugString() const = 0; + + protected: + ContentAnalysisEvent() = default; + ContentAnalysisEvent(const ContentAnalysisEvent& rhs) = delete; + ContentAnalysisEvent(ContentAnalysisEvent&& rhs) = delete; + ContentAnalysisEvent& operator=(const ContentAnalysisEvent& rhs) = delete; + ContentAnalysisEvent& operator=(ContentAnalysisEvent&& rhs) = delete; + +}; + +// Agents should implement this interface in order to handle events as needed. +// +// OnBrowserConnected() and OnBrowserDisonnected() notify the agent when +// instances of Google Chome start and stop. The agent may perform any one-time +// actions as required for these events. The default action is to do nothing +// for both events. If the agent does not need perform any special actions +// these methods do not need to be overridden. +// +// OnAnalysisRequested() notifies the agent of a new content analysis request +// from Google Chrome. The agent should perform the analysis and respond to +// the event. It is not required for the agent complete the analysis and +// respond to before this callback returns. The agent may pass the +// ContentAnalysisEvent to a background task and respond when ready. This +// callback has no default action and agents must override it. +// +// OnResponseAcknowledged() notifies the agent that Google Chrome has received +// the content analysis response and how it has handled it. +class AgentEventHandler { + public: + AgentEventHandler() = default; + virtual ~AgentEventHandler() = default; + + // Called when a new Google Chrome browser instance connects to the agent. + // This is always called before the first OnAnalysisRequested() from that + // browser. + virtual void OnBrowserConnected(const BrowserInfo& info) {} + + // Called when a Google Chrome browser instance disconnects from the agent. + // The agent will no longer receive new content analysis requests from this + // browser. + virtual void OnBrowserDisconnected(const BrowserInfo& info) {} + + // Called when a Google Chrome browser requests a content analysis. + virtual void OnAnalysisRequested( + std::unique_ptr<ContentAnalysisEvent> event) = 0; + + // Called when a Google Chrome browser acknowledges the content analysis + // response from the agent. The default action is to do nothing. + // If the agent does not need perform any special actions this methods does + // not need to be overridden. + virtual void OnResponseAcknowledged( + const ContentAnalysisAcknowledgement& ack) {} + + // Called when a Google Chrome browser asks the agent to cancels one or + // more content analysis requests. This happens when the user presses the + // Cancel button in the in-progress dialog. This is expected to be a best + // effort only; agents may choose to ignore this message or possibly only + // cancel a subset of requests with the given user action id. + // + // The default action is to do nothing. If the agent does not need perform + // any special actions this methods does not need to be overridden. + virtual void OnCancelRequests( + const ContentAnalysisCancelRequests& cancel) {} + + // Called whenever the Agent implementation detects an error. `context` + // is a string that provide a hint to the handler as to where the error + // happened in the agent. `error` represent the actual error detected. + virtual void OnInternalError(const char* context, ResultCode error) {} +}; + +// Represents an agent that can perform content analysis for the Google Chrome +// browser. This class holds the server endpoint that Google Chrome connects +// to when content analysis is required. +// +// Agent instances should outlive all ContentAnalysisEvent instances created +// with it. Agent instances are not thread safe except for Stop() which can be +// called from any thread to shutdown the agent. Outstanding +// ContentAnalysisEvents created from this agent may or may not still complete. +// +// See the demo directory for an example of how to use this class. +class Agent { + public: + // Configuration options where creating an agent. `name` is used to create + // a channel between the agent and Google Chrome. + struct Config { + // Used to create a channel between the agent and Google Chrome. Both must + // use the same name to properly rendezvous with each other. The channel + // is platform specific. + std::string name; + + // Set to true if there is a different agent instance per OS user. Defaults + // to false. + bool user_specific = false; + }; + + // Creates a new agent instance. If successful, an agent is returned. + // Otherwise a nullptr is returned and `rc` contains the reason for the + // failure. + static std::unique_ptr<Agent> Create( + Config config, + std::unique_ptr<AgentEventHandler> handler, + ResultCode* rc); + + virtual ~Agent() = default; + + // Returns the configuration parameters used to create the agent. + virtual const Config& GetConfig() const = 0; + + // Handles events triggered on this agent and calls the coresponding + // callbacks in the AgentEventHandler. This method is blocking and returns + // when Stop() is called or if an error occurs. + virtual ResultCode HandleEvents() = 0; + + // Prepares the agent for graceful shutdown. Any function blocked on + // HandleEvents() will return. It is safe to call this method from any + // thread. + virtual ResultCode Stop() = 0; + + // Returns a string containing internal state of the object that is useful + // for debugging. + virtual std::string DebugString() const = 0; + + protected: + Agent() = default; + Agent(const Agent& rhs) = delete; + Agent(Agent&& rhs) = delete; + Agent& operator=(const Agent& rhs) = delete; + Agent& operator=(Agent&& rhs) = delete; +}; + +// Update the tag or status of `response`. This function assumes that the +// response contains only one Result. If one already exists it is updated +// otherwise a new Result is created. +// +// The response contained within ContentAnalysisEvent has already been updated. +// This function is useful only when create a new instance of +// ContentAnalysisResponse. +// +// If `tag` is not empty it will replace the result's tag. +// If `status` is not STATUS_UNKNOWN it will will replace the result's status. +ResultCode UpdateResponse(ContentAnalysisResponse& response, + const std::string& tag, + ContentAnalysisResponse::Result::Status status); + +// Sets the response verdict of an event to `action`. This is a convenience +// function that is equivalent to the following: +// +// auto result = event->GetResponse().mutable_results(0); +// auto rule = result->mutable_triggered_rules(0); +// rule->set_action(action); +// +// This function assumes the event's response has already been initialized +// using UpdateResponse(). +ResultCode SetEventVerdictTo( + ContentAnalysisEvent* event, + ContentAnalysisResponse::Result::TriggeredRule::Action action); + +// Sets the reponse verdict of an event to "block". This is a convenience +// function that is equivalent to the following: +// +// SetEventVerdictTo(event, +// ContentAnalysisResponse::Result::TriggeredRule::BLOCK); +ResultCode SetEventVerdictToBlock(ContentAnalysisEvent* event); + +// Helper class to handle the lifetime and access of print data. +class ScopedPrintHandle { + public: + virtual ~ScopedPrintHandle() = default; + virtual const char* data() = 0; + virtual size_t size() = 0; + + protected: + ScopedPrintHandle() = default; + + ScopedPrintHandle(const ScopedPrintHandle&) = delete; + ScopedPrintHandle& operator=(const ScopedPrintHandle&) = delete; + + ScopedPrintHandle(ScopedPrintHandle&&) = default; + ScopedPrintHandle& operator=(ScopedPrintHandle&&) = default; +}; + +// Returns a `ScopedPrintHandle` initialized from the request's print data +// if it exists. +std::unique_ptr<ScopedPrintHandle> +CreateScopedPrintHandle(const ContentAnalysisRequest& request, + int64_t browser_pid); + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_AGENT_H_ diff --git a/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.h b/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.h new file mode 100644 index 0000000000..e2a858c069 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.h @@ -0,0 +1,36 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_RESULT_CODES_H_ +#define CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_RESULT_CODES_H_ + +#include <stdint.h> + +namespace content_analysis { +namespace sdk { + +// Result codes of methods and functions. + +#define RC_RECOVERABLE(RC, MSG) RC, +#define RC_UNRECOVERABLE(RC, MSG) RC, +enum class ResultCode { +#include "content_analysis/sdk/result_codes.inc" +}; +#undef RC_RECOVERABLE +#undef RC_UNRECOVERABLE + +// Returns true if the error is recoverable. A recoverable errors means the +// agent may still receive new requests from Google Chrome. An unrecoverable +// error means the agent is unlikely to get more request from Google Chrome. +inline bool IsRecoverableError(ResultCode rc) { + return rc < ResultCode::ERR_FIRST_UNRECOVERABLE_ERROR; +} + +// Returns a human readable error for the given result code. +const char* ResultCodeToString(ResultCode rc); + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_RESULT_CODES_H_ diff --git a/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.inc b/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.inc new file mode 100644 index 0000000000..05155ecb1e --- /dev/null +++ b/third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.inc @@ -0,0 +1,25 @@ +// This file is #included from C++ headers and source code to generate code +// specific to each ResultCode. The including code is expected to #define +// macros for RC_RECOVERABLE and RC_UNRECOVERABLE before #including this file +// and then #undef then after use. + +RC_RECOVERABLE(OK, "Operation completed successfully.") +RC_RECOVERABLE(ERR_MISSING_RESULT, "Response is missing a result message.") +RC_RECOVERABLE(ERR_RESPONSE_ALREADY_SENT, "A resonse has already been sent for this request.") +RC_RECOVERABLE(ERR_MISSING_REQUEST_TOKEN, "The request is missing a request token.") +RC_RECOVERABLE(ERR_AGENT_NOT_INITIALIZED, "The agent is not proplerly initialized to handle events.") +RC_RECOVERABLE(ERR_INVALID_REQUEST_FROM_BROWSER, "The browser sent an incorrectly formatted message.") +RC_RECOVERABLE(ERR_IO_PENDING, "IO incomplete, the operation is still pending.") +RC_RECOVERABLE(ERR_MORE_DATA, "There is more data to read before the entire message has been received.") +RC_RECOVERABLE(ERR_CANNOT_GET_BROWSER_PID, "Cannot get process Id of browser.") +RC_RECOVERABLE(ERR_CANNOT_GET_BROWSER_BINARY_PATH, "Cannot get the full path to the brower's main binary file.") +RC_RECOVERABLE(ERR_BROKEN_PIPE, "Browser process has disconnected.") +RC_RECOVERABLE(ERR_UNEXPECTED, "An internal error has occured.") + +// All unrecoverable errors should be declared below ERR_FIRST_UNRECOVERABLE_ERROR. +RC_UNRECOVERABLE(ERR_FIRST_UNRECOVERABLE_ERROR, "Marker for the first unrecoverable error.") +RC_UNRECOVERABLE(ERR_AGENT_ALREADY_EXISTS, "Another process is already running as an agent on this computer.") +RC_UNRECOVERABLE(ERR_AGENT_EVENT_HANDLER_NOT_SPECIFIED, "An agent handler was not specified when creating an agent.") +RC_UNRECOVERABLE(ERR_CANNOT_CREATE_AGENT_STOP_EVENT, "Could not create event to signal the agent to stop.") +RC_UNRECOVERABLE(ERR_INVALID_CHANNEL_NAME, "Invalid channel name specified in Agent::Config.") +RC_UNRECOVERABLE(ERR_CANNOT_CREATE_CHANNEL_IO_EVENT, "Could not create event to perform async IO with a client.") diff --git a/third_party/content_analysis_sdk/agent/src/agent_base.cc b/third_party/content_analysis_sdk/agent/src/agent_base.cc new file mode 100644 index 0000000000..c584dac5b6 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_base.cc @@ -0,0 +1,42 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <utility> + +#include "agent_base.h" + +namespace content_analysis { +namespace sdk { + +AgentBase::AgentBase(Config config, std::unique_ptr<AgentEventHandler> handler) + : config_(std::move(config)), handler_(std::move(handler)) {} + +const Agent::Config& AgentBase::GetConfig() const { + return config_; +} + +ResultCode AgentBase::Stop() { + return ResultCode::OK; +} + +ResultCode AgentBase::NotifyError(const char* context, ResultCode error) { + if (handler_) { + handler_->OnInternalError(context, error); + } + return error; +} + +#define RC_RECOVERABLE(RC, MSG) case ResultCode::RC: return MSG; +#define RC_UNRECOVERABLE(RC, MSG) case ResultCode::RC: return MSG; +const char* ResultCodeToString(ResultCode rc) { + switch (rc) { +#include "content_analysis/sdk/result_codes.inc" + } + return "Unknown error code."; +} +#undef RC_RECOVERABLE +#undef RC_UNRECOVERABLE + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/agent_base.h b/third_party/content_analysis_sdk/agent/src/agent_base.h new file mode 100644 index 0000000000..8ce7df29ef --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_base.h @@ -0,0 +1,40 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_AGENT_BASE_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_AGENT_BASE_H_ + +#include <memory> + +#include "content_analysis/sdk/analysis_agent.h" + +namespace content_analysis { +namespace sdk { + +// Base Agent class with code common to all platforms. +class AgentBase : public Agent { + public: + // Agent: + const Config& GetConfig() const override; + ResultCode Stop() override; + + protected: + AgentBase(Config config, std::unique_ptr<AgentEventHandler> handler); + + AgentEventHandler* handler() const { return handler_.get(); } + const Config& configuration() const { return config_; } + + // Notifies the handler of the given error. Returns the error + // passed into the method. + ResultCode NotifyError(const char* context, ResultCode error); + + private: + Config config_; + std::unique_ptr<AgentEventHandler> handler_; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_AGENT_BASE_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/agent/src/agent_mac.cc b/third_party/content_analysis_sdk/agent/src/agent_mac.cc new file mode 100644 index 0000000000..5bbac5fe49 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_mac.cc @@ -0,0 +1,34 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "agent_mac.h" +#include "event_mac.h" + +namespace content_analysis { +namespace sdk { + +// static +std::unique_ptr<Agent> Agent::Create( + Config config, + std::unique_ptr<AgentEventHandler> handler, + ResultCode* rc) { + *rc = ResultCode::ERR_UNEXPECTED; + return std::make_unique<AgentMac>(std::move(config), std::move(handler)); +} + +AgentMac::AgentMac( + Config config, + std::unique_ptr<AgentEventHandler> handler) + : AgentBase(std::move(config), std::move(handler)) {} + +ResultCode AgentMac::HandleEvents() { + return ResultCode::ERR_UNEXPECTED; +} + +std::string AgentMac::DebugString() const { + return std::string(); +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/agent_mac.h b/third_party/content_analysis_sdk/agent/src/agent_mac.h new file mode 100644 index 0000000000..6fd3d49f7b --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_mac.h @@ -0,0 +1,27 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_SRC_AGENT_MAC_H_ +#define CONTENT_ANALYSIS_SRC_AGENT_MAC_H_ + +#include "agent_base.h" + +namespace content_analysis { +namespace sdk { + +// Agent implementaton for macOS. +class AgentMac : public AgentBase { + public: + AgentMac(Config config, std::unique_ptr<AgentEventHandler> handler); + + ResultCode HandleEvents() override; + std::string DebugString() const override; + + // TODO(rogerta): Fill in implementation. +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_SRC_AGENT_MAC_H_ diff --git a/third_party/content_analysis_sdk/agent/src/agent_posix.cc b/third_party/content_analysis_sdk/agent/src/agent_posix.cc new file mode 100644 index 0000000000..c206261213 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_posix.cc @@ -0,0 +1,36 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <utility> + +#include "agent_posix.h" +#include "event_posix.h" + +namespace content_analysis { +namespace sdk { + +// static +std::unique_ptr<Agent> Agent::Create( + Config config, + std::unique_ptr<AgentEventHandler> handler, + ResultCode* rc) { + *rc = ResultCode::ERR_UNEXPECTED; + return std::make_unique<AgentPosix>(std::move(config), std::move(handler)); +} + +AgentPosix::AgentPosix( + Config config, + std::unique_ptr<AgentEventHandler> handler) + : AgentBase(std::move(config), std::move(handler)) {} + +ResultCode AgentPosix::HandleEvents() { + return ResultCode::ERR_UNEXPECTED; +} + +std::string AgentPosix::DebugString() const { + return std::string(); +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/agent_posix.h b/third_party/content_analysis_sdk/agent/src/agent_posix.h new file mode 100644 index 0000000000..cd9e67ad25 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_posix.h @@ -0,0 +1,27 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_SRC_AGENT_POSIX_H_ +#define CONTENT_ANALYSIS_SRC_AGENT_POSIX_H_ + +#include "agent_base.h" + +namespace content_analysis { +namespace sdk { + +// Agent implementaton for linux. +class AgentPosix : public AgentBase { + public: + AgentPosix(Config config, std::unique_ptr<AgentEventHandler> handler); + + ResultCode HandleEvents() override; + std::string DebugString() const override; + + // TODO(rogerta): Fill in implementation. +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_SRC_AGENT_POSIX_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/agent/src/agent_utils_win.cc b/third_party/content_analysis_sdk/agent/src/agent_utils_win.cc new file mode 100644 index 0000000000..5a5f411d84 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_utils_win.cc @@ -0,0 +1,28 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <windows.h> + +#include "content_analysis/sdk/result_codes.h" + +namespace content_analysis { +namespace sdk { + +#define ERR_TO_RC(ERR, RC) case ERR: return ResultCode::RC; + +ResultCode ErrorToResultCode(DWORD err) { + switch (err) { + ERR_TO_RC(ERROR_SUCCESS, OK); + ERR_TO_RC(ERROR_ACCESS_DENIED, ERR_AGENT_ALREADY_EXISTS); + ERR_TO_RC(ERROR_BROKEN_PIPE, ERR_BROKEN_PIPE); + ERR_TO_RC(ERROR_INVALID_NAME, ERR_INVALID_CHANNEL_NAME); + ERR_TO_RC(ERROR_MORE_DATA, ERR_MORE_DATA); + ERR_TO_RC(ERROR_IO_PENDING, ERR_IO_PENDING); + default: + return ResultCode::ERR_UNEXPECTED; + } +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/agent_utils_win.h b/third_party/content_analysis_sdk/agent/src/agent_utils_win.h new file mode 100644 index 0000000000..7368ac794e --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_utils_win.h @@ -0,0 +1,19 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ +#define CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ + +#include "event_base.h" + +namespace content_analysis { +namespace sdk { + +// Maps a Windows error status code (ERROR_xxx codes) to an SDK result code. +ResultCode ErrorToResultCode(DWORD err); + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ diff --git a/third_party/content_analysis_sdk/agent/src/agent_win.cc b/third_party/content_analysis_sdk/agent/src/agent_win.cc new file mode 100644 index 0000000000..5d0b2c62b5 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_win.cc @@ -0,0 +1,546 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <sstream> +#include <utility> +#include <vector> + +#include <windows.h> +#include <sddl.h> + +#include "common/utils_win.h" + +#include "agent_utils_win.h" +#include "agent_win.h" +#include "event_win.h" + +namespace content_analysis { +namespace sdk { + +// The minimum number of pipe in listening mode. This is greater than one to +// handle the case of multiple instance of Google Chrome browser starting +// at the same time. +const DWORD kMinNumListeningPipeInstances = 2; + +// The minimum number of handles to wait on. This is the minimum number +// of pipes in listening mode plus the stop event. +const DWORD kMinNumWaitHandles = kMinNumListeningPipeInstances + 1; + +// static +std::unique_ptr<Agent> Agent::Create( + Config config, + std::unique_ptr<AgentEventHandler> handler, + ResultCode* rc) { + auto agent = std::make_unique<AgentWin>(std::move(config), std::move(handler), rc); + return *rc == ResultCode::OK ? std::move(agent) : nullptr; +} + +AgentWin::Connection::Connection(const std::string& pipename, + bool user_specific, + AgentEventHandler* handler, + bool is_first_pipe, + ResultCode* rc) + : handler_(handler) { + *rc = ResultCode::OK; + memset(&overlapped_, 0, sizeof(overlapped_)); + // Create a manual reset event as specified for overlapped IO. + // Use default security attriutes and no name since this event is not + // shared with other processes. + overlapped_.hEvent = CreateEvent(/*securityAttr=*/nullptr, + /*manualReset=*/TRUE, + /*initialState=*/FALSE, + /*name=*/nullptr); + if (!overlapped_.hEvent) { + *rc = ResultCode::ERR_CANNOT_CREATE_CHANNEL_IO_EVENT; + return; + } + + *rc = ResetInternal(pipename, user_specific, is_first_pipe); +} + +AgentWin::Connection::~Connection() { + Cleanup(); + + if (handle_ != INVALID_HANDLE_VALUE) { + CloseHandle(handle_); + } + + // Invalid event handles are represented as null. + if (overlapped_.hEvent) { + CloseHandle(overlapped_.hEvent); + } +} + +ResultCode AgentWin::Connection::Reset( + const std::string& pipename, + bool user_specific) { + return NotifyIfError("ConnectionReset", + ResetInternal(pipename, user_specific, false)); +} + +ResultCode AgentWin::Connection::HandleEvent(HANDLE handle) { + auto rc = ResultCode::OK; + DWORD count; + BOOL success = GetOverlappedResult(handle, &overlapped_, &count, + /*wait=*/FALSE); + if (!is_connected_) { + // This connection is currently listing for a new connection from a Google + // Chrome browser. If the result is a success, this means the browser has + // connected as expected. Otherwise an error occured so report it to the + // caller. + if (success) { + // A Google Chrome browser connected to the agent. Reset this + // connection object to handle communication with the browser and then + // tell the handler about it. + + is_connected_ = true; + buffer_.resize(internal::kBufferSize); + + rc = BuildBrowserInfo(); + if (rc == ResultCode::OK) { + handler_->OnBrowserConnected(browser_info_); + } + } else { + rc = ErrorToResultCode(GetLastError()); + NotifyIfError("GetOverlappedResult", rc); + } + } else { + // Some data has arrived from Google Chrome. This data is (part of) an + // instance of the proto message `ChromeToAgent`. + // + // If the message is small it is received in by one call to ReadFile(). + // If the message is larger it is received in by multiple calls to + // ReadFile(). + // + // `success` is true if the data just read is the last bytes for a message. + // Otherwise it is false. + rc = OnReadFile(success, count); + } + + // If all data has been read, queue another read. + if (rc == ResultCode::OK || rc == ResultCode::ERR_MORE_DATA) { + rc = QueueReadFile(rc == ResultCode::OK); + } + + if (rc != ResultCode::OK && rc != ResultCode::ERR_IO_PENDING && + rc != ResultCode::ERR_MORE_DATA) { + Cleanup(); + } else { + // Don't propagate all the "success" error codes to the called to keep + // this simpler. + rc = ResultCode::OK; + } + + return rc; +} + +void AgentWin::Connection::AppendDebugString(std::stringstream& state) const { + state << "{handle=" << handle_; + state << " connected=" << is_connected_; + state << " pid=" << browser_info_.pid; + state << " rsize=" << read_size_; + state << " fsize=" << final_size_; + state << "}"; +} + +ResultCode AgentWin::Connection::ConnectPipe() { + // In overlapped mode, connecting to a named pipe always returns false. + if (ConnectNamedPipe(handle_, &overlapped_)) { + return ErrorToResultCode(GetLastError()); + } + + DWORD err = GetLastError(); + if (err == ERROR_IO_PENDING) { + // Waiting for a Google Chrome Browser to connect. + return ResultCode::OK; + } else if (err == ERROR_PIPE_CONNECTED) { + // A Google Chrome browser is already connected. Make sure event is in + // signaled state in order to process the connection. + if (SetEvent(overlapped_.hEvent)) { + err = ERROR_SUCCESS; + } else { + err = GetLastError(); + } + } + + return ErrorToResultCode(err); +} + +ResultCode AgentWin::Connection::ResetInternal(const std::string& pipename, + bool user_specific, + bool is_first_pipe) { + auto rc = ResultCode::OK; + + // If this is the not the first time, disconnect from any existing Google + // Chrome browser. Otherwise creater a new pipe. + if (handle_ != INVALID_HANDLE_VALUE) { + if (!DisconnectNamedPipe(handle_)) { + rc = ErrorToResultCode(GetLastError()); + } + } else { + rc = ErrorToResultCode( + internal::CreatePipe(pipename, user_specific, is_first_pipe, &handle_)); + } + + // Make sure event starts in reset state. + if (rc == ResultCode::OK && !ResetEvent(overlapped_.hEvent)) { + rc = ErrorToResultCode(GetLastError()); + } + + if (rc == ResultCode::OK) { + rc = ConnectPipe(); + } + + if (rc != ResultCode::OK) { + Cleanup(); + handle_ = INVALID_HANDLE_VALUE; + } + + return rc; +} + +void AgentWin::Connection::Cleanup() { + if (is_connected_ && handler_) { + handler_->OnBrowserDisconnected(browser_info_); + } + + is_connected_ = false; + browser_info_ = BrowserInfo(); + buffer_.clear(); + cursor_ = nullptr; + read_size_ = 0; + final_size_ = 0; + + if (handle_ != INVALID_HANDLE_VALUE) { + // Cancel all outstanding IO requests on this pipe by using a null for + // overlapped. + CancelIoEx(handle_, /*overlapped=*/nullptr); + } + + // This function does not close `handle_` or the event in `overlapped` so + // that the server can resuse the pipe with an new Google Chrome browser + // instance. +} + +ResultCode AgentWin::Connection::QueueReadFile(bool reset_cursor) { + if (reset_cursor) { + cursor_ = buffer_.data(); + read_size_ = buffer_.size(); + final_size_ = 0; + } + + // When this function is called there are the following possiblities: + // + // 1/ Data is already available and the buffer is filled in. ReadFile() + // return TRUE and the event is set. + // 2/ Data is not avaiable yet. ReadFile() returns FALSE and the last error + // is ERROR_IO_PENDING(997) and the event is reset. + // 3/ Some error occurred, like for example Google Chrome stops. ReadFile() + // returns FALSE and the last error is something other than + // ERROR_IO_PENDING, for example ERROR_BROKEN_PIPE(109). The event + // state is unchanged. + auto rc = ResultCode::OK; + DWORD count; + if (!ReadFile(handle_, cursor_, read_size_, &count, &overlapped_)) { + DWORD err = GetLastError(); + rc = ErrorToResultCode(err); + + // IO pending is not an error so don't notify. + // + // Ignore broken pipes for notifications since that happens when the Google + // Chrome browser shuts down. The agent will be notified of a browser + // disconnect in that case. + // + // More data means that `buffer_` was too small to read the entire message + // from the browser. The buffer has already been resized. Another call to + // ReadFile() is needed to get the remainder. + if (rc != ResultCode::ERR_IO_PENDING && + rc != ResultCode::ERR_BROKEN_PIPE && + rc != ResultCode::ERR_MORE_DATA) { + NotifyIfError("QueueReadFile", rc, err); + } + } + + return rc; +} + +ResultCode AgentWin::Connection::OnReadFile(BOOL done_reading, DWORD count) { + final_size_ += count; + + // If `done_reading` is TRUE, this means the full message has been read. + // Call the appropriate handler method. + if (done_reading) { + return CallHandler(); + } + + // Otherwise there are two possibilities: + // + // 1/ The last error is ERROR_MORE_DATA(234). This means there are more + // bytes to read before the request message is complete. Resize the + // buffer and adjust the cursor. The caller will queue up another read + // and wait. don't notify the handler since this is not an error. + // 2/ Some error occured. In this case notify the handler and return the + // error. + + DWORD err = GetLastError(); + if (err == ERROR_MORE_DATA) { + read_size_ = internal::kBufferSize; + buffer_.resize(buffer_.size() + read_size_); + cursor_ = buffer_.data() + buffer_.size() - read_size_; + return ErrorToResultCode(err); + } + + return NotifyIfError("OnReadFile", ErrorToResultCode(err)); +} + +ResultCode AgentWin::Connection::CallHandler() { + ChromeToAgent message; + if (!message.ParseFromArray(buffer_.data(), final_size_)) { + // Malformed message. + return NotifyIfError("ParseChromeToAgent", + ResultCode::ERR_INVALID_REQUEST_FROM_BROWSER); + } + + auto rc = ResultCode::OK; + + if (message.has_request()) { + // This is a request from Google Chrome to perform a content analysis + // request. + // + // Move the request from `message` to the event to reduce the amount + // of memory allocation/copying and also because the the handler takes + // ownership of the event. + auto event = std::make_unique<ContentAnalysisEventWin>( + handle_, browser_info_, std::move(*message.mutable_request())); + rc = event->Init(); + if (rc == ResultCode::OK) { + handler_->OnAnalysisRequested(std::move(event)); + } else { + NotifyIfError("RequestValidation", rc); + } + } else if (message.has_ack()) { + // This is an ack from Google Chrome that it has received a content + // analysis response from the agent. + handler_->OnResponseAcknowledged(message.ack()); + } else if (message.has_cancel()) { + // Google Chrome is informing the agent that the content analysis + // request(s) associated with the given user action id have been + // canceled by the user. + handler_->OnCancelRequests(message.cancel()); + } else { + // Malformed message. + rc = NotifyIfError("NoRequestOrAck", + ResultCode::ERR_INVALID_REQUEST_FROM_BROWSER); + } + + return rc; +} + +ResultCode AgentWin::Connection::BuildBrowserInfo() { + if (!GetNamedPipeClientProcessId(handle_, &browser_info_.pid)) { + return NotifyIfError("BuildBrowserInfo", + ResultCode::ERR_CANNOT_GET_BROWSER_PID); + } + + if (!internal::GetProcessPath(browser_info_.pid, + &browser_info_.binary_path)) { + return NotifyIfError("BuildBrowserInfo", + ResultCode::ERR_CANNOT_GET_BROWSER_BINARY_PATH); + } + + return ResultCode::OK; +} + +ResultCode AgentWin::Connection::NotifyIfError( + const char* context, + ResultCode rc, + DWORD err) { + if (handler_ && rc != ResultCode::OK) { + std::stringstream stm; + stm << context << " pid=" << browser_info_.pid; + if (err != ERROR_SUCCESS) { + stm << context << " err=" << err; + } + + handler_->OnInternalError(stm.str().c_str(), rc); + } + return rc; +} + +AgentWin::AgentWin( + Config config, + std::unique_ptr<AgentEventHandler> event_handler, + ResultCode* rc) + : AgentBase(std::move(config), std::move(event_handler)) { + *rc = ResultCode::OK; + if (handler() == nullptr) { + *rc = ResultCode::ERR_AGENT_EVENT_HANDLER_NOT_SPECIFIED; + return; + } + + stop_event_ = CreateEvent(/*securityAttr=*/nullptr, + /*manualReset=*/TRUE, + /*initialState=*/FALSE, + /*name=*/nullptr); + if (stop_event_ == nullptr) { + *rc = ResultCode::ERR_CANNOT_CREATE_AGENT_STOP_EVENT; + return; + } + + std::string pipename = + internal::GetPipeNameForAgent(configuration().name, + configuration().user_specific); + if (pipename.empty()) { + *rc = ResultCode::ERR_INVALID_CHANNEL_NAME; + return; + } + + pipename_ = pipename; + + connections_.reserve(kMinNumListeningPipeInstances); + for (DWORD i = 0; i < kMinNumListeningPipeInstances; ++i) { + connections_.emplace_back( + std::make_unique<Connection>(pipename_, configuration().user_specific, + handler(), i == 0, rc)); + if (*rc != ResultCode::OK || !connections_.back()->IsValid()) { + Shutdown(); + break; + } + } +} + +AgentWin::~AgentWin() { + Shutdown(); +} + +ResultCode AgentWin::HandleEvents() { + std::vector<HANDLE> wait_handles; + auto rc = ResultCode::OK; + bool stopped = false; + while (!stopped && rc == ResultCode::OK) { + rc = HandleOneEvent(wait_handles, &stopped); + } + + return rc; +} + +ResultCode AgentWin::Stop() { + SetEvent(stop_event_); + return AgentBase::Stop(); +} + +std::string AgentWin::DebugString() const { + std::stringstream state; + state.setf(std::ios::boolalpha); + state << "AgentWin{pipe=\"" << pipename_; + state << "\" stop=" << stop_event_; + + for (size_t i = 0; i < connections_.size(); ++i) { + state << " conn@" << i; + connections_[i]->AppendDebugString(state); + } + + state << "}" << std::ends; + return state.str(); +} + +void AgentWin::GetHandles(std::vector<HANDLE>& wait_handles) const { + // Reserve enough space in the handles vector to include the stop event plus + // all connections. + wait_handles.clear(); + wait_handles.reserve(1 + connections_.size()); + + for (auto& state : connections_) { + HANDLE wait_handle = state->GetWaitHandle(); + if (!wait_handle) { + wait_handles.clear(); + break; + } + wait_handles.push_back(wait_handle); + } + + // Push the stop event last so that connections_ index calculations in + // HandleOneEvent() don't have to account for this handle. + wait_handles.push_back(stop_event_); +} + +ResultCode AgentWin::HandleOneEventForTesting() { + std::vector<HANDLE> wait_handles; + bool stopped; + return HandleOneEvent(wait_handles, &stopped); +} + +bool AgentWin::IsAClientConnectedForTesting() { + for (const auto& state : connections_) { + if (state->IsConnected()) { + return true; + } + } + return false; +} + +ResultCode AgentWin::HandleOneEvent( + std::vector<HANDLE>& wait_handles, + bool* stopped) { + *stopped = false; + + // Wait on the specified handles for an event to occur. + GetHandles(wait_handles); + if (wait_handles.size() < kMinNumWaitHandles) { + return NotifyError("GetHandles", ResultCode::ERR_AGENT_NOT_INITIALIZED); + } + + DWORD index = WaitForMultipleObjects( + wait_handles.size(), wait_handles.data(), + /*waitAll=*/FALSE, /*timeoutMs=*/INFINITE); + if (index == WAIT_FAILED) { + return NotifyError("WaitForMultipleObjects", + ErrorToResultCode(GetLastError())); + } + + // If the index of signaled handle is the last one in wait_handles, then the + // stop event was signaled. + index -= WAIT_OBJECT_0; + if (index == wait_handles.size() - 1) { + *stopped = true; + return ResultCode::OK; + } + + auto& connection = connections_[index]; + bool was_listening = !connection->IsConnected(); + auto rc = connection->HandleEvent(wait_handles[index]); + if (rc != ResultCode::OK) { + // If `connection` was not listening and there are more than + // kMinNumListeningPipeInstances pipes, delete this connection. Otherwise + // reset it so that it becomes a listener. + if (!was_listening && + connections_.size() > kMinNumListeningPipeInstances) { + connections_.erase(connections_.begin() + index); + } else { + rc = connection->Reset(pipename_, configuration().user_specific); + } + } + + // If `connection` was listening and is now connected, create a new + // one so that there are always kMinNumListeningPipeInstances listening. + if (rc == ResultCode::OK && was_listening && connection->IsConnected()) { + connections_.emplace_back( + std::make_unique<Connection>(pipename_, configuration().user_specific, + handler(), false, &rc)); + } + + return ResultCode::OK; +} + +void AgentWin::Shutdown() { + connections_.clear(); + pipename_.clear(); + if (stop_event_ != nullptr) { + CloseHandle(stop_event_); + stop_event_ = nullptr; + } +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/agent_win.h b/third_party/content_analysis_sdk/agent/src/agent_win.h new file mode 100644 index 0000000000..a6a0c73311 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_win.h @@ -0,0 +1,196 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_AGENT_WIN_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_AGENT_WIN_H_ + +#include <windows.h> + +#include <memory> +#include <sstream> +#include <string> +#include <vector> + +#include "agent_base.h" + +namespace content_analysis { +namespace sdk { + +// Agent implementaton for Windows. +class AgentWin : public AgentBase { + public: + // Creates a new agent given the specific configuration and handler. + // If an error occurs during creation, `rc` is set to the specific + // error. Otherwise `rc` is ResultCode::OK. + AgentWin(Config config, std::unique_ptr<AgentEventHandler> handler, + ResultCode* rc); + ~AgentWin() override; + + // Agent: + ResultCode HandleEvents() override; + ResultCode Stop() override; + std::string DebugString() const override; + + // Handles one pipe event and returns. Used only in tests. + ResultCode HandleOneEventForTesting(); + + // Returns true if there is at least one client connected to this agent. + bool IsAClientConnectedForTesting(); + +private: + // Represents one connection to a Google Chrome browser, or one pipe + // listening for a Google Chrome browser to connect. + class Connection { + public: + // Starts listening on a pipe with the given name. `is_first_pipe` should + // be true only for the first pipe created by the agent. If an error + // occurs while creating the connction object it is returned in `rc`. + // If no errors occur then rc is set to ResultCode::OK. + // + // When `user_specific` is true there is a different agent instance per OS + // user. + // + // `Connection` objects cannot be copied or moved because the OVERLAPPED + // structure cannot be changed or moved in memory while an I/O operation + // is in progress. + Connection(const std::string& pipename, + bool user_specific, + AgentEventHandler* handler, + bool is_first_pipe, + ResultCode* rc); + Connection(const Connection& other) = delete; + Connection(Connection&& other) = delete; + Connection& operator=(const Connection& other) = delete; + Connection& operator=(Connection&& other) = delete; + ~Connection(); + + bool IsValid() const { return handle_ != INVALID_HANDLE_VALUE; } + bool IsConnected() const { return is_connected_; } + HANDLE GetWaitHandle() const { return overlapped_.hEvent; } + + // Resets this connection object to listen for a new Google Chrome browser. + // When `user_specific` is true there is a different agent instance per OS + // user. + ResultCode Reset(const std::string& pipename, bool user_specific); + + // Hnadles an event for this connection. `wait_handle` corresponds to + // this connections wait handle. + ResultCode HandleEvent(HANDLE wait_handle); + + // Append debuf information to the string stream. + void AppendDebugString(std::stringstream& state) const; + + private: + // Listens for a new connection from Google Chrome. + ResultCode ConnectPipe(); + + // Resets this connection object to listen for a new Google Chrome browser. + // When `user_specific` is true there is a different agent instance per OS + // user. + ResultCode ResetInternal(const std::string& pipename, + bool user_specific, + bool is_first_pipe); + + // Cleans up this connection object so that it can be reused with a new + // Google Chroem browser instance. The handles assocated with this object + // are not closed. On return, this object is neither connected nor + // listening and any buffer used to hold browser messages are cleared. + void Cleanup(); + + // Queues a read on the pipe to receive a message from Google Chrome. + // ERROR_SUCCESS, ERROR_IO_PENDING, and ERROR_MORE_DATA are successful + // return values. Other values represent an error with the connection. + // If `reset_cursor` is true the internal read buffer cursor is reset to + // the start of the buffer, otherwise it is unchanged. + ResultCode QueueReadFile(bool reset_cursor); + + // Called when data from Google Chrome is available for reading from the + // pipe. ERROR_SUCCESS and ERROR_MORE_DATA are both successful return + // values. Other values represent an error with the connection. + // + // `done_reading` is true if the code has finished reading an entire message + // from chrome. Regardless of whether reading is done, `count` contains + // the number of bytes read. + // + // If `done_reading` is true, the data received from the browser is parsed + // as if it were a `ChromeToAgent` proto message and the handler is called + // as needed. + // + // If `done_reading` is false, the data received from the browser is + // appended to the data already received from the browser. `buffer_` is + // resized to allow reading more data from the browser. + // + // In all cases the caller is expected to use QueueReadFile() to continue + // reading data from the browser. + ResultCode OnReadFile(BOOL done_reading, DWORD count); + + // Calls the appropriate method the handler depending on the message + // received from Google Chrome. + ResultCode CallHandler(); + + // Fills in the browser_info_ member of this Connection. Assumes + // IsConnected() is true. + ResultCode BuildBrowserInfo(); + + // Notifies the handler of the given error iff `rc` is not equal to + // ResultCode::OK. Appends the Google Chrome browser process id to the + // context before calling the handler. Also append `err` to the context + // if it is not ERROR_SUCCESS. + // + // Returns the error passed into the method. + ResultCode NotifyIfError(const char* context, + ResultCode rc, + DWORD err=ERROR_SUCCESS); + + // The handler to call for various agent events. + AgentEventHandler* handler_ = nullptr; + + // Members used to communicate with Google Chrome. + HANDLE handle_ = INVALID_HANDLE_VALUE; + OVERLAPPED overlapped_; + + // True if this connection is assigned to a specific Google Chrome browser, + // otherwise this connection is listening for a new browser. + bool is_connected_ = false; + + // Information about the Google Chrome browser process. + BrowserInfo browser_info_; + + // Members used to read messages from Google Chrome. + std::vector<char> buffer_; + char* cursor_ = nullptr; + DWORD read_size_ = 0; + DWORD final_size_ = 0; + }; + + // Returns handles that can be used to wait for events from all handles + // managed by this agent. This includes all connection objects and the + // stop event. The stop event is always last in the list. + void GetHandles(std::vector<HANDLE>& wait_handles) const; + + // Handles one pipe event and returns. If the return value is + // ResultCode::OK, the `stopped` argument is set to true if the agent + // should stop handling more events. If the return value is not + // ResultCode::OK, `stopped` is undefined. + ResultCode HandleOneEvent(std::vector<HANDLE>& wait_handles, bool* stopped); + + // Performs a clean shutdown of the agent. + void Shutdown(); + + // Name used to create the pipes between the agent and Google Chrome browsers. + std::string pipename_; + + // A list of pipes to already connected Google Chrome browsers. + // The first kMinNumListeningPipeInstances pipes in the list correspond to + // listening pipes. + std::vector<std::unique_ptr<Connection>> connections_; + + // An event that is set when the agent should stop. Set in Stop(). + HANDLE stop_event_ = nullptr; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_AGENT_WIN_H_ diff --git a/third_party/content_analysis_sdk/agent/src/agent_win_unittest.cc b/third_party/content_analysis_sdk/agent/src/agent_win_unittest.cc new file mode 100644 index 0000000000..c0bddf82f4 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_win_unittest.cc @@ -0,0 +1,522 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <latch> +#include <memory> +#include <thread> + +#include "agent/src/agent_win.h" +#include "agent/src/event_win.h" +#include "browser/src/client_win.h" +#include "gtest/gtest.h" + +namespace content_analysis { +namespace sdk { +namespace testing { + +// A handler that counts the number of times the callback methods are invoked. +// Also remembers the last BrowserInfo structure passed to it from any of the +// callbacks. +struct TestHandler : public AgentEventHandler { + void OnBrowserConnected(const BrowserInfo& info) override { + last_info_ = info; + ++connect_count_; + } + void OnBrowserDisconnected(const BrowserInfo& info) override { + last_info_ = info; + ++disconnect_count_; + } + void OnAnalysisRequested( + std::unique_ptr<ContentAnalysisEvent> event) override { + ++request_count_; + ResultCode ret = event->Send(); + ASSERT_EQ(ResultCode::OK, ret); + } + void OnResponseAcknowledged( + const ContentAnalysisAcknowledgement& ack) override { + ++ack_count_; + } + void OnCancelRequests( + const ContentAnalysisCancelRequests& cancel) override { + ++cancel_count_; + } + + int connect_count_ = 0; + int disconnect_count_ = 0; + int request_count_ = 0; + int ack_count_ = 0; + int cancel_count_ = 0; + BrowserInfo last_info_; +}; + +// A test handler that closes its event before sending the response. +struct CloseEventTestHandler : public TestHandler { + void OnAnalysisRequested( + std::unique_ptr<ContentAnalysisEvent> event) override { + ++request_count_; + + // Closing the event before sending should generate an error. + ResultCode ret = event->Close(); + ASSERT_EQ(ResultCode::OK, ret); + + ret = event->Send(); + ASSERT_NE(ResultCode::OK, ret); + } +}; + +// A test handler that attempts to send two responses for a given request. +struct DoubleSendTestHandler : public TestHandler { + void OnAnalysisRequested( + std::unique_ptr<ContentAnalysisEvent> event) override { + ++request_count_; + + ResultCode ret = event->Send(); + ASSERT_EQ(ResultCode::OK, ret); + + // Trying to send again fails. + ret = event->Send(); + ASSERT_NE(ResultCode::OK, ret); + } +}; + +// A test handler that signals a latch after a client connects. +// Can only be used with one client. +struct SignalClientConnectedTestHandler : public TestHandler { + void OnBrowserConnected(const BrowserInfo& info) override { + TestHandler::OnBrowserConnected(info); + wait_for_client.count_down(); + } + + std::latch wait_for_client{ 1 }; +}; + +// A test handler that signals a latch after a request is processed. +// Can only be used with one request. +struct SignalClientRequestedTestHandler : public TestHandler { + void OnAnalysisRequested( + std::unique_ptr<ContentAnalysisEvent> event) override { + TestHandler::OnAnalysisRequested(std::move(event)); + wait_for_request.count_down(); + } + + std::latch wait_for_request{ 1 }; +}; + +std::unique_ptr<AgentWin> CreateAgent( + Agent::Config config, + TestHandler** handler_ptr, + ResultCode expected_rc=ResultCode::OK) { + ResultCode rc; + auto handler = std::make_unique<TestHandler>(); + *handler_ptr = handler.get(); + auto agent = std::make_unique<AgentWin>( + std::move(config), std::move(handler), &rc); + EXPECT_EQ(expected_rc, rc); + return agent; +} + +std::unique_ptr<ClientWin> CreateClient( + Client::Config config) { + int rc; + auto client = std::make_unique<ClientWin>(std::move(config), &rc); + return rc == 0 ? std::move(client) : nullptr; +} + +ContentAnalysisRequest BuildRequest(std::string content=std::string()) { + ContentAnalysisRequest request; + request.set_request_token("req-token"); + *request.add_tags() = "dlp"; + request.set_text_content(content); // Moved. + return request; +} + +TEST(AgentTest, Create) { + const Agent::Config config{"test", false}; + TestHandler* handler_ptr; + auto agent = CreateAgent(config, &handler_ptr); + ASSERT_TRUE(agent); + ASSERT_TRUE(handler_ptr); + + ASSERT_EQ(config.name, agent->GetConfig().name); + ASSERT_EQ(config.user_specific, agent->GetConfig().user_specific); +} + +TEST(AgentTest, Create_InvalidPipename) { + // TODO(rogerta): The win32 docs say that a backslash is an invalid + // character for a pipename, but it seemed to work correctly in testing. + // Using an empty name was the easiest way to generate an invalid pipe + // name. + const Agent::Config config{"", false}; + TestHandler* handler_ptr; + auto agent = CreateAgent(config, &handler_ptr, + ResultCode::ERR_INVALID_CHANNEL_NAME); + ASSERT_TRUE(agent); + + ASSERT_EQ(ResultCode::ERR_AGENT_NOT_INITIALIZED, + agent->HandleOneEventForTesting()); +} + +// Can't create two agents with the same name. +TEST(AgentTest, Create_SecondFails) { + const Agent::Config config{ "test", false }; + TestHandler* handler_ptr1; + auto agent1 = CreateAgent(config, &handler_ptr1); + ASSERT_TRUE(agent1); + + TestHandler* handler_ptr2; + auto agent2 = CreateAgent(config, &handler_ptr2, + ResultCode::ERR_AGENT_ALREADY_EXISTS); + ASSERT_TRUE(agent2); + + ASSERT_EQ(ResultCode::ERR_AGENT_NOT_INITIALIZED, + agent2->HandleOneEventForTesting()); +} + +TEST(AgentTest, Stop) { + TestHandler* handler_ptr; + auto agent = CreateAgent({ "test", false }, &handler_ptr); + ASSERT_TRUE(agent); + + // Create a separate thread to stop the agent. + std::thread thread([&agent]() { + agent->Stop(); + }); + + agent->HandleEvents(); + thread.join(); +} + +TEST(AgentTest, ConnectAndStop) { + ResultCode rc; + auto handler = std::make_unique<SignalClientConnectedTestHandler>(); + auto* handler_ptr = handler.get(); + auto agent = std::make_unique<AgentWin>( + Agent::Config{"test", false}, std::move(handler), &rc); + ASSERT_TRUE(agent); + ASSERT_EQ(ResultCode::OK, rc); + + // Client thread waits until latch reaches zero. + std::latch stop_client{ 1 }; + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([&stop_client]() { + auto client = CreateClient({ "test", false }); + ASSERT_TRUE(client); + stop_client.wait(); + }); + + // A thread that stops the agent after one client connects. + std::thread stop_agent([&handler_ptr, &agent]() { + handler_ptr->wait_for_client.wait(); + agent->Stop(); + }); + + agent->HandleEvents(); + + stop_client.count_down(); + client_thread.join(); + stop_agent.join(); +} + +TEST(AgentTest, Connect_UserSpecific) { + ResultCode rc; + auto handler = std::make_unique<SignalClientConnectedTestHandler>(); + auto* handler_ptr = handler.get(); + auto agent = std::make_unique<AgentWin>( + Agent::Config{ "test", true }, std::move(handler), &rc); + ASSERT_TRUE(agent); + ASSERT_EQ(ResultCode::OK, rc); + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([]() { + // If the user_specific does not match the agent, the client should not + // connect. + auto client = CreateClient({ "test", false }); + ASSERT_FALSE(client); + + auto client2 = CreateClient({ "test", true }); + ASSERT_TRUE(client2); + }); + + // A thread that stops the agent after one client connects. + std::thread stop_agent([&handler_ptr, &agent]() { + handler_ptr->wait_for_client.wait(); + agent->Stop(); + }); + + agent->HandleEvents(); + + client_thread.join(); + stop_agent.join(); +} + +TEST(AgentTest, ConnectRequestAndStop) { + ResultCode rc; + auto handler = std::make_unique<SignalClientRequestedTestHandler>(); + auto* handler_ptr = handler.get(); + auto agent = std::make_unique<AgentWin>( + Agent::Config{"test", false}, std::move(handler), &rc); + ASSERT_TRUE(agent); + ASSERT_EQ(ResultCode::OK, rc); + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([]() { + auto client = CreateClient({ "test", false }); + ASSERT_TRUE(client); + + ContentAnalysisRequest request = BuildRequest("test"); + ContentAnalysisResponse response; + client->Send(request, &response); + }); + + // A thread that stops the agent after one client connects. + std::thread stop_agent([&handler_ptr, &agent]() { + handler_ptr->wait_for_request.wait(); + agent->Stop(); + }); + + agent->HandleEvents(); + + client_thread.join(); + stop_agent.join(); +} + +TEST(AgentTest, ConnectAndClose) { + const Agent::Config aconfig{ "test", false }; + const Client::Config cconfig{ "test", false }; + + // Create an agent and client that connects to it. + TestHandler* handler_ptr; + auto agent = CreateAgent(aconfig, &handler_ptr); + ASSERT_TRUE(agent); + auto client = CreateClient(cconfig); + ASSERT_TRUE(client); + ASSERT_EQ(cconfig.name, client->GetConfig().name); + ASSERT_EQ(cconfig.user_specific, client->GetConfig().user_specific); + + agent->HandleOneEventForTesting(); + ASSERT_EQ(1, handler_ptr->connect_count_); + ASSERT_EQ(0, handler_ptr->disconnect_count_); + ASSERT_EQ(0, handler_ptr->cancel_count_); + ASSERT_EQ(GetCurrentProcessId(), handler_ptr->last_info_.pid); + + // Close the client and make sure a disconnect is received. + client.reset(); + agent->HandleOneEventForTesting(); + ASSERT_EQ(1, handler_ptr->connect_count_); + ASSERT_EQ(1, handler_ptr->disconnect_count_); + ASSERT_EQ(0, handler_ptr->cancel_count_); + ASSERT_EQ(GetCurrentProcessId(), handler_ptr->last_info_.pid); +} + +TEST(AgentTest, Request) { + TestHandler* handler_ptr; + auto agent = CreateAgent({"test", false}, &handler_ptr); + ASSERT_TRUE(agent); + + bool done = false; + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([&done]() { + auto client = CreateClient({"test", false}); + ASSERT_TRUE(client); + + // Send a request and wait for a response. + ContentAnalysisRequest request = BuildRequest(); + ContentAnalysisResponse response; + int ret = client->Send(request, &response); + ASSERT_EQ(0, ret); + ASSERT_EQ(request.request_token(), response.request_token()); + + done = true; + }); + + while (!done) { + agent->HandleOneEventForTesting(); + } + ASSERT_EQ(1, handler_ptr->request_count_); + ASSERT_EQ(0, handler_ptr->ack_count_); + ASSERT_EQ(0, handler_ptr->cancel_count_); + + client_thread.join(); +} + +TEST(AgentTest, Request_Large) { + TestHandler* handler_ptr; + auto agent = CreateAgent({"test", false}, &handler_ptr); + ASSERT_TRUE(agent); + + bool done = false; + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([&done]() { + auto client = CreateClient({"test", false}); + ASSERT_TRUE(client); + + // Send a request and wait for a response. Create a large string, which + // means larger than the initial mesasge buffer size specified when + // creating the pipes (4096 bytes). + ContentAnalysisRequest request = BuildRequest(std::string(5000, 'a')); + ContentAnalysisResponse response; + int ret = client->Send(request, &response); + ASSERT_EQ(0, ret); + ASSERT_EQ(request.request_token(), response.request_token()); + + done = true; + }); + + while (!done) { + agent->HandleOneEventForTesting(); + } + ASSERT_EQ(1, handler_ptr->request_count_); + ASSERT_EQ(0, handler_ptr->ack_count_); + ASSERT_EQ(0, handler_ptr->cancel_count_); + + client_thread.join(); +} + +TEST(AgentTest, Request_DoubleSend) { + ResultCode rc; + auto handler = std::make_unique<DoubleSendTestHandler>(); + DoubleSendTestHandler* handler_ptr = handler.get(); + auto agent = std::make_unique<AgentWin>( + Agent::Config{"test", false}, std::move(handler), &rc); + ASSERT_TRUE(agent); + ASSERT_EQ(ResultCode::OK, rc); + + bool done = false; + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([&done]() { + auto client = CreateClient({ "test", false }); + ASSERT_TRUE(client); + + // Send a request and wait for a response. + ContentAnalysisRequest request = BuildRequest(); + ContentAnalysisResponse response; + int ret = client->Send(request, &response); + ASSERT_EQ(0, ret); + ASSERT_EQ(request.request_token(), response.request_token()); + + done = true; + }); + + while (!done) { + agent->HandleOneEventForTesting(); + } + ASSERT_EQ(1, handler_ptr->request_count_); + ASSERT_EQ(0, handler_ptr->ack_count_); + ASSERT_EQ(0, handler_ptr->cancel_count_); + + client_thread.join(); +} + +TEST(AgentTest, Request_CloseEvent) { + ResultCode rc; + auto handler = std::make_unique<CloseEventTestHandler>(); + CloseEventTestHandler* handler_ptr = handler.get(); + auto agent = std::make_unique<AgentWin>( + Agent::Config{"test", false}, std::move(handler), &rc); + ASSERT_TRUE(agent); + ASSERT_EQ(ResultCode::OK, rc); + + bool done = false; + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([&done]() { + auto client = CreateClient({"test", false}); + ASSERT_TRUE(client); + + // Send a request and wait for a response. + ContentAnalysisRequest request = BuildRequest(); + ContentAnalysisResponse response; + int ret = client->Send(request, &response); + ASSERT_EQ(0, ret); + ASSERT_EQ(request.request_token(), response.request_token()); + + done = true; + }); + + while (!done) { + agent->HandleOneEventForTesting(); + } + ASSERT_EQ(1, handler_ptr->request_count_); + + client_thread.join(); +} + +TEST(AgentTest, Ack) { + TestHandler* handler_ptr; + auto agent = CreateAgent({ "test", false }, &handler_ptr); + ASSERT_TRUE(agent); + + bool done = false; + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([&done]() { + auto client = CreateClient({"test", false}); + ASSERT_TRUE(client); + + // Send a request and wait for a response. + ContentAnalysisRequest request = BuildRequest(); + ContentAnalysisResponse response; + int ret = client->Send(request, &response); + ASSERT_EQ(0, ret); + + ContentAnalysisAcknowledgement ack; + ack.set_request_token(request.request_token()); + ret = client->Acknowledge(ack); + ASSERT_EQ(0, ret); + + done = true; + }); + + while (!done) { + agent->HandleOneEventForTesting(); + } + ASSERT_EQ(1, handler_ptr->request_count_); + ASSERT_EQ(1, handler_ptr->ack_count_); + ASSERT_EQ(0, handler_ptr->cancel_count_); + + client_thread.join(); +} + +TEST(AgentTest, Cancel) { + TestHandler* handler_ptr; + auto agent = CreateAgent({ "test", false }, &handler_ptr); + ASSERT_TRUE(agent); + + // Create a thread to handle the client. Since the client is sync, it can't + // run in the same thread as the agent. + std::thread client_thread([]() { + auto client = CreateClient({"test", false}); + ASSERT_TRUE(client); + + ContentAnalysisCancelRequests cancel; + cancel.set_user_action_id("1234567890"); + int ret = client->CancelRequests(cancel); + ASSERT_EQ(0, ret); + }); + + while (handler_ptr->cancel_count_ == 0) { + agent->HandleOneEventForTesting(); + } + ASSERT_EQ(0, handler_ptr->request_count_); + ASSERT_EQ(0, handler_ptr->ack_count_); + + client_thread.join(); +} + +} // namespace testing +} // namespace sdk +} // namespace content_analysis + diff --git a/third_party/content_analysis_sdk/agent/src/event_base.cc b/third_party/content_analysis_sdk/agent/src/event_base.cc new file mode 100644 index 0000000000..c8186a4112 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_base.cc @@ -0,0 +1,65 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "event_base.h" + +namespace content_analysis { +namespace sdk { + +ContentAnalysisEventBase::ContentAnalysisEventBase( + const BrowserInfo& browser_info) + : browser_info_(browser_info) {} + +ResultCode ContentAnalysisEventBase::Close() { + return ResultCode::OK; +} + +ResultCode UpdateResponse(ContentAnalysisResponse& response, + const std::string& tag, + ContentAnalysisResponse::Result::Status status) { + auto result = response.results_size() > 0 + ? response.mutable_results(0) + : response.add_results(); + + if (!tag.empty()) { + result->set_tag(tag); + } + + if (status != ContentAnalysisResponse::Result::STATUS_UNKNOWN) { + result->set_status(status); + } + + return ResultCode::OK; +} + +ResultCode SetEventVerdictTo( + ContentAnalysisEvent* event, + ContentAnalysisResponse::Result::TriggeredRule::Action action) { + // This function expects that the event's result has already been + // initialized by a call to UpdateResponse(). + + if (event->GetResponse().results_size() == 0) { + return ResultCode::ERR_MISSING_RESULT; + } + + auto result = event->GetResponse().mutable_results(0); + + // Content analysis responses generated with this SDK contain at most one + // triggered rule. + auto rule = result->triggered_rules_size() > 0 + ? result->mutable_triggered_rules(0) + : result->add_triggered_rules(); + + rule->set_action(action); + + return ResultCode::OK; +} + +ResultCode SetEventVerdictToBlock(ContentAnalysisEvent* event) { + return SetEventVerdictTo(event, + ContentAnalysisResponse::Result::TriggeredRule::BLOCK); +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/event_base.h b/third_party/content_analysis_sdk/agent/src/event_base.h new file mode 100644 index 0000000000..3e41c0ea44 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_base.h @@ -0,0 +1,38 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_EVENT_BASE_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_EVENT_BASE_H_ + +#include "content_analysis/sdk/analysis_agent.h" + +namespace content_analysis { +namespace sdk { + +// Base ContentAnalysisEvent class with code common to all platforms. +class ContentAnalysisEventBase : public ContentAnalysisEvent { + public: + // ContentAnalysisEvent: + ResultCode Close() override; + const BrowserInfo& GetBrowserInfo() const override { return browser_info_; } + const ContentAnalysisRequest& GetRequest() const override { return request_; } + ContentAnalysisResponse& GetResponse() override { return *response(); } + + protected: + explicit ContentAnalysisEventBase(const BrowserInfo& browser_info); + + ContentAnalysisRequest* request() { return &request_; } + AgentToChrome* agent_to_chrome() { return &agent_to_chrome_; } + ContentAnalysisResponse* response() { return agent_to_chrome()->mutable_response(); } + +private: + BrowserInfo browser_info_; + ContentAnalysisRequest request_; + AgentToChrome agent_to_chrome_; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_EVENT_BASE_H_ diff --git a/third_party/content_analysis_sdk/agent/src/event_mac.cc b/third_party/content_analysis_sdk/agent/src/event_mac.cc new file mode 100644 index 0000000000..46822e473d --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_mac.cc @@ -0,0 +1,29 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "event_mac.h" + +#include "scoped_print_handle_mac.h" + +namespace content_analysis { +namespace sdk { + +ContentAnalysisEventMac::ContentAnalysisEventMac( + const BrowserInfo& browser_info, + ContentAnalysisRequest req) + : ContentAnalysisEventBase(browser_info) { + *request() = std::move(req); +} + +ResultCode ContentAnalysisEventMac::Send() { + return ResultCode::ERR_UNEXPECTED; +} + +std::string ContentAnalysisEventMac::DebugString() const { + return std::string(); +} + + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/event_mac.h b/third_party/content_analysis_sdk/agent/src/event_mac.h new file mode 100644 index 0000000000..0d89c47035 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_mac.h @@ -0,0 +1,29 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ +#define CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ + +#include "event_base.h" + +namespace content_analysis { +namespace sdk { + +// ContentAnalysisEvent implementaton for macOS. +class ContentAnalysisEventMac : public ContentAnalysisEventBase { + public: + ContentAnalysisEventMac(const BrowserInfo& browser_info, + ContentAnalysisRequest request); + + // ContentAnalysisEvent: + ResultCode Send() override; + std::string DebugString() const override; + + // TODO(rogerta): Fill in implementation. +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ diff --git a/third_party/content_analysis_sdk/agent/src/event_mac_unittest.cc b/third_party/content_analysis_sdk/agent/src/event_mac_unittest.cc new file mode 100644 index 0000000000..cf41d6bcdb --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_mac_unittest.cc @@ -0,0 +1,53 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <memory> + +#include "agent/src/event_mac.h" +#include "content_analysis/sdk/analysis_agent.h" +#include "gtest/gtest.h" + +namespace content_analysis { +namespace sdk { +namespace testing { + +std::unique_ptr<ContentAnalysisEventMac> CreateEvent( + const BrowserInfo& browser_info, + ContentAnalysisRequest request) { + return std::make_unique<ContentAnalysisEventMac>( + browser_info, std::move(request)); +} + +TEST(EventTest, Create_BrowserInfo) { + const BrowserInfo bi{12345, "/path/to/binary"}; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(bi.pid, event->GetBrowserInfo().pid); + ASSERT_EQ(bi.binary_path, event->GetBrowserInfo().binary_path); +} + +TEST(EventTest, Create_Request) { + const BrowserInfo bi{ 12345, "/path/to/binary" }; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(1u, event->GetRequest().tags_size()); + ASSERT_EQ(request.tags(0), event->GetRequest().tags(0)); + ASSERT_TRUE(event->GetRequest().has_request_token()); + ASSERT_EQ(request.request_token(), event->GetRequest().request_token()); +} + +} // namespace testing +} // namespace sdk +} // namespace content_analysis + diff --git a/third_party/content_analysis_sdk/agent/src/event_posix.cc b/third_party/content_analysis_sdk/agent/src/event_posix.cc new file mode 100644 index 0000000000..1b0854af90 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_posix.cc @@ -0,0 +1,28 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "event_posix.h" + +#include "scoped_print_handle_posix.h" + +namespace content_analysis { +namespace sdk { + + ContentAnalysisEventPosix::ContentAnalysisEventPosix( + const BrowserInfo& browser_info, + ContentAnalysisRequest req) + : ContentAnalysisEventBase(browser_info) { + *request() = std::move(req); +} + +ResultCode ContentAnalysisEventPosix::Send() { + return ResultCode::ERR_UNEXPECTED; +} + +std::string ContentAnalysisEventPosix::DebugString() const { + return std::string(); +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/event_posix.h b/third_party/content_analysis_sdk/agent/src/event_posix.h new file mode 100644 index 0000000000..f3d0e24330 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_posix.h @@ -0,0 +1,29 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_SRC_EVENT_POSIX_H_ +#define CONTENT_ANALYSIS_SRC_EVENT_POSIX_H_ + +#include "event_base.h" + +namespace content_analysis { +namespace sdk { + +// ContentAnalysisEvent implementaton for linux. +class ContentAnalysisEventPosix : public ContentAnalysisEventBase { + public: + ContentAnalysisEventPosix(const BrowserInfo& browser_info, + ContentAnalysisRequest request); + + // ContentAnalysisEvent: + ResultCode Send() override; + std::string DebugString() const override; + + // TODO(rogerta): Fill in implementation. +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_SRC_EVENT_POSIX_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/agent/src/event_posix_unittest.cc b/third_party/content_analysis_sdk/agent/src/event_posix_unittest.cc new file mode 100644 index 0000000000..841934c2fd --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_posix_unittest.cc @@ -0,0 +1,53 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <memory> + +#include "agent/src/event_posix.h" +#include "content_analysis/sdk/analysis_agent.h" +#include "gtest/gtest.h" + +namespace content_analysis { +namespace sdk { +namespace testing { + +std::unique_ptr<ContentAnalysisEventPosix> CreateEvent( + const BrowserInfo& browser_info, + ContentAnalysisRequest request) { + return std::make_unique<ContentAnalysisEventPosix>( + browser_info, std::move(request)); +} + +TEST(EventTest, Create_BrowserInfo) { + const BrowserInfo bi{12345, "/path/to/binary"}; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(bi.pid, event->GetBrowserInfo().pid); + ASSERT_EQ(bi.binary_path, event->GetBrowserInfo().binary_path); +} + +TEST(EventTest, Create_Request) { + const BrowserInfo bi{ 12345, "/path/to/binary" }; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(1u, event->GetRequest().tags_size()); + ASSERT_EQ(request.tags(0), event->GetRequest().tags(0)); + ASSERT_TRUE(event->GetRequest().has_request_token()); + ASSERT_EQ(request.request_token(), event->GetRequest().request_token()); +} + +} // namespace testing +} // namespace sdk +} // namespace content_analysis + diff --git a/third_party/content_analysis_sdk/agent/src/event_win.cc b/third_party/content_analysis_sdk/agent/src/event_win.cc new file mode 100644 index 0000000000..907bdfb858 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_win.cc @@ -0,0 +1,133 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <ios> +#include <iostream> +#include <sstream> +#include <utility> + +#include "event_win.h" + +#include "common/utils_win.h" + +#include "agent_utils_win.h" +#include "scoped_print_handle_win.h" + +namespace content_analysis { +namespace sdk { + +// 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 WriteMessageToPipe(HANDLE pipe, const std::string& message) { + if (message.empty()) { + return ERROR_SUCCESS; + } + + internal::ScopedOverlapped overlapped; + if (!overlapped.is_valid()) { + return GetLastError(); + } + + DWORD err = ERROR_SUCCESS; + const char* cursor = message.data(); + for (DWORD size = message.length(); size > 0;) { + if (WriteFile(pipe, cursor, size, /*written=*/nullptr, overlapped)) { + err = ERROR_SUCCESS; + break; + } + + // If an I/O is not pending, return the error. + err = GetLastError(); + if (err != ERROR_IO_PENDING) { + break; + } + + DWORD written; + if (!GetOverlappedResult(pipe, overlapped, &written, /*wait=*/TRUE)) { + err = GetLastError(); + break; + } + + cursor += written; + size -= written; + } + + return err; +} + + +ContentAnalysisEventWin::ContentAnalysisEventWin( + HANDLE handle, + const BrowserInfo& browser_info, + ContentAnalysisRequest req) + : ContentAnalysisEventBase(browser_info), + hPipe_(handle) { + *request() = std::move(req); +} + +ContentAnalysisEventWin::~ContentAnalysisEventWin() { + Shutdown(); +} + +ResultCode ContentAnalysisEventWin::Init() { + // TODO(rogerta): do some extra validation of the request? + if (request()->request_token().empty()) { + return ResultCode::ERR_MISSING_REQUEST_TOKEN; + } + + response()->set_request_token(request()->request_token()); + + // Prepare the response so that ALLOW verdicts are the default(). + return UpdateResponse(*response(), + request()->tags_size() > 0 ? request()->tags(0) : std::string(), + ContentAnalysisResponse::Result::SUCCESS); +} + +ResultCode ContentAnalysisEventWin::Close() { + Shutdown(); + return ContentAnalysisEventBase::Close(); +} + +ResultCode ContentAnalysisEventWin::Send() { + if (response_sent_) { + return ResultCode::ERR_RESPONSE_ALREADY_SENT; + } + + response_sent_ = true; + + DWORD err = WriteMessageToPipe(hPipe_, + agent_to_chrome()->SerializeAsString()); + return ErrorToResultCode(err); +} + +std::string ContentAnalysisEventWin::DebugString() const { + std::stringstream state; + state.setf(std::ios::boolalpha); + state << "ContentAnalysisEventWin{handle=" << hPipe_; + state << " pid=" << GetBrowserInfo().pid; + state << " rtoken=" << GetRequest().request_token(); + state << " sent=" << response_sent_; + state << "}" << std::ends; + + return state.str(); +} + +void ContentAnalysisEventWin::Shutdown() { + if (hPipe_ != INVALID_HANDLE_VALUE) { + // If no response has been sent yet, attempt to send it now. Otherwise + // the client may be stuck waiting. After shutdown the agent will not + // have any other chance to respond. + if (!response_sent_) { + Send(); + } + + // This event does not own the pipe, so don't close it. + FlushFileBuffers(hPipe_); + hPipe_ = INVALID_HANDLE_VALUE; + } +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/event_win.h b/third_party/content_analysis_sdk/agent/src/event_win.h new file mode 100644 index 0000000000..f631f693dc --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_win.h @@ -0,0 +1,51 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_EVENT_WIN_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_EVENT_WIN_H_ + +#include <windows.h> + +#include "event_base.h" + +namespace content_analysis { +namespace sdk { + +// ContentAnalysisEvent implementaton for Windows. +class ContentAnalysisEventWin : public ContentAnalysisEventBase { + public: + ContentAnalysisEventWin(HANDLE handle, + const BrowserInfo& browser_info, + ContentAnalysisRequest request); + ~ContentAnalysisEventWin() override; + + // Initialize the event. This involves reading the request from Google + // Chrome and making sure it is well formed. + ResultCode Init(); + + // ContentAnalysisEvent: + 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(); + + // This handle is not owned by the event. + HANDLE hPipe_ = INVALID_HANDLE_VALUE; + + // Set to true when Send() is called the first time. + bool response_sent_ = false; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_EVENT_WIN_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/agent/src/event_win_unittest.cc b/third_party/content_analysis_sdk/agent/src/event_win_unittest.cc new file mode 100644 index 0000000000..ffc3103ac1 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/event_win_unittest.cc @@ -0,0 +1,116 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <memory> + +#include "agent/src/event_win.h" +#include "common/utils_win.h" +#include "gtest/gtest.h" + +namespace content_analysis { +namespace sdk { +namespace testing { + +std::unique_ptr<ContentAnalysisEventWin> CreateEvent( + HANDLE handle, + const BrowserInfo& browser_info, + ContentAnalysisRequest request) { + return std::make_unique<ContentAnalysisEventWin>( + handle, browser_info, std::move(request)); +} + +TEST(EventTest, Create_BrowserInfo) { + const BrowserInfo bi{12345, "/path/to/binary"}; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(bi.pid, event->GetBrowserInfo().pid); + ASSERT_EQ(bi.binary_path, event->GetBrowserInfo().binary_path); +} + +TEST(EventTest, Create_Request) { + const BrowserInfo bi{ 12345, "/path/to/binary" }; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(1u, event->GetRequest().tags_size()); + ASSERT_EQ(request.tags(0), event->GetRequest().tags(0)); + ASSERT_TRUE(event->GetRequest().has_request_token()); + ASSERT_EQ(request.request_token(), event->GetRequest().request_token()); +} + +TEST(EventTest, Create_Init) { + const BrowserInfo bi{ 12345, "/path/to/binary" }; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + request.set_request_token("req-token"); + + auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(ResultCode::OK, event->Init()); + + // Initializing an event should initialize the contained response for a + // success verdict that matches the request. + ASSERT_EQ(request.request_token(), event->GetResponse().request_token()); + ASSERT_EQ(1u, event->GetResponse().results_size()); + ASSERT_EQ(ContentAnalysisResponse::Result::SUCCESS, + event->GetResponse().results(0).status()); + ASSERT_TRUE(event->GetResponse().results(0).has_tag()); + ASSERT_EQ(request.tags(0), event->GetResponse().results(0).tag()); + ASSERT_EQ(0u, event->GetResponse().results(0).triggered_rules_size()); +} + +// Initializing an event whose request has no request token is an error. +TEST(EventTest, Create_Init_RequestNoRequestToken) { + const BrowserInfo bi{ 12345, "/path/to/binary" }; + ContentAnalysisRequest request; + *request.add_tags() = "foo"; + + auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); + ASSERT_TRUE(event); + + ASSERT_EQ(ResultCode::ERR_MISSING_REQUEST_TOKEN, event->Init()); +} + +TEST(EventTest, Write_BadPipe) { + HANDLE pipe; + DWORD err = internal::CreatePipe( + internal::GetPipeNameForAgent("testpipe", false), false, true, &pipe); + ASSERT_EQ(ERROR_SUCCESS, err); + ASSERT_NE(INVALID_HANDLE_VALUE, pipe); + + // Create an event with the dummy pipe and initilalize it. + const BrowserInfo bi{ 12345, "/path/to/binary" }; + ContentAnalysisRequest request; + request.set_request_token("req-token"); + *request.add_tags() = "dlp"; + request.set_text_content("test"); + auto event = std::make_unique<ContentAnalysisEventWin>( + pipe, bi, std::move(request)); + ASSERT_TRUE(event); + ResultCode rc = event->Init(); + ASSERT_EQ(ResultCode::OK, rc); + + // Close the handle before trying to send the response. + // This simulates an error with the pipe. + CloseHandle(pipe); + ASSERT_EQ(ERROR_SUCCESS, GetLastError()); + + // The following call should not hang. + event->Send(); +} + +} // namespace testing +} // namespace sdk +} // namespace content_analysis + diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.cc b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.cc new file mode 100644 index 0000000000..802b553b20 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.cc @@ -0,0 +1,17 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "scoped_print_handle_base.h" + +namespace content_analysis { +namespace sdk { + +ScopedPrintHandleBase::ScopedPrintHandleBase( + const ContentAnalysisRequest::PrintData& print_data) + : size_(print_data.size()) {} + +size_t ScopedPrintHandleBase::size() { return size_; } + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.h b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.h new file mode 100644 index 0000000000..1ae20381d7 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.h @@ -0,0 +1,25 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_BASE_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_BASE_H_ + +#include "content_analysis/sdk/analysis_agent.h" + +namespace content_analysis { +namespace sdk { + +class ScopedPrintHandleBase : public ScopedPrintHandle { + public: + ScopedPrintHandleBase(const ContentAnalysisRequest::PrintData& print_data); + + size_t size() override; + protected: + size_t size_ = 0; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_BASE_H_ diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.cc b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.cc new file mode 100644 index 0000000000..316576bf90 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.cc @@ -0,0 +1,36 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "scoped_print_handle_mac.h" + +namespace content_analysis { +namespace sdk { + +std::unique_ptr<ScopedPrintHandle> +CreateScopedPrintHandle(const ContentAnalysisRequest& request, + int64_t browser_pid) { + if (!request.has_print_data() || !request.print_data().has_handle()) { + return nullptr; + } + + return std::make_unique<ScopedPrintHandleMac>(request.print_data()); +} + +ScopedPrintHandleMac::ScopedPrintHandleMac( + const ContentAnalysisRequest::PrintData& print_data) + : ScopedPrintHandleBase(print_data) { + // TODO +} + +ScopedPrintHandleMac::~ScopedPrintHandleMac() { + // TODO +} + +const char* ScopedPrintHandleMac::data() { + // TODO + return nullptr; +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.h b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.h new file mode 100644 index 0000000000..d407ef5375 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.h @@ -0,0 +1,24 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_MAC_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_MAC_H_ + +#include "scoped_print_handle_base.h" + +namespace content_analysis { +namespace sdk { + +class ScopedPrintHandleMac : public ScopedPrintHandleBase { + public: + ScopedPrintHandleMac(const ContentAnalysisRequest::PrintData& print_data); + ~ScopedPrintHandleMac() override; + + const char* data() override; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_MAC_H_ diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.cc b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.cc new file mode 100644 index 0000000000..cd2eb2cc8e --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.cc @@ -0,0 +1,36 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "scoped_print_handle_posix.h" + +namespace content_analysis { +namespace sdk { + +std::unique_ptr<ScopedPrintHandle> +CreateScopedPrintHandle(const ContentAnalysisRequest& request, + int64_t browser_pid) { + if (!request.has_print_data() || !request.print_data().has_handle()) { + return nullptr; + } + + return std::make_unique<ScopedPrintHandlePosix>(request.print_data()); +} + +ScopedPrintHandlePosix::ScopedPrintHandlePosix( + const ContentAnalysisRequest::PrintData& print_data) + : ScopedPrintHandleBase(print_data) { + // TODO +} + +ScopedPrintHandlePosix::~ScopedPrintHandlePosix() { + // TODO +} + +const char* ScopedPrintHandlePosix::data() { + // TODO + return nullptr; +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.h b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.h new file mode 100644 index 0000000000..e9125f58eb --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.h @@ -0,0 +1,24 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_POSIX_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_POSIX_H_ + +#include "scoped_print_handle_base.h" + +namespace content_analysis { +namespace sdk { + +class ScopedPrintHandlePosix : public ScopedPrintHandleBase { + public: + ScopedPrintHandlePosix(const ContentAnalysisRequest::PrintData& print_data); + ~ScopedPrintHandlePosix() override; + + const char* data() override; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_POSIX_H_ diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.cc b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.cc new file mode 100644 index 0000000000..2573d676ea --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.cc @@ -0,0 +1,67 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "scoped_print_handle_win.h" + +namespace content_analysis { +namespace sdk { + +std::unique_ptr<ScopedPrintHandle> +CreateScopedPrintHandle(const ContentAnalysisRequest& request, + int64_t browser_pid) { + if (!request.has_print_data() || !request.print_data().has_handle()) { + return nullptr; + } + + // The handle in the request must be duped to be read by the agent + // process. If that doesn't work for any reason, return null. + // See https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle + // for details. + HANDLE browser_process = OpenProcess( + /*dwDesiredAccess=*/PROCESS_DUP_HANDLE, + /*bInheritHandle=*/false, + /*dwProcessId=*/browser_pid); + if (!browser_process) + return nullptr; + + HANDLE dupe = nullptr; + DuplicateHandle( + /*hSourceProcessHandle=*/browser_process, + /*hSourceHandle=*/reinterpret_cast<HANDLE>(request.print_data().handle()), + /*hTargetProcessHandle=*/GetCurrentProcess(), + /*lpTargetHandle=*/&dupe, + /*dwDesiredAccess=*/PROCESS_DUP_HANDLE | FILE_MAP_READ, + /*bInheritHandle=*/false, + /*dwOptions=*/0); + + CloseHandle(browser_process); + + if (!dupe) + return nullptr; + + ContentAnalysisRequest::PrintData dupe_print_data; + dupe_print_data.set_handle(reinterpret_cast<int64_t>(dupe)); + dupe_print_data.set_size(request.print_data().size()); + + + return std::make_unique<ScopedPrintHandleWin>(dupe_print_data); +} + +ScopedPrintHandleWin::ScopedPrintHandleWin( + const ContentAnalysisRequest::PrintData& print_data) + : ScopedPrintHandleBase(print_data), + handle_(reinterpret_cast<HANDLE>(print_data.handle())) { + mapped_ = MapViewOfFile(handle_, FILE_MAP_READ, 0, 0, 0); +} + +ScopedPrintHandleWin::~ScopedPrintHandleWin() { + CloseHandle(handle_); +} + +const char* ScopedPrintHandleWin::data() { + return reinterpret_cast<const char*>(mapped_); +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.h b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.h new file mode 100644 index 0000000000..31923285be --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.h @@ -0,0 +1,29 @@ +// Copyright 2023 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_WIN_H_ +#define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_WIN_H_ + +#include <windows.h> + +#include "scoped_print_handle_base.h" + +namespace content_analysis { +namespace sdk { + +class ScopedPrintHandleWin : public ScopedPrintHandleBase { + public: + ScopedPrintHandleWin(const ContentAnalysisRequest::PrintData& print_data); + ~ScopedPrintHandleWin() override; + + const char* data() override; + private: + void* mapped_ = nullptr; + HANDLE handle_ = nullptr; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_WIN_H_ diff --git a/third_party/content_analysis_sdk/browser/include/content_analysis/sdk/analysis_client.h b/third_party/content_analysis_sdk/browser/include/content_analysis/sdk/analysis_client.h new file mode 100644 index 0000000000..c7c5da8520 --- /dev/null +++ b/third_party/content_analysis_sdk/browser/include/content_analysis/sdk/analysis_client.h @@ -0,0 +1,84 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_BROWSER_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_CLIENT_H_ +#define CONTENT_ANALYSIS_BROWSER_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_CLIENT_H_ + +#include <memory> +#include <string> + +#include "content_analysis/sdk/analysis.pb.h" + +// This is the main include file for code using Content Analysis Connector +// Client SDK. No other include is needed. +// +// A browser begins by creating an instance of Client using the factory +// function Client::Create(). This instance should live as long as the browser +// intends to send content analysis requests to the content analysis agent. + +namespace content_analysis { +namespace sdk { + +// Represents information about one instance of a content analysis agent +// process that is connected to the browser. +struct AgentInfo { + unsigned long pid = 0; // Process ID of content analysis agent process. + std::string binary_path; // The full path to the process's main binary. +}; + +// Represents a client that can request content analysis for locally running +// agent. This class holds the client endpoint that the browser connects +// with when content analysis is required. +// +// See the demo directory for an example of how to use this class. +class Client { + public: + // Configuration options where creating an agent. `name` is used to create + // a channel between the agent and Google Chrome. + struct Config { + // Used to create a channel between the agent and Google Chrome. Both must + // use the same name to properly rendezvous with each other. The channel + // is platform specific. + std::string name; + + // Set to true if there is a different agent instance per OS user. Defaults + // to false. + bool user_specific = false; + }; + + // Returns a new client instance and calls Start(). + static std::unique_ptr<Client> Create(Config config); + + virtual ~Client() = default; + + // Returns the configuration parameters used to create the client. + virtual const Config& GetConfig() const = 0; + + // Retrives information about the agent that is connected to the browser. + virtual const AgentInfo& GetAgentInfo() const = 0; + + // Sends an analysis request to the agent and waits for a response. + virtual int Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) = 0; + + // Sends an response acknowledgment back to the agent. + virtual int Acknowledge(const ContentAnalysisAcknowledgement& ack) = 0; + + // Ask the agent to cancel all requests matching the criteria in `cancel`. + // This is a best effort only, the agent may cancel some, all, or no requests + // that match. + virtual int CancelRequests(const ContentAnalysisCancelRequests& cancel) = 0; + + protected: + Client() = default; + Client(const Client& rhs) = delete; + Client(Client&& rhs) = delete; + Client& operator=(const Client& rhs) = delete; + Client& operator=(Client&& rhs) = delete; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_BROWSER_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_CLIENT_H_ diff --git a/third_party/content_analysis_sdk/browser/src/client_base.cc b/third_party/content_analysis_sdk/browser/src/client_base.cc new file mode 100644 index 0000000000..0147c0cbee --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_base.cc @@ -0,0 +1,21 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "client_base.h" + +namespace content_analysis { +namespace sdk { + +ClientBase::ClientBase(Config config) : config_(config) {} + +const Client::Config& ClientBase::GetConfig() const { + return config_; +} + +const AgentInfo& ClientBase::GetAgentInfo() const { + return agent_info_; +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/browser/src/client_base.h b/third_party/content_analysis_sdk/browser/src/client_base.h new file mode 100644 index 0000000000..f8d7849394 --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_base.h @@ -0,0 +1,34 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_BASE_H_ +#define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_BASE_H_ + +#include "content_analysis/sdk/analysis_client.h" + +namespace content_analysis { +namespace sdk { + +// Base Client class with code common to all platforms. +class ClientBase : public Client { + public: + // Client: + const Config& GetConfig() const override; + const AgentInfo& GetAgentInfo() const override; + + protected: + ClientBase(Config config); + + const Config& configuration() const { return config_; } + AgentInfo& agent_info() { return agent_info_; } + +private: + Config config_; + AgentInfo agent_info_; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_BASE_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/browser/src/client_mac.cc b/third_party/content_analysis_sdk/browser/src/client_mac.cc new file mode 100644 index 0000000000..c6f5a798c1 --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_mac.cc @@ -0,0 +1,33 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <utility> + +#include "client_mac.h" + +namespace content_analysis { +namespace sdk { + +// static +std::unique_ptr<Client> Client::Create(Config config) { + return std::make_unique<ClientMac>(std::move(config)); +} + +ClientMac::ClientMac(Config config) : ClientBase(std::move(config)) {} + +int ClientMac::Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) { + return -1; +} + +int ClientMac::Acknowledge(const ContentAnalysisAcknowledgement& ack) { + return -1; +} + +int ClientMac::CancelRequests(const ContentAnalysisCancelRequests& cancel) { + return -1; +} + +} // namespace sdk +} // namespace content_analysis
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/browser/src/client_mac.h b/third_party/content_analysis_sdk/browser/src/client_mac.h new file mode 100644 index 0000000000..f3c640dd89 --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_mac.h @@ -0,0 +1,28 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_MAC_H_ +#define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_MAC_H_ + +#include "client_base.h" + +namespace content_analysis { +namespace sdk { + +// Client implementaton for macOS. +class ClientMac : public ClientBase { + public: + ClientMac(Config config); + + // Client: + int Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) override; + int Acknowledge(const ContentAnalysisAcknowledgement& ack) override; + int CancelRequests(const ContentAnalysisCancelRequests& cancel) override; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_MAC_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/browser/src/client_posix.cc b/third_party/content_analysis_sdk/browser/src/client_posix.cc new file mode 100644 index 0000000000..14277724fd --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_posix.cc @@ -0,0 +1,33 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <utility> + +#include "client_posix.h" + +namespace content_analysis { +namespace sdk { + +// static +std::unique_ptr<Client> Client::Create(Config config) { + return std::make_unique<ClientPosix>(std::move(config)); +} + +ClientPosix::ClientPosix(Config config) : ClientBase(std::move(config)) {} + +int ClientPosix::Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) { + return -1; +} + +int ClientPosix::Acknowledge(const ContentAnalysisAcknowledgement& ack) { + return -1; +} + +int ClientPosix::CancelRequests(const ContentAnalysisCancelRequests& cancel) { + return -1; +} + +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/browser/src/client_posix.h b/third_party/content_analysis_sdk/browser/src/client_posix.h new file mode 100644 index 0000000000..9e7666d681 --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_posix.h @@ -0,0 +1,28 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_POSIX_H_ +#define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_POSIX_H_ + +#include "client_base.h" + +namespace content_analysis { +namespace sdk { + +// Client implementaton for Posix. +class ClientPosix : public ClientBase { + public: + ClientPosix(Config config); + + // Client: + int Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) override; + int Acknowledge(const ContentAnalysisAcknowledgement& ack) override; + int CancelRequests(const ContentAnalysisCancelRequests& cancel) override; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_POSIX_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/browser/src/client_win.cc b/third_party/content_analysis_sdk/browser/src/client_win.cc new file mode 100644 index 0000000000..039946d131 --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_win.cc @@ -0,0 +1,432 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <windows.h> +#include <winternl.h> + +#include <cstring> +#include <memory> +#include <utility> +#include <vector> + +#include "common/utils_win.h" + +#include "client_win.h" + +namespace content_analysis { +namespace sdk { + +const DWORD kBufferSize = 4096; + +// Use the same default timeout value (50ms) as CreateNamedPipeA(), expressed +// in 100ns intervals. +constexpr LONGLONG kDefaultTimeout = 500000; + +// The following #defines and struct are copied from the official Microsoft +// Windows Driver Kit headers because they are not available in the official +// Microsoft Windows user mode SDK headers. + +#define FSCTL_PIPE_WAIT 0x110018 +#define STATUS_SUCCESS ((NTSTATUS)0) +#define STATUS_PIPE_NOT_AVAILABLE ((NTSTATUS)0xc00000ac) +#define STATUS_IO_TIMEOUT ((NTSTATUS)0xc00000b5) + +typedef struct _FILE_PIPE_WAIT_FOR_BUFFER { + LARGE_INTEGER Timeout; + ULONG NameLength; + BOOLEAN TimeoutSpecified; + WCHAR Name[1]; +} FILE_PIPE_WAIT_FOR_BUFFER, *PFILE_PIPE_WAIT_FOR_BUFFER; + +namespace { + +using NtCreateFileFn = decltype(&::NtCreateFile); + +NtCreateFileFn GetNtCreateFileFn() { + static NtCreateFileFn fnNtCreateFile = []() { + NtCreateFileFn fn = nullptr; + HMODULE h = LoadLibraryA("NtDll.dll"); + if (h != nullptr) { + fn = reinterpret_cast<NtCreateFileFn>(GetProcAddress(h, "NtCreateFile")); + FreeLibrary(h); + } + return fn; + }(); + + return fnNtCreateFile; +} + + +using NtFsControlFileFn = NTSTATUS (NTAPI *)( + HANDLE FileHandle, + HANDLE Event, + PIO_APC_ROUTINE ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG IoControlCode, + PVOID InputBuffer, + ULONG InputBufferLength, + PVOID OutputBuffer, + ULONG OutputBufferLength); + +NtFsControlFileFn GetNtFsControlFileFn() { + static NtFsControlFileFn fnNtFsControlFile = []() { + NtFsControlFileFn fn = nullptr; + HMODULE h = LoadLibraryA("NtDll.dll"); + if (h != nullptr) { + fn = reinterpret_cast<NtFsControlFileFn>(GetProcAddress(h, "NtFsControlFile")); + FreeLibrary(h); + } + return fn; + }(); + + return fnNtFsControlFile; +} + +NTSTATUS WaitForPipeAvailability(const UNICODE_STRING& path) { + NtCreateFileFn fnNtCreateFile = GetNtCreateFileFn(); + if (fnNtCreateFile == nullptr) { + return false; + } + NtFsControlFileFn fnNtFsControlFile = GetNtFsControlFileFn(); + if (fnNtFsControlFile == nullptr) { + return false; + } + + // Build the device name. This is the initial part of `path` which is + // assumed to start with the string `kPipePrefixForClient`. The `Length` + // field is measured in bytes, not characters, and does not include the null + // terminator. It's important that the device name ends with a trailing + // backslash. + size_t device_name_char_length = std::strlen(internal::kPipePrefixForClient); + UNICODE_STRING device_name; + device_name.Buffer = path.Buffer; + device_name.Length = device_name_char_length * sizeof(wchar_t); + device_name.MaximumLength = device_name.Length; + + // Build the pipe name. This is the remaining part of `path` after the device + // name. + UNICODE_STRING pipe_name; + pipe_name.Buffer = path.Buffer + device_name_char_length; + pipe_name.Length = path.Length - device_name.Length; + pipe_name.MaximumLength = pipe_name.Length; + + // Build the ioctl input buffer. This buffer is the size of + // FILE_PIPE_WAIT_FOR_BUFFER plus the length of the pipe name. Since + // FILE_PIPE_WAIT_FOR_BUFFER includes one WCHAR this includes space for + // the terminating null character of the name which wcsncpy() copies. + size_t buffer_size = sizeof(FILE_PIPE_WAIT_FOR_BUFFER) + pipe_name.Length; + std::vector<char> buffer(buffer_size); + FILE_PIPE_WAIT_FOR_BUFFER* wait_buffer = + reinterpret_cast<FILE_PIPE_WAIT_FOR_BUFFER*>(buffer.data()); + wait_buffer->Timeout.QuadPart = kDefaultTimeout; + wait_buffer->NameLength = pipe_name.Length; + wait_buffer->TimeoutSpecified = TRUE; + std::wcsncpy(wait_buffer->Name, pipe_name.Buffer, wait_buffer->NameLength / + sizeof(wchar_t)); + + OBJECT_ATTRIBUTES attr; + InitializeObjectAttributes(&attr, &device_name, OBJ_CASE_INSENSITIVE, nullptr, + nullptr); + + IO_STATUS_BLOCK io; + HANDLE h = INVALID_HANDLE_VALUE; + NTSTATUS sts = fnNtCreateFile(&h, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, + &attr, &io, /*AllocationSize=*/nullptr, FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN, + FILE_SYNCHRONOUS_IO_NONALERT, /*EaBuffer=*/nullptr, /*EaLength=*/0); + if (sts != STATUS_SUCCESS) { + return false; + } + + IO_STATUS_BLOCK io2; + sts = fnNtFsControlFile(h, /*Event=*/nullptr, /*ApcRoutine=*/nullptr, + /*ApcContext*/nullptr, &io2, FSCTL_PIPE_WAIT, buffer.data(), + buffer.size(), nullptr, 0); + CloseHandle(h); + return sts; +} + +// Reads the next message from the pipe and returns a buffer of chars. +// This function is synchronous. +std::vector<char> ReadNextMessageFromPipe( + HANDLE pipe, + OVERLAPPED* overlapped) { + DWORD err = ERROR_SUCCESS; + std::vector<char> buffer(kBufferSize); + char* p = buffer.data(); + int final_size = 0; + while (true) { + DWORD read; + + // Even though the pipe is opened for overlapped IO, the read operation + // could still completely synchronously. For example, a server's response + // message could already be available in the pipe's internal buffer. + // If ReadFile() does complete synchronously, TRUE is returned. In this + // case update the final size and exit the loop. + if (ReadFile(pipe, p, kBufferSize, &read, overlapped)) { + final_size += read; + break; + } else { + // Reaching here means that ReadFile() will either complete async or + // an error has occurred. The former case is detected if the error code + // is "IO pending", in which case GetOverlappedResult() is called to wait + // for the IO to complete. If that function returns TRUE then the read + // operation completed successfully and the code simply updates the final + // size and exits the loop. + err = GetLastError(); + if (err == ERROR_IO_PENDING) { + if (GetOverlappedResult(pipe, overlapped, &read, /*wait=*/TRUE)) { + final_size += read; + break; + } else { + err = GetLastError(); + } + } + + // Reaching here means an error has occurred. One error is recoverable: + // "more data". For any other type of error break out of the loop. + if (err != ERROR_MORE_DATA) { + final_size = 0; + break; + } + + // Reaching here means the error is "more data", that is, the buffer + // specified in ReadFile() was too small to contain the entire response + // message from the server. ReadFile() has placed the start of the + // message in the specified buffer but ReadFile() needs to be called + // again to read the remaining part. + // + // The buffer size is increased and the current pointer into the buffer + // `p` is adjusted so that when the loop re-runs, it calls ReadFile() + // with the correct point in the buffer. It's possible that this loop + // might have to run many times if the response message is rather large. + buffer.resize(buffer.size() + kBufferSize); + p = buffer.data() + buffer.size() - kBufferSize; + } + } + + buffer.resize(final_size); + return buffer; +} + +// Writes a string to the pipe. Returns true if successful, false otherwise. +// This function is synchronous. +bool WriteMessageToPipe( + HANDLE pipe, + const std::string& message, + OVERLAPPED* overlapped) { + if (message.empty()) + return false; + + // Even though the pipe is opened for overlapped IO, the write operation + // could still completely synchronously. If it does, TRUE is returned. + // In this case the function is done. + bool ok = WriteFile(pipe, message.data(), message.size(), nullptr, overlapped); + if (!ok) { + // Reaching here means that WriteFile() will either complete async or + // an error has occurred. The former case is detected if the error code + // is "IO pending", in which case GetOverlappedResult() is called to wait + // for the IO to complete. Whether the operation completes sync or async, + // return true if the operation succeeded and false otherwise. + DWORD err = GetLastError(); + if (err == ERROR_IO_PENDING) { + DWORD written; + ok = GetOverlappedResult(pipe, overlapped, &written, /*wait=*/TRUE); + } + } + + return ok; +} + +} // namespace + +// static +std::unique_ptr<Client> Client::Create(Config config) { + int rc; + auto client = std::make_unique<ClientWin>(std::move(config), &rc); + return rc == 0 ? std::move(client) : nullptr; +} + +ClientWin::ClientWin(Config config, int* rc) : ClientBase(std::move(config)) { + *rc = -1; + + std::string pipename = + internal::GetPipeNameForClient(configuration().name, + configuration().user_specific); + if (!pipename.empty()) { + unsigned long pid = 0; + if (ConnectToPipe(pipename, &hPipe_) == ERROR_SUCCESS && + GetNamedPipeServerProcessId(hPipe_, &pid)) { + agent_info().pid = pid; + + // Getting the process path is best effort. + *rc = 0; + std::string binary_path; + if (internal::GetProcessPath(pid, &binary_path)) { + agent_info().binary_path = std::move(binary_path); + } + } + } + + if (*rc != 0) { + Shutdown(); + } +} + +ClientWin::~ClientWin() { + Shutdown(); +} + +int ClientWin::Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) { + ChromeToAgent chrome_to_agent; + *chrome_to_agent.mutable_request() = std::move(request); + + internal::ScopedOverlapped overlapped; + if (!overlapped.is_valid()) { + return -1; + } + + bool success = WriteMessageToPipe(hPipe_, + chrome_to_agent.SerializeAsString(), + overlapped); + if (success) { + std::vector<char> buffer = ReadNextMessageFromPipe(hPipe_, overlapped); + AgentToChrome agent_to_chrome; + success = buffer.size() > 0 && + agent_to_chrome.ParseFromArray(buffer.data(), buffer.size()); + if (success) { + *response = std::move(*agent_to_chrome.mutable_response()); + } + } + + return success ? 0 : -1; +} + +int ClientWin::Acknowledge(const ContentAnalysisAcknowledgement& ack) { + // TODO: could avoid a copy by changing argument to be + // `ContentAnalysisAcknowledgement ack` and then using std::move() below and + // at call site. + ChromeToAgent chrome_to_agent; + *chrome_to_agent.mutable_ack() = ack; + + internal::ScopedOverlapped overlapped; + if (!overlapped.is_valid()) { + return -1; + } + + return WriteMessageToPipe(hPipe_, chrome_to_agent.SerializeAsString(), overlapped) + ? 0 : -1; +} + +int ClientWin::CancelRequests(const ContentAnalysisCancelRequests& cancel) { + // TODO: could avoid a copy by changing argument to be + // `ContentAnalysisCancelRequests cancel` and then using std::move() below and + // at call site. + ChromeToAgent chrome_to_agent; + *chrome_to_agent.mutable_cancel() = cancel; + + internal::ScopedOverlapped overlapped; + if (!overlapped.is_valid()) { + return -1; + } + + return WriteMessageToPipe(hPipe_, chrome_to_agent.SerializeAsString(), overlapped) + ? 0 : -1; +} + +// static +DWORD ClientWin::ConnectToPipe(const std::string& pipename, HANDLE* handle) { + // Get pointers to the Ntxxx functions. This is required to use absolute + // pipe names from the Windows NT Object Manager's namespace. This protects + // against the "\\.\pipe" symlink being redirected. + + NtCreateFileFn fnNtCreateFile = GetNtCreateFileFn(); + if (fnNtCreateFile == nullptr) { + return ERROR_INVALID_FUNCTION; + } + + // Convert the path to a wchar_t string. Pass pipename.size() as the + // `cbMultiByte` argument instead of -1 since the terminating null should not + // be counted. NtCreateFile() does not expect the object name to be + // terminated. Note that `buffer` and hence `name` created from it are both + // unterminated strings. + int wlen = MultiByteToWideChar(CP_ACP, 0, pipename.c_str(), pipename.size(), + nullptr, 0); + if (wlen == 0) { + return GetLastError(); + } + std::vector<wchar_t> buffer(wlen); + MultiByteToWideChar(CP_ACP, 0, pipename.c_str(), pipename.size(), + buffer.data(), wlen); + + UNICODE_STRING name; + name.Buffer = buffer.data(); + name.Length = wlen * sizeof(wchar_t); // Length in bytes, not characters. + name.MaximumLength = name.Length; + + OBJECT_ATTRIBUTES attr; + InitializeObjectAttributes(&attr, &name, OBJ_CASE_INSENSITIVE, nullptr, + nullptr); + + // Open the named pipe for overlapped IO, i.e. do not specify either of the + // FILE_SYNCHRONOUS_IO_xxxALERT in the creation option flags. If the pipe + // is not opened for overlapped IO, then the Send() method will block if + // called from different threads since only one read or write operation would + // be allowed at a time. + IO_STATUS_BLOCK io; + HANDLE h = INVALID_HANDLE_VALUE; + NTSTATUS sts = STATUS_IO_TIMEOUT; + while (sts == STATUS_IO_TIMEOUT) { + sts = fnNtCreateFile(&h, GENERIC_READ | GENERIC_WRITE | + SYNCHRONIZE, &attr, &io, /*AllocationSize=*/nullptr, + FILE_ATTRIBUTE_NORMAL, /*ShareAccess=*/0, FILE_OPEN, + FILE_NON_DIRECTORY_FILE, + /*EaBuffer=*/nullptr, /*EaLength=*/0); + if (sts != STATUS_SUCCESS) { + if (sts != STATUS_PIPE_NOT_AVAILABLE) { + break; + } + + sts = WaitForPipeAvailability(name); + if (sts != STATUS_SUCCESS && sts != STATUS_IO_TIMEOUT) { + break; + } + } + } + + if (sts != STATUS_SUCCESS) { + return ERROR_PIPE_NOT_CONNECTED; + } + + // Change to message read mode to match server side. Max connection count + // and timeout must be null if client and server are on the same machine. + DWORD mode = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(h, &mode, + /*maxCollectionCount=*/nullptr, + /*connectionTimeout=*/nullptr)) { + DWORD err = GetLastError(); + CloseHandle(h); + return err; + } + + *handle = h; + return ERROR_SUCCESS; +} + +void ClientWin::Shutdown() { + if (hPipe_ != INVALID_HANDLE_VALUE) { + // TODO: This trips the LateWriteObserver. We could move this earlier + // (before the LateWriteObserver is created) or just remove it, although + // the later could mean an ACK message is not processed by the agent + // in time. + // FlushFileBuffers(hPipe_); + CloseHandle(hPipe_); + hPipe_ = INVALID_HANDLE_VALUE; + } +} + +} // namespace sdk +} // namespace content_analysis
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/browser/src/client_win.h b/third_party/content_analysis_sdk/browser/src/client_win.h new file mode 100644 index 0000000000..f4bdd834fc --- /dev/null +++ b/third_party/content_analysis_sdk/browser/src/client_win.h @@ -0,0 +1,39 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_WIN_H_ +#define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_WIN_H_ + +#include <string> + +#include "client_base.h" + +namespace content_analysis { +namespace sdk { + +// Client implementaton for Windows. +class ClientWin : public ClientBase { + public: + ClientWin(Config config, int* rc); + ~ClientWin() override; + + // Client: + int Send(ContentAnalysisRequest request, + ContentAnalysisResponse* response) override; + int Acknowledge(const ContentAnalysisAcknowledgement& ack) override; + int CancelRequests(const ContentAnalysisCancelRequests& cancel) override; + + private: + static DWORD ConnectToPipe(const std::string& pipename, HANDLE* handle); + + // Performs a clean shutdown of the client. + void Shutdown(); + + HANDLE hPipe_ = INVALID_HANDLE_VALUE; +}; + +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_WIN_H_ diff --git a/third_party/content_analysis_sdk/common/utils_win.cc b/third_party/content_analysis_sdk/common/utils_win.cc new file mode 100644 index 0000000000..a87c2d9e02 --- /dev/null +++ b/third_party/content_analysis_sdk/common/utils_win.cc @@ -0,0 +1,174 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <vector> + +#include <windows.h> +#include <sddl.h> + +#include "utils_win.h" + +namespace content_analysis { +namespace sdk { +namespace internal { + +std::string GetUserSID() { + std::string sid; + + HANDLE handle; + if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &handle) && + !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &handle)) { + return std::string(); + } + + DWORD length = 0; + std::vector<char> buffer; + if (!GetTokenInformation(handle, TokenUser, nullptr, 0, &length)) { + DWORD err = GetLastError(); + if (err == ERROR_INSUFFICIENT_BUFFER) { + buffer.resize(length); + } + } + if (GetTokenInformation(handle, TokenUser, buffer.data(), buffer.size(), + &length)) { + TOKEN_USER* info = reinterpret_cast<TOKEN_USER*>(buffer.data()); + char* sid_string; + if (ConvertSidToStringSidA(info->User.Sid, &sid_string)) { + sid = sid_string; + LocalFree(sid_string); + } + } + + CloseHandle(handle); + return sid; +} + +std::string BuildPipeName(const char* prefix, + const std::string& base, + bool user_specific) { + std::string pipename = prefix; + + // If the agent is not user-specific, the assumption is that it runs with + // administrator privileges. Create the pipe in a location only available + // to administrators. + if (!user_specific) + pipename += "ProtectedPrefix\\Administrators\\"; + + pipename += base; + + if (user_specific) { + std::string sid = GetUserSID(); + if (sid.empty()) + return std::string(); + + pipename += "." + sid; + } + + return pipename; +} + +std::string GetPipeNameForAgent(const std::string& base, bool user_specific) { + return BuildPipeName(kPipePrefixForAgent, base, user_specific); +} + +std::string GetPipeNameForClient(const std::string& base, bool user_specific) { + return BuildPipeName(kPipePrefixForClient, base, user_specific); +} + +DWORD CreatePipe( + const std::string& name, + bool user_specific, + bool is_first_pipe, + HANDLE* handle) { + DWORD err = ERROR_SUCCESS; + DWORD mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED; + + // Create DACL for pipe to allow users on the local system to read and write + // to the pipe. The creator and owner as well as administrator always get + // full control of the pipe. + // + // If `user_specific` is true, a different agent instance is used for each + // OS user, so only allow the interactive logged on user to reads and write + // messages to the pipe. Otherwise only one agent instance used used for all + // OS users and all authenticated logged on users can reads and write + // messages. + // + // See https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-definition-language + // for a description of this string format. + constexpr char kDaclEveryone[] = "D:" + "(A;OICI;GA;;;CO)" // Allow full control to creator owner. + "(A;OICI;GA;;;BA)" // Allow full control to admins. + "(A;OICI;GRGW;;;WD)"; // Allow read and write to everyone. + constexpr char kDaclUserSpecific[] = "D:" + "(A;OICI;GA;;;CO)" // Allow full control to creator owner. + "(A;OICI;GA;;;BA)" // Allow full control to admins. + "(A;OICI;GRGW;;;IU)"; // Allow read and write to interactive user. + SECURITY_ATTRIBUTES sa; + memset(&sa, 0, sizeof(sa)); + sa.nLength = sizeof(sa); + sa.bInheritHandle = FALSE; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( + user_specific ? kDaclUserSpecific : kDaclEveryone, SDDL_REVISION_1, + &sa.lpSecurityDescriptor, /*outSdSize=*/nullptr)) { + err = GetLastError(); + return err; + } + + // When `is_first_pipe` is true, the agent expects there is no process that + // is currently listening on this pipe. If there is, CreateNamedPipeA() + // returns with ERROR_ACCESS_DENIED. This is used to detect if another + // process is listening for connections when there shouldn't be. + if (is_first_pipe) { + mode |= FILE_FLAG_FIRST_PIPE_INSTANCE; + } + *handle = CreateNamedPipeA(name.c_str(), mode, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | + PIPE_REJECT_REMOTE_CLIENTS, PIPE_UNLIMITED_INSTANCES, kBufferSize, + kBufferSize, 0, &sa); + if (*handle == INVALID_HANDLE_VALUE) { + err = GetLastError(); + } + + // Free the security descriptor as it is no longer needed once + // CreateNamedPipeA() returns. + LocalFree(sa.lpSecurityDescriptor); + + return err; +} + +bool GetProcessPath(unsigned long pid, std::string* binary_path) { + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (hProc == nullptr) { + return false; + } + + char path[MAX_PATH]; + DWORD size = sizeof(path); + DWORD length = QueryFullProcessImageNameA(hProc, /*flags=*/0, path, &size); + CloseHandle(hProc); + if (length == 0) { + return false; + } + + *binary_path = path; + return true; +} + +ScopedOverlapped::ScopedOverlapped() { + memset(&overlapped_, 0, sizeof(overlapped_)); + overlapped_.hEvent = CreateEvent(/*securityAttr=*/nullptr, + /*manualReset=*/TRUE, + /*initialState=*/FALSE, + /*name=*/nullptr); +} + +ScopedOverlapped::~ScopedOverlapped() { + if (overlapped_.hEvent != nullptr) { + CloseHandle(overlapped_.hEvent); + } +} + +} // internal +} // namespace sdk +} // namespace content_analysis diff --git a/third_party/content_analysis_sdk/common/utils_win.h b/third_party/content_analysis_sdk/common/utils_win.h new file mode 100644 index 0000000000..93b9b379f2 --- /dev/null +++ b/third_party/content_analysis_sdk/common/utils_win.h @@ -0,0 +1,76 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Utility and helper functions common to both the agent and browser code. +// This code is not publicly exposed from the SDK. + +#ifndef CONTENT_ANALYSIS_COMMON_UTILS_WIN_H_ +#define CONTENT_ANALYSIS_COMMON_UTILS_WIN_H_ + +#include <string> + +namespace content_analysis { +namespace sdk { +namespace internal { + +// The default size of the buffer used to hold messages received from +// Google Chrome. +const DWORD kBufferSize = 4096; + +// Named pipe prefixes used for agent and client side of pipe. +constexpr char kPipePrefixForAgent[] = R"(\\.\\pipe\)"; +constexpr char kPipePrefixForClient[] = R"(\Device\NamedPipe\)"; + +// Returns the user SID of the thread or process that calls thie function. +// Returns an empty string on error. +std::string GetUserSID(); + +// Returns the name of the pipe that should be used to communicate between +// the agent and Google Chrome. If `sid` is non-empty, make the pip name +// specific to that user. +// +// GetPipeNameForAgent() is meant to be used in the agent. The returned +// path can be used with CreatePipe() below. GetPipeNameForClient() is meant +// to be used in the client. The returned path can only be used with +// NtCreateFile() and not CreateFile(). +std::string GetPipeNameForAgent(const std::string& base, bool user_specific); +std::string GetPipeNameForClient(const std::string& base, bool user_specific); + +// Creates a named pipe with the give name. If `is_first_pipe` is true, +// fail if this is not the first pipe using this name. +// +// This function create a pipe whose DACL allow full control to the creator +// owner and administrators. If `user_specific` the DACL only allows the +// logged on user to read from and write to the pipe. Otherwise anyone logged +// in can read from and write to the pipe. +// +// A handle to the pipe is retuned in `handle`. +DWORD CreatePipe(const std::string& name, + bool user_specific, + bool is_first_pipe, + HANDLE* handle); + +// Returns the full path to the main binary file of the process with the given +// process ID. +bool GetProcessPath(unsigned long pid, std::string* binary_path); + +// A class that scopes the creation and destruction of an OVERLAPPED structure +// used for async IO. +class ScopedOverlapped { + public: + ScopedOverlapped(); + ~ScopedOverlapped(); + + bool is_valid() { return overlapped_.hEvent != nullptr; } + operator OVERLAPPED*() { return &overlapped_; } + + private: + OVERLAPPED overlapped_; +}; + +} // internal +} // namespace sdk +} // namespace content_analysis + +#endif // CONTENT_ANALYSIS_COMMON_UTILS_WIN_H_
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/demo/README.md b/third_party/content_analysis_sdk/demo/README.md new file mode 100644 index 0000000000..0f22912cc1 --- /dev/null +++ b/third_party/content_analysis_sdk/demo/README.md @@ -0,0 +1,16 @@ +# Google Chrome Content Analysis Connector Agent SDK Demo + +This directory holds the Google Chrome Content Analysis Connector Agent SDK Demo. +It contains an example of how to use the SDK. + +Build instructions are available in the main project `README.md`. + +## Demo agent permissions +On Microsoft Windows, if the demo agent is run without the `--user` command line +argument it must have Administrator privileges in order to properly create the +pipe used to communicate with the browser. The demo browser must also be run +without the `--user` command line argument. + +Otherwise the agent may run as any user, with or without Administrator +privileges. The demo browser must also be run with the `--user` command line +argument and run as the same user.
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/demo/agent.cc b/third_party/content_analysis_sdk/demo/agent.cc new file mode 100644 index 0000000000..c3640018e6 --- /dev/null +++ b/third_party/content_analysis_sdk/demo/agent.cc @@ -0,0 +1,189 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <algorithm> +#include <fstream> +#include <iostream> +#include <string> +#include <regex> +#include <vector> + +#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 +// values in chrome browser. +constexpr char kPathUser[] = "path_user"; +constexpr char kPathSystem[] = "brcm_chrm_cas"; + +// Global app config. +std::string path = kPathSystem; +bool use_queue = false; +bool user_specific = false; +std::vector<unsigned long> delays = {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 = "--delays="; +constexpr const char* kArgPath = "--path="; +constexpr const char* kArgQueued = "--queued"; +constexpr const char* kArgThreads = "--threads="; +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<std::string, Mode> sStringToMode = { +#define AGENT_MODE(name) {#name, Mode::Mode_##name}, +#include "modes.h" +#undef AGENT_MODE +}; + +std::map<Mode, std::string> sModeToString = { +#define AGENT_MODE(name) {Mode::Mode_##name, #name}, +#include "modes.h" +#undef AGENT_MODE +}; + +std::vector<std::pair<std::string, std::regex>> +ParseRegex(const std::string str) { + std::vector<std::pair<std::string, std::regex>> ret; + for (auto it = str.begin(); it != str.end(); /* nop */) { + auto it2 = std::find(it, str.end(), ','); + ret.push_back(std::make_pair(std::string(it, it2), std::regex(it, it2))); + it = it2 == str.end() ? it2 : it2 + 1; + } + + return ret; +} + +bool ParseCommandLine(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg.find(kArgUserSpecific) == 0) { + // If kArgPath was already used, abort. + if (path != kPathSystem) { + std::cout << std::endl << "ERROR: use --path=<path> after --user"; + return false; + } + path = kPathUser; + user_specific = true; + } else if (arg.find(kArgDelaySpecific) == 0) { + std::string delaysStr = arg.substr(strlen(kArgDelaySpecific)); + delays.clear(); + size_t posStart = 0, posEnd; + unsigned long delay; + while ((posEnd = delaysStr.find(',', posStart)) != std::string::npos) { + delay = std::stoul(delaysStr.substr(posStart, posEnd - posStart)); + if (delay > 30) { + delay = 30; + } + delays.push_back(delay); + posStart = posEnd + 1; + } + delay = std::stoul(delaysStr.substr(posStart)); + if (delay > 30) { + delay = 30; + } + delays.push_back(delay); + } else if (arg.find(kArgPath) == 0) { + path = arg.substr(strlen(kArgPath)); + } else if (arg.find(kArgQueued) == 0) { + use_queue = true; + } else if (arg.find(kArgThreads) == 0) { + num_threads = std::stoul(arg.substr(strlen(kArgThreads))); + } else if (arg.find(kArgToBlock) == 0) { + toBlock = ParseRegex(arg.substr(strlen(kArgToBlock))); + } else if (arg.find(kArgToWarn) == 0) { + 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) { + int arg_len = strlen(kArgSavePrintRequestDataTo); + save_print_data_path = arg.substr(arg_len); + } + } + + return true; +} + +void PrintHelp() { + std::cout + << std::endl << std::endl + << "Usage: agent [OPTIONS]" << std::endl + << "A simple agent to process content analysis requests." << std::endl + << "Data containing the string 'block' blocks the request data from being used." << std::endl + << std::endl << "Options:" << std::endl + << kArgDelaySpecific << "<delay1,delay2,...> : Add delays to request processing in seconds. Delays are limited to 30 seconds and are applied round-robin to requests. Default is 0." << std::endl + << kArgPath << " <path> : Used the specified path instead of default. Must come after --user." << std::endl + << kArgQueued << " : Queue requests for processing in a background thread" << std::endl + << kArgThreads << " : When queued, number of threads in the request processing thread pool" << std::endl + << kArgUserSpecific << " : Make agent OS user specific." << std::endl + << kArgSavePrintRequestDataTo << " : saves the PDF data to the given file path for print requests" << std::endl + << kArgToBlock << "<regex> : Regular expression matching file and text content to block." << std::endl + << kArgToWarn << "<regex> : Regular expression matching file and text content to warn about." << std::endl + << kArgToReport << "<regex> : Regular expression matching file and text content to report." << std::endl + << kArgMisbehave << "<mode> : Use 'misbehaving' agent in given mode for testing purposes." << std::endl + << kArgHelp << " : prints this help message" << std::endl; +} + +int main(int argc, char* argv[]) { + if (!ParseCommandLine(argc, argv)) { + PrintHelp(); + return 1; + } + + // TODO: Add toBlock, toWarn, toReport to QueueingHandler + auto handler = + useMisbehavingHandler + ? MisbehavingHandler::Create(delays[0], modeStr) + : use_queue + ? std::make_unique<QueuingHandler>(num_threads, std::move(delays), save_print_data_path, std::move(toBlock), std::move(toWarn), std::move(toReport)) + : std::make_unique<Handler>(std::move(delays), 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; + auto agent = content_analysis::sdk::Agent::Create( + {path, user_specific}, std::move(handler), &rc); + if (!agent || rc != content_analysis::sdk::ResultCode::OK) { + std::cout << "[Demo] Error starting agent: " + << content_analysis::sdk::ResultCodeToString(rc) + << std::endl; + return 1; + }; + + std::cout << "[Demo] " << agent->DebugString() << std::endl; + + // Blocks, sending events to the handler until agent->Stop() is called. + rc = agent->HandleEvents(); + if (rc != content_analysis::sdk::ResultCode::OK) { + std::cout << "[Demo] Error from handling events: " + << content_analysis::sdk::ResultCodeToString(rc) + << std::endl; + std::cout << "[Demo] " << agent->DebugString() << std::endl; + } + + return 0; +} diff --git a/third_party/content_analysis_sdk/demo/atomic_output.h b/third_party/content_analysis_sdk/demo/atomic_output.h new file mode 100644 index 0000000000..86ca8cdd75 --- /dev/null +++ b/third_party/content_analysis_sdk/demo/atomic_output.h @@ -0,0 +1,29 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <iostream> +#include <sstream> +#include <string> + +// Utility class to atomically write outout to std::cout. All data streamed +// the class is automatically sent to std::cout in the dtor. This is useful +// to keep the output of multiple threads writing to std::Cout from +// interleaving. + +class AtomicCout { + public: + ~AtomicCout() { + flush(); + } + + std::stringstream& stream() { return stream_; } + + void flush() { + std::cout << stream_.str(); + stream_.str(std::string()); + } + + private: + std::stringstream stream_; +};
\ No newline at end of file diff --git a/third_party/content_analysis_sdk/demo/client.cc b/third_party/content_analysis_sdk/demo/client.cc new file mode 100644 index 0000000000..5e47fca57f --- /dev/null +++ b/third_party/content_analysis_sdk/demo/client.cc @@ -0,0 +1,411 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <time.h> + +#include <iostream> +#include <memory> +#include <mutex> +#include <sstream> +#include <string> +#include <thread> +#include <vector> + +#include "content_analysis/sdk/analysis_client.h" +#include "demo/atomic_output.h" + +using content_analysis::sdk::Client; +using content_analysis::sdk::ContentAnalysisRequest; +using content_analysis::sdk::ContentAnalysisResponse; +using content_analysis::sdk::ContentAnalysisAcknowledgement; + +// 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 +// values in chrome browser. +constexpr char kPathUser[] = "path_user"; +constexpr char kPathSystem[] = "brcm_chrm_cas"; + +// Global app config. +std::string path = kPathSystem; +bool user_specific = false; +bool group = false; +std::unique_ptr<Client> client; + +// Paramters used to build the request. +content_analysis::sdk::AnalysisConnector connector = + content_analysis::sdk::FILE_ATTACHED; +time_t request_token_number = time(nullptr); +std::string request_token; +std::string tag = "dlp"; +bool threaded = false; +std::string digest = "sha256-123456"; +std::string url = "https://upload.example.com"; +std::string email = "me@example.com"; +std::string machine_user = "DOMAIN\\me"; +std::vector<std::string> datas; + +// When grouping, remember the tokens of all requests/responses in order to +// acknowledge them all with the same final action. +// +// This global state. It may be access from multiple thread so must be +// accessed from a critical section. +std::mutex global_mutex; +ContentAnalysisAcknowledgement::FinalAction global_final_action = + ContentAnalysisAcknowledgement::ALLOW; +std::vector<std::string> request_tokens; + +// Command line parameters. +constexpr const char* kArgConnector = "--connector="; +constexpr const char* kArgDigest = "--digest="; +constexpr const char* kArgEmail = "--email="; +constexpr const char* kArgGroup = "--group"; +constexpr const char* kArgMachineUser = "--machine-user="; +constexpr const char* kArgPath = "--path="; +constexpr const char* kArgRequestToken = "--request-token="; +constexpr const char* kArgTag = "--tag="; +constexpr const char* kArgThreaded = "--threaded"; +constexpr const char* kArgUrl = "--url="; +constexpr const char* kArgUserSpecific = "--user"; +constexpr const char* kArgHelp = "--help"; + +bool ParseCommandLine(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg.find(kArgConnector) == 0) { + std::string connector_str = arg.substr(strlen(kArgConnector)); + if (connector_str == "download") { + connector = content_analysis::sdk::FILE_DOWNLOADED; + } else if (connector_str == "attach") { + connector = content_analysis::sdk::FILE_ATTACHED; + } else if (connector_str == "bulk-data-entry") { + connector = content_analysis::sdk::BULK_DATA_ENTRY; + } else if (connector_str == "print") { + connector = content_analysis::sdk::PRINT; + } else if (connector_str == "file-transfer") { + connector = content_analysis::sdk::FILE_TRANSFER; + } else { + std::cout << "[Demo] Incorrect command line arg: " << arg << std::endl; + return false; + } + } else if (arg.find(kArgRequestToken) == 0) { + request_token = arg.substr(strlen(kArgRequestToken)); + } else if (arg.find(kArgTag) == 0) { + tag = arg.substr(strlen(kArgTag)); + } else if (arg.find(kArgThreaded) == 0) { + threaded = true; + } else if (arg.find(kArgDigest) == 0) { + digest = arg.substr(strlen(kArgDigest)); + } else if (arg.find(kArgUrl) == 0) { + url = arg.substr(strlen(kArgUrl)); + } else if (arg.find(kArgMachineUser) == 0) { + machine_user = arg.substr(strlen(kArgMachineUser)); + } else if (arg.find(kArgEmail) == 0) { + email = arg.substr(strlen(kArgEmail)); + } else if (arg.find(kArgPath) == 0) { + path = arg.substr(strlen(kArgPath)); + } else if (arg.find(kArgUserSpecific) == 0) { + // If kArgPath was already used, abort. + if (path != kPathSystem) { + std::cout << std::endl << "ERROR: use --path=<path> after --user"; + return false; + } + path = kPathUser; + user_specific = true; + } else if (arg.find(kArgGroup) == 0) { + group = true; + } else if (arg.find(kArgHelp) == 0) { + return false; + } else { + datas.push_back(arg); + } + } + + return true; +} + +void PrintHelp() { + std::cout + << std::endl << std::endl + << "Usage: client [OPTIONS] [@]content_or_file ..." << std::endl + << "A simple client to send content analysis requests to a running agent." << std::endl + << "Without @ the content to analyze is the argument itself." << std::endl + << "Otherwise the content is read from a file called 'content_or_file'." << std::endl + << "Multiple [@]content_or_file arguments may be specified, each generates one request." << std::endl + << std::endl << "Options:" << std::endl + << kArgConnector << "<connector> : one of 'download', 'attach' (default), 'bulk-data-entry', 'print', or 'file-transfer'" << std::endl + << kArgRequestToken << "<unique-token> : defaults to 'req-<number>' which auto increments" << std::endl + << kArgTag << "<tag> : defaults to 'dlp'" << std::endl + << kArgThreaded << " : handled multiple requests using threads" << std::endl + << kArgUrl << "<url> : defaults to 'https://upload.example.com'" << std::endl + << kArgMachineUser << "<machine-user> : defaults to 'DOMAIN\\me'" << std::endl + << kArgEmail << "<email> : defaults to 'me@example.com'" << std::endl + << kArgPath << " <path> : Used the specified path instead of default. Must come after --user." << std::endl + << kArgUserSpecific << " : Connects to an OS user specific agent" << std::endl + << kArgDigest << "<digest> : defaults to 'sha256-123456'" << std::endl + << kArgGroup << " : Generate the same final action for all requests" << std::endl + << kArgHelp << " : prints this help message" << std::endl; +} + +std::string GenerateRequestToken() { + std::stringstream stm; + stm << "req-" << request_token_number++; + return stm.str(); +} + +ContentAnalysisRequest BuildRequest(const std::string& data) { + std::string filepath; + std::string filename; + if (data[0] == '@') { + filepath = data.substr(1); + filename = filepath.substr(filepath.find_last_of("/\\") + 1); + } + + ContentAnalysisRequest request; + + // Set request to expire 5 minutes into the future. + request.set_expires_at(time(nullptr) + 5 * 60); + request.set_analysis_connector(connector); + request.set_request_token(!request_token.empty() + ? request_token : GenerateRequestToken()); + *request.add_tags() = tag; + + auto request_data = request.mutable_request_data(); + request_data->set_url(url); + request_data->set_email(email); + request_data->set_digest(digest); + if (!filename.empty()) { + request_data->set_filename(filename); + } + + auto client_metadata = request.mutable_client_metadata(); + auto browser = client_metadata->mutable_browser(); + browser->set_machine_user(machine_user); + + if (!filepath.empty()) { + request.set_file_path(filepath); + } else if (!data.empty()) { + request.set_text_content(data); + } else { + std::cout << "[Demo] Specify text content or a file path." << std::endl; + PrintHelp(); + exit(1); + } + + return request; +} + +// Gets the most severe action within the result. +ContentAnalysisResponse::Result::TriggeredRule::Action +GetActionFromResult(const ContentAnalysisResponse::Result& result) { + auto action = + ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED; + for (auto rule : result.triggered_rules()) { + if (rule.has_action() && rule.action() > action) + action = rule.action(); + } + return action; +} + +// Gets the most severe action within all the the results of a response. +ContentAnalysisResponse::Result::TriggeredRule::Action +GetActionFromResponse(const ContentAnalysisResponse& response) { + auto action = + ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED; + for (auto result : response.results()) { + auto action2 = GetActionFromResult(result); + if (action2 > action) + action = action2; + } + return action; +} + +void DumpResponse( + std::stringstream& stream, + const ContentAnalysisResponse& response) { + for (auto result : response.results()) { + auto tag = result.has_tag() ? result.tag() : "<no-tag>"; + + auto status = result.has_status() + ? result.status() + : ContentAnalysisResponse::Result::STATUS_UNKNOWN; + std::string status_str; + switch (status) { + case ContentAnalysisResponse::Result::STATUS_UNKNOWN: + status_str = "Unknown"; + break; + case ContentAnalysisResponse::Result::SUCCESS: + status_str = "Success"; + break; + case ContentAnalysisResponse::Result::FAILURE: + status_str = "Failure"; + break; + default: + status_str = "<Uknown>"; + break; + } + + auto action = GetActionFromResult(result); + std::string action_str; + switch (action) { + case ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED: + action_str = "allowed"; + break; + case ContentAnalysisResponse::Result::TriggeredRule::REPORT_ONLY: + action_str = "reported only"; + break; + case ContentAnalysisResponse::Result::TriggeredRule::WARN: + action_str = "warned"; + break; + case ContentAnalysisResponse::Result::TriggeredRule::BLOCK: + action_str = "blocked"; + break; + } + + time_t now = time(nullptr); + stream << "[Demo] Request " << response.request_token() << " is " << action_str + << " after " << tag + << " analysis, status=" << status_str + << " at " << ctime(&now); + } +} + +ContentAnalysisAcknowledgement BuildAcknowledgement( + const std::string& request_token, + ContentAnalysisAcknowledgement::FinalAction final_action) { + ContentAnalysisAcknowledgement ack; + ack.set_request_token(request_token); + ack.set_status(ContentAnalysisAcknowledgement::SUCCESS); + ack.set_final_action(final_action); + return ack; +} + +void HandleRequest(const ContentAnalysisRequest& request) { + AtomicCout aout; + ContentAnalysisResponse response; + int err = client->Send(request, &response); + if (err != 0) { + aout.stream() << "[Demo] Error sending request " << request.request_token() + << std::endl; + } else if (response.results_size() == 0) { + aout.stream() << "[Demo] Response " << request.request_token() << " is missing a result" + << std::endl; + } else { + DumpResponse(aout.stream(), response); + + auto final_action = ContentAnalysisAcknowledgement::ALLOW; + switch (GetActionFromResponse(response)) { + case ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED: + break; + case ContentAnalysisResponse::Result::TriggeredRule::REPORT_ONLY: + final_action = ContentAnalysisAcknowledgement::REPORT_ONLY; + break; + case ContentAnalysisResponse::Result::TriggeredRule::WARN: + final_action = ContentAnalysisAcknowledgement::WARN; + break; + case ContentAnalysisResponse::Result::TriggeredRule::BLOCK: + final_action = ContentAnalysisAcknowledgement::BLOCK; + break; + } + + // If grouping, remember the request's token in order to ack the response + // later. + if (group) { + std::unique_lock<std::mutex> lock(global_mutex); + request_tokens.push_back(request.request_token()); + if (final_action > global_final_action) + global_final_action = final_action; + } else { + int err = client->Acknowledge( + BuildAcknowledgement(request.request_token(), final_action)); + if (err != 0) { + aout.stream() << "[Demo] Error sending ack " << request.request_token() + << std::endl; + } + } + } +} + +void ProcessRequest(size_t i) { + auto request = BuildRequest(datas[i]); + + { + AtomicCout aout; + aout.stream() << "[Demo] Sending request " << request.request_token() << std::endl; + } + + HandleRequest(request); +} + +int main(int argc, char* argv[]) { + if (!ParseCommandLine(argc, argv)) { + PrintHelp(); + return 1; + } + + // Each client uses a unique name to identify itself with Google Chrome. + client = Client::Create({path, user_specific}); + if (!client) { + std::cout << "[Demo] Error starting client" << std::endl; + return 1; + }; + + auto info = client->GetAgentInfo(); + std::cout << "Agent pid=" << info.pid + << " path=" << info.binary_path << std::endl; + + if (threaded) { + std::vector<std::unique_ptr<std::thread>> threads; + for (int i = 0; i < datas.size(); ++i) { + AtomicCout aout; + aout.stream() << "Start thread " << i << std::endl; + threads.emplace_back(std::make_unique<std::thread>(ProcessRequest, i)); + } + + // Make sure all threads have terminated. + for (auto& thread : threads) { + thread->join(); + } + } + else { + for (size_t i = 0; i < datas.size(); ++i) { + ProcessRequest(i); + } + } + // It's safe to access global state beyond this point without locking since + // all no more responses will be touching them. + + if (group) { + std::cout << std::endl; + std::cout << "[Demo] Final action for all requests is "; + switch (global_final_action) { + // Google Chrome fails open, so if no action is specified that is the same + // as ALLOW. + case ContentAnalysisAcknowledgement::ACTION_UNSPECIFIED: + case ContentAnalysisAcknowledgement::ALLOW: + std::cout << "allowed"; + break; + case ContentAnalysisAcknowledgement::REPORT_ONLY: + std::cout << "reported only"; + break; + case ContentAnalysisAcknowledgement::WARN: + std::cout << "warned"; + break; + case ContentAnalysisAcknowledgement::BLOCK: + std::cout << "blocked"; + break; + } + std::cout << std::endl << std::endl; + + for (auto token : request_tokens) { + std::cout << "[Demo] Sending group Ack" << std::endl; + int err = client->Acknowledge( + BuildAcknowledgement(token, global_final_action)); + if (err != 0) { + std::cout << "[Demo] Error sending ack for " << token << std::endl; + } + } + } + + return 0; +}; diff --git a/third_party/content_analysis_sdk/demo/handler.h b/third_party/content_analysis_sdk/demo/handler.h new file mode 100644 index 0000000000..1c9871bd08 --- /dev/null +++ b/third_party/content_analysis_sdk/demo/handler.h @@ -0,0 +1,449 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_DEMO_HANDLER_H_ +#define CONTENT_ANALYSIS_DEMO_HANDLER_H_ + +#include <time.h> + +#include <algorithm> +#include <atomic> +#include <chrono> +#include <fstream> +#include <iostream> +#include <optional> +#include <thread> +#include <utility> +#include <regex> +#include <vector> + +#include "content_analysis/sdk/analysis_agent.h" +#include "demo/atomic_output.h" +#include "demo/request_queue.h" + +using RegexArray = std::vector<std::pair<std::string, std::regex>>; + +// An AgentEventHandler that dumps requests information to stdout and blocks +// any requests that have the keyword "block" in their data +class Handler : public content_analysis::sdk::AgentEventHandler { + public: + using Event = content_analysis::sdk::ContentAnalysisEvent; + + Handler(std::vector<unsigned long>&& delays, const std::string& print_data_file_path, + RegexArray&& toBlock = RegexArray(), + RegexArray&& toWarn = RegexArray(), + RegexArray&& toReport = RegexArray()) : + toBlock_(std::move(toBlock)), toWarn_(std::move(toWarn)), toReport_(std::move(toReport)), + delays_(std::move(delays)), print_data_file_path_(print_data_file_path) {} + + const std::vector<unsigned long> delays() { return delays_; } + size_t nextDelayIndex() const { return nextDelayIndex_; } + + protected: + // Analyzes one request from Google Chrome and responds back to the browser + // with either an allow or block verdict. + void AnalyzeContent(std::stringstream& stream, std::unique_ptr<Event> 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(). + + DumpEvent(stream, event.get()); + + bool success = true; + std::optional<content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action> caResponse = + content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK; + + if (event->GetRequest().has_text_content()) { + caResponse = DecideCAResponse( + event->GetRequest().text_content(), stream); + } else if (event->GetRequest().has_file_path()) { + // TODO: Fix downloads to store file *first* so we can check contents. + // Until then, just check the file name: + caResponse = DecideCAResponse( + event->GetRequest().file_path(), stream); + } else if (event->GetRequest().has_print_data()) { + // In the case of print request, normally the PDF bytes would be parsed + // for sensitive data violations. To keep this class simple, only the + // URL is checked for the word "block". + caResponse = DecideCAResponse(event->GetRequest().request_data().url(), stream); + } + + if (!success) { + content_analysis::sdk::UpdateResponse( + event->GetResponse(), + std::string(), + content_analysis::sdk::ContentAnalysisResponse::Result::FAILURE); + stream << " Verdict: failed to reach verdict: "; + stream << event->DebugString() << std::endl; + } else { + stream << " Verdict: "; + if (caResponse) { + switch (caResponse.value()) { + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_BLOCK: + stream << "BLOCK"; + break; + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_WARN: + stream << "WARN"; + break; + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY: + stream << "REPORT_ONLY"; + break; + case content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_ACTION_UNSPECIFIED: + stream << "ACTION_UNSPECIFIED"; + break; + default: + stream << "<error>"; + break; + } + auto rc = + content_analysis::sdk::SetEventVerdictTo(event.get(), caResponse.value()); + if (rc != content_analysis::sdk::ResultCode::OK) { + stream << " error: " + << content_analysis::sdk::ResultCodeToString(rc) << std::endl; + stream << " " << event->DebugString() << std::endl; + } + stream << std::endl; + } else { + stream << " Verdict: allow" << std::endl; + } + stream << std::endl; + } + stream << std::endl; + + // If a delay is specified, wait that much. + size_t nextDelayIndex = nextDelayIndex_.fetch_add(1); + unsigned long delay = delays_[nextDelayIndex % delays_.size()]; + if (delay > 0) { + std::this_thread::sleep_for(std::chrono::seconds(delay)); + } + + // Send the response back to Google Chrome. + auto rc = event->Send(); + if (rc != content_analysis::sdk::ResultCode::OK) { + stream << "[Demo] Error sending response: " + << content_analysis::sdk::ResultCodeToString(rc) + << std::endl; + stream << event->DebugString() << std::endl; + } + } + + private: + void OnBrowserConnected( + const content_analysis::sdk::BrowserInfo& info) override { + AtomicCout aout; + aout.stream() << std::endl << "==========" << std::endl; + aout.stream() << "Browser connected pid=" << info.pid + << " path=" << info.binary_path << std::endl; + } + + void OnBrowserDisconnected( + const content_analysis::sdk::BrowserInfo& info) override { + AtomicCout aout; + aout.stream() << std::endl << "Browser disconnected pid=" << info.pid << std::endl; + aout.stream() << "==========" << std::endl; + } + + void OnAnalysisRequested(std::unique_ptr<Event> 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. + AtomicCout aout; + aout.stream() << std::endl << "----------" << std::endl << std::endl; + AnalyzeContent(aout.stream(), std::move(event)); + } + + void OnResponseAcknowledged( + const content_analysis::sdk::ContentAnalysisAcknowledgement& + ack) override { + const char* final_action = "<Unknown>"; + if (ack.has_final_action()) { + switch (ack.final_action()) { + case content_analysis::sdk::ContentAnalysisAcknowledgement::ACTION_UNSPECIFIED: + final_action = "<Unspecified>"; + 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; + } + } + + AtomicCout aout; + aout.stream() << "Ack: " << ack.request_token() << std::endl; + aout.stream() << " Final action: " << final_action << std::endl; + } + void OnCancelRequests( + const content_analysis::sdk::ContentAnalysisCancelRequests& cancel) + override { + AtomicCout aout; + aout.stream() << "Cancel: " << std::endl; + aout.stream() << " User action ID: " << cancel.user_action_id() << std::endl; + } + + void OnInternalError( + const char* context, + content_analysis::sdk::ResultCode error) override { + AtomicCout aout; + aout.stream() << std::endl + << "*ERROR*: context=\"" << context << "\" " + << content_analysis::sdk::ResultCodeToString(error) + << std::endl; + } + + void DumpEvent(std::stringstream& stream, Event* event) { + time_t now = time(nullptr); + stream << "Received at: " << ctime(&now); // Returned string includes \n. + + const content_analysis::sdk::ContentAnalysisRequest& request = + event->GetRequest(); + std::string connector = "<Unknown>"; + 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() : "<No URL>"; + + std::string tab_title = + request.has_request_data() && request.request_data().has_tab_title() + ? request.request_data().tab_title() : "<No tab title>"; + + std::string filename = + request.has_request_data() && request.request_data().has_filename() + ? request.request_data().filename() : "<No filename>"; + + std::string digest = + request.has_request_data() && request.request_data().has_digest() + ? request.request_data().digest() : "<No digest>"; + + std::string file_path = + request.has_file_path() + ? request.file_path() : "<none>"; + + std::string text_content = + request.has_text_content() + ? request.text_content() : "<none>"; + + 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() : "<No machine user>"; + + std::string email = + request.has_request_data() && request.request_data().has_email() + ? request.request_data().email() : "<No email>"; + + time_t t = request.expires_at(); + std::string expires_at_str = ctime(&t); + // Returned string includes trailing \n, overwrite with null. + expires_at_str[expires_at_str.size() - 1] = 0; + time_t secs_remaining = t - now; + + std::string user_action_id = request.has_user_action_id() + ? request.user_action_id() : "<No user action id>"; + + stream << "Request: " << request.request_token() << std::endl; + stream << " User action ID: " << user_action_id << std::endl; + stream << " Expires at: " << expires_at_str << " (" + << secs_remaining << " seconds from now)" << std::endl; + stream << " Connector: " << connector << std::endl; + stream << " URL: " << url << std::endl; + stream << " Tab title: " << tab_title << std::endl; + stream << " Filename: " << filename << std::endl; + stream << " Digest: " << digest << std::endl; + stream << " Filepath: " << file_path << std::endl; + stream << " Text content: '" << text_content << "'" << std::endl; + stream << " Machine user: " << machine_user << std::endl; + stream << " Email: " << email << std::endl; + if (request.has_print_data() && !print_data_file_path_.empty()) { + if (request.request_data().has_print_metadata() && + request.request_data().print_metadata().has_printer_name()) { + stream << " Printer name: " + << request.request_data().print_metadata().printer_name() + << std::endl; + } else { + stream << " No printer name in request" << std::endl; + } + + stream << " Print data saved to: " << print_data_file_path_ + << std::endl; + using content_analysis::sdk::ContentAnalysisEvent; + auto print_data = + content_analysis::sdk::CreateScopedPrintHandle(event->GetRequest(), + event->GetBrowserInfo().pid); + std::ofstream file(print_data_file_path_, + std::ios::out | std::ios::trunc | std::ios::binary); + file.write(print_data->data(), print_data->size()); + file.flush(); + file.close(); + } + } + + 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; + } + + std::optional<content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action> + 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 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 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 content_analysis::sdk::ContentAnalysisResponse_Result_TriggeredRule_Action_REPORT_ONLY; + } + } + stream << "'" << content << "' was ALLOWed\n"; + return {}; + } + + // For the demo, block any content that matches these wildcards. + RegexArray toBlock_; + RegexArray toWarn_; + RegexArray toReport_; + + std::vector<unsigned long> delays_; + std::atomic<size_t> nextDelayIndex_; + std::string print_data_file_path_; +}; + +// An AgentEventHandler that dumps requests information to stdout and blocks +// any requests that have the keyword "block" in their data +class QueuingHandler : public Handler { + public: + QueuingHandler(unsigned long threads, std::vector<unsigned long>&& delays, const std::string& print_data_file_path, + RegexArray&& toBlock = RegexArray(), + RegexArray&& toWarn = RegexArray(), + RegexArray&& toReport = RegexArray()) + : Handler(std::move(delays), print_data_file_path, std::move(toBlock), std::move(toWarn), std::move(toReport)) { + StartBackgroundThreads(threads); + } + + ~QueuingHandler() override { + // Abort background process and wait for it to finish. + request_queue_.abort(); + WaitForBackgroundThread(); + } + + private: + void OnAnalysisRequested(std::unique_ptr<Event> event) override { + { + time_t now = time(nullptr); + const content_analysis::sdk::ContentAnalysisRequest& request = + event->GetRequest(); + AtomicCout aout; + aout.stream() << std::endl << "Queuing request: " << request.request_token() + << " at " << ctime(&now) << std::endl; + } + + request_queue_.push(std::move(event)); + } + + static void* ProcessRequests(void* qh) { + QueuingHandler* handler = reinterpret_cast<QueuingHandler*>(qh); + + while (true) { + auto event = handler->request_queue_.pop(); + if (!event) + break; + + AtomicCout aout; + aout.stream() << std::endl << "----------" << std::endl; + aout.stream() << "Thread: " << std::this_thread::get_id() << std::endl; + aout.stream() << "Delaying request processing for " + << handler->delays()[handler->nextDelayIndex() % handler->delays().size()] << "s" << std::endl << std::endl; + aout.flush(); + + handler->AnalyzeContent(aout.stream(), std::move(event)); + } + + return 0; + } + + // A list of outstanding content analysis requests. + RequestQueue request_queue_; + + void StartBackgroundThreads(unsigned long threads) { + threads_.reserve(threads); + for (unsigned long i = 0; i < threads; ++i) { + threads_.emplace_back(std::make_unique<std::thread>(ProcessRequests, this)); + } + } + + void WaitForBackgroundThread() { + for (auto& thread : threads_) { + thread->join(); + } + } + + // Thread id of backgrond thread. + std::vector<std::unique_ptr<std::thread>> threads_; +}; + +#endif // CONTENT_ANALYSIS_DEMO_HANDLER_H_ 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 0000000000..d303049d98 --- /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 <time.h> + +#include <algorithm> +#include <chrono> +#include <fstream> +#include <map> +#include <iostream> +#include <utility> +#include <vector> +#include <regex> +#include <windows.h> + +#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<std::string, Mode> sStringToMode; +extern std::map<Mode, std::string> 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<AgentEventHandler> 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<AgentEventHandler>(new MisbehavingHandler(delay, it->second)); + } + + private: + MisbehavingHandler(unsigned long delay, Mode mode) : delay_(delay), mode_(mode) {} + + template <size_t N> + DWORD SendBytesOverPipe(const unsigned char (&bytes)[N], + const std::unique_ptr<Event>& event) { + content_analysis::sdk::ContentAnalysisEventWin* eventWin = + static_cast<content_analysis::sdk::ContentAnalysisEventWin*>( + event.get()); + HANDLE pipe = eventWin->Pipe(); + std::string s(reinterpret_cast<const char*>(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> 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<content_analysis::sdk::ContentAnalysisEventWin*>( + 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<content_analysis::sdk::ContentAnalysisEventWin*>( + 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> 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 = "<Unknown>"; + if (ack.has_final_action()) { + switch (ack.final_action()) { + case content_analysis::sdk::ContentAnalysisAcknowledgement:: + ACTION_UNSPECIFIED: + final_action = "<Unspecified>"; + 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 = "<Unknown>"; + 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() + : "<No URL>"; + + std::string tab_title = + request.has_request_data() && request.request_data().has_tab_title() + ? request.request_data().tab_title() + : "<No tab title>"; + + std::string filename = + request.has_request_data() && request.request_data().has_filename() + ? request.request_data().filename() + : "<No filename>"; + + std::string digest = + request.has_request_data() && request.request_data().has_digest() + ? request.request_data().digest() + : "<No digest>"; + + std::string file_path = + request.has_file_path() ? request.file_path() : "<none>"; + + std::string text_content = + request.has_text_content() ? request.text_content() : "<none>"; + + 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() + : "<No machine user>"; + + std::string email = + request.has_request_data() && request.request_data().has_email() + ? request.request_data().email() + : "<No email>"; + + time_t t = request.expires_at(); + + std::string user_action_id = request.has_user_action_id() + ? request.user_action_id() + : "<No 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 0000000000..debefc9d1a --- /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/third_party/content_analysis_sdk/demo/request_queue.h b/third_party/content_analysis_sdk/demo/request_queue.h new file mode 100644 index 0000000000..8615774e2e --- /dev/null +++ b/third_party/content_analysis_sdk/demo/request_queue.h @@ -0,0 +1,70 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_ANALYSIS_DEMO_REQUST_QUEUE_H_ +#define CONTENT_ANALYSIS_DEMO_REQUST_QUEUE_H_ + +#include <condition_variable> +#include <memory> +#include <mutex> +#include <queue> + +#include "content_analysis/sdk/analysis_agent.h" + +// This class maintains a list of outstanding content analysis requests to +// process. Each request is encapsulated in one ContentAnalysisEvent. +// Requests are handled in FIFO order. +class RequestQueue { + public: + using Event = content_analysis::sdk::ContentAnalysisEvent; + + RequestQueue() = default; + virtual ~RequestQueue() = default; + + // Push a new content analysis event into the queue. + void push(std::unique_ptr<Event> event) { + std::lock_guard<std::mutex> lock(mutex_); + + events_.push(std::move(event)); + + // Wake before leaving to prevent unpredicatable scheduling. + cv_.notify_one(); + } + + // Pop the next request from the queue, blocking if necessary until an event + // is available. Returns a nullptr if the queue will produce no more + // events. + std::unique_ptr<Event> pop() { + std::unique_lock<std::mutex> lock(mutex_); + + while (!abort_ && events_.size() == 0) + cv_.wait(lock); + + std::unique_ptr<Event> event; + if (!abort_) { + event = std::move(events_.front()); + events_.pop(); + } + + return event; + } + + // Marks the queue as aborted. pop() will now return nullptr. + void abort() { + std::lock_guard<std::mutex> lg(mutex_); + + abort_ = true; + + // Wake before leaving to prevent unpredicatable scheduling. + cv_.notify_all(); + } + + private: + std::queue<std::unique_ptr<Event>> events_; + std::mutex mutex_; + std::condition_variable cv_; + bool abort_ = false; +}; + +#endif // CONTENT_ANALYSIS_DEMO_REQUST_QUEUE_H_ diff --git a/third_party/content_analysis_sdk/docs/code-of-conduct.md b/third_party/content_analysis_sdk/docs/code-of-conduct.md new file mode 100644 index 0000000000..be0f6056c3 --- /dev/null +++ b/third_party/content_analysis_sdk/docs/code-of-conduct.md @@ -0,0 +1,94 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *community@chromium.org*, the +Project Steward(s) for *content_analysis_sdk*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + diff --git a/third_party/content_analysis_sdk/docs/contributing.md b/third_party/content_analysis_sdk/docs/contributing.md new file mode 100644 index 0000000000..3bcdc104c8 --- /dev/null +++ b/third_party/content_analysis_sdk/docs/contributing.md @@ -0,0 +1,29 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to <https://cla.developers.google.com/> to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google/conduct/). + diff --git a/third_party/content_analysis_sdk/prepare_build b/third_party/content_analysis_sdk/prepare_build new file mode 100644 index 0000000000..ce68760f0a --- /dev/null +++ b/third_party/content_analysis_sdk/prepare_build @@ -0,0 +1,48 @@ +#!/bin/bash
+# Copyright 2022 The Chromium Authors.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This script is meant to be run once to setup the example demo agent.
+# Run it with one command line argument: the path to a directory where the
+# demo agent will be built. This should be a directory outside the SDK
+# directory tree. By default, if no directory is supplied, a directory
+# named `build` in the project root will be used.
+#
+# Once the build is prepared, the demo binary is built using the command
+# `cmake --build <build-dir>`, where <build-dir> is the same argument given
+# to this script.
+
+set -euo pipefail
+
+export ROOT_DIR=$(realpath $(dirname $0))
+export DEMO_DIR=$(realpath $ROOT_DIR/demo)
+export PROTO_DIR=$(realpath $ROOT_DIR/proto)
+# Defaults to $ROOT_DIR/build if no argument is provided.
+export BUILD_DIR=$(realpath ${1:-$ROOT_DIR/build})
+
+echo Root dir: $ROOT_DIR
+echo Build dir: $BUILD_DIR
+echo Demo dir: $DEMO_DIR
+echo Proto dir: $PROTO_DIR
+
+# Prepare build directory
+mkdir -p $BUILD_DIR
+# Prepare protobuf out directory
+mkdir -p $BUILD_DIR/gen
+# Enter build directory
+cd $BUILD_DIR
+
+# Install vcpkg and use it to install Google Protocol Buffers.
+test -d vcpkg || (
+ git clone https://github.com/microsoft/vcpkg
+ ./vcpkg/bootstrap-vcpkg.sh -disableMetrics
+)
+# Install any packages we want from vcpkg.
+./vcpkg/vcpkg install protobuf
+./vcpkg/vcpkg install gtest
+
+# Generate the build files.
+export CMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake
+cmake $ROOT_DIR
+
diff --git a/third_party/content_analysis_sdk/prepare_build.bat b/third_party/content_analysis_sdk/prepare_build.bat new file mode 100644 index 0000000000..445752090a --- /dev/null +++ b/third_party/content_analysis_sdk/prepare_build.bat @@ -0,0 +1,68 @@ +REM Copyright 2022 The Chromium Authors.
+REM Use of this source code is governed by a BSD-style license that can be
+REM found in the LICENSE file.
+@echo off
+setlocal
+
+REM This script is meant to be run once to setup the example demo agent.
+REM Run it with one command line argument: the path to a directory where the
+REM demo agent will be built. This should be a directory outside the SDK
+REM directory tree. By default, if no directory is supplied, a directory
+REM named `build` in the project root will be used.
+REM
+REM Once the build is prepared, the demo binary is built using the command
+REM `cmake --build <build-dir>`, where <build-dir> is the same argument given
+REM to this script.
+
+set ROOT_DIR=%~dp0
+call :ABSPATH "%ROOT_DIR%\demo" DEMO_DIR
+call :ABSPATH "%ROOT_DIR%\proto" PROTO_DIR
+
+REM BUILD_DIR defaults to $ROOT_DIR/build if no argument is provided.
+IF "%1" == "" (
+ call :ABSPATH "%ROOT_DIR%\build" BUILD_DIR
+) ELSE (
+ set BUILD_DIR=%~f1
+)
+
+echo .
+echo Root dir: %ROOT_DIR%
+echo Build dir: %BUILD_DIR%
+echo Demo dir: %DEMO_DIR%
+echo Proto dir: %PROTO_DIR%
+echo .
+
+REM Prepare build directory
+mkdir "%BUILD_DIR%"
+REM Prepare protobuf out directory
+mkdir "%BUILD_DIR%\gen"
+REM Enter build directory
+cd /d "%BUILD_DIR%"
+
+REM Install vcpkg and use it to install Google Protocol Buffers.
+IF NOT exist .\vcpkg\ (
+ cmd/c git clone https://github.com/microsoft/vcpkg
+ cmd/c .\vcpkg\bootstrap-vcpkg.bat -disableMetrics
+) ELSE (
+ echo vcpkg is already installed.
+)
+REM Install any packages we want from vcpkg.
+cmd/c .\vcpkg\vcpkg install protobuf:x64-windows
+cmd/c .\vcpkg\vcpkg install gtest:x64-windows
+
+REM Generate the build files.
+set CMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake
+cmake %ROOT_DIR%
+
+echo.
+echo.
+echo To build, type: cmake --build "%BUILD_DIR%"
+echo.
+
+exit /b
+
+REM Resolve relative path in %1 and set it into variable %2.
+:ABSPATH
+ set %2=%~f1
+ exit /b
+
diff --git a/third_party/content_analysis_sdk/proto/content_analysis/sdk/analysis.proto b/third_party/content_analysis_sdk/proto/content_analysis/sdk/analysis.proto new file mode 100644 index 0000000000..0bbd3d4368 --- /dev/null +++ b/third_party/content_analysis_sdk/proto/content_analysis/sdk/analysis.proto @@ -0,0 +1,255 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package content_analysis.sdk; + +// The values in this enum can be extended in future versions of Chrome to +// support new analysis triggers. +enum AnalysisConnector { + ANALYSIS_CONNECTOR_UNSPECIFIED = 0; + FILE_DOWNLOADED = 1; + FILE_ATTACHED = 2; + BULK_DATA_ENTRY = 3; + PRINT = 4; + // This value is not yet implemented in the SDK. It is kept for consistency with the Chromium code. + FILE_TRANSFER = 5; +} + +message ContentMetaData { + // The URL containing the file download/upload or to which web content is + // being uploaded. + optional string url = 1; + + // Name of file on user system (if applicable). + optional string filename = 2; + + // Sha256 digest of file. + optional string digest = 3; + + // Specifically for the download case. + optional ClientDownloadRequest csd = 4; + + // Optional email address of user. This field may be empty if the user + // is not signed in. + optional string email = 5; + + // Name of tab title. + optional string tab_title = 9; + + // Empty for non-print actions. + message PrintMetadata { + optional string printer_name = 1; + + enum PrinterType { + UNKNOWN = 0; + CLOUD = 1; + LOCAL = 2; + } + optional PrinterType printer_type = 2; + } + optional PrintMetadata print_metadata = 11; + + reserved 6 to 8, 10; +} + +message ClientMetadata { + // Describes the browser uploading a scan request. + message Browser { + // This is omitted on scans triggered at the profile level. + optional string machine_user = 4; + + reserved 1 to 3; + }; + optional Browser browser = 1; + + reserved 2 to 3; +}; + +message ClientDownloadRequest { + // Type of the resources stored below. + enum ResourceType { + // The final URL of the download payload. The resource URL should + // correspond to the URL field above. + DOWNLOAD_URL = 0; + // A redirect URL that was fetched before hitting the final DOWNLOAD_URL. + DOWNLOAD_REDIRECT = 1; + // The final top-level URL of the tab that triggered the download. + TAB_URL = 2; + // A redirect URL thas was fetched before hitting the final TAB_URL. + TAB_REDIRECT = 3; + // The document URL for a PPAPI plugin instance that initiated the download. + // This is the document.url for the container element for the plugin + // instance. + PPAPI_DOCUMENT = 4; + // The plugin URL for a PPAPI plugin instance that initiated the download. + PPAPI_PLUGIN = 5; + } + + message Resource { + required string url = 1; + required ResourceType type = 2; + + reserved 3 to 4; + } + + repeated Resource resources = 4; + + reserved 1 to 3, 5 to 84; +} + + +// Analysis request sent from chrome to backend. +// The proto in the Chromium codebase is the source of truth, the version here +// should always be in sync with it (https://osscs.corp.google.com/chromium/chromium/src/+/main:components/enterprise/common/proto/connectors.proto;l=87;drc=a8fb6888aff535f27654f03cd1643868ba066de9). +message ContentAnalysisRequest { + // Token used to correlate requests and responses. This is different than the + // FCM token in that it is unique for each request. + optional string request_token = 5; + + // Which enterprise connector fired this request. + optional AnalysisConnector analysis_connector = 9; + + // Information about the data that triggered the content analysis request. + optional ContentMetaData request_data = 10; + + // The tags configured for the URL that triggered the content analysis. + repeated string tags = 11; + + // Additional information about the browser, device or profile so events can + // be reported with device/user identifiable information. + optional ClientMetadata client_metadata = 12; + + // Data used to transmit print data from the browser. + message PrintData { + // A platform-specific handle that can be used to access the printed document. + optional int64 handle = 1; + + // The size of the data to be printed. + optional int64 size = 2; + } + + oneof content_data { + // The text content to analyze in local content analysis request. + string text_content = 13; + + // The full path to the file to analyze in local content analysis request. + // The path is expressed in a platform dependent way. + string file_path = 14; + + // The to-be-printed page/document in PDF format. + PrintData print_data = 18; + } + + // The absolute deadline (seconds since the UTC Epoch time) that Chrome will + // wait until a response from the agent is received. + optional int64 expires_at = 15; + + // ID for keeping track of analysis requests that belong to the same user + // action. + optional string user_action_id = 16; + + // Count of analysis requests that belong to the same user action. + optional int64 user_action_requests_count = 17; + + // Reserved to make sure there is no overlap with DeepScanningClientRequest. + reserved 1 to 4, 6 to 8; +} + +// Verdict response sent from agent to Google Chrome. +message ContentAnalysisResponse { + // Token used to correlate requests and responses. Corresponds to field in + // ContentAnalysisRequest with the same name. + optional string request_token = 1; + + // Represents the analysis result from a given tag. + message Result { + optional string tag = 1; + + // The status of this result. + enum Status { + STATUS_UNKNOWN = 0; + SUCCESS = 1; + FAILURE = 2; + } + optional Status status = 2; + + // Identifies the detection rules that were triggered by the analysis. + // Only relevant when status is SUCCESS. + message TriggeredRule { + enum Action { + ACTION_UNSPECIFIED = 0; + REPORT_ONLY = 1; + WARN = 2; + BLOCK = 3; + } + optional Action action = 1; + optional string rule_name = 2; + optional string rule_id = 3; + reserved 4; + } + repeated TriggeredRule triggered_rules = 3; + + reserved 4 to 7; + } + repeated Result results = 4; + + reserved 2 to 3; +} + +// An Acknowledgement is sent by the browser following the receipt of a response +// from the agent. +message ContentAnalysisAcknowledgement { + // Token used to correlate with the corresponding request and response. + optional string request_token = 1; + + // The action taken by google Chrome with the content analysis response. + enum Status { + // The response was handled as specified by the agent. + SUCCESS = 1; + + // The response from the agent was not properly formatted. + INVALID_RESPONSE = 2; + + // The response from the agent was too late and Google Chrome took the + // default action. + TOO_LATE = 3; + }; + optional Status status = 2; + + // The final action that chrome took with this request. This may be different + // from the action specified in the response if the response was too late or + // if the original request was part of a user action whose overall final + // differed from the action of this particular request. + enum FinalAction { + ACTION_UNSPECIFIED = 0; + ALLOW = 1; + REPORT_ONLY = 2; + WARN = 3; + BLOCK = 4; + }; + optional FinalAction final_action = 3; +} + +// A message that asks the agent to cancel all requests with the given user +// action id. Note that more that content analysis request may have the given +// user action id. +message ContentAnalysisCancelRequests { + optional string user_action_id = 1; +} + +// Generic message sent from Chrome to Agent. +message ChromeToAgent { + optional ContentAnalysisRequest request = 1; + optional ContentAnalysisAcknowledgement ack = 2; + optional ContentAnalysisCancelRequests cancel = 3; +} + +// Generic message sent from Agent to Chrome. +message AgentToChrome { + optional ContentAnalysisResponse response = 1; +} |