lbry-sdk/lbrynet/lbrynet_daemon/auth/server.py

299 lines
11 KiB
Python
Raw Normal View History

import logging
import urlparse
from decimal import Decimal
2016-09-22 03:36:06 +02:00
from zope.interface import implements
from twisted.web import server, resource
from twisted.internet import defer
2016-11-11 19:40:19 +01:00
from twisted.python.failure import Failure
from txjsonrpc import jsonrpclib
from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError
from lbrynet import conf
2016-09-22 03:36:06 +02:00
from lbrynet.lbrynet_daemon.auth.util import APIKey, get_auth_message
2016-09-21 09:49:52 +02:00
from lbrynet.lbrynet_daemon.auth.client import LBRY_SECRET
log = logging.getLogger(__name__)
2017-01-09 20:03:25 +01:00
EMPTY_PARAMS = [{}]
def default_decimal(obj):
if isinstance(obj, Decimal):
return float(obj)
2016-11-11 19:40:19 +01:00
class JSONRPCException(Exception):
def __init__(self, err, code):
self.faultCode = code
2016-11-14 19:53:11 +01:00
self.err = err
@property
def faultString(self):
return self.err.getTraceback()
2016-11-11 19:40:19 +01:00
2016-09-22 03:36:06 +02:00
class AuthorizedBase(object):
def __init__(self):
self.authorized_functions = []
self.callable_methods = {}
for methodname in dir(self):
if methodname.startswith("jsonrpc_"):
method = getattr(self, methodname)
self.callable_methods.update({methodname.split("jsonrpc_")[1]: method})
if hasattr(method, '_auth_required'):
self.authorized_functions.append(methodname.split("jsonrpc_")[1])
@staticmethod
def auth_required(f):
f._auth_required = True
return f
class AuthJSONRPCServer(AuthorizedBase):
2016-11-30 21:20:45 +01:00
"""Authorized JSONRPC server used as the base class for the LBRY API
2016-09-22 03:36:06 +02:00
API methods are named with a leading "jsonrpc_"
Decorators:
2016-11-30 21:20:45 +01:00
@AuthJSONRPCServer.auth_required: this requires the client
include a valid hmac authentication token in their request
2016-09-22 03:36:06 +02:00
Attributes:
2016-11-30 21:20:45 +01:00
allowed_during_startup (list): list of api methods that are
callable before the server has finished startup
2016-11-30 21:20:45 +01:00
sessions (dict): dictionary of active session_id:
lbrynet.lbrynet_daemon.auth.util.APIKey values
2016-09-22 03:36:06 +02:00
authorized_functions (list): list of api methods that require authentication
callable_methods (dict): dictionary of api_callable_name: method values
2016-11-30 21:20:45 +01:00
2016-09-22 03:36:06 +02:00
"""
implements(resource.IResource)
isLeaf = True
2016-09-22 03:36:06 +02:00
OK = 200
UNAUTHORIZED = 401
# TODO: codes should follow jsonrpc spec: http://www.jsonrpc.org/specification#error_object
2016-09-22 03:36:06 +02:00
NOT_FOUND = 8001
FAILURE = 8002
def __init__(self, use_authentication=None):
2016-09-22 03:36:06 +02:00
AuthorizedBase.__init__(self)
self._use_authentication = (
use_authentication if use_authentication is not None else conf.settings.use_auth_http)
2016-11-22 21:11:25 +01:00
self.announced_startup = False
2016-09-22 03:36:06 +02:00
self.allowed_during_startup = []
self.sessions = {}
2016-09-22 03:36:06 +02:00
def setup(self):
return NotImplementedError()
2017-01-03 03:05:44 +01:00
def _render_error(self, failure, request, id_,
version=jsonrpclib.VERSION_2, response_code=FAILURE):
2016-11-14 19:53:11 +01:00
err = JSONRPCException(Failure(failure), response_code)
2017-01-03 03:05:44 +01:00
fault = jsonrpclib.dumps(err, id=id_, version=version)
2016-11-11 19:40:19 +01:00
self._set_headers(request, fault)
if response_code != AuthJSONRPCServer.FAILURE:
request.setResponseCode(response_code)
request.write(fault)
request.finish()
2016-09-22 03:36:06 +02:00
def render(self, request):
2016-11-11 19:40:19 +01:00
notify_finish = request.notifyFinish()
2016-09-22 03:36:06 +02:00
assert self._check_headers(request), InvalidHeaderError
session = request.getSession()
session_id = session.uid
if self._use_authentication:
2016-11-14 19:53:11 +01:00
# if this is a new session, send a new secret and set the expiration
# otherwise, session.touch()
if self._initialize_session(session_id):
def expire_session():
self._unregister_user_session(session_id)
2017-01-09 20:03:25 +01:00
session.startCheckingExpiration()
session.notifyOnExpire(expire_session)
message = "OK"
request.setResponseCode(self.OK)
self._set_headers(request, message, True)
self._render_message(request, message)
return server.NOT_DONE_YET
2017-01-10 01:31:06 +01:00
else:
session.touch()
2016-09-22 03:36:06 +02:00
request.content.seek(0, 0)
content = request.content.read()
try:
parsed = jsonrpclib.loads(content)
2016-11-11 19:40:19 +01:00
except ValueError as err:
2016-11-14 19:53:11 +01:00
log.warning("Unable to decode request json")
2017-01-03 03:05:44 +01:00
self._render_error(err, request, None)
2016-11-11 19:40:19 +01:00
return server.NOT_DONE_YET
2016-09-22 03:36:06 +02:00
function_name = parsed.get('method')
args = parsed.get('params')
2017-01-03 03:05:44 +01:00
id_ = parsed.get('id')
2016-09-22 03:36:06 +02:00
token = parsed.pop('hmac', None)
2017-01-03 03:05:44 +01:00
version = self._get_jsonrpc_version(parsed.get('jsonrpc'), id_)
2016-09-22 03:36:06 +02:00
reply_with_next_secret = False
if self._use_authentication:
if function_name in self.authorized_functions:
try:
self._verify_token(session_id, parsed, token)
2016-11-11 19:40:19 +01:00
except InvalidAuthenticationToken as err:
2016-11-14 19:53:11 +01:00
log.warning("API validation failed")
2016-11-30 21:20:45 +01:00
self._render_error(
2017-01-03 03:05:44 +01:00
err, request, id_, version=version,
response_code=AuthJSONRPCServer.UNAUTHORIZED)
return server.NOT_DONE_YET
self._update_session_secret(session_id)
reply_with_next_secret = True
2016-09-22 03:36:06 +02:00
try:
function = self._get_jsonrpc_method(function_name)
2016-11-11 19:40:19 +01:00
except AttributeError as err:
2016-11-14 19:53:11 +01:00
log.warning("Unknown method: %s", function_name)
2017-01-03 03:05:44 +01:00
self._render_error(err, request, id_, version)
2016-11-11 19:40:19 +01:00
return server.NOT_DONE_YET
2016-09-22 03:36:06 +02:00
2017-01-09 20:03:25 +01:00
if args == EMPTY_PARAMS:
2016-11-14 19:53:11 +01:00
d = defer.maybeDeferred(function)
else:
d = defer.maybeDeferred(function, *args)
2016-11-11 19:40:19 +01:00
2016-09-22 03:36:06 +02:00
# cancel the response if the connection is broken
notify_finish.addErrback(self._response_failed, d)
2017-01-03 03:05:44 +01:00
d.addCallback(self._callback_render, request, id_, version, reply_with_next_secret)
2016-12-10 20:42:57 +01:00
d.addErrback(
2017-01-03 03:05:44 +01:00
log.fail(self._render_error, request, id_, version=version),
2016-12-10 20:42:57 +01:00
'Failed to process %s', function_name
)
2016-09-22 03:36:06 +02:00
return server.NOT_DONE_YET
def _register_user_session(self, session_id):
2016-09-22 03:36:06 +02:00
"""
Add or update a HMAC secret for a session
@param session_id:
@return: secret
"""
log.info("Register api session")
token = APIKey.new(seed=session_id)
self.sessions.update({session_id: token})
2016-09-22 03:36:06 +02:00
def _unregister_user_session(self, session_id):
log.info("Unregister API session")
del self.sessions[session_id]
def _response_failed(self, err, call):
log.debug(err.getTraceback())
2016-09-22 03:36:06 +02:00
def _set_headers(self, request, data, update_secret=False):
if conf.settings.allowed_origin:
request.setHeader("Access-Control-Allow-Origin", conf.settings.allowed_origin)
request.setHeader("Content-Type", "text/json")
request.setHeader("Content-Length", str(len(data)))
2016-09-22 03:36:06 +02:00
if update_secret:
session_id = request.getSession().uid
request.setHeader(LBRY_SECRET, self.sessions.get(session_id).secret)
def _render_message(self, request, message):
request.write(message)
request.finish()
def _check_headers(self, request):
return (
self._check_header_source(request, 'Origin') and
self._check_header_source(request, 'Referer'))
def _check_header_source(self, request, header):
"""Check if the source of the request is allowed based on the header value."""
source = request.getHeader(header)
if not self._check_source_of_request(source):
log.warning("Attempted api call from invalid %s: %s", header, source)
2016-09-22 03:36:06 +02:00
return False
return True
def _check_source_of_request(self, source):
if source is None:
return True
if conf.settings.API_INTERFACE == '0.0.0.0':
return True
server, port = self.get_server_port(source)
return (
server == conf.settings.API_INTERFACE and
port == conf.settings.api_port)
def get_server_port(self, origin):
parsed = urlparse.urlparse(origin)
server_port = parsed.netloc.split(':')
assert len(server_port) <= 2
if len(server_port) == 2:
return server_port[0], int(server_port[1])
else:
return server_port[0], 80
2016-09-22 03:36:06 +02:00
def _check_function_path(self, function_path):
if function_path not in self.callable_methods:
log.warning("Unknown method: %s", function_path)
return False
if not self.announced_startup:
if function_path not in self.allowed_during_startup:
log.warning("Cannot call %s during startup", function_path)
return False
return True
def _get_jsonrpc_method(self, function_path):
2016-11-14 19:53:11 +01:00
if not self._check_function_path(function_path):
raise AttributeError(function_path)
2016-09-22 03:36:06 +02:00
return self.callable_methods.get(function_path)
def _initialize_session(self, session_id):
if not self.sessions.get(session_id, False):
self._register_user_session(session_id)
2016-09-22 03:36:06 +02:00
return True
return False
2016-09-22 03:36:06 +02:00
def _verify_token(self, session_id, message, token):
2017-01-10 01:31:06 +01:00
assert token is not None, InvalidAuthenticationToken
2016-09-22 03:36:06 +02:00
to_auth = get_auth_message(message)
api_key = self.sessions.get(session_id)
assert api_key.compare_hmac(to_auth, token), InvalidAuthenticationToken
2016-09-22 03:36:06 +02:00
def _update_session_secret(self, session_id):
self.sessions.update({session_id: APIKey.new(name=session_id)})
2016-09-22 03:36:06 +02:00
def _get_jsonrpc_version(self, version=None, id=None):
if version:
2016-09-22 03:36:06 +02:00
version_for_return = int(float(version))
elif id and not version:
2016-09-22 03:36:06 +02:00
version_for_return = jsonrpclib.VERSION_1
else:
2016-09-22 03:36:06 +02:00
version_for_return = jsonrpclib.VERSION_PRE1
return version_for_return
2017-01-03 03:05:44 +01:00
def _callback_render(self, result, request, id_, version, auth_required=False):
2016-09-22 03:36:06 +02:00
result_for_return = result if not isinstance(result, dict) else result['result']
if version == jsonrpclib.VERSION_PRE1:
if not isinstance(result, jsonrpclib.Fault):
2016-09-22 03:36:06 +02:00
result_for_return = (result_for_return,)
2017-01-09 20:03:25 +01:00
try:
2016-11-30 21:20:45 +01:00
encoded_message = jsonrpclib.dumps(
2017-01-03 03:05:44 +01:00
result_for_return, id=id_, version=version, default=default_decimal)
2016-09-22 03:36:06 +02:00
self._set_headers(request, encoded_message, auth_required)
self._render_message(request, encoded_message)
2016-11-11 19:40:19 +01:00
except Exception as err:
log.exception("Failed to render API response: %s", result)
2017-01-03 03:05:44 +01:00
self._render_error(err, request, id_, version)
def _render_response(self, result):
return defer.succeed({'result': result, 'code': 200})