xref: /aosp_15_r20/external/cronet/build/android/lighttpd_server.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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