xref: /aosp_15_r20/external/autotest/client/cros/httpd.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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