Home | History | Annotate | Download | only in webpagereplay
      1 #!/usr/bin/env python
      2 # Copyright 2010 Google Inc. All Rights Reserved.
      3 #
      4 # Licensed under the Apache License, Version 2.0 (the "License");
      5 # you may not use this file except in compliance with the License.
      6 # You may obtain a copy of the License at
      7 #
      8 #      http://www.apache.org/licenses/LICENSE-2.0
      9 #
     10 # Unless required by applicable law or agreed to in writing, software
     11 # distributed under the License is distributed on an "AS IS" BASIS,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 
     16 """Handle special HTTP requests.
     17 
     18 /web-page-replay-generate-[RESPONSE_CODE]
     19   - Return the given RESPONSE_CODE.
     20 /web-page-replay-post-image-[FILENAME]
     21   - Save the posted image to local disk.
     22 /web-page-replay-command-[record|replay|status]
     23   - Optional. Enable by calling custom_handlers.add_server_manager_handler(...).
     24   - Change the server mode to either record or replay.
     25     + When switching to record, the http_archive is cleared.
     26     + When switching to replay, the http_archive is maintained.
     27 """
     28 
     29 import base64
     30 import httparchive
     31 import json
     32 import logging
     33 import os
     34 
     35 COMMON_URL_PREFIX = '/web-page-replay-'
     36 COMMAND_URL_PREFIX = COMMON_URL_PREFIX + 'command-'
     37 GENERATOR_URL_PREFIX = COMMON_URL_PREFIX + 'generate-'
     38 POST_IMAGE_URL_PREFIX = COMMON_URL_PREFIX + 'post-image-'
     39 IMAGE_DATA_PREFIX = 'data:image/png;base64,'
     40 
     41 
     42 def SimpleResponse(status):
     43   """Return a ArchivedHttpResponse with |status| code and a simple text body."""
     44   return httparchive.create_response(status)
     45 
     46 
     47 def JsonResponse(data):
     48   """Return a ArchivedHttpResponse with |data| encoded as json in the body."""
     49   status = 200
     50   reason = 'OK'
     51   headers = [('content-type', 'application/json')]
     52   body = json.dumps(data)
     53   return httparchive.create_response(status, reason, headers, body)
     54 
     55 
     56 class CustomHandlers(object):
     57 
     58   def __init__(self, options, http_archive):
     59     """Initialize CustomHandlers.
     60 
     61     Args:
     62       options: original options passed to the server.
     63       http_archive: reference to the HttpArchive object.
     64     """
     65     self.server_manager = None
     66     self.options = options
     67     self.http_archive = http_archive
     68     self.handlers = [
     69         (GENERATOR_URL_PREFIX, self.get_generator_url_response_code)]
     70     # screenshot_dir is a path to which screenshots are saved.
     71     if options.screenshot_dir:
     72       if not os.path.exists(options.screenshot_dir):
     73         try:
     74           os.makedirs(options.screenshot_dir)
     75         except IOError:
     76           logging.error('Unable to create screenshot dir: %s',
     77                          options.screenshot_dir)
     78           options.screenshot_dir = None
     79       if options.screenshot_dir:
     80         self.screenshot_dir = options.screenshot_dir
     81         self.handlers.append(
     82             (POST_IMAGE_URL_PREFIX, self.handle_possible_post_image))
     83 
     84   def handle(self, request):
     85     """Dispatches requests to matching handlers.
     86 
     87     Args:
     88       request: an http request
     89     Returns:
     90       ArchivedHttpResponse or None.
     91     """
     92     for prefix, handler in self.handlers:
     93       if request.full_path.startswith(prefix):
     94         return handler(request, request.full_path[len(prefix):])
     95     return None
     96 
     97   def get_generator_url_response_code(self, request, url_suffix):
     98     """Parse special generator URLs for the embedded response code.
     99 
    100     Args:
    101       request: an ArchivedHttpRequest instance
    102       url_suffix: string that is after the handler prefix (e.g. 304)
    103     Returns:
    104       On a match, an ArchivedHttpResponse.
    105       Otherwise, None.
    106     """
    107     del request
    108     try:
    109       response_code = int(url_suffix)
    110       return SimpleResponse(response_code)
    111     except ValueError:
    112       return None
    113 
    114   def handle_possible_post_image(self, request, url_suffix):
    115     """If sent, saves embedded image to local directory.
    116 
    117     Expects a special url containing the filename. If sent, saves the base64
    118     encoded request body as a PNG image locally. This feature is enabled by
    119     passing in screenshot_dir to the initializer for this class.
    120 
    121     Args:
    122       request: an ArchivedHttpRequest instance
    123       url_suffix: string that is after the handler prefix (e.g. 'foo.png')
    124     Returns:
    125       On a match, an ArchivedHttpResponse.
    126       Otherwise, None.
    127     """
    128     basename = url_suffix
    129     if not basename:
    130       return None
    131 
    132     data = request.request_body
    133     if not data.startswith(IMAGE_DATA_PREFIX):
    134       logging.error('Unexpected image format for: %s', basename)
    135       return SimpleResponse(400)
    136 
    137     data = data[len(IMAGE_DATA_PREFIX):]
    138     png = base64.b64decode(data)
    139     filename = os.path.join(self.screenshot_dir,
    140                             '%s-%s.png' % (request.host, basename))
    141     if not os.access(self.screenshot_dir, os.W_OK):
    142       logging.error('Unable to write to: %s', filename)
    143       return SimpleResponse(400)
    144 
    145     with file(filename, 'w') as f:
    146       f.write(png)
    147     return SimpleResponse(200)
    148 
    149   def add_server_manager_handler(self, server_manager):
    150     """Add the ability to change the server mode (e.g. to record mode).
    151     Args:
    152       server_manager: a servermanager.ServerManager instance.
    153     """
    154     self.server_manager = server_manager
    155     self.handlers.append(
    156         (COMMAND_URL_PREFIX, self.handle_server_manager_command))
    157 
    158   def handle_server_manager_command(self, request, url_suffix):
    159     """Parse special URLs for the embedded server manager command.
    160 
    161     Clients like webpagetest.org can use URLs of this form to change
    162     the replay server from record mode to replay mode.
    163 
    164     This handler is not in the default list of handlers. Call
    165     add_server_manager_handler to add it.
    166 
    167     In the future, this could be expanded to save or serve archive files.
    168 
    169     Args:
    170       request: an ArchivedHttpRequest instance
    171       url_suffix: string that is after the handler prefix (e.g. 'record')
    172     Returns:
    173       On a match, an ArchivedHttpResponse.
    174       Otherwise, None.
    175     """
    176     command = url_suffix
    177     if command == 'record':
    178       self.server_manager.SetRecordMode()
    179       return SimpleResponse(200)
    180     elif command == 'replay':
    181       self.server_manager.SetReplayMode()
    182       return SimpleResponse(200)
    183     elif command == 'status':
    184       status = {}
    185       is_record_mode = self.server_manager.IsRecordMode()
    186       status['is_record_mode'] = is_record_mode
    187       status['options'] = json.loads(str(self.options))
    188       archive_stats = self.http_archive.stats()
    189       if archive_stats:
    190         status['archive_stats'] = json.loads(archive_stats)
    191       return JsonResponse(status)
    192     elif command == 'exit':
    193       self.server_manager.should_exit = True
    194       return SimpleResponse(200)
    195     elif command == 'log':
    196       logging.info('log command: %s', str(request.request_body)[:1000000])
    197       return SimpleResponse(200)
    198     return None
    199