Commit 27d93f41 authored by Ryan Sturm's avatar Ryan Sturm Committed by Commit Bot

Creating an HTTPS Server Preview net URLLoader

This CL plumbs an URLLoader Factory (and other related information)
from navigation url loader impl to Previews URLLoader during creation.
This allows Previews URLLoader to create an underlying network service
URLLoader for fetching a lite page preview. This adds URLLoaderClient
handling for the serving loader, and allows both URLLoaders to actually
handle URLLoaderRequests instead of always falling back.

This is the minimal CL to be able to attempt server HTTPS lite page
previews. Browser tests will be turned on as more behavior is supported.

Bug: 921740
Change-Id: I8993d00d76c1013eb08b06c544f1c1a491a11da1
Reviewed-on: https://chromium-review.googlesource.com/c/1434521
Commit-Queue: Ryan Sturm <ryansturm@chromium.org>
Reviewed-by: default avatarCamille Lamy <clamy@chromium.org>
Reviewed-by: default avatarMartin Šrámek <msramek@chromium.org>
Reviewed-by: default avatarJohn Abd-El-Malek <jam@chromium.org>
Reviewed-by: default avatarRobert Ogden <robertogden@chromium.org>
Reviewed-by: default avatarTarun Bansal <tbansal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#632761}
parent af37a164
......@@ -4864,7 +4864,9 @@ bool ChromeContentBrowserClient::WillCreateURLLoaderFactory(
std::vector<std::unique_ptr<content::URLLoaderRequestInterceptor>>
ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors(
content::NavigationUIData* navigation_ui_data,
int frame_tree_node_id) {
int frame_tree_node_id,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory) {
std::vector<std::unique_ptr<content::URLLoaderRequestInterceptor>>
interceptors;
#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
......@@ -4876,14 +4878,15 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors(
}
#endif
// TODO(ryansturm): plumb a network service url loader factory and page id
// generator through here. https://crbug.com/921740
// TODO(ryansturm): Once this is on the UI thread, stop passing
// |network_loader_factory| and have interceptors create one themselves.
// https://crbug.com/931786
if (base::FeatureList::IsEnabled(network::features::kNetworkService) &&
base::FeatureList::IsEnabled(
previews::features::kHTTPSServerPreviewsUsingURLLoader)) {
interceptors.push_back(
std::make_unique<previews::PreviewsLitePageURLLoaderInterceptor>(
frame_tree_node_id));
network_loader_factory, frame_tree_node_id));
}
return interceptors;
......
......@@ -471,7 +471,9 @@ class ChromeContentBrowserClient : public content::ContentBrowserClient {
std::vector<std::unique_ptr<content::URLLoaderRequestInterceptor>>
WillCreateURLLoaderRequestInterceptors(
content::NavigationUIData* navigation_ui_data,
int frame_tree_node_id) override;
int frame_tree_node_id,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory) override;
void WillCreateWebSocket(
content::RenderFrameHost* frame,
network::mojom::WebSocketRequest* request,
......
......@@ -4,54 +4,162 @@
#include "chrome/browser/previews/previews_lite_page_redirect_url_loader.h"
#include <utility>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/previews/previews_lite_page_navigation_throttle.h"
#include "components/previews/core/previews_lite_page_redirect.h"
#include "content/public/common/previews_state.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/resource_request.h"
namespace previews {
namespace {
// Used for mojo pipe size. Same constant as navigation code.
constexpr size_t kRedirectDefaultAllocationSize = 512 * 1024;
} // namespace
PreviewsLitePageRedirectURLLoader::PreviewsLitePageRedirectURLLoader(
const network::ResourceRequest& tentative_resource_request,
HandleRequest callback)
: modified_resource_request_(tentative_resource_request),
callback_(std::move(callback)),
binding_(this),
weak_ptr_factory_(this) {}
PreviewsLitePageRedirectURLLoader::~PreviewsLitePageRedirectURLLoader() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
std::unique_ptr<PreviewsLitePageRedirectURLLoader>
PreviewsLitePageRedirectURLLoader::AttemptRedirectToPreview(
const network::ResourceRequest& tentative_resource_request,
HandleRequest callback) {
auto redirect_loader = base::WrapUnique(new PreviewsLitePageRedirectURLLoader(
tentative_resource_request, std::move(callback)));
void PreviewsLitePageRedirectURLLoader::StartRedirectToPreview(
const net::HttpRequestHeaders& chrome_proxy_headers,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory,
int frame_tree_node_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
redirect_loader->StartRedirectToPreview();
GURL lite_page_url = PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(
modified_resource_request_.url);
bool insecure_scheme_was_upgraded = false;
bool copy_fragment = true;
redirect_info_ = net::RedirectInfo::ComputeRedirectInfo(
modified_resource_request_.method, modified_resource_request_.url,
modified_resource_request_.site_for_cookies,
modified_resource_request_.top_frame_origin,
net::URLRequest::UPDATE_FIRST_PARTY_URL_ON_REDIRECT,
modified_resource_request_.referrer_policy,
modified_resource_request_.referrer.spec(), net::HTTP_TEMPORARY_REDIRECT,
lite_page_url, base::nullopt, insecure_scheme_was_upgraded,
copy_fragment);
bool should_clear_upload = false;
net::RedirectUtil::UpdateHttpRequest(
modified_resource_request_.url, modified_resource_request_.method,
redirect_info_, base::nullopt, base::nullopt,
&modified_resource_request_.headers, &should_clear_upload);
modified_resource_request_.headers.MergeFrom(chrome_proxy_headers);
DCHECK(!should_clear_upload);
modified_resource_request_.url = redirect_info_.new_url;
modified_resource_request_.method = redirect_info_.new_method;
modified_resource_request_.site_for_cookies =
redirect_info_.new_site_for_cookies;
modified_resource_request_.top_frame_origin =
redirect_info_.new_top_frame_origin;
modified_resource_request_.referrer = GURL(redirect_info_.new_referrer);
modified_resource_request_.referrer_policy =
redirect_info_.new_referrer_policy;
return redirect_loader;
serving_url_loader_ = std::make_unique<PreviewsLitePageServingURLLoader>(
base::BindOnce(&PreviewsLitePageRedirectURLLoader::OnResultDetermined,
weak_ptr_factory_.GetWeakPtr()));
// |serving_url_loader_| can be null after this call.
serving_url_loader_->StartNetworkRequest(
modified_resource_request_, network_loader_factory, frame_tree_node_id);
}
void PreviewsLitePageRedirectURLLoader::StartRedirectToPreview() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
modified_resource_request_.url =
PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(
modified_resource_request_.url);
void PreviewsLitePageRedirectURLLoader::OnResultDetermined(
ServingLoaderResult result) {
switch (result) {
case ServingLoaderResult::kSuccess:
OnLitePageSuccess();
return;
case ServingLoaderResult::kFallback:
OnLitePageFallback();
return;
}
NOTREACHED();
}
serving_url_loader_ = std::make_unique<PreviewsLitePageServingURLLoader>(
modified_resource_request_,
base::BindOnce(&PreviewsLitePageRedirectURLLoader::OnFallback,
weak_ptr_factory_.GetWeakPtr()));
void PreviewsLitePageRedirectURLLoader::OnLitePageSuccess() {
std::move(callback_).Run(
std::move(serving_url_loader_),
base::BindOnce(
&PreviewsLitePageRedirectURLLoader::StartHandlingRedirectToLitePage,
weak_ptr_factory_.GetWeakPtr()));
}
void PreviewsLitePageRedirectURLLoader::OnFallback() {
void PreviewsLitePageRedirectURLLoader::OnLitePageFallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(callback_).Run(nullptr, {});
}
void PreviewsLitePageRedirectURLLoader::StartHandlingRedirectToLitePage(
const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client) {
network::ResourceResponseHead response_head;
response_head.request_start = base::TimeTicks::Now();
response_head.response_start = response_head.request_start;
std::string header_string = base::StringPrintf(
"HTTP/1.1 %i Temporary Redirect\n"
"Location: %s\n",
net::HTTP_TEMPORARY_REDIRECT,
modified_resource_request_.url.spec().c_str());
scoped_refptr<net::HttpResponseHeaders> fake_headers_for_redirect =
new net::HttpResponseHeaders(net::HttpUtil::AssembleRawHeaders(
header_string.c_str(), header_string.length()));
response_head.headers = fake_headers_for_redirect;
response_head.encoded_data_length = 0;
StartHandlingRedirect(redirect_info_, response_head, resource_request,
std::move(request), std::move(client));
}
void PreviewsLitePageRedirectURLLoader::StartHandlingRedirect(
const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& head,
const network::ResourceRequest& /* resource_request */,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client) {
DCHECK(!binding_.is_bound());
binding_.Bind(std::move(request));
binding_.set_connection_error_handler(
base::BindOnce(&PreviewsLitePageRedirectURLLoader::OnConnectionClosed,
weak_ptr_factory_.GetWeakPtr()));
client_ = std::move(client);
mojo::DataPipe pipe(kRedirectDefaultAllocationSize);
if (!pipe.consumer_handle.is_valid()) {
delete this;
return;
}
client_->OnReceiveRedirect(redirect_info, head);
}
void PreviewsLitePageRedirectURLLoader::FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
......@@ -87,4 +195,12 @@ void PreviewsLitePageRedirectURLLoader::ResumeReadingBodyFromNet() {
serving_url_loader_->ResumeReadingBodyFromNet();
}
void PreviewsLitePageRedirectURLLoader::OnConnectionClosed() {
// This happens when content cancels the navigation. Close the network request
// and client handle and destroy |this|.
binding_.Close();
client_.reset();
delete this;
}
} // namespace previews
......@@ -10,6 +10,10 @@
#include "base/sequence_checker.h"
#include "chrome/browser/previews/previews_lite_page_serving_url_loader.h"
#include "content/public/browser/url_loader_request_interceptor.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/url_loader.mojom.h"
namespace previews {
......@@ -20,22 +24,25 @@ using HandleRequest = base::OnceCallback<void(
// A URL loader that attempts to fetch an HTTPS server lite page, and if
// successful, redirects to the lite page URL, and hands the underlying
// network URLLoader to a success callback. Currently, it can only fallback to
// default behavior.
// network URLLoader to a success callback. Currently, it supports serving the
// Preview and falling back to default behavior.
class PreviewsLitePageRedirectURLLoader : public network::mojom::URLLoader {
public:
static std::unique_ptr<PreviewsLitePageRedirectURLLoader>
AttemptRedirectToPreview(
PreviewsLitePageRedirectURLLoader(
const network::ResourceRequest& tentative_resource_request,
HandleRequest callback);
~PreviewsLitePageRedirectURLLoader() override;
private:
PreviewsLitePageRedirectURLLoader(
const network::ResourceRequest& tentative_resource_request,
HandleRequest callback);
// Creates and starts |serving_url_loader_|. |chrome_proxy_headers| are added
// to the request, and the other parameters are used to start the network
// service URLLoader.
void StartRedirectToPreview(
const net::HttpRequestHeaders& chrome_proxy_headers,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory,
int frame_tree_node_id);
private:
// network::mojom::URLLoader:
void FollowRedirect(const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
......@@ -46,11 +53,32 @@ class PreviewsLitePageRedirectURLLoader : public network::mojom::URLLoader {
void PauseReadingBodyFromNet() override;
void ResumeReadingBodyFromNet() override;
// Called when |serving_url_loader_| wants to fallback.
void OnFallback();
// Processes |result|. Used as a callback for |serving_url_loader_|.
void OnResultDetermined(ServingLoaderResult result);
// Creates and starts |serving_url_loader_|.
void StartRedirectToPreview();
// Called when the lite page can be successfully served.
void OnLitePageSuccess();
// Called when a non-200 response is received.
void OnLitePageFallback();
// The handler when trying to serve the lite page to the user. Serves a
// redirect to the lite page server URL.
void StartHandlingRedirectToLitePage(
const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client);
// Helper method for setting up and serving |redirect_info| to |client|.
void StartHandlingRedirect(
const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& head,
const network::ResourceRequest& /* resource_request */,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr client);
// Mojo error handling. Deletes |this|.
void OnConnectionClosed();
// The underlying URLLoader that speculatively tries to fetch the lite page.
std::unique_ptr<PreviewsLitePageServingURLLoader> serving_url_loader_;
......@@ -59,11 +87,20 @@ class PreviewsLitePageRedirectURLLoader : public network::mojom::URLLoader {
// lite page.
network::ResourceRequest modified_resource_request_;
// Information about the redirect to the lite page server.
net::RedirectInfo redirect_info_;
// Called upon success or failure to let content/ know whether this class
// intends to intercept the request. Must be passed a handler if this class
// intends to intercept the request.
HandleRequest callback_;
// Binding to the URLLoader interface.
mojo::Binding<network::mojom::URLLoader> binding_;
// The owning client. Used for serving redirects.
network::mojom::URLLoaderClientPtr client_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<PreviewsLitePageRedirectURLLoader> weak_ptr_factory_;
......
......@@ -4,33 +4,200 @@
#include "chrome/browser/previews/previews_lite_page_serving_url_loader.h"
#include <utility>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/previews_state.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
namespace previews {
namespace {
// Used for mojo pipe size. Same constant as navigation code.
constexpr size_t kServingDefaultAllocationSize = 512 * 1024;
const net::NetworkTrafficAnnotationTag kPreviewsTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("https_server_previews_navigation", R"(
semantics {
sender: "HTTPS server previews navigation URL Loader"
description:
"This request is issued by a main frame navigation to fetch a server "
"generated lite page version of page."
trigger:
"Navigating Chrome (by clicking on a link, bookmark, history item, "
"using session restore, etc) on a slow page."
data:
"Arbitrary site-controlled data can be included in the URL, HTTP "
"headers, and request body."
destination: WEBSITE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting: "Disable Data Saver on Android (Settings > Data Saver)."
chrome_policy {
URLBlacklist {
URLBlacklist: { entries: '*' }
}
}
chrome_policy {
URLWhitelist {
URLWhitelist { }
}
}
}
)");
} // namespace
PreviewsLitePageServingURLLoader::PreviewsLitePageServingURLLoader(
ResultCallback result_callback)
: url_loader_binding_(this),
result_callback_(std::move(result_callback)),
binding_(this),
weak_ptr_factory_(this) {}
void PreviewsLitePageServingURLLoader::StartNetworkRequest(
const network::ResourceRequest& request,
FallbackCallback fallback_callback)
: fallback_callback_(std::move(fallback_callback)),
weak_ptr_factory_(this) {
// TODO(ryansturm): Set up a network service URLLoader. For now, simulate a
// a fallback asynchronously.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&PreviewsLitePageServingURLLoader::Fallback,
weak_ptr_factory_.GetWeakPtr()));
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory,
int frame_tree_node_id) {
network::mojom::URLLoaderClientPtr client;
url_loader_binding_.Bind(mojo::MakeRequest(&client),
base::ThreadTaskRunnerHandle::Get());
url_loader_binding_.set_connection_error_handler(
base::BindOnce(&PreviewsLitePageServingURLLoader::OnConnectionError,
base::Unretained(this)));
// Create a network service URL loader with passed in params.
network_loader_factory->CreateLoaderAndStart(
mojo::MakeRequest(&network_url_loader_), frame_tree_node_id, 0,
network::mojom::kURLLoadOptionNone, request, std::move(client),
net::MutableNetworkTrafficAnnotationTag(kPreviewsTrafficAnnotation));
}
PreviewsLitePageServingURLLoader::~PreviewsLitePageServingURLLoader() = default;
void PreviewsLitePageServingURLLoader::Fallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(fallback_callback_).Run();
std::move(result_callback_).Run(ServingLoaderResult::kFallback);
}
RequestHandler PreviewsLitePageServingURLLoader::ServingResponseHandler() {
DCHECK(result_callback_.is_null());
return base::BindOnce(
&PreviewsLitePageServingURLLoader::SetUpForwardingClient,
weak_ptr_factory_.GetWeakPtr());
}
void PreviewsLitePageServingURLLoader::SetUpForwardingClient(
const network::ResourceRequest& /* resource_request */,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr forwarding_client) {
// Bind to the content/ navigation code.
DCHECK(!binding_.is_bound());
binding_.Bind(std::move(request));
binding_.set_connection_error_handler(
base::BindOnce(&PreviewsLitePageServingURLLoader::OnConnectionError,
weak_ptr_factory_.GetWeakPtr()));
forwarding_client_ = std::move(forwarding_client);
// If there was an URLLoader error between handing off this handler and
// running it, don't handle the request.
if (!network_url_loader_) {
binding_.Close();
forwarding_client_.reset();
delete this;
return;
}
mojo::DataPipe pipe(kServingDefaultAllocationSize);
if (!pipe.consumer_handle.is_valid()) {
network_url_loader_.reset();
url_loader_binding_.Close();
delete this;
return;
}
forwarding_client_->OnReceiveResponse(resource_response_->head);
// Resume previously paused network service URLLoader.
url_loader_binding_.ResumeIncomingMethodCallProcessing();
}
void PreviewsLitePageServingURLLoader::OnReceiveResponse(
const network::ResourceResponseHead& head) {
DCHECK(!forwarding_client_);
// TODO: evaluate all the responses we allow, don't hard code 200.
if (head.headers->response_code() != net::HTTP_OK) {
Fallback();
return;
}
// Store head and pause new messages until the forwarding client is set up.
// Make a deep copy of ResourceResponseHead before passing it cross-thread.
resource_response_ = base::MakeRefCounted<network::ResourceResponse>();
resource_response_->head = head;
url_loader_binding_.PauseIncomingMethodCallProcessing();
std::move(result_callback_).Run(ServingLoaderResult::kSuccess);
}
void PreviewsLitePageServingURLLoader::OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& head) {
DCHECK(!forwarding_client_);
// We might receive a redirect from the lite page server, and we should treat
// that appropriately. For now fallback.
// TODO(ryansturm): Handle redirects. https://crbug.com/921744
Fallback();
}
void PreviewsLitePageServingURLLoader::OnUploadProgress(
int64_t current_position,
int64_t total_size,
OnUploadProgressCallback callback) {
// We only handle GETs.
NOTREACHED();
}
void PreviewsLitePageServingURLLoader::OnReceiveCachedMetadata(
const std::vector<uint8_t>& data) {
// Do nothing. This is not supported for navigation loader.
}
void PreviewsLitePageServingURLLoader::OnTransferSizeUpdated(
int32_t transfer_size_diff) {
DCHECK(forwarding_client_);
forwarding_client_->OnTransferSizeUpdated(transfer_size_diff);
}
void PreviewsLitePageServingURLLoader::OnStartLoadingResponseBody(
mojo::ScopedDataPipeConsumerHandle body) {
DCHECK(forwarding_client_);
forwarding_client_->OnStartLoadingResponseBody(std::move(body));
}
void PreviewsLitePageServingURLLoader::OnComplete(
const network::URLLoaderCompletionStatus& status) {
if (forwarding_client_) {
forwarding_client_->OnComplete(status);
return;
}
// If OnComplete is called before, OnReceiveResponse, this is indicative of a
// failure of some sort.
// TODO(ryansturm): Handle specific errors with a bypass pattern.
// https://crbug.com/921744
Fallback();
}
void PreviewsLitePageServingURLLoader::FollowRedirect(
......@@ -44,24 +211,46 @@ void PreviewsLitePageServingURLLoader::FollowRedirect(
void PreviewsLitePageServingURLLoader::ProceedWithResponse() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(ryansturm): Pass through calls to a network service URLLoader.
// Pass through.
network_url_loader_->ProceedWithResponse();
}
void PreviewsLitePageServingURLLoader::SetPriority(
net::RequestPriority priority,
int32_t intra_priority_value) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(ryansturm): Pass through calls to a network service URLLoader.
// Pass through.
network_url_loader_->SetPriority(priority, intra_priority_value);
}
void PreviewsLitePageServingURLLoader::PauseReadingBodyFromNet() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(ryansturm): Pass through calls to a network service URLLoader.
// Pass through.
network_url_loader_->PauseReadingBodyFromNet();
}
void PreviewsLitePageServingURLLoader::ResumeReadingBodyFromNet() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(ryansturm): Pass through calls to a network service URLLoader.
// Pass through.
network_url_loader_->ResumeReadingBodyFromNet();
}
void PreviewsLitePageServingURLLoader::OnConnectionError() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// When we are not yet bound to the navigation code, fallback, which will
// destroy this object.
if (!result_callback_.is_null()) {
Fallback();
return;
}
network_url_loader_.reset();
url_loader_binding_.Close();
if (binding_.is_bound()) {
binding_.Close();
forwarding_client_.reset();
delete this;
}
}
} // namespace previews
......@@ -5,29 +5,57 @@
#ifndef CHROME_BROWSER_PREVIEWS_PREVIEWS_LITE_PAGE_SERVING_URL_LOADER_H_
#define CHROME_BROWSER_PREVIEWS_PREVIEWS_LITE_PAGE_SERVING_URL_LOADER_H_
#include <vector>
#include "base/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "content/public/browser/url_loader_request_interceptor.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/url_loader.mojom.h"
namespace previews {
using FallbackCallback = base::OnceCallback<void()>;
enum class ServingLoaderResult {
kSuccess, // The Preview can be served.
kFallback // The Preview cannot be served and fallback to default URLLoader
// should occur.
};
using ResultCallback = base::OnceCallback<void(ServingLoaderResult)>;
using RequestHandler =
base::OnceCallback<void(const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest,
network::mojom::URLLoaderClientPtr)>;
// This class attempts to fetch a LitePage from the LitePage server, and if
// successful, calls a success callback. Otherwise, it calls fallback in the
// case of a failure and redirect in the case of a redirect served from the lite
// pages service. For now, it is only partially implemented.
class PreviewsLitePageServingURLLoader : public network::mojom::URLLoader {
// pages service.
class PreviewsLitePageServingURLLoader
: public network::mojom::URLLoader,
public network::mojom::URLLoaderClient {
public:
// Creates a network service URLLoader, binds to the URL Loader, and stores
// the various callbacks.
PreviewsLitePageServingURLLoader(const network::ResourceRequest& request,
FallbackCallback fallback_callback);
explicit PreviewsLitePageServingURLLoader(ResultCallback result_callback);
~PreviewsLitePageServingURLLoader() override;
// Begins the underlying network URLLoader to fetch the preview.
// |network_loader_factory| creates the URLLoader using the other parameters
// in this method.
void StartNetworkRequest(const network::ResourceRequest& request,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory,
int frame_tree_node_id);
// Called when the response should be served to the user. Returns a handler.
RequestHandler ServingResponseHandler();
// network::mojom::URLLoader:
void FollowRedirect(const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
......@@ -39,12 +67,50 @@ class PreviewsLitePageServingURLLoader : public network::mojom::URLLoader {
void ResumeReadingBodyFromNet() override;
private:
// Calls |fallback_callback_| and cleans up.
// network::mojom::URLLoaderClient
void OnReceiveResponse(const network::ResourceResponseHead& head) override;
void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& head) override;
void OnUploadProgress(int64_t current_position,
int64_t total_size,
OnUploadProgressCallback callback) override;
void OnReceiveCachedMetadata(const std::vector<uint8_t>& data) override;
void OnTransferSizeUpdated(int32_t transfer_size_diff) override;
void OnStartLoadingResponseBody(
mojo::ScopedDataPipeConsumerHandle body) override;
void OnComplete(const network::URLLoaderCompletionStatus& status) override;
// When a connection error occurs in either mojo pipe, this objects lifetime
// needs to be managed and the connections need to be closed.
void OnConnectionError();
// Calls |result_callback_| with kFallback and cleans up.
void Fallback();
// When an error occurs or the LitePage is not suitable, this callback resumes
// default behavior.
FallbackCallback fallback_callback_;
// Sets up mojo forwarding to the navigation path. Resumes
// |network_url_loader_| calls. Serves the start of the response to the
// navigation path.
void SetUpForwardingClient(
const network::ResourceRequest&,
network::mojom::URLLoaderRequest request,
network::mojom::URLLoaderClientPtr forwarding_client);
// The network URLLoader that fetches the LitePage URL and its binding.
network::mojom::URLLoaderPtr network_url_loader_;
mojo::Binding<network::mojom::URLLoaderClient> url_loader_binding_;
// When a result is determined, this callback should be run with the
// appropriate ServingLoaderResult.
ResultCallback result_callback_;
// Once the LitePage response is received and is ready to be served, the
// response info related to the request. When this becomes populated, the
// network URL Loader calls are paused.
scoped_refptr<network::ResourceResponse> resource_response_;
// Forwarding client binding.
mojo::Binding<network::mojom::URLLoader> binding_;
network::mojom::URLLoaderClientPtr forwarding_client_;
SEQUENCE_CHECKER(sequence_checker_);
......
......@@ -4,12 +4,21 @@
#include "chrome/browser/previews/previews_lite_page_url_loader_interceptor.h"
#include <utility>
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/profiles/profile_io_data.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_request_options.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_headers.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_params.h"
#include "components/previews/core/previews_features.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/previews_state.h"
#include "content/public/common/resource_type.h"
#include "net/http/http_request_headers.h"
#include "net/nqe/effective_connection_type.h"
namespace previews {
......@@ -31,14 +40,40 @@ bool ShouldCreateLoader(const network::ResourceRequest& resource_request) {
return true;
}
net::HttpRequestHeaders GetChromeProxyHeaders(
content::ResourceContext* context) {
net::HttpRequestHeaders headers;
// Return empty headers for unittests.
if (!context)
return headers;
// TODO(ryansturm): If this switches to the UI thread, this needs to be
// re-worked. This information is all available on the UI thread.
// https://crbug.com/931786
auto* io_data = ProfileIOData::FromResourceContext(context);
if (io_data && io_data->data_reduction_proxy_io_data()) {
DCHECK(data_reduction_proxy::params::IsEnabledWithNetworkService());
data_reduction_proxy::DataReductionProxyRequestOptions* request_options =
io_data->data_reduction_proxy_io_data()->request_options();
request_options->AddRequestHeader(&headers,
request_options->GeneratePageId());
headers.SetHeader(data_reduction_proxy::chrome_proxy_ect_header(),
net::GetNameForEffectiveConnectionType(
io_data->data_reduction_proxy_io_data()
->GetEffectiveConnectionType()));
}
return headers;
}
} // namespace
PreviewsLitePageURLLoaderInterceptor::PreviewsLitePageURLLoaderInterceptor(
int frame_tree_node_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(ryansturm): use frame_tree_node_id and pass in other members.
// https://crbug.com/921740
}
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory,
int frame_tree_node_id)
: network_loader_factory_(network_loader_factory),
frame_tree_node_id_(frame_tree_node_id) {}
PreviewsLitePageURLLoaderInterceptor::~PreviewsLitePageURLLoaderInterceptor() {}
......@@ -48,6 +83,14 @@ void PreviewsLitePageURLLoaderInterceptor::MaybeCreateLoader(
content::URLLoaderRequestInterceptor::LoaderCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (serving_url_loader_) {
RequestHandler handler = serving_url_loader_->ServingResponseHandler();
// The serving loader manages its own lifetime at this point.
serving_url_loader_.release();
std::move(callback).Run(std::move(handler));
return;
}
// TODO(ryansturm): Handle reloads by handling non-intercepted attempts at
// fetching the lite page server. https://crbug.com/921756
......@@ -68,12 +111,16 @@ void PreviewsLitePageURLLoaderInterceptor::CreateRedirectLoader(
RecordInterceptAttempt(true);
redirect_url_loader_ =
PreviewsLitePageRedirectURLLoader::AttemptRedirectToPreview(
tentative_resource_request,
base::BindOnce(
&PreviewsLitePageURLLoaderInterceptor::HandleRedirectLoader,
base::Unretained(this), std::move(callback)));
redirect_url_loader_ = std::make_unique<PreviewsLitePageRedirectURLLoader>(
tentative_resource_request,
base::BindOnce(
&PreviewsLitePageURLLoaderInterceptor::HandleRedirectLoader,
base::Unretained(this), std::move(callback)));
// |redirect_url_loader_| can be null after this call.
redirect_url_loader_->StartRedirectToPreview(
GetChromeProxyHeaders(resource_context), network_loader_factory_,
frame_tree_node_id_);
}
void PreviewsLitePageURLLoaderInterceptor::HandleRedirectLoader(
......@@ -82,11 +129,20 @@ void PreviewsLitePageURLLoaderInterceptor::HandleRedirectLoader(
RequestHandler handler) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Handle any failure by using default loader.
// For now, only fallback is allowed.
DCHECK(!serving_url_loader);
DCHECK(handler.is_null());
redirect_url_loader_.reset();
std::move(callback).Run({});
if (!serving_url_loader) {
DCHECK(handler.is_null());
redirect_url_loader_.reset();
std::move(callback).Run({});
return;
}
// Save the serving loader to handle the next request.
serving_url_loader_ = std::move(serving_url_loader);
// |redirect_url_loader_| now manages its own lifetime via a mojo channel.
// |handler| is guaranteed to be called.
redirect_url_loader_.release();
std::move(callback).Run(std::move(handler));
}
} // namespace previews
......@@ -5,10 +5,14 @@
#ifndef CHROME_BROWSER_PREVIEWS_PREVIEWS_LITE_PAGE_URL_LOADER_INTERCEPTOR_H_
#define CHROME_BROWSER_PREVIEWS_PREVIEWS_LITE_PAGE_URL_LOADER_INTERCEPTOR_H_
#include <memory>
#include "base/memory/scoped_refptr.h"
#include "base/sequence_checker.h"
#include "chrome/browser/previews/previews_lite_page_redirect_url_loader.h"
#include "chrome/browser/previews/previews_lite_page_serving_url_loader.h"
#include "content/public/browser/url_loader_request_interceptor.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace previews {
......@@ -18,13 +22,11 @@ namespace previews {
// code. Currently, not fully implemented.
class PreviewsLitePageURLLoaderInterceptor
: public content::URLLoaderRequestInterceptor {
using RequestHandler =
base::OnceCallback<void(const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest,
network::mojom::URLLoaderClientPtr)>;
public:
explicit PreviewsLitePageURLLoaderInterceptor(int frame_tree_node_id);
PreviewsLitePageURLLoaderInterceptor(
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory,
int frame_tree_node_id);
~PreviewsLitePageURLLoaderInterceptor() override;
// content::URLLaoderRequestInterceptor:
......@@ -51,6 +53,18 @@ class PreviewsLitePageURLLoaderInterceptor
// made regarding serving the preview, this object will be null.
std::unique_ptr<PreviewsLitePageRedirectURLLoader> redirect_url_loader_;
// Once a decision to serve the lite page has been made (based on server
// response), this object will exist until a redirect to the lite page URL has
// been handed off to the navigation stack and the next request is being
// handled.
std::unique_ptr<PreviewsLitePageServingURLLoader> serving_url_loader_;
// Factory to create a network service URLLoader.
scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory_;
// Used to create the network service URLLoader.
int frame_tree_node_id_;
SEQUENCE_CHECKER(sequence_checker_);
DISALLOW_COPY_AND_ASSIGN(PreviewsLitePageURLLoaderInterceptor);
......
......@@ -4,26 +4,77 @@
#include "chrome/browser/previews/previews_lite_page_url_loader_interceptor.h"
#include <memory>
#include <string>
#include "base/bind.h"
#include "base/optional.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_task_environment.h"
#include "chrome/browser/previews/previews_lite_page_navigation_throttle.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/url_loader_request_interceptor.h"
#include "content/public/common/previews_state.h"
#include "content/public/common/resource_type.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_request_status.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace previews {
namespace {
void EmptyCallback(
content::URLLoaderRequestInterceptor::RequestHandler callback) {}
class PreviewsLitePageURLLoaderInterceptorTest : public testing::Test {
public:
PreviewsLitePageURLLoaderInterceptorTest()
: shared_factory_(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_)) {}
~PreviewsLitePageURLLoaderInterceptorTest() override {}
void TearDown() override {}
void SetUp() override {
interceptor_ = std::make_unique<PreviewsLitePageURLLoaderInterceptor>(
shared_factory_, 1);
}
void SetFakeResponse(const GURL& url,
const std::string& data,
net::HttpStatusCode code,
int net_error) {
test_url_loader_factory_.AddResponse(
url, network::CreateResourceResponseHead(code), data,
network::URLLoaderCompletionStatus(net_error));
}
void HandlerCallback(
content::URLLoaderRequestInterceptor::RequestHandler callback) {
callback_was_empty_ = callback.is_null();
}
base::Optional<bool> callback_was_empty() { return callback_was_empty_; }
void ResetTest() { callback_was_empty_ = base::nullopt; }
PreviewsLitePageURLLoaderInterceptor& interceptor() { return *interceptor_; }
protected:
base::test::ScopedTaskEnvironment scoped_task_environment_;
TEST(PreviewsLitePageURLLoaderInterceptorTest, InterceptRequest) {
base::test::ScopedTaskEnvironment scoped_task_environment;
PreviewsLitePageURLLoaderInterceptor interceptor(1);
private:
base::Optional<bool> callback_was_empty_;
std::unique_ptr<PreviewsLitePageURLLoaderInterceptor> interceptor_;
network::TestURLLoaderFactory test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> shared_factory_;
};
TEST_F(PreviewsLitePageURLLoaderInterceptorTest,
InterceptRequestPreviewsState) {
base::HistogramTester histogram_tester;
network::ResourceRequest request;
......@@ -31,23 +82,144 @@ TEST(PreviewsLitePageURLLoaderInterceptorTest, InterceptRequest) {
request.resource_type = static_cast<int>(content::RESOURCE_TYPE_MAIN_FRAME);
request.method = "GET";
SetFakeResponse(
PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(request.url),
"Fake Body", net::HTTP_OK, net::URLRequestStatus::SUCCESS);
// Check that we don't trigger when previews are not allowed.
request.previews_state = content::PREVIEWS_OFF;
interceptor.MaybeCreateLoader(request, nullptr,
base::BindOnce(&EmptyCallback));
interceptor().MaybeCreateLoader(
request, nullptr,
base::BindOnce(&PreviewsLitePageURLLoaderInterceptorTest::HandlerCallback,
base::Unretained(this)));
histogram_tester.ExpectUniqueSample(
"Previews.ServerLitePage.URLLoader.Attempted", false, 1);
scoped_task_environment_.RunUntilIdle();
EXPECT_TRUE(callback_was_empty().has_value());
EXPECT_TRUE(callback_was_empty().value());
ResetTest();
SetFakeResponse(request.url, "Fake Body", net::HTTP_OK,
net::URLRequestStatus::SUCCESS);
// Check that we trigger when previews are allowed.
request.previews_state = content::LITE_PAGE_REDIRECT_ON;
interceptor.MaybeCreateLoader(request, nullptr,
base::BindOnce(&EmptyCallback));
interceptor().MaybeCreateLoader(
request, nullptr,
base::BindOnce(&PreviewsLitePageURLLoaderInterceptorTest::HandlerCallback,
base::Unretained(this)));
histogram_tester.ExpectBucketCount(
"Previews.ServerLitePage.URLLoader.Attempted", true, 1);
histogram_tester.ExpectTotalCount(
"Previews.ServerLitePage.URLLoader.Attempted", 2);
scoped_task_environment_.RunUntilIdle();
EXPECT_TRUE(callback_was_empty().has_value());
EXPECT_FALSE(callback_was_empty().value());
}
TEST_F(PreviewsLitePageURLLoaderInterceptorTest, InterceptRequestRedirect) {
base::HistogramTester histogram_tester;
network::ResourceRequest request;
request.url = GURL("https://google.com");
request.resource_type = static_cast<int>(content::RESOURCE_TYPE_MAIN_FRAME);
request.method = "GET";
request.previews_state = content::LITE_PAGE_REDIRECT_ON;
SetFakeResponse(
PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(request.url),
"Fake Body", net::HTTP_TEMPORARY_REDIRECT,
net::URLRequestStatus::SUCCESS);
interceptor().MaybeCreateLoader(
request, nullptr,
base::BindOnce(&PreviewsLitePageURLLoaderInterceptorTest::HandlerCallback,
base::Unretained(this)));
histogram_tester.ExpectUniqueSample(
"Previews.ServerLitePage.URLLoader.Attempted", true, 1);
scoped_task_environment_.RunUntilIdle();
EXPECT_TRUE(callback_was_empty().has_value());
EXPECT_TRUE(callback_was_empty().value());
}
TEST_F(PreviewsLitePageURLLoaderInterceptorTest,
InterceptRequestServerOverloaded) {
base::HistogramTester histogram_tester;
network::ResourceRequest request;
request.url = GURL("https://google.com");
request.resource_type = static_cast<int>(content::RESOURCE_TYPE_MAIN_FRAME);
request.method = "GET";
request.previews_state = content::LITE_PAGE_REDIRECT_ON;
SetFakeResponse(
PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(request.url),
"Fake Body", net::HTTP_SERVICE_UNAVAILABLE,
net::URLRequestStatus::SUCCESS);
interceptor().MaybeCreateLoader(
request, nullptr,
base::BindOnce(&PreviewsLitePageURLLoaderInterceptorTest::HandlerCallback,
base::Unretained(this)));
histogram_tester.ExpectUniqueSample(
"Previews.ServerLitePage.URLLoader.Attempted", true, 1);
scoped_task_environment_.RunUntilIdle();
EXPECT_TRUE(callback_was_empty().has_value());
EXPECT_TRUE(callback_was_empty().value());
}
TEST_F(PreviewsLitePageURLLoaderInterceptorTest,
InterceptRequestServerNotHandling) {
base::HistogramTester histogram_tester;
network::ResourceRequest request;
request.url = GURL("https://google.com");
request.resource_type = static_cast<int>(content::RESOURCE_TYPE_MAIN_FRAME);
request.method = "GET";
request.previews_state = content::LITE_PAGE_REDIRECT_ON;
SetFakeResponse(
PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(request.url),
"Fake Body", net::HTTP_FORBIDDEN, net::URLRequestStatus::SUCCESS);
interceptor().MaybeCreateLoader(
request, nullptr,
base::BindOnce(&PreviewsLitePageURLLoaderInterceptorTest::HandlerCallback,
base::Unretained(this)));
histogram_tester.ExpectUniqueSample(
"Previews.ServerLitePage.URLLoader.Attempted", true, 1);
scoped_task_environment_.RunUntilIdle();
EXPECT_TRUE(callback_was_empty().has_value());
EXPECT_TRUE(callback_was_empty().value());
}
TEST_F(PreviewsLitePageURLLoaderInterceptorTest, NetStackError) {
base::HistogramTester histogram_tester;
network::ResourceRequest request;
request.url = GURL("https://google.com");
request.resource_type = static_cast<int>(content::RESOURCE_TYPE_MAIN_FRAME);
request.method = "GET";
request.previews_state = content::LITE_PAGE_REDIRECT_ON;
SetFakeResponse(
PreviewsLitePageNavigationThrottle::GetPreviewsURLForURL(request.url),
"Fake Body", net::HTTP_OK, net::URLRequestStatus::FAILED);
interceptor().MaybeCreateLoader(
request, nullptr,
base::BindOnce(&PreviewsLitePageURLLoaderInterceptorTest::HandlerCallback,
base::Unretained(this)));
histogram_tester.ExpectUniqueSample(
"Previews.ServerLitePage.URLLoader.Attempted", true, 1);
scoped_task_environment_.RunUntilIdle();
EXPECT_TRUE(callback_was_empty().has_value());
EXPECT_TRUE(callback_was_empty().value());
}
} // namespace
......
......@@ -764,11 +764,12 @@ class NavigationURLLoaderImpl::URLLoaderRequestController
// See if embedders want to add interceptors.
std::vector<std::unique_ptr<URLLoaderRequestInterceptor>>
browser_interceptors = GetContentClient()
->browser()
->WillCreateURLLoaderRequestInterceptors(
navigation_ui_data_.get(),
request_info->frame_tree_node_id);
browser_interceptors =
GetContentClient()
->browser()
->WillCreateURLLoaderRequestInterceptors(
navigation_ui_data_.get(), request_info->frame_tree_node_id,
network_loader_factory_);
if (!browser_interceptors.empty()) {
for (auto& browser_interceptor : browser_interceptors) {
interceptors_.push_back(
......
......@@ -751,8 +751,10 @@ void ContentBrowserClient::WillCreateWebSocket(
std::vector<std::unique_ptr<URLLoaderRequestInterceptor>>
ContentBrowserClient::WillCreateURLLoaderRequestInterceptors(
NavigationUIData* navigation_ui_data,
int frame_tree_node_id) {
content::NavigationUIData* navigation_ui_data,
int frame_tree_node_id,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory) {
return std::vector<std::unique_ptr<URLLoaderRequestInterceptor>>();
}
......
......@@ -1236,8 +1236,11 @@ class CONTENT_EXPORT ContentBrowserClient {
// Always called on the IO thread and only when the Network Service is
// enabled.
virtual std::vector<std::unique_ptr<URLLoaderRequestInterceptor>>
WillCreateURLLoaderRequestInterceptors(NavigationUIData* navigation_ui_data,
int frame_tree_node_id);
WillCreateURLLoaderRequestInterceptors(
content::NavigationUIData* navigation_ui_data,
int frame_tree_node_id,
const scoped_refptr<network::SharedURLLoaderFactory>&
network_loader_factory);
// Called when the NetworkService, accessible through
// content::GetNetworkService(), is created. Implementations should avoid
......
......@@ -136,6 +136,7 @@ Refer to README.md for content description and update process.
<item id="history_notice_utils_notice" hash_code="102595701" type="1" second_id="110307337" content_hash_code="130829410" os_list="linux,windows" semantics_fields="2,3,4" policy_fields="4" file_path="components/browsing_data/core/history_notice_utils.cc"/>
<item id="history_notice_utils_popup" hash_code="80832574" type="1" second_id="110307337" content_hash_code="30618510" os_list="linux,windows" semantics_fields="2,3,4" policy_fields="4" file_path="components/browsing_data/core/history_notice_utils.cc"/>
<item id="http_server_error_response" hash_code="32197336" type="0" content_hash_code="61082230" os_list="linux,windows" file_path="net/server/http_server.cc"/>
<item id="https_server_previews_navigation" hash_code="35725390" type="0" content_hash_code="84423109" os_list="linux,windows" file_path="chrome/browser/previews/previews_lite_page_serving_url_loader.cc"/>
<item id="icon_cacher" hash_code="103133150" type="0" content_hash_code="116368348" os_list="linux,windows" file_path="components/ntp_tiles/icon_cacher_impl.cc"/>
<item id="icon_catcher_get_large_icon" hash_code="44494884" type="0" content_hash_code="98262037" os_list="linux,windows" file_path="components/ntp_tiles/icon_cacher_impl.cc"/>
<item id="image_annotation" hash_code="107881858" type="0" content_hash_code="121106764" os_list="linux,windows" file_path="services/image_annotation/annotator.cc"/>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment