├── .gitignore ├── agent ├── src │ ├── scoped_print_handle_base.cc │ ├── agent_utils_win.h │ ├── agent_mac.h │ ├── agent_posix.h │ ├── event_mac.cc │ ├── scoped_print_handle_mac.h │ ├── event_posix.cc │ ├── scoped_print_handle_base.h │ ├── scoped_print_handle_posix.h │ ├── agent_utils_win.cc │ ├── event_mac.h │ ├── scoped_print_handle_win.h │ ├── event_posix.h │ ├── agent_mac.cc │ ├── agent_posix.cc │ ├── scoped_print_handle_mac.cc │ ├── scoped_print_handle_posix.cc │ ├── agent_base.cc │ ├── agent_base.h │ ├── event_win.h │ ├── event_base.h │ ├── event_mac_unittest.cc │ ├── event_posix_unittest.cc │ ├── event_base.cc │ ├── scoped_print_handle_win.cc │ ├── event_win.cc │ ├── event_win_unittest.cc │ ├── agent_win.h │ ├── agent_win_unittest.cc │ └── agent_win.cc ├── include │ └── content_analysis │ │ └── sdk │ │ ├── result_codes.h │ │ ├── result_codes.inc │ │ └── analysis_agent.h └── README.md ├── browser ├── src │ ├── client_base.cc │ ├── client_mac.cc │ ├── client_mac.h │ ├── client_posix.cc │ ├── client_posix.h │ ├── client_base.h │ ├── client_win.h │ └── client_win.cc └── include │ └── content_analysis │ └── sdk │ └── analysis_client.h ├── demo ├── README.md ├── atomic_output.h ├── request_queue.h ├── agent.cc ├── client.cc └── handler.h ├── docs ├── contributing.md └── code-of-conduct.md ├── .gitattributes ├── LICENSE ├── prepare_build ├── prepare_build.bat ├── common ├── utils_win.h └── utils_win.cc ├── README.md ├── CMakeLists.txt └── proto └── content_analysis └── sdk └── analysis.proto /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .ccls-cache/ 3 | .cache/ 4 | build/ 5 | *.bak 6 | *.swp 7 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_base.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "scoped_print_handle_base.h" 6 | 7 | namespace content_analysis { 8 | namespace sdk { 9 | 10 | ScopedPrintHandleBase::ScopedPrintHandleBase( 11 | const ContentAnalysisRequest::PrintData& print_data) 12 | : size_(print_data.size()) {} 13 | 14 | size_t ScopedPrintHandleBase::size() { return size_; } 15 | 16 | } // namespace sdk 17 | } // namespace content_analysis 18 | -------------------------------------------------------------------------------- /browser/src/client_base.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "client_base.h" 6 | 7 | namespace content_analysis { 8 | namespace sdk { 9 | 10 | ClientBase::ClientBase(Config config) : config_(config) {} 11 | 12 | const Client::Config& ClientBase::GetConfig() const { 13 | return config_; 14 | } 15 | 16 | const AgentInfo& ClientBase::GetAgentInfo() const { 17 | return agent_info_; 18 | } 19 | 20 | } // namespace sdk 21 | } // namespace content_analysis 22 | -------------------------------------------------------------------------------- /agent/src/agent_utils_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ 6 | #define CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ 7 | 8 | #include "event_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Maps a Windows error status code (ERROR_xxx codes) to an SDK result code. 14 | ResultCode ErrorToResultCode(DWORD err); 15 | 16 | } // namespace sdk 17 | } // namespace content_analysis 18 | 19 | #endif // CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ 20 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Google Chrome Content Analysis Connector Agent SDK Demo 2 | 3 | This directory holds the Google Chrome Content Analysis Connector Agent SDK Demo. 4 | It contains an example of how to use the SDK. 5 | 6 | Build instructions are available in the main project `README.md`. 7 | 8 | ## Demo agent permissions 9 | On Microsoft Windows, if the demo agent is run without the `--user` command line 10 | argument it must have Administrator privileges in order to properly create the 11 | pipe used to communicate with the browser. The demo browser must also be run 12 | without the `--user` command line argument. 13 | 14 | Otherwise the agent may run as any user, with or without Administrator 15 | privileges. The demo browser must also be run with the `--user` command line 16 | argument and run as the same user. -------------------------------------------------------------------------------- /agent/src/agent_mac.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_SRC_AGENT_MAC_H_ 6 | #define CONTENT_ANALYSIS_SRC_AGENT_MAC_H_ 7 | 8 | #include "agent_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Agent implementaton for macOS. 14 | class AgentMac : public AgentBase { 15 | public: 16 | AgentMac(Config config, std::unique_ptr handler); 17 | 18 | ResultCode HandleEvents() override; 19 | std::string DebugString() const override; 20 | 21 | // TODO(rogerta): Fill in implementation. 22 | }; 23 | 24 | } // namespace sdk 25 | } // namespace content_analysis 26 | 27 | #endif // CONTENT_ANALYSIS_SRC_AGENT_MAC_H_ 28 | -------------------------------------------------------------------------------- /agent/src/agent_posix.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_SRC_AGENT_POSIX_H_ 6 | #define CONTENT_ANALYSIS_SRC_AGENT_POSIX_H_ 7 | 8 | #include "agent_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Agent implementaton for linux. 14 | class AgentPosix : public AgentBase { 15 | public: 16 | AgentPosix(Config config, std::unique_ptr handler); 17 | 18 | ResultCode HandleEvents() override; 19 | std::string DebugString() const override; 20 | 21 | // TODO(rogerta): Fill in implementation. 22 | }; 23 | 24 | } // namespace sdk 25 | } // namespace content_analysis 26 | 27 | #endif // CONTENT_ANALYSIS_SRC_AGENT_POSIX_H_ -------------------------------------------------------------------------------- /agent/src/event_mac.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "event_mac.h" 6 | 7 | #include "scoped_print_handle_mac.h" 8 | 9 | namespace content_analysis { 10 | namespace sdk { 11 | 12 | ContentAnalysisEventMac::ContentAnalysisEventMac( 13 | const BrowserInfo& browser_info, 14 | ContentAnalysisRequest req) 15 | : ContentAnalysisEventBase(browser_info) { 16 | *request() = std::move(req); 17 | } 18 | 19 | ResultCode ContentAnalysisEventMac::Send() { 20 | return ResultCode::ERR_UNEXPECTED; 21 | } 22 | 23 | std::string ContentAnalysisEventMac::DebugString() const { 24 | return std::string(); 25 | } 26 | 27 | 28 | } // namespace sdk 29 | } // namespace content_analysis 30 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_mac.h: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_MAC_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_MAC_H_ 7 | 8 | #include "scoped_print_handle_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | class ScopedPrintHandleMac : public ScopedPrintHandleBase { 14 | public: 15 | ScopedPrintHandleMac(const ContentAnalysisRequest::PrintData& print_data); 16 | ~ScopedPrintHandleMac() override; 17 | 18 | const char* data() override; 19 | }; 20 | 21 | } // namespace sdk 22 | } // namespace content_analysis 23 | 24 | #endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_MAC_H_ 25 | -------------------------------------------------------------------------------- /demo/atomic_output.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | // Utility class to atomically write outout to std::cout. All data streamed 10 | // the class is automatically sent to std::cout in the dtor. This is useful 11 | // to keep the output of multiple threads writing to std::Cout from 12 | // interleaving. 13 | 14 | class AtomicCout { 15 | public: 16 | ~AtomicCout() { 17 | flush(); 18 | } 19 | 20 | std::stringstream& stream() { return stream_; } 21 | 22 | void flush() { 23 | std::cout << stream_.str(); 24 | stream_.str(std::string()); 25 | } 26 | 27 | private: 28 | std::stringstream stream_; 29 | }; -------------------------------------------------------------------------------- /agent/src/event_posix.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "event_posix.h" 6 | 7 | #include "scoped_print_handle_posix.h" 8 | 9 | namespace content_analysis { 10 | namespace sdk { 11 | 12 | ContentAnalysisEventPosix::ContentAnalysisEventPosix( 13 | const BrowserInfo& browser_info, 14 | ContentAnalysisRequest req) 15 | : ContentAnalysisEventBase(browser_info) { 16 | *request() = std::move(req); 17 | } 18 | 19 | ResultCode ContentAnalysisEventPosix::Send() { 20 | return ResultCode::ERR_UNEXPECTED; 21 | } 22 | 23 | std::string ContentAnalysisEventPosix::DebugString() const { 24 | return std::string(); 25 | } 26 | 27 | } // namespace sdk 28 | } // namespace content_analysis 29 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_base.h: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_BASE_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_BASE_H_ 7 | 8 | #include "content_analysis/sdk/analysis_agent.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | class ScopedPrintHandleBase : public ScopedPrintHandle { 14 | public: 15 | ScopedPrintHandleBase(const ContentAnalysisRequest::PrintData& print_data); 16 | 17 | size_t size() override; 18 | protected: 19 | size_t size_ = 0; 20 | }; 21 | 22 | } // namespace sdk 23 | } // namespace content_analysis 24 | 25 | #endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_BASE_H_ 26 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_posix.h: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_POSIX_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_POSIX_H_ 7 | 8 | #include "scoped_print_handle_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | class ScopedPrintHandlePosix : public ScopedPrintHandleBase { 14 | public: 15 | ScopedPrintHandlePosix(const ContentAnalysisRequest::PrintData& print_data); 16 | ~ScopedPrintHandlePosix() override; 17 | 18 | const char* data() override; 19 | }; 20 | 21 | } // namespace sdk 22 | } // namespace content_analysis 23 | 24 | #endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_POSIX_H_ 25 | -------------------------------------------------------------------------------- /browser/src/client_mac.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "client_mac.h" 8 | 9 | namespace content_analysis { 10 | namespace sdk { 11 | 12 | // static 13 | std::unique_ptr Client::Create(Config config) { 14 | return nullptr; 15 | } 16 | 17 | ClientMac::ClientMac(Config config) : ClientBase(std::move(config)) {} 18 | 19 | int ClientMac::Send(ContentAnalysisRequest request, 20 | ContentAnalysisResponse* response) { 21 | return -1; 22 | } 23 | 24 | int ClientMac::Acknowledge(const ContentAnalysisAcknowledgement& ack) { 25 | return -1; 26 | } 27 | 28 | int ClientMac::CancelRequests(const ContentAnalysisCancelRequests& cancel) { 29 | return -1; 30 | } 31 | 32 | } // namespace sdk 33 | } // namespace content_analysis 34 | -------------------------------------------------------------------------------- /agent/src/agent_utils_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "content_analysis/sdk/result_codes.h" 8 | 9 | namespace content_analysis { 10 | namespace sdk { 11 | 12 | #define ERR_TO_RC(ERR, RC) case ERR: return ResultCode::RC; 13 | 14 | ResultCode ErrorToResultCode(DWORD err) { 15 | switch (err) { 16 | ERR_TO_RC(ERROR_SUCCESS, OK); 17 | ERR_TO_RC(ERROR_ACCESS_DENIED, ERR_AGENT_ALREADY_EXISTS); 18 | ERR_TO_RC(ERROR_BROKEN_PIPE, ERR_BROKEN_PIPE); 19 | ERR_TO_RC(ERROR_INVALID_NAME, ERR_INVALID_CHANNEL_NAME); 20 | ERR_TO_RC(ERROR_MORE_DATA, ERR_MORE_DATA); 21 | ERR_TO_RC(ERROR_IO_PENDING, ERR_IO_PENDING); 22 | default: 23 | return ResultCode::ERR_UNEXPECTED; 24 | } 25 | } 26 | 27 | } // namespace sdk 28 | } // namespace content_analysis 29 | -------------------------------------------------------------------------------- /agent/src/event_mac.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ 6 | #define CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ 7 | 8 | #include "event_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // ContentAnalysisEvent implementaton for macOS. 14 | class ContentAnalysisEventMac : public ContentAnalysisEventBase { 15 | public: 16 | ContentAnalysisEventMac(const BrowserInfo& browser_info, 17 | ContentAnalysisRequest request); 18 | 19 | // ContentAnalysisEvent: 20 | ResultCode Send() override; 21 | std::string DebugString() const override; 22 | 23 | // TODO(rogerta): Fill in implementation. 24 | }; 25 | 26 | } // namespace sdk 27 | } // namespace content_analysis 28 | 29 | #endif // CONTENT_ANALYSIS_SRC_EVENT_MAC_H_ 30 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_WIN_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_WIN_H_ 7 | 8 | #include 9 | 10 | #include "scoped_print_handle_base.h" 11 | 12 | namespace content_analysis { 13 | namespace sdk { 14 | 15 | class ScopedPrintHandleWin : public ScopedPrintHandleBase { 16 | public: 17 | ScopedPrintHandleWin(const ContentAnalysisRequest::PrintData& print_data); 18 | ~ScopedPrintHandleWin() override; 19 | 20 | const char* data() override; 21 | private: 22 | void* mapped_ = nullptr; 23 | HANDLE handle_ = nullptr; 24 | }; 25 | 26 | } // namespace sdk 27 | } // namespace content_analysis 28 | 29 | #endif // CONTENT_ANALYSIS_AGENT_SRC_SCOPED_PRINT_HANDLE_WIN_H_ 30 | -------------------------------------------------------------------------------- /browser/src/client_mac.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_MAC_H_ 6 | #define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_MAC_H_ 7 | 8 | #include "client_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Client implementaton for macOS. 14 | class ClientMac : public ClientBase { 15 | public: 16 | ClientMac(Config config); 17 | 18 | // Client: 19 | int Send(ContentAnalysisRequest request, 20 | ContentAnalysisResponse* response) override; 21 | int Acknowledge(const ContentAnalysisAcknowledgement& ack) override; 22 | int CancelRequests(const ContentAnalysisCancelRequests& cancel) override; 23 | }; 24 | 25 | } // namespace sdk 26 | } // namespace content_analysis 27 | 28 | #endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_MAC_H_ -------------------------------------------------------------------------------- /browser/src/client_posix.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "client_posix.h" 8 | 9 | namespace content_analysis { 10 | namespace sdk { 11 | 12 | // static 13 | std::unique_ptr Client::Create(Config config) { 14 | return nullptr; 15 | } 16 | 17 | ClientPosix::ClientPosix(Config config) : ClientBase(std::move(config)) {} 18 | 19 | int ClientPosix::Send(ContentAnalysisRequest request, 20 | ContentAnalysisResponse* response) { 21 | return -1; 22 | } 23 | 24 | int ClientPosix::Acknowledge(const ContentAnalysisAcknowledgement& ack) { 25 | return -1; 26 | } 27 | 28 | int ClientPosix::CancelRequests(const ContentAnalysisCancelRequests& cancel) { 29 | return -1; 30 | } 31 | 32 | } // namespace sdk 33 | } // namespace content_analysis 34 | -------------------------------------------------------------------------------- /agent/src/event_posix.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_SRC_EVENT_POSIX_H_ 6 | #define CONTENT_ANALYSIS_SRC_EVENT_POSIX_H_ 7 | 8 | #include "event_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // ContentAnalysisEvent implementaton for linux. 14 | class ContentAnalysisEventPosix : public ContentAnalysisEventBase { 15 | public: 16 | ContentAnalysisEventPosix(const BrowserInfo& browser_info, 17 | ContentAnalysisRequest request); 18 | 19 | // ContentAnalysisEvent: 20 | ResultCode Send() override; 21 | std::string DebugString() const override; 22 | 23 | // TODO(rogerta): Fill in implementation. 24 | }; 25 | 26 | } // namespace sdk 27 | } // namespace content_analysis 28 | 29 | #endif // CONTENT_ANALYSIS_SRC_EVENT_POSIX_H_ -------------------------------------------------------------------------------- /browser/src/client_posix.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_POSIX_H_ 6 | #define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_POSIX_H_ 7 | 8 | #include "client_base.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Client implementaton for Posix. 14 | class ClientPosix : public ClientBase { 15 | public: 16 | ClientPosix(Config config); 17 | 18 | // Client: 19 | int Send(ContentAnalysisRequest request, 20 | ContentAnalysisResponse* response) override; 21 | int Acknowledge(const ContentAnalysisAcknowledgement& ack) override; 22 | int CancelRequests(const ContentAnalysisCancelRequests& cancel) override; 23 | }; 24 | 25 | } // namespace sdk 26 | } // namespace content_analysis 27 | 28 | #endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_POSIX_H_ -------------------------------------------------------------------------------- /agent/src/agent_mac.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "agent_mac.h" 6 | #include "event_mac.h" 7 | 8 | namespace content_analysis { 9 | namespace sdk { 10 | 11 | // static 12 | std::unique_ptr Agent::Create( 13 | Config config, 14 | std::unique_ptr handler, 15 | ResultCode* rc) { 16 | *rc = ResultCode::ERR_UNEXPECTED; 17 | return std::make_unique(std::move(config), std::move(handler)); 18 | } 19 | 20 | AgentMac::AgentMac( 21 | Config config, 22 | std::unique_ptr handler) 23 | : AgentBase(std::move(config), std::move(handler)) {} 24 | 25 | ResultCode AgentMac::HandleEvents() { 26 | return ResultCode::ERR_UNEXPECTED; 27 | } 28 | 29 | std::string AgentMac::DebugString() const { 30 | return std::string(); 31 | } 32 | 33 | } // namespace sdk 34 | } // namespace content_analysis 35 | -------------------------------------------------------------------------------- /browser/src/client_base.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_BASE_H_ 6 | #define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_BASE_H_ 7 | 8 | #include "content_analysis/sdk/analysis_client.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Base Client class with code common to all platforms. 14 | class ClientBase : public Client { 15 | public: 16 | // Client: 17 | const Config& GetConfig() const override; 18 | const AgentInfo& GetAgentInfo() const override; 19 | 20 | protected: 21 | ClientBase(Config config); 22 | 23 | const Config& configuration() const { return config_; } 24 | AgentInfo& agent_info() { return agent_info_; } 25 | 26 | private: 27 | Config config_; 28 | AgentInfo agent_info_; 29 | }; 30 | 31 | } // namespace sdk 32 | } // namespace content_analysis 33 | 34 | #endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_BASE_H_ -------------------------------------------------------------------------------- /agent/src/agent_posix.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "agent_posix.h" 8 | #include "event_posix.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // static 14 | std::unique_ptr Agent::Create( 15 | Config config, 16 | std::unique_ptr handler, 17 | ResultCode* rc) { 18 | *rc = ResultCode::ERR_UNEXPECTED; 19 | return std::make_unique(std::move(config), std::move(handler)); 20 | } 21 | 22 | AgentPosix::AgentPosix( 23 | Config config, 24 | std::unique_ptr handler) 25 | : AgentBase(std::move(config), std::move(handler)) {} 26 | 27 | ResultCode AgentPosix::HandleEvents() { 28 | return ResultCode::ERR_UNEXPECTED; 29 | } 30 | 31 | std::string AgentPosix::DebugString() const { 32 | return std::string(); 33 | } 34 | 35 | } // namespace sdk 36 | } // namespace content_analysis 37 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_mac.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "scoped_print_handle_mac.h" 6 | 7 | namespace content_analysis { 8 | namespace sdk { 9 | 10 | std::unique_ptr 11 | CreateScopedPrintHandle(const ContentAnalysisRequest& request, 12 | int64_t browser_pid) { 13 | if (!request.has_print_data() || !request.print_data().has_handle()) { 14 | return nullptr; 15 | } 16 | 17 | return std::make_unique(request.print_data()); 18 | } 19 | 20 | ScopedPrintHandleMac::ScopedPrintHandleMac( 21 | const ContentAnalysisRequest::PrintData& print_data) 22 | : ScopedPrintHandleBase(print_data) { 23 | // TODO 24 | } 25 | 26 | ScopedPrintHandleMac::~ScopedPrintHandleMac() { 27 | // TODO 28 | } 29 | 30 | const char* ScopedPrintHandleMac::data() { 31 | // TODO 32 | return nullptr; 33 | } 34 | 35 | } // namespace sdk 36 | } // namespace content_analysis 37 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_posix.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "scoped_print_handle_posix.h" 6 | 7 | namespace content_analysis { 8 | namespace sdk { 9 | 10 | std::unique_ptr 11 | CreateScopedPrintHandle(const ContentAnalysisRequest& request, 12 | int64_t browser_pid) { 13 | if (!request.has_print_data() || !request.print_data().has_handle()) { 14 | return nullptr; 15 | } 16 | 17 | return std::make_unique(request.print_data()); 18 | } 19 | 20 | ScopedPrintHandlePosix::ScopedPrintHandlePosix( 21 | const ContentAnalysisRequest::PrintData& print_data) 22 | : ScopedPrintHandleBase(print_data) { 23 | // TODO 24 | } 25 | 26 | ScopedPrintHandlePosix::~ScopedPrintHandlePosix() { 27 | // TODO 28 | } 29 | 30 | const char* ScopedPrintHandlePosix::data() { 31 | // TODO 32 | return nullptr; 33 | } 34 | 35 | } // namespace sdk 36 | } // namespace content_analysis 37 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | 30 | -------------------------------------------------------------------------------- /browser/src/client_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_WIN_H_ 6 | #define CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_WIN_H_ 7 | 8 | #include 9 | 10 | #include "client_base.h" 11 | 12 | namespace content_analysis { 13 | namespace sdk { 14 | 15 | // Client implementaton for Windows. 16 | class ClientWin : public ClientBase { 17 | public: 18 | ClientWin(Config config, int* rc); 19 | ~ClientWin() override; 20 | 21 | // Client: 22 | int Send(ContentAnalysisRequest request, 23 | ContentAnalysisResponse* response) override; 24 | int Acknowledge(const ContentAnalysisAcknowledgement& ack) override; 25 | int CancelRequests(const ContentAnalysisCancelRequests& cancel) override; 26 | 27 | private: 28 | static DWORD ConnectToPipe(const std::string& pipename, HANDLE* handle); 29 | 30 | // Performs a clean shutdown of the client. 31 | void Shutdown(); 32 | 33 | HANDLE hPipe_ = INVALID_HANDLE_VALUE; 34 | }; 35 | 36 | } // namespace sdk 37 | } // namespace content_analysis 38 | 39 | #endif // CONTENT_ANALYSIS_BROWSER_SRC_CLIENT_WIN_H_ 40 | -------------------------------------------------------------------------------- /agent/src/agent_base.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "agent_base.h" 8 | 9 | namespace content_analysis { 10 | namespace sdk { 11 | 12 | AgentBase::AgentBase(Config config, std::unique_ptr handler) 13 | : config_(std::move(config)), handler_(std::move(handler)) {} 14 | 15 | const Agent::Config& AgentBase::GetConfig() const { 16 | return config_; 17 | } 18 | 19 | ResultCode AgentBase::Stop() { 20 | return ResultCode::OK; 21 | } 22 | 23 | ResultCode AgentBase::NotifyError(const char* context, ResultCode error) { 24 | if (handler_) { 25 | handler_->OnInternalError(context, error); 26 | } 27 | return error; 28 | } 29 | 30 | #define RC_RECOVERABLE(RC, MSG) case ResultCode::RC: return MSG; 31 | #define RC_UNRECOVERABLE(RC, MSG) case ResultCode::RC: return MSG; 32 | const char* ResultCodeToString(ResultCode rc) { 33 | switch (rc) { 34 | #include "content_analysis/sdk/result_codes.inc" 35 | } 36 | return "Unknown error code."; 37 | } 38 | #undef RC_RECOVERABLE 39 | #undef RC_UNRECOVERABLE 40 | 41 | } // namespace sdk 42 | } // namespace content_analysis 43 | -------------------------------------------------------------------------------- /agent/src/agent_base.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_AGENT_BASE_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_AGENT_BASE_H_ 7 | 8 | #include 9 | 10 | #include "content_analysis/sdk/analysis_agent.h" 11 | 12 | namespace content_analysis { 13 | namespace sdk { 14 | 15 | // Base Agent class with code common to all platforms. 16 | class AgentBase : public Agent { 17 | public: 18 | // Agent: 19 | const Config& GetConfig() const override; 20 | ResultCode Stop() override; 21 | 22 | protected: 23 | AgentBase(Config config, std::unique_ptr handler); 24 | 25 | AgentEventHandler* handler() const { return handler_.get(); } 26 | const Config& configuration() const { return config_; } 27 | 28 | // Notifies the handler of the given error. Returns the error 29 | // passed into the method. 30 | ResultCode NotifyError(const char* context, ResultCode error); 31 | 32 | private: 33 | Config config_; 34 | std::unique_ptr handler_; 35 | }; 36 | 37 | } // namespace sdk 38 | } // namespace content_analysis 39 | 40 | #endif // CONTENT_ANALYSIS_AGENT_SRC_AGENT_BASE_H_ -------------------------------------------------------------------------------- /agent/include/content_analysis/sdk/result_codes.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_RESULT_CODES_H_ 6 | #define CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_RESULT_CODES_H_ 7 | 8 | #include 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Result codes of methods and functions. 14 | 15 | #define RC_RECOVERABLE(RC, MSG) RC, 16 | #define RC_UNRECOVERABLE(RC, MSG) RC, 17 | enum class ResultCode { 18 | #include "content_analysis/sdk/result_codes.inc" 19 | }; 20 | #undef RC_RECOVERABLE 21 | #undef RC_UNRECOVERABLE 22 | 23 | // Returns true if the error is recoverable. A recoverable errors means the 24 | // agent may still receive new requests from Google Chrome. An unrecoverable 25 | // error means the agent is unlikely to get more request from Google Chrome. 26 | inline bool IsRecoverableError(ResultCode rc) { 27 | return rc < ResultCode::ERR_FIRST_UNRECOVERABLE_ERROR; 28 | } 29 | 30 | // Returns a human readable error for the given result code. 31 | const char* ResultCodeToString(ResultCode rc); 32 | 33 | } // namespace sdk 34 | } // namespace content_analysis 35 | 36 | #endif // CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_RESULT_CODES_H_ 37 | -------------------------------------------------------------------------------- /agent/src/event_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_EVENT_WIN_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_EVENT_WIN_H_ 7 | 8 | #include 9 | 10 | #include "event_base.h" 11 | 12 | namespace content_analysis { 13 | namespace sdk { 14 | 15 | // ContentAnalysisEvent implementaton for Windows. 16 | class ContentAnalysisEventWin : public ContentAnalysisEventBase { 17 | public: 18 | ContentAnalysisEventWin(HANDLE handle, 19 | const BrowserInfo& browser_info, 20 | ContentAnalysisRequest request); 21 | ~ContentAnalysisEventWin() override; 22 | 23 | // Initialize the event. This involves reading the request from Google 24 | // Chrome and making sure it is well formed. 25 | ResultCode Init(); 26 | 27 | // ContentAnalysisEvent: 28 | ResultCode Close() override; 29 | ResultCode Send() override; 30 | std::string DebugString() const override; 31 | 32 | private: 33 | void Shutdown(); 34 | 35 | // This handle is not owned by the event. 36 | HANDLE hPipe_ = INVALID_HANDLE_VALUE; 37 | 38 | // Set to true when Send() is called the first time. 39 | bool response_sent_ = false; 40 | }; 41 | 42 | } // namespace sdk 43 | } // namespace content_analysis 44 | 45 | #endif // CONTENT_ANALYSIS_AGENT_SRC_EVENT_WIN_H_ -------------------------------------------------------------------------------- /agent/src/event_base.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_EVENT_BASE_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_EVENT_BASE_H_ 7 | 8 | #include "content_analysis/sdk/analysis_agent.h" 9 | 10 | namespace content_analysis { 11 | namespace sdk { 12 | 13 | // Base ContentAnalysisEvent class with code common to all platforms. 14 | class ContentAnalysisEventBase : public ContentAnalysisEvent { 15 | public: 16 | // ContentAnalysisEvent: 17 | ResultCode Close() override; 18 | const BrowserInfo& GetBrowserInfo() const override { return browser_info_; } 19 | const ContentAnalysisRequest& GetRequest() const override { return request_; } 20 | ContentAnalysisResponse& GetResponse() override { return *response(); } 21 | 22 | protected: 23 | explicit ContentAnalysisEventBase(const BrowserInfo& browser_info); 24 | 25 | ContentAnalysisRequest* request() { return &request_; } 26 | AgentToChrome* agent_to_chrome() { return &agent_to_chrome_; } 27 | ContentAnalysisResponse* response() { return agent_to_chrome()->mutable_response(); } 28 | 29 | private: 30 | BrowserInfo browser_info_; 31 | ContentAnalysisRequest request_; 32 | AgentToChrome agent_to_chrome_; 33 | }; 34 | 35 | } // namespace sdk 36 | } // namespace content_analysis 37 | 38 | #endif // CONTENT_ANALYSIS_AGENT_SRC_EVENT_BASE_H_ 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Stop Windows python license check presubmit errors by forcing LF checkout. 2 | *.py text eol=lf 3 | 4 | # Force LF checkout of the pins files to avoid transport_security_state_generator errors. 5 | /net/http/*.pins text eol=lf 6 | 7 | # Force LF checkout for all source files 8 | *.bin binary 9 | *.c text eol=lf 10 | *.cc text eol=lf 11 | *.cpp text eol=lf 12 | *.csv text eol=lf 13 | *.grd text eol=lf 14 | *.grdp text eol=lf 15 | *.gn text eol=lf 16 | *.gni text eol=lf 17 | *.h text eol=lf 18 | *.html text eol=lf 19 | *.idl text eol=lf 20 | *.in text eol=lf 21 | *.inc text eol=lf 22 | *.java text eol=lf 23 | *.js text eol=lf 24 | *.json text eol=lf 25 | *.json5 text eol=lf 26 | *.md text eol=lf 27 | *.mm text eol=lf 28 | *.mojom text eol=lf 29 | *.pdf -diff 30 | *.proto text eol=lf 31 | *.rs text eol=lf 32 | *.sh text eol=lf 33 | *.sql text eol=lf 34 | *.toml text eol=lf 35 | *.txt text eol=lf 36 | *.xml text eol=lf 37 | *.xslt text eol=lf 38 | .clang-format text eol=lf 39 | .eslintrc.js text eol=lf 40 | .git-blame-ignore-revs text eol=lf 41 | .gitattributes text eol=lf 42 | .gitignore text eol=lf 43 | .vpython text eol=lf 44 | codereview.settings text eol=lf 45 | DEPS text eol=lf 46 | ENG_REVIEW_OWNERS text eol=lf 47 | LICENSE text eol=lf 48 | LICENSE.* text eol=lf 49 | MAJOR_BRANCH_DATE text eol=lf 50 | OWNERS text eol=lf 51 | README text eol=lf 52 | README.* text eol=lf 53 | WATCHLISTS text eol=lf 54 | VERSION text eol=lf 55 | DIR_METADATA text eol=lf 56 | 57 | # Skip Tricium by default on files in third_party. 58 | third_party/** -tricium 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google LLC nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # Google Chrome Content Analysis Connector Agent SDK 2 | 3 | This directory holds the Google Chrome Content Analysis Connector Agent SDK. 4 | An Agent is an OS process running on the same computer as Google Chrome 5 | that listens for and processes content analysis requests from the browser. 6 | Supported OSes are Windows, Mac, and Linux. 7 | 8 | ## Google Protocol Buffers 9 | 10 | This SDK depends on Google Protocol Buffers version 3.18 or later. It may be 11 | installed from Google's [download page](https://developers.google.com/protocol-buffers/docs/downloads#release-packages) 12 | for your developement platform. It may also be installed using a package 13 | manager. 14 | 15 | The included demo uses the Microsoft [vcpkg](https://github.com/microsoft/vcpkg) 16 | package manager to install protobuf. vcpkg is available on all supported 17 | platforms. See the demo for more details. 18 | 19 | ## Adding the SDK into an agent 20 | 21 | Add the SDK to a content analysis agent as follows: 22 | 23 | 1. Clone the SDK from [Github](https://github.com/chromium/content_analysis_sdk). 24 | This document assumes that the SDK is downloaded and extracted into the 25 | directory $SDK_DIR. 26 | 27 | 2. Add the directory $SDK_DIR/include to the include path of the agent 28 | code base. 29 | 30 | 3. Add all the source files in the directory $SDK_DIR/src to the agent build. 31 | Note that files ending in _win.cc or _win.h are meant only for Windows, files 32 | ending in _posix.cc or _posix.h are meant only for Linux, and files ending in 33 | _mac.cc or _mac.h are meant only for Mac. 34 | 35 | 4. Reference the SDK in agent code using: 36 | ``` 37 | #include "content_analysis/sdk/local_analysis.h" 38 | ``` -------------------------------------------------------------------------------- /agent/src/event_mac_unittest.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "agent/src/event_mac.h" 8 | #include "content_analysis/sdk/analysis_agent.h" 9 | #include "gtest/gtest.h" 10 | 11 | namespace content_analysis { 12 | namespace sdk { 13 | namespace testing { 14 | 15 | std::unique_ptr CreateEvent( 16 | const BrowserInfo& browser_info, 17 | ContentAnalysisRequest request) { 18 | return std::make_unique( 19 | browser_info, std::move(request)); 20 | } 21 | 22 | TEST(EventTest, Create_BrowserInfo) { 23 | const BrowserInfo bi{12345, "/path/to/binary"}; 24 | ContentAnalysisRequest request; 25 | *request.add_tags() = "foo"; 26 | request.set_request_token("req-token"); 27 | 28 | auto event = CreateEvent(bi, request); 29 | ASSERT_TRUE(event); 30 | 31 | ASSERT_EQ(bi.pid, event->GetBrowserInfo().pid); 32 | ASSERT_EQ(bi.binary_path, event->GetBrowserInfo().binary_path); 33 | } 34 | 35 | TEST(EventTest, Create_Request) { 36 | const BrowserInfo bi{ 12345, "/path/to/binary" }; 37 | ContentAnalysisRequest request; 38 | *request.add_tags() = "foo"; 39 | request.set_request_token("req-token"); 40 | 41 | auto event = CreateEvent(bi, request); 42 | ASSERT_TRUE(event); 43 | 44 | ASSERT_EQ(1u, event->GetRequest().tags_size()); 45 | ASSERT_EQ(request.tags(0), event->GetRequest().tags(0)); 46 | ASSERT_TRUE(event->GetRequest().has_request_token()); 47 | ASSERT_EQ(request.request_token(), event->GetRequest().request_token()); 48 | } 49 | 50 | } // namespace testing 51 | } // namespace sdk 52 | } // namespace content_analysis 53 | 54 | -------------------------------------------------------------------------------- /prepare_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 The Chromium Authors. 3 | # Use of this source code is governed by a BSD-style license that can be 4 | # found in the LICENSE file. 5 | 6 | # This script is meant to be run once to setup the example demo agent. 7 | # Run it with one command line argument: the path to a directory where the 8 | # demo agent will be built. This should be a directory outside the SDK 9 | # directory tree. By default, if no directory is supplied, a directory 10 | # named `build` in the project root will be used. 11 | # 12 | # Once the build is prepared, the demo binary is built using the command 13 | # `cmake --build `, where is the same argument given 14 | # to this script. 15 | 16 | set -euo pipefail 17 | 18 | export ROOT_DIR=$(realpath $(dirname $0)) 19 | export DEMO_DIR=$(realpath $ROOT_DIR/demo) 20 | export PROTO_DIR=$(realpath $ROOT_DIR/proto) 21 | # Defaults to $ROOT_DIR/build if no argument is provided. 22 | export BUILD_DIR=$(realpath ${1:-$ROOT_DIR/build}) 23 | 24 | echo Root dir: $ROOT_DIR 25 | echo Build dir: $BUILD_DIR 26 | echo Demo dir: $DEMO_DIR 27 | echo Proto dir: $PROTO_DIR 28 | 29 | # Prepare build directory 30 | mkdir -p $BUILD_DIR 31 | # Prepare protobuf out directory 32 | mkdir -p $BUILD_DIR/gen 33 | # Enter build directory 34 | cd $BUILD_DIR 35 | 36 | # Install vcpkg and use it to install Google Protocol Buffers. 37 | test -d vcpkg || ( 38 | git clone https://github.com/microsoft/vcpkg 39 | ./vcpkg/bootstrap-vcpkg.sh -disableMetrics 40 | ) 41 | # Install any packages we want from vcpkg. 42 | ./vcpkg/vcpkg install protobuf 43 | ./vcpkg/vcpkg install gtest 44 | 45 | # Generate the build files. 46 | export CMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake 47 | cmake $ROOT_DIR 48 | 49 | -------------------------------------------------------------------------------- /agent/src/event_posix_unittest.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "agent/src/event_posix.h" 8 | #include "content_analysis/sdk/analysis_agent.h" 9 | #include "gtest/gtest.h" 10 | 11 | namespace content_analysis { 12 | namespace sdk { 13 | namespace testing { 14 | 15 | std::unique_ptr CreateEvent( 16 | const BrowserInfo& browser_info, 17 | ContentAnalysisRequest request) { 18 | return std::make_unique( 19 | browser_info, std::move(request)); 20 | } 21 | 22 | TEST(EventTest, Create_BrowserInfo) { 23 | const BrowserInfo bi{12345, "/path/to/binary"}; 24 | ContentAnalysisRequest request; 25 | *request.add_tags() = "foo"; 26 | request.set_request_token("req-token"); 27 | 28 | auto event = CreateEvent(bi, request); 29 | ASSERT_TRUE(event); 30 | 31 | ASSERT_EQ(bi.pid, event->GetBrowserInfo().pid); 32 | ASSERT_EQ(bi.binary_path, event->GetBrowserInfo().binary_path); 33 | } 34 | 35 | TEST(EventTest, Create_Request) { 36 | const BrowserInfo bi{ 12345, "/path/to/binary" }; 37 | ContentAnalysisRequest request; 38 | *request.add_tags() = "foo"; 39 | request.set_request_token("req-token"); 40 | 41 | auto event = CreateEvent(bi, request); 42 | ASSERT_TRUE(event); 43 | 44 | ASSERT_EQ(1u, event->GetRequest().tags_size()); 45 | ASSERT_EQ(request.tags(0), event->GetRequest().tags(0)); 46 | ASSERT_TRUE(event->GetRequest().has_request_token()); 47 | ASSERT_EQ(request.request_token(), event->GetRequest().request_token()); 48 | } 49 | 50 | } // namespace testing 51 | } // namespace sdk 52 | } // namespace content_analysis 53 | 54 | -------------------------------------------------------------------------------- /agent/src/event_base.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "event_base.h" 6 | 7 | namespace content_analysis { 8 | namespace sdk { 9 | 10 | ContentAnalysisEventBase::ContentAnalysisEventBase( 11 | const BrowserInfo& browser_info) 12 | : browser_info_(browser_info) {} 13 | 14 | ResultCode ContentAnalysisEventBase::Close() { 15 | return ResultCode::OK; 16 | } 17 | 18 | ResultCode UpdateResponse(ContentAnalysisResponse& response, 19 | const std::string& tag, 20 | ContentAnalysisResponse::Result::Status status) { 21 | auto result = response.results_size() > 0 22 | ? response.mutable_results(0) 23 | : response.add_results(); 24 | 25 | if (!tag.empty()) { 26 | result->set_tag(tag); 27 | } 28 | 29 | if (status != ContentAnalysisResponse::Result::STATUS_UNKNOWN) { 30 | result->set_status(status); 31 | } 32 | 33 | return ResultCode::OK; 34 | } 35 | 36 | ResultCode SetEventVerdictTo( 37 | ContentAnalysisEvent* event, 38 | ContentAnalysisResponse::Result::TriggeredRule::Action action) { 39 | // This function expects that the event's result has already been 40 | // initialized by a call to UpdateResponse(). 41 | 42 | if (event->GetResponse().results_size() == 0) { 43 | return ResultCode::ERR_MISSING_RESULT; 44 | } 45 | 46 | auto result = event->GetResponse().mutable_results(0); 47 | 48 | // Content analysis responses generated with this SDK contain at most one 49 | // triggered rule. 50 | auto rule = result->triggered_rules_size() > 0 51 | ? result->mutable_triggered_rules(0) 52 | : result->add_triggered_rules(); 53 | 54 | rule->set_action(action); 55 | 56 | return ResultCode::OK; 57 | } 58 | 59 | ResultCode SetEventVerdictToBlock(ContentAnalysisEvent* event) { 60 | return SetEventVerdictTo(event, 61 | ContentAnalysisResponse::Result::TriggeredRule::BLOCK); 62 | } 63 | 64 | } // namespace sdk 65 | } // namespace content_analysis 66 | -------------------------------------------------------------------------------- /agent/include/content_analysis/sdk/result_codes.inc: -------------------------------------------------------------------------------- 1 | // This file is #included from C++ headers and source code to generate code 2 | // specific to each ResultCode. The including code is expected to #define 3 | // macros for RC_RECOVERABLE and RC_UNRECOVERABLE before #including this file 4 | // and then #undef then after use. 5 | 6 | RC_RECOVERABLE(OK, "Operation completed successfully.") 7 | RC_RECOVERABLE(ERR_MISSING_RESULT, "Response is missing a result message.") 8 | RC_RECOVERABLE(ERR_RESPONSE_ALREADY_SENT, "A resonse has already been sent for this request.") 9 | RC_RECOVERABLE(ERR_MISSING_REQUEST_TOKEN, "The request is missing a request token.") 10 | RC_RECOVERABLE(ERR_AGENT_NOT_INITIALIZED, "The agent is not proplerly initialized to handle events.") 11 | RC_RECOVERABLE(ERR_INVALID_REQUEST_FROM_BROWSER, "The browser sent an incorrectly formatted message.") 12 | RC_RECOVERABLE(ERR_IO_PENDING, "IO incomplete, the operation is still pending.") 13 | RC_RECOVERABLE(ERR_MORE_DATA, "There is more data to read before the entire message has been received.") 14 | RC_RECOVERABLE(ERR_CANNOT_GET_BROWSER_PID, "Cannot get process Id of browser.") 15 | RC_RECOVERABLE(ERR_CANNOT_GET_BROWSER_BINARY_PATH, "Cannot get the full path to the brower's main binary file.") 16 | RC_RECOVERABLE(ERR_BROKEN_PIPE, "Browser process has disconnected.") 17 | RC_RECOVERABLE(ERR_UNEXPECTED, "An internal error has occured.") 18 | 19 | // All unrecoverable errors should be declared below ERR_FIRST_UNRECOVERABLE_ERROR. 20 | RC_UNRECOVERABLE(ERR_FIRST_UNRECOVERABLE_ERROR, "Marker for the first unrecoverable error.") 21 | RC_UNRECOVERABLE(ERR_AGENT_ALREADY_EXISTS, "Another process is already running as an agent on this computer.") 22 | RC_UNRECOVERABLE(ERR_AGENT_EVENT_HANDLER_NOT_SPECIFIED, "An agent handler was not specified when creating an agent.") 23 | RC_UNRECOVERABLE(ERR_CANNOT_CREATE_AGENT_STOP_EVENT, "Could not create event to signal the agent to stop.") 24 | RC_UNRECOVERABLE(ERR_INVALID_CHANNEL_NAME, "Invalid channel name specified in Agent::Config.") 25 | RC_UNRECOVERABLE(ERR_CANNOT_CREATE_CHANNEL_IO_EVENT, "Could not create event to perform async IO with a client.") 26 | -------------------------------------------------------------------------------- /demo/request_queue.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_DEMO_REQUST_QUEUE_H_ 6 | #define CONTENT_ANALYSIS_DEMO_REQUST_QUEUE_H_ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "content_analysis/sdk/analysis_agent.h" 14 | 15 | // This class maintains a list of outstanding content analysis requests to 16 | // process. Each request is encapsulated in one ContentAnalysisEvent. 17 | // Requests are handled in FIFO order. 18 | class RequestQueue { 19 | public: 20 | using Event = content_analysis::sdk::ContentAnalysisEvent; 21 | 22 | RequestQueue() = default; 23 | virtual ~RequestQueue() = default; 24 | 25 | // Push a new content analysis event into the queue. 26 | void push(std::unique_ptr event) { 27 | std::lock_guard lock(mutex_); 28 | 29 | events_.push(std::move(event)); 30 | 31 | // Wake before leaving to prevent unpredicatable scheduling. 32 | cv_.notify_one(); 33 | } 34 | 35 | // Pop the next request from the queue, blocking if necessary until an event 36 | // is available. Returns a nullptr if the queue will produce no more 37 | // events. 38 | std::unique_ptr pop() { 39 | std::unique_lock lock(mutex_); 40 | 41 | while (!abort_ && events_.size() == 0) 42 | cv_.wait(lock); 43 | 44 | std::unique_ptr event; 45 | if (!abort_) { 46 | event = std::move(events_.front()); 47 | events_.pop(); 48 | } 49 | 50 | return event; 51 | } 52 | 53 | // Marks the queue as aborted. pop() will now return nullptr. 54 | void abort() { 55 | std::lock_guard lg(mutex_); 56 | 57 | abort_ = true; 58 | 59 | // Wake before leaving to prevent unpredicatable scheduling. 60 | cv_.notify_all(); 61 | } 62 | 63 | private: 64 | std::queue> events_; 65 | std::mutex mutex_; 66 | std::condition_variable cv_; 67 | bool abort_ = false; 68 | }; 69 | 70 | #endif // CONTENT_ANALYSIS_DEMO_REQUST_QUEUE_H_ 71 | -------------------------------------------------------------------------------- /prepare_build.bat: -------------------------------------------------------------------------------- 1 | REM Copyright 2022 The Chromium Authors. 2 | REM Use of this source code is governed by a BSD-style license that can be 3 | REM found in the LICENSE file. 4 | @echo off 5 | setlocal 6 | 7 | REM This script is meant to be run once to setup the example demo agent. 8 | REM Run it with one command line argument: the path to a directory where the 9 | REM demo agent will be built. This should be a directory outside the SDK 10 | REM directory tree. By default, if no directory is supplied, a directory 11 | REM named `build` in the project root will be used. 12 | REM 13 | REM Once the build is prepared, the demo binary is built using the command 14 | REM `cmake --build `, where is the same argument given 15 | REM to this script. 16 | 17 | set ROOT_DIR=%~dp0 18 | call :ABSPATH "%ROOT_DIR%\demo" DEMO_DIR 19 | call :ABSPATH "%ROOT_DIR%\proto" PROTO_DIR 20 | 21 | REM BUILD_DIR defaults to $ROOT_DIR/build if no argument is provided. 22 | IF "%1" == "" ( 23 | call :ABSPATH "%ROOT_DIR%\build" BUILD_DIR 24 | ) ELSE ( 25 | set BUILD_DIR=%~f1 26 | ) 27 | 28 | echo . 29 | echo Root dir: %ROOT_DIR% 30 | echo Build dir: %BUILD_DIR% 31 | echo Demo dir: %DEMO_DIR% 32 | echo Proto dir: %PROTO_DIR% 33 | echo . 34 | 35 | REM Prepare build directory 36 | mkdir "%BUILD_DIR%" 37 | REM Prepare protobuf out directory 38 | mkdir "%BUILD_DIR%\gen" 39 | REM Enter build directory 40 | cd /d "%BUILD_DIR%" 41 | 42 | REM Install vcpkg and use it to install Google Protocol Buffers. 43 | IF NOT exist .\vcpkg\ ( 44 | cmd/c git clone https://github.com/microsoft/vcpkg 45 | cmd/c .\vcpkg\bootstrap-vcpkg.bat -disableMetrics 46 | ) ELSE ( 47 | echo vcpkg is already installed. 48 | ) 49 | REM Install any packages we want from vcpkg. 50 | cmd/c .\vcpkg\vcpkg install protobuf:x64-windows 51 | cmd/c .\vcpkg\vcpkg install gtest:x64-windows 52 | 53 | REM Generate the build files. 54 | set CMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake 55 | cmake %ROOT_DIR% 56 | 57 | echo. 58 | echo. 59 | echo To build, type: cmake --build "%BUILD_DIR%" 60 | echo. 61 | 62 | exit /b 63 | 64 | REM Resolve relative path in %1 and set it into variable %2. 65 | :ABSPATH 66 | set %2=%~f1 67 | exit /b 68 | 69 | -------------------------------------------------------------------------------- /agent/src/scoped_print_handle_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "scoped_print_handle_win.h" 6 | 7 | namespace content_analysis { 8 | namespace sdk { 9 | 10 | std::unique_ptr 11 | CreateScopedPrintHandle(const ContentAnalysisRequest& request, 12 | int64_t browser_pid) { 13 | if (!request.has_print_data() || !request.print_data().has_handle()) { 14 | return nullptr; 15 | } 16 | 17 | // The handle in the request must be duped to be read by the agent 18 | // process. If that doesn't work for any reason, return null. 19 | // See https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle 20 | // for details. 21 | HANDLE browser_process = OpenProcess( 22 | /*dwDesiredAccess=*/PROCESS_DUP_HANDLE, 23 | /*bInheritHandle=*/false, 24 | /*dwProcessId=*/browser_pid); 25 | if (!browser_process) 26 | return nullptr; 27 | 28 | HANDLE dupe = nullptr; 29 | DuplicateHandle( 30 | /*hSourceProcessHandle=*/browser_process, 31 | /*hSourceHandle=*/reinterpret_cast(request.print_data().handle()), 32 | /*hTargetProcessHandle=*/GetCurrentProcess(), 33 | /*lpTargetHandle=*/&dupe, 34 | /*dwDesiredAccess=*/PROCESS_DUP_HANDLE | FILE_MAP_READ, 35 | /*bInheritHandle=*/false, 36 | /*dwOptions=*/0); 37 | 38 | CloseHandle(browser_process); 39 | 40 | if (!dupe) 41 | return nullptr; 42 | 43 | ContentAnalysisRequest::PrintData dupe_print_data; 44 | dupe_print_data.set_handle(reinterpret_cast(dupe)); 45 | dupe_print_data.set_size(request.print_data().size()); 46 | 47 | 48 | return std::make_unique(dupe_print_data); 49 | } 50 | 51 | ScopedPrintHandleWin::ScopedPrintHandleWin( 52 | const ContentAnalysisRequest::PrintData& print_data) 53 | : ScopedPrintHandleBase(print_data), 54 | handle_(reinterpret_cast(print_data.handle())) { 55 | mapped_ = MapViewOfFile(handle_, FILE_MAP_READ, 0, 0, 0); 56 | } 57 | 58 | ScopedPrintHandleWin::~ScopedPrintHandleWin() { 59 | CloseHandle(handle_); 60 | } 61 | 62 | const char* ScopedPrintHandleWin::data() { 63 | return reinterpret_cast(mapped_); 64 | } 65 | 66 | } // namespace sdk 67 | } // namespace content_analysis 68 | -------------------------------------------------------------------------------- /common/utils_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Utility and helper functions common to both the agent and browser code. 6 | // This code is not publicly exposed from the SDK. 7 | 8 | #ifndef CONTENT_ANALYSIS_COMMON_UTILS_WIN_H_ 9 | #define CONTENT_ANALYSIS_COMMON_UTILS_WIN_H_ 10 | 11 | #include 12 | 13 | namespace content_analysis { 14 | namespace sdk { 15 | namespace internal { 16 | 17 | // The default size of the buffer used to hold messages received from 18 | // Google Chrome. 19 | const DWORD kBufferSize = 4096; 20 | 21 | // Named pipe prefixes used for agent and client side of pipe. 22 | constexpr char kPipePrefixForAgent[] = R"(\\.\\pipe\)"; 23 | constexpr char kPipePrefixForClient[] = R"(\Device\NamedPipe\)"; 24 | 25 | // Returns the user SID of the thread or process that calls thie function. 26 | // Returns an empty string on error. 27 | std::string GetUserSID(); 28 | 29 | // Returns the name of the pipe that should be used to communicate between 30 | // the agent and Google Chrome. If `sid` is non-empty, make the pip name 31 | // specific to that user. 32 | // 33 | // GetPipeNameForAgent() is meant to be used in the agent. The returned 34 | // path can be used with CreatePipe() below. GetPipeNameForClient() is meant 35 | // to be used in the client. The returned path can only be used with 36 | // NtCreateFile() and not CreateFile(). 37 | std::string GetPipeNameForAgent(const std::string& base, bool user_specific); 38 | std::string GetPipeNameForClient(const std::string& base, bool user_specific); 39 | 40 | // Creates a named pipe with the give name. If `is_first_pipe` is true, 41 | // fail if this is not the first pipe using this name. 42 | // 43 | // This function create a pipe whose DACL allow full control to the creator 44 | // owner and administrators. If `user_specific` the DACL only allows the 45 | // logged on user to read from and write to the pipe. Otherwise anyone logged 46 | // in can read from and write to the pipe. 47 | // 48 | // A handle to the pipe is retuned in `handle`. 49 | DWORD CreatePipe(const std::string& name, 50 | bool user_specific, 51 | bool is_first_pipe, 52 | HANDLE* handle); 53 | 54 | // Returns the full path to the main binary file of the process with the given 55 | // process ID. 56 | bool GetProcessPath(unsigned long pid, std::string* binary_path); 57 | 58 | // A class that scopes the creation and destruction of an OVERLAPPED structure 59 | // used for async IO. 60 | class ScopedOverlapped { 61 | public: 62 | ScopedOverlapped(); 63 | ~ScopedOverlapped(); 64 | 65 | bool is_valid() { return overlapped_.hEvent != nullptr; } 66 | operator OVERLAPPED*() { return &overlapped_; } 67 | 68 | private: 69 | OVERLAPPED overlapped_; 70 | }; 71 | 72 | } // internal 73 | } // namespace sdk 74 | } // namespace content_analysis 75 | 76 | #endif // CONTENT_ANALYSIS_COMMON_UTILS_WIN_H_ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Chrome Content Analysis Connector Agent SDK 2 | 3 | The Google Chrome Content Analysis Connector provides an official mechanism 4 | allowing Data Loss Prevention (DLP) agents to more deeply integrate their 5 | services with Google Chrome. 6 | 7 | DLP agents are background processes on managed computers that allow enterprises 8 | to monitor locally running applications for data exfiltration events. They can 9 | allow/block these activities based on customer defined DLP policies. 10 | 11 | This repository contains the SDK that DLP agents may use to become service 12 | providers for the Google Chrome Content Analysis Connector. The code that must 13 | be compiled and linked into the content analysis agent is located in the `agent` 14 | subdirectory. 15 | 16 | A demo implementation of a service provider is located in the `demo` subdirectory. 17 | 18 | The code that must be compiled and linked into Google Chrome is located in 19 | the `browser` subdirectory. 20 | 21 | The Protocol Buffer serialization format is used to serialize messages between the 22 | browser and the agent. The protobuf definitions used can be found in the `proto` 23 | subdirectory. 24 | 25 | ## Google Protocol Buffers 26 | 27 | This SDK depends on Google Protocol Buffers version 3.18 or later. It may be 28 | installed from Google's [download page](https://developers.google.com/protocol-buffers/docs/downloads#release-packages) 29 | for your developement platform. It may also be installed using a package 30 | manager. 31 | 32 | The included prepare_build scripts use the Microsoft [vcpkg](https://github.com/microsoft/vcpkg) 33 | package manager to install protobuf. vcpkg is available on all supported 34 | platforms. 35 | 36 | ## Build 37 | 38 | ### Pre-requisites 39 | 40 | The following must be installed on the computer before building the demo: 41 | 42 | - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) version 2.33 or later. 43 | - [cmake](https://cmake.org/install/) version 3.23 or later. 44 | - A C++ compiler toolchain for your platform. 45 | - On linux, the `realpath` shell tool, available in the `coreutils` package. 46 | In Debian-based distributions use `sudo apt intall coreutils`. 47 | In Red Hat distributions use `sudo yum install coreutils`. 48 | - On Mac, use `brew install cmake coreutils pkg-config googletest` or an equivalent setup 49 | 50 | ### Running prepare_build 51 | 52 | First get things ready by installing required dependencies: 53 | ``` 54 | $SDK_DIR/prepare_build 55 | ``` 56 | where `` is the path to a directory where the demo will be built. 57 | By default, if no argument is provided, a directory named `build` will be 58 | created in the project root. Any output within the `build/` directory will 59 | be ignored by version control. 60 | 61 | `prepare_build` performs the following steps: 62 | 1. Downloads the vcpkg package manager. 63 | 2. Downloads and builds the Google Protocol Buffers library. 64 | 3. Creates build files for your specific platform. 65 | 66 | ### Cmake Targets 67 | 68 | To build the demo run the command `cmake --build `. 69 | 70 | To build the protocol buffer targets run the command `cmake --build --target proto` 71 | -------------------------------------------------------------------------------- /browser/include/content_analysis/sdk/analysis_client.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_BROWSER_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_CLIENT_H_ 6 | #define CONTENT_ANALYSIS_BROWSER_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_CLIENT_H_ 7 | 8 | #include 9 | #include 10 | 11 | #include "content_analysis/sdk/analysis.pb.h" 12 | 13 | // This is the main include file for code using Content Analysis Connector 14 | // Client SDK. No other include is needed. 15 | // 16 | // A browser begins by creating an instance of Client using the factory 17 | // function Client::Create(). This instance should live as long as the browser 18 | // intends to send content analysis requests to the content analysis agent. 19 | 20 | namespace content_analysis { 21 | namespace sdk { 22 | 23 | // Represents information about one instance of a content analysis agent 24 | // process that is connected to the browser. 25 | struct AgentInfo { 26 | unsigned long pid = 0; // Process ID of content analysis agent process. 27 | std::string binary_path; // The full path to the process's main binary. 28 | }; 29 | 30 | // Represents a client that can request content analysis for locally running 31 | // agent. This class holds the client endpoint that the browser connects 32 | // with when content analysis is required. 33 | // 34 | // See the demo directory for an example of how to use this class. 35 | class Client { 36 | public: 37 | // Configuration options where creating an agent. `name` is used to create 38 | // a channel between the agent and Google Chrome. 39 | struct Config { 40 | // Used to create a channel between the agent and Google Chrome. Both must 41 | // use the same name to properly rendezvous with each other. The channel 42 | // is platform specific. 43 | std::string name; 44 | 45 | // Set to true if there is a different agent instance per OS user. Defaults 46 | // to false. 47 | bool user_specific = false; 48 | }; 49 | 50 | // Returns a new client instance and calls Start(). 51 | static std::unique_ptr Create(Config config); 52 | 53 | virtual ~Client() = default; 54 | 55 | // Returns the configuration parameters used to create the client. 56 | virtual const Config& GetConfig() const = 0; 57 | 58 | // Retrives information about the agent that is connected to the browser. 59 | virtual const AgentInfo& GetAgentInfo() const = 0; 60 | 61 | // Sends an analysis request to the agent and waits for a response. 62 | virtual int Send(ContentAnalysisRequest request, 63 | ContentAnalysisResponse* response) = 0; 64 | 65 | // Sends an response acknowledgment back to the agent. 66 | virtual int Acknowledge(const ContentAnalysisAcknowledgement& ack) = 0; 67 | 68 | // Ask the agent to cancel all requests matching the criteria in `cancel`. 69 | // This is a best effort only, the agent may cancel some, all, or no requests 70 | // that match. 71 | virtual int CancelRequests(const ContentAnalysisCancelRequests& cancel) = 0; 72 | 73 | protected: 74 | Client() = default; 75 | Client(const Client& rhs) = delete; 76 | Client(Client&& rhs) = delete; 77 | Client& operator=(const Client& rhs) = delete; 78 | Client& operator=(Client&& rhs) = delete; 79 | }; 80 | 81 | } // namespace sdk 82 | } // namespace content_analysis 83 | 84 | #endif // CONTENT_ANALYSIS_BROWSER_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_CLIENT_H_ 85 | -------------------------------------------------------------------------------- /agent/src/event_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "event_win.h" 10 | 11 | #include "common/utils_win.h" 12 | 13 | #include "agent_utils_win.h" 14 | #include "scoped_print_handle_win.h" 15 | 16 | namespace content_analysis { 17 | namespace sdk { 18 | 19 | // Writes a string to the pipe. Returns ERROR_SUCCESS if successful, else 20 | // returns GetLastError() of the write. This function does not return until 21 | // the entire message has been sent (or an error occurs). 22 | static DWORD WriteMessageToPipe(HANDLE pipe, const std::string& message) { 23 | if (message.empty()) { 24 | return ERROR_SUCCESS; 25 | } 26 | 27 | internal::ScopedOverlapped overlapped; 28 | if (!overlapped.is_valid()) { 29 | return GetLastError(); 30 | } 31 | 32 | DWORD err = ERROR_SUCCESS; 33 | const char* cursor = message.data(); 34 | for (DWORD size = message.length(); size > 0;) { 35 | if (WriteFile(pipe, cursor, size, /*written=*/nullptr, overlapped)) { 36 | err = ERROR_SUCCESS; 37 | break; 38 | } 39 | 40 | // If an I/O is not pending, return the error. 41 | err = GetLastError(); 42 | if (err != ERROR_IO_PENDING) { 43 | break; 44 | } 45 | 46 | DWORD written; 47 | if (!GetOverlappedResult(pipe, overlapped, &written, /*wait=*/TRUE)) { 48 | err = GetLastError(); 49 | break; 50 | } 51 | 52 | cursor += written; 53 | size -= written; 54 | } 55 | 56 | return err; 57 | } 58 | 59 | 60 | ContentAnalysisEventWin::ContentAnalysisEventWin( 61 | HANDLE handle, 62 | const BrowserInfo& browser_info, 63 | ContentAnalysisRequest req) 64 | : ContentAnalysisEventBase(browser_info), 65 | hPipe_(handle) { 66 | *request() = std::move(req); 67 | } 68 | 69 | ContentAnalysisEventWin::~ContentAnalysisEventWin() { 70 | Shutdown(); 71 | } 72 | 73 | ResultCode ContentAnalysisEventWin::Init() { 74 | // TODO(rogerta): do some extra validation of the request? 75 | if (request()->request_token().empty()) { 76 | return ResultCode::ERR_MISSING_REQUEST_TOKEN; 77 | } 78 | 79 | response()->set_request_token(request()->request_token()); 80 | 81 | // Prepare the response so that ALLOW verdicts are the default(). 82 | return UpdateResponse(*response(), 83 | request()->tags_size() > 0 ? request()->tags(0) : std::string(), 84 | ContentAnalysisResponse::Result::SUCCESS); 85 | } 86 | 87 | ResultCode ContentAnalysisEventWin::Close() { 88 | Shutdown(); 89 | return ContentAnalysisEventBase::Close(); 90 | } 91 | 92 | ResultCode ContentAnalysisEventWin::Send() { 93 | if (response_sent_) { 94 | return ResultCode::ERR_RESPONSE_ALREADY_SENT; 95 | } 96 | 97 | response_sent_ = true; 98 | 99 | DWORD err = WriteMessageToPipe(hPipe_, 100 | agent_to_chrome()->SerializeAsString()); 101 | return ErrorToResultCode(err); 102 | } 103 | 104 | std::string ContentAnalysisEventWin::DebugString() const { 105 | std::stringstream state; 106 | state.setf(std::ios::boolalpha); 107 | state << "ContentAnalysisEventWin{handle=" << hPipe_; 108 | state << " pid=" << GetBrowserInfo().pid; 109 | state << " rtoken=" << GetRequest().request_token(); 110 | state << " sent=" << response_sent_; 111 | state << "}" << std::ends; 112 | 113 | return state.str(); 114 | } 115 | 116 | void ContentAnalysisEventWin::Shutdown() { 117 | if (hPipe_ != INVALID_HANDLE_VALUE) { 118 | // If no response has been sent yet, attempt to send it now. Otherwise 119 | // the client may be stuck waiting. After shutdown the agent will not 120 | // have any other chance to respond. 121 | if (!response_sent_) { 122 | Send(); 123 | } 124 | 125 | // This event does not own the pipe, so don't close it. 126 | FlushFileBuffers(hPipe_); 127 | hPipe_ = INVALID_HANDLE_VALUE; 128 | } 129 | } 130 | 131 | } // namespace sdk 132 | } // namespace content_analysis 133 | -------------------------------------------------------------------------------- /agent/src/event_win_unittest.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include "agent/src/event_win.h" 8 | #include "common/utils_win.h" 9 | #include "gtest/gtest.h" 10 | 11 | namespace content_analysis { 12 | namespace sdk { 13 | namespace testing { 14 | 15 | std::unique_ptr CreateEvent( 16 | HANDLE handle, 17 | const BrowserInfo& browser_info, 18 | ContentAnalysisRequest request) { 19 | return std::make_unique( 20 | handle, browser_info, std::move(request)); 21 | } 22 | 23 | TEST(EventTest, Create_BrowserInfo) { 24 | const BrowserInfo bi{12345, "/path/to/binary"}; 25 | ContentAnalysisRequest request; 26 | *request.add_tags() = "foo"; 27 | request.set_request_token("req-token"); 28 | 29 | auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); 30 | ASSERT_TRUE(event); 31 | 32 | ASSERT_EQ(bi.pid, event->GetBrowserInfo().pid); 33 | ASSERT_EQ(bi.binary_path, event->GetBrowserInfo().binary_path); 34 | } 35 | 36 | TEST(EventTest, Create_Request) { 37 | const BrowserInfo bi{ 12345, "/path/to/binary" }; 38 | ContentAnalysisRequest request; 39 | *request.add_tags() = "foo"; 40 | request.set_request_token("req-token"); 41 | 42 | auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); 43 | ASSERT_TRUE(event); 44 | 45 | ASSERT_EQ(1u, event->GetRequest().tags_size()); 46 | ASSERT_EQ(request.tags(0), event->GetRequest().tags(0)); 47 | ASSERT_TRUE(event->GetRequest().has_request_token()); 48 | ASSERT_EQ(request.request_token(), event->GetRequest().request_token()); 49 | } 50 | 51 | TEST(EventTest, Create_Init) { 52 | const BrowserInfo bi{ 12345, "/path/to/binary" }; 53 | ContentAnalysisRequest request; 54 | *request.add_tags() = "foo"; 55 | request.set_request_token("req-token"); 56 | 57 | auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); 58 | ASSERT_TRUE(event); 59 | 60 | ASSERT_EQ(ResultCode::OK, event->Init()); 61 | 62 | // Initializing an event should initialize the contained response for a 63 | // success verdict that matches the request. 64 | ASSERT_EQ(request.request_token(), event->GetResponse().request_token()); 65 | ASSERT_EQ(1u, event->GetResponse().results_size()); 66 | ASSERT_EQ(ContentAnalysisResponse::Result::SUCCESS, 67 | event->GetResponse().results(0).status()); 68 | ASSERT_TRUE(event->GetResponse().results(0).has_tag()); 69 | ASSERT_EQ(request.tags(0), event->GetResponse().results(0).tag()); 70 | ASSERT_EQ(0u, event->GetResponse().results(0).triggered_rules_size()); 71 | } 72 | 73 | // Initializing an event whose request has no request token is an error. 74 | TEST(EventTest, Create_Init_RequestNoRequestToken) { 75 | const BrowserInfo bi{ 12345, "/path/to/binary" }; 76 | ContentAnalysisRequest request; 77 | *request.add_tags() = "foo"; 78 | 79 | auto event = CreateEvent(INVALID_HANDLE_VALUE, bi, request); 80 | ASSERT_TRUE(event); 81 | 82 | ASSERT_EQ(ResultCode::ERR_MISSING_REQUEST_TOKEN, event->Init()); 83 | } 84 | 85 | TEST(EventTest, Write_BadPipe) { 86 | HANDLE pipe; 87 | DWORD err = internal::CreatePipe( 88 | internal::GetPipeNameForAgent("testpipe", false), false, true, &pipe); 89 | ASSERT_EQ(ERROR_SUCCESS, err); 90 | ASSERT_NE(INVALID_HANDLE_VALUE, pipe); 91 | 92 | // Create an event with the dummy pipe and initilalize it. 93 | const BrowserInfo bi{ 12345, "/path/to/binary" }; 94 | ContentAnalysisRequest request; 95 | request.set_request_token("req-token"); 96 | *request.add_tags() = "dlp"; 97 | request.set_text_content("test"); 98 | auto event = std::make_unique( 99 | pipe, bi, std::move(request)); 100 | ASSERT_TRUE(event); 101 | ResultCode rc = event->Init(); 102 | ASSERT_EQ(ResultCode::OK, rc); 103 | 104 | // Close the handle before trying to send the response. 105 | // This simulates an error with the pipe. 106 | CloseHandle(pipe); 107 | ASSERT_EQ(ERROR_SUCCESS, GetLastError()); 108 | 109 | // The following call should not hang. 110 | event->Send(); 111 | } 112 | 113 | } // namespace testing 114 | } // namespace sdk 115 | } // namespace content_analysis 116 | 117 | -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *community@chromium.org*, the 73 | Project Steward(s) for *content_analysis_sdk*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | 95 | -------------------------------------------------------------------------------- /demo/agent.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "content_analysis/sdk/analysis_agent.h" 10 | #include "demo/handler.h" 11 | 12 | // Different paths are used depending on whether this agent should run as a 13 | // use specific agent or not. These values are chosen to match the test 14 | // values in chrome browser. 15 | constexpr char kPathUser[] = "path_user"; 16 | constexpr char kPathSystem[] = "brcm_chrm_cas"; 17 | 18 | // Global app config. 19 | std::string path = kPathSystem; 20 | bool use_queue = false; 21 | bool user_specific = false; 22 | unsigned long delay = 0; // In seconds. 23 | unsigned long num_threads = 8u; 24 | std::string save_print_data_path = ""; 25 | 26 | // Command line parameters. 27 | constexpr const char* kArgDelaySpecific = "--delay="; 28 | constexpr const char* kArgPath = "--path="; 29 | constexpr const char* kArgQueued = "--queued"; 30 | constexpr const char* kArgThreads = "--threads="; 31 | constexpr const char* kArgUserSpecific = "--user"; 32 | constexpr const char* kArgHelp = "--help"; 33 | constexpr const char* kArgSavePrintRequestDataTo = "--save-print-request-data-to="; 34 | 35 | bool ParseCommandLine(int argc, char* argv[]) { 36 | for (int i = 1; i < argc; ++i) { 37 | const std::string arg = argv[i]; 38 | if (arg.find(kArgUserSpecific) == 0) { 39 | // If kArgPath was already used, abort. 40 | if (path != kPathSystem) { 41 | std::cout << std::endl << "ERROR: use --path= after --user"; 42 | return false; 43 | } 44 | path = kPathUser; 45 | user_specific = true; 46 | } else if (arg.find(kArgDelaySpecific) == 0) { 47 | delay = std::stoul(arg.substr(strlen(kArgDelaySpecific))); 48 | if (delay > 30) { 49 | delay = 30; 50 | } 51 | } else if (arg.find(kArgPath) == 0) { 52 | path = arg.substr(strlen(kArgPath)); 53 | } else if (arg.find(kArgQueued) == 0) { 54 | use_queue = true; 55 | } else if (arg.find(kArgThreads) == 0) { 56 | num_threads = std::stoul(arg.substr(strlen(kArgThreads))); 57 | } else if (arg.find(kArgHelp) == 0) { 58 | return false; 59 | } else if (arg.find(kArgSavePrintRequestDataTo) == 0) { 60 | int arg_len = strlen(kArgSavePrintRequestDataTo); 61 | save_print_data_path = arg.substr(arg_len); 62 | } 63 | } 64 | 65 | return true; 66 | } 67 | 68 | void PrintHelp() { 69 | std::cout 70 | << std::endl << std::endl 71 | << "Usage: agent [OPTIONS]" << std::endl 72 | << "A simple agent to process content analysis requests." << std::endl 73 | << "Data containing the string 'block' blocks the request data from being used." << std::endl 74 | << std::endl << "Options:" << std::endl 75 | << kArgDelaySpecific << " : Add a delay to request processing in seconds (max 30)." << std::endl 76 | << kArgPath << " : Used the specified path instead of default. Must come after --user." << std::endl 77 | << kArgQueued << " : Queue requests for processing in a background thread" << std::endl 78 | << kArgThreads << " : When queued, number of threads in the request processing thread pool" << std::endl 79 | << kArgUserSpecific << " : Make agent OS user specific." << std::endl 80 | << kArgHelp << " : prints this help message" << std::endl 81 | << kArgSavePrintRequestDataTo << " : saves the PDF data to the given file path for print requests"; 82 | } 83 | 84 | int main(int argc, char* argv[]) { 85 | if (!ParseCommandLine(argc, argv)) { 86 | PrintHelp(); 87 | return 1; 88 | } 89 | 90 | auto handler = use_queue 91 | ? std::make_unique(num_threads, delay, save_print_data_path) 92 | : std::make_unique(delay, save_print_data_path); 93 | 94 | // Each agent uses a unique name to identify itself with Google Chrome. 95 | content_analysis::sdk::ResultCode rc; 96 | auto agent = content_analysis::sdk::Agent::Create( 97 | {path, user_specific}, std::move(handler), &rc); 98 | if (!agent || rc != content_analysis::sdk::ResultCode::OK) { 99 | std::cout << "[Demo] Error starting agent: " 100 | << content_analysis::sdk::ResultCodeToString(rc) 101 | << std::endl; 102 | return 1; 103 | }; 104 | 105 | std::cout << "[Demo] " << agent->DebugString() << std::endl; 106 | 107 | // Blocks, sending events to the handler until agent->Stop() is called. 108 | rc = agent->HandleEvents(); 109 | if (rc != content_analysis::sdk::ResultCode::OK) { 110 | std::cout << "[Demo] Error from handling events: " 111 | << content_analysis::sdk::ResultCodeToString(rc) 112 | << std::endl; 113 | std::cout << "[Demo] " << agent->DebugString() << std::endl; 114 | } 115 | 116 | return 0; 117 | } 118 | -------------------------------------------------------------------------------- /common/utils_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "utils_win.h" 11 | 12 | namespace content_analysis { 13 | namespace sdk { 14 | namespace internal { 15 | 16 | std::string GetUserSID() { 17 | std::string sid; 18 | 19 | HANDLE handle; 20 | if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &handle) && 21 | !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &handle)) { 22 | return std::string(); 23 | } 24 | 25 | DWORD length = 0; 26 | std::vector buffer; 27 | if (!GetTokenInformation(handle, TokenUser, nullptr, 0, &length)) { 28 | DWORD err = GetLastError(); 29 | if (err == ERROR_INSUFFICIENT_BUFFER) { 30 | buffer.resize(length); 31 | } 32 | } 33 | if (GetTokenInformation(handle, TokenUser, buffer.data(), buffer.size(), 34 | &length)) { 35 | TOKEN_USER* info = reinterpret_cast(buffer.data()); 36 | char* sid_string; 37 | if (ConvertSidToStringSidA(info->User.Sid, &sid_string)) { 38 | sid = sid_string; 39 | LocalFree(sid_string); 40 | } 41 | } 42 | 43 | CloseHandle(handle); 44 | return sid; 45 | } 46 | 47 | std::string BuildPipeName(const char* prefix, 48 | const std::string& base, 49 | bool user_specific) { 50 | std::string pipename = prefix; 51 | 52 | // If the agent is not user-specific, the assumption is that it runs with 53 | // administrator privileges. Create the pipe in a location only available 54 | // to administrators. 55 | if (!user_specific) 56 | pipename += "ProtectedPrefix\\Administrators\\"; 57 | 58 | pipename += base; 59 | 60 | if (user_specific) { 61 | std::string sid = GetUserSID(); 62 | if (sid.empty()) 63 | return std::string(); 64 | 65 | pipename += "." + sid; 66 | } 67 | 68 | return pipename; 69 | } 70 | 71 | std::string GetPipeNameForAgent(const std::string& base, bool user_specific) { 72 | return BuildPipeName(kPipePrefixForAgent, base, user_specific); 73 | } 74 | 75 | std::string GetPipeNameForClient(const std::string& base, bool user_specific) { 76 | return BuildPipeName(kPipePrefixForClient, base, user_specific); 77 | } 78 | 79 | DWORD CreatePipe( 80 | const std::string& name, 81 | bool user_specific, 82 | bool is_first_pipe, 83 | HANDLE* handle) { 84 | DWORD err = ERROR_SUCCESS; 85 | DWORD mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED; 86 | 87 | // Create DACL for pipe to allow users on the local system to read and write 88 | // to the pipe. The creator and owner as well as administrator always get 89 | // full control of the pipe. 90 | // 91 | // If `user_specific` is true, a different agent instance is used for each 92 | // OS user, so only allow the interactive logged on user to reads and write 93 | // messages to the pipe. Otherwise only one agent instance used used for all 94 | // OS users and all authenticated logged on users can reads and write 95 | // messages. 96 | // 97 | // See https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-definition-language 98 | // for a description of this string format. 99 | constexpr char kDaclEveryone[] = "D:" 100 | "(A;OICI;GA;;;CO)" // Allow full control to creator owner. 101 | "(A;OICI;GA;;;BA)" // Allow full control to admins. 102 | "(A;OICI;GRGW;;;WD)"; // Allow read and write to everyone. 103 | constexpr char kDaclUserSpecific[] = "D:" 104 | "(A;OICI;GA;;;CO)" // Allow full control to creator owner. 105 | "(A;OICI;GA;;;BA)" // Allow full control to admins. 106 | "(A;OICI;GRGW;;;IU)"; // Allow read and write to interactive user. 107 | SECURITY_ATTRIBUTES sa; 108 | memset(&sa, 0, sizeof(sa)); 109 | sa.nLength = sizeof(sa); 110 | sa.bInheritHandle = FALSE; 111 | if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( 112 | user_specific ? kDaclUserSpecific : kDaclEveryone, SDDL_REVISION_1, 113 | &sa.lpSecurityDescriptor, /*outSdSize=*/nullptr)) { 114 | err = GetLastError(); 115 | return err; 116 | } 117 | 118 | // When `is_first_pipe` is true, the agent expects there is no process that 119 | // is currently listening on this pipe. If there is, CreateNamedPipeA() 120 | // returns with ERROR_ACCESS_DENIED. This is used to detect if another 121 | // process is listening for connections when there shouldn't be. 122 | if (is_first_pipe) { 123 | mode |= FILE_FLAG_FIRST_PIPE_INSTANCE; 124 | } 125 | *handle = CreateNamedPipeA(name.c_str(), mode, 126 | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | 127 | PIPE_REJECT_REMOTE_CLIENTS, PIPE_UNLIMITED_INSTANCES, kBufferSize, 128 | kBufferSize, 0, &sa); 129 | if (*handle == INVALID_HANDLE_VALUE) { 130 | err = GetLastError(); 131 | } 132 | 133 | // Free the security descriptor as it is no longer needed once 134 | // CreateNamedPipeA() returns. 135 | LocalFree(sa.lpSecurityDescriptor); 136 | 137 | return err; 138 | } 139 | 140 | bool GetProcessPath(unsigned long pid, std::string* binary_path) { 141 | HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); 142 | if (hProc == nullptr) { 143 | return false; 144 | } 145 | 146 | char path[MAX_PATH]; 147 | DWORD size = sizeof(path); 148 | DWORD length = QueryFullProcessImageNameA(hProc, /*flags=*/0, path, &size); 149 | CloseHandle(hProc); 150 | if (length == 0) { 151 | return false; 152 | } 153 | 154 | *binary_path = path; 155 | return true; 156 | } 157 | 158 | ScopedOverlapped::ScopedOverlapped() { 159 | memset(&overlapped_, 0, sizeof(overlapped_)); 160 | overlapped_.hEvent = CreateEvent(/*securityAttr=*/nullptr, 161 | /*manualReset=*/TRUE, 162 | /*initialState=*/FALSE, 163 | /*name=*/nullptr); 164 | } 165 | 166 | ScopedOverlapped::~ScopedOverlapped() { 167 | if (overlapped_.hEvent != nullptr) { 168 | CloseHandle(overlapped_.hEvent); 169 | } 170 | } 171 | 172 | } // internal 173 | } // namespace sdk 174 | } // namespace content_analysis 175 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Chromium Authors. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | cmake_minimum_required(VERSION 3.22) 5 | 6 | project(chrome_enterprise_connector_local_analysis) 7 | 8 | # Ensure a C++14 compiler is used. 9 | set(CMAKE_CXX_STANDARD 14) 10 | 11 | # Determine the operating system being targeted. 12 | if(CMAKE_SYSTEM_NAME STREQUAL "Windows") 13 | set(WIN TRUE) 14 | set(MAC FALSE) 15 | set(LINUX FALSE) 16 | elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") 17 | set(WIN FALSE) 18 | set(MAC TRUE) 19 | set(LINUX FALSE) 20 | else() 21 | set(WIN FALSE) 22 | set(MAC FALSE) 23 | set(LINUX TRUE) 24 | endif() 25 | 26 | # Set the path to the protoc protobuf compiler. 27 | if(WIN) 28 | set(PROTOC ${PROJECT_BINARY_DIR}/vcpkg/installed/x64-windows/tools/protobuf/protoc.exe) 29 | elseif(MAC) 30 | set(PROTOC ${PROJECT_BINARY_DIR}/vcpkg/installed/x64-osx/tools/protobuf/protoc) 31 | elseif(LINUX) 32 | set(PROTOC ${PROJECT_BINARY_DIR}/vcpkg/installed/x64-linux/tools/protobuf/protoc) 33 | endif() 34 | 35 | # Calls the protoc compiler using the arguments specific to this project. 36 | # protobuf_generate_cpp is not flexible enough for our needs. 37 | add_custom_command( 38 | OUTPUT ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc 39 | COMMAND 40 | ${PROTOC} 41 | --cpp_out=${PROJECT_BINARY_DIR}/gen 42 | --proto_path=${PROJECT_SOURCE_DIR}/proto 43 | ${PROJECT_SOURCE_DIR}/proto/content_analysis/sdk/analysis.proto 44 | DEPENDS ./proto/content_analysis/sdk/analysis.proto 45 | WORKING_DIRECTORY ${PROJECT_BINARY_DIR} 46 | ) 47 | # Define proto target. Compile this target exclusively by calling: 48 | # `cmake --build --target proto` 49 | add_custom_target(proto 50 | ALL 51 | DEPENDS 52 | ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc 53 | ) 54 | 55 | # The include directory contains the header files needed by the demo code. 56 | # The gen directory contains generated protobuf headers describing the request 57 | # and response objects used to communicate with Google Chrome. 58 | set(AGENT_INCLUDES 59 | ./agent/include 60 | . 61 | ${PROJECT_BINARY_DIR}/gen 62 | ) 63 | set(BROWSER_INCLUDES 64 | ./browser/include 65 | . 66 | ${PROJECT_BINARY_DIR}/gen 67 | ) 68 | 69 | # The SDK contains platform specific code for each of the supported platforms. 70 | # ${PLATFORM_AGENT_CODE} holds the list of source files needed for the current 71 | # platform being built. 72 | if(WIN) 73 | set(PLATFORM_AGENT_CODE 74 | ./agent/src/agent_utils_win.cc 75 | ./agent/src/agent_utils_win.h 76 | ./agent/src/agent_win.cc 77 | ./agent/src/agent_win.h 78 | ./agent/src/event_win.cc 79 | ./agent/src/event_win.h 80 | ./agent/src/scoped_print_handle_win.cc 81 | ./agent/src/scoped_print_handle_win.h 82 | ./common/utils_win.cc 83 | ./common/utils_win.h 84 | ) 85 | set(PLATFORM_TEST_CODE 86 | ./agent/src/agent_win_unittest.cc 87 | ./agent/src/event_win_unittest.cc 88 | ) 89 | elseif(MAC) 90 | set(PLATFORM_AGENT_CODE 91 | ./agent/src/agent_mac.cc 92 | ./agent/src/agent_mac.h 93 | ./agent/src/event_mac.cc 94 | ./agent/src/event_mac.h 95 | ./agent/src/scoped_print_handle_mac.cc 96 | ./agent/src/scoped_print_handle_mac.h 97 | ) 98 | set(PLATFORM_TEST_CODE 99 | ./agent/src/event_mac_unittest.cc 100 | ) 101 | elseif(LINUX) 102 | set(PLATFORM_AGENT_CODE 103 | ./agent/src/agent_posix.cc 104 | ./agent/src/agent_posix.h 105 | ./agent/src/event_posix.cc 106 | ./agent/src/event_posix.h 107 | ./agent/src/scoped_print_handle_posix.cc 108 | ./agent/src/scoped_print_handle_posix.h 109 | ) 110 | set(PLATFORM_TEST_CODE 111 | ./agent/src/event_posix_unittest.cc 112 | ) 113 | endif() 114 | 115 | # The SDK contains platform specific code for each of the supported platforms. 116 | # ${PLATFORM_BROWSER_CODE} holds the list of source files needed for the current 117 | # platform being built. 118 | if(WIN) 119 | set(PLATFORM_BROWSER_CODE 120 | ./browser/src/client_win.cc 121 | ./browser/src/client_win.h 122 | ./common/utils_win.cc 123 | ./common/utils_win.h 124 | ) 125 | elseif(MAC) 126 | set(PLATFORM_BROWSER_CODE 127 | ./browser/src/client_mac.cc 128 | ./browser/src/client_mac.h 129 | ) 130 | elseif(LINUX) 131 | set(PLATFORM_BROWSER_CODE 132 | ./browser/src/client_posix.cc 133 | ./browser/src/client_posix.h 134 | ) 135 | endif() 136 | 137 | # Makes available the package definitions in vcpkg. 138 | include("${PROJECT_BINARY_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake") 139 | find_package(Protobuf CONFIG REQUIRED) 140 | # Unit tests 141 | enable_testing() 142 | find_package(GTest CONFIG REQUIRED) 143 | include(GoogleTest) 144 | 145 | add_executable(unit_tests 146 | ${PLATFORM_TEST_CODE} 147 | ) 148 | set_property(TARGET unit_tests PROPERTY CXX_STANDARD 20) 149 | target_include_directories(unit_tests 150 | PRIVATE 151 | ${AGENT_INCLUDES} 152 | ${BROWSER_INCLUDES} 153 | ) 154 | target_link_libraries(unit_tests 155 | PUBLIC 156 | cac_agent 157 | cac_browser 158 | GTest::gtest GTest::gtest_main 159 | ) 160 | 161 | gtest_discover_tests(unit_tests) 162 | 163 | # Builds the content analysis connector agent linker library. This library 164 | # is linked into the agent in order to listen for and process content analysis 165 | # requests from Google Chrome. 166 | add_library(cac_agent 167 | ./agent/include/content_analysis/sdk/analysis_agent.h 168 | ./agent/include/content_analysis/sdk/result_codes.h 169 | ./agent/src/agent_base.cc 170 | ./agent/src/agent_base.h 171 | ./agent/src/event_base.cc 172 | ./agent/src/event_base.h 173 | ./agent/src/scoped_print_handle_base.cc 174 | ./agent/src/scoped_print_handle_base.h 175 | ${PLATFORM_AGENT_CODE} 176 | ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc 177 | ) 178 | target_link_libraries(cac_agent 179 | PUBLIC 180 | protobuf::libprotoc 181 | protobuf::libprotobuf 182 | protobuf::libprotobuf-lite) 183 | target_include_directories(cac_agent PRIVATE ${AGENT_INCLUDES}) 184 | # Builds the content analysis connector browser linker library. This library 185 | # is linked into the client in order to send content analysis requests to the 186 | # agent. 187 | add_library(cac_browser 188 | ./browser/include/content_analysis/sdk/analysis_client.h 189 | ./browser/src/client_base.cc 190 | ./browser/src/client_base.h 191 | ${PLATFORM_BROWSER_CODE} 192 | ${PROJECT_BINARY_DIR}/gen/content_analysis/sdk/analysis.pb.cc 193 | ) 194 | target_include_directories(cac_browser PRIVATE ${BROWSER_INCLUDES}) 195 | target_link_libraries(cac_browser 196 | PUBLIC 197 | protobuf::libprotoc 198 | protobuf::libprotobuf 199 | protobuf::libprotobuf-lite) 200 | 201 | # The demo agent executable. 202 | add_executable(agent 203 | ./demo/agent.cc 204 | ./demo/handler.h 205 | ) 206 | target_include_directories(agent PRIVATE ${AGENT_INCLUDES}) 207 | target_link_libraries(agent PRIVATE cac_agent) 208 | 209 | # The demo client executable. 210 | add_executable(browser ./demo/client.cc) 211 | target_include_directories(browser PRIVATE ${BROWSER_INCLUDES}) 212 | target_link_libraries(browser PRIVATE cac_browser) 213 | 214 | -------------------------------------------------------------------------------- /agent/src/agent_win.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_SRC_AGENT_WIN_H_ 6 | #define CONTENT_ANALYSIS_AGENT_SRC_AGENT_WIN_H_ 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "agent_base.h" 16 | 17 | namespace content_analysis { 18 | namespace sdk { 19 | 20 | // Agent implementaton for Windows. 21 | class AgentWin : public AgentBase { 22 | public: 23 | // Creates a new agent given the specific configuration and handler. 24 | // If an error occurs during creation, `rc` is set to the specific 25 | // error. Otherwise `rc` is ResultCode::OK. 26 | AgentWin(Config config, std::unique_ptr handler, 27 | ResultCode* rc); 28 | ~AgentWin() override; 29 | 30 | // Agent: 31 | ResultCode HandleEvents() override; 32 | ResultCode Stop() override; 33 | std::string DebugString() const override; 34 | 35 | // Handles one pipe event and returns. Used only in tests. 36 | ResultCode HandleOneEventForTesting(); 37 | 38 | // Returns true if there is at least one client connected to this agent. 39 | bool IsAClientConnectedForTesting(); 40 | 41 | private: 42 | // Represents one connection to a Google Chrome browser, or one pipe 43 | // listening for a Google Chrome browser to connect. 44 | class Connection { 45 | public: 46 | // Starts listening on a pipe with the given name. `is_first_pipe` should 47 | // be true only for the first pipe created by the agent. If an error 48 | // occurs while creating the connction object it is returned in `rc`. 49 | // If no errors occur then rc is set to ResultCode::OK. 50 | // 51 | // When `user_specific` is true there is a different agent instance per OS 52 | // user. 53 | // 54 | // `Connection` objects cannot be copied or moved because the OVERLAPPED 55 | // structure cannot be changed or moved in memory while an I/O operation 56 | // is in progress. 57 | Connection(const std::string& pipename, 58 | bool user_specific, 59 | AgentEventHandler* handler, 60 | bool is_first_pipe, 61 | ResultCode* rc); 62 | Connection(const Connection& other) = delete; 63 | Connection(Connection&& other) = delete; 64 | Connection& operator=(const Connection& other) = delete; 65 | Connection& operator=(Connection&& other) = delete; 66 | ~Connection(); 67 | 68 | bool IsValid() const { return handle_ != INVALID_HANDLE_VALUE; } 69 | bool IsConnected() const { return is_connected_; } 70 | HANDLE GetWaitHandle() const { return overlapped_.hEvent; } 71 | 72 | // Resets this connection object to listen for a new Google Chrome browser. 73 | // When `user_specific` is true there is a different agent instance per OS 74 | // user. 75 | ResultCode Reset(const std::string& pipename, bool user_specific); 76 | 77 | // Hnadles an event for this connection. `wait_handle` corresponds to 78 | // this connections wait handle. 79 | ResultCode HandleEvent(HANDLE wait_handle); 80 | 81 | // Append debuf information to the string stream. 82 | void AppendDebugString(std::stringstream& state) const; 83 | 84 | private: 85 | // Listens for a new connection from Google Chrome. 86 | ResultCode ConnectPipe(); 87 | 88 | // Resets this connection object to listen for a new Google Chrome browser. 89 | // When `user_specific` is true there is a different agent instance per OS 90 | // user. 91 | ResultCode ResetInternal(const std::string& pipename, 92 | bool user_specific, 93 | bool is_first_pipe); 94 | 95 | // Cleans up this connection object so that it can be reused with a new 96 | // Google Chroem browser instance. The handles assocated with this object 97 | // are not closed. On return, this object is neither connected nor 98 | // listening and any buffer used to hold browser messages are cleared. 99 | void Cleanup(); 100 | 101 | // Queues a read on the pipe to receive a message from Google Chrome. 102 | // ERROR_SUCCESS, ERROR_IO_PENDING, and ERROR_MORE_DATA are successful 103 | // return values. Other values represent an error with the connection. 104 | // If `reset_cursor` is true the internal read buffer cursor is reset to 105 | // the start of the buffer, otherwise it is unchanged. 106 | ResultCode QueueReadFile(bool reset_cursor); 107 | 108 | // Called when data from Google Chrome is available for reading from the 109 | // pipe. ERROR_SUCCESS and ERROR_MORE_DATA are both successful return 110 | // values. Other values represent an error with the connection. 111 | // 112 | // `done_reading` is true if the code has finished reading an entire message 113 | // from chrome. Regardless of whether reading is done, `count` contains 114 | // the number of bytes read. 115 | // 116 | // If `done_reading` is true, the data received from the browser is parsed 117 | // as if it were a `ChromeToAgent` proto message and the handler is called 118 | // as needed. 119 | // 120 | // If `done_reading` is false, the data received from the browser is 121 | // appended to the data already received from the browser. `buffer_` is 122 | // resized to allow reading more data from the browser. 123 | // 124 | // In all cases the caller is expected to use QueueReadFile() to continue 125 | // reading data from the browser. 126 | ResultCode OnReadFile(BOOL done_reading, DWORD count); 127 | 128 | // Calls the appropriate method the handler depending on the message 129 | // received from Google Chrome. 130 | ResultCode CallHandler(); 131 | 132 | // Fills in the browser_info_ member of this Connection. Assumes 133 | // IsConnected() is true. 134 | ResultCode BuildBrowserInfo(); 135 | 136 | // Notifies the handler of the given error iff `rc` is not equal to 137 | // ResultCode::OK. Appends the Google Chrome browser process id to the 138 | // context before calling the handler. Also append `err` to the context 139 | // if it is not ERROR_SUCCESS. 140 | // 141 | // Returns the error passed into the method. 142 | ResultCode NotifyIfError(const char* context, 143 | ResultCode rc, 144 | DWORD err=ERROR_SUCCESS); 145 | 146 | // The handler to call for various agent events. 147 | AgentEventHandler* handler_ = nullptr; 148 | 149 | // Members used to communicate with Google Chrome. 150 | HANDLE handle_ = INVALID_HANDLE_VALUE; 151 | OVERLAPPED overlapped_; 152 | 153 | // True if this connection is assigned to a specific Google Chrome browser, 154 | // otherwise this connection is listening for a new browser. 155 | bool is_connected_ = false; 156 | 157 | // Information about the Google Chrome browser process. 158 | BrowserInfo browser_info_; 159 | 160 | // Members used to read messages from Google Chrome. 161 | std::vector buffer_; 162 | char* cursor_ = nullptr; 163 | DWORD read_size_ = 0; 164 | DWORD final_size_ = 0; 165 | }; 166 | 167 | // Returns handles that can be used to wait for events from all handles 168 | // managed by this agent. This includes all connection objects and the 169 | // stop event. The stop event is always last in the list. 170 | void GetHandles(std::vector& wait_handles) const; 171 | 172 | // Handles one pipe event and returns. If the return value is 173 | // ResultCode::OK, the `stopped` argument is set to true if the agent 174 | // should stop handling more events. If the return value is not 175 | // ResultCode::OK, `stopped` is undefined. 176 | ResultCode HandleOneEvent(std::vector& wait_handles, bool* stopped); 177 | 178 | // Performs a clean shutdown of the agent. 179 | void Shutdown(); 180 | 181 | // Name used to create the pipes between the agent and Google Chrome browsers. 182 | std::string pipename_; 183 | 184 | // A list of pipes to already connected Google Chrome browsers. 185 | // The first kMinNumListeningPipeInstances pipes in the list correspond to 186 | // listening pipes. 187 | std::vector> connections_; 188 | 189 | // An event that is set when the agent should stop. Set in Stop(). 190 | HANDLE stop_event_ = nullptr; 191 | }; 192 | 193 | } // namespace sdk 194 | } // namespace content_analysis 195 | 196 | #endif // CONTENT_ANALYSIS_AGENT_SRC_AGENT_WIN_H_ 197 | -------------------------------------------------------------------------------- /proto/content_analysis/sdk/analysis.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | syntax = "proto2"; 6 | 7 | option optimize_for = LITE_RUNTIME; 8 | 9 | package content_analysis.sdk; 10 | 11 | // The values in this enum can be extended in future versions of Chrome to 12 | // support new analysis triggers. 13 | enum AnalysisConnector { 14 | ANALYSIS_CONNECTOR_UNSPECIFIED = 0; 15 | FILE_DOWNLOADED = 1; 16 | FILE_ATTACHED = 2; 17 | BULK_DATA_ENTRY = 3; 18 | PRINT = 4; 19 | // This value is not yet implemented in the SDK. It is kept for consistency with the Chromium code. 20 | FILE_TRANSFER = 5; 21 | } 22 | 23 | message ContentMetaData { 24 | // The URL containing the file download/upload or to which web content is 25 | // being uploaded. 26 | optional string url = 1; 27 | 28 | // Name of file on user system (if applicable). 29 | optional string filename = 2; 30 | 31 | // Sha256 digest of file. 32 | optional string digest = 3; 33 | 34 | // Specifically for the download case. 35 | optional ClientDownloadRequest csd = 4; 36 | 37 | // Optional email address of user. This field may be empty if the user 38 | // is not signed in. 39 | optional string email = 5; 40 | 41 | // Name of tab title. 42 | optional string tab_title = 9; 43 | 44 | // Empty for non-print actions. 45 | message PrintMetadata { 46 | optional string printer_name = 1; 47 | 48 | enum PrinterType { 49 | UNKNOWN = 0; 50 | CLOUD = 1; 51 | LOCAL = 2; 52 | } 53 | optional PrinterType printer_type = 2; 54 | } 55 | optional PrintMetadata print_metadata = 11; 56 | 57 | reserved 6 to 8, 10; 58 | } 59 | 60 | message ClientMetadata { 61 | // Describes the browser uploading a scan request. 62 | message Browser { 63 | // This is omitted on scans triggered at the profile level. 64 | optional string machine_user = 4; 65 | 66 | reserved 1 to 3; 67 | }; 68 | optional Browser browser = 1; 69 | 70 | reserved 2 to 3; 71 | }; 72 | 73 | message ClientDownloadRequest { 74 | // Type of the resources stored below. 75 | enum ResourceType { 76 | // The final URL of the download payload. The resource URL should 77 | // correspond to the URL field above. 78 | DOWNLOAD_URL = 0; 79 | // A redirect URL that was fetched before hitting the final DOWNLOAD_URL. 80 | DOWNLOAD_REDIRECT = 1; 81 | // The final top-level URL of the tab that triggered the download. 82 | TAB_URL = 2; 83 | // A redirect URL thas was fetched before hitting the final TAB_URL. 84 | TAB_REDIRECT = 3; 85 | // The document URL for a PPAPI plugin instance that initiated the download. 86 | // This is the document.url for the container element for the plugin 87 | // instance. 88 | PPAPI_DOCUMENT = 4; 89 | // The plugin URL for a PPAPI plugin instance that initiated the download. 90 | PPAPI_PLUGIN = 5; 91 | } 92 | 93 | message Resource { 94 | required string url = 1; 95 | required ResourceType type = 2; 96 | 97 | reserved 3 to 4; 98 | } 99 | 100 | repeated Resource resources = 4; 101 | 102 | reserved 1 to 3, 5 to 84; 103 | } 104 | 105 | 106 | // Analysis request sent from chrome to backend. 107 | // The proto in the Chromium codebase is the source of truth, the version here 108 | // 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). 109 | message ContentAnalysisRequest { 110 | // Token used to correlate requests and responses. This is different than the 111 | // FCM token in that it is unique for each request. 112 | optional string request_token = 5; 113 | 114 | // Which enterprise connector fired this request. 115 | optional AnalysisConnector analysis_connector = 9; 116 | 117 | // Information about the data that triggered the content analysis request. 118 | optional ContentMetaData request_data = 10; 119 | 120 | // The tags configured for the URL that triggered the content analysis. 121 | repeated string tags = 11; 122 | 123 | // Additional information about the browser, device or profile so events can 124 | // be reported with device/user identifiable information. 125 | optional ClientMetadata client_metadata = 12; 126 | 127 | // Data used to transmit print data from the browser. 128 | message PrintData { 129 | // A platform-specific handle that can be used to access the printed document. 130 | optional int64 handle = 1; 131 | 132 | // The size of the data to be printed. 133 | optional int64 size = 2; 134 | } 135 | 136 | oneof content_data { 137 | // The text content to analyze in local content analysis request. 138 | string text_content = 13; 139 | 140 | // The full path to the file to analyze in local content analysis request. 141 | // The path is expressed in a platform dependent way. 142 | string file_path = 14; 143 | 144 | // The to-be-printed page/document in PDF format. 145 | PrintData print_data = 18; 146 | } 147 | 148 | // The absolute deadline (seconds since the UTC Epoch time) that Chrome will 149 | // wait until a response from the agent is received. 150 | optional int64 expires_at = 15; 151 | 152 | // ID for keeping track of analysis requests that belong to the same user 153 | // action. 154 | optional string user_action_id = 16; 155 | 156 | // Count of analysis requests that belong to the same user action. 157 | optional int64 user_action_requests_count = 17; 158 | 159 | // Indicates the exact reason the request was created, ie which user action 160 | // led to a data transfer. 161 | enum Reason { 162 | UNKNOWN = 0; 163 | 164 | // Only possible for the `FILE_ATTACHED` and `BULK_DATA_ENTRY` actions. 165 | CLIPBOARD_PASTE = 1; 166 | DRAG_AND_DROP = 2; 167 | 168 | // Only possible for the `FILE_ATTACHED` action. 169 | FILE_PICKER_DIALOG = 3; 170 | 171 | // Only possible for the `PRINT` analysis connector. 172 | PRINT_PREVIEW_PRINT = 4; 173 | SYSTEM_DIALOG_PRINT = 5; 174 | 175 | // Only possible for the `FILE_DOWNLOADED` analysis connector. 176 | NORMAL_DOWNLOAD = 6; 177 | SAVE_AS_DOWNLOAD = 7; 178 | } 179 | optional Reason reason = 19; 180 | 181 | // Reserved to make sure there is no overlap with DeepScanningClientRequest. 182 | reserved 1 to 4, 6 to 8, 20; 183 | } 184 | 185 | // Verdict response sent from agent to Google Chrome. 186 | message ContentAnalysisResponse { 187 | // Token used to correlate requests and responses. Corresponds to field in 188 | // ContentAnalysisRequest with the same name. 189 | optional string request_token = 1; 190 | 191 | // Represents the analysis result from a given tag. 192 | message Result { 193 | optional string tag = 1; 194 | 195 | // The status of this result. 196 | enum Status { 197 | STATUS_UNKNOWN = 0; 198 | SUCCESS = 1; 199 | FAILURE = 2; 200 | } 201 | optional Status status = 2; 202 | 203 | // Identifies the detection rules that were triggered by the analysis. 204 | // Only relevant when status is SUCCESS. 205 | message TriggeredRule { 206 | enum Action { 207 | ACTION_UNSPECIFIED = 0; 208 | REPORT_ONLY = 1; 209 | WARN = 2; 210 | BLOCK = 3; 211 | } 212 | optional Action action = 1; 213 | optional string rule_name = 2; 214 | optional string rule_id = 3; 215 | reserved 4; 216 | } 217 | repeated TriggeredRule triggered_rules = 3; 218 | 219 | reserved 4 to 7; 220 | } 221 | repeated Result results = 4; 222 | 223 | reserved 2 to 3; 224 | } 225 | 226 | // An Acknowledgement is sent by the browser following the receipt of a response 227 | // from the agent. 228 | message ContentAnalysisAcknowledgement { 229 | // Token used to correlate with the corresponding request and response. 230 | optional string request_token = 1; 231 | 232 | // The action taken by google Chrome with the content analysis response. 233 | enum Status { 234 | // The response was handled as specified by the agent. 235 | SUCCESS = 1; 236 | 237 | // The response from the agent was not properly formatted. 238 | INVALID_RESPONSE = 2; 239 | 240 | // The response from the agent was too late and Google Chrome took the 241 | // default action. 242 | TOO_LATE = 3; 243 | }; 244 | optional Status status = 2; 245 | 246 | // The final action that chrome took with this request. This may be different 247 | // from the action specified in the response if the response was too late or 248 | // if the original request was part of a user action whose overall final 249 | // differed from the action of this particular request. 250 | enum FinalAction { 251 | ACTION_UNSPECIFIED = 0; 252 | ALLOW = 1; 253 | REPORT_ONLY = 2; 254 | WARN = 3; 255 | BLOCK = 4; 256 | }; 257 | optional FinalAction final_action = 3; 258 | } 259 | 260 | // A message that asks the agent to cancel all requests with the given user 261 | // action id. Note that more that content analysis request may have the given 262 | // user action id. 263 | message ContentAnalysisCancelRequests { 264 | optional string user_action_id = 1; 265 | } 266 | 267 | // Generic message sent from Chrome to Agent. 268 | message ChromeToAgent { 269 | optional ContentAnalysisRequest request = 1; 270 | optional ContentAnalysisAcknowledgement ack = 2; 271 | optional ContentAnalysisCancelRequests cancel = 3; 272 | } 273 | 274 | // Generic message sent from Agent to Chrome. 275 | message AgentToChrome { 276 | optional ContentAnalysisResponse response = 1; 277 | } 278 | -------------------------------------------------------------------------------- /agent/include/content_analysis/sdk/analysis_agent.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_AGENT_H_ 6 | #define CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_AGENT_H_ 7 | 8 | #include 9 | #include 10 | 11 | #include "content_analysis/sdk/analysis.pb.h" 12 | #include "content_analysis/sdk/result_codes.h" 13 | 14 | // This is the main include file for code using Content Analysis Connector 15 | // Agent SDK. No other include is needed. 16 | // 17 | // An agent begins by creating an instance of Agent using the factory 18 | // function Agent::Create(). This instance should live as long as the agent 19 | // intends to receive content analysis requests from Google Chrome. 20 | // 21 | // Agent::Create() must be passed an object that implements the 22 | // AgentEventHandler interface. Methods on this interface will be called 23 | // at the approriate time to handle specific events. The events are: 24 | // 25 | // - A Google Chrome browser has started or stopped. This events contains 26 | // information about the browser such as process id and executable path. 27 | // - A request to analyze content. The agent reads and analyses the 28 | // request in to determine a verdict: allow or block. When the verdict is 29 | // known the response is sent back to Google Chrome. 30 | // - An acknowledgement that Google Chrome has properly received the agent's 31 | // verdict. 32 | // 33 | // The agent is not required to serialize event handling. That is, content 34 | // analysis events can be analyze in the background and the response does 35 | // not need to be sent before OnAnalysisRequested() returns. 36 | // 37 | // Google Chrome thottles the number of requests sent to the agent to 5 38 | // current requests at a time but this is subject to change. 39 | 40 | namespace content_analysis { 41 | namespace sdk { 42 | 43 | // Represents information about one instance of a Google Chrome browser 44 | // process that is connected to the agent. 45 | struct BrowserInfo { 46 | unsigned long pid = 0; // Process ID of Google Chrome browser process. 47 | std::string binary_path; // The full path to the process's main binary. 48 | }; 49 | 50 | // Represents one content analysis request as generated by a given user action 51 | // in Google Chrome. 52 | // 53 | // The agent should retrieve information about the content analysis request 54 | // using the GetRequest() method. The agent should analyze the request and 55 | // update the response, returned by GetResponse(), with a verdict (allow or 56 | // block). Once the verdict is set the response can be sent back to Google 57 | // Chrome by calling Send(). 58 | // 59 | // The default verdict is to allow the requested user action. If the final 60 | // verdict is to allow then the agent does not need to update the response and 61 | // can simply call Send(). 62 | // 63 | // If the final verdict should be to block, the agent should first update the 64 | // response by calling SetEventVerdictToBlock() before calling Send(). 65 | // 66 | // This class is not thread safe. However, it may be passed to another thread 67 | // as long as the agent properly serializses access to the event. 68 | // 69 | // See the demo directory for an example of how to use this class. 70 | class ContentAnalysisEvent { 71 | public: 72 | virtual ~ContentAnalysisEvent() = default; 73 | 74 | // Prepares the event for graceful shutdown. Upon return calls to all 75 | // other methods of this class will fail. 76 | virtual ResultCode Close() = 0; 77 | 78 | // Retrives information about the browser that generated this content 79 | // analysis event. 80 | virtual const BrowserInfo& GetBrowserInfo() const = 0; 81 | 82 | // Retrieves a read-only reference to the content analysis request received 83 | // from Google Chrome. 84 | virtual const ContentAnalysisRequest& GetRequest() const = 0; 85 | 86 | // Retrieves a writable reference to the content analysis response that will 87 | // be sent to Google Chrome as the verdict for the request of this event. 88 | // The agent may modify this response in place before calling Send(). 89 | virtual ContentAnalysisResponse& GetResponse() = 0; 90 | 91 | // Send the verdict to Google Chrome. Once this method is called further 92 | // changes to the response are ignored. 93 | virtual ResultCode Send() = 0; 94 | 95 | // Returns a string containing internal state of the object that is useful 96 | // for debugging. 97 | virtual std::string DebugString() const = 0; 98 | 99 | protected: 100 | ContentAnalysisEvent() = default; 101 | ContentAnalysisEvent(const ContentAnalysisEvent& rhs) = delete; 102 | ContentAnalysisEvent(ContentAnalysisEvent&& rhs) = delete; 103 | ContentAnalysisEvent& operator=(const ContentAnalysisEvent& rhs) = delete; 104 | ContentAnalysisEvent& operator=(ContentAnalysisEvent&& rhs) = delete; 105 | 106 | }; 107 | 108 | // Agents should implement this interface in order to handle events as needed. 109 | // 110 | // OnBrowserConnected() and OnBrowserDisonnected() notify the agent when 111 | // instances of Google Chome start and stop. The agent may perform any one-time 112 | // actions as required for these events. The default action is to do nothing 113 | // for both events. If the agent does not need perform any special actions 114 | // these methods do not need to be overridden. 115 | // 116 | // OnAnalysisRequested() notifies the agent of a new content analysis request 117 | // from Google Chrome. The agent should perform the analysis and respond to 118 | // the event. It is not required for the agent complete the analysis and 119 | // respond to before this callback returns. The agent may pass the 120 | // ContentAnalysisEvent to a background task and respond when ready. This 121 | // callback has no default action and agents must override it. 122 | // 123 | // OnResponseAcknowledged() notifies the agent that Google Chrome has received 124 | // the content analysis response and how it has handled it. 125 | class AgentEventHandler { 126 | public: 127 | AgentEventHandler() = default; 128 | virtual ~AgentEventHandler() = default; 129 | 130 | // Called when a new Google Chrome browser instance connects to the agent. 131 | // This is always called before the first OnAnalysisRequested() from that 132 | // browser. 133 | virtual void OnBrowserConnected(const BrowserInfo& info) {} 134 | 135 | // Called when a Google Chrome browser instance disconnects from the agent. 136 | // The agent will no longer receive new content analysis requests from this 137 | // browser. 138 | virtual void OnBrowserDisconnected(const BrowserInfo& info) {} 139 | 140 | // Called when a Google Chrome browser requests a content analysis. 141 | virtual void OnAnalysisRequested( 142 | std::unique_ptr event) = 0; 143 | 144 | // Called when a Google Chrome browser acknowledges the content analysis 145 | // response from the agent. The default action is to do nothing. 146 | // If the agent does not need perform any special actions this methods does 147 | // not need to be overridden. 148 | virtual void OnResponseAcknowledged( 149 | const ContentAnalysisAcknowledgement& ack) {} 150 | 151 | // Called when a Google Chrome browser asks the agent to cancels one or 152 | // more content analysis requests. This happens when the user presses the 153 | // Cancel button in the in-progress dialog. This is expected to be a best 154 | // effort only; agents may choose to ignore this message or possibly only 155 | // cancel a subset of requests with the given user action id. 156 | // 157 | // The default action is to do nothing. If the agent does not need perform 158 | // any special actions this methods does not need to be overridden. 159 | virtual void OnCancelRequests( 160 | const ContentAnalysisCancelRequests& cancel) {} 161 | 162 | // Called whenever the Agent implementation detects an error. `context` 163 | // is a string that provide a hint to the handler as to where the error 164 | // happened in the agent. `error` represent the actual error detected. 165 | virtual void OnInternalError(const char* context, ResultCode error) {} 166 | }; 167 | 168 | // Represents an agent that can perform content analysis for the Google Chrome 169 | // browser. This class holds the server endpoint that Google Chrome connects 170 | // to when content analysis is required. 171 | // 172 | // Agent instances should outlive all ContentAnalysisEvent instances created 173 | // with it. Agent instances are not thread safe except for Stop() which can be 174 | // called from any thread to shutdown the agent. Outstanding 175 | // ContentAnalysisEvents created from this agent may or may not still complete. 176 | // 177 | // See the demo directory for an example of how to use this class. 178 | class Agent { 179 | public: 180 | // Configuration options where creating an agent. `name` is used to create 181 | // a channel between the agent and Google Chrome. 182 | struct Config { 183 | // Used to create a channel between the agent and Google Chrome. Both must 184 | // use the same name to properly rendezvous with each other. The channel 185 | // is platform specific. 186 | std::string name; 187 | 188 | // Set to true if there is a different agent instance per OS user. Defaults 189 | // to false. 190 | bool user_specific = false; 191 | }; 192 | 193 | // Creates a new agent instance. If successful, an agent is returned. 194 | // Otherwise a nullptr is returned and `rc` contains the reason for the 195 | // failure. 196 | static std::unique_ptr Create( 197 | Config config, 198 | std::unique_ptr handler, 199 | ResultCode* rc); 200 | 201 | virtual ~Agent() = default; 202 | 203 | // Returns the configuration parameters used to create the agent. 204 | virtual const Config& GetConfig() const = 0; 205 | 206 | // Handles events triggered on this agent and calls the coresponding 207 | // callbacks in the AgentEventHandler. This method is blocking and returns 208 | // when Stop() is called or if an error occurs. 209 | virtual ResultCode HandleEvents() = 0; 210 | 211 | // Prepares the agent for graceful shutdown. Any function blocked on 212 | // HandleEvents() will return. It is safe to call this method from any 213 | // thread. 214 | virtual ResultCode Stop() = 0; 215 | 216 | // Returns a string containing internal state of the object that is useful 217 | // for debugging. 218 | virtual std::string DebugString() const = 0; 219 | 220 | protected: 221 | Agent() = default; 222 | Agent(const Agent& rhs) = delete; 223 | Agent(Agent&& rhs) = delete; 224 | Agent& operator=(const Agent& rhs) = delete; 225 | Agent& operator=(Agent&& rhs) = delete; 226 | }; 227 | 228 | // Update the tag or status of `response`. This function assumes that the 229 | // response contains only one Result. If one already exists it is updated 230 | // otherwise a new Result is created. 231 | // 232 | // The response contained within ContentAnalysisEvent has already been updated. 233 | // This function is useful only when create a new instance of 234 | // ContentAnalysisResponse. 235 | // 236 | // If `tag` is not empty it will replace the result's tag. 237 | // If `status` is not STATUS_UNKNOWN it will will replace the result's status. 238 | ResultCode UpdateResponse(ContentAnalysisResponse& response, 239 | const std::string& tag, 240 | ContentAnalysisResponse::Result::Status status); 241 | 242 | // Sets the response verdict of an event to `action`. This is a convenience 243 | // function that is equivalent to the following: 244 | // 245 | // auto result = event->GetResponse().mutable_results(0); 246 | // auto rule = result->mutable_triggered_rules(0); 247 | // rule->set_action(action); 248 | // 249 | // This function assumes the event's response has already been initialized 250 | // using UpdateResponse(). 251 | ResultCode SetEventVerdictTo( 252 | ContentAnalysisEvent* event, 253 | ContentAnalysisResponse::Result::TriggeredRule::Action action); 254 | 255 | // Sets the reponse verdict of an event to "block". This is a convenience 256 | // function that is equivalent to the following: 257 | // 258 | // SetEventVerdictTo(event, 259 | // ContentAnalysisResponse::Result::TriggeredRule::BLOCK); 260 | ResultCode SetEventVerdictToBlock(ContentAnalysisEvent* event); 261 | 262 | // Helper class to handle the lifetime and access of print data. 263 | class ScopedPrintHandle { 264 | public: 265 | virtual ~ScopedPrintHandle() = default; 266 | virtual const char* data() = 0; 267 | virtual size_t size() = 0; 268 | 269 | protected: 270 | ScopedPrintHandle() = default; 271 | 272 | ScopedPrintHandle(const ScopedPrintHandle&) = delete; 273 | ScopedPrintHandle& operator=(const ScopedPrintHandle&) = delete; 274 | 275 | ScopedPrintHandle(ScopedPrintHandle&&) = default; 276 | ScopedPrintHandle& operator=(ScopedPrintHandle&&) = default; 277 | }; 278 | 279 | // Returns a `ScopedPrintHandle` initialized from the request's print data 280 | // if it exists. 281 | std::unique_ptr 282 | CreateScopedPrintHandle(const ContentAnalysisRequest& request, 283 | int64_t browser_pid); 284 | 285 | } // namespace sdk 286 | } // namespace content_analysis 287 | 288 | #endif // CONTENT_ANALYSIS_AGENT_INCLUDE_CONTENT_ANALYSIS_SDK_ANALYSIS_AGENT_H_ 289 | -------------------------------------------------------------------------------- /demo/client.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "content_analysis/sdk/analysis_client.h" 16 | #include "demo/atomic_output.h" 17 | 18 | using content_analysis::sdk::Client; 19 | using content_analysis::sdk::ContentAnalysisRequest; 20 | using content_analysis::sdk::ContentAnalysisResponse; 21 | using content_analysis::sdk::ContentAnalysisAcknowledgement; 22 | 23 | // Different paths are used depending on whether this agent should run as a 24 | // use specific agent or not. These values are chosen to match the test 25 | // values in chrome browser. 26 | constexpr char kPathUser[] = "path_user"; 27 | constexpr char kPathSystem[] = "brcm_chrm_cas"; 28 | 29 | // Global app config. 30 | std::string path = kPathSystem; 31 | bool user_specific = false; 32 | bool group = false; 33 | std::unique_ptr client; 34 | 35 | // Paramters used to build the request. 36 | content_analysis::sdk::AnalysisConnector connector = 37 | content_analysis::sdk::FILE_ATTACHED; 38 | time_t request_token_number = time(nullptr); 39 | std::string request_token; 40 | std::string tag = "dlp"; 41 | bool threaded = false; 42 | std::string digest = "sha256-123456"; 43 | std::string url = "https://upload.example.com"; 44 | std::string email = "me@example.com"; 45 | std::string machine_user = "DOMAIN\\me"; 46 | std::vector datas; 47 | 48 | // When grouping, remember the tokens of all requests/responses in order to 49 | // acknowledge them all with the same final action. 50 | // 51 | // This global state. It may be access from multiple thread so must be 52 | // accessed from a critical section. 53 | std::mutex global_mutex; 54 | ContentAnalysisAcknowledgement::FinalAction global_final_action = 55 | ContentAnalysisAcknowledgement::ALLOW; 56 | std::vector request_tokens; 57 | 58 | // Command line parameters. 59 | constexpr const char* kArgConnector = "--connector="; 60 | constexpr const char* kArgDigest = "--digest="; 61 | constexpr const char* kArgEmail = "--email="; 62 | constexpr const char* kArgGroup = "--group"; 63 | constexpr const char* kArgMachineUser = "--machine-user="; 64 | constexpr const char* kArgPath = "--path="; 65 | constexpr const char* kArgRequestToken = "--request-token="; 66 | constexpr const char* kArgTag = "--tag="; 67 | constexpr const char* kArgThreaded = "--threaded"; 68 | constexpr const char* kArgUrl = "--url="; 69 | constexpr const char* kArgUserSpecific = "--user"; 70 | constexpr const char* kArgHelp = "--help"; 71 | 72 | bool ParseCommandLine(int argc, char* argv[]) { 73 | for (int i = 1; i < argc; ++i) { 74 | const std::string arg = argv[i]; 75 | if (arg.find(kArgConnector) == 0) { 76 | std::string connector_str = arg.substr(strlen(kArgConnector)); 77 | if (connector_str == "download") { 78 | connector = content_analysis::sdk::FILE_DOWNLOADED; 79 | } else if (connector_str == "attach") { 80 | connector = content_analysis::sdk::FILE_ATTACHED; 81 | } else if (connector_str == "bulk-data-entry") { 82 | connector = content_analysis::sdk::BULK_DATA_ENTRY; 83 | } else if (connector_str == "print") { 84 | connector = content_analysis::sdk::PRINT; 85 | } else if (connector_str == "file-transfer") { 86 | connector = content_analysis::sdk::FILE_TRANSFER; 87 | } else { 88 | std::cout << "[Demo] Incorrect command line arg: " << arg << std::endl; 89 | return false; 90 | } 91 | } else if (arg.find(kArgRequestToken) == 0) { 92 | request_token = arg.substr(strlen(kArgRequestToken)); 93 | } else if (arg.find(kArgTag) == 0) { 94 | tag = arg.substr(strlen(kArgTag)); 95 | } else if (arg.find(kArgThreaded) == 0) { 96 | threaded = true; 97 | } else if (arg.find(kArgDigest) == 0) { 98 | digest = arg.substr(strlen(kArgDigest)); 99 | } else if (arg.find(kArgUrl) == 0) { 100 | url = arg.substr(strlen(kArgUrl)); 101 | } else if (arg.find(kArgMachineUser) == 0) { 102 | machine_user = arg.substr(strlen(kArgMachineUser)); 103 | } else if (arg.find(kArgEmail) == 0) { 104 | email = arg.substr(strlen(kArgEmail)); 105 | } else if (arg.find(kArgPath) == 0) { 106 | path = arg.substr(strlen(kArgPath)); 107 | } else if (arg.find(kArgUserSpecific) == 0) { 108 | // If kArgPath was already used, abort. 109 | if (path != kPathSystem) { 110 | std::cout << std::endl << "ERROR: use --path= after --user"; 111 | return false; 112 | } 113 | path = kPathUser; 114 | user_specific = true; 115 | } else if (arg.find(kArgGroup) == 0) { 116 | group = true; 117 | } else if (arg.find(kArgHelp) == 0) { 118 | return false; 119 | } else { 120 | datas.push_back(arg); 121 | } 122 | } 123 | 124 | return true; 125 | } 126 | 127 | void PrintHelp() { 128 | std::cout 129 | << std::endl << std::endl 130 | << "Usage: client [OPTIONS] [@]content_or_file ..." << std::endl 131 | << "A simple client to send content analysis requests to a running agent." << std::endl 132 | << "Without @ the content to analyze is the argument itself." << std::endl 133 | << "Otherwise the content is read from a file called 'content_or_file'." << std::endl 134 | << "Multiple [@]content_or_file arguments may be specified, each generates one request." << std::endl 135 | << std::endl << "Options:" << std::endl 136 | << kArgConnector << " : one of 'download', 'attach' (default), 'bulk-data-entry', 'print', or 'file-transfer'" << std::endl 137 | << kArgRequestToken << " : defaults to 'req-' which auto increments" << std::endl 138 | << kArgTag << " : defaults to 'dlp'" << std::endl 139 | << kArgThreaded << " : handled multiple requests using threads" << std::endl 140 | << kArgUrl << " : defaults to 'https://upload.example.com'" << std::endl 141 | << kArgMachineUser << " : defaults to 'DOMAIN\\me'" << std::endl 142 | << kArgEmail << " : defaults to 'me@example.com'" << std::endl 143 | << kArgPath << " : Used the specified path instead of default. Must come after --user." << std::endl 144 | << kArgUserSpecific << " : Connects to an OS user specific agent" << std::endl 145 | << kArgDigest << " : defaults to 'sha256-123456'" << std::endl 146 | << kArgGroup << " : Generate the same final action for all requests" << std::endl 147 | << kArgHelp << " : prints this help message" << std::endl; 148 | } 149 | 150 | std::string GenerateRequestToken() { 151 | std::stringstream stm; 152 | stm << "req-" << request_token_number++; 153 | return stm.str(); 154 | } 155 | 156 | ContentAnalysisRequest BuildRequest(const std::string& data) { 157 | std::string filepath; 158 | std::string filename; 159 | if (data[0] == '@') { 160 | filepath = data.substr(1); 161 | filename = filepath.substr(filepath.find_last_of("/\\") + 1); 162 | } 163 | 164 | ContentAnalysisRequest request; 165 | 166 | // Set request to expire 5 minutes into the future. 167 | request.set_expires_at(time(nullptr) + 5 * 60); 168 | request.set_analysis_connector(connector); 169 | request.set_request_token(!request_token.empty() 170 | ? request_token : GenerateRequestToken()); 171 | *request.add_tags() = tag; 172 | 173 | auto request_data = request.mutable_request_data(); 174 | request_data->set_url(url); 175 | request_data->set_email(email); 176 | request_data->set_digest(digest); 177 | if (!filename.empty()) { 178 | request_data->set_filename(filename); 179 | } 180 | 181 | auto client_metadata = request.mutable_client_metadata(); 182 | auto browser = client_metadata->mutable_browser(); 183 | browser->set_machine_user(machine_user); 184 | 185 | if (!filepath.empty()) { 186 | request.set_file_path(filepath); 187 | } else if (!data.empty()) { 188 | request.set_text_content(data); 189 | } else { 190 | std::cout << "[Demo] Specify text content or a file path." << std::endl; 191 | PrintHelp(); 192 | exit(1); 193 | } 194 | 195 | return request; 196 | } 197 | 198 | // Gets the most severe action within the result. 199 | ContentAnalysisResponse::Result::TriggeredRule::Action 200 | GetActionFromResult(const ContentAnalysisResponse::Result& result) { 201 | auto action = 202 | ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED; 203 | for (auto rule : result.triggered_rules()) { 204 | if (rule.has_action() && rule.action() > action) 205 | action = rule.action(); 206 | } 207 | return action; 208 | } 209 | 210 | // Gets the most severe action within all the the results of a response. 211 | ContentAnalysisResponse::Result::TriggeredRule::Action 212 | GetActionFromResponse(const ContentAnalysisResponse& response) { 213 | auto action = 214 | ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED; 215 | for (auto result : response.results()) { 216 | auto action2 = GetActionFromResult(result); 217 | if (action2 > action) 218 | action = action2; 219 | } 220 | return action; 221 | } 222 | 223 | void DumpResponse( 224 | std::stringstream& stream, 225 | const ContentAnalysisResponse& response) { 226 | for (auto result : response.results()) { 227 | auto tag = result.has_tag() ? result.tag() : ""; 228 | 229 | auto status = result.has_status() 230 | ? result.status() 231 | : ContentAnalysisResponse::Result::STATUS_UNKNOWN; 232 | std::string status_str; 233 | switch (status) { 234 | case ContentAnalysisResponse::Result::STATUS_UNKNOWN: 235 | status_str = "Unknown"; 236 | break; 237 | case ContentAnalysisResponse::Result::SUCCESS: 238 | status_str = "Success"; 239 | break; 240 | case ContentAnalysisResponse::Result::FAILURE: 241 | status_str = "Failure"; 242 | break; 243 | default: 244 | status_str = ""; 245 | break; 246 | } 247 | 248 | auto action = GetActionFromResult(result); 249 | std::string action_str; 250 | switch (action) { 251 | case ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED: 252 | action_str = "allowed"; 253 | break; 254 | case ContentAnalysisResponse::Result::TriggeredRule::REPORT_ONLY: 255 | action_str = "reported only"; 256 | break; 257 | case ContentAnalysisResponse::Result::TriggeredRule::WARN: 258 | action_str = "warned"; 259 | break; 260 | case ContentAnalysisResponse::Result::TriggeredRule::BLOCK: 261 | action_str = "blocked"; 262 | break; 263 | } 264 | 265 | time_t now = time(nullptr); 266 | stream << "[Demo] Request " << response.request_token() << " is " << action_str 267 | << " after " << tag 268 | << " analysis, status=" << status_str 269 | << " at " << ctime(&now); 270 | } 271 | } 272 | 273 | ContentAnalysisAcknowledgement BuildAcknowledgement( 274 | const std::string& request_token, 275 | ContentAnalysisAcknowledgement::FinalAction final_action) { 276 | ContentAnalysisAcknowledgement ack; 277 | ack.set_request_token(request_token); 278 | ack.set_status(ContentAnalysisAcknowledgement::SUCCESS); 279 | ack.set_final_action(final_action); 280 | return ack; 281 | } 282 | 283 | void HandleRequest(const ContentAnalysisRequest& request) { 284 | AtomicCout aout; 285 | ContentAnalysisResponse response; 286 | int err = client->Send(request, &response); 287 | if (err != 0) { 288 | aout.stream() << "[Demo] Error sending request " << request.request_token() 289 | << std::endl; 290 | } else if (response.results_size() == 0) { 291 | aout.stream() << "[Demo] Response " << request.request_token() << " is missing a result" 292 | << std::endl; 293 | } else { 294 | DumpResponse(aout.stream(), response); 295 | 296 | auto final_action = ContentAnalysisAcknowledgement::ALLOW; 297 | switch (GetActionFromResponse(response)) { 298 | case ContentAnalysisResponse::Result::TriggeredRule::ACTION_UNSPECIFIED: 299 | break; 300 | case ContentAnalysisResponse::Result::TriggeredRule::REPORT_ONLY: 301 | final_action = ContentAnalysisAcknowledgement::REPORT_ONLY; 302 | break; 303 | case ContentAnalysisResponse::Result::TriggeredRule::WARN: 304 | final_action = ContentAnalysisAcknowledgement::WARN; 305 | break; 306 | case ContentAnalysisResponse::Result::TriggeredRule::BLOCK: 307 | final_action = ContentAnalysisAcknowledgement::BLOCK; 308 | break; 309 | } 310 | 311 | // If grouping, remember the request's token in order to ack the response 312 | // later. 313 | if (group) { 314 | std::unique_lock lock(global_mutex); 315 | request_tokens.push_back(request.request_token()); 316 | if (final_action > global_final_action) 317 | global_final_action = final_action; 318 | } else { 319 | int err = client->Acknowledge( 320 | BuildAcknowledgement(response.request_token(), final_action)); 321 | if (err != 0) { 322 | aout.stream() << "[Demo] Error sending ack " << request.request_token() 323 | << std::endl; 324 | } 325 | } 326 | } 327 | } 328 | 329 | void ProcessRequest(size_t i) { 330 | auto request = BuildRequest(datas[i]); 331 | 332 | { 333 | AtomicCout aout; 334 | aout.stream() << "[Demo] Sending request " << request.request_token() << std::endl; 335 | } 336 | 337 | HandleRequest(request); 338 | } 339 | 340 | int main(int argc, char* argv[]) { 341 | if (!ParseCommandLine(argc, argv)) { 342 | PrintHelp(); 343 | return 1; 344 | } 345 | 346 | // Each client uses a unique name to identify itself with Google Chrome. 347 | client = Client::Create({path, user_specific}); 348 | if (!client) { 349 | std::cout << "[Demo] Error starting client" << std::endl; 350 | return 1; 351 | }; 352 | 353 | auto info = client->GetAgentInfo(); 354 | std::cout << "Agent pid=" << info.pid 355 | << " path=" << info.binary_path << std::endl; 356 | 357 | if (threaded) { 358 | std::vector> threads; 359 | for (int i = 0; i < datas.size(); ++i) { 360 | AtomicCout aout; 361 | aout.stream() << "Start thread " << i << std::endl; 362 | threads.emplace_back(std::make_unique(ProcessRequest, i)); 363 | } 364 | 365 | // Make sure all threads have terminated. 366 | for (auto& thread : threads) { 367 | thread->join(); 368 | } 369 | } 370 | else { 371 | for (size_t i = 0; i < datas.size(); ++i) { 372 | ProcessRequest(i); 373 | } 374 | } 375 | // It's safe to access global state beyond this point without locking since 376 | // all no more responses will be touching them. 377 | 378 | if (group) { 379 | std::cout << std::endl; 380 | std::cout << "[Demo] Final action for all requests is "; 381 | switch (global_final_action) { 382 | // Google Chrome fails open, so if no action is specified that is the same 383 | // as ALLOW. 384 | case ContentAnalysisAcknowledgement::ACTION_UNSPECIFIED: 385 | case ContentAnalysisAcknowledgement::ALLOW: 386 | std::cout << "allowed"; 387 | break; 388 | case ContentAnalysisAcknowledgement::REPORT_ONLY: 389 | std::cout << "reported only"; 390 | break; 391 | case ContentAnalysisAcknowledgement::WARN: 392 | std::cout << "warned"; 393 | break; 394 | case ContentAnalysisAcknowledgement::BLOCK: 395 | std::cout << "blocked"; 396 | break; 397 | } 398 | std::cout << std::endl << std::endl; 399 | 400 | for (auto token : request_tokens) { 401 | std::cout << "[Demo] Sending group Ack" << std::endl; 402 | int err = client->Acknowledge( 403 | BuildAcknowledgement(token, global_final_action)); 404 | if (err != 0) { 405 | std::cout << "[Demo] Error sending ack for " << token << std::endl; 406 | } 407 | } 408 | } 409 | 410 | return 0; 411 | }; 412 | -------------------------------------------------------------------------------- /browser/src/client_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "common/utils_win.h" 14 | 15 | #include "client_win.h" 16 | 17 | namespace content_analysis { 18 | namespace sdk { 19 | 20 | const DWORD kBufferSize = 4096; 21 | 22 | // Use the same default timeout value (50ms) as CreateNamedPipeA(), expressed 23 | // in 100ns intervals. 24 | constexpr LONGLONG kDefaultTimeout = 500000; 25 | 26 | // The following #defines and struct are copied from the official Microsoft 27 | // Windows Driver Kit headers because they are not available in the official 28 | // Microsoft Windows user mode SDK headers. 29 | 30 | #define FSCTL_PIPE_WAIT 0x110018 31 | #define STATUS_SUCCESS ((NTSTATUS)0) 32 | #define STATUS_PIPE_NOT_AVAILABLE ((NTSTATUS)0xc00000ac) 33 | #define STATUS_IO_TIMEOUT ((NTSTATUS)0xc00000b5) 34 | 35 | typedef struct _FILE_PIPE_WAIT_FOR_BUFFER { 36 | LARGE_INTEGER Timeout; 37 | ULONG NameLength; 38 | BOOLEAN TimeoutSpecified; 39 | WCHAR Name[1]; 40 | } FILE_PIPE_WAIT_FOR_BUFFER, *PFILE_PIPE_WAIT_FOR_BUFFER; 41 | 42 | namespace { 43 | 44 | using NtCreateFileFn = decltype(&::NtCreateFile); 45 | 46 | NtCreateFileFn GetNtCreateFileFn() { 47 | static NtCreateFileFn fnNtCreateFile = []() { 48 | NtCreateFileFn fn = nullptr; 49 | HMODULE h = LoadLibraryA("NtDll.dll"); 50 | if (h != nullptr) { 51 | fn = reinterpret_cast(GetProcAddress(h, "NtCreateFile")); 52 | FreeLibrary(h); 53 | } 54 | return fn; 55 | }(); 56 | 57 | return fnNtCreateFile; 58 | } 59 | 60 | 61 | using NtFsControlFileFn = NTSTATUS (NTAPI *)( 62 | HANDLE FileHandle, 63 | HANDLE Event, 64 | PIO_APC_ROUTINE ApcRoutine, 65 | PVOID ApcContext, 66 | PIO_STATUS_BLOCK IoStatusBlock, 67 | ULONG IoControlCode, 68 | PVOID InputBuffer, 69 | ULONG InputBufferLength, 70 | PVOID OutputBuffer, 71 | ULONG OutputBufferLength); 72 | 73 | NtFsControlFileFn GetNtFsControlFileFn() { 74 | static NtFsControlFileFn fnNtFsControlFile = []() { 75 | NtFsControlFileFn fn = nullptr; 76 | HMODULE h = LoadLibraryA("NtDll.dll"); 77 | if (h != nullptr) { 78 | fn = reinterpret_cast(GetProcAddress(h, "NtFsControlFile")); 79 | FreeLibrary(h); 80 | } 81 | return fn; 82 | }(); 83 | 84 | return fnNtFsControlFile; 85 | } 86 | 87 | NTSTATUS WaitForPipeAvailability(const UNICODE_STRING& path) { 88 | NtCreateFileFn fnNtCreateFile = GetNtCreateFileFn(); 89 | if (fnNtCreateFile == nullptr) { 90 | return false; 91 | } 92 | NtFsControlFileFn fnNtFsControlFile = GetNtFsControlFileFn(); 93 | if (fnNtFsControlFile == nullptr) { 94 | return false; 95 | } 96 | 97 | // Build the device name. This is the initial part of `path` which is 98 | // assumed to start with the string `kPipePrefixForClient`. The `Length` 99 | // field is measured in bytes, not characters, and does not include the null 100 | // terminator. It's important that the device name ends with a trailing 101 | // backslash. 102 | size_t device_name_char_length = std::strlen(internal::kPipePrefixForClient); 103 | UNICODE_STRING device_name; 104 | device_name.Buffer = path.Buffer; 105 | device_name.Length = device_name_char_length * sizeof(wchar_t); 106 | device_name.MaximumLength = device_name.Length; 107 | 108 | // Build the pipe name. This is the remaining part of `path` after the device 109 | // name. 110 | UNICODE_STRING pipe_name; 111 | pipe_name.Buffer = path.Buffer + device_name_char_length; 112 | pipe_name.Length = path.Length - device_name.Length; 113 | pipe_name.MaximumLength = pipe_name.Length; 114 | 115 | // Build the ioctl input buffer. This buffer is the size of 116 | // FILE_PIPE_WAIT_FOR_BUFFER plus the length of the pipe name. Since 117 | // FILE_PIPE_WAIT_FOR_BUFFER includes one WCHAR this includes space for 118 | // the terminating null character of the name which wcsncpy() copies. 119 | size_t buffer_size = sizeof(FILE_PIPE_WAIT_FOR_BUFFER) + pipe_name.Length; 120 | std::vector buffer(buffer_size); 121 | FILE_PIPE_WAIT_FOR_BUFFER* wait_buffer = 122 | reinterpret_cast(buffer.data()); 123 | wait_buffer->Timeout.QuadPart = kDefaultTimeout; 124 | wait_buffer->NameLength = pipe_name.Length; 125 | wait_buffer->TimeoutSpecified = TRUE; 126 | std::wcsncpy(wait_buffer->Name, pipe_name.Buffer, wait_buffer->NameLength / 127 | sizeof(wchar_t)); 128 | 129 | OBJECT_ATTRIBUTES attr; 130 | InitializeObjectAttributes(&attr, &device_name, OBJ_CASE_INSENSITIVE, nullptr, 131 | nullptr); 132 | 133 | IO_STATUS_BLOCK io; 134 | HANDLE h = INVALID_HANDLE_VALUE; 135 | NTSTATUS sts = fnNtCreateFile(&h, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, 136 | &attr, &io, /*AllocationSize=*/nullptr, FILE_ATTRIBUTE_NORMAL, 137 | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN, 138 | FILE_SYNCHRONOUS_IO_NONALERT, /*EaBuffer=*/nullptr, /*EaLength=*/0); 139 | if (sts != STATUS_SUCCESS) { 140 | return false; 141 | } 142 | 143 | IO_STATUS_BLOCK io2; 144 | sts = fnNtFsControlFile(h, /*Event=*/nullptr, /*ApcRoutine=*/nullptr, 145 | /*ApcContext*/nullptr, &io2, FSCTL_PIPE_WAIT, buffer.data(), 146 | buffer.size(), nullptr, 0); 147 | CloseHandle(h); 148 | return sts; 149 | } 150 | 151 | // Reads the next message from the pipe and returns a buffer of chars. 152 | // This function is synchronous. 153 | std::vector ReadNextMessageFromPipe( 154 | HANDLE pipe, 155 | OVERLAPPED* overlapped) { 156 | DWORD err = ERROR_SUCCESS; 157 | std::vector buffer(kBufferSize); 158 | char* p = buffer.data(); 159 | int final_size = 0; 160 | while (true) { 161 | DWORD read; 162 | 163 | // Even though the pipe is opened for overlapped IO, the read operation 164 | // could still completely synchronously. For example, a server's response 165 | // message could already be available in the pipe's internal buffer. 166 | // If ReadFile() does complete synchronously, TRUE is returned. In this 167 | // case update the final size and exit the loop. 168 | if (ReadFile(pipe, p, kBufferSize, &read, overlapped)) { 169 | final_size += read; 170 | break; 171 | } else { 172 | // Reaching here means that ReadFile() will either complete async or 173 | // an error has occurred. The former case is detected if the error code 174 | // is "IO pending", in which case GetOverlappedResult() is called to wait 175 | // for the IO to complete. If that function returns TRUE then the read 176 | // operation completed successfully and the code simply updates the final 177 | // size and exits the loop. 178 | err = GetLastError(); 179 | if (err == ERROR_IO_PENDING) { 180 | if (GetOverlappedResult(pipe, overlapped, &read, /*wait=*/TRUE)) { 181 | final_size += read; 182 | break; 183 | } else { 184 | err = GetLastError(); 185 | } 186 | } 187 | 188 | // Reaching here means an error has occurred. One error is recoverable: 189 | // "more data". For any other type of error break out of the loop. 190 | if (err != ERROR_MORE_DATA) { 191 | final_size = 0; 192 | break; 193 | } 194 | 195 | // Reaching here means the error is "more data", that is, the buffer 196 | // specified in ReadFile() was too small to contain the entire response 197 | // message from the server. ReadFile() has placed the start of the 198 | // message in the specified buffer but ReadFile() needs to be called 199 | // again to read the remaining part. 200 | // 201 | // The buffer size is increased and the current pointer into the buffer 202 | // `p` is adjusted so that when the loop re-runs, it calls ReadFile() 203 | // with the correct point in the buffer. It's possible that this loop 204 | // might have to run many times if the response message is rather large. 205 | buffer.resize(buffer.size() + kBufferSize); 206 | p = buffer.data() + buffer.size() - kBufferSize; 207 | } 208 | } 209 | 210 | buffer.resize(final_size); 211 | return buffer; 212 | } 213 | 214 | // Writes a string to the pipe. Returns true if successful, false otherwise. 215 | // This function is synchronous. 216 | bool WriteMessageToPipe( 217 | HANDLE pipe, 218 | const std::string& message, 219 | OVERLAPPED* overlapped) { 220 | if (message.empty()) 221 | return false; 222 | 223 | // Even though the pipe is opened for overlapped IO, the write operation 224 | // could still completely synchronously. If it does, TRUE is returned. 225 | // In this case the function is done. 226 | bool ok = WriteFile(pipe, message.data(), message.size(), nullptr, overlapped); 227 | if (!ok) { 228 | // Reaching here means that WriteFile() will either complete async or 229 | // an error has occurred. The former case is detected if the error code 230 | // is "IO pending", in which case GetOverlappedResult() is called to wait 231 | // for the IO to complete. Whether the operation completes sync or async, 232 | // return true if the operation succeeded and false otherwise. 233 | DWORD err = GetLastError(); 234 | if (err == ERROR_IO_PENDING) { 235 | DWORD written; 236 | ok = GetOverlappedResult(pipe, overlapped, &written, /*wait=*/TRUE); 237 | } 238 | } 239 | 240 | return ok; 241 | } 242 | 243 | } // namespace 244 | 245 | // static 246 | std::unique_ptr Client::Create(Config config) { 247 | int rc; 248 | auto client = std::make_unique(std::move(config), &rc); 249 | return rc == 0 ? std::move(client) : nullptr; 250 | } 251 | 252 | ClientWin::ClientWin(Config config, int* rc) : ClientBase(std::move(config)) { 253 | *rc = -1; 254 | 255 | std::string pipename = 256 | internal::GetPipeNameForClient(configuration().name, 257 | configuration().user_specific); 258 | if (!pipename.empty()) { 259 | unsigned long pid = 0; 260 | if (ConnectToPipe(pipename, &hPipe_) == ERROR_SUCCESS && 261 | GetNamedPipeServerProcessId(hPipe_, &pid)) { 262 | agent_info().pid = pid; 263 | 264 | // Getting the process path is best effort. 265 | *rc = 0; 266 | std::string binary_path; 267 | if (internal::GetProcessPath(pid, &binary_path)) { 268 | agent_info().binary_path = std::move(binary_path); 269 | } 270 | } 271 | } 272 | 273 | if (*rc != 0) { 274 | Shutdown(); 275 | } 276 | } 277 | 278 | ClientWin::~ClientWin() { 279 | Shutdown(); 280 | } 281 | 282 | int ClientWin::Send(ContentAnalysisRequest request, 283 | ContentAnalysisResponse* response) { 284 | ChromeToAgent chrome_to_agent; 285 | *chrome_to_agent.mutable_request() = std::move(request); 286 | 287 | internal::ScopedOverlapped overlapped; 288 | if (!overlapped.is_valid()) { 289 | return -1; 290 | } 291 | 292 | bool success = WriteMessageToPipe(hPipe_, 293 | chrome_to_agent.SerializeAsString(), 294 | overlapped); 295 | if (success) { 296 | std::vector buffer = ReadNextMessageFromPipe(hPipe_, overlapped); 297 | AgentToChrome agent_to_chrome; 298 | success = buffer.size() > 0 && 299 | agent_to_chrome.ParseFromArray(buffer.data(), buffer.size()); 300 | if (success) { 301 | *response = std::move(*agent_to_chrome.mutable_response()); 302 | } 303 | } 304 | 305 | return success ? 0 : -1; 306 | } 307 | 308 | int ClientWin::Acknowledge(const ContentAnalysisAcknowledgement& ack) { 309 | // TODO: could avoid a copy by changing argument to be 310 | // `ContentAnalysisAcknowledgement ack` and then using std::move() below and 311 | // at call site. 312 | ChromeToAgent chrome_to_agent; 313 | *chrome_to_agent.mutable_ack() = ack; 314 | 315 | internal::ScopedOverlapped overlapped; 316 | if (!overlapped.is_valid()) { 317 | return -1; 318 | } 319 | 320 | return WriteMessageToPipe(hPipe_, chrome_to_agent.SerializeAsString(), overlapped) 321 | ? 0 : -1; 322 | } 323 | 324 | int ClientWin::CancelRequests(const ContentAnalysisCancelRequests& cancel) { 325 | // TODO: could avoid a copy by changing argument to be 326 | // `ContentAnalysisCancelRequests cancel` and then using std::move() below and 327 | // at call site. 328 | ChromeToAgent chrome_to_agent; 329 | *chrome_to_agent.mutable_cancel() = cancel; 330 | 331 | internal::ScopedOverlapped overlapped; 332 | if (!overlapped.is_valid()) { 333 | return -1; 334 | } 335 | 336 | return WriteMessageToPipe(hPipe_, chrome_to_agent.SerializeAsString(), overlapped) 337 | ? 0 : -1; 338 | } 339 | 340 | // static 341 | DWORD ClientWin::ConnectToPipe(const std::string& pipename, HANDLE* handle) { 342 | // Get pointers to the Ntxxx functions. This is required to use absolute 343 | // pipe names from the Windows NT Object Manager's namespace. This protects 344 | // against the "\\.\pipe" symlink being redirected. 345 | 346 | NtCreateFileFn fnNtCreateFile = GetNtCreateFileFn(); 347 | if (fnNtCreateFile == nullptr) { 348 | return ERROR_INVALID_FUNCTION; 349 | } 350 | 351 | // Convert the path to a wchar_t string. Pass pipename.size() as the 352 | // `cbMultiByte` argument instead of -1 since the terminating null should not 353 | // be counted. NtCreateFile() does not expect the object name to be 354 | // terminated. Note that `buffer` and hence `name` created from it are both 355 | // unterminated strings. 356 | int wlen = MultiByteToWideChar(CP_ACP, 0, pipename.c_str(), pipename.size(), 357 | nullptr, 0); 358 | if (wlen == 0) { 359 | return GetLastError(); 360 | } 361 | std::vector buffer(wlen); 362 | MultiByteToWideChar(CP_ACP, 0, pipename.c_str(), pipename.size(), 363 | buffer.data(), wlen); 364 | 365 | UNICODE_STRING name; 366 | name.Buffer = buffer.data(); 367 | name.Length = wlen * sizeof(wchar_t); // Length in bytes, not characters. 368 | name.MaximumLength = name.Length; 369 | 370 | OBJECT_ATTRIBUTES attr; 371 | InitializeObjectAttributes(&attr, &name, OBJ_CASE_INSENSITIVE, nullptr, 372 | nullptr); 373 | 374 | // Open the named pipe for overlapped IO, i.e. do not specify either of the 375 | // FILE_SYNCHRONOUS_IO_xxxALERT in the creation option flags. If the pipe 376 | // is not opened for overlapped IO, then the Send() method will block if 377 | // called from different threads since only one read or write operation would 378 | // be allowed at a time. 379 | IO_STATUS_BLOCK io; 380 | HANDLE h = INVALID_HANDLE_VALUE; 381 | NTSTATUS sts = STATUS_IO_TIMEOUT; 382 | while (sts == STATUS_IO_TIMEOUT) { 383 | sts = fnNtCreateFile(&h, GENERIC_READ | GENERIC_WRITE | 384 | SYNCHRONIZE, &attr, &io, /*AllocationSize=*/nullptr, 385 | FILE_ATTRIBUTE_NORMAL, /*ShareAccess=*/0, FILE_OPEN, 386 | FILE_NON_DIRECTORY_FILE, 387 | /*EaBuffer=*/nullptr, /*EaLength=*/0); 388 | if (sts != STATUS_SUCCESS) { 389 | if (sts != STATUS_PIPE_NOT_AVAILABLE) { 390 | break; 391 | } 392 | 393 | sts = WaitForPipeAvailability(name); 394 | if (sts != STATUS_SUCCESS && sts != STATUS_IO_TIMEOUT) { 395 | break; 396 | } 397 | } 398 | } 399 | 400 | if (sts != STATUS_SUCCESS) { 401 | return ERROR_PIPE_NOT_CONNECTED; 402 | } 403 | 404 | // Change to message read mode to match server side. Max connection count 405 | // and timeout must be null if client and server are on the same machine. 406 | DWORD mode = PIPE_READMODE_MESSAGE; 407 | if (!SetNamedPipeHandleState(h, &mode, 408 | /*maxCollectionCount=*/nullptr, 409 | /*connectionTimeout=*/nullptr)) { 410 | DWORD err = GetLastError(); 411 | CloseHandle(h); 412 | return err; 413 | } 414 | 415 | *handle = h; 416 | return ERROR_SUCCESS; 417 | } 418 | 419 | void ClientWin::Shutdown() { 420 | if (hPipe_ != INVALID_HANDLE_VALUE) { 421 | FlushFileBuffers(hPipe_); 422 | CloseHandle(hPipe_); 423 | hPipe_ = INVALID_HANDLE_VALUE; 424 | } 425 | } 426 | 427 | } // namespace sdk 428 | } // namespace content_analysis -------------------------------------------------------------------------------- /agent/src/agent_win_unittest.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "agent/src/agent_win.h" 10 | #include "agent/src/event_win.h" 11 | #include "browser/src/client_win.h" 12 | #include "gtest/gtest.h" 13 | 14 | namespace content_analysis { 15 | namespace sdk { 16 | namespace testing { 17 | 18 | // A handler that counts the number of times the callback methods are invoked. 19 | // Also remembers the last BrowserInfo structure passed to it from any of the 20 | // callbacks. 21 | struct TestHandler : public AgentEventHandler { 22 | void OnBrowserConnected(const BrowserInfo& info) override { 23 | last_info_ = info; 24 | ++connect_count_; 25 | } 26 | void OnBrowserDisconnected(const BrowserInfo& info) override { 27 | last_info_ = info; 28 | ++disconnect_count_; 29 | } 30 | void OnAnalysisRequested( 31 | std::unique_ptr event) override { 32 | ++request_count_; 33 | ResultCode ret = event->Send(); 34 | ASSERT_EQ(ResultCode::OK, ret); 35 | } 36 | void OnResponseAcknowledged( 37 | const ContentAnalysisAcknowledgement& ack) override { 38 | ++ack_count_; 39 | } 40 | void OnCancelRequests( 41 | const ContentAnalysisCancelRequests& cancel) override { 42 | ++cancel_count_; 43 | } 44 | 45 | int connect_count_ = 0; 46 | int disconnect_count_ = 0; 47 | int request_count_ = 0; 48 | int ack_count_ = 0; 49 | int cancel_count_ = 0; 50 | BrowserInfo last_info_; 51 | }; 52 | 53 | // A test handler that closes its event before sending the response. 54 | struct CloseEventTestHandler : public TestHandler { 55 | void OnAnalysisRequested( 56 | std::unique_ptr event) override { 57 | ++request_count_; 58 | 59 | // Closing the event before sending should generate an error. 60 | ResultCode ret = event->Close(); 61 | ASSERT_EQ(ResultCode::OK, ret); 62 | 63 | ret = event->Send(); 64 | ASSERT_NE(ResultCode::OK, ret); 65 | } 66 | }; 67 | 68 | // A test handler that attempts to send two responses for a given request. 69 | struct DoubleSendTestHandler : public TestHandler { 70 | void OnAnalysisRequested( 71 | std::unique_ptr event) override { 72 | ++request_count_; 73 | 74 | ResultCode ret = event->Send(); 75 | ASSERT_EQ(ResultCode::OK, ret); 76 | 77 | // Trying to send again fails. 78 | ret = event->Send(); 79 | ASSERT_NE(ResultCode::OK, ret); 80 | } 81 | }; 82 | 83 | // A test handler that signals a latch after a client connects. 84 | // Can only be used with one client. 85 | struct SignalClientConnectedTestHandler : public TestHandler { 86 | void OnBrowserConnected(const BrowserInfo& info) override { 87 | TestHandler::OnBrowserConnected(info); 88 | wait_for_client.count_down(); 89 | } 90 | 91 | std::latch wait_for_client{ 1 }; 92 | }; 93 | 94 | // A test handler that signals a latch after a request is processed. 95 | // Can only be used with one request. 96 | struct SignalClientRequestedTestHandler : public TestHandler { 97 | void OnAnalysisRequested( 98 | std::unique_ptr event) override { 99 | TestHandler::OnAnalysisRequested(std::move(event)); 100 | wait_for_request.count_down(); 101 | } 102 | 103 | std::latch wait_for_request{ 1 }; 104 | }; 105 | 106 | std::unique_ptr CreateAgent( 107 | Agent::Config config, 108 | TestHandler** handler_ptr, 109 | ResultCode expected_rc=ResultCode::OK) { 110 | ResultCode rc; 111 | auto handler = std::make_unique(); 112 | *handler_ptr = handler.get(); 113 | auto agent = std::make_unique( 114 | std::move(config), std::move(handler), &rc); 115 | EXPECT_EQ(expected_rc, rc); 116 | return agent; 117 | } 118 | 119 | std::unique_ptr CreateClient( 120 | Client::Config config) { 121 | int rc; 122 | auto client = std::make_unique(std::move(config), &rc); 123 | return rc == 0 ? std::move(client) : nullptr; 124 | } 125 | 126 | ContentAnalysisRequest BuildRequest(std::string content=std::string()) { 127 | ContentAnalysisRequest request; 128 | request.set_request_token("req-token"); 129 | *request.add_tags() = "dlp"; 130 | request.set_text_content(content); // Moved. 131 | return request; 132 | } 133 | 134 | TEST(AgentTest, Create) { 135 | const Agent::Config config{"test", false}; 136 | TestHandler* handler_ptr; 137 | auto agent = CreateAgent(config, &handler_ptr); 138 | ASSERT_TRUE(agent); 139 | ASSERT_TRUE(handler_ptr); 140 | 141 | ASSERT_EQ(config.name, agent->GetConfig().name); 142 | ASSERT_EQ(config.user_specific, agent->GetConfig().user_specific); 143 | } 144 | 145 | TEST(AgentTest, Create_InvalidPipename) { 146 | // TODO(rogerta): The win32 docs say that a backslash is an invalid 147 | // character for a pipename, but it seemed to work correctly in testing. 148 | // Using an empty name was the easiest way to generate an invalid pipe 149 | // name. 150 | const Agent::Config config{"", false}; 151 | TestHandler* handler_ptr; 152 | auto agent = CreateAgent(config, &handler_ptr, 153 | ResultCode::ERR_INVALID_CHANNEL_NAME); 154 | ASSERT_TRUE(agent); 155 | 156 | ASSERT_EQ(ResultCode::ERR_AGENT_NOT_INITIALIZED, 157 | agent->HandleOneEventForTesting()); 158 | } 159 | 160 | // Can't create two agents with the same name. 161 | TEST(AgentTest, Create_SecondFails) { 162 | const Agent::Config config{ "test", false }; 163 | TestHandler* handler_ptr1; 164 | auto agent1 = CreateAgent(config, &handler_ptr1); 165 | ASSERT_TRUE(agent1); 166 | 167 | TestHandler* handler_ptr2; 168 | auto agent2 = CreateAgent(config, &handler_ptr2, 169 | ResultCode::ERR_AGENT_ALREADY_EXISTS); 170 | ASSERT_TRUE(agent2); 171 | 172 | ASSERT_EQ(ResultCode::ERR_AGENT_NOT_INITIALIZED, 173 | agent2->HandleOneEventForTesting()); 174 | } 175 | 176 | TEST(AgentTest, Stop) { 177 | TestHandler* handler_ptr; 178 | auto agent = CreateAgent({ "test", false }, &handler_ptr); 179 | ASSERT_TRUE(agent); 180 | 181 | // Create a separate thread to stop the agent. 182 | std::thread thread([&agent]() { 183 | agent->Stop(); 184 | }); 185 | 186 | agent->HandleEvents(); 187 | thread.join(); 188 | } 189 | 190 | TEST(AgentTest, ConnectAndStop) { 191 | ResultCode rc; 192 | auto handler = std::make_unique(); 193 | auto* handler_ptr = handler.get(); 194 | auto agent = std::make_unique( 195 | Agent::Config{"test", false}, std::move(handler), &rc); 196 | ASSERT_TRUE(agent); 197 | ASSERT_EQ(ResultCode::OK, rc); 198 | 199 | // Client thread waits until latch reaches zero. 200 | std::latch stop_client{ 1 }; 201 | 202 | // Create a thread to handle the client. Since the client is sync, it can't 203 | // run in the same thread as the agent. 204 | std::thread client_thread([&stop_client]() { 205 | auto client = CreateClient({ "test", false }); 206 | ASSERT_TRUE(client); 207 | stop_client.wait(); 208 | }); 209 | 210 | // A thread that stops the agent after one client connects. 211 | std::thread stop_agent([&handler_ptr, &agent]() { 212 | handler_ptr->wait_for_client.wait(); 213 | agent->Stop(); 214 | }); 215 | 216 | agent->HandleEvents(); 217 | 218 | stop_client.count_down(); 219 | client_thread.join(); 220 | stop_agent.join(); 221 | } 222 | 223 | TEST(AgentTest, Connect_UserSpecific) { 224 | ResultCode rc; 225 | auto handler = std::make_unique(); 226 | auto* handler_ptr = handler.get(); 227 | auto agent = std::make_unique( 228 | Agent::Config{ "test", true }, std::move(handler), &rc); 229 | ASSERT_TRUE(agent); 230 | ASSERT_EQ(ResultCode::OK, rc); 231 | 232 | // Create a thread to handle the client. Since the client is sync, it can't 233 | // run in the same thread as the agent. 234 | std::thread client_thread([]() { 235 | // If the user_specific does not match the agent, the client should not 236 | // connect. 237 | auto client = CreateClient({ "test", false }); 238 | ASSERT_FALSE(client); 239 | 240 | auto client2 = CreateClient({ "test", true }); 241 | ASSERT_TRUE(client2); 242 | }); 243 | 244 | // A thread that stops the agent after one client connects. 245 | std::thread stop_agent([&handler_ptr, &agent]() { 246 | handler_ptr->wait_for_client.wait(); 247 | agent->Stop(); 248 | }); 249 | 250 | agent->HandleEvents(); 251 | 252 | client_thread.join(); 253 | stop_agent.join(); 254 | } 255 | 256 | TEST(AgentTest, ConnectRequestAndStop) { 257 | ResultCode rc; 258 | auto handler = std::make_unique(); 259 | auto* handler_ptr = handler.get(); 260 | auto agent = std::make_unique( 261 | Agent::Config{"test", false}, std::move(handler), &rc); 262 | ASSERT_TRUE(agent); 263 | ASSERT_EQ(ResultCode::OK, rc); 264 | 265 | // Create a thread to handle the client. Since the client is sync, it can't 266 | // run in the same thread as the agent. 267 | std::thread client_thread([]() { 268 | auto client = CreateClient({ "test", false }); 269 | ASSERT_TRUE(client); 270 | 271 | ContentAnalysisRequest request = BuildRequest("test"); 272 | ContentAnalysisResponse response; 273 | client->Send(request, &response); 274 | }); 275 | 276 | // A thread that stops the agent after one client connects. 277 | std::thread stop_agent([&handler_ptr, &agent]() { 278 | handler_ptr->wait_for_request.wait(); 279 | agent->Stop(); 280 | }); 281 | 282 | agent->HandleEvents(); 283 | 284 | client_thread.join(); 285 | stop_agent.join(); 286 | } 287 | 288 | TEST(AgentTest, ConnectAndClose) { 289 | const Agent::Config aconfig{ "test", false }; 290 | const Client::Config cconfig{ "test", false }; 291 | 292 | // Create an agent and client that connects to it. 293 | TestHandler* handler_ptr; 294 | auto agent = CreateAgent(aconfig, &handler_ptr); 295 | ASSERT_TRUE(agent); 296 | auto client = CreateClient(cconfig); 297 | ASSERT_TRUE(client); 298 | ASSERT_EQ(cconfig.name, client->GetConfig().name); 299 | ASSERT_EQ(cconfig.user_specific, client->GetConfig().user_specific); 300 | 301 | agent->HandleOneEventForTesting(); 302 | ASSERT_EQ(1, handler_ptr->connect_count_); 303 | ASSERT_EQ(0, handler_ptr->disconnect_count_); 304 | ASSERT_EQ(0, handler_ptr->cancel_count_); 305 | ASSERT_EQ(GetCurrentProcessId(), handler_ptr->last_info_.pid); 306 | 307 | // Close the client and make sure a disconnect is received. 308 | client.reset(); 309 | agent->HandleOneEventForTesting(); 310 | ASSERT_EQ(1, handler_ptr->connect_count_); 311 | ASSERT_EQ(1, handler_ptr->disconnect_count_); 312 | ASSERT_EQ(0, handler_ptr->cancel_count_); 313 | ASSERT_EQ(GetCurrentProcessId(), handler_ptr->last_info_.pid); 314 | } 315 | 316 | TEST(AgentTest, Request) { 317 | TestHandler* handler_ptr; 318 | auto agent = CreateAgent({"test", false}, &handler_ptr); 319 | ASSERT_TRUE(agent); 320 | 321 | bool done = false; 322 | 323 | // Create a thread to handle the client. Since the client is sync, it can't 324 | // run in the same thread as the agent. 325 | std::thread client_thread([&done]() { 326 | auto client = CreateClient({"test", false}); 327 | ASSERT_TRUE(client); 328 | 329 | // Send a request and wait for a response. 330 | ContentAnalysisRequest request = BuildRequest(); 331 | ContentAnalysisResponse response; 332 | int ret = client->Send(request, &response); 333 | ASSERT_EQ(0, ret); 334 | ASSERT_EQ(request.request_token(), response.request_token()); 335 | 336 | done = true; 337 | }); 338 | 339 | while (!done) { 340 | agent->HandleOneEventForTesting(); 341 | } 342 | ASSERT_EQ(1, handler_ptr->request_count_); 343 | ASSERT_EQ(0, handler_ptr->ack_count_); 344 | ASSERT_EQ(0, handler_ptr->cancel_count_); 345 | 346 | client_thread.join(); 347 | } 348 | 349 | TEST(AgentTest, Request_Large) { 350 | TestHandler* handler_ptr; 351 | auto agent = CreateAgent({"test", false}, &handler_ptr); 352 | ASSERT_TRUE(agent); 353 | 354 | bool done = false; 355 | 356 | // Create a thread to handle the client. Since the client is sync, it can't 357 | // run in the same thread as the agent. 358 | std::thread client_thread([&done]() { 359 | auto client = CreateClient({"test", false}); 360 | ASSERT_TRUE(client); 361 | 362 | // Send a request and wait for a response. Create a large string, which 363 | // means larger than the initial mesasge buffer size specified when 364 | // creating the pipes (4096 bytes). 365 | ContentAnalysisRequest request = BuildRequest(std::string(5000, 'a')); 366 | ContentAnalysisResponse response; 367 | int ret = client->Send(request, &response); 368 | ASSERT_EQ(0, ret); 369 | ASSERT_EQ(request.request_token(), response.request_token()); 370 | 371 | done = true; 372 | }); 373 | 374 | while (!done) { 375 | agent->HandleOneEventForTesting(); 376 | } 377 | ASSERT_EQ(1, handler_ptr->request_count_); 378 | ASSERT_EQ(0, handler_ptr->ack_count_); 379 | ASSERT_EQ(0, handler_ptr->cancel_count_); 380 | 381 | client_thread.join(); 382 | } 383 | 384 | TEST(AgentTest, Request_DoubleSend) { 385 | ResultCode rc; 386 | auto handler = std::make_unique(); 387 | DoubleSendTestHandler* handler_ptr = handler.get(); 388 | auto agent = std::make_unique( 389 | Agent::Config{"test", false}, std::move(handler), &rc); 390 | ASSERT_TRUE(agent); 391 | ASSERT_EQ(ResultCode::OK, rc); 392 | 393 | bool done = false; 394 | 395 | // Create a thread to handle the client. Since the client is sync, it can't 396 | // run in the same thread as the agent. 397 | std::thread client_thread([&done]() { 398 | auto client = CreateClient({ "test", false }); 399 | ASSERT_TRUE(client); 400 | 401 | // Send a request and wait for a response. 402 | ContentAnalysisRequest request = BuildRequest(); 403 | ContentAnalysisResponse response; 404 | int ret = client->Send(request, &response); 405 | ASSERT_EQ(0, ret); 406 | ASSERT_EQ(request.request_token(), response.request_token()); 407 | 408 | done = true; 409 | }); 410 | 411 | while (!done) { 412 | agent->HandleOneEventForTesting(); 413 | } 414 | ASSERT_EQ(1, handler_ptr->request_count_); 415 | ASSERT_EQ(0, handler_ptr->ack_count_); 416 | ASSERT_EQ(0, handler_ptr->cancel_count_); 417 | 418 | client_thread.join(); 419 | } 420 | 421 | TEST(AgentTest, Request_CloseEvent) { 422 | ResultCode rc; 423 | auto handler = std::make_unique(); 424 | CloseEventTestHandler* handler_ptr = handler.get(); 425 | auto agent = std::make_unique( 426 | Agent::Config{"test", false}, std::move(handler), &rc); 427 | ASSERT_TRUE(agent); 428 | ASSERT_EQ(ResultCode::OK, rc); 429 | 430 | bool done = false; 431 | 432 | // Create a thread to handle the client. Since the client is sync, it can't 433 | // run in the same thread as the agent. 434 | std::thread client_thread([&done]() { 435 | auto client = CreateClient({"test", false}); 436 | ASSERT_TRUE(client); 437 | 438 | // Send a request and wait for a response. 439 | ContentAnalysisRequest request = BuildRequest(); 440 | ContentAnalysisResponse response; 441 | int ret = client->Send(request, &response); 442 | ASSERT_EQ(0, ret); 443 | ASSERT_EQ(request.request_token(), response.request_token()); 444 | 445 | done = true; 446 | }); 447 | 448 | while (!done) { 449 | agent->HandleOneEventForTesting(); 450 | } 451 | ASSERT_EQ(1, handler_ptr->request_count_); 452 | 453 | client_thread.join(); 454 | } 455 | 456 | TEST(AgentTest, Ack) { 457 | TestHandler* handler_ptr; 458 | auto agent = CreateAgent({ "test", false }, &handler_ptr); 459 | ASSERT_TRUE(agent); 460 | 461 | bool done = false; 462 | 463 | // Create a thread to handle the client. Since the client is sync, it can't 464 | // run in the same thread as the agent. 465 | std::thread client_thread([&done]() { 466 | auto client = CreateClient({"test", false}); 467 | ASSERT_TRUE(client); 468 | 469 | // Send a request and wait for a response. 470 | ContentAnalysisRequest request = BuildRequest(); 471 | ContentAnalysisResponse response; 472 | int ret = client->Send(request, &response); 473 | ASSERT_EQ(0, ret); 474 | 475 | ContentAnalysisAcknowledgement ack; 476 | ack.set_request_token(request.request_token()); 477 | ret = client->Acknowledge(ack); 478 | ASSERT_EQ(0, ret); 479 | 480 | done = true; 481 | }); 482 | 483 | while (!done) { 484 | agent->HandleOneEventForTesting(); 485 | } 486 | ASSERT_EQ(1, handler_ptr->request_count_); 487 | ASSERT_EQ(1, handler_ptr->ack_count_); 488 | ASSERT_EQ(0, handler_ptr->cancel_count_); 489 | 490 | client_thread.join(); 491 | } 492 | 493 | TEST(AgentTest, Cancel) { 494 | TestHandler* handler_ptr; 495 | auto agent = CreateAgent({ "test", false }, &handler_ptr); 496 | ASSERT_TRUE(agent); 497 | 498 | // Create a thread to handle the client. Since the client is sync, it can't 499 | // run in the same thread as the agent. 500 | std::thread client_thread([]() { 501 | auto client = CreateClient({"test", false}); 502 | ASSERT_TRUE(client); 503 | 504 | ContentAnalysisCancelRequests cancel; 505 | cancel.set_user_action_id("1234567890"); 506 | int ret = client->CancelRequests(cancel); 507 | ASSERT_EQ(0, ret); 508 | }); 509 | 510 | while (handler_ptr->cancel_count_ == 0) { 511 | agent->HandleOneEventForTesting(); 512 | } 513 | ASSERT_EQ(0, handler_ptr->request_count_); 514 | ASSERT_EQ(0, handler_ptr->ack_count_); 515 | 516 | client_thread.join(); 517 | } 518 | 519 | } // namespace testing 520 | } // namespace sdk 521 | } // namespace content_analysis 522 | 523 | -------------------------------------------------------------------------------- /demo/handler.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef CONTENT_ANALYSIS_DEMO_HANDLER_H_ 6 | #define CONTENT_ANALYSIS_DEMO_HANDLER_H_ 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "content_analysis/sdk/analysis_agent.h" 19 | #include "demo/atomic_output.h" 20 | #include "demo/request_queue.h" 21 | 22 | // An AgentEventHandler that dumps requests information to stdout and blocks 23 | // any requests that have the keyword "block" in their data 24 | class Handler : public content_analysis::sdk::AgentEventHandler { 25 | public: 26 | using Event = content_analysis::sdk::ContentAnalysisEvent; 27 | 28 | Handler(unsigned long delay, const std::string& print_data_file_path) : 29 | delay_(delay), print_data_file_path_(print_data_file_path) { 30 | } 31 | 32 | unsigned long delay() { return delay_; } 33 | 34 | protected: 35 | // Analyzes one request from Google Chrome and responds back to the browser 36 | // with either an allow or block verdict. 37 | void AnalyzeContent(AtomicCout& aout, std::unique_ptr event) { 38 | // An event represents one content analysis request and response triggered 39 | // by a user action in Google Chrome. The agent determines whether the 40 | // user is allowed to perform the action by examining event->GetRequest(). 41 | // The verdict, which can be "allow" or "block" is written into 42 | // event->GetResponse(). 43 | 44 | DumpEvent(aout.stream(), event.get()); 45 | 46 | bool block = false; 47 | bool success = true; 48 | unsigned long delay = delay_; 49 | 50 | if (event->GetRequest().has_text_content()) { 51 | block = ShouldBlockRequest( 52 | event->GetRequest().text_content()); 53 | GetFileSpecificDelay(event->GetRequest().text_content(), &delay); 54 | } else if (event->GetRequest().has_file_path()) { 55 | std::string content; 56 | success = 57 | ReadContentFromFile(event->GetRequest().file_path(), 58 | &content); 59 | if (success) { 60 | block = ShouldBlockRequest(content); 61 | GetFileSpecificDelay(content, &delay); 62 | } 63 | } else if (event->GetRequest().has_print_data()) { 64 | // In the case of print request, normally the PDF bytes would be parsed 65 | // for sensitive data violations. To keep this class simple, only the 66 | // URL is checked for the word "block". 67 | block = ShouldBlockRequest(event->GetRequest().request_data().url()); 68 | GetFileSpecificDelay(event->GetRequest().request_data().url(), &delay); 69 | } 70 | 71 | if (!success) { 72 | content_analysis::sdk::UpdateResponse( 73 | event->GetResponse(), 74 | std::string(), 75 | content_analysis::sdk::ContentAnalysisResponse::Result::FAILURE); 76 | aout.stream() << " Verdict: failed to reach verdict: "; 77 | aout.stream() << event->DebugString() << std::endl; 78 | } else if (block) { 79 | auto rc = content_analysis::sdk::SetEventVerdictToBlock(event.get()); 80 | aout.stream() << " Verdict: block"; 81 | if (rc != content_analysis::sdk::ResultCode::OK) { 82 | aout.stream() << " error: " 83 | << content_analysis::sdk::ResultCodeToString(rc) << std::endl; 84 | aout.stream() << " " << event->DebugString() << std::endl; 85 | } 86 | aout.stream() << std::endl; 87 | } else { 88 | aout.stream() << " Verdict: allow" << std::endl; 89 | } 90 | 91 | aout.stream() << std::endl; 92 | 93 | // If a delay is specified, wait that much. 94 | if (delay > 0) { 95 | aout.stream() << "Delaying response to " << event->GetRequest().request_token() 96 | << " for " << delay << "s" << std::endl<< std::endl; 97 | aout.flush(); 98 | std::this_thread::sleep_for(std::chrono::seconds(delay)); 99 | } 100 | 101 | // Send the response back to Google Chrome. 102 | auto rc = event->Send(); 103 | if (rc != content_analysis::sdk::ResultCode::OK) { 104 | aout.stream() << "[Demo] Error sending response: " 105 | << content_analysis::sdk::ResultCodeToString(rc) 106 | << std::endl; 107 | aout.stream() << event->DebugString() << std::endl; 108 | } 109 | } 110 | 111 | private: 112 | void OnBrowserConnected( 113 | const content_analysis::sdk::BrowserInfo& info) override { 114 | AtomicCout aout; 115 | aout.stream() << std::endl << "==========" << std::endl; 116 | aout.stream() << "Browser connected pid=" << info.pid 117 | << " path=" << info.binary_path << std::endl; 118 | } 119 | 120 | void OnBrowserDisconnected( 121 | const content_analysis::sdk::BrowserInfo& info) override { 122 | AtomicCout aout; 123 | aout.stream() << std::endl << "Browser disconnected pid=" << info.pid << std::endl; 124 | aout.stream() << "==========" << std::endl; 125 | } 126 | 127 | void OnAnalysisRequested(std::unique_ptr event) override { 128 | // If the agent is capable of analyzing content in the background, the 129 | // events may be handled in background threads. Having said that, a 130 | // event should not be assumed to be thread safe, that is, it should not 131 | // be accessed by more than one thread concurrently. 132 | // 133 | // In this example code, the event is handled synchronously. 134 | AtomicCout aout; 135 | aout.stream() << std::endl << "----------" << std::endl << std::endl; 136 | AnalyzeContent(aout, std::move(event)); 137 | } 138 | 139 | void OnResponseAcknowledged( 140 | const content_analysis::sdk::ContentAnalysisAcknowledgement& 141 | ack) override { 142 | const char* final_action = ""; 143 | if (ack.has_final_action()) { 144 | switch (ack.final_action()) { 145 | case content_analysis::sdk::ContentAnalysisAcknowledgement::ACTION_UNSPECIFIED: 146 | final_action = ""; 147 | break; 148 | case content_analysis::sdk::ContentAnalysisAcknowledgement::ALLOW: 149 | final_action = "Allow"; 150 | break; 151 | case content_analysis::sdk::ContentAnalysisAcknowledgement::REPORT_ONLY: 152 | final_action = "Report only"; 153 | break; 154 | case content_analysis::sdk::ContentAnalysisAcknowledgement::WARN: 155 | final_action = "Warn"; 156 | break; 157 | case content_analysis::sdk::ContentAnalysisAcknowledgement::BLOCK: 158 | final_action = "Block"; 159 | break; 160 | } 161 | } 162 | 163 | AtomicCout aout; 164 | aout.stream() << " Ack: " << ack.request_token() << std::endl; 165 | aout.stream() << " Final action: " << final_action << std::endl; 166 | } 167 | void OnCancelRequests( 168 | const content_analysis::sdk::ContentAnalysisCancelRequests& cancel) 169 | override { 170 | AtomicCout aout; 171 | aout.stream() << "Cancel: " << std::endl; 172 | aout.stream() << " User action ID: " << cancel.user_action_id() << std::endl; 173 | } 174 | 175 | void OnInternalError( 176 | const char* context, 177 | content_analysis::sdk::ResultCode error) override { 178 | AtomicCout aout; 179 | aout.stream() << std::endl 180 | << "*ERROR*: context=\"" << context << "\" " 181 | << content_analysis::sdk::ResultCodeToString(error) 182 | << std::endl; 183 | } 184 | 185 | void DumpEvent(std::stringstream& stream, Event* event) { 186 | time_t now = time(nullptr); 187 | stream << "Received at: " << ctime(&now); // Includes \n. 188 | stream << "Received from: pid=" << event->GetBrowserInfo().pid 189 | << " path=" << event->GetBrowserInfo().binary_path << std::endl; 190 | 191 | const content_analysis::sdk::ContentAnalysisRequest& request = 192 | event->GetRequest(); 193 | std::string connector = ""; 194 | if (request.has_analysis_connector()) { 195 | switch (request.analysis_connector()) { 196 | case content_analysis::sdk::FILE_DOWNLOADED: 197 | connector = "download"; 198 | break; 199 | case content_analysis::sdk::FILE_ATTACHED: 200 | connector = "attach"; 201 | break; 202 | case content_analysis::sdk::BULK_DATA_ENTRY: 203 | connector = "bulk-data-entry"; 204 | break; 205 | case content_analysis::sdk::PRINT: 206 | connector = "print"; 207 | break; 208 | case content_analysis::sdk::FILE_TRANSFER: 209 | connector = "file-transfer"; 210 | break; 211 | default: 212 | break; 213 | } 214 | } 215 | std::string reason; 216 | if (request.has_reason()) { 217 | using content_analysis::sdk::ContentAnalysisRequest; 218 | switch (request.reason()) { 219 | case content_analysis::sdk::ContentAnalysisRequest::UNKNOWN: 220 | reason = ""; 221 | break; 222 | case content_analysis::sdk::ContentAnalysisRequest::CLIPBOARD_PASTE: 223 | reason = "CLIPBOARD_PASTE"; 224 | break; 225 | case content_analysis::sdk::ContentAnalysisRequest::DRAG_AND_DROP: 226 | reason = "DRAG_AND_DROP"; 227 | break; 228 | case content_analysis::sdk::ContentAnalysisRequest::FILE_PICKER_DIALOG: 229 | reason = "FILE_PICKER_DIALOG"; 230 | break; 231 | case content_analysis::sdk::ContentAnalysisRequest::PRINT_PREVIEW_PRINT: 232 | reason = "PRINT_PREVIEW_PRINT"; 233 | break; 234 | case content_analysis::sdk::ContentAnalysisRequest::SYSTEM_DIALOG_PRINT: 235 | reason = "SYSTEM_DIALOG_PRINT"; 236 | break; 237 | case content_analysis::sdk::ContentAnalysisRequest::NORMAL_DOWNLOAD: 238 | reason = "NORMAL_DOWNLOAD"; 239 | break; 240 | case content_analysis::sdk::ContentAnalysisRequest::SAVE_AS_DOWNLOAD: 241 | reason = "SAVE_AS_DOWNLOAD"; 242 | break; 243 | } 244 | } 245 | 246 | std::string url = 247 | request.has_request_data() && request.request_data().has_url() 248 | ? request.request_data().url() : ""; 249 | 250 | std::string tab_title = 251 | request.has_request_data() && request.request_data().has_tab_title() 252 | ? request.request_data().tab_title() : ""; 253 | 254 | std::string filename = 255 | request.has_request_data() && request.request_data().has_filename() 256 | ? request.request_data().filename() : ""; 257 | 258 | std::string digest = 259 | request.has_request_data() && request.request_data().has_digest() 260 | ? request.request_data().digest() : ""; 261 | 262 | std::string file_path = 263 | request.has_file_path() 264 | ? request.file_path() : "None, bulk text entry or print"; 265 | 266 | std::string machine_user = 267 | request.has_client_metadata() && 268 | request.client_metadata().has_browser() && 269 | request.client_metadata().browser().has_machine_user() 270 | ? request.client_metadata().browser().machine_user() : ""; 271 | 272 | std::string email = 273 | request.has_request_data() && request.request_data().has_email() 274 | ? request.request_data().email() : ""; 275 | 276 | time_t t = request.expires_at(); 277 | std::string expires_at_str = ctime(&t); 278 | // Returned string includes trailing \n, overwrite with null. 279 | expires_at_str[expires_at_str.size() - 1] = 0; 280 | time_t secs_remaining = t - now; 281 | 282 | std::string user_action_id = request.has_user_action_id() 283 | ? request.user_action_id() : ""; 284 | 285 | stream << "Request: " << request.request_token() << std::endl; 286 | stream << " User action ID: " << user_action_id << std::endl; 287 | stream << " Expires at: " << expires_at_str << " (" 288 | << secs_remaining << " seconds from now)" << std::endl; 289 | stream << " Connector: " << connector << std::endl; 290 | if (!reason.empty()) { 291 | stream << " Reason: " << reason << std::endl; 292 | } 293 | stream << " URL: " << url << std::endl; 294 | stream << " Tab title: " << tab_title << std::endl; 295 | stream << " Filename: " << filename << std::endl; 296 | stream << " Digest: " << digest << std::endl; 297 | stream << " Filepath: " << file_path << std::endl; 298 | stream << " Machine user: " << machine_user << std::endl; 299 | stream << " Email: " << email << std::endl; 300 | 301 | if (request.has_text_content() && !request.text_content().empty()) { 302 | std::string prefix = " Pasted data: "; 303 | std::string text_content = request.text_content(); 304 | 305 | // Truncate the text past 50 bytes to keep it to a reasonable length in 306 | // the terminal window. 307 | if (text_content.size() > 50) { 308 | prefix = " Pasted data (truncated): "; 309 | text_content = text_content.substr(0, 50) + "..."; 310 | } 311 | stream << prefix 312 | << text_content 313 | << std::endl; 314 | stream << " Pasted data size (bytes): " 315 | << request.text_content().size() 316 | << std::endl; 317 | } 318 | 319 | if (request.has_print_data() && !print_data_file_path_.empty()) { 320 | if (request.request_data().has_print_metadata() && 321 | request.request_data().print_metadata().has_printer_name()) { 322 | stream << " Printer name: " 323 | << request.request_data().print_metadata().printer_name() 324 | << std::endl; 325 | } else { 326 | stream << " No printer name in request" << std::endl; 327 | } 328 | 329 | stream << " Print data saved to: " << print_data_file_path_ 330 | << std::endl; 331 | using content_analysis::sdk::ContentAnalysisEvent; 332 | auto print_data = 333 | content_analysis::sdk::CreateScopedPrintHandle(event->GetRequest(), 334 | event->GetBrowserInfo().pid); 335 | std::ofstream file(print_data_file_path_, 336 | std::ios::out | std::ios::trunc | std::ios::binary); 337 | file.write(print_data->data(), print_data->size()); 338 | file.flush(); 339 | file.close(); 340 | } 341 | } 342 | 343 | bool ReadContentFromFile(const std::string& file_path, 344 | std::string* content) { 345 | std::ifstream file(file_path, 346 | std::ios::in | std::ios::binary | std::ios::ate); 347 | if (!file.is_open()) 348 | return false; 349 | 350 | // Get file size. This example does not handle files larger than 1MB. 351 | // Make sure content string can hold the contents of the file. 352 | int size = file.tellg(); 353 | if (size > 1024 * 1024) 354 | return false; 355 | 356 | content->resize(size + 1); 357 | 358 | // Read file into string. 359 | file.seekg(0, std::ios::beg); 360 | file.read(&(*content)[0], size); 361 | content->at(size) = 0; 362 | return true; 363 | } 364 | 365 | bool ShouldBlockRequest(const std::string& content) { 366 | // Determines if the request should be blocked. For this simple example 367 | // the content is blocked if the string "block" is found. Otherwise the 368 | // content is allowed. 369 | return content.find("block") != std::string::npos; 370 | } 371 | 372 | void GetFileSpecificDelay(const std::string& content, unsigned long* delay) { 373 | auto pos = content.find("delay="); 374 | if (pos != std::string::npos) { 375 | std::sscanf(content.substr(pos).c_str(), "delay=%lu", delay); 376 | } 377 | } 378 | 379 | unsigned long delay_; 380 | std::string print_data_file_path_; 381 | }; 382 | 383 | // An AgentEventHandler that dumps requests information to stdout and blocks 384 | // any requests that have the keyword "block" in their data 385 | class QueuingHandler : public Handler { 386 | public: 387 | QueuingHandler(unsigned long threads, unsigned long delay, const std::string& print_data_file_path) 388 | : Handler(delay, print_data_file_path) { 389 | StartBackgroundThreads(threads); 390 | } 391 | 392 | ~QueuingHandler() override { 393 | // Abort background process and wait for it to finish. 394 | request_queue_.abort(); 395 | WaitForBackgroundThread(); 396 | } 397 | 398 | private: 399 | void OnAnalysisRequested(std::unique_ptr event) override { 400 | { 401 | time_t now = time(nullptr); 402 | const content_analysis::sdk::ContentAnalysisRequest& request = 403 | event->GetRequest(); 404 | AtomicCout aout; 405 | aout.stream() << std::endl << "Queuing request: " << request.request_token() 406 | << " at " << ctime(&now) << std::endl; 407 | } 408 | 409 | request_queue_.push(std::move(event)); 410 | } 411 | 412 | static void* ProcessRequests(void* qh) { 413 | QueuingHandler* handler = reinterpret_cast(qh); 414 | 415 | while (true) { 416 | auto event = handler->request_queue_.pop(); 417 | if (!event) 418 | break; 419 | 420 | AtomicCout aout; 421 | aout.stream() << std::endl << "----------" << std::endl; 422 | aout.stream() << "Thread: " << std::this_thread::get_id() 423 | << std::endl; 424 | aout.flush(); 425 | 426 | handler->AnalyzeContent(aout, std::move(event)); 427 | } 428 | 429 | return 0; 430 | } 431 | 432 | // A list of outstanding content analysis requests. 433 | RequestQueue request_queue_; 434 | 435 | void StartBackgroundThreads(unsigned long threads) { 436 | threads_.reserve(threads); 437 | for (unsigned long i = 0; i < threads; ++i) { 438 | threads_.emplace_back(std::make_unique(ProcessRequests, this)); 439 | } 440 | } 441 | 442 | void WaitForBackgroundThread() { 443 | for (auto& thread : threads_) { 444 | thread->join(); 445 | } 446 | } 447 | 448 | // Thread id of backgrond thread. 449 | std::vector> threads_; 450 | }; 451 | 452 | #endif // CONTENT_ANALYSIS_DEMO_HANDLER_H_ 453 | -------------------------------------------------------------------------------- /agent/src/agent_win.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "common/utils_win.h" 13 | 14 | #include "agent_utils_win.h" 15 | #include "agent_win.h" 16 | #include "event_win.h" 17 | 18 | namespace content_analysis { 19 | namespace sdk { 20 | 21 | // The minimum number of pipe in listening mode. This is greater than one to 22 | // handle the case of multiple instance of Google Chrome browser starting 23 | // at the same time. 24 | const DWORD kMinNumListeningPipeInstances = 2; 25 | 26 | // The minimum number of handles to wait on. This is the minimum number 27 | // of pipes in listening mode plus the stop event. 28 | const DWORD kMinNumWaitHandles = kMinNumListeningPipeInstances + 1; 29 | 30 | // static 31 | std::unique_ptr Agent::Create( 32 | Config config, 33 | std::unique_ptr handler, 34 | ResultCode* rc) { 35 | auto agent = std::make_unique(std::move(config), std::move(handler), rc); 36 | return *rc == ResultCode::OK ? std::move(agent) : nullptr; 37 | } 38 | 39 | AgentWin::Connection::Connection(const std::string& pipename, 40 | bool user_specific, 41 | AgentEventHandler* handler, 42 | bool is_first_pipe, 43 | ResultCode* rc) 44 | : handler_(handler) { 45 | *rc = ResultCode::OK; 46 | memset(&overlapped_, 0, sizeof(overlapped_)); 47 | // Create a manual reset event as specified for overlapped IO. 48 | // Use default security attriutes and no name since this event is not 49 | // shared with other processes. 50 | overlapped_.hEvent = CreateEvent(/*securityAttr=*/nullptr, 51 | /*manualReset=*/TRUE, 52 | /*initialState=*/FALSE, 53 | /*name=*/nullptr); 54 | if (!overlapped_.hEvent) { 55 | *rc = ResultCode::ERR_CANNOT_CREATE_CHANNEL_IO_EVENT; 56 | return; 57 | } 58 | 59 | *rc = ResetInternal(pipename, user_specific, is_first_pipe); 60 | } 61 | 62 | AgentWin::Connection::~Connection() { 63 | Cleanup(); 64 | 65 | if (handle_ != INVALID_HANDLE_VALUE) { 66 | CloseHandle(handle_); 67 | } 68 | 69 | // Invalid event handles are represented as null. 70 | if (overlapped_.hEvent) { 71 | CloseHandle(overlapped_.hEvent); 72 | } 73 | } 74 | 75 | ResultCode AgentWin::Connection::Reset( 76 | const std::string& pipename, 77 | bool user_specific) { 78 | return NotifyIfError("ConnectionReset", 79 | ResetInternal(pipename, user_specific, false)); 80 | } 81 | 82 | ResultCode AgentWin::Connection::HandleEvent(HANDLE handle) { 83 | auto rc = ResultCode::OK; 84 | DWORD count; 85 | BOOL success = GetOverlappedResult(handle, &overlapped_, &count, 86 | /*wait=*/FALSE); 87 | if (!is_connected_) { 88 | // This connection is currently listing for a new connection from a Google 89 | // Chrome browser. If the result is a success, this means the browser has 90 | // connected as expected. Otherwise an error occured so report it to the 91 | // caller. 92 | if (success) { 93 | // A Google Chrome browser connected to the agent. Reset this 94 | // connection object to handle communication with the browser and then 95 | // tell the handler about it. 96 | 97 | is_connected_ = true; 98 | buffer_.resize(internal::kBufferSize); 99 | 100 | rc = BuildBrowserInfo(); 101 | if (rc == ResultCode::OK) { 102 | handler_->OnBrowserConnected(browser_info_); 103 | } 104 | } else { 105 | rc = ErrorToResultCode(GetLastError()); 106 | NotifyIfError("GetOverlappedResult", rc); 107 | } 108 | } else { 109 | // Some data has arrived from Google Chrome. This data is (part of) an 110 | // instance of the proto message `ChromeToAgent`. 111 | // 112 | // If the message is small it is received in by one call to ReadFile(). 113 | // If the message is larger it is received in by multiple calls to 114 | // ReadFile(). 115 | // 116 | // `success` is true if the data just read is the last bytes for a message. 117 | // Otherwise it is false. 118 | rc = OnReadFile(success, count); 119 | } 120 | 121 | // If all data has been read, queue another read. 122 | if (rc == ResultCode::OK || rc == ResultCode::ERR_MORE_DATA) { 123 | rc = QueueReadFile(rc == ResultCode::OK); 124 | } 125 | 126 | if (rc != ResultCode::OK && rc != ResultCode::ERR_IO_PENDING && 127 | rc != ResultCode::ERR_MORE_DATA) { 128 | Cleanup(); 129 | } else { 130 | // Don't propagate all the "success" error codes to the called to keep 131 | // this simpler. 132 | rc = ResultCode::OK; 133 | } 134 | 135 | return rc; 136 | } 137 | 138 | void AgentWin::Connection::AppendDebugString(std::stringstream& state) const { 139 | state << "{handle=" << handle_; 140 | state << " connected=" << is_connected_; 141 | state << " pid=" << browser_info_.pid; 142 | state << " rsize=" << read_size_; 143 | state << " fsize=" << final_size_; 144 | state << "}"; 145 | } 146 | 147 | ResultCode AgentWin::Connection::ConnectPipe() { 148 | // In overlapped mode, connecting to a named pipe always returns false. 149 | if (ConnectNamedPipe(handle_, &overlapped_)) { 150 | return ErrorToResultCode(GetLastError()); 151 | } 152 | 153 | DWORD err = GetLastError(); 154 | if (err == ERROR_IO_PENDING) { 155 | // Waiting for a Google Chrome Browser to connect. 156 | return ResultCode::OK; 157 | } else if (err == ERROR_PIPE_CONNECTED) { 158 | // A Google Chrome browser is already connected. Make sure event is in 159 | // signaled state in order to process the connection. 160 | if (SetEvent(overlapped_.hEvent)) { 161 | err = ERROR_SUCCESS; 162 | } else { 163 | err = GetLastError(); 164 | } 165 | } 166 | 167 | return ErrorToResultCode(err); 168 | } 169 | 170 | ResultCode AgentWin::Connection::ResetInternal(const std::string& pipename, 171 | bool user_specific, 172 | bool is_first_pipe) { 173 | auto rc = ResultCode::OK; 174 | 175 | // If this is the not the first time, disconnect from any existing Google 176 | // Chrome browser. Otherwise creater a new pipe. 177 | if (handle_ != INVALID_HANDLE_VALUE) { 178 | if (!DisconnectNamedPipe(handle_)) { 179 | rc = ErrorToResultCode(GetLastError()); 180 | } 181 | } else { 182 | rc = ErrorToResultCode( 183 | internal::CreatePipe(pipename, user_specific, is_first_pipe, &handle_)); 184 | } 185 | 186 | // Make sure event starts in reset state. 187 | if (rc == ResultCode::OK && !ResetEvent(overlapped_.hEvent)) { 188 | rc = ErrorToResultCode(GetLastError()); 189 | } 190 | 191 | if (rc == ResultCode::OK) { 192 | rc = ConnectPipe(); 193 | } 194 | 195 | if (rc != ResultCode::OK) { 196 | Cleanup(); 197 | handle_ = INVALID_HANDLE_VALUE; 198 | } 199 | 200 | return rc; 201 | } 202 | 203 | void AgentWin::Connection::Cleanup() { 204 | if (is_connected_ && handler_) { 205 | handler_->OnBrowserDisconnected(browser_info_); 206 | } 207 | 208 | is_connected_ = false; 209 | browser_info_ = BrowserInfo(); 210 | buffer_.clear(); 211 | cursor_ = nullptr; 212 | read_size_ = 0; 213 | final_size_ = 0; 214 | 215 | if (handle_ != INVALID_HANDLE_VALUE) { 216 | // Cancel all outstanding IO requests on this pipe by using a null for 217 | // overlapped. 218 | CancelIoEx(handle_, /*overlapped=*/nullptr); 219 | } 220 | 221 | // This function does not close `handle_` or the event in `overlapped` so 222 | // that the server can resuse the pipe with an new Google Chrome browser 223 | // instance. 224 | } 225 | 226 | ResultCode AgentWin::Connection::QueueReadFile(bool reset_cursor) { 227 | if (reset_cursor) { 228 | cursor_ = buffer_.data(); 229 | read_size_ = buffer_.size(); 230 | final_size_ = 0; 231 | } 232 | 233 | // When this function is called there are the following possiblities: 234 | // 235 | // 1/ Data is already available and the buffer is filled in. ReadFile() 236 | // return TRUE and the event is set. 237 | // 2/ Data is not avaiable yet. ReadFile() returns FALSE and the last error 238 | // is ERROR_IO_PENDING(997) and the event is reset. 239 | // 3/ Some error occurred, like for example Google Chrome stops. ReadFile() 240 | // returns FALSE and the last error is something other than 241 | // ERROR_IO_PENDING, for example ERROR_BROKEN_PIPE(109). The event 242 | // state is unchanged. 243 | auto rc = ResultCode::OK; 244 | DWORD count; 245 | if (!ReadFile(handle_, cursor_, read_size_, &count, &overlapped_)) { 246 | DWORD err = GetLastError(); 247 | rc = ErrorToResultCode(err); 248 | 249 | // IO pending is not an error so don't notify. 250 | // 251 | // Ignore broken pipes for notifications since that happens when the Google 252 | // Chrome browser shuts down. The agent will be notified of a browser 253 | // disconnect in that case. 254 | // 255 | // More data means that `buffer_` was too small to read the entire message 256 | // from the browser. The buffer has already been resized. Another call to 257 | // ReadFile() is needed to get the remainder. 258 | if (rc != ResultCode::ERR_IO_PENDING && 259 | rc != ResultCode::ERR_BROKEN_PIPE && 260 | rc != ResultCode::ERR_MORE_DATA) { 261 | NotifyIfError("QueueReadFile", rc, err); 262 | } 263 | } 264 | 265 | return rc; 266 | } 267 | 268 | ResultCode AgentWin::Connection::OnReadFile(BOOL done_reading, DWORD count) { 269 | final_size_ += count; 270 | 271 | // If `done_reading` is TRUE, this means the full message has been read. 272 | // Call the appropriate handler method. 273 | if (done_reading) { 274 | return CallHandler(); 275 | } 276 | 277 | // Otherwise there are two possibilities: 278 | // 279 | // 1/ The last error is ERROR_MORE_DATA(234). This means there are more 280 | // bytes to read before the request message is complete. Resize the 281 | // buffer and adjust the cursor. The caller will queue up another read 282 | // and wait. don't notify the handler since this is not an error. 283 | // 2/ Some error occured. In this case notify the handler and return the 284 | // error. 285 | 286 | DWORD err = GetLastError(); 287 | if (err == ERROR_MORE_DATA) { 288 | read_size_ = internal::kBufferSize; 289 | buffer_.resize(buffer_.size() + read_size_); 290 | cursor_ = buffer_.data() + buffer_.size() - read_size_; 291 | return ErrorToResultCode(err); 292 | } 293 | 294 | return NotifyIfError("OnReadFile", ErrorToResultCode(err)); 295 | } 296 | 297 | ResultCode AgentWin::Connection::CallHandler() { 298 | ChromeToAgent message; 299 | if (!message.ParseFromArray(buffer_.data(), final_size_)) { 300 | // Malformed message. 301 | return NotifyIfError("ParseChromeToAgent", 302 | ResultCode::ERR_INVALID_REQUEST_FROM_BROWSER); 303 | } 304 | 305 | auto rc = ResultCode::OK; 306 | 307 | if (message.has_request()) { 308 | // This is a request from Google Chrome to perform a content analysis 309 | // request. 310 | // 311 | // Move the request from `message` to the event to reduce the amount 312 | // of memory allocation/copying and also because the the handler takes 313 | // ownership of the event. 314 | auto event = std::make_unique( 315 | handle_, browser_info_, std::move(*message.mutable_request())); 316 | rc = event->Init(); 317 | if (rc == ResultCode::OK) { 318 | handler_->OnAnalysisRequested(std::move(event)); 319 | } else { 320 | NotifyIfError("RequestValidation", rc); 321 | } 322 | } else if (message.has_ack()) { 323 | // This is an ack from Google Chrome that it has received a content 324 | // analysis response from the agent. 325 | handler_->OnResponseAcknowledged(message.ack()); 326 | } else if (message.has_cancel()) { 327 | // Google Chrome is informing the agent that the content analysis 328 | // request(s) associated with the given user action id have been 329 | // canceled by the user. 330 | handler_->OnCancelRequests(message.cancel()); 331 | } else { 332 | // Malformed message. 333 | rc = NotifyIfError("NoRequestOrAck", 334 | ResultCode::ERR_INVALID_REQUEST_FROM_BROWSER); 335 | } 336 | 337 | return rc; 338 | } 339 | 340 | ResultCode AgentWin::Connection::BuildBrowserInfo() { 341 | if (!GetNamedPipeClientProcessId(handle_, &browser_info_.pid)) { 342 | return NotifyIfError("BuildBrowserInfo", 343 | ResultCode::ERR_CANNOT_GET_BROWSER_PID); 344 | } 345 | 346 | if (!internal::GetProcessPath(browser_info_.pid, 347 | &browser_info_.binary_path)) { 348 | return NotifyIfError("BuildBrowserInfo", 349 | ResultCode::ERR_CANNOT_GET_BROWSER_BINARY_PATH); 350 | } 351 | 352 | return ResultCode::OK; 353 | } 354 | 355 | ResultCode AgentWin::Connection::NotifyIfError( 356 | const char* context, 357 | ResultCode rc, 358 | DWORD err) { 359 | if (handler_ && rc != ResultCode::OK) { 360 | std::stringstream stm; 361 | stm << context << " pid=" << browser_info_.pid; 362 | if (err != ERROR_SUCCESS) { 363 | stm << context << " err=" << err; 364 | } 365 | 366 | handler_->OnInternalError(stm.str().c_str(), rc); 367 | } 368 | return rc; 369 | } 370 | 371 | AgentWin::AgentWin( 372 | Config config, 373 | std::unique_ptr event_handler, 374 | ResultCode* rc) 375 | : AgentBase(std::move(config), std::move(event_handler)) { 376 | *rc = ResultCode::OK; 377 | if (handler() == nullptr) { 378 | *rc = ResultCode::ERR_AGENT_EVENT_HANDLER_NOT_SPECIFIED; 379 | return; 380 | } 381 | 382 | stop_event_ = CreateEvent(/*securityAttr=*/nullptr, 383 | /*manualReset=*/TRUE, 384 | /*initialState=*/FALSE, 385 | /*name=*/nullptr); 386 | if (stop_event_ == nullptr) { 387 | *rc = ResultCode::ERR_CANNOT_CREATE_AGENT_STOP_EVENT; 388 | return; 389 | } 390 | 391 | std::string pipename = 392 | internal::GetPipeNameForAgent(configuration().name, 393 | configuration().user_specific); 394 | if (pipename.empty()) { 395 | *rc = ResultCode::ERR_INVALID_CHANNEL_NAME; 396 | return; 397 | } 398 | 399 | pipename_ = pipename; 400 | 401 | connections_.reserve(kMinNumListeningPipeInstances); 402 | for (DWORD i = 0; i < kMinNumListeningPipeInstances; ++i) { 403 | connections_.emplace_back( 404 | std::make_unique(pipename_, configuration().user_specific, 405 | handler(), i == 0, rc)); 406 | if (*rc != ResultCode::OK || !connections_.back()->IsValid()) { 407 | Shutdown(); 408 | break; 409 | } 410 | } 411 | } 412 | 413 | AgentWin::~AgentWin() { 414 | Shutdown(); 415 | } 416 | 417 | ResultCode AgentWin::HandleEvents() { 418 | std::vector wait_handles; 419 | auto rc = ResultCode::OK; 420 | bool stopped = false; 421 | while (!stopped && rc == ResultCode::OK) { 422 | rc = HandleOneEvent(wait_handles, &stopped); 423 | } 424 | 425 | return rc; 426 | } 427 | 428 | ResultCode AgentWin::Stop() { 429 | SetEvent(stop_event_); 430 | return AgentBase::Stop(); 431 | } 432 | 433 | std::string AgentWin::DebugString() const { 434 | std::stringstream state; 435 | state.setf(std::ios::boolalpha); 436 | state << "AgentWin{pipe=\"" << pipename_; 437 | state << "\" stop=" << stop_event_; 438 | 439 | for (size_t i = 0; i < connections_.size(); ++i) { 440 | state << " conn@" << i; 441 | connections_[i]->AppendDebugString(state); 442 | } 443 | 444 | state << "}" << std::ends; 445 | return state.str(); 446 | } 447 | 448 | void AgentWin::GetHandles(std::vector& wait_handles) const { 449 | // Reserve enough space in the handles vector to include the stop event plus 450 | // all connections. 451 | wait_handles.clear(); 452 | wait_handles.reserve(1 + connections_.size()); 453 | 454 | for (auto& state : connections_) { 455 | HANDLE wait_handle = state->GetWaitHandle(); 456 | if (!wait_handle) { 457 | wait_handles.clear(); 458 | break; 459 | } 460 | wait_handles.push_back(wait_handle); 461 | } 462 | 463 | // Push the stop event last so that connections_ index calculations in 464 | // HandleOneEvent() don't have to account for this handle. 465 | wait_handles.push_back(stop_event_); 466 | } 467 | 468 | ResultCode AgentWin::HandleOneEventForTesting() { 469 | std::vector wait_handles; 470 | bool stopped; 471 | return HandleOneEvent(wait_handles, &stopped); 472 | } 473 | 474 | bool AgentWin::IsAClientConnectedForTesting() { 475 | for (const auto& state : connections_) { 476 | if (state->IsConnected()) { 477 | return true; 478 | } 479 | } 480 | return false; 481 | } 482 | 483 | ResultCode AgentWin::HandleOneEvent( 484 | std::vector& wait_handles, 485 | bool* stopped) { 486 | *stopped = false; 487 | 488 | // Wait on the specified handles for an event to occur. 489 | GetHandles(wait_handles); 490 | if (wait_handles.size() < kMinNumWaitHandles) { 491 | return NotifyError("GetHandles", ResultCode::ERR_AGENT_NOT_INITIALIZED); 492 | } 493 | 494 | DWORD index = WaitForMultipleObjects( 495 | wait_handles.size(), wait_handles.data(), 496 | /*waitAll=*/FALSE, /*timeoutMs=*/INFINITE); 497 | if (index == WAIT_FAILED) { 498 | return NotifyError("WaitForMultipleObjects", 499 | ErrorToResultCode(GetLastError())); 500 | } 501 | 502 | // If the index of signaled handle is the last one in wait_handles, then the 503 | // stop event was signaled. 504 | index -= WAIT_OBJECT_0; 505 | if (index == wait_handles.size() - 1) { 506 | *stopped = true; 507 | return ResultCode::OK; 508 | } 509 | 510 | auto& connection = connections_[index]; 511 | bool was_listening = !connection->IsConnected(); 512 | auto rc = connection->HandleEvent(wait_handles[index]); 513 | if (rc != ResultCode::OK) { 514 | // If `connection` was not listening and there are more than 515 | // kMinNumListeningPipeInstances pipes, delete this connection. Otherwise 516 | // reset it so that it becomes a listener. 517 | if (!was_listening && 518 | connections_.size() > kMinNumListeningPipeInstances) { 519 | connections_.erase(connections_.begin() + index); 520 | } else { 521 | rc = connection->Reset(pipename_, configuration().user_specific); 522 | } 523 | } 524 | 525 | // If `connection` was listening and is now connected, create a new 526 | // one so that there are always kMinNumListeningPipeInstances listening. 527 | if (rc == ResultCode::OK && was_listening && connection->IsConnected()) { 528 | connections_.emplace_back( 529 | std::make_unique(pipename_, configuration().user_specific, 530 | handler(), false, &rc)); 531 | } 532 | 533 | return ResultCode::OK; 534 | } 535 | 536 | void AgentWin::Shutdown() { 537 | connections_.clear(); 538 | pipename_.clear(); 539 | if (stop_event_ != nullptr) { 540 | CloseHandle(stop_event_); 541 | stop_event_ = nullptr; 542 | } 543 | } 544 | 545 | } // namespace sdk 546 | } // namespace content_analysis 547 | --------------------------------------------------------------------------------