changeset 4619:9edfdbed12a8

werkzeug updated
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sun, 01 Mar 2009 16:51:28 +0100
parents 0e7c007f8ed9
children 158a65df340e
files MoinMoin/support/werkzeug/__init__.py MoinMoin/support/werkzeug/contrib/lint.py MoinMoin/support/werkzeug/contrib/sessions.py MoinMoin/support/werkzeug/http.py MoinMoin/support/werkzeug/serving.py MoinMoin/support/werkzeug/utils.py
diffstat 6 files changed, 403 insertions(+), 27 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/support/werkzeug/__init__.py	Sun Mar 01 15:07:39 2009 +0100
+++ b/MoinMoin/support/werkzeug/__init__.py	Sun Mar 01 16:51:28 2009 +0100
@@ -71,7 +71,8 @@
                              'ETagResponseMixin', 'ResponseStreamMixin',
                              'CommonResponseDescriptorsMixin',
                              'UserAgentMixin', 'AuthorizationMixin',
-                             'WWWAuthenticateMixin'],
+                             'WWWAuthenticateMixin',
+                             'CommonRequestDescriptorsMixin'],
     # the undocumented easteregg ;-)
     'werkzeug._internal':   ['_easteregg']
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/support/werkzeug/contrib/lint.py	Sun Mar 01 16:51:28 2009 +0100
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+"""
+    werkzeug.contrib.lint
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    .. versionadded:: 0.5
+
+    This module provides a middleware that performs sanity checks of the WSGI
+    application.  It checks that :pep:`333` is properly implemented and warns
+    on some common HTTP errors such as non-empty responses for 304 status
+    codes.
+
+    This module provides a middleware, the :class:`LintMiddleware`.  Wrap your
+    application with it and it will warn about common problems with WSGI and
+    HTTP while your application is running.
+
+    It's strongly recommended to use it during development.
+
+    :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
+    :license: BSD, see LICENSE for more details.
+"""
+from urlparse import urlparse
+from warnings import warn
+from werkzeug.datastructures import Headers
+from werkzeug.utils import FileWrapper
+from werkzeug.http import is_entity_header
+
+
+class WSGIWarning(Warning):
+    """Warning class for WSGI warnings."""
+
+
+class HTTPWarning(Warning):
+    """Warning class for HTTP warnings."""
+
+
+def check_string(context, obj, stacklevel=3):
+    if type(obj) is not str:
+        warn(WSGIWarning('%s requires bytestrings, got %s' %
+            (context, obj.__class__.__name__)))
+
+
+class InputStream(object):
+
+    def __init__(self, stream):
+        self._stream = stream
+
+    def read(self, *args):
+        if len(args) == 0:
+            warn(WSGIWarning('wsgi does not guarantee an EOF marker on the '
+                             'input stream, thus making calls to '
+                             'wsgi.input.read() unsafe.  Conforming servers '
+                             'may never return from this call.'),
+                 stacklevel=2)
+        elif len(args) != 1:
+            warn(WSGIWarning('too many parameters passed to wsgi.input.read()'),
+                 stacklevel=2)
+        return self._stream.read(*args, **kwargs)
+
+    def readline(self, *args):
+        if len(args) == 0:
+            warn(WSGIWarning('Calls to wsgi.input.readline() without arguments'
+                             ' are unsafe.  Use wsgi.input.read() instead.'),
+                 stacklevel=2)
+        elif len(args) == 1:
+            warn(WSGIWarning('wsgi.input.readline() was called with a size hint. '
+                             'WSGI does not support this, although it\'s available '
+                             'on all major servers.'),
+                 stacklevel=2)
+        else:
+            raise TypeError('too many arguments passed to wsgi.input.readline()')
+        return self._stream.readline(*args, **kwargs)
+
+    def __iter__(self):
+        try:
+            return iter(self._stream)
+        except TypeError:
+            warn(WSGIWarning('wsgi.input is not iterable.'), stacklevel=2)
+            return iter(())
+
+    def close(self):
+        warn(WSGIWarning('application closed the input stream!'),
+             stacklevel=2)
+        self._stream.close()
+
+
+class ErrorStream(object):
+
+    def __init__(self, stream):
+        self._stream = stream
+
+    def write(self, s):
+        check_string('wsgi.error.write()', s)
+        self._stream.write(s)
+
+    def flush(self):
+        self._stream.flush()
+
+    def writelines(self, seq):
+        for line in seq:
+            self.write(seq)
+
+    def close(self):
+        warn(WSGIWarning('application closed the error stream!'),
+             stacklevel=2)
+        self._stream.close()
+
+
+class GuardedWrite(object):
+
+    def __init__(self, write, chunks):
+        self._write = write
+        self._chunks = chunks
+
+    def __call__(self, s):
+        check_string('write()', s)
+        self._write.write(s)
+        self._chunks.append(len(s))
+
+
+class GuardedIterator(object):
+
+    def __init__(self, iterator, headers_set, chunks):
+        self._iterator = iterator
+        self._next = iter(iterator).next
+        self.closed = False
+        self.headers_set = headers_set
+        self.chunks = chunks
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        if self.closed:
+            warn(WSGIWarning('iterated over closed app_iter'),
+                 stacklevel=2)
+        rv = self._next()
+        if not self.headers_set:
+            warn(WSGIWarning('Application returned before it '
+                             'started the response'), stacklevel=2)
+        check_string('application iterator items', rv)
+        self.chunks.append(len(rv))
+        return rv
+
+    def close(self):
+        self.closed = True
+        if hasattr(self._iterator, 'close'):
+            self._iterator.close()
+
+        if self.headers_set:
+            status_code, headers = self.headers_set
+            bytes_sent = sum(self.chunks)
+            content_length = headers.get('content-length', type=int)
+
+            if status_code == 304:
+                for key, value in headers:
+                    key = key.lower()
+                    if key not in ('expires', 'content-location') and \
+                       is_entity_header(key):
+                        warn(HTTPWarning('entity header %r found in 304 '
+                            'response' % key))
+                if bytes_sent:
+                    warn(HTTPWarning('304 responses must not have a body'))
+            elif 100 <= status_code < 200 or status_code == 204:
+                if content_length != 0:
+                    warn(HTTPWarning('%r responses must have an empty '
+                                     'content length') % status_code)
+                if bytes_sent:
+                    warn(HTTPWarning('%r responses must not have a body' %
+                                     status_code))
+            else:
+                if content_length is None:
+                    warn(WSGIWarning('Content-Length header missing'))
+                elif content_length != bytes_sent:
+                    warn(WSGIWarning('Content-Length and the number of bytes '
+                                     'sent to the client do not match.'))
+
+    def __del__(self):
+        if not self.closed:
+            try:
+                warn(WSGIWarning('Iterator was garbage collected before '
+                                 'it was closed.'))
+            except:
+                pass
+
+
+class LintMiddleware(object):
+    """This middleware wraps an application and warns on common errors.
+    Among other thing it currently checks for the following problems:
+
+    -   invalid status codes
+    -   non-bytestrings sent to the WSGI server
+    -   strings returned from the WSGI application
+    -   non-empty conditional responses
+    -   unquoted etags
+    -   relative URLs in the Location header
+    -   unsafe calls to wsgi.input
+    -   unclosed iterators
+
+    Detected errors are emitted using the standard Python :mod:`warnings`
+    system and usually end up on :data:`stderr`.
+
+    ::
+
+        from werkzeug.contrib.lint import LintMiddleware
+        app = LintMiddleware(app)
+
+    :param app: the application to wrap
+    """
+
+    def __init__(self, app):
+        self.app = app
+
+    def check_environ(self, environ):
+        if type(environ) is not dict:
+            warn(WSGIWarning('WSGI environment is not a standard python dict.'),
+                 stacklevel=4)
+        for key in ('REQUEST_METHOD', 'SERVER_NAME', 'SERVER_PORT',
+                    'wsgi.version', 'wsgi.input', 'wsgi.errors',
+                    'wsgi.multithread', 'wsgi.multiprocess',
+                    'wsgi.run_once'):
+            if key not in environ:
+                warn(WSGIWarning('required environment key %r not found'
+                     % key), stacklevel=3)
+        if environ['wsgi.version'] != (1, 0):
+            warn(WSGIWarning('environ is not a WSGI 1.0 environ'),
+                 stacklevel=3)
+
+        script_name = environ.get('SCRIPT_NAME', '')
+        if script_name and script_name[:1] != '/':
+            warn(WSGIWarning('SCRIPT_NAME does not start with a slash: %r'
+                             % script_name), stacklevel=3)
+        path_info = environ.get('PATH_INFO', '')
+        if path_info[:1] != '/':
+            warn(WSGIWarning('PATH_INFO does not start with a slash: %r'
+                             % path_info), stacklevel=3)
+
+
+    def check_start_response(self, status, headers, exc_info):
+        check_string('status', status)
+        status_code = status.split(None, 1)[0]
+        if len(status_code) != 3 or not status_code.isdigit():
+            warn(WSGIWarning('Status code must be three digits'), stacklevel=3)
+        if len(status) < 4 or status[3] != ' ':
+            warn(WSGIWarning('Invalid value for status %r.  Valid '
+                             'status strings are three digits, a space '
+                             'and a status explanation'), stacklevel=3)
+        status_code = int(status_code)
+        if status_code < 100:
+            warn(WSGIWarning('status code < 100 detected'), stacklevel=3)
+
+        if type(headers) is not list:
+            warn(WSGIWarning('header list is not a list'), stacklevel=3)
+        for item in headers:
+            if type(item) is not tuple or len(item) != 2:
+                warn(WSGIWarning('Headers must tuple 2-item tuples'),
+                     stacklevel=3)
+            name, value = item
+            if type(name) is not str or type(value) is not str:
+                warn(WSGIWarning('header items must be strings'),
+                     stacklevel=3)
+            if name.lower() == 'status':
+                warn(WSGIWarning('The status header is not supported due to '
+                                 'conflicts with the CGI spec.'),
+                                 stacklevel=3)
+
+        if exc_info is not None and not isinstance(exc_info, tuple):
+            warn(WSGIWarning('invalid value for exc_info'), stacklevel=3)
+
+        headers = Headers(headers)
+        self.check_headers(headers)
+
+        return status_code, headers
+
+    def check_headers(self, headers):
+        etag = headers.get('etag')
+        if etag is not None:
+            if etag.startswith('w/'):
+                etag = etag[2:]
+            if not (etag[:1] == etag[-1:] == '"'):
+                warn(HTTPWarning('unquoted etag emitted.'), stacklevel=4)
+
+        location = headers.get('location')
+        if location is not None:
+            if not urlparse(location).netloc:
+                warn(HTTPWarning('absolute URLs required for location header'),
+                     stacklevel=4)
+
+    def check_iterator(self, app_iter):
+        if isinstance(app_iter, basestring):
+            warn(WSGIWarning('application returned string.  Response will '
+                             'send character for character to the client '
+                             'which will kill the performance.  Return a '
+                             'list or iterable instead.'), stacklevel=3)
+
+    def __call__(self, *args, **kwargs):
+        if len(args) != 2:
+            warn(WSGIWarning('Two arguments to WSGI app required'), stacklevel=2)
+        if kwargs:
+            warn(WSGIWarning('No keyword arguments to WSGI app allowed'),
+                 stacklevel=2)
+        environ, start_response = args
+
+        self.check_environ(environ)
+        environ['wsgi.input'] = InputStream(environ['wsgi.input'])
+        environ['wsgi.errors'] = ErrorStream(environ['wsgi.errors'])
+
+        # hook our own file wrapper in so that applications will always
+        # iterate to the end and we can check the content length
+        environ['wsgi.file_wrapper'] = FileWrapper
+
+        headers_set = []
+        chunks = []
+
+        def checking_start_response(*args, **kwargs):
+            if len(args) not in (2, 3):
+                warn(WSGIWarning('Invalid number of arguments: %s, expected '
+                     '2 or 3' % len(args), stacklevel=2))
+            if kwargs:
+                warn(WSGIWarning('no keyword arguments allowed.'))
+
+            status, headers = args[:2]
+            if len(args) == 3:
+                exc_info = args[2]
+            else:
+                exc_info = None
+
+            headers_set[:] = self.check_start_response(status, headers,
+                                                       exc_info)
+            return GuardedWrite(start_response(status, headers, exc_info),
+                                chunks)
+
+        app_iter = self.app(environ, checking_start_response)
+        self.check_iterator(app_iter)
+        return GuardedIterator(app_iter, headers_set, chunks)
--- a/MoinMoin/support/werkzeug/contrib/sessions.py	Sun Mar 01 15:07:39 2009 +0100
+++ b/MoinMoin/support/werkzeug/contrib/sessions.py	Sun Mar 01 16:51:28 2009 +0100
@@ -1,12 +1,18 @@
 # -*- coding: utf-8 -*-
