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 5 import BaseHTTPServer 6 import cgi 7 import mimetypes 8 import os 9 import os.path 10 import posixpath 11 import SimpleHTTPServer 12 import SocketServer 13 import threading 14 import time 15 import urllib 16 import urlparse 17 18 class 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 else: 128 self.send_error(404, 'File not found') 129 130 self.server.ResetTimeout() 131 132 def do_GET(self): 133 # Backwards compatible - treat result as tuple without named fields. 134 _, _, path, _, query, _ = urlparse.urlparse(self.path) 135 136 tester = '/TESTER/' 137 if path.startswith(tester): 138 # If the path starts with '/TESTER/', the GET is an RPC call. 139 name = path[len(tester):] 140 # Supporting Python 2.5 prevents us from using urlparse.parse_qs 141 query = cgi.parse_qs(query, True) 142 143 self.server.rpc_lock.acquire() 144 try: 145 self.HandleRPC(name, query) 146 finally: 147 self.server.rpc_lock.release() 148 149 # Don't reset the timeout. This is not "part of the test", rather it's 150 # used to tell us if the renderer process is still alive. 151 if name == 'JavaScriptIsAlive': 152 self.server.JavaScriptIsAlive() 153 return 154 155 elif path in self.server.redirect_mapping: 156 dest = self.server.redirect_mapping[path] 157 self.send_response(301, 'Moved') 158 self.send_header('Location', dest) 159 self.end_headers() 160 self.wfile.write(self.error_message_format % 161 {'code': 301, 162 'message': 'Moved', 163 'explain': 'Object moved permanently'}) 164 self.server.listener.Log('REDIRECT %s (%s -> %s)' % 165 (self.path, path, dest)) 166 else: 167 self.server.listener.Log('GET %s (%s)' % (self.path, path)) 168 # A normal GET request for transferring files, etc. 169 f = self.send_head() 170 if f: 171 self.copyfile(f, self.wfile) 172 f.close() 173 174 self.server.ResetTimeout() 175 176 def copyfile(self, source, outputfile): 177 # Bandwidth values <= 0.0 are considered infinite 178 if self.server.bandwidth <= 0.0: 179 return SimpleHTTPServer.SimpleHTTPRequestHandler.copyfile( 180 self, source, outputfile) 181 182 self.server.listener.Log('Simulating %f mbps server BW' % 183 self.server.bandwidth) 184 chunk_size = 1500 # What size to use? 185 bits_per_sec = self.server.bandwidth * 1000000 186 start_time = time.time() 187 data_sent = 0 188 while True: 189 chunk = source.read(chunk_size) 190 if len(chunk) == 0: 191 break 192 cur_elapsed = time.time() - start_time 193 target_elapsed = (data_sent + len(chunk)) * 8 / bits_per_sec 194 if (cur_elapsed < target_elapsed): 195 time.sleep(target_elapsed - cur_elapsed) 196 outputfile.write(chunk) 197 data_sent += len(chunk) 198 self.server.listener.Log('Streamed %d bytes in %f s' % 199 (data_sent, time.time() - start_time)) 200 201 # Disable the built-in logging 202 def log_message(self, format, *args): 203 pass 204 205 206 # The ThreadingMixIn allows the server to handle multiple requests 207 # concurently (or at least as concurently as Python allows). This is desirable 208 # because server sockets only allow a limited "backlog" of pending connections 209 # and in the worst case the browser could make multiple connections and exceed 210 # this backlog - causing the server to drop requests. Using ThreadingMixIn 211 # helps reduce the chance this will happen. 212 # There were apparently some problems using this Mixin with Python 2.5, but we 213 # are no longer using anything older than 2.6. 214 class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 215 216 def Configure( 217 self, file_mapping, redirect_mapping, extensions_mapping, allow_404, 218 bandwidth, listener, serving_dirs=[]): 219 self.file_mapping = file_mapping 220 self.redirect_mapping = redirect_mapping 221 self.extensions_mapping.update(extensions_mapping) 222 self.allow_404 = allow_404 223 self.bandwidth = bandwidth 224 self.listener = listener 225 self.rpc_lock = threading.Lock() 226 self.serving_dirs = serving_dirs 227 228 def TestingBegun(self, timeout): 229 self.test_in_progress = True 230 # self.timeout does not affect Python 2.5. 231 self.timeout = timeout 232 self.ResetTimeout() 233 self.JavaScriptIsAlive() 234 # Have we seen any requests from the browser? 235 self.received_request = False 236 237 def ResetTimeout(self): 238 self.last_activity = time.time() 239 self.received_request = True 240 241 def JavaScriptIsAlive(self): 242 self.last_js_activity = time.time() 243 244 def TimeSinceJSHeartbeat(self): 245 return time.time() - self.last_js_activity 246 247 def TestingEnded(self): 248 self.test_in_progress = False 249 250 def TimedOut(self, total_time): 251 return (total_time >= 0.0 and 252 (time.time() - self.last_activity) >= total_time) 253 254 255 def Create(host, port): 256 server = Server((host, port), RequestHandler) 257 server.extensions_mapping = mimetypes.types_map.copy() 258 server.extensions_mapping.update({ 259 '': 'application/octet-stream' # Default 260 }) 261 return server 262