summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormek <mek@chromium.org>2016-03-08 18:26:45 -0800
committerCommit bot <commit-bot@chromium.org>2016-03-09 02:28:21 +0000
commit493412af1482220d43ae3dce390ed09617b2d95a (patch)
tree84bd3292a490d02dd46d9af45524abcdecb30309
parent8a976c4e23265d7ad391101e7a7a0c18962d8113 (diff)
downloadchromium_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}
-rw-r--r--content/browser/loader/resource_dispatcher_host_impl.cc3
-rw-r--r--content/browser/service_worker/link_header_support.cc311
-rw-r--r--content/browser/service_worker/link_header_support.h38
-rw-r--r--content/browser/service_worker/link_header_support_unittest.cc410
-rw-r--r--content/browser/service_worker/service_worker_dispatcher_host.cc21
-rw-r--r--content/common/service_worker/service_worker_utils.cc19
-rw-r--r--content/common/service_worker/service_worker_utils.h4
-rw-r--r--content/content_browser.gypi2
-rw-r--r--content/content_tests.gypi1
-rw-r--r--net/url_request/url_request.cc3
-rw-r--r--net/url_request/url_request.h3
-rw-r--r--third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/register-link-header.html57
-rw-r--r--third_party/WebKit/LayoutTests/http/tests/serviceworker/chromium/resources/link-header.php3
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, &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
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, &params));
+ 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, &registrations));
+ 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']);
+?>