From 130f9cfc4d8dd239e6c5abce0359497702604ae0 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 20 Sep 2016 16:58:30 -0400 Subject: [PATCH] api sessions -user starts a httpauthsession with an api key and name -user initializes jsonrpc hmac secret to sha256 of session id -server sends new random hmac secret after each api call -a user without an authenticated session will get a authorization error --- lbrynet/conf.py | 5 + lbrynet/core/Error.py | 6 + lbrynet/lbrynet_daemon/LBRYDaemon.py | 164 ++++++----------- lbrynet/lbrynet_daemon/LBRYDaemonCLI.py | 9 +- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 17 +- lbrynet/lbrynet_daemon/auth/__init__.py | 0 lbrynet/lbrynet_daemon/auth/auth.py | 50 +++++ lbrynet/lbrynet_daemon/auth/client.py | 168 +++++++++++++++++ lbrynet/lbrynet_daemon/auth/server.py | 193 ++++++++++++++++++++ lbrynet/lbrynet_daemon/auth/util.py | 82 +++++++++ 10 files changed, 577 insertions(+), 117 deletions(-) create mode 100644 lbrynet/lbrynet_daemon/auth/__init__.py create mode 100644 lbrynet/lbrynet_daemon/auth/auth.py create mode 100644 lbrynet/lbrynet_daemon/auth/client.py create mode 100644 lbrynet/lbrynet_daemon/auth/server.py create mode 100644 lbrynet/lbrynet_daemon/auth/util.py diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 2005d7784..05f735352 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -71,6 +71,11 @@ CURRENCIES = { 'USD': {'type': 'fiat'}, } +ALLOWED_DURING_STARTUP = ['is_running', 'is_first_run', + 'get_time_behind_blockchain', 'stop', + 'daemon_status', 'get_start_notice', + 'version', 'get_search_servers'] + LOGGLY_TOKEN = 'LJEzATH4AzRgAwxjAP00LwZ2YGx3MwVgZTMuBQZ3MQuxLmOv' ANALYTICS_ENDPOINT = 'https://api.segment.io/v1' diff --git a/lbrynet/core/Error.py b/lbrynet/core/Error.py index 8146dc169..363dddb3d 100644 --- a/lbrynet/core/Error.py +++ b/lbrynet/core/Error.py @@ -87,4 +87,10 @@ class NoSuchStreamHashError(Exception): class InvalidBlobHashError(Exception): + pass + +class InvalidHeaderError(Exception): + pass + +class InvalidAuthenticationToken(Exception): pass \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index b258d914d..f72cef38a 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -40,6 +40,7 @@ from lbrynet.lbrynet_daemon.LBRYDownloader import GetStream from lbrynet.lbrynet_daemon.LBRYPublisher import Publisher from lbrynet.lbrynet_daemon.LBRYExchangeRateManager import ExchangeRateManager from lbrynet.lbrynet_daemon.Lighthouse import LighthouseClient +from lbrynet.lbrynet_daemon.auth.server import LBRYJSONRPCServer, auth_required, authorizer from lbrynet.metadata.LBRYMetadata import Metadata, verify_name_characters from lbrynet.core import log_support from lbrynet.core import utils @@ -48,7 +49,7 @@ from lbrynet.lbrynet_console.LBRYSettings import LBRYSettings from lbrynet.conf import MIN_BLOB_DATA_PAYMENT_RATE, DEFAULT_MAX_SEARCH_RESULTS, \ KNOWN_DHT_NODES, DEFAULT_MAX_KEY_FEE, DEFAULT_WALLET, \ DEFAULT_SEARCH_TIMEOUT, DEFAULT_CACHE_TIME, DEFAULT_UI_BRANCH, \ - LOG_POST_URL, LOG_FILE_NAME, REFLECTOR_SERVERS, SEARCH_SERVERS + LOG_POST_URL, LOG_FILE_NAME, REFLECTOR_SERVERS, SEARCH_SERVERS, ALLOWED_DURING_STARTUP from lbrynet.conf import DEFAULT_SD_DOWNLOAD_TIMEOUT from lbrynet.conf import DEFAULT_TIMEOUT from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier, download_sd_blob, BlobStreamDescriptorReader @@ -117,11 +118,6 @@ CONNECTION_PROBLEM_CODES = [ (CONNECT_CODE_WALLET, "Synchronization with the blockchain is lagging... if this continues try restarting LBRY") ] -ALLOWED_DURING_STARTUP = ['is_running', 'is_first_run', - 'get_time_behind_blockchain', 'stop', - 'daemon_status', 'get_start_notice', - 'version', 'get_search_servers'] - BAD_REQUEST = 400 NOT_FOUND = 404 OK_CODE = 200 @@ -138,15 +134,14 @@ class Parameters(object): self.__dict__.update(kwargs) -class LBRYDaemon(jsonrpc.JSONRPC): +@authorizer +class LBRYDaemon(LBRYJSONRPCServer): """ LBRYnet daemon, a jsonrpc interface to lbry functions """ - isLeaf = True - def __init__(self, root, wallet_type=None): - jsonrpc.JSONRPC.__init__(self) + LBRYJSONRPCServer.__init__(self) reactor.addSystemEventTrigger('before', 'shutdown', self._shutdown) self.startup_status = STARTUP_STAGES[0] @@ -397,99 +392,6 @@ class LBRYDaemon(jsonrpc.JSONRPC): f.write("rpcpassword=" + password) log.info("Done writing lbrycrd.conf") - def _responseFailed(self, err, call): - log.debug(err.getTraceback()) - - def render(self, request): - origin = request.getHeader("Origin") - referer = request.getHeader("Referer") - - if origin not in [None, 'http://localhost:5279']: - log.warning("Attempted api call from %s", origin) - return server.failure - - if referer is not None and not referer.startswith('http://localhost:5279/'): - log.warning("Attempted api call from %s", referer) - return server.failure - - request.content.seek(0, 0) - # Unmarshal the JSON-RPC data. - content = request.content.read() - parsed = jsonrpclib.loads(content) - functionPath = parsed.get("method") - args = parsed.get('params') - - #TODO convert args to correct types if possible - - id = parsed.get('id') - version = parsed.get('jsonrpc') - if version: - version = int(float(version)) - elif id and not version: - version = jsonrpclib.VERSION_1 - else: - version = jsonrpclib.VERSION_PRE1 - # XXX this all needs to be re-worked to support logic for multiple - # versions... - - if not self.announced_startup: - if functionPath not in ALLOWED_DURING_STARTUP: - return server.failure - - if self.wallet_type == "lbryum" and functionPath in ['set_miner', 'get_miner_status']: - return server.failure - - try: - function = self._getFunction(functionPath) - except jsonrpclib.Fault, f: - self._cbRender(f, request, id, version) - else: - request.setHeader("Access-Control-Allow-Origin", "localhost") - request.setHeader("content-type", "text/json") - if args == [{}]: - d = defer.maybeDeferred(function) - else: - d = defer.maybeDeferred(function, *args) - - # cancel the response if the connection is broken - notify_finish = request.notifyFinish() - notify_finish.addErrback(self._responseFailed, d) - d.addErrback(self._ebRender, id) - d.addCallback(self._cbRender, request, id, version) - d.addErrback(notify_finish.errback) - return server.NOT_DONE_YET - - def _cbRender(self, result, request, id, version): - def default_decimal(obj): - if isinstance(obj, Decimal): - return float(obj) - - if isinstance(result, Handler): - result = result.result - - if isinstance(result, dict): - result = result['result'] - - if version == jsonrpclib.VERSION_PRE1: - if not isinstance(result, jsonrpclib.Fault): - result = (result,) - # Convert the result (python) to JSON-RPC - try: - s = jsonrpclib.dumps(result, version=version, default=default_decimal) - except: - f = jsonrpclib.Fault(self.FAILURE, "can't serialize output") - s = jsonrpclib.dumps(f, version=version) - - request.setHeader("content-length", str(len(s))) - request.write(s) - request.finish() - - def _ebRender(self, failure, id): - if isinstance(failure.value, jsonrpclib.Fault): - return failure.value - log.error(failure) - return jsonrpclib.Fault(self.FAILURE, "error") - def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False, host_ui=True): def _log_starting_vals(): log.info("Starting balance: " + str(self.session.wallet.wallet_balance)) @@ -1435,9 +1337,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _search(self, search): return self.lighthouse_client.search(search) - def _render_response(self, result, code): - return defer.succeed({'result': result, 'code': code}) - + @auth_required def jsonrpc_is_running(self): """ Check if lbrynet daemon is running @@ -1454,6 +1354,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: return self._render_response(False, OK_CODE) + @auth_required def jsonrpc_daemon_status(self): """ Get lbrynet daemon status information @@ -1488,6 +1389,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("daemon status: " + str(r)) return self._render_response(r, OK_CODE) + @auth_required def jsonrpc_is_first_run(self): """ Check if this is the first time lbrynet daemon has been run @@ -1508,6 +1410,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_get_start_notice(self): """ Get special message to be displayed at startup @@ -1527,6 +1430,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: self._render_response(self.startup_message, OK_CODE) + @auth_required def jsonrpc_version(self): """ Get lbry version information @@ -1561,6 +1465,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("Get version info: " + json.dumps(msg)) return self._render_response(msg, OK_CODE) + @auth_required def jsonrpc_get_settings(self): """ Get lbrynet daemon settings @@ -1589,6 +1494,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("Get daemon settings") return self._render_response(self.session_settings, OK_CODE) + @auth_required def jsonrpc_set_settings(self, p): """ Set lbrynet daemon settings @@ -1616,6 +1522,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_help(self, p=None): """ Function to retrieve docstring for API function @@ -1640,6 +1547,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: return self._render_response(self.jsonrpc_help.__doc__, OK_CODE) + @auth_required def jsonrpc_get_balance(self): """ Get balance @@ -1653,6 +1561,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("Get balance") return self._render_response(float(self.session.wallet.wallet_balance), OK_CODE) + @auth_required def jsonrpc_stop(self): """ Stop lbrynet-daemon @@ -1672,6 +1581,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return self._render_response("Shutting down", OK_CODE) + @auth_required def jsonrpc_get_lbry_files(self): """ Get LBRY files @@ -1698,6 +1608,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_get_lbry_file(self, p): """ Get lbry file @@ -1727,6 +1638,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_resolve_name(self, p): """ Resolve stream info from a LBRY uri @@ -1748,6 +1660,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallbacks(lambda info: self._render_response(info, OK_CODE), lambda _: server.failure) return d + @auth_required def jsonrpc_get_claim_info(self, p): """ Resolve claim info from a LBRY uri @@ -1772,6 +1685,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def _process_get_parameters(self, p): """Extract info from input parameters and fill in default values for `get` call.""" # TODO: this process can be abstracted s.t. each method @@ -1793,6 +1707,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): name=name ) + @auth_required def jsonrpc_get(self, p): """Download stream from a LBRY uri. @@ -1823,6 +1738,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda message: self._render_response(message, OK_CODE)) return d + @auth_required def jsonrpc_stop_lbry_file(self, p): """ Stop lbry file @@ -1848,6 +1764,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_start_lbry_file(self, p): """ Stop lbry file @@ -1872,6 +1789,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_est_cost(self, p): """ Get estimated cost for a lbry uri @@ -1893,6 +1811,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_search_nametrie(self, p): """ Search the nametrie for claims @@ -1929,6 +1848,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_delete_lbry_file(self, p): """ Delete a lbry file @@ -1958,6 +1878,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_publish(self, p): """ Make a new name claim and publish associated data to lbrynet @@ -2034,6 +1955,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_abandon_claim(self, p): """ Abandon a name and reclaim credits from the claim @@ -2060,6 +1982,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_abandon_name(self, p): """ DEPRECIATED, use abandon_claim @@ -2072,7 +1995,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return self.jsonrpc_abandon_claim(p) - + @auth_required def jsonrpc_support_claim(self, p): """ Support a name claim @@ -2092,6 +2015,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_name_claims(self): """ Get my name claims @@ -2115,6 +2039,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_get_claims_for_name(self, p): """ Get claims for a name @@ -2130,6 +2055,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_transaction_history(self): """ Get transaction history @@ -2144,6 +2070,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_transaction(self, p): """ Get a decoded transaction from a txid @@ -2160,6 +2087,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_address_is_mine(self, p): """ Checks if an address is associated with the current wallet. @@ -2177,7 +2105,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d - + @auth_required def jsonrpc_get_public_key_from_wallet(self, p): """ Get public key from wallet address @@ -2192,6 +2120,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = self.session.wallet.get_pub_keys(wallet) d.addCallback(lambda r: self._render_response(r, OK_CODE)) + @auth_required def jsonrpc_get_time_behind_blockchain(self): """ Get number of blocks behind the blockchain @@ -2215,6 +2144,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_get_new_address(self): """ Generate a new wallet address @@ -2234,6 +2164,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda address: self._render_response(address, OK_CODE)) return d + @auth_required def jsonrpc_send_amount_to_address(self, p): """ Send credits to an address @@ -2258,6 +2189,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d + @auth_required def jsonrpc_get_best_blockhash(self): """ Get hash of most recent block @@ -2272,6 +2204,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_block(self, p): """ Get contents of a block @@ -2294,6 +2227,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_claims_for_tx(self, p): """ Get claims for tx @@ -2313,6 +2247,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_download_descriptor(self, p): """ Download and return a sd blob @@ -2329,6 +2264,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallbacks(lambda r: self._render_response(r, OK_CODE), lambda _: self._render_response(False, OK_CODE)) return d + @auth_required def jsonrpc_get_nametrie(self): """ Get the nametrie @@ -2344,6 +2280,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_set_miner(self, p): """ Start of stop the miner, function only available when lbrycrd is set as the wallet @@ -2363,6 +2300,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_miner_status(self): """ Get status of miner @@ -2377,6 +2315,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_log(self, p): """ Log message @@ -2391,6 +2330,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("API client log request: %s" % message) return self._render_response(True, OK_CODE) + @auth_required def jsonrpc_upload_log(self, p=None): """ Upload log @@ -2432,6 +2372,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d + @auth_required def jsonrpc_configure_ui(self, p): """ Configure the UI being hosted @@ -2456,6 +2397,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d + @auth_required def jsonrpc_reveal(self, p): """ Reveal a file or directory in file browser @@ -2475,6 +2417,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d + @auth_required def jsonrpc_get_peers_for_hash(self, p): """ Get peers for blob hash @@ -2492,6 +2435,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_announce_all_blobs_to_dht(self): """ Announce all blobs to the dht @@ -2506,6 +2450,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._render_response("Announced", OK_CODE)) return d + @auth_required def jsonrpc_reflect(self, p): """ Reflect a stream @@ -2522,6 +2467,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallbacks(lambda _: self._render_response(True, OK_CODE), lambda err: self._render_response(err.getTraceback(), OK_CODE)) return d + @auth_required def jsonrpc_get_blob_hashes(self): """ Returns all blob hashes @@ -2536,6 +2482,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_reflect_all_blobs(self): """ Reflects all saved blobs @@ -2551,6 +2498,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + @auth_required def jsonrpc_get_search_servers(self): """ Get list of lighthouse servers diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py b/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py index ea4f2234d..b63e1af25 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py @@ -2,8 +2,7 @@ import sys import json import argparse -from lbrynet.conf import API_CONNECTION_STRING -from jsonrpc.proxy import JSONRPCProxy +from lbrynet.lbrynet_daemon.auth.client import LBRYAPIClient help_msg = "Usage: lbrynet-cli method json-args\n" \ + "Examples: " \ @@ -36,7 +35,7 @@ def get_params_from_kwargs(params): def main(): - api = JSONRPCProxy.from_url(API_CONNECTION_STRING) + api = LBRYAPIClient() try: s = api.is_running() @@ -72,9 +71,9 @@ def main(): if meth in api.help(): try: if params: - r = api.call(meth, params) + r = LBRYAPIClient(service=meth)(params) else: - r = api.call(meth) + r = LBRYAPIClient(service=meth)() print json.dumps(r, sort_keys=True) except: print "Something went wrong, here's the usage for %s:" % meth diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 10b5b5d0b..e6464a8e5 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -7,12 +7,15 @@ import sys import socket from appdirs import user_data_dir -from twisted.web import server -from twisted.internet import reactor, defer +from twisted.web import server, guard +from twisted.internet import defer, reactor +from twisted.cred import portal + from jsonrpc.proxy import JSONRPCProxy from lbrynet.core import log_support from lbrynet.lbrynet_daemon.LBRYDaemonServer import LBRYDaemonServer, LBRYDaemonRequest +from lbrynet.lbrynet_daemon.auth.auth import PasswordChecker, HttpPasswordRealm from lbrynet.conf import API_CONNECTION_STRING, API_INTERFACE, API_PORT, \ UI_ADDRESS, DEFAULT_UI_BRANCH, LOG_FILE_NAME @@ -113,8 +116,14 @@ def start(): if args.launchui: d.addCallback(lambda _: webbrowser.open(UI_ADDRESS)) - lbrynet_server = server.Site(lbry.root) + checker = PasswordChecker() + realm = HttpPasswordRealm(lbry.root) + p = portal.Portal(realm, [checker, ]) + factory = guard.BasicCredentialFactory('Login to lbrynet api') + protected_resource = guard.HTTPAuthSessionWrapper(p, [factory, ]) + lbrynet_server = server.Site(protected_resource) lbrynet_server.requestFactory = LBRYDaemonRequest + reactor.listenTCP(API_PORT, lbrynet_server, interface=API_INTERFACE) reactor.run() @@ -127,4 +136,4 @@ def start(): return if __name__ == "__main__": - start() + start() \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/auth/__init__.py b/lbrynet/lbrynet_daemon/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lbrynet/lbrynet_daemon/auth/auth.py b/lbrynet/lbrynet_daemon/auth/auth.py new file mode 100644 index 000000000..57762e2b3 --- /dev/null +++ b/lbrynet/lbrynet_daemon/auth/auth.py @@ -0,0 +1,50 @@ +import logging +import os +from zope.interface import implements, implementer +from twisted.cred import portal, checkers, credentials, error as cred_error +from twisted.internet import defer +from twisted.web import resource +from lbrynet.lbrynet_daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME, save_api_keys +from lbrynet.lbrynet_daemon.LBRYDaemon import log_dir as DATA_DIR + +log = logging.getLogger(__name__) + + +# initialize api key if none exist +if not os.path.isfile(os.path.join(DATA_DIR, ".api_keys")): + keys = {} + api_key = APIKey.new() + api_key.rename(API_KEY_NAME) + keys.update(api_key) + save_api_keys(keys, os.path.join(DATA_DIR, ".api_keys")) + + +@implementer(portal.IRealm) +class HttpPasswordRealm: + def __init__(self, resource): + self.resource = resource + + def requestAvatar(self, avatarId, mind, *interfaces): + log.info("Processing request for %s", avatarId) + if resource.IResource in interfaces: + return (resource.IResource, self.resource, lambda: None) + raise NotImplementedError() + + +class PasswordChecker: + implements(checkers.ICredentialsChecker) + credentialInterfaces = (credentials.IUsernamePassword,) + + def __init__(self): + keys = load_api_keys(os.path.join(DATA_DIR, ".api_keys")) + self.passwords = {key: keys[key]['token'] for key in keys} + + def requestAvatarId(self, creds): + if creds.username in self.passwords: + pw = self.passwords.get(creds.username) + pw_match = creds.checkPassword(pw) + if pw_match is True: + 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/lbrynet_daemon/auth/client.py b/lbrynet/lbrynet_daemon/auth/client.py new file mode 100644 index 000000000..a4370e3b5 --- /dev/null +++ b/lbrynet/lbrynet_daemon/auth/client.py @@ -0,0 +1,168 @@ +try: + import http.client as httplib +except ImportError: + import httplib +try: + import urllib.parse as urlparse +except ImportError: + import urlparse + +import logging +import requests +import os +import base64 +import json + +from lbrynet.lbrynet_daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME +from lbrynet.conf import API_INTERFACE, API_ADDRESS, API_PORT +from lbrynet.lbrynet_daemon.LBRYDaemon import log_dir as DATA_DIR + +log = logging.getLogger(__name__) +USER_AGENT = "AuthServiceProxy/0.1" +HTTP_TIMEOUT = 30 + + +class JSONRPCException(Exception): + def __init__(self, rpc_error): + Exception.__init__(self) + self.error = rpc_error + + +class LBRYAPIClient(object): + __api_token = None + + def __init__(self, key_name=None, key=None, pw_path=None, timeout=HTTP_TIMEOUT, connection=None, count=0, + service=None, cookies=None, auth=None, url=None, login_url=None): + self.__api_key_name = API_KEY_NAME if not key_name else key_name + self.__api_token = key + self.__pw_path = os.path.join(DATA_DIR, ".api_keys") if not pw_path else pw_path + self.__service_name = service + + if not key: + keys = load_api_keys(self.__pw_path) + api_key = keys.get(self.__api_key_name, False) + self.__api_token = api_key['token'] + self.__api_key_obj = api_key + else: + self.__api_key_obj = APIKey({'token': key}) + + if login_url is None: + self.__service_url = "http://%s:%s@%s:%i/%s" % (self.__api_key_name, self.__api_token, + API_INTERFACE, API_PORT, API_ADDRESS) + else: + self.__service_url = login_url + + self.__id_count = count + + if auth is None and connection is None and cookies is None and url is None: + self.__url = urlparse.urlparse(self.__service_url) + (user, passwd) = (self.__url.username, self.__url.password) + try: + user = user.encode('utf8') + except AttributeError: + pass + try: + passwd = passwd.encode('utf8') + except AttributeError: + pass + authpair = user + b':' + passwd + self.__auth_header = b'Basic ' + base64.b64encode(authpair) + + self.__conn = requests.Session() + self.__conn.auth = (user, passwd) + + req = requests.Request(method='POST', + url=self.__service_url, + auth=self.__conn.auth, + headers={'Host': self.__url.hostname, + 'User-Agent': USER_AGENT, + 'Authorization': self.__auth_header, + 'Content-type': 'application/json'},) + r = req.prepare() + http_response = self.__conn.send(r) + cookies = http_response.cookies + self.__cookies = cookies + # print "Logged in" + + uid = cookies.get('TWISTED_SESSION') + api_key = APIKey.new(seed=uid) + # print "Created temporary api key" + + self.__api_token = api_key.token() + self.__api_key_obj = api_key + else: + self.__auth_header = auth + self.__conn = connection + self.__cookies = cookies + self.__url = url + + if cookies.get("secret", False): + self.__api_token = cookies.get("secret") + self.__api_key_obj = APIKey({'name': self.__api_key_name, 'token': self.__api_token}) + + + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + # Python internal stuff + raise AttributeError + if self.__service_name is not None: + name = "%s.%s" % (self.__service_name, name) + return LBRYAPIClient(key_name=self.__api_key_name, + key=self.__api_token, + connection=self.__conn, + service=name, + count=self.__id_count, + cookies=self.__cookies, + auth=self.__auth_header, + url=self.__url, + login_url=self.__service_url) + + def __call__(self, *args): + self.__id_count += 1 + pre_auth_postdata = {'version': '1.1', + 'method': self.__service_name, + 'params': args, + 'id': self.__id_count} + to_auth = str(pre_auth_postdata['method']).encode('hex') + str(pre_auth_postdata['id']).encode('hex') + token = self.__api_key_obj.get_hmac(to_auth.decode('hex')) + pre_auth_postdata.update({'hmac': token}) + postdata = json.dumps(pre_auth_postdata) + 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=postdata, + 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) + self.__cookies = http_response.cookies + headers = http_response.headers + next_secret = headers.get('Next-Secret', False) + + if next_secret: + cookies.update({'secret': next_secret}) + + # print "Postdata: %s" % postdata + if http_response is None: + raise JSONRPCException({ + 'code': -342, 'message': 'missing HTTP response from server'}) + + # print "-----\n%s\n------" % http_response.text + http_response.raise_for_status() + + response = http_response.json() + + if response['error'] is not None: + raise JSONRPCException(response['error']) + elif 'result' not in response: + raise JSONRPCException({ + 'code': -343, 'message': 'missing JSON-RPC result'}) + else: + return response['result'] \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/auth/server.py b/lbrynet/lbrynet_daemon/auth/server.py new file mode 100644 index 000000000..b2ad51402 --- /dev/null +++ b/lbrynet/lbrynet_daemon/auth/server.py @@ -0,0 +1,193 @@ +import logging + +from decimal import Decimal +from twisted.web import server +from twisted.internet import defer +from txjsonrpc import jsonrpclib +from txjsonrpc.web import jsonrpc +from txjsonrpc.web.jsonrpc import Handler + +from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError +from lbrynet.lbrynet_daemon.auth.util import APIKey +from lbrynet.conf import ALLOWED_DURING_STARTUP + +log = logging.getLogger(__name__) + + +def default_decimal(obj): + if isinstance(obj, Decimal): + return float(obj) + + +def authorizer(cls): + cls.authorized_functions = [] + for methodname in dir(cls): + if methodname.startswith("jsonrpc_"): + method = getattr(cls, methodname) + if hasattr(method, '_auth_required'): + cls.authorized_functions.append(methodname.split("jsonrpc_")[1]) + return cls + + +def auth_required(f): + f._auth_required = True + return f + + +@authorizer +class LBRYJSONRPCServer(jsonrpc.JSONRPC): + + isLeaf = True + + def __init__(self): + jsonrpc.JSONRPC.__init__(self) + self.sessions = {} + + def _register_user_session(self, session_id): + token = APIKey.new() + self.sessions.update({session_id: token}) + return token + + def _responseFailed(self, err, call): + log.debug(err.getTraceback()) + + def _set_headers(self, request, data): + request.setHeader("Access-Control-Allow-Origin", "localhost") + request.setHeader("Content-Type", "text/json") + request.setHeader("Content-Length", str(len(data))) + + def _render_message(self, request, message): + request.write(message) + request.finish() + + def _check_headers(self, request): + origin = request.getHeader("Origin") + referer = request.getHeader("Referer") + + if origin not in [None, 'http://localhost:5279']: + log.warning("Attempted api call from %s", origin) + raise InvalidHeaderError + + if referer is not None and not referer.startswith('http://localhost:5279/'): + log.warning("Attempted api call from %s", referer) + raise InvalidHeaderError + + def _handle(self, request): + def _check_function_path(function_path): + if not self.announced_startup: + if function_path not in ALLOWED_DURING_STARTUP: + log.warning("Cannot call %s during startup", function_path) + raise Exception("Function not allowed") + + def _get_function(function_path): + function = self._getFunction(function_path) + return function + + def _verify_token(session_id, message, token): + request.setHeader("Next-Secret", "") + api_key = self.sessions.get(session_id, None) + assert api_key is not None, InvalidAuthenticationToken + r = api_key.compare_hmac(message, token) + assert r, InvalidAuthenticationToken + # log.info("Generating new token for next request") + self.sessions.update({session_id: APIKey.new()}) + request.setHeader("Next-Secret", self.sessions.get(session_id).token()) + + session = request.getSession() + session_id = session.uid + session_store = self.sessions.get(session_id, False) + + if not session_store: + token = APIKey.new(seed=session_id) + log.info("Initializing new api session") + self.sessions.update({session_id: token}) + # log.info("Generated token %s", str(self.sessions[session_id])) + + request.content.seek(0, 0) + content = request.content.read() + + parsed = jsonrpclib.loads(content) + + functionPath = parsed.get("method") + + _check_function_path(functionPath) + require_auth = functionPath in self.authorized_functions + if require_auth: + token = parsed.pop('hmac') + to_auth = functionPath.encode('hex') + str(parsed.get('id')).encode('hex') + _verify_token(session_id, to_auth.decode('hex'), token) + + args = parsed.get('params') + id = parsed.get('id') + version = parsed.get('jsonrpc') + + if version: + version = int(float(version)) + elif id and not version: + version = jsonrpclib.VERSION_1 + else: + version = jsonrpclib.VERSION_PRE1 + + if self.wallet_type == "lbryum" and functionPath in ['set_miner', 'get_miner_status']: + log.warning("Mining commands are not available in lbryum") + raise Exception("Command not available in lbryum") + + try: + function = _get_function(functionPath) + if args == [{}]: + d = defer.maybeDeferred(function) + else: + d = defer.maybeDeferred(function, *args) + except jsonrpclib.Fault as f: + d = self._cbRender(f, request, id, version) + finally: + # cancel the response if the connection is broken + notify_finish = request.notifyFinish() + notify_finish.addErrback(self._responseFailed, d) + d.addErrback(self._ebRender, id) + d.addCallback(self._cbRender, request, id, version) + d.addErrback(notify_finish.errback) + + def _cbRender(self, result, request, id, version): + if isinstance(result, Handler): + result = result.result + + if isinstance(result, dict): + result = result['result'] + + if version == jsonrpclib.VERSION_PRE1: + if not isinstance(result, jsonrpclib.Fault): + result = (result,) + # Convert the result (python) to JSON-RPC + try: + s = jsonrpclib.dumps(result, version=version, default=default_decimal) + self._render_message(request, s) + except: + f = jsonrpclib.Fault(self.FAILURE, "can't serialize output") + s = jsonrpclib.dumps(f, version=version) + self._set_headers(request, s) + self._render_message(request, s) + + def _ebRender(self, failure, id): + log.error(failure) + log.error(failure.value) + log.error(id) + if isinstance(failure.value, jsonrpclib.Fault): + return failure.value + return server.failure + + def render(self, request): + try: + self._check_headers(request) + except InvalidHeaderError: + return server.failure + + try: + self._handle(request) + except: + return server.failure + + return server.NOT_DONE_YET + + def _render_response(self, result, code): + return defer.succeed({'result': result, 'code': code}) diff --git a/lbrynet/lbrynet_daemon/auth/util.py b/lbrynet/lbrynet_daemon/auth/util.py new file mode 100644 index 000000000..69daeee56 --- /dev/null +++ b/lbrynet/lbrynet_daemon/auth/util.py @@ -0,0 +1,82 @@ +import base58 +import hmac +import hashlib +import yaml +import os +import logging + +log = logging.getLogger(__name__) + +API_KEY_NAME = "api" + + +def sha(x): + h = hashlib.sha256(x).digest() + return base58.b58encode(h) + + +def generate_key(x=None): + if x is None: + return sha(os.urandom(256)) + else: + return sha(x) + + +class APIKey(dict): + def __init__(self, key, name=None): + self.key = key if isinstance(key, str) else key['token'] + self.name = name if name else hashlib.sha256(self.key).hexdigest() + self.expiration = None if isinstance(key, str) else key.get('expiration', None) + self.update({self.name: {'token': self.key, 'expiration': self.expiration}}) + + @classmethod + def new(cls, expiration=None, seed=None, name=None): + key_val = generate_key(seed) + key = {'token': key_val, 'expiration': expiration} + return APIKey(key, name) + + def token(self): + return self[self.name]['token'] + + def _raw_key(self): + return base58.b58decode(self.token()) + + def get_hmac(self, message): + decoded_key = self._raw_key() + signature = hmac.new(decoded_key, message, hashlib.sha256) + return base58.b58encode(signature.digest()) + + 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) + except: + return False + return r + + def rename(self, name): + old = self.keys()[0] + t = self.pop(old) + self.update({name: t}) + + +def load_api_keys(path): + if not os.path.isfile(path): + raise Exception("Invalid api key path") + + f = open(path, "r") + data = yaml.load(f.read()) + f.close() + + keys = {key: APIKey(data[key], name=key)[key] for key in data} + + return keys + + +def save_api_keys(keys, path): + data = yaml.safe_dump(dict(keys)) + f = open(path, "w") + f.write(data) + f.close()