Skip to content

Commit

Permalink
Add support for Link rel=serviceworker in HTTP headers.
Browse files Browse the repository at this point in the history
This adds a new LinkHeaderResourceThrottle resource throttle which inspects
responses for Link headers, and tries to install a service worker when a
header with rel=serviceworker is encountered.

This implements the HTTP header part of the spec change discussed at
w3c/ServiceWorker#685

Even though this code doesn't live in blink, the feature is still guarded
by the experimental web platform features flag.

BUG=582310

Review URL: https://codereview.chromium.org/1736143002

Cr-Commit-Position: refs/heads/master@{#380035}
  • Loading branch information
mkruisselbrink authored and Commit bot committed Mar 9, 2016
1 parent 8a976c4 commit 493412a
Show file tree
Hide file tree
Showing 13 changed files with 854 additions and 21 deletions.
3 changes: 3 additions & 0 deletions content/browser/loader/resource_dispatcher_host_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/resource_context_impl.h"
#include "content/browser/service_worker/foreign_fetch_request_handler.h"
#include "content/browser/service_worker/link_header_support.h"
#include "content/browser/service_worker/service_worker_request_handler.h"
#include "content/browser/streams/stream.h"
#include "content/browser/streams/stream_context.h"
Expand Down Expand Up @@ -996,6 +997,8 @@ void ResourceDispatcherHostImpl::DidReceiveResponse(ResourceLoader* loader) {
scheduler_.get());
}

ProcessRequestForLinkHeaders(request);

int render_process_id, render_frame_host;
if (!info->GetAssociatedRenderFrame(&render_process_id, &render_frame_host))
return;
Expand Down
311 changes: 311 additions & 0 deletions content/browser/service_worker/link_header_support.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
// Copyright 2016 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/service_worker/link_header_support.h"

#include "base/command_line.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "content/browser/loader/resource_message_filter.h"
#include "content/browser/loader/resource_request_info_impl.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/common/service_worker/service_worker_utils.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/origin_util.h"
#include "net/http/http_util.h"
#include "net/url_request/url_request.h"

