""" open/DurusWorks/qp/hub/web.py Provides a HubServer-based HTTP/SCGI server. """ from durus.utils import as_bytes, join_bytes from qp.hub.dispatcher import HubServer, worker from qp.lib.site import Site from qp.pub.common import get_publisher, clear_publisher from qp.fill.html import url_unquote from signal import signal, SIGHUP import os import qp import socket import sys if sys.version < "3": from BaseHTTPServer import BaseHTTPRequestHandler else: from http.server import BaseHTTPRequestHandler try: import ssl class _LowLevelSocketWrapper(object): def __init__(self, low_level_socket): self._sock = low_level_socket def wrap_low_level_socket(socket, https_address): return ssl.wrap_socket( _LowLevelSocketWrapper(socket), certfile=https_address[2], keyfile=https_address[-1], server_side=True) except ImportError: pass # no ssl def shutdown_connection(connection): try: connection.shutdown(socket.SHUT_RDWR) except socket.error: pass class HTTPRequestHandler(BaseHTTPRequestHandler): server_version = "qp/1.0" def __init__(self, connection, address, info, **kwargs): self.required_cgi_environment = dict( SERVER_SOFTWARE=self.server_version, GATEWAY_INTERFACE='CGI/1.1') self.required_cgi_environment.update(kwargs) BaseHTTPRequestHandler.__init__( self, connection, address, info) def get_cgi_env(self, method): env = dict( SERVER_NAME=self.server.server_name, SERVER_PROTOCOL=self.protocol_version, SERVER_PORT=str(self.server.server_port), REQUEST_METHOD=method, REMOTE_ADDR=self.client_address[0], REMOTE_PORT=str(self.client_address[1])) if '?' in self.path: env['PATH_INFO'], env['QUERY_STRING'] = self.path.split('?', 1) else: env['PATH_INFO'] = self.path env['PATH_INFO'] = url_unquote(env['PATH_INFO']) script_name = self.required_cgi_environment.get('SCRIPT_NAME', '') if script_name: path_info = env['PATH_INFO'] if path_info.startswith(script_name): # and it better! env['PATH_INFO'] = path_info[len(script_name):] else: print("\nAll paths are expected to start with the script_name.") print(" The script_name is %r." % script_name) print(" The requested path is %r.\n" % self.path) env['CONTENT_TYPE'] = self.headers.get( "content-type") or "plain/text" 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 get_headers = (getattr(self.headers, 'get_all', None) or # py3k getattr(self.headers, 'getheaders')) accept = [] for line in get_headers('accept'): for part in line.split(','): part = part.strip() if part: accept.append(part) env['HTTP_ACCEPT'] = ','.join(accept) co = list(filter(None, get_headers('cookie') or [])) if co: env['HTTP_COOKIE'] = ', '.join(co) env.update(self.required_cgi_environment) return env def process(self, env): hit = get_publisher().process(self.rfile, env) response = hit.get_response() try: self.send_response(*response.get_status()) response.write(self.wfile, include_status=False, include_body=(self.command!='HEAD')) self.wfile.flush() except (IOError, socket.error): err = sys.exc_info()[1] print("Error while sending response ignored: %s" % err) def handle_one_request(self): self.raw_requestline = self.rfile.readline() if not self.raw_requestline: self.close_connection = 1 return if not self.parse_request(): return return self.process(self.get_cgi_env(self.command)) def finish(self): shutdown_connection(self.connection) def log_request(self, *args, **kwargs): pass def send_response(self, code, message=None): """ Copied, with regret, from BaseHTTPRequestHandler, except that the line that adds the 'Date' header is removed. """ self.log_request(code) if message is None: if code in self.responses: message = self.responses[code][0] else: message = '' if self.request_version != 'HTTP/0.9': self.wfile.write(as_bytes("%s %d %s\r\n" % (self.protocol_version, code, message))) self.send_header('Server', self.version_string()) COLON = as_bytes(":") # netstring utility functions def ns_read_size(input): """ The input is a binary-mode stream. If any bytes are available, the result is an int. If no bytes are available, the result is None. """ size = [] while 1: c = input.read(1) if c == COLON: break elif not c: if not size: return None raise IOError('short netstring read') size.append(c) return int(join_bytes(size)) COMMA = as_bytes(",") def ns_reads(input): """ The input is a binary-mode stream. If any bytes are available, the result is a byte_string. If no bytes are available, the result is None. """ size = ns_read_size(input) if size is None: return None data = [] while size > 0: s = input.read(size) if not s: raise IOError('short netstring read') data.append(s) size -= len(s) if input.read(1) != COMMA: raise IOError('missing netstring terminator') return join_bytes(data) def read_env(input): """ The input is a binary-mode stream. The result is a dict whose keys and values are str instances. """ headers = ns_reads(input) if headers is None: return None if not isinstance(headers, str): headers = headers.decode('iso-8859-1') items = headers.split("\0") items = items[:-1] assert len(items) % 2 == 0, items env = {} for i in range(0, len(items), 2): env[items[i]] = items[i+1] return env SCRIPT_NAME = "SCRIPT_NAME" PATH_INFO = "PATH_INFO" class SCGIHandler(object): def __init__(self, script_name=''): self.script_name = script_name def handle_connection(self, conn): input = conn.makefile("rb") output = conn.makefile("wb") env = read_env(input) if env is None: # Nothing here. return if (env.get(SCRIPT_NAME) and env.get(SCRIPT_NAME).startswith(self.script_name) and env.get(PATH_INFO) is None): # This looks like it is coming through mod_scgi and # needs repair. env[PATH_INFO] = env[SCRIPT_NAME][len(self.script_name):] env[SCRIPT_NAME] = self.script_name hit = get_publisher().process(input, env) response = hit.get_response() try: response.write(output) input.close() output.close() conn.close() except IOError: err = sys.exc_info()[1] print("IOError while sending response: %s" % err) shutdown_connection(conn) def run_web(site): logfile = open(site.get_logfile(), 'a') sys.stderr = sys.stdout = logfile site.start_durus_logging() site.get_publisher() # Make sure that the Publisher constructor works. clear_publisher() server = HubServer(site.get_name(), max_children=site.get_max_children(), busy_limit=site.get_busy_limit(), banned=site.get_banned(), dispatcher_command_prefix=site.get_dispatcher_command_prefix()) https_address = site.get_https_address() if https_address is not None and https_address[2:]: https_listen_address = https_address else: https_listen_address = None server.listen( site.get_scgi_address(), site.get_http_address(), https_listen_address, site.get_as_https_address(), *(site.get_other_addresses() or [])) site.ensure_uid_gid_not_root() def reexec(*args): server.do_stop() source = __file__ if source.endswith('.pyc') or source.endswith('.pyo'): source = source[:-1] argv = [sys.executable, source, site.get_name()] print("RESTART:", " ".join(argv)) os.execv(sys.executable, argv) signal(SIGHUP, reexec) server.run(handle_connection=get_connection_handler(site.get_name())) def get_connection_handler(site_name): site = Site(site_name) scgi_address = site.get_scgi_address() http_address = site.get_http_address() as_https_address = site.get_as_https_address() https_address = site.get_https_address() scgi_port = scgi_address and scgi_address[1] http_port = http_address and http_address[1] https_port = https_address and https_address[2:] and https_address[1] as_https_port = as_https_address and as_https_address[1] script_name = site.get_script_name() class ConnectionHandler(object): def handle_connection(self, connection): port = connection.getsockname()[1] if port == scgi_port: return SCGIHandler( script_name=script_name).handle_connection(connection) elif port == http_port: self.server_name, self.server_port = http_address client_address = connection.getpeername() HTTPRequestHandler(connection, client_address, self, SCRIPT_NAME=script_name) elif port == as_https_port: self.server_name, self.server_port = https_address client_address = connection.getpeername() HTTPRequestHandler( connection, client_address, self, HTTPS='on', SCRIPT_NAME=script_name) elif port == https_port: self.server_name, self.server_port = https_address[:2] client_address = connection.getpeername() ssl_connection = wrap_low_level_socket(connection, https_address) HTTPRequestHandler( ssl_connection, client_address, self, HTTPS='on', SSL_VERSION=ssl_connection.ssl_version, SCRIPT_NAME=script_name) else: site.get_publisher().handle_connection(connection) site.get_create_publisher()(hub=True) return ConnectionHandler().handle_connection def run_worker(site_name, parent_fd): worker(get_connection_handler(site_name), parent_fd) if __name__ == '__main__': if len(sys.argv) == 2: run_web(Site(sys.argv[-1])) else: run_worker(*sys.argv[-2:])