1# Copyright (c) 2011 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 5import BaseHTTPServer 6import cgi 7import mimetypes 8import os 9import os.path 10import posixpath 11import SimpleHTTPServer 12import SocketServer 13import threading 14import time 15import urllib 16import urlparse 17 18class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 19 20 def NormalizePath(self, path): 21 path = path.split('?', 1)[0] 22 path = path.split('#', 1)[0] 23 path = posixpath.normpath(urllib.unquote(path)) 24 words = path.split('/') 25 26 bad = set((os.curdir, os.pardir, '')) 27 words = [word for word in words if word not in bad] 28 # The path of the request should always use POSIX-style path separators, so 29 # that the filename input of --map_file can be a POSIX-style path and still 30 # match correctly in translate_path(). 31 return '/'.join(words) 32 33 def translate_path(self, path): 34 path = self.NormalizePath(path) 35 if path in self.server.file_mapping: 36 return self.server.file_mapping[path] 37 for extra_dir in self.server.serving_dirs: 38 # TODO(halyavin): set allowed paths in another parameter? 39 full_path = os.path.join(extra_dir, os.path.basename(path)) 40 if os.path.isfile(full_path): 41 return full_path 42 43 # Try the complete relative path, not just a basename. This allows the 44 # user to serve everything recursively under extra_dir, not just one 45 # level deep. 46 # 47 # One use case for this is the Native Client SDK examples. The examples 48 # expect to be able to access files as relative paths from the root of 49 # the example directory. 50 # Sometimes two subdirectories contain files with the same name, so 51 # including all subdirectories in self.server.serving_dirs will not do 52 # the correct thing; (i.e. the wrong file will be chosen, even though the 53 # correct path was given). 54 full_path = os.path.join(extra_dir, path) 55 if os.path.isfile(full_path): 56 return full_path 57 if not path.endswith('favicon.ico') and not self.server.allow_404: 58 self.server.listener.ServerError('Cannot find file \'%s\'' % path) 59 return path 60 61 def guess_type(self, path): 62 # We store the extension -> MIME type mapping in the server instead of the 63 # request handler so we that can add additional mapping entries via the 64 # command line. 65 base, ext = posixpath.splitext(path) 66 if ext in self.server.extensions_mapping: 67 return self.server.extensions_mapping[ext] 68 ext = ext.lower() 69 if ext in self.server.extensions_mapping: 70 return self.server.extensions_mapping[ext] 71 else: 72 return self.server.extensions_mapping[''] 73 74 def SendRPCResponse(self, response): 75 self.send_response(200) 76 self.send_header("Content-type", "text/plain") 77 self.send_header("Content-length", str(len(response))) 78 self.end_headers() 79 self.wfile.write(response) 80 81 # shut down the connection 82 self.wfile.flush() 83 self.connection.shutdown(1) 84 85 def HandleRPC(self, name, query): 86 kargs = {} 87 for k, v in query.iteritems(): 88 assert len(v) == 1, k 89 kargs[k] = v[0] 90 91 l = self.server.listener 92 try: 93 response = getattr(l, name)(**kargs) 94 except Exception, e: 95 self.SendRPCResponse('%r' % (e,)) 96 raise 97 else: 98 self.SendRPCResponse(response) 99 100 # For Last-Modified-based caching, the timestamp needs to be old enough 101 # for the browser cache to be used (at least 60 seconds). 102 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html 103 # Often we clobber and regenerate files for testing, so this is needed 104 # to actually use the browser cache. 105 def send_header(self, keyword, value): 106 if keyword == 'Last-Modified': 107 last_mod_format = '%a, %d %b %Y %H:%M:%S GMT' 108 old_value_as_t = time.strptime(value, last_mod_format) 109 old_value_in_secs = time.mktime(old_value_as_t) 110 new_value_in_secs = old_value_in_secs - 360 111 value = time.strftime(last_mod_format, 112 time.localtime(new_value_in_secs)) 113 SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(self, 114 keyword, 115 value) 116 117 def do_POST(self): 118 # Backwards compatible - treat result as tuple without named fields. 119 _, _, path, _, query, _ = urlparse.urlparse(self.path) 120 121 self.server.listener.Log('POST %s (%s)' % (self.path, path)) 122 if path == '/echo': 123 self.send_response(200) 124 self.end_headers() 125 data = self.rfile.read(int(self.headers.getheader('content-length'))) 126 self.wfile.write(data) 127 elif self.server.output_dir is not None: 128 # Try to write the file to disk. 129 path = self.NormalizePath(path) 130 output_path = os.path.join(self.server.output_dir, path) 131 try: 132 outfile = open(output_path, 'w') 133 except IOError: 134 error_message = 'File not found: %r' % output_path 135 self.server.listener.ServerError(error_message) 136 self.send_error(404, error_message) 137 return 138 139 try: 140 data = self.rfile.read(int(self.headers.getheader('content-length'))) 141 outfile.write(data) 142 except IOError, e: 143 outfile.close() 144 try: 145 os.remove(output_path) 146 except OSError: 147 # Oh, well. 148 pass 149 error_message = 'Can\'t write file: %r\n' % output_path 150 error_message += 'Exception:\n%s' % str(e) 151 self.server.listener.ServerError(error_message) 152 self.send_error(500, error_message) 153 return 154 155 outfile.close() 156 157 # Send a success response. 158 self.send_response(200) 159 self.end_headers() 160 else: 161 error_message = 'File not found: %r' % path 162 self.server.listener.ServerError(error_message) 163 self.send_error(404, error_message) 164 165 self.server.ResetTimeout() 166 167 def do_GET(self): 168 # Backwards compatible - treat result as tuple without named fields. 169 _, _, path, _, query, _ = urlparse.urlparse(self.path) 170 171 tester = '/TESTER/' 172 if path.startswith(tester): 173 # If the path starts with '/TESTER/', the GET is an RPC call. 174 name = path[len(tester):] 175 # Supporting Python 2.5 prevents us from using urlparse.parse_qs 176 query = cgi.parse_qs(query, True) 177 178 self.server.rpc_lock.acquire() 179 try: 180 self.HandleRPC(name, query) 181 finally: 182 self.server.rpc_lock.release() 183 184 # Don't reset the timeout. This is not "part of the test", rather it's 185 # used to tell us if the renderer process is still alive. 186 if name == 'JavaScriptIsAlive': 187 self.server.JavaScriptIsAlive() 188 return 189 190 elif path in self.server.redirect_mapping: 191 dest = self.server.redirect_mapping[path] 192 self.send_response(301, 'Moved') 193 self.send_header('Location', dest) 194 self.end_headers() 195 self.wfile.write(self.error_message_format % 196 {'code': 301, 197 'message': 'Moved', 198 'explain': 'Object moved permanently'}) 199 self.server.listener.Log('REDIRECT %s (%s -> %s)' % 200 (self.path, path, dest)) 201 else: 202 self.server.listener.Log('GET %s (%s)' % (self.path, path)) 203 # A normal GET request for transferring files, etc. 204 f = self.send_head() 205 if f: 206 self.copyfile(f, self.wfile) 207 f.close() 208 209 self.server.ResetTimeout() 210 211 def copyfile(self, source, outputfile): 212 # Bandwidth values <= 0.0 are considered infinite 213 if self.server.bandwidth <= 0.0: 214 return SimpleHTTPServer.SimpleHTTPRequestHandler.copyfile( 215 self, source, outputfile) 216 217 self.server.listener.Log('Simulating %f mbps server BW' % 218 self.server.bandwidth) 219 chunk_size = 1500 # What size to use? 220 bits_per_sec = self.server.bandwidth * 1000000 221 start_time = time.time() 222 data_sent = 0 223 while True: 224 chunk = source.read(chunk_size) 225 if len(chunk) == 0: 226 break 227 cur_elapsed = time.time() - start_time 228 target_elapsed = (data_sent + len(chunk)) * 8 / bits_per_sec 229 if (cur_elapsed < target_elapsed): 230 time.sleep(target_elapsed - cur_elapsed) 231 outputfile.write(chunk) 232 data_sent += len(chunk) 233 self.server.listener.Log('Streamed %d bytes in %f s' % 234 (data_sent, time.time() - start_time)) 235 236 # Disable the built-in logging 237 def log_message(self, format, *args): 238 pass 239 240 241# The ThreadingMixIn allows the server to handle multiple requests 242# concurently (or at least as concurently as Python allows). This is desirable 243# because server sockets only allow a limited "backlog" of pending connections 244# and in the worst case the browser could make multiple connections and exceed 245# this backlog - causing the server to drop requests. Using ThreadingMixIn 246# helps reduce the chance this will happen. 247# There were apparently some problems using this Mixin with Python 2.5, but we 248# are no longer using anything older than 2.6. 249class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 250 251 def Configure( 252 self, file_mapping, redirect_mapping, extensions_mapping, allow_404, 253 bandwidth, listener, serving_dirs=[], output_dir=None): 254 self.file_mapping = file_mapping 255 self.redirect_mapping = redirect_mapping 256 self.extensions_mapping.update(extensions_mapping) 257 self.allow_404 = allow_404 258 self.bandwidth = bandwidth 259 self.listener = listener 260 self.rpc_lock = threading.Lock() 261 self.serving_dirs = serving_dirs 262 self.output_dir = output_dir 263 264 def TestingBegun(self, timeout): 265 self.test_in_progress = True 266 # self.timeout does not affect Python 2.5. 267 self.timeout = timeout 268 self.ResetTimeout() 269 self.JavaScriptIsAlive() 270 # Have we seen any requests from the browser? 271 self.received_request = False 272 273 def ResetTimeout(self): 274 self.last_activity = time.time() 275 self.received_request = True 276 277 def JavaScriptIsAlive(self): 278 self.last_js_activity = time.time() 279 280 def TimeSinceJSHeartbeat(self): 281 return time.time() - self.last_js_activity 282 283 def TestingEnded(self): 284 self.test_in_progress = False 285 286 def TimedOut(self, total_time): 287 return (total_time >= 0.0 and 288 (time.time() - self.last_activity) >= total_time) 289 290 291def Create(host, port): 292 server = Server((host, port), RequestHandler) 293 server.extensions_mapping = mimetypes.types_map.copy() 294 server.extensions_mapping.update({ 295 '': 'application/octet-stream' # Default 296 }) 297 return server 298