// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/string_number_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "components/prefs/pref_service.h"
#import "components/security_interstitials/core/https_only_mode_metrics.h"
#import "ios/chrome/browser/prerender/model/prerender_service.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/components/security_interstitials/https_only_mode/feature.h"
#import "ios/components/security_interstitials/https_only_mode/https_only_mode_blocking_page.h"
#import "ios/components/security_interstitials/https_only_mode/https_only_mode_container.h"
#import "ios/components/security_interstitials/https_only_mode/https_only_mode_controller_client.h"
#import "ios/components/security_interstitials/https_only_mode/https_only_mode_error.h"
#import "ios/components/security_interstitials/https_only_mode/https_upgrade_service.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "net/base/apple/url_conversions.h"
#import "net/base/url_util.h"
#import "url/url_constants.h"
using security_interstitials::https_only_mode::Event;
namespace {
void RecordUMA(Event event) {
base::UmaHistogramEnumeration(
security_interstitials::https_only_mode::kEventHistogram, event);
}
} // namespace
HttpsOnlyModeUpgradeTabHelper::~HttpsOnlyModeUpgradeTabHelper() = default;
void HttpsOnlyModeUpgradeTabHelper::WebStateDestroyed(
web::WebState* web_state) {
web_state->RemoveObserver(this);
}
void HttpsOnlyModeUpgradeTabHelper::WebStateDestroyed() {}
bool HttpsOnlyModeUpgradeTabHelper::IsTimerRunningForTesting() const {
return timer_.IsRunning();
}
void HttpsOnlyModeUpgradeTabHelper::ClearAllowlistForTesting() {
service_->ClearAllowlist(base::Time(), base::Time::Max());
}
bool HttpsOnlyModeUpgradeTabHelper::IsHttpAllowedForUrl(const GURL& url) const {
return service_->IsHttpAllowedForHost(url.host());
}
HttpsOnlyModeUpgradeTabHelper::HttpsOnlyModeUpgradeTabHelper(
web::WebState* web_state,
PrefService* prefs,
PrerenderService* prerender_service,
HttpsUpgradeService* service)
: web::WebStatePolicyDecider(web_state),
prefs_(prefs),
prerender_service_(prerender_service),
service_(service) {
web_state->AddObserver(this);
}
void HttpsOnlyModeUpgradeTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (navigation_context->IsSameDocument()) {
return;
}
if (state_ == State::kUpgraded) {
DCHECK(!timer_.IsRunning());
// `timer_` is deleted when the tab helper is deleted, so it's safe to use
// Unretained here.
timer_.Start(
FROM_HERE, service_->GetFallbackDelay(),
base::BindOnce(&HttpsOnlyModeUpgradeTabHelper::OnHttpsLoadTimeout,
base::Unretained(this), web_state->GetWeakPtr()));
return;
}
if (state_ == State::kNone) {
// Store navigation parameters on initial navigation.
navigation_transition_type_ = navigation_context->GetPageTransition();
navigation_is_renderer_initiated_ =
navigation_context->IsRendererInitiated();
navigation_is_post_ = navigation_context->IsPost();
}
}
void HttpsOnlyModeUpgradeTabHelper::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (navigation_context->IsSameDocument()) {
return;
}
navigation_is_post_ = false;
if (state_ == State::kNone) {
return;
}
if (state_ == State::kStoppedToUpgrade) {
state_ = State::kUpgraded;
// Start an upgraded navigation.
RecordUMA(Event::kUpgradeAttempted);
web::NavigationManager::WebLoadParams params(upgraded_https_url_);
params.transition_type = navigation_transition_type_;
params.is_renderer_initiated = navigation_is_renderer_initiated_;
params.referrer = referrer_;
params.https_upgrade_type = web::HttpsUpgradeType::kHttpsOnlyMode;
web_state->GetNavigationManager()->LoadURLWithParams(params);
return;
}
if (state_ == State::kStoppedWithTimeout ||
state_ == State::kStoppedToFallback) {
DCHECK(!timer_.IsRunning());
RecordUMA(state_ == State::kStoppedWithTimeout ? Event::kUpgradeTimedOut
: Event::kUpgradeFailed);
FallbackToHttp();
return;
}
DCHECK(state_ == State::kUpgraded || state_ == State::kDone);
// The upgrade either failed or succeeded. In both cases, stop the timer.
timer_.Stop();
if (navigation_context->GetFailedHttpsUpgradeType() ==
web::HttpsUpgradeType::kHttpsOnlyMode) {
RecordUMA(Event::kUpgradeFailed);
FallbackToHttp();
return;
}
if (state_ == State::kDone &&
(navigation_context->GetUrl().SchemeIs(url::kHttpsScheme) ||
service_->IsFakeHTTPSForTesting(navigation_context->GetUrl()))) {
RecordUMA(Event::kUpgradeSucceeded);
}
state_ = State::kNone;
}
void HttpsOnlyModeUpgradeTabHelper::StopToUpgrade(
const GURL& url,
const web::Referrer& referrer,
base::OnceCallback<void(web::WebStatePolicyDecider::PolicyDecision)>
callback) {
state_ = State::kStoppedToUpgrade;
// Copy navigation parameters, then cancel the current navigation.
http_url_ = url;
referrer_ = referrer;
upgraded_https_url_ = service_->GetUpgradedHttpsUrl(url);
DCHECK(upgraded_https_url_.is_valid());
std::move(callback).Run(web::WebStatePolicyDecider::PolicyDecision::Cancel());
}
void HttpsOnlyModeUpgradeTabHelper::FallbackToHttp() {
DCHECK(state_ == State::kUpgraded || state_ == State::kStoppedWithTimeout ||
state_ == State::kStoppedToFallback);
DCHECK(!timer_.IsRunning());
state_ = State::kFallbackStarted;
// Start a new navigation to the original HTTP page. We'll then show an
// interstitial for this navigation in ShouldAllowRequest().
web::NavigationManager::WebLoadParams params(http_url_);
params.transition_type = navigation_transition_type_;
params.is_renderer_initiated = navigation_is_renderer_initiated_;
params.referrer = referrer_;
// Even though this is an HTTP navigation, mark it as "upgraded" so that we
// don't attempt to upgrade it again.
params.https_upgrade_type = web::HttpsUpgradeType::kHttpsOnlyMode;
// Post a task to navigate to the fallback URL. We don't want to navigate
// synchronously from a DidNavigationFinish() call.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<web::WebState> web_state,
const web::NavigationManager::WebLoadParams& params) {
if (web_state) {
web_state->GetNavigationManager()->LoadURLWithParams(params);
}
},
web_state()->GetWeakPtr(), std::move(params)));
}
void HttpsOnlyModeUpgradeTabHelper::ResetState() {
state_ = State::kNone;
timer_.Stop();
}
void HttpsOnlyModeUpgradeTabHelper::OnHttpsLoadTimeout(
base::WeakPtr<web::WebState> weak_web_state) {
DCHECK(state_ == State::kUpgraded);
state_ = State::kStoppedWithTimeout;
if (weak_web_state) {
weak_web_state->Stop();
}
}
void HttpsOnlyModeUpgradeTabHelper::ShouldAllowResponse(
NSURLResponse* response,
WebStatePolicyDecider::ResponseInfo response_info,
base::OnceCallback<void(web::WebStatePolicyDecider::PolicyDecision)>
callback) {
GURL url = net::GURLWithNSURL(response.URL);
// Ignore subframe navigations and schemes that we don't care about.
if (!response_info.for_main_frame ||
!(url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kHttpsScheme))) {
ResetState();
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
// If the URL is in the allowlist, don't upgrade.
if (IsHttpAllowedForUrl(url)) {
// The URL might have been allowlisted in another tab while we are loading
// this tab, so we can't make any assumptions about the state. Simply clear
// it.
ResetState();
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
// If already HTTPS (real or faux), simply allow the response.
if (url.SchemeIs(url::kHttpsScheme) || service_->IsFakeHTTPSForTesting(url)) {
timer_.Stop();
if (state_ != State::kNone) {
// Only call it done if the navigation was originally upgraded.
state_ = State::kDone;
}
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
if (state_ == State::kFallbackStarted) {
DCHECK(!timer_.IsRunning());
state_ = State::kDone;
// If HTTPS-First Mode is enabled, show the interstitial.
if (prefs_ && prefs_->GetBoolean(prefs::kHttpsOnlyModeEnabled)) {
HttpsOnlyModeContainer* container =
HttpsOnlyModeContainer::FromWebState(web_state());
container->SetHttpUrl(http_url_);
std::move(callback).Run(CreateHttpsOnlyModeErrorDecision());
return;
}
// Otherwise, this is a failed HTTPS-Upgrade. Allow the response.
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
web::NavigationItem* item_pending =
web_state()->GetNavigationManager()->GetPendingItem();
if (!item_pending) {
ResetState();
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
// Upgrade to HTTPS if the navigation wasn't upgraded before. Ignore POST
// navigations.
if (item_pending->GetHttpsUpgradeType() == web::HttpsUpgradeType::kNone &&
!navigation_is_post_) {
if ((!base::FeatureList::IsEnabled(
security_interstitials::features::kHttpsUpgrades) &&
!(prefs_ && prefs_->GetBoolean(prefs::kHttpsOnlyModeEnabled))) ||
service_->IsLocalhost(url)) {
// Don't upgrade if the feature is disabled or the URL is localhost.
// See ShouldCreateLoader() function in
// https_only_mode_upgrade_interceptor.cc for the desktop/Android
// implementation.
ResetState();
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
// If the tab is being prerendered, cancel the HTTP response.
if (prerender_service_ &&
prerender_service_->IsWebStatePrerendered(web_state())) {
RecordUMA(Event::kPrerenderCancelled);
ResetState();
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Cancel());
prerender_service_->CancelPrerender();
// IMPORTANT: CancelPrerender() destroys the web state. Do not access
// it after here.
return;
}
StopToUpgrade(url, item_pending->GetReferrer(), std::move(callback));
return;
}
// Omnibox upgrade failures are handled in TypedNavigationUpgradeTabHelper.
// Ignore them here.
if (item_pending->GetHttpsUpgradeType() !=
web::HttpsUpgradeType::kHttpsOnlyMode) {
DCHECK(state_ == State::kNone);
std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
return;
}
if (state_ == State::kNone && !navigation_is_post_) {
// If the pending item was a failed upgrade but the upgrade bit wasn't set,
// this is likely an interstitial reload.
timer_.Stop();
StopToUpgrade(url, item_pending->GetReferrer(), std::move(callback));
return;
}
// The navigation was already upgraded but landed on an HTTP URL, possibly
// through redirects (e.g. upgraded HTTPS -> HTTP). In this case, show the
// interstitial.
// Note that this doesn't handle HTTP URLs in the middle of redirects such as
// HTTPS -> HTTP -> HTTPS. The alternative is to do this check in
// ShouldAllowRequest(), but we don't have enough information there to ensure
// whether the HTTP URL is part of the redirect chain or a completely new
// navigation.
// This is a divergence from the desktop implementation of this feature which
// relies on a redirect loop triggering a net error.
DCHECK(state_ == State::kUpgraded || state_ == State::kNone);
timer_.Stop();
state_ = State::kDone;
RecordUMA(Event::kUpgradeFailed);
// If HTTPS-First Mode is enabled, show the interstitial.
if (prefs_ && prefs_->GetBoolean(prefs::kHttpsOnlyModeEnabled)) {
HttpsOnlyModeContainer* container =
HttpsOnlyModeContainer::FromWebState(web_state());
container->SetHttpUrl(url);
std::move(callback).Run(CreateHttpsOnlyModeErrorDecision());
return;
}
// Otherwise, this is a failed HTTPS-Upgrade. Allow the response.
std::move(callback).Run(web::WebStatePolicyDecider::PolicyDecision::Allow());
}
WEB_STATE_USER_DATA_KEY_IMPL(HttpsOnlyModeUpgradeTabHelper)