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