-"""
+r"""
     werkzeug.contrib.sessions
     ~~~~~~~~~~~~~~~~~~~~~~~~~
 
     This module contains some helper classes that help one to add session
-    support to a python WSGI application.
+    support to a python WSGI application.  For full client-side session
+    storage see :mod:`~werkzeug.contrib.securecookie` which implements a
+    secure, client-side session storage.
 
-    Example::
+
+    Application Integration
+    =======================
+
+    ::
 
         from werkzeug.contrib.sessions import SessionMiddleware, \
              FilesystemSessionStore
@@ -98,8 +104,7 @@
 
 
 class Session(ModificationTrackingDict):
-    """
-    Subclass of a dict that keeps track of direct object changes.  Changes
+    """Subclass of a dict that keeps track of direct object changes.  Changes
     in mutable structures are not tracked, for those you have to set
     `modified` to `True` by hand.
     """
@@ -127,6 +132,9 @@
     """Baseclass for all session stores.  The Werkzeug contrib module does not
     implement any useful stores besides the filesystem store, application
     developers are encouraged to create their own stores.
+
+    :param session_class: The session class to use.  Defaults to
+                          :class:`Session`.
     """
 
     def __init__(self, session_class=None):
@@ -168,10 +176,18 @@
 class FilesystemSessionStore(SessionStore):
     """Simple example session store that saves sessions in the filesystem like
     PHP does.
