Merge pull request #407 from lbryio/handle-closed-api-requests-rebase

handle dropped api requests
This commit is contained in:
Job Evers‐Meltzer 2017-01-13 13:33:42 -06:00 committed by GitHub
commit ddf24de16d
2 changed files with 60 additions and 52 deletions

View file

@ -1486,20 +1486,7 @@ class Daemon(AuthJSONRPCServer):
return self._render_response(None) return self._render_response(None)
d = self._resolve_name(name, force_refresh=force) d = self._resolve_name(name, force_refresh=force)
d.addCallbacks( d.addCallback(self._render_response)
lambda info: self._render_response(info),
# TODO: Is server.failure a module? It looks like it:
#
# In [1]: import twisted.web.server
# In [2]: twisted.web.server.failure
# Out[2]: <module 'twisted.python.failure' from
# '.../site-packages/twisted/python/failure.pyc'>
#
# If so, maybe we should return something else.
errback=log.fail(lambda err: server.failure),
errbackArgs=('Failed to resolve name',),
errbackKeywords={'level': 'INFO'},
)
return d return d
def jsonrpc_get_claim_info(self, p): def jsonrpc_get_claim_info(self, p):

View file

@ -6,10 +6,12 @@ from zope.interface import implements
from twisted.web import server, resource from twisted.web import server, resource
from twisted.internet import defer from twisted.internet import defer
from twisted.python.failure import Failure from twisted.python.failure import Failure
from twisted.internet.error import ConnectionDone, ConnectionLost
from txjsonrpc import jsonrpclib from txjsonrpc import jsonrpclib
from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError
from lbrynet import conf from lbrynet import conf
from lbrynet.core.Error import InvalidAuthenticationToken, InvalidHeaderError
from lbrynet.core import utils
from lbrynet.lbrynet_daemon.auth.util import APIKey, get_auth_message from lbrynet.lbrynet_daemon.auth.util import APIKey, get_auth_message
from lbrynet.lbrynet_daemon.auth.client import LBRY_SECRET from lbrynet.lbrynet_daemon.auth.client import LBRY_SECRET
@ -33,6 +35,18 @@ class JSONRPCException(Exception):
return self.err.getTraceback() return self.err.getTraceback()
class UnknownAPIMethodError(Exception):
pass
class NotAllowedDuringStartupError(Exception):
pass
def trap(err, *to_trap):
err.trap(*to_trap)
class AuthorizedBase(object): class AuthorizedBase(object):
def __init__(self): def __init__(self):
self.authorized_functions = [] self.authorized_functions = []
@ -93,6 +107,19 @@ class AuthJSONRPCServer(AuthorizedBase):
def setup(self): def setup(self):
return NotImplementedError() return NotImplementedError()
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)))
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 _render_error(self, failure, request, id_, def _render_error(self, failure, request, id_,
version=jsonrpclib.VERSION_2, response_code=FAILURE): version=jsonrpclib.VERSION_2, response_code=FAILURE):
err = JSONRPCException(Failure(failure), response_code) err = JSONRPCException(Failure(failure), response_code)
@ -100,14 +127,19 @@ class AuthJSONRPCServer(AuthorizedBase):
self._set_headers(request, fault) self._set_headers(request, fault)
if response_code != AuthJSONRPCServer.FAILURE: if response_code != AuthJSONRPCServer.FAILURE:
request.setResponseCode(response_code) request.setResponseCode(response_code)
request.write(fault) self._render_message(request, fault)
request.finish()
def _handle_dropped_request(self, result, d, function_name):
if not d.called:
log.warning("Cancelling dropped api request %s", function_name)
d.cancel()
def render(self, request): def render(self, request):
notify_finish = request.notifyFinish() time_in = utils.now()
assert self._check_headers(request), InvalidHeaderError assert self._check_headers(request), InvalidHeaderError
session = request.getSession() session = request.getSession()
session_id = session.uid session_id = session.uid
finished_deferred = request.notifyFinish()
if self._use_authentication: if self._use_authentication:
# if this is a new session, send a new secret and set the expiration # if this is a new session, send a new secret and set the expiration
@ -157,9 +189,9 @@ class AuthJSONRPCServer(AuthorizedBase):
try: try:
function = self._get_jsonrpc_method(function_name) function = self._get_jsonrpc_method(function_name)
except AttributeError as err: except (UnknownAPIMethodError, NotAllowedDuringStartupError) as err:
log.warning("Unknown method: %s", function_name) log.warning('Failed to get function %s: %s', function_name, err)
self._render_error(err, request, id_, version) self._render_error(err, request, version)
return server.NOT_DONE_YET return server.NOT_DONE_YET
if args == EMPTY_PARAMS: if args == EMPTY_PARAMS:
@ -167,13 +199,22 @@ class AuthJSONRPCServer(AuthorizedBase):
else: else:
d = defer.maybeDeferred(function, *args) d = defer.maybeDeferred(function, *args)
# cancel the response if the connection is broken # finished_deferred will callback when the request is finished
notify_finish.addErrback(self._response_failed, d) # and errback if something went wrong. If the errback is
# called, cancel the deferred stack. This is to prevent
# request.finish() from being called on a closed request.
finished_deferred.addErrback(self._handle_dropped_request, d, function_name)
d.addCallback(self._callback_render, request, id_, version, reply_with_next_secret) d.addCallback(self._callback_render, request, id_, version, reply_with_next_secret)
d.addErrback( # TODO: don't trap RuntimeError, which is presently caught to
log.fail(self._render_error, request, id_, version=version), # handle deferredLists that won't peacefully cancel, namely
'Failed to process %s', function_name # get_lbry_files
) d.addErrback(trap, ConnectionDone, ConnectionLost, defer.CancelledError, RuntimeError)
d.addErrback(log.fail(self._render_error, request, version=version),
'Failed to process %s', function_name)
d.addBoth(lambda _: log.debug("%s took %f",
function_name,
(utils.now() - time_in).total_seconds()))
return server.NOT_DONE_YET return server.NOT_DONE_YET
def _register_user_session(self, session_id): def _register_user_session(self, session_id):
@ -191,22 +232,6 @@ class AuthJSONRPCServer(AuthorizedBase):
log.info("Unregister API session") log.info("Unregister API session")
del self.sessions[session_id] del self.sessions[session_id]
def _response_failed(self, err, call):
log.debug(err.getTraceback())
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)))
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): def _check_headers(self, request):
return ( return (
self._check_header_source(request, 'Origin') and self._check_header_source(request, 'Origin') and
@ -239,19 +264,15 @@ class AuthJSONRPCServer(AuthorizedBase):
else: else:
return server_port[0], 80 return server_port[0], 80
def _check_function_path(self, function_path): def _verify_method_is_callable(self, function_path):
if function_path not in self.callable_methods: if function_path not in self.callable_methods:
log.warning("Unknown method: %s", function_path) raise UnknownAPIMethodError(function_path)
return False
if not self.announced_startup: if not self.announced_startup:
if function_path not in self.allowed_during_startup: if function_path not in self.allowed_during_startup:
log.warning("Cannot call %s during startup", function_path) raise NotAllowedDuringStartupError(function_path)
return False
return True
def _get_jsonrpc_method(self, function_path): def _get_jsonrpc_method(self, function_path):
if not self._check_function_path(function_path): self._verify_method_is_callable(function_path)
raise AttributeError(function_path)
return self.callable_methods.get(function_path) return self.callable_methods.get(function_path)
def _initialize_session(self, session_id): def _initialize_session(self, session_id):