| // Copyright 2018 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 "content/browser/web_package/web_package_loader.h" |
| |
| #include <memory> |
| |
| #include "base/callback.h" |
| #include "base/feature_list.h" |
| #include "base/strings/stringprintf.h" |
| #include "content/browser/loader/data_pipe_to_source_stream.h" |
| #include "content/browser/loader/source_stream_to_data_pipe.h" |
| #include "content/browser/web_package/signed_exchange_cert_fetcher_factory.h" |
| #include "content/browser/web_package/signed_exchange_devtools_proxy.h" |
| #include "content/browser/web_package/signed_exchange_handler.h" |
| #include "content/browser/web_package/signed_exchange_utils.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/origin_util.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/cert_status_flags.h" |
| #include "net/http/http_util.h" |
| #include "net/url_request/url_request_context_getter.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/url_loader_factory.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| net::RedirectInfo CreateRedirectInfo(const GURL& new_url) { |
| net::RedirectInfo redirect_info; |
| redirect_info.new_url = new_url; |
| redirect_info.new_method = "GET"; |
| redirect_info.status_code = 302; |
| redirect_info.new_site_for_cookies = redirect_info.new_url; |
| return redirect_info; |
| } |
| |
| constexpr static int kDefaultBufferSize = 64 * 1024; |
| |
| SignedExchangeHandlerFactory* g_signed_exchange_factory_for_testing_ = nullptr; |
| |
| } // namespace |
| |
| class WebPackageLoader::ResponseTimingInfo { |
| public: |
| explicit ResponseTimingInfo(const network::ResourceResponseHead& response) |
| : request_start_(response.request_start), |
| response_start_(response.response_start), |
| request_time_(response.request_time), |
| response_time_(response.response_time), |
| load_timing_(response.load_timing) {} |
| |
| network::ResourceResponseHead CreateRedirectResponseHead() const { |
| network::ResourceResponseHead response_head; |
| response_head.encoded_data_length = 0; |
| std::string buf(base::StringPrintf("HTTP/1.1 %d %s\r\n", 302, "Found")); |
| response_head.headers = new net::HttpResponseHeaders( |
| net::HttpUtil::AssembleRawHeaders(buf.c_str(), buf.size())); |
| response_head.encoded_data_length = 0; |
| response_head.request_start = request_start_; |
| response_head.response_start = response_start_; |
| response_head.request_time = request_time_; |
| response_head.response_time = response_time_; |
| response_head.load_timing = load_timing_; |
| return response_head; |
| } |
| |
| private: |
| const base::TimeTicks request_start_; |
| const base::TimeTicks response_start_; |
| const base::Time request_time_; |
| const base::Time response_time_; |
| const net::LoadTimingInfo load_timing_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ResponseTimingInfo); |
| }; |
| |
| WebPackageLoader::WebPackageLoader( |
| const GURL& outer_request_url, |
| const network::ResourceResponseHead& outer_response, |
| network::mojom::URLLoaderClientPtr forwarding_client, |
| network::mojom::URLLoaderClientEndpointsPtr endpoints, |
| url::Origin request_initiator, |
| uint32_t url_loader_options, |
| int load_flags, |
| std::unique_ptr<SignedExchangeDevToolsProxy> devtools_proxy, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| URLLoaderThrottlesGetter url_loader_throttles_getter, |
| scoped_refptr<net::URLRequestContextGetter> request_context_getter) |
| : outer_response_timing_info_( |
| std::make_unique<ResponseTimingInfo>(outer_response)), |
| outer_response_(outer_response), |
| forwarding_client_(std::move(forwarding_client)), |
| url_loader_client_binding_(this), |
| request_initiator_(request_initiator), |
| url_loader_options_(url_loader_options), |
| load_flags_(load_flags), |
| devtools_proxy_(std::move(devtools_proxy)), |
| url_loader_factory_(std::move(url_loader_factory)), |
| url_loader_throttles_getter_(std::move(url_loader_throttles_getter)), |
| request_context_getter_(std::move(request_context_getter)), |
| weak_factory_(this) { |
| DCHECK(signed_exchange_utils::IsSignedExchangeHandlingEnabled()); |
| |
| // https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#privacy-considerations |
| // This can be difficult to determine when the exchange is being loaded from |
| // local disk, but when the client itself requested the exchange over a |
| // network it SHOULD require TLS ([I-D.ietf-tls-tls13]) or a successor |
| // transport layer, and MUST NOT accept exchanges transferred over plain HTTP |
| // without TLS. [spec text] |
| if (!IsOriginSecure(outer_request_url)) { |
| devtools_proxy_->ReportError( |
| "Signed exchange response from non secure origin is not supported.", |
| base::nullopt /* error_field */); |
| // Calls OnSignedExchangeReceived() to show the outer response in DevTool's |
| // Network panel and the error message in the Preview panel. |
| devtools_proxy_->OnSignedExchangeReceived(base::nullopt /* header */, |
| nullptr /* certificate */, |
| nullptr /* ssl_info */); |
| // This will asynchronously delete |this|. |
| forwarding_client_->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_INVALID_SIGNED_EXCHANGE)); |
| return; |
| } |
| |
| // TODO(https://crbug.com/849935): Remove this once we have Network Service |
| // friendly cert, OCSP, and CT verification. |
| if (base::FeatureList::IsEnabled(network::features::kNetworkService)) { |
| devtools_proxy_->ReportError( |
| "Currently, signed exchange does not work when " |
| "chrome://flags/#network-service is enabled. " |
| "See http://crbug.com/849935 for details.", |
| base::nullopt /* error_field */); |
| // Calls OnSignedExchangeReceived() to show the outer response in DevTool's |
| // Network panel and the error message in the Preview panel. |
| devtools_proxy_->OnSignedExchangeReceived(base::nullopt /* header */, |
| nullptr /* certificate */, |
| nullptr /* ssl_info */); |
| // This will asynchronously delete |this|. |
| forwarding_client_->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_INVALID_SIGNED_EXCHANGE)); |
| return; |
| } |
| |
| // Can't use HttpResponseHeaders::GetMimeType() because SignedExchangeHandler |
| // checks "v=" parameter. |
| outer_response.headers->EnumerateHeader(nullptr, "content-type", |
| &content_type_); |
| |
| url_loader_.Bind(std::move(endpoints->url_loader)); |
| |
| if (url_loader_options_ & |
| network::mojom::kURLLoadOptionPauseOnResponseStarted) { |
| // We don't propagate the response to the navigation request and its |
| // throttles, therefore we need to call this here internally in order to |
| // move it forward. |
| // TODO(https://crbug.com/791049): Remove this when NetworkService is |
| // enabled by default. |
| url_loader_->ProceedWithResponse(); |
| } |
| |
| // Bind the endpoint with |this| to get the body DataPipe. |
| url_loader_client_binding_.Bind(std::move(endpoints->url_loader_client)); |
| |
| // |client_| will be bound with a forwarding client by ConnectToClient(). |
| pending_client_request_ = mojo::MakeRequest(&client_); |
| } |
| |
| WebPackageLoader::~WebPackageLoader() = default; |
| |
| void WebPackageLoader::OnReceiveResponse( |
| const network::ResourceResponseHead& response_head) { |
| // Must not be called because this WebPackageLoader and the client endpoints |
| // were bound after OnReceiveResponse() is called. |
| NOTREACHED(); |
| } |
| |
| void WebPackageLoader::OnReceiveRedirect( |
| const net::RedirectInfo& redirect_info, |
| const network::ResourceResponseHead& response_head) { |
| // Must not be called because this WebPackageLoader and the client endpoints |
| // were bound after OnReceiveResponse() is called. |
| NOTREACHED(); |
| } |
| |
| void WebPackageLoader::OnUploadProgress(int64_t current_position, |
| int64_t total_size, |
| OnUploadProgressCallback ack_callback) { |
| // Must not be called because this WebPackageLoader and the client endpoints |
| // were bound after OnReceiveResponse() is called. |
| NOTREACHED(); |
| } |
| |
| void WebPackageLoader::OnReceiveCachedMetadata( |
| const std::vector<uint8_t>& data) { |
| // Curerntly CachedMetadata for WebPackage is not supported. |
| NOTREACHED(); |
| } |
| |
| void WebPackageLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) { |
| // TODO(https://crbug.com/803774): Implement this to progressively update the |
| // encoded data length in DevTools. |
| } |
| |
| void WebPackageLoader::OnStartLoadingResponseBody( |
| mojo::ScopedDataPipeConsumerHandle body) { |
| auto cert_fetcher_factory = SignedExchangeCertFetcherFactory::Create( |
| std::move(request_initiator_), std::move(url_loader_factory_), |
| std::move(url_loader_throttles_getter_)); |
| |
| if (g_signed_exchange_factory_for_testing_) { |
| signed_exchange_handler_ = g_signed_exchange_factory_for_testing_->Create( |
| std::make_unique<DataPipeToSourceStream>(std::move(body)), |
| base::BindOnce(&WebPackageLoader::OnHTTPExchangeFound, |
| weak_factory_.GetWeakPtr()), |
| std::move(cert_fetcher_factory)); |
| return; |
| } |
| |
| signed_exchange_handler_ = std::make_unique<SignedExchangeHandler>( |
| content_type_, std::make_unique<DataPipeToSourceStream>(std::move(body)), |
| base::BindOnce(&WebPackageLoader::OnHTTPExchangeFound, |
| weak_factory_.GetWeakPtr()), |
| std::move(cert_fetcher_factory), load_flags_, |
| std::move(request_context_getter_), std::move(devtools_proxy_)); |
| } |
| |
| void WebPackageLoader::OnComplete( |
| const network::URLLoaderCompletionStatus& status) {} |
| |
| void WebPackageLoader::FollowRedirect( |
| const base::Optional<std::vector<std::string>>& |
| to_be_removed_request_headers, |
| const base::Optional<net::HttpRequestHeaders>& modified_request_headers) { |
| NOTREACHED(); |
| } |
| |
| void WebPackageLoader::ProceedWithResponse() { |
| // TODO(https://crbug.com/791049): Remove this when NetworkService is |
| // enabled by default. |
| DCHECK(!base::FeatureList::IsEnabled(network::features::kNetworkService)); |
| DCHECK(body_data_pipe_adapter_); |
| DCHECK(pending_body_consumer_.is_valid()); |
| |
| body_data_pipe_adapter_->Start(); |
| client_->OnStartLoadingResponseBody(std::move(pending_body_consumer_)); |
| } |
| |
| void WebPackageLoader::SetPriority(net::RequestPriority priority, |
| int intra_priority_value) { |
| // TODO(https://crbug.com/803774): Implement this. |
| } |
| |
| void WebPackageLoader::PauseReadingBodyFromNet() { |
| // TODO(https://crbug.com/803774): Implement this. |
| } |
| |
| void WebPackageLoader::ResumeReadingBodyFromNet() { |
| // TODO(https://crbug.com/803774): Implement this. |
| } |
| |
| void WebPackageLoader::ConnectToClient( |
| network::mojom::URLLoaderClientPtr client) { |
| DCHECK(pending_client_request_.is_pending()); |
| mojo::FuseInterface(std::move(pending_client_request_), |
| client.PassInterface()); |
| } |
| |
| void WebPackageLoader::OnHTTPExchangeFound( |
| net::Error error, |
| const GURL& request_url, |
| const std::string& request_method, |
| const network::ResourceResponseHead& resource_response, |
| std::unique_ptr<net::SourceStream> payload_stream) { |
| if (error) { |
| // This will eventually delete |this|. |
| forwarding_client_->OnComplete(network::URLLoaderCompletionStatus(error)); |
| return; |
| } |
| |
| // TODO(https://crbug.com/803774): Handle no-GET request_method as a error. |
| DCHECK(outer_response_timing_info_); |
| forwarding_client_->OnReceiveRedirect( |
| CreateRedirectInfo(request_url), |
| std::move(outer_response_timing_info_)->CreateRedirectResponseHead()); |
| forwarding_client_.reset(); |
| |
| const base::Optional<net::SSLInfo>& ssl_info = resource_response.ssl_info; |
| if (ssl_info.has_value() && |
| (url_loader_options_ & |
| network::mojom::kURLLoadOptionSendSSLInfoForCertificateError) && |
| net::IsCertStatusError(ssl_info->cert_status) && |
| !net::IsCertStatusMinorError(ssl_info->cert_status)) { |
| ssl_info_ = ssl_info; |
| } |
| if (ssl_info.has_value() && |
| !(url_loader_options_ & |
| network::mojom::kURLLoadOptionSendSSLInfoWithResponse)) { |
| network::ResourceResponseHead response_info = resource_response; |
| response_info.ssl_info = base::nullopt; |
| client_->OnReceiveResponse(response_info); |
| } else { |
| client_->OnReceiveResponse(resource_response); |
| } |
| |
| // Currently we always assume that we have body. |
| // TODO(https://crbug.com/80374): Add error handling and bail out |
| // earlier if there's an error. |
| |
| mojo::DataPipe data_pipe(kDefaultBufferSize); |
| pending_body_consumer_ = std::move(data_pipe.consumer_handle); |
| |
| body_data_pipe_adapter_ = std::make_unique<SourceStreamToDataPipe>( |
| std::move(payload_stream), std::move(data_pipe.producer_handle), |
| base::BindOnce(&WebPackageLoader::FinishReadingBody, |
| base::Unretained(this))); |
| |
| if (url_loader_options_ & |
| network::mojom::kURLLoadOptionPauseOnResponseStarted) { |
| // Need to wait until ProceedWithResponse() is called. |
| return; |
| } |
| |
| // Start reading. |
| body_data_pipe_adapter_->Start(); |
| client_->OnStartLoadingResponseBody(std::move(pending_body_consumer_)); |
| } |
| |
| void WebPackageLoader::FinishReadingBody(int result) { |
| // TODO(https://crbug.com/803774): Fill the data length information too. |
| network::URLLoaderCompletionStatus status; |
| status.error_code = result; |
| |
| if (ssl_info_) { |
| DCHECK((url_loader_options_ & |
| network::mojom::kURLLoadOptionSendSSLInfoForCertificateError) && |
| net::IsCertStatusError(ssl_info_->cert_status) && |
| !net::IsCertStatusMinorError(ssl_info_->cert_status)); |
| status.ssl_info = *ssl_info_; |
| } |
| |
| // This will eventually delete |this|. |
| client_->OnComplete(status); |
| } |
| |
| void WebPackageLoader::SetSignedExchangeHandlerFactoryForTest( |
| SignedExchangeHandlerFactory* factory) { |
| g_signed_exchange_factory_for_testing_ = factory; |
| } |
| |
| } // namespace content |