blob: 2de71e1332663f1c0963d0f46b7518a7f94feadd [file] [log] [blame]
# 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=0):
"""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