| // 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/protocol/target_handler.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <optional> |
| #include <string_view> |
| |
| #include "base/base64.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/unguessable_token.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "content/browser/devtools/browser_devtools_agent_host.h" |
| #include "content/browser/devtools/devtools_agent_host_impl.h" |
| #include "content/browser/devtools/devtools_manager.h" |
| #include "content/browser/devtools/protocol/browser_handler.h" |
| #include "content/browser/devtools/protocol/target_auto_attacher.h" |
| #include "content/browser/devtools/render_frame_devtools_agent_host.h" |
| #include "content/browser/devtools/web_contents_devtools_agent_host.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/cors_origin_pattern_setter.h" |
| #include "content/public/browser/devtools_agent_host_client.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "url/url_constants.h" |
| |
| namespace content::protocol { |
| |
| namespace { |
| |
| constexpr net::NetworkTrafficAnnotationTag |
| kSettingsProxyConfigTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("devtools_proxy_config", R"( |
| semantics { |
| sender: "Proxy Configuration over Developer Tools" |
| description: |
| "Used to fetch HTTP/HTTPS/SOCKS5/PAC proxy configuration when " |
| "proxy is configured by DevTools. It is equivalent to the one " |
| "configured via the --proxy-server command line flag. " |
| "When proxy implies automatic configuration, it can send network " |
| "requests in the scope of this annotation." |
| trigger: |
| "Whenever a network request is made when the system proxy settings " |
| "are used, and they indicate to use a proxy server." |
| data: |
| "Proxy configuration." |
| destination: OTHER |
| destination_other: "The proxy server specified in the configuration." |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be disabled in settings. However it will never " |
| "be made if user does not run with '--remote-debugging-*' switches " |
| "and does not explicitly send this data over Chrome remote debugging." |
| policy_exception_justification: |
| "Not implemented, only used in DevTools and is behind a switch." |
| })"); |
| |
| static const char kNotAllowedError[] = "Not allowed"; |
| static const char kMethod[] = "method"; |
| static const char kResumeMethod[] = "Runtime.runIfWaitingForDebugger"; |
| |
| static const char kInitializerScript[] = R"( |
| (function() { |
| const bindingName = "%s"; |
| const binding = window[bindingName]; |
| delete window[bindingName]; |
| if (window.self === window.top) { |
| window[bindingName] = { |
| onmessage: () => {}, |
| send: binding |
| }; |
| } |
| })(); |
| )"; |
| |
| static const char kTargetNotFound[] = "No target with given id found"; |
| |
| std::unique_ptr<Target::TargetInfo> BuildTargetInfo( |
| DevToolsAgentHost* agent_host) { |
| auto* host = static_cast<DevToolsAgentHostImpl*>(agent_host); |
| std::unique_ptr<Target::TargetInfo> target_info = |
| Target::TargetInfo::Create() |
| .SetTargetId(host->GetId()) |
| .SetTitle(host->GetTitle()) |
| .SetUrl(host->GetURL().spec()) |
| .SetType(host->GetType()) |
| .SetAttached(host->IsAttached()) |
| .SetCanAccessOpener(host->CanAccessOpener()) |
| .Build(); |
| if (!host->GetOpenerId().empty()) { |
| target_info->SetOpenerId(host->GetOpenerId()); |
| } |
| if (!host->GetOpenerFrameId().empty()) { |
| target_info->SetOpenerFrameId(host->GetOpenerFrameId()); |
| } |
| if (!host->GetParentFrameId().empty()) { |
| target_info->SetParentFrameId(host->GetParentFrameId()); |
| } |
| if (host->GetBrowserContext()) { |
| target_info->SetBrowserContextId(host->GetBrowserContext()->UniqueId()); |
| } |
| std::string subtype = host->GetSubtype(); |
| if (!subtype.empty()) { |
| target_info->SetSubtype(subtype); |
| } |
| return target_info; |
| } |
| |
| static std::string TerminationStatusToString(base::TerminationStatus status) { |
| switch (status) { |
| case base::TERMINATION_STATUS_NORMAL_TERMINATION: |
| return "normal"; |
| case base::TERMINATION_STATUS_ABNORMAL_TERMINATION: |
| return "abnormal"; |
| case base::TERMINATION_STATUS_PROCESS_WAS_KILLED: |
| return "killed"; |
| case base::TERMINATION_STATUS_PROCESS_CRASHED: |
| return "crashed"; |
| case base::TERMINATION_STATUS_STILL_RUNNING: |
| return "still running"; |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Used for the case when oom-killer kills a process on ChromeOS. |
| case base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM: |
| return "oom killed"; |
| #endif |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android processes are spawned from the system Zygote and we do not get |
| // the termination status. We can't know if the termination was a crash or |
| // an oom kill for sure: but we can use status of the strong process |
| // bindings as a hint. |
| case base::TERMINATION_STATUS_OOM_PROTECTED: |
| return "oom protected"; |
| #endif |
| case base::TERMINATION_STATUS_LAUNCH_FAILED: |
| return "failed to launch"; |
| case base::TERMINATION_STATUS_OOM: |
| return "oom"; |
| #if BUILDFLAG(IS_WIN) |
| case base::TERMINATION_STATUS_INTEGRITY_FAILURE: |
| return "integrity failure"; |
| #endif |
| case base::TERMINATION_STATUS_MAX_ENUM: |
| break; |
| } |
| NOTREACHED() << "Unknown Termination Status."; |
| } |
| |
| class BrowserToPageConnector; |
| |
| // Contains permissions for the instances of BrowserConnectorHostClient. |
| // Currently, only permissions for |
| // DevToolsAgentHostClient::AllowUnsafeOperations are implemented. |
| struct BrowserConnectorHostClientPermissions { |
| // Defines what DevToolsAgentHostClient::AllowUnsafeOperations |
| // returns for BrowserConnectorHostClient instances. See |
| // DevToolsAgentHostClient::AllowUnsafeOperations for more details. |
| bool allow_unsafe_operations = false; |
| }; |
| |
| class BrowserToPageConnector { |
| public: |
| class BrowserConnectorHostClient : public DevToolsAgentHostClient { |
| public: |
| BrowserConnectorHostClient( |
| BrowserToPageConnector* connector, |
| DevToolsAgentHost* host, |
| const BrowserConnectorHostClientPermissions& permissions) |
| : connector_(connector), permissions_(permissions) { |
| // TODO(dgozman): handle return value of AttachClient. |
| host->AttachClient(this); |
| } |
| |
| BrowserConnectorHostClient(const BrowserConnectorHostClient&) = delete; |
| BrowserConnectorHostClient& operator=(const BrowserConnectorHostClient&) = |
| delete; |
| |
| void DispatchProtocolMessage(DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) override { |
| connector_->DispatchProtocolMessage(agent_host, message); |
| } |
| void AgentHostClosed(DevToolsAgentHost* agent_host) override { |
| connector_->AgentHostClosed(agent_host); |
| } |
| |
| bool AllowUnsafeOperations() override { |
| return permissions_.allow_unsafe_operations; |
| } |
| |
| private: |
| raw_ptr<BrowserToPageConnector> connector_; |
| BrowserConnectorHostClientPermissions permissions_; |
| }; |
| |
| BrowserToPageConnector( |
| const std::string& binding_name, |
| DevToolsAgentHost* page_host, |
| BrowserConnectorHostClientPermissions permissions, |
| std::unique_ptr<Target::Backend::ExposeDevToolsProtocolCallback> callback) |
| : binding_name_(binding_name), |
| page_host_(page_host), |
| pending_callback_(std::move(callback)) { |
| browser_host_ = BrowserDevToolsAgentHost::CreateForDiscovery(); |
| browser_host_client_ = std::make_unique<BrowserConnectorHostClient>( |
| this, browser_host_.get(), permissions); |
| page_host_client_ = std::make_unique<BrowserConnectorHostClient>( |
| this, page_host_.get(), BrowserConnectorHostClientPermissions()); |
| |
| SendProtocolMessageToPage("Page.enable", base::Value()); |
| SendProtocolMessageToPage("Runtime.enable", base::Value()); |
| |
| base::Value::Dict add_binding_params; |
| add_binding_params.Set("name", binding_name); |
| SendProtocolMessageToPage("Runtime.addBinding", |
| base::Value(std::move(add_binding_params))); |
| |
| std::string initializer_script = |
| base::StringPrintf(kInitializerScript, binding_name.c_str()); |
| |
| base::Value::Dict params; |
| params.Set("scriptSource", initializer_script); |
| SendProtocolMessageToPage("Page.addScriptToEvaluateOnLoad", |
| base::Value(std::move(params))); |
| |
| base::Value::Dict evaluate_params; |
| evaluate_params.Set("expression", initializer_script); |
| pending_request_id_ = SendProtocolMessageToPage( |
| "Runtime.evaluate", base::Value(std::move(evaluate_params))); |
| GetInstanceMap()[page_host_.get()].reset(this); |
| } |
| |
| BrowserToPageConnector(const BrowserToPageConnector&) = delete; |
| BrowserToPageConnector& operator=(const BrowserToPageConnector&) = delete; |
| |
| using BrowserToPageConnectorMap = |
| base::flat_map<DevToolsAgentHost*, |
| std::unique_ptr<BrowserToPageConnector>>; |
| static BrowserToPageConnectorMap& GetInstanceMap() { |
| static base::NoDestructor<BrowserToPageConnectorMap> map; |
| return *map; |
| } |
| |
| private: |
| int SendProtocolMessageToPage(const char* method, base::Value params) { |
| base::Value::Dict message_dict; |
| int id = page_message_id_++; |
| message_dict.Set("id", id); |
| message_dict.Set("method", method); |
| message_dict.Set("params", std::move(params)); |
| base::Value message(std::move(message_dict)); |
| std::string json_message; |
| base::JSONWriter::Write(message, &json_message); |
| page_host_->DispatchProtocolMessage(page_host_client_.get(), |
| base::as_byte_span(json_message)); |
| return id; |
| } |
| |
| void DispatchProtocolMessage(DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) { |
| std::string_view message_sp(reinterpret_cast<const char*>(message.data()), |
| message.size()); |
| if (agent_host == page_host_.get()) { |
| std::optional<base::Value::Dict> value = |
| base::JSONReader::ReadDict(message_sp); |
| if (!value) { |
| return; |
| } |
| |
| std::optional<int> id = value->FindInt("id"); |
| if (id && *id == pending_request_id_ && pending_callback_) { |
| pending_callback_->sendSuccess(); |
| pending_callback_.reset(); |
| pending_request_id_ = -1; |
| return; |
| } |
| |
| // Make sure this is a binding call. |
| const std::string* method = value->FindString("method"); |
| if (!method || *method != "Runtime.bindingCalled") { |
| return; |
| } |
| |
| const base::Value::Dict* params = value->FindDict("params"); |
| if (!params) { |
| return; |
| } |
| |
| const std::string* name = params->FindString("name"); |
| if (!name || *name != binding_name_) { |
| return; |
| } |
| |
| const std::string* payload = params->FindString("payload"); |
| if (!payload) { |
| return; |
| } |
| browser_host_->DispatchProtocolMessage(browser_host_client_.get(), |
| base::as_byte_span(*payload)); |
| return; |
| } |
| DCHECK(agent_host == browser_host_.get()); |
| |
| std::string encoded = base::Base64Encode(message_sp); |
| std::string eval_code = |
| "try { window." + binding_name_ + ".onmessage(atob(\""; |
| std::string eval_suffix = "\")); } catch(e) { console.error(e); }"; |
| eval_code.reserve(eval_code.size() + encoded.size() + eval_suffix.size()); |
| eval_code.append(encoded); |
| eval_code.append(eval_suffix); |
| |
| base::Value::Dict params; |
| params.Set("expression", std::move(eval_code)); |
| SendProtocolMessageToPage("Runtime.evaluate", |
| base::Value(std::move(params))); |
| } |
| |
| void AgentHostClosed(DevToolsAgentHost* agent_host) { |
| if (agent_host == browser_host_.get()) { |
| page_host_->DetachClient(page_host_client_.get()); |
| } else { |
| DCHECK(agent_host == page_host_.get()); |
| browser_host_->DetachClient(browser_host_client_.get()); |
| } |
| GetInstanceMap().erase(page_host_.get()); |
| } |
| |
| std::string binding_name_; |
| scoped_refptr<DevToolsAgentHost> browser_host_; |
| scoped_refptr<DevToolsAgentHost> page_host_; |
| std::unique_ptr<BrowserConnectorHostClient> browser_host_client_; |
| std::unique_ptr<BrowserConnectorHostClient> page_host_client_; |
| int page_message_id_ = 0; |
| std::unique_ptr<Target::Backend::ExposeDevToolsProtocolCallback> |
| pending_callback_; |
| int pending_request_id_ = -1; |
| }; |
| |
| } // namespace |
| |
| // Throttle is owned externally by the navigation subsystem. |
| class TargetHandler::Throttle : public NavigationThrottle { |
| public: |
| Throttle(const Throttle&) = delete; |
| Throttle& operator=(const Throttle&) = delete; |
| |
| ~Throttle() override { CleanupPointers(); } |
| TargetAutoAttacher* auto_attacher() const { return auto_attacher_; } |
| void Clear(); |
| // NavigationThrottle implementation: |
| const char* GetNameForLogging() override; |
| |
| protected: |
| Throttle(base::WeakPtr<protocol::TargetHandler> target_handler, |
| TargetAutoAttacher* auto_attacher, |
| NavigationThrottleRegistry& registry) |
| : NavigationThrottle(registry), |
| target_handler_(target_handler), |
| auto_attacher_(auto_attacher) { |
| target_handler->throttles_.insert(this); |
| } |
| void SetThrottledAgentHost(DevToolsAgentHost* agent_host); |
| |
| bool is_deferring_ = false; |
| scoped_refptr<DevToolsAgentHost> agent_host_; |
| base::WeakPtr<protocol::TargetHandler> target_handler_; |
| |
| private: |
| void CleanupPointers(); |
| raw_ptr<TargetAutoAttacher> auto_attacher_; |
| }; |
| |
| class TargetHandler::ResponseThrottle : public TargetHandler::Throttle { |
| public: |
| ResponseThrottle(base::WeakPtr<protocol::TargetHandler> target_handler, |
| TargetAutoAttacher* auto_attacher, |
| NavigationThrottleRegistry& registry) |
| : Throttle(target_handler, auto_attacher, registry) {} |
| ~ResponseThrottle() override = default; |
| |
| private: |
| // NavigationThrottle implementation: |
| ThrottleCheckResult WillProcessResponse() override { return MaybeThrottle(); } |
| |
| ThrottleCheckResult WillFailRequest() override { return MaybeThrottle(); } |
| |
| ThrottleCheckResult MaybeThrottle() { |
| if (target_handler_ && auto_attacher()) { |
| NavigationRequest* request = NavigationRequest::From(navigation_handle()); |
| const bool wait_for_debugger_on_start = |
| target_handler_->ShouldWaitForDebuggerOnStart(request); |
| scoped_refptr<RenderFrameDevToolsAgentHost> new_host = |
| auto_attacher()->HandleNavigation(request, |
| wait_for_debugger_on_start); |
| if (new_host && |
| target_handler_->AutoAttach(auto_attacher(), new_host.get(), |
| wait_for_debugger_on_start) && |
| wait_for_debugger_on_start) { |
| SetThrottledAgentHost(new_host.get()); |
| } else { |
| SetThrottledAgentHost(nullptr); |
| } |
| } |
| is_deferring_ = !!agent_host_; |
| return is_deferring_ ? DEFER : PROCEED; |
| } |
| }; |
| |
| class TargetHandler::RequestThrottle : public TargetHandler::Throttle { |
| public: |
| RequestThrottle(base::WeakPtr<protocol::TargetHandler> target_handler, |
| NavigationThrottleRegistry& registry, |
| DevToolsAgentHost* throttled_agent_host) |
| : Throttle(target_handler, target_handler->auto_attacher_, registry) { |
| SetThrottledAgentHost(throttled_agent_host); |
| } |
| ~RequestThrottle() override = default; |
| |
| private: |
| // NavigationThrottle implementation: |
| ThrottleCheckResult WillStartRequest() override { |
| is_deferring_ = !!agent_host_; |
| return is_deferring_ ? DEFER : PROCEED; |
| } |
| }; |
| |
| class TargetHandler::Session : public DevToolsAgentHostClient { |
| public: |
| static std::string Attach(TargetHandler* handler, |
| DevToolsAgentHost* agent_host, |
| bool waiting_for_debugger, |
| bool flatten_protocol) { |
| std::string id = base::UnguessableToken::Create().ToString(); |
| // We don't support or allow the non-flattened protocol when in binary mode. |
| // So, we coerce the setting to true, as the non-flattened mode is |
| // deprecated anyway. |
| if (handler->root_session_->GetClient()->UsesBinaryProtocol()) { |
| flatten_protocol = true; |
| } |
| Session* session = new Session(handler, agent_host, id, flatten_protocol); |
| handler->attached_sessions_[id].reset(session); |
| DevToolsAgentHostImpl* agent_host_impl = |
| static_cast<DevToolsAgentHostImpl*>(agent_host); |
| if (flatten_protocol) { |
| using Mode = DevToolsSession::Mode; |
| const Mode mode = |
| agent_host_impl->GetSessionMode() == Mode::kSupportsTabTarget |
| ? Mode::kSupportsTabTarget |
| : handler->session_mode_; |
| |
| base::OnceClosure resume_callback; |
| if (waiting_for_debugger) { |
| resume_callback = base::BindOnce(&Session::ResumeIfThrottled, |
| base::Unretained(session)); |
| } |
| DevToolsSession* devtools_session = |
| handler->root_session_->AttachChildSession( |
| id, agent_host_impl, session, mode, std::move(resume_callback)); |
| session->devtools_session_ = devtools_session; |
| } else { |
| agent_host_impl->AttachClient(session); |
| } |
| handler->frontend_->AttachedToTarget(id, BuildTargetInfo(agent_host), |
| waiting_for_debugger); |
| return id; |
| } |
| |
| Session(const Session&) = delete; |
| Session& operator=(const Session&) = delete; |
| |
| ~Session() override { |
| if (!agent_host_) { |
| return; |
| } |
| if (flatten_protocol_) { |
| handler_->root_session_->DetachChildSession(id_); |
| } |
| agent_host_->DetachClient(this); |
| } |
| |
| std::string GetTypeForMetrics() override { return "DevTools"; } |
| |
| void Detach(bool host_closed) { |
| handler_->frontend_->DetachedFromTarget(id_, agent_host_->GetId()); |
| if (flatten_protocol_) { |
| handler_->root_session_->DetachChildSession(id_); |
| } |
| if (!host_closed) { |
| agent_host_->DetachClient(this); |
| } |
| handler_->auto_attached_sessions_.erase(agent_host_.get()); |
| devtools_session_ = nullptr; |
| agent_host_ = nullptr; |
| handler_->attached_sessions_.erase(id_); |
| } |
| |
| bool IsWaitingForDebuggerOnStart() const { |
| return devtools_session_ && |
| devtools_session_->IsWaitingForDebuggerOnStart(); |
| } |
| |
| void ResumeSendingMessagesToAgent() const { |
| if (devtools_session_) { |
| devtools_session_->ResumeSendingMessagesToAgent(); |
| } |
| } |
| |
| void SetThrottle(Throttle* throttle) { throttle_ = throttle; } |
| void SetWorkerThrottle( |
| scoped_refptr<DevToolsThrottleHandle> worker_throttle) { |
| worker_throttle_ = std::move(worker_throttle); |
| } |
| |
| void ResumeIfThrottled() { |
| if (throttle_) { |
| throttle_->Clear(); |
| } |
| worker_throttle_.reset(); |
| } |
| |
| void SendMessageToAgentHost(base::span<const uint8_t> message) { |
| // This method is only used in the non-flat mode, it's the implementation |
| // for Target.SendMessageToTarget. And since the binary mode implies |
| // flatten_protocol_ (we force the flag to true), we can assume in this |
| // method that |message| is JSON. |
| DCHECK(!flatten_protocol_); |
| |
| if (throttle_ || worker_throttle_) { |
| std::optional<base::Value::Dict> value = |
| base::JSONReader::ReadDict(std::string_view( |
| reinterpret_cast<const char*>(message.data()), message.size())); |
| const std::string* method; |
| if (value && (method = value->FindString(kMethod)) && |
| *method == kResumeMethod) { |
| ResumeIfThrottled(); |
| } |
| } |
| |
| agent_host_->DispatchProtocolMessage(this, message); |
| } |
| |
| bool IsAttachedTo(const std::string& target_id) { |
| return agent_host_->GetId() == target_id; |
| } |
| |
| bool UsesBinaryProtocol() override { |
| return handler_->root_session_->GetClient()->UsesBinaryProtocol(); |
| } |
| |
| private: |
| friend class TargetHandler; |
| |
| Session(TargetHandler* handler, |
| DevToolsAgentHost* agent_host, |
| const std::string& id, |
| bool flatten_protocol) |
| : handler_(handler), |
| agent_host_(agent_host), |
| id_(id), |
| flatten_protocol_(flatten_protocol) {} |
| |
| // DevToolsAgentHostClient implementation. |
| void DispatchProtocolMessage(DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) override { |
| DCHECK(agent_host == agent_host_.get()); |
| if (flatten_protocol_) { |
| // TODO(johannes): It's not clear that this check is useful, but |
| // a similar check has been in the code ever since the flattened protocol |
| // was introduced. Try a DCHECK instead and possibly remove the check. |
| if (!handler_->root_session_->HasChildSession(id_)) { |
| return; |
| } |
| GetRootClient()->DispatchProtocolMessage( |
| handler_->root_session_->GetAgentHost(), message); |
| return; |
| } |
| // TODO(johannes): For now, We need to copy here because |
| // ReceivedMessageFromTarget is generated code and we're using const |
| // std::string& for such parameters. Perhaps we should switch this to |
| // std::string_view? |
| std::string message_copy(message.begin(), message.end()); |
| handler_->frontend_->ReceivedMessageFromTarget(id_, message_copy, |
| agent_host_->GetId()); |
| } |
| |
| void AgentHostClosed(DevToolsAgentHost* agent_host) override { |
| DCHECK(agent_host == agent_host_.get()); |
| Detach(true); |
| } |
| |
| bool MayAttachToURL(const GURL& url, bool is_webui) override { |
| return GetRootClient()->MayAttachToURL(url, is_webui); |
| } |
| |
| bool IsTrusted() override { return GetRootClient()->IsTrusted(); } |
| |
| bool MayReadLocalFiles() override { |
| return GetRootClient()->MayReadLocalFiles(); |
| } |
| |
| bool MayWriteLocalFiles() override { |
| return GetRootClient()->MayWriteLocalFiles(); |
| } |
| |
| bool AllowUnsafeOperations() override { |
| return GetRootClient()->AllowUnsafeOperations(); |
| } |
| |
| DevToolsAgentHostClient* GetRootClient() { |
| return handler_->root_session_->GetClient(); |
| } |
| |
| raw_ptr<TargetHandler> handler_; |
| scoped_refptr<DevToolsAgentHost> agent_host_; |
| std::string id_; |
| bool flatten_protocol_; |
| raw_ptr<DevToolsSession, DanglingUntriaged> devtools_session_ = nullptr; |
| raw_ptr<Throttle> throttle_ = nullptr; |
| scoped_refptr<DevToolsThrottleHandle> worker_throttle_; |
| // This is needed to identify sessions associated with given |
| // AutoAttacher to properly support SetAttachedTargetsOfType() |
| // for a TargetHandler that serves as a client to multiple |
| // different TargetAttachers. We don't want a pointer here, |
| // because a session may survive the source AutoAttacher. |
| uintptr_t auto_attacher_id_ = 0; |
| }; |
| |
| void TargetHandler::Throttle::CleanupPointers() { |
| if (target_handler_ && agent_host_) { |
| auto it = target_handler_->auto_attached_sessions_.find(agent_host_.get()); |
| if (it != target_handler_->auto_attached_sessions_.end()) { |
| it->second->SetThrottle(nullptr); |
| } |
| } |
| if (target_handler_) { |
| target_handler_->throttles_.erase(this); |
| target_handler_ = nullptr; |
| } |
| } |
| |
| void TargetHandler::Throttle::SetThrottledAgentHost( |
| DevToolsAgentHost* agent_host) { |
| agent_host_ = agent_host; |
| if (agent_host_) { |
| target_handler_->auto_attached_sessions_[agent_host_.get()]->SetThrottle( |
| this); |
| } |
| } |
| |
| const char* TargetHandler::Throttle::GetNameForLogging() { |
| return "DevToolsTargetNavigationThrottle"; |
| } |
| |
| void TargetHandler::Throttle::Clear() { |
| CleanupPointers(); |
| agent_host_ = nullptr; |
| auto_attacher_ = nullptr; |
| if (is_deferring_) { |
| is_deferring_ = false; |
| Resume(); |
| // DO NOT ADD CODE after this. The callback above might have destroyed the |
| // NavigationHandle that owns this NavigationThrottle. |
| } |
| } |
| |
| class TargetHandler::TargetFilter { |
| public: |
| using Filter = std::vector<std::unique_ptr<protocol::Target::FilterEntry>>; |
| |
| static std::unique_ptr<TargetFilter> CreateDefault() { |
| Filter default_filter; |
| // - Exclude `browser`. |
| default_filter.push_back(protocol::Target::FilterEntry::Create() |
| .SetExclude(true) |
| .SetType(DevToolsAgentHost::kTypeBrowser) |
| .Build()); |
| // - Exclude `tab`. |
| default_filter.push_back(protocol::Target::FilterEntry::Create() |
| .SetExclude(true) |
| .SetType(DevToolsAgentHost::kTypeTab) |
| .Build()); |
| // - Allow everything else. |
| default_filter.push_back(protocol::Target::FilterEntry::Create().Build()); |
| return base::WrapUnique(new TargetFilter(std::move(default_filter))); |
| } |
| static std::unique_ptr<TargetFilter> Create(std::unique_ptr<Filter> filter) { |
| if (!filter) { |
| return CreateDefault(); |
| } |
| return base::WrapUnique(new TargetFilter(std::move(*filter))); |
| } |
| |
| bool Match(DevToolsAgentHost& host) const { return Match(host.GetType()); } |
| |
| bool Match(std::string_view type) const { |
| for (const auto& entry : entries_) { |
| if (!entry->HasType() || entry->GetType("") == type) { |
| return !entry->GetExclude(false); |
| } |
| } |
| return false; |
| } |
| |
| private: |
| explicit TargetFilter(Filter entries) : entries_(std::move(entries)) {} |
| |
| const Filter entries_; |
| }; |
| |
| TargetHandler::TargetHandler(AccessMode access_mode, |
| const std::string& owner_target_id, |
| TargetAutoAttacher* auto_attacher, |
| DevToolsSession* session) |
| : DevToolsDomainHandler(Target::Metainfo::domainName), |
| access_mode_(access_mode), |
| owner_target_id_(owner_target_id), |
| session_mode_(session->session_mode()), |
| root_session_(session->GetRootSession()), |
| auto_attacher_(auto_attacher) {} |
| |
| TargetHandler::~TargetHandler() = default; |
| |
| // static |
| std::vector<TargetHandler*> TargetHandler::ForAgentHost( |
| DevToolsAgentHostImpl* host) { |
| return host->HandlersByName<TargetHandler>(Target::Metainfo::domainName); |
| } |
| |
| void TargetHandler::Wire(UberDispatcher* dispatcher) { |
| frontend_ = std::make_unique<Target::Frontend>(dispatcher->channel()); |
| Target::Dispatcher::wire(dispatcher, this); |
| } |
| |
| Response TargetHandler::Disable() { |
| SetAutoAttachInternal(false, false, false, base::DoNothing()); |
| SetDiscoverTargets(false, {}); |
| hidden_target_manager_.Clear(); |
| auto_attached_sessions_.clear(); |
| attached_sessions_.clear(); |
| |
| DevToolsManagerDelegate* delegate = |
| DevToolsManager::GetInstance()->delegate(); |
| if (!delegate) { |
| return Response::Success(); |
| } |
| |
| if (dispose_on_detach_context_ids_.size()) { |
| for (auto* context : delegate->GetBrowserContexts()) { |
| if (!dispose_on_detach_context_ids_.contains(context->UniqueId())) { |
| continue; |
| } |
| delegate->DisposeBrowserContext(context, base::DoNothing()); |
| } |
| dispose_on_detach_context_ids_.clear(); |
| } |
| contexts_with_overridden_proxy_.clear(); |
| return Response::Success(); |
| } |
| |
| void TargetHandler::MaybeCreateAndAddNavigationThrottle( |
| TargetAutoAttacher* auto_attacher, |
| NavigationThrottleRegistry& registry) { |
| DCHECK(auto_attach_ || !auto_attach_related_targets_.empty()); |
| auto* navigation_handle = ®istry.GetNavigationHandle(); |
| FrameTreeNode* frame_tree_node = |
| NavigationRequest::From(navigation_handle)->frame_tree_node(); |
| DCHECK(access_mode_ != AccessMode::kBrowser || |
| !auto_attach_related_targets_.empty() || !frame_tree_node->parent()); |
| // All child frames start navigating with their parent settings applied and |
| // are only throttled at response where we know if they require a new host. |
| // Note that fenced frames start as remote frames right away and get a RFDTAH |
| // of their own, so they require a RequestThrottle rather than a Response one. |
| if (!frame_tree_node->IsMainFrame()) { |
| registry.AddThrottle(std::make_unique<ResponseThrottle>( |
| weak_factory_.GetWeakPtr(), auto_attacher, registry)); |
| return; |
| } |
| // If we got here for main frame, it must be either browser or tab target. |
| DCHECK(auto_attacher == auto_attacher_); |
| DevToolsAgentHost* host = |
| RenderFrameDevToolsAgentHost::GetFor(frame_tree_node); |
| TargetHandler::Session* waiting_session = FindWaitingSession(host); |
| if (waiting_session) { |
| // RFDTAHs created during auto-attach had no renderer allocated originally, |
| // and hence have messages paused, but with navigation we're supposed to |
| // have a live host, so we can send messages to renderer now. |
| DCHECK(frame_tree_node->current_frame_host()->IsRenderFrameLive()); |
| // Only resume sending messages to frame agents (i.e. skip for WebContents |
| // ones). |
| waiting_session->ResumeSendingMessagesToAgent(); |
| } else { |
| // Currently, either RFDTAH or WCDTAH may be waiting for debugger (when |
| // `waitForDebuggerOnStart` is honored for the Tab target, it is ignored |
| // for the Page target), so in case no Page-level sessions are waiting, |
| // also check the tab target. |
| host = WebContentsDevToolsAgentHost::GetFor( |
| WebContentsImpl::FromFrameTreeNode(frame_tree_node)); |
| waiting_session = FindWaitingSession(host); |
| if (!waiting_session) { |
| return; |
| } |
| } |
| // window.open() navigations are throttled on the renderer side and the main |
| // request will not be sent until runIfWaitingForDebugger is received from |
| // the client, so there is no need to throttle the navigation in the |
| // browser. |
| // |
| // New window navigations (such as ctrl+click) should be throttled before |
| // the main request is sent to apply user agent and other overrides. |
| if (frame_tree_node->opener()) { |
| return; |
| } |
| registry.AddThrottle(std::make_unique<RequestThrottle>( |
| weak_factory_.GetWeakPtr(), registry, host)); |
| } |
| |
| TargetHandler::Session* TargetHandler::FindWaitingSession( |
| DevToolsAgentHost* host) { |
| if (!host) { |
| return nullptr; |
| } |
| auto it = auto_attached_sessions_.find(host); |
| if (it == auto_attached_sessions_.end()) { |
| return nullptr; |
| } |
| if (!it->second->IsWaitingForDebuggerOnStart()) { |
| return nullptr; |
| } |
| return it->second; |
| } |
| |
| void TargetHandler::ClearThrottles() { |
| base::flat_set<raw_ptr<Throttle, CtnExperimental>> copy(throttles_); |
| for (Throttle* throttle : copy) { |
| throttle->Clear(); |
| } |
| throttles_.clear(); |
| } |
| |
| void TargetHandler::SetAutoAttachInternal(bool auto_attach, |
| bool wait_for_debugger_on_start, |
| bool flatten, |
| base::OnceClosure callback) { |
| for (auto& entry : auto_attach_related_targets_) { |
| entry.first->RemoveClient(this); |
| } |
| auto_attach_related_targets_.clear(); |
| flatten_auto_attach_ = flatten; |
| if (auto_attach_) { |
| auto_attacher_->RemoveClient(this); |
| } |
| auto_attach_ = auto_attach; |
| wait_for_debugger_on_start_ = wait_for_debugger_on_start; |
| if (auto_attach_) { |
| auto_attacher_->AddClient(this, wait_for_debugger_on_start, |
| std::move(callback)); |
| } else { |
| while (!auto_attached_sessions_.empty()) { |
| auto_attached_sessions_.begin()->second->Detach(false); |
| } |
| ClearThrottles(); |
| auto_attach_target_filter_.reset(); |
| std::move(callback).Run(); |
| } |
| } |
| |
| void TargetHandler::UpdateAgentHostObserver() { |
| if (discover() == observing_agent_hosts_) { |
| return; |
| } |
| observing_agent_hosts_ = discover(); |
| if (observing_agent_hosts_) { |
| DevToolsAgentHost::AddObserver(this); |
| } else { |
| DevToolsAgentHost::RemoveObserver(this); |
| } |
| } |
| |
| bool TargetHandler::AutoAttach(TargetAutoAttacher* source, |
| DevToolsAgentHost* host, |
| bool waiting_for_debugger) { |
| DCHECK(host); |
| DCHECK(auto_attach_target_filter_); |
| if (!auto_attach_target_filter_->Match(*host)) { |
| return false; |
| } |
| if (base::Contains(auto_attached_sessions_, host)) { |
| return false; |
| } |
| if (!auto_attach_service_workers_ && |
| host->GetType() == DevToolsAgentHost::kTypeServiceWorker) { |
| return false; |
| } |
| std::string session_id = |
| Session::Attach(this, host, waiting_for_debugger, flatten_auto_attach_); |
| Session* session = attached_sessions_[session_id].get(); |
| session->auto_attacher_id_ = reinterpret_cast<uintptr_t>(source); |
| auto_attached_sessions_[host] = session; |
| return true; |
| } |
| |
| void TargetHandler::AutoDetach(TargetAutoAttacher* source, |
| DevToolsAgentHost* host) { |
| auto it = auto_attached_sessions_.find(host); |
| if (it == auto_attached_sessions_.end()) { |
| return; |
| } |
| it->second->Detach(false); |
| } |
| |
| void TargetHandler::SetAttachedTargetsOfType( |
| TargetAutoAttacher* source, |
| const base::flat_set<scoped_refptr<DevToolsAgentHost>>& new_hosts, |
| const std::string& type) { |
| DCHECK(!type.empty()); |
| auto old_sessions = auto_attached_sessions_; |
| for (auto& entry : old_sessions) { |
| scoped_refptr<DevToolsAgentHost> host(entry.first); |
| if (host->GetType() == type && |
| entry.second->auto_attacher_id_ == |
| reinterpret_cast<uintptr_t>(source) && |
| !base::Contains(new_hosts, host)) { |
| AutoDetach(source, host.get()); |
| } |
| } |
| for (auto& host : new_hosts) { |
| if (!base::Contains(old_sessions, host.get())) { |
| AutoAttach(source, host.get(), false); |
| } |
| } |
| } |
| |
| void TargetHandler::TargetInfoChanged(DevToolsAgentHost* host) { |
| // Only send target info for targets we reported in any way. |
| if (!base::Contains(reported_hosts_, host) && |
| auto_attached_sessions_.find(host) == auto_attached_sessions_.end()) { |
| return; |
| } |
| frontend_->TargetInfoChanged(BuildTargetInfo(host)); |
| } |
| |
| void TargetHandler::AutoAttacherDestroyed(TargetAutoAttacher* auto_attacher) { |
| auto throttles = throttles_; |
| for (Throttle* throttle : throttles_) { |
| if (throttle->auto_attacher() == auto_attacher) { |
| throttle->Clear(); |
| } |
| } |
| for (auto& entry : auto_attached_sessions_) { |
| if (entry.second->auto_attacher_id_ == |
| reinterpret_cast<uintptr_t>(auto_attacher)) { |
| entry.second->auto_attacher_id_ = 0; |
| } |
| } |
| auto_attach_related_targets_.erase(auto_attacher); |
| } |
| |
| bool TargetHandler::ShouldWaitForDebuggerOnStart( |
| NavigationRequest* navigation_request) const { |
| if (auto_attach_) { |
| return wait_for_debugger_on_start_; |
| } |
| DCHECK(!auto_attach_related_targets_.empty()); |
| auto* host = RenderFrameDevToolsAgentHost::GetFor( |
| navigation_request->frame_tree_node()); |
| if (!host) { |
| return false; |
| } |
| auto it = auto_attach_related_targets_.find(host->auto_attacher()); |
| return it != auto_attach_related_targets_.end() && it->second; |
| } |
| |
| bool TargetHandler::ShouldThrottlePopups() const { |
| return auto_attach_; |
| } |
| |
| void TargetHandler::DisableAutoAttachOfServiceWorkers() { |
| auto_attach_service_workers_ = false; |
| } |
| |
| Response TargetHandler::FindSession(std::optional<std::string> session_id, |
| std::optional<std::string> target_id, |
| Session** session) { |
| *session = nullptr; |
| if (session_id.has_value()) { |
| auto it = attached_sessions_.find(session_id.value()); |
| if (it == attached_sessions_.end()) { |
| return Response::InvalidParams("No session with given id"); |
| } |
| *session = it->second.get(); |
| return Response::Success(); |
| } |
| if (target_id.has_value()) { |
| for (auto& it : attached_sessions_) { |
| if (it.second->IsAttachedTo(target_id.value())) { |
| if (*session) { |
| return Response::ServerError( |
| "Multiple sessions attached, specify id."); |
| } |
| *session = it.second.get(); |
| } |
| } |
| if (!*session) { |
| return Response::InvalidParams("No session for given target id"); |
| } |
| return Response::Success(); |
| } |
| return Response::InvalidParams("Session id must be specified"); |
| } |
| |
| // ----------------- Protocol ---------------------- |
| |
| Response TargetHandler::SetDiscoverTargets( |
| bool discover, |
| std::unique_ptr<protocol::Array<protocol::Target::FilterEntry>> filter) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| if (!discover && filter && !filter->empty()) { |
| return Response::InvalidParams( |
| "Filter should not be present with `discover` is off"); |
| } |
| const bool old_discover = TargetHandler::discover(); |
| discover_target_filter_ = |
| discover ? TargetFilter::Create(std::move(filter)) : nullptr; |
| if (old_discover == discover) { |
| // Report the newly matching targets that were not yet reported. |
| if (discover) { |
| for (const auto& target : DevToolsAgentHost::GetOrCreateAll()) { |
| DevToolsAgentHostCreated(target.get()); |
| } |
| } |
| return Response::Success(); |
| } |
| UpdateAgentHostObserver(); |
| if (!TargetHandler::discover()) { |
| reported_hosts_.clear(); |
| } |
| return Response::Success(); |
| } |
| |
| void TargetHandler::SetAutoAttach( |
| bool auto_attach, |
| bool wait_for_debugger_on_start, |
| std::optional<bool> flatten, |
| std::unique_ptr<protocol::Array<protocol::Target::FilterEntry>> filter, |
| std::unique_ptr<SetAutoAttachCallback> callback) { |
| if (access_mode_ == AccessMode::kBrowser && !flatten.value_or(false)) { |
| callback->sendFailure(Response::InvalidParams( |
| "Only flatten protocol is supported with browser level auto-attach")); |
| return; |
| } |
| if (!auto_attach && filter && !filter->empty()) { |
| callback->sendFailure(Response::InvalidParams( |
| "Target filter should be empty when disabling auto-attach")); |
| return; |
| } |
| auto_attach_target_filter_ = |
| auto_attach ? TargetFilter::Create(std::move(filter)) : nullptr; |
| if (auto_attach_target_filter_ && access_mode_ == AccessMode::kBrowser && |
| auto_attach_target_filter_->Match(DevToolsAgentHost::kTypeTab) && |
| auto_attach_target_filter_->Match(DevToolsAgentHost::kTypePage)) { |
| callback->sendFailure(Response::InvalidParams( |
| "Filter should not simultaneously allow \"tab\" and \"page\", " |
| "page targets are attached via tab targets")); |
| return; |
| } |
| SetAutoAttachInternal( |
| auto_attach, wait_for_debugger_on_start, flatten.value_or(false), |
| base::BindOnce(&SetAutoAttachCallback::sendSuccess, std::move(callback))); |
| } |
| |
| void TargetHandler::AutoAttachRelated( |
| const std::string& targetId, |
| bool wait_for_debugger_on_start, |
| std::unique_ptr<protocol::Array<protocol::Target::FilterEntry>> filter, |
| std::unique_ptr<AutoAttachRelatedCallback> callback) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| callback->sendFailure(Response::ServerError( |
| "Target.autoAttachRelated is only supported on the Browser target")); |
| return; |
| } |
| scoped_refptr<DevToolsAgentHostImpl> host = |
| DevToolsAgentHostImpl::GetForId(targetId); |
| if (!host) { |
| callback->sendFailure(Response::InvalidParams(kTargetNotFound)); |
| return; |
| } |
| TargetAutoAttacher* auto_attacher = host->auto_attacher(); |
| if (!auto_attacher) { |
| callback->sendFailure( |
| Response::InvalidParams("Target does not support auto-attaching")); |
| return; |
| } |
| if (auto_attach_) { |
| DCHECK(auto_attach_related_targets_.empty()); |
| SetAutoAttachInternal(false, false, true, base::DoNothing()); |
| } |
| flatten_auto_attach_ = true; |
| auto_attach_target_filter_ = TargetFilter::Create(std::move(filter)); |
| AutoAttach(auto_attacher_, host.get(), false); |
| auto inserted = auto_attach_related_targets_.insert( |
| std::make_pair(auto_attacher, wait_for_debugger_on_start)); |
| if (!inserted.second) { |
| auto_attacher->UpdateWaitForDebuggerOnStart( |
| this, wait_for_debugger_on_start, |
| base::BindOnce(&AutoAttachRelatedCallback::sendSuccess, |
| std::move(callback))); |
| inserted.first->second = wait_for_debugger_on_start; |
| return; |
| } |
| auto_attacher->AddClient( |
| this, wait_for_debugger_on_start, |
| base::BindOnce(&AutoAttachRelatedCallback::sendSuccess, |
| std::move(callback))); |
| } |
| |
| Response TargetHandler::SetRemoteLocations( |
| std::unique_ptr<protocol::Array<Target::RemoteLocation>>) { |
| return Response::ServerError("Not supported"); |
| } |
| |
| Response TargetHandler::AttachToTarget(const std::string& target_id, |
| std::optional<bool> flatten, |
| std::string* out_session_id) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| // TODO(dgozman): only allow reported hosts. |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| DevToolsAgentHost::GetForId(target_id); |
| if (!agent_host) { |
| return Response::InvalidParams(kTargetNotFound); |
| } |
| *out_session_id = |
| Session::Attach(this, agent_host.get(), false, flatten.value_or(false)); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::AttachToBrowserTarget(std::string* out_session_id) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| DevToolsAgentHost::CreateForBrowser( |
| nullptr, DevToolsAgentHost::CreateServerSocketCallback()); |
| *out_session_id = Session::Attach(this, agent_host.get(), false, true); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::DetachFromTarget(std::optional<std::string> session_id, |
| std::optional<std::string> target_id) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| Session* session = nullptr; |
| Response response = |
| FindSession(std::move(session_id), std::move(target_id), &session); |
| if (!response.IsSuccess()) { |
| return response; |
| } |
| session->Detach(false); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::SendMessageToTarget( |
| const std::string& message, |
| std::optional<std::string> session_id, |
| std::optional<std::string> target_id) { |
| Session* session = nullptr; |
| Response response = |
| FindSession(std::move(session_id), std::move(target_id), &session); |
| if (!response.IsSuccess()) { |
| return response; |
| } |
| if (session->flatten_protocol_) { |
| return Response::ServerError( |
| "When using flat protocol, messages are routed to the target " |
| "via the sessionId attribute."); |
| } |
| session->SendMessageToAgentHost(base::as_byte_span(message)); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::GetTargetInfo( |
| std::optional<std::string> maybe_target_id, |
| std::unique_ptr<Target::TargetInfo>* target_info) { |
| const std::string& target_id = maybe_target_id.value_or(owner_target_id_); |
| if (access_mode_ == AccessMode::kAutoAttachOnly && |
| target_id != owner_target_id_) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| // TODO(dgozman): only allow reported hosts. |
| scoped_refptr<DevToolsAgentHost> agent_host( |
| DevToolsAgentHost::GetForId(target_id)); |
| if (!agent_host) { |
| return Response::InvalidParams(kTargetNotFound); |
| } |
| *target_info = BuildTargetInfo(agent_host.get()); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::ActivateTarget(const std::string& target_id) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| // TODO(dgozman): only allow reported hosts. |
| scoped_refptr<DevToolsAgentHost> agent_host( |
| DevToolsAgentHost::GetForId(target_id)); |
| if (!agent_host) { |
| return Response::InvalidParams(kTargetNotFound); |
| } |
| agent_host->Activate(); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::CloseTarget(const std::string& target_id, |
| bool* out_success) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| DevToolsAgentHost::GetForId(target_id); |
| if (!agent_host) { |
| return Response::InvalidParams(kTargetNotFound); |
| } |
| if (!agent_host->Close()) { |
| return Response::InvalidParams("Specified target doesn't support closing"); |
| } |
| *out_success = true; |
| return Response::Success(); |
| } |
| |
| void TargetHandler::ExposeDevToolsProtocol( |
| const std::string& target_id, |
| std::optional<std::string> binding_name, |
| std::optional<bool> inherit_permissions, |
| std::unique_ptr<ExposeDevToolsProtocolCallback> callback) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| callback->sendFailure(Response::InvalidParams(kNotAllowedError)); |
| } |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| DevToolsAgentHost::GetForId(target_id); |
| if (!agent_host) { |
| callback->sendFailure(Response::InvalidParams(kTargetNotFound)); |
| } |
| |
| if (BrowserToPageConnector::GetInstanceMap()[agent_host.get()]) { |
| callback->sendFailure(Response::ServerError(base::StringPrintf( |
| "Target with id %s is already granted remote debugging bindings.", |
| target_id.c_str()))); |
| } |
| if (!agent_host->GetWebContents()) { |
| callback->sendFailure(Response::ServerError( |
| "RemoteDebuggingBinding can be granted only to page targets")); |
| } |
| |
| BrowserConnectorHostClientPermissions permissions; |
| if (inherit_permissions.value_or(false)) { |
| permissions.allow_unsafe_operations = |
| root_session_->GetClient()->AllowUnsafeOperations(); |
| } |
| |
| new BrowserToPageConnector(binding_name.value_or("cdp"), agent_host.get(), |
| permissions, std::move(callback)); |
| } |
| |
| Response TargetHandler::CreateTarget( |
| const std::string& url, |
| std::optional<int> left, |
| std::optional<int> top, |
| std::optional<int> width, |
| std::optional<int> height, |
| std::optional<std::string> window_state, |
| std::optional<std::string> browser_context_id, |
| std::optional<bool> enable_begin_frame_control, |
| std::optional<bool> new_window, |
| std::optional<bool> background, |
| std::optional<bool> for_tab, |
| std::optional<bool> hidden, |
| std::string* out_target_id) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| |
| GURL gurl(url); |
| if (gurl.is_empty()) { |
| gurl = GURL(url::kAboutBlankURL); |
| } |
| |
| if (hidden.value_or(false)) { |
| if (for_tab.value_or(false)) { |
| return protocol::Response::InvalidParams( |
| "Hidden target cannot be created for tab"); |
| } |
| if (new_window) { |
| return protocol::Response::InvalidParams( |
| "Hidden target cannot be created in a new window"); |
| } |
| if (!background.value_or(true)) { |
| return protocol::Response::InvalidParams( |
| "Hidden target can be created only in background"); |
| } |
| |
| BrowserContext* browser_context = nullptr; |
| Response response = BrowserHandler::FindBrowserContext(browser_context_id, |
| &browser_context); |
| if (!response.IsSuccess()) { |
| return response; |
| } |
| |
| *out_target_id = |
| hidden_target_manager_.CreateHiddenTarget(gurl, browser_context); |
| return Response::Success(); |
| } |
| |
| DevToolsManagerDelegate* delegate = |
| DevToolsManager::GetInstance()->delegate(); |
| if (!delegate) { |
| return Response::ServerError("Not supported"); |
| } |
| DevToolsManagerDelegate::TargetType target_type = |
| for_tab.value_or(session_mode_ == |
| DevToolsSession::Mode::kSupportsTabTarget) |
| ? DevToolsManagerDelegate::kTab |
| : DevToolsManagerDelegate::kFrame; |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| delegate->CreateNewTarget(gurl, target_type, new_window.value_or(false)); |
| if (!agent_host) { |
| return Response::ServerError("Not supported"); |
| } |
| *out_target_id = agent_host->GetId(); |
| return Response::Success(); |
| } |
| |
| Response TargetHandler::GetTargets( |
| std::unique_ptr<protocol::Array<protocol::Target::FilterEntry>> filter, |
| std::unique_ptr<protocol::Array<Target::TargetInfo>>* target_infos) { |
| if (access_mode_ == AccessMode::kAutoAttachOnly) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| std::unique_ptr<TargetFilter> passed_filter = |
| filter || !discover_target_filter_ |
| ? TargetFilter::Create(std::move(filter)) |
| : nullptr; |
| const TargetFilter* effective_filter = |
| passed_filter ? passed_filter.get() : discover_target_filter_.get(); |
| DCHECK(effective_filter); |
| *target_infos = std::make_unique<protocol::Array<Target::TargetInfo>>(); |
| for (const auto& host : DevToolsAgentHost::GetOrCreateAll()) { |
| if (effective_filter->Match(*host)) { |
| (*target_infos)->emplace_back(BuildTargetInfo(host.get())); |
| } |
| } |
| return Response::Success(); |
| } |
| |
| // -------------- DevToolsAgentHostObserver ----------------- |
| |
| bool TargetHandler::ShouldForceDevToolsAgentHostCreation() { |
| return true; |
| } |
| |
| void TargetHandler::DevToolsAgentHostCreated(DevToolsAgentHost* host) { |
| DCHECK(discover()); |
| DCHECK(host); |
| if (!discover_target_filter_->Match(*host)) { |
| return; |
| } |
| // If we start discovering late, all existing agent hosts will be reported, |
| // but we could have already attached to some. |
| if (!base::Contains(reported_hosts_, host)) { |
| frontend_->TargetCreated(BuildTargetInfo(host)); |
| reported_hosts_.insert(host); |
| } |
| } |
| |
| void TargetHandler::DevToolsAgentHostNavigated(DevToolsAgentHost* host) { |
| TargetInfoChanged(host); |
| } |
| |
| void TargetHandler::DevToolsAgentHostDestroyed(DevToolsAgentHost* host) { |
| if (!base::Contains(reported_hosts_, host)) { |
| return; |
| } |
| frontend_->TargetDestroyed(host->GetId()); |
| reported_hosts_.erase(host); |
| } |
| |
| void TargetHandler::DevToolsAgentHostAttached(DevToolsAgentHost* host) { |
| TargetInfoChanged(host); |
| } |
| |
| void TargetHandler::DevToolsAgentHostDetached(DevToolsAgentHost* host) { |
| TargetInfoChanged(host); |
| } |
| |
| void TargetHandler::DevToolsAgentHostCrashed(DevToolsAgentHost* host, |
| base::TerminationStatus status) { |
| if (!base::Contains(reported_hosts_, host)) { |
| return; |
| } |
| frontend_->TargetCrashed(host->GetId(), TerminationStatusToString(status), |
| host->GetWebContents() |
| ? host->GetWebContents()->GetCrashedErrorCode() |
| : 0); |
| } |
| |
| // ----------------- More protocol methods ------------------- |
| |
| void TargetHandler::CreateBrowserContext( |
| std::optional<bool> in_disposeOnDetach, |
| std::optional<String> in_proxyServer, |
| std::optional<String> in_proxyBypassList, |
| std::unique_ptr<protocol::Array<String>> |
| in_originsToGrantUniversalNetworkAccess, |
| std::unique_ptr<CreateBrowserContextCallback> callback) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| callback->sendFailure(Response::ServerError(kNotAllowedError)); |
| return; |
| } |
| DevToolsManagerDelegate* delegate = |
| DevToolsManager::GetInstance()->delegate(); |
| if (!delegate) { |
| callback->sendFailure( |
| Response::ServerError("Browser context management is not supported.")); |
| return; |
| } |
| |
| if (in_proxyServer.has_value()) { |
| pending_proxy_config_ = net::ProxyConfig(); |
| pending_proxy_config_->proxy_rules().ParseFromString( |
| in_proxyServer.value()); |
| if (in_proxyBypassList.has_value()) { |
| pending_proxy_config_->proxy_rules().bypass_rules.ParseFromString( |
| in_proxyBypassList.value()); |
| } |
| } |
| |
| // Pre-process universal network access origins before actual context creation |
| // in case we need to bail out with error. |
| std::vector<url::Origin> originsToGrantUniversalNetworkAccess; |
| if (in_originsToGrantUniversalNetworkAccess) { |
| for (const auto& origin_str : *in_originsToGrantUniversalNetworkAccess) { |
| GURL url(origin_str); |
| url::Origin origin = url::Origin::Create(url); |
| if (!url.is_valid() || origin.opaque()) { |
| callback->sendFailure( |
| Response::InvalidParams("Invalid origin " + origin_str)); |
| return; |
| } |
| originsToGrantUniversalNetworkAccess.push_back(std::move(origin)); |
| } |
| } |
| |
| BrowserContext* context = delegate->CreateBrowserContext(); |
| if (!context) { |
| callback->sendFailure( |
| Response::ServerError("Failed to create browser context.")); |
| pending_proxy_config_.reset(); |
| return; |
| } |
| |
| for (const auto& origin : originsToGrantUniversalNetworkAccess) { |
| std::vector<network::mojom::CorsOriginPatternPtr> allow_patterns; |
| allow_patterns.push_back(network::mojom::CorsOriginPattern::New()); |
| allow_patterns.back()->protocol = "http"; |
| allow_patterns.back()->priority = |
| network::mojom::CorsOriginAccessMatchPriority::kMaxPriority; |
| allow_patterns.push_back(network::mojom::CorsOriginPattern::New()); |
| allow_patterns.back()->protocol = "https"; |
| allow_patterns.back()->priority = |
| network::mojom::CorsOriginAccessMatchPriority::kMaxPriority; |
| |
| // It's fine to not await the completion here -- this is implicitly |
| // serialized with the actual URLLoaderFactory / URLLoader creation. |
| CorsOriginPatternSetter::Set( |
| context, origin, std::move(allow_patterns), {}, base::DoNothing()); |
| } |
| |
| if (pending_proxy_config_) { |
| contexts_with_overridden_proxy_[context->UniqueId()] = |
| std::move(*pending_proxy_config_); |
| pending_proxy_config_.reset(); |
| } |
| |
| if (in_disposeOnDetach.value_or(false)) { |
| dispose_on_detach_context_ids_.insert(context->UniqueId()); |
| } |
| callback->sendSuccess(context->UniqueId()); |
| } |
| |
| protocol::Response TargetHandler::GetBrowserContexts( |
| std::unique_ptr<protocol::Array<protocol::String>>* browser_context_ids) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| return Response::ServerError(kNotAllowedError); |
| } |
| DevToolsManagerDelegate* delegate = |
| DevToolsManager::GetInstance()->delegate(); |
| if (!delegate) { |
| return Response::ServerError( |
| "Browser context management is not supported."); |
| } |
| std::vector<BrowserContext*> contexts = |
| delegate->GetBrowserContexts(); |
| *browser_context_ids = std::make_unique<protocol::Array<protocol::String>>(); |
| for (auto* context : contexts) { |
| (*browser_context_ids)->emplace_back(context->UniqueId()); |
| } |
| return Response::Success(); |
| } |
| |
| void TargetHandler::DisposeBrowserContext( |
| const std::string& context_id, |
| std::unique_ptr<DisposeBrowserContextCallback> callback) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| callback->sendFailure(Response::ServerError(kNotAllowedError)); |
| return; |
| } |
| DevToolsManagerDelegate* delegate = |
| DevToolsManager::GetInstance()->delegate(); |
| if (!delegate) { |
| callback->sendFailure( |
| Response::ServerError("Browser context management is not supported.")); |
| return; |
| } |
| std::vector<BrowserContext*> contexts = |
| delegate->GetBrowserContexts(); |
| auto context_it = std::ranges::find(contexts, context_id, |
| &BrowserContext::UniqueId); |
| if (context_it == contexts.end()) { |
| callback->sendFailure( |
| Response::ServerError("Failed to find context with id " + context_id)); |
| return; |
| } |
| dispose_on_detach_context_ids_.erase(context_id); |
| delegate->DisposeBrowserContext( |
| *context_it, |
| base::BindOnce( |
| [](std::unique_ptr<DisposeBrowserContextCallback> callback, |
| bool success, const std::string& error) { |
| if (success) { |
| callback->sendSuccess(); |
| } else { |
| callback->sendFailure(Response::ServerError(error)); |
| } |
| }, |
| std::move(callback))); |
| } |
| |
| void TargetHandler::ApplyNetworkContextParamsOverrides( |
| BrowserContext* browser_context, |
| network::mojom::NetworkContextParams* context_params) { |
| // Note #1: below we clear the proxy config client receiver, |
| // and effectively disable proxy updates based on the OS settings. |
| // This way our "initial proxy config" is not overridden by any |
| // OS settings and stays the same. |
| // This relies on ApplyNetworkContextParamsOverrides() being called |
| // after the client receiver was setup for the network context. |
| // |
| // Note #2: Under certain conditions, storage partition is created |
| // synchronously for |
| // the browser context. Account for this use case. |
| if (pending_proxy_config_) { |
| context_params->initial_proxy_config = |
| net::ProxyConfigWithAnnotation(std::move(*pending_proxy_config_), |
| kSettingsProxyConfigTrafficAnnotation); |
| context_params->proxy_config_client_receiver = mojo::NullReceiver(); |
| pending_proxy_config_.reset(); |
| return; |
| } |
| auto it = contexts_with_overridden_proxy_.find(browser_context->UniqueId()); |
| if (it != contexts_with_overridden_proxy_.end()) { |
| context_params->initial_proxy_config = net::ProxyConfigWithAnnotation( |
| std::move(it->second), kSettingsProxyConfigTrafficAnnotation); |
| context_params->proxy_config_client_receiver = mojo::NullReceiver(); |
| contexts_with_overridden_proxy_.erase(browser_context->UniqueId()); |
| } |
| } |
| |
| void TargetHandler::AddWorkerThrottle( |
| DevToolsAgentHost* agent_host, |
| scoped_refptr<DevToolsThrottleHandle> throttle_handle) { |
| if (!agent_host) { |
| return; |
| } |
| |
| if (auto_attached_sessions_.count(agent_host)) { |
| if (auto_attached_sessions_[agent_host]->IsWaitingForDebuggerOnStart()) { |
| auto_attached_sessions_[agent_host]->SetWorkerThrottle( |
| std::move(throttle_handle)); |
| } |
| } |
| } |
| |
| Response TargetHandler::OpenDevTools(const std::string& target_id, |
| std::string* out_target_id) { |
| if (access_mode_ != AccessMode::kBrowser) { |
| return protocol::Response::ServerError(kNotAllowedError); |
| } |
| scoped_refptr<DevToolsAgentHostImpl> agent_host = |
| DevToolsAgentHostImpl::GetForId(target_id); |
| |
| if (!agent_host) { |
| return protocol::Response::InvalidParams(kTargetNotFound); |
| } |
| |
| scoped_refptr<DevToolsAgentHost> devtools_agent_host = |
| agent_host->OpenDevTools(); |
| if (!devtools_agent_host) { |
| return protocol::Response::ServerError("Failed to create DevTools window"); |
| } |
| |
| *out_target_id = devtools_agent_host->GetId(); |
| |
| return protocol::Response::Success(); |
| } |
| |
| } // namespace content::protocol |