summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorianb <devnull@localhost>2005-12-13 07:00:20 +0000
committerianb <devnull@localhost>2005-12-13 07:00:20 +0000
commit4e73bff9da87e35c7154ab1cc923bb4f9d40711d (patch)
treed2e4c92965398700457280d5829dfaa5cdf5b4fb
parent55b404e53bc834daf3852069af6de9b1fca4c742 (diff)
downloadpaste-4e73bff9da87e35c7154ab1cc923bb4f9d40711d.tar.gz
Merged changes from cce branch (r3727:HEAD/4008); the branch is now in sync with trunk
-rw-r--r--paste/auth/__init__.py1
-rw-r--r--paste/auth/basic.py67
-rw-r--r--paste/auth/cas.py94
-rw-r--r--paste/auth/cookie.py229
-rw-r--r--paste/auth/digest.py193
-rw-r--r--paste/auth/form.py73
-rw-r--r--paste/auth/multi.py81
-rw-r--r--paste/debug/__init__.py1
-rwxr-xr-xpaste/debug/testserver.py97
-rw-r--r--paste/exceptions/errormiddleware.py76
-rw-r--r--paste/httpexceptions.py539
-rw-r--r--paste/lint.py20
-rw-r--r--paste/request.py214
-rw-r--r--paste/transaction.py79
-rwxr-xr-xpaste/util/baseserver.py127
-rw-r--r--paste/util/quoting.py10
-rw-r--r--paste/wsgilib.py295
-rw-r--r--tests/test_auth/test_auth_cookie.py41
-rw-r--r--tests/test_auth/test_auth_digest.py86
-rw-r--r--tests/test_exceptions/test_error_middleware.py30
-rw-r--r--tests/test_exceptions/test_formatter.py6
-rw-r--r--tests/test_exceptions/test_httpexceptions.py82
-rw-r--r--tests/test_request.py7
23 files changed, 2074 insertions, 374 deletions
diff --git a/paste/auth/__init__.py b/paste/auth/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/paste/auth/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/paste/auth/basic.py b/paste/auth/basic.py
new file mode 100644
index 0000000..8a7e787
--- /dev/null
+++ b/paste/auth/basic.py
@@ -0,0 +1,67 @@
+# (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
+"""
+Basic Authentication
+
+"""
+from paste.httpexceptions import HTTPUnauthorized
+
+class BasicAuthenticator:
+ """ Implementation of only 'Basic' authentication in 2617 """
+ def __init__(self, realm, userfunc):
+ """
+ realm is a globally unique URI like tag:clarkevans.com,2005:basic
+ that represents the authenticating authority
+ userfunc(username, password) -> boolean
+ """
+ self.realm = realm
+ self.userfunc = userfunc
+
+ def build_authentication(self):
+ head = [('WWW-Authenticate','Basic realm="%s"' % self.realm)]
+ return HTTPUnauthorized(headers=head)
+
+ def authenticate(self, authorization):
+ if not authorization:
+ return self.build_authentication()
+ (authmeth, auth) = authorization.split(" ",1)
+ if 'basic' != authmeth.lower():
+ return self.build_authentication()
+ auth = auth.strip().decode('base64')
+ username, password = auth.split(':')
+ if self.userfunc(username, password):
+ return username
+ return self.build_authentication()
+
+ __call__ = authenticate
+
+def AuthBasicHandler(application, realm, userfunc):
+ authenticator = BasicAuthenticator(realm, userfunc)
+ def basic_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if not username:
+ authorization = environ.get('HTTP_AUTHORIZATION','')
+ result = authenticator(authorization)
+ if isinstance(result,str):
+ environ['AUTH_TYPE'] = 'basic'
+ environ['REMOTE_USER'] = result
+ else:
+ return result.wsgi_application(environ, start_response)
+ return application(environ, start_response)
+ return basic_application
+
+middleware = AuthBasicHandler
+
+__all__ = ['AuthBasicHandler']
+
+if '__main__' == __name__:
+ realm = 'tag:clarkevans.com,2005:basic'
+ def userfunc(username, password):
+ return username == password
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthBasicHandler(dump_environ, realm, userfunc)))
diff --git a/paste/auth/cas.py b/paste/auth/cas.py
new file mode 100644
index 0000000..193b79a
--- /dev/null
+++ b/paste/auth/cas.py
@@ -0,0 +1,94 @@
+# (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
+"""
+CAS 1.0 Authentication
+
+The Central Authentication System is a straight-forward single sign-on
+mechanism developed by Yale University's ITS department. It has since
+enjoyed widespread success and is deployed at many major universities
+and some corporations.
+
+ https://clearinghouse.ja-sig.org/wiki/display/CAS/Home
+ http://www.yale.edu/tp/auth/usingcasatyale.html
+
+This implementation has the goal of maintaining current path arguments
+passed to the system so that it can be used as middleware at any stage
+of processing. It has the secondary goal of allowing for other
+authentication methods to be used concurrently.
+"""
+import urllib
+from paste.wsgilib import construct_url
+from paste.httpexceptions import HTTPSeeOther, HTTPForbidden
+
+class CASLoginFailure(HTTPForbidden):
+ """ The exception raised if the authority returns 'no' """
+
+class CASAuthenticate(HTTPSeeOther):
+ """ The exception raised to authenticate the user """
+
+def AuthCASHandler(application, authority):
+ """
+ This middleware implements CAS 1.0 Authentication There are several
+ possible outcomes:
+
+ 0. If the REMOTE_USER environment variable is already populated;
+ then this middleware is a no-op, and the request is passed along
+ to the application.
+
+ 1. If a query argument 'ticket' is found, then an attempt to
+ validate said ticket /w the authentication service done. If the
+ ticket is not validated; an 403 'Forbidden' exception is raised.
+ Otherwise, the REMOTE_USER variable is set with the NetID that
+ was validated and AUTH_TYPE is set to "cas".
+
+ 2. Otherwise, a 303 'See Other' is returned to the client directing
+ them to login using the CAS service. After logon, the service
+ will send them back to this same URL, only with a 'ticket' query
+ argument.
+
+ authority:
+ This is a fully-qualified URL to a CAS 1.0 service. The URL
+ should end with a '/' and have the 'login' and 'validate'
+ sub-paths as described in the CAS 1.0 documentation.
+ """
+ assert authority.endswith("/") and authority.startswith("http")
+ def cas_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if username:
+ return application(environ, start_response)
+ qs = environ.get('QUERY_STRING','').split("&")
+ if qs and qs[-1].startswith("ticket="):
+ # assume a response from the authority
+ ticket = qs.pop().split("=",1)[1]
+ environ['QUERY_STRING'] = "&".join(qs)
+ service = construct_url(environ)
+ args = urllib.urlencode(
+ {'service': service,'ticket': ticket})
+ requrl = authority + "validate?" + args
+ result = urllib.urlopen(requrl).read().split("\n")
+ if 'yes' == result[0]:
+ environ['REMOTE_USER'] = result[1]
+ environ['AUTH_TYPE'] = 'cas'
+ return application(environ, start_response)
+ exce = CASLoginFailure()
+ else:
+ service = construct_url(environ)
+ args = urllib.urlencode({'service': service})
+ location = authority + "login?" + args
+ exce = CASAuthenticate(location)
+ return exce.wsgi_application(environ, start_response)
+ return cas_application
+
+middleware = AuthCASHandler
+
+__all__ = ['CASLoginFailure', 'CASAuthenticate', 'AuthCASHandler' ]
+
+if '__main__' == __name__:
+ authority = "https://secure.its.yale.edu/cas/servlet/"
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthCASHandler(dump_environ, authority)))
diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py
new file mode 100644
index 0000000..96071d7
--- /dev/null
+++ b/paste/auth/cookie.py
@@ -0,0 +1,229 @@
+# (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
+"""
+Cookie "Saved" Authentication
+
+This Authentication middleware saves the current REMOTE_USER, and any
+other environment variables specified, in a cookie so that it can be
+retrieved during the next request without requiring re-authentication.
+This uses a session cookie on the client side (so it goes away when the
+user closes their window) and does server-side expiration.
+
+ NOTE: If you use HTTPFound or other redirections; it is likely that
+ this module will not work unless it is _before_ the middleware
+ that converts the exception into a response. Therefore, in your
+ component stack, put this component darn near the top (before
+ the exception handler).
+
+According to the cookie specifications, RFC2068 and RFC2109, browsers
+should allow each domain at least 20 cookies; each one with a content
+size of at least 4k (4096 bytes). This is rather small; so one should
+be parsimonious in your cookie name/sizes.
+"""
+import sha, base64, random, time, string, warnings
+from paste.wsgilib import get_cookies
+
+def make_time(value):
+ """ return a human readable timestmp """
+ return time.strftime("%Y%m%d%H%M",time.gmtime(value))
+_signature_size = len(sha.sha("").digest())
+_header_size = _signature_size + len(make_time(time.time()))
+
+# build encode/decode functions to safely pack away values
+_encode = [('\\','\\x5c'),('"','\\x22'),('=','\\x3d'),(';','\\x3b')]
+_decode = [(v,k) for (k,v) in _encode]
+_decode.reverse()
+def encode(s, sublist = _encode):
+ return reduce((lambda a,(b,c): string.replace(a,b,c)), sublist, str(s))
+decode = lambda s: encode(s,_decode)
+
+class CookieTooLarge(RuntimeError):
+ def __init__(self, content, cookie):
+ RuntimeError.__init__("Signed cookie exceeds maximum size of 4096")
+ self.content = content
+ self.cookie = cookie
+
+class CookieSigner:
+ """
+ This class converts content into a timed and digitally signed
+ cookie, as well as having the facility to reverse this procedure.
+ If the cookie, after the content is encoded and signed exceeds the
+ maximum length (4096), then CookieTooLarge exception is raised.
+
+ The timeout of the cookie is handled on the server side for a few
+ reasons. First, if a 'Expires' directive is added to a cookie, then
+ the cookie becomes persistent (lasting even after the browser window
+ has closed). Second, the user's clock may be wrong (perhaps
+ intentionally). The timeout is specified in minutes; and expiration
+ date returned is rounded to one second.
+ """
+ def __init__(self, secret = None, timeout = None, maxlen = None):
+ self.timeout = timeout or 30
+ self.maxlen = maxlen or 4096
+ self.secret = secret or sha.sha(str(random.random()) +
+ str(time.time())).digest()
+
+ def sign(self, content):
+ """
+ Sign the content returning a valid cookie (that does not
+ need to be escaped and quoted). The expiration of this
+ cookie is handled server-side in the auth() function.
+ """
+ cookie = base64.b64encode(
+ sha.sha(content+self.secret).digest() +
+ make_time(time.time()+60*self.timeout) +
+ content).replace("/","_").replace("=","~")
+ if len(cookie) > self.maxlen:
+ raise CookieTooLarge(content,cookie)
+ return cookie
+
+ def auth(self,cookie):
+ """
+ Authenticate the cooke using the signature, verify that it
+ has not expired; and return the cookie's content
+ """
+ decode = base64.b64decode(
+ cookie.replace("_","/").replace("~","="))
+ signature = decode[:_signature_size]
+ expires = decode[_signature_size:_header_size]
+ content = decode[_header_size:]
+ if signature == sha.sha(content+self.secret).digest():
+ if int(expires) > int(make_time(time.time())):
+ return content
+ else:
+ # This is the normal case of an expired cookie; just
+ # don't bother doing anything here.
+ pass
+ else:
+ # This case can happen if the server is restarted with a
+ # different secret; or if the user's IP address changed
+ # due to a proxy. However, it could also be a break-in
+ # attempt -- so should it be reported?
+ pass
+
+class AuthCookieEnviron(list):
+ """
+ This object is a list of `environ` keys that were restored from or
+ will be added to the digially signed cookie. This object can be
+ accessed from an `environ` variable by using this module's name.
+
+ environ['paste.auth.cookie'].append('your.environ.variable')
+
+ This environment-specific object can also be used to access/configure
+ the base handler for all requests by using:
+
+ environ['paste.auth.cookie'].handler
+
+ """
+ def __init__(self, handler, scanlist):
+ list.__init__(self, scanlist)
+ self.handler = handler
+ def append(self, value):
+ if value in self:
+ return
+ list.append(self,str(value))
+
+class AuthCookieHandler:
+ """
+ This middleware uses cookies to stash-away a previously authenticated
+ user (and perhaps other variables) so that re-authentication is not
+ needed. This does not implement sessions; and therefore N servers
+ can be syncronized to accept the same saved authentication if they
+ all use the same cookie_name and secret.
+
+ By default, this handler scans the `environ` for the REMOTE_USER
+ key; if found, it is stored. It can be configured to scan other
+ `environ` keys as well -- but be careful not to exceed 2-3k (so that
+ the encoded and signed cookie does not exceed 4k). You can ask it
+ to handle other environment variables by doing:
+
+ environ['paste.auth.cookie'].append('your.environ.variable')
+
+ """
+ environ_name = 'paste.auth.cookie'
+ signer_class = CookieSigner
+ environ_class = AuthCookieEnviron
+
+ def __init__(self, application, cookie_name=None, secret=None,
+ timeout=None, maxlen=None, signer=None, scanlist = None):
+ if not signer:
+ signer = self.signer_class(secret,timeout,maxlen)
+ self.signer = signer
+ self.scanlist = scanlist or ('REMOTE_USER',)
+ self.application = application
+ self.cookie_name = cookie_name or 'PASTE_AUTH_COOKIE'
+
+ def __call__(self, environ, start_response):
+ if self.environ_name in environ:
+ raise AssertionError("AuthCookie already installed!")
+ scanlist = self.environ_class(self,self.scanlist)
+ jar = get_cookies(environ)
+ if jar.has_key(self.cookie_name):
+ content = self.signer.auth(jar[self.cookie_name].value)
+ if content:
+ for pair in content.split(";"):
+ (k,v) = pair.split("=")
+ k = decode(k)
+ if k not in scanlist:
+ scanlist.append(k)
+ if k in environ:
+ continue
+ environ[k] = decode(v)
+ if 'REMOTE_USER' == k:
+ environ['AUTH_TYPE'] = 'cookie'
+ environ[self.environ_name] = scanlist
+ if "paste.httpexceptions" in environ:
+ warnings.warn("Since paste.httpexceptions is hooked in your "
+ "processing chain before paste.auth.cookie, if an "
+ "HTTPRedirection is raised, the cookies this module sets "
+ "will not be included in your response.\n")
+
+ def response_hook(status, response_headers, exc_info=None):
+ """
+ Scan the environment for keys specified in the scanlist,
+ pack up their values, signs the content and issues a cookie.
+ """
+ scanlist = environ.get(self.environ_name)
+ assert scanlist and isinstance(scanlist,self.environ_class)
+ content = []
+ for k in scanlist:
+ v = environ.get(k,None)
+ if v is not None:
+ content.append("%s=%s" % (encode(k),encode(v)))
+ if content:
+ content = ";".join(content)
+ content = self.signer.sign(content)
+ cookie = '%s=%s; Path=/;' % (self.cookie_name, content)
+ if 'https' == environ['wsgi.url_scheme']:
+ cookie += ' secure;'
+ response_headers.append(('Set-Cookie',cookie))
+ return start_response(status, response_headers, exc_info)
+ return self.application(environ, response_hook)
+
+middleware = AuthCookieHandler
+
+__all__ = ['AuthCookieHandler']
+
+if '__main__' == __name__:
+ from paste.wsgilib import parse_querystring
+ def AuthStupidHandler(application):
+ def authstupid_application(environ, start_response):
+ args = dict(parse_querystring(environ))
+ user = args.get('user','')
+ if user:
+ environ['REMOTE_USER'] = user
+ environ['AUTH_TYPE'] = 'stupid'
+ test = args.get('test','')
+ if test:
+ environ['paste.auth.cookie.test'] = test
+ environ['paste.auth.cookie'].append('paste.auth.cookie.test')
+ return application(environ, start_response)
+ return authstupid_application
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(AuthCookieHandler(
+ HTTPExceptionHandler(
+ AuthStupidHandler(dump_environ))))
diff --git a/paste/auth/digest.py b/paste/auth/digest.py
new file mode 100644
index 0000000..598a103
--- /dev/null
+++ b/paste/auth/digest.py
@@ -0,0 +1,193 @@
+# (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
+"""
+HTTP Digest Authentication (RFC 2617)
+
+NOTE: This has not been audited by a security expert, please use
+ with caution (or better yet, report security holes).
+
+ At this time, this implementation does not provide for further
+ challenges, nor does it support Authentication-Info header. It
+ also uses md5, and an option to use sha would be a good thing.
+"""
+from paste.httpexceptions import HTTPUnauthorized
+import md5, time, random, urllib2
+
+def digest_password(username, realm, password):
+ """ Constructs the appropriate hashcode needed for HTTP Digest """
+ return md5.md5("%s:%s:%s" % (username,realm,password)).hexdigest()
+
+def response(challenge, realm, path, username, password):
+ """
+ Build an authorization response for a given challenge. This
+ implementation uses urllib2 to do the dirty work.
+ """
+ auth = urllib2.AbstractDigestAuthHandler()
+ auth.add_password(realm,path,username,password)
+ (token,challenge) = challenge.split(' ',1)
+ chal = urllib2.parse_keqv_list(urllib2.parse_http_list(challenge))
+ class FakeRequest:
+ def get_full_url(self):
+ return path
+ def has_data(self):
+ return False
+ def get_method(self):
+ return "GET"
+ get_selector = get_full_url
+ return "Digest %s" % auth.get_authorization(FakeRequest(), chal)
+
+class DigestAuthenticator:
+ """ Simple implementation of RFC 2617 - HTTP Digest Authentication """
+ def __init__(self, realm, userfunc):
+ """
+ realm is a globally unique URI, like tag:clarkevans.com,2005:bing
+ userfunc(realm, username) -> MD5('%s:%s:%s') % (user,realm,pass)
+ """
+ self.nonce = {} # list to prevent replay attacks
+ self.userfunc = userfunc
+ self.realm = realm
+
+ def build_authentication(self, stale = ''):
+ """ raises an authentication exception """
+ nonce = md5.md5("%s:%s" % (time.time(),random.random())).hexdigest()
+ opaque = md5.md5("%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()])
+ head = [("WWW-Authenticate", 'Digest %s' % head)]
+ return HTTPUnauthorized(headers=head)
+
+ def compute(self, ha1, username, response, method,
+ path, nonce, nc, cnonce, qop):
+ """ computes the authentication, raises error if unsuccessful """
+ if not ha1:
+ return self.build_authentication()
+ ha2 = md5.md5('%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.md5(chk).hexdigest():
+ if nonce in self.nonce:
+ del self.nonce[nonce]
+ return self.build_authentication()
+ pnc = self.nonce.get(nonce,'00000000')
+ if nc <= pnc:
+ if nonce in self.nonce:
+ del self.nonce[nonce]
+ return self.build_authentication(stale = True)
+ self.nonce[nonce] = nc
+ return username
+
+ def authenticate(self, authorization, path, method):
+ """ This function takes the value of the 'Authorization' header,
+ the method used (e.g. GET), and the path of the request
+ relative to the server. The function either returns an
+ authenticated user, or it raises an exception.
+ """
+ if not authorization:
+ return self.build_authentication()
+ (authmeth, auth) = authorization.split(" ",1)
+ if 'digest' != authmeth.lower():
+ return self.build_authentication()
+ amap = {}
+ 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 path
+ 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.build_authentication()
+ ha1 = self.userfunc(realm,username)
+ return self.compute(ha1, username, response, method, authpath,
+ nonce, nc, cnonce, qop)
+
+ __call__ = authenticate
+
+def AuthDigestHandler(application, realm, userfunc):
+ """
+ This middleware implements HTTP Digest authentication (RFC 2617) on
+ the incoming request. There are several possible outcomes:
+
+ 0. If the REMOTE_USER environment variable is already populated;
+ then this middleware is a no-op, and the request is passed along
+ to the application.
+
+ 1. If the HTTP_AUTHORIZATION header was not provided, then a
+ HTTPUnauthorized exception is raised containing the challenge.
+
+ 2. If the HTTP_AUTHORIZATION header specifies anything other
+ than digest; the REMOTE_USER is left unset and application
+ processing continues.
+
+ 3. If the response is malformed or or if the user's credientials
+ do not pass muster, another HTTPUnauthorized is raised.
+
+ 4. IF all goes well, and the user's credintials pass; then
+ REMOTE_USER environment variable is filled in and the
+ AUTH_TYPE is listed as 'digest'.
+
+ Besides the application to delegate requests, this middleware
+ requires two additional arguments:
+
+ realm:
+ This is a globally unique identifier used to indicate the
+ authority that is performing the authentication. The taguri
+ such as tag:yourdomain.com,2006 is sufficient.
+
+ userfunc:
+ This is a callback function which performs the actual
+ authentication; the signature of this callback is:
+
+ userfunc(realm, username) -> hashcode
+
+ This module provides a 'digest_password' helper function which
+ can help construct the hashcode; it is recommended that the
+ hashcode is stored in a database, not the user's actual password.
+ """
+ authenticator = DigestAuthenticator(realm, userfunc)
+ def digest_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if not username:
+ method = environ['REQUEST_METHOD']
+ fullpath = environ['SCRIPT_NAME'] + environ["PATH_INFO"]
+ authorization = environ.get('HTTP_AUTHORIZATION','')
+ result = authenticator(authorization, fullpath, method)
+ if isinstance(result, str):
+ environ['AUTH_TYPE'] = 'digest'
+ environ['REMOTE_USER'] = result
+ else:
+ return result.wsgi_application(environ, start_response)
+ return application(environ, start_response)
+ return digest_application
+
+middleware = AuthDigestHandler
+
+__all__ = ['digest_password', 'AuthDigestHandler' ]
+
+if '__main__' == __name__:
+ realm = 'tag:clarkevans.com,2005:digest'
+ def userfunc(realm, username):
+ return digest_password(username, realm, username)
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthDigestHandler(dump_environ, realm, userfunc)))
diff --git a/paste/auth/form.py b/paste/auth/form.py
new file mode 100644
index 0000000..b53952f
--- /dev/null
+++ b/paste/auth/form.py
@@ -0,0 +1,73 @@
+# (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
+"""
+HTTP Form Authentication
+
+"""
+from paste.wsgilib import parse_formvars, construct_url
+
+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>
+"""
+
+def AuthFormHandler(application, userfunc, login_page = None):
+ """ This causes a HTML form to be returned if REMOTE_USER has not
+ been provided. This is a really simple implementation, it
+ requires that the query arguments returned from the form have two
+ variables "username" and "password". These are then passed to
+ the userfunc; which should return True if authentication is granted.
+ """
+ login_page = login_page or template
+ def form_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if username:
+ return application(environ, start_response)
+ if 'POST' == environ['REQUEST_METHOD']:
+ formvars = parse_formvars(environ)
+ username = formvars.get('username')
+ password = formvars.get('password')
+ if username and password:
+ if userfunc(username,password):
+ environ['AUTH_TYPE'] = 'form'
+ environ['REMOTE_USER'] = username
+ environ['REQUEST_METHOD'] = 'GET'
+ del environ['paste.parsed_formvars']
+ return application(environ, start_response)
+ start_response("200 OK",(('Content-Type', 'text/html'),
+ ('Content-Length', len(login_page))))
+ if "%s" in login_page:
+ return [login_page % construct_url(environ) ]
+ return [login_page]
+ return form_application
+
+middleware = AuthFormHandler
+
+__all__ = ['AuthFormHandler']
+
+if '__main__' == __name__:
+ def userfunc(username, password):
+ return username == password
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ from paste.httpexceptions import *
+ from cookie import AuthCookieHandler
+ serve(HTTPExceptionHandler(
+ AuthCookieHandler(
+ AuthFormHandler(dump_environ, userfunc))))
diff --git a/paste/auth/multi.py b/paste/auth/multi.py
new file mode 100644
index 0000000..1b593ae
--- /dev/null
+++ b/paste/auth/multi.py
@@ -0,0 +1,81 @@
+# (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
+"""
+Multi Authentication
+
+In some environments, the choice of authentication method to be used
+depends upon the environment and is not "fixed". This middleware
+allows N authentication methods to be registered along with a goodness
+function which determines which method should be used.
+
+Strictly speaking this is not limited to authentication, but it is a
+common requirement in that domain; this is why it isn't named
+AuthMultiHandler (for now).
+"""
+
+class MultiHandler:
+ """ This middleware provides two othogonal facilities:
+ (a) a way to register any number of middlewares
+ (b) a way to register predicates which cause one of
+ the registered middlewares to be used
+ If none of the predicates returns True, then the
+ application is invoked directly without middleware
+ """
+ def __init__(self, application):
+ self.application = application
+ self.default = application
+ self.binding = {}
+ self.predicate = []
+ def add_method(self, name, factory, *args, **kwargs):
+ self.binding[name] = factory(self.application, *args, **kwargs)
+ def add_predicate(self, name, checker):
+ self.predicate.append((checker,self.binding[name]))
+ def set_default(self, name):
+ """
+ This method sets the default middleware to be executed,
+ if none of the rules apply.
+ """
+ self.default = self.binding[name]
+ def set_query_argument(self, name, key = '*authmeth', value = None):
+ """
+ This method indicates that the named middleware component should
+ be executed if the given key/value pair occurs in the query args.
+ """
+ lookfor = "%s=%s" % (key, value or name)
+ self.add_predicate(name,
+ lambda environ: lookfor in environ.get('QUERY_STRING',''))
+ def __call__(self, environ, start_response):
+ for (checker,binding) in self.predicate:
+ if checker(environ):
+ return binding(environ, start_response)
+ return self.default(environ, start_response)
+
+middleware = MultiHandler
+
+__all__ = ['MultiHandler']
+
+if '__main__' == __name__:
+ import basic, digest, cas, cookie, form
+ from paste.httpexceptions import *
+ from paste.wsgilib import dump_environ
+ from paste.util.baseserver import serve
+ multi = MultiHandler(dump_environ)
+ multi.add_method('basic',basic.middleware,
+ 'tag:clarkevans.com,2005:basic',
+ lambda n,p: n == p )
+ multi.set_query_argument('basic')
+ multi.add_method('digest',digest.middleware,
+ 'tag:clarkevans.com,2005:digest',
+ lambda r,u: digest.digest_password(u,r,u))
+ multi.set_query_argument('digest')
+ multi.add_method('form',lambda ap: cookie.middleware(
+ form.middleware(ap,
+ lambda n,p: n == p)))
+ multi.set_query_argument('form')
+ #authority = "https://secure.its.yale.edu/cas/servlet/"
+ #multi.add_method('cas',lambda ap: cookie.middleware(
+ # cas.middleware(ap,authority)))
+ #multi.set_default('cas')
+ serve(HTTPExceptionHandler(multi))
diff --git a/paste/debug/__init__.py b/paste/debug/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/paste/debug/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/paste/debug/testserver.py b/paste/debug/testserver.py
new file mode 100755
index 0000000..832142c
--- /dev/null
+++ b/paste/debug/testserver.py
@@ -0,0 +1,97 @@
+# (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 Test Server
+
+This builds upon paste.util.baseserver to customize it for regressions
+where using raw_interactive won't do.
+
+
+"""
+import time
+from paste.util.testserver import *
+
+class WSGIRegressionServer(WSGIServer):
+ """
+ A threaded WSGIServer for use in regression testing. To use this
+ module, call serve(application, regression=True), and then call
+ server.accept() to let it handle one request. When finished, use
+ server.stop() to shutdown the server. Note that all pending requests
+ are processed before the server shuts down.
+ """
+ defaulttimeout = 10
+ def __init__ (self, *args, **kwargs):
+ WSGIServer.__init__(self, *args, **kwargs)
+ self.stopping = []
+ self.pending = []
+ self.timeout = self.defaulttimeout
+ # this is a local connection, be quick
+ self.socket.settimeout(2)
+ def serve_forever(self):
+ from threading import Thread
+ thread = Thread(target=self.serve_pending)
+ thread.start()
+ def reset_expires(self):
+ if self.timeout:
+ self.expires = time.time() + self.timeout
+ def close_request(self, *args, **kwargs):
+ WSGIServer.close_request(self, *args, **kwargs)
+ self.pending.pop()
+ self.reset_expires()
+ def serve_pending(self):
+ self.reset_expires()
+ while not self.stopping or self.pending:
+ now = time.time()
+ if now > self.expires and self.timeout:
+ # note regression test doesn't handle exceptions in
+ # threads very well; so we just print and exit
+ print "\nWARNING: WSGIRegressionServer timeout exceeded\n"
+ break
+ if self.pending:
+ self.handle_request()
+ time.sleep(.1)
+ def stop(self):
+ """ stop the server (called from tester's thread) """
+ self.stopping.append(True)
+ def accept(self, count = 1):
+ """ accept another request (called from tester's thread) """
+ assert not self.stopping
+ [self.pending.append(True) for x in range(count)]
+
+def serve(application, host=None, port=None, handler=None):
+ server = WSGIRegressionServer(application,host,port,handler)
+ print "serving on %s:%s" % server.server_address
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ # allow CTRL+C to shutdown
+ pass
+ return server
+
+if __name__ == '__main__':
+ import urllib
+ from paste.wsgilib import dump_environ
+ server = serve(dump_environ)
+ baseuri = ("http://%s:%s" % server.server_address)
+
+ def fetch(path):
+ # tell the server to humor exactly one more request
+ server.accept(1)
+ # not needed; but this is what you do if the server
+ # may not respond in a resonable time period
+ import socket
+ socket.setdefaulttimeout(5)
+ # build a uri, fetch and return
+ return urllib.urlopen(baseuri + path).read()
+
+ assert "PATH_INFO: /foo" in fetch("/foo")
+ assert "PATH_INFO: /womble" in fetch("/womble")
+
+ # ok, let's make one more final request...
+ server.accept(1)
+ # and then schedule a stop()
+ server.stop()
+ # and then... fetch it...
+ urllib.urlopen(baseuri)
diff --git a/paste/exceptions/errormiddleware.py b/paste/exceptions/errormiddleware.py
index 0a21cf0..275bfa4 100644
--- a/paste/exceptions/errormiddleware.py
+++ b/paste/exceptions/errormiddleware.py
@@ -31,46 +31,51 @@ class ErrorMiddleware(object):
error_caching_wsgi_app = ErrorMiddleware(wsgi_app)
- By setting 'paste.throw_errors' in the request environment to a
- true value, this middleware is disabled. This can be useful in a
- testing environment where you don't want errors to be caught and
- transformed.
-
Settings:
``debug``:
If true, then tracebacks will be shown in the browser.
``error_email``:
- An email address (or list of addresses) to send exception reports
- to.
+ an email address (or list of addresses) to send exception
+ reports to
``error_log``:
- A filename to append tracebacks to.
+ a filename to append tracebacks to
``show_exceptions_in_wsgi_errors``:
- If true, then errors will be printed to ``wsgi.errors`` (frequently
- a server error log, or stderr).
+ If true, then errors will be printed to ``wsgi.errors``
+ (frequently a server error log, or stderr).
``from_address``, ``smtp_server``, ``error_subject_prefix``:
- Variables to control the emailed exception reports.
+ variables to control the emailed exception reports
``error_message``:
When debug mode is off, the error message to show to users.
``xmlhttp_key``:
-
When this key (default ``_``) is in the request GET variables
(not POST!), expect that this is an XMLHttpRequest, and the
response should be more minimal; it should not be a complete
HTML page.
- This also looks for a special key ``'paste.expected_exceptions``,
- which should be a list of exception classes. When an exception is
- raised, if it is found in this list then it will be re-raised
- instead of being caught. This should generally be set by
- middleware that may (but probably shouldn't be) installed above
- this middleware, and wants to get certain exceptions.
+ Environment Configuration:
+
+ ``paste.throw_errors``:
+ If this setting in the request environment is true, then this
+ middleware is disabled. This can be useful in a testing situation
+ where you don't want errors to be caught and transformed.
+
+ ``paste.expected_exceptions``:
+ When this middleware encounters an exception listed in this
+ environment variable and when the ``start_response`` has not
+ yet occurred, the exception will be re-raised instead of being
+ caught. This should generally be set by middleware that may
+ (but probably shouldn't be) installed above this middleware,
+ and wants to get certain exceptions. Exceptions raised after
+ ``start_response`` have been called are always caught since
+ by definition they are no longer expected.
+
"""
def __init__(self, application, global_conf=None,
@@ -124,28 +129,31 @@ class ErrorMiddleware(object):
environ['paste.throw_errors'] = True
def detect_start_response(status, headers, exc_info=None):
- try:
- return start_response(status, headers, exc_info)
- except:
- raise
- else:
- started.append(True)
+ started.append(True)
+ return start_response(status, headers, exc_info)
+
try:
__traceback_supplement__ = Supplement, self, environ
app_iter = self.application(environ, detect_start_response)
return self.catching_iter(app_iter, environ)
except:
exc_info = sys.exc_info()
- for expected in environ.get('paste.expected_exceptions', []):
- if issubclass(exc_info[0], expected):
- raise
- if not started:
- start_response('500 Internal Server Error',
- [('content-type', 'text/html')],
- exc_info)
- # @@: it would be nice to deal with bad content types here
- response = self.exception_handler(exc_info, environ)
- return [response]
+ try:
+ if not started:
+ # Only delegate expected exceptions if the response
+ # has not been started.
+ for expect in environ.get('paste.expected_exceptions', []):
+ if issubclass(exc_info[0], expect):
+ raise
+ start_response('500 Internal Server Error',
+ [('content-type', 'text/html')],
+ exc_info)
+ # @@: it would be nice to deal with bad content types here
+ response = self.exception_handler(exc_info, environ)
+ return [response]
+ finally:
+ # clean up locals...
+ exc_info = None
def catching_iter(self, app_iter, environ):
__traceback_supplement__ = Supplement, self, environ
diff --git a/paste/httpexceptions.py b/paste/httpexceptions.py
index 2378b2c..c38618c 100644
--- a/paste/httpexceptions.py
+++ b/paste/httpexceptions.py
@@ -1,57 +1,287 @@
-# (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
"""
-WSGI middleware
-
-Processes Python exceptions that relate to HTTP exceptions. This
-defines a set of extensions, all subclasses of HTTPException, and a
-middleware (`middleware`) that catches these exceptions and turns them
-into proper responses.
-
-Note: if ``'paste.debug_suppress_httpexceptions'`` is in the request
-and is true, then this middleware will be skipped.
+HTTP Exception Middleware
+
+This module processes Python exceptions that relate to HTTP exceptions
+by defining a set of exceptions, all subclasses of HTTPException, and a
+request handler (`middleware`) that catches these exceptions and turns
+them into proper responses.
+
+This module defines exceptions according to RFC 2068 [1]: codes with
+100-300 are not really errors; 400's are client errors, and 500's are
+server errors. According to the WSGI specification [2], the application
+can call ``start_response`` more then once only under two conditions:
+(a) the response has not yet been sent, or (b) if the second and
+subsequent invocations of ``start_response`` have a valid ``exc_info``
+argument obtained from ``sys.exc_info()``. The WSGI specification then
+requires the server or gateway to handle the case where content has been
+sent and then an exception was encountered.
+
+Exceptions in the 5xx range and those raised after ``start_response``
+has been called are treated as serious errors and the ``exc_info`` is
+filled-in with information needed for a lower level module to generate a
+stack trace and log information.
+
+References:
+[1] http://www.python.org/peps/pep-0333.html#error-handling
+[2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
+
+Exception
+ HTTPException
+ HTTPRedirection
+ # 300 Multiple Choices
+ 301 - HTTPMovedPermanently
+ 302 - HTTPFound
+ 303 - HTTPSeeOther
+ 304 - HTTPNotModified
+ 305 - HTTPUseProxy
+ # 306 Unused
+ 307 - HTTPTemporaryRedirect
+ HTTPError
+ HTTPClientError
+ 400 - HTTPBadRequest
+ 401 - HTTPUnauthorized
+ # 402 Payment Required
+ 403 - HTTPForbidden
+ 404 - HTTPNotFound
+ 405 - HTTPMethodNotAllowed
+ 406 - HTTPNotAcceptable
+ # 407 Proxy Authentication Required
+ # 408 Request Timeout
+ 409 - HTTPConfict
+ 410 - HTTPGone
+ 411 - HTTPLengthRequired
+ 412 - HTTPPreconditionFailed
+ 413 - HTTPRequestEntityTooLarge
+ 414 - HTTPRequestURITooLong
+ 415 - HTTPUnsupportedMediaType
+ 416 - HTTPRequestRangeNotSatisfiable
+ 417 - HTTPExpectationFailed
+ HTTPServerError
+ 500 - HTTPInternalServerError
+ 501 - HTTPNotImplemented
+ 502 - HTTPBadGateway
+ 503 - HTTPServiceUnavailable
+ 504 - HTTPGatewayTimeout
+ 505 - HTTPVersionNotSupported
"""
import types
+import sys
+from wsgilib import has_header, header_value
+from util.quoting import strip_html, html_quote
class HTTPException(Exception):
+ """
+ Base class for all HTTP exceptions
+
+ This encapsulates an HTTP response that interrupts normal application
+ flow; but one which is not necessarly an error condition. For
+ example, codes in the 300's are exceptions in that they interrupt
+ normal processing; however, they are not considered errors.
+
+ This class is complicated by 4 factors:
+
+ 1. The content given to the exception may either be plain-text or
+ as html-text.
+
+ 2. The template may want to have string-substitutions taken from
+ the current ``environ`` or values from incoming headers. This
+ is especially troublesome due to case sensitivity.
+
+ 3. The final output may either be text/plain or text/html
+ mime-type as requested by the client application.
+
+ 4. Each exception has a default explanation, but those who
+ raise exceptions may want to provide additional detail.
+
+ Attributes:
+
+ ``code``
+ the HTTP status code for the exception
+
+ ``title``
+ remainder of the status line (stuff after the code)
+
+ ``explanation``
+ a plain-text explanation of the error message that is
+ not subject to environment or header substitutions;
+ it is accessable in the template via %(explanation)s
+
+ ``detail``
+ a plain-text message customization that is not subject
+ to environment or header substutions; accessable in
+ the template via %(detail)s
+
+ ``template``
+ a content fragment (in HTML) used for environment and
+ header substution; the default template includes both
+ the explanation and further detail provided in the
+ message
+
+ ``required_headers``
+ a sequence of headers which are required for proper
+ construction of the exception
+
+ Parameters:
+
+ ``detail`` a plain-text override of the default ``detail``
+ ``headers`` a list of (k,v) header pairs
+ ``comment`` a plain-text additional information which is
+ usually stripped/hidden for end-users
+
+ To override the template (which is HTML content) or the plain-text
+ explanation, one must subclass the given exception; or customize it
+ after it has been created. This particular breakdown of a message
+ into explanation, detail and template allows both the creation of
+ plain-text and html messages for various clients as well as
+ error-free substution of environment variables and headers.
+ """
+
code = None
title = None
- message = None
- # @@: not currently used:
+ explanation = ''
+ detail = ''
+ comment = ''
+ template = "%(explanation)s\n<br/>%(detail)s\n<!-- %(comment)s -->"
required_headers = ()
- def __init__(self, message=None, headers=None):
- self.headers = headers
- if message is not None:
- self.message = message
- Exception.__init__(self, self.message)
+ server_name = 'WSGI server'
+
+ def __init__(self, detail=None, headers=None, comment=None):
+ assert self.code, "Do not directly instantiate abstract exceptions."
+ assert isinstance(headers, (type(None), list))
+ assert isinstance(detail, (type(None), basestring))
+ assert isinstance(comment, (type(None), basestring))
+ self.headers = headers or tuple()
+ for req in self.required_headers:
+ assert has_header(headers, req)
+ if detail is not None:
+ self.detail = detail
+ if comment is not None:
+ self.comment = comment
+ Exception.__init__(self,"%s %s\n%s\n%s\n" % (
+ self.code, self.title, self.explanation, self.detail))
+
+ def make_body(self, environ, template, escfunc):
+ args = {'explanation': escfunc(self.explanation),
+ 'detail': escfunc(self.detail),
+ 'comment': escfunc(self.comment)}
+ if HTTPException.template == self.template:
+ return template % args
+ for (k, v) in environ.items():
+ args[k] = escfunc(v)
+ if self.headers:
+ for (k, v) in self.headers:
+ args[k.lower()] = escfunc(v)
+ return template % args
+
+ def plain(self, environ):
+ """ text/plain representation of the exception """
+ noop = lambda _: _
+ body = self.make_body(environ, strip_html(self.template), noop)
+ return ('%s %s\n%s\n' % (self.code, self.title, body))
def html(self, environ):
- message = self.message
- args = environ.copy()
- if self.headers:
- args.update(self.headers)
- message = message % args
+ """ text/html representation of the exception """
+ body = self.make_body(environ, self.template, html_quote)
return ('<html><head><title>%(title)s</title></head>\n'
'<body>\n'
'<h1>%(title)s</h1>\n'
- '<p>%(message)s</p>\n'
+ '<p>%(body)s</p>\n'
'<hr noshade>\n'
- '<div align="right">WSGI server</div>\n'
+ '<div align="right">%(server)s</div>\n'
'</body></html>\n'
% {'title': self.title,
'code': self.code,
- 'message': message})
+ 'server': self.server_name,
+ 'body': body})
+
+ def wsgi_application(self, environ, start_response, exc_info=None):
+ """
+ This exception as a WSGI application
+ """
+ if 'html' in environ.get('HTTP_ACCEPT',''):
+ headers = {'content-type': 'text/html'}
+ content = self.html(environ)
+ else:
+ headers = {'content-type': 'text/plain'}
+ content = self.plain(environ)
+ if self.headers:
+ headers.update(self.headers)
+ if isinstance(content, unicode):
+ content = content.encode('utf8')
+ headers['content_type'] += '; charset=utf8'
+ start_response('%s %s' % (self.code, self.title),
+ headers.items(),
+ exc_info)
+ yield content
+
def __repr__(self):
return '<%s %s; code=%s>' % (self.__class__.__name__,
self.title, self.code)
-class _HTTPMove(HTTPException):
+class HTTPError(HTTPException):
+ """
+ This is an exception which indicates that an error has occured,
+ and that any work in progress should not be committed. These are
+ typically results in the 400's and 500's.
+ """
+
+#
+# 3xx Redirection
+#
+# This class of status code indicates that further action needs to be
+# taken by the user agent in order to fulfill the request. The action
+# required MAY be carried out by the user agent without interaction with
+# the user if and only if the method used in the second request is GET or
+# HEAD. A client SHOULD detect infinite redirection loops, since such
+# loops generate network traffic for each redirection.
+#
+
+class HTTPRedirection(HTTPException):
+ """
+ This is an abstract base class for 3xx redirection. It indicates
+ that further action needs to be taken by the user agent in order
+ to fulfill the request. It does not necessarly signal an error
+ condition.
+ """
+
+class _HTTPMove(HTTPRedirection):
+ """
+ Base class for redirections which require a Location field.
+
+ Since a 'Location' header is a required attribute of 301, 302, 303,
+ 305 and 307 (but not 304), this base class provides the mechanics to
+ make this easy. While this has the same parameters as HTTPException,
+ if a location is not provided in the headers; it is assumed that the
+ detail _is_ the location (this for backward compatibility, otherwise
+ we'd add a new attribute).
+ """
required_headers = ('location',)
- message = ('The resource has been moved to <a href="%(location)s">'
- '%(location)s</a>; you should be redirected automatically.')
+ explanation = 'The resource has been moved to'
+ template = (
+ '%(explanation)s <a href="%(location)s">%(location)s</a>;\n'
+ 'you should be redirected automatically.\n'
+ '%(detail)s\n')
+
+ def __init__(self, detail=None, headers=None, comment=None):
+ assert isinstance(headers, (type(None), list))
+ headers = headers or []
+ location = header_value(headers,'location')
+ if not location:
+ location = detail
+ detail = ''
+ headers.append(('location', location))
+ assert location, ("HTTPRedirection specified neither a "
+ "location in the headers nor did it "
+ "provide a detail argument.")
+ HTTPRedirection.__init__(self, location, headers, comment)
+ if detail is not None:
+ self.detail = detail
class HTTPMovedPermanently(_HTTPMove):
code = 301
@@ -60,6 +290,7 @@ class HTTPMovedPermanently(_HTTPMove):
class HTTPFound(_HTTPMove):
code = 302
title = 'Found'
+ explanation = 'The resource was found at'
# This one is safe after a POST (the redirected location will be
# retrieved with GET):
@@ -67,142 +298,206 @@ class HTTPSeeOther(_HTTPMove):
code = 303
title = 'See Other'
-class HTTPNotModified(HTTPException):
+class HTTPNotModified(HTTPRedirection):
# @@: but not always (HTTP section 14.18.1)...?
required_headers = ('date',)
code = 304
title = 'Not Modified'
message = ''
# @@: should include date header, optionally other headers
+ # @@: should not return a content body
+ def plain(self, environ):
+ return ''
+ def html(self, environ):
+ """ text/html representation of the exception """
+ return ''
class HTTPUseProxy(_HTTPMove):
# @@: OK, not a move, but looks a little like one
code = 305
title = 'Use Proxy'
- message = ('This resource must be accessed through the proxy located '
- 'at <a href="%(location)s">%(location)s</a>')
+ explanation = (
+ 'The resource must be accessed through a proxy '
+ 'located at')
class HTTPTemporaryRedirect(_HTTPMove):
code = 307
title = 'Temporary Redirect'
-class HTTPBadRequest(HTTPException):
+#
+# 4xx Client Error
+#
+# The 4xx class of status code is intended for cases in which the client
+# seems to have erred. Except when responding to a HEAD request, the
+# server SHOULD include an entity containing an explanation of the error
+# situation, and whether it is a temporary or permanent condition. These
+# status codes are applicable to any request method. User agents SHOULD
+# display any included entity to the user.
+#
+
+class HTTPClientError(HTTPError):
+ """
+ This is an error condition in which the client is presumed to be
+ in-error. This is an expected problem, and thus is not considered
+ a bug. A server-side traceback is not warranted. Unless specialized,
+ this is a '400 Bad Request'
+ """
code = 400
title = 'Bad Request'
- message = ('The server could not understand your request')
+ explanation = 'The server could not understand your request.'
+
+HTTPBadRequest = HTTPClientError
-class HTTPUnauthorized(HTTPException):
+class HTTPUnauthorized(HTTPClientError):
required_headers = ('WWW-Authenticate',)
code = 401
title = 'Unauthorized'
- # @@: should require WWW-Authenticate header
- message = ('Authorization is required to access this resource; '
- 'you must login.')
+ explanation = (
+ 'This server could not verify that you are authorized to\n'
+ 'access the document you requested. Either you supplied the\n'
+ 'wrong credentials (e.g., bad password), or your browser\n'
+ 'does not understand how to supply the credentials required.\n')
-class HTTPForbidden(HTTPException):
+class HTTPForbidden(HTTPClientError):
code = 403
title = 'Forbidden'
- message = ('Access was denied to this resource.')
+ explanation = ('Access was denied to this resource.')
-class HTTPNotFound(HTTPException):
+class HTTPNotFound(HTTPClientError):
code = 404
title = 'Not Found'
- message = ('The resource could not be found.')
+ explanation = ('The resource could not be found.')
-class HTTPMethodNotAllowed(HTTPException):
+class HTTPMethodNotAllowed(HTTPClientError):
required_headers = ('allowed',)
code = 405
title = 'Method Not Allowed'
- message = ('The method %(REQUEST_METHOD)s is not allowed for this '
- 'resource.')
+ # override template since we need an environment variable
+ template = ('The method %(REQUEST_METHOD)s is not allowed for '
+ 'this resource.\n%(detail)s')
-class HTTPNotAcceptable(HTTPException):
+class HTTPNotAcceptable(HTTPClientError):
code = 406
title = 'Not Acceptable'
- message = ('The resource could not be generated that was acceptable '
- 'to your browser (content of type %(HTTP_ACCEPT)s).')
+ # override template since we need an environment variable
+ template = ('The resource could not be generated that was '
+ 'acceptable to your browser (content\nof type '
+ '%(HTTP_ACCEPT)s).\n%(detail)s')
+<<<<<<< .working
class HTTPConflict(HTTPException):
+=======
+class HTTPConflict(HTTPClientError):
+>>>>>>> .merge-right.r4008
code = 409
title = 'Conflict'
- message = ('There was a conflict when trying to complete your '
- 'request.')
+ explanation = ('There was a conflict when trying to complete '
+ 'your request.')
-class HTTPGone(HTTPException):
+class HTTPGone(HTTPClientError):
code = 410
title = 'Gone'
- message = ('This resource is no longer available. No forwarding '
- 'address is aavailable.')
+ explanation = ('This resource is no longer available. No forwarding '
+ 'address is given.')
-class HTTPLengthRequired(HTTPException):
+class HTTPLengthRequired(HTTPClientError):
code = 411
title = 'Length Required'
- message = ('Content-Length header required.')
+ explanation = ('Content-Length header required.')
-class HTTPPreconditionFailed(HTTPException):
+class HTTPPreconditionFailed(HTTPClientError):
code = 412
title = 'Precondition Failed'
- message = ('Request precondition failed.')
+ explanation = ('Request precondition failed.')
-class HTTPRequestEntityTooLarge(HTTPException):
+class HTTPRequestEntityTooLarge(HTTPClientError):
code = 413
title = 'Request Entity Too Large'
- message = ('The body of your request was too large for this server.')
+ explanation = ('The body of your request was too large for this server.')
-class HTTPRequestURITooLong(HTTPException):
+class HTTPRequestURITooLong(HTTPClientError):
code = 414
title = 'Request-URI Too Long'
- message = ('The request URI was too long for this server.')
+ explanation = ('The request URI was too long for this server.')
-class HTTPUnsupportedMediaType(HTTPException):
+class HTTPUnsupportedMediaType(HTTPClientError):
code = 415
title = 'Unsupported Media Type'
- message = ('The request media type %(CONTENT_TYPE)s is not '
- 'supported by this server.')
+ # override template since we need an environment variable
+ template = ('The request media type %(CONTENT_TYPE)s is not '
+ 'supported by this server.\n%(detail)s')
-class HTTPRequestRangeNotSatisfiable(HTTPException):
+class HTTPRequestRangeNotSatisfiable(HTTPClientError):
code = 416
title = 'Request Range Not Satisfiable'
- message = ('The Range requested is not available.')
+ explanation = ('The Range requested is not available.')
-class HTTPExpectationFailed(HTTPException):
+class HTTPExpectationFailed(HTTPClientError):
code = 417
title = 'Expectation Failed'
- message = ('Expectation failed.')
-
-class HTTPServerError(HTTPException):
+ explanation = ('Expectation failed.')
+
+#
+# 5xx Server Error
+#
+# Response status codes beginning with the digit "5" indicate cases in
+# which the server is aware that it has erred or is incapable of
+# performing the request. Except when responding to a HEAD request, the
+# server SHOULD include an entity containing an explanation of the error
+# situation, and whether it is a temporary or permanent condition. User
+# agents SHOULD display any included entity to the user. These response
+# codes are applicable to any request method.
+#
+
+class HTTPServerError(HTTPError):
+ """
+ This is an error condition in which the server is presumed to be
+ in-error. This is usually unexpected, and thus requires a traceback;
+ ideally, opening a support ticket for the customer. Unless specialized,
+ this is a '500 Internal Server Error'
+ """
code = 500
title = 'Internal Server Error'
- message = ('An internal server error occurred.')
+ explanation = ('An internal server error occurred.')
+<<<<<<< .working
class HTTPNotImplemented(HTTPException):
code = 501
+=======
+HTTPInternalServerError = HTTPServerError
+
+class HTTPNotImplemented(HTTPServerError):
+ code = 501
+>>>>>>> .merge-right.r4008
title = 'Not Implemented'
- message = ('The request method %(REQUEST_METHOD)s is not implemented '
- 'for this server.')
+ # override template since we need an environment variable
+ template = ('The request method %(REQUEST_METHOD)s is not implemented '
+ 'for this server.\n%(detail)s')
-class HTTPBadGateway(HTTPException):
+class HTTPBadGateway(HTTPServerError):
code = 502
title = 'Bad Gateway'
- message = ('Bad gateway.')
+ explanation = ('Bad gateway.')
-class HTTPServiceUnavailable(HTTPException):
+class HTTPServiceUnavailable(HTTPServerError):
code = 503
title = 'Service Unavailable'
- message = ('The server is currently unavailable. Please try again '
- 'at a later time.')
+ explanation = ('The server is currently unavailable. '
+ 'Please try again at a later time.')
-class HTTPGatewayTimeout(HTTPException):
+class HTTPGatewayTimeout(HTTPServerError):
code = 504
title = 'Gateway Timeout'
- message = ('The gateway has timed out.')
+ explanation = ('The gateway has timed out.')
-class HTTPHttpVersionNotSupported(HTTPException):
+class HTTPVersionNotSupported(HTTPServerError):
code = 505
title = 'HTTP Version Not Supported'
- message = ('The HTTP version is not supported.')
+ explanation = ('The HTTP version is not supported.')
+
+# abstract HTTP related exceptions
+__all__ = ['HTTPException', 'HTTPRedirection', 'HTTPError' ]
-__all__ = []
_exceptions = {}
for name, value in globals().items():
if (isinstance(value, (type, types.ClassType)) and
@@ -210,6 +505,7 @@ for name, value in globals().items():
value.code):
_exceptions[value.code] = value
__all__.append(name)
+
def get_exception(code):
return _exceptions[code]
@@ -217,40 +513,61 @@ def get_exception(code):
## Middleware implementation:
############################################################
-def middleware(application, global_conf=None):
-
+class HTTPExceptionHandler:
"""
This middleware catches any exceptions (which are subclasses of
- `HTTPException`) and turns them into proper HTTP responses.
+ ``HTTPException``) and turns them into proper HTTP responses.
+
+ Attributes:
+
+ ``warning_level``
+ This attribute determines for what exceptions a stack
+ trace is kept for lower level reporting; by default, it
+ only keeps stack trace for 5xx, HTTPServerError exceptions.
+ To keep a stack trace for 4xx, HTTPClientError exceptions,
+ set this to 400.
+
+
+
+ Note if the headers have already been sent, the stack trace is
+ always maintained as this indicates a programming error.
+
"""
- def start_application(environ, start_response):
- environ.setdefault('paste.expected_exceptions', []).append(
- HTTPException)
- app_started = []
- def checked_start_response(status, headers, exc_info=None):
- app_started.append(None)
+ def __init__(self, application, global_conf=None, warning_level=None):
+ assert not warning_level or ( warning_level > 99 and
+ warning_level < 600)
+ self.warning_level = warning_level or 500
+ self.application = application
+
+ def __call__(self, environ, start_response):
+ environ['paste.httpexceptions'] = self
+ environ.setdefault('paste.expected_exceptions',
+ []).append(HTTPException)
+ headers_sent = []
+ def httpexce_start_response(status, headers, exc_info = None):
+ headers_sent.append(True)
return start_response(status, headers, exc_info)
-
try:
- v = application(environ, checked_start_response)
- environ['paste.expected_exceptions'].remove(HTTPException)
- return v
+ result = self.application(environ, httpexce_start_response)
+ for chunk in result:
+ yield chunk
except HTTPException, e:
if environ.get('paste.debug_suppress_httpexceptions'):
raise
- if app_started:
- # They've already started the response, so we can't
- # do the right thing anymore.
- raise
- headers = {'content-type': 'text/html'}
- if e.headers:
- headers.update(e.headers)
- start_response('%s %s' % (e.code, e.title),
- headers.items())
- return [e.html(environ)]
-
- return start_application
-
-__all__.extend(['middleware', 'get_exception'])
+ if headers_sent or e.code >= self.warning_level:
+ exc_info = sys.exc_info()
+ else:
+ exc_info = None
+ try:
+ result = e.wsgi_application(environ, start_response, exc_info)
+ finally:
+ # clean up
+ exc_info = None
+ for chunk in result:
+ yield chunk
+
+middleware = HTTPExceptionHandler
+
+__all__.extend(['HTTPExceptionHandler', 'get_exception'])
diff --git a/paste/lint.py b/paste/lint.py
index b3bee02..916f1af 100644
--- a/paste/lint.py
+++ b/paste/lint.py
@@ -192,7 +192,8 @@ def check_environ(environ):
check_errors(environ['wsgi.errors'])
# @@: these need filling out:
- assert environ['REQUEST_METHOD'] in ('GET', 'HEAD', 'POST'), (
+ assert environ['REQUEST_METHOD'] in ('GET', 'HEAD', 'POST',
+ 'OPTIONS','PUT','DELETE','TRACE'), (
"Unknown REQUEST_METHOD: %r" % environ['REQUEST_METHOD'])
assert (not environ.get('SCRIPT_NAME')
@@ -262,16 +263,17 @@ def check_headers(headers):
def check_content_type(status, headers):
code = int(status.split(None, 1)[0])
- if code == 204:
- # 204 No Content is the only code where there's no body,
- # and so it doesn't need a content-type header.
- # @@: Not 100% sure this is the only case where a content-type
- # header can be left out
- return
+ # @@: need one more person to verify this interpretation of RFC 2616
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ NO_MESSAGE_BODY = (204,304)
for name, value in headers:
if name.lower() == 'content-type':
- return
- assert 0, "No Content-Type header found in headers (%s)" % headers
+ if code not in NO_MESSAGE_BODY:
+ return
+ assert 0, (("Content-Type header found in a %s response, "
+ "which must not return content.") % code)
+ if code not in NO_MESSAGE_BODY:
+ assert 0, "No Content-Type header found in headers (%s)" % headers
def check_exc_info(exc_info):
assert not exc_info or type(exc_info) is type(()), (
diff --git a/paste/request.py b/paste/request.py
new file mode 100644
index 0000000..d09dfee
--- /dev/null
+++ b/paste/request.py
@@ -0,0 +1,214 @@
+# (c) 2005 Ian Bicking 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
+"""
+This module provides helper routines with work directly on a WSGI
+environment to solve common requirements.
+
+ * get_cookies(environ)
+ * parse_querystring(environ)
+ * parse_formvars(environ, all_as_list=False, include_get_vars=True)
+ * construct_url(environ, with_query_string=True, with_path_info=True,
+ script_name=None, path_info=None, querystring=None)
+ * path_info_split(path_info)
+ * path_info_pop(environ)
+
+"""
+import cgi, string
+from Cookie import SimpleCookie
+
+__all__ = ['get_cookies', 'parse_querystring', 'parse_formvars',
+ 'construct_url', 'path_info_split', 'path_info_pop']
+
+def get_cookies(environ):
+ """
+ Gets a cookie object (which is a dictionary-like object) from the
+ request environment; caches this value in case get_cookies is
+ called again for the same request.
+ """
+ header = environ.get('HTTP_COOKIE', '')
+ if environ.has_key('paste.cookies'):
+ cookies, check_header = environ['paste.cookies']
+ if check_header == header:
+ return cookies
+ cookies = SimpleCookie()
+ cookies.load(header)
+ environ['paste.cookies'] = (cookies, header)
+ return cookies
+
+def parse_querystring(environ):
+ """
+ Parses a query string into a list like ``[(name, value)]``.
+ Caches this value in case parse_querystring is called again
+ for the same request.
+
+ You can pass the result to ``dict()``, but be aware that keys that
+ appear multiple times will be lost (only the last value will be
+ preserved).
+ """
+ source = environ.get('QUERY_STRING', '')
+ if not source:
+ return []
+ if 'paste.parsed_querystring' in environ:
+ parsed, check_source = environ['paste.parsed_querystring']
+ if check_source == source:
+ return parsed
+ parsed = cgi.parse_qsl(source, keep_blank_values=True,
+ strict_parsing=False)
+ environ['paste.parsed_querystring'] = (parsed, source)
+ return parsed
+
+def parse_formvars(environ, all_as_list=False, include_get_vars=True):
+ """
+ Parses the request, returning a dictionary of the keys.
+
+ If ``all_as_list`` is true, then all values will be lists. If
+ not, then only values that show up multiple times will be lists.
+
+ If ``include_get_vars`` is true and this was a POST request, then
+ GET (query string) variables will also be folded into the
+ dictionary.
+
+ All values should be strings, except for file uploads which are
+ left as FieldStorage instances.
+ """
+ source = (environ.get('QUERY_STRING', ''),
+ environ['wsgi.input'], environ['REQUEST_METHOD'],
+ all_as_list, include_get_vars)
+ if 'paste.parsed_formvars' in environ:
+ parsed, check_source = environ['paste.parsed_formvars']
+ if check_source == source:
+ return parsed
+ fs = cgi.FieldStorage(fp=environ['wsgi.input'],
+ environ=environ,
+ keep_blank_values=1)
+ formvars = {}
+ for name in fs.keys():
+ values = fs[name]
+ if not isinstance(values, list):
+ values = [values]
+ for value in values:
+ if not value.filename:
+ value = value.value
+ if name in formvars:
+ if isinstance(formvars[name], list):
+ formvars[name].append(value)
+ else:
+ formvars[name] = [formvars[name], value]
+ elif all_as_list:
+ formvars[name] = [value]
+ else:
+ formvars[name] = value
+ if environ['REQUEST_METHOD'] == 'POST' and include_get_vars:
+ for name, value in parse_querystring(environ):
+ if name in formvars:
+ if isinstance(formvars[name], list):
+ formvars[name].append(value)
+ else:
+ formvars[name] = [formvars[name], value]
+ elif all_as_list:
+ formvars[name] = [value]
+ else:
+ formvars[name] = value
+ environ['paste.parsed_formvars'] = (formvars, source)
+ return formvars
+
+def construct_url(environ, with_query_string=True, with_path_info=True,
+ script_name=None, path_info=None, querystring=None):
+ """
+ Reconstructs the URL from the WSGI environment. You may override
+ SCRIPT_NAME, PATH_INFO, and QUERYSTRING with the keyword
+ arguments.
+ """
+ url = environ['wsgi.url_scheme']+'://'
+
+ if environ.get('HTTP_HOST'):
+ url += environ['HTTP_HOST'].split(':')[0]
+ else:
+ url += environ['SERVER_NAME']
+
+ if environ['wsgi.url_scheme'] == 'https':
+ if environ['SERVER_PORT'] != '443':
+ url += ':' + environ['SERVER_PORT']
+ else:
+ if environ['SERVER_PORT'] != '80':
+ url += ':' + environ['SERVER_PORT']
+
+ if script_name is None:
+ url += environ.get('SCRIPT_NAME','')
+ else:
+ url += script_name
+ if with_path_info:
+ if path_info is None:
+ url += environ.get('PATH_INFO','')
+ else:
+ url += path_info
+ if with_query_string:
+ if querystring is None:
+ if environ.get('QUERY_STRING'):
+ url += '?' + environ['QUERY_STRING']
+ elif querystring:
+ url += '?' + querystring
+ return url
+
+def path_info_split(path_info):
+ """
+ Splits off the first segment of the path. Returns (first_part,
+ rest_of_path). first_part can be None (if PATH_INFO is empty), ''
+ (if PATH_INFO is '/'), or a name without any /'s. rest_of_path
+ can be '' or a string starting with /.
+ """
+ if not path_info:
+ return None, ''
+ assert path_info.startswith('/'), (
+ "PATH_INFO should start with /: %r" % path_info)
+ path_info = path_info.lstrip('/')
+ if '/' in path_info:
+ first, rest = path_info.split('/', 1)
+ return first, '/' + rest
+ else:
+ return path_info, ''
+
+def path_info_pop(environ):
+ """
+ 'Pops' off the next segment of PATH_INFO, pushing it onto
+ SCRIPT_NAME, and returning that segment.
+
+ For instance::
+
+ >>> def call_it(script_name, path_info):
+ ... env = {'SCRIPT_NAME': script_name, 'PATH_INFO': path_info}
+ ... result = path_info_pop(env)
+ ... print 'SCRIPT_NAME=%r; PATH_INFO=%r; returns=%r' % (
+ ... env['SCRIPT_NAME'], env['PATH_INFO'], result)
+ >>> call_it('/foo', '/bar')
+ SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns='bar'
+ >>> call_it('/foo/bar', '')
+ SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns=None
+ >>> call_it('/foo/bar', '/')
+ SCRIPT_NAME='/foo/bar/'; PATH_INFO=''; returns=''
+ >>> call_it('', '/1/2/3')
+ SCRIPT_NAME='/1'; PATH_INFO='/2/3'; returns='1'
+ >>> call_it('', '//1/2')
+ SCRIPT_NAME='//1'; PATH_INFO='/2'; returns='1'
+ """
+ path = environ.get('PATH_INFO', '')
+ if not path:
+ return None
+ while path.startswith('/'):
+ environ['SCRIPT_NAME'] += '/'
+ path = path[1:]
+ if '/' not in path:
+ environ['SCRIPT_NAME'] += path
+ environ['PATH_INFO'] = ''
+ return path
+ else:
+ segment, path = path.split('/', 1)
+ environ['PATH_INFO'] = '/' + path
+ environ['SCRIPT_NAME'] += segment
+ return segment
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
+
diff --git a/paste/transaction.py b/paste/transaction.py
new file mode 100644
index 0000000..17b434d
--- /dev/null
+++ b/paste/transaction.py
@@ -0,0 +1,79 @@
+# (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
+"""
+Middleware related to transactions and database connections.
+
+At this time it is very basic; but will eventually sprout all that
+two-phase commit goodness that I don't need.
+"""
+from paste.httpexceptions import HTTPError, HTTPException
+
+class ConnectionFactory(object):
+ """
+ Provides a callable interface for connecting to ADBAPI databases in
+ a WSGI style (using the environment). More advanced connection
+ factories might use the REMOTE_USER and/or other environment
+ variables to make the connection returned depend upon the request.
+ """
+ def __init__(self, module, *args, **kwargs):
+ #assert getattr(module,'threadsaftey',0) > 0
+ self.module = module
+ self.args = args
+ self.kwargs = kwargs
+
+ # deal with database string quoting issues
+ self.quote = lambda s: "'%s'" % s.replace("'","''")
+ if hasattr(self.module,'PgQuoteString'):
+ self.quote = self.module.PgQuoteString
+
+ def __call__(self, environ):
+ conn = self.module.connect(*self.args,**self.kwargs)
+ conn.__dict__['module'] = self.module
+ conn.__dict__['quote'] = self.quote
+ return conn
+
+def BasicTransactionHandler(application, factory):
+ """
+ Provides a simple mechanism for starting a transaction based on the
+ factory; and for either committing or rolling back the transaction
+ depending on the result. It checks for the response's current
+ status code either through the latest call to start_response; or
+ through a HTTPException's code. If it is a 100, 200, or 300; the
+ transaction is committed; otherwise it is rolled back.
+ """
+
+ def basic_transaction(environ, start_response):
+ conn = factory(environ)
+ environ['paste.connection'] = conn
+ should_commit = [500]
+ def finalizer():
+ if should_commit.pop() < 400:
+ conn.commit()
+ else:
+ conn.rollback()
+ conn.close()
+ def basictrans_start_response(status, headers, exc_info = None):
+ should_commit.append(int(status.split(" ")[0]))
+ return start_response(status, headers, exc_info)
+ try:
+ for chunk in application(environ, basictrans_start_response):
+ yield chunk
+ except Exception, e:
+ if isinstance(e,HTTPException):
+ should_commit.append(e.code)
+ finalizer()
+ raise
+ finalizer()
+ return basic_transaction
+
+__all__ = ['ConnectionFactory','BasicTransactionHandler']
+
+if '__main__' == __name__ and False:
+ from pyPgSQL import PgSQL
+ factory = ConnectionFactory(PgSQL,database="testing")
+ conn = factory(None)
+ curr = conn.cursor()
+ curr.execute("SELECT now(), %s" % conn.quote("B'n\\'gles"))
+ (time,bing) = curr.fetchone()
+ print bing, time
diff --git a/paste/util/baseserver.py b/paste/util/baseserver.py
new file mode 100755
index 0000000..599dc2d
--- /dev/null
+++ b/paste/util/baseserver.py
@@ -0,0 +1,127 @@
+# (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 Base Server
+
+Very minimalistic WSGI server using Python's built-in BaseHTTPServer; it
+is intended for use in the regression tests suites using a separate
+thread for urlib2 requests. This is probably not a good thing to use in
+a production setting; the focus here is transparency, not robustness.
+"""
+
+import BaseHTTPServer, SocketServer
+import urlparse, sys, time, socket
+try:
+ from paste.httpexceptions import HTTPServerError
+except ImportError:
+ # so we can run this module independent of paste
+ HTTPServerError = RuntimeError
+
+__all__ = ['WSGIServer','WSGIHandler', 'serve']
+
+class WSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ server_version = 'WSGIHandler/0.1'
+ protocol_version = 'HTTP/1.0'
+
+ def write_chunk(self, chunk):
+ if not self.headers_sent:
+ self.headers_sent = True
+ (status, headers) = self.curr_headers
+ code, message = status.split(" ",1)
+ self.send_response(int(code),message)
+ for (k,v) in headers:
+ self.send_header(k,v)
+ self.end_headers()
+ self.wfile.write(chunk)
+
+ def start_response(self,status,response_headers,exc_info=None):
+ if exc_info:
+ try:
+ if self.headers_sent:
+ raise exc_info[0], exc_info[1], exc_info[2]
+ else:
+ self.log_error(exc_info)
+ finally:
+ exc_info = None
+ elif self.curr_headers:
+ assert 0, "Attempt to set headers a second time w/o an exc_info"
+ self.curr_headers = (status, response_headers)
+ return self.write_chunk
+
+ def run_application(self, environ):
+ try:
+ result = self.server.application(environ, self.start_response)
+ try:
+ for chunk in result:
+ self.write_chunk(chunk)
+ finally:
+ if hasattr(result,'close'):
+ result.close()
+ except socket.error, exce:
+ self.log_error("Network Error: %s", exce)
+ return
+ except:
+ if not self.headers_sent:
+ self.curr_headers = ('500 Internal Server Error',
+ [('Content-type', 'text/plain')])
+ self.write_chunk("Internal Server Error\n")
+ raise
+
+ def do_GET(self):
+ (_,_,path,query,fragment) = urlparse.urlsplit(self.path)
+ (server_name, server_port) = self.server.server_address
+ env = { 'wsgi.version': (1,0)
+ ,'wsgi.url_scheme': 'http'
+ ,'wsgi.input': self.rfile
+ ,'wsgi.errors': sys.stderr
+ ,'wsgi.multithread': True
+ ,'wsgi.multiprocess': False
+ ,'wsgi.run_once': True
+ # CGI variables required by PEP-333
+ ,'REQUEST_METHOD': self.command
+ ,'SCRIPT_NAME': '' # application is root of server
+ ,'PATH_INFO': path
+ ,'QUERY_STRING': query
+ ,'CONTENT_TYPE': self.headers.get('Content-Type', '')
+ ,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
+ ,'REQUEST_SCHEME': 'http'
+ ,'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():
+ env['HTTP_%s' % k.replace ('-', '_').upper()] = v
+ self.curr_headers = None
+ self.headers_sent = False
+ self.run_application(env)
+
+ do_POST = do_GET
+
+class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+ def __init__ (self, application, host=None, port=None, handler=None):
+ server_address = (host or "127.0.0.1", port or 8080)
+ BaseHTTPServer.HTTPServer.__init__ (self,server_address,
+ handler or WSGIHandler)
+ self.application = application
+
+def serve(application, host=None, port=None, handler=None):
+ server = WSGIServer(application,host,port,handler)
+ print "serving on %s:%s" % server.server_address
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ # allow CTRL+C to shutdown
+ pass
+ return server
+
+if __name__ == '__main__':
+ # serve exactly 3 requests and then stop, use an external
+ # program like wget or curl to submit these 3 requests.
+ import os
+ from paste.wsgilib import dump_environ
+ serve(dump_environ)
diff --git a/paste/util/quoting.py b/paste/util/quoting.py
index d029518..5f42e6a 100644
--- a/paste/util/quoting.py
+++ b/paste/util/quoting.py
@@ -6,7 +6,8 @@ import htmlentitydefs
import urllib
import re
-__all__ = ['html_quote', 'html_unquote', 'url_quote', 'url_unquote']
+__all__ = ['html_quote', 'html_unquote', 'url_quote', 'url_unquote',
+ 'strip_html']
default_encoding = 'UTF-8'
@@ -59,6 +60,13 @@ def html_unquote(s, encoding=None):
s = s.decode(encoding or default_encoding)
return _unquote_re.sub(_entity_subber, s)
+def strip_html(s):
+ # should this use html_unquote?
+ s = re.sub('<.*?>', '', s)
+ s = s.replace('&nbsp;', ' ').replace('&lt;', '<')
+ s = s.replace('&gt;', '>').replace('&amp;','&')
+ return s
+
url_quote = urllib.quote
url_unquote = urllib.unquote
diff --git a/paste/wsgilib.py b/paste/wsgilib.py
index 89efd63..69adda4 100644
--- a/paste/wsgilib.py
+++ b/paste/wsgilib.py
@@ -5,111 +5,26 @@
A module of many disparate routines.
"""
+# functions which moved to paste.request
+from request import get_cookies, parse_querystring, parse_formvars
+from request import construct_url, path_info_split, path_info_pop
+
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']
+ 'catch_errors', 'dump_environ']
-def get_cookies(environ):
- """
- Gets a cookie object (which is a dictionary-like object) from the
- request environment; caches this value in case get_cookies is
- called again for the same request.
- """
- header = environ.get('HTTP_COOKIE', '')
- if environ.has_key('paste.cookies'):
- cookies, check_header = environ['paste.cookies']
- if check_header == header:
- return cookies
- cookies = SimpleCookie()
- cookies.load(header)
- environ['paste.cookies'] = (cookies, header)
- return cookies
-
-def parse_querystring(environ):
- """
- Parses a query string into a list like ``[(name, value)]``.
- Caches this value in case parse_querystring is called again
- for the same request.
-
- You can pass the result to ``dict()``, but be aware that keys that
- appear multiple times will be lost (only the last value will be
- preserved).
- """
- source = environ.get('QUERY_STRING', '')
- if not source:
- return []
- if 'paste.parsed_querystring' in environ:
- parsed, check_source = environ['paste.parsed_querystring']
- if check_source == source:
- return parsed
- parsed = cgi.parse_qsl(source, keep_blank_values=True,
- strict_parsing=False)
- environ['paste.parsed_querystring'] = (parsed, source)
- return parsed
-
-def parse_formvars(environ, all_as_list=False, include_get_vars=True):
- """
- Parses the request, returning a dictionary of the keys.
-
- If ``all_as_list`` is true, then all values will be lists. If
- not, then only values that show up multiple times will be lists.
-
- If ``include_get_vars`` is true and this was a POST request, then
- GET (query string) variables will also be folded into the
- dictionary.
-
- All values should be strings, except for file uploads which are
- left as FieldStorage instances.
- """
- source = (environ.get('QUERY_STRING', ''),
- environ['wsgi.input'], environ['REQUEST_METHOD'],
- all_as_list, include_get_vars)
- if 'paste.parsed_formvars' in environ:
- parsed, check_source = environ['paste.parsed_formvars']
- if check_source == source:
- return parsed
- fs = cgi.FieldStorage(fp=environ['wsgi.input'],
- environ=environ,
- keep_blank_values=1)
- formvars = {}
- for name in fs.keys():
- values = fs[name]
- if not isinstance(values, list):
- values = [values]
- for value in values:
- if not value.filename:
- value = value.value
- if name in formvars:
- if isinstance(formvars[name], list):
- formvars[name].append(value)
- else:
- formvars[name] = [formvars[name], value]
- elif all_as_list:
- formvars[name] = [value]
- else:
- formvars[name] = value
- if environ['REQUEST_METHOD'] == 'POST' and include_get_vars:
- for name, value in parse_querystring(environ):
- if name in formvars:
- if isinstance(formvars[name], list):
- formvars[name].append(value)
- else:
- formvars[name] = [formvars[name], value]
- elif all_as_list:
- formvars[name] = [value]
- else:
- formvars[name] = value
- environ['paste.parsed_formvars'] = (formvars, source)
- return formvars
class add_close:
"""
@@ -179,38 +94,64 @@ class _wrap_app_iter(object):
self.error_callback(sys.exc_info())
raise
-def raw_interactive(application, path_info='', **environ):
+def raw_interactive(application, path='', **environ):
"""
Runs the application in a fake environment.
"""
+ assert "path_info" not in environ, "argument list changed"
errors = StringIO()
basic_environ = {
- 'PATH_INFO': str(path_info),
- 'SCRIPT_NAME': '',
- 'SERVER_NAME': 'localhost',
- 'SERVER_PORT': '80',
- 'REQUEST_METHOD': 'GET',
- 'HTTP_HOST': 'localhost:80',
- 'CONTENT_LENGTH': '0',
- 'REMOTE_ADDR': '127.0.0.1',
+ # 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.version': (1, 0),
'wsgi.multithread': False,
'wsgi.multiprocess': False,
'wsgi.run_once': False,
- 'wsgi.url_scheme': 'http',
}
+ 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 isinstance(basic_environ['wsgi.input'], str):
- basic_environ['wsgi.input'] = StringIO(basic_environ['wsgi.input'])
- output = StringIO()
+ istream = basic_environ['wsgi.input']
+ if isinstance(istream, str):
+ basic_environ['wsgi.input'] = StringIO(istream)
+ basic_environ['CONTENT_LENGTH'] = len(istream)
data = {}
+ output = StringIO()
+ headers_set = []
+ headers_sent = []
def start_response(status, headers, exc_info=None):
if exc_info:
- raise exc_info[0], exc_info[1], exc_info[2]
+ try:
+ if headers_sent:
+ # Re-raise original exception only if headers sent
+ raise exc_info[0], exc_info[1], exc_info[2]
+ else:
+ # We assume that the sender, who is probably setting
+ # the headers a second time /w a 500 has produced
+ # a more appropriate response.
+ pass
+ 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.write
@@ -218,6 +159,9 @@ def raw_interactive(application, path_info='', **environ):
try:
try:
for s in app_iter:
+ headers_sent.append(True)
+ if not headers_set:
+ raise AssertionError("Content sent w/o headers!")
output.write(s)
except TypeError, e:
# Typically "iteration over non-sequence", so we want
@@ -249,43 +193,22 @@ def interactive(*args, **kw):
return full.getvalue()
interactive.proxy = 'raw_interactive'
-def construct_url(environ, with_query_string=True, with_path_info=True,
- script_name=None, path_info=None, querystring=None):
- """
- Reconstructs the URL from the WSGI environment. You may override
- SCRIPT_NAME, PATH_INFO, and QUERYSTRING with the keyword
- arguments.
- """
- url = environ['wsgi.url_scheme']+'://'
-
- if environ.get('HTTP_HOST'):
- url += environ['HTTP_HOST'].split(':')[0]
- else:
- url += environ['SERVER_NAME']
-
- if environ['wsgi.url_scheme'] == 'https':
- if environ['SERVER_PORT'] != '443':
- url += ':' + environ['SERVER_PORT']
- else:
- if environ['SERVER_PORT'] != '80':
- url += ':' + environ['SERVER_PORT']
-
- if script_name is None:
- url += environ.get('SCRIPT_NAME','')
- else:
- url += script_name
- if with_path_info:
- if path_info is None:
- url += environ.get('PATH_INFO','')
- else:
- url += path_info
- if with_query_string:
- if querystring is None:
- if environ.get('QUERY_STRING'):
- url += '?' + environ['QUERY_STRING']
- elif querystring:
- url += '?' + querystring
- return url
+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 = "".join(output)
+ headers = [('Content-Type', 'text/plain'),
+ ('Content-Length', len(output))]
+ start_response("200 OK",headers)
+ return [output]
def error_body_response(error_code, message):
"""
@@ -425,62 +348,6 @@ def remove_header(headers, name):
i += 1
return result
-def path_info_split(path_info):
- """
- Splits off the first segment of the path. Returns (first_part,
- rest_of_path). first_part can be None (if PATH_INFO is empty), ''
- (if PATH_INFO is '/'), or a name without any /'s. rest_of_path
- can be '' or a string starting with /.
- """
- if not path_info:
- return None, ''
- assert path_info.startswith('/'), (
- "PATH_INFO should start with /: %r" % path_info)
- path_info = path_info.lstrip('/')
- if '/' in path_info:
- first, rest = path_info.split('/', 1)
- return first, '/' + rest
- else:
- return path_info, ''
-
-def path_info_pop(environ):
- """
- 'Pops' off the next segment of PATH_INFO, pushing it onto
- SCRIPT_NAME, and returning that segment.
-
- For instance::
-
- >>> def call_it(script_name, path_info):
- ... env = {'SCRIPT_NAME': script_name, 'PATH_INFO': path_info}
- ... result = path_info_pop(env)
- ... print 'SCRIPT_NAME=%r; PATH_INFO=%r; returns=%r' % (
- ... env['SCRIPT_NAME'], env['PATH_INFO'], result)
- >>> call_it('/foo', '/bar')
- SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns='bar'
- >>> call_it('/foo/bar', '')
- SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns=None
- >>> call_it('/foo/bar', '/')
- SCRIPT_NAME='/foo/bar/'; PATH_INFO=''; returns=''
- >>> call_it('', '/1/2/3')
- SCRIPT_NAME='/1'; PATH_INFO='/2/3'; returns='1'
- >>> call_it('', '//1/2')
- SCRIPT_NAME='//1'; PATH_INFO='/2'; returns='1'
- """
- path = environ.get('PATH_INFO', '')
- if not path:
- return None
- while path.startswith('/'):
- environ['SCRIPT_NAME'] += '/'
- path = path[1:]
- if '/' not in path:
- environ['SCRIPT_NAME'] += path
- environ['PATH_INFO'] = ''
- return path
- else:
- segment, path = path.split('/', 1)
- environ['PATH_INFO'] = '/' + path
- environ['SCRIPT_NAME'] += segment
- return segment
def capture_output(environ, start_response, application):
"""
@@ -502,6 +369,10 @@ def capture_output(environ, start_response, application):
return [body]
return replacement_app
"""
+ warnings.warn(
+ 'wsgilib.capture_output has been deprecated in favor '
+ 'of wsgilib.intercept_output',
+ DeprecationWarning, 1)
data = []
output = StringIO()
def replacement_start_response(status, headers, exc_info=None):
@@ -629,9 +500,29 @@ class ResponseHeaderDict(dict):
else:
result.append((key, str(self[key])))
return result
-
+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)
+ replacement.func_name = new_func.func_name
+ 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()
diff --git a/tests/test_auth/test_auth_cookie.py b/tests/test_auth/test_auth_cookie.py
new file mode 100644
index 0000000..2bec414
--- /dev/null
+++ b/tests/test_auth/test_auth_cookie.py
@@ -0,0 +1,41 @@
+# (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
+
+from paste.auth import cookie
+from paste.wsgilib import raw_interactive, header_value, dump_environ
+from paste.httpexceptions import *
+from Cookie import SimpleCookie
+import urllib2, os
+
+def build(application,setenv, *args, **kwargs):
+ def setter(environ, start_response):
+ save = environ['paste.auth.cookie'].append
+ for (k,v) in setenv.items():
+ save(k)
+ environ[k] = v
+ return application(environ, start_response)
+ return cookie.middleware(setter,*args,**kwargs)
+
+def test_noop():
+ app = build(dump_environ,{})
+ (status,headers,content,errors) = \
+ raw_interactive(app)
+ assert not header_value(headers,'Set-Cookie')
+
+def test_basic(key='key', val='bingles'):
+ app = build(dump_environ,{key:val})
+ (status,headers,content,errors) = \
+ raw_interactive(app)
+ value = header_value(headers,'Set-Cookie')
+ assert "Path=/;" in value
+ assert "expires=" not in value
+ cookie = value.split(";")[0]
+ (status,headers,content,errors) = \
+ raw_interactive(app,{'HTTP_COOKIE': cookie})
+ assert ("%s: %s" % (key,val.replace("\n","\n "))) in content
+
+def test_roundtrip():
+ roundtrip = str('').join(map(chr,xrange(256)))
+ test_basic(roundtrip,roundtrip)
+
diff --git a/tests/test_auth/test_auth_digest.py b/tests/test_auth/test_auth_digest.py
new file mode 100644
index 0000000..1c0ced0
--- /dev/null
+++ b/tests/test_auth/test_auth_digest.py
@@ -0,0 +1,86 @@
+# (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
+
+from paste.auth import digest
+from paste.wsgilib import raw_interactive, header_value
+from paste.httpexceptions import *
+import os
+
+def application(environ, start_response):
+ content = environ.get('REMOTE_USER','')
+ start_response("200 OK",(('Content-Type', 'text/plain'),
+ ('Content-Length', len(content))))
+ return content
+
+realm = "tag:clarkevans.com,2005:testing"
+
+def backwords(realm,username):
+ """ dummy password hash, where user password is just reverse """
+ password = list(username)
+ password.reverse()
+ password = "".join(password)
+ return digest.digest_password(username,realm,password)
+
+application = digest.middleware(application,realm,backwords)
+application = HTTPExceptionHandler(application)
+
+def check(username, password, path="/"):
+ """ perform two-stage authentication to verify login """
+ (status,headers,content,errors) = \
+ raw_interactive(application,path, accept='text/html')
+ assert status.startswith("401")
+ challenge = header_value(headers,'WWW-Authenticate')
+ response = digest.response(challenge, realm, path, username, password)
+ assert "Digest" in response
+ (status,headers,content,errors) = \
+ raw_interactive(application,path,
+ HTTP_AUTHORIZATION=response)
+ if status.startswith("200"):
+ return content
+ if status.startswith("401"):
+ return None
+ assert False, "Unexpected Status: %s" % status
+
+def test_digest():
+ assert 'bing' == check("bing","gnib")
+ assert check("bing","bad") is None
+
+#
+# The following code uses sockets to test the functionality,
+# to enable use:
+#
+# $ TEST_SOCKET py.test
+#
+
+if os.environ.get("TEST_SOCKET",""):
+ import urllib2
+ from paste.debug.testserver import serve
+ server = serve(application)
+
+ def authfetch(username,password,path="/",realm=realm):
+ server.accept(2)
+ import socket
+ socket.setdefaulttimeout(5)
+ uri = ("http://%s:%s" % server.server_address) + path
+ auth = urllib2.HTTPDigestAuthHandler()
+ auth.add_password(realm,uri,username,password)
+ opener = urllib2.build_opener(auth)
+ result = opener.open(uri)
+ return result.read()
+
+ def test_success():
+ assert "bing" == authfetch('bing','gnib')
+
+ def test_failure():
+ # urllib tries 5 more times before it gives up
+ server.accept(5)
+ try:
+ authfetch('bing','wrong')
+ assert False, "this should raise an exception"
+ except urllib2.HTTPError, e:
+ assert e.code == 401
+
+ def test_shutdown():
+ server.stop()
+
diff --git a/tests/test_exceptions/test_error_middleware.py b/tests/test_exceptions/test_error_middleware.py
index 5b1df26..658d887 100644
--- a/tests/test_exceptions/test_error_middleware.py
+++ b/tests/test_exceptions/test_error_middleware.py
@@ -1,11 +1,11 @@
from paste.fixture import *
from paste.exceptions.errormiddleware import ErrorMiddleware
from paste import lint
-
-def strip_html(s):
- s = re.sub('<.*?>', '', s)
- s = s.replace('&nbsp;', ' ').replace('&lt;', '<').replace('&gt;', '>')
- return s
+from paste.util.quoting import strip_html
+#
+# For some strange reason, these 4 lines cannot be removed or the regression
+# test breaks; is it counting the number of lines in the file somehow?
+#
def do_request(app, expect_status=500):
app = lint.middleware(app)
@@ -19,10 +19,16 @@ def do_request(app, expect_status=500):
def clear_middleware(app):
"""
The fixture sets paste.throw_errors, which suppresses exactly what
- we want to test in this case.
+ we want to test in this case. This wrapper also strips exc_info
+ on the *first* call to start_response (but not the second, or
+ subsequent calls.
"""
def clear_throw_errors(environ, start_response):
+ headers_sent = []
def replacement(status, headers, exc_info=None):
+ if headers_sent:
+ return start_response(status, headers, exc_info)
+ headers_sent.append(True)
return start_response(status, headers)
if 'paste.throw_errors' in environ:
del environ['paste.throw_errors']
@@ -64,7 +70,7 @@ def test_makes_exception():
res = do_request(bad_app)
assert '<html' in res
res = strip_html(str(res))
- print res
+ #print res
assert 'bad_app() takes no arguments (2 given' in res
assert 'iterator = application(environ, start_response_wrapper)' in res
assert 'paste.lint' in res
@@ -73,21 +79,21 @@ def test_makes_exception():
def test_start_res():
res = do_request(start_response_app)
res = strip_html(str(res))
- print res
+ #print res
assert 'ValueError: hi' in res
assert 'test_error_middleware' in res
- assert ':43 in start_response_app' in res
+ assert ':49 in start_response_app' in res
def test_after_start():
res = do_request(after_start_response_app, 200)
res = strip_html(str(res))
- print res
+ #print res
assert 'ValueError: error2' in res
- assert ':47' in res
+ assert ':53' in res
def test_iter_app():
res = do_request(iter_app, 200)
- print res
+ #print res
assert 'None raises error' in res
assert 'yielder' in res
diff --git a/tests/test_exceptions/test_formatter.py b/tests/test_exceptions/test_formatter.py
index dad8ed7..8e31fff 100644
--- a/tests/test_exceptions/test_formatter.py
+++ b/tests/test_exceptions/test_formatter.py
@@ -1,5 +1,6 @@
from paste.exceptions import formatter
from paste.exceptions import collector
+from paste.util.quoting import strip_html
import sys
import os
import difflib
@@ -28,11 +29,6 @@ class BadSupplement(Supplement):
def getInfo(self):
raise ValueError("This supplemental info is buggy")
-def strip_html(s):
- s = re.sub('<.*?>', '', s)
- s = s.replace('&nbsp;', ' ').replace('&lt;', '<').replace('&gt;', '>')
- return s
-
def call_error(sup):
1 + 2
__traceback_supplement__ = (sup, ())
diff --git a/tests/test_exceptions/test_httpexceptions.py b/tests/test_exceptions/test_httpexceptions.py
new file mode 100644
index 0000000..5b0101b
--- /dev/null
+++ b/tests/test_exceptions/test_httpexceptions.py
@@ -0,0 +1,82 @@
+# (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
+"""
+WSGI Exception Middleware
+
+Regression Test Suite
+"""
+from paste.httpexceptions import *
+from paste.wsgilib import header_value
+import py
+
+def test_HTTPMove():
+ """ make sure that location is a mandatory attribute of Redirects """
+ py.test.raises(AssertionError,HTTPFound)
+ py.test.raises(AssertionError,HTTPTemporaryRedirect,
+ headers=[('l0cation','/bing')])
+ assert isinstance(HTTPMovedPermanently("This is a message",
+ headers=[('Location','/bing')])
+ ,HTTPRedirection)
+ assert isinstance(HTTPUseProxy(headers=[('LOCATION','/bing')])
+ ,HTTPRedirection)
+ assert isinstance(HTTPFound('/foobar'),HTTPRedirection)
+
+def test_badapp():
+ """ verify that the middleware handles previously-started responses """
+ def badapp(environ, start_response):
+ start_response("200 OK",[])
+ raise HTTPBadRequest("Do not do this at home.")
+ newapp = HTTPExceptionHandler(badapp)
+ assert 'Bad Request' in ''.join(newapp({'HTTP_ACCEPT': 'text/html'},
+ (lambda a, b, c=None: None)))
+
+def test_template():
+ """ verify that html() and plain() output methods work """
+ e = HTTPInternalServerError()
+ e.template = 'A %(ping)s and <b>%(pong)s</b> message.'
+ assert str(e).startswith("500 Internal Server Error")
+ assert e.plain({'ping': 'fun', 'pong': 'happy'}) == (
+ '500 Internal Server Error\n'
+ 'A fun and happy message.\n')
+ assert '<p>A fun and <b>happy</b> message.</p>' in \
+ e.html({'ping': 'fun', 'pong': 'happy'})
+
+def test_redapp():
+ """ check that redirect returns the correct, expected results """
+ saved = []
+ def saveit(status, headers, exc_info = None):
+ saved.append((status,headers))
+ def redapp(environ, start_response):
+ raise HTTPFound("/bing/foo")
+ app = HTTPExceptionHandler(redapp)
+ result = list(app({'HTTP_ACCEPT': 'text/html'},saveit))
+ assert '<a href="/bing/foo">' in result[0]
+ assert "302 Found" == saved[0][0]
+ assert "text/html" == header_value(saved[0][1], 'content-type')
+ assert "/bing/foo" == header_value(saved[0][1],'location')
+ result = list(app({'HTTP_ACCEPT': 'text/plain'},saveit))
+ print result[0] == (
+ '302 Found\n'
+ 'This resource was found at /bing/foo;\n'
+ 'you should be redirected automatically.\n')
+ assert "text/plain" == header_value(saved[1][1],'content-type')
+ assert "/bing/foo" == header_value(saved[1][1],'location')
+
+def test_misc():
+ assert get_exception(301) == HTTPMovedPermanently
+ redirect = HTTPFound("/some/path")
+ assert isinstance(redirect,HTTPException)
+ assert isinstance(redirect,HTTPRedirection)
+ assert not isinstance(redirect,HTTPError)
+ notfound = HTTPNotFound()
+ assert isinstance(notfound,HTTPException)
+ assert isinstance(notfound,HTTPError)
+ assert isinstance(notfound,HTTPClientError)
+ assert not isinstance(notfound,HTTPServerError)
+ notimpl = HTTPNotImplemented()
+ assert isinstance(notimpl,HTTPException)
+ assert isinstance(notimpl,HTTPError)
+ assert isinstance(notimpl,HTTPServerError)
+ assert not isinstance(notimpl,HTTPClientError)
+
diff --git a/tests/test_request.py b/tests/test_request.py
new file mode 100644
index 0000000..12a4ee9
--- /dev/null
+++ b/tests/test_request.py
@@ -0,0 +1,7 @@
+# (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
+from paste.request import *
+from py.test import raises
+
+#@@: regressions needed ;)