| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Implements the Chrome Extensions Debugger API. |
| |
| #include "chrome/browser/extensions/api/debugger/debugger_api.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/singleton.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/no_destructor.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_view_util.h" |
| #include "base/types/optional_util.h" |
| #include "base/values.h" |
| #include "chrome/browser/extensions/api/debugger/extension_dev_tools_infobar_delegate.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/lifetime/termination_notification.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_observer.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/guest_view/buildflags/buildflags.h" |
| #include "components/security_interstitials/content/security_interstitial_tab_helper.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/devtools_agent_host.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_widget_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/common/url_utils.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_registry_observer.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/switches.h" |
| #include "pdf/buildflags.h" |
| #include "url/origin.h" |
| #include "url/url_constants.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "chrome/browser/devtools/chrome_devtools_manager_delegate.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_GUEST_VIEW) |
| #include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| #include "components/pdf/common/pdf_util.h" |
| #include "pdf/pdf_features.h" |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| using content::DevToolsAgentHost; |
| using content::RenderProcessHost; |
| using content::RenderWidgetHost; |
| using content::WebContents; |
| |
| namespace Attach = extensions::api::debugger::Attach; |
| namespace Detach = extensions::api::debugger::Detach; |
| namespace OnDetach = extensions::api::debugger::OnDetach; |
| namespace OnEvent = extensions::api::debugger::OnEvent; |
| namespace SendCommand = extensions::api::debugger::SendCommand; |
| |
| namespace extensions { |
| class ExtensionRegistry; |
| class ExtensionDevToolsClientHost; |
| |
| namespace { |
| |
| constexpr char kAlreadyAttachedError[] = |
| "Another debugger is already attached to the * with id: *."; |
| constexpr char kNoTargetError[] = "No * with given id *."; |
| constexpr char kInvalidTargetError[] = |
| "Either tab id or extension id must be specified."; |
| constexpr char kNotAttachedError[] = |
| "Debugger is not attached to the * with id: *."; |
| constexpr char kProtocolVersionNotSupportedError[] = |
| "Requested protocol version is not supported: *."; |
| constexpr char kRestrictedError[] = "Cannot attach to this target."; |
| constexpr char kDetachedWhileHandlingError[] = |
| "Detached while handling command."; |
| |
| constexpr char kTabTargetType[] = "tab"; |
| constexpr char kBackgroundPageTargetType[] = "background page"; |
| constexpr char kOpaqueTargetType[] = "target"; |
| |
| // Helpers -------------------------------------------------------------------- |
| |
| void CopyDebuggee(Debuggee* dst, const Debuggee& src) { |
| dst->tab_id = src.tab_id; |
| dst->extension_id = src.extension_id; |
| dst->target_id = src.target_id; |
| } |
| |
| void DebuggerSessionFromDebugee(DebuggerSession& dst, |
| const Debuggee& src, |
| std::string* maybe_session_id) { |
| dst.tab_id = src.tab_id; |
| dst.extension_id = src.extension_id; |
| dst.target_id = src.target_id; |
| if (maybe_session_id) { |
| dst.session_id = *maybe_session_id; |
| } |
| } |
| |
| void DebuggeeFromDebuggerSession(Debuggee& dst, const DebuggerSession& src) { |
| dst.tab_id = src.tab_id; |
| dst.extension_id = src.extension_id; |
| dst.target_id = src.target_id; |
| } |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| // Returns whether `url` is the URL for the built-in PDF extension. |
| bool IsPdfExtensionUrl(const GURL& url) { |
| return url.scheme() == kExtensionScheme && |
| url.host() == extension_misc::kPdfExtensionId; |
| } |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| bool ExtensionMayAttachToTargetProfile(Profile* extension_profile, |
| bool allow_incognito_access, |
| DevToolsAgentHost& agent_host) { |
| Profile* profile = |
| Profile::FromBrowserContext(agent_host.GetBrowserContext()); |
| if (!profile) |
| return false; |
| if (!extension_profile->IsSameOrParent(profile)) |
| return false; |
| return profile == extension_profile || allow_incognito_access; |
| } |
| |
| // Returns true if the given |Extension| is allowed to attach to the specified |
| // |url|. |
| bool ExtensionMayAttachToURL(const Extension& extension, |
| Profile* extension_profile, |
| const GURL& url, |
| std::string* error) { |
| // Allow the extension to attach to about:blank and empty URLs. |
| if (url.is_empty() || url == "about:") |
| return true; |
| |
| if (url == content::kUnreachableWebDataURL) |
| return true; |
| |
| // NOTE: The `debugger` permission implies all URLs access (and indicates |
| // such to the user), so we don't check explicit page access. However, we |
| // still need to check if it's an otherwise-restricted URL. |
| // NOTE: blob URLs are generally restricted but debugger should be able to |
| // attach if it has access to the origin that created the blob. |
| // See https://crbug.com/1492134. |
| const GURL& url_for_restriction_check = |
| url.SchemeIsBlob() ? url::Origin::Create(url).GetURL() : url; |
| if (extension.permissions_data()->IsRestrictedUrl(url_for_restriction_check, |
| error)) { |
| return false; |
| } |
| |
| // Policy blocked hosts supersede the `debugger` permission. |
| if (extension.permissions_data()->IsPolicyBlockedHost(url) || |
| extension.permissions_data()->IsPolicyBlockedHost( |
| url_for_restriction_check)) { |
| *error = kRestrictedError; |
| return false; |
| } |
| |
| if (url.SchemeIsFile() && |
| !util::AllowFileAccess(extension.id(), extension_profile)) { |
| *error = kRestrictedError; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Returns whether the extension may attach to a frame. `frame_url` is the URL |
| // of the frame, If querying about a frame, `page_url` is non-null, and |
| // identifies the URL of the outermost frame. |
| bool ExtensionMayAttachToURLOrInnerURL(const Extension& extension, |
| Profile* extension_profile, |
| const GURL& frame_url, |
| const GURL* page_url, |
| std::string* error) { |
| // cid: URLs within file: urls are just parts of the top level page, and don't |
| // represent new origins. We skip over these and instead test against the |
| // top-level page URL. |
| if (page_url && page_url->SchemeIsFile() && |
| frame_url.SchemeIs(url::kContentIDScheme)) { |
| return ExtensionMayAttachToURLOrInnerURL(extension, extension_profile, |
| *page_url, nullptr, error); |
| } |
| |
| if (!ExtensionMayAttachToURL(extension, extension_profile, frame_url, |
| error)) { |
| return false; |
| } |
| // For nested URLs, make sure ExtensionMayAttachToURL() allows both |
| // the outer and the inner URLs. |
| if (frame_url.inner_url() && |
| !ExtensionMayAttachToURL(extension, extension_profile, |
| *frame_url.inner_url(), error)) { |
| return false; |
| } |
| return true; |
| } |
| |
| constexpr char kBrowserTargetId[] = "browser"; |
| |
| constexpr char kPerfettoUIExtensionId[] = "lfmkphfpdbjijhpomgecfikhfohaoine"; |
| |
| bool ExtensionIsTrusted(const Extension& extension) { |
| return extension.id() == kPerfettoUIExtensionId; |
| } |
| |
| bool ExtensionMayAttachToRenderFrameHost( |
| const Extension& extension, |
| Profile* extension_profile, |
| content::RenderFrameHost* render_frame_host, |
| std::string* error) { |
| bool result = true; |
| const GURL& page_url = render_frame_host->GetLastCommittedURL(); |
| render_frame_host->ForEachRenderFrameHostWithAction( |
| [&page_url, &extension, extension_profile, error, |
| &result](content::RenderFrameHost* render_frame_host) { |
| #if BUILDFLAG(ENABLE_GUEST_VIEW) |
| // If |render_frame_host| is attached to an inner MimeHandlerViewGuest |
| // skip it. This is done to fix crbug.com/1293856 because an extension |
| // cannot inspect another extension. |
| if (MimeHandlerViewGuest::FromRenderFrameHost(render_frame_host)) { |
| return content::RenderFrameHost::FrameIterationAction::kSkipChildren; |
| } |
| #endif // BUILDFLAG(ENABLE_GUEST_VIEW) |
| |
| #if BUILDFLAG(ENABLE_PDF) |
| // The PDF extension frame would normally prevent all other frames in |
| // the frame tree from being attachable. Skip it so this doesn't occur. |
| // This should be okay, since the PDF extension frame and PDF content |
| // frame aren't listed in chrome.debugger.getTargets(). Check both the |
| // last committed origin and the SiteURL for the PDF extension frame, |
| // because this method may be called in the middle of a navigation where |
| // the SiteURL has been updated but navigation hasn't committed yet. |
| if (chrome_pdf::features::IsOopifPdfEnabled() && |
| (IsPdfExtensionOrigin( |
| render_frame_host->GetLastCommittedOrigin()) || |
| IsPdfExtensionUrl( |
| render_frame_host->GetSiteInstance()->GetSiteURL()))) { |
| return content::RenderFrameHost::FrameIterationAction::kContinue; |
| } |
| #endif // BUILDFLAG(ENABLE_PDF) |
| |
| if (render_frame_host->GetWebUI()) { |
| *error = kRestrictedError; |
| result = false; |
| return content::RenderFrameHost::FrameIterationAction::kStop; |
| } |
| |
| // We check both the last committed URL and the SiteURL because this |
| // method may be called in the middle of a navigation where the SiteURL |
| // has been updated but navigation hasn't committed yet. |
| if (!ExtensionMayAttachToURLOrInnerURL( |
| extension, extension_profile, |
| render_frame_host->GetLastCommittedURL(), &page_url, error) || |
| !ExtensionMayAttachToURLOrInnerURL( |
| extension, extension_profile, |
| render_frame_host->GetSiteInstance()->GetSiteURL(), &page_url, |
| error)) { |
| result = false; |
| return content::RenderFrameHost::FrameIterationAction::kStop; |
| } |
| |
| return content::RenderFrameHost::FrameIterationAction::kContinue; |
| }); |
| return result; |
| } |
| |
| bool ExtensionMayAttachToWebContents(const Extension& extension, |
| Profile* extension_profile, |
| WebContents& web_contents, |
| std::string* error) { |
| security_interstitials::SecurityInterstitialTabHelper* |
| security_interstitial_tab_helper = security_interstitials:: |
| SecurityInterstitialTabHelper::FromWebContents(&web_contents); |
| if (security_interstitial_tab_helper && |
| security_interstitial_tab_helper->IsDisplayingInterstitial()) { |
| *error = kRestrictedError; |
| return false; |
| } |
| // This is *not* redundant to the checks below, as |
| // web_contents.GetLastCommittedURL() may be different from |
| // web_contents.GetPrimaryMainFrame()->GetLastCommittedURL(), with the |
| // former being a 'virtual' URL as obtained from NavigationEntry. |
| if (!ExtensionMayAttachToURL(extension, extension_profile, |
| web_contents.GetLastCommittedURL(), error)) { |
| return false; |
| } |
| if (web_contents.GetController().GetPendingEntry() && |
| !ExtensionMayAttachToURL( |
| extension, extension_profile, |
| web_contents.GetController().GetPendingEntry()->GetURL(), error)) { |
| return false; |
| } |
| |
| return ExtensionMayAttachToRenderFrameHost( |
| extension, extension_profile, web_contents.GetPrimaryMainFrame(), error); |
| } |
| |
| bool ExtensionMayAttachToAgentHost(const Extension& extension, |
| bool allow_incognito_access, |
| Profile* extension_profile, |
| DevToolsAgentHost& agent_host, |
| std::string* error) { |
| if (!ExtensionMayAttachToTargetProfile(extension_profile, |
| allow_incognito_access, agent_host)) { |
| *error = kRestrictedError; |
| return false; |
| } |
| if (WebContents* wc = agent_host.GetWebContents()) { |
| return ExtensionMayAttachToWebContents(extension, extension_profile, *wc, |
| error); |
| } |
| |
| return ExtensionMayAttachToURL(extension, extension_profile, |
| agent_host.GetURL(), error); |
| } |
| |
| } // namespace |
| |
| // ExtensionDevToolsClientHost ------------------------------------------------ |
| |
| using AttachedClientHosts = std::set<ExtensionDevToolsClientHost*>; |
| AttachedClientHosts& GetAttachedClientHosts() { |
| static base::NoDestructor<AttachedClientHosts> attached_client_hosts; |
| return *attached_client_hosts; |
| } |
| |
| class ExtensionDevToolsClientHost : public content::DevToolsAgentHostClient, |
| public ExtensionRegistryObserver, |
| public ProfileObserver { |
| public: |
| ExtensionDevToolsClientHost( |
| Profile* profile, |
| DevToolsAgentHost* agent_host, |
| scoped_refptr<const Extension> extension, |
| std::optional<WorkerId> extension_service_worker_id, |
| const Debuggee& debuggee); |
| |
| ExtensionDevToolsClientHost(const ExtensionDevToolsClientHost&) = delete; |
| ExtensionDevToolsClientHost& operator=(const ExtensionDevToolsClientHost&) = |
| delete; |
| |
| ~ExtensionDevToolsClientHost() override; |
| |
| std::string GetTypeForMetrics() override { return "Extension"; } |
| |
| bool Attach(); |
| const ExtensionId& extension_id() { return extension_->id(); } |
| DevToolsAgentHost* agent_host() { return agent_host_.get(); } |
| void RespondDetachedToPendingRequests(); |
| void Close(); |
| void SendMessageToBackend(DebuggerSendCommandFunction* function, |
| const std::string& method, |
| SendCommand::Params::CommandParams* command_params, |
| std::optional<std::string> session_id); |
| |
| // Closes connection as terminated by the user. |
| void InfoBarDestroyed(); |
| |
| // DevToolsAgentHostClient interface. |
| void AgentHostClosed(DevToolsAgentHost* agent_host) override; |
| void DispatchProtocolMessage(DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) override; |
| bool MayAttachToRenderFrameHost( |
| content::RenderFrameHost* render_frame_host) override; |
| bool MayAttachToURL(const GURL& url, bool is_webui) override; |
| bool IsTrusted() override; |
| bool MayReadLocalFiles() override; |
| bool MayWriteLocalFiles() override; |
| std::optional<url::Origin> GetNavigationInitiatorOrigin() override; |
| |
| private: |
| using PendingRequests = |
| std::map<int, scoped_refptr<DebuggerSendCommandFunction>>; |
| |
| void SendDetachedEvent(); |
| |
| void OnAppTerminating(); |
| |
| // ExtensionRegistryObserver implementation. |
| void OnExtensionUnloaded(content::BrowserContext* browser_context, |
| const Extension* extension, |
| UnloadedExtensionReason reason) override; |
| // ProfileObserver implementation |
| void OnProfileWillBeDestroyed(Profile* profile) override; |
| |
| raw_ptr<Profile> profile_; |
| scoped_refptr<DevToolsAgentHost> agent_host_; |
| scoped_refptr<const Extension> extension_; |
| // The WorkerId of the extension service worker that called attach() for this |
| // client host, if any. |
| const std::optional<WorkerId> extension_service_worker_id_; |
| |
| Debuggee debuggee_; |
| base::CallbackListSubscription on_app_terminating_subscription_; |
| int last_request_id_ = 0; |
| PendingRequests pending_requests_; |
| base::CallbackListSubscription subscription_; |
| api::debugger::DetachReason detach_reason_ = |
| api::debugger::DetachReason::kTargetClosed; |
| |
| // A service worker keepalive used to keep the associated worker alive while |
| // this client is attached. Only used if `extension_service_worker_id_` has a |
| // value. |
| std::optional<base::Uuid> service_worker_keepalive_; |
| |
| // Listen to extension unloaded notification. |
| base::ScopedObservation<ExtensionRegistry, ExtensionRegistryObserver> |
| extension_registry_observation_{this}; |
| base::ScopedObservation<Profile, ProfileObserver> profile_observation_{this}; |
| }; |
| |
| ExtensionDevToolsClientHost::ExtensionDevToolsClientHost( |
| Profile* profile, |
| DevToolsAgentHost* agent_host, |
| scoped_refptr<const Extension> extension, |
| std::optional<WorkerId> extension_service_worker_id, |
| const Debuggee& debuggee) |
| : profile_(profile), |
| agent_host_(agent_host), |
| extension_(std::move(extension)), |
| extension_service_worker_id_(std::move(extension_service_worker_id)) { |
| CopyDebuggee(&debuggee_, debuggee); |
| |
| GetAttachedClientHosts().insert(this); |
| |
| // ExtensionRegistryObserver listen extension unloaded and detach debugger |
| // from there. |
| extension_registry_observation_.Observe(ExtensionRegistry::Get(profile_)); |
| profile_observation_.Observe(profile_); |
| |
| // RVH-based agents disconnect from their clients when the app is terminating |
| // but shared worker-based agents do not. |
| // Disconnect explicitly to make sure that |this| observer is not leaked. |
| on_app_terminating_subscription_ = |
| browser_shutdown::AddAppTerminatingCallback( |
| base::BindOnce(&ExtensionDevToolsClientHost::OnAppTerminating, |
| base::Unretained(this))); |
| } |
| |
| bool ExtensionDevToolsClientHost::Attach() { |
| // Attach to debugger and tell it we are ready. |
| if (!agent_host_->AttachClient(this)) { |
| return false; |
| } |
| |
| // We allow policy-installed extensions to circumvent the normal |
| // infobar warning. See crbug.com/693621. |
| const bool suppress_infobar = |
| base::CommandLine::ForCurrentProcess()->HasSwitch( |
| ::switches::kSilentDebuggerExtensionAPI) || |
| Manifest::IsPolicyLocation(extension_->location()); |
| |
| if (!suppress_infobar) { |
| subscription_ = ExtensionDevToolsInfoBarDelegate::Create( |
| extension_id(), extension_->name(), |
| base::BindOnce(&ExtensionDevToolsClientHost::InfoBarDestroyed, |
| base::Unretained(this))); |
| } |
| |
| if (extension_service_worker_id_) { |
| ProcessManager* process_manager = ProcessManager::Get(profile_); |
| CHECK(process_manager); |
| // The service worker should definitely be registered at this point. |
| CHECK(process_manager->HasServiceWorker(*extension_service_worker_id_)); |
| service_worker_keepalive_ = |
| process_manager->IncrementServiceWorkerKeepaliveCount( |
| *extension_service_worker_id_, |
| content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout, |
| Activity::DEBUGGER, /*extra_data=*/std::string()); |
| } |
| |
| return true; |
| } |
| |
| ExtensionDevToolsClientHost::~ExtensionDevToolsClientHost() { |
| GetAttachedClientHosts().erase(this); |
| |
| // Decrement the associated worker keepalive, if any. |
| if (service_worker_keepalive_) { |
| CHECK(extension_service_worker_id_); |
| ProcessManager* process_manager = ProcessManager::Get(profile_); |
| CHECK(process_manager); |
| // The worker may have terminated for other reasons. Only decrement the |
| // keepalive if it's still around. |
| if (process_manager->HasServiceWorker(*extension_service_worker_id_)) { |
| process_manager->DecrementServiceWorkerKeepaliveCount( |
| *extension_service_worker_id_, *service_worker_keepalive_, |
| Activity::DEBUGGER, /*extra_data=*/std::string()); |
| } |
| } |
| } |
| |
| // DevToolsAgentHostClient implementation. |
| void ExtensionDevToolsClientHost::AgentHostClosed( |
| DevToolsAgentHost* agent_host) { |
| DCHECK(agent_host == agent_host_.get()); |
| RespondDetachedToPendingRequests(); |
| SendDetachedEvent(); |
| delete this; |
| } |
| |
| void ExtensionDevToolsClientHost::Close() { |
| agent_host_->DetachClient(this); |
| delete this; |
| } |
| |
| void ExtensionDevToolsClientHost::SendMessageToBackend( |
| DebuggerSendCommandFunction* function, |
| const std::string& method, |
| SendCommand::Params::CommandParams* command_params, |
| std::optional<std::string> session_id) { |
| base::Value::Dict protocol_request; |
| int request_id = ++last_request_id_; |
| pending_requests_[request_id] = function; |
| protocol_request.Set("id", request_id); |
| protocol_request.Set("method", method); |
| if (command_params) { |
| protocol_request.Set("params", |
| command_params->additional_properties.Clone()); |
| } |
| if (session_id.has_value()) { |
| protocol_request.Set("sessionId", session_id.value()); |
| } |
| |
| std::string json; |
| base::JSONWriter::Write(protocol_request, &json); |
| |
| agent_host_->DispatchProtocolMessage(this, base::as_byte_span(json)); |
| } |
| |
| void ExtensionDevToolsClientHost::InfoBarDestroyed() { |
| detach_reason_ = api::debugger::DetachReason::kCanceledByUser; |
| RespondDetachedToPendingRequests(); |
| SendDetachedEvent(); |
| Close(); |
| } |
| |
| void ExtensionDevToolsClientHost::RespondDetachedToPendingRequests() { |
| for (const auto& it : pending_requests_) |
| it.second->SendDetachedError(); |
| pending_requests_.clear(); |
| } |
| |
| void ExtensionDevToolsClientHost::SendDetachedEvent() { |
| if (!EventRouter::Get(profile_)) |
| return; |
| |
| auto args(OnDetach::Create(debuggee_, detach_reason_)); |
| auto event = |
| std::make_unique<Event>(events::DEBUGGER_ON_DETACH, OnDetach::kEventName, |
| std::move(args), profile_); |
| EventRouter::Get(profile_)->DispatchEventToExtension(extension_id(), |
| std::move(event)); |
| } |
| |
| void ExtensionDevToolsClientHost::OnProfileWillBeDestroyed(Profile* profile) { |
| if (profile == profile_) { |
| Close(); |
| } |
| } |
| |
| void ExtensionDevToolsClientHost::OnExtensionUnloaded( |
| content::BrowserContext* browser_context, |
| const Extension* extension, |
| UnloadedExtensionReason reason) { |
| if (extension->id() == extension_id()) |
| Close(); |
| } |
| |
| void ExtensionDevToolsClientHost::OnAppTerminating() { |
| Close(); |
| } |
| |
| void ExtensionDevToolsClientHost::DispatchProtocolMessage( |
| DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) { |
| DCHECK(agent_host == agent_host_.get()); |
| if (!EventRouter::Get(profile_)) |
| return; |
| |
| std::string_view message_str = base::as_string_view(message); |
| std::optional<base::Value> result = base::JSONReader::Read( |
| message_str, base::JSON_REPLACE_INVALID_CHARACTERS); |
| if (!result || !result->is_dict()) { |
| LOG(ERROR) << "Tried to send invalid message to extension: " << message_str; |
| return; |
| } |
| base::Value::Dict& dictionary = result->GetDict(); |
| |
| std::optional<int> id = dictionary.FindInt("id"); |
| if (!id) { |
| std::string* method_name = dictionary.FindString("method"); |
| if (!method_name) |
| return; |
| |
| OnEvent::Params params; |
| if (base::Value::Dict* params_value = dictionary.FindDict("params")) { |
| params.additional_properties = std::move(*params_value); |
| } |
| |
| DebuggerSession session; |
| DebuggerSessionFromDebugee(session, debuggee_, |
| dictionary.FindString("sessionId")); |
| |
| auto args(OnEvent::Create(session, *method_name, params)); |
| auto event = |
| std::make_unique<Event>(events::DEBUGGER_ON_EVENT, OnEvent::kEventName, |
| std::move(args), profile_); |
| EventRouter::Get(profile_)->DispatchEventToExtension(extension_id(), |
| std::move(event)); |
| } else { |
| auto it = pending_requests_.find(*id); |
| if (it == pending_requests_.end()) |
| return; |
| |
| it->second->SendResponseBody(base::Value(std::move(dictionary))); |
| pending_requests_.erase(it); |
| } |
| } |
| |
| bool ExtensionDevToolsClientHost::MayAttachToRenderFrameHost( |
| content::RenderFrameHost* render_frame_host) { |
| std::string error; |
| return ExtensionMayAttachToRenderFrameHost(*extension_, profile_, |
| render_frame_host, &error); |
| } |
| |
| bool ExtensionDevToolsClientHost::MayAttachToURL(const GURL& url, |
| bool is_webui) { |
| if (is_webui) |
| return false; |
| std::string error; |
| return ExtensionMayAttachToURLOrInnerURL(*extension_, profile_, url, nullptr, |
| &error); |
| } |
| |
| bool ExtensionDevToolsClientHost::IsTrusted() { |
| return ExtensionIsTrusted(*extension_); |
| } |
| |
| bool ExtensionDevToolsClientHost::MayReadLocalFiles() { |
| return util::AllowFileAccess(extension_->id(), profile_); |
| } |
| |
| bool ExtensionDevToolsClientHost::MayWriteLocalFiles() { |
| return false; |
| } |
| |
| std::optional<url::Origin> |
| ExtensionDevToolsClientHost::GetNavigationInitiatorOrigin() { |
| // Ensure that navigations started by debugger API are treated as |
| // renderer-initiated by this extension, so that URL spoof defenses are in |
| // effect. |
| return extension_->origin(); |
| } |
| |
| // DebuggerFunction ----------------------------------------------------------- |
| |
| DebuggerFunction::DebuggerFunction() : client_host_(nullptr) {} |
| |
| DebuggerFunction::~DebuggerFunction() = default; |
| |
| std::string DebuggerFunction::FormatErrorMessage(const std::string& format) { |
| if (debuggee_.tab_id) { |
| return ErrorUtils::FormatErrorMessage( |
| format, kTabTargetType, base::NumberToString(*debuggee_.tab_id)); |
| } |
| if (debuggee_.extension_id) { |
| return ErrorUtils::FormatErrorMessage(format, kBackgroundPageTargetType, |
| *debuggee_.extension_id); |
| } |
| |
| return ErrorUtils::FormatErrorMessage(format, kOpaqueTargetType, |
| *debuggee_.target_id); |
| } |
| |
| bool DebuggerFunction::InitAgentHost(std::string* error) { |
| if (debuggee_.tab_id) { |
| WebContents* web_contents = nullptr; |
| bool result = ExtensionTabUtil::GetTabById( |
| *debuggee_.tab_id, browser_context(), include_incognito_information(), |
| &web_contents); |
| if (result && web_contents) { |
| if (!ExtensionMayAttachToWebContents( |
| *extension(), Profile::FromBrowserContext(browser_context()), |
| *web_contents, error)) { |
| return false; |
| } |
| |
| agent_host_ = DevToolsAgentHost::GetOrCreateFor(web_contents); |
| } |
| } else if (debuggee_.extension_id) { |
| ExtensionHost* extension_host = |
| ProcessManager::Get(browser_context()) |
| ->GetBackgroundHostForExtension(*debuggee_.extension_id); |
| if (extension_host) { |
| const GURL& url = extension_host->GetLastCommittedURL(); |
| if (extension()->permissions_data()->IsRestrictedUrl(url, error) || |
| extension()->permissions_data()->IsPolicyBlockedHost(url)) { |
| return false; |
| } |
| agent_host_ = |
| DevToolsAgentHost::GetOrCreateFor(extension_host->host_contents()); |
| } |
| } else if (debuggee_.target_id) { |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| DevToolsAgentHost::GetForId(*debuggee_.target_id); |
| if (agent_host) { |
| if (!ExtensionMayAttachToAgentHost( |
| *extension(), include_incognito_information(), |
| Profile::FromBrowserContext(browser_context()), *agent_host, |
| error)) { |
| return false; |
| } |
| agent_host_ = std::move(agent_host); |
| } else if (*debuggee_.target_id == kBrowserTargetId && |
| ExtensionIsTrusted(*extension())) { |
| // TODO(caseq): get rid of the below code, browser agent host should |
| // really be a singleton. |
| // Re-use existing browser agent hosts. |
| const ExtensionId& extension_id = extension()->id(); |
| AttachedClientHosts& hosts = GetAttachedClientHosts(); |
| auto it = std::ranges::find_if( |
| hosts, [&extension_id](ExtensionDevToolsClientHost* client_host) { |
| return client_host->extension_id() == extension_id && |
| client_host->agent_host() && |
| client_host->agent_host()->GetType() == |
| DevToolsAgentHost::kTypeBrowser; |
| }); |
| agent_host_ = it != hosts.end() |
| ? (*it)->agent_host() |
| : DevToolsAgentHost::CreateForBrowser( |
| nullptr /* tethering_task_runner */, |
| DevToolsAgentHost::CreateServerSocketCallback()); |
| } |
| } else { |
| *error = kInvalidTargetError; |
| return false; |
| } |
| |
| if (!agent_host_.get()) { |
| *error = FormatErrorMessage(kNoTargetError); |
| return false; |
| } |
| return true; |
| } |
| |
| bool DebuggerFunction::InitClientHost(std::string* error) { |
| if (!InitAgentHost(error)) |
| return false; |
| |
| client_host_ = FindClientHost(); |
| if (!client_host_) { |
| *error = FormatErrorMessage(kNotAttachedError); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| ExtensionDevToolsClientHost* DebuggerFunction::FindClientHost() { |
| if (!agent_host_.get()) |
| return nullptr; |
| |
| const ExtensionId& extension_id = extension()->id(); |
| DevToolsAgentHost* agent_host = agent_host_.get(); |
| AttachedClientHosts& hosts = GetAttachedClientHosts(); |
| auto it = std::ranges::find_if( |
| hosts, |
| [&agent_host, &extension_id](ExtensionDevToolsClientHost* client_host) { |
| return client_host->agent_host() == agent_host && |
| client_host->extension_id() == extension_id; |
| }); |
| |
| return it == hosts.end() ? nullptr : *it; |
| } |
| |
| // DebuggerAttachFunction ----------------------------------------------------- |
| |
| DebuggerAttachFunction::DebuggerAttachFunction() = default; |
| |
| DebuggerAttachFunction::~DebuggerAttachFunction() = default; |
| |
| ExtensionFunction::ResponseAction DebuggerAttachFunction::Run() { |
| std::optional<Attach::Params> params = Attach::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| CopyDebuggee(&debuggee_, params->target); |
| std::string error; |
| if (!InitAgentHost(&error)) |
| return RespondNow(Error(std::move(error))); |
| |
| if (!DevToolsAgentHost::IsSupportedProtocolVersion( |
| params->required_version)) { |
| return RespondNow(Error(ErrorUtils::FormatErrorMessage( |
| kProtocolVersionNotSupportedError, params->required_version))); |
| } |
| |
| if (FindClientHost()) { |
| return RespondNow(Error(FormatErrorMessage(kAlreadyAttachedError))); |
| } |
| |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| auto host = std::make_unique<ExtensionDevToolsClientHost>( |
| profile, agent_host_.get(), extension(), worker_id(), debuggee_); |
| |
| if (!host->Attach()) { |
| return RespondNow(Error(kRestrictedError)); |
| } |
| |
| host.release(); // An attached client host manages its own lifetime. |
| |
| if (!(Manifest::IsPolicyLocation(extension()->location()) || |
| Manifest::IsComponentLocation(extension()->location()))) { |
| bool is_developer_mode = |
| profile->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode); |
| base::UmaHistogramBoolean("Extensions.Debugger.UserIsInDeveloperMode", |
| is_developer_mode); |
| } |
| |
| return RespondNow(NoArguments()); |
| } |
| |
| // DebuggerDetachFunction ----------------------------------------------------- |
| |
| DebuggerDetachFunction::DebuggerDetachFunction() = default; |
| |
| DebuggerDetachFunction::~DebuggerDetachFunction() = default; |
| |
| ExtensionFunction::ResponseAction DebuggerDetachFunction::Run() { |
| std::optional<Detach::Params> params = Detach::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| CopyDebuggee(&debuggee_, params->target); |
| std::string error; |
| if (!InitClientHost(&error)) |
| return RespondNow(Error(std::move(error))); |
| |
| client_host_->RespondDetachedToPendingRequests(); |
| client_host_->Close(); |
| return RespondNow(NoArguments()); |
| } |
| |
| // DebuggerSendCommandFunction ------------------------------------------------ |
| |
| DebuggerSendCommandFunction::DebuggerSendCommandFunction() = default; |
| |
| DebuggerSendCommandFunction::~DebuggerSendCommandFunction() = default; |
| |
| ExtensionFunction::ResponseAction DebuggerSendCommandFunction::Run() { |
| std::optional<SendCommand::Params> params = |
| SendCommand::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| DebuggeeFromDebuggerSession(debuggee_, params->target); |
| std::string error; |
| if (!InitClientHost(&error)) |
| return RespondNow(Error(std::move(error))); |
| |
| client_host_->SendMessageToBackend( |
| this, params->method, base::OptionalToPtr(params->command_params), |
| params->target.session_id); |
| if (did_respond()) |
| return AlreadyResponded(); |
| return RespondLater(); |
| } |
| |
| void DebuggerSendCommandFunction::SendResponseBody(base::Value response) { |
| if (base::Value* error_body = response.GetDict().Find("error")) { |
| std::string error; |
| base::JSONWriter::Write(*error_body, &error); |
| Respond(Error(std::move(error))); |
| return; |
| } |
| |
| SendCommand::Results::Result result; |
| if (base::Value::Dict* result_body = response.GetDict().FindDict("result")) { |
| result.additional_properties = std::move(*result_body); |
| } |
| |
| Respond(ArgumentList(SendCommand::Results::Create(result))); |
| } |
| |
| void DebuggerSendCommandFunction::SendDetachedError() { |
| Respond(Error(kDetachedWhileHandlingError)); |
| } |
| |
| // DebuggerGetTargetsFunction ------------------------------------------------- |
| |
| namespace { |
| |
| const char kTargetIdField[] = "id"; |
| const char kTargetTypeField[] = "type"; |
| const char kTargetTitleField[] = "title"; |
| const char kTargetAttachedField[] = "attached"; |
| const char kTargetUrlField[] = "url"; |
| const char kTargetFaviconUrlField[] = "faviconUrl"; |
| const char kTargetTabIdField[] = "tabId"; |
| const char kTargetExtensionIdField[] = "extensionId"; |
| const char kTargetTypePage[] = "page"; |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| const char kTargetTypeBackgroundPage[] = "background_page"; |
| #endif |
| const char kTargetTypeWorker[] = "worker"; |
| const char kTargetTypeOther[] = "other"; |
| |
| base::Value::Dict SerializeTarget(scoped_refptr<DevToolsAgentHost> host) { |
| base::Value::Dict dictionary; |
| dictionary.Set(kTargetIdField, host->GetId()); |
| dictionary.Set(kTargetTitleField, host->GetTitle()); |
| dictionary.Set(kTargetAttachedField, host->IsAttached()); |
| dictionary.Set(kTargetUrlField, host->GetURL().spec()); |
| |
| std::string type = host->GetType(); |
| std::string target_type = kTargetTypeOther; |
| if (type == DevToolsAgentHost::kTypePage) { |
| int tab_id = |
| extensions::ExtensionTabUtil::GetTabId(host->GetWebContents()); |
| if (tab_id != api::tabs::TAB_ID_NONE) { |
| dictionary.Set(kTargetTabIdField, tab_id); |
| } else { |
| dictionary.Set(kTargetExtensionIdField, host->GetURL().host()); |
| } |
| target_type = kTargetTypePage; |
| // TODO(crbug.com/405218860): Support background pages on desktop Android. |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| } else if (type == ChromeDevToolsManagerDelegate::kTypeBackgroundPage) { |
| dictionary.Set(kTargetExtensionIdField, host->GetURL().host()); |
| target_type = kTargetTypeBackgroundPage; |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| } else if (type == DevToolsAgentHost::kTypeServiceWorker || |
| type == DevToolsAgentHost::kTypeSharedWorker) { |
| target_type = kTargetTypeWorker; |
| } |
| |
| dictionary.Set(kTargetTypeField, target_type); |
| |
| GURL favicon_url = host->GetFaviconURL(); |
| if (favicon_url.is_valid()) |
| dictionary.Set(kTargetFaviconUrlField, favicon_url.spec()); |
| |
| return dictionary; |
| } |
| |
| } // namespace |
| |
| DebuggerGetTargetsFunction::DebuggerGetTargetsFunction() = default; |
| |
| DebuggerGetTargetsFunction::~DebuggerGetTargetsFunction() = default; |
| |
| ExtensionFunction::ResponseAction DebuggerGetTargetsFunction::Run() { |
| content::DevToolsAgentHost::List list = DevToolsAgentHost::GetOrCreateAll(); |
| base::Value::List result; |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| for (auto& host : list) { |
| // TODO(crbug.com/40233332): hide all Tab targets for now to avoid |
| // compatibility problems. Consider exposing them later when they're fully |
| // supported, and compatibility considerations are better understood. |
| if (host->GetType() == DevToolsAgentHost::kTypeTab) |
| continue; |
| if (!ExtensionMayAttachToTargetProfile( |
| profile, include_incognito_information(), *host)) { |
| continue; |
| } |
| #if BUILDFLAG(ENABLE_PDF) |
| // OOPIF PDF viewer only. Don't list the `content::DevToolsAgentHost`s for |
| // inner PDF frames. PDF extension frames and PDF content frames shouldn't |
| // be exposed to chrome.debugger clients. |
| auto* process_host = host->GetProcessHost(); |
| if (chrome_pdf::features::IsOopifPdfEnabled() && |
| (IsPdfExtensionUrl(host->GetURL()) || |
| (process_host && process_host->IsPdf()))) { |
| continue; |
| } |
| #endif // BUILDFLAG(ENABLE_PDF) |
| result.Append(SerializeTarget(host)); |
| } |
| |
| return RespondNow(WithArguments(std::move(result))); |
| } |
| |
| } // namespace extensions |