+
+    :param path: the path to the folder used for storing the sessions.
+                 If not provided the default temporary directory is used.
+    :param filename_template: a string template used to give the session
+                              a filename.  ``%s`` is replaced with the
+                              session id.
+    :param session_class: The session class to use.  Defaults to
+                          :class:`Session`.
     """
 
     def __init__(self, path=None, filename_template='werkzeug_%s.sess',
-                 session_class=Session):
+                 session_class=None):
         SessionStore.__init__(self, session_class)
         if path is None:
             from tempfile import gettempdir
@@ -220,17 +236,17 @@
     fast as sessions managed by the application itself and will put a key into
     the WSGI environment only relevant for the application which is against
     the concept of WSGI.
+
+    The cookie parameters are the same as for the :func:`~werkzeug.dump_cookie`
+    function just prefixed with ``cookie_``.  Additionally `max_age` is
+    called `cookie_age` and not `cookie_max_age` because of backwards
+    compatibility.
     """
 
     def __init__(self, app, store, cookie_name='session_id',
                  cookie_age=None, cookie_expires=None, cookie_path='/',
                  cookie_domain=None, cookie_secure=None,
                  cookie_httponly=False, environ_key='werkzeug.session'):
-        """The cookie parameters are the same as for the `dump_cookie`
-        function just prefixed with "cookie_".  Additionally "max_age" is
-        called "cookie_age" and not "cookie_max_age" because of backwards
-        compatibility.
-        """
         self.app = app
         self.store = store
         self.cookie_name = cookie_name
