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

317 lines
12 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
2016-09-22 03:36:06 +02:00
from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError, SubhandlerError
2016-10-27 21:18:25 +02:00
from lbrynet.conf import settings
from lbrynet.core import log_support
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__)
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.subhandlers = []
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])
elif not methodname.startswith("__"):
method = getattr(self, methodname)
if hasattr(method, '_subhandler'):
self.subhandlers.append(method)
@staticmethod
def auth_required(f):
f._auth_required = True
return f
@staticmethod
def subhandler(f):
f._subhandler = True
return f
class AuthJSONRPCServer(AuthorizedBase):
"""
Authorized JSONRPC server used as the base class for the LBRY API
API methods are named with a leading "jsonrpc_"
Decorators:
@AuthJSONRPCServer.auth_required: this requires the client include a valid hmac authentication token in their
request
2016-09-22 03:36:06 +02:00
@AuthJSONRPCServer.subhandler: include the tagged method in the processing of requests, to allow inheriting
classes to modify request handling. Tagged methods will be passed the request
object, and return True when finished to indicate success
2016-09-22 03:36:06 +02:00
Attributes:
allowed_during_startup (list): list of api methods that are callable before the server has finished
startup
2016-09-22 03:36:06 +02: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
subhandlers (list): list of subhandlers
callable_methods (dict): dictionary of api_callable_name: method values
"""
implements(resource.IResource)
isLeaf = True
2016-09-22 03:36:06 +02:00
OK = 200
UNAUTHORIZED = 401
NOT_FOUND = 8001
FAILURE = 8002
def __init__(self, use_authentication=settings.use_auth_http):
2016-09-22 03:36:06 +02:00
AuthorizedBase.__init__(self)
self._use_authentication = use_authentication
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()
2016-11-22 21:11:25 +01:00
def _render_error(self, failure, request, version=jsonrpclib.VERSION_1, response_code=FAILURE):
2016-11-14 19:53:11 +01:00
err = JSONRPCException(Failure(failure), response_code)
fault = jsonrpclib.dumps(err, 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-11-22 21:11:25 +01:00
def _log_and_render_error(self, failure, request, message=None, **kwargs):
msg = message or "API Failure: %s"
log_support.failure(Failure(failure), log, msg)
self._render_error(failure, request, **kwargs)
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)
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
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")
2016-11-16 21:16:15 +01:00
self._render_error(err, request)
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')
id = parsed.get('id')
token = parsed.pop('hmac', None)
version = self._get_jsonrpc_version(parsed.get('jsonrpc'), id)
try:
self._run_subhandlers(request)
2016-11-11 19:40:19 +01:00
except SubhandlerError as err:
2016-11-16 21:16:15 +01:00
self._render_error(err, request, version)
2016-11-11 19:40:19 +01:00
return server.NOT_DONE_YET
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-22 21:11:25 +01:00
self._render_error(err, request, 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)
2016-11-16 21:16:15 +01:00
self._render_error(err, request, version)
2016-11-11 19:40:19 +01:00
return server.NOT_DONE_YET
2016-09-22 03:36:06 +02:00
2016-11-14 19:53:11 +01:00
if args == [{}]:
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)
2016-11-11 19:40:19 +01:00
d.addCallback(self._callback_render, request, version, reply_with_next_secret)
2016-11-22 21:11:25 +01:00
d.addErrback(self._log_and_render_error, request, version=version)
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 settings.allowed_origin:
request.setHeader("Access-Control-Allow-Origin", 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 settings.API_INTERFACE == '0.0.0.0':
return True
server, port = self.get_server_port(source)
return (
server == settings.API_INTERFACE and
port == 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):
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):
# log.info("Generating new token for next request")
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
2016-09-22 03:36:06 +02:00
def _run_subhandlers(self, request):
for handler in self.subhandlers:
2016-11-14 19:53:11 +01:00
if not handler(request):
raise SubhandlerError("Subhandler error processing request: %s", request)
2016-11-11 19:40:19 +01:00
def _callback_render(self, result, request, 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,)
# Convert the result (python) to JSON-RPC
try:
2016-09-22 03:36:06 +02:00
encoded_message = jsonrpclib.dumps(result_for_return, version=version, default=default_decimal)
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:
2016-11-16 21:16:15 +01:00
msg = "Failed to render API response: %s"
2016-11-22 21:11:25 +01:00
self._log_and_render_error(err, request, message=msg, version=version)
def _render_response(self, result, code):
return defer.succeed({'result': result, 'code': code})