blob: 86b163539b57dd9de144e0fbcf54db6940a42852 [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/web/cache.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/json/string_escape.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "cobalt/base/source_location.h"
#include "cobalt/cache/cache.h"
#include "cobalt/script/global_environment.h"
#include "cobalt/script/source_code.h"
#include "cobalt/script/v8c/conversion_helpers.h"
#include "cobalt/script/v8c/v8c_user_object_holder.h"
#include "cobalt/script/v8c/v8c_value_handle.h"
#include "cobalt/web/cache_utils.h"
#include "cobalt/web/context.h"
#include "cobalt/web/environment_settings_helper.h"
#include "net/url_request/url_request_context.h"
#include "v8/include/v8.h"
namespace cobalt {
namespace web {
namespace {
const network::disk_cache::ResourceType kResourceType =
network::disk_cache::ResourceType::kCacheApi;
} // namespace
Cache::Fetcher::Fetcher(network::NetworkModule* network_module, const GURL& url,
base::OnceCallback<void(bool)> callback)
: network_module_(network_module),
url_(url),
callback_(std::move(callback)),
buffer_(new net::GrowableIOBuffer()) {
network_module_->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&Cache::Fetcher::Start, base::Unretained(this)));
}
std::vector<uint8_t> Cache::Fetcher::BufferToVector() const {
auto* buffer_begin = reinterpret_cast<const uint8_t*>(buffer_->data());
return std::vector<uint8_t>(buffer_begin, buffer_begin + buffer_size_);
}
std::string Cache::Fetcher::BufferToString() const {
return std::string(buffer_->data(), buffer_size_);
}
void Cache::Fetcher::Start() {
request_ = network_module_->url_request_context()->CreateRequest(
url_, net::RequestPriority::DEFAULT_PRIORITY, this);
request_->Start();
}
void Cache::Fetcher::Notify(bool success) {
// Need to delete |URLRequest| instance on network thread.
request_.reset();
std::move(callback_).Run(success);
}
void Cache::Fetcher::OnDone(bool success) {
buffer_size_ = buffer_->offset();
buffer_->set_offset(0);
network_module_->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&Cache::Fetcher::Notify, base::Unretained(this), success));
}
void Cache::Fetcher::ReadResponse(net::URLRequest* request) {
int bytes_read = request->Read(buffer_, buffer_->RemainingCapacity());
bool read_running_async = bytes_read == net::ERR_IO_PENDING;
bool response_complete = !read_running_async && bytes_read <= 0;
// If read is still running, the buffer will be written to and then
// |OnReadCompleted()| will be called.
if (read_running_async) {
return;
}
if (response_complete) {
OnDone(/*success=*/bytes_read == net::OK);
return;
}
// Read completed synchronously. Call |OnReadCompleted()| asynchronously to
// avoid blocking IO.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&Cache::Fetcher::OnReadCompleted,
base::Unretained(this), request, bytes_read));
}
void Cache::Fetcher::OnResponseStarted(net::URLRequest* request,
int net_error) {
DCHECK_NE(net::ERR_IO_PENDING, net_error);
request->GetMimeType(&mime_type_);
if (net_error != net::OK) {
OnDone(/*success=*/false);
return;
}
status_text_ = request->response_headers()->GetStatusText();
response_code_ = request->response_headers()->response_code();
size_t iter = 0;
std::string name;
std::string value;
while (
request->response_headers()->EnumerateHeaderLines(&iter, &name, &value)) {
base::ListValue header;
header.GetList().emplace_back(name);
header.GetList().emplace_back(value);
headers_.GetList().push_back(std::move(header));
}
int initial_capacity = request->response_headers()->HasHeader(
net::HttpRequestHeaders::kContentLength)
? request->response_headers()->GetContentLength()
: 64 * 1024;
buffer_->SetCapacity(initial_capacity);
ReadResponse(request);
}
void Cache::Fetcher::OnReadCompleted(net::URLRequest* request, int bytes_read) {
DCHECK_NE(net::ERR_IO_PENDING, bytes_read);
if (bytes_read <= 0) {
OnDone(/*success=*/bytes_read == net::OK);
return;
}
// The offset is how much of the buffer is used.
buffer_->set_offset(buffer_->offset() + bytes_read);
// Grow the buffer, if needed.
if (buffer_->RemainingCapacity() == 0) {
buffer_->SetCapacity(buffer_->capacity() + 16 * 1024);
}
ReadResponse(request);
}
script::HandlePromiseAny Cache::Match(
script::EnvironmentSettings* environment_settings,
const script::ValueHandleHolder& request) {
auto* isolate = get_isolate(environment_settings);
script::v8c::EntryScope entry_scope(isolate);
auto resolver =
v8::Promise::Resolver::New(isolate->GetCurrentContext()).ToLocalChecked();
std::vector<v8::TracedGlobal<v8::Value>*> traced_globals;
base::OnceClosure cleanup_traced;
cache_utils::Trace(isolate, {resolver}, traced_globals, cleanup_traced);
auto traced_resolver = traced_globals[0]->As<v8::Promise::Resolver>();
auto context = get_context(environment_settings);
context->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](script::EnvironmentSettings* environment_settings, uint32_t key,
v8::TracedGlobal<v8::Promise::Resolver> traced_resolver,
base::OnceClosure cleanup_traced) {
base::ScopedClosureRunner finally(std::move(cleanup_traced));
auto* isolate = get_isolate(environment_settings);
auto cached =
cache::Cache::GetInstance()->Retrieve(kResourceType, key);
auto metadata =
cache::Cache::GetInstance()->Metadata(kResourceType, key);
script::v8c::EntryScope entry_scope(isolate);
auto resolver = traced_resolver.Get(isolate);
if (!cached || !metadata || !metadata->FindKey("options")) {
cache_utils::Resolve(resolver);
return;
}
auto response = cache_utils::CreateResponse(
isolate, *cached, *(metadata->FindKey("options")));
if (!response) {
cache_utils::Reject(resolver);
return;
}
cache_utils::Resolve(resolver, response.value());
},
environment_settings,
cache_utils::GetKey(environment_settings->base_url(), request),
traced_resolver, std::move(cleanup_traced)));
return cache_utils::FromResolver(resolver);
}
void Cache::PerformAdd(
script::EnvironmentSettings* environment_settings,
std::unique_ptr<script::ValueHandleHolder::Reference> request_reference,
std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference) {
base::AutoLock auto_lock(fetcher_lock_);
auto* global_environment = get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
script::v8c::EntryScope entry_scope(isolate);
uint32_t key = cache_utils::GetKey(environment_settings->base_url(),
request_reference->referenced_value());
if (fetchers_.find(key) != fetchers_.end()) {
auto* promises = &(fetch_contexts_[key].first);
promises->push_back(std::move(promise_reference));
return;
}
auto promises =
std::vector<std::unique_ptr<script::ValuePromiseVoid::Reference>>();
promises.push_back(std::move(promise_reference));
fetch_contexts_[key] =
std::make_pair(std::move(promises), environment_settings);
auto* context = get_context(environment_settings);
fetchers_[key] = std::make_unique<Cache::Fetcher>(
context->network_module(),
GURL(cache_utils::GetUrl(environment_settings->base_url(),
request_reference->referenced_value())),
base::BindOnce(&Cache::OnFetchCompleted, base::Unretained(this), key));
}
script::HandlePromiseVoid Cache::Add(
script::EnvironmentSettings* environment_settings,
const script::ValueHandleHolder& request) {
auto* global_wrappable = get_global_wrappable(environment_settings);
auto request_reference =
std::make_unique<script::ValueHandleHolder::Reference>(global_wrappable,
request);
script::HandlePromiseVoid promise =
get_script_value_factory(environment_settings)
->CreateBasicPromise<void>();
auto promise_reference =
std::make_unique<script::ValuePromiseVoid::Reference>(global_wrappable,
promise);
auto context = get_context(environment_settings);
context->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&Cache::PerformAdd, base::Unretained(this),
environment_settings, std::move(request_reference),
std::move(promise_reference)));
return promise;
}
script::HandlePromiseVoid Cache::Put(
script::EnvironmentSettings* environment_settings,
const script::ValueHandleHolder& request,
const script::ValueHandleHolder& response) {
auto* global_wrappable = get_global_wrappable(environment_settings);
auto request_reference =
std::make_unique<script::ValueHandleHolder::Reference>(global_wrappable,
request);
auto response_reference =
std::make_unique<script::ValueHandleHolder::Reference>(global_wrappable,
response);
script::HandlePromiseVoid promise =
get_script_value_factory(environment_settings)
->CreateBasicPromise<void>();
auto promise_reference =
std::make_unique<script::ValuePromiseVoid::Reference>(global_wrappable,
promise);
auto* global_environment = get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
auto context = isolate->GetCurrentContext();
script::v8c::EntryScope entry_scope(isolate);
auto v8_response =
GetV8Value(response_reference->referenced_value()).As<v8::Object>();
auto body_used = cache_utils::Get(v8_response, "bodyUsed");
if (!body_used || body_used->As<v8::Boolean>()->Value()) {
promise_reference->value().Reject(script::kTypeError);
return promise;
}
auto array_buffer_promise = cache_utils::Call(v8_response, "arrayBuffer");
if (!array_buffer_promise) {
promise_reference->value().Reject();
return promise;
}
cache_utils::Then(
array_buffer_promise.value(),
base::BindOnce(
[](script::EnvironmentSettings* environment_settings,
std::unique_ptr<script::ValueHandleHolder::Reference>
request_reference,
std::unique_ptr<script::ValueHandleHolder::Reference>
response_reference,
std::unique_ptr<script::ValuePromiseVoid::Reference>
promise_reference,
v8::Local<v8::Promise> array_buffer_promise)
-> base::Optional<v8::Local<v8::Promise>> {
uint32_t key =
cache_utils::GetKey(environment_settings->base_url(),
request_reference->referenced_value());
std::string url =
cache_utils::GetUrl(environment_settings->base_url(),
request_reference->referenced_value());
auto options = cache_utils::ExtractResponseOptions(
cache_utils::ToV8Value(response_reference->referenced_value()));
if (!options) {
promise_reference->value().Reject();
return base::nullopt;
}
base::DictionaryValue metadata;
metadata.SetKey("url", base::Value(url));
metadata.SetKey("options", std::move(options.value()));
cache::Cache::GetInstance()->Store(
kResourceType, key,
cache_utils::ToUint8Vector(array_buffer_promise->Result()),
std::move(metadata));
promise_reference->value().Resolve();
return base::nullopt;
},
environment_settings, std::move(request_reference),
std::move(response_reference), std::move(promise_reference)));
return promise;
}
script::HandlePromiseBool Cache::Delete(
script::EnvironmentSettings* environment_settings,
const script::ValueHandleHolder& request) {
auto* global_wrappable = get_global_wrappable(environment_settings);
script::HandlePromiseBool promise =
get_script_value_factory(environment_settings)
->CreateBasicPromise<bool>();
auto request_reference =
std::make_unique<script::ValueHandleHolder::Reference>(global_wrappable,
request);
auto promise_reference =
std::make_unique<script::ValuePromiseBool::Reference>(global_wrappable,
promise);
auto context = get_context(environment_settings);
context->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](script::EnvironmentSettings* environment_settings,
std::unique_ptr<script::ValueHandleHolder::Reference>
request_reference,
std::unique_ptr<script::ValuePromiseBool::Reference>
promise_reference) {
auto* global_environment =
get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
script::v8c::EntryScope entry_scope(isolate);
promise_reference->value().Resolve(
cache::Cache::GetInstance()->Delete(
kResourceType, cache_utils::GetKey(
environment_settings->base_url(),
request_reference->referenced_value())));
},
environment_settings, std::move(request_reference),
std::move(promise_reference)));
return promise;
}
script::HandlePromiseAny Cache::Keys(
script::EnvironmentSettings* environment_settings) {
auto* global_wrappable = get_global_wrappable(environment_settings);
script::HandlePromiseAny promise =
get_script_value_factory(environment_settings)
->CreateBasicPromise<script::Any>();
auto promise_reference = std::make_unique<script::ValuePromiseAny::Reference>(
global_wrappable, promise);
auto context = get_context(environment_settings);
context->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](script::EnvironmentSettings* environment_settings,
std::unique_ptr<script::ValuePromiseAny::Reference>
promise_reference) {
auto* global_environment =
get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
script::v8c::EntryScope entry_scope(isolate);
std::vector<v8::Local<v8::Value>> requests;
auto keys =
cache::Cache::GetInstance()->KeysWithMetadata(kResourceType);
for (uint32_t key : keys) {
auto metadata =
cache::Cache::GetInstance()->Metadata(kResourceType, key);
if (!metadata) {
continue;
}
auto url = metadata->FindKey("url");
if (!url) {
continue;
}
base::Optional<v8::Local<v8::Value>> request =
cache_utils::CreateRequest(isolate, url->GetString());
if (request) {
requests.push_back(std::move(request.value()));
}
}
promise_reference->value().Resolve(
script::Any(new script::v8c::V8cValueHandleHolder(
isolate, v8::Array::New(isolate, requests.data(),
requests.size()))));
},
environment_settings, std::move(promise_reference)));
return promise;
}
void Cache::OnFetchCompleted(uint32_t key, bool success) {
base::AutoLock auto_lock(fetcher_lock_);
auto* environment_settings = fetch_contexts_[key].second;
auto* context = get_context(environment_settings);
context->message_loop()->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&Cache::OnFetchCompletedMainThread,
base::Unretained(this), key, success));
}
void Cache::OnFetchCompletedMainThread(uint32_t key, bool success) {
base::AutoLock auto_lock(fetcher_lock_);
auto* fetcher = fetchers_[key].get();
auto* promises = &(fetch_contexts_[key].first);
int status = fetcher->response_code();
// |status| of 200-299 excluding 206 "Partial Content" should be cached.
if (!success || status == 206 || status < 200 || status > 299) {
while (promises->size() > 0) {
promises->back()->value().Reject();
promises->pop_back();
}
fetchers_.erase(key);
fetch_contexts_.erase(key);
return;
}
{
base::DictionaryValue metadata;
metadata.SetKey("url", base::Value(fetcher->url().spec()));
base::DictionaryValue options;
options.SetKey("status", base::Value(status));
options.SetKey("statusText", base::Value(fetcher->status_text()));
options.SetKey("headers", std::move(fetcher->headers()));
metadata.SetKey("options", std::move(options));
cache::Cache::GetInstance()->Store(
kResourceType, key, fetcher->BufferToVector(), std::move(metadata));
}
if (fetcher->mime_type() == "text/javascript") {
auto* environment_settings = fetch_contexts_[key].second;
auto* global_environment = get_global_environment(environment_settings);
auto* isolate = global_environment->isolate();
script::v8c::EntryScope entry_scope(isolate);
// TODO: compile async or maybe don't cache if compile fails.
global_environment->Compile(script::SourceCode::CreateSourceCode(
fetcher->BufferToString(), base::SourceLocation(__FILE__, 1, 1)));
}
while (promises->size() > 0) {
promises->back()->value().Resolve();
promises->pop_back();
}
fetchers_.erase(key);
fetch_contexts_.erase(key);
}
} // namespace web
} // namespace cobalt