Lynn Rees avatar Lynn Rees committed a3d7fc5

[svn] made a copy

Comments (0)

Files changed (11)

branches/0.1/trunk/ez_setup.py

+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c3"
+DEFAULT_URL     = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+    'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+    'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+    'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+    'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+    'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+    'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+    'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        from md5 import md5
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    try:
+        import setuptools
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+    except ImportError:
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+
+    import pkg_resources
+    try:
+        pkg_resources.require("setuptools>="+version)
+
+    except pkg_resources.VersionConflict, e:
+        # XXX could we install in a subprocess here?
+        print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first.\n\n(Currently using %r)"
+        ) % (version, e.args[0])
+        sys.exit(2)
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+
+    try:
+        import setuptools
+    except ImportError:
+        egg = None
+        try:
+            egg = download_setuptools(version, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            if egg and os.path.exists(egg):
+                os.unlink(egg)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            # tell the user to uninstall obsolete version
+            use_setuptools(version)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+    from md5 import md5
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+

branches/0.1/trunk/setup.py

+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''setup - setuptools based setup for wsgiauth.'''
+
+import ez_setup
+ez_setup.use_setuptools()
+
+try:
+    from setuptools import setup
+except:
+    from distutils.core import setup
+
+setup(name='wsgiauth',
+      version='0.1',
+      description='''WSGI authentication middleware.''',
+      long_description='''WSGI authentication middleware that supports HTTP basic
+      and digest authentication, IP authentication, and form-based or OpenID
+      authentication using signed cookies or URL query parameters.''',
+      author='L. C. Rees',
+      author_email='lcrees@gmail.com',
+      license='MIT',
+      packages = ['wsgiauth'],
+      zip_safe = True,
+      keywords='WSGI dispatch middleware web HTTP decorators',
+      classifiers=['Development Status :: 3 - Alpha',
+                    'Environment :: Web Environment',
+                    'License :: OSI Approved :: MIT License',
+                    'Natural Language :: English',
+                    'Operating System :: OS Independent',
+                    'Programming Language :: Python',
+                    'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware'])

branches/0.1/trunk/wsgiauth/__init__.py

+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+__all__ = ['base', 'basic', 'cookie', 'digest', 'ip', 'openid', 'url', 'util']

branches/0.1/trunk/wsgiauth/base.py

+# (c) 2005 Clark C. Evans
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''Base WSGI Authentication Classes.'''
+
+import os
+import sha
+import hmac
+import base64
+import time
+from datetime import datetime
+from wsgiauth.util import extract, getpath, Response    
+
+__all__ = ['BaseAuth', 'Scheme', 'HTTPAuth']
+
+# ASCII chars
+_chars = ''.join(chr(c) for c in range(0, 255))
+# Size of HMAC sign w/ SHA as hash
+_cryptsize = len(hmac.new('x', 'x', sha).hexdigest())
+
+def getsecret():
+    '''Returns a 64 byte secret.'''
+    return ''.join(_chars[ord(i) % len(_chars)] for i in os.urandom(64))
+
+def gettime(date):
+    '''Returns a datetime object from a date string.
+
+    @param date Date/time string
+    '''
+    return datetime(*time.strptime(date)[0:7])
+
+# Fallback secret
+_secret = getsecret()
+# Fallback tracker
+_tracker = dict()
+# Fallback authorization response template
+TEMPLATE = '''<html>
+ <head><title>Please Login</title></head>
+ <body>
+  <h1>Please Login</h1>
+  <form action="%s" method="post">
+   <dl>
+    <dt>Username:</dt>
+    <dd><input type="text" name="username"></dd>
+    <dt>Password:</dt>
+    <dd><input type="password" name="password"></dd>
+   </dl>
+   <input type="submit" name="authform" />
+   <hr />
+  </form>
+ </body>
+</html>'''
+
+
+class BaseAuth(object):
+
+    '''Base class for authentication persistence.'''
+
+    # Default 
+    _tokename = '_CA_'
+    authtype = None
+
+    def __init__(self, application, authfunc, **kw):
+        self.application = application
+        # Authorization function
+        self.authfunc = authfunc
+        # Signing secret
+        self._secret = kw.get('secret', _secret)
+        # Authorization response
+        self.response = kw.get('response', Response(template=TEMPLATE))
+        # Token name
+        self.name = kw.get('name', self._tokename)
+        # Token storage
+        self.store = kw.get('tracker', _tracker)
+        # Authentication level (1-4)
+        self.authlevel = kw.get('authlevel', 1)
+        # Session timeout
+        self.timeout = kw.get('timeout', 3600)
+        # Form variable for username
+        self.namevar = kw.get('namevar', 'username')       
+
+    def __call__(self, environ, start_response):
+        # Check authentication
+        if not self.authenticate(environ):
+            result = self.authorize(environ)
+            # Request credentials if no authority
+            if hasattr(result, '__call__'):
+                return result(environ, start_response)
+            # Set auth-related environ entries
+            environ['REMOTE_USER'] = result
+            environ['AUTH_TYPE'] = self.authtype
+            environ['REQUEST_METHOD'] = 'GET'
+            environ['CONTENT_LENGTH'] = ''
+            environ['CONTENT_TYPE'] = ''
+            # Send initial response
+            return self.initial(environ, start_response)            
+        return self.application(environ, start_response)        
+        
+    def authorize(self, environ):
+        '''Checks authorization credentials for a request.'''
+        # Provide persistence for pre-authenticated requests
+        if environ.get('REMOTE_USER') is not None:
+            return environ.get('REMOTE_USER')
+        # Complete authorization process
+        elif environ['REQUEST_METHOD'] == 'POST':
+            # Get user credentials
+            userdata = extract(environ)
+            # Check authorization of user credentials
+            if self.authfunc(userdata):
+                # Return username
+                return userdata[self.namevar]
+            return self.response
+        return self.response
+
+    def _authtoken(self, environ, token):
+        '''Authenticates tokens.'''
+        authtoken = base64.urlsafe_b64decode(token)
+        # Get authentication token
+        current = authtoken[:_cryptsize]
+        # Get expiration time
+        date = gettime(authtoken[_cryptsize:].decode('hex'))
+        # Check if authentication has expired
+        if date > datetime.now().replace(microsecond=0):
+            # Get onetime token info
+            once = self.store[token]
+            user, path, nonce = once['user'], once['path'], once['nonce'] 
+            # Perform full token authentication if authlevel != 4
+            if self.authlevel != 4:
+                agent = environ['HTTP_USER_AGENT']                
+                raddr = environ['REMOTE_ADDR']
+                server = environ['SERVER_NAME']
+                newtoken = self.compute(user, raddr, server, path, agent, nonce)
+                if newtoken != current: return False
+            # Set user and authentication type
+            environ['REMOTE_USER'] = user
+            environ['AUTH_TYPE'] = self.authtype
+            return True
+
+    def compute(self, user, raddr, server, path, agent, nonce):
+        '''Computes an authentication token.'''
+       
+        # Verify minimum path and user auth
+        if self.authlevel == 3 or 4:
+            key = self._secret.join([path, nonce, user])
+        # Verify through 3 + agent and originating server
+        elif self.authlevel == 2:
+            key = self._secret.join([user, path, nonce, server, agent])
+        # Verify through 2 + IP address
+        elif self.authlevel == 1:
+            key = self._secret.join([raddr, user, server, nonce, agent, path])
+        # Return HMAC signed token
+        return hmac.new(self._secret, key, sha).hexdigest()        
+
+    def _gettoken(self, environ):
+        '''Generates authentication tokens.'''
+        user, path = environ['REMOTE_USER'], getpath(environ)
+        agent = environ['HTTP_USER_AGENT']
+        raddr, server = environ['REMOTE_ADDR'], environ['SERVER_NAME']
+        # Onetime secret
+        nonce = getsecret()
+        # Compute authentication token
+        authtoken = self.compute(user, raddr, server, path, agent, nonce)
+        # Compute token timeout
+        timeout = datetime.fromtimestamp(time.time() + self.timeout).ctime()
+        # Generate persistent token
+        token = base64.urlsafe_b64encode(authtoken + timeout.encode('hex'))
+        # Store onetime token info for future authentication
+        self.store[token] =  {'user':user, 'path':path, 'nonce':nonce}
+        return token
+
+    def authenticate(self, environ):
+        '''"Interface" for subclasses.'''
+        raise NotImplementedError()
+
+    def generate(self, environ):
+        '''"Interface" for subclasses.'''
+        raise NotImplementedError()
+
+    def initial(self, environ, start_response):
+        '''"Interface" for subclasses.'''
+        raise NotImplementedError()
+
+
+class Scheme(object):
+
+    '''HTTP Authentication Base.'''    
+
+    _msg = 'This server could not verify that you are authorized to\r\n' \
+    'access the document you requested.  Either you supplied the\r\n' \
+    'wrong credentials (e.g., bad password), or your browser\r\n' \
+    'does not understand how to supply the credentials required.' 
+    
+    def __init__(self, realm, authfunc, **kw):
+        self.realm, self.authfunc = realm, authfunc
+        # WSGI app that sends a 401 response
+        self.response = kw.get('response', self._response)
+        # Message to return with 401 response
+        self.message = kw.get('message', self._msg)
+
+    def _response(self, environ, start_response):
+        raise NotImplementedError()        
+    
+
+class HTTPAuth(object):
+
+    '''HTTP authentication middleware.'''    
+    
+    def __init__(self, application, realm, authfunc, scheme, **kw):
+        '''
+        @param application WSGI application.
+        @param realm Identifier for authority requesting authorization.
+        @param authfunc Mandatory user-defined function
+        @param scheme HTTP authentication scheme            
+        '''
+        self.application = application
+        self.authenticate = scheme(realm, authfunc, **kw)
+        self.scheme = scheme.authtype
+
+    def __call__(self, environ, start_response):
+        if environ.get('REMOTE_USER') is None:
+            result = self.authenticate(environ)
+            if not isinstance(result, str):
+                # Request credentials if authentication fails
+                return result(environ, start_response)
+            environ['REMOTE_USER'] = result
+            environ['AUTH_TYPE'] = self.scheme    
+        return self.application(environ, start_response)    

branches/0.1/trunk/wsgiauth/basic.py

+# (c) 2005 Clark C. Evans
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''HTTP Basic Authentication
+
+This module implements basic HTTP authentication as described in
+HTTP 1.0:
+
+http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA
+'''
+
+from wsgiauth.base import Scheme, HTTPAuth
+
+__all__ = ['basic']
+
+def basic(realm, authfunc, **kw):
+    '''Decorator for HTTP basic authentication.'''
+    def decorator(application):
+        return HTTPAuth(application, realm, authfunc, BasicAuth, **kw)
+    return decorator
+
+
+class BasicAuth(Scheme):
+
+    '''Performs HTTP basic authentication.'''
+
+    authtype = 'basic'
+
+    def _response(self, environ, start_response):
+        '''Default HTTP basic authentication response.'''
+        # Send 401 response + realm
+        start_response('401 Unauthorized',
+            [('content-type', 'text/plain'),
+            ('WWW-Authenticate', 'Basic realm="%s"' % self.realm)])
+        return [self.message]
+
+    def __call__(self, environ):
+        # Check authorization
+        authorization = environ.get('HTTP_AUTHORIZATION')
+        # Request credentials if no authorization
+        if authorization is None: return self.response
+        # Verify scheme
+        authmeth, auth = authorization.split(' ', 1)
+        if authmeth.lower() != 'basic': return self.response
+        # Get username, password
+        auth = auth.strip().decode('base64')
+        username, password = auth.split(':', 1)
+        # Authorize user
+        if self.authfunc(environ, username, password): return username
+        return self.response

branches/0.1/trunk/wsgiauth/cookie.py

+# (c) 2005 Clark C. Evans
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''WSGI middleware for persistent authentication tokens in cookies.'''
+
+from Cookie import SimpleCookie
+from wsgiauth.base import BaseAuth
+
+__all__ = ['Cookie', 'cookie']
+
+
+def cookie(authfunc, **kw):
+    '''Decorator for persistent authentication tokens in cookies.'''
+    def decorator(application):
+        return Cookie(application, authfunc, **kw)
+    return decorator
+
+
+class Cookie(BaseAuth):
+
+    '''Persists authentication tokens in HTTP cookies.'''    
+
+    authtype = 'cookie'
+
+    def __init__(self, application, authfunc, **kw):
+        super(Cookie, self).__init__(application, authfunc, **kw)
+        # Cookie domain
+        self.domain = kw.get('domain')
+        # Cookie age
+        self.age = kw.get('age', 7200)
+        # Cookie path, comment
+        self.path, self.comment = kw.get('path', '/'), kw.get('comment')    
+        
+    def authenticate(self, environ):
+        '''Authenticates a token embedded in a cookie.'''
+        try:
+            cookies = SimpleCookie(environ['HTTP_COOKIE'])
+            scookie = cookies[self.name]
+            auth = self._authtoken(environ, scookie.value)
+            # Tell user agent to expire cookie if invalid
+            if not auth:
+                scookie[scookie.value]['expires'] = -365*24*60*60
+                scookie[scookie.value]['max-age'] = 0
+            return auth
+        except KeyError:
+            return False
+
+    def generate(self, environ):
+        '''Returns an authentication token embedded in a cookie.'''
+        scookie = SimpleCookie()
+        scookie[self.name] = self._gettoken(environ)
+        scookie[self.name]['path'] = self.path
+        scookie[self.name]['max-age'] = self.age
+        if self.domain is not None:
+            scookie[self.name]['domain'] = self.domain
+        if self.comment is not None:
+            scookie[self.name]['comment'] = self.comment
+        # Mark cookie as secure if using SSL
+        if environ['wsgi.url_scheme'] == 'https':
+            scookie[self.name]['secure'] = ''
+        return scookie[self.name].OutputString()
+
+    def initial(self, environ, start_response):
+        '''Initial response to a request.'''
+        def cookie_response(status, headers, exc_info=None):
+            headers.append(('Set-Cookie', self.generate(environ)))
+            return start_response(status, headers, exc_info)
+        return self.application(environ, cookie_response)

branches/0.1/trunk/wsgiauth/digest.py

+# (c) 2005 Clark C. Evans
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''HTTP Digest Authentication
+
+This module implements digest HTTP authentication as described in the
+HTTP 1.1 specification:
+
+http://www.w3.org/Protocols/HTTP/1.1/spec.html#DigestAA
+'''
+
+import md5
+import time
+import random
+from wsgiauth.base import HTTPAuth, Scheme
+
+__all__ = ['digest', 'digest_password']
+
+def digest_password(realm, username, password):
+    ''' construct the appropriate hashcode needed for HTTP digest '''
+    return md5.new('%s:%s:%s' % (username, realm, password)).hexdigest()
+
+def digest(realm, authfunc, **kw):
+    '''Decorator for HTTP digest middleware.'''
+    def decorator(application):
+        return HTTPAuth(application, realm, authfunc, DigestAuth, **kw)
+    return decorator
+
+_nonce = dict()
+
+class DigestAuth(Scheme):
+    
+    '''Performs HTTP digest authentication.'''
+
+    authtype = 'digest'        
+    
+    def __init__(self, realm, authfunc, **kw):
+        super(DigestAuth, self).__init__(realm, authfunc, **kw)
+        self.nonce = kw.get('nonce', _nonce) # dict to prevent replay attacks
+
+    def _response(self, stale = ''):
+        '''Builds the authentication error.'''
+        def coroutine(environ, start_response):
+            nonce = md5.new('%s:%s' % (time.time(),
+                random.random())).hexdigest()
+            opaque = md5.new('%s:%s' % (time.time(),
+                random.random())).hexdigest()
+            self.nonce[nonce] = None
+            parts = {'realm':self.realm, 'qop':'auth', 'nonce':nonce,
+                'opaque':opaque}
+            if stale: parts['stale'] = 'true'
+            head = ', '.join(['%s="%s"' % (k, v) for (k, v) in parts.items()])
+            start_response('401 Unauthorized', [('content-type','text/plain'),
+                ('WWW-Authenticate', 'Digest %s' % head)])
+            return [self.message]
+        return coroutine
+
+    def compute(self, ha1, username, response, method, path, nonce, nc,
+            cnonce, qop):
+        '''Computes the authentication, raises error if unsuccessful.'''
+        if not ha1: return self.response()
+        ha2 = md5.new('%s:%s' % (method, path)).hexdigest()
+        if qop:
+            chk = '%s:%s:%s:%s:%s:%s' % (ha1, nonce, nc, cnonce, qop, ha2)
+        else:
+            chk = '%s:%s:%s' % (ha1, nonce, ha2)
+        if response != md5.new(chk).hexdigest():
+            if nonce in self.nonce: del self.nonce[nonce]
+            return self.response()
+        pnc = self.nonce.get(nonce, '00000000')
+        if nc <= pnc:
+            if nonce in self.nonce: del self.nonce[nonce]
+            return self.response(stale=True)
+        self.nonce[nonce] = nc
+        return username
+
+    def __call__(self, environ):
+        '''This function takes a WSGI environment and authenticates
+        the request returning authenticated user or error.
+        '''
+        method = environ['REQUEST_METHOD']
+        fullpath = environ['SCRIPT_NAME'] + environ['PATH_INFO']
+        authorization = environ.get('HTTP_AUTHORIZATION')
+        if authorization is None: return self.response()
+        authmeth, auth = authorization.split(' ', 1)
+        if 'digest' != authmeth.lower(): return self.response()
+        amap = dict()
+        for itm in auth.split(', '):
+            k, v = [s.strip() for s in itm.split('=', 1)]
+            amap[k] = v.replace('"', '')
+        try:
+            username = amap['username']
+            authpath = amap['uri']
+            nonce = amap['nonce']
+            realm = amap['realm']
+            response = amap['response']
+            assert authpath.split('?', 1)[0] in fullpath
+            assert realm == self.realm
+            qop = amap.get('qop', '')
+            cnonce = amap.get('cnonce', '')
+            nc = amap.get('nc', '00000000')
+            if qop:
+                assert 'auth' == qop
+                assert nonce and nc
+        except:
+            return self.response()
+        ha1 = self.authfunc(environ, realm, username)
+        return self.compute(ha1, username, response, method, authpath, nonce,
+            nc, cnonce, qop)

branches/0.1/trunk/wsgiauth/ip.py

+# (c) 2005 Ian Bicking and contributors; written for Paste
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''Authenticate an IP address.'''
+
+from wsgiauth.util import Forbidden
+
+__all__ = ['IP', 'ip']
+
+def ip(authfunc, **kw):
+    '''Decorator for IP address-based authentication.'''
+    def decorator(application):
+        return IP(application, authfunc, **kw)
+    return decorator
+
+
+class IP(object):
+
+    '''On each request, `REMOTE_ADDR` is authenticated and access allowed based
+    on IP address.
+    '''
+
+    def __init__(self, app, authfunc, **kw):
+        self.app, self.authfunc = app, authfunc
+        self.response = kw.get('response', Forbidden())
+
+    def __call__(self, environ, start_response):
+        ipaddr = environ.get('REMOTE_ADDR')
+        if not self.authfunc(environ, ipaddr):
+            return self.response(environ, start_response)            
+        return self.app(environ, start_response)

branches/0.1/trunk/wsgiauth/openID.py

+# (c) 2005 Ben Bangert
+# (c) 2005 Clark C. Evans
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''OpenID Authentication (Consumer)
+
+OpenID is a distributed authentication system for single sign-on:
+
+    http://openid.net/
+
+This module is based on the consumer.py example that comes with the Python
+OpenID library 1.1+.
+'''
+
+import cgi
+import urlparse
+import sys
+from Cookie import SimpleCookie
+try:
+    import openid
+except ImportError:
+    print >> sys.stderr, '''Failed to import the OpenID library.
+In order to use this example, you must either install the library
+(see INSTALL in the root of the distribution) or else add the
+library to python's import path (the PYTHONPATH environment variable).
+
+For more information, see the README in the root of the library
+distribution or http://www.openidenabled.com/
+'''
+    sys.exit(1)
+from openid.store import filestore
+from openid.consumer import consumer
+from openid.oidutil import appendArgs
+from openid.cryptutil import randomString
+from yadis.discover import DiscoveryFailure
+from urljr.fetchers import HTTPFetchingError
+from wsgiauth.util import geturl, getpath, Redirect, Response
+from wsgiauth.cookie import Cookie
+
+__all__ = ['OpenID', 'openid']
+
+def quote(s):
+    '''Quotes URLs passed as query parameters.'''
+    return '"%s"' % cgi.escape(s, 1)
+
+def openid(store, **kw):
+    '''Decorator for OpenID authorized middleware.'''
+    def decorator(application):
+        return OpenID(application, store, **kw)
+    return decorator
+
+# Fallback session tracker
+_tracker = {}
+# Fallback template
+TEMPLATE = '''<html>
+  <head><title>OpenID Form</title></head>
+  <body>
+    <h1>%s</h1>
+    <p>Enter your OpenID identity URL:</p>
+      <form method="get" action=%s>
+        Identity&nbsp;URL:
+        <input type="text" name="openid_url" value=%s />
+        <input type="submit" value="Verify" />
+      </form>
+    </div>
+  </body>
+</html>'''
+
+class OpenID(Cookie):
+
+    def __init__(self, app, store, **kw):
+        # Make OpenIDAuth the authorization function
+        auth = OpenIDAuth(store, **kw)
+        super(OpenID, self).__init__(app, auth, **kw)
+        self.authorize = auth
+
+    def initial(self, environ, start_response):
+        '''Initial response to a request.'''
+        # Add authentication cookie
+        def cookie_response(status, headers, exc_info=None):            
+            headers.append(('Set-Cookie', self.generate(environ)))
+            return start_response(status, headers, exc_info)
+        # Redirect to original URL
+        redirect = Redirect(environ['openid.redirect'])
+        return redirect(environ, cookie_response)
+        
+
+class OpenIDAuth(object):
+
+    '''Authenticates a URL against an OpenID Server.'''
+
+    cname = '_OIDA_'
+
+    def __init__(self, store, **kw):
+        # OpenID store
+        self.store = filestore.FileOpenIDStore(store)     
+        # Session tracker
+        self.tracker = kw.get('tracker', _tracker)
+        # Set template
+        self.template = kw.get('template', TEMPLATE)
+
+    def __call__(self, environ):
+        # Base URL
+        environ['openid.baseurl'] = geturl(environ, False, False)
+        # Query string
+        environ['openid.query'] = dict(cgi.parse_qsl(environ['QUERY_STRING']))
+        # Path
+        path = getpath(environ)
+        # Start verification
+        if path == '/verify':
+            return self.verify(environ)
+        # Process response
+        elif path == '/process':
+            return self.process(environ)
+        # Prompt for URL
+        else:            
+            message = 'Enter an OpenID Identifier to verify.'
+            return self.response(message, environ)
+
+    def verify(self, environ):
+        '''Process the form submission, initating OpenID verification.'''
+        # First, make sure that the user entered something
+        openid_url = environ['openid.query'].get('openid_url')
+        # Ensure a URL is entered
+        if not openid_url:
+            message = 'Enter an OpenID Identifier to verify.'
+            return self.response(message, environ)
+        # Start open id session
+        oidconsumer = self.getconsumer(environ)
+        # Start verification
+        try:
+            request = oidconsumer.begin(openid_url)
+        # Handle HTTP errors
+        except HTTPFetchingError, exc:
+            message = 'Error in discovery: %s' % cgi.escape(str(exc.why))
+            return self.response(message, environ, openid_url)
+        # Handle Discovery errors
+        except DiscoveryFailure, exc:
+            message = 'Error in discovery: %s' % cgi.escape(str(exc[0]))
+            return self.response(message, environ, openid_url)
+        else:
+            # Handle URLs that don't have a discernable OpenID server
+            if request is None:
+                fmt = 'No OpenID services found for %s'
+                return self.response(fmt % cgi.escape(openid_url), environ)
+            # Start redirect
+            else:
+                return self.redirect(environ, request)      
+
+    def process(self, environ):
+        '''Handle redirect from the OpenID server.'''
+        oidconsumer, openid_url = self.getconsumer(environ), ''
+        # Verify OpenID server response
+        info = oidconsumer.complete(environ['openid.query'])
+        # Handle successful responses
+        if info.status == consumer.SUCCESS:            
+            # Fetch original requested URL
+            redirecturl = self.tracker[self.getsid(environ)]['redirect']
+            environ['openid.redirect'] = redirecturl
+            # Handle i-names
+            if info.endpoint.canonicalID:                    
+                return info.endpoint.canonicalID
+            # Otherwise, return identity URL as user name
+            else:
+                return info.identity_url
+        # Handle failure to verify a URL where URL is returned.
+        elif info.status == consumer.FAILURE and info.identity_url:
+            openid_url = info.identity_url
+            message = 'Verification of %s failed.' % cgi.escape(openid_url)
+        # User cancelled verification
+        elif info.status == consumer.CANCEL:            
+            message = 'Verification cancelled'
+        # Handle other errors
+        else:            
+            message = 'Verification failed.'
+        return self.response(message, environ, openid_url)
+
+    def buildurl(self, environ, action, **query):
+        '''Build a URL relative to the server base url, with the given
+        query parameters added.'''
+        base = urlparse.urljoin(environ['openid.baseurl'], action)
+        return appendArgs(base, query)
+
+    def getconsumer(self, environ):
+        '''Get an OpenID consumer with session.'''
+        return consumer.Consumer(self.getsession(environ), self.store)
+
+    def response(self, message, env, url=''):
+        '''Default response.'''
+        # Set OpenID session cookie
+        hdrs = [('Set-Cookie', self.setsession(env))]
+        # Build message
+        cmessage = (message, quote(self.buildurl(env, 'verify')), quote(url))
+        return Response(cmessage, template=self.template, headers=hdrs)
+
+    def redirect(self, environ, request):
+        '''Redirect response.'''
+        # Set OpenID session cookie
+        hdrs = [('Set-Cookie', self.setsession(environ))]
+        # Build redirect URL
+        trust_root = environ['openid.baseurl']                
+        return_to = self.buildurl(environ, 'process')
+        redirect_url = request.redirectURL(trust_root, return_to)
+        return Redirect(redirect_url, headers=hdrs)
+
+    def getsession(self, environ):
+        '''Return the existing session or a new session'''
+        # Get value of cookie header that was sent
+        sid = self.getsid(environ)
+        # If a session id was not set, create a new one
+        if sid is None:
+            sid = randomString(16, '0123456789abcdef')
+            session = None
+        else:
+            session = self.tracker.get(sid)
+        # If no session exists for this session ID, create one
+        if session is None:
+            session = self.tracker[sid] = {}
+            session['redirect'] = geturl(environ)
+        session['id'] = sid        
+        return session
+
+    def getsid(self, environ):
+        '''Returns a session identifier.'''
+        # Fetch cookie
+        cookie_str = environ.get('HTTP_COOKIE')        
+        if cookie_str:
+            cookie_obj = SimpleCookie(cookie_str)
+            sid_morsel = cookie_obj.get(self.cname, None)
+            # Get session id from cookie
+            if sid_morsel is not None:
+                sid = sid_morsel.value
+            else:
+                sid = None
+        else:
+            sid = None
+        return sid
+
+    def setsession(self, environ):
+        '''Returns a session identifier.'''
+        sid = self.getsession(environ)['id']
+        return '%s=%s;' % (self.cname, sid)     

branches/0.1/trunk/wsgiauth/url.py

+# (c) 2005 Clark C. Evans
+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+'''Persistent authentication tokens in URL query components.'''
+
+import cgi
+from wsgiauth.base import BaseAuth
+from wsgiauth.util import Redirect, geturl
+
+__all__ = ['URLAuth', 'urlauth']
+
+def urlauth(authfunc, **kw):
+    '''Decorator for persistent authentication tokens in URLs.'''
+    def decorator(application):
+        return URLAuth(application, authfunc, **kw)
+    return decorator
+
+
+class URLAuth(BaseAuth):
+
+    '''Persists authentication tokens in URL query components.'''    
+
+    authtype = 'url'
+
+    def __init__(self, application, authfunc, **kw):
+        super(URLAuth, self).__init__(application, authfunc, **kw)
+        # Redirect method
+        self.redirect = kw.get('redirect', Redirect)
+
+    def authenticate(self, environ):
+        '''Authenticates a token embedded in a query component.'''
+        try:            
+            query = cgi.parse_qs(environ['QUERY_STRING'])
+            return self._authtoken(environ, query[self.name][0])
+        except KeyError:
+            return False
+        
+    def generate(self, env):
+        '''Embeds authentication token in query component.'''
+        env['QUERY_STRING'] = '='.join([self.name, self._gettoken(env)])
+
+    def initial(self, environ, start_response):
+        # Embed auth token
+        self.generate(environ)
+        # Redirect to requested URL with auth token in query string
+        redirect = self.redirect(geturl(environ))
+        return redirect(environ, start_response)     

branches/0.1/trunk/wsgiauth/util.py

+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import cgi
+from urllib import quote
+
+
+class Response(object):
+
+    '''Generic WSGI application for HTTP responses.'''    
+
+    _template = None    
+    _status = '200 OK'
+    _ctype = 'text/html'
+
+    def __init__(self, message=None, **kw):
+        # Status
+        self.status = kw.get('status', self._status)
+        # Response iterator        
+        self.response = kw.get('response', self._response)
+        # Authorization message
+        self.message = message        
+        # Authorization response template
+        self.template = kw.get('template', self._template)
+        # Header list
+        self.headers = kw.get('headers', list())
+        # Content type
+        ctype = kw.get('content', self._ctype)
+        self.headers.append(('Content-type', ctype))
+
+    def __call__(self, environ, start_response):
+        start_response(self.status, self.headers)
+        return self.response(self.message or geturl(environ))
+
+    def _response(self, message):
+        '''Returns an iterator containing a message body.'''
+        return [self.template % message]
+    
+
+class Redirect(Response):
+
+    '''WSGI application for HTTP 30x redirects.'''
+
+    _template = '<html>\n<head><title>Redirecting to %s</title></head>\n' \
+        '<body>\nYou are being redirected to <a href="%s">%s</a>\n' \
+        '</body>\n</html>'
+    _status = '302 Found'
+
+    def __call__(self, environ, start_response):
+        location = self.message
+        self.headers.append(('location', location))
+        start_response(self.status, self.headers)
+        return self.response((location, location, location))
+    
+
+class Forbidden(Response):
+    
+    '''WSGI application for 403 responses.'''
+
+    _template = 'This server could not verify that you are authorized to ' \
+             'access resource %s from your current location.'
+    _status = '403 Forbidden'
+    _ctype = 'text/plain'
+        
+    def __call__(self, environ, start_response):
+        start_response(self.status, self.headers)
+        return self.response(self.message or geturl(environ))
+
+def extract(environ, empty=False, err=False):
+    '''Extracts strings in form data and returns a dict.
+
+    @param environ WSGI environ
+    @param empty Stops on empty fields (default: Fault)
+    @param err Stops on errors in fields (default: Fault)
+    '''
+    formdata = cgi.parse(environ['wsgi.input'], environ, empty, err)    
+    # Remove single entries from lists
+    for key, value in formdata.iteritems():
+        if len(value) == 1: formdata[key] = value[0]
+    return formdata
+
+def geturl(environ, query=True, path=True):
+    '''Rebuilds a request URL (from PEP 333).
+    
+    @param include_query Is QUERY_STRING included in URI (default: True)
+    @param include_path Is path included in URI (default: True)
+    '''    
+    url = [environ['wsgi.url_scheme'] + '://']
+    if environ.get('HTTP_HOST'):
+        url.append(environ['HTTP_HOST'])
+    else:
+        url.append(environ['SERVER_NAME'])
+        if environ['wsgi.url_scheme'] == 'https':
+            if environ['SERVER_PORT'] != '443':
+                url.append(':' + environ['SERVER_PORT'])
+        else:
+            if environ['SERVER_PORT'] != '80':
+                url.append(':' + environ['SERVER_PORT'])
+    if path:
+        url.append(getpath(environ))
+    if query and environ.get('QUERY_STRING'):
+        url.append('?' + environ['QUERY_STRING'])
+    return ''.join(url)
+
+def getpath(environ):
+    '''Builds a path.'''
+    return ''.join([quote(environ.get('SCRIPT_NAME', '')),
+        quote(environ.get('PATH_INFO', ''))])
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.