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