diff options
| author | cce <devnull@localhost> | 2006-01-09 06:26:36 +0000 |
|---|---|---|
| committer | cce <devnull@localhost> | 2006-01-09 06:26:36 +0000 |
| commit | 95c32e90659ff1969ae542e3d6533283a34a76c2 (patch) | |
| tree | e908afe34ff0ef71e15ac3c4a68f8088fdf78844 /paste/httpserver.py | |
| parent | 2a9341bf0034b61510ae575dcceefa8ecbfd2bc8 (diff) | |
| download | paste-95c32e90659ff1969ae542e3d6533283a34a76c2.tar.gz | |
moving httpserver from util sub-package up a level
Diffstat (limited to 'paste/httpserver.py')
| -rwxr-xr-x | paste/httpserver.py | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/paste/httpserver.py b/paste/httpserver.py new file mode 100755 index 0000000..b317a84 --- /dev/null +++ b/paste/httpserver.py @@ -0,0 +1,373 @@ +# (c) 2005 Clark C. Evans +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# This code was written with funding by http://prometheusresearch.com +""" +WSGI HTTP Server + +This is a minimalistic WSGI server using Python's built-in BaseHTTPServer; +if pyOpenSSL is installed, it also provides SSL capabilities. +""" + +# @@: add in protection against HTTP/1.0 clients who claim to +# be 1.1 but do not send a Content-Length + +# @@: add support for chunked encoding, this is not a 1.1 server +# till this is completed. + + +import BaseHTTPServer, SocketServer +import urlparse, sys, socket + +__all__ = ['WSGIHandlerMixin','WSGIServer','WSGIHandler', 'serve'] +__version__ = "0.2" + +class ContinueHook(object): + """ + When a client request includes a 'Expect: 100-continue' header, then + it is the responsibility of the server to send 100 Continue when it + is ready for the content body. This allows authentication, access + levels, and other exceptions to be detected *before* bandwith is + spent on the request body. + + This is a rfile wrapper that implements this functionality by + sending 100 Continue to the client immediately after the user + requests the content via a read() operation on the rfile stream. + After this response is sent, it becomes a pass-through object. + """ + + def __init__(self, rfile, write): + self._ContinueFile_rfile = rfile + self._ContinueFile_write = write + for attr in ('close','closed','fileno','flush', + 'mode','bufsize','softspace'): + if hasattr(rfile,attr): + setattr(self,attr,getattr(rfile,attr)) + for attr in ('read','readline','readlines'): + if hasattr(rfile,attr): + setattr(self,attr,getattr(self,'_ContinueFile_' + attr)) + + def _ContinueFile_send(self): + self._ContinueFile_write("HTTP/1.1 100 Continue\r\n\r\n") + rfile = self._ContinueFile_rfile + for attr in ('read','readline','readlines'): + if hasattr(rfile,attr): + setattr(self,attr,getattr(rfile,attr)) + + def _ContinueFile_read(self, size=-1): + self._ContinueFile_send() + return self._ContinueFile_rfile.readline(size) + + def _ContinueFile_readline(self, size=-1): + self._ContinueFile_send() + return self._ContinueFile_rfile.readline(size) + + def _ContinueFile_readlines(self, sizehint=0): + self._ContinueFile_send() + return self._ContinueFile_rfile.readlines(sizehint) + + +class WSGIHandlerMixin: + """ + WSGI mix-in for HTTPRequestHandler + + This class is a mix-in to provide WSGI functionality to any + HTTPRequestHandler derivative (as provided in Python's BaseHTTPServer). + This assumes a ``wsgi_application`` handler on ``self.server``. + """ + + def version_string(self): + """ behavior that BaseHTTPServer should have had """ + if not self.sys_version: + return self.server_version + else: + return self.server_version + ' ' + self.sys_version + + def wsgi_write_chunk(self, chunk): + """ + Write a chunk of the output stream; send headers if they + have not already been sent. + """ + if not self.wsgi_headers_sent: + self.wsgi_headers_sent = True + (status, headers) = self.wsgi_curr_headers + code, message = status.split(" ",1) + self.send_response(int(code),message) + # + # HTTP/1.1 compliance; either send Content-Length or + # signal that the connection is being closed. + # + send_close = True + for (k,v) in headers: + k = k.lower() + if 'content-length' == k: + send_close = False + if 'connection' == k: + if 'close' == v.lower(): + self.close_connection = 1 + send_close = False + self.send_header(k,v) + if send_close: + self.close_connection = 1 + self.send_header('Connection','close') + + self.end_headers() + self.wfile.write(chunk) + + def wsgi_start_response(self,status,response_headers,exc_info=None): + if exc_info: + try: + if self.wsgi_headers_sent: + raise exc_info[0], exc_info[1], exc_info[2] + else: + # In this case, we're going to assume that the + # higher-level code is currently handling the + # issue and returning a resonable response. + # self.log_error(repr(exc_info)) + pass + finally: + exc_info = None + elif self.wsgi_curr_headers: + assert 0, "Attempt to set headers a second time w/o an exc_info" + self.wsgi_curr_headers = (status, response_headers) + return self.wsgi_write_chunk + + def wsgi_setup(self, environ=None): + """ + Setup the member variables used by this WSGI mixin, including + the ``environ`` and status member variables. + + After the basic environment is created; the optional ``environ`` + argument can be used to override any settings. + """ + + (_,_,path,query,fragment) = urlparse.urlsplit(self.path) + (server_name, server_port) = self.server.server_address + + rfile = self.rfile + if 'HTTP/1.1' == self.protocol_version and \ + '100-continue' == self.headers.get('Expect',''): + rfile = ContinueHook(rfile, self.wfile.write) + + self.wsgi_environ = { + 'wsgi.version': (1,0) + ,'wsgi.url_scheme': 'http' + ,'wsgi.input': rfile + ,'wsgi.errors': sys.stderr + ,'wsgi.multithread': True + ,'wsgi.multiprocess': False + ,'wsgi.run_once': True + # CGI variables required by PEP-333 + ,'REQUEST_METHOD': self.command + ,'SCRIPT_NAME': '' # application is root of server + ,'PATH_INFO': path + ,'QUERY_STRING': query + ,'CONTENT_TYPE': self.headers.get('Content-Type', '') + ,'CONTENT_LENGTH': self.headers.get('Content-Length', '') + ,'SERVER_NAME': server_name + ,'SERVER_PORT': str(server_port) + ,'SERVER_PROTOCOL': self.request_version + # CGI not required by PEP-333 + ,'REMOTE_ADDR': self.client_address[0] + ,'REMOTE_HOST': self.address_string() + } + + for k,v in self.headers.items(): + k = 'HTTP_' + k.replace("-","_").upper() + if k in ('HTTP_CONTENT_TYPE','HTTP_CONTENT_LENGTH'): + continue + self.wsgi_environ[k] = v + + if hasattr(self.connection,'get_context'): + self.wsgi_environ['wsgi.url_scheme'] = 'https' + # @@: extract other SSL parameters from pyOpenSSL at... + # http://www.modssl.org/docs/2.8/ssl_reference.html#ToC25 + + if environ: + assert isinstance(environ,dict) + self.wsgi_environ.update(environ) + if 'on' == environ.get('HTTPS'): + self.wsgi_environ['wsgi.url_scheme'] = 'https' + + self.wsgi_curr_headers = None + self.wsgi_headers_sent = False + + def wsgi_connection_drop(self, environ, exce): + """ + Override this if you're interested in socket exceptions, such + as when the user clicks 'Cancel' during a file download. + """ + pass + + def wsgi_execute(self, environ=None): + """ + Invoke the server's ``wsgi_application``. + """ + + self.wsgi_setup(environ) + + try: + result = self.server.wsgi_application(self.wsgi_environ, + self.wsgi_start_response) + try: + for chunk in result: + self.wsgi_write_chunk(chunk) + finally: + if hasattr(result,'close'): + result.close() + except socket.error, exce: + self.wsgi_connection_drop(environ, exce) + return + except: + if not self.wsgi_headers_sent: + self.wsgi_curr_headers = ('500 Internal Server Error', + [('Content-type', 'text/plain')]) + self.wsgi_write_chunk("Internal Server Error\n") + raise + +class WSGIHandler(WSGIHandlerMixin, BaseHTTPServer.BaseHTTPRequestHandler): + """ + A WSGI handler that overrides POST, GET and HEAD to delegate + requests to the server's ``wsgi_application``. + """ + do_POST = do_GET = do_HEAD = do_DELETE = do_PUT = do_TRACE = \ + WSGIHandlerMixin.wsgi_execute + +# +# SSL Functionality +# +# This implementation was motivated by Sebastien Martini's SSL example +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 +# +try: + from OpenSSL import SSL +except ImportError: + # Do not require pyOpenSSL to be installed, but disable SSL + # functionality in that case. + SSL = None + class SecureHTTPServer(BaseHTTPServer.HTTPServer): + def __init__(self, server_address, RequestHandlerClass, + ssl_context=None): + assert not ssl_context, "pyOpenSSL not installed" + BaseHTTPServer.HTTPServer.__init__(self, server_address, + RequestHandlerClass) +else: + + class _ConnFixer(object): + """ wraps a socket connection so it implements makefile """ + def __init__(self, conn): + self.__conn = conn + def makefile(self, mode, bufsize): + return socket._fileobject(self.__conn, mode, bufsize) + def __getattr__(self, attrib): + return getattr(self.__conn, attrib) + + class SecureHTTPServer(BaseHTTPServer.HTTPServer): + """ + Provides SSL server functionality on top of the BaseHTTPServer + by overriding _private_ members of Python's standard + distribution. The interface for this instance only changes by + adding a an optional ssl_context attribute to the constructor: + + cntx = SSL.Context(SSL.SSLv23_METHOD) + cntx.use_privatekey_file("host.pem") + cntx.use_certificate_file("host.pem") + + The certificates can be generated with openssl as follows: + + $ openssl genrsa 1024 > host.key + $ chmod 400 host.key + $ openssl req -new -x509 -nodes -sha1 -days 365 \ + -key host.key > host.cert + $ cat host.cert host.key > host.pem + $ chmod 400 host.pem + + """ + + def __init__(self, server_address, RequestHandlerClass, + ssl_context=None): + # This overrides the implementation of __init__ in python's + # SocketServer.TCPServer (which BaseHTTPServer.HTTPServer + # does not override, thankfully). + BaseHTTPServer.HTTPServer.__init__(self, server_address, + RequestHandlerClass) + self.socket = socket.socket(self.address_family, + self.socket_type) + self.ssl_context = ssl_context + if ssl_context: + self.socket = SSL.Connection(ssl_context, self.socket) + self.server_bind() + self.server_activate() + + def get_request(self): + # The default SSL request object does not seem to have a + # ``makefile(mode, bufsize)`` method as expected by + # Socketserver.StreamRequestHandler. + (conn,info) = self.socket.accept() + if self.ssl_context: + conn = _ConnFixer(conn) + return (conn,info) + +class WSGIServer(SocketServer.ThreadingMixIn, SecureHTTPServer): + server_version = 'WSGIServer/' + __version__ + def __init__(self, wsgi_application, server_address, + RequestHandlerClass=None, ssl_context=None): + SecureHTTPServer.__init__(self, server_address, + RequestHandlerClass, ssl_context) + self.wsgi_application = wsgi_application + +def serve(application, host=None, port=None, handler=None, ssl_pem=None, + server_version=None, protocol_version=None, start_loop=True): + + ssl_context = None + if ssl_pem: + assert SSL, "pyOpenSSL is not installed" + port = port or 4443 + ssl_context = SSL.Context(SSL.SSLv23_METHOD) + ssl_context.use_privatekey_file(ssl_pem) + ssl_context.use_certificate_file(ssl_pem) + + server_address = (host or "127.0.0.1", port or 8080) + + if not handler: + handler = WSGIHandler + if server_version: + handler.server_version = server_version + handler.sys_version = None + + if protocol_version: + handler.protocol_version = protocol_version + + server = WSGIServer(application, server_address, handler, ssl_context) + print "serving on %s:%s" % server.server_address + if start_loop: + try: + server.serve_forever() + except KeyboardInterrupt: + # allow CTRL+C to shutdown + pass + return server + +# For paste.deploy server instantiation (egg:Paste#http) +# Note: this gets a separate function because it has to expect string +# arguments (though that's not much of an issue yet, ever?) +def server_runner(wsgi_app, global_conf, host=None, port=None, ssl_pem=None, + server_version=None, protocol_version=None): + """ + A simple HTTP server. Also supports SSL if you give it an + ``ssl_pem`` argument. + """ + if port: + port = int(port) + serve(wsgi_app, host=host, port=port, ssl_pem=ssl_pem, + server_version=server_version, protocol_version=protocol_version, + start_loop=True) + +if __name__ == '__main__': + # serve exactly 3 requests and then stop, use an external + # program like wget or curl to submit these 3 requests. + from paste.wsgilib import dump_environ + #serve(dump_environ, ssl_pem="test.pem") + serve(dump_environ, server_version="Wombles/1.0", + protocol_version="HTTP/1.1") + |
