1#!/usr/bin/env python3 2# 3# Copyright 2012 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Provides a convenient wrapper for spawning a test lighttpd instance. 8 9Usage: 10 lighttpd_server PATH_TO_DOC_ROOT 11""" 12 13 14import codecs 15import contextlib 16import http.client 17import os 18import random 19import shutil 20import socket 21import subprocess 22import sys 23import tempfile 24import time 25 26from pylib import constants 27from pylib import pexpect 28 29 30class LighttpdServer: 31 """Wraps lighttpd server, providing robust startup. 32 33 Args: 34 document_root: Path to root of this server's hosted files. 35 port: TCP port on the _host_ machine that the server will listen on. If 36 omitted it will attempt to use 9000, or if unavailable it will find 37 a free port from 8001 - 8999. 38 lighttpd_path, lighttpd_module_path: Optional paths to lighttpd binaries. 39 base_config_path: If supplied this file will replace the built-in default 40 lighttpd config file. 41 extra_config_contents: If specified, this string will be appended to the 42 base config (default built-in, or from base_config_path). 43 config_path, error_log, access_log: Optional paths where the class should 44 place temporary files for this session. 45 """ 46 47 def __init__(self, document_root, port=None, 48 lighttpd_path=None, lighttpd_module_path=None, 49 base_config_path=None, extra_config_contents=None, 50 config_path=None, error_log=None, access_log=None): 51 self.temp_dir = tempfile.mkdtemp(prefix='lighttpd_for_chrome_android') 52 self.document_root = os.path.abspath(document_root) 53 self.fixed_port = port 54 self.port = port or constants.LIGHTTPD_DEFAULT_PORT 55 self.server_tag = 'LightTPD ' + str(random.randint(111111, 999999)) 56 self.lighttpd_path = lighttpd_path or '/usr/sbin/lighttpd' 57 self.lighttpd_module_path = lighttpd_module_path or '/usr/lib/lighttpd' 58 self.base_config_path = base_config_path 59 self.extra_config_contents = extra_config_contents 60 self.config_path = config_path or self._Mktmp('config') 61 self.error_log = error_log or self._Mktmp('error_log') 62 self.access_log = access_log or self._Mktmp('access_log') 63 self.pid_file = self._Mktmp('pid_file') 64 self.process = None 65 66 def _Mktmp(self, name): 67 return os.path.join(self.temp_dir, name) 68 69 @staticmethod 70 def _GetRandomPort(): 71 # The ports of test server is arranged in constants.py. 72 return random.randint(constants.LIGHTTPD_RANDOM_PORT_FIRST, 73 constants.LIGHTTPD_RANDOM_PORT_LAST) 74 75 def StartupHttpServer(self): 76 """Starts up a http server with specified document root and port.""" 77 # If we want a specific port, make sure no one else is listening on it. 78 if self.fixed_port: 79 self._KillProcessListeningOnPort(self.fixed_port) 80 while True: 81 if self.base_config_path: 82 # Read the config 83 with codecs.open(self.base_config_path, 'r', 'utf-8') as f: 84 config_contents = f.read() 85 else: 86 config_contents = self._GetDefaultBaseConfig() 87 if self.extra_config_contents: 88 config_contents += self.extra_config_contents 89 # Write out the config, filling in placeholders from the members of |self| 90 with codecs.open(self.config_path, 'w', 'utf-8') as f: 91 f.write(config_contents % self.__dict__) 92 if (not os.path.exists(self.lighttpd_path) or 93 not os.access(self.lighttpd_path, os.X_OK)): 94 raise EnvironmentError( 95 'Could not find lighttpd at %s.\n' 96 'It may need to be installed (e.g. sudo apt-get install lighttpd)' 97 % self.lighttpd_path) 98 # pylint: disable=no-member 99 self.process = pexpect.spawn(self.lighttpd_path, 100 ['-D', '-f', self.config_path, 101 '-m', self.lighttpd_module_path], 102 cwd=self.temp_dir) 103 client_error, server_error = self._TestServerConnection() 104 if not client_error: 105 assert int(open(self.pid_file, 'r').read()) == self.process.pid 106 break 107 self.process.close() 108 109 if self.fixed_port or 'in use' not in server_error: 110 print('Client error:', client_error) 111 print('Server error:', server_error) 112 return False 113 self.port = self._GetRandomPort() 114 return True 115 116 def ShutdownHttpServer(self): 117 """Shuts down our lighttpd processes.""" 118 if self.process: 119 self.process.terminate() 120 shutil.rmtree(self.temp_dir, ignore_errors=True) 121 122 def _TestServerConnection(self): 123 # Wait for server to start 124 server_msg = '' 125 for timeout in range(1, 5): 126 client_error = None 127 try: 128 with contextlib.closing( 129 http.client.HTTPConnection('127.0.0.1', self.port, 130 timeout=timeout)) as http_client: 131 http_client.set_debuglevel(timeout > 3) 132 http_client.request('HEAD', '/') 133 r = http_client.getresponse() 134 r.read() 135 if (r.status == 200 and r.reason == 'OK' and 136 r.getheader('Server') == self.server_tag): 137 return (None, server_msg) 138 client_error = ('Bad response: %s %s version %s\n ' % 139 (r.status, r.reason, r.version) + 140 '\n '.join([': '.join(h) for h in r.getheaders()])) 141 except (http.client.HTTPException, socket.error) as client_error: 142 pass # Probably too quick connecting: try again 143 # Check for server startup error messages 144 # pylint: disable=no-member 145 ix = self.process.expect([pexpect.TIMEOUT, pexpect.EOF, '.+'], 146 timeout=timeout) 147 if ix == 2: # stdout spew from the server 148 server_msg += self.process.match.group(0) # pylint: disable=no-member 149 elif ix == 1: # EOF -- server has quit so giveup. 150 client_error = client_error or 'Server exited' 151 break 152 return (client_error or 'Timeout', server_msg) 153 154 @staticmethod 155 def _KillProcessListeningOnPort(port): 156 """Checks if there is a process listening on port number |port| and 157 terminates it if found. 158 159 Args: 160 port: Port number to check. 161 """ 162 if subprocess.call(['fuser', '-kv', '%d/tcp' % port]) == 0: 163 # Give the process some time to terminate and check that it is gone. 164 time.sleep(2) 165 assert subprocess.call(['fuser', '-v', '%d/tcp' % port]) != 0, \ 166 'Unable to kill process listening on port %d.' % port 167 168 @staticmethod 169 def _GetDefaultBaseConfig(): 170 return """server.tag = "%(server_tag)s" 171server.modules = ( "mod_access", 172 "mod_accesslog", 173 "mod_alias", 174 "mod_cgi", 175 "mod_rewrite" ) 176 177# default document root required 178#server.document-root = "." 179 180# files to check for if .../ is requested 181index-file.names = ( "index.php", "index.pl", "index.cgi", 182 "index.html", "index.htm", "default.htm" ) 183# mimetype mapping 184mimetype.assign = ( 185 ".gif" => "image/gif", 186 ".jpg" => "image/jpeg", 187 ".jpeg" => "image/jpeg", 188 ".png" => "image/png", 189 ".svg" => "image/svg+xml", 190 ".css" => "text/css", 191 ".html" => "text/html", 192 ".htm" => "text/html", 193 ".xhtml" => "application/xhtml+xml", 194 ".xhtmlmp" => "application/vnd.wap.xhtml+xml", 195 ".js" => "application/x-javascript", 196 ".log" => "text/plain", 197 ".conf" => "text/plain", 198 ".text" => "text/plain", 199 ".txt" => "text/plain", 200 ".dtd" => "text/xml", 201 ".xml" => "text/xml", 202 ".manifest" => "text/cache-manifest", 203 ) 204 205# Use the "Content-Type" extended attribute to obtain mime type if possible 206mimetype.use-xattr = "enable" 207 208## 209# which extensions should not be handle via static-file transfer 210# 211# .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi 212static-file.exclude-extensions = ( ".php", ".pl", ".cgi" ) 213 214server.bind = "127.0.0.1" 215server.port = %(port)s 216 217## virtual directory listings 218dir-listing.activate = "enable" 219#dir-listing.encoding = "iso-8859-2" 220#dir-listing.external-css = "style/oldstyle.css" 221 222## enable debugging 223#debug.log-request-header = "enable" 224#debug.log-response-header = "enable" 225#debug.log-request-handling = "enable" 226#debug.log-file-not-found = "enable" 227 228#### SSL engine 229#ssl.engine = "enable" 230#ssl.pemfile = "server.pem" 231 232# Autogenerated test-specific config follows. 233 234cgi.assign = ( ".cgi" => "/usr/bin/env", 235 ".pl" => "/usr/bin/env", 236 ".asis" => "/bin/cat", 237 ".php" => "/usr/bin/php-cgi" ) 238 239server.errorlog = "%(error_log)s" 240accesslog.filename = "%(access_log)s" 241server.upload-dirs = ( "/tmp" ) 242server.pid-file = "%(pid_file)s" 243server.document-root = "%(document_root)s" 244 245""" 246 247 248def main(argv): 249 server = LighttpdServer(*argv[1:]) 250 try: 251 if server.StartupHttpServer(): 252 input('Server running at http://127.0.0.1:%s -' 253 ' press Enter to exit it.' % server.port) 254 else: 255 print('Server exit code:', server.process.exitstatus) 256 finally: 257 server.ShutdownHttpServer() 258 259 260if __name__ == '__main__': 261 sys.exit(main(sys.argv)) 262