fix lbrynet-cli when using authentication

-add explanation of daemon authentication to AuthJSONRPCServer docstring
-remove auth_required decorator, use auth for all api methods if use_authentication is true
-fix issues with the command line --http-auth flag to lbrynet-daemon and the use_http_auth setting in the config file
This commit is contained in:
Jack Robison 2018-03-31 18:42:57 -04:00
parent ea5190aa9a
commit 743ae59d54
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
8 changed files with 126 additions and 178 deletions

View file

@ -15,6 +15,9 @@ at anytime.
### Fixed ### Fixed
* handling error from dht clients with old `ping` method * 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 * 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 ### Deprecated
* *
@ -36,6 +39,8 @@ at anytime.
* dht logging to be more verbose with errors and warnings * dht logging to be more verbose with errors and warnings
* added `single_announce` and `last_announced_time` columns to the `blob` table in sqlite * 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 * 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 ### Added
* virtual kademlia network and mock udp transport for dht integration tests * virtual kademlia network and mock udp transport for dht integration tests
@ -45,6 +50,8 @@ at anytime.
### Removed ### Removed
* `announce_all` argument from `blob_announce` * `announce_all` argument from `blob_announce`
* old `blob_announce_all` command * 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 ## [0.19.2] - 2018-03-28

View file

