summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcce <devnull@localhost>2006-01-01 20:58:49 +0000
committercce <devnull@localhost>2006-01-01 20:58:49 +0000
commit71e140a32dfaa515e1ae204ec5cd0f84ccac7a51 (patch)
tree825af9f12fe67f0b703f2d99f31f12a12d8c087d
parent6c3cf1e9c877b1b0af5cf7fde1d9a43c7fadae6c (diff)
downloadpaste-71e140a32dfaa515e1ae204ec5cd0f84ccac7a51.tar.gz
- fixed logic/definition problem /w multi-entry headers;
__call__ now always returns a string value - renamed resolve to values in HTTPHeader to better reflect the public-interface for this (esp for multi-entry headers) - a few bugs in mult-entry headers - added common CGI headers to httpheaders; I know they don't really belong here, but error checking is nice - updated auth.digest and auth.basic to use httpheaders (this is what prompted the above changes) - added WWW_AUTHENTICATe header which will build a response to a digest challenge - fixed capitalization error in fileapp and added corresponding test
-rw-r--r--paste/auth/basic.py21
-rw-r--r--paste/auth/cookie.py1
-rw-r--r--paste/auth/digest.py35
-rw-r--r--paste/fileapp.py8
-rw-r--r--paste/httpheaders.py141
-rw-r--r--tests/test_auth/test_auth_digest.py12
-rw-r--r--tests/test_fileapp.py1
7 files changed, 133 insertions, 86 deletions
diff --git a/paste/auth/basic.py b/paste/auth/basic.py
index a255c59..cfacb28 100644
--- a/paste/auth/basic.py
+++ b/paste/auth/basic.py
@@ -12,7 +12,7 @@ use ``digest`` authentication.
>>> from paste.wsgilib import dump_environ
>>> from paste.util.httpserver import serve
->>> from paste.auth.basic import AuthBasicHandler
+>>> # from paste.auth.basic import AuthBasicHandler
>>> realm = 'Test Realm'
>>> def authfunc(username, password):
... return username == password
@@ -22,6 +22,7 @@ serving on...
.. [1] http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA
"""
from paste.httpexceptions import HTTPUnauthorized
+from paste.httpheaders import *
class AuthBasicAuthenticator:
"""
@@ -33,17 +34,18 @@ class AuthBasicAuthenticator:
self.authfunc = authfunc
def build_authentication(self):
- head = [('WWW-Authenticate','Basic realm="%s"' % self.realm)]
+ head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
return HTTPUnauthorized(headers=head)
- def authenticate(self, authorization):
+ def authenticate(self, environ):
+ authorization = AUTHORIZATION(environ)
if not authorization:
return self.build_authentication()
- (authmeth, auth) = authorization.split(" ",1)
+ (authmeth, auth) = authorization.split(' ',1)
if 'basic' != authmeth.lower():
return self.build_authentication()
auth = auth.strip().decode('base64')
- username, password = auth.split(':')
+ username, password = auth.split(':',1)
if self.authfunc(username, password):
return username
return self.build_authentication()
@@ -82,13 +84,12 @@ class AuthBasicHandler:
self.authenticate = AuthBasicAuthenticator(realm, authfunc)
def __call__(self, environ, start_response):
- username = environ.get('REMOTE_USER','')
+ username = REMOTE_USER(environ)
if not username:
- authorization = environ.get('HTTP_AUTHORIZATION','')
- result = self.authenticate(authorization)
+ result = self.authenticate(environ)
if isinstance(result, str):
- environ['AUTH_TYPE'] = 'basic'
- environ['REMOTE_USER'] = result
+ AUTH_TYPE.update(environ, 'basic')
+ REMOTE_USER.update(environ, result)
else:
return result.wsgi_application(environ, start_response)
return self.application(environ, start_response)
diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py
index b2f93f0..e53f23d 100644
--- a/paste/auth/cookie.py
+++ b/paste/auth/cookie.py
@@ -19,6 +19,7 @@ cookie.
>>> from paste.util.httpserver import serve
>>> from paste.fileapp import DataApp
>>> from paste.httpexceptions import *
+>>> # from paste.auth.cookie import AuthCookiehandler
>>> from paste.wsgilib import parse_querystring
>>> def testapp(environ, start_response):
... user = dict(parse_querystring(environ)).get('user','')
diff --git a/paste/auth/digest.py b/paste/auth/digest.py
index 4fcef65..b8005c3 100644
--- a/paste/auth/digest.py
+++ b/paste/auth/digest.py
@@ -14,7 +14,7 @@ module has been tested with several common browsers "out-in-the-wild".
>>> from paste.wsgilib import dump_environ
>>> from paste.util.httpserver import serve
->>> from paste.auth.digest import digest_password, AuthDigestHandler
+>>> # from paste.auth.digest import digest_password, AuthDigestHandler
>>> realm = 'Test Realm'
>>> def authfunc(realm, username):
... return digest_password(username, realm, username)
@@ -30,30 +30,13 @@ to use sha would be a good thing.
.. [1] http://www.faqs.org/rfcs/rfc2617.html
"""
from paste.httpexceptions import HTTPUnauthorized
+from paste.httpheaders import *
import md5, time, random, urllib2
def digest_password(username, realm, password):
""" construct the appropriate hashcode needed for HTTP digest """
return md5.md5("%s:%s:%s" % (username,realm,password)).hexdigest()
-def digest_response(challenge, realm, path, username, password):
- """
- builds an authorization response for a given challenge
- """
- 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 AuthDigestAuthenticator:
""" implementation of RFC 2617 - HTTP Digest Authentication """
def __init__(self, realm, authfunc):
@@ -186,22 +169,22 @@ class AuthDigestHandler:
self.application = application
def __call__(self, environ, start_response):
- username = environ.get('REMOTE_USER','')
+ username = REMOTE_USER(environ)
if not username:
- method = environ['REQUEST_METHOD']
- fullpath = environ['SCRIPT_NAME'] + environ["PATH_INFO"]
- authorization = environ.get('HTTP_AUTHORIZATION','')
+ method = REQUEST_METHOD(environ)
+ fullpath = SCRIPT_NAME(environ) + PATH_INFO(environ)
+ authorization = AUTHORIZATION(environ)
result = self.authenticate(authorization, fullpath, method)
if isinstance(result, str):
- environ['AUTH_TYPE'] = 'digest'
- environ['REMOTE_USER'] = result
+ AUTH_TYPE.update(environ,'digest')
+ REMOTE_USER.update(environ, result)
else:
return result.wsgi_application(environ, start_response)
return self.application(environ, start_response)
middleware = AuthDigestHandler
-__all__ = ['digest_password', 'digest_response', 'AuthDigestHandler' ]
+__all__ = ['digest_password', 'AuthDigestHandler' ]
if "__main__" == __name__:
import doctest
diff --git a/paste/fileapp.py b/paste/fileapp.py
index 46cf49f..2fb5ae1 100644
--- a/paste/fileapp.py
+++ b/paste/fileapp.py
@@ -105,9 +105,9 @@ class DataApp(object):
(lower,upper) = range[1][0]
upper = upper or (self.content_length - 1)
if upper >= self.content_length or lower > upper:
- return HTTPRequestRANGENotSatisfiable((
- "RANGE request was made beyond the end of the content,\r\n"
- "which is %s long.\r\n RANGE: %s\r\n") % (
+ return HTTPRequestRangeNotSatisfiable((
+ "Range request was made beyond the end of the content,\r\n"
+ "which is %s long.\r\n Range: %s\r\n") % (
self.content_length, RANGE(environ))
).wsgi_application(environ, start_response)
@@ -154,7 +154,7 @@ class FileApp(DataApp):
self.last_modified = stat.st_mtime
def __call__(self, environ, start_response):
- if 'max-age=0' in CACHE_CONTROL(environ):
+ if 'max-age=0' in CACHE_CONTROL(environ).lower():
self.update(force=True) # RFC 2616 13.2.6
else:
self.update()
diff --git a/paste/httpheaders.py b/paste/httpheaders.py
index 2f8cf90..f714ba9 100644
--- a/paste/httpheaders.py
+++ b/paste/httpheaders.py
@@ -133,15 +133,37 @@ dashes to give CamelCase style names.
"""
+import urllib2
from mimetypes import guess_type
from rfc822 import formatdate, parsedate_tz, mktime_tz
from time import time as now
from httpexceptions import HTTPBadRequest
-__all__ = ['get_header', 'list_headers', 'normalize_headers', 'HTTPHeader',
- # additionally, all header instance objects are exported
-# '_CacheControl', '_ContentDisposition', '_Range' for docos
-]
+__all__ = ['get_header', 'list_headers', 'normalize_headers',
+ 'HTTPHeader', 'EnvironVariable' ]
+
+class EnvironVariable(str):
+ """
+ a CGI ``environ`` variable as described by WSGI
+
+ This is a helper object so that standard WSGI ``environ`` variables
+ can be extracted w/o syntax error possibility.
+ """
+ def __call__(self, environ):
+ return environ.get(self,'')
+ def __repr__(self):
+ return '<EnvironVariable %s>' % self
+ def update(self, environ, value):
+ environ[self] = value
+REMOTE_USER = EnvironVariable("REMOTE_USER")
+AUTH_TYPE = EnvironVariable("AUTH_TYPE")
+REQUEST_METHOD = EnvironVariable("REQUEST_METHOD")
+SCRIPT_NAME = EnvironVariable("SCRIPT_NAME")
+PATH_INFO = EnvironVariable("PATH_INFO")
+
+for _name, _obj in globals().items():
+ if isinstance(_obj, EnvironVariable):
+ __all__.append(_name)
_headers = {}
@@ -240,7 +262,6 @@ class HTTPHeader(object):
#
# Things which can be customized
#
- case_sensitive = False
version = '1.1'
category = 'general'
reference = ''
@@ -262,7 +283,7 @@ class HTTPHeader(object):
"""
convert raw header value into more usable form
- This method invokes ``resolve()`` with the arguments provided,
+ This method invokes ``values()`` with the arguments provided,
parses the header results, and then returns a header-specific
data structure corresponding to the header. For example, the
``Expires`` header returns seconds (as returned by time.time())
@@ -341,7 +362,7 @@ class HTTPHeader(object):
ref = self.reference and (' (%s)' % self.reference) or ''
return '<%s %s%s>' % (self.__class__.__name__, self.name, ref)
- def resolve(self, *args, **kwargs):
+ def values(self, *args, **kwargs):
"""
find/construct field-value(s) for the given header
@@ -390,19 +411,34 @@ class HTTPHeader(object):
def __call__(self, *args, **kwargs):
"""
- converts ``resolve()`` into a string value
+ converts ``values()`` into a string value
+
+ This method converts the results of ``values()`` into a string
+ value for common usage. By default, it is asserted that only
+ one value exists; if you need to access all values then either
+ call ``values()`` directly, or inherit ``_MultiValueHeader``
+ which overrides this method to return a comma separated list of
+ values as described by section 4.2 of RFC 2616.
+ """
+ values = self.values(*args, **kwargs)
+ assert isinstance(values, (tuple,list))
+ if not values:
+ return ''
+ assert len(values) == 1, "more than one value: %s" % repr(values)
+ return str(values[0]).strip()
+
+ def __call__(self, *args, **kwargs):
+ """
+ converts ``values()`` into a string value
- This method converts the results of ``resolve()`` into a string
+ This method converts the results of ``values()`` into a string
value for common usage. If more than one result are found; they
are comma delimited as described by section 4.2 of RFC 2616.
"""
- results = self.resolve(*args, **kwargs)
+ results = self.values(*args, **kwargs)
if not results:
return ''
- result = ", ".join([str(v).strip() for v in results])
- if self.case_sensitive:
- return result
- return result.lower()
+ return ", ".join([str(v).strip() for v in results])
def delete(self, collection):
"""
@@ -437,7 +473,7 @@ class HTTPHeader(object):
return
if type(collection) == dict:
collection[self._environ_name] = value
- return value
+ return
assert list == type(collection)
i = 0
found = False
@@ -462,29 +498,26 @@ class _SingleValueHeader(HTTPHeader):
"""
a ``HTTPHeader`` with exactly a single value
- The field-value for these header instances is a single instance and
- therefore all results constructed or obtained from a collection are
- asserted to ensure that only one result was there.
+ This is the default behavior of ``HTTPHeader`` where returning a
+ the string-value of headers via ``__call__`` assumes that only
+ a single value exists.
"""
- def __call__(self, *args, **kwargs):
- results = HTTPHeader.resolve(self, *args, **kwargs)
- assert isinstance(results, (tuple,list))
- if not results:
- return ''
- assert len(results) == 1, "more than one value: %s" % repr(results)
- result = str(results[0]).strip()
- if self.case_sensitive:
- return result
- return result.lower()
+ pass
class _MultiValueHeader(HTTPHeader):
"""
a ``HTTPHeader`` with one or more values
- This is the default behavior of ``HTTPHeader`` where the header is
- assumed to be multi-valued and values can be combined with a comma.
+ The field-value for these header instances is is allowed to be more
+ than one value; whereby the ``__call__`` method returns a comma
+ separated list as described by section 4.2 of RFC 2616.
"""
- pass
+
+ def __call__(self, *args, **kwargs):
+ results = self.values(*args, **kwargs)
+ if not results:
+ return ''
+ return ", ".join([str(v).strip() for v in results])
class _MultiEntryHeader(HTTPHeader):
"""
@@ -493,14 +526,8 @@ class _MultiEntryHeader(HTTPHeader):
This header is multi-valued, but the values should not be combined
with a comma since the header is not in compliance with RFC 2616
(Set-Cookie due to Expires parameter) or which common user-agents do
- not behave well when the header values are combined. The values
- returned for this case are _always_ a ``list`` instead of a string.
+ not behave well when the header values are combined.
"""
- def __call__(self, *args, **kwargs):
- results = self.resolve(*args, **kwargs)
- if self.case_sensitive:
- return [str(v).strip() for v in results]
- return [str(v).strip().lower() for v in results]
def update(self, collection, *args, **kwargs):
assert list == type(collection), "``environ`` may not be updated"
@@ -509,10 +536,10 @@ class _MultiEntryHeader(HTTPHeader):
return value
def tuples(self, *args, **kwargs):
- values = self.__call__(*args, **kwargs)
+ values = self.values(*args, **kwargs)
if not values:
return ()
- return [(self.name, value) for value in values]
+ return [(self.name, value.strip()) for value in values]
def get_header(name, raiseError=True):
"""
@@ -868,6 +895,7 @@ class _Range(_MultiValueHeader):
indicates that a syntax error in the Range request should result in
the header being ignored rather than a '400 Bad Request'.
"""
+
def parse(self, *args, **kwargs):
"""
Returns a tuple (units, list), where list is a sequence of
@@ -924,6 +952,37 @@ class _ContentRange(_SingleValueHeader):
return (retval,)
_ContentRange('Content-Range', 'entity', 'RFC 2616, 14.6')
+class _Authorization(_SingleValueHeader):
+ """
+ Authorization, RFC 2617 (RFC 2616, 14.8)
+ """
+ def compose(self, digest=None, basic=None, username=None, password=None,
+ challenge=None, path=None, method=None):
+ assert username and password
+ if basic or not challenge:
+ assert not digest
+ userpass = "%s:%s" % (username.strip(),password.strip())
+ return "Basic %s" % userpass.encode('base64').strip()
+ assert challenge and not basic
+ path = path or "/"
+ (_,realm) = challenge.split('realm="')
+ (realm,_) = realm.split('"',1)
+ 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 method or "GET"
+ get_selector = get_full_url
+ retval = "Digest %s" % auth.get_authorization(FakeRequest(), chal)
+ return (retval,)
+_Authorization('Authorization', 'request', 'RFC 2617')
+
#
# For now, construct a minimalistic version of the field-names; at a
# later date more complicated headers may sprout content constructors.
@@ -937,7 +996,7 @@ for (name, category, version, style, comment) in \
#,("Accept-Ranges" ,'response','1.1','multi-value','RFC 2616, 14.5' )
,("Age" ,'response','1.1','singular' ,'RFC 2616, 14.6' )
,("Allow" ,'entity' ,'1.0','multi-value','RFC 2616, 14.7' )
-,("Authorization" ,'request' ,'1.0','singular' ,'RFC 2616, 14.8' )
+#,("Authorization" ,'request' ,'1.0','singular' ,'RFC 2616, 14.8' )
#,("Cache-Control" ,'general' ,'1.1','multi-value','RFC 2616, 14.9' )
,("Cookie" ,'request' ,'1.0','multi-value','RFC 2109/Netscape')
,("Connection" ,'general' ,'1.1','multi-value','RFC 2616, 14.10')
diff --git a/tests/test_auth/test_auth_digest.py b/tests/test_auth/test_auth_digest.py
index c97b7d2..62c3b24 100644
--- a/tests/test_auth/test_auth_digest.py
+++ b/tests/test_auth/test_auth_digest.py
@@ -6,10 +6,11 @@ from paste.auth.digest import *
from paste.wsgilib import raw_interactive
from paste.response import header_value
from paste.httpexceptions import *
+from paste.httpheaders import AUTHORIZATION, WWW_AUTHENTICATE, REMOTE_USER
import os
def application(environ, start_response):
- content = environ.get('REMOTE_USER','')
+ content = REMOTE_USER(environ)
start_response("200 OK",(('Content-Type', 'text/plain'),
('Content-Length', len(content))))
return content
@@ -31,9 +32,10 @@ def check(username, password, path="/"):
(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
+ challenge = WWW_AUTHENTICATE(headers)
+ response = AUTHORIZATION(username=username, password=password,
+ challenge=challenge, path=path)
+ assert "Digest" in response and username in response
(status,headers,content,errors) = \
raw_interactive(application,path,
HTTP_AUTHORIZATION=response)
@@ -75,7 +77,7 @@ if os.environ.get("TEST_SOCKET",""):
def test_failure():
# urllib tries 5 more times before it gives up
- server.accept(5)
+ server.accept(5)
try:
authfetch('bing','wrong')
assert False, "this should raise an exception"
diff --git a/tests/test_fileapp.py b/tests/test_fileapp.py
index c02da52..c3188e3 100644
--- a/tests/test_fileapp.py
+++ b/tests/test_fileapp.py
@@ -148,6 +148,7 @@ def test_range():
app = DataApp(content)
return TestApp(app).get("/",headers={'Range': range}, status=status)
_excercize_range(build,content)
+ build('bytes=0-%d' % (len(content)+1), 416)
def test_file_range():
from paste import fileapp