PK¥…û48®.ãÀÀpaste/profilemiddleware.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php # Deprecated 18 Dec 2005 """ Deprecated -- see ``paste.debug.profile`` """ import warnings from paste.debug.profile import * __deprecated__ = True warnings.warn( "The paste.profilemiddleware module has been moved to " "paste.debug.profile", DeprecationWarning, 2) PK¥…û4ñ°„ù»»paste/wsgiwrappers.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php import paste.httpexceptions from paste.request import EnvironHeaders, parse_formvars, parse_dict_querystring, get_cookie_dict from paste.util.multidict import MultiDict from paste.response import HeaderDict import paste.registry as registry import paste.httpexceptions from Cookie import SimpleCookie # This should be set with the registry to a dict having at least: # content_type, charset settings = registry.StackedObjectProxy(default=dict(content_type='text/html', charset='UTF-8')) class environ_getter(object): """For delegating an attribute to a key in self.environ.""" # @@: Also __set__? Should setting be allowed? def __init__(self, key, default='', default_factory=None): self.key = key self.default = default self.default_factory = default_factory def __get__(self, obj, type=None): if type is None: return self if self.key not in obj.environ: if self.default_factory: val = obj.environ[self.key] = self.default_factory() return val else: return self.default return obj.environ[self.key] def __repr__(self): return '' % self.key class WSGIRequest(object): """WSGI Request API Object This object represents a WSGI request with a more friendly interface. This does not expose every detail of the WSGI environment, and does not in any way express anything beyond what is available in the environment dictionary. *All* state is kept in the environment dictionary; this is essential for interoperability. You are free to subclass this object. """ def __init__(self, environ, urlvars={}): self.environ = environ # This isn't "state" really, since the object is derivative: self.headers = EnvironHeaders(environ) body = environ_getter('wsgi.input') scheme = environ_getter('wsgi.url_scheme') method = environ_getter('REQUEST_METHOD') script_name = environ_getter('SCRIPT_NAME') path_info = environ_getter('PATH_INFO') urlvars = environ_getter('paste.urlvars', default_factory=dict) def host(self): """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME""" return self.environ.get('HTTP_HOST', self.environ.get('SERVER_NAME')) host = property(host, doc=host.__doc__) def GET(self): """ Dictionary-like object representing the QUERY_STRING parameters. Always present, if possibly empty. If the same key is present in the query string multiple times, it will be present as a list. """ return parse_dict_querystring(self.environ) GET = property(GET, doc=GET.__doc__) def POST(self): """Dictionary-like object representing the POST body. Most values are strings, but file uploads can be FieldStorage objects. If this is not a POST request, or the body is not encoded fields (e.g., an XMLRPC request) then this will be empty. This will consume wsgi.input when first accessed if applicable, but the output will be put in environ['paste.post_vars'] """ return parse_formvars(self.environ, include_get_vars=False) POST = property(POST, doc=POST.__doc__) def params(self): """MultiDict of keys from POST, GET, URL dicts Return a key value from the parameters, they are checked in the following order: POST, GET, URL Additional methods supported: ``getlist(key)`` Returns a list of all the values by that key, collected from POST, GET, URL dicts """ pms = MultiDict() pms.update(self.POST) pms.update(self.GET) return pms params = property(params, doc=params.__doc__) def cookies(self): """Dictionary of cookies keyed by cookie name. Just a plain dictionary, may be empty but not None. """ return get_cookie_dict(self.environ) cookies = property(cookies, doc=cookies.__doc__) class WSGIResponse(object): "A basic HTTP response, with content and dictionary-accessed headers" def __init__(self, content='', mimetype=None, code=200): if not mimetype: mimetype = "%s; charset=%s" % (settings['content_type'], settings['charset']) self.content = [content] self.headers = HeaderDict() self.headers['Content-Type'] = mimetype self.cookies = SimpleCookie() self.status_code = code def __str__(self): "Full HTTP message, including headers" return '\n'.join(['%s: %s' % (key, value) for key, value in self.headers.items()]) \ + '\n\n' + ''.join(self.content) def has_header(self, header): "Case-insensitive check for a header" header = header.lower() for key in self.headers.keys(): if key.lower() == header: return True return False def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=None): self.cookies[key] = value for var in ('max_age', 'path', 'domain', 'secure', 'expires'): val = locals()[var] if val is not None: self.cookies[key][var.replace('_', '-')] = val def delete_cookie(self, key): try: self.cookies[key]['max_age'] = 0 except KeyError: pass def get_content_as_string(self, encoding): """ Returns the content as a string, encoding it from a Unicode object if necessary. """ if isinstance(self.content, unicode): return [''.join(self.content).encode(encoding)] return self.content def wsgi_response(self, encoding=None): if not encoding: encoding = settings['charset'] status_text = STATUS_CODE_TEXT[self.status_code] status = '%s %s' % (self.status_code, status_text) response_headers = self.headers.items() for c in self.cookies.values(): response_headers.append(('Set-Cookie', c.output(header=''))) output = self.get_content_as_string(encoding) return status, response_headers, output # The remaining methods partially implement the file-like object interface. # See http://docs.python.org/lib/bltin-file-objects.html def write(self, content): self.content.append(content) def flush(self): pass def tell(self): return len(self.content) ## @@ I'd love to remove this, but paste.httpexceptions.get_exception ## doesn't seem to work... # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html STATUS_CODE_TEXT = { 100: 'CONTINUE', 101: 'SWITCHING PROTOCOLS', 200: 'OK', 201: 'CREATED', 202: 'ACCEPTED', 203: 'NON-AUTHORITATIVE INFORMATION', 204: 'NO CONTENT', 205: 'RESET CONTENT', 206: 'PARTIAL CONTENT', 300: 'MULTIPLE CHOICES', 301: 'MOVED PERMANENTLY', 302: 'FOUND', 303: 'SEE OTHER', 304: 'NOT MODIFIED', 305: 'USE PROXY', 306: 'RESERVED', 307: 'TEMPORARY REDIRECT', 400: 'BAD REQUEST', 401: 'UNAUTHORIZED', 402: 'PAYMENT REQUIRED', 403: 'FORBIDDEN', 404: 'NOT FOUND', 405: 'METHOD NOT ALLOWED', 406: 'NOT ACCEPTABLE', 407: 'PROXY AUTHENTICATION REQUIRED', 408: 'REQUEST TIMEOUT', 409: 'CONFLICT', 410: 'GONE', 411: 'LENGTH REQUIRED', 412: 'PRECONDITION FAILED', 413: 'REQUEST ENTITY TOO LARGE', 414: 'REQUEST-URI TOO LONG', 415: 'UNSUPPORTED MEDIA TYPE', 416: 'REQUESTED RANGE NOT SATISFIABLE', 417: 'EXPECTATION FAILED', 500: 'INTERNAL SERVER ERROR', 501: 'NOT IMPLEMENTED', 502: 'BAD GATEWAY', 503: 'SERVICE UNAVAILABLE', 504: 'GATEWAY TIMEOUT', 505: 'HTTP VERSION NOT SUPPORTED', } PK¥…û4Ãwüíípaste/response.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """Routines to generate WSGI responses""" ############################################################ ## Headers ############################################################ import warnings class HeaderDict(dict): """ This represents response headers. It handles the headers as a dictionary, with case-insensitive keys. Also there is an ``.add(key, value)`` method, which sets the key, or adds the value to the current value (turning it into a list if necessary). For passing to WSGI there is a ``.headeritems()`` method which is like ``.items()`` but unpacks value that are lists. It also handles encoding -- all headers are encoded in ASCII (if they are unicode). @@: Should that encoding be ISO-8859-1 or UTF-8? I'm not sure what the spec says. """ def __getitem__(self, key): return dict.__getitem__(self, self.normalize(key)) def __setitem__(self, key, value): dict.__setitem__(self, self.normalize(key), value) def __delitem__(self, key): dict.__delitem__(self, self.normalize(key)) def __contains__(self, key): return dict.__contains__(self, self.normalize(key)) has_key = __contains__ def pop(self, key): return dict.pop(self, self.normalize(key)) def update(self, other): for key in other: self[self.normalize(key)] = other[key] def normalize(self, key): return str(key).lower().strip() def add(self, key, value): key = self.normalize(key) if key in self: if isinstance(self[key], list): self[key].append(value) else: self[key] = [self[key], value] else: self[key] = value def headeritems(self): result = [] for key in self: if isinstance(self[key], list): for v in self[key]: result.append((key, str(v))) else: result.append((key, str(self[key]))) return result #@classmethod def fromlist(cls, seq): self = cls() for name, value in seq: self.add(name, value) return self fromlist = classmethod(fromlist) def has_header(headers, name): """ Is header named ``name`` present in headers? """ name = name.lower() for header, value in headers: if header.lower() == name: return True return False def header_value(headers, name): """ Returns the header's value, or None if no such header. If a header appears more than once, all the values of the headers are joined with ','. Note that this is consistent /w RFC 2616 section 4.2 which states: It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. However, note that the original netscape usage of 'Set-Cookie', especially in MSIE which contains an 'expires' date will is not compatible with this particular concatination method. """ name = name.lower() result = [value for header, value in headers if header.lower() == name] if result: return ','.join(result) else: return None def remove_header(headers, name): """ Removes the named header from the list of headers. Returns the value of that header, or None if no header found. If multiple headers are found, only the last one is returned. """ name = name.lower() i = 0 result = None while i < len(headers): if headers[i][0].lower() == name: result = headers[i][1] del headers[i] continue i += 1 return result def replace_header(headers, name, value): """ Updates the headers replacing the first occurance of the given name with the value provided; asserting that no further occurances happen. Note that this is _not_ the same as remove_header and then append, as two distinct operations (del followed by an append) are not atomic in a threaded environment. Returns the previous header value for the provided name, if any. Clearly one should not use this function with ``set-cookie`` or other names that may have more than one occurance in the headers. """ name = name.lower() i = 0 result = None while i < len(headers): if headers[i][0].lower() == name: assert not result, "two values for the header '%s' found" % name result = headers[i][1] headers[i] = (name,value) i += 1 if not result: headers.append((name,value)) return result ############################################################ ## Deprecated methods ############################################################ def error_body_response(error_code, message, __warn=True): """ Returns a standard HTML response page for an HTTP error. **Note:** Deprecated """ if __warn: warnings.warn( 'wsgilib.error_body_response is deprecated; use the ' 'wsgi_application method on an HTTPException object ' 'instead', DeprecationWarning, 2) return '''\ %(error_code)s