@ -1150,7 +1150,6 @@ class Daemon(AuthJSONRPCServer):
""" """
return self._render_response(conf.settings.get_adjustable_settings_dict()) return self._render_response(conf.settings.get_adjustable_settings_dict())
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_settings_set(self, **kwargs): def jsonrpc_settings_set(self, **kwargs):
""" """
@ -1495,7 +1494,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(claim_results) response = yield self._render_response(claim_results)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_resolve(self, force=False, uri=None, uris=[]): def jsonrpc_resolve(self, force=False, uri=None, uris=[]):
""" """
@ -1586,7 +1584,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(results) response = yield self._render_response(results)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_get(self, uri, file_name=None, timeout=None): def jsonrpc_get(self, uri, file_name=None, timeout=None):
""" """
@ -1675,7 +1672,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(result) response = yield self._render_response(result)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_file_set_status(self, status, **kwargs): def jsonrpc_file_set_status(self, status, **kwargs):
""" """
@ -1716,7 +1712,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(msg) response = yield self._render_response(msg)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_file_delete(self, delete_from_download_dir=False, delete_all=False, **kwargs): 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) cost = yield self.get_est_cost(uri, size)
defer.returnValue(cost) defer.returnValue(cost)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_channel_new(self, channel_name, amount): def jsonrpc_channel_new(self, channel_name, amount):
""" """
@ -1852,7 +1846,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(result) response = yield self._render_response(result)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_channel_list(self): def jsonrpc_channel_list(self):
""" """
@ -1874,7 +1867,6 @@ class Daemon(AuthJSONRPCServer):
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.deprecated("channel_list") @AuthJSONRPCServer.deprecated("channel_list")
@AuthJSONRPCServer.auth_required
def jsonrpc_channel_list_mine(self): def jsonrpc_channel_list_mine(self):
""" """
Get certificate claim infos for channels that can be published to (deprecated) Get certificate claim infos for channels that can be published to (deprecated)
@ -1891,7 +1883,6 @@ class Daemon(AuthJSONRPCServer):
return self.jsonrpc_channel_list() return self.jsonrpc_channel_list()
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_channel_export(self, claim_id): def jsonrpc_channel_export(self, claim_id):
""" """
@ -1910,7 +1901,6 @@ class Daemon(AuthJSONRPCServer):
result = yield self.session.wallet.export_certificate_info(claim_id) result = yield self.session.wallet.export_certificate_info(claim_id)
defer.returnValue(result) defer.returnValue(result)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_channel_import(self, serialized_certificate_info): 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) result = yield self.session.wallet.import_certificate_info(serialized_certificate_info)
defer.returnValue(result) defer.returnValue(result)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_publish(self, name, bid, metadata=None, file_path=None, fee=None, title=None, def jsonrpc_publish(self, name, bid, metadata=None, file_path=None, fee=None, title=None,
description=None, author=None, language=None, license=None, description=None, author=None, language=None, license=None,
@ -2139,7 +2128,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(result) response = yield self._render_response(result)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_claim_abandon(self, claim_id=None, txid=None, nout=None): 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') self.analytics_manager.send_claim_action('abandon')
defer.returnValue(result) defer.returnValue(result)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_claim_new_support(self, name, claim_id, amount): 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') self.analytics_manager.send_claim_action('new_support')
defer.returnValue(result) defer.returnValue(result)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_claim_renew(self, outpoint=None, height=None): 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) result = yield self.session.wallet.claim_renew_all_before_expiration(height)
defer.returnValue(result) defer.returnValue(result)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_claim_send_to_address(self, claim_id, address, amount=None): def jsonrpc_claim_send_to_address(self, claim_id, address, amount=None):
""" """
@ -2276,7 +2261,6 @@ class Daemon(AuthJSONRPCServer):
defer.returnValue(response) defer.returnValue(response)
# TODO: claim_list_mine should be merged into claim_list, but idk how to authenticate it -Grin # 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): def jsonrpc_claim_list_mine(self):
""" """
List my name claims List my name claims
@ -2351,7 +2335,6 @@ class Daemon(AuthJSONRPCServer):
claims = yield self.session.wallet.get_claims_for_name(name) claims = yield self.session.wallet.get_claims_for_name(name)
defer.returnValue(claims) defer.returnValue(claims)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_claim_list_by_channel(self, page=0, page_size=10, uri=None, uris=[]): 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) response = yield self._render_response(results)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
def jsonrpc_transaction_list(self): def jsonrpc_transaction_list(self):
""" """
List transactions belonging to wallet List transactions belonging to wallet
@ -2521,7 +2503,6 @@ class Daemon(AuthJSONRPCServer):
d.addCallback(lambda r: self._render_response(r)) d.addCallback(lambda r: self._render_response(r))
return d return d
@AuthJSONRPCServer.auth_required
def jsonrpc_wallet_is_address_mine(self, address): def jsonrpc_wallet_is_address_mine(self, address):
""" """
Checks if an address is associated with the current wallet. 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)) d.addCallback(lambda is_mine: self._render_response(is_mine))
return d return d
@AuthJSONRPCServer.auth_required
def jsonrpc_wallet_public_key(self, address): def jsonrpc_wallet_public_key(self, address):
""" """
Get public key from wallet address Get public key from wallet address
@ -2560,7 +2540,6 @@ class Daemon(AuthJSONRPCServer):
d.addCallback(lambda r: self._render_response(r)) d.addCallback(lambda r: self._render_response(r))
return d return d
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_wallet_list(self): def jsonrpc_wallet_list(self):
""" """
@ -2580,7 +2559,6 @@ class Daemon(AuthJSONRPCServer):
response = yield self._render_response(addresses) response = yield self._render_response(addresses)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
def jsonrpc_wallet_new_address(self): def jsonrpc_wallet_new_address(self):
""" """
Generate a new wallet address Generate a new wallet address
@ -2604,7 +2582,6 @@ class Daemon(AuthJSONRPCServer):
d.addCallback(lambda address: self._render_response(address)) d.addCallback(lambda address: self._render_response(address))
return d return d
@AuthJSONRPCServer.auth_required
def jsonrpc_wallet_unused_address(self): def jsonrpc_wallet_unused_address(self):
""" """
Return an address containing no balance, will create Return an address containing no balance, will create
@ -2630,7 +2607,6 @@ class Daemon(AuthJSONRPCServer):
return d return d
@AuthJSONRPCServer.deprecated("wallet_send") @AuthJSONRPCServer.deprecated("wallet_send")
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_send_amount_to_address(self, amount, address): def jsonrpc_send_amount_to_address(self, amount, address):
""" """
@ -2659,7 +2635,6 @@ class Daemon(AuthJSONRPCServer):
self.analytics_manager.send_credits_sent() self.analytics_manager.send_credits_sent()
defer.returnValue(True) defer.returnValue(True)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_wallet_send(self, amount, address=None, claim_id=None): 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') self.analytics_manager.send_claim_action('new_support')
defer.returnValue(result) defer.returnValue(result)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_wallet_prefill_addresses(self, num_addresses, amount, no_broadcast=False): 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)) d.addCallback(lambda r: self._render_response(r))
return d return d
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_blob_get(self, blob_hash, timeout=None, encoding=None, payment_rate_manager=None): 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) response = yield self._render_response(result)
defer.returnValue(response) defer.returnValue(response)
@AuthJSONRPCServer.auth_required
@defer.inlineCallbacks @defer.inlineCallbacks
def jsonrpc_blob_delete(self, blob_hash): def jsonrpc_blob_delete(self, blob_hash):
""" """

