Bug 1811076: Part 1 - Initial commit of content analysis SDK r=cmartin

SDK from https://github.com/chromium/content_analysis_sdk
Pulled July 31, 2023

Differential Revision: https://phabricator.services.mozilla.com/D189567
This commit is contained in:
David P 2023-10-26 01:17:09 +00:00
parent efeeadc85f
commit 5fdb72e1f5
62 changed files with 5657 additions and 0 deletions

View file

@ -0,0 +1,58 @@
# Stop Windows python license check presubmit errors by forcing LF checkout.
*.py text eol=lf
# Force LF checkout of the pins files to avoid transport_security_state_generator errors.
/net/http/*.pins text eol=lf
# Force LF checkout for all source files
*.bin binary
*.c text eol=lf
*.cc text eol=lf
*.cpp text eol=lf
*.csv text eol=lf
*.grd text eol=lf
*.grdp text eol=lf
*.gn text eol=lf
*.gni text eol=lf
*.h text eol=lf
*.html text eol=lf
*.idl text eol=lf
*.in text eol=lf
*.inc text eol=lf
*.java text eol=lf
*.js text eol=lf
*.json text eol=lf
*.json5 text eol=lf
*.md text eol=lf
*.mm text eol=lf
*.mojom text eol=lf
*.pdf -diff
*.proto text eol=lf
*.rs text eol=lf
*.sh text eol=lf
*.sql text eol=lf
*.toml text eol=lf
*.txt text eol=lf
*.xml text eol=lf
*.xslt text eol=lf
.clang-format text eol=lf
.eslintrc.js text eol=lf
.git-blame-ignore-revs text eol=lf
.gitattributes text eol=lf
.gitignore text eol=lf
.vpython text eol=lf
codereview.settings text eol=lf
DEPS text eol=lf
ENG_REVIEW_OWNERS text eol=lf
LICENSE text eol=lf
LICENSE.* text eol=lf
MAJOR_BRANCH_DATE text eol=lf
OWNERS text eol=lf
README text eol=lf
README.* text eol=lf
WATCHLISTS text eol=lf
VERSION text eol=lf
DIR_METADATA text eol=lf
# Skip Tricium by default on files in third_party.
third_party/** -tricium

View file

@ -0,0 +1,6 @@
.vscode/
.ccls-cache/
.cache/
build/
*.bak
*.swp

View file

@ -0,0 +1,213 @@
# 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_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)

View file

@ -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.

View file

@ -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`

View file

@ -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"
```

View file

@ -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_

View file

@ -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_

View file

@ -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.")

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,45 @@
// 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;
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_

View file

@ -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

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -0,0 +1,428 @@
// 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) {
FlushFileBuffers(hPipe_);
CloseHandle(hPipe_);
hPipe_ = INVALID_HANDLE_VALUE;
}
}
} // namespace sdk
} // namespace content_analysis

View file

@ -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_

View file

@ -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

View file

@ -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_

View file

@ -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.

View file

@ -0,0 +1,117 @@
// 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 <fstream>
#include <iostream>
#include <string>
#include "content_analysis/sdk/analysis_agent.h"
#include "demo/handler.h"
// 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;
unsigned long delay = 0; // In seconds.
unsigned long num_threads = 8u;
std::string save_print_data_path = "";
// Command line parameters.
constexpr const char* kArgDelaySpecific = "--delay=";
constexpr const char* kArgPath = "--path=";
constexpr const char* kArgQueued = "--queued";
constexpr const char* kArgThreads = "--threads=";
constexpr const char* kArgUserSpecific = "--user";
constexpr const char* kArgHelp = "--help";
constexpr const char* kArgSavePrintRequestDataTo = "--save-print-request-data-to=";
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) {
delay = std::stoul(arg.substr(strlen(kArgDelaySpecific)));
if (delay > 30) {
delay = 30;
}
} 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(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 << "<delay> : Add a delay to request processing in seconds (max 30)." << 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
<< kArgHelp << " : prints this help message" << std::endl
<< kArgSavePrintRequestDataTo << " : saves the PDF data to the given file path for print requests";
}
int main(int argc, char* argv[]) {
if (!ParseCommandLine(argc, argv)) {
PrintHelp();
return 1;
}
auto handler = use_queue
? std::make_unique<QueuingHandler>(num_threads, delay, save_print_data_path)
: std::make_unique<Handler>(delay, save_print_data_path);
// 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;
}

View file

@ -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_;
};

View file

@ -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;
};

View file

@ -0,0 +1,384 @@
// 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 <chrono>
#include <fstream>
#include <iostream>
#include <thread>
#include <utility>
#include "content_analysis/sdk/analysis_agent.h"
#include "demo/atomic_output.h"
#include "demo/request_queue.h"
// 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(unsigned long delay, const std::string& print_data_file_path) :
delay_(delay), print_data_file_path_(print_data_file_path) {
}
unsigned long delay() { return delay_; }
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 block = false;
bool success = true;
if (event->GetRequest().has_text_content()) {
block = ShouldBlockRequest(
event->GetRequest().text_content());
} else if (event->GetRequest().has_file_path()) {
std::string content;
success =
ReadContentFromFile(event->GetRequest().file_path(),
&content);
if (success) {
block = ShouldBlockRequest(content);
}
} 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".
block = ShouldBlockRequest(event->GetRequest().request_data().url());
}
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 if (block) {
auto rc = content_analysis::sdk::SetEventVerdictToBlock(event.get());
stream << " Verdict: block";
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;
// If a delay is specified, wait that much.
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, bulk text entry or print";
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 << " 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;
}
bool ShouldBlockRequest(const std::string& content) {
// Determines if the request should be blocked. For this simple example
// the content is blocked if the string "block" is found. Otherwise the
// content is allowed.
return content.find("block") != std::string::npos;
}
unsigned long delay_;
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, unsigned long delay, const std::string& print_data_file_path)
: Handler(delay, print_data_file_path) {
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->delay() << "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_

View file

@ -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_

View file

@ -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 projects 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 Stewards 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

View file

@ -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/).

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -7,6 +7,9 @@ with Files('moz.build'):
with Files('aom/**'):
BUG_COMPONENT = ('Core', 'Audio/Video: Playback')
with Files('content_analysis_sdk/**'):
BUG_COMPONENT = ('Firefox', 'Data Loss Prevention')
with Files('cups/**'):
BUG_COMPONENT = ('Core', 'Printing: Setup')