| // Copyright 2020 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 "third_party/blink/renderer/core/html/link_web_bundle.h" |
| |
| #include "base/unguessable_token.h" |
| #include "services/network/public/mojom/web_bundle_handle.mojom-blink.h" |
| #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h" |
| #include "third_party/blink/public/mojom/web_feature/web_feature.mojom-blink.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/public/web/web_local_frame_client.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_client.h" |
| #include "third_party/blink/renderer/core/html/cross_origin_attribute.h" |
| #include "third_party/blink/renderer/core/html/html_link_element.h" |
| #include "third_party/blink/renderer/core/inspector/console_message.h" |
| #include "third_party/blink/renderer/core/loader/threadable_loader.h" |
| #include "third_party/blink/renderer/core/loader/threadable_loader_client.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| #include "third_party/blink/renderer/platform/loader/cors/cors.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/bytes_consumer.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_request.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| |
| namespace blink { |
| |
| class WebBundleLoader : public GarbageCollected<WebBundleLoader>, |
| public ThreadableLoaderClient, |
| public network::mojom::WebBundleHandle { |
| public: |
| WebBundleLoader(LinkWebBundle& link_web_bundle, |
| Document& document, |
| const KURL& url, |
| CrossOriginAttributeValue cross_origin_attribute_value) |
| : link_web_bundle_(&link_web_bundle), |
| url_(url), |
| security_origin_(SecurityOrigin::Create(url)), |
| web_bundle_token_(base::UnguessableToken::Create()) { |
| ResourceRequest request(url); |
| request.SetUseStreamOnResponse(true); |
| // TODO(crbug.com/1082020): Revisit these once the fetch and process the |
| // linked resource algorithm [1] for <link rel=webbundle> is defined. |
| // [1] |
| // https://html.spec.whatwg.org/multipage/semantics.html#fetch-and-process-the-linked-resource |
| request.SetRequestContext( |
| mojom::blink::RequestContextType::SUBRESOURCE_WEBBUNDLE); |
| |
| // https://github.com/WICG/webpackage/blob/main/explainers/subresource-loading.md#requests-mode-and-credentials-mode |
| request.SetMode(network::mojom::blink::RequestMode::kCors); |
| switch (cross_origin_attribute_value) { |
| case kCrossOriginAttributeNotSet: |
| case kCrossOriginAttributeAnonymous: |
| request.SetCredentialsMode( |
| network::mojom::CredentialsMode::kSameOrigin); |
| break; |
| case kCrossOriginAttributeUseCredentials: |
| request.SetCredentialsMode(network::mojom::CredentialsMode::kInclude); |
| break; |
| } |
| request.SetRequestDestination( |
| network::mojom::RequestDestination::kWebBundle); |
| request.SetPriority(ResourceLoadPriority::kHigh); |
| |
| mojo::PendingRemote<network::mojom::WebBundleHandle> web_bundle_handle; |
| web_bundle_handles_.Add(this, |
| web_bundle_handle.InitWithNewPipeAndPassReceiver()); |
| request.SetWebBundleTokenParams(ResourceRequestHead::WebBundleTokenParams( |
| url_, web_bundle_token_, std::move(web_bundle_handle))); |
| |
| ExecutionContext* execution_context = document.GetExecutionContext(); |
| ResourceLoaderOptions resource_loader_options( |
| execution_context->GetCurrentWorld()); |
| resource_loader_options.data_buffering_policy = kDoNotBufferData; |
| |
| loader_ = MakeGarbageCollected<ThreadableLoader>(*execution_context, this, |
| resource_loader_options); |
| loader_->Start(std::move(request)); |
| } |
| |
| void Trace(Visitor* visitor) const override { |
| visitor->Trace(link_web_bundle_); |
| visitor->Trace(loader_); |
| } |
| |
| bool HasLoaded() const { return !failed_; } |
| |
| // ThreadableLoaderClient |
| void DidStartLoadingResponseBody(BytesConsumer& consumer) override { |
| // Drain |consumer| so that DidFinishLoading is surely called later. |
| consumer.DrainAsDataPipe(); |
| } |
| void DidFail(const ResourceError&) override { DidFailInternal(); } |
| void DidFailRedirectCheck() override { DidFailInternal(); } |
| |
| // network::mojom::WebBundleHandle |
| void Clone(mojo::PendingReceiver<network::mojom::WebBundleHandle> receiver) |
| override { |
| web_bundle_handles_.Add(this, std::move(receiver)); |
| } |
| void OnWebBundleError(network::mojom::WebBundleErrorType type, |
| const std::string& message) override { |
| link_web_bundle_->OnWebBundleError(url_.ElidedString() + ": " + |
| message.c_str()); |
| } |
| void OnWebBundleLoadFinished(bool success) override { |
| if (failed_) |
| return; |
| failed_ = !success; |
| link_web_bundle_->NotifyLoaded(); |
| } |
| |
| const KURL& url() const { return url_; } |
| scoped_refptr<SecurityOrigin> GetSecurityOrigin() const { |
| return security_origin_; |
| } |
| const base::UnguessableToken& WebBundleToken() const { |
| return web_bundle_token_; |
| } |
| |
| private: |
| void DidFailInternal() { |
| if (failed_) |
| return; |
| failed_ = true; |
| link_web_bundle_->NotifyLoaded(); |
| } |
| |
| Member<LinkWebBundle> link_web_bundle_; |
| Member<ThreadableLoader> loader_; |
| bool failed_ = false; |
| KURL url_; |
| scoped_refptr<SecurityOrigin> security_origin_; |
| base::UnguessableToken web_bundle_token_; |
| // we need ReceiverSet here because WebBundleHandle is cloned when |
| // ResourceRequest is copied. |
| mojo::ReceiverSet<network::mojom::WebBundleHandle> web_bundle_handles_; |
| }; |
| |
| // static |
| bool LinkWebBundle::IsFeatureEnabled(const ExecutionContext* context) { |
| return context && context->IsSecureContext() && |
| RuntimeEnabledFeatures::SubresourceWebBundlesEnabled(context); |
| } |
| |
| LinkWebBundle::LinkWebBundle(HTMLLinkElement* owner) : LinkResource(owner) { |
| UseCounter::Count(owner_->GetDocument().GetExecutionContext(), |
| WebFeature::kSubresourceWebBundles); |
| } |
| LinkWebBundle::~LinkWebBundle() = default; |
| |
| void LinkWebBundle::Trace(Visitor* visitor) const { |
| visitor->Trace(bundle_loader_); |
| LinkResource::Trace(visitor); |
| SubresourceWebBundle::Trace(visitor); |
| } |
| |
| void LinkWebBundle::NotifyLoaded() { |
| if (owner_) |
| owner_->ScheduleEvent(); |
| } |
| |
| void LinkWebBundle::OnWebBundleError(const String& message) const { |
| if (!owner_) |
| return; |
| ExecutionContext* context = owner_->GetDocument().GetExecutionContext(); |
| if (!context) |
| return; |
| context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kOther, |
| mojom::blink::ConsoleMessageLevel::kWarning, message)); |
| } |
| |
| void LinkWebBundle::Process() { |
| if (!owner_ || !owner_->GetDocument().GetFrame()) |
| return; |
| if (!owner_->ShouldLoadLink()) |
| return; |
| |
| ResourceFetcher* resource_fetcher = owner_->GetDocument().Fetcher(); |
| if (!resource_fetcher) |
| return; |
| |
| // We don't support crossorigin= attribute's dynamic change. It seems |
| // other types of link elements doesn't support that too. See |
| // HTMLlinkElement::ParseAttribute, which doesn't call Process() for |
| // crossorigin= attribute change. |
| if (!bundle_loader_ || bundle_loader_->url() != owner_->Href()) { |
| if (resource_fetcher->ShouldBeLoadedFromWebBundle(owner_->Href())) { |
| // This can happen when a requested bundle is a nested bundle. |
| // |
| // clang-format off |
| // Example: |
| // <link rel="webbundle" href=".../nested-main.wbn" resources=".../nested-sub.wbn"> |
| // <link rel="webbundle" href=".../nested-sub.wbn" resources="..."> |
| // clang-format on |
| if (bundle_loader_) { |
| resource_fetcher->RemoveSubresourceWebBundle(*this); |
| bundle_loader_ = nullptr; |
| } |
| NotifyLoaded(); |
| OnWebBundleError("A nested bundle is not supported: " + |
| owner_->Href().ElidedString()); |
| return; |
| } |
| bundle_loader_ = MakeGarbageCollected<WebBundleLoader>( |
| *this, owner_->GetDocument(), owner_->Href(), |
| GetCrossOriginAttributeValue( |
| owner_->FastGetAttribute(html_names::kCrossoriginAttr))); |
| } |
| |
| resource_fetcher->AddSubresourceWebBundle(*this); |
| } |
| |
| LinkResource::LinkResourceType LinkWebBundle::GetType() const { |
| return kOther; |
| } |
| |
| bool LinkWebBundle::HasLoaded() const { |
| return bundle_loader_ && bundle_loader_->HasLoaded(); |
| } |
| |
| void LinkWebBundle::OwnerRemoved() { |
| if (!owner_) |
| return; |
| ResourceFetcher* resource_fetcher = owner_->GetDocument().Fetcher(); |
| if (!resource_fetcher) |
| return; |
| resource_fetcher->RemoveSubresourceWebBundle(*this); |
| bundle_loader_ = nullptr; |
| } |
| |
| bool LinkWebBundle::CanHandleRequest(const KURL& url) const { |
| if (!url.IsValid()) |
| return false; |
| if (!ResourcesOrScopesMatch(url)) |
| return false; |
| if (url.Protocol() == "urn") |
| return true; |
| DCHECK(bundle_loader_); |
| if (!bundle_loader_->GetSecurityOrigin()->IsSameOriginWith( |
| SecurityOrigin::Create(url).get())) { |
| OnWebBundleError(url.ElidedString() + " cannot be loaded from WebBundle " + |
| bundle_loader_->url().ElidedString() + |
| ": bundled resource must be same origin with the bundle."); |
| return false; |
| } |
| |
| if (!url.GetString().StartsWith(bundle_loader_->url().BaseAsString())) { |
| OnWebBundleError( |
| url.ElidedString() + " cannot be loaded from WebBundle " + |
| bundle_loader_->url().ElidedString() + |
| ": bundled resource path must contain the bundle's path as a prefix."); |
| return false; |
| } |
| return true; |
| } |
| |
| bool LinkWebBundle::ResourcesOrScopesMatch(const KURL& url) const { |
| if (!owner_) |
| return false; |
| if (owner_->ValidResourceUrls().Contains(url)) |
| return true; |
| for (const auto& scope : owner_->ValidScopeUrls()) { |
| if (url.GetString().StartsWith(scope.GetString())) |
| return true; |
| } |
| return false; |
| } |
| |
| String LinkWebBundle::GetCacheIdentifier() const { |
| DCHECK(bundle_loader_); |
| return bundle_loader_->url().GetString(); |
| } |
| |
| const KURL& LinkWebBundle::GetBundleUrl() const { |
| DCHECK(bundle_loader_); |
| return bundle_loader_->url(); |
| } |
| |
| const base::UnguessableToken& LinkWebBundle::WebBundleToken() const { |
| DCHECK(bundle_loader_); |
| return bundle_loader_->WebBundleToken(); |
| } |
| |
| // static |
| KURL LinkWebBundle::ParseResourceUrl(const AtomicString& str) { |
| // The implementation is almost copy and paste from ParseExchangeURL() defined |
| // in services/data_decoder/web_bundle_parser.cc, replacing GURL with KURL. |
| |
| // TODO(hayato): Consider to support a relative URL. |
| KURL url(str); |
| if (!url.IsValid()) |
| return KURL(); |
| |
| // Exchange URL must not have a fragment or credentials. |
| if (url.HasFragmentIdentifier() || !url.User().IsEmpty() || |
| !url.Pass().IsEmpty()) |
| return KURL(); |
| |
| // For now, we allow only http:, https: and urn: schemes in Web Bundle URLs. |
| // TODO(crbug.com/966753): Revisit this once |
| // https://github.com/WICG/webpackage/issues/468 is resolved. |
| if (!url.ProtocolIsInHTTPFamily() && !url.ProtocolIs("urn")) |
| return KURL(); |
| |
| return url; |
| } |
| |
| } // namespace blink |