--- a/MoinMoin/support/werkzeug/http.py	Sun Mar 01 15:07:39 2009 +0100
+++ b/MoinMoin/support/werkzeug/http.py	Sun Mar 01 16:51:28 2009 +0100
@@ -640,14 +640,22 @@
     return not unmodified
 
 
-def remove_entity_headers(headers):
+def remove_entity_headers(headers, allowed=('expires', 'content-location')):
     """Remove all entity headers from a list or :class:`Headers` object.  This
-    operation works in-place.
+    operation works in-place.  `Expires` and `Content-Location` headers are
+    by default not removed.  The reason for this is :rfc:`2616` section
+    10.3.5 which specifies some entity headers that should be sent.
+
+    .. versionchanged:: 0.5
+       added `allowed` parameter.
 
     :param headers: a list or :class:`Headers` object.
+    :param allowed: a list of headers that should still be allowed even though
+                    they are entity headers.
     """
+    allowed = set(x.lower() for x in allowed)
     headers[:] = [(key, value) for key, value in headers if
-                  not is_entity_header(key)]
+                  not is_entity_header(key) or key.lower() in allowed]
 
 
 def remove_hop_by_hop_headers(headers):
--- a/MoinMoin/support/werkzeug/serving.py	Sun Mar 01 15:07:39 2009 +0100
+++ b/MoinMoin/support/werkzeug/serving.py	Sun Mar 01 16:51:28 2009 +0100
@@ -134,6 +134,8 @@
         except (socket.error, socket.timeout):
             return
         except:
+            if self.server.passthrough_errors:
+                raise
             from werkzeug.debug.tbtools import get_current_traceback
             traceback = get_current_traceback(ignore_system_exceptions=True)
             try:
