| // 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. | |
| #include "content/browser/devtools/devtools_http_handler.h" | |
| #include <stddef.h> | |
| #include <stdint.h> | |
| #include <algorithm> | |
| #include <memory> | |
| #include <optional> | |
| #include <string_view> | |
| #include <utility> | |
| #include "base/command_line.h" | |
| #include "base/compiler_specific.h" | |
| #include "base/files/file_util.h" | |
| #include "base/functional/bind.h" | |
| #include "base/functional/callback_helpers.h" | |
| #include "base/json/json_writer.h" | |
| #include "base/location.h" | |
| #include "base/logging.h" | |
| #include "base/memory/raw_ptr.h" | |
| #include "base/memory/ref_counted_memory.h" | |
| #include "base/message_loop/message_pump_type.h" | |
| #include "base/strings/escape.h" | |
| #include "base/strings/strcat.h" | |
| #include "base/strings/string_number_conversions.h" | |
| #include "base/strings/string_util.h" | |
| #include "base/strings/string_view_util.h" | |
| #include "base/strings/stringprintf.h" | |
| #include "base/task/single_thread_task_runner.h" | |
| #include "base/task/thread_pool.h" | |
| #include "base/threading/thread.h" | |
| #include "base/uuid.h" | |
| #include "base/values.h" | |
| #include "build/build_config.h" | |
| #include "build/util/chromium_git_revision.h" | |
| #include "content/browser/devtools/devtools_manager.h" | |
| #include "content/public/browser/browser_task_traits.h" | |
| #include "content/public/browser/browser_thread.h" | |
| #include "content/public/browser/content_browser_client.h" | |
| #include "content/public/browser/devtools_external_agent_proxy_delegate.h" | |
| #include "content/public/browser/devtools_frontend_host.h" | |
| #include "content/public/browser/devtools_manager_delegate.h" | |
| #include "content/public/browser/devtools_socket_factory.h" | |
| #include "content/public/common/content_client.h" | |
| #include "content/public/common/content_constants.h" | |
| #include "content/public/common/content_switches.h" | |
| #include "content/public/common/url_constants.h" | |
| #include "net/base/io_buffer.h" | |
| #include "net/base/net_errors.h" | |
| #include "net/base/url_util.h" | |
| #include "net/http/http_request_headers.h" | |
| #include "net/server/http_server.h" | |
| #include "net/server/http_server_request_info.h" | |
| #include "net/server/http_server_response_info.h" | |
| #include "net/socket/server_socket.h" | |
| #include "net/traffic_annotation/network_traffic_annotation.h" | |
| #include "url/origin.h" | |
| #include "v8/include/v8-version-string.h" | |
| #if BUILDFLAG(IS_ANDROID) | |
| #include "base/android/apk_info.h" | |
| #endif | |
| #if BUILDFLAG(ENABLE_DEVTOOLS_FRONTEND) | |
| extern const int kCcompressedProtocolJSON; | |
| #endif | |
| namespace content { | |
| namespace { | |
| const char kDevToolsHandlerThreadName[] = "Chrome_DevToolsHandlerThread"; | |
| const char kPageUrlPrefix[] = "/devtools/page/"; | |
| const char kBrowserUrlPrefix[] = "/devtools/browser"; | |
| const char kTargetIdField[] = "id"; | |
| const char kTargetParentIdField[] = "parentId"; | |
| const char kTargetTypeField[] = "type"; | |
| const char kTargetTitleField[] = "title"; | |
| const char kTargetDescriptionField[] = "description"; | |
| const char kTargetUrlField[] = "url"; | |
| const char kTargetFaviconUrlField[] = "faviconUrl"; | |
| const char kTargetWebSocketDebuggerUrlField[] = "webSocketDebuggerUrl"; | |
| const char kTargetDevtoolsFrontendUrlField[] = "devtoolsFrontendUrl"; | |
| const char kMissingGitRevision[] = "@0000000000000000000000000000000000000000"; | |
| #if BUILDFLAG(IS_WIN) | |
| const char kNativeFilePathStyle[] = "windows"; | |
| #elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA) | |
| const char kNativeFilePathStyle[] = "posix"; | |
| #else | |
| #error Unsupported native file path style | |
| #endif | |
| const int32_t kSendBufferSizeForDevTools = 256 * 1024 * 1024; // 256Mb | |
| const int32_t kReceiveBufferSizeForDevTools = 100 * 1024 * 1024; // 100Mb | |
| const char kRemoteUrlPattern[] = | |
| "https://chrome-devtools-frontend.appspot.com/serve_rev/%s/%s.html"; | |
| constexpr net::NetworkTrafficAnnotationTag | |
| kDevtoolsHttpHandlerTrafficAnnotation = | |
| net::DefineNetworkTrafficAnnotation("devtools_http_handler", R"( | |
| semantics { | |
| sender: "Devtools Http Handler" | |
| description: | |
| "This is a remote debugging server, only enabled by " | |
| "'--remote-debugging-port' switch. It exposes debugging protocol " | |
| "over websockets." | |
| trigger: "Run with '--remote-debugging-port' switch." | |
| data: "Debugging data, including any data on the open pages." | |
| destination: OTHER | |
| destination_other: "The data can be sent to any destination." | |
| } | |
| 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-port' switch." | |
| policy_exception_justification: | |
| "Not implemented, only used in Devtools and is behind a switch." | |
| })"); | |
| bool RequestIsSafeToServe(const net::HttpServerRequestInfo& info) { | |
| // For browser-originating requests, serve only those that are coming from | |
| // pages loaded off localhost or fixed IPs. | |
| std::string header = info.GetHeaderValue("host"); | |
| if (header.empty()) | |
| return true; | |
| GURL url = GURL("https://" + header); | |
| return url.HostIsIPAddress() || net::IsLocalHostname(url.GetHost()); | |
| } | |
| // Returns the (incorrectly named, for historical reasons) WebKit version, in | |
| // the form "major.minor (@chromium_git_revision)". | |
| std::string GetWebKitVersion() { | |
| return base::StringPrintf("537.36 (%s)", CHROMIUM_GIT_REVISION); | |
| } | |
| } // namespace | |
| // ServerWrapper ------------------------------------------------------------- | |
| // All methods in this class are only called on handler thread. | |
| class ServerWrapper : net::HttpServer::Delegate { | |
| public: | |
| ServerWrapper(base::WeakPtr<DevToolsHttpHandler> handler, | |
| std::unique_ptr<net::ServerSocket> socket, | |
| const base::FilePath& debug_frontend_dir, | |
| bool bundles_resources); | |
| int GetLocalAddress(net::IPEndPoint* address); | |
| void AcceptWebSocket(int connection_id, | |
| const net::HttpServerRequestInfo& request); | |
| void SendOverWebSocket(int connection_id, std::string message); | |
| void SendResponse(int connection_id, | |
| const net::HttpServerResponseInfo& response); | |
| void Send200(int connection_id, | |
| std::string_view data, | |
| std::string_view mime_type); | |
| void Send404(int connection_id); | |
| void Send500(int connection_id, const std::string& message); | |
| void Close(int connection_id); | |
| ~ServerWrapper() override {} | |
| private: | |
| // net::HttpServer::Delegate implementation. | |
| void OnConnect(int connection_id) override {} | |
| void OnHttpRequest(int connection_id, | |
| const net::HttpServerRequestInfo& info) override; | |
| void OnWebSocketRequest(int connection_id, | |
| const net::HttpServerRequestInfo& info) override; | |
| void OnWebSocketMessage(int connection_id, std::string data) override; | |
| void OnClose(int connection_id) override; | |
| base::WeakPtr<DevToolsHttpHandler> handler_; | |
| std::unique_ptr<net::HttpServer> server_; | |
| base::FilePath debug_frontend_dir_; | |
| bool bundles_resources_; | |
| }; | |
| ServerWrapper::ServerWrapper(base::WeakPtr<DevToolsHttpHandler> handler, | |
| std::unique_ptr<net::ServerSocket> socket, | |
| const base::FilePath& debug_frontend_dir, | |
| bool bundles_resources) | |
| : handler_(handler), | |
| server_(new net::HttpServer(std::move(socket), this)), | |
| debug_frontend_dir_(debug_frontend_dir), | |
| bundles_resources_(bundles_resources) {} | |
| int ServerWrapper::GetLocalAddress(net::IPEndPoint* address) { | |
| return server_->GetLocalAddress(address); | |
| } | |
| void ServerWrapper::AcceptWebSocket(int connection_id, | |
| const net::HttpServerRequestInfo& request) { | |
| server_->SetSendBufferSize(connection_id, kSendBufferSizeForDevTools); | |
| server_->SetReceiveBufferSize(connection_id, kReceiveBufferSizeForDevTools); | |
| server_->AcceptWebSocket(connection_id, request, | |
| kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::SendOverWebSocket(int connection_id, std::string message) { | |
| server_->SendOverWebSocket(connection_id, std::move(message), | |
| kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::SendResponse(int connection_id, | |
| const net::HttpServerResponseInfo& response) { | |
| server_->SendResponse(connection_id, response, | |
| kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::Send200(int connection_id, | |
| std::string_view data, | |
| std::string_view mime_type) { | |
| server_->Send200(connection_id, data, mime_type, | |
| kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::Send404(int connection_id) { | |
| server_->Send404(connection_id, kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::Send500(int connection_id, | |
| const std::string& message) { | |
| server_->Send500(connection_id, message, | |
| kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::Close(int connection_id) { | |
| server_->Close(connection_id); | |
| } | |
| // Thread and ServerWrapper lifetime management ------------------------------ | |
| void TerminateOnUI(std::unique_ptr<base::Thread> thread, | |
| std::unique_ptr<ServerWrapper> server_wrapper, | |
| std::unique_ptr<DevToolsSocketFactory> socket_factory) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| if (server_wrapper) | |
| thread->task_runner()->DeleteSoon(FROM_HERE, std::move(server_wrapper)); | |
| if (socket_factory) | |
| thread->task_runner()->DeleteSoon(FROM_HERE, std::move(socket_factory)); | |
| if (thread) { | |
| base::ThreadPool::PostTask( | |
| FROM_HERE, | |
| {base::WithBaseSyncPrimitives(), base::TaskPriority::BEST_EFFORT}, | |
| DoNothingWithBoundArgs(std::move(thread))); | |
| } | |
| } | |
| void ServerStartedOnUI(base::WeakPtr<DevToolsHttpHandler> handler, | |
| std::unique_ptr<base::Thread> thread, | |
| ServerWrapper* server_wrapper, | |
| DevToolsSocketFactory* socket_factory, | |
| std::unique_ptr<net::IPEndPoint> ip_address) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| if (handler && thread && server_wrapper) { | |
| handler->ServerStarted( | |
| std::move(thread), std::unique_ptr<ServerWrapper>(server_wrapper), | |
| std::unique_ptr<DevToolsSocketFactory>(socket_factory), | |
| std::move(ip_address)); | |
| } else { | |
| TerminateOnUI(std::move(thread), | |
| std::unique_ptr<ServerWrapper>(server_wrapper), | |
| std::unique_ptr<DevToolsSocketFactory>(socket_factory)); | |
| } | |
| } | |
| void StartServerOnHandlerThread( | |
| base::WeakPtr<DevToolsHttpHandler> handler, | |
| std::unique_ptr<base::Thread> thread, | |
| std::unique_ptr<DevToolsSocketFactory> socket_factory, | |
| const base::FilePath& output_directory, | |
| const base::FilePath& debug_frontend_dir, | |
| const std::string& browser_guid, | |
| bool bundles_resources) { | |
| CHECK(thread->task_runner()->BelongsToCurrentThread(), | |
| base::NotFatalUntil::M159); | |
| std::unique_ptr<ServerWrapper> server_wrapper; | |
| std::unique_ptr<net::ServerSocket> server_socket = | |
| socket_factory->CreateForHttpServer(); | |
| std::unique_ptr<net::IPEndPoint> ip_address(new net::IPEndPoint); | |
| if (server_socket) { | |
| server_wrapper = | |
| std::make_unique<ServerWrapper>(handler, std::move(server_socket), | |
| debug_frontend_dir, bundles_resources); | |
| if (server_wrapper->GetLocalAddress(ip_address.get()) != net::OK) | |
| ip_address.reset(); | |
| } else { | |
| ip_address.reset(); | |
| } | |
| if (ip_address) { | |
| std::string message = base::StringPrintf( | |
| "\nDevTools listening on ws://%s%s\n", ip_address->ToString().c_str(), | |
| browser_guid.c_str()); | |
| fprintf(stderr, "%s", message.c_str()); | |
| fflush(stderr); | |
| // Write this port to a well-known file in the profile directory | |
| // so Telemetry, ChromeDriver, etc. can pick it up. | |
| if (!output_directory.empty()) { | |
| base::FilePath path = | |
| output_directory.Append(kDevToolsActivePortFileName); | |
| std::string port_target_string = base::StringPrintf( | |
| "%d\n%s", ip_address->port(), browser_guid.c_str()); | |
| if (!base::WriteFile(path, port_target_string)) { | |
| PLOG(ERROR) << "Error writing DevTools active port to file " << path; | |
| } | |
| } | |
| } else { | |
| #if !BUILDFLAG(IS_ANDROID) | |
| // Android uses UNIX domain sockets which don't have an IP address. | |
| LOG(ERROR) << "Cannot start http server for devtools."; | |
| #endif | |
| } | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&ServerStartedOnUI, std::move(handler), std::move(thread), | |
| server_wrapper.release(), socket_factory.release(), | |
| std::move(ip_address))); | |
| } | |
| // DevToolsAgentHostClientImpl ----------------------------------------------- | |
| // An internal implementation of DevToolsAgentHostClient that delegates | |
| // messages sent to a DebuggerShell instance. | |
| class DevToolsAgentHostClientImpl : public DevToolsAgentHostClient { | |
| public: | |
| DevToolsAgentHostClientImpl( | |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner, | |
| ServerWrapper* server_wrapper, | |
| int connection_id, | |
| scoped_refptr<DevToolsAgentHost> agent_host) | |
| : task_runner_(std::move(task_runner)), | |
| server_wrapper_(server_wrapper), | |
| connection_id_(connection_id), | |
| agent_host_(agent_host) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| // TODO(dgozman): handle return value of AttachClient. | |
| agent_host_->AttachClient(this); | |
| } | |
| ~DevToolsAgentHostClientImpl() override { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| if (agent_host_) | |
| agent_host_->DetachClient(this); | |
| } | |
| std::string GetTypeForMetrics() override { return "RemoteDebugger"; } | |
| void AgentHostClosed(DevToolsAgentHost* agent_host) override { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| CHECK(agent_host == agent_host_.get(), base::NotFatalUntil::M159); | |
| constexpr char kMsg[] = | |
| "{\"method\":\"Inspector.detached\"," | |
| "\"params\":{\"reason\":\"target_closed\"}}"; | |
| DispatchProtocolMessage(agent_host, base::byte_span_from_cstring(kMsg)); | |
| agent_host_ = nullptr; | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&ServerWrapper::Close, base::Unretained(server_wrapper_), | |
| connection_id_)); | |
| } | |
| void DispatchProtocolMessage(DevToolsAgentHost* agent_host, | |
| base::span<const uint8_t> message) override { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| CHECK(agent_host == agent_host_.get(), base::NotFatalUntil::M159); | |
| task_runner_->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&ServerWrapper::SendOverWebSocket, | |
| base::Unretained(server_wrapper_), connection_id_, | |
| std::string(message.begin(), message.end()))); | |
| } | |
| void OnMessage(base::span<const uint8_t> message) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| if (agent_host_) | |
| agent_host_->DispatchProtocolMessage(this, message); | |
| } | |
| private: | |
| const scoped_refptr<base::SingleThreadTaskRunner> task_runner_; | |
| const raw_ptr<ServerWrapper> server_wrapper_; | |
| const int connection_id_; | |
| scoped_refptr<DevToolsAgentHost> agent_host_; | |
| }; | |
| static bool TimeComparator(scoped_refptr<DevToolsAgentHost> host1, | |
| scoped_refptr<DevToolsAgentHost> host2) { | |
| return host1->GetLastActivityTime() > host2->GetLastActivityTime(); | |
| } | |
| // DevToolsHttpHandler ------------------------------------------------------- | |
| net::IPEndPoint DevToolsHttpHandler::GetServerIpAddress() const { | |
| if (server_ip_address_) { | |
| return *server_ip_address_; | |
| } | |
| return net::IPEndPoint(); | |
| } | |
| DevToolsHttpHandler::~DevToolsHttpHandler() { | |
| if (delegate_ && | |
| mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| delegate_->SetActiveWebSocketConnections(0); | |
| } | |
| // Disconnecting sessions might lead to the last minute messages generated | |
| // by the targets. It is essential that this happens before we issue delete | |
| // soon for the server wrapper. | |
| connection_to_client_.clear(); | |
| TerminateOnUI(std::move(thread_), std::move(server_wrapper_), | |
| std::move(socket_factory_)); | |
| delegate_ = nullptr; | |
| } | |
| static std::string_view PathWithoutParams(std::string_view path) { | |
| size_t query_position = path.find('?'); | |
| if (query_position != std::string_view::npos) { | |
| return path.substr(0, query_position); | |
| } | |
| return path; | |
| } | |
| static std::string_view GetMimeType(std::string_view filename) { | |
| if (base::EndsWith(filename, ".html", base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "text/html"; | |
| } else if (base::EndsWith(filename, ".css", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "text/css"; | |
| } else if (base::EndsWith(filename, ".js", | |
| base::CompareCase::INSENSITIVE_ASCII) || | |
| base::EndsWith(filename, ".mjs", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "text/javascript"; | |
| } else if (base::EndsWith(filename, ".png", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "image/png"; | |
| } else if (base::EndsWith(filename, ".gif", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "image/gif"; | |
| } else if (base::EndsWith(filename, ".json", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "application/json"; | |
| } else if (base::EndsWith(filename, ".svg", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "image/svg+xml"; | |
| } else if (base::EndsWith(filename, ".avif", | |
| base::CompareCase::INSENSITIVE_ASCII)) { | |
| return "image/avif"; | |
| } | |
| LOG(ERROR) << "GetMimeType doesn't know mime type for: " | |
| << filename | |
| << " text/plain will be returned"; | |
| return "text/plain"; | |
| } | |
| void ServerWrapper::OnHttpRequest(int connection_id, | |
| const net::HttpServerRequestInfo& info) { | |
| if (!RequestIsSafeToServe(info)) { | |
| Send500(connection_id, | |
| "Host header is specified and is not an IP address or localhost."); | |
| return; | |
| } | |
| server_->SetSendBufferSize(connection_id, kSendBufferSizeForDevTools); | |
| if (base::StartsWith(info.path, "/json", base::CompareCase::SENSITIVE)) { | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, base::BindOnce(&DevToolsHttpHandler::OnJsonRequest, handler_, | |
| connection_id, info)); | |
| return; | |
| } | |
| if (info.path.empty() || info.path == "/") { | |
| // Discovery page request. | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, base::BindOnce(&DevToolsHttpHandler::OnDiscoveryPageRequest, | |
| handler_, connection_id)); | |
| return; | |
| } | |
| if (!base::StartsWith(info.path, "/devtools/", | |
| base::CompareCase::SENSITIVE)) { | |
| server_->Send404(connection_id, kDevtoolsHttpHandlerTrafficAnnotation); | |
| return; | |
| } | |
| std::string_view filename = | |
| PathWithoutParams(std::string_view(info.path).substr(10)); | |
| std::string_view mime_type = GetMimeType(filename); | |
| if (!debug_frontend_dir_.empty()) { | |
| base::FilePath filepath = debug_frontend_dir_.AppendASCII(filename); | |
| std::string data; | |
| base::ReadFileToString(filepath, &data); | |
| server_->Send200(connection_id, data, mime_type, | |
| kDevtoolsHttpHandlerTrafficAnnotation); | |
| return; | |
| } | |
| if (bundles_resources_) { | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&DevToolsHttpHandler::OnFrontendResourceRequest, | |
| handler_, connection_id, std::string(filename))); | |
| return; | |
| } | |
| server_->Send404(connection_id, kDevtoolsHttpHandlerTrafficAnnotation); | |
| } | |
| void ServerWrapper::OnWebSocketRequest( | |
| int connection_id, | |
| const net::HttpServerRequestInfo& request) { | |
| if (!RequestIsSafeToServe(request)) { | |
| Send500(connection_id, | |
| "Host header is specified and is not an IP address or localhost."); | |
| return; | |
| } | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, base::BindOnce(&DevToolsHttpHandler::OnWebSocketRequest, | |
| handler_, connection_id, request)); | |
| } | |
| void ServerWrapper::OnWebSocketMessage(int connection_id, std::string data) { | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, base::BindOnce(&DevToolsHttpHandler::OnWebSocketMessage, | |
| handler_, connection_id, std::move(data))); | |
| } | |
| void ServerWrapper::OnClose(int connection_id) { | |
| GetUIThreadTaskRunner({})->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&DevToolsHttpHandler::OnClose, handler_, connection_id)); | |
| } | |
| std::string DevToolsHttpHandler::GetFrontendURLInternal( | |
| scoped_refptr<DevToolsAgentHost> agent_host, | |
| const std::string& id, | |
| const std::string& host) { | |
| std::string frontend_url; | |
| const std::string git_revision = CHROMIUM_GIT_REVISION; | |
| if (git_revision == kMissingGitRevision && | |
| delegate_->HasBundledFrontendResources()) { | |
| frontend_url = "/devtools/inspector.html"; | |
| } else { | |
| std::string type = agent_host->GetType(); | |
| bool is_worker = type == DevToolsAgentHost::kTypeServiceWorker || | |
| type == DevToolsAgentHost::kTypeSharedWorker; | |
| frontend_url = base::StringPrintf(kRemoteUrlPattern, git_revision.c_str(), | |
| is_worker ? "worker_app" : "inspector"); | |
| } | |
| return base::StringPrintf("%s?ws=%s%s%s", frontend_url.c_str(), host.c_str(), | |
| kPageUrlPrefix, id.c_str()); | |
| } | |
| static bool ParseJsonPath(std::string_view path, | |
| std::string* command, | |
| std::string* target_id) { | |
| // Fall back to list in case of empty query. | |
| if (path.empty()) { | |
| *command = "list"; | |
| return true; | |
| } | |
| if (!base::StartsWith(path, "/", base::CompareCase::SENSITIVE)) { | |
| // Malformed command. | |
| return false; | |
| } | |
| std::string_view command_view = path.substr(1); | |
| size_t separator_pos = command_view.find("/"); | |
| if (separator_pos != std::string_view::npos) { | |
| *target_id = command_view.substr(separator_pos + 1); | |
| command_view = command_view.substr(0, separator_pos); | |
| } | |
| *command = command_view; | |
| return true; | |
| } | |
| void DevToolsHttpHandler::OnJsonRequest( | |
| int connection_id, | |
| const net::HttpServerRequestInfo& info) { | |
| if (mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| Send404(connection_id); | |
| return; | |
| } | |
| // Trim /json | |
| std::string_view path = std::string_view(info.path).substr(5); | |
| // Trim fragment and query | |
| std::string_view query; | |
| size_t query_pos = path.find('?'); | |
| if (query_pos != std::string_view::npos) { | |
| query = path.substr(query_pos + 1); | |
| path = path.substr(0, query_pos); | |
| } | |
| size_t fragment_pos = path.find('#'); | |
| if (fragment_pos != std::string_view::npos) { | |
| path = path.substr(0, fragment_pos); | |
| } | |
| std::string command; | |
| std::string target_id; | |
| if (!ParseJsonPath(path, &command, &target_id)) { | |
| SendJson(connection_id, net::HTTP_NOT_FOUND, std::nullopt, | |
| "Malformed query: " + info.path); | |
| return; | |
| } | |
| if (command == "version") { | |
| base::DictValue version; | |
| version.Set("Protocol-Version", DevToolsAgentHost::GetProtocolVersion()); | |
| version.Set("WebKit-Version", GetWebKitVersion()); | |
| version.Set("Browser", GetContentClient()->browser()->GetProduct()); | |
| version.Set("File-Path-Style", kNativeFilePathStyle); | |
| version.Set("User-Agent", GetContentClient()->browser()->GetUserAgent()); | |
| version.Set("V8-Version", V8_VERSION_STRING); | |
| std::string host = info.GetHeaderValue("host"); | |
| version.Set( | |
| kTargetWebSocketDebuggerUrlField, | |
| base::StringPrintf("ws://%s%s", host.c_str(), browser_guid_.c_str())); | |
| #if BUILDFLAG(IS_ANDROID) | |
| version.Set("Android-Package", | |
| base::android::apk_info::host_package_name()); | |
| #endif | |
| SendJson(connection_id, net::HTTP_OK, version, ""); | |
| return; | |
| } | |
| if (command == "protocol") { | |
| DecompressAndSendJsonProtocol(connection_id); | |
| return; | |
| } | |
| std::vector<std::string_view> query_components = base::SplitStringPiece( | |
| query, "&", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | |
| bool for_tab = std::ranges::contains(query_components, "for_tab"); | |
| if (command == "list") { | |
| DevToolsManager* manager = DevToolsManager::GetInstance(); | |
| DevToolsAgentHost::List list = | |
| manager->delegate() ? manager->delegate()->RemoteDebuggingTargets( | |
| for_tab ? DevToolsManagerDelegate::kTab | |
| : DevToolsManagerDelegate::kFrame) | |
| : DevToolsAgentHost::GetOrCreateAll(); | |
| RespondToJsonList(connection_id, info.GetHeaderValue("host"), | |
| std::move(list), for_tab); | |
| return; | |
| } | |
| if (command == "new") { | |
| if (!base::EqualsCaseInsensitiveASCII( | |
| info.method, net::HttpRequestHeaders::kPutMethod)) { | |
| SendJson( | |
| connection_id, net::HTTP_METHOD_NOT_ALLOWED, std::nullopt, | |
| base::StringPrintf("Using unsafe HTTP verb %s to invoke /json/new. " | |
| "This action supports only PUT verb.", | |
| info.method.c_str())); | |
| return; | |
| } | |
| std::string_view escaped_url = | |
| query_components.empty() ? "" : query_components[0]; | |
| GURL url(base::UnescapeBinaryURLComponent(escaped_url)); | |
| if (!url.is_valid()) | |
| url = GURL(url::kAboutBlankURL); | |
| // TODO(dsv): Remove for "for_tab" support once DevTools Frontend | |
| // no longer needs it for e2e tests | |
| scoped_refptr<DevToolsAgentHost> agent_host = | |
| delegate_->CreateNewTarget(url, | |
| for_tab ? DevToolsManagerDelegate::kTab | |
| : DevToolsManagerDelegate::kFrame, | |
| /*new_window=*/false); | |
| if (!agent_host) { | |
| SendJson(connection_id, net::HTTP_INTERNAL_SERVER_ERROR, std::nullopt, | |
| "Could not create new page"); | |
| return; | |
| } | |
| std::string host = info.GetHeaderValue("host"); | |
| base::DictValue descriptor = SerializeDescriptor(agent_host, host); | |
| SendJson(connection_id, net::HTTP_OK, descriptor, ""); | |
| return; | |
| } | |
| if (command == "activate" || command == "close") { | |
| scoped_refptr<DevToolsAgentHost> agent_host = | |
| DevToolsAgentHost::GetForId(target_id); | |
| if (!agent_host) { | |
| SendJson(connection_id, net::HTTP_NOT_FOUND, std::nullopt, | |
| "No such target id: " + target_id); | |
| return; | |
| } | |
| DevToolsManager* manager = DevToolsManager::GetInstance(); | |
| if (manager->delegate() && | |
| !manager->delegate()->AllowInspectingTarget(agent_host.get())) { | |
| SendJson(connection_id, net::HTTP_FORBIDDEN, std::nullopt, | |
| "Target is not inspectable"); | |
| return; | |
| } | |
| if (command == "activate") { | |
| if (agent_host->Activate()) { | |
| SendJson(connection_id, net::HTTP_OK, std::nullopt, "Target activated"); | |
| } else { | |
| SendJson(connection_id, net::HTTP_INTERNAL_SERVER_ERROR, std::nullopt, | |
| "Could not activate target id: " + target_id); | |
| } | |
| return; | |
| } | |
| if (command == "close") { | |
| if (agent_host->Close()) { | |
| SendJson(connection_id, net::HTTP_OK, std::nullopt, | |
| "Target is closing"); | |
| } else { | |
| SendJson(connection_id, net::HTTP_INTERNAL_SERVER_ERROR, std::nullopt, | |
| "Could not close target id: " + target_id); | |
| } | |
| return; | |
| } | |
| } | |
| SendJson(connection_id, net::HTTP_NOT_FOUND, std::nullopt, | |
| "Unknown command: " + command); | |
| } | |
| void DevToolsHttpHandler::DecompressAndSendJsonProtocol(int connection_id) { | |
| #if BUILDFLAG(ENABLE_DEVTOOLS_FRONTEND) | |
| scoped_refptr<base::RefCountedMemory> bytes = | |
| GetContentClient()->GetDataResourceBytes(kCcompressedProtocolJSON); | |
| CHECK(bytes) << "Could not load protocol"; | |
| net::HttpServerResponseInfo response(net::HTTP_OK); | |
| response.SetBody(base::as_string_view(*bytes), | |
| "application/json; charset=UTF-8"); | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, base::BindOnce(&ServerWrapper::SendResponse, | |
| base::Unretained(server_wrapper_.get()), | |
| connection_id, response)); | |
| #else | |
| NOTREACHED(); | |
| #endif // BUILDFLAG(ENABLE_DEVTOOLS_FRONTEND) | |
| } | |
| void DevToolsHttpHandler::RespondToJsonList(int connection_id, | |
| const std::string& host, | |
| DevToolsAgentHost::List hosts, | |
| bool for_tab) { | |
| DevToolsAgentHost::List agent_hosts = std::move(hosts); | |
| std::sort(agent_hosts.begin(), agent_hosts.end(), TimeComparator); | |
| base::ListValue list_value; | |
| for (auto& agent_host : agent_hosts) { | |
| if (agent_host->GetType() != DevToolsAgentHost::kTypeTab || for_tab) { | |
| list_value.Append(SerializeDescriptor(agent_host, host)); | |
| } | |
| } | |
| SendJson(connection_id, net::HTTP_OK, list_value, ""); | |
| } | |
| void DevToolsHttpHandler::OnDiscoveryPageRequest(int connection_id) { | |
| if (mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| Send404(connection_id); | |
| return; | |
| } | |
| net::HttpServerResponseInfo response(net::HTTP_OK); | |
| response.AddHeader("X-Frame-Options", "DENY"); | |
| response.SetBody(delegate_->GetDiscoveryPageHTML(), | |
| "text/html; charset=UTF-8"); | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, base::BindOnce(&ServerWrapper::SendResponse, | |
| base::Unretained(server_wrapper_.get()), | |
| connection_id, response)); | |
| } | |
| void DevToolsHttpHandler::OnFrontendResourceRequest(int connection_id, | |
| std::string_view path) { | |
| if (mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| Send404(connection_id); | |
| return; | |
| } | |
| #if BUILDFLAG(ENABLE_DEVTOOLS_FRONTEND) | |
| Send200(connection_id, | |
| content::DevToolsFrontendHost::GetFrontendResource(path), | |
| GetMimeType(path)); | |
| #else | |
| Send404(connection_id); | |
| #endif | |
| } | |
| void DevToolsHttpHandler::HandleDebuggingApproval( | |
| int connection_id, | |
| const net::HttpServerRequestInfo& request, | |
| DevToolsManagerDelegate::AcceptConnectionResult result) { | |
| if (result == DevToolsManagerDelegate::AcceptConnectionResult::kAllow) { | |
| scoped_refptr<DevToolsAgentHost> browser_agent = | |
| DevToolsAgentHost::CreateForBrowser( | |
| thread_->task_runner(), | |
| base::BindRepeating(&DevToolsSocketFactory::CreateForTethering, | |
| base::Unretained(socket_factory_.get()))); | |
| connection_to_client_[connection_id] = | |
| std::make_unique<DevToolsAgentHostClientImpl>( | |
| thread_->task_runner(), server_wrapper_.get(), connection_id, | |
| browser_agent); | |
| AcceptWebSocket(connection_id, request); | |
| } else { | |
| Send403(connection_id, "Connection rejected"); | |
| } | |
| } | |
| void DevToolsHttpHandler::OnWebSocketRequest( | |
| int connection_id, | |
| const net::HttpServerRequestInfo& request) { | |
| if (!thread_) | |
| return; | |
| bool is_same_origin = | |
| server_ip_address_ && | |
| url::Origin::Create( | |
| GURL(base::StrCat({"http://", server_ip_address_->ToString()}))) | |
| .IsSameOriginWith(GURL(request.GetHeaderValue("origin"))); | |
| if (request.headers.count("origin") && !is_same_origin && | |
| !remote_allow_origins_.count( | |
| base::ToLowerASCII(request.headers.at("origin"))) && | |
| !remote_allow_origins_.count("*")) { | |
| const std::string& origin = request.headers.at("origin"); | |
| const std::string message = base::StringPrintf( | |
| "Rejected an incoming WebSocket connection from the %s origin. " | |
| "Use the command line flag --remote-allow-origins=%s to allow " | |
| "connections from this origin or --remote-allow-origins=* to allow all " | |
| "origins.", | |
| origin.c_str(), origin.c_str()); | |
| Send403(connection_id, message); | |
| LOG(ERROR) << message; | |
| return; | |
| } | |
| // If we require user approval, we do not require guid. | |
| if (mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| if (base::StartsWith(request.path, kBrowserUrlPrefix, | |
| base::CompareCase::SENSITIVE)) { | |
| delegate_->AcceptDebugging( | |
| base::BindOnce(&DevToolsHttpHandler::HandleDebuggingApproval, | |
| weak_factory_.GetWeakPtr(), connection_id, request)); | |
| return; | |
| } | |
| Send403(connection_id, "Connection rejected"); | |
| return; | |
| } | |
| if (base::StartsWith(request.path, browser_guid_, | |
| base::CompareCase::SENSITIVE)) { | |
| scoped_refptr<DevToolsAgentHost> browser_agent = | |
| DevToolsAgentHost::CreateForBrowser( | |
| thread_->task_runner(), | |
| base::BindRepeating(&DevToolsSocketFactory::CreateForTethering, | |
| base::Unretained(socket_factory_.get()))); | |
| connection_to_client_[connection_id] = | |
| std::make_unique<DevToolsAgentHostClientImpl>( | |
| thread_->task_runner(), server_wrapper_.get(), connection_id, | |
| browser_agent); | |
| AcceptWebSocket(connection_id, request); | |
| return; | |
| } | |
| if (!base::StartsWith(request.path, kPageUrlPrefix, | |
| base::CompareCase::SENSITIVE)) { | |
| Send404(connection_id); | |
| return; | |
| } | |
| std::string target_id = request.path.substr(strlen(kPageUrlPrefix)); | |
| scoped_refptr<DevToolsAgentHost> agent = | |
| DevToolsAgentHost::GetForId(target_id); | |
| if (!agent) { | |
| Send500(connection_id, "No such target id: " + target_id); | |
| return; | |
| } | |
| DevToolsManager* manager = DevToolsManager::GetInstance(); | |
| if (manager->delegate() && | |
| !manager->delegate()->AllowInspectingTarget(agent.get())) { | |
| Send403(connection_id, "Target is not inspectable"); | |
| return; | |
| } | |
| connection_to_client_[connection_id] = | |
| std::make_unique<DevToolsAgentHostClientImpl>( | |
| thread_->task_runner(), server_wrapper_.get(), connection_id, agent); | |
| AcceptWebSocket(connection_id, request); | |
| } | |
| void DevToolsHttpHandler::OnWebSocketMessage(int connection_id, | |
| std::string data) { | |
| auto it = connection_to_client_.find(connection_id); | |
| if (it != connection_to_client_.end()) { | |
| it->second->OnMessage(base::as_byte_span(data)); | |
| } | |
| } | |
| void DevToolsHttpHandler::OnClose(int connection_id) { | |
| auto removed_count = connection_to_client_.erase(connection_id); | |
| if (delegate_ && removed_count > 0 && | |
| mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| delegate_->SetActiveWebSocketConnections(connection_to_client_.size()); | |
| } | |
| } | |
| DevToolsHttpHandler::DevToolsHttpHandler( | |
| DevToolsManagerDelegate* delegate, | |
| std::unique_ptr<DevToolsSocketFactory> socket_factory, | |
| const base::FilePath& output_directory, | |
| const base::FilePath& debug_frontend_dir, | |
| DevToolsAgentHost::RemoteDebuggingServerMode mode) | |
| : mode_(mode), delegate_(delegate) { | |
| browser_guid_ = | |
| delegate_->IsBrowserTargetDiscoverable() | |
| ? kBrowserUrlPrefix | |
| : base::StringPrintf( | |
| "%s/%s", kBrowserUrlPrefix, | |
| base::Uuid::GenerateRandomV4().AsLowercaseString().c_str()); | |
| std::unique_ptr<base::Thread> thread( | |
| new base::Thread(kDevToolsHandlerThreadName)); | |
| base::Thread::Options options; | |
| options.message_pump_type = base::MessagePumpType::IO; | |
| if (thread->StartWithOptions(std::move(options))) { | |
| auto task_runner = thread->task_runner(); | |
| task_runner->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&StartServerOnHandlerThread, weak_factory_.GetWeakPtr(), | |
| std::move(thread), std::move(socket_factory), | |
| output_directory, debug_frontend_dir, browser_guid_, | |
| delegate_->HasBundledFrontendResources())); | |
| } | |
| std::string remote_allow_origins = base::ToLowerASCII( | |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | |
| switches::kRemoteAllowOrigins)); | |
| auto origins = | |
| base::SplitString(remote_allow_origins, ",", base::TRIM_WHITESPACE, | |
| base::SPLIT_WANT_NONEMPTY); | |
| remote_allow_origins_.insert(origins.begin(), origins.end()); | |
| } | |
| void DevToolsHttpHandler::ServerStarted( | |
| std::unique_ptr<base::Thread> thread, | |
| std::unique_ptr<ServerWrapper> server_wrapper, | |
| std::unique_ptr<DevToolsSocketFactory> socket_factory, | |
| std::unique_ptr<net::IPEndPoint> ip_address) { | |
| thread_ = std::move(thread); | |
| server_wrapper_ = std::move(server_wrapper); | |
| socket_factory_ = std::move(socket_factory); | |
| server_ip_address_ = std::move(ip_address); | |
| } | |
| void DevToolsHttpHandler::SendJson(int connection_id, | |
| net::HttpStatusCode status_code, | |
| std::optional<base::ValueView> value, | |
| std::string_view message) { | |
| if (!thread_) | |
| return; | |
| // Serialize value and message. | |
| std::string json_value; | |
| if (value) { | |
| base::JSONWriter::WriteWithOptions( | |
| *value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_value); | |
| } | |
| std::string json_message = base::WriteJson(message).value_or(""); | |
| net::HttpServerResponseInfo response(status_code); | |
| response.AddHeader("Content-Security-Policy", "frame-ancestors 'none'"); | |
| response.SetBody(base::StrCat({json_value, message}), | |
| "application/json; charset=UTF-8"); | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, base::BindOnce(&ServerWrapper::SendResponse, | |
| base::Unretained(server_wrapper_.get()), | |
| connection_id, response)); | |
| } | |
| void DevToolsHttpHandler::Send200(int connection_id, | |
| std::string_view data, | |
| std::string_view mime_type) { | |
| if (!thread_) | |
| return; | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&ServerWrapper::Send200, | |
| base::Unretained(server_wrapper_.get()), connection_id, | |
| std::string(data), std::string(mime_type))); | |
| } | |
| void DevToolsHttpHandler::Send404(int connection_id) { | |
| if (!thread_) | |
| return; | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&ServerWrapper::Send404, | |
| base::Unretained(server_wrapper_.get()), connection_id)); | |
| } | |
| void DevToolsHttpHandler::Send403(int connection_id, | |
| const std::string& message) { | |
| if (!thread_) { | |
| return; | |
| } | |
| net::HttpServerResponseInfo response(net::HTTP_FORBIDDEN); | |
| response.SetBody(message, "text/html"); | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, base::BindOnce(&ServerWrapper::SendResponse, | |
| base::Unretained(server_wrapper_.get()), | |
| connection_id, response)); | |
| } | |
| void DevToolsHttpHandler::Send500(int connection_id, | |
| const std::string& message) { | |
| if (!thread_) | |
| return; | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, base::BindOnce(&ServerWrapper::Send500, | |
| base::Unretained(server_wrapper_.get()), | |
| connection_id, message)); | |
| } | |
| void DevToolsHttpHandler::AcceptWebSocket( | |
| int connection_id, | |
| const net::HttpServerRequestInfo& request) { | |
| if (!thread_) | |
| return; | |
| if (mode_ == | |
| DevToolsAgentHost::RemoteDebuggingServerMode::kWithApprovalOnly) { | |
| delegate_->SetActiveWebSocketConnections(connection_to_client_.size()); | |
| } | |
| thread_->task_runner()->PostTask( | |
| FROM_HERE, base::BindOnce(&ServerWrapper::AcceptWebSocket, | |
| base::Unretained(server_wrapper_.get()), | |
| connection_id, request)); | |
| } | |
| base::DictValue DevToolsHttpHandler::SerializeDescriptor( | |
| scoped_refptr<DevToolsAgentHost> agent_host, | |
| const std::string& host) { | |
| base::DictValue dictionary; | |
| std::string id = agent_host->GetId(); | |
| dictionary.Set(kTargetIdField, id); | |
| std::string parent_id = agent_host->GetParentId(); | |
| if (!parent_id.empty()) | |
| dictionary.Set(kTargetParentIdField, parent_id); | |
| dictionary.Set(kTargetTypeField, agent_host->GetType()); | |
| dictionary.Set(kTargetTitleField, | |
| base::EscapeForHTML(agent_host->GetTitle())); | |
| dictionary.Set(kTargetDescriptionField, agent_host->GetDescription()); | |
| GURL url = agent_host->GetURL(); | |
| dictionary.Set(kTargetUrlField, url.spec()); | |
| GURL favicon_url = agent_host->GetFaviconURL(); | |
| if (favicon_url.is_valid()) | |
| dictionary.Set(kTargetFaviconUrlField, favicon_url.spec()); | |
| dictionary.Set(kTargetWebSocketDebuggerUrlField, | |
| base::StringPrintf("ws://%s%s%s", host.c_str(), kPageUrlPrefix, | |
| id.c_str())); | |
| dictionary.Set(kTargetDevtoolsFrontendUrlField, | |
| GetFrontendURLInternal(agent_host, id, host)); | |
| return dictionary; | |
| } | |
| } // namespace content |