diff options
| author | mek <mek@chromium.org> | 2016-03-08 18:26:45 -0800 |
|---|---|---|
| committer | Commit bot <commit-bot@chromium.org> | 2016-03-09 02:28:21 +0000 |
| commit | 493412af1482220d43ae3dce390ed09617b2d95a (patch) | |
| tree | 84bd3292a490d02dd46d9af45524abcdecb30309 | |
| parent | 8a976c4e23265d7ad391101e7a7a0c18962d8113 (diff) | |
| download | chromium_src-493412af1482220d43ae3dce390ed09617b2d95a.zip chromium_src-493412af1482220d43ae3dce390ed09617b2d95a.tar.gz chromium_src-493412af1482220d43ae3dce390ed09617b2d95a.tar.bz2 | |
Add support for Link rel=serviceworker in HTTP headers.
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
https://github.com/slightlyoff/ServiceWorker/issues/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}
13 files changed, 854 insertions, 21 deletions
diff --git a/content/browser/loader/resource_dispatcher_host_impl.cc b/content/browser/loader/resource_dispatcher_host_impl.cc index 7b160b9..8ac1fd7 100644 --- a/content/browser/loader/resource_dispatcher_host_impl.cc +++ b/content/browser/loader/resource_dispatcher_host_impl.cc @@ -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" @@ -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; diff --git a/content/browser/service_worker/link_header_support.cc b/content/browser/service_worker/link_header_support.cc new file mode 100644 index 0000000..1a957c7 --- /dev/null +++ b/content/browser/service_worker/link_header_support.cc @@ -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, ¶ms)) + 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 diff --git a/content/browser/service_worker/link_header_support.h b/content/browser/service_worker/link_header_support.h new file mode 100644 index 0000000..b47a6b0 --- /dev/null +++ b/content/browser/service_worker/link_header_support.h @@ -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_ diff --git a/content/browser/service_worker/link_header_support_unittest.cc b/content/browser/service_worker/link_header_support_unittest.cc new file mode 100644 index 0000000..ae293fe --- /dev/null +++ b/content/browser/service_worker/link_header_support_unittest.cc @@ -0,0 +1,410 @@ +// 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/logging.h" +#include "base/run_loop.h" +#include "content/browser/service_worker/embedded_worker_test_helper.h" +#include "content/browser/service_worker/service_worker_context_wrapper.h" +#include "content/browser/service_worker/service_worker_registration.h" +#include "content/public/browser/resource_request_info.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/mock_resource_context.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request_test_job.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +TEST(LinkHeaderTest, SplitEmpty) { + std::vector<std::string> values; + SplitLinkHeaderForTesting("", &values); + ASSERT_EQ(0u, values.size()); +} + +TEST(LinkHeaderTest, SplitSimple) { + std::vector<std::string> values; + SplitLinkHeaderForTesting("hello", &values); + ASSERT_EQ(1u, values.size()); + EXPECT_EQ("hello", values[0]); + + SplitLinkHeaderForTesting("foo, bar", &values); + ASSERT_EQ(2u, values.size()); + EXPECT_EQ("foo", values[0]); + EXPECT_EQ("bar", values[1]); + + SplitLinkHeaderForTesting(" 1\t,\t2,3", &values); + ASSERT_EQ(3u, values.size()); + EXPECT_EQ("1", values[0]); + EXPECT_EQ("2", values[1]); + EXPECT_EQ("3", values[2]); +} + +TEST(LinkHeaderTest, SplitSkipsEmpty) { + std::vector<std::string> values; + SplitLinkHeaderForTesting(", foo, , \t, bar", &values); + ASSERT_EQ(2u, values.size()); + EXPECT_EQ("foo", values[0]); + EXPECT_EQ("bar", values[1]); +} + +TEST(LinkHeaderTest, SplitQuotes) { + std::vector<std::string> values; + SplitLinkHeaderForTesting("\"foo,bar\", 'bar,foo', <hel,lo>", &values); + ASSERT_EQ(3u, values.size()); + EXPECT_EQ("\"foo,bar\"", values[0]); + EXPECT_EQ("'bar,foo'", values[1]); + EXPECT_EQ("<hel,lo>", values[2]); +} + +TEST(LinkHeaderTest, SplitEscapedQuotes) { + std::vector<std::string> values; + SplitLinkHeaderForTesting("\"f\\\"oo,bar\", 'b\\'ar,foo', <hel\\>,lo>", + &values); + ASSERT_EQ(4u, values.size()); + EXPECT_EQ("\"f\\\"oo,bar\"", values[0]); + EXPECT_EQ("'b\\'ar,foo'", values[1]); + EXPECT_EQ("<hel\\>", values[2]); + EXPECT_EQ("lo>", values[3]); +} + +struct SimpleParseTestData { + const char* link; + bool valid; + const char* url; + const char* rel; + const char* as; +}; + +void PrintTo(const SimpleParseTestData& test, std::ostream* os) { + *os << ::testing::PrintToString(test.link); +} + +class SimpleParseTest : public ::testing::TestWithParam<SimpleParseTestData> {}; + +TEST_P(SimpleParseTest, Simple) { + const SimpleParseTestData test = GetParam(); + + std::string url; + std::unordered_map<std::string, std::string> params; + EXPECT_EQ(test.valid, + ParseLinkHeaderValueForTesting(test.link, &url, ¶ms)); + if (test.valid) { + EXPECT_EQ(test.url, url); + EXPECT_EQ(test.rel, params["rel"]); + EXPECT_EQ(test.as, params["as"]); + } +} + +// Test data mostly copied from blink::LinkHeaderTest. Expectations for some +// test cases are different though. Mostly because blink::LinkHeader is stricter +// about validity while parsing (primarily things like mismatched quotes), and +// factors in knowledge about semantics of Link headers (parameters that are +// required to have a value if they occur, some parameters are auto-lower-cased, +// headers with an "anchor" parameter are rejected by base::LinkHeader). +// The code this tests purely parses without actually interpreting the data, as +// it is expected that another layer on top will do more specific validations. +const SimpleParseTestData simple_parse_tests[] = { + {"</images/cat.jpg>; rel=prefetch", true, "/images/cat.jpg", "prefetch", + ""}, + {"</images/cat.jpg>;rel=prefetch", true, "/images/cat.jpg", "prefetch", ""}, + {"</images/cat.jpg> ;rel=prefetch", true, "/images/cat.jpg", "prefetch", + ""}, + {"</images/cat.jpg> ; rel=prefetch", true, "/images/cat.jpg", + "prefetch", ""}, + {"< /images/cat.jpg> ; rel=prefetch", true, "/images/cat.jpg", + "prefetch", ""}, + {"</images/cat.jpg > ; rel=prefetch", true, "/images/cat.jpg", + "prefetch", ""}, + {"</images/cat.jpg wutwut> ; rel=prefetch", true, + "/images/cat.jpg wutwut", "prefetch", ""}, + {"</images/cat.jpg wutwut \t > ; rel=prefetch", true, + "/images/cat.jpg wutwut", "prefetch", ""}, + {"</images/cat.jpg>; rel=prefetch ", true, "/images/cat.jpg", "prefetch", + ""}, + {"</images/cat.jpg>; Rel=prefetch ", true, "/images/cat.jpg", "prefetch", + ""}, + {"</images/cat.jpg>; Rel=PReFetCh ", true, "/images/cat.jpg", "PReFetCh", + ""}, + {"</images/cat.jpg>; rel=prefetch; rel=somethingelse", true, + "/images/cat.jpg", "prefetch", ""}, + {"</images/cat.jpg>\t\t ; \trel=prefetch \t ", true, "/images/cat.jpg", + "prefetch", ""}, + {"</images/cat.jpg>; rel= prefetch", true, "/images/cat.jpg", "prefetch", + ""}, + {"<../images/cat.jpg?dog>; rel= prefetch", true, "../images/cat.jpg?dog", + "prefetch", ""}, + {"</images/cat.jpg>; rel =prefetch", true, "/images/cat.jpg", "prefetch", + ""}, + {"</images/cat.jpg>; rel pel=prefetch", false}, + {"< /images/cat.jpg>", true, "/images/cat.jpg", "", ""}, + {"</images/cat.jpg>; wut=sup; rel =prefetch", true, "/images/cat.jpg", + "prefetch", ""}, + {"</images/cat.jpg>; wut=sup ; rel =prefetch", true, "/images/cat.jpg", + "prefetch", ""}, + {"</images/cat.jpg>; wut=sup ; rel =prefetch \t ;", true, + "/images/cat.jpg", "prefetch", ""}, + {"</images/cat.jpg> wut=sup ; rel =prefetch \t ;", false}, + {"< /images/cat.jpg", false}, + {"< http://wut.com/ sdfsdf ?sd>; rel=dns-prefetch", true, + "http://wut.com/ sdfsdf ?sd", "dns-prefetch", ""}, + {"< http://wut.com/%20%20%3dsdfsdf?sd>; rel=dns-prefetch", true, + "http://wut.com/%20%20%3dsdfsdf?sd", "dns-prefetch", ""}, + {"< http://wut.com/dfsdf?sdf=ghj&wer=rty>; rel=prefetch", true, + "http://wut.com/dfsdf?sdf=ghj&wer=rty", "prefetch", ""}, + {"< http://wut.com/dfsdf?sdf=ghj&wer=rty>;;;;; rel=prefetch", true, + "http://wut.com/dfsdf?sdf=ghj&wer=rty", "prefetch", ""}, + {"< http://wut.com/%20%20%3dsdfsdf?sd>; rel=preload;as=image", true, + "http://wut.com/%20%20%3dsdfsdf?sd", "preload", "image"}, + {"< http://wut.com/%20%20%3dsdfsdf?sd>; rel=preload;as=whatever", true, + "http://wut.com/%20%20%3dsdfsdf?sd", "preload", "whatever"}, + {"</images/cat.jpg>; rel=prefetch;", true, "/images/cat.jpg", "prefetch", + ""}, + {"</images/cat.jpg>; rel=prefetch ;", true, "/images/cat.jpg", + "prefetch", ""}, + {"</images/ca,t.jpg>; rel=prefetch ;", true, "/images/ca,t.jpg", + "prefetch", ""}, + {"<simple.css>; rel=stylesheet; title=\"title with a DQUOTE and " + "backslash\"", + true, "simple.css", "stylesheet", ""}, + {"<simple.css>; rel=stylesheet; title=\"title with a DQUOTE \\\" and " + "backslash: \\\"", + true, "simple.css", "stylesheet", ""}, + {"<simple.css>; title=\"title with a DQUOTE \\\" and backslash: \"; " + "rel=stylesheet; ", + true, "simple.css", "stylesheet", ""}, + {"<simple.css>; title=\'title with a DQUOTE \\\' and backslash: \'; " + "rel=stylesheet; ", + true, "simple.css", "stylesheet", ""}, + {"<simple.css>; title=\"title with a DQUOTE \\\" and ;backslash,: \"; " + "rel=stylesheet; ", + true, "simple.css", "stylesheet", ""}, + {"<simple.css>; title=\"title with a DQUOTE \' and ;backslash,: \"; " + "rel=stylesheet; ", + true, "simple.css", "stylesheet", ""}, + {"<simple.css>; title=\"\"; rel=stylesheet; ", true, "simple.css", + "stylesheet", ""}, + {"<simple.css>; title=\"\"; rel=\"stylesheet\"; ", true, "simple.css", + "stylesheet", ""}, + {"<simple.css>; rel=stylesheet; title=\"", true, "simple.css", "stylesheet", + ""}, + {"<simple.css>; rel=stylesheet; title=\"\"", true, "simple.css", + "stylesheet", ""}, + {"<simple.css>; rel=\"stylesheet\"; title=\"", true, "simple.css", + "stylesheet", ""}, + {"<simple.css>; rel=\";style,sheet\"; title=\"", true, "simple.css", + ";style,sheet", ""}, + {"<simple.css>; rel=\"bla'sdf\"; title=\"", true, "simple.css", "bla'sdf", + ""}, + {"<simple.css>; rel=\"\"; title=\"\"", true, "simple.css", "", ""}, + {"<simple.css>; rel=''; title=\"\"", true, "simple.css", "", ""}, + {"<simple.css>; rel=''; bla", true, "simple.css", "", ""}, + {"<simple.css>; rel='prefetch", true, "simple.css", "prefetch", ""}, + {"<simple.css>; rel=\"prefetch", true, "simple.css", "prefetch", ""}, + {"<simple.css>; rel=\"", true, "simple.css", "", ""}, + {"simple.css; rel=prefetch", false}, + {"<simple.css>; rel=prefetch; rel=foobar", true, "simple.css", "prefetch", + ""}, +}; + +INSTANTIATE_TEST_CASE_P(LinkHeaderTest, + SimpleParseTest, + testing::ValuesIn(simple_parse_tests)); + +void SaveFoundRegistrationsCallback( + ServiceWorkerStatusCode expected_status, + bool* called, + std::vector<ServiceWorkerRegistrationInfo>* registrations, + ServiceWorkerStatusCode status, + const std::vector<ServiceWorkerRegistrationInfo>& result) { + EXPECT_EQ(expected_status, status); + *called = true; + *registrations = result; +} + +ServiceWorkerContextWrapper::GetRegistrationsInfosCallback +SaveFoundRegistrations( + ServiceWorkerStatusCode expected_status, + bool* called, + std::vector<ServiceWorkerRegistrationInfo>* registrations) { + *called = false; + return base::Bind(&SaveFoundRegistrationsCallback, expected_status, called, + registrations); +} + +class LinkHeaderServiceWorkerTest : public ::testing::Test { + public: + LinkHeaderServiceWorkerTest() + : thread_bundle_(TestBrowserThreadBundle::IO_MAINLOOP), + resource_context_(&request_context_) { + base::CommandLine::ForCurrentProcess()->AppendSwitch( + switches::kEnableExperimentalWebPlatformFeatures); + } + ~LinkHeaderServiceWorkerTest() override {} + + void SetUp() override { + helper_.reset(new EmbeddedWorkerTestHelper(base::FilePath())); + } + + void TearDown() override { helper_.reset(); } + + ServiceWorkerContextWrapper* context_wrapper() { + return helper_->context_wrapper(); + } + + void ProcessLinkHeader(const GURL& request_url, + const std::string& link_header) { + scoped_ptr<net::URLRequest> request = request_context_.CreateRequest( + request_url, net::DEFAULT_PRIORITY, &request_delegate_); + ResourceRequestInfo::AllocateForTesting( + request.get(), RESOURCE_TYPE_SCRIPT, &resource_context_, + -1 /* render_process_id */, -1 /* render_view_id */, + -1 /* render_frame_id */, false /* is_main_frame */, + false /* parent_is_main_frame */, true /* allow_download */, + true /* is_async */, false /* is_using_lofi */); + + ProcessLinkHeaderForRequest(request.get(), link_header, context_wrapper()); + base::RunLoop().RunUntilIdle(); + } + + std::vector<ServiceWorkerRegistrationInfo> GetRegistrations() { + bool called; + std::vector<ServiceWorkerRegistrationInfo> registrations; + context_wrapper()->GetAllRegistrations( + SaveFoundRegistrations(SERVICE_WORKER_OK, &called, ®istrations)); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(called); + return registrations; + } + + private: + TestBrowserThreadBundle thread_bundle_; + scoped_ptr<EmbeddedWorkerTestHelper> helper_; + net::TestURLRequestContext request_context_; + net::TestDelegate request_delegate_; + MockResourceContext resource_context_; +}; + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_Basic) { + ProcessLinkHeader(GURL("https://example.com/foo/bar/"), + "<../foo.js>; rel=serviceworker"); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(1u, registrations.size()); + EXPECT_EQ(GURL("https://example.com/foo/"), registrations[0].pattern); + EXPECT_EQ(GURL("https://example.com/foo/foo.js"), + registrations[0].active_version.script_url); +} + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_ScopeWithFragment) { + ProcessLinkHeader(GURL("https://example.com/foo/bar/"), + "<../bar.js>; rel=serviceworker; scope=\"scope#ref\""); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(1u, registrations.size()); + EXPECT_EQ(GURL("https://example.com/foo/bar/scope"), + registrations[0].pattern); + EXPECT_EQ(GURL("https://example.com/foo/bar.js"), + registrations[0].active_version.script_url); +} + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_ScopeAbsoluteUrl) { + ProcessLinkHeader(GURL("https://example.com/foo/bar/"), + "<bar.js>; rel=serviceworker; " + "scope=\"https://example.com:443/foo/bar/scope\""); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(1u, registrations.size()); + EXPECT_EQ(GURL("https://example.com/foo/bar/scope"), + registrations[0].pattern); + EXPECT_EQ(GURL("https://example.com/foo/bar/bar.js"), + registrations[0].active_version.script_url); +} + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_ScopeDifferentOrigin) { + ProcessLinkHeader( + GURL("https://example.com/foobar/"), + "<bar.js>; rel=serviceworker; scope=\"https://google.com/scope\""); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(0u, registrations.size()); +} + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_ScopeUrlEncodedSlash) { + ProcessLinkHeader(GURL("https://example.com/foobar/"), + "<bar.js>; rel=serviceworker; scope=\"./foo%2Fbar\""); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(0u, registrations.size()); +} + +TEST_F(LinkHeaderServiceWorkerTest, + InstallServiceWorker_ScriptUrlEncodedSlash) { + ProcessLinkHeader(GURL("https://example.com/foobar/"), + "<foo%2Fbar.js>; rel=serviceworker"); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(0u, registrations.size()); +} + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_ScriptAbsoluteUrl) { + ProcessLinkHeader( + GURL("https://example.com/foobar/"), + "<https://example.com/bar.js>; rel=serviceworker; scope=foo"); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(1u, registrations.size()); + EXPECT_EQ(GURL("https://example.com/foobar/foo"), registrations[0].pattern); + EXPECT_EQ(GURL("https://example.com/bar.js"), + registrations[0].active_version.script_url); +} + +TEST_F(LinkHeaderServiceWorkerTest, + InstallServiceWorker_ScriptDifferentOrigin) { + ProcessLinkHeader( + GURL("https://example.com/foobar/"), + "<https://google.com/bar.js>; rel=serviceworker; scope=foo"); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(0u, registrations.size()); +} + +TEST_F(LinkHeaderServiceWorkerTest, InstallServiceWorker_MultipleWorkers) { + ProcessLinkHeader(GURL("https://example.com/foobar/"), + "<bar.js>; rel=serviceworker; scope=foo, <baz.js>; " + "rel=serviceworker; scope=scope"); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(2u, registrations.size()); + EXPECT_EQ(GURL("https://example.com/foobar/foo"), registrations[0].pattern); + EXPECT_EQ(GURL("https://example.com/foobar/bar.js"), + registrations[0].active_version.script_url); + EXPECT_EQ(GURL("https://example.com/foobar/scope"), registrations[1].pattern); + EXPECT_EQ(GURL("https://example.com/foobar/baz.js"), + registrations[1].active_version.script_url); +} + +TEST_F(LinkHeaderServiceWorkerTest, + InstallServiceWorker_ValidAndInvalidValues) { + ProcessLinkHeader( + GURL("https://example.com/foobar/"), + "<https://google.com/bar.js>; rel=serviceworker; scope=foo, <baz.js>; " + "rel=serviceworker; scope=scope"); + + std::vector<ServiceWorkerRegistrationInfo> registrations = GetRegistrations(); + ASSERT_EQ(1u, registrations.size()); + EXPECT_EQ(GURL("https://example.com/foobar/scope"), registrations[0].pattern); + EXPECT_EQ(GURL("https://example.com/foobar/baz.js"), + registrations[0].active_version.script_url); +} + +} // namespace + +} // namespace content diff --git a/content/browser/service_worker/service_worker_dispatcher_host.cc b/content/browser/service_worker/service_worker_dispatcher_host.cc index 40a3220..7a6c482 100644 --- a/content/browser/service_worker/service_worker_dispatcher_host.cc +++ b/content/browser/service_worker/service_worker_dispatcher_host.cc @@ -51,23 +51,6 @@ const uint32_t kFilteredMessageClasses[] = { ServiceWorkerMsgStart, EmbeddedWorkerMsgStart, }; -bool AllOriginsMatch(const GURL& url_a, const GURL& url_b, const GURL& url_c) { - return url_a.GetOrigin() == url_b.GetOrigin() && - url_a.GetOrigin() == url_c.GetOrigin(); -} - -bool CanRegisterServiceWorker(const GURL& document_url, - const GURL& pattern, - const GURL& script_url) { - DCHECK(document_url.is_valid()); - DCHECK(pattern.is_valid()); - DCHECK(script_url.is_valid()); - return AllOriginsMatch(document_url, pattern, script_url) && - OriginCanAccessServiceWorkers(document_url) && - OriginCanAccessServiceWorkers(pattern) && - OriginCanAccessServiceWorkers(script_url); -} - bool CanUnregisterServiceWorker(const GURL& document_url, const GURL& pattern) { DCHECK(document_url.is_valid()); @@ -330,8 +313,8 @@ void ServiceWorkerDispatcherHost::OnRegisterServiceWorker( return; } - if (!CanRegisterServiceWorker( - provider_host->document_url(), pattern, script_url)) { + if (!ServiceWorkerUtils::CanRegisterServiceWorker( + provider_host->document_url(), pattern, script_url)) { bad_message::ReceivedBadMessage(this, bad_message::SWDH_REGISTER_CANNOT); return; } diff --git a/content/common/service_worker/service_worker_utils.cc b/content/common/service_worker/service_worker_utils.cc index 62f1975..31f1f30 100644 --- a/content/common/service_worker/service_worker_utils.cc +++ b/content/common/service_worker/service_worker_utils.cc @@ -8,6 +8,7 @@ #include "base/logging.h" #include "base/strings/string_util.h" +#include "content/public/common/origin_util.h" namespace content { @@ -30,6 +31,11 @@ bool PathContainsDisallowedCharacter(const GURL& url) { return false; } +bool AllOriginsMatch(const GURL& url_a, const GURL& url_b, const GURL& url_c) { + return url_a.GetOrigin() == url_b.GetOrigin() && + url_a.GetOrigin() == url_c.GetOrigin(); +} + } // namespace // static @@ -104,6 +110,19 @@ bool ServiceWorkerUtils::ContainsDisallowedCharacter( return false; } +// static +bool ServiceWorkerUtils::CanRegisterServiceWorker(const GURL& context_url, + const GURL& pattern, + const GURL& script_url) { + DCHECK(context_url.is_valid()); + DCHECK(pattern.is_valid()); + DCHECK(script_url.is_valid()); + return AllOriginsMatch(context_url, pattern, script_url) && + OriginCanAccessServiceWorkers(context_url) && + OriginCanAccessServiceWorkers(pattern) && + OriginCanAccessServiceWorkers(script_url); +} + bool LongestScopeMatcher::MatchLongest(const GURL& scope) { if (!ServiceWorkerUtils::ScopeMatches(scope, url_)) return false; diff --git a/content/common/service_worker/service_worker_utils.h b/content/common/service_worker/service_worker_utils.h index 8538b5e..5be59c0 100644 --- a/content/common/service_worker/service_worker_utils.h +++ b/content/common/service_worker/service_worker_utils.h @@ -40,6 +40,10 @@ class ServiceWorkerUtils { const GURL& script_url, std::string* error_message); + static bool CanRegisterServiceWorker(const GURL& context_url, + const GURL& pattern, + const GURL& script_url); + // PlzNavigate // Returns true if the |provider_id| was assigned by the browser process. static bool IsBrowserAssignedProviderId(int provider_id) { diff --git a/content/content_browser.gypi b/content/content_browser.gypi index 6d40b22..5d5e591 100644 --- a/content/content_browser.gypi +++ b/content/content_browser.gypi @@ -1384,6 +1384,8 @@ 'browser/service_worker/embedded_worker_registry.h', 'browser/service_worker/foreign_fetch_request_handler.cc', 'browser/service_worker/foreign_fetch_request_handler.h', + 'browser/service_worker/link_header_support.cc', + 'browser/service_worker/link_header_support.h', 'browser/service_worker/service_worker_cache_writer.cc', 'browser/service_worker/service_worker_cache_writer.h', 'browser/service_worker/service_worker_client_utils.cc', diff --git a/content/content_tests.gypi b/content/content_tests.gypi index fcc0c38..53b52ca 100644 --- a/content/content_tests.gypi +++ b/content/content_tests.gypi @@ -584,6 +584,7 @@ 'browser/service_worker/embedded_worker_instance_unittest.cc', 'browser/service_worker/embedded_worker_test_helper.cc', 'browser/service_worker/embedded_worker_test_helper.h', + 'browser/service_worker/link_header_support_unittest.cc', 'browser/service_worker/service_worker_cache_writer_unittest.cc', 'browser/service_worker/service_worker_context_request_handler_unittest.cc', 'browser/service_worker/service_worker_context_unittest.cc', diff --git a/net/url_request/url_request.cc b/net/url_request/url_request.cc index 9fa81a7..f52029a 100644 --- a/net/url_request/url_request.cc +++ b/net/url_request/url_request.cc @@ -370,7 +370,8 @@ UploadProgress URLRequest::GetUploadProgress() const { return job_->GetUploadProgress(); } -void URLRequest::GetResponseHeaderByName(const string& name, string* value) { +void URLRequest::GetResponseHeaderByName(const string& name, + string* value) const { DCHECK(value); if (response_info_.headers.get()) { response_info_.headers->GetNormalizedHeader(name, value); diff --git a/net/url_request/url_request.h b/net/url_request/url_request.h index 1a17470..d3b0b7a 100644 --- a/net/url_request/url_request.h +++ b/net/url_request/url_request.h @@ -435,7 +435,8 @@ class NET_EXPORT URLRequest : NON_EXPORTED_BASE(public base::NonThreadSafe), // that appear more than once in the response are coalesced, with values // separated by commas (per RFC 2616). This will not work with cookies since // comma can be used in cookie values. - void GetResponseHeaderByName(const std::string& name, std::string* value); + void GetResponseHeaderByName(const std::string& name, + std::string* value) const; // The time when |this| was constructed. base::TimeTicks creation_time() const { return creation_time_; } diff --git a/third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/register-link-header.html b/third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/register-link-header.html new file mode 100644 index 0000000..deacae0 --- /dev/null +++ b/third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/register-link-header.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- FIXME: Move this test out of chromium/ when PHP is no longer needed + to set the Link header (crbug.com/347864). +--> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-helpers.js"></script> +<body> +<script> +promise_test(function(t) { + var scope = normalizeURL('resources/blank.html?fetch'); + var header = '<empty-worker.js>; rel=serviceworker; scope="' + scope + '"'; + var resource = 'resources/link-header.php?Link=' + + encodeURIComponent(header); + return with_iframe(scope) + .then(frame => + Promise.all([frame.contentWindow.navigator.serviceWorker.ready, + fetch(resource)])) + .then(([registration, response]) => { + assert_equals(registration.scope, scope); + }); + }, 'fetch can trigger service worker installation'); + +promise_test(function(t) { + var scope = normalizeURL('resources/blank.html?iframe'); + var header = '<empty-worker.js>; rel=serviceworker; scope="' + scope + '"'; + var resource = 'resources/link-header.php?Link=' + + encodeURIComponent(header); + return with_iframe(scope) + .then(frame => + Promise.all([frame.contentWindow.navigator.serviceWorker.ready, + with_iframe(resource)])) + .then(([registration, frame]) => { + assert_equals(registration.scope, scope); + }); + }, 'An iframe can trigger service worker installation'); + +promise_test(function(t) { + var scope = normalizeURL('resources/blank.html?css'); + var header = '<empty-worker.js>; rel=serviceworker; scope="' + scope + '"'; + var resource = 'resources/link-header.php?Link=' + + encodeURIComponent(header); + return with_iframe(scope) + .then(frame => { + var link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('href', resource); + document.getElementsByTagName('head')[0].appendChild(link); + return frame.contentWindow.navigator.serviceWorker.ready; + }) + .then(registration => { + assert_equals(registration.scope, scope); + }); + }, 'A stylesheet can trigger service worker installation'); + +</script> diff --git a/third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/resources/link-header.php b/third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/resources/link-header.php new file mode 100644 index 0000000..3e5da8f --- /dev/null +++ b/third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/resources/link-header.php @@ -0,0 +1,3 @@ +<?php + header('Link: ' . $_GET['Link']); +?> |
