blob: 2524815ff9539b6569f6bdd2e9945ca73b43c347 [file] [log] [blame]
// 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