From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../content_analysis_sdk/agent/src/agent_win.cc | 546 +++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 third_party/content_analysis_sdk/agent/src/agent_win.cc (limited to 'third_party/content_analysis_sdk/agent/src/agent_win.cc') diff --git a/third_party/content_analysis_sdk/agent/src/agent_win.cc b/third_party/content_analysis_sdk/agent/src/agent_win.cc new file mode 100644 index 0000000000..5d0b2c62b5 --- /dev/null +++ b/third_party/content_analysis_sdk/agent/src/agent_win.cc @@ -0,0 +1,546 @@ +// Copyright 2022 The Chromium Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include +#include + +#include "common/utils_win.h" + +#include "agent_utils_win.h" +#include "agent_win.h" +#include "event_win.h" + +namespace content_analysis { +namespace sdk { + +// The minimum number of pipe in listening mode. This is greater than one to +// handle the case of multiple instance of Google Chrome browser starting +// at the same time. +const DWORD kMinNumListeningPipeInstances = 2; + +// The minimum number of handles to wait on. This is the minimum number +// of pipes in listening mode plus the stop event. +const DWORD kMinNumWaitHandles = kMinNumListeningPipeInstances + 1; + +// static +std::unique_ptr Agent::Create( + Config config, + std::unique_ptr handler, + ResultCode* rc) { + auto agent = std::make_unique(std::move(config), std::move(handler), rc); + return *rc == ResultCode::OK ? std::move(agent) : nullptr; +} + +AgentWin::Connection::Connection(const std::string& pipename, + bool user_specific, + AgentEventHandler* handler, + bool is_first_pipe, + ResultCode* rc) + : handler_(handler) { + *rc = ResultCode::OK; + memset(&overlapped_, 0, sizeof(overlapped_)); + // Create a manual reset event as specified for overlapped IO. + // Use default security attriutes and no name since this event is not + // shared with other processes. + overlapped_.hEvent = CreateEvent(/*securityAttr=*/nullptr, + /*manualReset=*/TRUE, + /*initialState=*/FALSE, + /*name=*/nullptr); + if (!overlapped_.hEvent) { + *rc = ResultCode::ERR_CANNOT_CREATE_CHANNEL_IO_EVENT; + return; + } + + *rc = ResetInternal(pipename, user_specific, is_first_pipe); +} + +AgentWin::Connection::~Connection() { + Cleanup(); + + if (handle_ != INVALID_HANDLE_VALUE) { + CloseHandle(handle_); + } + + // Invalid event handles are represented as null. + if (overlapped_.hEvent) { + CloseHandle(overlapped_.hEvent); + } +} + +ResultCode AgentWin::Connection::Reset( + const std::string& pipename, + bool user_specific) { + return NotifyIfError("ConnectionReset", + ResetInternal(pipename, user_specific, false)); +} + +ResultCode AgentWin::Connection::HandleEvent(HANDLE handle) { + auto rc = ResultCode::OK; + DWORD count; + BOOL success = GetOverlappedResult(handle, &overlapped_, &count, + /*wait=*/FALSE); + if (!is_connected_) { + // This connection is currently listing for a new connection from a Google + // Chrome browser. If the result is a success, this means the browser has + // connected as expected. Otherwise an error occured so report it to the + // caller. + if (success) { + // A Google Chrome browser connected to the agent. Reset this + // connection object to handle communication with the browser and then + // tell the handler about it. + + is_connected_ = true; + buffer_.resize(internal::kBufferSize); + + rc = BuildBrowserInfo(); + if (rc == ResultCode::OK) { + handler_->OnBrowserConnected(browser_info_); + } + } else { + rc = ErrorToResultCode(GetLastError()); + NotifyIfError("GetOverlappedResult", rc); + } + } else { + // Some data has arrived from Google Chrome. This data is (part of) an + // instance of the proto message `ChromeToAgent`. + // + // If the message is small it is received in by one call to ReadFile(). + // If the message is larger it is received in by multiple calls to + // ReadFile(). + // + // `success` is true if the data just read is the last bytes for a message. + // Otherwise it is false. + rc = OnReadFile(success, count); + } + + // If all data has been read, queue another read. + if (rc == ResultCode::OK || rc == ResultCode::ERR_MORE_DATA) { + rc = QueueReadFile(rc == ResultCode::OK); + } + + if (rc != ResultCode::OK && rc != ResultCode::ERR_IO_PENDING && + rc != ResultCode::ERR_MORE_DATA) { + Cleanup(); + } else { + // Don't propagate all the "success" error codes to the called to keep + // this simpler. + rc = ResultCode::OK; + } + + return rc; +} + +void AgentWin::Connection::AppendDebugString(std::stringstream& state) const { + state << "{handle=" << handle_; + state << " connected=" << is_connected_; + state << " pid=" << browser_info_.pid; + state << " rsize=" << read_size_; + state << " fsize=" << final_size_; + state << "}"; +} + +ResultCode AgentWin::Connection::ConnectPipe() { + // In overlapped mode, connecting to a named pipe always returns false. + if (ConnectNamedPipe(handle_, &overlapped_)) { + return ErrorToResultCode(GetLastError()); + } + + DWORD err = GetLastError(); + if (err == ERROR_IO_PENDING) { + // Waiting for a Google Chrome Browser to connect. + return ResultCode::OK; + } else if (err == ERROR_PIPE_CONNECTED) { + // A Google Chrome browser is already connected. Make sure event is in + // signaled state in order to process the connection. + if (SetEvent(overlapped_.hEvent)) { + err = ERROR_SUCCESS; + } else { + err = GetLastError(); + } + } + + return ErrorToResultCode(err); +} + +ResultCode AgentWin::Connection::ResetInternal(const std::string& pipename, + bool user_specific, + bool is_first_pipe) { + auto rc = ResultCode::OK; + + // If this is the not the first time, disconnect from any existing Google + // Chrome browser. Otherwise creater a new pipe. + if (handle_ != INVALID_HANDLE_VALUE) { + if (!DisconnectNamedPipe(handle_)) { + rc = ErrorToResultCode(GetLastError()); + } + } else { + rc = ErrorToResultCode( + internal::CreatePipe(pipename, user_specific, is_first_pipe, &handle_)); + } + + // Make sure event starts in reset state. + if (rc == ResultCode::OK && !ResetEvent(overlapped_.hEvent)) { + rc = ErrorToResultCode(GetLastError()); + } + + if (rc == ResultCode::OK) { + rc = ConnectPipe(); + } + + if (rc != ResultCode::OK) { + Cleanup(); + handle_ = INVALID_HANDLE_VALUE; + } + + return rc; +} + +void AgentWin::Connection::Cleanup() { + if (is_connected_ && handler_) { + handler_->OnBrowserDisconnected(browser_info_); + } + + is_connected_ = false; + browser_info_ = BrowserInfo(); + buffer_.clear(); + cursor_ = nullptr; + read_size_ = 0; + final_size_ = 0; + + if (handle_ != INVALID_HANDLE_VALUE) { + // Cancel all outstanding IO requests on this pipe by using a null for + // overlapped. + CancelIoEx(handle_, /*overlapped=*/nullptr); + } + + // This function does not close `handle_` or the event in `overlapped` so + // that the server can resuse the pipe with an new Google Chrome browser + // instance. +} + +ResultCode AgentWin::Connection::QueueReadFile(bool reset_cursor) { + if (reset_cursor) { + cursor_ = buffer_.data(); + read_size_ = buffer_.size(); + final_size_ = 0; + } + + // When this function is called there are the following possiblities: + // + // 1/ Data is already available and the buffer is filled in. ReadFile() + // return TRUE and the event is set. + // 2/ Data is not avaiable yet. ReadFile() returns FALSE and the last error + // is ERROR_IO_PENDING(997) and the event is reset. + // 3/ Some error occurred, like for example Google Chrome stops. ReadFile() + // returns FALSE and the last error is something other than + // ERROR_IO_PENDING, for example ERROR_BROKEN_PIPE(109). The event + // state is unchanged. + auto rc = ResultCode::OK; + DWORD count; + if (!ReadFile(handle_, cursor_, read_size_, &count, &overlapped_)) { + DWORD err = GetLastError(); + rc = ErrorToResultCode(err); + + // IO pending is not an error so don't notify. + // + // Ignore broken pipes for notifications since that happens when the Google + // Chrome browser shuts down. The agent will be notified of a browser + // disconnect in that case. + // + // More data means that `buffer_` was too small to read the entire message + // from the browser. The buffer has already been resized. Another call to + // ReadFile() is needed to get the remainder. + if (rc != ResultCode::ERR_IO_PENDING && + rc != ResultCode::ERR_BROKEN_PIPE && + rc != ResultCode::ERR_MORE_DATA) { + NotifyIfError("QueueReadFile", rc, err); + } + } + + return rc; +} + +ResultCode AgentWin::Connection::OnReadFile(BOOL done_reading, DWORD count) { + final_size_ += count; + + // If `done_reading` is TRUE, this means the full message has been read. + // Call the appropriate handler method. + if (done_reading) { + return CallHandler(); + } + + // Otherwise there are two possibilities: + // + // 1/ The last error is ERROR_MORE_DATA(234). This means there are more + // bytes to read before the request message is complete. Resize the + // buffer and adjust the cursor. The caller will queue up another read + // and wait. don't notify the handler since this is not an error. + // 2/ Some error occured. In this case notify the handler and return the + // error. + + DWORD err = GetLastError(); + if (err == ERROR_MORE_DATA) { + read_size_ = internal::kBufferSize; + buffer_.resize(buffer_.size() + read_size_); + cursor_ = buffer_.data() + buffer_.size() - read_size_; + return ErrorToResultCode(err); + } + + return NotifyIfError("OnReadFile", ErrorToResultCode(err)); +} + +ResultCode AgentWin::Connection::CallHandler() { + ChromeToAgent message; + if (!message.ParseFromArray(buffer_.data(), final_size_)) { + // Malformed message. + return NotifyIfError("ParseChromeToAgent", + ResultCode::ERR_INVALID_REQUEST_FROM_BROWSER); + } + + auto rc = ResultCode::OK; + + if (message.has_request()) { + // This is a request from Google Chrome to perform a content analysis + // request. + // + // Move the request from `message` to the event to reduce the amount + // of memory allocation/copying and also because the the handler takes + // ownership of the event. + auto event = std::make_unique( + handle_, browser_info_, std::move(*message.mutable_request())); + rc = event->Init(); + if (rc == ResultCode::OK) { + handler_->OnAnalysisRequested(std::move(event)); + } else { + NotifyIfError("RequestValidation", rc); + } + } else if (message.has_ack()) { + // This is an ack from Google Chrome that it has received a content + // analysis response from the agent. + handler_->OnResponseAcknowledged(message.ack()); + } else if (message.has_cancel()) { + // Google Chrome is informing the agent that the content analysis + // request(s) associated with the given user action id have been + // canceled by the user. + handler_->OnCancelRequests(message.cancel()); + } else { + // Malformed message. + rc = NotifyIfError("NoRequestOrAck", + ResultCode::ERR_INVALID_REQUEST_FROM_BROWSER); + } + + return rc; +} + +ResultCode AgentWin::Connection::BuildBrowserInfo() { + if (!GetNamedPipeClientProcessId(handle_, &browser_info_.pid)) { + return NotifyIfError("BuildBrowserInfo", + ResultCode::ERR_CANNOT_GET_BROWSER_PID); + } + + if (!internal::GetProcessPath(browser_info_.pid, + &browser_info_.binary_path)) { + return NotifyIfError("BuildBrowserInfo", + ResultCode::ERR_CANNOT_GET_BROWSER_BINARY_PATH); + } + + return ResultCode::OK; +} + +ResultCode AgentWin::Connection::NotifyIfError( + const char* context, + ResultCode rc, + DWORD err) { + if (handler_ && rc != ResultCode::OK) { + std::stringstream stm; + stm << context << " pid=" << browser_info_.pid; + if (err != ERROR_SUCCESS) { + stm << context << " err=" << err; + } + + handler_->OnInternalError(stm.str().c_str(), rc); + } + return rc; +} + +AgentWin::AgentWin( + Config config, + std::unique_ptr event_handler, + ResultCode* rc) + : AgentBase(std::move(config), std::move(event_handler)) { + *rc = ResultCode::OK; + if (handler() == nullptr) { + *rc = ResultCode::ERR_AGENT_EVENT_HANDLER_NOT_SPECIFIED; + return; + } + + stop_event_ = CreateEvent(/*securityAttr=*/nullptr, + /*manualReset=*/TRUE, + /*initialState=*/FALSE, + /*name=*/nullptr); + if (stop_event_ == nullptr) { + *rc = ResultCode::ERR_CANNOT_CREATE_AGENT_STOP_EVENT; + return; + } + + std::string pipename = + internal::GetPipeNameForAgent(configuration().name, + configuration().user_specific); + if (pipename.empty()) { + *rc = ResultCode::ERR_INVALID_CHANNEL_NAME; + return; + } + + pipename_ = pipename; + + connections_.reserve(kMinNumListeningPipeInstances); + for (DWORD i = 0; i < kMinNumListeningPipeInstances; ++i) { + connections_.emplace_back( + std::make_unique(pipename_, configuration().user_specific, + handler(), i == 0, rc)); + if (*rc != ResultCode::OK || !connections_.back()->IsValid()) { + Shutdown(); + break; + } + } +} + +AgentWin::~AgentWin() { + Shutdown(); +} + +ResultCode AgentWin::HandleEvents() { + std::vector wait_handles; + auto rc = ResultCode::OK; + bool stopped = false; + while (!stopped && rc == ResultCode::OK) { + rc = HandleOneEvent(wait_handles, &stopped); + } + + return rc; +} + +ResultCode AgentWin::Stop() { + SetEvent(stop_event_); + return AgentBase::Stop(); +} + +std::string AgentWin::DebugString() const { + std::stringstream state; + state.setf(std::ios::boolalpha); + state << "AgentWin{pipe=\"" << pipename_; + state << "\" stop=" << stop_event_; + + for (size_t i = 0; i < connections_.size(); ++i) { + state << " conn@" << i; + connections_[i]->AppendDebugString(state); + } + + state << "}" << std::ends; + return state.str(); +} + +void AgentWin::GetHandles(std::vector& wait_handles) const { + // Reserve enough space in the handles vector to include the stop event plus + // all connections. + wait_handles.clear(); + wait_handles.reserve(1 + connections_.size()); + + for (auto& state : connections_) { + HANDLE wait_handle = state->GetWaitHandle(); + if (!wait_handle) { + wait_handles.clear(); + break; + } + wait_handles.push_back(wait_handle); + } + + // Push the stop event last so that connections_ index calculations in + // HandleOneEvent() don't have to account for this handle. + wait_handles.push_back(stop_event_); +} + +ResultCode AgentWin::HandleOneEventForTesting() { + std::vector wait_handles; + bool stopped; + return HandleOneEvent(wait_handles, &stopped); +} + +bool AgentWin::IsAClientConnectedForTesting() { + for (const auto& state : connections_) { + if (state->IsConnected()) { + return true; + } + } + return false; +} + +ResultCode AgentWin::HandleOneEvent( + std::vector& wait_handles, + bool* stopped) { + *stopped = false; + + // Wait on the specified handles for an event to occur. + GetHandles(wait_handles); + if (wait_handles.size() < kMinNumWaitHandles) { + return NotifyError("GetHandles", ResultCode::ERR_AGENT_NOT_INITIALIZED); + } + + DWORD index = WaitForMultipleObjects( + wait_handles.size(), wait_handles.data(), + /*waitAll=*/FALSE, /*timeoutMs=*/INFINITE); + if (index == WAIT_FAILED) { + return NotifyError("WaitForMultipleObjects", + ErrorToResultCode(GetLastError())); + } + + // If the index of signaled handle is the last one in wait_handles, then the + // stop event was signaled. + index -= WAIT_OBJECT_0; + if (index == wait_handles.size() - 1) { + *stopped = true; + return ResultCode::OK; + } + + auto& connection = connections_[index]; + bool was_listening = !connection->IsConnected(); + auto rc = connection->HandleEvent(wait_handles[index]); + if (rc != ResultCode::OK) { + // If `connection` was not listening and there are more than + // kMinNumListeningPipeInstances pipes, delete this connection. Otherwise + // reset it so that it becomes a listener. + if (!was_listening && + connections_.size() > kMinNumListeningPipeInstances) { + connections_.erase(connections_.begin() + index); + } else { + rc = connection->Reset(pipename_, configuration().user_specific); + } + } + + // If `connection` was listening and is now connected, create a new + // one so that there are always kMinNumListeningPipeInstances listening. + if (rc == ResultCode::OK && was_listening && connection->IsConnected()) { + connections_.emplace_back( + std::make_unique(pipename_, configuration().user_specific, + handler(), false, &rc)); + } + + return ResultCode::OK; +} + +void AgentWin::Shutdown() { + connections_.clear(); + pipename_.clear(); + if (stop_event_ != nullptr) { + CloseHandle(stop_event_); + stop_event_ = nullptr; + } +} + +} // namespace sdk +} // namespace content_analysis -- cgit v1.2.3