summaryrefslogtreecommitdiffstats
path: root/third_party/content_analysis_sdk
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/content_analysis_sdk')
-rw-r--r--third_party/content_analysis_sdk/.gitignore6
-rw-r--r--third_party/content_analysis_sdk/CMakeLists.txt214
-rw-r--r--third_party/content_analysis_sdk/LICENSE28
-rw-r--r--third_party/content_analysis_sdk/README.md70
-rw-r--r--third_party/content_analysis_sdk/agent/README.md38
-rw-r--r--third_party/content_analysis_sdk/agent/include/content_analysis/sdk/analysis_agent.h288
-rw-r--r--third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.h36
-rw-r--r--third_party/content_analysis_sdk/agent/include/content_analysis/sdk/result_codes.inc25
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_base.cc42
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_base.h40
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_mac.cc34
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_mac.h27
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_posix.cc36
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_posix.h27
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_utils_win.cc28
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_utils_win.h19
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_win.cc546
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_win.h196
-rw-r--r--third_party/content_analysis_sdk/agent/src/agent_win_unittest.cc522
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_base.cc65
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_base.h38
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_mac.cc29
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_mac.h29
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_mac_unittest.cc53
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_posix.cc28
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_posix.h29
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_posix_unittest.cc53
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_win.cc133
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_win.h51
-rw-r--r--third_party/content_analysis_sdk/agent/src/event_win_unittest.cc116
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.cc17
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_base.h25
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.cc36
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_mac.h24
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.cc36
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_posix.h24
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.cc67
-rw-r--r--third_party/content_analysis_sdk/agent/src/scoped_print_handle_win.h29
-rw-r--r--third_party/content_analysis_sdk/browser/include/content_analysis/sdk/analysis_client.h84
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_base.cc21
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_base.h34
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_mac.cc33
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_mac.h28
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_posix.cc33
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_posix.h28
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_win.cc432
-rw-r--r--third_party/content_analysis_sdk/browser/src/client_win.h39
-rw-r--r--third_party/content_analysis_sdk/common/utils_win.cc174
-rw-r--r--third_party/content_analysis_sdk/common/utils_win.h76
-rw-r--r--third_party/content_analysis_sdk/demo/README.md16
-rw-r--r--third_party/content_analysis_sdk/demo/agent.cc189
-rw-r--r--third_party/content_analysis_sdk/demo/atomic_output.h29
-rw-r--r--third_party/content_analysis_sdk/demo/client.cc411
-rw-r--r--third_party/content_analysis_sdk/demo/handler.h449
-rw-r--r--third_party/content_analysis_sdk/demo/handler_misbehaving.h495
-rw-r--r--third_party/content_analysis_sdk/demo/modes.h25
-rw-r--r--third_party/content_analysis_sdk/demo/request_queue.h70
-rw-r--r--third_party/content_analysis_sdk/docs/code-of-conduct.md94
-rw-r--r--third_party/content_analysis_sdk/docs/contributing.md29
-rw-r--r--third_party/content_analysis_sdk/prepare_build48
-rw-r--r--third_party/content_analysis_sdk/prepare_build.bat68
-rw-r--r--third_party/content_analysis_sdk/proto/content_analysis/sdk/analysis.proto255
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;
+}