namespace content {

namespace {

// A variation of base::StringTokenizer and net::HttpUtil::ValuesIterator.
// Takes the parsing of StringTokenizer and adds support for quoted strings that
// are quoted by matching <> (and does not support escaping in those strings).
// Also has the behavior of ValuesIterator where it strips whitespace from all
// values and only outputs non-empty values.
// Only supports ',' as separator and supports '' "" and <> as quote chars.
// TODO(mek): Figure out if there is a way to share this with the parsing code
// in blink::LinkHeader.
class ValueTokenizer {
public:
ValueTokenizer(std::string::const_iterator begin,
std::string::const_iterator end)
: token_begin_(begin), token_end_(begin), end_(end) {}

std::string::const_iterator token_begin() const { return token_begin_; }
std::string::const_iterator token_end() const { return token_end_; }

bool GetNext() {
while (GetNextInternal()) {
net::HttpUtil::TrimLWS(&token_begin_, &token_end_);

// Only return non-empty values.
if (token_begin_ != token_end_)
return true;
}
return false;
}

private:
// Updates token_begin_ and token_end_ to point to the (possibly empty) next
// token. Returns false if end-of-string was reached first.
bool GetNextInternal() {
// First time this is called token_end_ points to the first character in the
// input. Every other time token_end_ points to the delimiter at the end of
// the last returned token (which could be the end of the string).

// End of string, return false.
if (token_end_ == end_)
return false;

// Skip past the delimiter.
if (*token_end_ == ',')
++token_end_;

// Make token_begin_ point to the beginning of the next token, and search
// for the end of the token in token_end_.
token_begin_ = token_end_;

// Set to true if we're currently inside a quoted string.
bool in_quote = false;
// Set to true if we're currently inside a quoted string, and have just
// encountered an escape character. In this case a closing quote will be
// ignored.
bool in_escape = false;
// If currently in a quoted string, this is the character that (when not
// escaped) indicates the end of the string.
char quote_close_char = '\0';
// If currently in a quoted string, this is set to true if it is possible to
// escape the closing quote using '\'.
bool quote_allows_escape = false;

while (token_end_ != end_) {
char c = *token_end_;
if (in_quote) {
if (in_escape) {
in_escape = false;
} else if (quote_allows_escape && c == '\\') {
in_escape = true;
} else if (c == quote_close_char) {
in_quote = false;
}
} else {
if (c == ',')
break;
if (c == '\'' || c == '"' || c == '<') {
in_quote = true;
quote_close_char = (c == '<' ? '>' : c);
quote_allows_escape = (c != '<');
}
}
++token_end_;
}
return true;
}

std::string::const_iterator token_begin_;
std::string::const_iterator token_end_;
std::string::const_iterator end_;
};

// Parses one link in a link header into its url and parameters.
// A link is of the form "<some-url>; param1=value1; param2=value2".
// Returns false if parsing the link failed, returns true on success. This
// method is more lenient than the RFC. It doesn't fail on things like invalid
// characters in the URL, and also doesn't verify that certain parameters should
// or shouldn't be quoted strings.
// If a parameter occurs more than once in the link, only the first value is
// returned in params as this is the required behavior for all attributes chrome
// currently cares about in link headers.
bool ParseLink(std::string::const_iterator begin,
std::string::const_iterator end,
std::string* url,
std::unordered_map<std::string, std::string>* params) {
// Can't parse an empty string.
if (begin == end)
return false;

// Extract the URL part (everything between '<' and first '>' character).
if (*begin != '<')
return false;
++begin;
std::string::const_iterator url_begin = begin;
std::string::const_iterator url_end = std::find(begin, end, '>');
// Fail if we did not find a '>'.
if (url_end == end)
return false;
begin = url_end;
net::HttpUtil::TrimLWS(&url_begin, &url_end);
*url = std::string(url_begin, url_end);

// Skip the '>' at the end of the URL, trim any remaining whitespace, and make
// sure it is followed by a ';' to indicate the start of parameters.
++begin;
net::HttpUtil::TrimLWS(&begin, &end);
if (begin != end && *begin != ';')
return false;

// Parse all the parameters.
net::HttpUtil::NameValuePairsIterator params_iterator(
begin, end, ';', net::HttpUtil::NameValuePairsIterator::VALUES_OPTIONAL);
while (params_iterator.GetNext()) {
if (!net::HttpUtil::IsToken(params_iterator.name_begin(),
params_iterator.name_end()))
return false;
std::string name = base::ToLowerASCII(base::StringPiece(
params_iterator.name_begin(), params_iterator.name_end()));
params->insert(std::make_pair(name, params_iterator.value()));
}
return params_iterator.valid();
}

void RegisterServiceWorkerFinished(int64_t trace_id, bool result) {
TRACE_EVENT_ASYNC_END1("ServiceWorker",
"LinkHeaderResourceThrottle::HandleServiceWorkerLink",
trace_id, "Success", result);
}

void HandleServiceWorkerLink(
const net::URLRequest* request,
const std::string& url,
const std::unordered_map<std::string, std::string>& params,
ServiceWorkerContextWrapper* service_worker_context_for_testing) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);

if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableExperimentalWebPlatformFeatures)) {
// TODO(mek): Integrate with experimental framework.
return;
}

if (ContainsKey(params, "anchor"))
return;

const ResourceRequestInfoImpl* request_info =
ResourceRequestInfoImpl::ForRequest(request);
ResourceMessageFilter* filter = request_info->filter();
ServiceWorkerContext* service_worker_context =
filter ? filter->service_worker_context()
: service_worker_context_for_testing;
if (!service_worker_context)
return;

// TODO(mek): serviceworker links should only be supported on requests from
// secure contexts. For now just check the initiator origin, even though that
// is not correct: 1) the initiator isn't the origin that matters in case of
// navigations, and 2) more than just a secure origin this needs to be a
// secure context.
if (!request->initiator().unique() &&
!IsOriginSecure(GURL(request->initiator().Serialize())))
return;

// TODO(mek): support for a serviceworker link on a request that wouldn't ever
// be able to be intercepted by a serviceworker isn't very useful, so this
// should share logic with ServiceWorkerRequestHandler and
// ForeignFetchRequestHandler to limit the requests for which serviceworker
// links are processed.

GURL context_url = request->url();
GURL script_url = context_url.Resolve(url);
auto scope_param = params.find("scope");
GURL scope_url = scope_param == params.end()
? script_url.Resolve("./")
: context_url.Resolve(scope_param->second);

