- add sources.
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / memory_cache_http_server.py
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.
4
5 import BaseHTTPServer
6 from collections import namedtuple
7 import gzip
8 import mimetypes
9 import os
10 import SimpleHTTPServer
11 import SocketServer
12 import StringIO
13 import sys
14
15
16 ByteRange = namedtuple('ByteRange', ['from_byte', 'to_byte'])
17 ResourceAndRange = namedtuple('ResourceAndRange', ['resource', 'byte_range'])
18
19
20 class MemoryCacheHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
21
22   def do_GET(self):
23     """Serve a GET request."""
24     resource_range = self.SendHead()
25
26     if not resource_range or not resource_range.resource:
27       return
28     response = resource_range.resource['response']
29
30     if not resource_range.byte_range:
31       self.wfile.write(response)
32       return
33
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])
37
38   def do_HEAD(self):
39     """Serve a HEAD request."""
40     self.SendHead()
41
42   def SendHead(self):
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')
46       return None
47
48     resource = self.server.resource_map[path]
49     total_num_of_bytes = resource['content-length']
50     byte_range = self.GetByteRange(total_num_of_bytes)
51     if byte_range:
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,
56                                            byte_range.to_byte,
57                                            total_num_of_bytes))
58       total_num_of_bytes = byte_range.to_byte - byte_range.from_byte + 1
59     else:
60       self.send_response(200)
61
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')
68     self.end_headers()
69     return ResourceAndRange(resource, byte_range)
70
71   def GetByteRange(self, total_num_of_bytes):
72     """Parse the header and get the range values specified.
73
74     Args:
75       total_num_of_bytes: Total # of bytes in requested resource,
76       used to calculate upper range limit.
77     Returns:
78       A ByteRange namedtuple object with the requested byte-range values.
79       If no Range is explicitly requested or there is a failure parsing,
80       return None.
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.
84     """
85
86     range_header = self.headers.getheader('Range')
87     if range_header is None:
88       return None
89     if not range_header.startswith('bytes='):
90       return None
91
92     # The range header is expected to be a string in this format:
93     # bytes=0-1
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('-')
97     from_byte = 0
98     to_byte = 0
99
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)
107     else:
108       return None
109
110     # Do some validation.
111     if from_byte < 0:
112       return None
113
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
117
118     return ByteRange(from_byte, to_byte)
119
120
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
128
129   def __init__(self, host_port, handler, paths):
130     BaseHTTPServer.HTTPServer.__init__(self, host_port, handler)
131     self.resource_map = {}
132     for path in paths:
133       if os.path.isdir(path):
134         self.AddDirectoryToResourceMap(path)
135       else:
136         self.AddFileToResourceMap(path)
137
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] != '.']
144
145       for f in files:
146         file_path = os.path.join(root, f)
147         if not os.path.exists(file_path):  # Allow for '.#' files
148           continue
149         self.AddFileToResourceMap(file_path)
150
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:
155       return
156
157     with open(file_path, 'rb') as fd:
158       response = fd.read()
159       fs = os.fstat(fd.fileno())
160     content_type = mimetypes.guess_type(file_path)[0]
161     zipped = False
162     if content_type in ['text/html', 'text/css', 'application/javascript']:
163       zipped = True
164       sio = StringIO.StringIO()
165       gzf = gzip.GzipFile(fileobj=sio, compresslevel=9, mode='wb')
166       gzf.write(response)
167       gzf.close()
168       response = sio.getvalue()
169       sio.close()
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,
175         'zipped': zipped
176         }
177
178     index = 'index.html'
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]
182
183
184 def _PrintUsageAndExit():
185   print >> sys.stderr, 'usage: %prog <port> [<path1>, <path2>, ...]'
186   sys.exit(1)
187
188
189 def Main():
190   if len(sys.argv) < 3:
191     _PrintUsageAndExit()
192
193   port = sys.argv[1]
194   paths = sys.argv[2:]
195
196   try:
197     port = int(port)
198   except ValueError:
199     _PrintUsageAndExit()
200   for path in paths:
201     if not os.path.realpath(path).startswith(os.path.realpath(os.getcwd())):
202       print >> sys.stderr, '"%s" is not under the cwd.' % path
203       sys.exit(1)
204
205   server_address = ('127.0.0.1', port)
206   MemoryCacheHTTPRequestHandler.protocol_version = 'HTTP/1.1'
207   httpd = MemoryCacheHTTPServer(server_address, MemoryCacheHTTPRequestHandler,
208                                 paths)
209   httpd.serve_forever()
210
211
212 if __name__ == '__main__':
213   Main()