// Copyright (c) 2012 The Chromium Authors. All rights reserved. // 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 #include #include #include #include #include "base/command_line.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/lazy_instance.h" #include "base/macros.h" #include "base/memory/singleton.h" #include "base/scoped_observer.h" #include "base/stl_util.h" #include "base/strings/string_number_conversions.h" #include "base/values.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/devtools/chrome_devtools_manager_delegate.h" #include "chrome/browser/extensions/api/debugger/debugger_api_constants.h" #include "chrome/browser/extensions/api/debugger/extension_dev_tools_infobar.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/infobars/infobar_service.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/webui/chrome_web_ui_controller_factory.h" #include "chrome/common/chrome_switches.h" #include "components/infobars/core/infobar.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.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_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/common/constants.h" #include "extensions/common/error_utils.h" #include "extensions/common/extension.h" #include "extensions/common/manifest_constants.h" #include "extensions/common/permissions/permissions_data.h" #include "extensions/common/switches.h" using content::DevToolsAgentHost; using content::RenderProcessHost; using content::RenderWidgetHost; using content::WebContents; namespace keys = debugger_api_constants; 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 { // Helpers -------------------------------------------------------------------- void CopyDebuggee(Debuggee* dst, const Debuggee& src) { if (src.tab_id) dst->tab_id.reset(new int(*src.tab_id)); if (src.extension_id) dst->extension_id.reset(new std::string(*src.extension_id)); if (src.target_id) dst->target_id.reset(new std::string(*src.target_id)); } } // namespace // ExtensionDevToolsClientHost ------------------------------------------------ using AttachedClientHosts = std::set; base::LazyInstance::Leaky g_attached_client_hosts = LAZY_INSTANCE_INITIALIZER; class ExtensionDevToolsClientHost : public content::DevToolsAgentHostClient, public content::NotificationObserver, public ExtensionRegistryObserver { public: ExtensionDevToolsClientHost(Profile* profile, DevToolsAgentHost* agent_host, const std::string& extension_id, const std::string& extension_name, const Debuggee& debuggee); ~ExtensionDevToolsClientHost() override; bool Attach(); const std::string& 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); // Closes connection as terminated by the user. void InfoBarDismissed(); // DevToolsAgentHostClient interface. void AgentHostClosed(DevToolsAgentHost* agent_host) override; void DispatchProtocolMessage(DevToolsAgentHost* agent_host, const std::string& message) override; private: using PendingRequests = std::map>; void SendDetachedEvent(); // content::NotificationObserver implementation. void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) override; // ExtensionRegistryObserver implementation. void OnExtensionUnloaded(content::BrowserContext* browser_context, const Extension* extension, UnloadedExtensionReason reason) override; Profile* profile_; scoped_refptr agent_host_; std::string extension_id_; std::string extension_name_; Debuggee debuggee_; content::NotificationRegistrar registrar_; int last_request_id_; PendingRequests pending_requests_; ExtensionDevToolsInfoBar* infobar_; api::debugger::DetachReason detach_reason_; // Listen to extension unloaded notification. ScopedObserver extension_registry_observer_; DISALLOW_COPY_AND_ASSIGN(ExtensionDevToolsClientHost); }; ExtensionDevToolsClientHost::ExtensionDevToolsClientHost( Profile* profile, DevToolsAgentHost* agent_host, const std::string& extension_id, const std::string& extension_name, const Debuggee& debuggee) : profile_(profile), agent_host_(agent_host), extension_id_(extension_id), extension_name_(extension_name), last_request_id_(0), infobar_(nullptr), detach_reason_(api::debugger::DETACH_REASON_TARGET_CLOSED), extension_registry_observer_(this) { CopyDebuggee(&debuggee_, debuggee); g_attached_client_hosts.Get().insert(this); // ExtensionRegistryObserver listen extension unloaded and detach debugger // from there. extension_registry_observer_.Add(ExtensionRegistry::Get(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. registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING, content::NotificationService::AllSources()); } bool ExtensionDevToolsClientHost::Attach() { // Attach to debugger and tell it we are ready. if (!agent_host_->AttachRestrictedClient(this)) return false; if (base::CommandLine::ForCurrentProcess()->HasSwitch( ::switches::kSilentDebuggerExtensionAPI)) { return true; } // We allow policy-installed extensions to circumvent the normal // infobar warning. See crbug.com/693621. const Extension* extension = ExtensionRegistry::Get(profile_)->enabled_extensions().GetByID( extension_id_); // TODO(dgozman): null-checking |extension| below is sketchy. // We probably should not allow debugging in this case. Or maybe // it's never null? if (extension && Manifest::IsPolicyLocation(extension->location())) return true; infobar_ = ExtensionDevToolsInfoBar::Create( extension_id_, extension_name_, this, base::Bind(&ExtensionDevToolsClientHost::InfoBarDismissed, base::Unretained(this))); return true; } ExtensionDevToolsClientHost::~ExtensionDevToolsClientHost() { if (infobar_) infobar_->Remove(this); g_attached_client_hosts.Get().erase(this); } // 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) { base::DictionaryValue protocol_request; int request_id = ++last_request_id_; pending_requests_[request_id] = function; protocol_request.SetInteger("id", request_id); protocol_request.SetString("method", method); if (command_params) { protocol_request.Set( "params", command_params->additional_properties.CreateDeepCopy()); } std::string json_args; base::JSONWriter::Write(protocol_request, &json_args); agent_host_->DispatchProtocolMessage(this, json_args); } void ExtensionDevToolsClientHost::InfoBarDismissed() { detach_reason_ = api::debugger::DETACH_REASON_CANCELED_BY_USER; 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; std::unique_ptr args( OnDetach::Create(debuggee_, detach_reason_)); auto event = std::make_unique(events::DEBUGGER_ON_DETACH, OnDetach::kEventName, std::move(args), profile_); EventRouter::Get(profile_) ->DispatchEventToExtension(extension_id_, std::move(event)); } void ExtensionDevToolsClientHost::OnExtensionUnloaded( content::BrowserContext* browser_context, const Extension* extension, UnloadedExtensionReason reason) { if (extension->id() == extension_id_) Close(); } void ExtensionDevToolsClientHost::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type); Close(); } void ExtensionDevToolsClientHost::DispatchProtocolMessage( DevToolsAgentHost* agent_host, const std::string& message) { DCHECK(agent_host == agent_host_.get()); if (!EventRouter::Get(profile_)) return; std::unique_ptr result = base::JSONReader::Read(message); if (!result || !result->is_dict()) return; base::DictionaryValue* dictionary = static_cast(result.get()); int id; if (!dictionary->GetInteger("id", &id)) { std::string method_name; if (!dictionary->GetString("method", &method_name)) return; OnEvent::Params params; base::DictionaryValue* params_value; if (dictionary->GetDictionary("params", ¶ms_value)) params.additional_properties.Swap(params_value); std::unique_ptr args( OnEvent::Create(debuggee_, method_name, params)); auto event = std::make_unique(events::DEBUGGER_ON_EVENT, OnEvent::kEventName, std::move(args), profile_); EventRouter::Get(profile_) ->DispatchEventToExtension(extension_id_, std::move(event)); } else { DebuggerSendCommandFunction* function = pending_requests_[id].get(); if (!function) return; function->SendResponseBody(dictionary); pending_requests_.erase(id); } } // DebuggerFunction ----------------------------------------------------------- DebuggerFunction::DebuggerFunction() : client_host_(NULL) { } DebuggerFunction::~DebuggerFunction() { } void DebuggerFunction::FormatErrorMessage(const std::string& format) { if (debuggee_.tab_id) error_ = ErrorUtils::FormatErrorMessage( format, keys::kTabTargetType, base::IntToString(*debuggee_.tab_id)); else if (debuggee_.extension_id) error_ = ErrorUtils::FormatErrorMessage( format, keys::kBackgroundPageTargetType, *debuggee_.extension_id); else error_ = ErrorUtils::FormatErrorMessage( format, keys::kOpaqueTargetType, *debuggee_.target_id); } bool DebuggerFunction::InitAgentHost() { if (debuggee_.tab_id) { WebContents* web_contents = NULL; bool result = ExtensionTabUtil::GetTabById(*debuggee_.tab_id, GetProfile(), include_incognito(), NULL, NULL, &web_contents, NULL); if (result && web_contents) { // TODO(rdevlin.cronin) This should definitely be GetLastCommittedURL(). GURL url = web_contents->GetVisibleURL(); if (extension()->permissions_data()->IsRestrictedUrl(url, &error_)) return false; agent_host_ = DevToolsAgentHost::GetOrCreateFor(web_contents); } } else if (debuggee_.extension_id) { ExtensionHost* extension_host = ProcessManager::Get(GetProfile()) ->GetBackgroundHostForExtension(*debuggee_.extension_id); if (extension_host) { if (extension()->permissions_data()->IsRestrictedUrl( extension_host->GetURL(), &error_)) { return false; } agent_host_ = DevToolsAgentHost::GetOrCreateFor(extension_host->host_contents()); } } else if (debuggee_.target_id) { agent_host_ = DevToolsAgentHost::GetForId(*debuggee_.target_id); if (agent_host_.get()) { if (extension()->permissions_data()->IsRestrictedUrl( agent_host_->GetURL(), &error_)) { agent_host_ = nullptr; return false; } } } else { error_ = keys::kInvalidTargetError; return false; } if (!agent_host_.get()) { FormatErrorMessage(keys::kNoTargetError); return false; } return true; } bool DebuggerFunction::InitClientHost() { if (!InitAgentHost()) return false; client_host_ = FindClientHost(); if (!client_host_) { FormatErrorMessage(keys::kNotAttachedError); return false; } return true; } ExtensionDevToolsClientHost* DebuggerFunction::FindClientHost() { if (!agent_host_.get()) return nullptr; const std::string& extension_id = extension()->id(); DevToolsAgentHost* agent_host = agent_host_.get(); AttachedClientHosts& hosts = g_attached_client_hosts.Get(); AttachedClientHosts::iterator it = std::find_if( hosts.begin(), hosts.end(), [&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() { } DebuggerAttachFunction::~DebuggerAttachFunction() { } bool DebuggerAttachFunction::RunAsync() { std::unique_ptr params(Attach::Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); CopyDebuggee(&debuggee_, params->target); if (!InitAgentHost()) return false; if (!DevToolsAgentHost::IsSupportedProtocolVersion( params->required_version)) { error_ = ErrorUtils::FormatErrorMessage( keys::kProtocolVersionNotSupportedError, params->required_version); return false; } if (FindClientHost()) { FormatErrorMessage(keys::kAlreadyAttachedError); return false; } auto host = std::make_unique( GetProfile(), agent_host_.get(), extension()->id(), extension()->name(), debuggee_); if (!host->Attach()) { FormatErrorMessage(keys::kRestrictedError); return false; } host.release(); // An attached client host manages its own lifetime. SendResponse(true); return true; } // DebuggerDetachFunction ----------------------------------------------------- DebuggerDetachFunction::DebuggerDetachFunction() { } DebuggerDetachFunction::~DebuggerDetachFunction() { } bool DebuggerDetachFunction::RunAsync() { std::unique_ptr params(Detach::Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); CopyDebuggee(&debuggee_, params->target); if (!InitClientHost()) return false; client_host_->RespondDetachedToPendingRequests(); client_host_->Close(); SendResponse(true); return true; } // DebuggerSendCommandFunction ------------------------------------------------ DebuggerSendCommandFunction::DebuggerSendCommandFunction() { } DebuggerSendCommandFunction::~DebuggerSendCommandFunction() { } bool DebuggerSendCommandFunction::RunAsync() { std::unique_ptr params( SendCommand::Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); CopyDebuggee(&debuggee_, params->target); if (!InitClientHost()) return false; client_host_->SendMessageToBackend(this, params->method, params->command_params.get()); return true; } void DebuggerSendCommandFunction::SendResponseBody( base::DictionaryValue* response) { base::Value* error_body; if (response->Get("error", &error_body)) { base::JSONWriter::Write(*error_body, &error_); SendResponse(false); return; } base::DictionaryValue* result_body; SendCommand::Results::Result result; if (response->GetDictionary("result", &result_body)) result.additional_properties.Swap(result_body); results_ = SendCommand::Results::Create(result); SendResponse(true); } void DebuggerSendCommandFunction::SendDetachedError() { error_ = keys::kDetachedWhileHandlingError; SendResponse(false); } // 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 kTargetTypeWorker[] = "worker"; std::unique_ptr SerializeTarget( scoped_refptr host) { std::unique_ptr dictionary( new base::DictionaryValue()); dictionary->SetString(kTargetIdField, host->GetId()); dictionary->SetString(kTargetTitleField, host->GetTitle()); dictionary->SetBoolean(kTargetAttachedField, host->IsAttached()); dictionary->SetString(kTargetUrlField, host->GetURL().spec()); std::string type = host->GetType(); if (type == DevToolsAgentHost::kTypePage) { int tab_id = extensions::ExtensionTabUtil::GetTabId(host->GetWebContents()); dictionary->SetInteger(kTargetTabIdField, tab_id); } else if (type == ChromeDevToolsManagerDelegate::kTypeBackgroundPage) { dictionary->SetString(kTargetExtensionIdField, host->GetURL().host()); } if (type == DevToolsAgentHost::kTypeServiceWorker || type == DevToolsAgentHost::kTypeSharedWorker) { type = kTargetTypeWorker; } dictionary->SetString(kTargetTypeField, type); GURL favicon_url = host->GetFaviconURL(); if (favicon_url.is_valid()) dictionary->SetString(kTargetFaviconUrlField, favicon_url.spec()); return dictionary; } } // namespace DebuggerGetTargetsFunction::DebuggerGetTargetsFunction() { } DebuggerGetTargetsFunction::~DebuggerGetTargetsFunction() { } bool DebuggerGetTargetsFunction::RunAsync() { content::DevToolsAgentHost::List list = DevToolsAgentHost::GetOrCreateAll(); content::BrowserThread::PostTask( content::BrowserThread::UI, FROM_HERE, base::BindOnce(&DebuggerGetTargetsFunction::SendTargetList, this, list)); return true; } void DebuggerGetTargetsFunction::SendTargetList( const content::DevToolsAgentHost::List& target_list) { std::unique_ptr result(new base::ListValue()); for (size_t i = 0; i < target_list.size(); ++i) result->Append(SerializeTarget(target_list[i])); SetResult(std::move(result)); SendResponse(true); } } // namespace extensions