Content server: Workaround for android stock browser not support HTTP AUTH.

This commit is contained in:
Kovid Goyal 2012-04-03 15:46:22 +05:30
parent 103854e242
commit 1ed30fdcb5
3 changed files with 64 additions and 12 deletions

View File

@ -15,7 +15,7 @@ from cherrypy.process.plugins import SimplePlugin
from calibre.constants import __appname__, __version__ from calibre.constants import __appname__, __version__
from calibre.utils.date import fromtimestamp from calibre.utils.date import fromtimestamp
from calibre.library.server import listen_on, log_access_file, log_error_file 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, \ from calibre.utils.mdns import publish as publish_zeroconf, \
stop_server as stop_zeroconf, get_external_ip stop_server as stop_zeroconf, get_external_ip
from calibre.library.server.content import ContentServer from calibre.library.server.content import ContentServer
@ -31,10 +31,11 @@ from calibre import prints, as_unicode
class DispatchController(object): # {{{ class DispatchController(object): # {{{
def __init__(self, prefix, wsgi=False): def __init__(self, prefix, wsgi=False, auth_controller=None):
self.dispatcher = cherrypy.dispatch.RoutesDispatcher() self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
self.funcs = [] self.funcs = []
self.seen = set() self.seen = set()
self.auth_controller = auth_controller
self.prefix = prefix if prefix else '' self.prefix = prefix if prefix else ''
if wsgi: if wsgi:
self.prefix = '' self.prefix = ''
@ -44,6 +45,7 @@ class DispatchController(object): # {{{
raise NameError('Route name: '+ repr(name) + ' already used') raise NameError('Route name: '+ repr(name) + ' already used')
self.seen.add(name) self.seen.add(name)
kwargs['action'] = 'f_%d'%len(self.funcs) kwargs['action'] = 'f_%d'%len(self.funcs)
aw = kwargs.pop('android_workaround', False)
if route != '/': if route != '/':
route = self.prefix + route route = self.prefix + route
elif self.prefix: elif self.prefix:
@ -52,6 +54,8 @@ class DispatchController(object): # {{{
self.dispatcher.connect(name+'prefix_extra_trailing', self.dispatcher.connect(name+'prefix_extra_trailing',
self.prefix+'/', self, **kwargs) self.prefix+'/', self, **kwargs)
self.dispatcher.connect(name, route, 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)) self.funcs.append(expose(func))
def __getattr__(self, attr): def __getattr__(self, attr):
@ -156,6 +160,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
self.config = {} self.config = {}
self.is_running = False self.is_running = False
self.exception = None self.exception = None
auth_controller = None
self.users_dict = {}
#self.config['/'] = { #self.config['/'] = {
# 'tools.sessions.on' : True, # 'tools.sessions.on' : True,
# 'tools.sessions.timeout': 60, # Session times out after 60 minutes # 'tools.sessions.timeout': 60, # Session times out after 60 minutes
@ -171,15 +177,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
} }
if opts.password: if opts.password:
self.config['/'] = { self.users_dict[opts.username.strip()] = opts.password.strip()
'tools.digest_auth.on' : True, auth_controller = AuthController('Your calibre library',
'tools.digest_auth.realm' : ( self.users_dict)
'Your calibre library. Username: '
+ opts.username.strip()),
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
}
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__: for x in self.__class__.__bases__:
if hasattr(x, 'add_routes'): if hasattr(x, 'add_routes'):
x.__init__(self) x.__init__(self)

View File

@ -41,7 +41,8 @@ class ContentServer(object):
connect('root', '/', self.index) connect('root', '/', self.index)
connect('old', '/old', self.old) connect('old', '/old', self.old)
connect('get', '/get/{what}/{id}', self.get, 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, connect('static', '/static/{name:.*?}', self.static,
conditions=dict(method=["GET", "HEAD"])) conditions=dict(method=["GET", "HEAD"]))
connect('favicon', '/favicon.png', self.favicon, connect('favicon', '/favicon.png', self.favicon,

View File

@ -5,11 +5,12 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import time, sys import time, sys, uuid, hashlib
from urllib import quote as quote_, unquote as unquote_ from urllib import quote as quote_, unquote as unquote_
from functools import wraps from functools import wraps
import cherrypy import cherrypy
from cherrypy.lib.auth_digest import digest_auth, get_ha1_dict_plain
from calibre import strftime as _strftime, prints, isbytestring from calibre import strftime as _strftime, prints, isbytestring
from calibre.utils.date import now as nowf from calibre.utils.date import now as nowf
@ -58,6 +59,53 @@ def expose(func):
return do 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): def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
if not hasattr(dt, 'timetuple'): if not hasattr(dt, 'timetuple'):