diff options
author | kalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-02 23:33:16 +0000 |
---|---|---|
committer | kalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-02 23:33:16 +0000 |
commit | 4e749ee69699283124fceaea3fc1b026b32dd34d (patch) | |
tree | 2a8fca418b51a0770b5059dd36a03b0b121bf796 | |
parent | e831647ff8378fc444c5188c9a777e10b2adc079 (diff) | |
download | chromium_src-4e749ee69699283124fceaea3fc1b026b32dd34d.zip chromium_src-4e749ee69699283124fceaea3fc1b026b32dd34d.tar.gz chromium_src-4e749ee69699283124fceaea3fc1b026b32dd34d.tar.bz2 |
Docs: Use ETags.
BUG=270818
R=jyasskin@chromium.org
NOTRY=true
Review URL: https://codereview.chromium.org/218363002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@261222 0039d316-1c4b-4281-b951-d872f2087c98
9 files changed, 100 insertions, 18 deletions
diff --git a/chrome/common/extensions/docs/server2/app.yaml b/chrome/common/extensions/docs/server2/app.yaml index c3ddbcc..0df4ad1 100644 --- a/chrome/common/extensions/docs/server2/app.yaml +++ b/chrome/common/extensions/docs/server2/app.yaml @@ -1,5 +1,5 @@ application: chrome-apps-doc -version: 3-17-1 +version: 3-17-2 runtime: python27 api_version: 1 threadsafe: false diff --git a/chrome/common/extensions/docs/server2/content_provider.py b/chrome/common/extensions/docs/server2/content_provider.py index 2a6de2a..8dd963a 100644 --- a/chrome/common/extensions/docs/server2/content_provider.py +++ b/chrome/common/extensions/docs/server2/content_provider.py @@ -27,9 +27,10 @@ class ContentAndType(object): '''Return value from ContentProvider.GetContentAndType. ''' - def __init__(self, content, content_type): + def __init__(self, content, content_type, version): self.content = content self.content_type = content_type + self.version = version class ContentProvider(object): @@ -100,7 +101,9 @@ class ContentProvider(object): content = ToUnicode(text) else: content = text - return ContentAndType(content, mimetype) + return ContentAndType(content, + mimetype, + self.file_system.Stat(path).version) def GetCanonicalPath(self, path): '''Gets the canonical location of |path|. This class is tolerant of @@ -133,7 +136,7 @@ class ContentProvider(object): if self._directory_zipper and ext == '.zip': zip_future = self._directory_zipper.Zip(ToDirectory(base)) return Future(callback= - lambda: ContentAndType(zip_future.Get(), 'application/zip')) + lambda: ContentAndType(zip_future.Get(), 'application/zip', None)) # If there is no file extension, look for a file with one of the default # extensions. diff --git a/chrome/common/extensions/docs/server2/cron.yaml b/chrome/common/extensions/docs/server2/cron.yaml index 85fb013..b503692 100644 --- a/chrome/common/extensions/docs/server2/cron.yaml +++ b/chrome/common/extensions/docs/server2/cron.yaml @@ -2,4 +2,4 @@ cron: - description: Repopulates all cached data. url: /_cron schedule: every 5 minutes - target: 3-17-1 + target: 3-17-2 diff --git a/chrome/common/extensions/docs/server2/local_renderer.py b/chrome/common/extensions/docs/server2/local_renderer.py index 96b15f1..ef43dcd 100644 --- a/chrome/common/extensions/docs/server2/local_renderer.py +++ b/chrome/common/extensions/docs/server2/local_renderer.py @@ -14,7 +14,7 @@ class LocalRenderer(object): '''Renders pages fetched from the local file system. ''' @staticmethod - def Render(path): + def Render(path, headers=None): assert not '\\' in path - return RenderServlet(Request.ForTest(path), + return RenderServlet(Request.ForTest(path, headers=headers), _LocalRenderServletDelegate()).Get() diff --git a/chrome/common/extensions/docs/server2/patch_servlet_test.py b/chrome/common/extensions/docs/server2/patch_servlet_test.py index 0dcebaa..11d0289 100755 --- a/chrome/common/extensions/docs/server2/patch_servlet_test.py +++ b/chrome/common/extensions/docs/server2/patch_servlet_test.py @@ -73,8 +73,9 @@ class PatchServletTest(unittest.TestCase): ''' patched_response = self._RenderWithPatch(path, issue) unpatched_response = self._RenderWithoutPatch(path) - patched_response.headers.pop('cache-control', None) - unpatched_response.headers.pop('cache-control', None) + for header in ('Cache-Control', 'ETag'): + patched_response.headers.pop(header, None) + unpatched_response.headers.pop(header, None) unpatched_content = unpatched_response.content.ToString() # Check that all links in the patched content are qualified with diff --git a/chrome/common/extensions/docs/server2/preview.py b/chrome/common/extensions/docs/server2/preview.py index 1d10205..c2c3bfe 100755 --- a/chrome/common/extensions/docs/server2/preview.py +++ b/chrome/common/extensions/docs/server2/preview.py @@ -47,7 +47,8 @@ class _RequestHandler(BaseHTTPRequestHandler): '/favicon.ico': '../../server2/chrome-32.ico', '/apple-touch-icon-precomposed.png': '../../server2/chrome-128.png' }.get(self.path, self.path) - response = LocalRenderer.Render(self.path) + response = LocalRenderer.Render(self.path, headers=dict(self.headers)) + self.protocol_version = 'HTTP/1.1' self.send_response(response.status) for k, v in response.headers.iteritems(): self.send_header(k, v) diff --git a/chrome/common/extensions/docs/server2/render_servlet.py b/chrome/common/extensions/docs/server2/render_servlet.py index c714b5d..3c783d4 100644 --- a/chrome/common/extensions/docs/server2/render_servlet.py +++ b/chrome/common/extensions/docs/server2/render_servlet.py @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import hashlib import logging import posixpath import traceback @@ -15,12 +16,16 @@ from special_paths import SITE_VERIFICATION_FILE from third_party.handlebar import Handlebar -def _MakeHeaders(content_type): - return { - 'X-Frame-Options': 'sameorigin', +def _MakeHeaders(content_type, etag=None): + headers = { + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1. + 'Cache-Control': 'public, max-age=0, no-cache', 'Content-Type': content_type, - 'Cache-Control': 'max-age=300', + 'X-Frame-Options': 'sameorigin', } + if etag is not None: + headers['ETag'] = etag + return headers class RenderServlet(Servlet): @@ -122,10 +127,28 @@ class RenderServlet(Servlet): if warnings: sep = '\n - ' logging.warning('Rendering %s:%s%s' % (path, sep, sep.join(warnings))) + # Content was dynamic. The new etag is a hash of the content. + etag = None + elif content_and_type.version is not None: + # Content was static. The new etag is the version of the content. + etag = '"%s"' % content_and_type.version + else: + # Sometimes non-dynamic content does not have a version, for example + # .zip files. The new etag is a hash of the content. + etag = None content_type = content_and_type.content_type if isinstance(content, unicode): content = content.encode('utf-8') content_type += '; charset=utf-8' - return Response.Ok(content, headers=_MakeHeaders(content_type)) + if etag is None: + # Note: we're using md5 as a convenient and fast-enough way to identify + # content. It's not intended to be cryptographic in any way, and this + # is *not* what etags is for. That's what SSL is for, this is unrelated. + etag = '"%s"' % hashlib.md5(content).hexdigest() + + headers = _MakeHeaders(content_type, etag=etag) + if etag == self._request.headers.get('If-None-Match'): + return Response.NotModified('Not Modified', headers=headers) + return Response.Ok(content, headers=headers) diff --git a/chrome/common/extensions/docs/server2/render_servlet_test.py b/chrome/common/extensions/docs/server2/render_servlet_test.py index 1857028..5ab95a9 100755 --- a/chrome/common/extensions/docs/server2/render_servlet_test.py +++ b/chrome/common/extensions/docs/server2/render_servlet_test.py @@ -19,8 +19,8 @@ class _RenderServletDelegate(RenderServlet.Delegate): class RenderServletTest(unittest.TestCase): - def _Render(self, path): - return RenderServlet(Request.ForTest(path), + def _Render(self, path, headers=None): + return RenderServlet(Request.ForTest(path, headers=headers), _RenderServletDelegate()).Get() def testExtensionAppRedirect(self): @@ -110,6 +110,35 @@ class RenderServletTest(unittest.TestCase): # whether or not that exists. self.assertEqual(('/dir', False), self._Render('dir/').GetRedirect()) + def testEtags(self): + def test_path(path, content_type): + # Render without etag. + response = self._Render(path) + self.assertEqual(200, response.status) + etag = response.headers.get('ETag') + self.assertTrue(etag is not None) + + # Render with an If-None-Match which doesn't match. + response = self._Render(path, headers={ + 'If-None-Match': '"fake etag"', + }) + self.assertEqual(200, response.status) + self.assertEqual(content_type, response.headers.get('Content-Type')) + self.assertEqual(etag, response.headers.get('ETag')) + + # Render with the correct matching If-None-Match. + response = self._Render(path, headers={ + 'If-None-Match': etag, + }) + self.assertEqual(304, response.status) + self.assertEqual('Not Modified', response.content.ToString()) + self.assertEqual(content_type, response.headers.get('Content-Type')) + self.assertEqual(etag, response.headers.get('ETag')) + + # Test with a static path and a dynamic path. + test_path('static/css/out/site.css', 'text/css; charset=utf-8') + test_path('extensions/storage', 'text/html; charset=utf-8') + if __name__ == '__main__': unittest.main() diff --git a/chrome/common/extensions/docs/server2/servlet.py b/chrome/common/extensions/docs/server2/servlet.py index f7c14e7..7672ca5 100644 --- a/chrome/common/extensions/docs/server2/servlet.py +++ b/chrome/common/extensions/docs/server2/servlet.py @@ -2,13 +2,34 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. + +class RequestHeaders(object): + '''A custom dictionary impementation for headers which ignores the case + of requests, since different HTTP libraries seem to mangle them. + ''' + def __init__(self, dict_): + if isinstance(dict_, RequestHeaders): + self._dict = dict_ + else: + self._dict = dict((k.lower(), v) for k, v in dict_.iteritems()) + + def get(self, key, default=None): + return self._dict.get(key.lower(), default) + + def __repr__(self): + return repr(self._dict) + + def __str__(self): + return repr(self._dict) + + class Request(object): '''Request data. ''' def __init__(self, path, host, headers): self.path = path.lstrip('/') self.host = host.rstrip('/') - self.headers = headers + self.headers = RequestHeaders(headers) @staticmethod def ForTest(path, host='http://developer.chrome.com', headers=None): @@ -77,6 +98,10 @@ class Response(object): return Response(content=content, headers=headers, status=404) @staticmethod + def NotModified(content, headers=None): + return Response(content=content, headers=headers, status=304) + + @staticmethod def InternalError(content, headers=None): '''Returns an internal error (500) response. ''' |