// 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->data);
}

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) {
    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
