mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
Implement workaround for android lack of support for HTTP Auth when downloading files
This commit is contained in:
parent
f6e4eaf375
commit
1ea0f8ddab
@ -7,12 +7,12 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import binascii, os, random, struct, base64, httplib
|
||||
from hashlib import md5, sha1, sha256
|
||||
from hashlib import md5, sha256
|
||||
from itertools import permutations
|
||||
|
||||
from calibre.srv.errors import HTTPAuthRequired, HTTPSimpleResponse, InvalidCredentials
|
||||
from calibre.srv.http_request import parse_uri
|
||||
from calibre.srv.utils import parse_http_dict
|
||||
from calibre.srv.utils import parse_http_dict, encode_path
|
||||
from calibre.utils.monotonic import monotonic
|
||||
|
||||
MAX_AGE_SECONDS = 3600
|
||||
@ -25,9 +25,6 @@ def as_bytestring(x):
|
||||
def md5_hex(s):
|
||||
return md5(as_bytestring(s)).hexdigest().decode('ascii')
|
||||
|
||||
def sha1_hex(s):
|
||||
return sha1(as_bytestring(s)).hexdigest().decode('ascii')
|
||||
|
||||
def sha256_hex(s):
|
||||
return sha256(as_bytestring(s)).hexdigest().decode('ascii')
|
||||
|
||||
@ -180,6 +177,7 @@ class AuthController(object):
|
||||
not implement the digest auth spec properly (it sends out of order nc
|
||||
values).
|
||||
'''
|
||||
ANDROID_COOKIE = 'android_workaround'
|
||||
|
||||
def __init__(self, user_credentials=None, prefer_basic_auth=False, realm='calibre', max_age_seconds=MAX_AGE_SECONDS, log=None):
|
||||
self.user_credentials, self.prefer_basic_auth = user_credentials, prefer_basic_auth
|
||||
@ -202,8 +200,15 @@ class AuthController(object):
|
||||
return pw and self.user_credentials.get(un) == pw
|
||||
|
||||
def __call__(self, data, endpoint):
|
||||
# TODO: Implement Android workaround for /get
|
||||
http_auth_needed = not (endpoint.android_workaround and self.validate_android_cookie(data.cookies.get(self.ANDROID_COOKIE)))
|
||||
if http_auth_needed:
|
||||
self.do_http_auth(data, endpoint)
|
||||
if endpoint.android_workaround:
|
||||
data.outcookie[self.ANDROID_COOKIE] = synthesize_nonce(self.key_order, self.realm, self.secret)
|
||||
data.outcookie[self.ANDROID_COOKIE]['path'] = encode_path(*data.path)
|
||||
|
||||
def validate_android_cookie(self, cookie):
|
||||
return cookie and validate_nonce(self.key_order, cookie, self.realm, self.secret) and not is_nonce_stale(cookie, self.max_age_seconds)
|
||||
|
||||
def do_http_auth(self, data, endpoint):
|
||||
auth = data.inheaders.get('Authorization')
|
||||
|
@ -21,7 +21,7 @@ from calibre.srv.http_request import HTTPRequest, read_headers
|
||||
from calibre.srv.sendfile import file_metadata, sendfile_to_socket_async, CannotSendfile, SendfileInterrupted
|
||||
from calibre.srv.utils import (
|
||||
MultiDict, http_date, HTTP1, HTTP11, socket_errors_socket_closed,
|
||||
sort_q_values, get_translator_for_lang)
|
||||
sort_q_values, get_translator_for_lang, Cookie)
|
||||
from calibre.utils.monotonic import monotonic
|
||||
|
||||
Range = namedtuple('Range', 'start stop size')
|
||||
@ -194,6 +194,7 @@ class RequestData(object): # {{{
|
||||
self.remote_addr, self.remote_port = remote_addr, remote_port
|
||||
self.opts = opts
|
||||
self.status_code = httplib.CREATED if self.method == 'POST' else httplib.OK
|
||||
self.outcookie = Cookie()
|
||||
|
||||
def generate_static_output(self, name, generator):
|
||||
ans = self.static_cache.get(name)
|
||||
@ -411,6 +412,12 @@ class HTTPConnection(HTTPRequest):
|
||||
buf = [HTTP11 + (' %d ' % data.status_code) + httplib.responses[data.status_code]]
|
||||
for header, value in sorted(outheaders.iteritems(), key=itemgetter(0)):
|
||||
buf.append('%s: %s' % (header, value))
|
||||
for morsel in data.outcookie.itervalues():
|
||||
morsel['version'] = '1'
|
||||
x = morsel.output()
|
||||
if isinstance(x, bytes):
|
||||
x = x.decode('ascii')
|
||||
buf.append(x)
|
||||
buf.append('')
|
||||
self.response_ready(BytesIO(b''.join((x + '\r\n').encode('ascii') for x in buf)), output=output)
|
||||
|
||||
|
@ -190,7 +190,9 @@ class Router(object):
|
||||
if x:
|
||||
k, v = x.partition('=')[::2]
|
||||
if k:
|
||||
c[k] = v
|
||||
# Since we only set simple hex encoded cookies, we dont
|
||||
# need more sophisticated value parsing
|
||||
c[k] = v.strip('"')
|
||||
|
||||
def dispatch(self, data):
|
||||
endpoint_, args = self.find_route(data.path)
|
||||
|
@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import httplib, base64, urllib2, subprocess, os
|
||||
import httplib, base64, urllib2, subprocess, os, cookielib
|
||||
from distutils.spawn import find_executable
|
||||
|
||||
from calibre.srv.tests.base import BaseTest, TestServer
|
||||
@ -24,7 +24,7 @@ def auth(ctx, data):
|
||||
|
||||
@endpoint('/android', auth_required=True, android_workaround=True)
|
||||
def android(ctx, data):
|
||||
return '/android'
|
||||
return 'android'
|
||||
|
||||
def router(prefer_basic_auth=False):
|
||||
from calibre.srv.auth import AuthController
|
||||
@ -171,3 +171,32 @@ class TestAuth(BaseTest):
|
||||
docurl(b'', '--digest', '--user', 'testuser:xtestpw')
|
||||
docurl(b'closed', '--digest', '--user', 'testuser:testpw')
|
||||
# }}}
|
||||
|
||||
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, httplib.UNAUTHORIZED)
|
||||
|
||||
auth_handler = urllib2.HTTPDigestAuthHandler()
|
||||
url = 'http://localhost:%d%s' % (server.address[1], '/android')
|
||||
auth_handler.add_password(realm=REALM, uri=url, user='testuser', passwd='testpw')
|
||||
cj = cookielib.CookieJar()
|
||||
cookie_handler = urllib2.HTTPCookieProcessor(cj)
|
||||
r = urllib2.build_opener(auth_handler, cookie_handler).open(url)
|
||||
self.ae(r.getcode(), httplib.OK)
|
||||
cookies = tuple(cj)
|
||||
self.ae(len(cookies), 1)
|
||||
cookie = cookies[0]
|
||||
self.assertIn(b':', cookie.value)
|
||||
self.ae(cookie.path, b'/android')
|
||||
r = urllib2.build_opener(cookie_handler).open(url)
|
||||
self.ae(r.getcode(), httplib.OK)
|
||||
self.ae(r.read(), b'android')
|
||||
# }}}
|
||||
|
@ -7,12 +7,14 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import errno, socket, select
|
||||
from Cookie import SimpleCookie
|
||||
from contextlib import closing
|
||||
from urlparse import parse_qs
|
||||
import repr as reprlib
|
||||
from email.utils import formatdate
|
||||
from operator import itemgetter
|
||||
from future_builtins import map
|
||||
from urllib import quote as urlquote
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import iswindows
|
||||
@ -261,6 +263,17 @@ def get_translator_for_lang(cache, bcp_47_code):
|
||||
cache[bcp_47_code] = ans = get_translator(bcp_47_code)
|
||||
return ans
|
||||
|
||||
def encode_path(*components):
|
||||
'Encode the path specified as a list of path components using URL encoding'
|
||||
return '/' + '/'.join(urlquote(x.encode('utf-8'), '').decode('ascii') for x in components)
|
||||
|
||||
class Cookie(SimpleCookie):
|
||||
|
||||
def _BaseCookie__set(self, key, real_value, coded_value):
|
||||
if not isinstance(key, bytes):
|
||||
key = key.encode('ascii') # Python 2.x cannot handle unicode keys
|
||||
return SimpleCookie._BaseCookie__set(self, key, real_value, coded_value)
|
||||
|
||||
# Logging {{{
|
||||
|
||||
class ServerLog(ThreadSafeLog):
|
||||
|
Loading…
x
Reference in New Issue
Block a user