View file

@ -6,7 +6,7 @@ from docopt import docopt
from collections import OrderedDict from collections import OrderedDict
from lbrynet import conf from lbrynet import conf
from lbrynet.core import utils 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.daemon.Daemon import LOADING_WALLET_CODE, Daemon
from lbrynet.core.system_info import get_platform from lbrynet.core.system_info import get_platform
from jsonrpc.common import RPCError from jsonrpc.common import RPCError
@ -93,12 +93,19 @@ def main():
status = api.status() status = api.status()
except URLError as err: except URLError as err:
if isinstance(err, HTTPError) and err.code == UNAUTHORIZED: if isinstance(err, HTTPError) and err.code == UNAUTHORIZED:
print_error("Daemon requires authentication, but none was provided.", api = AuthAPIClient.config()
suggest_help=False) # 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: else:
print_error("Could not connect to daemon. Are you sure it's running?", print_error("Could not connect to daemon. Are you sure it's running?",
suggest_help=False) suggest_help=False)
return 1 return 1
status_code = status['startup_status']['code'] status_code = status['startup_status']['code']

View file

@ -32,12 +32,6 @@ def start():
type=str, type=str,
default=None default=None
) )
parser.add_argument(
"--wallet",
help="lbryum or ptc for testing, default lbryum",
type=str,
default=conf.settings['wallet']
)
parser.add_argument( parser.add_argument(
"--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http'] "--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http']
) )
@ -82,23 +76,25 @@ def start():
if test_internet_connection(): if test_internet_connection():
analytics_manager = analytics.Manager.new_instance() analytics_manager = analytics.Manager.new_instance()
start_server_and_listen(args.useauth, analytics_manager) start_server_and_listen(analytics_manager)
reactor.run() reactor.run()
else: else:
log.info("Not connected to internet, unable to start") log.info("Not connected to internet, unable to start")
def update_settings_from_args(args): def update_settings_from_args(args):
conf.settings.update({ if args.conf:
'use_auth_http': args.useauth, conf.conf_file = args.conf
'wallet': args.wallet,
}, data_types=(conf.TYPE_CLI,)) if args.useauth:
conf.settings.update({
'use_auth_http': args.useauth,
}, data_types=(conf.TYPE_CLI,))
conf.conf_file = args.conf
@defer.inlineCallbacks @defer.inlineCallbacks
def start_server_and_listen(use_auth, analytics_manager): def start_server_and_listen(analytics_manager):
""" """
Args: Args:
use_auth: set to true to enable http authentication 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() analytics_manager.send_server_startup()
daemon_server = DaemonServer(analytics_manager) daemon_server = DaemonServer(analytics_manager)
try: try:
yield daemon_server.start(use_auth) yield daemon_server.start(conf.settings['use_auth_http'])
analytics_manager.send_server_startup_success() analytics_manager.send_server_startup_success()
except Exception as e: except Exception as e:
log.exception('Failed to start lbrynet-daemon') log.exception('Failed to start lbrynet-daemon')

View file

@ -35,6 +35,7 @@ class PasswordChecker(object):
@classmethod @classmethod
def load(cls, password_dict): def load(cls, password_dict):
passwords = {key: password_dict[key].secret for key in 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) return cls(passwords)
def requestAvatarId(self, creds): def requestAvatarId(self, creds):
@ -45,4 +46,3 @@ class PasswordChecker(object):
return defer.succeed(creds.username) return defer.succeed(creds.username)
log.warning('Incorrect username or password') log.warning('Incorrect username or password')
return defer.fail(cred_error.UnauthorizedLogin('Incorrect username or password')) return defer.fail(cred_error.UnauthorizedLogin('Incorrect username or password'))

