diff --git a/docs/api/web-request.md b/docs/api/web-request.md index 092abd35f3438..1370f0b47a5a6 100644 --- a/docs/api/web-request.md +++ b/docs/api/web-request.md @@ -146,6 +146,7 @@ response are visible by the time this listener is fired. * `timestamp` Double * `statusLine` String * `statusCode` Integer + * `requestHeaders` Record * `responseHeaders` Record (optional) * `callback` Function * `headersReceivedResponse` Object @@ -228,6 +229,7 @@ redirect is about to occur. * `fromCache` Boolean * `statusCode` Integer * `statusLine` String + * `error` String The `listener` will be called with `listener(details)` when a request is completed. diff --git a/filenames.gni b/filenames.gni index 233c3e96a4b09..ee4172692d286 100644 --- a/filenames.gni +++ b/filenames.gni @@ -238,12 +238,15 @@ filenames = { "shell/browser/net/node_stream_loader.h", "shell/browser/net/proxying_url_loader_factory.cc", "shell/browser/net/proxying_url_loader_factory.h", + "shell/browser/net/proxying_websocket.cc", + "shell/browser/net/proxying_websocket.h", "shell/browser/net/resolve_proxy_helper.cc", "shell/browser/net/resolve_proxy_helper.h", "shell/browser/net/system_network_context_manager.cc", "shell/browser/net/system_network_context_manager.h", "shell/browser/net/url_pipe_loader.cc", "shell/browser/net/url_pipe_loader.h", + "shell/browser/net/web_request_api_interface.h", "shell/browser/network_hints_handler_impl.cc", "shell/browser/network_hints_handler_impl.h", "shell/browser/node_debugger.cc", diff --git a/shell/browser/api/electron_api_web_request_ns.h b/shell/browser/api/electron_api_web_request_ns.h index 9c79eb9b3c710..2b138424d2e84 100644 --- a/shell/browser/api/electron_api_web_request_ns.h +++ b/shell/browser/api/electron_api_web_request_ns.h @@ -13,7 +13,7 @@ #include "gin/arguments.h" #include "gin/handle.h" #include "gin/wrappable.h" -#include "shell/browser/net/proxying_url_loader_factory.h" +#include "shell/browser/net/web_request_api_interface.h" namespace content { class BrowserContext; @@ -50,10 +50,6 @@ class WebRequestNS : public gin::Wrappable, public WebRequestAPI { v8::Isolate* isolate) override; const char* GetTypeName() override; - private: - WebRequestNS(v8::Isolate* isolate, content::BrowserContext* browser_context); - ~WebRequestNS() override; - // WebRequestAPI: bool HasListener() const override; int OnBeforeRequest(extensions::WebRequestInfo* info, @@ -87,6 +83,10 @@ class WebRequestNS : public gin::Wrappable, public WebRequestAPI { int net_error) override; void OnRequestWillBeDestroyed(extensions::WebRequestInfo* info) override; + private: + WebRequestNS(v8::Isolate* isolate, content::BrowserContext* browser_context); + ~WebRequestNS() override; + enum SimpleEvent { kOnSendHeaders, kOnBeforeRedirect, diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc index 5aa1a23ba4bb7..638140aa7409e 100644 --- a/shell/browser/electron_browser_client.cc +++ b/shell/browser/electron_browser_client.cc @@ -72,6 +72,7 @@ #include "shell/browser/net/network_context_service.h" #include "shell/browser/net/network_context_service_factory.h" #include "shell/browser/net/proxying_url_loader_factory.h" +#include "shell/browser/net/proxying_websocket.h" #include "shell/browser/net/system_network_context_manager.h" #include "shell/browser/network_hints_handler_impl.h" #include "shell/browser/notifications/notification_presenter.h" @@ -985,6 +986,42 @@ void ElectronBrowserClient::RegisterNonNetworkSubresourceURLLoaderFactories( } } +bool ElectronBrowserClient::WillInterceptWebSocket( + content::RenderFrameHost* frame) { + if (!frame) + return false; + + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + auto* browser_context = frame->GetProcess()->GetBrowserContext(); + auto web_request = api::WebRequestNS::FromOrCreate(isolate, browser_context); + + // NOTE: Some unit test environments do not initialize + // BrowserContextKeyedAPI factories for e.g. WebRequest. + if (!web_request.get()) + return false; + + return web_request->HasListener(); +} + +void ElectronBrowserClient::CreateWebSocket( + content::RenderFrameHost* frame, + WebSocketFactory factory, + const GURL& url, + const GURL& site_for_cookies, + const base::Optional& user_agent, + mojo::PendingRemote + handshake_client) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + auto* browser_context = frame->GetProcess()->GetBrowserContext(); + auto web_request = api::WebRequestNS::FromOrCreate(isolate, browser_context); + DCHECK(web_request.get()); + ProxyingWebSocket::StartProxying( + web_request.get(), std::move(factory), url, site_for_cookies, user_agent, + std::move(handshake_client), true, frame->GetProcess()->GetID(), + frame->GetRoutingID(), frame->GetLastCommittedOrigin(), browser_context, + &next_id_); +} + bool ElectronBrowserClient::WillCreateURLLoaderFactory( content::BrowserContext* browser_context, content::RenderFrameHost* frame_host, @@ -1026,7 +1063,7 @@ bool ElectronBrowserClient::WillCreateURLLoaderFactory( new ProxyingURLLoaderFactory( web_request.get(), protocol->intercept_handlers(), browser_context, - render_process_id, std::move(navigation_ui_data), + render_process_id, &next_id_, std::move(navigation_ui_data), std::move(navigation_id), std::move(proxied_receiver), std::move(target_factory_remote), std::move(header_client_receiver), type); diff --git a/shell/browser/electron_browser_client.h b/shell/browser/electron_browser_client.h index 55abc7ce788ea..f32ada6bf5f90 100644 --- a/shell/browser/electron_browser_client.h +++ b/shell/browser/electron_browser_client.h @@ -174,6 +174,15 @@ class ElectronBrowserClient : public content::ContentBrowserClient, int render_process_id, int render_frame_id, NonNetworkURLLoaderFactoryMap* factories) override; + void CreateWebSocket( + content::RenderFrameHost* frame, + WebSocketFactory factory, + const GURL& url, + const GURL& site_for_cookies, + const base::Optional& user_agent, + mojo::PendingRemote + handshake_client) override; + bool WillInterceptWebSocket(content::RenderFrameHost*) override; bool WillCreateURLLoaderFactory( content::BrowserContext* browser_context, content::RenderFrameHost* frame, @@ -285,6 +294,10 @@ class ElectronBrowserClient : public content::ContentBrowserClient, bool disable_process_restart_tricks_ = false; bool disable_process_restart_tricks_is_default_value_ = true; + // Simple shared ID generator, used by ProxyingURLLoaderFactory and + // ProxyingWebSocket classes. + uint64_t next_id_ = 0; + DISALLOW_COPY_AND_ASSIGN(ElectronBrowserClient); }; diff --git a/shell/browser/net/proxying_url_loader_factory.cc b/shell/browser/net/proxying_url_loader_factory.cc index 7f9933f5b7a1c..47df711b9d190 100644 --- a/shell/browser/net/proxying_url_loader_factory.cc +++ b/shell/browser/net/proxying_url_loader_factory.cc @@ -19,13 +19,6 @@ #include "shell/common/options_switches.h" namespace electron { - -namespace { - -int64_t g_request_id = 0; - -} // namespace - ProxyingURLLoaderFactory::InProgressRequest::FollowRedirectParams:: FollowRedirectParams() = default; ProxyingURLLoaderFactory::InProgressRequest::FollowRedirectParams:: @@ -675,6 +668,7 @@ ProxyingURLLoaderFactory::ProxyingURLLoaderFactory( const HandlersMap& intercepted_handlers, content::BrowserContext* browser_context, int render_process_id, + uint64_t* request_id_generator, std::unique_ptr navigation_ui_data, base::Optional navigation_id, network::mojom::URLLoaderFactoryRequest loader_request, @@ -686,6 +680,7 @@ ProxyingURLLoaderFactory::ProxyingURLLoaderFactory( intercepted_handlers_(intercepted_handlers), browser_context_(browser_context), render_process_id_(render_process_id), + request_id_generator_(request_id_generator), navigation_ui_data_(std::move(navigation_ui_data)), navigation_id_(std::move(navigation_id)), loader_factory_type_(loader_factory_type) { @@ -763,7 +758,7 @@ void ProxyingURLLoaderFactory::CreateLoaderAndStart( // per-BrowserContext so extensions can make sense of it. Note that // |network_service_request_id_| by contrast is not necessarily unique, so we // don't use it for identity here. - const uint64_t web_request_id = ++g_request_id; + const uint64_t web_request_id = ++(*request_id_generator_); // Notes: Chromium assumes that requests with zero-ID would never use the // "extraHeaders" code path, however in Electron requests started from diff --git a/shell/browser/net/proxying_url_loader_factory.h b/shell/browser/net/proxying_url_loader_factory.h index a1eb05b90d729..b4d501d9e109e 100644 --- a/shell/browser/net/proxying_url_loader_factory.h +++ b/shell/browser/net/proxying_url_loader_factory.h @@ -24,52 +24,10 @@ #include "services/network/public/mojom/network_context.mojom.h" #include "services/network/public/mojom/url_loader.mojom.h" #include "shell/browser/net/electron_url_loader_factory.h" +#include "shell/browser/net/web_request_api_interface.h" namespace electron { -// Defines the interface for WebRequest API, implemented by api::WebRequestNS. -class WebRequestAPI { - public: - virtual ~WebRequestAPI() {} - - using BeforeSendHeadersCallback = - base::OnceCallback& removed_headers, - const std::set& set_headers, - int error_code)>; - - virtual bool HasListener() const = 0; - virtual int OnBeforeRequest(extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - net::CompletionOnceCallback callback, - GURL* new_url) = 0; - virtual int OnBeforeSendHeaders(extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - BeforeSendHeadersCallback callback, - net::HttpRequestHeaders* headers) = 0; - virtual int OnHeadersReceived( - extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - net::CompletionOnceCallback callback, - const net::HttpResponseHeaders* original_response_headers, - scoped_refptr* override_response_headers, - GURL* allowed_unsafe_redirect_url) = 0; - virtual void OnSendHeaders(extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - const net::HttpRequestHeaders& headers) = 0; - virtual void OnBeforeRedirect(extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - const GURL& new_location) = 0; - virtual void OnResponseStarted(extensions::WebRequestInfo* info, - const network::ResourceRequest& request) = 0; - virtual void OnErrorOccurred(extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - int net_error) = 0; - virtual void OnCompleted(extensions::WebRequestInfo* info, - const network::ResourceRequest& request, - int net_error) = 0; - virtual void OnRequestWillBeDestroyed(extensions::WebRequestInfo* info) = 0; -}; - // This class is responsible for following tasks when NetworkService is enabled: // 1. handling intercepted protocols; // 2. implementing webRequest module; @@ -210,6 +168,7 @@ class ProxyingURLLoaderFactory const HandlersMap& intercepted_handlers, content::BrowserContext* browser_context, int render_process_id, + uint64_t* request_id_generator, std::unique_ptr navigation_ui_data, base::Optional navigation_id, network::mojom::URLLoaderFactoryRequest loader_request, @@ -269,6 +228,7 @@ class ProxyingURLLoaderFactory content::BrowserContext* const browser_context_; const int render_process_id_; + uint64_t* request_id_generator_; // managed by AtomBrowserClient std::unique_ptr navigation_ui_data_; base::Optional navigation_id_; mojo::ReceiverSet proxy_receivers_; diff --git a/shell/browser/net/proxying_websocket.cc b/shell/browser/net/proxying_websocket.cc new file mode 100644 index 0000000000000..c7c23839aa90e --- /dev/null +++ b/shell/browser/net/proxying_websocket.cc @@ -0,0 +1,456 @@ +// Copyright 2020 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. + +#include "electron/shell/browser/net/proxying_websocket.h" + +#include + +#include "base/bind.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "components/keyed_service/content/browser_context_keyed_service_shutdown_notifier_factory.h" +#include "content/public/browser/browser_thread.h" +#include "extensions/browser/extension_navigation_ui_data.h" +#include "net/base/ip_endpoint.h" +#include "net/http/http_util.h" + +namespace electron { + +ProxyingWebSocket::ProxyingWebSocket( + WebRequestAPI* web_request_api, + WebSocketFactory factory, + const network::ResourceRequest& request, + mojo::PendingRemote + handshake_client, + bool has_extra_headers, + int process_id, + int render_frame_id, + content::BrowserContext* browser_context, + uint64_t* request_id_generator) + : web_request_api_(web_request_api), + request_(request), + factory_(std::move(factory)), + forwarding_handshake_client_(std::move(handshake_client)), + request_headers_(request.headers), + response_(network::mojom::URLResponseHead::New()), + has_extra_headers_(has_extra_headers), + info_(extensions::WebRequestInfoInitParams( + ++(*request_id_generator), + process_id, + render_frame_id, + nullptr, + MSG_ROUTING_NONE, + request, + /*is_download=*/false, + /*is_async=*/true, + /*is_service_worker_script=*/false, + /*navigation_id=*/base::nullopt)) {} + +ProxyingWebSocket::~ProxyingWebSocket() { + if (on_before_send_headers_callback_) { + std::move(on_before_send_headers_callback_) + .Run(net::ERR_ABORTED, base::nullopt); + } + if (on_headers_received_callback_) { + std::move(on_headers_received_callback_) + .Run(net::ERR_ABORTED, base::nullopt, GURL()); + } +} + +void ProxyingWebSocket::Start() { + // If the header client will be used, we start the request immediately, and + // OnBeforeSendHeaders and OnSendHeaders will be handled there. Otherwise, + // send these events before the request starts. + base::RepeatingCallback continuation; + if (has_extra_headers_) { + continuation = base::BindRepeating( + &ProxyingWebSocket::ContinueToStartRequest, weak_factory_.GetWeakPtr()); + } else { + continuation = + base::BindRepeating(&ProxyingWebSocket::OnBeforeRequestComplete, + weak_factory_.GetWeakPtr()); + } + + int result = web_request_api_->OnBeforeRequest(&info_, request_, continuation, + &redirect_url_); + + if (result == net::ERR_BLOCKED_BY_CLIENT) { + OnError(result); + return; + } + + if (result == net::ERR_IO_PENDING) { + return; + } + + DCHECK_EQ(net::OK, result); + continuation.Run(net::OK); +} + +void ProxyingWebSocket::OnOpeningHandshakeStarted( + network::mojom::WebSocketHandshakeRequestPtr request) { + DCHECK(forwarding_handshake_client_); + forwarding_handshake_client_->OnOpeningHandshakeStarted(std::move(request)); +} + +void ProxyingWebSocket::ContinueToHeadersReceived() { + auto continuation = + base::BindRepeating(&ProxyingWebSocket::OnHeadersReceivedComplete, + weak_factory_.GetWeakPtr()); + info_.AddResponseInfoFromResourceResponse(*response_); + int result = web_request_api_->OnHeadersReceived( + &info_, request_, continuation, response_->headers.get(), + &override_headers_, &redirect_url_); + + if (result == net::ERR_BLOCKED_BY_CLIENT) { + OnError(result); + return; + } + + PauseIncomingMethodCallProcessing(); + if (result == net::ERR_IO_PENDING) + return; + + DCHECK_EQ(net::OK, result); + OnHeadersReceivedComplete(net::OK); +} + +void ProxyingWebSocket::OnConnectionEstablished( + mojo::PendingRemote websocket, + mojo::PendingReceiver client_receiver, + network::mojom::WebSocketHandshakeResponsePtr response, + mojo::ScopedDataPipeConsumerHandle readable) { + DCHECK(forwarding_handshake_client_); + DCHECK(!is_done_); + is_done_ = true; + websocket_ = std::move(websocket); + client_receiver_ = std::move(client_receiver); + handshake_response_ = std::move(response); + readable_ = std::move(readable); + + response_->remote_endpoint = handshake_response_->remote_endpoint; + + // response_->headers will be set in OnBeforeSendHeaders if + // |receiver_as_header_client_| is set. + if (receiver_as_header_client_.is_bound()) { + ContinueToCompleted(); + return; + } + + response_->headers = + base::MakeRefCounted(base::StringPrintf( + "HTTP/%d.%d %d %s", handshake_response_->http_version.major_value(), + handshake_response_->http_version.minor_value(), + handshake_response_->status_code, + handshake_response_->status_text.c_str())); + for (const auto& header : handshake_response_->headers) + response_->headers->AddHeader(header->name + ": " + header->value); + + ContinueToHeadersReceived(); +} + +void ProxyingWebSocket::ContinueToCompleted() { + DCHECK(forwarding_handshake_client_); + DCHECK(is_done_); + web_request_api_->OnCompleted(&info_, request_, net::ERR_WS_UPGRADE); + forwarding_handshake_client_->OnConnectionEstablished( + std::move(websocket_), std::move(client_receiver_), + std::move(handshake_response_), std::move(readable_)); + + // Deletes |this|. + delete this; +} + +void ProxyingWebSocket::OnAuthRequired( + const net::AuthChallengeInfo& auth_info, + const scoped_refptr& headers, + const net::IPEndPoint& remote_endpoint, + OnAuthRequiredCallback callback) { + if (!callback) { + OnError(net::ERR_FAILED); + return; + } + + response_->headers = headers; + response_->remote_endpoint = remote_endpoint; + auth_required_callback_ = std::move(callback); + + auto continuation = + base::BindRepeating(&ProxyingWebSocket::OnHeadersReceivedCompleteForAuth, + weak_factory_.GetWeakPtr(), auth_info); + info_.AddResponseInfoFromResourceResponse(*response_); + int result = web_request_api_->OnHeadersReceived( + &info_, request_, continuation, response_->headers.get(), + &override_headers_, &redirect_url_); + + if (result == net::ERR_BLOCKED_BY_CLIENT) { + OnError(result); + return; + } + + PauseIncomingMethodCallProcessing(); + if (result == net::ERR_IO_PENDING) + return; + + DCHECK_EQ(net::OK, result); + OnHeadersReceivedCompleteForAuth(auth_info, net::OK); +} + +void ProxyingWebSocket::OnBeforeSendHeaders( + const net::HttpRequestHeaders& headers, + OnBeforeSendHeadersCallback callback) { + DCHECK(receiver_as_header_client_.is_bound()); + + request_headers_ = headers; + on_before_send_headers_callback_ = std::move(callback); + OnBeforeRequestComplete(net::OK); +} + +void ProxyingWebSocket::OnHeadersReceived(const std::string& headers, + const net::IPEndPoint& endpoint, + OnHeadersReceivedCallback callback) { + DCHECK(receiver_as_header_client_.is_bound()); + + on_headers_received_callback_ = std::move(callback); + response_->headers = base::MakeRefCounted(headers); + + ContinueToHeadersReceived(); +} + +void ProxyingWebSocket::StartProxying( + WebRequestAPI* web_request_api, + WebSocketFactory factory, + const GURL& url, + const GURL& site_for_cookies, + const base::Optional& user_agent, + mojo::PendingRemote + handshake_client, + bool has_extra_headers, + int process_id, + int render_frame_id, + const url::Origin& origin, + content::BrowserContext* browser_context, + uint64_t* request_id_generator) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + network::ResourceRequest request; + request.url = url; + request.site_for_cookies = site_for_cookies; + if (user_agent) { + request.headers.SetHeader(net::HttpRequestHeaders::kUserAgent, *user_agent); + } + request.request_initiator = origin; + + auto* proxy = new ProxyingWebSocket( + web_request_api, std::move(factory), request, std::move(handshake_client), + has_extra_headers, process_id, render_frame_id, browser_context, + request_id_generator); + proxy->Start(); +} + +void ProxyingWebSocket::OnBeforeRequestComplete(int error_code) { + DCHECK(receiver_as_header_client_.is_bound() || + !receiver_as_handshake_client_.is_bound()); + DCHECK(info_.url.SchemeIsWSOrWSS()); + if (error_code != net::OK) { + OnError(error_code); + return; + } + + auto continuation = + base::BindRepeating(&ProxyingWebSocket::OnBeforeSendHeadersComplete, + weak_factory_.GetWeakPtr()); + + info_.AddResponseInfoFromResourceResponse(*response_); + int result = web_request_api_->OnBeforeSendHeaders( + &info_, request_, continuation, &request_headers_); + + if (result == net::ERR_BLOCKED_BY_CLIENT) { + OnError(result); + return; + } + + if (result == net::ERR_IO_PENDING) + return; + + DCHECK_EQ(net::OK, result); + OnBeforeSendHeadersComplete(std::set(), std::set(), + net::OK); +} + +void ProxyingWebSocket::OnBeforeSendHeadersComplete( + const std::set& removed_headers, + const std::set& set_headers, + int error_code) { + DCHECK(receiver_as_header_client_.is_bound() || + !receiver_as_handshake_client_.is_bound()); + if (error_code != net::OK) { + OnError(error_code); + return; + } + + if (receiver_as_header_client_.is_bound()) { + CHECK(on_before_send_headers_callback_); + std::move(on_before_send_headers_callback_) + .Run(error_code, request_headers_); + } + + info_.AddResponseInfoFromResourceResponse(*response_); + web_request_api_->OnSendHeaders(&info_, request_, request_headers_); + + if (!receiver_as_header_client_.is_bound()) + ContinueToStartRequest(net::OK); +} + +void ProxyingWebSocket::ContinueToStartRequest(int error_code) { + if (error_code != net::OK) { + OnError(error_code); + return; + } + + base::flat_set used_header_names; + std::vector additional_headers; + for (net::HttpRequestHeaders::Iterator it(request_headers_); it.GetNext();) { + additional_headers.push_back( + network::mojom::HttpHeader::New(it.name(), it.value())); + used_header_names.insert(base::ToLowerASCII(it.name())); + } + for (const auto& header : additional_headers_) { + if (!used_header_names.contains(base::ToLowerASCII(header->name))) { + additional_headers.push_back( + network::mojom::HttpHeader::New(header->name, header->value)); + } + } + + mojo::PendingRemote + trusted_header_client = mojo::NullRemote(); + if (has_extra_headers_) { + trusted_header_client = + receiver_as_header_client_.BindNewPipeAndPassRemote(); + } + + std::move(factory_).Run( + info_.url, std::move(additional_headers), + receiver_as_handshake_client_.BindNewPipeAndPassRemote(), + receiver_as_auth_handler_.BindNewPipeAndPassRemote(), + std::move(trusted_header_client)); + + // Here we detect mojo connection errors on |receiver_as_handshake_client_|. + // See also CreateWebSocket in + // //network/services/public/mojom/network_context.mojom. + receiver_as_handshake_client_.set_disconnect_with_reason_handler( + base::BindOnce(&ProxyingWebSocket::OnMojoConnectionErrorWithCustomReason, + base::Unretained(this))); + forwarding_handshake_client_.set_disconnect_handler(base::BindOnce( + &ProxyingWebSocket::OnMojoConnectionError, base::Unretained(this))); +} + +void ProxyingWebSocket::OnHeadersReceivedComplete(int error_code) { + if (error_code != net::OK) { + OnError(error_code); + return; + } + + if (on_headers_received_callback_) { + base::Optional headers; + if (override_headers_) + headers = override_headers_->raw_headers(); + std::move(on_headers_received_callback_) + .Run(net::OK, headers, base::nullopt); + } + + if (override_headers_) { + response_->headers = override_headers_; + override_headers_ = nullptr; + } + + ResumeIncomingMethodCallProcessing(); + info_.AddResponseInfoFromResourceResponse(*response_); + web_request_api_->OnResponseStarted(&info_, request_); + + if (!receiver_as_header_client_.is_bound()) + ContinueToCompleted(); +} + +void ProxyingWebSocket::OnAuthRequiredComplete( + extensions::ExtensionWebRequestEventRouter::AuthRequiredResponse rv) { + CHECK(auth_required_callback_); + ResumeIncomingMethodCallProcessing(); + switch (rv) { + case extensions::ExtensionWebRequestEventRouter::AuthRequiredResponse:: + AUTH_REQUIRED_RESPONSE_NO_ACTION: + case extensions::ExtensionWebRequestEventRouter::AuthRequiredResponse:: + AUTH_REQUIRED_RESPONSE_CANCEL_AUTH: + std::move(auth_required_callback_).Run(base::nullopt); + break; + + case extensions::ExtensionWebRequestEventRouter::AuthRequiredResponse:: + AUTH_REQUIRED_RESPONSE_SET_AUTH: + std::move(auth_required_callback_).Run(auth_credentials_); + break; + case extensions::ExtensionWebRequestEventRouter::AuthRequiredResponse:: + AUTH_REQUIRED_RESPONSE_IO_PENDING: + NOTREACHED(); + break; + } +} + +void ProxyingWebSocket::OnHeadersReceivedCompleteForAuth( + const net::AuthChallengeInfo& auth_info, + int rv) { + if (rv != net::OK) { + OnError(rv); + return; + } + ResumeIncomingMethodCallProcessing(); + info_.AddResponseInfoFromResourceResponse(*response_); + + auto continuation = base::BindRepeating( + &ProxyingWebSocket::OnAuthRequiredComplete, weak_factory_.GetWeakPtr()); + auto auth_rv = extensions::ExtensionWebRequestEventRouter:: + AuthRequiredResponse::AUTH_REQUIRED_RESPONSE_IO_PENDING; + PauseIncomingMethodCallProcessing(); + + OnAuthRequiredComplete(auth_rv); +} + +void ProxyingWebSocket::PauseIncomingMethodCallProcessing() { + receiver_as_handshake_client_.Pause(); + receiver_as_auth_handler_.Pause(); + if (receiver_as_header_client_.is_bound()) + receiver_as_header_client_.Pause(); +} + +void ProxyingWebSocket::ResumeIncomingMethodCallProcessing() { + receiver_as_handshake_client_.Resume(); + receiver_as_auth_handler_.Resume(); + if (receiver_as_header_client_.is_bound()) + receiver_as_header_client_.Resume(); +} + +void ProxyingWebSocket::OnError(int error_code) { + if (!is_done_) { + is_done_ = true; + web_request_api_->OnErrorOccurred(&info_, request_, error_code); + } + + // Deletes |this|. + delete this; +} + +void ProxyingWebSocket::OnMojoConnectionErrorWithCustomReason( + uint32_t custom_reason, + const std::string& description) { + // Here we want to nofiy the custom reason to the client, which is why + // we reset |forwarding_handshake_client_| manually. + forwarding_handshake_client_.ResetWithReason(custom_reason, description); + OnError(net::ERR_FAILED); + // Deletes |this|. +} + +void ProxyingWebSocket::OnMojoConnectionError() { + OnError(net::ERR_FAILED); + // Deletes |this|. +} + +} // namespace electron diff --git a/shell/browser/net/proxying_websocket.h b/shell/browser/net/proxying_websocket.h new file mode 100644 index 0000000000000..0e37c9e36a021 --- /dev/null +++ b/shell/browser/net/proxying_websocket.h @@ -0,0 +1,165 @@ +// Copyright 2020 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. + +#ifndef SHELL_BROWSER_NET_PROXYING_WEBSOCKET_H_ +#define SHELL_BROWSER_NET_PROXYING_WEBSOCKET_H_ + +#include +#include +#include +#include + +#include "base/optional.h" +#include "components/keyed_service/core/keyed_service_shutdown_notifier.h" +#include "extensions/browser/api/web_request/web_request_api.h" +#include "extensions/browser/api/web_request/web_request_info.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "services/network/public/mojom/websocket.mojom.h" +#include "shell/browser/net/web_request_api_interface.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace electron { + +// A ProxyingWebSocket proxies a WebSocket connection and dispatches +// WebRequest API events. +// +// The code is referenced from the +// extensions::WebRequestProxyingWebSocket class. +class ProxyingWebSocket : public network::mojom::WebSocketHandshakeClient, + public network::mojom::AuthenticationHandler, + public network::mojom::TrustedHeaderClient { + public: + using WebSocketFactory = content::ContentBrowserClient::WebSocketFactory; + + ProxyingWebSocket( + WebRequestAPI* web_request_api, + WebSocketFactory factory, + const network::ResourceRequest& request, + mojo::PendingRemote + handshake_client, + bool has_extra_headers, + int process_id, + int render_frame_id, + content::BrowserContext* browser_context, + uint64_t* request_id_generator); + ~ProxyingWebSocket() override; + + void Start(); + + // network::mojom::WebSocketHandshakeClient methods: + void OnOpeningHandshakeStarted( + network::mojom::WebSocketHandshakeRequestPtr request) override; + void OnConnectionEstablished( + mojo::PendingRemote websocket, + mojo::PendingReceiver client_receiver, + network::mojom::WebSocketHandshakeResponsePtr response, + mojo::ScopedDataPipeConsumerHandle readable) override; + + // network::mojom::AuthenticationHandler method: + void OnAuthRequired(const net::AuthChallengeInfo& auth_info, + const scoped_refptr& headers, + const net::IPEndPoint& remote_endpoint, + OnAuthRequiredCallback callback) override; + + // network::mojom::TrustedHeaderClient methods: + void OnBeforeSendHeaders(const net::HttpRequestHeaders& headers, + OnBeforeSendHeadersCallback callback) override; + void OnHeadersReceived(const std::string& headers, + const net::IPEndPoint& endpoint, + OnHeadersReceivedCallback callback) override; + + static void StartProxying( + WebRequestAPI* web_request_api, + WebSocketFactory factory, + const GURL& url, + const GURL& site_for_cookies, + const base::Optional& user_agent, + mojo::PendingRemote + handshake_client, + bool has_extra_headers, + int process_id, + int render_frame_id, + const url::Origin& origin, + content::BrowserContext* browser_context, + uint64_t* request_id_generator); + + WebRequestAPI* web_request_api() { return web_request_api_; } + + private: + void OnBeforeRequestComplete(int error_code); + void OnBeforeSendHeadersComplete(const std::set& removed_headers, + const std::set& set_headers, + int error_code); + void ContinueToStartRequest(int error_code); + void OnHeadersReceivedComplete(int error_code); + void ContinueToHeadersReceived(); + void OnAuthRequiredComplete( + extensions::ExtensionWebRequestEventRouter::AuthRequiredResponse rv); + void OnHeadersReceivedCompleteForAuth(const net::AuthChallengeInfo& auth_info, + int rv); + void ContinueToCompleted(); + + void PauseIncomingMethodCallProcessing(); + void ResumeIncomingMethodCallProcessing(); + void OnError(int result); + // This is used for detecting errors on mojo connection with the network + // service. + void OnMojoConnectionErrorWithCustomReason(uint32_t custom_reason, + const std::string& description); + // This is used for detecting errors on mojo connection with original client + // (i.e., renderer). + void OnMojoConnectionError(); + + // Passed from api::WebRequest. + WebRequestAPI* web_request_api_; + + // Saved to feed the api::WebRequest. + network::ResourceRequest request_; + + WebSocketFactory factory_; + mojo::Remote + forwarding_handshake_client_; + mojo::Receiver + receiver_as_handshake_client_{this}; + mojo::Receiver + receiver_as_auth_handler_{this}; + mojo::Receiver + receiver_as_header_client_{this}; + + net::HttpRequestHeaders request_headers_; + network::mojom::URLResponseHeadPtr response_; + net::AuthCredentials auth_credentials_; + OnAuthRequiredCallback auth_required_callback_; + scoped_refptr override_headers_; + std::vector additional_headers_; + + OnBeforeSendHeadersCallback on_before_send_headers_callback_; + OnHeadersReceivedCallback on_headers_received_callback_; + + GURL redirect_url_; + bool is_done_ = false; + bool has_extra_headers_; + mojo::PendingRemote websocket_; + mojo::PendingReceiver client_receiver_; + network::mojom::WebSocketHandshakeResponsePtr handshake_response_ = nullptr; + mojo::ScopedDataPipeConsumerHandle readable_; + + extensions::WebRequestInfo info_; + + // Notifies the proxy that the browser context has been shutdown. + std::unique_ptr + shutdown_notifier_; + + base::WeakPtrFactory weak_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(ProxyingWebSocket); +}; + +} // namespace electron + +#endif // SHELL_BROWSER_NET_PROXYING_WEBSOCKET_H_ diff --git a/shell/browser/net/web_request_api_interface.h b/shell/browser/net/web_request_api_interface.h new file mode 100644 index 0000000000000..649145f6f66aa --- /dev/null +++ b/shell/browser/net/web_request_api_interface.h @@ -0,0 +1,61 @@ +// Copyright (c) 2020 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_NET_WEB_REQUEST_API_INTERFACE_H_ +#define SHELL_BROWSER_NET_WEB_REQUEST_API_INTERFACE_H_ + +#include +#include + +#include "extensions/browser/api/web_request/web_request_info.h" +#include "services/network/public/cpp/resource_request.h" + +namespace electron { + +// Defines the interface for WebRequest API, implemented by api::WebRequestNS. +class WebRequestAPI { + public: + virtual ~WebRequestAPI() {} + + using BeforeSendHeadersCallback = + base::OnceCallback& removed_headers, + const std::set& set_headers, + int error_code)>; + + virtual bool HasListener() const = 0; + virtual int OnBeforeRequest(extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + net::CompletionOnceCallback callback, + GURL* new_url) = 0; + virtual int OnBeforeSendHeaders(extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + BeforeSendHeadersCallback callback, + net::HttpRequestHeaders* headers) = 0; + virtual int OnHeadersReceived( + extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + net::CompletionOnceCallback callback, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers, + GURL* allowed_unsafe_redirect_url) = 0; + virtual void OnSendHeaders(extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + const net::HttpRequestHeaders& headers) = 0; + virtual void OnBeforeRedirect(extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + const GURL& new_location) = 0; + virtual void OnResponseStarted(extensions::WebRequestInfo* info, + const network::ResourceRequest& request) = 0; + virtual void OnErrorOccurred(extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + int net_error) = 0; + virtual void OnCompleted(extensions::WebRequestInfo* info, + const network::ResourceRequest& request, + int net_error) = 0; + virtual void OnRequestWillBeDestroyed(extensions::WebRequestInfo* info) = 0; +}; + +} // namespace electron + +#endif // SHELL_BROWSER_NET_WEB_REQUEST_API_INTERFACE_H_ diff --git a/spec-main/api-web-request-spec.ts b/spec-main/api-web-request-spec.ts index 006436f34a575..b212c705a3666 100644 --- a/spec-main/api-web-request-spec.ts +++ b/spec-main/api-web-request-spec.ts @@ -2,8 +2,10 @@ import { expect } from 'chai' import * as http from 'http' import * as qs from 'querystring' import * as path from 'path' -import { session, WebContents, webContents } from 'electron' +import * as WebSocket from 'ws' +import { ipcMain, session, WebContents, webContents } from 'electron' import { AddressInfo } from 'net'; +import { emittedOnce } from './events-helpers' const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures') @@ -348,4 +350,100 @@ describe('webRequest module', () => { await expect(ajax(defaultURL)).to.eventually.be.rejectedWith('404') }) }) + + describe('WebSocket connections', () => { + it('can be proxyed', async () => { + // Setup server. + const reqHeaders : { [key: string] : any } = {} + const server = http.createServer((req, res) => { + reqHeaders[req.url!] = req.headers + res.setHeader('foo1', 'bar1') + res.end('ok') + }) + const wss = new WebSocket.Server({ noServer: true }) + wss.on('connection', function connection (ws) { + ws.on('message', function incoming (message) { + if (message === 'foo') { + ws.send('bar') + } + }) + }) + server.on('upgrade', function upgrade (request, socket, head) { + const pathname = require('url').parse(request.url).pathname + if (pathname === '/websocket') { + reqHeaders[request.url] = request.headers + wss.handleUpgrade(request, socket, head, function done (ws) { + wss.emit('connection', ws, request) + }) + } + }) + + // Start server. + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const port = String((server.address() as AddressInfo).port) + + // Use a separate session for testing. + const ses = session.fromPartition('WebRequestWebSocket') + + // Setup listeners. + const receivedHeaders : { [key: string] : any } = {} + ses.webRequest.onBeforeSendHeaders((details, callback) => { + details.requestHeaders.foo = 'bar' + callback({ requestHeaders: details.requestHeaders }) + }) + ses.webRequest.onHeadersReceived((details, callback) => { + const pathname = require('url').parse(details.url).pathname + receivedHeaders[pathname] = details.responseHeaders + callback({ cancel: false }) + }) + ses.webRequest.onResponseStarted((details) => { + if (details.url.startsWith('ws://')) { + expect(details.responseHeaders!['Connection'][0]).be.equal('Upgrade') + } else if (details.url.startsWith('http')) { + expect(details.responseHeaders!['foo1'][0]).be.equal('bar1') + } + }) + ses.webRequest.onSendHeaders((details) => { + if (details.url.startsWith('ws://')) { + expect(details.requestHeaders['foo']).be.equal('bar') + expect(details.requestHeaders['Upgrade']).be.equal('websocket') + } else if (details.url.startsWith('http')) { + expect(details.requestHeaders['foo']).be.equal('bar') + } + }) + ses.webRequest.onCompleted((details) => { + if (details.url.startsWith('ws://')) { + expect(details['error']).be.equal('net::ERR_WS_UPGRADE') + } else if (details.url.startsWith('http')) { + expect(details['error']).be.equal('net::OK') + } + }) + + const contents = (webContents as any).create({ + session: ses, + nodeIntegration: true, + webSecurity: false + }) + + // Cleanup. + after(() => { + contents.destroy() + server.close() + ses.webRequest.onBeforeRequest(null) + ses.webRequest.onBeforeSendHeaders(null) + ses.webRequest.onHeadersReceived(null) + ses.webRequest.onResponseStarted(null) + ses.webRequest.onSendHeaders(null) + ses.webRequest.onCompleted(null) + }) + + contents.loadFile(path.join(__dirname, 'fixtures', 'api', 'webrequest.html'), { query: { port } }) + await emittedOnce(ipcMain, 'websocket-success') + + expect(receivedHeaders['/websocket']['Upgrade'][0]).to.equal('websocket') + expect(receivedHeaders['/']['foo1'][0]).to.equal('bar1') + expect(reqHeaders['/websocket']['foo']).to.equal('bar') + expect(reqHeaders['/']['foo']).to.equal('bar') + }) + }) }) diff --git a/spec-main/fixtures/api/webrequest.html b/spec-main/fixtures/api/webrequest.html new file mode 100644 index 0000000000000..6cc9f0bd18908 --- /dev/null +++ b/spec-main/fixtures/api/webrequest.html @@ -0,0 +1,27 @@ + diff --git a/spec-main/package.json b/spec-main/package.json index eb5a7e949a10d..bb272f81db0d0 100644 --- a/spec-main/package.json +++ b/spec-main/package.json @@ -4,7 +4,9 @@ "main": "index.js", "version": "0.1.0", "devDependencies": { + "@types/ws": "^7.2.0", "echo": "file:fixtures/native-addon/echo", - "q": "^1.5.1" + "q": "^1.5.1", + "ws": "^7.2.1" } } diff --git a/spec-main/yarn.lock b/spec-main/yarn.lock index 79ec47695e196..a40f5e0e8487b 100644 --- a/spec-main/yarn.lock +++ b/spec-main/yarn.lock @@ -2,6 +2,18 @@ # yarn lockfile v1 +"@types/node@*": + version "13.7.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" + integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== + +"@types/ws@^7.2.0": + version "7.2.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.1.tgz#b800f2b8aee694e2b581113643e20d79dd3b8556" + integrity sha512-UEmRNbXFGvfs/sLncf01GuVv6U1mZP3Df0iXWx4kUlikJxbFyFADp95mDn1XDTE2mXpzzoHcKlfFcbytLq4vaA== + dependencies: + "@types/node" "*" + "echo@file:fixtures/native-addon/echo": version "0.0.1" @@ -9,3 +21,8 @@ q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +ws@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" + integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==