%(error_code)s

%(message)s ''' % { 'error_code': error_code, 'message': message, } def error_response(environ, error_code, message, debug_message=None, __warn=True): """ Returns the status, headers, and body of an error response. Use like:: status, headers, body = wsgilib.error_response( '301 Moved Permanently', 'Moved to %s' % (url, url)) start_response(status, headers) return [body] **Note:** Deprecated """ if __warn: warnings.warn( 'wsgilib.error_response is deprecated; use the ' 'wsgi_application method on an HTTPException object ' 'instead', DeprecationWarning, 2) if debug_message and environ.get('paste.config', {}).get('debug'): message += '\n\n' % debug_message body = error_body_response(error_code, message, __warn=False) headers = [('content-type', 'text/html'), ('content-length', str(len(body)))] return error_code, headers, body def error_response_app(error_code, message, debug_message=None, __warn=True): """ An application that emits the given error response. **Note:** Deprecated """ if __warn: warnings.warn( 'wsgilib.error_response_app is deprecated; use the ' 'wsgi_application method on an HTTPException object ' 'instead', DeprecationWarning, 2) def application(environ, start_response): status, headers, body = error_response( environ, error_code, message, debug_message=debug_message, __warn=False) start_response(status, headers) return [body] return application PK¥…û4j¢*Z*Zpaste/httpserver.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php # (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 errno, socket, sys, threading, urlparse, Queue from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from SocketServer import ThreadingMixIn from paste.deploy import converters __all__ = ['WSGIHandlerMixin','WSGIServer','WSGIHandler', 'serve'] __version__ = "0.5" 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 log_request(self, *args, **kwargs): """ disable success request logging Logging transactions should not be part of a WSGI server, if you want logging; look at paste.translogger """ pass def log_message(self, *args, **kwargs): """ disable error message logging Logging transactions should not be part of a WSGI server, if you want logging; look at paste.translogger """ pass 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','').lower(): 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': False # 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(): key = 'HTTP_' + k.replace("-","_").upper() if key in ('HTTP_CONTENT_TYPE','HTTP_CONTENT_LENGTH'): continue self.wsgi_environ[key] = ','.join(self.headers.getheaders(k)) 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, exce, environ=None): """ 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) if not self.wsgi_headers_sent: self.wsgi_write_chunk('') finally: if hasattr(result,'close'): result.close() except socket.error, exce: self.wsgi_connection_drop(exce, environ) 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 # # 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 SocketErrors = (socket.error, SSL.ZeroReturnError, SSL.SysCallError) except ImportError: # Do not require pyOpenSSL to be installed, but disable SSL # functionality in that case. SSL = None SocketErrors = (socket.error,) class SecureHTTPServer(HTTPServer): def __init__(self, server_address, RequestHandlerClass, ssl_context=None): assert not ssl_context, "pyOpenSSL not installed" 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(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") """ 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). 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 WSGIHandler(WSGIHandlerMixin, BaseHTTPRequestHandler): """ A WSGI handler that overrides POST, GET and HEAD to delegate requests to the server's ``wsgi_application``. """ server_version = 'PasteWSGIServer/' + __version__ do_POST = do_GET = do_HEAD = do_DELETE = do_PUT = do_TRACE = \ WSGIHandlerMixin.wsgi_execute def handle(self): # don't bother logging disconnects while handling a request try: BaseHTTPRequestHandler.handle(self) except SocketErrors, exce: self.wsgi_connection_drop(exce) class ThreadPool(object): """ Generic thread pool with a queue of callables to consume. """ SHUTDOWN = object() def __init__(self, nworkers, name="ThreadPool"): """ Create thread pool with `nworkers` worker threads. """ self.nworkers = nworkers self.name = name self.queue = Queue.Queue() self.workers = [] for i in range(self.nworkers): worker = threading.Thread(target=self.worker_thread_callback, name=("%s worker %d" % (self.name, i))) worker.start() self.workers.append(worker) def worker_thread_callback(self): """ Worker thread should call this method to get and process queued callables. """ while True: runnable = self.queue.get() if runnable is ThreadPool.SHUTDOWN: return else: runnable() def shutdown(self): """ Shutdown the queue (after finishing any pending requests). """ # Add a shutdown request for every worker for i in range(self.nworkers): self.queue.put(ThreadPool.SHUTDOWN) # Wait for each thread to terminate for worker in self.workers: worker.join() class ThreadPoolMixIn: """ Mix-in class to process requests from a thread pool """ def __init__(self, nworkers): # Create and start the workers self.running = True assert nworkers > 0, "ThreadPoolMixin servers must have at least one worker" self.thread_pool = ThreadPool(nworkers, "ThreadPoolMixin HTTP server on %s:%d" % (self.server_name, self.server_port)) def process_request(self, request, client_address): """ Queue the request to be processed by on of the thread pool threads """ # This sets the socket to blocking mode (and no timeout) since it # may take the thread pool a little while to get back to it. (This # is the default but since we set a timeout on the parent socket so # that we can trap interrupts we need to restore this,.) request.setblocking(1) # Queue processing of the request self.thread_pool.queue.put( lambda: self.process_request_in_thread(request, client_address)) def process_request_in_thread(self, request, client_address): """ The worker thread should call back here to do the rest of the request processing. Error handling normaller done in 'handle_request' must be done here. """ try: self.finish_request(request, client_address) self.close_request(request) except: self.handle_error(request, client_address) self.close_request(request) def serve_forever(self): """ Overrides `serve_forever` to shut the threadpool down cleanly. """ try: while self.running: try: self.handle_request() except socket.timeout: # Timeout is expected, gives interrupts a chance to # propogate, just keep handling pass finally: self.thread_pool.shutdown() def server_activate(self): """ Overrides server_activate to set timeout on our listener socket. """ # We set the timeout here so that we can trap interrupts on windows self.socket.settimeout(1) self.socket.listen(self.request_queue_size) def server_close(self): """ Finish pending requests and shutdown the server. """ self.running = False self.socket.close() class WSGIServerBase(SecureHTTPServer): 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 self.wsgi_socket_timeout = None def get_request(self): # If there is a socket_timeout, set it on the accepted (conn,info) = SecureHTTPServer.get_request(self) if self.wsgi_socket_timeout: conn.settimeout(self.wsgi_socket_timeout) return (conn, info) class WSGIServer(ThreadingMixIn, WSGIServerBase): daemon_threads = False class WSGIThreadPoolServer(ThreadPoolMixIn, WSGIServerBase): def __init__(self, wsgi_application, server_address, RequestHandlerClass=None, ssl_context=None, nworkers=10): WSGIServerBase.__init__(self, wsgi_application, server_address, RequestHandlerClass, ssl_context) ThreadPoolMixIn.__init__(self, nworkers) def serve(application, host=None, port=None, handler=None, ssl_pem=None, server_version=None, protocol_version=None, start_loop=True, daemon_threads=None, socket_timeout=None, use_threadpool=True, threadpool_workers=10): """ Serves your ``application`` over HTTP(S) via WSGI interface ``host`` This is the ipaddress to bind to (or a hostname if your nameserver is properly configured). This defaults to 127.0.0.1, which is not a public interface. ``port`` The port to run on, defaults to 8080 for HTTP, or 4443 for HTTPS. This can be a string or an integer value. ``handler`` This is the HTTP request handler to use, it defaults to ``WSGIHandler`` in this module. ``ssl_pem`` This an optional SSL certificate file (via OpenSSL) You can generate a self-signed test PEM certificate file 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 ``server_version`` The version of the server as reported in HTTP response line. This defaults to something like "PasteWSGIServer/0.5". Many servers hide their code-base identity with a name like 'Amnesiac/1.0' ``protocol_version`` This sets the protocol used by the server, by default ``HTTP/1.0``. There is some support for ``HTTP/1.1``, which defaults to nicer keep-alive connections. This server supports ``100 Continue``, but does not yet support HTTP/1.1 Chunked Encoding. Hence, if you use HTTP/1.1, you're somewhat in error since chunked coding is a mandatory requirement of a HTTP/1.1 server. If you specify HTTP/1.1, every response *must* have a ``Content-Length`` and you must be careful not to read past the end of the socket. ``start_loop`` This specifies if the server loop (aka ``server.serve_forever()``) should be called; it defaults to ``True``. ``daemon_threads`` This flag specifies if when your webserver terminates all in-progress client connections should be droppped. It defaults to ``False``. You might want to set this to ``True`` if you are using ``HTTP/1.1`` and don't set a ``socket_timeout``. ``socket_timeout`` This specifies the maximum amount of time that a connection to a given client will be kept open. At this time, it is a rude disconnect, but at a later time it might follow the RFC a bit more closely. ``use_threadpool`` Server requests from a pool of worker threads (``threadpool_workers``) rather than creating a new thread for each request. This can substantially reduce latency since there is a high cost associated with thread creation. ``threadpool_workers`` Number of worker threads to create when ``use_threadpool`` is true. This can be a string or an integer value. """ ssl_context = None if ssl_pem: assert SSL, "pyOpenSSL is not installed" port = int(port or 4443) ssl_context = SSL.Context(SSL.SSLv23_METHOD) ssl_context.use_privatekey_file(ssl_pem) ssl_context.use_certificate_file(ssl_pem) host = host or '127.0.0.1' if not port: if ':' in host: host, port = host.split(':', 1) else: port = 8080 server_address = (host, int(port)) if not handler: handler = WSGIHandler if server_version: handler.server_version = server_version handler.sys_version = None if protocol_version: assert protocol_version in ('HTTP/0.9','HTTP/1.0','HTTP/1.1') handler.protocol_version = protocol_version if converters.asbool(use_threadpool): server = WSGIThreadPoolServer(application, server_address, handler, ssl_context, int(threadpool_workers)) else: server = WSGIServer(application, server_address, handler, ssl_context) if daemon_threads: server.daemon_threads = daemon_threads if socket_timeout: server.wsgi_socket_timeout = int(socket_timeout) if converters.asbool(start_loop): print "serving on %s:%s" % server.server_address 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, *args, **kwargs): """ A simple HTTP server. Also supports SSL if you give it an ``ssl_pem`` argument, see documentation for ``serve()``. """ serve(wsgi_app, *args, **kwargs) 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", port="8888") PK¥…û4W…úSSpaste/__init__.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php try: import pkg_resources pkg_resources.declare_namespace('paste') except ImportError: # don't prevent use of paste if pkg_resources isn't installed pass PK¥…û4‹ûR_#_#paste/urlmap.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ Map URL prefixes to WSGI applications. See ``URLMap`` """ from UserDict import DictMixin import re import os import httpexceptions __all__ = ['URLMap', 'PathProxyURLMap'] def urlmap_factory(loader, global_conf, **local_conf): if 'not_found_app' in local_conf: not_found_app = local_conf.pop('not_found_app') else: not_found_app = global_conf.get('not_found_app') if not_found_app: not_found_app = loader.get_app(not_found_app, global_conf=global_conf) urlmap = URLMap(not_found_app=not_found_app) for path, app_name in local_conf.items(): path = parse_path_expression(path) app = loader.get_app(app_name, global_conf=global_conf) urlmap[path] = app return urlmap def parse_path_expression(path): """ Parses a path expression like 'domain foobar.com port 20 /' or just '/foobar' for a path alone. Returns as an address that URLMap likes. """ parts = path.split() domain = port = path = None while parts: if parts[0] == 'domain': parts.pop(0) if not parts: raise ValueError("'domain' must be followed with a domain name") if domain: raise ValueError("'domain' given twice") domain = parts.pop(0) elif parts[0] == 'port': parts.pop(0) if not parts: raise ValueError("'port' must be followed with a port number") if port: raise ValueError("'port' given twice") port = parts.pop(0) else: if path: raise ValueError("more than one path given (have %r, got %r)" % (path, parts[0])) path = parts.pop(0) s = '' if domain: s = 'http://%s' % domain if port: if not domain: raise ValueError("If you give a port, you must also give a domain") s += ':' + port if path: if s: s += '/' s += path return s class URLMap(DictMixin): """ URLMap instances are dictionary-like object that dispatch to one of several applications based on the URL. The dictionary keys are URLs to match (like ``PATH_INFO.startswith(url)``), and the values are applications to dispatch to. URLs are matched most-specific-first, i.e., longest URL first. The ``SCRIPT_NAME`` and ``PATH_INFO`` environmental variables are adjusted to indicate the new context. URLs can also include domains, like ``http://blah.com/foo``, or as tuples ``('blah.com', '/foo')``. This will match domain names; without the ``http://domain`` or with a domain of ``None`` any domain will be matched (so long as no other explicit domain matches). """ def __init__(self, not_found_app=None): self.applications = [] if not not_found_app: not_found_app = self.not_found_app self.not_found_application = not_found_app norm_url_re = re.compile('//+') domain_url_re = re.compile('^(http|https)://') def not_found_app(self, environ, start_response): mapper = environ.get('paste.urlmap_object') if mapper: matches = [p for p, a in mapper.applications] extra = 'defined apps: %s' % ( ',\n '.join(map(repr, matches))) else: extra = '' extra += '\nSCRIPT_NAME: %r' % environ.get('SCRIPT_NAME') extra += '\nPATH_INFO: %r' % environ.get('PATH_INFO') extra += '\nHTTP_HOST: %r' % environ.get('HTTP_HOST') app = httpexceptions.HTTPNotFound( 'The resource was not found', comment=extra).wsgi_application return app(environ, start_response) def normalize_url(self, url, trim=True): if isinstance(url, (list, tuple)): domain = url[0] url = self.normalize_url(url[1])[1] return domain, url assert (not url or url.startswith('/') or self.domain_url_re.search(url)), ( "URL fragments must start with / or http:// (you gave %r)" % url) match = self.domain_url_re.search(url) if match: url = url[match.end():] if '/' in url: domain, url = url.split('/', 1) url = '/' + url else: domain, url = url, '' else: domain = None url = self.norm_url_re.sub('/', url) if trim: url = url.rstrip('/') return domain, url def sort_apps(self): """ Make sure applications are sorted with longest URLs first """ def key(app_desc): (domain, url), app = app_desc if not domain: # Make sure empty domains sort last: return '\xff', -len(url) else: return domain, -len(url) apps = [(key(desc), desc) for desc in self.applications] apps.sort() self.applications = [desc for (sortable, desc) in apps] def __setitem__(self, url, app): if app is None: try: del self[url] except KeyError: pass return dom_url = self.normalize_url(url) if dom_url in self: del self[dom_url] self.applications.append((dom_url, app)) self.sort_apps() def __getitem__(self, url): dom_url = self.normalize_url(url) for app_url, app in self.applications: if app_url == dom_url: return app raise KeyError( "No application with the url %r (domain: %r; existing: %s)" % (url[1], url[0] or '*', self.applications)) def __delitem__(self, url): url = self.normalize_url(url) for app_url, app in self.applications: if app_url == url: self.applications.remove((app_url, app)) break else: raise KeyError( "No application with the url %r" % (url,)) def keys(self): return [app_url for app_url, app in self.applications] def __call__(self, environ, start_response): host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() if ':' in host: host, port = host.split(':', 1) else: if environ['wsgi.url_scheme'] == 'http': port = '80' else: port = '443' path_info = environ.get('PATH_INFO') path_info = self.normalize_url(path_info, False)[1] for (domain, app_url), app in self.applications: if domain and domain != host and domain != host+':'+port: continue if (path_info == app_url or path_info.startswith(app_url + '/')): environ['SCRIPT_NAME'] += app_url environ['PATH_INFO'] = path_info[len(app_url):] return app(environ, start_response) environ['paste.urlmap_object'] = self return self.not_found_application(environ, start_response) class PathProxyURLMap(object): """ This is a wrapper for URLMap that catches any strings that are passed in as applications; these strings are treated as filenames (relative to `base_path`) and are passed to the callable `builder`, which will return an application. This is intended for cases when configuration files can be treated as applications. `base_paste_url` is the URL under which all applications added through this wrapper must go. Use ``""`` if you want this to not change incoming URLs. """ def __init__(self, map, base_paste_url, base_path, builder): self.map = map self.base_paste_url = self.map.normalize_url(base_paste_url) self.base_path = base_path self.builder = builder def __setitem__(self, url, app): if isinstance(app, (str, unicode)): app_fn = os.path.join(self.base_path, app) app = self.builder(app_fn) url = self.map.normalize_url(url) # @@: This means http://foo.com/bar will potentially # match foo.com, but /base_paste_url/bar, which is unintuitive url = (url[0] or self.base_paste_url[0], self.base_paste_url[1] + url[1]) self.map[url] = app def __getattr__(self, attr): return getattr(self.map, attr) # This is really the only settable attribute def not_found_application__get(self): return self.map.not_found_application def not_found_application__set(self, value): self.map.not_found_application = value not_found_application = property(not_found_application__get, not_found_application__set) PK¥…û4äø¦¶JJpaste/wsgilib.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ A module of many disparate routines. """ # functions which moved to paste.request and paste.response # Deprecated around 15 Dec 2005 from request import get_cookies, parse_querystring, parse_formvars from request import construct_url, path_info_split, path_info_pop from response import HeaderDict, has_header, header_value, remove_header from response import error_body_response, error_response, error_response_app from traceback import print_exception from Cookie import SimpleCookie from cStringIO import StringIO import mimetypes import os import cgi import sys import re from urlparse import urlsplit import warnings __all__ = ['get_cookies', 'add_close', 'raw_interactive', 'interactive', 'construct_url', 'error_body_response', 'error_response', 'send_file', 'has_header', 'header_value', 'path_info_split', 'path_info_pop', 'capture_output', 'catch_errors', 'dump_environ', 'intercept_output', 'chained_app_iters'] class add_close: """ An an iterable that iterates over app_iter, then calls close_func. """ def __init__(self, app_iterable, close_func): self.app_iterable = app_iterable self.app_iter = iter(app_iterable) self.close_func = close_func self._closed = False def __iter__(self): return self def next(self): return self.app_iter.next() def close(self): self._closed = True if hasattr(self.app_iterable, 'close'): self.app_iterable.close() self.close_func() def __del__(self): if not self._closed: # We can't raise an error or anything at this stage print >> sys.stderr, ( "Error: app_iter.close() was not called when finishing " "WSGI request. finalization function %s not called" % self.close_func) class add_start_close: """ An an iterable that iterates over app_iter, calls start_func before the first item is returned, then calls close_func at the end. """ def __init__(self, app_iterable, start_func, close_func=None): self.app_iterable = app_iterable self.app_iter = iter(app_iterable) self.first = True self.start_func = start_func self.close_func = close_func self._closed = False def __iter__(self): return self def next(self): if self.first: self.start_func() self.first = False return self.app_iter.next() def close(self): self._closed = True if hasattr(self.app_iterable, 'close'): self.app_iterable.close() if self.close_func is not None: self.close_func() def __del__(self): if not self._closed: # We can't raise an error or anything at this stage print >> sys.stderr, ( "Error: app_iter.close() was not called when finishing " "WSGI request. finalization function %s not called" % self.close_func) class chained_app_iters: """ Chains several app_iters together, also delegating .close() to each of them. """ def __init__(self, *chained): self.app_iters = chained self.chained = [iter(item) for item in chained] self._closed = False def __iter__(self): return self def next(self): if len(self.chained) == 1: return self.chained[0].next() else: try: return self.chained[0].next() except StopIteration: self.chained.pop(0) return self.next() def close(self): self._closed = True got_exc = None for app_iter in self.app_iters: try: if hasattr(app_iter, 'close'): app_iter.close() except: got_exc = sys.exc_info() if got_exc: raise got_exc[0], got_exc[1], got_exc[2] def __del__(self): if not self._closed: # We can't raise an error or anything at this stage print >> sys.stderr, ( "Error: app_iter.close() was not called when finishing " "WSGI request. finalization function %s not called" % self.close_func) def catch_errors(application, environ, start_response, error_callback, ok_callback=None): """ Runs the application, and returns the application iterator (which should be passed upstream). If an error occurs then error_callback will be called with exc_info as its sole argument. If no errors occur and ok_callback is given, then it will be called with no arguments. """ error_occurred = False try: app_iter = application(environ, start_response) except: error_callback(sys.exc_info()) raise if type(app_iter) in (list, tuple): # These won't produce exceptions if ok_callback: ok_callback() return app_iter else: return _wrap_app_iter(app_iter, error_callback, ok_callback) class _wrap_app_iter(object): def __init__(self, app_iterable, error_callback, ok_callback): self.app_iterable = app_iterable self.app_iter = iter(app_iterable) self.error_callback = error_callback self.ok_callback = ok_callback if hasattr(self.app_iterable, 'close'): self.close = self.app_iterable.close def __iter__(self): return self def next(self): try: return self.app_iter.next() except StopIteration: if self.ok_callback: self.ok_callback() raise except: self.error_callback(sys.exc_info()) raise def catch_errors_app(application, environ, start_response, error_callback_app, ok_callback=None, catch=Exception): """ Like ``catch_errors``, except error_callback_app should be a callable that will receive *three* arguments -- ``environ``, ``start_response``, and ``exc_info``. It should call ``start_response`` (*with* the exc_info argument!) and return an iterator. """ error_occurred = False try: app_iter = application(environ, start_response) except catch: return error_callback_app(environ, start_response, sys.exc_info()) if type(app_iter) in (list, tuple): # These won't produce exceptions if ok_callback: ok_callback() return app_iter else: return _wrap_app_iter_app( environ, start_response, app_iter, error_callback_app, ok_callback) class _wrap_app_iter_app(object): def __init__(self, environ, start_response, app_iterable, error_callback_app, ok_callback): self.environ = environ self.start_response = start_response self.app_iterable = app_iterable self.app_iter = iter(app_iterable) self.error_callback_app = error_callback_app self.ok_callback = ok_callback if hasattr(self.app_iterable, 'close'): self.close = self.app_iterable.close def __iter__(self): return self def next(self): try: return self.app_iter.next() except StopIteration: if self.ok_callback: self.ok_callback() raise except: if hasattr(self.app_iterable, 'close'): try: self.app_iterable.close() except: # @@: Print to wsgi.errors? pass new_app_iterable = self.error_callback_app( self.environ, self.start_response, sys.exc_info()) app_iter = iter(new_app_iterable) if hasattr(new_app_iterable, 'close'): self.close = new_app_iterable.close self.next = app_iter.next return self.next() def raw_interactive(application, path='', raise_on_wsgi_error=False, **environ): """ Runs the application in a fake environment. """ assert "path_info" not in environ, "argument list changed" if raise_on_wsgi_error: errors = ErrorRaiser() else: errors = StringIO() basic_environ = { # mandatory CGI variables 'REQUEST_METHOD': 'GET', # always mandatory 'SCRIPT_NAME': '', # may be empty if app is at the root 'PATH_INFO': '', # may be empty if at root of app 'SERVER_NAME': 'localhost', # always mandatory 'SERVER_PORT': '80', # always mandatory 'SERVER_PROTOCOL': 'HTTP/1.0', # mandatory wsgi variables 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': StringIO(''), 'wsgi.errors': errors, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, } if path: (_,_,path_info,query,fragment) = urlsplit(str(path)) basic_environ['PATH_INFO'] = path_info if query: basic_environ['QUERY_STRING'] = query for name, value in environ.items(): name = name.replace('__', '.') basic_environ[name] = value if ('SERVER_NAME' in basic_environ and 'HTTP_HOST' not in basic_environ): basic_environ['HTTP_HOST'] = basic_environ['SERVER_NAME'] istream = basic_environ['wsgi.input'] if isinstance(istream, str): basic_environ['wsgi.input'] = StringIO(istream) basic_environ['CONTENT_LENGTH'] = len(istream) data = {} output = [] headers_set = [] headers_sent = [] def start_response(status, headers, exc_info=None): if exc_info: try: if headers_sent: # Re-raise original exception only if headers sent raise exc_info[0], exc_info[1], exc_info[2] finally: # avoid dangling circular reference exc_info = None elif headers_set: # You cannot set the headers more than once, unless the # exc_info is provided. raise AssertionError("Headers already set and no exc_info!") headers_set.append(True) data['status'] = status data['headers'] = headers return output.append app_iter = application(basic_environ, start_response) try: try: for s in app_iter: if not isinstance(s, str): raise ValueError( "The app_iter response can only contain str (not " "unicode); got: %r" % s) headers_sent.append(True) if not headers_set: raise AssertionError("Content sent w/o headers!") output.append(s) except TypeError, e: # Typically "iteration over non-sequence", so we want # to give better debugging information... e.args = ((e.args[0] + ' iterable: %r' % app_iter),) + e.args[1:] raise finally: if hasattr(app_iter, 'close'): app_iter.close() return (data['status'], data['headers'], ''.join(output), errors.getvalue()) class ErrorRaiser(object): def flush(self): pass def write(self, value): if not value: return raise AssertionError( "No errors should be written (got: %r)" % value) def writelines(self, seq): raise AssertionError( "No errors should be written (got lines: %s)" % list(seq)) def getvalue(self): return '' def interactive(*args, **kw): """ Runs the application interatively, wrapping `raw_interactive` but returning the output in a formatted way. """ status, headers, content, errors = raw_interactive(*args, **kw) full = StringIO() if errors: full.write('Errors:\n') full.write(errors.strip()) full.write('\n----------end errors\n') full.write(status + '\n') for name, value in headers: full.write('%s: %s\n' % (name, value)) full.write('\n') full.write(content) return full.getvalue() interactive.proxy = 'raw_interactive' def dump_environ(environ,start_response): """ Application which simply dumps the current environment variables out as a plain text response. """ output = [] keys = environ.keys() keys.sort() for k in keys: v = str(environ[k]).replace("\n","\n ") output.append("%s: %s\n" % (k,v)) output.append("\n") content_length = environ.get("CONTENT_LENGTH",'') if content_length: output.append(environ['wsgi.input'].read(int(content_length))) output.append("\n") output = "".join(output) headers = [('Content-Type', 'text/plain'), ('Content-Length', len(output))] start_response("200 OK",headers) return [output] def send_file(filename): warnings.warn( "wsgilib.send_file has been moved to paste.fileapp.FileApp", DeprecationWarning, 2) import fileapp return fileapp.FileApp(filename) def capture_output(environ, start_response, application): """ Runs application with environ and start_response, and captures status, headers, and body. Sends status and header, but *not* body. Returns (status, headers, body). Typically this is used like:: def dehtmlifying_middleware(application): def replacement_app(environ, start_response): status, headers, body = capture_output( environ, start_response, application) content_type = header_value(headers, 'content-type') if (not content_type or not content_type.startswith('text/html')): return [body] body = re.sub(r'<.*?>', '', body) return [body] return replacement_app """ warnings.warn( 'wsgilib.capture_output has been deprecated in favor ' 'of wsgilib.intercept_output', DeprecationWarning, 2) data = [] output = StringIO() def replacement_start_response(status, headers, exc_info=None): if data: data[:] = [] data.append(status) data.append(headers) start_response(status, headers, exc_info) return output.write app_iter = application(environ, replacement_start_response) try: for item in app_iter: output.write(item) finally: if hasattr(app_iter, 'close'): app_iter.close() if not data: data.append(None) if len(data) < 2: data.append(None) data.append(output.getvalue()) return data def intercept_output(environ, application, conditional=None, start_response=None): """ Runs application with environ and captures status, headers, and body. None are sent on; you must send them on yourself (unlike ``capture_output``) Typically this is used like:: def dehtmlifying_middleware(application): def replacement_app(environ, start_response): status, headers, body = intercept_output( environ, application) content_type = header_value(headers, 'content-type') if (not content_type or not content_type.startswith('text/html')): return [body] body = re.sub(r'<.*?>', '', body) return [body] return replacement_app A third optional argument ``conditional`` should be a function that takes ``conditional(status, headers)`` and returns False if the request should not be intercepted. In that case ``start_response`` will be called and ``(None, None, app_iter)`` will be returned. You must detect that in your code and return the app_iter, like:: def dehtmlifying_middleware(application): def replacement_app(environ, start_response): status, headers, body = intercept_output( environ, application, lambda s, h: header_value(headers, 'content-type').startswith('text/html'), start_response) if status is None: return body body = re.sub(r'<.*?>', '', body) return [body] return replacement_app """ if conditional is not None and start_response is None: raise TypeError( "If you provide conditional you must also provide " "start_response") data = [] output = StringIO() def replacement_start_response(status, headers, exc_info=None): if conditional is not None and not conditional(status, headers): data.append(None) return start_response(status, headers) if data: data[:] = [] data.append(status) data.append(headers) return output.write app_iter = application(environ, replacement_start_response) if data[0] is None: return (None, None, app_iter) try: for item in app_iter: output.write(item) finally: if hasattr(app_iter, 'close'): app_iter.close() if not data: data.append(None) if len(data) < 2: data.append(None) data.append(output.getvalue()) return data ## Deprecation warning wrapper: class ResponseHeaderDict(HeaderDict): def __init__(self, *args, **kw): warnings.warn( "The class wsgilib.ResponseHeaderDict has been moved " "to paste.response.HeaderDict", DeprecationWarning, 2) HeaderDict.__init__(self, *args, **kw) def _warn_deprecated(new_func): new_name = new_func.func_name new_path = new_func.func_globals['__name__'] + '.' + new_name def replacement(*args, **kw): warnings.warn( "The function wsgilib.%s has been moved to %s" % (new_name, new_path), DeprecationWarning, 2) return new_func(*args, **kw) try: replacement.func_name = new_func.func_name except: pass return replacement # Put warnings wrapper in place for all public functions that # were imported from elsewhere: for _name in __all__: _func = globals()[_name] if (hasattr(_func, 'func_globals') and _func.func_globals['__name__'] != __name__): globals()[_name] = _warn_deprecated(_func) if __name__ == '__main__': import doctest doctest.testmod() PK¥…û4Ï*ì 9 9 paste/url.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ This module implements a class for handling URLs. """ import urllib import cgi from paste import request # Imported lazily from FormEncode: variabledecode = None __all__ = ["URL", "Image"] def html_quote(v): if v is None: return '' return cgi.escape(str(v), 1) def url_quote(v): if v is None: return '' return urllib.quote(str(v)) url_unquote = urllib.unquote def js_repr(v): if v is None: return 'null' elif v is False: return 'false' elif v is True: return 'true' elif isinstance(v, list): return '[%s]' % ', '.join(map(js_repr, v)) elif isinstance(v, dict): return '{%s}' % ', '.join( ['%s: %s' % (js_repr(key), js_repr(value)) for key, value in v]) elif isinstance(v, str): return repr(v) elif isinstance(v, unicode): # @@: how do you do Unicode literals in Javascript? return repr(v.encode('UTF-8')) elif isinstance(v, (float, int)): return repr(v) elif isinstance(v, long): return repr(v).lstrip('L') elif hasattr(v, '__js_repr__'): return v.__js_repr__() else: raise ValueError( "I don't know how to turn %r into a Javascript representation" % v) class URLResource(object): """ This is an abstract superclass for different kinds of URLs """ default_params = {} def __init__(self, url, vars=None, attrs=None, params=None): self.url = url or '/' self.vars = vars or [] self.attrs = attrs or {} self.params = self.default_params.copy() self.original_params = params or {} if params: self.params.update(params) #@classmethod def from_environ(cls, environ, with_query_string=True, with_path_info=True, script_name=None, path_info=None, querystring=None): url = request.construct_url( environ, with_query_string=False, with_path_info=with_path_info, script_name=script_name, path_info=path_info) if with_query_string: if querystring is None: vars = request.parse_querystring(environ) else: vars = cgi.parse_qsl( querystring, keep_blank_values=True, strict_parsing=False) else: vars = None v = cls(url, vars=vars) return v from_environ = classmethod(from_environ) def __call__(self, *args, **kw): res = self._add_positional(args) res = res._add_vars(kw) return res def __getitem__(self, item): if '=' in item: name, value = item.split('=', 1) return self._add_vars({url_unquote(name): url_unquote(value)}) return self._add_positional((item,)) def attr(self, **kw): for key in kw.keys(): if key.endswith('_'): kw[key[:-1]] = kw[key] del kw[key] new_attrs = self.attrs.copy() new_attrs.update(kw) return self.__class__(self.url, vars=self.vars, attrs=new_attrs, params=self.original_params) def param(self, **kw): new_params = self.original_params.copy() new_params.update(kw) return self.__class__(self.url, vars=self.vars, attrs=self.attrs, params=new_params) def coerce_vars(self, vars): global variabledecode need_variable_encode = False for key, value in vars.items(): if isinstance(value, dict): need_variable_encode = True if key.endswith('_'): vars[key[:-1]] = vars[key] del vars[key] if need_variable_encode: if variabledecode is None: from formencode import variabledecode vars = variabledecode.variable_encode(vars) return vars def var(self, **kw): kw = self.coerce_vars(kw) new_vars = self.vars + kw.items() return self.__class__(self.url, vars=new_vars, attrs=self.attrs, params=self.original_params) def setvar(self, **kw): """ Like ``.var(...)``, except overwrites keys, where .var simply extends the keys. Setting a variable to None here will effectively delete it. """ kw = self.coerce_vars(kw) new_vars = [] for name, values in self.vars: if name in kw: continue new_vars.append((name, values)) new_vars.extend(kw.items()) return self.__class__(self.url, vars=new_vars, attrs=self.attrs, params=self.original_params) def setvars(self, **kw): """ Creates a copy of this URL, but with all the variables set/reset (like .setvar(), except clears past variables at the same time) """ return self.__class__(self.url, vars=kw.items(), attrs=self.attrs, params=self.original_params) def addpath(self, *paths): u = self for path in paths: path = str(path).lstrip('/') new_url = u.url if not new_url.endswith('/'): new_url += '/' u = u.__class__(new_url+path, vars=u.vars, attrs=u.attrs, params=u.original_params) return u __div__ = addpath def become(self, OtherClass): return OtherClass(self.url, vars=self.vars, attrs=self.attrs, params=self.original_params) def href__get(self): s = self.url if self.vars: s += '?' vars = [] for name, val in self.vars: if isinstance(val, (list, tuple)): val = [v for v in val if v is not None] elif val is None: continue vars.append((name, val)) s += urllib.urlencode(vars, True) return s href = property(href__get) def __repr__(self): base = '<%s %s' % (self.__class__.__name__, self.href or "''") if self.attrs: base += ' attrs(%s)' % ( ' '.join(['%s="%s"' % (html_quote(n), html_quote(v)) for n, v in self.attrs.items()])) if self.original_params: base += ' params(%s)' % ( ', '.join(['%s=%r' % (n, v) for n, v in self.attrs.items()])) return base + '>' def html__get(self): if not self.params.get('tag'): raise ValueError( "You cannot get the HTML of %r until you set the " "'tag' param'" % self) content = self._get_content() tag = '<%s' % self.params.get('tag') attrs = ' '.join([ '%s="%s"' % (html_quote(n), html_quote(v)) for n, v in self._html_attrs()]) if attrs: tag += ' ' + attrs tag += self._html_extra() if content is None: return tag + ' />' else: return '%s>%s' % (tag, content, self.params.get('tag')) html = property(html__get) def _html_attrs(self): return self.attrs.items() def _html_extra(self): return '' def _get_content(self): """ Return the content for a tag (for self.html); return None for an empty tag (like ````) """ raise NotImplementedError def _add_vars(self, vars): raise NotImplementedError def _add_positional(self, args): raise NotImplementedError class URL(URLResource): r""" >>> u = URL('http://localhost') >>> u >>> u = u['view'] >>> str(u) 'http://localhost/view' >>> u['//foo'].param(content='view').html 'view' >>> u.param(confirm='Really?', content='goto').html 'goto' >>> u(title='See "it"', content='goto').html 'goto' >>> u('another', var='fuggetaboutit', content='goto').html 'goto' >>> u.attr(content='goto').html Traceback (most recent call last): .... ValueError: You must give a content param to generate anchor tags >>> str(u['foo=bar%20stuff']) 'http://localhost/view?foo=bar+stuff' """ default_params = {'tag': 'a'} def __str__(self): return self.href def _get_content(self): if not self.params.get('content'): raise ValueError( "You must give a content param to %r generate anchor tags" % self) return self.params['content'] def _add_vars(self, vars): url = self for name in ('confirm', 'content'): if name in vars: url = url.param(**{name: vars.pop(name)}) if 'target' in vars: url = url.attr(target=vars.pop('target')) return url.var(**vars) def _add_positional(self, args): return self.addpath(*args) def _html_attrs(self): attrs = self.attrs.items() attrs.insert(0, ('href', self.href)) if self.params.get('confirm'): attrs.append(('onclick', 'return confirm(%s)' % js_repr(self.params['confirm']))) return attrs def onclick_goto__get(self): return 'location.href=%s; return false' % js_repr(self.href) onclick_goto = property(onclick_goto__get) def button__get(self): return self.become(Button) button = property(button__get) def js_popup__get(self): return self.become(JSPopup) js_popup = property(js_popup__get) class Image(URLResource): r""" >>> i = Image('/images') >>> i = i / '/foo.png' >>> i.html '' >>> str(i['alt=foo']) 'foo' >>> i.href '/images/foo.png' """ default_params = {'tag': 'img'} def __str__(self): return self.html def _get_content(self): return None def _add_vars(self, vars): return self.attr(**vars) def _add_positional(self, args): return self.addpath(*args) def _html_attrs(self): attrs = self.attrs.items() attrs.insert(0, ('src', self.href)) return attrs class Button(URLResource): r""" >>> u = URL('/') >>> u = u / 'delete' >>> b = u.button['confirm=Sure?'](id=5, content='del') >>> str(b) '' """ default_params = {'tag': 'button'} def __str__(self): return self.html def _get_content(self): if self.params.get('content'): return self.params['content'] if self.attrs.get('value'): return self.attrs['content'] # @@: Error? return None def _add_vars(self, vars): button = self if 'confirm' in vars: button = button.param(confirm=vars.pop('confirm')) if 'content' in vars: button = button.param(content=vars.pop('content')) return button.var(**vars) def _add_positional(self, args): return self.addpath(*args) def _html_attrs(self): attrs = self.attrs.items() onclick = 'location.href=%s' % js_repr(self.href) if self.params.get('confirm'): onclick = 'if (confirm(%s)) {%s}' % ( js_repr(self.params['confirm']), onclick) onclick += '; return false' attrs.insert(0, ('onclick', onclick)) return attrs class JSPopup(URLResource): r""" >>> u = URL('/') >>> u = u / 'view' >>> j = u.js_popup(content='view') >>> j.html 'view' """ default_params = {'tag': 'a', 'target': '_blank'} def _add_vars(self, vars): button = self for var in ('width', 'height', 'stripped', 'content'): if var in vars: button = button.param(**{var: vars.pop(var)}) return button.var(**vars) def _window_args(self): p = self.params features = [] if p.get('stripped'): p['location'] = p['status'] = p['toolbar'] = '0' for param in 'channelmode directories fullscreen location menubar resizable scrollbars status titlebar'.split(): if param not in p: continue v = p[param] if v not in ('yes', 'no', '1', '0'): if v: v = '1' else: v = '0' features.append('%s=%s' % (param, v)) for param in 'height left top width': if not p.get(param): continue features.append('%s=%s' % (param, p[param])) args = [self.href, p['target']] if features: args.append(','.join(features)) return ', '.join(map(js_repr, args)) def _html_attrs(self): attrs = self.attrs.items() onclick = ('window.open(%s); return false' % self._window_args()) attrs.insert(0, ('target', self.params['target'])) attrs.insert(0, ('onclick', onclick)) attrs.insert(0, ('href', self.href)) return attrs def _get_content(self): if not self.params.get('content'): raise ValueError( "You must give a content param to %r generate anchor tags" % self) return self.params['content'] def _add_positional(self, args): return self.addpath(*args) if __name__ == '__main__': import doctest doctest.testmod() PK¥…û4¡.µl l paste/cgiapp.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ Application that runs a CGI script. """ import os import subprocess try: import select except ImportError: select = None from paste.deploy import converters __all__ = ['CGIError', 'CGIApplication'] class CGIError(Exception): pass class CGIApplication(object): """ This object acts as a proxy to a CGI application. You pass in the script path (``script``), an optional path to search for the script (if the name isn't absolute) (``path``). If you don't give a path, then ``$PATH`` will be used. """ def __init__(self, global_conf, script, path=None, include_os_environ=True, query_string=None): self.script_filename = script if path is None: path = (global_conf.get('path') or global_conf.get('PATH')) if path is None: path = os.environ.get('PATH', '').split(':') self.path = converters.aslist(path, ':') if '?' in script: assert query_string is None, ( "You cannot have '?' in your script name (%r) and also " "give a query_string (%r)" % (self.script, query_string)) script, query_string = script.split('?', 1) if os.path.abspath(script) != script: # relative path for path_dir in self.path: if os.path.exists(os.path.join(path_dir, script)): self.script = os.path.join(path_dir, script) break else: raise CGIError( "Script %r not found in path %r" % (script, self.path)) else: self.script = script self.include_os_environ = converters.asbool(include_os_environ) self.query_string = query_string def __call__(self, environ, start_response): if 'REQUEST_URI' not in environ: environ['REQUEST_URI'] = ( environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')) if self.include_os_environ: cgi_environ = os.environ.copy() else: cgi_environ = {} for name in environ: # Should unicode values be encoded? if (name.upper() == name and isinstance(environ[name], str)): cgi_environ[name] = environ[name] if self.query_string is not None: old = cgi_environ.get('QUERY_STRING', '') if old: old += '&' cgi_environ['QUERY_STRING'] = old + self.query_string cgi_environ['SCRIPT_FILENAME'] = self.script proc = subprocess.Popen( [self.script], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=cgi_environ, cwd=os.path.dirname(self.script), ) writer = CGIWriter(environ, start_response) if select: proc_communicate( proc, stdin=StdinReader.from_environ(environ), stdout=writer, stderr=environ['wsgi.errors']) else: stdout, stderr = proc.communicate(StdinReader.from_environ(environ).read()) if stderr: environ['wsgi.errors'].write(stderr) writer(stdout) if not writer.headers_finished: start_response(writer.status, writer.headers) return [] class CGIWriter(object): def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response self.status = '200 OK' self.headers = [] self.headers_finished = False self.writer = None self.buffer = '' def write(self, data): if self.headers_finished: self.writer(data) return self.buffer += data while '\n' in self.buffer: if '\r\n' in self.buffer: line1, self.buffer = self.buffer.split('\r\n', 1) else: line1, self.buffer = self.buffer.split('\n', 1) if not line1: self.headers_finished = True self.writer = self.start_response( self.status, self.headers) self.writer(self.buffer) del self.buffer del self.headers del self.status break elif ':' not in line1: raise CGIError( "Bad header line: %r" % line1) else: name, value = line1.split(':', 1) value = value.lstrip() name = name.strip() if name.lower() == 'status': self.status = value else: self.headers.append((name, value)) class StdinReader(object): def __init__(self, stdin, content_length): self.stdin = stdin self.content_length = content_length def from_environ(cls, environ): length = environ.get('CONTENT_LENGTH') if length: length = int(length) else: length = 0 return cls(environ['wsgi.input'], length) from_environ = classmethod(from_environ) def read(self, size=None): if not self.content_length: return '' if size is None: text = self.stdin.read(self.content_length) else: text = self.stdin.read(min(self.content_length, size)) self.content_length -= len(text) return text def proc_communicate(proc, stdin=None, stdout=None, stderr=None): """ Run the given process, piping input/output/errors to the given file-like objects (which need not be actual file objects, unlike the arguments passed to Popen). Wait for process to terminate. Note: this is taken from the posix version of subprocess.Popen.communicate, but made more general through the use of file-like objects. """ read_set = [] write_set = [] input_buffer = '' trans_nl = proc.universal_newlines and hasattr(open, 'newlines') if proc.stdin: # Flush stdio buffer. This might block, if the user has # been writing to .stdin in an uncontrolled fashion. proc.stdin.flush() if input: write_set.append(proc.stdin) else: proc.stdin.close() else: assert stdin is None if proc.stdout: read_set.append(proc.stdout) else: assert stdout is None if proc.stderr: read_set.append(proc.stderr) else: assert stderr is None while read_set or write_set: rlist, wlist, xlist = select.select(read_set, write_set, []) if proc.stdin in wlist: # When select has indicated that the file is writable, # we can write up to PIPE_BUF bytes without risk # blocking. POSIX defines PIPE_BUF >= 512 next, input_buffer = input_buffer, '' next_len = 512-len(next) if next_len: next += stdin.read(next_len) if not next: proc.stdin.close() write_set.remove(proc.stdin) else: bytes_written = os.write(proc.stdin.fileno(), next) if bytes_written < len(next): input_buffer = next[bytes_written:] if proc.stdout in rlist: data = os.read(proc.stdout.fileno(), 1024) if data == "": proc.stdout.close() read_set.remove(proc.stdout) if trans_nl: data = proc._translate_newlines(data) stdout.write(data) if proc.stderr in rlist: data = os.read(proc.stderr.fileno(), 1024) if data == "": proc.stderr.close() read_set.remove(proc.stderr) if trans_nl: data = proc._translate_newlines(data) stderr.write(data) proc.wait() PK¥…û4§3¤A A paste/flup_session.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ Creates a session object. In your application, use:: environ['paste.flup_session_service'].session This will return a dictionary. The contents of this dictionary will be saved to disk when the request is completed. The session will be created when you first fetch the session dictionary, and a cookie will be sent in that case. There's current no way to use sessions without cookies, and there's no way to delete a session except to clear its data. """ import httpexceptions import wsgilib import flup.middleware.session flup_session = flup.middleware.session # This is a dictionary of existing stores, keyed by a tuple of # store type and parameters store_cache = {} class NoDefault: pass class SessionMiddleware(object): session_classes = { 'memory': (flup_session.MemorySessionStore, [('session_timeout', 'timeout', int, 60)]), 'disk': (flup_session.DiskSessionStore, [('session_timeout', 'timeout', int, 60), ('session_dir', 'storeDir', str, '/tmp/sessions')]), 'shelve': (flup_session.ShelveSessionStore, [('session_timeout', 'timeout', int, 60), ('session_file', 'storeFile', str, '/tmp/session.shelve')]), } def __init__(self, app, global_conf, session_type=NoDefault, cookie_name=NoDefault, **store_config ): self.application = app if session_type is NoDefault: session_type = global_conf.get('session_type', 'disk') self.session_type = session_type try: self.store_class, self.store_args = self.session_classes[self.session_type] except KeyError: raise KeyError( "The session_type %s is unknown (I know about %s)" % (self.session_type, ', '.join(self.session_classes.keys()))) kw = {} for config_name, kw_name, coercer, default in self.store_args: value = coercer(store_config.get(config_name, default)) kw[kw_name] = value self.store = self.store_class(**kw) if cookie_name is NoDefault: cookie_name = global_conf.get('session_cookie', '_SID_') self.cookie_name = cookie_name def __call__(self, environ, start_response): service = flup_session.SessionService( self.store, environ, cookieName=self.cookie_name, fieldName=self.cookie_name) environ['paste.flup_session_service'] = service def cookie_start_response(status, headers, exc_info=None): service.addCookie(headers) return start_response(status, headers, exc_info) try: app_iter = self.application(environ, cookie_start_response) except httpexceptions.HTTPException, e: headers = (e.headers or {}).items() service.addCookie(headers) e.headers = dict(headers) service.close() raise except: service.close() raise return wsgilib.add_close(app_iter, service.close) PK¥…û4»¾' % ( self.__class__.__module__, self.__class__.__name__, self.original_environ.get('SCRIPT_NAME') or '/') class Forwarder(Recursive): """ The forwarder will try to restart the request, except with the new `path` (replacing ``PATH_INFO`` in the request). It must not be called after and headers have been returned. It returns an iterator that must be returned back up the call stack, so it must be used like:: return environ['paste.recursive.forward'](path) Meaningful transformations cannot be done, since headers are sent directly to the server and cannot be inspected or rewritten. """ def activate(self, environ): warnings.warn( "recursive.Forwarder has been deprecated; please use " "ForwardRequestException", DeprecationWarning, 2) return self.application(environ, self.start_response) class Includer(Recursive): """ Starts another request with the given path and adding or overwriting any values in the `extra_environ` dictionary. Returns an IncludeResponse object. """ def activate(self, environ): response = IncludedResponse() def start_response(status, headers, exc_info=None): if exc_info: raise exc_info[0], exc_info[1], exc_info[2] response.status = status response.headers = headers return response.write app_iter = self.application(environ, start_response) try: for s in app_iter: response.write(s) finally: if hasattr(app_iter, 'close'): app_iter.close() response.close() return response class IncludedResponse(object): def __init__(self): self.headers = None self.status = None self.output = StringIO() self.str = None def close(self): self.str = self.output.getvalue() self.output.close() self.output = None def write(self, s): assert self.output is not None, ( "This response has already been closed and no further data " "can be written.") self.output.write(s) def __str__(self): return self.body def body__get(self): if self.str is None: return self.output.getvalue() else: return self.str body = property(body__get) PK¥…û4©®êòÓÓpaste/error_document.pyimport warnings # Deprecated Mar 3 2006 (remove quickly: April 2006) warnings.warn( 'paste.error_document has been moved to paste.errordocument', DeprecationWarning, 2) from paste.errordocument import * PK¥…û4ˆA\§!£!£paste/httpheaders.py# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php # (c) 2005 Ian Bicking, Clark C. Evans and contributors # This module is part of the Python Paste Project and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php # Some of this code was funded by: http://prometheusresearch.com """ HTTP Message Header Fields (see RFC 4229) This contains general support for HTTP/1.1 message headers [1]_ in a manner that supports WSGI ``environ`` [2]_ and ``response_headers`` [3]_. Specifically, this module defines a ``HTTPHeader`` class whose instances correspond to field-name items. The actual field-content for the message-header is stored in the appropriate WSGI collection (either the ``environ`` for requests, or ``response_headers`` for responses). Each ``HTTPHeader`` instance is a callable (defining ``__call__``) that takes one of the following: - an ``environ`` dictionary, returning the corresponding header value by according to the WSGI's ``HTTP_`` prefix mechanism, e.g., ``USER_AGENT(environ)`` returns ``environ.get('HTTP_USER_AGENT')`` - a ``response_headers`` list, giving a comma-delimited string for each corresponding ``header_value`` tuple entries (see below). - a sequence of string ``*args`` that are comma-delimited into a single string value: ``CONTENT_TYPE("text/html","text/plain")`` returns ``"text/html, text/plain"`` - a set of ``**kwargs`` keyword arguments that are used to create a header value, in a manner dependent upon the particular header in question (to make value construction easier and error-free): ``CONTENT_DISPOSITION(max_age=CONTENT_DISPOSITION.ONEWEEK)`` returns ``"public, max-age=60480"`` Each ``HTTPHeader`` instance also provides several methods to act on a WSGI collection, for removing and setting header values. ``delete(collection)`` This method removes all entries of the corresponding header from the given collection (``environ`` or ``response_headers``), e.g., ``USER_AGENT.remove(environ)`` deletes the 'HTTP_USER_AGENT' entry from the ``environ``. ``update(collection, *args, **kwargs)`` This method does an in-place replacement of the given header entry, for example: ``CONTENT_LENGTH(response_headers,len(body))`` The first argument is a valid ``environ`` dictionary or ``response_headers`` list; remaining arguments are passed on to ``__call__(*args, **kwargs)`` for value construction. ``apply(collection, **kwargs)`` This method is similar to update, only that it may affect other headers. For example, according to recommendations in RFC 2616, certain Cache-Control configurations should also set the ``Expires`` header for HTTP/1.0 clients. By default, ``apply()`` is simply ``update()`` but limited to keyword arguments. This particular approach to managing headers within a WSGI collection has several advantages: 1. Typos in the header name are easily detected since they become a ``NameError`` when executed. The approach of using header strings directly can be problematic; for example, the following should return ``None`` : ``environ.get("HTTP_ACCEPT_LANGUAGES")`` 2. For specific headers with validation, using ``__call__`` will result in an automatic header value check. For example, the _ContentDisposition header will reject a value having ``maxage`` or ``max_age`` (the appropriate parameter is ``max-age`` ). 3. When appending/replacing headers, the field-name has the suggested RFC capitalization (e.g. ``Content-Type`` or ``ETag``) for user-agents that incorrectly use case-sensitive matches. 4. Some headers (such as ``Content-Type``) are 0, that is, only one entry of this type may occur in a given set of ``response_headers``. This module knows about those cases and enforces this cardinality constraint. 5. The exact details of WSGI header management are abstracted so the programmer need not worry about operational differences between ``environ`` dictionary or ``response_headers`` list. 6. Sorting of ``HTTPHeaders`` is done following the RFC suggestion that general-headers come first, followed by request and response headers, and finishing with entity-headers. 7. Special care is given to exceptional cases such as Set-Cookie which violates the RFC's recommendation about combining header content into a single entry using comma separation. A particular difficulty with HTTP message headers is a categorization of sorts as described in section 4.2: Multiple message-header fields with the same field-name MAY be present in a message if and only if the entire field-value for that header field is defined as a comma-separated list [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. This creates three fundamentally different kinds of headers: - Those that do not have a #(values) production, and hence are singular and may only occur once in a set of response fields; this case is handled by the ``_SingleValueHeader`` subclass. - Those which have the #(values) production and follow the combining rule outlined above; our ``_MultiValueHeader`` case. - Those which are multi-valued, but cannot be combined (such as the ``Set-Cookie`` header due to its ``Expires`` parameter); or where combining them into a single header entry would cause common user-agents to fail (``WWW-Authenticate``, ``Warning``) since they fail to handle dates even when properly quoted. This case is handled by ``_MultiEntryHeader``. Since this project does not have time to provide rigorous support and validation for all headers, it does a basic construction of headers listed in RFC 2616 (plus a few others) so that they can be obtained by simply doing ``from paste.httpheaders import *``; the name of the header instance is the "common name" less any dashes to give CamelCase style names. .. [1] http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 .. [2] http://www.python.org/peps/pep-0333.html#environ-variables .. [3] http://www.python.org/peps/pep-0333.html#the-start-response-callable """ import urllib2 from mimetypes import guess_type from rfc822 import formatdate, parsedate_tz, mktime_tz from time import time as now from httpexceptions import HTTPBadRequest __all__ = ['get_header', 'list_headers', 'normalize_headers', 'HTTPHeader', 'EnvironVariable' ] class EnvironVariable(str): """ a CGI ``environ`` variable as described by WSGI This is a helper object so that standard WSGI ``environ`` variables can be extracted w/o syntax error possibility. """ def __call__(self, environ): return environ.get(self,'') def __repr__(self): return '' % self def update(self, environ, value): environ[self] = value REMOTE_USER = EnvironVariable("REMOTE_USER") REMOTE_SESSION = EnvironVariable("REMOTE_SESSION") AUTH_TYPE = EnvironVariable("AUTH_TYPE") REQUEST_METHOD = EnvironVariable("REQUEST_METHOD") SCRIPT_NAME = EnvironVariable("SCRIPT_NAME") PATH_INFO = EnvironVariable("PATH_INFO") for _name, _obj in globals().items(): if isinstance(_obj, EnvironVariable): __all__.append(_name) _headers = {} class HTTPHeader(object): """ an HTTP header HTTPHeader instances represent a particular ``field-name`` of an HTTP message header. They do not hold a field-value, but instead provide operations that work on is corresponding values. Storage of the actual field values is done with WSGI ``environ`` or ``response_headers`` as appropriate. Typically, a sub-classes that represent a specific HTTP header, such as _ContentDisposition, are 0. Once constructed the HTTPHeader instances themselves are immutable and stateless. For purposes of documentation a "container" refers to either a WSGI ``environ`` dictionary, or a ``response_headers`` list. Member variables (and correspondingly constructor arguments). ``name`` the ``field-name`` of the header, in "common form" as presented in RFC 2616; e.g. 'Content-Type' ``category`` one of 'general', 'request', 'response', or 'entity' ``version`` version of HTTP (informational) with which the header should be recognized ``sort_order`` sorting order to be applied before sorting on field-name when ordering headers in a response Special Methods: ``__call__`` The primary method of the HTTPHeader instance is to make it a callable, it takes either a collection, a string value, or keyword arguments and attempts to find/construct a valid field-value ``__lt__`` This method is used so that HTTPHeader objects can be sorted in a manner suggested by RFC 2616. ``__str__`` The string-value for instances of this class is the ``field-name``. Primary Methods: ``delete()`` remove the all occurrences (if any) of the given header in the collection provided ``update()`` replaces (if they exist) all field-value items in the given collection with the value provided ``tuples()`` returns a set of (field-name, field-value) tuples 5 for extending ``response_headers`` Custom Methods (these may not be implemented): ``apply()`` similar to ``update``, but with two differences; first, only keyword arguments can be used, and second, specific sub-classes may introduce side-effects ``parse()`` converts a string value of the header into a more usable form, such as time in seconds for a date header, etc. The collected versions of initialized header instances are immediately registered and accessible through the ``get_header`` function. Do not inherit from this directly, use one of ``_SingleValueHeader``, ``_MultiValueHeader``, or ``_MultiEntryHeader`` as appropriate. """ # # Things which can be customized # version = '1.1' category = 'general' reference = '' extensions = {} def compose(self, **kwargs): """ build header value from keyword arguments This method is used to build the corresponding header value when keyword arguments (or no arguments) were provided. The result should be a sequence of values. For example, the ``Expires`` header takes a keyword argument ``time`` (e.g. time.time()) from which it returns a the corresponding date. """ raise NotImplementedError() def parse(self, *args, **kwargs): """ convert raw header value into more usable form This method invokes ``values()`` with the arguments provided, parses the header results, and then returns a header-specific data structure corresponding to the header. For example, the ``Expires`` header returns seconds (as returned by time.time()) """ raise NotImplementedError() def apply(self, collection, **kwargs): """ update the collection /w header value (may have side effects) This method is similar to ``update`` only that usage may result in other headers being changed as recommended by the corresponding specification. The return value is defined by the particular sub-class. For example, the ``_CacheControl.apply()`` sets the ``Expires`` header in addition to its normal behavior. """ self.update(collection, **kwargs) # # Things which are standardized (mostly) # def __new__(cls, name, category=None, reference=None, version=None): """ construct a new ``HTTPHeader`` instance We use the ``__new__`` operator to ensure that only one ``HTTPHeader`` instance exists for each field-name, and to register the header so that it can be found/enumerated. """ self = get_header(name, raiseError=False) if self: # Allow the registration to happen again, but assert # that everything is identical. assert self.name == name, \ "duplicate registration with different capitalization" assert self.category == category, \ "duplicate registration with different category" assert cls == self.__class__, \ "duplicate registration with different class" return self self = object.__new__(cls) self.name = name assert isinstance(self.name,str) self.category = category or self.category self.version = version or self.version self.reference = reference or self.reference _headers[self.name.lower()] = self self.sort_order = {'general': 1, 'request': 2, 'response': 3, 'entity': 4 }[self.category] self._environ_name = getattr(self, '_environ_name', 'HTTP_'+ self.name.upper().replace("-","_")) self._headers_name = getattr(self, '_headers_name', self.name.lower()) assert self.version in ('1.1','1.0','0.9') return self def __str__(self): return self.name def __lt__(self, other): """ sort header instances as specified by RFC 2616 Re-define sorting so that general headers are first, followed by request/response headers, and then entity headers. The list.sort() methods use the less-than operator for this purpose. """ if isinstance(other,HTTPHeader): if self.sort_order != other.sort_order: return self.sort_order < other.sort_order return self.name < other.name return False def __repr__(self): ref = self.reference and (' (%s)' % self.reference) or '' return '<%s %s%s>' % (self.__class__.__name__, self.name, ref) def values(self, *args, **kwargs): """ find/construct field-value(s) for the given header Resolution is done according to the following arguments: - If only keyword arguments are given, then this is equivalent to ``compose(**kwargs)``. - If the first (and only) argument is a dict, it is assumed to be a WSGI ``environ`` and the result of the corresponding ``HTTP_`` entry is returned. - If the first (and only) argument is a list, it is assumed to be a WSGI ``response_headers`` and the field-value(s) for this header are collected and returned. - In all other cases, the arguments are collected, checked that they are string values, possibly verified by the header's logic, and returned. At this time it is an error to provide keyword arguments if args is present (this might change). It is an error to provide both a WSGI object and also string arguments. If no arguments are provided, then ``compose()`` is called to provide a default value for the header; if there is not default it is an error. """ if not args: return self.compose(**kwargs) if list == type(args[0]): assert 1 == len(args) result = [] name = self.name.lower() for value in [value for header, value in args[0] if header.lower() == name]: result.append(value) return result if dict == type(args[0]): assert 1 == len(args) and 'wsgi.version' in args[0] value = args[0].get(self._environ_name) if not value: return () return (value,) for item in args: assert not type(item) in (dict, list) return args def __call__(self, *args, **kwargs): """ converts ``values()`` into a string value This method converts the results of ``values()`` into a string value for common usage. By default, it is asserted that only one value exists; if you need to access all values then either call ``values()`` directly, or inherit ``_MultiValueHeader`` which overrides this method to return a comma separated list of values as described by section 4.2 of RFC 2616. """ values = self.values(*args, **kwargs) assert isinstance(values, (tuple,list)) if not values: return '' assert len(values) == 1, "more than one value: %s" % repr(values) return str(values[0]).strip() def delete(self, collection): """ removes all occurances of the header from the collection provided """ if type(collection) == dict: if self._environ_name in collection: del collection[self._environ_name] return self assert list == type(collection) i = 0 while i < len(collection): if collection[i][0].lower() == self._headers_name: del collection[i] continue i += 1 def update(self, collection, *args, **kwargs): """ updates the collection with the provided header value This method replaces (in-place when possible) all occurrences of the given header with the provided value. If no value is provided, this is the same as ``remove`` (note that this case can only occur if the target is a collection w/o a corresponding header value). The return value is the new header value (which could be a list for ``_MultiEntryHeader`` instances). """ value = self.__call__(*args, **kwargs) if not value: self.remove(connection) return if type(collection) == dict: collection[self._environ_name] = value return assert list == type(collection) i = 0 found = False while i < len(collection): if collection[i][0].lower() == self._headers_name: if found: del collection[i] continue collection[i] = (self.name, value) found = True i += 1 if not found: collection.append((self.name, value)) def tuples(self, *args, **kwargs): value = self.__call__(*args, **kwargs) if not value: return () return [(self.name, value)] class _SingleValueHeader(HTTPHeader): """ a ``HTTPHeader`` with exactly a single value This is the default behavior of ``HTTPHeader`` where returning a the string-value of headers via ``__call__`` assumes that only a single value exists. """ pass class _MultiValueHeader(HTTPHeader): """ a ``HTTPHeader`` with one or more values The field-value for these header instances is is allowed to be more than one value; whereby the ``__call__`` method returns a comma separated list as described by section 4.2 of RFC 2616. """ def __call__(self, *args, **kwargs): results = self.values(*args, **kwargs) if not results: return '' return ", ".join([str(v).strip() for v in results]) def parse(self, *args, **kwargs): value = self.__call__(*args, **kwargs) values = value.split(',') return [ v.strip() for v in values if v.strip()] class _MultiEntryHeader(HTTPHeader): """ a multi-value ``HTTPHeader`` where items cannot be combined with a comma This header is multi-valued, but the values should not be combined with a comma since the header is not in compliance with RFC 2616 (Set-Cookie due to Expires parameter) or which common user-agents do not behave well when the header values are combined. """ def update(self, collection, *args, **kwargs): assert list == type(collection), "``environ`` may not be updated" self.delete(collection) collection.extend(self.tuples(*args,**kwargs)) return value def tuples(self, *args, **kwargs): values = self.values(*args, **kwargs) if not values: return () return [(self.name, value.strip()) for value in values] def get_header(name, raiseError=True): """ find the given ``HTTPHeader`` instance This function finds the corresponding ``HTTPHeader`` for the ``name`` provided. So that python-style names can be used, underscores are converted to dashes before the lookup. """ retval = _headers.get(str(name).strip().lower().replace("_","-")) if not retval and raiseError: raise AssertionError("'%s' is an unknown header" % name) return retval def list_headers(general=None, request=None, response=None, entity=None): " list all headers for a given category " if not (general or request or response or entity): general = request = response = entity = True search = [] for (bool,strval) in ((general,'general'), (request,'request'), (response,'response'), (entity,'entity')): if bool: search.append(strval) return [head for head in _headers.values() if head.category in search] def normalize_headers(response_headers, strict=True): """ sort headers as suggested by RFC 2616 This alters the underlying response_headers to use the common name for each header; as well as sorting them with general headers first, followed by request/response headers, then entity headers, and unknown headers last. """ category = {} for idx in range(len(response_headers)): (key,val) = response_headers[idx] head = get_header(key, strict) if not head: newhead = '-'.join([x.capitalize() for x in \ key.replace("_","-").split("-")]) response_headers[idx] = (newhead,val) category[newhead] = 4 continue response_headers[idx] = (str(head),val) category[str(head)] = head.sort_order def compare(a,b): ac = category[a[0]] bc = category[b[0]] if ac == bc: return cmp(a[0],b[0]) return cmp(ac,bc) response_headers.sort(compare) class _DateHeader(_SingleValueHeader): """ handle date-based headers This extends the ``_SingleValueHeader`` object with specific treatment of time values: - It overrides ``compose`` to provide a sole keyword argument ``time`` which is an offset in seconds from the current time. - A ``time`` method is provided which parses the g