View file

@ -1,13 +1,12 @@
import urlparse
import logging
import requests
import os import os
import base64
import json import json
import urlparse
from lbrynet.daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME, get_auth_message import requests
from lbrynet import conf from requests.cookies import RequestsCookieJar
import logging
from jsonrpc.proxy import JSONRPCProxy 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__) log = logging.getLogger(__name__)
USER_AGENT = "AuthServiceProxy/0.1" USER_AGENT = "AuthServiceProxy/0.1"
@ -16,6 +15,12 @@ LBRY_SECRET = "LBRY_SECRET"
HTTP_TIMEOUT = 30 HTTP_TIMEOUT = 30
def copy_cookies(cookies):
result = RequestsCookieJar()
result.update(cookies)
return result
class JSONRPCException(Exception): class JSONRPCException(Exception):
def __init__(self, rpc_error): def __init__(self, rpc_error):
Exception.__init__(self) Exception.__init__(self)
@ -23,25 +28,25 @@ class JSONRPCException(Exception):
class AuthAPIClient(object): 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.__api_key = key
self.__service_url = login_url self.__service_url = login_url
self.__id_count = count self.__id_count = count
self.__url = url self.__url = url
self.__auth_header = auth
self.__conn = connection self.__conn = connection
self.__cookies = cookies self.__cookies = copy_cookies(cookies)
def __getattr__(self, name): def __getattr__(self, name):
if name.startswith('__') and name.endswith('__'): if name.startswith('__') and name.endswith('__'):
raise AttributeError # Python internal stuff raise AttributeError(name)
def f(*args): def f(*args):
return self.call(name, args[0] if args else {}) return self.call(name, args[0] if args else {})
return f return f
def call(self, method, params={}): def call(self, method, params=None):
params = params or {}
self.__id_count += 1 self.__id_count += 1
pre_auth_post_data = { pre_auth_post_data = {
'version': '2', 'version': '2',
@ -50,41 +55,27 @@ class AuthAPIClient(object):
'id': self.__id_count 'id': self.__id_count
} }
to_auth = get_auth_message(pre_auth_post_data) to_auth = get_auth_message(pre_auth_post_data)
token = self.__api_key.get_hmac(to_auth) pre_auth_post_data.update({'hmac': self.__api_key.get_hmac(to_auth)})
pre_auth_post_data.update({'hmac': token})
post_data = json.dumps(pre_auth_post_data) post_data = json.dumps(pre_auth_post_data)
service_url = self.__service_url cookies = copy_cookies(self.__cookies)
auth_header = self.__auth_header req = requests.Request(
cookies = self.__cookies method='POST', url=self.__service_url, data=post_data, cookies=cookies,
host = self.__url.hostname headers={
'Host': self.__url.hostname,
req = requests.Request(method='POST', 'User-Agent': USER_AGENT,
url=service_url, 'Content-type': 'application/json'
data=post_data, }
headers={ )
'Host': host, http_response = self.__conn.send(req.prepare())
'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
if http_response is None: if http_response is None:
raise JSONRPCException({ raise JSONRPCException({
'code': -342, 'message': 'missing HTTP response from server'}) 'code': -342, 'message': 'missing HTTP response from server'})
http_response.raise_for_status() 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() response = http_response.json()
if response.get('error') is not None: if response.get('error') is not None:
raise JSONRPCException(response['error']) raise JSONRPCException(response['error'])
elif 'result' not in response: elif 'result' not in response:
@ -94,13 +85,10 @@ class AuthAPIClient(object):
return response['result'] return response['result']
@classmethod @classmethod
def config(cls, key_name=None, key=None, pw_path=None, def config(cls, key_name=None, key=None, pw_path=None, timeout=HTTP_TIMEOUT, connection=None, count=0,
timeout=HTTP_TIMEOUT, cookies=None, auth=None, url=None, login_url=None):
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 pw_path = os.path.join(conf.settings['data_dir'], ".api_keys") if not pw_path else pw_path
if not key: if not key:
keys = load_api_keys(pw_path) keys = load_api_keys(pw_path)
@ -118,41 +106,28 @@ class AuthAPIClient(object):
id_count = count id_count = count
if auth is None and connection is None and cookies is None and url is None: 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) 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 = requests.Session()
conn.auth = (user, passwd)
req = requests.Request(method='POST', req = requests.Request(method='POST',
url=service_url, url=service_url,
auth=conn.auth,
headers={'Host': url.hostname, headers={'Host': url.hostname,
'User-Agent': USER_AGENT, 'User-Agent': USER_AGENT,
'Authorization': auth_header,
'Content-type': 'application/json'},) 'Content-type': 'application/json'},)
r = req.prepare() r = req.prepare()
http_response = conn.send(r) http_response = conn.send(r)
cookies = http_response.cookies cookies = RequestsCookieJar()
cookies.update(http_response.cookies)
uid = cookies.get(TWISTED_SESSION) uid = cookies.get(TWISTED_SESSION)
api_key = APIKey.new(seed=uid) api_key = APIKey.new(seed=uid)
else: else:
# This is a client that already has a session, use it # This is a client that already has a session, use it
auth_header = auth
conn = connection 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) secret = cookies.get(LBRY_SECRET)
api_key = APIKey(secret, api_key_name) 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): class LBRYAPIClient(object):

