Home | History | Annotate | Download | only in server2
      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.motemplate import Motemplate
     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, Motemplate):
    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