blob: 7bf72c0bf020f1e517174dbea44bf79ff9efcb5d [file] [log] [blame]
// Copyright 2022 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/worker/worker_global_scope.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/logging.h"
#include "base/message_loop/message_loop_current.h"
#include "base/threading/thread.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/loader/net_fetcher.h"
#include "cobalt/loader/origin.h"
#include "cobalt/script/environment_settings.h"
#include "cobalt/web/context.h"
#include "cobalt/web/dom_exception.h"
#include "cobalt/web/user_agent_platform_info.h"
#include "cobalt/web/window_or_worker_global_scope.h"
#include "cobalt/web/window_timers.h"
#include "cobalt/worker/service_worker_object.h"
#include "cobalt/worker/worker_consts.h"
#include "cobalt/worker/worker_location.h"
#include "cobalt/worker/worker_navigator.h"
#include "net/base/mime_util.h"
#include "starboard/atomic.h"
#include "url/gurl.h"
namespace cobalt {
namespace worker {
namespace {
class ScriptLoader : public base::MessageLoop::DestructionObserver {
public:
explicit ScriptLoader(web::Context* context) : context_(context) {
thread_.reset(new base::Thread("ImportScriptsLoader"));
thread_->Start();
// Register as a destruction observer to shut down the Loaders once all
// pending tasks have been executed and the message loop is about to be
// destroyed. This allows us to safely stop the thread, drain the task
// queue, then destroy the internal components before the message loop is
// reset. No posted tasks will be executed once the thread is stopped.
thread_->message_loop()->task_runner()->PostTask(
FROM_HERE, base::Bind(&base::MessageLoop::AddDestructionObserver,
base::Unretained(thread_->message_loop()),
base::Unretained(this)));
thread_->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](loader::FetcherFactory* fetcher_factory,
std::unique_ptr<loader::ScriptLoaderFactory>*
script_loader_factory_) {
TRACE_EVENT0("cobalt::worker",
"ScriptLoader::ScriptLoaderFactory Task");
script_loader_factory_->reset(new loader::ScriptLoaderFactory(
"ImportScriptsLoader", fetcher_factory));
},
context_->fetcher_factory(), &script_loader_factory_));
}
~ScriptLoader() {
if (thread_) {
// Stop the thread. This will cause the destruction observer to be
// notified.
thread_->Stop();
}
}
void WillDestroyCurrentMessageLoop() {
// Destroy members that were constructed in the worker thread.
errors_.clear();
errors_.shrink_to_fit();
contents_.clear();
contents_.shrink_to_fit();
loaders_.clear();
loaders_.shrink_to_fit();
script_loader_factory_.reset();
}
void Load(web::CspDelegate* csp_delegate, const loader::Origin& origin,
const std::vector<GURL>& resolved_urls) {
TRACE_EVENT0("cobalt::worker", "ScriptLoader::Load()");
number_of_loads_ = resolved_urls.size();
contents_.resize(resolved_urls.size());
loaders_.resize(resolved_urls.size());
errors_.resize(resolved_urls.size());
if (resolved_urls.empty()) return;
for (int i = 0; i < resolved_urls.size(); ++i) {
const GURL& url = resolved_urls[i];
thread_->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&ScriptLoader::LoaderTask, base::Unretained(this),
csp_delegate, &loaders_[i], origin, url, &contents_[i],
&errors_[i]));
}
load_finished_.Wait();
}
void LoaderTask(web::CspDelegate* csp_delegate,
std::unique_ptr<loader::Loader>* loader,
const loader::Origin& origin, const GURL& url,
std::unique_ptr<std::string>* content,
std::unique_ptr<std::string>* error) {
TRACE_EVENT0("cobalt::worker", "ScriptLoader::LoaderTask()");
csp::SecurityCallback csp_callback =
base::Bind(&web::CspDelegate::CanLoad, base::Unretained(csp_delegate),
web::CspDelegate::kWorker);
bool skip_fetch_intercept =
context_->GetWindowOrWorkerGlobalScope()->IsServiceWorker();
// If there is a request callback, call it to possibly retrieve previously
// requested content.
*loader = script_loader_factory_->CreateScriptLoader(
url, origin, csp_callback,
base::Bind(
[](std::unique_ptr<std::string>* output_content,
const loader::Origin& last_url_origin,
std::unique_ptr<std::string> content) {
*output_content = std::move(content);
},
content),
base::Bind(&ScriptLoader::UpdateOnResponseStarted,
base::Unretained(this), error),
base::Bind(&ScriptLoader::LoadingCompleteCallback,
base::Unretained(this), loader, error),
net::HttpRequestHeaders(), skip_fetch_intercept);
}
void LoadingCompleteCallback(std::unique_ptr<loader::Loader>* loader,
std::unique_ptr<std::string>* output_error,
const base::Optional<std::string>& error) {
TRACE_EVENT0("cobalt::worker", "ScriptLoader::LoadingCompleteCallback()");
if (error) {
output_error->reset(new std::string(std::move(error.value())));
}
if (!SbAtomicNoBarrier_Increment(&number_of_loads_, -1)) {
// Clear the loader factory after this callback
// completes.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(
[](base::WaitableEvent* load_finished_) {
load_finished_->Signal();
},
&load_finished_));
}
}
bool UpdateOnResponseStarted(
std::unique_ptr<std::string>* error, loader::Fetcher* fetcher,
const scoped_refptr<net::HttpResponseHeaders>& headers) {
std::string content_type;
bool mime_type_is_javascript = false;
if (headers->GetNormalizedHeader("Content-type", &content_type)) {
for (auto mime_type : WorkerConsts::kJavaScriptMimeTypes) {
if (net::MatchesMimeType(mime_type, content_type)) {
mime_type_is_javascript = true;
break;
}
}
}
if (content_type.empty()) {
error->reset(new std::string(
base::StringPrintf(WorkerConsts::kServiceWorkerRegisterNoMIMEError)));
} else if (!mime_type_is_javascript) {
error->reset(new std::string(
base::StringPrintf(WorkerConsts::kServiceWorkerRegisterBadMIMEError,
content_type.c_str())));
}
return true;
}
std::unique_ptr<std::string>& GetContents(int index) {
return contents_[index];
}
const std::unique_ptr<std::string>& GetError(int index) {
return errors_[index];
}
private:
web::Context* context_;
base::WaitableEvent load_finished_ = {
base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED};
std::unique_ptr<base::Thread> thread_;
std::unique_ptr<loader::ScriptLoaderFactory> script_loader_factory_;
volatile SbAtomic32 number_of_loads_;
std::vector<std::unique_ptr<std::string>> contents_;
std::vector<std::unique_ptr<std::string>> errors_;
std::vector<std::unique_ptr<loader::Loader>> loaders_;
base::Optional<std::string> error_;
};
} // namespace
WorkerGlobalScope::WorkerGlobalScope(
script::EnvironmentSettings* settings,
const web::WindowOrWorkerGlobalScope::Options& options)
: web::WindowOrWorkerGlobalScope(settings, options),
location_(new WorkerLocation(settings->creation_url())),
navigator_(new WorkerNavigator(settings)) {
set_navigator_base(navigator_);
}
bool WorkerGlobalScope::InitializePolicyContainerCallback(
loader::Fetcher* fetcher,
const scoped_refptr<net::HttpResponseHeaders>& headers) {
DCHECK(headers);
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#initialize-worker-policy-container
// 1. If workerGlobalScope's url is local but its scheme is not "blob":
// 1. Assert: workerGlobalScope's owner set's size is 1.
// 2. Set workerGlobalScope's policy container to a clone of
// workerGlobalScope's owner set[0]'s relevant settings object's policy
// container.
// 2. Otherwise, set workerGlobalScope's policy container to the result of
// creating a policy container from a fetch response given response and
// environment.
// Steps from create a policy container from a fetch response:
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#creating-a-policy-container-from-a-fetch-response
// 1. If response's URL's scheme is "blob", then return a clone of response's
// URL's blob URL entry's environment's policy container.
// 2. Let result be a new policy container.
// 3. Set result's CSP list to the result of parsing a response's Content
// Security Policies given response.
// 4. If environment is non-null, then set result's embedder policy to the
// result of obtaining an embedder policy given response and environment.
// Otherwise, set it to "unsafe-none".
// 5. Set result's referrer policy to the result of parsing the
// `Referrer-Policy` header given response. [REFERRERPOLICY]
// 6. Return result.
// Steps 3-6. Since csp_delegate doesn't fully mirror PolicyContainer, we
// don't create a new one here and return it. Instead we update the existing
// one for this worker and return true for success and false for failure.
csp::ResponseHeaders csp_headers(headers);
if (csp_delegate()->OnReceiveHeaders(csp_headers)) {
return true;
}
// Only NetFetchers are expected to call this, since only they have the
// response headers.
loader::NetFetcher* net_fetcher =
fetcher ? base::polymorphic_downcast<loader::NetFetcher*>(fetcher)
: nullptr;
net::URLFetcher* url_fetcher =
net_fetcher ? net_fetcher->url_fetcher() : nullptr;
const GURL& url = url_fetcher ? url_fetcher->GetURL()
: environment_settings()->creation_url();
LOG(INFO) << "Failure receiving Content Security Policy headers for URL: "
<< url << ".";
// Return true regardless of CSP headers being received to continue loading
// the response.
return true;
}
void WorkerGlobalScope::ImportScripts(const std::vector<std::string>& urls,
script::ExceptionState* exception_state) {
ImportScriptsInternal(urls, exception_state, URLLookupCallback(),
ResponseCallback());
}
bool WorkerGlobalScope::LoadImportsAndReturnIfUpdated(
const ScriptResourceMap& previous_resource_map,
ScriptResourceMap* new_resource_map) {
bool has_updated_resources = false;
// Steps from Algorithm for Update:
// https://www.w3.org/TR/2022/CRD-service-workers-20220712/#update-algorithm
// 8.21.1. For each importUrl -> storedResponse of newestWorker’s script
// resource map:
std::vector<GURL> request_urls;
for (const auto& resource_map_entry : previous_resource_map) {
// 8.21.1.1. If importUrl is url, then continue.
if (new_resource_map->find(resource_map_entry.first) !=
new_resource_map->end()) {
continue;
}
request_urls.push_back(resource_map_entry.first);
}
// 8.21.1.2. Let importRequest be a new request whose url is importUrl,
// client is job’s client, destination is "script", parser
// metadata is "not parser-inserted", synchronous flag is set,
// and whose use-URL-credentials flag is set.
// 8.21.1.3. Set importRequest’s cache mode to "no-cache" if any of the
// following are true:
// - registration’s update via cache mode is "none".
// - job’s force bypass cache flag is set.
// - registration is stale.
// 8.21.1.4. Let fetchedResponse be the result of fetching importRequest.
web::EnvironmentSettings* settings = environment_settings();
const GURL& base_url = settings->base_url();
loader::Origin origin = loader::Origin(base_url.GetOrigin());
ScriptLoader script_loader(settings->context());
script_loader.Load(csp_delegate(), origin, request_urls);
for (int index = 0; index < request_urls.size(); ++index) {
const auto& error = script_loader.GetError(index);
if (error) continue;
const GURL& url = request_urls[index];
// 8.21.1.5. Set updatedResourceMap[importRequest’s url] to
// fetchedResponse.
// Note: The headers of imported scripts aren't used anywhere.
auto result = new_resource_map->insert(std::make_pair(
url, ScriptResource(std::move(script_loader.GetContents(index)))));
// Assert that the insert was successful.
DCHECK(result.second);
// 8.21.1.6. Set fetchedResponse to fetchedResponse’s unsafe response.
// 8.21.1.7. If fetchedResponse’s cache state is not
// "local", set registration’s last update check time to the
// current time.
// 8.21.1.8. If fetchedResponse is a bad import script response, continue.
// 8.21.1.9. If fetchedResponse’s body is not byte-for-byte identical with
// storedResponse’s unsafe response's body, set
// hasUpdatedResources to true.
DCHECK(previous_resource_map.find(url) != previous_resource_map.end());
if (*result.first->second.content !=
*(previous_resource_map.find(url)->second.content)) {
has_updated_resources = true;
}
}
return has_updated_resources;
}
void WorkerGlobalScope::ImportScriptsInternal(
const std::vector<std::string>& urls,
script::ExceptionState* exception_state,
URLLookupCallback url_lookup_callback, ResponseCallback response_callback) {
// Algorithm for import scripts into worker global scope:
// https://html.spec.whatwg.org/multipage/workers.html#import-scripts-into-worker-global-scope
// 1. If worker global scope's type is "module", throw a TypeError exception.
// Cobalt does not support "module" type scripts.
// 2. Let settings object be the current settings object.
web::EnvironmentSettings* settings = environment_settings();
DCHECK(settings->context()
->message_loop()
->task_runner()
->BelongsToCurrentThread());
// 3. If urls is empty, return.
if (urls.empty()) return;
// 4. Parse each value in urls relative to settings object. If any fail, throw
// a "SyntaxError" DOMException.
std::vector<GURL> request_urls;
std::vector<std::string*> looked_up_content;
request_urls.reserve(urls.size());
std::vector<GURL> resolved_urls(urls.size());
std::vector<int> request_url_indexes(urls.size());
const GURL& base_url = settings->base_url();
for (int index = 0; index < urls.size(); ++index) {
const std::string& url = urls[index];
resolved_urls[index] = base_url.Resolve(url);
if (resolved_urls[index].is_empty()) {
web::DOMException::Raise(web::DOMException::kSyntaxErr, exception_state);
return;
}
std::string* content =
url_lookup_callback
? url_lookup_callback.Run(resolved_urls[index], exception_state)
: nullptr;
// Return if the url lookup callback has set the exception state.
if (exception_state->is_exception_set()) return;
if (content) {
// Store the result of the url lookup callback.
request_url_indexes[index] = -1;
looked_up_content.push_back(content);
} else {
// Add the url to the list to pass to ScriptLoader for loading.
request_url_indexes[index] = request_urls.size();
request_urls.push_back(resolved_urls[index]);
}
}
loader::Origin origin = loader::Origin(base_url.GetOrigin());
// 5. For each url in the resulting URL records, run these substeps:
// 5.1. Fetch a classic worker-imported script given url and settings
// object, passing along any custom perform the fetch steps provided.
// If this succeeds, let script be the result. Otherwise, rethrow the
// exception.
ScriptLoader script_loader(settings->context());
script_loader.Load(csp_delegate(), origin, request_urls);
// 5. For each url in the resulting URL records, run these substeps:
int content_lookup_index = 0;
for (int index = 0; index < resolved_urls.size(); ++index) {
int request_index = request_url_indexes[index];
std::string* script = nullptr;
if (request_index == -1) {
// The content at this index was received from the url lookup callback.
script = looked_up_content[content_lookup_index++];
} else {
const auto& error = script_loader.GetError(request_index);
if (error) {
LOG(WARNING) << "Script Loading Failed : " << *error;
web::DOMException::Raise(web::DOMException::kNetworkErr, *error,
exception_state);
break;
}
script = script_loader.GetContents(request_index).get();
}
// 5.2. Run the classic script script, with the rethrow errors argument
// set to true.
if (script) {
if (response_callback) {
script = response_callback.Run(resolved_urls[index], script);
}
bool succeeded = false;
std::string retval;
if (script) {
bool mute_errors = false;
const base::SourceLocation script_location(resolved_urls[index].spec(),
1, 1);
retval = settings->context()->script_runner()->Execute(
*script, script_location, mute_errors, &succeeded);
}
if (!succeeded) {
// TODO(): Handle script execution errors.
web::DOMException::Raise(web::DOMException::kSyntaxErr,
exception_state);
return;
}
}
}
// If an exception was thrown or if the script was prematurely aborted, then
// abort all these steps, letting the exception or aborting continue to be
// processed by the calling script.
}
} // namespace worker
} // namespace cobalt