| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/navigation_interception/intercept_navigation_delegate.h" |
| |
| #include <memory> |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_string.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/escape.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/page_visibility_state.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "net/http/http_status_code.h" |
| #include "net/http/http_util.h" |
| #include "net/url_request/redirect_info.h" |
| #include "net/url_request/redirect_util.h" |
| #include "services/network/public/cpp/parsed_headers.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/single_request_url_loader_factory.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "url/android/gurl_android.h" |
| #include "url/gurl.h" |
| |
| // Must come after all headers that specialize FromJniType() / ToJniType(). |
| #include "components/navigation_interception/jni_headers/InterceptNavigationDelegate_jni.h" |
| |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::ScopedJavaLocalRef; |
| using content::BrowserThread; |
| using content::RenderViewHost; |
| using content::WebContents; |
| using ui::PageTransition; |
| |
| namespace navigation_interception { |
| |
| namespace { |
| |
| const void* const kInterceptNavigationDelegateUserDataKey = |
| &kInterceptNavigationDelegateUserDataKey; |
| |
| void AllowNavigationToProceed( |
| content::NavigationHandle* navigation_handle, |
| bool should_run_async, |
| InterceptNavigationThrottle::ResultCallback result_callback) { |
| std::move(result_callback).Run(false); |
| } |
| |
| class RedirectURLLoader : public network::mojom::URLLoader { |
| public: |
| RedirectURLLoader(const network::ResourceRequest& resource_request, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) |
| : client_(std::move(client)), request_(resource_request) {} |
| |
| void DoRedirect(std::unique_ptr<GURL> url) { |
| net::HttpStatusCode response_code = net::HTTP_TEMPORARY_REDIRECT; |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->encoded_data_length = 0; |
| response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>( |
| net::HttpUtil::AssembleRawHeaders("HTTP/1.1 307 Temporary Redirect")); |
| |
| // Avoid a round-trip to the network service by pre-parsing headers. |
| // This doesn't violate: `docs/security/rule-of-2.md`, because the input is |
| // trusted, before appending the Location: <url> header. |
| response_head->parsed_headers = |
| network::PopulateParsedHeaders(response_head->headers.get(), *url); |
| |
| response_head->headers->AddHeader("Location", url->spec()); |
| |
| auto first_party_url_policy = |
| request_.update_first_party_url_on_redirect |
| ? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT |
| : net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL; |
| |
| client_->OnReceiveRedirect( |
| net::RedirectInfo::ComputeRedirectInfo( |
| request_.method, request_.url, request_.site_for_cookies, |
| first_party_url_policy, request_.referrer_policy, |
| request_.referrer.spec(), response_code, *url, std::nullopt, |
| /*insecure_scheme_was_upgraded=*/false, |
| /*copy_fragment=*/false), |
| std::move(response_head)); |
| } |
| |
| void OnNonRedirectAsyncAction() { |
| client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_ABORTED)); |
| } |
| |
| RedirectURLLoader(const RedirectURLLoader&) = delete; |
| RedirectURLLoader& operator=(const RedirectURLLoader&) = delete; |
| |
| ~RedirectURLLoader() override = default; |
| |
| private: |
| // network::mojom::URLLoader overrides: |
| void FollowRedirect( |
| const std::vector<std::string>& removed_headers, |
| const net::HttpRequestHeaders& modified_headers, |
| const net::HttpRequestHeaders& modified_cors_exempt_headers, |
| const std::optional<GURL>& new_url) override { |
| NOTREACHED(); |
| } |
| void SetPriority(net::RequestPriority priority, |
| int intra_priority_value) override {} |
| |
| mojo::Remote<network::mojom::URLLoaderClient> client_; |
| network::ResourceRequest request_; |
| }; |
| |
| } // namespace |
| |
| // static |
| void InterceptNavigationDelegate::Associate( |
| WebContents* web_contents, |
| std::unique_ptr<InterceptNavigationDelegate> delegate) { |
| if (!delegate) { |
| web_contents->RemoveUserData(kInterceptNavigationDelegateUserDataKey); |
| } else { |
| web_contents->SetUserData(kInterceptNavigationDelegateUserDataKey, |
| std::move(delegate)); |
| } |
| } |
| |
| // static |
| InterceptNavigationDelegate* InterceptNavigationDelegate::Get( |
| WebContents* web_contents) { |
| return static_cast<InterceptNavigationDelegate*>( |
| web_contents->GetUserData(kInterceptNavigationDelegateUserDataKey)); |
| } |
| |
| // static |
| void InterceptNavigationDelegate::MaybeCreateAndAdd( |
| content::NavigationThrottleRegistry& registry, |
| navigation_interception::SynchronyMode mode) { |
| // Navigations in a subframe or non-primary frame tree should not be |
| // intercepted. As examples of a non-primary frame tree, a navigation |
| // occurring in a Portal element or an unactivated prerendering page should |
| // not launch an app. |
| // TODO(bokan): This is a bit of a stopgap approach since we won't run |
| // throttles again when the prerender is activated which means links that are |
| // prerendered will avoid launching an app intent that a regular navigation |
| // would have. Longer term we'll want prerender activation to check for app |
| // intents, or have this throttle cancel the prerender if an intent would |
| // have been launched (without launching the intent). It's also not clear |
| // what the right behavior for <portal> elements is. |
| // https://crbug.com/1227659. |
| if (!registry.GetNavigationHandle().IsInPrimaryMainFrame()) { |
| return; |
| } |
| |
| InterceptNavigationDelegate* intercept_navigation_delegate = |
| InterceptNavigationDelegate::Get( |
| registry.GetNavigationHandle().GetWebContents()); |
| |
| if (!intercept_navigation_delegate) { |
| registry.AddThrottle(std::make_unique<InterceptNavigationThrottle>( |
| registry, base::BindRepeating(&AllowNavigationToProceed), mode, |
| base::DoNothing())); |
| } else { |
| registry.AddThrottle(std::make_unique<InterceptNavigationThrottle>( |
| registry, |
| base::BindRepeating(&InterceptNavigationDelegate::ShouldIgnoreNavigation, |
| base::Unretained(intercept_navigation_delegate)), |
| mode, |
| base::BindRepeating( |
| &InterceptNavigationDelegate::RequestFinishPendingShouldIgnoreCheck, |
| base::Unretained(intercept_navigation_delegate)))); |
| } |
| } |
| |
| InterceptNavigationDelegate::InterceptNavigationDelegate( |
| JNIEnv* env, |
| const jni_zero::JavaRef<jobject>& jdelegate, |
| bool escape_external_handler_value) |
| : weak_jdelegate_(env, jdelegate), |
| escape_external_handler_value_(escape_external_handler_value) {} |
| |
| InterceptNavigationDelegate::~InterceptNavigationDelegate() = default; |
| |
| void InterceptNavigationDelegate::ShouldIgnoreNavigation( |
| content::NavigationHandle* navigation_handle, |
| bool should_run_async, |
| InterceptNavigationThrottle::ResultCallback result_callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // Avoid having two outstanding checks at once for simplicity. |
| if (should_ignore_result_callback_) { |
| std::move(result_callback).Run(false); |
| return; |
| } |
| GURL escaped_url = escape_external_handler_value_ |
| ? GURL(base::EscapeExternalHandlerValue( |
| navigation_handle->GetURL().spec())) |
| : navigation_handle->GetURL(); |
| |
| if (!escaped_url.is_valid()) { |
| std::move(result_callback).Run(false); |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> jdelegate = weak_jdelegate_.get(env); |
| |
| if (jdelegate.is_null()) { |
| std::move(result_callback).Run(false); |
| return; |
| } |
| |
| bool hidden_cross_frame = false; |
| // Only main frame navigations use this path, so we only need to check if the |
| // navigation is cross-frame to the main frame. |
| if (navigation_handle->GetInitiatorFrameToken() && |
| navigation_handle->GetInitiatorFrameToken() != |
| navigation_handle->GetWebContents() |
| ->GetPrimaryMainFrame() |
| ->GetFrameToken()) { |
| content::RenderFrameHost* initiator_frame_host = |
| content::RenderFrameHost::FromFrameToken( |
| content::GlobalRenderFrameHostToken( |
| navigation_handle->GetInitiatorProcessId(), |
| navigation_handle->GetInitiatorFrameToken().value())); |
| // If the initiator is gone treat it as not visible. |
| hidden_cross_frame = |
| !initiator_frame_host || initiator_frame_host->GetVisibilityState() != |
| content::PageVisibilityState::kVisible; |
| } |
| |
| // We don't care which sandbox flags are present, only that any sandbox flags |
| // are present, as we don't support persisting sandbox flags through fallback |
| // URL navigation. |
| bool is_sandboxed = navigation_handle->SandboxFlagsInherited() != |
| network::mojom::WebSandboxFlags::kNone || |
| navigation_handle->SandboxFlagsInitiator() != |
| network::mojom::WebSandboxFlags::kNone; |
| |
| should_ignore_result_callback_ = std::move(result_callback); |
| Java_InterceptNavigationDelegate_callShouldIgnoreNavigation( |
| env, jdelegate, navigation_handle->GetJavaNavigationHandle(), |
| url::GURLAndroid::FromNativeGURL(env, escaped_url), hidden_cross_frame, |
| is_sandboxed, should_run_async); |
| } |
| |
| void InterceptNavigationDelegate::OnShouldIgnoreNavigationResult( |
| bool should_ignore) { |
| std::move(should_ignore_result_callback_).Run(should_ignore); |
| } |
| |
| void InterceptNavigationDelegate::RequestFinishPendingShouldIgnoreCheck() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> jdelegate = weak_jdelegate_.get(env); |
| |
| if (jdelegate.is_null()) { |
| OnShouldIgnoreNavigationResult(false); |
| return; |
| } |
| Java_InterceptNavigationDelegate_requestFinishPendingShouldIgnoreCheck( |
| env, jdelegate); |
| } |
| |
| void InterceptNavigationDelegate::HandleSubframeExternalProtocol( |
| const GURL& url, |
| ui::PageTransition page_transition, |
| bool has_user_gesture, |
| const std::optional<url::Origin>& initiating_origin, |
| mojo::PendingRemote<network::mojom::URLLoaderFactory>* out_factory) { |
| // If there's a pending async subframe action, don't consider external |
| // navigation for the current navigation. |
| if (subframe_redirect_url_ || url_loader_) { |
| return; |
| } |
| |
| GURL escaped_url = escape_external_handler_value_ |
| ? GURL(base::EscapeExternalHandlerValue(url.spec())) |
| : url; |
| if (!escaped_url.is_valid()) { |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> jdelegate = weak_jdelegate_.get(env); |
| |
| if (jdelegate.is_null()) { |
| return; |
| } |
| ScopedJavaLocalRef<jobject> j_gurl = |
| Java_InterceptNavigationDelegate_handleSubframeExternalProtocol( |
| env, jdelegate, url::GURLAndroid::FromNativeGURL(env, escaped_url), |
| page_transition, has_user_gesture, |
| initiating_origin ? initiating_origin->ToJavaObject(env) : nullptr); |
| if (j_gurl.is_null()) { |
| return; |
| } |
| subframe_redirect_url_ = |
| std::make_unique<GURL>(url::GURLAndroid::ToNativeGURL(env, j_gurl)); |
| |
| mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver = |
| out_factory->InitWithNewPipeAndPassReceiver(); |
| scoped_refptr<network::SharedURLLoaderFactory> loader_factory = |
| base::MakeRefCounted<network::SingleRequestURLLoaderFactory>( |
| base::BindOnce(&InterceptNavigationDelegate::LoaderCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| loader_factory->Clone(std::move(receiver)); |
| } |
| |
| void InterceptNavigationDelegate::LoaderCallback( |
| const network::ResourceRequest& resource_request, |
| mojo::PendingReceiver<network::mojom::URLLoader> pending_receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> pending_client) { |
| url_loader_ = mojo::MakeSelfOwnedReceiver( |
| std::make_unique<RedirectURLLoader>(resource_request, |
| std::move(pending_client)), |
| std::move(pending_receiver)); |
| MaybeHandleSubframeAction(); |
| } |
| |
| void InterceptNavigationDelegate::MaybeHandleSubframeAction() { |
| // An empty subframe_redirect_url_ implies a pending async action. |
| if (!url_loader_ || |
| (subframe_redirect_url_ && subframe_redirect_url_->is_empty())) { |
| return; |
| } |
| RedirectURLLoader* loader = |
| static_cast<RedirectURLLoader*>(url_loader_->impl()); |
| if (!subframe_redirect_url_) { |
| loader->OnNonRedirectAsyncAction(); |
| } else { |
| loader->DoRedirect(std::move(subframe_redirect_url_)); |
| } |
| url_loader_.reset(); |
| } |
| |
| void InterceptNavigationDelegate::OnResourceRequestWithGesture() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> jdelegate = weak_jdelegate_.get(env); |
| if (jdelegate.is_null()) { |
| return; |
| } |
| Java_InterceptNavigationDelegate_onResourceRequestWithGesture(env, jdelegate); |
| } |
| |
| void InterceptNavigationDelegate::OnSubframeAsyncActionTaken( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& j_gurl) { |
| // subframe_redirect_url_ no longer empty indicates the async action has been |
| // taken. |
| subframe_redirect_url_ = |
| j_gurl.is_null() |
| ? nullptr |
| : std::make_unique<GURL>(url::GURLAndroid::ToNativeGURL(env, j_gurl)); |
| MaybeHandleSubframeAction(); |
| } |
| |
| static void JNI_InterceptNavigationDelegate_OnShouldIgnoreNavigationResult( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& jweb_contents, |
| jboolean should_ignore) { |
| content::WebContents* web_contents = |
| content::WebContents::FromJavaWebContents(jweb_contents); |
| if (!web_contents) { |
| return; |
| } |
| navigation_interception::InterceptNavigationDelegate* delegate = |
| navigation_interception::InterceptNavigationDelegate::Get(web_contents); |
| CHECK(delegate); |
| delegate->OnShouldIgnoreNavigationResult(should_ignore); |
| } |
| |
| } // namespace navigation_interception |