__init__.py
269 lines
| 9.5 KiB
| text/x-python
|
PythonLexer
Bradley M. Kuhn
|
r4187 | # -*- coding: utf-8 -*- | ||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
""" | ||||
kallithea.controllers.api | ||||
~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
JSON RPC controller | ||||
Bradley M. Kuhn
|
r4211 | This file was forked by the Kallithea project in July 2014. | ||
Original author and date, and relevant copyright and licensing information is below: | ||||
Bradley M. Kuhn
|
r4187 | :created_on: Aug 20, 2011 | ||
:author: marcink | ||||
Bradley M. Kuhn
|
r4211 | :copyright: (c) 2013 RhodeCode GmbH, and others. | ||
Bradley M. Kuhn
|
r4208 | :license: GPLv3, see LICENSE.md for more details. | ||
Bradley M. Kuhn
|
r4187 | """ | ||
import inspect | ||||
Mads Kiilerich
|
r7718 | import itertools | ||
Bradley M. Kuhn
|
r4187 | import logging | ||
import time | ||||
Mads Kiilerich
|
r7718 | import traceback | ||
import types | ||||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7718 | from tg import Response, TGController, request, response | ||
Mads Kiilerich
|
r7719 | from webob.exc import HTTPError, HTTPException | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7987 | from kallithea.lib import ext_json | ||
Mads Kiilerich
|
r7718 | from kallithea.lib.auth import AuthUser | ||
from kallithea.lib.base import _get_ip_addr as _get_ip | ||||
Mads Kiilerich
|
r7948 | from kallithea.lib.base import get_path_info | ||
Mads Kiilerich
|
r8076 | from kallithea.lib.utils2 import ascii_bytes | ||
Mads Kiilerich
|
r7718 | from kallithea.model.db import User | ||
Bradley M. Kuhn
|
r4187 | |||
log = logging.getLogger('JSONRPC') | ||||
class JSONRPCError(BaseException): | ||||
def __init__(self, message): | ||||
self.message = message | ||||
super(JSONRPCError, self).__init__() | ||||
def __str__(self): | ||||
Mads Kiilerich
|
r8076 | return self.message | ||
Bradley M. Kuhn
|
r4187 | |||
Alessandro Molina
|
r6522 | class JSONRPCErrorResponse(Response, HTTPException): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Generate a Response object with a JSON-RPC error body | ||||
Mads Kiilerich
|
r6339 | """ | ||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r6339 | def __init__(self, message=None, retid=None, code=None): | ||
Alessandro Molina
|
r6522 | HTTPException.__init__(self, message, self) | ||
Mads Kiilerich
|
r6339 | Response.__init__(self, | ||
Alessandro Molina
|
r6522 | json_body=dict(id=retid, result=None, error=message), | ||
Mads Kiilerich
|
r6339 | status=code, | ||
content_type='application/json') | ||||
Bradley M. Kuhn
|
r4187 | |||
Alessandro Molina
|
r6522 | class JSONRPCController(TGController): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
A WSGI-speaking JSON-RPC controller class | ||||
See the specification: | ||||
<http://json-rpc.org/wiki/specification>`. | ||||
Valid controller return values should be json-serializable objects. | ||||
Sub-classes should catch their exceptions and raise JSONRPCError | ||||
if they want to pass meaningful errors to the client. | ||||
""" | ||||
def _get_ip_addr(self, environ): | ||||
return _get_ip(environ) | ||||
def _get_method_args(self): | ||||
""" | ||||
Return `self._rpc_args` to dispatched controller method | ||||
chosen by __call__ | ||||
""" | ||||
return self._rpc_args | ||||
Alessandro Molina
|
r6522 | def _dispatch(self, state, remainder=None): | ||
Bradley M. Kuhn
|
r4187 | """ | ||
Parse the request body as JSON, look up the method on the | ||||
controller and if it exists, dispatch to it. | ||||
""" | ||||
Alessandro Molina
|
r6522 | # Since we are here we should respond as JSON | ||
response.content_type = 'application/json' | ||||
Bradley M. Kuhn
|
r4187 | |||
Alessandro Molina
|
r6522 | environ = state.request.environ | ||
Bradley M. Kuhn
|
r4187 | start = time.time() | ||
Mads Kiilerich
|
r7603 | ip_addr = self._get_ip_addr(environ) | ||
Bradley M. Kuhn
|
r4187 | self._req_id = None | ||
if 'CONTENT_LENGTH' not in environ: | ||||
log.debug("No Content-Length") | ||||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message="No Content-Length in request") | ||||
Bradley M. Kuhn
|
r4187 | else: | ||
length = environ['CONTENT_LENGTH'] or 0 | ||||
length = int(environ['CONTENT_LENGTH']) | ||||
Mads Kiilerich
|
r5375 | log.debug('Content-Length: %s', length) | ||
Bradley M. Kuhn
|
r4187 | |||
if length == 0: | ||||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message="Content-Length is 0") | ||||
Bradley M. Kuhn
|
r4187 | |||
raw_body = environ['wsgi.input'].read(length) | ||||
try: | ||||
Mads Kiilerich
|
r7987 | json_body = ext_json.loads(raw_body) | ||
Mads Kiilerich
|
r5374 | except ValueError as e: | ||
Bradley M. Kuhn
|
r4187 | # catch JSON errors Here | ||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message="JSON parse error ERR:%s RAW:%r" | ||||
% (e, raw_body)) | ||||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r5124 | # check AUTH based on API key | ||
Bradley M. Kuhn
|
r4187 | try: | ||
self._req_api_key = json_body['api_key'] | ||||
self._req_id = json_body['id'] | ||||
self._req_method = json_body['method'] | ||||
self._request_params = json_body['args'] | ||||
if not isinstance(self._request_params, dict): | ||||
self._request_params = {} | ||||
Mads Kiilerich
|
r6339 | log.debug('method: %s, params: %s', | ||
self._req_method, self._request_params) | ||||
Mads Kiilerich
|
r5374 | except KeyError as e: | ||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message='Incorrect JSON query missing %s' % e) | ||||
Bradley M. Kuhn
|
r4187 | |||
# check if we can find this session using api_key | ||||
try: | ||||
u = User.get_by_api_key(self._req_api_key) | ||||
Mads Kiilerich
|
r7603 | auth_user = AuthUser.make(dbuser=u, ip_addr=ip_addr) | ||
Mads Kiilerich
|
r7602 | if auth_user is None: | ||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message='Invalid API key') | ||||
Mads Kiilerich
|
r5374 | except Exception as e: | ||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message='Invalid API key') | ||||
Bradley M. Kuhn
|
r4187 | |||
Mads Kiilerich
|
r7602 | request.authuser = auth_user | ||
Mads Kiilerich
|
r7603 | request.ip_addr = ip_addr | ||
Mads Kiilerich
|
r7602 | |||
Bradley M. Kuhn
|
r4187 | self._error = None | ||
try: | ||||
self._func = self._find_method() | ||||
Mads Kiilerich
|
r5374 | except AttributeError as e: | ||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse(retid=self._req_id, | ||
message=str(e)) | ||||
Bradley M. Kuhn
|
r4187 | |||
# now that we have a method, add self._req_params to | ||||
# self.kargs and dispatch control to WGIController | ||||
Mads Kiilerich
|
r8072 | argspec = inspect.getfullargspec(self._func) | ||
arglist = argspec.args[1:] | ||||
argtypes = [type(arg) for arg in argspec.defaults or []] | ||||
Mads Kiilerich
|
r7891 | default_empty = type(NotImplemented) | ||
Bradley M. Kuhn
|
r4187 | |||
# kw arguments required by this method | ||||
Mads Kiilerich
|
r8072 | func_kwargs = dict(itertools.zip_longest(reversed(arglist), reversed(argtypes), | ||
Mads Kiilerich
|
r6171 | fillvalue=default_empty)) | ||
Bradley M. Kuhn
|
r4187 | |||
# This attribute will need to be first param of a method that uses | ||||
# api_key, which is translated to instance of user at that name | ||||
USER_SESSION_ATTR = 'apiuser' | ||||
# get our arglist and check if we provided them as args | ||||
Mads Kiilerich
|
r8059 | for arg, default in func_kwargs.items(): | ||
Bradley M. Kuhn
|
r4187 | if arg == USER_SESSION_ATTR: | ||
Mads Kiilerich
|
r5124 | # USER_SESSION_ATTR is something translated from API key and | ||
Bradley M. Kuhn
|
r4187 | # this is checked before so we don't need validate it | ||
continue | ||||
# skip the required param check if it's default value is | ||||
# NotImplementedType (default_empty) | ||||
if default == default_empty and arg not in self._request_params: | ||||
Mads Kiilerich
|
r6339 | raise JSONRPCErrorResponse( | ||
Bradley M. Kuhn
|
r4187 | retid=self._req_id, | ||
Mads Kiilerich
|
r6339 | message='Missing non optional `%s` arg in JSON DATA' % arg, | ||
Bradley M. Kuhn
|
r4187 | ) | ||
Mads Kiilerich
|
r6175 | extra = set(self._request_params).difference(func_kwargs) | ||
if extra: | ||||
Mads Kiilerich
|
r7731 | raise JSONRPCErrorResponse( | ||
retid=self._req_id, | ||||
message='Unknown %s arg in JSON DATA' % | ||||
', '.join('`%s`' % arg for arg in extra), | ||||
) | ||||
Mads Kiilerich
|
r6175 | |||
Mads Kiilerich
|
r6174 | self._rpc_args = {} | ||
Bradley M. Kuhn
|
r4187 | self._rpc_args.update(self._request_params) | ||
self._rpc_args['action'] = self._req_method | ||||
self._rpc_args['environ'] = environ | ||||
log.info('IP: %s Request to %s time: %.3fs' % ( | ||||
self._get_ip_addr(environ), | ||||
Mads Kiilerich
|
r7948 | get_path_info(environ), time.time() - start) | ||
Bradley M. Kuhn
|
r4187 | ) | ||
Alessandro Molina
|
r6522 | state.set_action(self._rpc_call, []) | ||
state.set_params(self._rpc_args) | ||||
return state | ||||
def _rpc_call(self, action, environ, **rpc_args): | ||||
Bradley M. Kuhn
|
r4187 | """ | ||
Alessandro Molina
|
r6522 | Call the specified RPC Method | ||
Bradley M. Kuhn
|
r4187 | """ | ||
raw_response = '' | ||||
try: | ||||
Alessandro Molina
|
r6522 | raw_response = getattr(self, action)(**rpc_args) | ||
Bradley M. Kuhn
|
r4187 | if isinstance(raw_response, HTTPError): | ||
self._error = str(raw_response) | ||||
Mads Kiilerich
|
r5374 | except JSONRPCError as e: | ||
Mads Kiilerich
|
r7912 | self._error = unicode(e) | ||
Mads Kiilerich
|
r5374 | except Exception as e: | ||
Mads Kiilerich
|
r5375 | log.error('Encountered unhandled exception: %s', | ||
traceback.format_exc(),) | ||||
Bradley M. Kuhn
|
r4187 | json_exc = JSONRPCError('Internal server error') | ||
Mads Kiilerich
|
r7912 | self._error = unicode(json_exc) | ||
Bradley M. Kuhn
|
r4187 | |||
if self._error is not None: | ||||
raw_response = None | ||||
response = dict(id=self._req_id, result=raw_response, error=self._error) | ||||
try: | ||||
Mads Kiilerich
|
r7987 | return ascii_bytes(ext_json.dumps(response)) | ||
Mads Kiilerich
|
r5374 | except TypeError as e: | ||
Mads Kiilerich
|
r7954 | log.error('API FAILED. Error encoding response for %s %s: %s\n%s', action, rpc_args, e, traceback.format_exc()) | ||
Mads Kiilerich
|
r7987 | return ascii_bytes(ext_json.dumps( | ||
Bradley M. Kuhn
|
r4187 | dict( | ||
id=self._req_id, | ||||
result=None, | ||||
Mads Kiilerich
|
r7987 | error="Error encoding response", | ||
Bradley M. Kuhn
|
r4187 | ) | ||
Mads Kiilerich
|
r7987 | )) | ||
Bradley M. Kuhn
|
r4187 | |||
def _find_method(self): | ||||
""" | ||||
Return method named by `self._req_method` in controller if able | ||||
""" | ||||
Mads Kiilerich
|
r5375 | log.debug('Trying to find JSON-RPC method: %s', self._req_method) | ||
Bradley M. Kuhn
|
r4187 | if self._req_method.startswith('_'): | ||
raise AttributeError("Method not allowed") | ||||
try: | ||||
func = getattr(self, self._req_method, None) | ||||
except UnicodeEncodeError: | ||||
raise AttributeError("Problem decoding unicode in requested " | ||||
"method name.") | ||||
if isinstance(func, types.MethodType): | ||||
return func | ||||
else: | ||||
raise AttributeError("No such method: %s" % (self._req_method,)) | ||||