blob: b12a5b6db5f27bebe13ece28cb114af333b20e7b [file] [log] [blame]
// Copyright (c) 2012 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 "content/browser/appcache/appcache_url_request_job.h"
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "content/browser/appcache/appcache.h"
#include "content/browser/appcache/appcache_group.h"
#include "content/browser/appcache/appcache_histograms.h"
#include "content/browser/appcache/appcache_host.h"
#include "content/browser/appcache/appcache_service_impl.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/net_log.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_status.h"
namespace content {
AppCacheURLRequestJob::AppCacheURLRequestJob(
net::URLRequest* request,
net::NetworkDelegate* network_delegate,
AppCacheStorage* storage,
AppCacheHost* host,
bool is_main_resource)
: net::URLRequestJob(request, network_delegate),
host_(host),
storage_(storage),
has_been_started_(false), has_been_killed_(false),
delivery_type_(AWAITING_DELIVERY_ORDERS),
group_id_(0), cache_id_(kAppCacheNoCacheId), is_fallback_(false),
is_main_resource_(is_main_resource),
cache_entry_not_found_(false),
weak_factory_(this) {
DCHECK(storage_);
}
void AppCacheURLRequestJob::DeliverAppCachedResponse(
const GURL& manifest_url, int64 group_id, int64 cache_id,
const AppCacheEntry& entry, bool is_fallback) {
DCHECK(!has_delivery_orders());
DCHECK(entry.has_response_id());
delivery_type_ = APPCACHED_DELIVERY;
manifest_url_ = manifest_url;
group_id_ = group_id;
cache_id_ = cache_id;
entry_ = entry;
is_fallback_ = is_fallback;
MaybeBeginDelivery();
}
void AppCacheURLRequestJob::DeliverNetworkResponse() {
DCHECK(!has_delivery_orders());
delivery_type_ = NETWORK_DELIVERY;
storage_ = NULL; // not needed
MaybeBeginDelivery();
}
void AppCacheURLRequestJob::DeliverErrorResponse() {
DCHECK(!has_delivery_orders());
delivery_type_ = ERROR_DELIVERY;
storage_ = NULL; // not needed
MaybeBeginDelivery();
}
void AppCacheURLRequestJob::MaybeBeginDelivery() {
if (has_been_started() && has_delivery_orders()) {
// Start asynchronously so that all error reporting and data
// callbacks happen as they would for network requests.
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&AppCacheURLRequestJob::BeginDelivery,
weak_factory_.GetWeakPtr()));
}
}
void AppCacheURLRequestJob::BeginDelivery() {
DCHECK(has_delivery_orders() && has_been_started());
if (has_been_killed())
return;
switch (delivery_type_) {
case NETWORK_DELIVERY:
AppCacheHistograms::AddNetworkJobStartDelaySample(
base::TimeTicks::Now() - start_time_tick_);
// To fallthru to the network, we restart the request which will
// cause a new job to be created to retrieve the resource from the
// network. Our caller is responsible for arranging to not re-intercept
// the same request.
NotifyRestartRequired();
break;
case ERROR_DELIVERY:
AppCacheHistograms::AddErrorJobStartDelaySample(
base::TimeTicks::Now() - start_time_tick_);
request()->net_log().AddEvent(
net::NetLog::TYPE_APPCACHE_DELIVERING_ERROR_RESPONSE);
NotifyStartError(net::URLRequestStatus(net::URLRequestStatus::FAILED,
net::ERR_FAILED));
break;
case APPCACHED_DELIVERY:
if (entry_.IsExecutable()) {
BeginExecutableHandlerDelivery();
return;
}
AppCacheHistograms::AddAppCacheJobStartDelaySample(
base::TimeTicks::Now() - start_time_tick_);
request()->net_log().AddEvent(
is_fallback_ ?
net::NetLog::TYPE_APPCACHE_DELIVERING_FALLBACK_RESPONSE :
net::NetLog::TYPE_APPCACHE_DELIVERING_CACHED_RESPONSE);
storage_->LoadResponseInfo(
manifest_url_, group_id_, entry_.response_id(), this);
break;
default:
NOTREACHED();
break;
}
}
void AppCacheURLRequestJob::BeginExecutableHandlerDelivery() {
DCHECK(CommandLine::ForCurrentProcess()->
HasSwitch(kEnableExecutableHandlers));
if (!storage_->service()->handler_factory()) {
BeginErrorDelivery("missing handler factory");
return;
}
request()->net_log().AddEvent(
net::NetLog::TYPE_APPCACHE_DELIVERING_EXECUTABLE_RESPONSE);
// We defer job delivery until the executable handler is spun up and
// provides a response. The sequence goes like this...
//
// 1. First we load the cache.
// 2. Then if the handler is not spun up, we load the script resource which
// is needed to spin it up.
// 3. Then we ask then we ask the handler to compute a response.
// 4. Finally we deilver that response to the caller.
storage_->LoadCache(cache_id_, this);
}
void AppCacheURLRequestJob::OnCacheLoaded(AppCache* cache, int64 cache_id) {
DCHECK_EQ(cache_id_, cache_id);
DCHECK(!has_been_killed());
if (!cache) {
BeginErrorDelivery("cache load failed");
return;
}
// Keep references to ensure they don't go out of scope until job completion.
cache_ = cache;
group_ = cache->owning_group();
// If the handler is spun up, ask it to compute a response.
AppCacheExecutableHandler* handler =
cache->GetExecutableHandler(entry_.response_id());
if (handler) {
InvokeExecutableHandler(handler);
return;
}
// Handler is not spun up yet, load the script resource to do that.
// NOTE: This is not ideal since multiple jobs may be doing this,
// concurrently but close enough for now, the first to load the script
// will win.
// Read the script data, truncating if its too large.
// NOTE: we just issue one read and don't bother chaining if the resource
// is very (very) large, close enough for now.
const int64 kLimit = 500 * 1000;
handler_source_buffer_ = new net::GrowableIOBuffer();
handler_source_buffer_->SetCapacity(kLimit);
handler_source_reader_.reset(storage_->CreateResponseReader(
manifest_url_, group_id_, entry_.response_id()));
handler_source_reader_->ReadData(
handler_source_buffer_.get(),
kLimit,
base::Bind(&AppCacheURLRequestJob::OnExecutableSourceLoaded,
base::Unretained(this)));
}
void AppCacheURLRequestJob::OnExecutableSourceLoaded(int result) {
DCHECK(!has_been_killed());
handler_source_reader_.reset();
if (result < 0) {
BeginErrorDelivery("script source load failed");
return;
}
handler_source_buffer_->SetCapacity(result); // Free up some memory.
AppCacheExecutableHandler* handler = cache_->GetOrCreateExecutableHandler(
entry_.response_id(), handler_source_buffer_.get());
handler_source_buffer_ = NULL; // not needed anymore
if (handler) {
InvokeExecutableHandler(handler);
return;
}
BeginErrorDelivery("factory failed to produce a handler");
}
void AppCacheURLRequestJob::InvokeExecutableHandler(
AppCacheExecutableHandler* handler) {
handler->HandleRequest(
request(),
base::Bind(&AppCacheURLRequestJob::OnExecutableResponseCallback,
weak_factory_.GetWeakPtr()));
}
void AppCacheURLRequestJob::OnExecutableResponseCallback(
const AppCacheExecutableHandler::Response& response) {
DCHECK(!has_been_killed());
if (response.use_network) {
delivery_type_ = NETWORK_DELIVERY;
storage_ = NULL;
BeginDelivery();
return;
}
if (!response.cached_resource_url.is_empty()) {
AppCacheEntry* entry_ptr = cache_->GetEntry(response.cached_resource_url);
if (entry_ptr && !entry_ptr->IsExecutable()) {
entry_ = *entry_ptr;
BeginDelivery();
return;
}
}
if (!response.redirect_url.is_empty()) {
// TODO(michaeln): playback a redirect
// response_headers_(new HttpResponseHeaders(response_headers)),
// fallthru for now to deliver an error
}
// Otherwise, return an error.
BeginErrorDelivery("handler returned an invalid response");
}
void AppCacheURLRequestJob::BeginErrorDelivery(const char* message) {
if (host_)
host_->frontend()->OnLogMessage(host_->host_id(), APPCACHE_LOG_ERROR,
message);
delivery_type_ = ERROR_DELIVERY;
storage_ = NULL;
BeginDelivery();
}
AppCacheURLRequestJob::~AppCacheURLRequestJob() {
if (storage_)
storage_->CancelDelegateCallbacks(this);
}
void AppCacheURLRequestJob::OnResponseInfoLoaded(
AppCacheResponseInfo* response_info, int64 response_id) {
DCHECK(is_delivering_appcache_response());
scoped_refptr<AppCacheURLRequestJob> protect(this);
if (response_info) {
info_ = response_info;
reader_.reset(storage_->CreateResponseReader(
manifest_url_, group_id_, entry_.response_id()));
if (is_range_request())
SetupRangeResponse();
NotifyHeadersComplete();
} else {
if (storage_->service()->storage() == storage_) {
// A resource that is expected to be in the appcache is missing.
// See http://code.google.com/p/chromium/issues/detail?id=50657
// Instead of failing the request, we restart the request. The retry
// attempt will fallthru to the network instead of trying to load
// from the appcache.
storage_->service()->CheckAppCacheResponse(manifest_url_, cache_id_,
entry_.response_id());
AppCacheHistograms::CountResponseRetrieval(
false, is_main_resource_, manifest_url_.GetOrigin());
}
cache_entry_not_found_ = true;
NotifyRestartRequired();
}
}
const net::HttpResponseInfo* AppCacheURLRequestJob::http_info() const {
if (!info_.get())
return NULL;
if (range_response_info_)
return range_response_info_.get();
return info_->http_response_info();
}
void AppCacheURLRequestJob::SetupRangeResponse() {
DCHECK(is_range_request() && info_.get() && reader_.get() &&
is_delivering_appcache_response());
int resource_size = static_cast<int>(info_->response_data_size());
if (resource_size < 0 || !range_requested_.ComputeBounds(resource_size)) {
range_requested_ = net::HttpByteRange();
return;
}
DCHECK(range_requested_.IsValid());
int offset = static_cast<int>(range_requested_.first_byte_position());
int length = static_cast<int>(range_requested_.last_byte_position() -
range_requested_.first_byte_position() + 1);
// Tell the reader about the range to read.
reader_->SetReadRange(offset, length);
// Make a copy of the full response headers and fix them up
// for the range we'll be returning.
range_response_info_.reset(
new net::HttpResponseInfo(*info_->http_response_info()));
net::HttpResponseHeaders* headers = range_response_info_->headers.get();
headers->UpdateWithNewRange(
range_requested_, resource_size, true /* replace status line */);
}
void AppCacheURLRequestJob::OnReadComplete(int result) {
DCHECK(is_delivering_appcache_response());
if (result == 0) {
NotifyDone(net::URLRequestStatus());
AppCacheHistograms::CountResponseRetrieval(
true, is_main_resource_, manifest_url_.GetOrigin());
} else if (result < 0) {
if (storage_->service()->storage() == storage_) {
storage_->service()->CheckAppCacheResponse(manifest_url_, cache_id_,
entry_.response_id());
}
NotifyDone(net::URLRequestStatus(net::URLRequestStatus::FAILED, result));
AppCacheHistograms::CountResponseRetrieval(
false, is_main_resource_, manifest_url_.GetOrigin());
} else {
SetStatus(net::URLRequestStatus()); // Clear the IO_PENDING status
}
NotifyReadComplete(result);
}
// net::URLRequestJob overrides ------------------------------------------------
void AppCacheURLRequestJob::Start() {
DCHECK(!has_been_started());
has_been_started_ = true;
start_time_tick_ = base::TimeTicks::Now();
MaybeBeginDelivery();
}
void AppCacheURLRequestJob::Kill() {
if (!has_been_killed_) {
has_been_killed_ = true;
reader_.reset();
handler_source_reader_.reset();
if (storage_) {
storage_->CancelDelegateCallbacks(this);
storage_ = NULL;
}
host_ = NULL;
info_ = NULL;
cache_ = NULL;
group_ = NULL;
range_response_info_.reset();
net::URLRequestJob::Kill();
weak_factory_.InvalidateWeakPtrs();
}
}
net::LoadState AppCacheURLRequestJob::GetLoadState() const {
if (!has_been_started())
return net::LOAD_STATE_IDLE;
if (!has_delivery_orders())
return net::LOAD_STATE_WAITING_FOR_APPCACHE;
if (delivery_type_ != APPCACHED_DELIVERY)
return net::LOAD_STATE_IDLE;
if (!info_.get())
return net::LOAD_STATE_WAITING_FOR_APPCACHE;
if (reader_.get() && reader_->IsReadPending())
return net::LOAD_STATE_READING_RESPONSE;
return net::LOAD_STATE_IDLE;
}
bool AppCacheURLRequestJob::GetMimeType(std::string* mime_type) const {
if (!http_info())
return false;
return http_info()->headers->GetMimeType(mime_type);
}
bool AppCacheURLRequestJob::GetCharset(std::string* charset) {
if (!http_info())
return false;
return http_info()->headers->GetCharset(charset);
}
void AppCacheURLRequestJob::GetResponseInfo(net::HttpResponseInfo* info) {
if (!http_info())
return;
*info = *http_info();
}
int AppCacheURLRequestJob::GetResponseCode() const {
if (!http_info())
return -1;
return http_info()->headers->response_code();
}
bool AppCacheURLRequestJob::ReadRawData(net::IOBuffer* buf, int buf_size,
int *bytes_read) {
DCHECK(is_delivering_appcache_response());
DCHECK_NE(buf_size, 0);
DCHECK(bytes_read);
DCHECK(!reader_->IsReadPending());
reader_->ReadData(
buf, buf_size, base::Bind(&AppCacheURLRequestJob::OnReadComplete,
base::Unretained(this)));
SetStatus(net::URLRequestStatus(net::URLRequestStatus::IO_PENDING, 0));
return false;
}
void AppCacheURLRequestJob::SetExtraRequestHeaders(
const net::HttpRequestHeaders& headers) {
std::string value;
std::vector<net::HttpByteRange> ranges;
if (!headers.GetHeader(net::HttpRequestHeaders::kRange, &value) ||
!net::HttpUtil::ParseRangeHeader(value, &ranges)) {
return;
}
// If multiple ranges are requested, we play dumb and
// return the entire response with 200 OK.
if (ranges.size() == 1U)
range_requested_ = ranges[0];
}
} // namespace content