| OLD | NEW |
| (Empty) | |
| 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 if not path.endswith('favicon.ico') and not self.server.allow_404: |
| 43 self.server.listener.ServerError('Cannot find file \'%s\'' % path) |
| 44 return path |
| 45 |
| 46 def guess_type(self, path): |
| 47 # We store the extension -> MIME type mapping in the server instead of the |
| 48 # request handler so we that can add additional mapping entries via the |
| 49 # command line. |
| 50 base, ext = posixpath.splitext(path) |
| 51 if ext in self.server.extensions_mapping: |
| 52 return self.server.extensions_mapping[ext] |
| 53 ext = ext.lower() |
| 54 if ext in self.server.extensions_mapping: |
| 55 return self.server.extensions_mapping[ext] |
| 56 else: |
| 57 return self.server.extensions_mapping[''] |
| 58 |
| 59 def SendRPCResponse(self, response): |
| 60 self.send_response(200) |
| 61 self.send_header("Content-type", "text/plain") |
| 62 self.send_header("Content-length", str(len(response))) |
| 63 self.end_headers() |
| 64 self.wfile.write(response) |
| 65 |
| 66 # shut down the connection |
| 67 self.wfile.flush() |
| 68 self.connection.shutdown(1) |
| 69 |
| 70 def HandleRPC(self, name, query): |
| 71 kargs = {} |
| 72 for k, v in query.iteritems(): |
| 73 assert len(v) == 1, k |
| 74 kargs[k] = v[0] |
| 75 |
| 76 l = self.server.listener |
| 77 try: |
| 78 response = getattr(l, name)(**kargs) |
| 79 except Exception, e: |
| 80 self.SendRPCResponse('%r' % (e,)) |
| 81 raise |
| 82 else: |
| 83 self.SendRPCResponse(response) |
| 84 |
| 85 # For Last-Modified-based caching, the timestamp needs to be old enough |
| 86 # for the browser cache to be used (at least 60 seconds). |
| 87 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html |
| 88 # Often we clobber and regenerate files for testing, so this is needed |
| 89 # to actually use the browser cache. |
| 90 def send_header(self, keyword, value): |
| 91 if keyword == 'Last-Modified': |
| 92 last_mod_format = '%a, %d %b %Y %H:%M:%S GMT' |
| 93 old_value_as_t = time.strptime(value, last_mod_format) |
| 94 old_value_in_secs = time.mktime(old_value_as_t) |
| 95 new_value_in_secs = old_value_in_secs - 360 |
| 96 value = time.strftime(last_mod_format, |
| 97 time.localtime(new_value_in_secs)) |
| 98 SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(self, |
| 99 keyword, |
| 100 value) |
| 101 |
| 102 def do_POST(self): |
| 103 # Backwards compatible - treat result as tuple without named fields. |
| 104 _, _, path, _, query, _ = urlparse.urlparse(self.path) |
| 105 |
| 106 self.server.listener.Log('POST %s (%s)' % (self.path, path)) |
| 107 if path == '/echo': |
| 108 self.send_response(200) |
| 109 self.end_headers() |
| 110 data = self.rfile.read(int(self.headers.getheader('content-length'))) |
| 111 self.wfile.write(data) |
| 112 else: |
| 113 self.send_error(404, 'File not found') |
| 114 |
| 115 self.server.ResetTimeout() |
| 116 |
| 117 def do_GET(self): |
| 118 # Backwards compatible - treat result as tuple without named fields. |
| 119 _, _, path, _, query, _ = urlparse.urlparse(self.path) |
| 120 |
| 121 tester = '/TESTER/' |
| 122 if path.startswith(tester): |
| 123 # If the path starts with '/TESTER/', the GET is an RPC call. |
| 124 name = path[len(tester):] |
| 125 # Supporting Python 2.5 prevents us from using urlparse.parse_qs |
| 126 query = cgi.parse_qs(query, True) |
| 127 |
| 128 self.server.rpc_lock.acquire() |
| 129 try: |
| 130 self.HandleRPC(name, query) |
| 131 finally: |
| 132 self.server.rpc_lock.release() |
| 133 |
| 134 # Don't reset the timeout. This is not "part of the test", rather it's |
| 135 # used to tell us if the renderer process is still alive. |
| 136 if name == 'JavaScriptIsAlive': |
| 137 self.server.JavaScriptIsAlive() |
| 138 return |
| 139 |
| 140 elif path in self.server.redirect_mapping: |
| 141 dest = self.server.redirect_mapping[path] |
| 142 self.send_response(301, 'Moved') |
| 143 self.send_header('Location', dest) |
| 144 self.end_headers() |
| 145 self.wfile.write(self.error_message_format % |
| 146 {'code': 301, |
| 147 'message': 'Moved', |
| 148 'explain': 'Object moved permanently'}) |
| 149 self.server.listener.Log('REDIRECT %s (%s -> %s)' % |
| 150 (self.path, path, dest)) |
| 151 else: |
| 152 self.server.listener.Log('GET %s (%s)' % (self.path, path)) |
| 153 # A normal GET request for transferring files, etc. |
| 154 f = self.send_head() |
| 155 if f: |
| 156 self.copyfile(f, self.wfile) |
| 157 f.close() |
| 158 |
| 159 self.server.ResetTimeout() |
| 160 |
| 161 def copyfile(self, source, outputfile): |
| 162 # Bandwidth values <= 0.0 are considered infinite |
| 163 if self.server.bandwidth <= 0.0: |
| 164 return SimpleHTTPServer.SimpleHTTPRequestHandler.copyfile( |
| 165 self, source, outputfile) |
| 166 |
| 167 self.server.listener.Log('Simulating %f mbps server BW' % |
| 168 self.server.bandwidth) |
| 169 chunk_size = 1500 # What size to use? |
| 170 bits_per_sec = self.server.bandwidth * 1000000 |
| 171 start_time = time.time() |
| 172 data_sent = 0 |
| 173 while True: |
| 174 chunk = source.read(chunk_size) |
| 175 if len(chunk) == 0: |
| 176 break |
| 177 cur_elapsed = time.time() - start_time |
| 178 target_elapsed = (data_sent + len(chunk)) * 8 / bits_per_sec |
| 179 if (cur_elapsed < target_elapsed): |
| 180 time.sleep(target_elapsed - cur_elapsed) |
| 181 outputfile.write(chunk) |
| 182 data_sent += len(chunk) |
| 183 self.server.listener.Log('Streamed %d bytes in %f s' % |
| 184 (data_sent, time.time() - start_time)) |
| 185 |
| 186 # Disable the built-in logging |
| 187 def log_message(self, format, *args): |
| 188 pass |
| 189 |
| 190 |
| 191 # The ThreadingMixIn allows the server to handle multiple requests |
| 192 # concurently (or at least as concurently as Python allows). This is desirable |
| 193 # because server sockets only allow a limited "backlog" of pending connections |
| 194 # and in the worst case the browser could make multiple connections and exceed |
| 195 # this backlog - causing the server to drop requests. Using ThreadingMixIn |
| 196 # helps reduce the chance this will happen. |
| 197 # There were apparently some problems using this Mixin with Python 2.5, but we |
| 198 # are no longer using anything older than 2.6. |
| 199 class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): |
| 200 |
| 201 def Configure( |
| 202 self, file_mapping, redirect_mapping, extensions_mapping, allow_404, |
| 203 bandwidth, listener, serving_dirs=[]): |
| 204 self.file_mapping = file_mapping |
| 205 self.redirect_mapping = redirect_mapping |
| 206 self.extensions_mapping.update(extensions_mapping) |
| 207 self.allow_404 = allow_404 |
| 208 self.bandwidth = bandwidth |
| 209 self.listener = listener |
| 210 self.rpc_lock = threading.Lock() |
| 211 self.serving_dirs = serving_dirs |
| 212 |
| 213 def TestingBegun(self, timeout): |
| 214 self.test_in_progress = True |
| 215 # self.timeout does not affect Python 2.5. |
| 216 self.timeout = timeout |
| 217 self.ResetTimeout() |
| 218 self.JavaScriptIsAlive() |
| 219 # Have we seen any requests from the browser? |
| 220 self.received_request = False |
| 221 |
| 222 def ResetTimeout(self): |
| 223 self.last_activity = time.time() |
| 224 self.received_request = True |
| 225 |
| 226 def JavaScriptIsAlive(self): |
| 227 self.last_js_activity = time.time() |
| 228 |
| 229 def TimeSinceJSHeartbeat(self): |
| 230 return time.time() - self.last_js_activity |
| 231 |
| 232 def TestingEnded(self): |
| 233 self.test_in_progress = False |
| 234 |
| 235 def TimedOut(self, total_time): |
| 236 return (total_time >= 0.0 and |
| 237 (time.time() - self.last_activity) >= total_time) |
| 238 |
| 239 |
| 240 def Create(host, port): |
| 241 server = Server((host, port), RequestHandler) |
| 242 server.extensions_mapping = mimetypes.types_map.copy() |
| 243 server.extensions_mapping.update({ |
| 244 '': 'application/octet-stream' # Default |
| 245 }) |
| 246 return server |
| OLD | NEW |