View file

@ -13,7 +13,7 @@ from txjsonrpc import jsonrpclib
from traceback import format_exc from traceback import format_exc
from lbrynet import conf from lbrynet import conf
from lbrynet.core.Error import InvalidAuthenticationToken from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError
from lbrynet.core import utils from lbrynet.core import utils
from lbrynet.daemon.auth.util import APIKey, get_auth_message from lbrynet.daemon.auth.util import APIKey, get_auth_message
from lbrynet.daemon.auth.client import LBRY_SECRET from lbrynet.daemon.auth.client import LBRY_SECRET
@ -119,15 +119,12 @@ class JSONRPCServerType(type):
klass = type.__new__(mcs, name, bases, newattrs) klass = type.__new__(mcs, name, bases, newattrs)
klass.callable_methods = {} klass.callable_methods = {}
klass.deprecated_methods = {} klass.deprecated_methods = {}
klass.authorized_functions = []
for methodname in dir(klass): for methodname in dir(klass):
if methodname.startswith("jsonrpc_"): if methodname.startswith("jsonrpc_"):
method = getattr(klass, methodname) method = getattr(klass, methodname)
if not hasattr(method, '_deprecated'): if not hasattr(method, '_deprecated'):
klass.callable_methods.update({methodname.split("jsonrpc_")[1]: method}) klass.callable_methods.update({methodname.split("jsonrpc_")[1]: method})
if hasattr(method, '_auth_required'):
klass.authorized_functions.append(methodname.split("jsonrpc_")[1])
else: else:
klass.deprecated_methods.update({methodname.split("jsonrpc_")[1]: method}) klass.deprecated_methods.update({methodname.split("jsonrpc_")[1]: method})
return klass return klass
@ -136,11 +133,6 @@ class JSONRPCServerType(type):
class AuthorizedBase(object): class AuthorizedBase(object):
__metaclass__ = JSONRPCServerType __metaclass__ = JSONRPCServerType
@staticmethod
def auth_required(f):
f._auth_required = True
return f
@staticmethod @staticmethod
def deprecated(new_command=None): def deprecated(new_command=None):
def _deprecated_wrapper(f): def _deprecated_wrapper(f):
@ -151,26 +143,28 @@ class AuthorizedBase(object):
class AuthJSONRPCServer(AuthorizedBase): 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_" 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: Attributes:
allowed_during_startup (list): list of api methods that are allowed_during_startup (list): list of api methods that are callable before the server has finished startup
callable before the server has finished startup sessions (dict): (dict): {<session id>: <lbrynet.daemon.auth.util.APIKey>}
callable_methods (dict): {<api method name>: <api method>}
sessions (dict): dictionary of active session_id: Authentication:
lbrynet.lbrynet_daemon.auth.util.APIKey values 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 To start an authenticated session a client sends an HTTP POST to <user>:<password>@<api host>:<api port>.
If accepted, the server replies with a TWISTED_SESSION cookie containing a session id and the message "OK".
callable_methods (dict): dictionary of api_callable_name: method values 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) implements(resource.IResource)
@ -178,9 +172,7 @@ class AuthJSONRPCServer(AuthorizedBase):
allowed_during_startup = [] allowed_during_startup = []
def __init__(self, use_authentication=None): def __init__(self, use_authentication=None):
self._use_authentication = ( self._use_authentication = use_authentication or conf.settings['use_auth_http']
use_authentication if use_authentication is not None else conf.settings['use_auth_http']
)
self.announced_startup = False self.announced_startup = False
self.sessions = {} self.sessions = {}
@ -239,7 +231,9 @@ class AuthJSONRPCServer(AuthorizedBase):
def _render(self, request): def _render(self, request):
time_in = utils.now() 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 = request.getSession()
session_id = session.uid session_id = session.uid
finished_deferred = request.notifyFinish() finished_deferred = request.notifyFinish()
@ -270,36 +264,36 @@ class AuthJSONRPCServer(AuthorizedBase):
self._render_error(JSONRPCError(None, JSONRPCError.CODE_PARSE_ERROR), request, None) self._render_error(JSONRPCError(None, JSONRPCError.CODE_PARSE_ERROR), request, None)
return server.NOT_DONE_YET return server.NOT_DONE_YET
id_ = None request_id = None
try: try:
function_name = parsed.get('method') function_name = parsed.get('method')
args = parsed.get('params', {}) args = parsed.get('params', {})
id_ = parsed.get('id', None) request_id = parsed.get('id', None)
token = parsed.pop('hmac', None) token = parsed.pop('hmac', None)
except AttributeError as err: except AttributeError as err:
log.warning(err) log.warning(err)
self._render_error( 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 return server.NOT_DONE_YET
reply_with_next_secret = False reply_with_next_secret = False
if self._use_authentication: if self._use_authentication:
if function_name in self.authorized_functions: try:
try: self._verify_token(session_id, parsed, token)
self._verify_token(session_id, parsed, token) except InvalidAuthenticationToken as err:
except InvalidAuthenticationToken as err: log.warning("API validation failed")
log.warning("API validation failed") self._render_error(
self._render_error( JSONRPCError.create_from_exception(
JSONRPCError.create_from_exception( err, code=JSONRPCError.CODE_AUTHENTICATION_ERROR,
err.message, code=JSONRPCError.CODE_AUTHENTICATION_ERROR, traceback=format_exc()
traceback=format_exc() ),
), request, request_id
request, id_ )
) return server.NOT_DONE_YET
return server.NOT_DONE_YET request.addCookie("TWISTED_SESSION", session_id)
self._update_session_secret(session_id) self._update_session_secret(session_id)
reply_with_next_secret = True reply_with_next_secret = True
try: try:
fn = self._get_jsonrpc_method(function_name) 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) log.warning('Failed to get function %s: %s', function_name, err)
self._render_error( self._render_error(
JSONRPCError(None, JSONRPCError.CODE_METHOD_NOT_FOUND), JSONRPCError(None, JSONRPCError.CODE_METHOD_NOT_FOUND),
request, id_ request, request_id
) )
return server.NOT_DONE_YET return server.NOT_DONE_YET
except NotAllowedDuringStartupError: except NotAllowedDuringStartupError:
@ -315,7 +309,7 @@ class AuthJSONRPCServer(AuthorizedBase):
self._render_error( self._render_error(
JSONRPCError("This method is unavailable until the daemon is fully started", JSONRPCError("This method is unavailable until the daemon is fully started",
code=JSONRPCError.CODE_INVALID_REQUEST), code=JSONRPCError.CODE_INVALID_REQUEST),
request, id_ request, request_id
) )
return server.NOT_DONE_YET return server.NOT_DONE_YET
@ -341,7 +335,7 @@ class AuthJSONRPCServer(AuthorizedBase):
log.warning(params_error_message) log.warning(params_error_message)
self._render_error( self._render_error(
JSONRPCError(params_error_message, code=JSONRPCError.CODE_INVALID_PARAMS), JSONRPCError(params_error_message, code=JSONRPCError.CODE_INVALID_PARAMS),
request, id_ request, request_id
) )
return server.NOT_DONE_YET return server.NOT_DONE_YET
@ -353,12 +347,9 @@ class AuthJSONRPCServer(AuthorizedBase):
# request.finish() from being called on a closed request. # request.finish() from being called on a closed request.
finished_deferred.addErrback(self._handle_dropped_request, d, function_name) finished_deferred.addErrback(self._handle_dropped_request, d, function_name)
d.addCallback(self._callback_render, request, id_, reply_with_next_secret) d.addCallback(self._callback_render, request, request_id, reply_with_next_secret)
# TODO: don't trap RuntimeError, which is presently caught to d.addErrback(trap, ConnectionDone, ConnectionLost, defer.CancelledError)
# handle deferredLists that won't peacefully cancel, namely d.addErrback(self._render_error, request, request_id)
# get_lbry_files
d.addErrback(trap, ConnectionDone, ConnectionLost, defer.CancelledError, RuntimeError)
d.addErrback(self._render_error, request, id_)
d.addBoth(lambda _: log.debug("%s took %f", d.addBoth(lambda _: log.debug("%s took %f",
function_name, function_name,
(utils.now() - time_in).total_seconds())) (utils.now() - time_in).total_seconds()))
@ -371,7 +362,7 @@ class AuthJSONRPCServer(AuthorizedBase):
@param session_id: @param session_id:
@return: secret @return: secret
""" """
log.info("Register api session") log.info("Started new api session")
token = APIKey.new(seed=session_id) token = APIKey.new(seed=session_id)
self.sessions.update({session_id: token}) self.sessions.update({session_id: token})
@ -382,7 +373,8 @@ class AuthJSONRPCServer(AuthorizedBase):
def _check_headers(self, request): def _check_headers(self, request):
return ( return (
self._check_header_source(request, 'Origin') and 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): def _check_header_source(self, request, header):
"""Check if the source of the request is allowed based on the header value.""" """Check if the source of the request is allowed based on the header value."""
@ -461,7 +453,7 @@ class AuthJSONRPCServer(AuthorizedBase):
return None, None return None, None
def _initialize_session(self, session_id): 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) self._register_user_session(session_id)
return True return True
return False return False

