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