diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 46db62a299..0b5fead634 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -15,7 +15,7 @@ from cherrypy.process.plugins import SimplePlugin from calibre.constants import __appname__, __version__ from calibre.utils.date import fromtimestamp from calibre.library.server import listen_on, log_access_file, log_error_file -from calibre.library.server.utils import expose +from calibre.library.server.utils import expose, AuthController from calibre.utils.mdns import publish as publish_zeroconf, \ stop_server as stop_zeroconf, get_external_ip from calibre.library.server.content import ContentServer @@ -31,10 +31,11 @@ from calibre import prints, as_unicode class DispatchController(object): # {{{ - def __init__(self, prefix, wsgi=False): + def __init__(self, prefix, wsgi=False, auth_controller=None): self.dispatcher = cherrypy.dispatch.RoutesDispatcher() self.funcs = [] self.seen = set() + self.auth_controller = auth_controller self.prefix = prefix if prefix else '' if wsgi: self.prefix = '' @@ -44,6 +45,7 @@ class DispatchController(object): # {{{ raise NameError('Route name: '+ repr(name) + ' already used') self.seen.add(name) kwargs['action'] = 'f_%d'%len(self.funcs) + aw = kwargs.pop('android_workaround', False) if route != '/': route = self.prefix + route elif self.prefix: @@ -52,6 +54,8 @@ class DispatchController(object): # {{{ self.dispatcher.connect(name+'prefix_extra_trailing', self.prefix+'/', self, **kwargs) self.dispatcher.connect(name, route, self, **kwargs) + if self.auth_controller is not None: + func = self.auth_controller(func, aw) self.funcs.append(expose(func)) def __getattr__(self, attr): @@ -156,6 +160,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, self.config = {} self.is_running = False self.exception = None + auth_controller = None + self.users_dict = {} #self.config['/'] = { # 'tools.sessions.on' : True, # 'tools.sessions.timeout': 60, # Session times out after 60 minutes @@ -171,15 +177,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, } if opts.password: - self.config['/'] = { - 'tools.digest_auth.on' : True, - 'tools.digest_auth.realm' : ( - 'Your calibre library. Username: ' - + opts.username.strip()), - 'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()}, - } + self.users_dict[opts.username.strip()] = opts.password.strip() + auth_controller = AuthController('Your calibre library', + self.users_dict) - self.__dispatcher__ = DispatchController(self.opts.url_prefix, wsgi) + self.__dispatcher__ = DispatchController(self.opts.url_prefix, + wsgi=wsgi, auth_controller=auth_controller) for x in self.__class__.__bases__: if hasattr(x, 'add_routes'): x.__init__(self) diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 8ab44d27f3..5b723d078e 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -41,7 +41,8 @@ class ContentServer(object): connect('root', '/', self.index) connect('old', '/old', self.old) connect('get', '/get/{what}/{id}', self.get, - conditions=dict(method=["GET", "HEAD"])) + conditions=dict(method=["GET", "HEAD"]), + android_workaround=True) connect('static', '/static/{name:.*?}', self.static, conditions=dict(method=["GET", "HEAD"])) connect('favicon', '/favicon.png', self.favicon, diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py index 111f535686..1c58e4fa8e 100644 --- a/src/calibre/library/server/utils.py +++ b/src/calibre/library/server/utils.py @@ -5,11 +5,12 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import time, sys +import time, sys, uuid, hashlib from urllib import quote as quote_, unquote as unquote_ from functools import wraps import cherrypy +from cherrypy.lib.auth_digest import digest_auth, get_ha1_dict_plain from calibre import strftime as _strftime, prints, isbytestring from calibre.utils.date import now as nowf @@ -58,6 +59,53 @@ def expose(func): return do +class AuthController(object): + + MAX_AGE = 3600 # Number of seconds after a successful digest auth for which + # the cookie auth will be allowed + + def __init__(self, realm, users_dict): + self.realm = realm + self.users_dict = users_dict + self.secret = bytes(uuid.uuid4().hex) + self.cookie_name = 'android_workaround' + + def hashit(self, raw): + return hashlib.sha1(raw).hexdigest() + + def __call__(self, func, allow_cookie_auth): + + @wraps(func) + def authenticate(*args, **kwargs): + cookie = cherrypy.request.cookie.get(self.cookie_name, None) + if not (allow_cookie_auth and self.is_valid(cookie)): + digest_auth(self.realm, get_ha1_dict_plain(self.users_dict), + self.secret) + + cookie = cherrypy.response.cookie + cookie[self.cookie_name] = self.generate_cookie() + cookie[self.cookie_name]['path'] = '/' + cookie[self.cookie_name]['version'] = '1' + + return func(*args, **kwargs) + + authenticate.im_self = func.im_self + return authenticate + + def generate_cookie(self, timestamp=None): + timestamp = int(time.time()) if timestamp is None else timestamp + key = self.hashit('%d:%s'%(timestamp, self.secret)) + return '%d:%s'%(timestamp, key) + + def is_valid(self, cookie): + try: + timestamp, hashpart = cookie.value.split(':', 1) + timestamp = int(timestamp) + except: + return False + s_timestamp, s_hashpart = self.generate_cookie(timestamp).split(':', 1) + is_valid = s_hashpart == hashpart + return (is_valid and (time.time() - timestamp) < self.MAX_AGE) def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): if not hasattr(dt, 'timetuple'):