mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
224 lines
7.4 KiB
Python
224 lines
7.4 KiB
Python
# 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 gzip
|
|
import hashlib
|
|
import logging
|
|
import math
|
|
import os.path
|
|
import shutil
|
|
import socket
|
|
import threading
|
|
import tempfile
|
|
|
|
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()
|
|
|
|
|
|
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 = 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)
|
|
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, gzipped=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 normalized_path.startswith(prefix):
|
|
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 gzipped:
|
|
if not self.gzipped_file:
|
|
self.gzipped_file = tempfile.NamedTemporaryFile(delete=False)
|
|
with open(candidate, 'rb') as source:
|
|
with gzip.GzipFile(fileobj=self.gzipped_file) as target:
|
|
shutil.copyfileobj(source, target)
|
|
self.gzipped_file.close()
|
|
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.
|
|
if path.endswith('.dart') or path.endswith('.dart.gz'):
|
|
return 'application/dart'
|
|
return SimpleHTTPServer.SimpleHTTPRequestHandler.guess_type(self, path)
|
|
|
|
def log_message(self, *_):
|
|
"""Override the base class method to disable logging."""
|
|
pass
|
|
|
|
def __del__(self):
|
|
if self.gzipped_file:
|
|
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:
|
|
print (' Run `fuser %d/tcp` to find out which process is using the port.'
|
|
% host_port)
|
|
print '---'
|
|
raise
|