%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/tests/ |
| Current File : //lib/calibre/calibre/srv/tests/auth.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import subprocess, os, time, shutil
from collections import namedtuple
from calibre.ptempfile import TemporaryDirectory
from calibre.srv.errors import HTTPForbidden
from calibre.srv.tests.base import BaseTest, TestServer
from calibre.srv.routes import endpoint, Router
from polyglot.builtins import iteritems, itervalues
from polyglot import http_client
from polyglot.http_cookie import CookieJar
from polyglot.urllib import (build_opener, HTTPBasicAuthHandler,
HTTPCookieProcessor, HTTPDigestAuthHandler, HTTPError)
from polyglot.binary import as_base64_bytes
REALM = 'calibre-test'
@endpoint('/open', auth_required=False)
def noauth(ctx, data):
return 'open'
@endpoint('/closed', auth_required=True)
def auth(ctx, data):
return 'closed'
@endpoint('/android', auth_required=True, android_workaround=True)
def android(ctx, data):
return 'android'
@endpoint('/android2', auth_required=True, android_workaround=True)
def android2(ctx, data):
return 'android2'
def router(prefer_basic_auth=False, ban_for=0, ban_after=5):
from calibre.srv.auth import AuthController
return Router(itervalues(globals()), auth_controller=AuthController(
{'testuser':'testpw', '!@#$%^&*()-=_+':'!@#$%^&*()-=_+'},
ban_time_in_minutes=ban_for, ban_after=ban_after,
prefer_basic_auth=prefer_basic_auth, realm=REALM, max_age_seconds=1))
def urlopen(server, path='/closed', un='testuser', pw='testpw', method='digest'):
auth_handler = HTTPBasicAuthHandler() if method == 'basic' else HTTPDigestAuthHandler()
url = 'http://localhost:%d%s' % (server.address[1], path)
auth_handler.add_password(realm=REALM, uri=url, user=un, passwd=pw)
return 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:
def __init__(self):
self.method = method
def peek(self):
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):
def test_basic_auth(self): # {{{
'Test HTTP Basic auth'
r = router(prefer_basic_auth=True)
with TestServer(r.dispatch) as server:
r.auth_controller.log = server.log
conn = server.connect()
conn.request('GET', '/open')
r = conn.getresponse()
self.ae(r.status, http_client.OK)
self.ae(r.read(), b'open')
conn.request('GET', '/closed')
r = conn.getresponse()
self.ae(r.status, http_client.UNAUTHORIZED)
self.ae(r.getheader('WWW-Authenticate'), 'Basic realm="%s"' % REALM)
self.assertFalse(r.read())
conn.request('GET', '/closed', headers={'Authorization': b'Basic ' + as_base64_bytes(b'testuser:testpw')})
r = conn.getresponse()
self.ae(r.read(), b'closed')
self.ae(r.status, http_client.OK)
self.ae(b'closed', urlopen(server, method='basic').read())
self.ae(b'closed', urlopen(server, un='!@#$%^&*()-=_+', pw='!@#$%^&*()-=_+', method='basic').read())
def request(un='testuser', pw='testpw'):
conn.request('GET', '/closed', headers={'Authorization': b'Basic ' + as_base64_bytes(f'{un}:{pw}')})
r = conn.getresponse()
return r.status, r.read()
warnings = []
server.loop.log.warn = lambda *args, **kwargs: warnings.append(' '.join(args))
self.ae((http_client.OK, b'closed'), request())
self.ae((http_client.UNAUTHORIZED, b''), request('x', 'y'))
self.ae((http_client.BAD_REQUEST, b'The username or password was empty'), request('', ''))
self.ae(1, len(warnings))
self.ae((http_client.UNAUTHORIZED, b''), request('testuser', 'y'))
self.ae((http_client.BAD_REQUEST, b'The username or password was empty'), request('testuser', ''))
self.ae((http_client.BAD_REQUEST, b'The username or password was empty'), request(''))
self.ae((http_client.UNAUTHORIZED, b''), request('asf', 'testpw'))
# }}}
def test_library_restrictions(self): # {{{
from calibre.srv.opts import Options
from calibre.srv.handler import Handler
from calibre.db.legacy import create_backend
opts = Options(userdb=':memory:')
Data = namedtuple('Data', 'username')
with TemporaryDirectory() as base:
l1, l2, l3 = map(lambda x: os.path.join(base, 'l' + x), '123')
for l in (l1, l2, l3):
create_backend(l).close()
ctx = Handler((l1, l2, l3), opts).router.ctx
um = ctx.user_manager
def get_library(username=None, library_id=None):
ans = ctx.get_library(Data(username), library_id=library_id)
return os.path.basename(ans.backend.library_path)
def library_info(username=None):
lmap, defaultlib = ctx.library_info(Data(username))
lmap = {k:os.path.basename(v) for k, v in iteritems(lmap)}
return lmap, defaultlib
self.assertEqual(get_library(), 'l1')
self.assertEqual(library_info()[0], {'l%d'%i:'l%d'%i for i in range(1, 4)})
self.assertEqual(library_info()[1], 'l1')
self.assertRaises(HTTPForbidden, get_library, 'xxx')
um.add_user('a', 'a')
self.assertEqual(library_info('a')[0], {'l%d'%i:'l%d'%i for i in range(1, 4)})
um.update_user_restrictions('a', {'blocked_library_names': ['L2']})
self.assertEqual(library_info('a')[0], {'l%d'%i:'l%d'%i for i in range(1, 4) if i != 2})
um.update_user_restrictions('a', {'allowed_library_names': ['l3']})
self.assertEqual(library_info('a')[0], {'l%d'%i:'l%d'%i for i in range(1, 4) if i == 3})
self.assertEqual(library_info('a')[1], 'l3')
self.assertRaises(HTTPForbidden, get_library, 'a', 'l1')
self.assertRaises(HTTPForbidden, get_library, 'xxx')
# }}}
def test_digest_auth(self): # {{{
'Test HTTP Digest auth'
from calibre.srv.http_request import normalize_header_name
from calibre.srv.utils import parse_http_dict
r = router()
with TestServer(r.dispatch) as server:
r.auth_controller.log = server.log
def test(conn, path, headers={}, status=http_client.OK, body=b'', request_body=b''):
conn.request('GET', path, request_body, headers)
r = conn.getresponse()
self.ae(r.status, status)
self.ae(r.read(), body)
return {normalize_header_name(k):v for k, v in r.getheaders()}
conn = server.connect()
test(conn, '/open', body=b'open')
auth = parse_http_dict(test(conn, '/closed', status=http_client.UNAUTHORIZED)['WWW-Authenticate'].partition(' ')[2])
nonce = auth['nonce']
auth = parse_http_dict(test(conn, '/closed', status=http_client.UNAUTHORIZED)['WWW-Authenticate'].partition(' ')[2])
self.assertNotEqual(nonce, auth['nonce'], 'nonce was re-used')
self.ae(auth['realm'], REALM)
self.ae(auth['algorithm'], 'MD5')
self.ae(auth['qop'], '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))
warnings = []
server.loop.log.warn = lambda *args, **kwargs: warnings.append(' '.join(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=http_client.UNAUTHORIZED)['WWW-Authenticate'].partition(' ')[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', http_client.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', '/'))
fail_test(conn, lambda da:setattr(da, 'username', ''))
fail_test(conn, lambda da:setattr(da, 'pw', ''))
fail_test(conn, lambda da:(setattr(da, 'pw', ''), setattr(da, 'username', '')))
# Check against python's stdlib
self.ae(urlopen(server).read(), b'closed')
# Check using curl
curl = shutil.which('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')
# }}}
def test_fail_ban(self): # {{{
ban_for = 0.5/60.0
r = router(prefer_basic_auth=True, ban_for=ban_for, ban_after=2)
with TestServer(r.dispatch) as server:
r.auth_controller.log = server.log
conn = server.connect()
def request(un='testuser', pw='testpw'):
conn.request('GET', '/closed', headers={'Authorization': b'Basic ' + as_base64_bytes(f'{un}:{pw}')})
r = conn.getresponse()
return r.status, r.read()
warnings = []
server.loop.log.warn = lambda *args, **kwargs: warnings.append(' '.join(args))
self.ae((http_client.OK, b'closed'), request())
self.ae((http_client.UNAUTHORIZED, b''), request('x', 'y'))
self.ae((http_client.UNAUTHORIZED, b''), request('x', 'y'))
self.ae(http_client.FORBIDDEN, request('x', 'y')[0])
self.ae(http_client.FORBIDDEN, request()[0])
time.sleep(ban_for * 60 + 0.01)
self.ae((http_client.OK, b'closed'), request())
# }}}
def test_android_auth_workaround(self): # {{{
'Test authentication workaround for Android'
r = router()
with TestServer(r.dispatch) as server:
r.auth_controller.log = server.log
conn = server.connect()
# First check that unauth access fails
conn.request('GET', '/android')
r = conn.getresponse()
self.ae(r.status, http_client.UNAUTHORIZED)
auth_handler = HTTPDigestAuthHandler()
url = 'http://localhost:%d%s' % (server.address[1], '/android')
auth_handler.add_password(realm=REALM, uri=url, user='testuser', passwd='testpw')
cj = CookieJar()
cookie_handler = HTTPCookieProcessor(cj)
r = build_opener(auth_handler, cookie_handler).open(url)
self.ae(r.getcode(), http_client.OK)
cookies = tuple(cj)
self.ae(len(cookies), 1)
cookie = cookies[0]
self.assertIn(':', cookie.value)
self.ae(cookie.path, '/android')
r = build_opener(cookie_handler).open(url)
self.ae(r.getcode(), http_client.OK)
self.ae(r.read(), b'android')
# Test that a replay attack against a different URL does not work
try:
build_opener(cookie_handler).open(url+'2')
assert ('Replay attack succeeded')
except HTTPError as e:
self.ae(e.code, http_client.UNAUTHORIZED)
# }}}