##// END OF EJS Templates
fixed RPC call for api that was missing request id
marcink -
r1796:6274adc0 beta
parent child Browse files
Show More
@@ -1,257 +1,262
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.api
3 rhodecode.controllers.api
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 JSON RPC controller
6 JSON RPC controller
7
7
8 :created_on: Aug 20, 2011
8 :created_on: Aug 20, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
16 # of the License or (at your opinion) any later version of the license.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
27
27
28 import inspect
28 import inspect
29 import logging
29 import logging
30 import types
30 import types
31 import urllib
31 import urllib
32 import traceback
32 import traceback
33
33
34 from rhodecode.lib.compat import izip_longest, json
34 from rhodecode.lib.compat import izip_longest, json
35
35
36 from paste.response import replace_header
36 from paste.response import replace_header
37
37
38 from pylons.controllers import WSGIController
38 from pylons.controllers import WSGIController
39
39
40
40
41 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
41 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
42 HTTPBadRequest, HTTPError
42 HTTPBadRequest, HTTPError
43
43
44 from rhodecode.model.db import User
44 from rhodecode.model.db import User
45 from rhodecode.lib.auth import AuthUser
45 from rhodecode.lib.auth import AuthUser
46
46
47 log = logging.getLogger('JSONRPC')
47 log = logging.getLogger('JSONRPC')
48
48
49
49
50 class JSONRPCError(BaseException):
50 class JSONRPCError(BaseException):
51
51
52 def __init__(self, message):
52 def __init__(self, message):
53 self.message = message
53 self.message = message
54 super(JSONRPCError, self).__init__()
54 super(JSONRPCError, self).__init__()
55
55
56 def __str__(self):
56 def __str__(self):
57 return str(self.message)
57 return str(self.message)
58
58
59
59
60 def jsonrpc_error(message, code=None):
60 def jsonrpc_error(message, code=None):
61 """
61 """
62 Generate a Response object with a JSON-RPC error body
62 Generate a Response object with a JSON-RPC error body
63 """
63 """
64 from pylons.controllers.util import Response
64 from pylons.controllers.util import Response
65 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
65 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
66 status=code,
66 status=code,
67 content_type='application/json')
67 content_type='application/json')
68 return resp
68 return resp
69
69
70
70
71 class JSONRPCController(WSGIController):
71 class JSONRPCController(WSGIController):
72 """
72 """
73 A WSGI-speaking JSON-RPC controller class
73 A WSGI-speaking JSON-RPC controller class
74
74
75 See the specification:
75 See the specification:
76 <http://json-rpc.org/wiki/specification>`.
76 <http://json-rpc.org/wiki/specification>`.
77
77
78 Valid controller return values should be json-serializable objects.
78 Valid controller return values should be json-serializable objects.
79
79
80 Sub-classes should catch their exceptions and raise JSONRPCError
80 Sub-classes should catch their exceptions and raise JSONRPCError
81 if they want to pass meaningful errors to the client.
81 if they want to pass meaningful errors to the client.
82
82
83 """
83 """
84
84
85 def _get_method_args(self):
85 def _get_method_args(self):
86 """
86 """
87 Return `self._rpc_args` to dispatched controller method
87 Return `self._rpc_args` to dispatched controller method
88 chosen by __call__
88 chosen by __call__
89 """
89 """
90 return self._rpc_args
90 return self._rpc_args
91
91
92 def __call__(self, environ, start_response):
92 def __call__(self, environ, start_response):
93 """
93 """
94 Parse the request body as JSON, look up the method on the
94 Parse the request body as JSON, look up the method on the
95 controller and if it exists, dispatch to it.
95 controller and if it exists, dispatch to it.
96 """
96 """
97 if 'CONTENT_LENGTH' not in environ:
97 if 'CONTENT_LENGTH' not in environ:
98 log.debug("No Content-Length")
98 log.debug("No Content-Length")
99 return jsonrpc_error(message="No Content-Length in request")
99 return jsonrpc_error(message="No Content-Length in request")
100 else:
100 else:
101 length = environ['CONTENT_LENGTH'] or 0
101 length = environ['CONTENT_LENGTH'] or 0
102 length = int(environ['CONTENT_LENGTH'])
102 length = int(environ['CONTENT_LENGTH'])
103 log.debug('Content-Length: %s', length)
103 log.debug('Content-Length: %s', length)
104
104
105 if length == 0:
105 if length == 0:
106 log.debug("Content-Length is 0")
106 log.debug("Content-Length is 0")
107 return jsonrpc_error(message="Content-Length is 0")
107 return jsonrpc_error(message="Content-Length is 0")
108
108
109 raw_body = environ['wsgi.input'].read(length)
109 raw_body = environ['wsgi.input'].read(length)
110
110
111 try:
111 try:
112 json_body = json.loads(urllib.unquote_plus(raw_body))
112 json_body = json.loads(urllib.unquote_plus(raw_body))
113 except ValueError, e:
113 except ValueError, e:
114 # catch JSON errors Here
114 # catch JSON errors Here
115 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
115 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
116 % (e, urllib.unquote_plus(raw_body)))
116 % (e, urllib.unquote_plus(raw_body)))
117
117
118 # check AUTH based on API KEY
118 # check AUTH based on API KEY
119 try:
119 try:
120 self._req_api_key = json_body['api_key']
120 self._req_api_key = json_body['api_key']
121 self._req_id = json_body['id']
121 self._req_id = json_body['id']
122 self._req_method = json_body['method']
122 self._req_method = json_body['method']
123 self._request_params = json_body['args']
123 self._request_params = json_body['args']
124 log.debug('method: %s, params: %s',
124 log.debug('method: %s, params: %s',
125 self._req_method,
125 self._req_method,
126 self._request_params)
126 self._request_params)
127 except KeyError, e:
127 except KeyError, e:
128 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
128 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
129
129
130 # check if we can find this session using api_key
130 # check if we can find this session using api_key
131 try:
131 try:
132 u = User.get_by_api_key(self._req_api_key)
132 u = User.get_by_api_key(self._req_api_key)
133 if u is None:
133 if u is None:
134 return jsonrpc_error(message='Invalid API KEY')
134 return jsonrpc_error(message='Invalid API KEY')
135 auth_u = AuthUser(u.user_id, self._req_api_key)
135 auth_u = AuthUser(u.user_id, self._req_api_key)
136 except Exception, e:
136 except Exception, e:
137 return jsonrpc_error(message='Invalid API KEY')
137 return jsonrpc_error(message='Invalid API KEY')
138
138
139 self._error = None
139 self._error = None
140 try:
140 try:
141 self._func = self._find_method()
141 self._func = self._find_method()
142 except AttributeError, e:
142 except AttributeError, e:
143 return jsonrpc_error(message=str(e))
143 return jsonrpc_error(message=str(e))
144
144
145 # now that we have a method, add self._req_params to
145 # now that we have a method, add self._req_params to
146 # self.kargs and dispatch control to WGIController
146 # self.kargs and dispatch control to WGIController
147 argspec = inspect.getargspec(self._func)
147 argspec = inspect.getargspec(self._func)
148 arglist = argspec[0][1:]
148 arglist = argspec[0][1:]
149 defaults = map(type, argspec[3] or [])
149 defaults = map(type, argspec[3] or [])
150 default_empty = types.NotImplementedType
150 default_empty = types.NotImplementedType
151
151
152 # kw arguments required by this method
152 # kw arguments required by this method
153 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
153 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
154 fillvalue=default_empty))
154 fillvalue=default_empty))
155
155
156 # this is little trick to inject logged in user for
156 # this is little trick to inject logged in user for
157 # perms decorators to work they expect the controller class to have
157 # perms decorators to work they expect the controller class to have
158 # rhodecode_user attribute set
158 # rhodecode_user attribute set
159 self.rhodecode_user = auth_u
159 self.rhodecode_user = auth_u
160
160
161 # This attribute will need to be first param of a method that uses
161 # This attribute will need to be first param of a method that uses
162 # api_key, which is translated to instance of user at that name
162 # api_key, which is translated to instance of user at that name
163 USER_SESSION_ATTR = 'apiuser'
163 USER_SESSION_ATTR = 'apiuser'
164
164
165 if USER_SESSION_ATTR not in arglist:
165 if USER_SESSION_ATTR not in arglist:
166 return jsonrpc_error(message='This method [%s] does not support '
166 return jsonrpc_error(message='This method [%s] does not support '
167 'authentication (missing %s param)' %
167 'authentication (missing %s param)' %
168 (self._func.__name__, USER_SESSION_ATTR))
168 (self._func.__name__, USER_SESSION_ATTR))
169
169
170 # get our arglist and check if we provided them as args
170 # get our arglist and check if we provided them as args
171 for arg, default in func_kwargs.iteritems():
171 for arg, default in func_kwargs.iteritems():
172 if arg == USER_SESSION_ATTR:
172 if arg == USER_SESSION_ATTR:
173 # USER_SESSION_ATTR is something translated from api key and
173 # USER_SESSION_ATTR is something translated from api key and
174 # this is checked before so we don't need validate it
174 # this is checked before so we don't need validate it
175 continue
175 continue
176
176
177 # skip the required param check if it's default value is
177 # skip the required param check if it's default value is
178 # NotImplementedType (default_empty)
178 # NotImplementedType (default_empty)
179 if (default == default_empty and arg not in self._request_params):
179 if (default == default_empty and arg not in self._request_params):
180 return jsonrpc_error(
180 return jsonrpc_error(
181 message=(
181 message=(
182 'Missing non optional `%s` arg in JSON DATA' % arg
182 'Missing non optional `%s` arg in JSON DATA' % arg
183 )
183 )
184 )
184 )
185
185
186 self._rpc_args = {USER_SESSION_ATTR: u}
186 self._rpc_args = {USER_SESSION_ATTR: u}
187 self._rpc_args.update(self._request_params)
187 self._rpc_args.update(self._request_params)
188
188
189 self._rpc_args['action'] = self._req_method
189 self._rpc_args['action'] = self._req_method
190 self._rpc_args['environ'] = environ
190 self._rpc_args['environ'] = environ
191 self._rpc_args['start_response'] = start_response
191 self._rpc_args['start_response'] = start_response
192
192
193 status = []
193 status = []
194 headers = []
194 headers = []
195 exc_info = []
195 exc_info = []
196
196
197 def change_content(new_status, new_headers, new_exc_info=None):
197 def change_content(new_status, new_headers, new_exc_info=None):
198 status.append(new_status)
198 status.append(new_status)
199 headers.extend(new_headers)
199 headers.extend(new_headers)
200 exc_info.append(new_exc_info)
200 exc_info.append(new_exc_info)
201
201
202 output = WSGIController.__call__(self, environ, change_content)
202 output = WSGIController.__call__(self, environ, change_content)
203 output = list(output)
203 output = list(output)
204 headers.append(('Content-Length', str(len(output[0]))))
204 headers.append(('Content-Length', str(len(output[0]))))
205 replace_header(headers, 'Content-Type', 'application/json')
205 replace_header(headers, 'Content-Type', 'application/json')
206 start_response(status[0], headers, exc_info[0])
206 start_response(status[0], headers, exc_info[0])
207
207
208 return output
208 return output
209
209
210 def _dispatch_call(self):
210 def _dispatch_call(self):
211 """
211 """
212 Implement dispatch interface specified by WSGIController
212 Implement dispatch interface specified by WSGIController
213 """
213 """
214 try:
214 try:
215 raw_response = self._inspect_call(self._func)
215 raw_response = self._inspect_call(self._func)
216 if isinstance(raw_response, HTTPError):
216 if isinstance(raw_response, HTTPError):
217 self._error = str(raw_response)
217 self._error = str(raw_response)
218 except JSONRPCError, e:
218 except JSONRPCError, e:
219 self._error = str(e)
219 self._error = str(e)
220 except Exception, e:
220 except Exception, e:
221 log.error('Encountered unhandled exception: %s' \
221 log.error('Encountered unhandled exception: %s' \
222 % traceback.format_exc())
222 % traceback.format_exc())
223 json_exc = JSONRPCError('Internal server error')
223 json_exc = JSONRPCError('Internal server error')
224 self._error = str(json_exc)
224 self._error = str(json_exc)
225
225
226 if self._error is not None:
226 if self._error is not None:
227 raw_response = None
227 raw_response = None
228
228
229 response = dict(id=self._req_id, result=raw_response,
229 response = dict(id=self._req_id, result=raw_response,
230 error=self._error)
230 error=self._error)
231
231
232 try:
232 try:
233 return json.dumps(response)
233 return json.dumps(response)
234 except TypeError, e:
234 except TypeError, e:
235 log.debug('Error encoding response: %s', e)
235 log.debug('Error encoding response: %s', e)
236 return json.dumps(dict(result=None,
236 return json.dumps(
237 error="Error encoding response"))
237 dict(
238 self._req_id,
239 result=None,
240 error="Error encoding response"
241 )
242 )
238
243
239 def _find_method(self):
244 def _find_method(self):
240 """
245 """
241 Return method named by `self._req_method` in controller if able
246 Return method named by `self._req_method` in controller if able
242 """
247 """
243 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
248 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
244 if self._req_method.startswith('_'):
249 if self._req_method.startswith('_'):
245 raise AttributeError("Method not allowed")
250 raise AttributeError("Method not allowed")
246
251
247 try:
252 try:
248 func = getattr(self, self._req_method, None)
253 func = getattr(self, self._req_method, None)
249 except UnicodeEncodeError:
254 except UnicodeEncodeError:
250 raise AttributeError("Problem decoding unicode in requested "
255 raise AttributeError("Problem decoding unicode in requested "
251 "method name.")
256 "method name.")
252
257
253 if isinstance(func, types.MethodType):
258 if isinstance(func, types.MethodType):
254 return func
259 return func
255 else:
260 else:
256 raise AttributeError("No such method: %s" % self._req_method)
261 raise AttributeError("No such method: %s" % self._req_method)
257
262
General Comments 0
You need to be logged in to leave comments. Login now