More tests for digest auth

This commit is contained in:
Kovid Goyal 2015-06-11 13:54:55 +05:30
parent 1fc2082c4a
commit 9f209ae695
2 changed files with 84 additions and 7 deletions

View File

@ -71,7 +71,7 @@ class DigestAuth(object): # {{{
timestamp, server secret and realm. This allows the timestamp to be timestamp, server secret and realm. This allows the timestamp to be
validated and stale nonce's to be rejected.''' validated and stale nonce's to be rejected.'''
if timestamp is None: if timestamp is None:
timestamp = binascii.hexlify(struct.pack(b'!f', float(monotonic()))) timestamp = binascii.hexlify(struct.pack(b'!d', float(monotonic())))
h = sha1_hex(':'.join((timestamp, realm, secret))) h = sha1_hex(':'.join((timestamp, realm, secret)))
nonce = ':'.join((timestamp, h)) nonce = ':'.join((timestamp, h))
return nonce return nonce
@ -83,7 +83,7 @@ class DigestAuth(object): # {{{
def is_nonce_stale(self, max_age_seconds=MAX_AGE_SECONDS): def is_nonce_stale(self, max_age_seconds=MAX_AGE_SECONDS):
try: try:
timestamp = struct.unpack(b'!f', binascii.unhexlify(as_bytestring(self.nonce.partition(':')[0])))[0] timestamp = struct.unpack(b'!d', binascii.unhexlify(as_bytestring(self.nonce.partition(':')[0])))[0]
return timestamp + max_age_seconds < monotonic() return timestamp + max_age_seconds < monotonic()
except Exception: except Exception:
pass pass

View File

