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 logging 8 import multiprocessing 9 import optparse 10 import os 11 import SimpleHTTPServer # pylint: disable=W0611 12 import socket 13 import sys 14 import time 15 import urlparse 16 17 if sys.version_info < (2, 6, 0): 18 sys.stderr.write("python 2.6 or later is required run this script\n") 19 sys.exit(1) 20 21 22 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 23 NACL_SDK_ROOT = os.path.dirname(SCRIPT_DIR) 24 25 26 # We only run from the examples directory so that not too much is exposed 27 # via this HTTP server. Everything in the directory is served, so there should 28 # never be anything potentially sensitive in the serving directory, especially 29 # if the machine might be a multi-user machine and not all users are trusted. 30 # We only serve via the loopback interface. 31 def SanityCheckDirectory(dirname): 32 abs_serve_dir = os.path.abspath(dirname) 33 34 # Verify we don't serve anywhere above NACL_SDK_ROOT. 35 if abs_serve_dir[:len(NACL_SDK_ROOT)] == NACL_SDK_ROOT: 36 return 37 logging.error('For security, httpd.py should only be run from within the') 38 logging.error('example directory tree.') 39 logging.error('Attempting to serve from %s.' % abs_serve_dir) 40 logging.error('Run with --no-dir-check to bypass this check.') 41 sys.exit(1) 42 43 44 class HTTPServer(BaseHTTPServer.HTTPServer): 45 def __init__(self, *args, **kwargs): 46 BaseHTTPServer.HTTPServer.__init__(self, *args) 47 self.running = True 48 self.result = 0 49 50 def Shutdown(self, result=0): 51 self.running = False 52 self.result = result 53 54 55 class HTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 56 def _SendNothingAndDie(self, result=0): 57 self.send_response(200, 'OK') 58 self.send_header('Content-type', 'text/html') 59 self.send_header('Content-length', '0') 60 self.end_headers() 61 self.server.Shutdown(result) 62 63 def do_GET(self): 64 # Browsing to ?quit=1 will kill the server cleanly. 65 _, _, _, query, _ = urlparse.urlsplit(self.path) 66 if query: 67 params = urlparse.parse_qs(query) 68 if '1' in params.get('quit', []): 69 self._SendNothingAndDie() 70 return 71 72 return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 73 74 75 class LocalHTTPServer(object): 76 """Class to start a local HTTP server as a child process.""" 77 78 def __init__(self, dirname, port): 79 parent_conn, child_conn = multiprocessing.Pipe() 80 self.process = multiprocessing.Process( 81 target=_HTTPServerProcess, 82 args=(child_conn, dirname, port, {})) 83 self.process.start() 84 if parent_conn.poll(10): # wait 10 seconds 85 self.port = parent_conn.recv() 86 else: 87 raise Exception('Unable to launch HTTP server.') 88 89 self.conn = parent_conn 90 91 def ServeForever(self): 92 """Serve until the child HTTP process tells us to stop. 93 94 Returns: 95 The result from the child (as an errorcode), or 0 if the server was 96 killed not by the child (by KeyboardInterrupt for example). 97 """ 98 child_result = 0 99 try: 100 # Block on this pipe, waiting for a response from the child process. 101 child_result = self.conn.recv() 102 except KeyboardInterrupt: 103 pass 104 finally: 105 self.Shutdown() 106 return child_result 107 108 def ServeUntilSubprocessDies(self, process): 109 """Serve until the child HTTP process tells us to stop or |subprocess| dies. 110 111 Returns: 112 The result from the child (as an errorcode), or 0 if |subprocess| died, 113 or the server was killed some other way (by KeyboardInterrupt for 114 example). 115 """ 116 child_result = 0 117 try: 118 while True: 119 if process.poll() is not None: 120 child_result = 0 121 break 122 if self.conn.poll(): 123 child_result = self.conn.recv() 124 break 125 time.sleep(0) 126 except KeyboardInterrupt: 127 pass 128 finally: 129 self.Shutdown() 130 return child_result 131 132 def Shutdown(self): 133 """Send a message to the child HTTP server process and wait for it to 134 finish.""" 135 self.conn.send(False) 136 self.process.join() 137 138 def GetURL(self, rel_url): 139 """Get the full url for a file on the local HTTP server. 140 141 Args: 142 rel_url: A URL fragment to convert to a full URL. For example, 143 GetURL('foobar.baz') -> 'http://localhost:1234/foobar.baz' 144 """ 145 return 'http://localhost:%d/%s' % (self.port, rel_url) 146 147 148 def _HTTPServerProcess(conn, dirname, port, server_kwargs): 149 """Run a local httpserver with the given port or an ephemeral port. 150 151 This function assumes it is run as a child process using multiprocessing. 152 153 Args: 154 conn: A connection to the parent process. The child process sends 155 the local port, and waits for a message from the parent to 156 stop serving. It also sends a "result" back to the parent -- this can 157 be used to allow a client-side test to notify the server of results. 158 dirname: The directory to serve. All files are accessible through 159 http://localhost:<port>/path/to/filename. 160 port: The port to serve on. If 0, an ephemeral port will be chosen. 161 server_kwargs: A dict that will be passed as kwargs to the server. 162 """ 163 try: 164 os.chdir(dirname) 165 httpd = HTTPServer(('', port), HTTPRequestHandler, **server_kwargs) 166 except socket.error as e: 167 sys.stderr.write('Error creating HTTPServer: %s\n' % e) 168 sys.exit(1) 169 170 try: 171 conn.send(httpd.server_address[1]) # the chosen port number 172 httpd.timeout = 0.5 # seconds 173 while httpd.running: 174 # Flush output for MSVS Add-In. 175 sys.stdout.flush() 176 sys.stderr.flush() 177 httpd.handle_request() 178 if conn.poll(): 179 httpd.running = conn.recv() 180 except KeyboardInterrupt: 181 pass 182 finally: 183 conn.send(httpd.result) 184 conn.close() 185 186 187 def main(args): 188 parser = optparse.OptionParser() 189 parser.add_option('-C', '--serve-dir', 190 help='Serve files out of this directory.', 191 default=os.path.abspath('.')) 192 parser.add_option('-p', '--port', 193 help='Run server on this port.', default=5103) 194 parser.add_option('--no-dir-check', '--no_dir_check', 195 help='No check to ensure serving from safe directory.', 196 dest='do_safe_check', action='store_false', default=True) 197 198 # To enable bash completion for this command first install optcomplete 199 # and then add this line to your .bashrc: 200 # complete -F _optcomplete httpd.py 201 try: 202 import optcomplete 203 optcomplete.autocomplete(parser) 204 except ImportError: 205 pass 206 207 options, args = parser.parse_args(args) 208 if options.do_safe_check: 209 SanityCheckDirectory(options.serve_dir) 210 211 server = LocalHTTPServer(options.serve_dir, int(options.port)) 212 213 # Serve until the client tells us to stop. When it does, it will give us an 214 # errorcode. 215 print 'Serving %s on %s...' % (options.serve_dir, server.GetURL('')) 216 return server.ServeForever() 217 218 if __name__ == '__main__': 219 sys.exit(main(sys.argv[1:])) 220