1 # Copyright (c) 2012 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.
6 from collections import namedtuple
10 import SimpleHTTPServer
16 ByteRange = namedtuple('ByteRange', ['from_byte', 'to_byte'])
17 ResourceAndRange = namedtuple('ResourceAndRange', ['resource', 'byte_range'])
20 class MemoryCacheHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
23 """Serve a GET request."""
24 resource_range = self.SendHead()
26 if not resource_range or not resource_range.resource:
28 response = resource_range.resource['response']
30 if not resource_range.byte_range:
31 self.wfile.write(response)
34 start_index = resource_range.byte_range.from_byte
35 end_index = resource_range.byte_range.to_byte
36 self.wfile.write(response[start_index:end_index + 1])
39 """Serve a HEAD request."""
43 path = os.path.realpath(self.translate_path(self.path))
44 if path not in self.server.resource_map:
45 self.send_error(404, 'File not found')
48 resource = self.server.resource_map[path]
49 total_num_of_bytes = resource['content-length']
50 byte_range = self.GetByteRange(total_num_of_bytes)
52 # request specified a range, so set response code to 206.
53 self.send_response(206)
54 self.send_header('Content-Range',
55 'bytes %d-%d/%d' % (byte_range.from_byte,
58 total_num_of_bytes = byte_range.to_byte - byte_range.from_byte + 1
60 self.send_response(200)
62 self.send_header('Content-Length', str(total_num_of_bytes))
63 self.send_header('Content-Type', resource['content-type'])
64 self.send_header('Last-Modified',
65 self.date_time_string(resource['last-modified']))
66 if resource['zipped']:
67 self.send_header('Content-Encoding', 'gzip')
69 return ResourceAndRange(resource, byte_range)
71 def GetByteRange(self, total_num_of_bytes):
72 """Parse the header and get the range values specified.
75 total_num_of_bytes: Total # of bytes in requested resource,
76 used to calculate upper range limit.
78 A ByteRange namedtuple object with the requested byte-range values.
79 If no Range is explicitly requested or there is a failure parsing,
81 If range specified is in the format "N-", return N-END. Refer to
82 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for details.
83 If upper range limit is greater than total # of bytes, return upper index.
86 range_header = self.headers.getheader('Range')
87 if range_header is None:
89 if not range_header.startswith('bytes='):
92 # The range header is expected to be a string in this format:
94 # Get the upper and lower limits of the specified byte-range.
95 # We've already confirmed that range_header starts with 'bytes='.
96 byte_range_values = range_header[len('bytes='):].split('-')
100 if len(byte_range_values) == 2:
101 # If to_range is not defined return all bytes starting from from_byte.
102 to_byte = (int(byte_range_values[1]) if byte_range_values[1]
103 else total_num_of_bytes - 1)
104 # If from_range is not defined return last 'to_byte' bytes.
105 from_byte = (int(byte_range_values[0]) if byte_range_values[0]
106 else total_num_of_bytes - to_byte)
110 # Do some validation.
114 # Make to_byte the end byte by default in edge cases.
115 if to_byte < from_byte or to_byte >= total_num_of_bytes:
116 to_byte = total_num_of_bytes - 1
118 return ByteRange(from_byte, to_byte)
121 class MemoryCacheHTTPServer(SocketServer.ThreadingMixIn,
122 BaseHTTPServer.HTTPServer):
123 # Increase the request queue size. The default value, 5, is set in
124 # SocketServer.TCPServer (the parent of BaseHTTPServer.HTTPServer).
125 # Since we're intercepting many domains through this single server,
126 # it is quite possible to get more than 5 concurrent requests.
127 request_queue_size = 128
129 def __init__(self, host_port, handler, paths):
130 BaseHTTPServer.HTTPServer.__init__(self, host_port, handler)
131 self.resource_map = {}
133 if os.path.isdir(path):
134 self.AddDirectoryToResourceMap(path)
136 self.AddFileToResourceMap(path)
138 def AddDirectoryToResourceMap(self, directory_path):
139 """Loads all files in directory_path into the in-memory resource map."""
140 for root, dirs, files in os.walk(directory_path):
141 # Skip hidden files and folders (like .svn and .git).
142 files = [f for f in files if f[0] != '.']
143 dirs[:] = [d for d in dirs if d[0] != '.']
146 file_path = os.path.join(root, f)
147 if not os.path.exists(file_path): # Allow for '.#' files
149 self.AddFileToResourceMap(file_path)
151 def AddFileToResourceMap(self, file_path):
152 """Loads file_path into the in-memory resource map."""
153 file_path = os.path.realpath(file_path)
154 if file_path in self.resource_map:
157 with open(file_path, 'rb') as fd:
159 fs = os.fstat(fd.fileno())
160 content_type = mimetypes.guess_type(file_path)[0]
162 if content_type in ['text/html', 'text/css', 'application/javascript']:
164 sio = StringIO.StringIO()
165 gzf = gzip.GzipFile(fileobj=sio, compresslevel=9, mode='wb')
168 response = sio.getvalue()
170 self.resource_map[file_path] = {
171 'content-type': content_type,
172 'content-length': len(response),
173 'last-modified': fs.st_mtime,
174 'response': response,
179 if os.path.basename(file_path) == index:
180 dir_path = os.path.dirname(file_path)
181 self.resource_map[dir_path] = self.resource_map[file_path]
184 def _PrintUsageAndExit():
185 print >> sys.stderr, 'usage: %prog <port> [<path1>, <path2>, ...]'
190 if len(sys.argv) < 3:
201 if not os.path.realpath(path).startswith(os.path.realpath(os.getcwd())):
202 print >> sys.stderr, '"%s" is not under the cwd.' % path
205 server_address = ('127.0.0.1', port)
206 MemoryCacheHTTPRequestHandler.protocol_version = 'HTTP/1.1'
207 httpd = MemoryCacheHTTPServer(server_address, MemoryCacheHTTPRequestHandler,
209 httpd.serve_forever()
212 if __name__ == '__main__':