summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-04-02 23:33:16 +0000
committerkalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-04-02 23:33:16 +0000
commit4e749ee69699283124fceaea3fc1b026b32dd34d (patch)
tree2a8fca418b51a0770b5059dd36a03b0b121bf796
parente831647ff8378fc444c5188c9a777e10b2adc079 (diff)
downloadchromium_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
-rw-r--r--chrome/common/extensions/docs/server2/app.yaml2
-rw-r--r--chrome/common/extensions/docs/server2/content_provider.py9
-rw-r--r--chrome/common/extensions/docs/server2/cron.yaml2
-rw-r--r--chrome/common/extensions/docs/server2/local_renderer.py4
-rwxr-xr-xchrome/common/extensions/docs/server2/patch_servlet_test.py5
-rwxr-xr-xchrome/common/extensions/docs/server2/preview.py3
-rw-r--r--chrome/common/extensions/docs/server2/render_servlet.py33
-rwxr-xr-xchrome/common/extensions/docs/server2/render_servlet_test.py33
-rw-r--r--chrome/common/extensions/docs/server2/servlet.py27
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.
'''