1 #!/usr/bin/env python 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 import BaseHTTPServer 7 import imp 8 import logging 9 import multiprocessing 10 import optparse 11 import os 12 import SimpleHTTPServer # pylint: disable=W0611 13 import socket 14 import sys 15 import time 16 import urlparse 17 18 if sys.version_info < (2, 6, 0): 19 sys.stderr.write("python 2.6 or later is required run this script\n") 20 sys.exit(1) 21 22 23 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 24 NACL_SDK_ROOT = os.path.dirname(SCRIPT_DIR) 25 26 27 # We only run from the examples directory so that not too much is exposed 28 # via this HTTP server. Everything in the directory is served, so there should 29 # never be anything potentially sensitive in the serving directory, especially 30 # if the machine might be a multi-user machine and not all users are trusted. 31 # We only serve via the loopback interface. 32 def SanityCheckDirectory(dirname): 33 abs_serve_dir = os.path.abspath(dirname) 34 35 # Verify we don't serve anywhere above NACL_SDK_ROOT. 36 if abs_serve_dir[:len(NACL_SDK_ROOT)] == NACL_SDK_ROOT: 37 return 38 logging.error('For security, httpd.py should only be run from within the') 39 logging.error('example directory tree.') 40 logging.error('Attempting to serve from %s.' % abs_serve_dir) 41 logging.error('Run with --no_dir_check to bypass this check.') 42 sys.exit(1) 43 44 45 class PluggableHTTPServer(BaseHTTPServer.HTTPServer): 46 def __init__(self, *args, **kwargs): 47 BaseHTTPServer.HTTPServer.__init__(self, *args) 48 self.serve_dir = kwargs.get('serve_dir', '.') 49 self.test_mode = kwargs.get('test_mode', False) 50 self.delegate_map = {} 51 self.running = True 52 self.result = 0 53 54 def Shutdown(self, result=0): 55 self.running = False 56 self.result = result 57 58 59 class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 60 def _FindDelegateAtPath(self, dirname): 61 # First check the cache... 62 logging.debug('Looking for cached delegate in %s...' % dirname) 63 handler_script = os.path.join(dirname, 'handler.py') 64 65 if dirname in self.server.delegate_map: 66 result = self.server.delegate_map[dirname] 67 if result is None: 68 logging.debug('Found None.') 69 else: 70 logging.debug('Found delegate.') 71 return result 72 73 # Don't have one yet, look for one. 74 delegate = None 75 logging.debug('Testing file %s for existence...' % handler_script) 76 if os.path.exists(handler_script): 77 logging.debug( 78 'File %s exists, looking for HTTPRequestHandlerDelegate.' % 79 handler_script) 80 81 module = imp.load_source('handler', handler_script) 82 delegate_class = getattr(module, 'HTTPRequestHandlerDelegate', None) 83 delegate = delegate_class() 84 if not delegate: 85 logging.warn( 86 'Unable to find symbol HTTPRequestHandlerDelegate in module %s.' % 87 handler_script) 88 89 return delegate 90 91 def _FindDelegateForURLRecurse(self, cur_dir, abs_root): 92 delegate = self._FindDelegateAtPath(cur_dir) 93 if not delegate: 94 # Didn't find it, try the parent directory, but stop if this is the server 95 # root. 96 if cur_dir != abs_root: 97 parent_dir = os.path.dirname(cur_dir) 98 delegate = self._FindDelegateForURLRecurse(parent_dir, abs_root) 99 100 logging.debug('Adding delegate to cache for %s.' % cur_dir) 101 self.server.delegate_map[cur_dir] = delegate 102 return delegate 103 104 def _FindDelegateForURL(self, url_path): 105 path = self.translate_path(url_path) 106 if os.path.isdir(path): 107 dirname = path 108 else: 109 dirname = os.path.dirname(path) 110 111 abs_serve_dir = os.path.abspath(self.server.serve_dir) 112 delegate = self._FindDelegateForURLRecurse(dirname, abs_serve_dir) 113 if not delegate: 114 logging.info('No handler found for path %s. Using default.' % url_path) 115 return delegate 116 117 def _SendNothingAndDie(self, result=0): 118 self.send_response(200, 'OK') 119 self.send_header('Content-type', 'text/html') 120 self.send_header('Content-length', '0') 121 self.end_headers() 122 self.server.Shutdown(result) 123 124 def send_head(self): 125 delegate = self._FindDelegateForURL(self.path) 126 if delegate: 127 return delegate.send_head(self) 128 return self.base_send_head() 129 130 def base_send_head(self): 131 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) 132 133 def do_GET(self): 134 # TODO(binji): pyauto tests use the ?quit=1 method to kill the server. 135 # Remove this when we kill the pyauto tests. 136 _, _, _, query, _ = urlparse.urlsplit(self.path) 137 if query: 138 params = urlparse.parse_qs(query) 139 if '1' in params.get('quit', []): 140 self._SendNothingAndDie() 141 return 142 143 delegate = self._FindDelegateForURL(self.path) 144 if delegate: 145 return delegate.do_GET(self) 146 return self.base_do_GET() 147 148 def base_do_GET(self): 149 return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 150 151 def do_POST(self): 152 delegate = self._FindDelegateForURL(self.path) 153 if delegate: 154 return delegate.do_POST(self) 155 return self.base_do_POST() 156 157 def base_do_POST(self): 158 if self.server.test_mode: 159 if self.path == '/ok': 160 self._SendNothingAndDie(0) 161 elif self.path == '/fail': 162 self._SendNothingAndDie(1) 163 164 165 class LocalHTTPServer(object): 166 """Class to start a local HTTP server as a child process.""" 167 168 def __init__(self, dirname, port, test_mode): 169 parent_conn, child_conn = multiprocessing.Pipe() 170 self.process = multiprocessing.Process( 171 target=_HTTPServerProcess, 172 args=(child_conn, dirname, port, { 173 'serve_dir': dirname, 174 'test_mode': test_mode, 175 })) 176 self.process.start() 177 if parent_conn.poll(10): # wait 10 seconds 178 self.port = parent_conn.recv() 179 else: 180 raise Exception('Unable to launch HTTP server.') 181 182 self.conn = parent_conn 183 184 def ServeForever(self): 185 """Serve until the child HTTP process tells us to stop. 186 187 Returns: 188 The result from the child (as an errorcode), or 0 if the server was 189 killed not by the child (by KeyboardInterrupt for example). 190 """ 191 child_result = 0 192 try: 193 # Block on this pipe, waiting for a response from the child process. 194 child_result = self.conn.recv() 195 except KeyboardInterrupt: 196 pass 197 finally: 198 self.Shutdown() 199 return child_result 200 201 def ServeUntilSubprocessDies(self, process): 202 """Serve until the child HTTP process tells us to stop or |subprocess| dies. 203 204 Returns: 205 The result from the child (as an errorcode), or 0 if |subprocess| died, 206 or the server was killed some other way (by KeyboardInterrupt for 207 example). 208 """ 209 child_result = 0 210 try: 211 while True: 212 if process.poll() is not None: 213 child_result = 0 214 break 215 if self.conn.poll(): 216 child_result = self.conn.recv() 217 break 218 time.sleep(0) 219 except KeyboardInterrupt: 220 pass 221 finally: 222 self.Shutdown() 223 return child_result 224 225 def Shutdown(self): 226 """Send a message to the child HTTP server process and wait for it to 227 finish.""" 228 self.conn.send(False) 229 self.process.join() 230 231 def GetURL(self, rel_url): 232 """Get the full url for a file on the local HTTP server. 233 234 Args: 235 rel_url: A URL fragment to convert to a full URL. For example, 236 GetURL('foobar.baz') -> 'http://localhost:1234/foobar.baz' 237 """ 238 return 'http://localhost:%d/%s' % (self.port, rel_url) 239 240 241 def _HTTPServerProcess(conn, dirname, port, server_kwargs): 242 """Run a local httpserver with the given port or an ephemeral port. 243 244 This function assumes it is run as a child process using multiprocessing. 245 246 Args: 247 conn: A connection to the parent process. The child process sends 248 the local port, and waits for a message from the parent to 249 stop serving. It also sends a "result" back to the parent -- this can 250 be used to allow a client-side test to notify the server of results. 251 dirname: The directory to serve. All files are accessible through 252 http://localhost:<port>/path/to/filename. 253 port: The port to serve on. If 0, an ephemeral port will be chosen. 254 server_kwargs: A dict that will be passed as kwargs to the server. 255 """ 256 try: 257 os.chdir(dirname) 258 httpd = PluggableHTTPServer(('', port), PluggableHTTPRequestHandler, 259 **server_kwargs) 260 except socket.error as e: 261 sys.stderr.write('Error creating HTTPServer: %s\n' % e) 262 sys.exit(1) 263 264 try: 265 conn.send(httpd.server_address[1]) # the chosen port number 266 httpd.timeout = 0.5 # seconds 267 while httpd.running: 268 # Flush output for MSVS Add-In. 269 sys.stdout.flush() 270 sys.stderr.flush() 271 httpd.handle_request() 272 if conn.poll(): 273 httpd.running = conn.recv() 274 except KeyboardInterrupt: 275 pass 276 finally: 277 conn.send(httpd.result) 278 conn.close() 279 280 281 def main(args): 282 parser = optparse.OptionParser() 283 parser.add_option('-C', '--serve-dir', 284 help='Serve files out of this directory.', 285 dest='serve_dir', default=os.path.abspath('.')) 286 parser.add_option('-p', '--port', 287 help='Run server on this port.', 288 dest='port', default=5103) 289 parser.add_option('--no_dir_check', 290 help='No check to ensure serving from safe directory.', 291 dest='do_safe_check', action='store_false', default=True) 292 parser.add_option('--test-mode', 293 help='Listen for posts to /ok or /fail and shut down the server with ' 294 ' errorcodes 0 and 1 respectively.', 295 dest='test_mode', action='store_true') 296 options, args = parser.parse_args(args) 297 if options.do_safe_check: 298 SanityCheckDirectory(options.serve_dir) 299 300 server = LocalHTTPServer(options.serve_dir, int(options.port), 301 options.test_mode) 302 303 # Serve until the client tells us to stop. When it does, it will give us an 304 # errorcode. 305 print 'Serving %s on %s...' % (options.serve_dir, server.GetURL('')) 306 return server.ServeForever() 307 308 if __name__ == '__main__': 309 sys.exit(main(sys.argv[1:])) 310