##// END OF EJS Templates
api: fixed potential crash when returning error response using JSON objects that fail to parse.
marcink -
r3316:899b726f default
parent child Browse files
Show More
@@ -1,542 +1,542 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
21 import inspect
22 import itertools
22 import itertools
23 import logging
23 import logging
24 import types
24 import types
25 import fnmatch
25 import fnmatch
26
26
27 import decorator
27 import decorator
28 import venusian
28 import venusian
29 from collections import OrderedDict
29 from collections import OrderedDict
30
30
31 from pyramid.exceptions import ConfigurationError
31 from pyramid.exceptions import ConfigurationError
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34 from pyramid.httpexceptions import HTTPNotFound
34 from pyramid.httpexceptions import HTTPNotFound
35
35
36 from rhodecode.api.exc import (
36 from rhodecode.api.exc import (
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 from rhodecode.apps._base import TemplateArgs
38 from rhodecode.apps._base import TemplateArgs
39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.lib.plugins.utils import get_plugin_settings
43 from rhodecode.lib.plugins.utils import get_plugin_settings
44 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.db import User, UserApiKeys
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 DEFAULT_URL = '/_admin/apiv2'
49 DEFAULT_URL = '/_admin/apiv2'
50
50
51
51
52 def find_methods(jsonrpc_methods, pattern):
52 def find_methods(jsonrpc_methods, pattern):
53 matches = OrderedDict()
53 matches = OrderedDict()
54 if not isinstance(pattern, (list, tuple)):
54 if not isinstance(pattern, (list, tuple)):
55 pattern = [pattern]
55 pattern = [pattern]
56
56
57 for single_pattern in pattern:
57 for single_pattern in pattern:
58 for method_name, method in jsonrpc_methods.items():
58 for method_name, method in jsonrpc_methods.items():
59 if fnmatch.fnmatch(method_name, single_pattern):
59 if fnmatch.fnmatch(method_name, single_pattern):
60 matches[method_name] = method
60 matches[method_name] = method
61 return matches
61 return matches
62
62
63
63
64 class ExtJsonRenderer(object):
64 class ExtJsonRenderer(object):
65 """
65 """
66 Custom renderer that mkaes use of our ext_json lib
66 Custom renderer that mkaes use of our ext_json lib
67
67
68 """
68 """
69
69
70 def __init__(self, serializer=json.dumps, **kw):
70 def __init__(self, serializer=json.dumps, **kw):
71 """ Any keyword arguments will be passed to the ``serializer``
71 """ Any keyword arguments will be passed to the ``serializer``
72 function."""
72 function."""
73 self.serializer = serializer
73 self.serializer = serializer
74 self.kw = kw
74 self.kw = kw
75
75
76 def __call__(self, info):
76 def __call__(self, info):
77 """ Returns a plain JSON-encoded string with content-type
77 """ Returns a plain JSON-encoded string with content-type
78 ``application/json``. The content-type may be overridden by
78 ``application/json``. The content-type may be overridden by
79 setting ``request.response.content_type``."""
79 setting ``request.response.content_type``."""
80
80
81 def _render(value, system):
81 def _render(value, system):
82 request = system.get('request')
82 request = system.get('request')
83 if request is not None:
83 if request is not None:
84 response = request.response
84 response = request.response
85 ct = response.content_type
85 ct = response.content_type
86 if ct == response.default_content_type:
86 if ct == response.default_content_type:
87 response.content_type = 'application/json'
87 response.content_type = 'application/json'
88
88
89 return self.serializer(value, **self.kw)
89 return self.serializer(value, **self.kw)
90
90
91 return _render
91 return _render
92
92
93
93
94 def jsonrpc_response(request, result):
94 def jsonrpc_response(request, result):
95 rpc_id = getattr(request, 'rpc_id', None)
95 rpc_id = getattr(request, 'rpc_id', None)
96 response = request.response
96 response = request.response
97
97
98 # store content_type before render is called
98 # store content_type before render is called
99 ct = response.content_type
99 ct = response.content_type
100
100
101 ret_value = ''
101 ret_value = ''
102 if rpc_id:
102 if rpc_id:
103 ret_value = {
103 ret_value = {
104 'id': rpc_id,
104 'id': rpc_id,
105 'result': result,
105 'result': result,
106 'error': None,
106 'error': None,
107 }
107 }
108
108
109 # fetch deprecation warnings, and store it inside results
109 # fetch deprecation warnings, and store it inside results
110 deprecation = getattr(request, 'rpc_deprecation', None)
110 deprecation = getattr(request, 'rpc_deprecation', None)
111 if deprecation:
111 if deprecation:
112 ret_value['DEPRECATION_WARNING'] = deprecation
112 ret_value['DEPRECATION_WARNING'] = deprecation
113
113
114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 response.body = safe_str(raw_body, response.charset)
115 response.body = safe_str(raw_body, response.charset)
116
116
117 if ct == response.default_content_type:
117 if ct == response.default_content_type:
118 response.content_type = 'application/json'
118 response.content_type = 'application/json'
119
119
120 return response
120 return response
121
121
122
122
123 def jsonrpc_error(request, message, retid=None, code=None):
123 def jsonrpc_error(request, message, retid=None, code=None):
124 """
124 """
125 Generate a Response object with a JSON-RPC error body
125 Generate a Response object with a JSON-RPC error body
126
126
127 :param code:
127 :param code:
128 :param retid:
128 :param retid:
129 :param message:
129 :param message:
130 """
130 """
131 err_dict = {'id': retid, 'result': None, 'error': message}
131 err_dict = {'id': retid, 'result': None, 'error': message}
132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 return Response(
133 return Response(
134 body=body,
134 body=body,
135 status=code,
135 status=code,
136 content_type='application/json'
136 content_type='application/json'
137 )
137 )
138
138
139
139
140 def exception_view(exc, request):
140 def exception_view(exc, request):
141 rpc_id = getattr(request, 'rpc_id', None)
141 rpc_id = getattr(request, 'rpc_id', None)
142
142
143 fault_message = 'undefined error'
143 fault_message = 'undefined error'
144 if isinstance(exc, JSONRPCError):
144 if isinstance(exc, JSONRPCError):
145 fault_message = exc.message
145 fault_message = safe_str(exc.message)
146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
147 elif isinstance(exc, JSONRPCValidationError):
147 elif isinstance(exc, JSONRPCValidationError):
148 colander_exc = exc.colander_exception
148 colander_exc = exc.colander_exception
149 # TODO(marcink): think maybe of nicer way to serialize errors ?
149 # TODO(marcink): think maybe of nicer way to serialize errors ?
150 fault_message = colander_exc.asdict()
150 fault_message = colander_exc.asdict()
151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
151 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
152 elif isinstance(exc, JSONRPCForbidden):
152 elif isinstance(exc, JSONRPCForbidden):
153 fault_message = 'Access was denied to this resource.'
153 fault_message = 'Access was denied to this resource.'
154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
155 elif isinstance(exc, HTTPNotFound):
155 elif isinstance(exc, HTTPNotFound):
156 method = request.rpc_method
156 method = request.rpc_method
157 log.debug('json-rpc method `%s` not found in list of '
157 log.debug('json-rpc method `%s` not found in list of '
158 'api calls: %s, rpc_id:%s',
158 'api calls: %s, rpc_id:%s',
159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
160
160
161 similar = 'none'
161 similar = 'none'
162 try:
162 try:
163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
164 similar_found = find_methods(
164 similar_found = find_methods(
165 request.registry.jsonrpc_methods, similar_paterns)
165 request.registry.jsonrpc_methods, similar_paterns)
166 similar = ', '.join(similar_found.keys()) or similar
166 similar = ', '.join(similar_found.keys()) or similar
167 except Exception:
167 except Exception:
168 # make the whole above block safe
168 # make the whole above block safe
169 pass
169 pass
170
170
171 fault_message = "No such method: {}. Similar methods: {}".format(
171 fault_message = "No such method: {}. Similar methods: {}".format(
172 method, similar)
172 method, similar)
173
173
174 return jsonrpc_error(request, fault_message, rpc_id)
174 return jsonrpc_error(request, fault_message, rpc_id)
175
175
176
176
177 def request_view(request):
177 def request_view(request):
178 """
178 """
179 Main request handling method. It handles all logic to call a specific
179 Main request handling method. It handles all logic to call a specific
180 exposed method
180 exposed method
181 """
181 """
182
182
183 # check if we can find this session using api_key, get_by_auth_token
183 # check if we can find this session using api_key, get_by_auth_token
184 # search not expired tokens only
184 # search not expired tokens only
185
185
186 try:
186 try:
187 api_user = User.get_by_auth_token(request.rpc_api_key)
187 api_user = User.get_by_auth_token(request.rpc_api_key)
188
188
189 if api_user is None:
189 if api_user is None:
190 return jsonrpc_error(
190 return jsonrpc_error(
191 request, retid=request.rpc_id, message='Invalid API KEY')
191 request, retid=request.rpc_id, message='Invalid API KEY')
192
192
193 if not api_user.active:
193 if not api_user.active:
194 return jsonrpc_error(
194 return jsonrpc_error(
195 request, retid=request.rpc_id,
195 request, retid=request.rpc_id,
196 message='Request from this user not allowed')
196 message='Request from this user not allowed')
197
197
198 # check if we are allowed to use this IP
198 # check if we are allowed to use this IP
199 auth_u = AuthUser(
199 auth_u = AuthUser(
200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
201 if not auth_u.ip_allowed:
201 if not auth_u.ip_allowed:
202 return jsonrpc_error(
202 return jsonrpc_error(
203 request, retid=request.rpc_id,
203 request, retid=request.rpc_id,
204 message='Request from IP:%s not allowed' % (
204 message='Request from IP:%s not allowed' % (
205 request.rpc_ip_addr,))
205 request.rpc_ip_addr,))
206 else:
206 else:
207 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
207 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
208
208
209 # register our auth-user
209 # register our auth-user
210 request.rpc_user = auth_u
210 request.rpc_user = auth_u
211 request.environ['rc_auth_user_id'] = auth_u.user_id
211 request.environ['rc_auth_user_id'] = auth_u.user_id
212
212
213 # now check if token is valid for API
213 # now check if token is valid for API
214 auth_token = request.rpc_api_key
214 auth_token = request.rpc_api_key
215 token_match = api_user.authenticate_by_token(
215 token_match = api_user.authenticate_by_token(
216 auth_token, roles=[UserApiKeys.ROLE_API])
216 auth_token, roles=[UserApiKeys.ROLE_API])
217 invalid_token = not token_match
217 invalid_token = not token_match
218
218
219 log.debug('Checking if API KEY is valid with proper role')
219 log.debug('Checking if API KEY is valid with proper role')
220 if invalid_token:
220 if invalid_token:
221 return jsonrpc_error(
221 return jsonrpc_error(
222 request, retid=request.rpc_id,
222 request, retid=request.rpc_id,
223 message='API KEY invalid or, has bad role for an API call')
223 message='API KEY invalid or, has bad role for an API call')
224
224
225 except Exception:
225 except Exception:
226 log.exception('Error on API AUTH')
226 log.exception('Error on API AUTH')
227 return jsonrpc_error(
227 return jsonrpc_error(
228 request, retid=request.rpc_id, message='Invalid API KEY')
228 request, retid=request.rpc_id, message='Invalid API KEY')
229
229
230 method = request.rpc_method
230 method = request.rpc_method
231 func = request.registry.jsonrpc_methods[method]
231 func = request.registry.jsonrpc_methods[method]
232
232
233 # now that we have a method, add request._req_params to
233 # now that we have a method, add request._req_params to
234 # self.kargs and dispatch control to WGIController
234 # self.kargs and dispatch control to WGIController
235 argspec = inspect.getargspec(func)
235 argspec = inspect.getargspec(func)
236 arglist = argspec[0]
236 arglist = argspec[0]
237 defaults = map(type, argspec[3] or [])
237 defaults = map(type, argspec[3] or [])
238 default_empty = types.NotImplementedType
238 default_empty = types.NotImplementedType
239
239
240 # kw arguments required by this method
240 # kw arguments required by this method
241 func_kwargs = dict(itertools.izip_longest(
241 func_kwargs = dict(itertools.izip_longest(
242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
243
243
244 # This attribute will need to be first param of a method that uses
244 # This attribute will need to be first param of a method that uses
245 # api_key, which is translated to instance of user at that name
245 # api_key, which is translated to instance of user at that name
246 user_var = 'apiuser'
246 user_var = 'apiuser'
247 request_var = 'request'
247 request_var = 'request'
248
248
249 for arg in [user_var, request_var]:
249 for arg in [user_var, request_var]:
250 if arg not in arglist:
250 if arg not in arglist:
251 return jsonrpc_error(
251 return jsonrpc_error(
252 request,
252 request,
253 retid=request.rpc_id,
253 retid=request.rpc_id,
254 message='This method [%s] does not support '
254 message='This method [%s] does not support '
255 'required parameter `%s`' % (func.__name__, arg))
255 'required parameter `%s`' % (func.__name__, arg))
256
256
257 # get our arglist and check if we provided them as args
257 # get our arglist and check if we provided them as args
258 for arg, default in func_kwargs.items():
258 for arg, default in func_kwargs.items():
259 if arg in [user_var, request_var]:
259 if arg in [user_var, request_var]:
260 # user_var and request_var are pre-hardcoded parameters and we
260 # user_var and request_var are pre-hardcoded parameters and we
261 # don't need to do any translation
261 # don't need to do any translation
262 continue
262 continue
263
263
264 # skip the required param check if it's default value is
264 # skip the required param check if it's default value is
265 # NotImplementedType (default_empty)
265 # NotImplementedType (default_empty)
266 if default == default_empty and arg not in request.rpc_params:
266 if default == default_empty and arg not in request.rpc_params:
267 return jsonrpc_error(
267 return jsonrpc_error(
268 request,
268 request,
269 retid=request.rpc_id,
269 retid=request.rpc_id,
270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
271 )
271 )
272
272
273 # sanitize extra passed arguments
273 # sanitize extra passed arguments
274 for k in request.rpc_params.keys()[:]:
274 for k in request.rpc_params.keys()[:]:
275 if k not in func_kwargs:
275 if k not in func_kwargs:
276 del request.rpc_params[k]
276 del request.rpc_params[k]
277
277
278 call_params = request.rpc_params
278 call_params = request.rpc_params
279 call_params.update({
279 call_params.update({
280 'request': request,
280 'request': request,
281 'apiuser': auth_u
281 'apiuser': auth_u
282 })
282 })
283
283
284 # register some common functions for usage
284 # register some common functions for usage
285 attach_context_attributes(
285 attach_context_attributes(
286 TemplateArgs(), request, request.rpc_user.user_id)
286 TemplateArgs(), request, request.rpc_user.user_id)
287
287
288 try:
288 try:
289 ret_value = func(**call_params)
289 ret_value = func(**call_params)
290 return jsonrpc_response(request, ret_value)
290 return jsonrpc_response(request, ret_value)
291 except JSONRPCBaseError:
291 except JSONRPCBaseError:
292 raise
292 raise
293 except Exception:
293 except Exception:
294 log.exception('Unhandled exception occurred on api call: %s', func)
294 log.exception('Unhandled exception occurred on api call: %s', func)
295 return jsonrpc_error(request, retid=request.rpc_id,
295 return jsonrpc_error(request, retid=request.rpc_id,
296 message='Internal server error')
296 message='Internal server error')
297
297
298
298
299 def setup_request(request):
299 def setup_request(request):
300 """
300 """
301 Parse a JSON-RPC request body. It's used inside the predicates method
301 Parse a JSON-RPC request body. It's used inside the predicates method
302 to validate and bootstrap requests for usage in rpc calls.
302 to validate and bootstrap requests for usage in rpc calls.
303
303
304 We need to raise JSONRPCError here if we want to return some errors back to
304 We need to raise JSONRPCError here if we want to return some errors back to
305 user.
305 user.
306 """
306 """
307
307
308 log.debug('Executing setup request: %r', request)
308 log.debug('Executing setup request: %r', request)
309 request.rpc_ip_addr = get_ip_addr(request.environ)
309 request.rpc_ip_addr = get_ip_addr(request.environ)
310 # TODO(marcink): deprecate GET at some point
310 # TODO(marcink): deprecate GET at some point
311 if request.method not in ['POST', 'GET']:
311 if request.method not in ['POST', 'GET']:
312 log.debug('unsupported request method "%s"', request.method)
312 log.debug('unsupported request method "%s"', request.method)
313 raise JSONRPCError(
313 raise JSONRPCError(
314 'unsupported request method "%s". Please use POST' % request.method)
314 'unsupported request method "%s". Please use POST' % request.method)
315
315
316 if 'CONTENT_LENGTH' not in request.environ:
316 if 'CONTENT_LENGTH' not in request.environ:
317 log.debug("No Content-Length")
317 log.debug("No Content-Length")
318 raise JSONRPCError("Empty body, No Content-Length in request")
318 raise JSONRPCError("Empty body, No Content-Length in request")
319
319
320 else:
320 else:
321 length = request.environ['CONTENT_LENGTH']
321 length = request.environ['CONTENT_LENGTH']
322 log.debug('Content-Length: %s', length)
322 log.debug('Content-Length: %s', length)
323
323
324 if length == 0:
324 if length == 0:
325 log.debug("Content-Length is 0")
325 log.debug("Content-Length is 0")
326 raise JSONRPCError("Content-Length is 0")
326 raise JSONRPCError("Content-Length is 0")
327
327
328 raw_body = request.body
328 raw_body = request.body
329 try:
329 try:
330 json_body = json.loads(raw_body)
330 json_body = json.loads(raw_body)
331 except ValueError as e:
331 except ValueError as e:
332 # catch JSON errors Here
332 # catch JSON errors Here
333 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
334
334
335 request.rpc_id = json_body.get('id')
335 request.rpc_id = json_body.get('id')
336 request.rpc_method = json_body.get('method')
336 request.rpc_method = json_body.get('method')
337
337
338 # check required base parameters
338 # check required base parameters
339 try:
339 try:
340 api_key = json_body.get('api_key')
340 api_key = json_body.get('api_key')
341 if not api_key:
341 if not api_key:
342 api_key = json_body.get('auth_token')
342 api_key = json_body.get('auth_token')
343
343
344 if not api_key:
344 if not api_key:
345 raise KeyError('api_key or auth_token')
345 raise KeyError('api_key or auth_token')
346
346
347 # TODO(marcink): support passing in token in request header
347 # TODO(marcink): support passing in token in request header
348
348
349 request.rpc_api_key = api_key
349 request.rpc_api_key = api_key
350 request.rpc_id = json_body['id']
350 request.rpc_id = json_body['id']
351 request.rpc_method = json_body['method']
351 request.rpc_method = json_body['method']
352 request.rpc_params = json_body['args'] \
352 request.rpc_params = json_body['args'] \
353 if isinstance(json_body['args'], dict) else {}
353 if isinstance(json_body['args'], dict) else {}
354
354
355 log.debug('method: %s, params: %s', request.rpc_method, request.rpc_params)
355 log.debug('method: %s, params: %s', request.rpc_method, request.rpc_params)
356 except KeyError as e:
356 except KeyError as e:
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
357 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358
358
359 log.debug('setup complete, now handling method:%s rpcid:%s',
359 log.debug('setup complete, now handling method:%s rpcid:%s',
360 request.rpc_method, request.rpc_id, )
360 request.rpc_method, request.rpc_id, )
361
361
362
362
363 class RoutePredicate(object):
363 class RoutePredicate(object):
364 def __init__(self, val, config):
364 def __init__(self, val, config):
365 self.val = val
365 self.val = val
366
366
367 def text(self):
367 def text(self):
368 return 'jsonrpc route = %s' % self.val
368 return 'jsonrpc route = %s' % self.val
369
369
370 phash = text
370 phash = text
371
371
372 def __call__(self, info, request):
372 def __call__(self, info, request):
373 if self.val:
373 if self.val:
374 # potentially setup and bootstrap our call
374 # potentially setup and bootstrap our call
375 setup_request(request)
375 setup_request(request)
376
376
377 # Always return True so that even if it isn't a valid RPC it
377 # Always return True so that even if it isn't a valid RPC it
378 # will fall through to the underlaying handlers like notfound_view
378 # will fall through to the underlaying handlers like notfound_view
379 return True
379 return True
380
380
381
381
382 class NotFoundPredicate(object):
382 class NotFoundPredicate(object):
383 def __init__(self, val, config):
383 def __init__(self, val, config):
384 self.val = val
384 self.val = val
385 self.methods = config.registry.jsonrpc_methods
385 self.methods = config.registry.jsonrpc_methods
386
386
387 def text(self):
387 def text(self):
388 return 'jsonrpc method not found = {}.'.format(self.val)
388 return 'jsonrpc method not found = {}.'.format(self.val)
389
389
390 phash = text
390 phash = text
391
391
392 def __call__(self, info, request):
392 def __call__(self, info, request):
393 return hasattr(request, 'rpc_method')
393 return hasattr(request, 'rpc_method')
394
394
395
395
396 class MethodPredicate(object):
396 class MethodPredicate(object):
397 def __init__(self, val, config):
397 def __init__(self, val, config):
398 self.method = val
398 self.method = val
399
399
400 def text(self):
400 def text(self):
401 return 'jsonrpc method = %s' % self.method
401 return 'jsonrpc method = %s' % self.method
402
402
403 phash = text
403 phash = text
404
404
405 def __call__(self, context, request):
405 def __call__(self, context, request):
406 # we need to explicitly return False here, so pyramid doesn't try to
406 # we need to explicitly return False here, so pyramid doesn't try to
407 # execute our view directly. We need our main handler to execute things
407 # execute our view directly. We need our main handler to execute things
408 return getattr(request, 'rpc_method') == self.method
408 return getattr(request, 'rpc_method') == self.method
409
409
410
410
411 def add_jsonrpc_method(config, view, **kwargs):
411 def add_jsonrpc_method(config, view, **kwargs):
412 # pop the method name
412 # pop the method name
413 method = kwargs.pop('method', None)
413 method = kwargs.pop('method', None)
414
414
415 if method is None:
415 if method is None:
416 raise ConfigurationError(
416 raise ConfigurationError(
417 'Cannot register a JSON-RPC method without specifying the '
417 'Cannot register a JSON-RPC method without specifying the '
418 '"method"')
418 '"method"')
419
419
420 # we define custom predicate, to enable to detect conflicting methods,
420 # we define custom predicate, to enable to detect conflicting methods,
421 # those predicates are kind of "translation" from the decorator variables
421 # those predicates are kind of "translation" from the decorator variables
422 # to internal predicates names
422 # to internal predicates names
423
423
424 kwargs['jsonrpc_method'] = method
424 kwargs['jsonrpc_method'] = method
425
425
426 # register our view into global view store for validation
426 # register our view into global view store for validation
427 config.registry.jsonrpc_methods[method] = view
427 config.registry.jsonrpc_methods[method] = view
428
428
429 # we're using our main request_view handler, here, so each method
429 # we're using our main request_view handler, here, so each method
430 # has a unified handler for itself
430 # has a unified handler for itself
431 config.add_view(request_view, route_name='apiv2', **kwargs)
431 config.add_view(request_view, route_name='apiv2', **kwargs)
432
432
433
433
434 class jsonrpc_method(object):
434 class jsonrpc_method(object):
435 """
435 """
436 decorator that works similar to @add_view_config decorator,
436 decorator that works similar to @add_view_config decorator,
437 but tailored for our JSON RPC
437 but tailored for our JSON RPC
438 """
438 """
439
439
440 venusian = venusian # for testing injection
440 venusian = venusian # for testing injection
441
441
442 def __init__(self, method=None, **kwargs):
442 def __init__(self, method=None, **kwargs):
443 self.method = method
443 self.method = method
444 self.kwargs = kwargs
444 self.kwargs = kwargs
445
445
446 def __call__(self, wrapped):
446 def __call__(self, wrapped):
447 kwargs = self.kwargs.copy()
447 kwargs = self.kwargs.copy()
448 kwargs['method'] = self.method or wrapped.__name__
448 kwargs['method'] = self.method or wrapped.__name__
449 depth = kwargs.pop('_depth', 0)
449 depth = kwargs.pop('_depth', 0)
450
450
451 def callback(context, name, ob):
451 def callback(context, name, ob):
452 config = context.config.with_package(info.module)
452 config = context.config.with_package(info.module)
453 config.add_jsonrpc_method(view=ob, **kwargs)
453 config.add_jsonrpc_method(view=ob, **kwargs)
454
454
455 info = venusian.attach(wrapped, callback, category='pyramid',
455 info = venusian.attach(wrapped, callback, category='pyramid',
456 depth=depth + 1)
456 depth=depth + 1)
457 if info.scope == 'class':
457 if info.scope == 'class':
458 # ensure that attr is set if decorating a class method
458 # ensure that attr is set if decorating a class method
459 kwargs.setdefault('attr', wrapped.__name__)
459 kwargs.setdefault('attr', wrapped.__name__)
460
460
461 kwargs['_info'] = info.codeinfo # fbo action_method
461 kwargs['_info'] = info.codeinfo # fbo action_method
462 return wrapped
462 return wrapped
463
463
464
464
465 class jsonrpc_deprecated_method(object):
465 class jsonrpc_deprecated_method(object):
466 """
466 """
467 Marks method as deprecated, adds log.warning, and inject special key to
467 Marks method as deprecated, adds log.warning, and inject special key to
468 the request variable to mark method as deprecated.
468 the request variable to mark method as deprecated.
469 Also injects special docstring that extract_docs will catch to mark
469 Also injects special docstring that extract_docs will catch to mark
470 method as deprecated.
470 method as deprecated.
471
471
472 :param use_method: specify which method should be used instead of
472 :param use_method: specify which method should be used instead of
473 the decorated one
473 the decorated one
474
474
475 Use like::
475 Use like::
476
476
477 @jsonrpc_method()
477 @jsonrpc_method()
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
478 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 def old_func(request, apiuser, arg1, arg2):
479 def old_func(request, apiuser, arg1, arg2):
480 ...
480 ...
481 """
481 """
482
482
483 def __init__(self, use_method, deprecated_at_version):
483 def __init__(self, use_method, deprecated_at_version):
484 self.use_method = use_method
484 self.use_method = use_method
485 self.deprecated_at_version = deprecated_at_version
485 self.deprecated_at_version = deprecated_at_version
486 self.deprecated_msg = ''
486 self.deprecated_msg = ''
487
487
488 def __call__(self, func):
488 def __call__(self, func):
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
489 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 method=self.use_method)
490 method=self.use_method)
491
491
492 docstring = """\n
492 docstring = """\n
493 .. deprecated:: {version}
493 .. deprecated:: {version}
494
494
495 {deprecation_message}
495 {deprecation_message}
496
496
497 {original_docstring}
497 {original_docstring}
498 """
498 """
499 func.__doc__ = docstring.format(
499 func.__doc__ = docstring.format(
500 version=self.deprecated_at_version,
500 version=self.deprecated_at_version,
501 deprecation_message=self.deprecated_msg,
501 deprecation_message=self.deprecated_msg,
502 original_docstring=func.__doc__)
502 original_docstring=func.__doc__)
503 return decorator.decorator(self.__wrapper, func)
503 return decorator.decorator(self.__wrapper, func)
504
504
505 def __wrapper(self, func, *fargs, **fkwargs):
505 def __wrapper(self, func, *fargs, **fkwargs):
506 log.warning('DEPRECATED API CALL on function %s, please '
506 log.warning('DEPRECATED API CALL on function %s, please '
507 'use `%s` instead', func, self.use_method)
507 'use `%s` instead', func, self.use_method)
508 # alter function docstring to mark as deprecated, this is picked up
508 # alter function docstring to mark as deprecated, this is picked up
509 # via fabric file that generates API DOC.
509 # via fabric file that generates API DOC.
510 result = func(*fargs, **fkwargs)
510 result = func(*fargs, **fkwargs)
511
511
512 request = fargs[0]
512 request = fargs[0]
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
513 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 return result
514 return result
515
515
516
516
517 def includeme(config):
517 def includeme(config):
518 plugin_module = 'rhodecode.api'
518 plugin_module = 'rhodecode.api'
519 plugin_settings = get_plugin_settings(
519 plugin_settings = get_plugin_settings(
520 plugin_module, config.registry.settings)
520 plugin_module, config.registry.settings)
521
521
522 if not hasattr(config.registry, 'jsonrpc_methods'):
522 if not hasattr(config.registry, 'jsonrpc_methods'):
523 config.registry.jsonrpc_methods = OrderedDict()
523 config.registry.jsonrpc_methods = OrderedDict()
524
524
525 # match filter by given method only
525 # match filter by given method only
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
526 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527
527
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
528 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 serializer=json.dumps, indent=4))
529 serializer=json.dumps, indent=4))
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
530 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531
531
532 config.add_route_predicate(
532 config.add_route_predicate(
533 'jsonrpc_call', RoutePredicate)
533 'jsonrpc_call', RoutePredicate)
534
534
535 config.add_route(
535 config.add_route(
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
536 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537
537
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
538 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 # register some exception handling view
539 # register some exception handling view
540 config.add_view(exception_view, context=JSONRPCBaseError)
540 config.add_view(exception_view, context=JSONRPCBaseError)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
541 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
542 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
General Comments 0
You need to be logged in to leave comments. Login now