diff --git a/CHANGELOG.md b/CHANGELOG.md index 9605f478b..43701e481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ at anytime. ### Fixed * handling error from dht clients with old `ping` method * blobs not being re-announced if no peers successfully stored, now failed announcements are re-queued + * issue where an `AuthAPIClient` (used by `lbrynet-cli`) would fail to update its session secret and keep making new auth sessions, with every other request failing + * `use_auth_http` in a config file being overridden by the default command line argument to `lbrynet-daemon`, now the command line value will only override the config file value if it is provided + * `lbrynet-cli` not automatically switching to the authenticated client if the server is detected to be using authentication. This resulted in `lbrynet-cli` failing to run when `lbrynet-daemon` was run with the `--http-auth` flag ### Deprecated * @@ -36,6 +39,8 @@ at anytime. * dht logging to be more verbose with errors and warnings * added `single_announce` and `last_announced_time` columns to the `blob` table in sqlite * pass the sd hash to reflector ClientFactory instead of looking it up + * if the `use_authentication` setting is configured, use authentication for all api methods instead of only those with the `auth_required` decorator + * regenerate api keys on startup if the using authentication ### Added * virtual kademlia network and mock udp transport for dht integration tests @@ -45,6 +50,8 @@ at anytime. ### Removed * `announce_all` argument from `blob_announce` * old `blob_announce_all` command + * `AuthJSONRPCServer.auth_required` decorator + * unused `--wallet` argument to `lbrynet-daemon`, which used to be to support `PTCWallet`. ## [0.19.2] - 2018-03-28 diff --git a/lbrynet/daemon/Daemon.py b/lbrynet/daemon/Daemon.py index 05d368b7d..44a87200c 100644 --- a/lbrynet/daemon/Daemon.py +++ b/lbrynet/daemon/Daemon.py @@ -1150,7 +1150,6 @@ class Daemon(AuthJSONRPCServer): """ return self._render_response(conf.settings.get_adjustable_settings_dict()) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_settings_set(self, **kwargs): """ @@ -1495,7 +1494,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(claim_results) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_resolve(self, force=False, uri=None, uris=[]): """ @@ -1586,7 +1584,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(results) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_get(self, uri, file_name=None, timeout=None): """ @@ -1675,7 +1672,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(result) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_file_set_status(self, status, **kwargs): """ @@ -1716,7 +1712,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(msg) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_file_delete(self, delete_from_download_dir=False, delete_all=False, **kwargs): """ @@ -1797,7 +1792,6 @@ class Daemon(AuthJSONRPCServer): cost = yield self.get_est_cost(uri, size) defer.returnValue(cost) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_channel_new(self, channel_name, amount): """ @@ -1852,7 +1846,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(result) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_channel_list(self): """ @@ -1874,7 +1867,6 @@ class Daemon(AuthJSONRPCServer): defer.returnValue(response) @AuthJSONRPCServer.deprecated("channel_list") - @AuthJSONRPCServer.auth_required def jsonrpc_channel_list_mine(self): """ Get certificate claim infos for channels that can be published to (deprecated) @@ -1891,7 +1883,6 @@ class Daemon(AuthJSONRPCServer): return self.jsonrpc_channel_list() - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_channel_export(self, claim_id): """ @@ -1910,7 +1901,6 @@ class Daemon(AuthJSONRPCServer): result = yield self.session.wallet.export_certificate_info(claim_id) defer.returnValue(result) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_channel_import(self, serialized_certificate_info): """ @@ -1929,7 +1919,6 @@ class Daemon(AuthJSONRPCServer): result = yield self.session.wallet.import_certificate_info(serialized_certificate_info) defer.returnValue(result) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_publish(self, name, bid, metadata=None, file_path=None, fee=None, title=None, description=None, author=None, language=None, license=None, @@ -2139,7 +2128,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(result) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_claim_abandon(self, claim_id=None, txid=None, nout=None): """ @@ -2172,7 +2160,6 @@ class Daemon(AuthJSONRPCServer): self.analytics_manager.send_claim_action('abandon') defer.returnValue(result) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_claim_new_support(self, name, claim_id, amount): """ @@ -2200,7 +2187,6 @@ class Daemon(AuthJSONRPCServer): self.analytics_manager.send_claim_action('new_support') defer.returnValue(result) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_claim_renew(self, outpoint=None, height=None): """ @@ -2243,7 +2229,6 @@ class Daemon(AuthJSONRPCServer): result = yield self.session.wallet.claim_renew_all_before_expiration(height) defer.returnValue(result) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_claim_send_to_address(self, claim_id, address, amount=None): """ @@ -2276,7 +2261,6 @@ class Daemon(AuthJSONRPCServer): defer.returnValue(response) # TODO: claim_list_mine should be merged into claim_list, but idk how to authenticate it -Grin - @AuthJSONRPCServer.auth_required def jsonrpc_claim_list_mine(self): """ List my name claims @@ -2351,7 +2335,6 @@ class Daemon(AuthJSONRPCServer): claims = yield self.session.wallet.get_claims_for_name(name) defer.returnValue(claims) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_claim_list_by_channel(self, page=0, page_size=10, uri=None, uris=[]): """ @@ -2441,7 +2424,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(results) defer.returnValue(response) - @AuthJSONRPCServer.auth_required def jsonrpc_transaction_list(self): """ List transactions belonging to wallet @@ -2521,7 +2503,6 @@ class Daemon(AuthJSONRPCServer): d.addCallback(lambda r: self._render_response(r)) return d - @AuthJSONRPCServer.auth_required def jsonrpc_wallet_is_address_mine(self, address): """ Checks if an address is associated with the current wallet. @@ -2540,7 +2521,6 @@ class Daemon(AuthJSONRPCServer): d.addCallback(lambda is_mine: self._render_response(is_mine)) return d - @AuthJSONRPCServer.auth_required def jsonrpc_wallet_public_key(self, address): """ Get public key from wallet address @@ -2560,7 +2540,6 @@ class Daemon(AuthJSONRPCServer): d.addCallback(lambda r: self._render_response(r)) return d - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_wallet_list(self): """ @@ -2580,7 +2559,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(addresses) defer.returnValue(response) - @AuthJSONRPCServer.auth_required def jsonrpc_wallet_new_address(self): """ Generate a new wallet address @@ -2604,7 +2582,6 @@ class Daemon(AuthJSONRPCServer): d.addCallback(lambda address: self._render_response(address)) return d - @AuthJSONRPCServer.auth_required def jsonrpc_wallet_unused_address(self): """ Return an address containing no balance, will create @@ -2630,7 +2607,6 @@ class Daemon(AuthJSONRPCServer): return d @AuthJSONRPCServer.deprecated("wallet_send") - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_send_amount_to_address(self, amount, address): """ @@ -2659,7 +2635,6 @@ class Daemon(AuthJSONRPCServer): self.analytics_manager.send_credits_sent() defer.returnValue(True) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_wallet_send(self, amount, address=None, claim_id=None): """ @@ -2708,7 +2683,6 @@ class Daemon(AuthJSONRPCServer): self.analytics_manager.send_claim_action('new_support') defer.returnValue(result) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_wallet_prefill_addresses(self, num_addresses, amount, no_broadcast=False): """ @@ -2805,7 +2779,6 @@ class Daemon(AuthJSONRPCServer): d.addCallback(lambda r: self._render_response(r)) return d - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_blob_get(self, blob_hash, timeout=None, encoding=None, payment_rate_manager=None): """ @@ -2849,7 +2822,6 @@ class Daemon(AuthJSONRPCServer): response = yield self._render_response(result) defer.returnValue(response) - @AuthJSONRPCServer.auth_required @defer.inlineCallbacks def jsonrpc_blob_delete(self, blob_hash): """ diff --git a/lbrynet/daemon/DaemonCLI.py b/lbrynet/daemon/DaemonCLI.py index d8dd5404e..594a2f215 100644 --- a/lbrynet/daemon/DaemonCLI.py +++ b/lbrynet/daemon/DaemonCLI.py @@ -6,7 +6,7 @@ from docopt import docopt from collections import OrderedDict from lbrynet import conf from lbrynet.core import utils -from lbrynet.daemon.auth.client import JSONRPCException, LBRYAPIClient +from lbrynet.daemon.auth.client import JSONRPCException, LBRYAPIClient, AuthAPIClient from lbrynet.daemon.Daemon import LOADING_WALLET_CODE, Daemon from lbrynet.core.system_info import get_platform from jsonrpc.common import RPCError @@ -93,12 +93,19 @@ def main(): status = api.status() except URLError as err: if isinstance(err, HTTPError) and err.code == UNAUTHORIZED: - print_error("Daemon requires authentication, but none was provided.", - suggest_help=False) + api = AuthAPIClient.config() + # this can happen if the daemon is using auth with the --http-auth flag + # when the config setting is to not use it + try: + status = api.status() + except: + print_error("Daemon requires authentication, but none was provided.", + suggest_help=False) + return 1 else: print_error("Could not connect to daemon. Are you sure it's running?", suggest_help=False) - return 1 + return 1 status_code = status['startup_status']['code'] diff --git a/lbrynet/daemon/DaemonControl.py b/lbrynet/daemon/DaemonControl.py index 2638bca4b..23cd04450 100644 --- a/lbrynet/daemon/DaemonControl.py +++ b/lbrynet/daemon/DaemonControl.py @@ -32,12 +32,6 @@ def start(): type=str, default=None ) - parser.add_argument( - "--wallet", - help="lbryum or ptc for testing, default lbryum", - type=str, - default=conf.settings['wallet'] - ) parser.add_argument( "--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http'] ) @@ -82,23 +76,25 @@ def start(): if test_internet_connection(): analytics_manager = analytics.Manager.new_instance() - start_server_and_listen(args.useauth, analytics_manager) + start_server_and_listen(analytics_manager) reactor.run() else: log.info("Not connected to internet, unable to start") def update_settings_from_args(args): - conf.settings.update({ - 'use_auth_http': args.useauth, - 'wallet': args.wallet, - }, data_types=(conf.TYPE_CLI,)) + if args.conf: + conf.conf_file = args.conf + + if args.useauth: + conf.settings.update({ + 'use_auth_http': args.useauth, + }, data_types=(conf.TYPE_CLI,)) - conf.conf_file = args.conf @defer.inlineCallbacks -def start_server_and_listen(use_auth, analytics_manager): +def start_server_and_listen(analytics_manager): """ Args: use_auth: set to true to enable http authentication @@ -107,7 +103,7 @@ def start_server_and_listen(use_auth, analytics_manager): analytics_manager.send_server_startup() daemon_server = DaemonServer(analytics_manager) try: - yield daemon_server.start(use_auth) + yield daemon_server.start(conf.settings['use_auth_http']) analytics_manager.send_server_startup_success() except Exception as e: log.exception('Failed to start lbrynet-daemon') diff --git a/lbrynet/daemon/auth/auth.py b/lbrynet/daemon/auth/auth.py index b1a8ad702..368a4ccde 100644 --- a/lbrynet/daemon/auth/auth.py +++ b/lbrynet/daemon/auth/auth.py @@ -35,6 +35,7 @@ class PasswordChecker(object): @classmethod def load(cls, password_dict): passwords = {key: password_dict[key].secret for key in password_dict} + log.info("Loaded %i api key(s)", len(passwords)) return cls(passwords) def requestAvatarId(self, creds): @@ -45,4 +46,3 @@ class PasswordChecker(object): return defer.succeed(creds.username) log.warning('Incorrect username or password') return defer.fail(cred_error.UnauthorizedLogin('Incorrect username or password')) - diff --git a/lbrynet/daemon/auth/client.py b/lbrynet/daemon/auth/client.py index 3008bec8d..2e18ac748 100644 --- a/lbrynet/daemon/auth/client.py +++ b/lbrynet/daemon/auth/client.py @@ -1,13 +1,12 @@ -import urlparse -import logging -import requests import os -import base64 import json - -from lbrynet.daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME, get_auth_message -from lbrynet import conf +import urlparse +import requests +from requests.cookies import RequestsCookieJar +import logging from jsonrpc.proxy import JSONRPCProxy +from lbrynet import conf +from lbrynet.daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME, get_auth_message log = logging.getLogger(__name__) USER_AGENT = "AuthServiceProxy/0.1" @@ -16,6 +15,12 @@ LBRY_SECRET = "LBRY_SECRET" HTTP_TIMEOUT = 30 +def copy_cookies(cookies): + result = RequestsCookieJar() + result.update(cookies) + return result + + class JSONRPCException(Exception): def __init__(self, rpc_error): Exception.__init__(self) @@ -23,25 +28,25 @@ class JSONRPCException(Exception): class AuthAPIClient(object): - def __init__(self, key, timeout, connection, count, cookies, auth, url, login_url): + def __init__(self, key, timeout, connection, count, cookies, url, login_url): self.__api_key = key self.__service_url = login_url self.__id_count = count self.__url = url - self.__auth_header = auth self.__conn = connection - self.__cookies = cookies + self.__cookies = copy_cookies(cookies) def __getattr__(self, name): if name.startswith('__') and name.endswith('__'): - raise AttributeError # Python internal stuff + raise AttributeError(name) def f(*args): return self.call(name, args[0] if args else {}) return f - def call(self, method, params={}): + def call(self, method, params=None): + params = params or {} self.__id_count += 1 pre_auth_post_data = { 'version': '2', @@ -50,41 +55,27 @@ class AuthAPIClient(object): 'id': self.__id_count } to_auth = get_auth_message(pre_auth_post_data) - token = self.__api_key.get_hmac(to_auth) - pre_auth_post_data.update({'hmac': token}) + pre_auth_post_data.update({'hmac': self.__api_key.get_hmac(to_auth)}) post_data = json.dumps(pre_auth_post_data) - service_url = self.__service_url - auth_header = self.__auth_header - cookies = self.__cookies - host = self.__url.hostname - - req = requests.Request(method='POST', - url=service_url, - data=post_data, - headers={ - 'Host': host, - 'User-Agent': USER_AGENT, - 'Authorization': auth_header, - 'Content-type': 'application/json' - }, - cookies=cookies) - r = req.prepare() - http_response = self.__conn.send(r) - cookies = http_response.cookies - headers = http_response.headers - next_secret = headers.get(LBRY_SECRET, False) - if next_secret: - self.__api_key.secret = next_secret - self.__cookies = cookies - + cookies = copy_cookies(self.__cookies) + req = requests.Request( + method='POST', url=self.__service_url, data=post_data, cookies=cookies, + headers={ + 'Host': self.__url.hostname, + 'User-Agent': USER_AGENT, + 'Content-type': 'application/json' + } + ) + http_response = self.__conn.send(req.prepare()) if http_response is None: raise JSONRPCException({ 'code': -342, 'message': 'missing HTTP response from server'}) - http_response.raise_for_status() - + next_secret = http_response.headers.get(LBRY_SECRET, False) + if next_secret: + self.__api_key.secret = next_secret + self.__cookies = copy_cookies(http_response.cookies) response = http_response.json() - if response.get('error') is not None: raise JSONRPCException(response['error']) elif 'result' not in response: @@ -94,13 +85,10 @@ class AuthAPIClient(object): return response['result'] @classmethod - def config(cls, key_name=None, key=None, pw_path=None, - timeout=HTTP_TIMEOUT, - connection=None, count=0, - cookies=None, auth=None, - url=None, login_url=None): + def config(cls, key_name=None, key=None, pw_path=None, timeout=HTTP_TIMEOUT, connection=None, count=0, + cookies=None, auth=None, url=None, login_url=None): - api_key_name = API_KEY_NAME if not key_name else key_name + api_key_name = key_name or API_KEY_NAME pw_path = os.path.join(conf.settings['data_dir'], ".api_keys") if not pw_path else pw_path if not key: keys = load_api_keys(pw_path) @@ -118,41 +106,28 @@ class AuthAPIClient(object): id_count = count if auth is None and connection is None and cookies is None and url is None: - # This is a new client instance, initialize the auth header and start a session + # This is a new client instance, start an authenticated session url = urlparse.urlparse(service_url) - (user, passwd) = (url.username, url.password) - try: - user = user.encode('utf8') - except AttributeError: - pass - try: - passwd = passwd.encode('utf8') - except AttributeError: - pass - authpair = user + b':' + passwd - auth_header = b'Basic ' + base64.b64encode(authpair) conn = requests.Session() - conn.auth = (user, passwd) req = requests.Request(method='POST', url=service_url, - auth=conn.auth, headers={'Host': url.hostname, 'User-Agent': USER_AGENT, - 'Authorization': auth_header, 'Content-type': 'application/json'},) r = req.prepare() http_response = conn.send(r) - cookies = http_response.cookies + cookies = RequestsCookieJar() + cookies.update(http_response.cookies) uid = cookies.get(TWISTED_SESSION) api_key = APIKey.new(seed=uid) else: # This is a client that already has a session, use it - auth_header = auth conn = connection - assert cookies.get(LBRY_SECRET, False), "Missing cookie" + if not cookies.get(LBRY_SECRET): + raise Exception("Missing cookie") secret = cookies.get(LBRY_SECRET) api_key = APIKey(secret, api_key_name) - return cls(api_key, timeout, conn, id_count, cookies, auth_header, url, service_url) + return cls(api_key, timeout, conn, id_count, cookies, url, service_url) class LBRYAPIClient(object): diff --git a/lbrynet/daemon/auth/server.py b/lbrynet/daemon/auth/server.py index af08f8c46..3b3581099 100644 --- a/lbrynet/daemon/auth/server.py +++ b/lbrynet/daemon/auth/server.py @@ -13,7 +13,7 @@ from txjsonrpc import jsonrpclib from traceback import format_exc from lbrynet import conf -from lbrynet.core.Error import InvalidAuthenticationToken +from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError from lbrynet.core import utils from lbrynet.daemon.auth.util import APIKey, get_auth_message from lbrynet.daemon.auth.client import LBRY_SECRET @@ -119,15 +119,12 @@ class JSONRPCServerType(type): klass = type.__new__(mcs, name, bases, newattrs) klass.callable_methods = {} klass.deprecated_methods = {} - klass.authorized_functions = [] for methodname in dir(klass): if methodname.startswith("jsonrpc_"): method = getattr(klass, methodname) if not hasattr(method, '_deprecated'): klass.callable_methods.update({methodname.split("jsonrpc_")[1]: method}) - if hasattr(method, '_auth_required'): - klass.authorized_functions.append(methodname.split("jsonrpc_")[1]) else: klass.deprecated_methods.update({methodname.split("jsonrpc_")[1]: method}) return klass @@ -136,11 +133,6 @@ class JSONRPCServerType(type): class AuthorizedBase(object): __metaclass__ = JSONRPCServerType - @staticmethod - def auth_required(f): - f._auth_required = True - return f - @staticmethod def deprecated(new_command=None): def _deprecated_wrapper(f): @@ -151,26 +143,28 @@ class AuthorizedBase(object): class AuthJSONRPCServer(AuthorizedBase): - """Authorized JSONRPC server used as the base class for the LBRY API + """ + Authorized JSONRPC server used as the base class for the LBRY API API methods are named with a leading "jsonrpc_" - Decorators: - - @AuthJSONRPCServer.auth_required: this requires that the client - include a valid hmac authentication token in their request - Attributes: - allowed_during_startup (list): list of api methods that are - callable before the server has finished startup + allowed_during_startup (list): list of api methods that are callable before the server has finished startup + sessions (dict): (dict): {: } + callable_methods (dict): {: } - sessions (dict): dictionary of active session_id: - lbrynet.lbrynet_daemon.auth.util.APIKey values + Authentication: + If use_authentication is true, basic HTTP and HMAC authentication will be used for all requests and the + service url will require a username and password. - authorized_functions (list): list of api methods that require authentication - - callable_methods (dict): dictionary of api_callable_name: method values + To start an authenticated session a client sends an HTTP POST to :@:. + If accepted, the server replies with a TWISTED_SESSION cookie containing a session id and the message "OK". + The client initializes their shared secret for hmac to be the b64 encoded sha256 of their session id. + To send an authenticated request a client sends an HTTP POST to the auth api url with the TWISTED_SESSION + cookie and includes a hmac token in the message using the previously set shared secret. If the token is valid + the server will randomize the shared secret and return the new value under the LBRY_SECRET header, which the + client uses to generate the token for their next request. """ implements(resource.IResource) @@ -178,9 +172,7 @@ class AuthJSONRPCServer(AuthorizedBase): allowed_during_startup = [] def __init__(self, use_authentication=None): - self._use_authentication = ( - use_authentication if use_authentication is not None else conf.settings['use_auth_http'] - ) + self._use_authentication = use_authentication or conf.settings['use_auth_http'] self.announced_startup = False self.sessions = {} @@ -239,7 +231,9 @@ class AuthJSONRPCServer(AuthorizedBase): def _render(self, request): time_in = utils.now() - # assert self._check_headers(request), InvalidHeaderError + if not self._check_headers(request): + self._render_error(Failure(InvalidHeaderError()), request, None) + return server.NOT_DONE_YET session = request.getSession() session_id = session.uid finished_deferred = request.notifyFinish() @@ -270,36 +264,36 @@ class AuthJSONRPCServer(AuthorizedBase): self._render_error(JSONRPCError(None, JSONRPCError.CODE_PARSE_ERROR), request, None) return server.NOT_DONE_YET - id_ = None + request_id = None try: function_name = parsed.get('method') args = parsed.get('params', {}) - id_ = parsed.get('id', None) + request_id = parsed.get('id', None) token = parsed.pop('hmac', None) except AttributeError as err: log.warning(err) self._render_error( - JSONRPCError(None, code=JSONRPCError.CODE_INVALID_REQUEST), request, id_ + JSONRPCError(None, code=JSONRPCError.CODE_INVALID_REQUEST), request, request_id ) return server.NOT_DONE_YET reply_with_next_secret = False if self._use_authentication: - if function_name in self.authorized_functions: - try: - self._verify_token(session_id, parsed, token) - except InvalidAuthenticationToken as err: - log.warning("API validation failed") - self._render_error( - JSONRPCError.create_from_exception( - err.message, code=JSONRPCError.CODE_AUTHENTICATION_ERROR, - traceback=format_exc() - ), - request, id_ - ) - return server.NOT_DONE_YET - self._update_session_secret(session_id) - reply_with_next_secret = True + try: + self._verify_token(session_id, parsed, token) + except InvalidAuthenticationToken as err: + log.warning("API validation failed") + self._render_error( + JSONRPCError.create_from_exception( + err, code=JSONRPCError.CODE_AUTHENTICATION_ERROR, + traceback=format_exc() + ), + request, request_id + ) + return server.NOT_DONE_YET + request.addCookie("TWISTED_SESSION", session_id) + self._update_session_secret(session_id) + reply_with_next_secret = True try: fn = self._get_jsonrpc_method(function_name) @@ -307,7 +301,7 @@ class AuthJSONRPCServer(AuthorizedBase): log.warning('Failed to get function %s: %s', function_name, err) self._render_error( JSONRPCError(None, JSONRPCError.CODE_METHOD_NOT_FOUND), - request, id_ + request, request_id ) return server.NOT_DONE_YET except NotAllowedDuringStartupError: @@ -315,7 +309,7 @@ class AuthJSONRPCServer(AuthorizedBase): self._render_error( JSONRPCError("This method is unavailable until the daemon is fully started", code=JSONRPCError.CODE_INVALID_REQUEST), - request, id_ + request, request_id ) return server.NOT_DONE_YET @@ -341,7 +335,7 @@ class AuthJSONRPCServer(AuthorizedBase): log.warning(params_error_message) self._render_error( JSONRPCError(params_error_message, code=JSONRPCError.CODE_INVALID_PARAMS), - request, id_ + request, request_id ) return server.NOT_DONE_YET @@ -353,12 +347,9 @@ class AuthJSONRPCServer(AuthorizedBase): # request.finish() from being called on a closed request. finished_deferred.addErrback(self._handle_dropped_request, d, function_name) - d.addCallback(self._callback_render, request, id_, reply_with_next_secret) - # TODO: don't trap RuntimeError, which is presently caught to - # handle deferredLists that won't peacefully cancel, namely - # get_lbry_files - d.addErrback(trap, ConnectionDone, ConnectionLost, defer.CancelledError, RuntimeError) - d.addErrback(self._render_error, request, id_) + d.addCallback(self._callback_render, request, request_id, reply_with_next_secret) + d.addErrback(trap, ConnectionDone, ConnectionLost, defer.CancelledError) + d.addErrback(self._render_error, request, request_id) d.addBoth(lambda _: log.debug("%s took %f", function_name, (utils.now() - time_in).total_seconds())) @@ -371,7 +362,7 @@ class AuthJSONRPCServer(AuthorizedBase): @param session_id: @return: secret """ - log.info("Register api session") + log.info("Started new api session") token = APIKey.new(seed=session_id) self.sessions.update({session_id: token}) @@ -382,7 +373,8 @@ class AuthJSONRPCServer(AuthorizedBase): def _check_headers(self, request): return ( self._check_header_source(request, 'Origin') and - self._check_header_source(request, 'Referer')) + self._check_header_source(request, 'Referer') + ) def _check_header_source(self, request, header): """Check if the source of the request is allowed based on the header value.""" @@ -461,7 +453,7 @@ class AuthJSONRPCServer(AuthorizedBase): return None, None def _initialize_session(self, session_id): - if not self.sessions.get(session_id, False): + if not self.sessions.get(session_id): self._register_user_session(session_id) return True return False diff --git a/lbrynet/daemon/auth/util.py b/lbrynet/daemon/auth/util.py index eaef0896b..7db751248 100644 --- a/lbrynet/daemon/auth/util.py +++ b/lbrynet/daemon/auth/util.py @@ -46,12 +46,13 @@ class APIKey(object): def compare_hmac(self, message, token): decoded_token = base58.b58decode(token) target = base58.b58decode(self.get_hmac(message)) + try: - assert len(decoded_token) == len(target), "Length mismatch" - r = hmac.compare_digest(decoded_token, target) + if len(decoded_token) != len(target): + return False + return hmac.compare_digest(decoded_token, target) except: return False - return r def load_api_keys(path): @@ -67,7 +68,6 @@ def load_api_keys(path): secret = key['secret'] expiration = key['expiration'] keys_for_return.update({key_name: APIKey(secret, key_name, expiration)}) - return keys_for_return @@ -81,11 +81,10 @@ def save_api_keys(keys, path): def initialize_api_key_file(key_path): - if not os.path.isfile(key_path): - keys = {} - new_api_key = APIKey.new(name=API_KEY_NAME) - keys.update({new_api_key.name: new_api_key}) - save_api_keys(keys, key_path) + keys = {} + new_api_key = APIKey.new(name=API_KEY_NAME) + keys.update({new_api_key.name: new_api_key}) + save_api_keys(keys, key_path) def get_auth_message(message_dict):