1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""Spins up a trivial HTTP cgi form listener in a thread. 7*9c5db199SXin Li 8*9c5db199SXin Li This HTTPThread class is a utility for use with test cases that 9*9c5db199SXin Li need to call back to the Autotest test case with some form value, e.g. 10*9c5db199SXin Li http://localhost:nnnn/?status="Browser started!" 11*9c5db199SXin Li""" 12*9c5db199SXin Li 13*9c5db199SXin Liimport cgi, errno, logging, os, posixpath, six.moves.SimpleHTTPServer, socket, ssl, sys 14*9c5db199SXin Liimport threading, six.moves.urllib.parse 15*9c5db199SXin Lifrom six.moves import urllib 16*9c5db199SXin Lifrom six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 17*9c5db199SXin Lifrom six.moves.socketserver import BaseServer, ThreadingMixIn 18*9c5db199SXin Li 19*9c5db199SXin Li 20*9c5db199SXin Lidef _handle_http_errors(func): 21*9c5db199SXin Li """Decorator function for cleaner presentation of certain exceptions.""" 22*9c5db199SXin Li def wrapper(self): 23*9c5db199SXin Li try: 24*9c5db199SXin Li func(self) 25*9c5db199SXin Li except IOError as e: 26*9c5db199SXin Li if e.errno == errno.EPIPE or e.errno == errno.ECONNRESET: 27*9c5db199SXin Li # Instead of dumping a stack trace, a single line is sufficient. 28*9c5db199SXin Li self.log_error(str(e)) 29*9c5db199SXin Li else: 30*9c5db199SXin Li raise 31*9c5db199SXin Li 32*9c5db199SXin Li return wrapper 33*9c5db199SXin Li 34*9c5db199SXin Li 35*9c5db199SXin Liclass FormHandler(six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler): 36*9c5db199SXin Li """Implements a form handler (for POST requests only) which simply 37*9c5db199SXin Li echoes the key=value parameters back in the response. 38*9c5db199SXin Li 39*9c5db199SXin Li If the form submission is a file upload, the file will be written 40*9c5db199SXin Li to disk with the name contained in the 'filename' field. 41*9c5db199SXin Li """ 42*9c5db199SXin Li 43*9c5db199SXin Li six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map.update({ 44*9c5db199SXin Li '.webm': 'video/webm', 45*9c5db199SXin Li }) 46*9c5db199SXin Li 47*9c5db199SXin Li # Override the default logging methods to use the logging module directly. 48*9c5db199SXin Li def log_error(self, format, *args): 49*9c5db199SXin Li logging.warning("(httpd error) %s - - [%s] %s\n" % 50*9c5db199SXin Li (self.address_string(), self.log_date_time_string(), 51*9c5db199SXin Li format%args)) 52*9c5db199SXin Li 53*9c5db199SXin Li def log_message(self, format, *args): 54*9c5db199SXin Li logging.debug("%s - - [%s] %s\n" % 55*9c5db199SXin Li (self.address_string(), self.log_date_time_string(), 56*9c5db199SXin Li format%args)) 57*9c5db199SXin Li 58*9c5db199SXin Li @_handle_http_errors 59*9c5db199SXin Li def do_POST(self): 60*9c5db199SXin Li form = cgi.FieldStorage( 61*9c5db199SXin Li fp=self.rfile, 62*9c5db199SXin Li headers=self.headers, 63*9c5db199SXin Li environ={'REQUEST_METHOD': 'POST', 64*9c5db199SXin Li 'CONTENT_TYPE': self.headers['Content-Type']}) 65*9c5db199SXin Li # You'd think form.keys() would just return [], like it does for empty 66*9c5db199SXin Li # python dicts; you'd be wrong. It raises TypeError if called when it 67*9c5db199SXin Li # has no keys. 68*9c5db199SXin Li if form: 69*9c5db199SXin Li for field in form.keys(): 70*9c5db199SXin Li field_item = form[field] 71*9c5db199SXin Li self.server._form_entries[field] = field_item.value 72*9c5db199SXin Li path = six.moves.urllib.parse.urlparse(self.path)[2] 73*9c5db199SXin Li if path in self.server._url_handlers: 74*9c5db199SXin Li self.server._url_handlers[path](self, form) 75*9c5db199SXin Li else: 76*9c5db199SXin Li # Echo back information about what was posted in the form. 77*9c5db199SXin Li self.write_post_response(form) 78*9c5db199SXin Li self._fire_event() 79*9c5db199SXin Li 80*9c5db199SXin Li 81*9c5db199SXin Li def write_post_response(self, form): 82*9c5db199SXin Li """Called to fill out the response to an HTTP POST. 83*9c5db199SXin Li 84*9c5db199SXin Li Override this class to give custom responses. 85*9c5db199SXin Li """ 86*9c5db199SXin Li # Send response boilerplate 87*9c5db199SXin Li self.send_response(200) 88*9c5db199SXin Li self.end_headers() 89*9c5db199SXin Li self.wfile.write(('Hello from Autotest!\nClient: %s\n' % 90*9c5db199SXin Li str(self.client_address)).encode('utf-8')) 91*9c5db199SXin Li self.wfile.write(('Request for path: %s\n' % self.path).encode('utf-8')) 92*9c5db199SXin Li self.wfile.write(b'Got form data:\n') 93*9c5db199SXin Li 94*9c5db199SXin Li # See the note in do_POST about form.keys(). 95*9c5db199SXin Li if form: 96*9c5db199SXin Li for field in form.keys(): 97*9c5db199SXin Li field_item = form[field] 98*9c5db199SXin Li if field_item.filename: 99*9c5db199SXin Li # The field contains an uploaded file 100*9c5db199SXin Li upload = field_item.file.read() 101*9c5db199SXin Li self.wfile.write(('\tUploaded %s (%d bytes)<br>' % 102*9c5db199SXin Li (field, len(upload))).encode('utf-8')) 103*9c5db199SXin Li # Write submitted file to specified filename. 104*9c5db199SXin Li open(field_item.filename, 'w').write(upload) 105*9c5db199SXin Li del upload 106*9c5db199SXin Li else: 107*9c5db199SXin Li self.wfile.write(('\t%s=%s<br>' % (field, form[field].value)).encode('utf-8')) 108*9c5db199SXin Li 109*9c5db199SXin Li 110*9c5db199SXin Li def translate_path(self, path): 111*9c5db199SXin Li """Override SimpleHTTPRequestHandler's translate_path to serve 112*9c5db199SXin Li from arbitrary docroot 113*9c5db199SXin Li """ 114*9c5db199SXin Li # abandon query parameters 115*9c5db199SXin Li path = six.moves.urllib.parse.urlparse(path)[2] 116*9c5db199SXin Li path = posixpath.normpath(urllib.parse.unquote(path)) 117*9c5db199SXin Li words = path.split('/') 118*9c5db199SXin Li words = [_f for _f in words if _f] 119*9c5db199SXin Li path = self.server.docroot 120*9c5db199SXin Li for word in words: 121*9c5db199SXin Li drive, word = os.path.splitdrive(word) 122*9c5db199SXin Li head, word = os.path.split(word) 123*9c5db199SXin Li if word in (os.curdir, os.pardir): continue 124*9c5db199SXin Li path = os.path.join(path, word) 125*9c5db199SXin Li logging.debug('Translated path: %s', path) 126*9c5db199SXin Li return path 127*9c5db199SXin Li 128*9c5db199SXin Li 129*9c5db199SXin Li def _fire_event(self): 130*9c5db199SXin Li wait_urls = self.server._wait_urls 131*9c5db199SXin Li if self.path in wait_urls: 132*9c5db199SXin Li _, e = wait_urls[self.path] 133*9c5db199SXin Li e.set() 134*9c5db199SXin Li del wait_urls[self.path] 135*9c5db199SXin Li else: 136*9c5db199SXin Li if self.path not in self.server._urls: 137*9c5db199SXin Li # if the url is not in _urls, this means it was neither setup 138*9c5db199SXin Li # as a permanent, or event url. 139*9c5db199SXin Li logging.debug('URL %s not in watch list' % self.path) 140*9c5db199SXin Li 141*9c5db199SXin Li 142*9c5db199SXin Li @_handle_http_errors 143*9c5db199SXin Li def do_GET(self): 144*9c5db199SXin Li form = cgi.FieldStorage( 145*9c5db199SXin Li fp=self.rfile, 146*9c5db199SXin Li headers=self.headers, 147*9c5db199SXin Li environ={'REQUEST_METHOD': 'GET'}) 148*9c5db199SXin Li split_url = six.moves.urllib.parse.urlsplit(self.path) 149*9c5db199SXin Li path = split_url[2] 150*9c5db199SXin Li # Strip off query parameters to ensure that the url path 151*9c5db199SXin Li # matches any registered events. 152*9c5db199SXin Li self.path = path 153*9c5db199SXin Li args = six.moves.urllib.parse.parse_qs(split_url[3]) 154*9c5db199SXin Li if path in self.server._url_handlers: 155*9c5db199SXin Li self.server._url_handlers[path](self, args) 156*9c5db199SXin Li else: 157*9c5db199SXin Li six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 158*9c5db199SXin Li self._fire_event() 159*9c5db199SXin Li 160*9c5db199SXin Li 161*9c5db199SXin Li @_handle_http_errors 162*9c5db199SXin Li def do_HEAD(self): 163*9c5db199SXin Li six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self) 164*9c5db199SXin Li 165*9c5db199SXin Li 166*9c5db199SXin Liclass ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 167*9c5db199SXin Li def __init__(self, server_address, HandlerClass): 168*9c5db199SXin Li HTTPServer.__init__(self, server_address, HandlerClass) 169*9c5db199SXin Li 170*9c5db199SXin Li 171*9c5db199SXin Liclass HTTPListener(object): 172*9c5db199SXin Li # Point default docroot to a non-existent directory (instead of None) to 173*9c5db199SXin Li # avoid exceptions when page content is served through handlers only. 174*9c5db199SXin Li def __init__(self, port=0, docroot='/_', wait_urls={}, url_handlers={}): 175*9c5db199SXin Li self._server = ThreadedHTTPServer(('', port), FormHandler) 176*9c5db199SXin Li self.config_server(self._server, docroot, wait_urls, url_handlers) 177*9c5db199SXin Li 178*9c5db199SXin Li def config_server(self, server, docroot, wait_urls, url_handlers): 179*9c5db199SXin Li # Stuff some convenient data fields into the server object. 180*9c5db199SXin Li self._server.docroot = docroot 181*9c5db199SXin Li self._server._urls = set() 182*9c5db199SXin Li self._server._wait_urls = wait_urls 183*9c5db199SXin Li self._server._url_handlers = url_handlers 184*9c5db199SXin Li self._server._form_entries = {} 185*9c5db199SXin Li self._server_thread = threading.Thread( 186*9c5db199SXin Li target=self._server.serve_forever) 187*9c5db199SXin Li 188*9c5db199SXin Li def add_url(self, url): 189*9c5db199SXin Li """ 190*9c5db199SXin Li Add a url to the urls that the http server is actively watching for. 191*9c5db199SXin Li 192*9c5db199SXin Li Not adding a url via add_url or add_wait_url, and only installing a 193*9c5db199SXin Li handler will still result in that handler being executed, but this 194*9c5db199SXin Li server will warn in the debug logs that it does not expect that url. 195*9c5db199SXin Li 196*9c5db199SXin Li Args: 197*9c5db199SXin Li url (string): url suffix to listen to 198*9c5db199SXin Li """ 199*9c5db199SXin Li self._server._urls.add(url) 200*9c5db199SXin Li 201*9c5db199SXin Li def add_wait_url(self, url='/', matchParams={}): 202*9c5db199SXin Li """ 203*9c5db199SXin Li Add a wait url to the urls that the http server is aware of. 204*9c5db199SXin Li 205*9c5db199SXin Li Not adding a url via add_url or add_wait_url, and only installing a 206*9c5db199SXin Li handler will still result in that handler being executed, but this 207*9c5db199SXin Li server will warn in the debug logs that it does not expect that url. 208*9c5db199SXin Li 209*9c5db199SXin Li Args: 210*9c5db199SXin Li url (string): url suffix to listen to 211*9c5db199SXin Li matchParams (dictionary): an unused dictionary 212*9c5db199SXin Li 213*9c5db199SXin Li Returns: 214*9c5db199SXin Li e, and event object. Call e.wait() on the object to wait (block) 215*9c5db199SXin Li until the server receives the first request for the wait url. 216*9c5db199SXin Li 217*9c5db199SXin Li """ 218*9c5db199SXin Li e = threading.Event() 219*9c5db199SXin Li self._server._wait_urls[url] = (matchParams, e) 220*9c5db199SXin Li self._server._urls.add(url) 221*9c5db199SXin Li return e 222*9c5db199SXin Li 223*9c5db199SXin Li def add_url_handler(self, url, handler_func): 224*9c5db199SXin Li self._server._url_handlers[url] = handler_func 225*9c5db199SXin Li 226*9c5db199SXin Li def clear_form_entries(self): 227*9c5db199SXin Li self._server._form_entries = {} 228*9c5db199SXin Li 229*9c5db199SXin Li 230*9c5db199SXin Li def get_form_entries(self): 231*9c5db199SXin Li """Returns a dictionary of all field=values recieved by the server. 232*9c5db199SXin Li """ 233*9c5db199SXin Li return self._server._form_entries 234*9c5db199SXin Li 235*9c5db199SXin Li 236*9c5db199SXin Li def run(self): 237*9c5db199SXin Li logging.debug('http server on %s:%d' % 238*9c5db199SXin Li (self._server.server_name, self._server.server_port)) 239*9c5db199SXin Li self._server_thread.start() 240*9c5db199SXin Li 241*9c5db199SXin Li 242*9c5db199SXin Li def stop(self): 243*9c5db199SXin Li self._server.shutdown() 244*9c5db199SXin Li self._server.socket.close() 245*9c5db199SXin Li self._server_thread.join() 246*9c5db199SXin Li 247*9c5db199SXin Li 248*9c5db199SXin Liclass SecureHTTPServer(ThreadingMixIn, HTTPServer): 249*9c5db199SXin Li def __init__(self, server_address, HandlerClass, cert_path, key_path): 250*9c5db199SXin Li _socket = socket.socket(self.address_family, self.socket_type) 251*9c5db199SXin Li self.socket = ssl.wrap_socket(_socket, 252*9c5db199SXin Li server_side=True, 253*9c5db199SXin Li ssl_version=ssl.PROTOCOL_TLSv1, 254*9c5db199SXin Li certfile=cert_path, 255*9c5db199SXin Li keyfile=key_path) 256*9c5db199SXin Li BaseServer.__init__(self, server_address, HandlerClass) 257*9c5db199SXin Li self.server_bind() 258*9c5db199SXin Li self.server_activate() 259*9c5db199SXin Li 260*9c5db199SXin Li 261*9c5db199SXin Liclass SecureHTTPRequestHandler(FormHandler): 262*9c5db199SXin Li def setup(self): 263*9c5db199SXin Li self.connection = self.request 264*9c5db199SXin Li self.rfile = socket._fileobject(self.request, 'rb', self.rbufsize) 265*9c5db199SXin Li self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize) 266*9c5db199SXin Li 267*9c5db199SXin Li # Override the default logging methods to use the logging module directly. 268*9c5db199SXin Li def log_error(self, format, *args): 269*9c5db199SXin Li logging.warning("(httpd error) %s - - [%s] %s\n" % 270*9c5db199SXin Li (self.address_string(), self.log_date_time_string(), 271*9c5db199SXin Li format%args)) 272*9c5db199SXin Li 273*9c5db199SXin Li def log_message(self, format, *args): 274*9c5db199SXin Li logging.debug("%s - - [%s] %s\n" % 275*9c5db199SXin Li (self.address_string(), self.log_date_time_string(), 276*9c5db199SXin Li format%args)) 277*9c5db199SXin Li 278*9c5db199SXin Li 279*9c5db199SXin Liclass SecureHTTPListener(HTTPListener): 280*9c5db199SXin Li def __init__(self, 281*9c5db199SXin Li cert_path='/etc/login_trust_root.pem', 282*9c5db199SXin Li key_path='/etc/mock_server.key', 283*9c5db199SXin Li port=0, 284*9c5db199SXin Li docroot='/_', 285*9c5db199SXin Li wait_urls={}, 286*9c5db199SXin Li url_handlers={}): 287*9c5db199SXin Li self._server = SecureHTTPServer(('', port), 288*9c5db199SXin Li SecureHTTPRequestHandler, 289*9c5db199SXin Li cert_path, 290*9c5db199SXin Li key_path) 291*9c5db199SXin Li self.config_server(self._server, docroot, wait_urls, url_handlers) 292*9c5db199SXin Li 293*9c5db199SXin Li 294*9c5db199SXin Li def getsockname(self): 295*9c5db199SXin Li return self._server.socket.getsockname() 296*9c5db199SXin Li 297