| // Copyright 2016 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "content/browser/devtools/devtools_session.h" | |
| #include <utility> | |
| #include <vector> | |
| #include "base/containers/flat_set.h" | |
| #include "base/functional/bind.h" | |
| #include "base/trace_event/trace_event.h" | |
| #include "content/browser/bad_message.h" | |
| #include "content/browser/devtools/devtools_manager.h" | |
| #include "content/public/browser/render_process_host.h" | |
| #include "content/browser/devtools/protocol/devtools_domain_handler.h" | |
| #include "content/browser/devtools/protocol/protocol.h" | |
| #include "content/browser/devtools/render_frame_devtools_agent_host.h" | |
| #include "content/public/browser/devtools_external_agent_proxy_delegate.h" | |
| #include "content/public/browser/devtools_manager_delegate.h" | |
| #include "content/public/browser/network_service_instance.h" | |
| #include "third_party/inspector_protocol/crdtp/cbor.h" | |
| #include "third_party/inspector_protocol/crdtp/dispatch.h" | |
| #include "third_party/inspector_protocol/crdtp/json.h" | |
| #include "third_party/perfetto/include/perfetto/tracing/track_event_args.h" | |
| namespace content { | |
| namespace { | |
| // Keep in sync with WebDevToolsAgent::ShouldInterruptForMethod. | |
| // TODO(petermarshall): find a way to share this. | |
| bool ShouldSendOnIO(crdtp::span<uint8_t> method) { | |
| static auto* kEntries = new std::vector<crdtp::span<uint8_t>>{ | |
| crdtp::SpanFrom("Debugger.getPossibleBreakpoints"), | |
| crdtp::SpanFrom("Debugger.getScriptSource"), | |
| crdtp::SpanFrom("Debugger.getStackTrace"), | |
| crdtp::SpanFrom("Debugger.pause"), | |
| crdtp::SpanFrom("Debugger.removeBreakpoint"), | |
| crdtp::SpanFrom("Debugger.resume"), | |
| crdtp::SpanFrom("Debugger.setBreakpoint"), | |
| crdtp::SpanFrom("Debugger.setBreakpointByUrl"), | |
| crdtp::SpanFrom("Debugger.setBreakpointsActive"), | |
| crdtp::SpanFrom("Emulation.setScriptExecutionDisabled"), | |
| crdtp::SpanFrom("Page.crash"), | |
| crdtp::SpanFrom("Performance.getMetrics"), | |
| crdtp::SpanFrom("Runtime.terminateExecution"), | |
| }; | |
| DCHECK(std::is_sorted(kEntries->begin(), kEntries->end(), crdtp::SpanLt())); | |
| return std::binary_search(kEntries->begin(), kEntries->end(), method, | |
| crdtp::SpanLt()); | |
| } | |
| // During navigation, we only suspend the main-thread messages. The IO thread | |
| // messages should go through so that the renderer can be woken up | |
| // via the IO thread even if the renderer does not process message loops. | |
| // | |
| // In particular, we are looking to deadlocking the renderer when | |
| // reloading the page during the instrumentation pause (crbug.com/1354043): | |
| // | |
| // - If we are in the pause, there is no way to commit or fail the navigation | |
| // in the renderer because the instrumentation pause does not process message | |
| // loops. | |
| // - At the same time, the instrumentation pause could not wake up if | |
| // the resume message was blocked by the suspension of message sending during | |
| // navigation. | |
| // | |
| // To give the renderer a chance to wake up, we always forward the messages | |
| // for the IO thread to the renderer. | |
| bool ShouldSuspendDuringNavigation(crdtp::span<uint8_t> method) { | |
| return !ShouldSendOnIO(method); | |
| } | |
| // Async control commands (such as CSS.enable) are idempotant and can | |
| // be safely replayed in the new RenderFrameHost. We will always forward | |
| // them to the new renderer on cross process navigation. Main rationale for | |
| // it is that the client doesn't expect such calls to fail in normal | |
| // circumstances. | |
| // | |
| // Ideally all non-control async commands shoulds be listed here but we | |
| // conservatively start with Runtime domain where the decision is more | |
| // clear. | |
| bool TerminateOnCrossProcessNavigation(crdtp::span<uint8_t> method) { | |
| static auto* kEntries = new std::vector<crdtp::span<uint8_t>>{ | |
| crdtp::SpanFrom("Runtime.awaitPromise"), | |
| crdtp::SpanFrom("Runtime.callFunctionOn"), | |
| crdtp::SpanFrom("Runtime.evaluate"), | |
| crdtp::SpanFrom("Runtime.runScript"), | |
| crdtp::SpanFrom("Runtime.terminateExecution"), | |
| crdtp::SpanFrom("WebMCP.invokeTool"), | |
| }; | |
| DCHECK(std::is_sorted(kEntries->begin(), kEntries->end(), crdtp::SpanLt())); | |
| return std::binary_search(kEntries->begin(), kEntries->end(), method, | |
| crdtp::SpanLt()); | |
| } | |
| const char kResumeMethod[] = "Runtime.runIfWaitingForDebugger"; | |
| const char kSessionId[] = "sessionId"; | |
| // Clients match against this error message verbatim (http://crbug.com/1001678). | |
| const char kTargetClosedMessage[] = "Inspected target navigated or closed"; | |
| const char kTargetCrashedMessage[] = "Target crashed"; | |
| std::atomic<int> g_root_session_count{0}; | |
| } // namespace | |
| DevToolsSession::PendingMessage::PendingMessage(PendingMessage&&) = default; | |
| DevToolsSession::PendingMessage::PendingMessage(int call_id, | |
| crdtp::span<uint8_t> method, | |
| crdtp::span<uint8_t> payload, | |
| std::string fallthrough_data) | |
| : call_id(call_id), | |
| method(method.begin(), method.end()), | |
| payload(payload.begin(), payload.end()), | |
| fallthrough_data(std::move(fallthrough_data)) {} | |
| DevToolsSession::PendingMessage::~PendingMessage() = default; | |
| // static | |
| int DevToolsSession::GetRootSessionCount() { | |
| return g_root_session_count.load(); | |
| } | |
| DevToolsSession::DevToolsSession(DevToolsAgentHostClient* client, Mode mode) | |
| : client_(client), mode_(mode) { | |
| ++g_root_session_count; | |
| session_state_cookie_ = blink::mojom::DevToolsSessionState::New(); | |
| session_state_cookie_->browser_originating_session_state = | |
| blink::mojom::BrowserOriginatingSessionState::New(); | |
| handle_command_callback_ = base::BindRepeating( | |
| &DevToolsSession::HandleCommand, weak_factory_.GetWeakPtr()); | |
| } | |
| DevToolsSession::DevToolsSession(DevToolsAgentHostClient* client, | |
| const std::string& session_id, | |
| DevToolsSession* parent, | |
| Mode mode) | |
| : client_(client), | |
| root_session_(parent->GetRootSession()), | |
| session_id_(session_id), | |
| mode_(mode) { | |
| DCHECK(root_session_); | |
| DCHECK(!session_id_.empty()); | |
| session_state_cookie_ = blink::mojom::DevToolsSessionState::New(); | |
| session_state_cookie_->browser_originating_session_state = | |
| blink::mojom::BrowserOriginatingSessionState::New(); | |
| handle_command_callback_ = base::BindRepeating( | |
| &DevToolsSession::HandleCommand, weak_factory_.GetWeakPtr()); | |
| } | |
| DevToolsSession::~DevToolsSession() { | |
| if (!root_session_) { | |
| --g_root_session_count; | |
| CHECK_GE(g_root_session_count, 0); | |
| } | |
| if (proxy_delegate_) | |
| proxy_delegate_->Detach(this); | |
| // It is Ok for session to be deleted without the dispose - | |
| // it can be kicked out by an extension connect / disconnect. | |
| if (dispatcher_) | |
| Dispose(); | |
| } | |
| void DevToolsSession::SetAgentHost(DevToolsAgentHostImpl* agent_host) { | |
| DCHECK(!agent_host_); | |
| agent_host_ = agent_host; | |
| } | |
| void DevToolsSession::SetRuntimeResumeCallback( | |
| base::OnceClosure runtime_resume) { | |
| runtime_resume_ = std::move(runtime_resume); | |
| } | |
| bool DevToolsSession::IsWaitingForDebuggerOnStart() const { | |
| return !runtime_resume_.is_null(); | |
| } | |
| void DevToolsSession::Dispose() { | |
| dispatcher_.reset(); | |
| for (auto& pair : handlers_) | |
| pair.second->Disable(); | |
| handlers_.clear(); | |
| } | |
| DevToolsSession* DevToolsSession::GetRootSession() { | |
| return root_session_ ? root_session_.get() : this; | |
| } | |
| void DevToolsSession::AddHandler( | |
| std::unique_ptr<protocol::DevToolsDomainHandler> handler) { | |
| DCHECK(agent_host_); | |
| handler->Wire(dispatcher_.get()); | |
| handler->SetSession(this); | |
| handlers_[handler->name()] = std::move(handler); | |
| } | |
| void DevToolsSession::SetBrowserOnly(bool browser_only) { | |
| browser_only_ = browser_only; | |
| } | |
| void DevToolsSession::TurnIntoExternalProxy( | |
| DevToolsExternalAgentProxyDelegate* proxy_delegate) { | |
| proxy_delegate_ = proxy_delegate; | |
| proxy_delegate_->Attach(this); | |
| } | |
| void DevToolsSession::AttachToAgent(blink::mojom::DevToolsAgent* agent, | |
| bool force_using_io_session) { | |
| DCHECK(agent_host_); | |
| if (!agent) { | |
| receiver_.reset(); | |
| session_.reset(); | |
| io_session_.reset(); | |
| return; | |
| } | |
| // TODO(crbug.com/41467868): Consider a reset flow since new mojo types | |
| // checks is_bound strictly. | |
| if (receiver_.is_bound()) { | |
| receiver_.reset(); | |
| session_.reset(); | |
| io_session_.reset(); | |
| } | |
| use_io_session_ = force_using_io_session; | |
| agent->AttachDevToolsSession( | |
| receiver_.BindNewEndpointAndPassRemote(), | |
| session_.BindNewEndpointAndPassReceiver(), | |
| io_session_.BindNewPipeAndPassReceiver(), session_state_cookie_.Clone(), | |
| client_->UsesBinaryProtocol(), client_->IsTrusted(), session_id_, | |
| IsWaitingForDebuggerOnStart()); | |
| session_.set_disconnect_handler(base::BindOnce( | |
| &DevToolsSession::MojoConnectionDestroyed, base::Unretained(this))); | |
| // Set cookie to an empty struct to reattach next time instead of attaching. | |
| if (!session_state_cookie_->renderer_originating_session_state) { | |
| session_state_cookie_->renderer_originating_session_state = | |
| blink::mojom::RendererOriginatingSessionState::New(); | |
| } | |
| // Only use script_to_evaluate_on_load_once once. | |
| session_state_cookie_->browser_originating_session_state | |
| ->script_to_evaluate_on_load_once.clear(); | |
| // We're attaching to a new agent while suspended; therefore, messages that | |
| // have been sent previously either need to be terminated or re-sent once we | |
| // resume, as we will not get any responses from the old agent at this point. | |
| if (suspended_sending_messages_to_agent_) { | |
| for (auto it = pending_messages_.begin(); it != pending_messages_.end();) { | |
| const PendingMessage& message = *it; | |
| if (waiting_for_response_.count(message.call_id) && | |
| TerminateOnCrossProcessNavigation(crdtp::SpanFrom(message.method))) { | |
| // Send error to the client and remove the message from pending. | |
| SendProtocolResponse( | |
| message.call_id, | |
| crdtp::CreateErrorResponse( | |
| message.call_id, | |
| crdtp::DispatchResponse::ServerError(kTargetClosedMessage))); | |
| it = pending_messages_.erase(it); | |
| } else { | |
| // We'll send or re-send the message in ResumeSendingMessagesToAgent. | |
| ++it; | |
| } | |
| } | |
| waiting_for_response_.clear(); | |
| return; | |
| } | |
| // The session is not suspended but the RenderFrameHost may be updated | |
| // during navigation because: | |
| // - auto attached to a new OOPIF | |
| // - cross-process navigation in the main frame | |
| // Therefore, we re-send outstanding messages to the new host. | |
| for (const PendingMessage& message : pending_messages_) { | |
| if (waiting_for_response_.count(message.call_id)) | |
| DispatchToAgent(message); | |
| } | |
| } | |
| void DevToolsSession::MojoConnectionDestroyed() { | |
| receiver_.reset(); | |
| session_.reset(); | |
| io_session_.reset(); | |
| } | |
| // The client of the devtools session will call this method to send a message | |
| // to handlers / agents that the session is connected with. | |
| void DevToolsSession::DispatchProtocolMessage( | |
| base::span<const uint8_t> message) { | |
| if (client_->UsesBinaryProtocol()) { | |
| crdtp::StatusOr<size_t> status = | |
| crdtp::cbor::CheckCBORMessage(crdtp::SpanFrom(message)); | |
| if (!status.ok()) { | |
| DispatchProtocolMessageToClient( | |
| crdtp::CreateErrorNotification(crdtp::DispatchResponse::ParseError( | |
| status.status().ToASCIIString())) | |
| ->Serialize()); | |
| return; | |
| } | |
| } | |
| // If the session is in proxy mode, then |message| will be sent to | |
| // an external session, so it needs to be sent as JSON. | |
| // TODO(dgozman): revisit the proxy delegate. | |
| if (proxy_delegate_) { // External session wants JSON. | |
| if (!client_->UsesBinaryProtocol()) { // Client sent JSON. | |
| proxy_delegate_->SendMessageToBackend(this, message); | |
| return; | |
| } | |
| // External session wants JSON, but client provided CBOR. | |
| std::vector<uint8_t> json; | |
| crdtp::Status status = | |
| crdtp::json::ConvertCBORToJSON(crdtp::SpanFrom(message), &json); | |
| if (status.ok()) { | |
| proxy_delegate_->SendMessageToBackend(this, json); | |
| return; | |
| } | |
| DispatchProtocolMessageToClient( | |
| crdtp::CreateErrorNotification( | |
| crdtp::DispatchResponse::ParseError(status.ToASCIIString())) | |
| ->Serialize()); | |
| return; | |
| } | |
| // Before dispatching, convert the message to CBOR if needed. | |
| std::vector<uint8_t> converted_cbor_message; | |
| if (!client_->UsesBinaryProtocol()) { // Client sent JSON. | |
| crdtp::Status status = crdtp::json::ConvertJSONToCBOR( | |
| crdtp::SpanFrom(message), &converted_cbor_message); | |
| if (!status.ok()) { | |
| DispatchProtocolMessageToClient( | |
| crdtp::CreateErrorNotification( | |
| crdtp::DispatchResponse::ParseError(status.ToASCIIString())) | |
| ->Serialize()); | |
| return; | |
| } | |
| message = converted_cbor_message; | |
| } | |
| // At this point |message| is CBOR. | |
| crdtp::Dispatchable dispatchable( | |
| crdtp::SpanFrom(message), std::string_view(), | |
| [cb = base::BindRepeating(&DevToolsSession::FallThrough, | |
| weak_factory_.GetWeakPtr())]( | |
| int call_id, crdtp::span<uint8_t> method, | |
| crdtp::span<uint8_t> message, std::string_view fallthrough_data) { | |
| cb.Run(call_id, method, message, fallthrough_data); | |
| }); | |
| if (!dispatchable.ok()) { | |
| DispatchProtocolMessageToClient( | |
| (dispatchable.HasCallId() | |
| ? crdtp::CreateErrorResponse(dispatchable.CallId(), | |
| dispatchable.DispatchError()) | |
| : crdtp::CreateErrorNotification(dispatchable.DispatchError())) | |
| ->Serialize()); | |
| return; | |
| } | |
| if (dispatchable.SessionId().empty()) { | |
| DispatchProtocolMessageInternal(std::move(dispatchable), message); | |
| return; | |
| } | |
| std::string session_id(dispatchable.SessionId().begin(), | |
| dispatchable.SessionId().end()); | |
| auto it = child_sessions_.find(session_id); | |
| if (it == child_sessions_.end()) { | |
| auto error = crdtp::DispatchResponse::SessionNotFound( | |
| "Session with given id not found."); | |
| DispatchProtocolMessageToClient( | |
| (dispatchable.HasCallId() | |
| ? crdtp::CreateErrorResponse(dispatchable.CallId(), | |
| std::move(error)) | |
| : crdtp::CreateErrorNotification(std::move(error))) | |
| ->Serialize()); | |
| return; | |
| } | |
| DevToolsSession* session = it->second; | |
| DCHECK(!session->proxy_delegate_); | |
| session->DispatchProtocolMessageInternal(std::move(dispatchable), message); | |
| } | |
| void DevToolsSession::DispatchProtocolMessageInternal( | |
| crdtp::Dispatchable dispatchable, | |
| base::span<const uint8_t> message) { | |
| if ((browser_only_ || runtime_resume_) && | |
| crdtp::SpanEquals(crdtp::SpanFrom(kResumeMethod), | |
| dispatchable.Method())) { | |
| if (runtime_resume_) { | |
| std::move(runtime_resume_).Run(); | |
| } | |
| if (browser_only_) { | |
| DispatchProtocolMessageToClient( | |
| crdtp::CreateResponse(dispatchable.CallId(), nullptr)->Serialize()); | |
| return; | |
| } | |
| } | |
| DevToolsManagerDelegate* delegate = | |
| DevToolsManager::GetInstance()->delegate(); | |
| if (delegate && !dispatchable.Method().empty()) { | |
| delegate->HandleCommand(this, message, handle_command_callback_); | |
| } else { | |
| HandleCommandInternal(std::move(dispatchable), message); | |
| } | |
| } | |
| void DevToolsSession::HandleCommand(base::span<const uint8_t> message) { | |
| HandleCommandInternal( | |
| crdtp::Dispatchable( | |
| crdtp::SpanFrom(message), std::string_view(), | |
| [cb = base::BindRepeating(&DevToolsSession::FallThrough, | |
| weak_factory_.GetWeakPtr())]( | |
| int call_id, crdtp::span<uint8_t> method, | |
| crdtp::span<uint8_t> message, std::string_view fallthrough_data) { | |
| cb.Run(call_id, method, message, fallthrough_data); | |
| }), | |
| message); | |
| } | |
| void DevToolsSession::HandleCommandInternal(crdtp::Dispatchable dispatchable, | |
| base::span<const uint8_t> message) { | |
| DCHECK(dispatchable.ok()); | |
| TRACE_EVENT( | |
| "devtools", "DevToolsSession::HandleCommand in Browser", | |
| perfetto::Flow::ProcessScoped(dispatchable.CallId()), "method", | |
| std::string(dispatchable.Method().begin(), dispatchable.Method().end()), | |
| "call_id", dispatchable.CallId()); | |
| dispatcher_->Dispatch(dispatchable); | |
| } | |
| void DevToolsSession::FallThrough(int call_id, | |
| crdtp::span<uint8_t> method, | |
| crdtp::span<uint8_t> message, | |
| std::string_view fallthrough_data) { | |
| if (browser_only_) { | |
| dispatcher_->SendMethodNotFound(call_id, method); | |
| return; | |
| } | |
| if (waiting_for_response_.contains(call_id)) { | |
| DispatchProtocolMessageToClient( | |
| crdtp::CreateErrorResponse(call_id, | |
| crdtp::DispatchResponse::InvalidRequest( | |
| "Duplicate `id` in protocol request")) | |
| ->Serialize()); | |
| } | |
| auto it = pending_messages_.emplace(pending_messages_.end(), call_id, method, | |
| message, std::string(fallthrough_data)); | |
| if (suspended_sending_messages_to_agent_ && | |
| ShouldSuspendDuringNavigation(method)) | |
| return; | |
| DispatchToAgent(pending_messages_.back()); | |
| waiting_for_response_[call_id] = it; | |
| } | |
| // This method implements DevtoolsAgentHostClientChannel and | |
| // sends messages coming from the browser to the client. | |
| void DevToolsSession::DispatchProtocolMessageToClient( | |
| std::vector<uint8_t> message) { | |
| DCHECK(crdtp::cbor::IsCBORMessage(crdtp::SpanFrom(message))); | |
| if (!session_id_.empty()) { | |
| crdtp::Status status = crdtp::cbor::AppendString8EntryToCBORMap( | |
| crdtp::SpanFrom(kSessionId), crdtp::SpanFrom(session_id_), &message); | |
| DCHECK(status.ok()) << status.ToASCIIString(); | |
| } | |
| if (!client_->UsesBinaryProtocol()) { | |
| std::vector<uint8_t> json; | |
| crdtp::Status status = | |
| crdtp::json::ConvertCBORToJSON(crdtp::SpanFrom(message), &json); | |
| DCHECK(status.ok()) << status.ToASCIIString(); | |
| message = std::move(json); | |
| } | |
| client_->DispatchProtocolMessage(agent_host_, message); | |
| } | |
| content::DevToolsAgentHost* DevToolsSession::GetAgentHost() { | |
| return agent_host_; | |
| } | |
| content::DevToolsAgentHostClient* DevToolsSession::GetClient() { | |
| return client_; | |
| } | |
| void DevToolsSession::DispatchToAgent(const PendingMessage& message) { | |
| DCHECK(!browser_only_); | |
| // We send all messages on the IO channel for workers so that messages like | |
| // Debugger.pause don't get stuck behind other blocking messages. | |
| if (ShouldSendOnIO(crdtp::SpanFrom(message.method)) || use_io_session_) { | |
| if (io_session_) { | |
| TRACE_EVENT("devtools", "DevToolsSession::DispatchToAgent on IO", | |
| perfetto::Flow::ProcessScoped(message.call_id), "method", | |
| message.method, "call_id", message.call_id); | |
| io_session_->DispatchProtocolCommand(message.call_id, message.method, | |
| message.payload, | |
| message.fallthrough_data); | |
| } | |
| } else { | |
| if (session_) { | |
| TRACE_EVENT("devtools", "DevToolsSession::DispatchToAgent", | |
| perfetto::Flow::ProcessScoped(message.call_id), "method", | |
| message.method, "call_id", message.call_id); | |
| session_->DispatchProtocolCommand(message.call_id, message.method, | |
| message.payload, | |
| message.fallthrough_data); | |
| } | |
| } | |
| } | |
| void DevToolsSession::SuspendSendingMessagesToAgent() { | |
| DCHECK(!browser_only_); | |
| suspended_sending_messages_to_agent_ = true; | |
| } | |
| void DevToolsSession::ResumeSendingMessagesToAgent() { | |
| DCHECK(!browser_only_); | |
| suspended_sending_messages_to_agent_ = false; | |
| for (auto it = pending_messages_.begin(); it != pending_messages_.end(); | |
| ++it) { | |
| const PendingMessage& message = *it; | |
| if (waiting_for_response_.count(message.call_id)) | |
| continue; | |
| DispatchToAgent(message); | |
| waiting_for_response_[message.call_id] = it; | |
| } | |
| } | |
| void DevToolsSession::ClearPendingMessages(bool did_crash) { | |
| for (auto it = pending_messages_.begin(); it != pending_messages_.end();) { | |
| const PendingMessage& message = *it; | |
| // TODO(caseq): remove when non-RenderDocument code paths are gone. | |
| if (message.method == "Page.reload") { | |
| ++it; | |
| continue; | |
| } | |
| // Send error to the client and remove the message from pending. | |
| std::string error_message = | |
| did_crash ? kTargetCrashedMessage : kTargetClosedMessage; | |
| SendProtocolResponse( | |
| message.call_id, | |
| crdtp::CreateErrorResponse( | |
| message.call_id, | |
| crdtp::DispatchResponse::ServerError(error_message))); | |
| waiting_for_response_.erase(message.call_id); | |
| it = pending_messages_.erase(it); | |
| } | |
| } | |
| // The following methods handle responses or notifications coming from | |
| // the browser to the client. | |
| void DevToolsSession::SendProtocolResponse( | |
| int call_id, | |
| std::unique_ptr<protocol::Serializable> message) { | |
| DispatchProtocolMessageToClient(message->Serialize()); | |
| // |this| may be deleted at this point. | |
| } | |
| void DevToolsSession::SendProtocolNotification( | |
| std::unique_ptr<protocol::Serializable> message) { | |
| DispatchProtocolMessageToClient(message->Serialize()); | |
| // |this| may be deleted at this point. | |
| } | |
| void DevToolsSession::FlushProtocolNotifications() {} | |
| // The following methods handle responses or notifications coming from the | |
| // renderer (blink) to the client. It is important that these messages not be | |
| // parsed and sent as is, since a renderer may be compromised; so therefore, | |
| // we're not sending them via the DevToolsAgentHostClientChannel interface | |
| // (::DispatchProtocolMessageToClient) but directly to the client instead. | |
| void DevToolsSession::DispatchProtocolResponseOrNotification( | |
| DevToolsAgentHostClient* client, | |
| DevToolsAgentHostImpl* agent_host, | |
| blink::mojom::DevToolsMessagePtr message, | |
| const std::string& session_id, | |
| const bool& is_notification) { | |
| // If BigBuffer is backed by shared memory, make a copy so that a compromised | |
| // renderer wouldn't be able to mess with the message as we validate it. | |
| std::vector<uint8_t> message_bytes; | |
| base::span<const uint8_t> message_span; | |
| switch (message->data.storage_type()) { | |
| case mojo_base::BigBuffer::StorageType::kBytes: | |
| message_span = message->data.byte_span(); | |
| break; | |
| case mojo_base::BigBuffer::StorageType::kSharedMemory: | |
| message_bytes.assign(message->data.begin(), message->data.end()); | |
| message_span = message_bytes; | |
| break; | |
| default: | |
| // just keep span empty, this will cause renderer killed for invalid | |
| // message below. | |
| break; | |
| } | |
| const bool message_is_valid = | |
| !message_span.empty() && | |
| ValidateMessage(session_id, | |
| /*expected_has_id=*/!is_notification, message_span, | |
| /*expect_cbor */ client->UsesBinaryProtocol()); | |
| if (!message_is_valid) { | |
| if (RenderProcessHost* process_host = agent_host->GetProcessHost()) { | |
| bad_message::ReceivedBadMessage( | |
| process_host, bad_message::RFH_INCONSISTENT_DEVTOOLS_MESSAGE); | |
| } | |
| return; | |
| } | |
| client->DispatchProtocolMessage(agent_host, message_span); | |
| } | |
| void DevToolsSession::DispatchProtocolResponse( | |
| blink::mojom::DevToolsMessagePtr message, | |
| int call_id, | |
| blink::mojom::RendererOriginatingSessionStatePtr updates) { | |
| TRACE_EVENT("devtools", "DevToolsSession::DispatchProtocolResponse", | |
| perfetto::TerminatingFlow::ProcessScoped(call_id), "call_id", | |
| call_id); | |
| ApplySessionStateUpdates(std::move(updates)); | |
| auto it = waiting_for_response_.find(call_id); | |
| // TODO(johannes): Consider shutting down renderer instead of just | |
| // dropping the message. See shutdownForBadMessage(). | |
| if (it == waiting_for_response_.end()) | |
| return; | |
| pending_messages_.erase(it->second); | |
| waiting_for_response_.erase(it); | |
| DispatchProtocolResponseOrNotification(client_, agent_host_, | |
| std::move(message), session_id_, | |
| /*is_notification=*/false); | |
| // |this| may be deleted at this point. | |
| } | |
| void DevToolsSession::DispatchProtocolNotification( | |
| blink::mojom::DevToolsMessagePtr message, | |
| blink::mojom::RendererOriginatingSessionStatePtr updates) { | |
| ApplySessionStateUpdates(std::move(updates)); | |
| DispatchProtocolResponseOrNotification(client_, agent_host_, | |
| std::move(message), session_id_, | |
| /*is_notification=*/true); | |
| // |this| may be deleted at this point. | |
| } | |
| void DevToolsSession::DispatchOnClientHost(base::span<const uint8_t> message) { | |
| // |message| either comes from a web socket, in which case it's JSON. | |
| // Or it comes from another devtools_session, in which case it may be CBOR | |
| // already. We auto-detect and convert to what the client wants as needed. | |
| bool is_cbor_message = crdtp::cbor::IsCBORMessage(crdtp::SpanFrom(message)); | |
| if (client_->UsesBinaryProtocol() == is_cbor_message) { | |
| client_->DispatchProtocolMessage(agent_host_, message); | |
| return; | |
| } | |
| std::vector<uint8_t> converted; | |
| crdtp::Status status = | |
| client_->UsesBinaryProtocol() | |
| ? crdtp::json::ConvertJSONToCBOR(crdtp::SpanFrom(message), &converted) | |
| : crdtp::json::ConvertCBORToJSON(crdtp::SpanFrom(message), | |
| &converted); | |
| LOG_IF(ERROR, !status.ok()) << status.ToASCIIString(); | |
| client_->DispatchProtocolMessage(agent_host_, converted); | |
| // |this| may be deleted at this point. | |
| } | |
| void DevToolsSession::ConnectionClosed() { | |
| DevToolsAgentHostClient* client = client_; | |
| DevToolsAgentHostImpl* agent_host = agent_host_; | |
| agent_host->DetachInternal(this); | |
| // |this| is deleted here, do not use any fields below. | |
| client->AgentHostClosed(agent_host); | |
| } | |
| void DevToolsSession::ApplySessionStateUpdates( | |
| blink::mojom::RendererOriginatingSessionStatePtr updates) { | |
| if (!updates) | |
| return; | |
| if (!session_state_cookie_->renderer_originating_session_state) { | |
| session_state_cookie_->renderer_originating_session_state = | |
| blink::mojom::RendererOriginatingSessionState::New(); | |
| } | |
| for (auto& entry : updates->entries) { | |
| if (entry.second.has_value()) { | |
| session_state_cookie_->renderer_originating_session_state | |
| ->entries[entry.first] = std::move(*entry.second); | |
| } else { | |
| session_state_cookie_->renderer_originating_session_state->entries.erase( | |
| entry.first); | |
| } | |
| } | |
| } | |
| DevToolsSession* DevToolsSession::AttachChildSession( | |
| const std::string& session_id, | |
| DevToolsAgentHostImpl* agent_host, | |
| DevToolsAgentHostClient* client, | |
| Mode mode, | |
| base::OnceClosure resume_callback) { | |
| DCHECK(!agent_host->SessionByClient(client)); | |
| DCHECK(!root_session_); | |
| std::unique_ptr<DevToolsSession> session( | |
| new DevToolsSession(client, session_id, this, mode)); | |
| session->SetRuntimeResumeCallback(std::move(resume_callback)); | |
| DevToolsSession* session_ptr = session.get(); | |
| // If attach did not succeed, |session| is already destroyed. | |
| if (!agent_host->AttachInternal(std::move(session))) | |
| return nullptr; | |
| child_sessions_[session_id] = session_ptr; | |
| for (auto& observer : child_observers_) { | |
| observer.SessionAttached(*session_ptr); | |
| } | |
| return session_ptr; | |
| } | |
| void DevToolsSession::DetachChildSession(const std::string& session_id) { | |
| child_sessions_.erase(session_id); | |
| } | |
| bool DevToolsSession::HasChildSession(const std::string& session_id) { | |
| return child_sessions_.contains(session_id); | |
| } | |
| void DevToolsSession::AddObserver(ChildObserver* obs) { | |
| child_observers_.AddObserver(obs); | |
| for (auto& entry : child_sessions_) { | |
| obs->SessionAttached(*entry.second); | |
| } | |
| } | |
| void DevToolsSession::RemoveObserver(ChildObserver* obs) { | |
| child_observers_.RemoveObserver(obs); | |
| } | |
| void DevToolsSession::PrepareForReload(std::string script_to_evaluate_on_load) { | |
| session_state_cookie_->browser_originating_session_state | |
| ->script_to_evaluate_on_load_once = std::move(script_to_evaluate_on_load); | |
| io_session_->UnpauseAndTerminate(); | |
| } | |
| void DevToolsSession::EnableDurableMessageCollector( | |
| const base::UnguessableToken& devtools_token, | |
| network::mojom::NetworkDurableMessageConfigPtr config, | |
| base::OnceClosure callback) { | |
| CHECK(!root_session_); | |
| if (!durable_message_collector_.is_bound()) { | |
| content::GetNetworkService()->AddDurableMessageCollector( | |
| durable_message_collector_.BindNewPipeAndPassReceiver()); | |
| } | |
| durable_message_collector_->Configure(std::move(config), base::DoNothing()); | |
| durable_message_collector_->EnableForProfile(devtools_token, | |
| std::move(callback)); | |
| } | |
| void DevToolsSession::DisableDurableMessageCollectorForProfile( | |
| const base::UnguessableToken& devtools_token, | |
| base::OnceClosure callback) { | |
| CHECK(!root_session_); | |
| if (!durable_message_collector_.is_bound()) { | |
| std::move(callback).Run(); | |
| return; | |
| } | |
| durable_message_collector_->DisableForProfile(devtools_token, | |
| std::move(callback)); | |
| } | |
| network::mojom::DurableMessageCollector* | |
| DevToolsSession::MaybeGetDurableMessageCollector() { | |
| return durable_message_collector_.is_bound() | |
| ? durable_message_collector_.get() | |
| : nullptr; | |
| } | |
| DevToolsSession* DevToolsSession::GetSessionById(const std::string& session_id) { | |
| auto it = child_sessions_.find(session_id); | |
| return it == child_sessions_.end() ? nullptr : it->second.get(); | |
| } | |
| // static | |
| bool DevToolsSession::ValidateMessage(const std::string& expected_session_id, | |
| const bool expected_has_id, | |
| base::span<const uint8_t> message, | |
| bool expect_cbor) { | |
| std::vector<uint8_t> cbor_message; | |
| crdtp::span<uint8_t> span_message = crdtp::SpanFrom(message); | |
| const bool is_cbor = crdtp::cbor::IsCBORMessage(span_message); | |
| if (expect_cbor != is_cbor) { | |
| // The renderer has sent a message in the format different that we asked | |
| // for, something is fishy. | |
| return false; | |
| } | |
| if (!is_cbor) { | |
| // '\0' is definitely not allowed unescaped in JSON and is of particular | |
| // concern since it is used as a framing character for sending JSON via | |
| // the remote debugging pipe. | |
| if (std::ranges::find(span_message, '\0') != span_message.end()) { | |
| return false; | |
| } | |
| if (!crdtp::json::ConvertJSONToCBOR(span_message, &cbor_message).ok()) { | |
| return false; // Safely terminate renderer on malformed JSON | |
| } | |
| span_message = crdtp::SpanFrom(cbor_message); | |
| } else { | |
| // Perform top-level validation of CBOR envelope. | |
| crdtp::StatusOr<size_t> status_or_outer_size = | |
| crdtp::cbor::CheckCBORMessage(span_message); | |
| if (!status_or_outer_size.ok()) { | |
| DLOG(ERROR) << status_or_outer_size.status().ToASCIIString(); | |
| return false; | |
| } | |
| if (status_or_outer_size.value() != message.size()) { | |
| DLOG(ERROR) << "Unexpected trailing data in CBOR message from child"; | |
| return false; | |
| } | |
| } | |
| if (!expected_has_id && | |
| crdtp::cbor::HasKeyInMap(span_message, crdtp::SpanFrom("id"))) { | |
| DLOG(ERROR) << "Expected no id in the message but received one"; | |
| return false; | |
| } | |
| // Do NOT use crdtp::Dispatchable here. It enforces the presence of both | |
| // 'id' and 'method', which are not guaranteed in Responses and Notifications. | |
| crdtp::span<uint8_t> extracted_session_id = | |
| crdtp::cbor::GetString8ValueFromMap(span_message, | |
| crdtp::SpanFrom("sessionId")); | |
| if (expected_session_id.empty()) { | |
| if (!extracted_session_id.empty()) { | |
| DLOG(ERROR) << "Root session expected no sessionId but received one: " | |
| << (extracted_session_id.empty() | |
| ? "" | |
| : std::string(extracted_session_id.begin(), | |
| extracted_session_id.end())); | |
| return false; | |
| } | |
| // This additional check is necessary since GetString8ValueFromMap returns | |
| // the empty span when encountering duplicate entries. | |
| if (crdtp::cbor::HasKeyInMap(span_message, crdtp::SpanFrom("sessionId"))) { | |
| DLOG(ERROR) << "Root session expected no sessionId but received dupes."; | |
| return false; | |
| } | |
| return true; | |
| } | |
| if (extracted_session_id.empty() || | |
| !crdtp::SpanEquals(crdtp::SpanFrom(expected_session_id), | |
| extracted_session_id)) { | |
| DLOG(ERROR) << "Child session expected sessionId: " << expected_session_id | |
| << ", but got: " | |
| << (extracted_session_id.empty() | |
| ? "" | |
| : std::string(extracted_session_id.begin(), | |
| extracted_session_id.end())); | |
| return false; | |
| } | |
| return true; | |
| } | |
| } // namespace content |