blob: 7fdff6c5572b390a483daac678d2f2d7835b5a92 [file] [log] [blame]
// Copyright 2019 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 "chrome/renderer/subresource_redirect/subresource_redirect_url_loader_throttle.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "chrome/renderer/previews/resource_loading_hints_agent.h"
#include "chrome/renderer/subresource_redirect/redirect_result.h"
#include "chrome/renderer/subresource_redirect/subresource_redirect_params.h"
#include "chrome/renderer/subresource_redirect/subresource_redirect_util.h"
#include "components/data_reduction_proxy/core/common/data_reduction_proxy_headers.h"
#include "components/subresource_redirect/common/subresource_redirect_features.h"
#include "content/public/renderer/render_frame.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/loader/previews_state.h"
#include "third_party/blink/public/platform/web_network_state_notifier.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/platform/web_url_request.h"
namespace subresource_redirect {
namespace {
// Whether the url points to compressed server origin.
bool IsCompressionServerOrigin(const GURL& url) {
auto compression_server = GetSubresourceRedirectOrigin();
return url.DomainIs(compression_server.host()) &&
(url.EffectiveIntPort() == compression_server.port()) &&
(url.scheme() == compression_server.scheme());
}
// Returns the decider for the render frame
PublicResourceDeciderAgent* GetPublicResourceDeciderAgent(int render_frame_id) {
return PublicResourceDeciderAgent::Get(
content::RenderFrame::FromRoutingID(render_frame_id));
}
} // namespace
// static
std::unique_ptr<SubresourceRedirectURLLoaderThrottle>
SubresourceRedirectURLLoaderThrottle::MaybeCreateThrottle(
const blink::WebURLRequest& request,
int render_frame_id) {
if (!ShouldEnablePublicImageHintsBasedCompression() &&
!ShouldEnableLoginRobotsCheckedCompression()) {
return nullptr;
}
if (request.GetRequestDestination() ==
network::mojom::RequestDestination::kImage &&
request.Url().ProtocolIs(url::kHttpsScheme) &&
blink::WebNetworkStateNotifier::SaveDataEnabled() &&
request.GetRequestContext() !=
blink::mojom::RequestContextType::FAVICON) {
return base::WrapUnique<SubresourceRedirectURLLoaderThrottle>(
new SubresourceRedirectURLLoaderThrottle(
render_frame_id, request.GetPreviewsState() &
blink::PreviewsTypes::kSubresourceRedirectOn));
}
return nullptr;
}
SubresourceRedirectURLLoaderThrottle::SubresourceRedirectURLLoaderThrottle(
int render_frame_id,
bool allowed_to_redirect)
: render_frame_id_(render_frame_id) {
DCHECK(ShouldEnablePublicImageHintsBasedCompression() ||
ShouldEnableLoginRobotsCheckedCompression());
redirect_result_ = allowed_to_redirect
? RedirectResult::kRedirectable
: RedirectResult::kIneligibleBlinkDisallowed;
}
SubresourceRedirectURLLoaderThrottle::~SubresourceRedirectURLLoaderThrottle() =
default;
void SubresourceRedirectURLLoaderThrottle::WillStartRequest(
network::ResourceRequest* request,
bool* defer) {
DCHECK_EQ(request->destination, network::mojom::RequestDestination::kImage);
DCHECK(request->url.SchemeIs(url::kHttpsScheme));
if (redirect_result_ != RedirectResult::kRedirectable)
return;
// Do not redirect if its already a litepage subresource.
if (IsCompressionServerOrigin(request->url))
return;
if (!ShouldCompressRedirectSubresource())
return;
auto* public_resource_decider_agent =
GetPublicResourceDeciderAgent(render_frame_id_);
if (!public_resource_decider_agent)
return;
auto redirect_result =
public_resource_decider_agent->ShouldRedirectSubresource(
request->url, base::BindOnce(&SubresourceRedirectURLLoaderThrottle::
NotifyRedirectDeciderDecision,
weak_ptr_factory_.GetWeakPtr()));
if (!redirect_result) {
// Decision cannot be made yet. Defer the subresource and change the URL to
// compression server URL. The NotifyRedirectDeciderDecision callback will
// continue with compression or disable compression by resetting to original
// URL.
redirect_state_ = RedirectState::kRedirectDecisionPending;
*defer = true;
request->url = GetSubresourceURLForURL(request->url);
return;
}
// The decider decision has been made.
*defer = false;
redirect_result_ = *redirect_result;
if (redirect_result_ != RedirectResult::kRedirectable) {
redirect_state_ = RedirectState::kRedirectNotAllowedByDecider;
return;
}
// Redirect is allowed.
redirect_state_ = RedirectState::kRedirectAttempted;
request->url = GetSubresourceURLForURL(request->url);
StartRedirectTimeoutTimer();
}
void SubresourceRedirectURLLoaderThrottle::NotifyRedirectDeciderDecision(
RedirectResult redirect_result) {
DCHECK_EQ(RedirectState::kRedirectDecisionPending, redirect_state_);
redirect_result_ = redirect_result;
if (redirect_result_ != RedirectResult::kRedirectable) {
// Restart the fetch to the original URL.
redirect_state_ = RedirectState::kRedirectNotAllowedByDecider;
delegate_->RestartWithURLResetAndFlags(net::LOAD_NORMAL);
delegate_->Resume();
return;
}
// Redirect is allowed.
redirect_state_ = RedirectState::kRedirectAttempted;
delegate_->Resume();
StartRedirectTimeoutTimer();
}
void SubresourceRedirectURLLoaderThrottle::WillRedirectRequest(
net::RedirectInfo* redirect_info,
const network::mojom::URLResponseHead& response_head,
bool* defer,
std::vector<std::string>* to_be_removed_request_headers,
net::HttpRequestHeaders* modified_request_headers,
net::HttpRequestHeaders* modified_cors_exempt_request_headers) {
// Check if the redirect is in some terminal state.
DCHECK((redirect_state_ == RedirectState::kNone) ||
(redirect_state_ == RedirectState::kRedirectAttempted) ||
(redirect_state_ == RedirectState::kRedirectNotAllowedByDecider) ||
redirect_state_ == RedirectState::kRedirectFailed);
if (redirect_state_ == RedirectState::kRedirectAttempted &&
redirect_timeout_timer_) {
redirect_timeout_timer_->Start(
FROM_HERE, GetCompressionRedirectTimeout(),
base::BindOnce(&SubresourceRedirectURLLoaderThrottle::OnRedirectTimeout,
base::Unretained(this)));
}
UMA_HISTOGRAM_ENUMERATION(
"SubresourceRedirect.CompressionAttempt.ResponseCode",
static_cast<net::HttpStatusCode>(response_head.headers->response_code()),
net::HTTP_VERSION_NOT_SUPPORTED);
}
void SubresourceRedirectURLLoaderThrottle::BeforeWillProcessResponse(
const GURL& response_url,
const network::mojom::URLResponseHead& response_head,
bool* defer) {
// Check if the redirect is in some terminal state.
DCHECK((redirect_state_ == RedirectState::kNone) ||
(redirect_state_ == RedirectState::kRedirectAttempted) ||
(redirect_state_ == RedirectState::kRedirectNotAllowedByDecider) ||
redirect_state_ == RedirectState::kRedirectFailed);
if (redirect_state_ != RedirectState::kRedirectAttempted)
return;
DCHECK(ShouldCompressRedirectSubresource());
// If response was not from the compression server, don't restart it.
if (!response_url.is_valid())
return;
// Log all response codes, from the compression server.
UMA_HISTOGRAM_ENUMERATION(
"SubresourceRedirect.CompressionAttempt.ResponseCode",
static_cast<net::HttpStatusCode>(response_head.headers->response_code()),
net::HTTP_VERSION_NOT_SUPPORTED);
redirect_timeout_timer_.reset();
// Do nothing with 2XX responses, as these requests were handled
// correctly by the compression server.
if ((response_head.headers->response_code() >= 200 &&
response_head.headers->response_code() <= 299) ||
response_head.headers->response_code() == 304) {
return;
}
redirect_result_ = RedirectResult::kIneligibleRedirectFailed;
// 503 response code indicates loadshed from the compression server. Notify
// the browser process which will bypass subresource redirect for subsequent
// page loads. Retry-After response header may mention the bypass duration,
// otherwise the browser will choose a random duration.
if (response_head.headers->response_code() == 503) {
std::string retry_after_string;
base::TimeDelta retry_after;
if (response_head.headers->EnumerateHeader(nullptr, "Retry-After",
&retry_after_string)) {
net::HttpUtil::ParseRetryAfterHeader(retry_after_string,
base::Time::Now(), &retry_after);
}
if (auto* public_resource_decider_agent =
GetPublicResourceDeciderAgent(render_frame_id_)) {
public_resource_decider_agent->NotifyCompressedResourceFetchFailed(
retry_after);
}
}
// Non 2XX responses from the compression server need to have unaltered
// requests sent to the original resource.
redirect_state_ = RedirectState::kRedirectFailed;
delegate_->RestartWithURLResetAndFlags(net::LOAD_NORMAL);
}
void SubresourceRedirectURLLoaderThrottle::WillProcessResponse(
const GURL& response_url,
network::mojom::URLResponseHead* response_head,
bool* defer) {
// Check if the redirect is in some terminal state.
DCHECK((redirect_state_ == RedirectState::kNone) ||
(redirect_state_ == RedirectState::kRedirectAttempted) ||
(redirect_state_ == RedirectState::kRedirectNotAllowedByDecider) ||
redirect_state_ == RedirectState::kRedirectFailed);
// If response was not from the compression server, don't record any
// metrics.
if (!response_url.is_valid())
return;
if (response_head->was_fetched_via_cache)
return;
int64_t content_length = response_head->headers->GetContentLength();
if (content_length < 0)
return;
if (auto* public_resource_decider_agent =
GetPublicResourceDeciderAgent(render_frame_id_)) {
public_resource_decider_agent->RecordMetricsOnLoadFinished(
response_url, content_length, redirect_result_);
}
if (redirect_state_ != RedirectState::kRedirectAttempted)
return;
DCHECK(ShouldCompressRedirectSubresource());
// Record that the server responded.
UMA_HISTOGRAM_BOOLEAN(
"SubresourceRedirect.CompressionAttempt.ServerResponded", true);
// If compression was unsuccessful don't try and record compression percent.
if (response_head->headers->response_code() != 200)
return;
float ofcl =
static_cast<float>(data_reduction_proxy::GetDataReductionProxyOFCL(
response_head->headers.get()));
// If |ofcl| is missing don't compute compression percent.
if (ofcl <= 0.0)
return;
UMA_HISTOGRAM_PERCENTAGE(
"SubresourceRedirect.DidCompress.CompressionPercent",
static_cast<int>(100 - ((content_length / ofcl) * 100)));
UMA_HISTOGRAM_COUNTS_1M("SubresourceRedirect.DidCompress.BytesSaved",
static_cast<int>(ofcl - content_length));
}
void SubresourceRedirectURLLoaderThrottle::WillOnCompleteWithError(
const network::URLLoaderCompletionStatus& status,
bool* defer) {
if (redirect_state_ != RedirectState::kRedirectAttempted)
return;
DCHECK(ShouldCompressRedirectSubresource());
redirect_result_ = RedirectResult::kIneligibleRedirectFailed;
// If the server fails, restart the request to the original resource, and
// record it.
redirect_state_ = RedirectState::kRedirectFailed;
redirect_timeout_timer_.reset();
delegate_->RestartWithURLResetAndFlags(net::LOAD_NORMAL);
UMA_HISTOGRAM_BOOLEAN(
"SubresourceRedirect.CompressionAttempt.ServerResponded", false);
}
void SubresourceRedirectURLLoaderThrottle::StartRedirectTimeoutTimer() {
DCHECK(!redirect_timeout_timer_);
redirect_timeout_timer_ = std::make_unique<base::OneShotTimer>();
redirect_timeout_timer_->Start(
FROM_HERE, GetCompressionRedirectTimeout(),
base::BindOnce(&SubresourceRedirectURLLoaderThrottle::OnRedirectTimeout,
base::Unretained(this)));
}
void SubresourceRedirectURLLoaderThrottle::OnRedirectTimeout() {
DCHECK_EQ(RedirectState::kRedirectAttempted, redirect_state_);
redirect_state_ = RedirectState::kRedirectFailed;
delegate_->RestartWithURLResetAndFlagsNow(net::LOAD_NORMAL);
if (auto* public_resource_decider_agent =
GetPublicResourceDeciderAgent(render_frame_id_)) {
public_resource_decider_agent->NotifyCompressedResourceFetchFailed(
base::TimeDelta());
}
UMA_HISTOGRAM_BOOLEAN("SubresourceRedirect.CompressionFetchTimeout", true);
}
void SubresourceRedirectURLLoaderThrottle::DetachFromCurrentSequence() {}
} // namespace subresource_redirect