if (!context_url.is_valid() || !script_url.is_valid() ||
!scope_url.is_valid())
return;
if (!ServiceWorkerUtils::CanRegisterServiceWorker(context_url, scope_url,
script_url))
return;
std::string error;
if (ServiceWorkerUtils::ContainsDisallowedCharacter(scope_url, script_url,
&error))
return;

int render_process_id = -1;
int render_frame_id = -1;
ResourceRequestInfo::GetRenderFrameForRequest(request, &render_process_id,
&render_frame_id);

if (!GetContentClient()->browser()->AllowServiceWorker(
scope_url, request->first_party_for_cookies(),
request_info->GetContext(), render_process_id, render_frame_id))
return;

static int64_t trace_id = 0;
TRACE_EVENT_ASYNC_BEGIN2(
"ServiceWorker", "LinkHeaderResourceThrottle::HandleServiceWorkerLink",
++trace_id, "Pattern", scope_url.spec(), "Script URL", script_url.spec());
service_worker_context->RegisterServiceWorker(
scope_url, script_url,
base::Bind(&RegisterServiceWorkerFinished, trace_id));
}

void ProcessLinkHeaderValueForRequest(
const net::URLRequest* request,
std::string::const_iterator value_begin,
std::string::const_iterator value_end,
ServiceWorkerContextWrapper* service_worker_context_for_testing) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);

std::string url;
std::unordered_map<std::string, std::string> params;
if (!ParseLink(value_begin, value_end, &url, &params))
return;

for (const auto& rel :
base::SplitStringPiece(params["rel"], HTTP_LWS, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
if (base::EqualsCaseInsensitiveASCII(rel, "serviceworker"))
HandleServiceWorkerLink(request, url, params,
service_worker_context_for_testing);
}
}

} // namespace

void ProcessRequestForLinkHeaders(const net::URLRequest* request) {
std::string link_header;
request->GetResponseHeaderByName("link", &link_header);
if (link_header.empty())
return;

ProcessLinkHeaderForRequest(request, link_header);
}

void ProcessLinkHeaderForRequest(
const net::URLRequest* request,
const std::string& link_header,
ServiceWorkerContextWrapper* service_worker_context_for_testing) {
ValueTokenizer tokenizer(link_header.begin(), link_header.end());
while (tokenizer.GetNext()) {
ProcessLinkHeaderValueForRequest(request, tokenizer.token_begin(),
tokenizer.token_end(),
service_worker_context_for_testing);
}
}

void SplitLinkHeaderForTesting(const std::string& header,
std::vector<std::string>* values) {
values->clear();
ValueTokenizer tokenizer(header.begin(), header.end());
while (tokenizer.GetNext()) {
values->push_back(
std::string(tokenizer.token_begin(), tokenizer.token_end()));
}
}

bool ParseLinkHeaderValueForTesting(
const std::string& link,
std::string* url,
std::unordered_map<std::string, std::string>* params) {
return ParseLink(link.begin(), link.end(), url, params);
}

} // namespace content
38 changes: 38 additions & 0 deletions content/browser/service_worker/link_header_support.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2016 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.

#ifndef CONTENT_BROWSER_SERVICE_WORKER_LINK_HEADER_SUPPORT_H_
#define CONTENT_BROWSER_SERVICE_WORKER_LINK_HEADER_SUPPORT_H_

#include <string>
#include <unordered_map>
#include <vector>

#include "base/macros.h"
#include "content/common/content_export.h"

namespace net {
class URLRequest;
}

namespace content {
class ServiceWorkerContextWrapper;

void ProcessRequestForLinkHeaders(const net::URLRequest* request);

CONTENT_EXPORT void ProcessLinkHeaderForRequest(
const net::URLRequest* request,
const std::string& link_header,
ServiceWorkerContextWrapper* service_worker_context_for_testing = nullptr);

CONTENT_EXPORT void SplitLinkHeaderForTesting(const std::string& header,
std::vector<std::string>* values);
CONTENT_EXPORT bool ParseLinkHeaderValueForTesting(
const std::string& link,
std::string* url,
std::unordered_map<std::string, std::string>* params);

} // namespace content

#endif // CONTENT_BROWSER_SERVICE_WORKER_LINK_HEADER_SUPPORT_H_
Loading

0 comments on commit 493412a

Please sign in to comment.