View file

@ -46,12 +46,13 @@ class APIKey(object):
def compare_hmac(self, message, token): def compare_hmac(self, message, token):
decoded_token = base58.b58decode(token) decoded_token = base58.b58decode(token)
target = base58.b58decode(self.get_hmac(message)) target = base58.b58decode(self.get_hmac(message))
try: try:
assert len(decoded_token) == len(target), "Length mismatch" if len(decoded_token) != len(target):
r = hmac.compare_digest(decoded_token, target) return False
return hmac.compare_digest(decoded_token, target)
except: except:
return False return False
return r
def load_api_keys(path): def load_api_keys(path):
@ -67,7 +68,6 @@ def load_api_keys(path):
secret = key['secret'] secret = key['secret']
expiration = key['expiration'] expiration = key['expiration']
keys_for_return.update({key_name: APIKey(secret, key_name, expiration)}) keys_for_return.update({key_name: APIKey(secret, key_name, expiration)})
return keys_for_return return keys_for_return
@ -81,11 +81,10 @@ def save_api_keys(keys, path):
def initialize_api_key_file(key_path): def initialize_api_key_file(key_path):
if not os.path.isfile(key_path): keys = {}
keys = {} new_api_key = APIKey.new(name=API_KEY_NAME)
new_api_key = APIKey.new(name=API_KEY_NAME) keys.update({new_api_key.name: new_api_key})
keys.update({new_api_key.name: new_api_key}) save_api_keys(keys, key_path)
save_api_keys(keys, key_path)
def get_auth_message(message_dict): def get_auth_message(message_dict):