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
This commit is contained in:
Jack 2016-09-20 16:58:30 -04:00
parent 4d52a33ee5
commit 130f9cfc4d
10 changed files with 577 additions and 117 deletions

View file

@ -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'

View file

@ -87,4 +87,10 @@ class NoSuchStreamHashError(Exception):
class InvalidBlobHashError(Exception):
pass
class InvalidHeaderError(Exception):
pass
class InvalidAuthenticationToken(Exception):
pass

View file

@ -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

View file

@ -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

View file

@ -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()

View file

View file

@ -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'))

View file

@ -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']

View file

@ -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})

View file

@ -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()