156 lines
5.8 KiB
Python
Executable File
156 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""A simple, single threaded, synchronous HTTP server.
|
|
"""
|
|
import sys
|
|
import socket
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
from socketserver import BaseServer
|
|
import urllib.request, urllib.parse, urllib.error
|
|
import quixote
|
|
from quixote import get_publisher
|
|
from quixote.util import import_object
|
|
try:
|
|
from scgi.systemd_socket import get_systemd_socket
|
|
except ImportError:
|
|
def get_systemd_socket():
|
|
return None
|
|
|
|
|
|
class SockInheritHTTPServer(HTTPServer):
|
|
def __init__(self, address_info, handler, bind_and_activate=True):
|
|
# This is ugly. We have to re-implement HTTPServer.__init__
|
|
# and server_bind(). We want to get the inherited socket if
|
|
# available. If we inherit then we need to skip the bind() call.
|
|
BaseServer.__init__(self, address_info, handler)
|
|
sock = get_systemd_socket()
|
|
if sock is not None:
|
|
print('Using inherited socket %s' % (sock.getsockname(),))
|
|
self._skip_bind = True
|
|
else:
|
|
sock = socket.socket(self.address_family, self.socket_type)
|
|
self._skip_bind = False
|
|
self.socket = sock
|
|
if bind_and_activate:
|
|
try:
|
|
self.server_bind()
|
|
self.server_activate()
|
|
except:
|
|
self.server_close()
|
|
raise
|
|
|
|
def server_bind(self):
|
|
if not self._skip_bind:
|
|
HTTPServer.server_bind(self)
|
|
else:
|
|
self.server_address = self.socket.getsockname()
|
|
host, port = self.socket.getsockname()[:2]
|
|
self.server_name = socket.getfqdn(host)
|
|
self.server_port = port
|
|
|
|
|
|
|
|
class HTTPRequestHandler(BaseHTTPRequestHandler):
|
|
|
|
required_cgi_environment = {}
|
|
|
|
protocol_version = 'HTTP/1.1'
|
|
|
|
def get_cgi_env(self, method):
|
|
env = dict(
|
|
SERVER_SOFTWARE="Quixote/%s" % quixote.__version__,
|
|
SERVER_NAME=self.server.server_name,
|
|
GATEWAY_INTERFACE='CGI/1.1',
|
|
SERVER_PROTOCOL=self.protocol_version,
|
|
SERVER_PORT=str(self.server.server_port),
|
|
REQUEST_METHOD=method,
|
|
REMOTE_ADDR=self.client_address[0],
|
|
SCRIPT_NAME='')
|
|
if '?' in self.path:
|
|
env['PATH_INFO'], env['QUERY_STRING'] = self.path.split('?', 1)
|
|
else:
|
|
env['PATH_INFO'] = self.path
|
|
env['PATH_INFO'] = urllib.parse.unquote(env['PATH_INFO'])
|
|
env['CONTENT_TYPE'] = self.headers.get('content-type')
|
|
env['CONTENT_LENGTH'] = self.headers.get('content-length') or "0"
|
|
for name, value in self.headers.items():
|
|
header_name = 'HTTP_' + name.upper().replace('-', '_')
|
|
env[header_name] = value
|
|
accept = []
|
|
for line in self.headers.getallmatchingheaders('accept'):
|
|
if line[:1] in "\t\n\r ":
|
|
accept.append(line.strip())
|
|
else:
|
|
accept = accept + line[7:].split(',')
|
|
env['HTTP_ACCEPT'] = ','.join(accept)
|
|
co = [c for c in self.headers.get_all('cookie') or [] if c]
|
|
if co:
|
|
env['HTTP_COOKIE'] = ', '.join(co)
|
|
env.update(self.required_cgi_environment)
|
|
return env
|
|
|
|
def process(self, env, include_body=True):
|
|
response = get_publisher().process(self.rfile, env)
|
|
if self.protocol_version == 'HTTP/1.1':
|
|
# single threaded server, persistent connections will block others
|
|
response.set_header('connection', 'close')
|
|
try:
|
|
self.send_response(response.get_status_code(), response.get_reason_phrase())
|
|
self.flush_headers()
|
|
response.write(self.wfile, include_status=False, include_body=include_body)
|
|
except IOError as err:
|
|
print("IOError while sending response ignored: %s" % err)
|
|
|
|
def do_POST(self):
|
|
return self.process(self.get_cgi_env('POST'))
|
|
|
|
def do_GET(self):
|
|
return self.process(self.get_cgi_env('GET'))
|
|
|
|
def do_HEAD(self):
|
|
return self.process(self.get_cgi_env('HEAD'), include_body=False)
|
|
|
|
def do_OPTIONS(self):
|
|
return self.process(self.get_cgi_env('OPTIONS'), include_body=False)
|
|
|
|
def send_response(self, code, message=None):
|
|
"""
|
|
Copied, with regret, from BaseHTTPRequestHandler, except that the line
|
|
that adds the 'Date' header is removed to avoid duplicating the one
|
|
that Quixote adds and the log_request() call has been removed.
|
|
"""
|
|
self.send_response_only(code, message)
|
|
self.send_header('Server', self.version_string())
|
|
|
|
def run(create_publisher, host='', port=80, https=False):
|
|
"""Runs a simple, single threaded, synchronous HTTP server that
|
|
publishes a Quixote application.
|
|
"""
|
|
if https:
|
|
HTTPRequestHandler.required_cgi_environment['HTTPS'] = 'on'
|
|
httpd = SockInheritHTTPServer((host, port), HTTPRequestHandler)
|
|
def handle_error(request, client_address):
|
|
HTTPServer.handle_error(httpd, request, client_address)
|
|
if sys.exc_info()[0] is SystemExit:
|
|
raise
|
|
httpd.handle_error = handle_error
|
|
publisher = create_publisher()
|
|
if publisher.logger.access_log is None:
|
|
publisher.logger.access_log = sys.stdout
|
|
try:
|
|
httpd.serve_forever()
|
|
finally:
|
|
httpd.server_close()
|
|
|
|
if __name__ == '__main__':
|
|
from quixote.server.util import get_server_parser
|
|
parser = get_server_parser(run.__doc__)
|
|
parser.add_option(
|
|
'--https', dest="https", default=False, action="store_true",
|
|
help=("Force the scheme for all requests to be https. "
|
|
"Not that this is for running the simple server "
|
|
"through a proxy or tunnel that provides real SSL "
|
|
"support. The simple server itself does not. "))
|
|
(options, args) = parser.parse_args()
|
|
run(import_object(options.factory), host=options.host, port=options.port,
|
|
https=options.https)
|