__init__.py
302 lines
| 10.3 KiB
| text/x-python
|
PythonLexer
r1445 | # -*- coding: utf-8 -*- | |||
|
r4116 | # 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. | ||||
r1818 | # | |||
r1445 | # 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. | ||||
r1818 | # | |||
r1445 | # You should have received a copy of the GNU General Public License | |||
|
r4116 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
""" | ||||
rhodecode.controllers.api | ||||
~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
JSON RPC controller | ||||
:created_on: Aug 20, 2011 | ||||
:author: marcink | ||||
:copyright: (c) 2013 RhodeCode GmbH. | ||||
:license: GPLv3, see LICENSE for more details. | ||||
""" | ||||
r1445 | ||||
import inspect | ||||
import logging | ||||
import types | ||||
import urllib | ||||
r1508 | import traceback | |||
r2656 | import time | |||
r1514 | ||||
r1445 | from paste.response import replace_header | |||
from pylons.controllers import WSGIController | ||||
r1661 | ||||
|
r4116 | from webob.exc import HTTPError | ||
r1445 | ||||
r1489 | from rhodecode.model.db import User | |||
r3163 | from rhodecode.model import meta | |||
from rhodecode.lib.compat import izip_longest, json | ||||
r3146 | from rhodecode.lib.auth import AuthUser | |||
|
r4116 | from rhodecode.lib.base import _get_ip_addr as _get_ip, _get_access_path | ||
from rhodecode.lib.utils2 import safe_unicode, safe_str | ||||
r1445 | ||||
log = logging.getLogger('JSONRPC') | ||||
r1793 | ||||
r1445 | class JSONRPCError(BaseException): | |||
def __init__(self, message): | ||||
self.message = message | ||||
r1708 | super(JSONRPCError, self).__init__() | |||
r1445 | ||||
def __str__(self): | ||||
|
r4116 | return safe_str(self.message) | ||
r1445 | ||||
r2379 | def jsonrpc_error(message, retid=None, code=None): | |||
r1661 | """ | |||
Generate a Response object with a JSON-RPC error body | ||||
|
r4116 | |||
:param code: | ||||
:param retid: | ||||
:param message: | ||||
r1661 | """ | |||
from pylons.controllers.util import Response | ||||
r2379 | return Response( | |||
|
r4116 | body=json.dumps(dict(id=retid, result=None, error=message)), | ||
status=code, | ||||
content_type='application/json' | ||||
r2379 | ) | |||
r1661 | ||||
r1445 | ||||
class JSONRPCController(WSGIController): | ||||
""" | ||||
A WSGI-speaking JSON-RPC controller class | ||||
r1793 | ||||
r1445 | See the specification: | |||
<http://json-rpc.org/wiki/specification>`. | ||||
r1793 | ||||
r1445 | Valid controller return values should be json-serializable objects. | |||
r1793 | ||||
r1445 | Sub-classes should catch their exceptions and raise JSONRPCError | |||
if they want to pass meaningful errors to the client. | ||||
r1793 | ||||
r1445 | """ | |||
r3126 | def _get_ip_addr(self, environ): | |||
|
r4116 | return _get_ip(environ) | ||
r3126 | ||||
r1445 | def _get_method_args(self): | |||
""" | ||||
Return `self._rpc_args` to dispatched controller method | ||||
chosen by __call__ | ||||
""" | ||||
return self._rpc_args | ||||
def __call__(self, environ, start_response): | ||||
""" | ||||
Parse the request body as JSON, look up the method on the | ||||
controller and if it exists, dispatch to it. | ||||
""" | ||||
|
r4116 | try: | ||
return self._handle_request(environ, start_response) | ||||
finally: | ||||
meta.Session.remove() | ||||
def _handle_request(self, environ, start_response): | ||||
r2656 | start = time.time() | |||
r3126 | ip_addr = self.ip_addr = self._get_ip_addr(environ) | |||
r2379 | self._req_id = None | |||
r1445 | if 'CONTENT_LENGTH' not in environ: | |||
log.debug("No Content-Length") | ||||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
message="No Content-Length in request") | ||||
r1445 | else: | |||
length = environ['CONTENT_LENGTH'] or 0 | ||||
length = int(environ['CONTENT_LENGTH']) | ||||
r1976 | log.debug('Content-Length: %s' % length) | |||
r1445 | ||||
if length == 0: | ||||
log.debug("Content-Length is 0") | ||||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
message="Content-Length is 0") | ||||
r1445 | ||||
raw_body = environ['wsgi.input'].read(length) | ||||
try: | ||||
|
r4116 | json_body = json.loads(raw_body) | ||
r1514 | except ValueError, e: | |||
r1793 | # catch JSON errors Here | |||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
|
r4116 | message="JSON parse error ERR:%s RAW:%r" | ||
% (e, raw_body)) | ||||
r1445 | ||||
r1693 | # check AUTH based on API KEY | |||
r1445 | try: | |||
self._req_api_key = json_body['api_key'] | ||||
r1708 | self._req_id = json_body['id'] | |||
r1445 | self._req_method = json_body['method'] | |||
r1793 | self._request_params = json_body['args'] | |||
r3165 | if not isinstance(self._request_params, dict): | |||
self._request_params = {} | ||||
r1976 | log.debug( | |||
'method: %s, params: %s' % (self._req_method, | ||||
self._request_params) | ||||
) | ||||
r1514 | except KeyError, e: | |||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
message='Incorrect JSON query missing %s' % e) | ||||
r1445 | ||||
r1693 | # check if we can find this session using api_key | |||
r1445 | try: | |||
u = User.get_by_api_key(self._req_api_key) | ||||
r1693 | if u is None: | |||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
message='Invalid API KEY') | ||||
r3146 | ||||
r3125 | #check if we are allowed to use this IP | |||
r3146 | auth_u = AuthUser(u.user_id, self._req_api_key, ip_addr=ip_addr) | |||
if not auth_u.ip_allowed: | ||||
r3125 | return jsonrpc_error(retid=self._req_id, | |||
|
r4116 | message='request from IP:%s not allowed' % (ip_addr,)) | ||
r3125 | else: | |||
|
r4116 | log.info('Access for IP:%s allowed' % (ip_addr,)) | ||
r3125 | ||||
r1514 | except Exception, e: | |||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
message='Invalid API KEY') | ||||
r1445 | ||||
self._error = None | ||||
try: | ||||
self._func = self._find_method() | ||||
except AttributeError, e: | ||||
r2379 | return jsonrpc_error(retid=self._req_id, | |||
message=str(e)) | ||||
r1445 | ||||
# now that we have a method, add self._req_params to | ||||
# self.kargs and dispatch control to WGIController | ||||
r1504 | argspec = inspect.getargspec(self._func) | |||
arglist = argspec[0][1:] | ||||
r1793 | defaults = map(type, argspec[3] or []) | |||
r1504 | default_empty = types.NotImplementedType | |||
r1514 | ||||
r1793 | # kw arguments required by this method | |||
func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults), | ||||
fillvalue=default_empty)) | ||||
r1445 | ||||
r1793 | # this is little trick to inject logged in user for | |||
r1445 | # perms decorators to work they expect the controller class to have | |||
r1500 | # rhodecode_user attribute set | |||
r1445 | self.rhodecode_user = auth_u | |||
r1500 | # 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' | ||||
if USER_SESSION_ATTR not in arglist: | ||||
r2379 | return jsonrpc_error( | |||
retid=self._req_id, | ||||
message='This method [%s] does not support ' | ||||
'authentication (missing %s param)' % ( | ||||
self._func.__name__, USER_SESSION_ATTR) | ||||
) | ||||
r1445 | ||||
# get our arglist and check if we provided them as args | ||||
r1793 | for arg, default in func_kwargs.iteritems(): | |||
r1500 | if arg == USER_SESSION_ATTR: | |||
r1793 | # USER_SESSION_ATTR is something translated from api key and | |||
r1500 | # this is checked before so we don't need validate it | |||
r1445 | continue | |||
r1514 | ||||
r1793 | # skip the required param check if it's default value is | |||
r1504 | # NotImplementedType (default_empty) | |||
|
r4116 | if default == default_empty and arg not in self._request_params: | ||
r1793 | return jsonrpc_error( | |||
r2379 | retid=self._req_id, | |||
r1793 | message=( | |||
'Missing non optional `%s` arg in JSON DATA' % arg | ||||
) | ||||
) | ||||
r1445 | ||||
r1793 | self._rpc_args = {USER_SESSION_ATTR: u} | |||
r3165 | ||||
r1793 | self._rpc_args.update(self._request_params) | |||
r1445 | ||||
self._rpc_args['action'] = self._req_method | ||||
self._rpc_args['environ'] = environ | ||||
self._rpc_args['start_response'] = start_response | ||||
status = [] | ||||
headers = [] | ||||
exc_info = [] | ||||
r1793 | ||||
r1445 | def change_content(new_status, new_headers, new_exc_info=None): | |||
status.append(new_status) | ||||
headers.extend(new_headers) | ||||
exc_info.append(new_exc_info) | ||||
output = WSGIController.__call__(self, environ, change_content) | ||||
output = list(output) | ||||
headers.append(('Content-Length', str(len(output[0])))) | ||||
replace_header(headers, 'Content-Type', 'application/json') | ||||
start_response(status[0], headers, exc_info[0]) | ||||
r2656 | log.info('IP: %s Request to %s time: %.3fs' % ( | |||
|
r4116 | self._get_ip_addr(environ), | ||
r2656 | safe_unicode(_get_access_path(environ)), time.time() - start) | |||
) | ||||
r1445 | return output | |||
def _dispatch_call(self): | ||||
""" | ||||
Implement dispatch interface specified by WSGIController | ||||
""" | ||||
|
r4116 | raw_response = '' | ||
r1445 | try: | |||
raw_response = self._inspect_call(self._func) | ||||
if isinstance(raw_response, HTTPError): | ||||
self._error = str(raw_response) | ||||
r1514 | except JSONRPCError, e: | |||
|
r4116 | self._error = safe_str(e) | ||
r1514 | except Exception, e: | |||
|
r4116 | log.error('Encountered unhandled exception: %s' | ||
% (traceback.format_exc(),)) | ||||
r1445 | json_exc = JSONRPCError('Internal server error') | |||
|
r4116 | self._error = safe_str(json_exc) | ||
r1445 | ||||
if self._error is not None: | ||||
raw_response = None | ||||
|
r4116 | response = dict(id=self._req_id, result=raw_response, error=self._error) | ||
r1445 | try: | |||
return json.dumps(response) | ||||
except TypeError, e: | ||||
r2151 | log.error('API FAILED. Error encoding response: %s' % e) | |||
r1796 | return json.dumps( | |||
dict( | ||||
r2151 | id=self._req_id, | |||
r1796 | result=None, | |||
error="Error encoding response" | ||||
) | ||||
) | ||||
r1445 | ||||
def _find_method(self): | ||||
""" | ||||
Return method named by `self._req_method` in controller if able | ||||
""" | ||||
|
r4116 | log.debug('Trying to find JSON-RPC method: %s' % (self._req_method,)) | ||
r1445 | 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: | ||||
|
r4116 | raise AttributeError("No such method: %s" % (self._req_method,)) | ||