##// END OF EJS Templates
removed id param from required api params. It was left over by mistake
marcink -
r1994:419ad277 default
parent child Browse files
Show More
@@ -1,257 +1,256
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(result=None, error=message)),
65 resp = Response(body=json.dumps(dict(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']
122 self._req_method = json_body['method']
121 self._req_method = json_body['method']
123 self._request_params = json_body['args']
122 self._request_params = json_body['args']
124 log.debug('method: %s, params: %s',
123 log.debug('method: %s, params: %s',
125 self._req_method,
124 self._req_method,
126 self._request_params)
125 self._request_params)
127 except KeyError, e:
126 except KeyError, e:
128 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
127 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
129
128
130 # check if we can find this session using api_key
129 # check if we can find this session using api_key
131 try:
130 try:
132 u = User.get_by_api_key(self._req_api_key)
131 u = User.get_by_api_key(self._req_api_key)
133 if u is None:
132 if u is None:
134 return jsonrpc_error(message='Invalid API KEY')
133 return jsonrpc_error(message='Invalid API KEY')
135 auth_u = AuthUser(u.user_id, self._req_api_key)
134 auth_u = AuthUser(u.user_id, self._req_api_key)
136 except Exception, e:
135 except Exception, e:
137 return jsonrpc_error(message='Invalid API KEY')
136 return jsonrpc_error(message='Invalid API KEY')
138
137
139 self._error = None
138 self._error = None
140 try:
139 try:
141 self._func = self._find_method()
140 self._func = self._find_method()
142 except AttributeError, e:
141 except AttributeError, e:
143 return jsonrpc_error(message=str(e))
142 return jsonrpc_error(message=str(e))
144
143
145 # now that we have a method, add self._req_params to
144 # now that we have a method, add self._req_params to
146 # self.kargs and dispatch control to WGIController
145 # self.kargs and dispatch control to WGIController
147 argspec = inspect.getargspec(self._func)
146 argspec = inspect.getargspec(self._func)
148 arglist = argspec[0][1:]
147 arglist = argspec[0][1:]
149 defaults = map(type, argspec[3] or [])
148 defaults = map(type, argspec[3] or [])
150 default_empty = types.NotImplementedType
149 default_empty = types.NotImplementedType
151
150
152 # kw arguments required by this method
151 # kw arguments required by this method
153 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
152 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
154 fillvalue=default_empty))
153 fillvalue=default_empty))
155
154
156 # this is little trick to inject logged in user for
155 # this is little trick to inject logged in user for
157 # perms decorators to work they expect the controller class to have
156 # perms decorators to work they expect the controller class to have
158 # rhodecode_user attribute set
157 # rhodecode_user attribute set
159 self.rhodecode_user = auth_u
158 self.rhodecode_user = auth_u
160
159
161 # This attribute will need to be first param of a method that uses
160 # 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
161 # api_key, which is translated to instance of user at that name
163 USER_SESSION_ATTR = 'apiuser'
162 USER_SESSION_ATTR = 'apiuser'
164
163
165 if USER_SESSION_ATTR not in arglist:
164 if USER_SESSION_ATTR not in arglist:
166 return jsonrpc_error(message='This method [%s] does not support '
165 return jsonrpc_error(message='This method [%s] does not support '
167 'authentication (missing %s param)' %
166 'authentication (missing %s param)' %
168 (self._func.__name__, USER_SESSION_ATTR))
167 (self._func.__name__, USER_SESSION_ATTR))
169
168
170 # get our arglist and check if we provided them as args
169 # get our arglist and check if we provided them as args
171 for arg, default in func_kwargs.iteritems():
170 for arg, default in func_kwargs.iteritems():
172 if arg == USER_SESSION_ATTR:
171 if arg == USER_SESSION_ATTR:
173 # USER_SESSION_ATTR is something translated from api key and
172 # USER_SESSION_ATTR is something translated from api key and
174 # this is checked before so we don't need validate it
173 # this is checked before so we don't need validate it
175 continue
174 continue
176
175
177 # skip the required param check if it's default value is
176 # skip the required param check if it's default value is
178 # NotImplementedType (default_empty)
177 # NotImplementedType (default_empty)
179 if (default == default_empty and arg not in self._request_params):
178 if (default == default_empty and arg not in self._request_params):
180 return jsonrpc_error(
179 return jsonrpc_error(
181 message=(
180 message=(
182 'Missing non optional `%s` arg in JSON DATA' % arg
181 'Missing non optional `%s` arg in JSON DATA' % arg
183 )
182 )
184 )
183 )
185
184
186 self._rpc_args = {USER_SESSION_ATTR: u}
185 self._rpc_args = {USER_SESSION_ATTR: u}
187 self._rpc_args.update(self._request_params)
186 self._rpc_args.update(self._request_params)
188
187
189 self._rpc_args['action'] = self._req_method
188 self._rpc_args['action'] = self._req_method
190 self._rpc_args['environ'] = environ
189 self._rpc_args['environ'] = environ
191 self._rpc_args['start_response'] = start_response
190 self._rpc_args['start_response'] = start_response
192
191
193 status = []
192 status = []
194 headers = []
193 headers = []
195 exc_info = []
194 exc_info = []
196
195
197 def change_content(new_status, new_headers, new_exc_info=None):
196 def change_content(new_status, new_headers, new_exc_info=None):
198 status.append(new_status)
197 status.append(new_status)
199 headers.extend(new_headers)
198 headers.extend(new_headers)
200 exc_info.append(new_exc_info)
199 exc_info.append(new_exc_info)
201
200
202 output = WSGIController.__call__(self, environ, change_content)
201 output = WSGIController.__call__(self, environ, change_content)
203 output = list(output)
202 output = list(output)
204 headers.append(('Content-Length', str(len(output[0]))))
203 headers.append(('Content-Length', str(len(output[0]))))
205 replace_header(headers, 'Content-Type', 'application/json')
204 replace_header(headers, 'Content-Type', 'application/json')
206 start_response(status[0], headers, exc_info[0])
205 start_response(status[0], headers, exc_info[0])
207
206
208 return output
207 return output
209
208
210 def _dispatch_call(self):
209 def _dispatch_call(self):
211 """
210 """
212 Implement dispatch interface specified by WSGIController
211 Implement dispatch interface specified by WSGIController
213 """
212 """
214 try:
213 try:
215 raw_response = self._inspect_call(self._func)
214 raw_response = self._inspect_call(self._func)
216 if isinstance(raw_response, HTTPError):
215 if isinstance(raw_response, HTTPError):
217 self._error = str(raw_response)
216 self._error = str(raw_response)
218 except JSONRPCError, e:
217 except JSONRPCError, e:
219 self._error = str(e)
218 self._error = str(e)
220 except Exception, e:
219 except Exception, e:
221 log.error('Encountered unhandled exception: %s' \
220 log.error('Encountered unhandled exception: %s' \
222 % traceback.format_exc())
221 % traceback.format_exc())
223 json_exc = JSONRPCError('Internal server error')
222 json_exc = JSONRPCError('Internal server error')
224 self._error = str(json_exc)
223 self._error = str(json_exc)
225
224
226 if self._error is not None:
225 if self._error is not None:
227 raw_response = None
226 raw_response = None
228
227
229 response = dict(result=raw_response,
228 response = dict(result=raw_response,
230 error=self._error)
229 error=self._error)
231
230
232 try:
231 try:
233 return json.dumps(response)
232 return json.dumps(response)
234 except TypeError, e:
233 except TypeError, e:
235 log.debug('Error encoding response: %s', e)
234 log.debug('Error encoding response: %s', e)
236 return json.dumps(dict(result=None,
235 return json.dumps(dict(result=None,
237 error="Error encoding response"))
236 error="Error encoding response"))
238
237
239 def _find_method(self):
238 def _find_method(self):
240 """
239 """
241 Return method named by `self._req_method` in controller if able
240 Return method named by `self._req_method` in controller if able
242 """
241 """
243 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
242 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
244 if self._req_method.startswith('_'):
243 if self._req_method.startswith('_'):
245 raise AttributeError("Method not allowed")
244 raise AttributeError("Method not allowed")
246
245
247 try:
246 try:
248 func = getattr(self, self._req_method, None)
247 func = getattr(self, self._req_method, None)
249 except UnicodeEncodeError:
248 except UnicodeEncodeError:
250 raise AttributeError("Problem decoding unicode in requested "
249 raise AttributeError("Problem decoding unicode in requested "
251 "method name.")
250 "method name.")
252
251
253 if isinstance(func, types.MethodType):
252 if isinstance(func, types.MethodType):
254 return func
253 return func
255 else:
254 else:
256 raise AttributeError("No such method: %s" % self._req_method)
255 raise AttributeError("No such method: %s" % self._req_method)
257
256
General Comments 0
You need to be logged in to leave comments. Login now