| # Copyright 2015 The Chromium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | import atexit | 
 | import datetime | 
 | import email.utils | 
 | import errno | 
 | import hashlib | 
 | import logging | 
 | import math | 
 | import os.path | 
 | import socket | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 | import threading | 
 |  | 
 | import SimpleHTTPServer | 
 | import SocketServer | 
 |  | 
 | _ZERO = datetime.timedelta(0) | 
 |  | 
 |  | 
 | class UTC_TZINFO(datetime.tzinfo): | 
 |   """UTC time zone representation.""" | 
 |  | 
 |   def utcoffset(self, _): | 
 |     return _ZERO | 
 |  | 
 |   def tzname(self, _): | 
 |     return "UTC" | 
 |  | 
 |   def dst(self, _): | 
 |      return _ZERO | 
 |  | 
 | _UTC = UTC_TZINFO() | 
 |  | 
 |  | 
 | def _gzip(file_path): | 
 |   """Gzips the given file storing the result in a temporary file. | 
 |  | 
 |   Returns: | 
 |     Path to the resulting file. | 
 |   """ | 
 |   gzipped_file = tempfile.NamedTemporaryFile(delete=False) | 
 |   try: | 
 |     subprocess.check_call(['gzip', '-c', file_path], | 
 |                           stdout=gzipped_file) | 
 |   except Exception: | 
 |     print ('http_server: call to gzip failed, make sure that ' | 
 |            'gzip is installed on the host.') | 
 |     raise | 
 |   gzipped_file.close() | 
 |   return gzipped_file.name | 
 |  | 
 |  | 
 | class _SilentTCPServer(SocketServer.TCPServer): | 
 |   """A TCPServer that won't display any error, unless debugging is enabled. This | 
 |   is useful because the client might stop while it is fetching an URL, which | 
 |   causes spurious error messages. | 
 |   """ | 
 |   allow_reuse_address = True | 
 |  | 
 |   def handle_error(self, request, client_address): | 
 |     """Override the base class method to have conditional logging.""" | 
 |     if logging.getLogger().isEnabledFor(logging.DEBUG): | 
 |       SocketServer.TCPServer.handle_error(self, request, client_address) | 
 |  | 
 |  | 
 | def _get_handler_class_for_path(mappings): | 
 |   """Creates a handler override for SimpleHTTPServer. | 
 |  | 
 |   Args: | 
 |     mappings: List of tuples (prefix, local_base_path_list) mapping URLs that | 
 |         start with |prefix| to one or more local directories enumerated in | 
 |         |local_base_path_list|. The prefixes should skip the leading slash. | 
 |         The first matching prefix and the first location that contains the | 
 |         requested file will be used each time. | 
 |   """ | 
 |   for prefix, _ in mappings: | 
 |     assert not prefix.startswith('/'), ('Prefixes for the http server mappings ' | 
 |                                         'should skip the leading slash.') | 
 |  | 
 |   class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): | 
 |     """Handler for SocketServer.TCPServer that will serve the files from | 
 |     local directiories over http. | 
 |  | 
 |     A new instance is created for each request. | 
 |     """ | 
 |  | 
 |     def __init__(self, *args, **kwargs): | 
 |       self.etag = None | 
 |       self.gzipped_file_name = None | 
 |       self.original_file_name = None | 
 |       SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) | 
 |  | 
 |     def get_etag(self): | 
 |       if self.etag: | 
 |         return self.etag | 
 |  | 
 |       path = self.translate_path(self.path, False) | 
 |       if not os.path.isfile(path): | 
 |         return None | 
 |  | 
 |       sha256 = hashlib.sha256() | 
 |       BLOCKSIZE = 65536 | 
 |       with open(path, 'rb') as hashed: | 
 |         buf = hashed.read(BLOCKSIZE) | 
 |         while len(buf) > 0: | 
 |           sha256.update(buf) | 
 |           buf = hashed.read(BLOCKSIZE) | 
 |       self.etag = '"%s"' % sha256.hexdigest() | 
 |       return self.etag | 
 |  | 
 |     def send_head(self): | 
 |       # Always close the connection after each request, as the keep alive | 
 |       # support from SimpleHTTPServer doesn't like when the client requests to | 
 |       # close the connection before downloading the full response content. | 
 |       # pylint: disable=W0201 | 
 |       self.close_connection = 1 | 
 |  | 
 |       path = self.translate_path(self.path, False) | 
 |       if os.path.isfile(path): | 
 |         # Handle If-None-Match | 
 |         etag = self.get_etag() | 
 |         if ('If-None-Match' in self.headers and | 
 |             etag == self.headers['If-None-Match']): | 
 |           self.send_response(304) | 
 |           return None | 
 |  | 
 |         # Handle If-Modified-Since | 
 |         if ('If-None-Match' not in self.headers and | 
 |             'If-Modified-Since' in self.headers): | 
 |           last_modified = datetime.datetime.fromtimestamp( | 
 |               math.floor(os.stat(path).st_mtime), tz=_UTC) | 
 |           ims = datetime.datetime( | 
 |               *email.utils.parsedate(self.headers['If-Modified-Since'])[:6], | 
 |               tzinfo=_UTC) | 
 |           if last_modified <= ims: | 
 |             self.send_response(304) | 
 |             return None | 
 |  | 
 |       return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) | 
 |  | 
 |     def end_headers(self): | 
 |       path = self.translate_path(self.path) | 
 |  | 
 |       if os.path.isfile(path): | 
 |         self.send_header('Content-Encoding', 'gzip') | 
 |         etag = self.get_etag() | 
 |         if etag: | 
 |           self.send_header('ETag', etag) | 
 |           self.send_header('Cache-Control', 'must-revalidate') | 
 |  | 
 |       return SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self) | 
 |  | 
 |     # pylint: disable=W0221 | 
 |     def translate_path(self, path, gzip=True): | 
 |       # Parent translate_path() will strip away the query string and fragment | 
 |       # identifier, but also will prepend the cwd to the path. relpath() gives | 
 |       # us the relative path back. | 
 |       normalized_path = os.path.relpath( | 
 |           SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, path)) | 
 |  | 
 |       for prefix, local_base_path_list in mappings: | 
 |         if not normalized_path.startswith(prefix): | 
 |           continue | 
 |  | 
 |         for local_base_path in local_base_path_list: | 
 |           candidate = os.path.join(local_base_path, | 
 |                                    normalized_path[len(prefix):]) | 
 |           if os.path.isfile(candidate): | 
 |             if gzip: | 
 |               if not self.gzipped_file_name: | 
 |                 self.original_file_name = candidate | 
 |                 self.gzipped_file_name = _gzip(candidate) | 
 |               return self.gzipped_file_name | 
 |             return candidate | 
 |         else: | 
 |           self.send_response(404) | 
 |           return None | 
 |       self.send_response(404) | 
 |       return None | 
 |  | 
 |     def guess_type(self, path): | 
 |       # This is needed so that exploded Sky apps without shebang can still run | 
 |       # thanks to content-type mappings. | 
 |       # TODO(ppi): drop this part once we can rely on the Sky files declaring | 
 |       # correct shebang. | 
 |       actual_path = self.original_file_name or path | 
 |  | 
 |       if actual_path.endswith('.dart'): | 
 |         return 'application/dart' | 
 |       return SimpleHTTPServer.SimpleHTTPRequestHandler.guess_type(self, | 
 |                                                                   actual_path) | 
 |  | 
 |     def log_message(self, *_): | 
 |       """Override the base class method to disable logging.""" | 
 |       pass | 
 |  | 
 |     def __del__(self): | 
 |       if self.gzipped_file_name: | 
 |         os.remove(self.gzipped_file_name) | 
 |  | 
 |   RequestHandler.protocol_version = 'HTTP/1.1' | 
 |   return RequestHandler | 
 |  | 
 |  | 
 | def start_http_server(mappings, host_port): | 
 |   """Starts an http server serving files from |local_dir_path| on |host_port|. | 
 |  | 
 |   Args: | 
 |     mappings: List of tuples (prefix, local_base_path_list) mapping URLs that | 
 |         start with |prefix| to one or more local directories enumerated in | 
 |         |local_base_path_list|. The prefixes should skip the leading slash. | 
 |         The first matching prefix and the first location that contains the | 
 |         requested file will be used each time. | 
 |     host_port: Port on the host machine to run the server on. Pass 0 to use a | 
 |         system-assigned port. | 
 |  | 
 |   Returns: | 
 |     Tuple of the server address and the port on which it runs. | 
 |   """ | 
 |   assert mappings | 
 |   handler_class = _get_handler_class_for_path(mappings) | 
 |  | 
 |   try: | 
 |     httpd = _SilentTCPServer(('127.0.0.1', host_port), handler_class) | 
 |     atexit.register(httpd.shutdown) | 
 |  | 
 |     http_thread = threading.Thread(target=httpd.serve_forever) | 
 |     http_thread.daemon = True | 
 |     http_thread.start() | 
 |     return httpd.server_address | 
 |   except socket.error as v: | 
 |     error_code = v[0] | 
 |     print 'Failed to start http server for %s on port %d: %s.' % ( | 
 |         str(mappings), host_port, os.strerror(error_code)) | 
 |     if error_code == errno.EADDRINUSE: | 
 |       if sys.platform == 'darwin': | 
 |         find_cmd = 'lsof -i :%d' % host_port | 
 |         terminate_cmd = ( | 
 |             'lsof -i :%d | grep LISTEN | awk \'{print $2}\' | xargs kill -9' | 
 |             % host_port) | 
 |       else: | 
 |         find_cmd = 'fuser %d/tcp' % host_port | 
 |         terminate_cmd = 'fuser -k %d/tcp' % host_port | 
 |       print ('  Run `%s` to find out which process is using the port;' | 
 |              % find_cmd) | 
 |       print ('  or `%s` terminate it.' % terminate_cmd) | 
 |     print '---' | 
 |     raise |