1 # Copyright 2013 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import hashlib 6 import logging 7 import posixpath 8 import traceback 9 10 from branch_utility import BranchUtility 11 from environment import IsPreviewServer 12 from file_system import FileNotFoundError 13 from redirector import Redirector 14 from servlet import Servlet, Response 15 from special_paths import SITE_VERIFICATION_FILE 16 from third_party.handlebar import Handlebar 17 18 19 def _MakeHeaders(content_type, etag=None): 20 headers = { 21 # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1. 22 'Cache-Control': 'public, max-age=0, no-cache', 23 'Content-Type': content_type, 24 'X-Frame-Options': 'sameorigin', 25 } 26 if etag is not None: 27 headers['ETag'] = etag 28 return headers 29 30 31 class RenderServlet(Servlet): 32 '''Servlet which renders templates. 33 ''' 34 35 class Delegate(object): 36 def CreateServerInstance(self): 37 raise NotImplementedError(self.__class__) 38 39 def __init__(self, request, delegate): 40 Servlet.__init__(self, request) 41 self._delegate = delegate 42 43 def Get(self): 44 ''' Render the page for a request. 45 ''' 46 path = self._request.path.lstrip('/') 47 48 # The server used to be partitioned based on Chrome channel, but it isn't 49 # anymore. Redirect from the old state. 50 channel_name, path = BranchUtility.SplitChannelNameFromPath(path) 51 if channel_name is not None: 52 return Response.Redirect('/' + path, permanent=True) 53 54 server_instance = self._delegate.CreateServerInstance() 55 56 try: 57 return self._GetSuccessResponse(path, server_instance) 58 except FileNotFoundError: 59 # Find the closest 404.html file and serve that, e.g. if the path is 60 # extensions/manifest/typo.html then first look for 61 # extensions/manifest/404.html, then extensions/404.html, then 404.html. 62 # 63 # Failing that just print 'Not Found' but that should preferrably never 64 # happen, because it would look really bad. 65 path_components = path.split('/') 66 for i in xrange(len(path_components) - 1, -1, -1): 67 try: 68 path_404 = posixpath.join(*(path_components[0:i] + ['404'])) 69 response = self._GetSuccessResponse(path_404, server_instance) 70 if response.status != 200: 71 continue 72 return Response.NotFound(response.content.ToString(), 73 headers=response.headers) 74 except FileNotFoundError: continue 75 logging.warning('No 404.html found in %s' % path) 76 return Response.NotFound('Not Found', headers=_MakeHeaders('text/plain')) 77 78 def _GetSuccessResponse(self, request_path, server_instance): 79 '''Returns the Response from trying to render |path| with 80 |server_instance|. If |path| isn't found then a FileNotFoundError will be 81 raised, such that the only responses that will be returned from this method 82 are Ok and Redirect. 83 ''' 84 content_provider, serve_from, path = ( 85 server_instance.content_providers.GetByServeFrom(request_path)) 86 assert content_provider, 'No ContentProvider found for %s' % path 87 88 redirect = Redirector( 89 server_instance.compiled_fs_factory, 90 content_provider.file_system).Redirect(self._request.host, path) 91 if redirect is not None: 92 # Absolute redirects stay absolute, relative redirects are relative to 93 # |serve_from|; all redirects eventually need to be *served* as absolute. 94 if not redirect.startswith(('/', 'http://', 'https://')): 95 redirect = '/' + posixpath.join(serve_from, redirect) 96 return Response.Redirect(redirect, permanent=False) 97 98 canonical_path = content_provider.GetCanonicalPath(path) 99 if canonical_path != path: 100 redirect_path = posixpath.join(serve_from, canonical_path) 101 return Response.Redirect('/' + redirect_path, permanent=False) 102 103 if request_path.endswith('/'): 104 # Directory request hasn't been redirected by now. Default behaviour is 105 # to redirect as though it were a file. 106 return Response.Redirect('/' + request_path.rstrip('/'), 107 permanent=False) 108 109 content_and_type = content_provider.GetContentAndType(path).Get() 110 if not content_and_type.content: 111 logging.error('%s had empty content' % path) 112 113 content = content_and_type.content 114 if isinstance(content, Handlebar): 115 template_content, template_warnings = ( 116 server_instance.template_renderer.Render(content, self._request)) 117 # HACK: the site verification file (google2ed...) doesn't have a title. 118 content, doc_warnings = server_instance.document_renderer.Render( 119 template_content, 120 path, 121 render_title=path != SITE_VERIFICATION_FILE) 122 warnings = template_warnings + doc_warnings 123 if warnings: 124 sep = '\n - ' 125 logging.warning('Rendering %s:%s%s' % (path, sep, sep.join(warnings))) 126 # Content was dynamic. The new etag is a hash of the content. 127 etag = None 128 elif content_and_type.version is not None: 129 # Content was static. The new etag is the version of the content. Hash it 130 # to make sure it's valid. 131 etag = '"%s"' % hashlib.md5(str(content_and_type.version)).hexdigest() 132 else: 133 # Sometimes non-dynamic content does not have a version, for example 134 # .zip files. The new etag is a hash of the content. 135 etag = None 136 137 content_type = content_and_type.content_type 138 if isinstance(content, unicode): 139 content = content.encode('utf-8') 140 content_type += '; charset=utf-8' 141 142 if etag is None: 143 # Note: we're using md5 as a convenient and fast-enough way to identify 144 # content. It's not intended to be cryptographic in any way, and this 145 # is *not* what etags is for. That's what SSL is for, this is unrelated. 146 etag = '"%s"' % hashlib.md5(content).hexdigest() 147 148 headers = _MakeHeaders(content_type, etag=etag) 149 if etag == self._request.headers.get('If-None-Match'): 150 return Response.NotModified('Not Modified', headers=headers) 151 return Response.Ok(content, headers=headers) 152