@ -6,7 +6,8 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import httplib, base64, urllib2 import httplib, base64, urllib2, subprocess, os
from distutils.spawn import find_executable
from calibre.srv.tests.base import BaseTest, TestServer from calibre.srv.tests.base import BaseTest, TestServer
from calibre.srv.routes import endpoint, Router from calibre.srv.routes import endpoint, Router
@ -37,9 +38,27 @@ def urlopen(server, path='/closed', un='testuser', pw='testpw', method='digest')
auth_handler.add_password(realm=REALM, uri=url, user=un, passwd=pw) auth_handler.add_password(realm=REALM, uri=url, user=un, passwd=pw)
return urllib2.build_opener(auth_handler).open(url) return urllib2.build_opener(auth_handler).open(url)
def digest(un, pw, nonce=None, uri=None, method='GET', nc=1, qop='auth', realm=REALM, cnonce=None, algorithm='MD5', body=b'', modify=lambda x:None):
'Create the payload for a digest based Authorization header'
from calibre.srv.auth import DigestAuth
templ = ('username="{un}", realm="{realm}", qop={qop}, method="{method}",'
' nonce="{nonce}", uri="{uri}", nc={nc}, algorithm="{algorithm}", cnonce="{cnonce}", response="{response}"')
h = templ.format(un=un, realm=realm, qop=qop, uri=uri, method=method, nonce=nonce, nc=nc, cnonce=cnonce, algorithm=algorithm, response=None)
da = DigestAuth(h)
modify(da)
pw = getattr(da, 'pw', pw)
class Data(object):
def __init__(self):
self.method = method
def peek():
return body
response = da.request_digest(pw, Data())
return ('Digest ' + templ.format(
un=un, realm=realm, qop=qop, uri=uri, method=method, nonce=nonce, nc=nc, cnonce=cnonce, algorithm=algorithm, response=response)).encode('ascii')
class TestAuth(BaseTest): class TestAuth(BaseTest):
def test_basic_auth(self): def test_basic_auth(self): # {{{
'Test HTTP Basic auth' 'Test HTTP Basic auth'
r = router(prefer_basic_auth=True) r = router(prefer_basic_auth=True)
with TestServer(r.dispatch) as server: with TestServer(r.dispatch) as server:
@ -74,8 +93,9 @@ class TestAuth(BaseTest):
self.ae(1, len(warnings)) self.ae(1, len(warnings))
self.ae((httplib.UNAUTHORIZED, b''), request('testuser', 'y')) self.ae((httplib.UNAUTHORIZED, b''), request('testuser', 'y'))
self.ae((httplib.UNAUTHORIZED, b''), request('asf', 'testpw')) self.ae((httplib.UNAUTHORIZED, b''), request('asf', 'testpw'))
# }}}
def test_digest_auth(self): def test_digest_auth(self): # {{{
'Test HTTP Digest auth' 'Test HTTP Digest auth'
from calibre.srv.http_request import normalize_header_name from calibre.srv.http_request import normalize_header_name
from calibre.srv.utils import parse_http_dict from calibre.srv.utils import parse_http_dict
@ -90,7 +110,64 @@ class TestAuth(BaseTest):
return {normalize_header_name(k):v for k, v in r.getheaders()} return {normalize_header_name(k):v for k, v in r.getheaders()}
conn = server.connect() conn = server.connect()
test(conn, '/open', body=b'open') test(conn, '/open', body=b'open')
auth = parse_http_dict(test(conn, '/closed', status=httplib.UNAUTHORIZED)['WWW-Authenticate']) auth = parse_http_dict(test(conn, '/closed', status=httplib.UNAUTHORIZED)['WWW-Authenticate'].partition(b' ')[2])
self.ae(auth[b'Digest realm'], bytes(REALM)), self.ae(auth[b'algorithm'], b'MD5'), self.ae(auth[b'qop'], b'auth') nonce = auth['nonce']
auth = parse_http_dict(test(conn, '/closed', status=httplib.UNAUTHORIZED)['WWW-Authenticate'].partition(b' ')[2])
self.assertNotEqual(nonce, auth['nonce'], 'nonce was re-used')
self.ae(auth[b'realm'], bytes(REALM)), self.ae(auth[b'algorithm'], b'MD5'), self.ae(auth[b'qop'], b'auth')
self.assertNotIn('stale', auth) self.assertNotIn('stale', auth)
args = auth.copy()
args['un'], args['pw'], args['uri'] = 'testuser', 'testpw', '/closed'
def ok_test(conn, dh, **args):
args['body'] = args.get('body', b'closed')
return test(conn, '/closed', headers={'Authorization':dh}, **args)
ok_test(conn, digest(**args))
# Check that server ignores repeated nc values
ok_test(conn, digest(**args))
# Check stale nonces
orig, r.auth_controller.max_age_seconds = r.auth_controller.max_age_seconds, -1
auth = parse_http_dict(test(conn, '/closed', headers={
'Authorization':digest(**args)},status=httplib.UNAUTHORIZED)['WWW-Authenticate'].partition(b' ')[2])
self.assertIn('stale', auth)
r.auth_controller.max_age_seconds = orig
ok_test(conn, digest(**args))
def fail_test(conn, modify, **kw):
kw['body'] = kw.get('body', b'')
kw['status'] = kw.get('status', httplib.UNAUTHORIZED)
args['modify'] = modify
return test(conn, '/closed', headers={'Authorization':digest(**args)}, **kw)
# Check modified nonce fails
fail_test(conn, lambda da:setattr(da, 'nonce', 'xyz'))
fail_test(conn, lambda da:setattr(da, 'nonce', 'x' + da.nonce))
# Check mismatched uri fails
fail_test(conn, lambda da:setattr(da, 'uri', '/'))
fail_test(conn, lambda da:setattr(da, 'uri', '/closed2'))
fail_test(conn, lambda da:setattr(da, 'uri', '/closed/2'))
# Check that incorrect user/password fails
fail_test(conn, lambda da:setattr(da, 'pw', '/'))
fail_test(conn, lambda da:setattr(da, 'username', '/'))
# Check against python's stdlib
self.ae(urlopen(server).read(), b'closed') self.ae(urlopen(server).read(), b'closed')
# Check using curl
curl = find_executable('curl')
if curl:
def docurl(data, *args):
cmd = [curl] + list(args) + ['http://localhost:%d/closed' % server.address[1]]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=open(os.devnull, 'wb'))
x = p.stdout.read()
p.wait()
self.ae(x, data)
docurl(b'')
docurl(b'', '--digest', '--user', 'xxxx:testpw')
docurl(b'', '--digest', '--user', 'testuser:xtestpw')
docurl(b'closed', '--digest', '--user', 'testuser:testpw')
# }}}