@@ -157,11 +159,13 @@
     multithread = False
     multiprocess = False
 
-    def __init__(self, host, port, app, handler=None):
+    def __init__(self, host, port, app, handler=None,
+                 passthrough_errors=False):
         if handler is None:
             handler = BaseRequestHandler
         HTTPServer.__init__(self, (host, int(port)), handler)
         self.app = app
+        self.passthrough_errors = passthrough_errors
 
     def log(self, type, message, *args):
         _log(type, message, *args)
@@ -180,13 +184,15 @@
 class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer):
     multiprocess = True
 
-    def __init__(self, host, port, app, processes=40, handler=None):
-        BaseWSGIServer.__init__(self, host, port, app, handler)
+    def __init__(self, host, port, app, processes=40, handler=None,
+                 passthrough_errors=False):
+        BaseWSGIServer.__init__(self, host, port, app, handler,
+                                passthrough_errors)
         self.max_children = processes
 
 
 def make_server(host, port, app=None, threaded=False, processes=1,
-                request_handler=None):
+                request_handler=None, passthrough_errors=False):
     """Create a new server instance that is either threaded, or forks
     or just processes one request after another.
     """
@@ -194,11 +200,14 @@
         raise ValueError("cannot have a multithreaded and "
                          "multi process server.")
     elif threaded:
-        return ThreadedWSGIServer(host, port, app, request_handler)
+        return ThreadedWSGIServer(host, port, app, request_handler,
+                                  passthrough_errors)
     elif processes > 1:
-        return ForkingWSGIServer(host, port, app, processes, request_handler)
+        return ForkingWSGIServer(host, port, app, processes, request_handler,
+                                 passthrough_errors)
     else:
-        return BaseWSGIServer(host, port, app, request_handler)
+        return BaseWSGIServer(host, port, app, request_handler,
+                              passthrough_errors)
 
 
 def reloader_loop(extra_files=None, interval=1):
@@ -274,13 +283,15 @@
 def run_simple(hostname, port, application, use_reloader=False,
                use_debugger=False, use_evalex=True,
                extra_files=None, reloader_interval=1, threaded=False,
-               processes=1, request_handler=None, static_files=None):
+               processes=1, request_handler=None, static_files=None,
+               passthrough_errors=False):
     """Start an application using wsgiref and with an optional reloader.  This
     wraps `wsgiref` to fix the wrong default reporting of the multithreaded
     WSGI variable and adds optional multithreading and fork support.
 
     .. versionadded:: 0.5
-       `static_files` was added to simplify serving of static files.
+       `static_files` was added to simplify serving of static files as well
+       as `passthrough_errors`.
 
     :param hostname: The host for the application.  eg: ``'localhost'``
     :param port: The port for the server.  eg: ``8080``
@@ -305,6 +316,9 @@
                          like :class:`SharedDataMiddleware`, it's actually
                          just wrapping the application in that middleware before
                          serving.
+    :param passthrough_errors: set this to `False` to disable the error catching.
+                               This means that the server will die on errors but
+                               it can be useful to hook debuggers in (pdb etc.)
     """
     if use_debugger:
         from werkzeug.debug import DebuggedApplication
@@ -315,7 +329,8 @@
 
     def inner():
         make_server(hostname, port, application, threaded,
-                    processes, request_handler).serve_forever()
+                    processes, request_handler,
+                    passthrough_errors).serve_forever()
 
     if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
         display_hostname = hostname or '127.0.0.1'
--- a/MoinMoin/support/werkzeug/utils.py	Sun Mar 01 15:07:39 2009 +0100
+++ b/MoinMoin/support/werkzeug/utils.py	Sun Mar 01 16:51:28 2009 +0100
@@ -228,7 +228,7 @@
         mime_type = guessed_type[0] or 'text/plain'
         f, mtime, file_size = file_loader()
 
-        headers = [('Last-Modified', http_date(mtime)), ('Date', http_date())]
+        headers = [('Date', http_date())]
         if self.cache:
             timeout = self.cache_timeout
             etag = 'wzsdm-%s-%s-%s' % (mtime, file_size, hash(real_filename))
@@ -246,7 +246,8 @@
 
         headers.extend((
             ('Content-Type', mime_type),
-            ('Content-Length', str(file_size))
+            ('Content-Length', str(file_size)),
+            ('Last-Modified', http_date(mtime))
         ))
         start_response('200 OK', headers)
         return wrap_file(environ, f)