1
# -*- coding: utf-8 -*-
5
(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
9
from itertools import izip
11
from types import GeneratorType
13
from trac.core import *
14
from trac.perm import PermissionError
15
from trac.resource import ResourceNotFound
16
from trac.util.datefmt import utc
17
from trac.util.text import to_unicode
18
from trac.web.api import RequestDone
20
from tracrpc.api import IRPCProtocol, XMLRPCSystem, Binary, \
21
RPCError, MethodNotFound, ProtocolException
22
from tracrpc.util import exception_to_unicode, empty, prepare_docs
24
__all__ = ['JsonRpcProtocol']
28
if not (hasattr(json, 'JSONEncoder') \
29
and hasattr(json, 'JSONDecoder')):
30
raise AttributeError("Incorrect JSON library found.")
31
except (ImportError, AttributeError):
33
import simplejson as json
39
class TracRpcJSONEncoder(json.JSONEncoder):
40
""" Extending the JSON encoder to support some additional types:
41
1. datetime.datetime => {'__jsonclass__': ["datetime", "<rfc3339str>"]}
42
2. tracrpc.api.Binary => {'__jsonclass__': ["binary", "<base64str>"]}
45
def default(self, obj):
46
if isinstance(obj, datetime.datetime):
47
# http://www.ietf.org/rfc/rfc3339.txt
48
return {'__jsonclass__': ["datetime",
49
obj.strftime('%Y-%m-%dT%H:%M:%S')]}
50
elif isinstance(obj, Binary):
51
return {'__jsonclass__': ["binary",
52
obj.data.encode("base64")]}
56
return json.JSONEncoder(self, obj)
58
class TracRpcJSONDecoder(json.JSONDecoder):
59
""" Extending the JSON decoder to support some additional types:
60
1. {'__jsonclass__': ["datetime", "<rfc3339str>"]} => datetime.datetime
61
2. {'__jsonclass__': ["binary", "<base64str>"]} => tracrpc.api.Binary """
64
'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,}))?')
66
def _normalize(self, obj):
67
""" Helper to traverse JSON decoded object for custom types. """
68
if isinstance(obj, tuple):
69
return tuple(self._normalize(item) for item in obj)
70
elif isinstance(obj, list):
71
return [self._normalize(item) for item in obj]
72
elif isinstance(obj, dict):
73
if obj.keys() == ['__jsonclass__']:
74
kind, val = obj['__jsonclass__']
75
if kind == 'datetime':
76
dt = self.dt.match(val)
79
"Invalid datetime string (%s)" % val)
80
dt = tuple([int(i) for i in dt.groups() if i])
81
kw_args = {'tzinfo': utc}
82
return datetime.datetime(*dt, **kw_args)
83
elif kind == 'binary':
85
bin = val.decode("base64")
88
raise Exception("Invalid base64 string")
90
raise Exception("Unknown __jsonclass__: %s" % kind)
92
return dict(self._normalize(obj.items()))
93
elif isinstance(obj, basestring):
94
return to_unicode(obj)
98
def decode(self, obj, *args, **kwargs):
99
obj = json.JSONDecoder.decode(self, obj, *args, **kwargs)
100
return self._normalize(obj)
102
class JsonProtocolException(ProtocolException):
103
"""Impossible to handle JSON-RPC request."""
104
def __init__(self, details, code=-32603, title=None, show_traceback=False):
105
ProtocolException.__init__(self, details, title, show_traceback)
108
class JsonRpcProtocol(Component):
110
Example `POST` request using `curl` with `Content-Type` header
114
user: ~ > cat body.json
115
{"params": ["WikiStart"], "method": "wiki.getPage", "id": 123}
116
user: ~ > curl -H "Content-Type: application/json" --data @body.json ${req.abs_href.rpc()}
117
{"id": 123, "error": null, "result": "= Welcome to....
120
Implementation details:
122
* JSON-RPC has no formalized type system, so a class-hint system is used
123
for input and output of non-standard types:
124
* `{"__jsonclass__": ["datetime", "YYYY-MM-DDTHH:MM:SS"]} => DateTime (UTC)`
125
* `{"__jsonclass__": ["binary", "<base64-encoded>"]} => Binary`
126
* `"id"` is optional, and any marker value received with a
127
request is returned with the response.
130
implements(IRPCProtocol)
132
# IRPCProtocol methods
135
return ('JSON-RPC', prepare_docs(self.__doc__))
138
yield('rpc', 'application/json')
139
# Legacy path - provided for backwards compatibility:
140
yield ('jsonrpc', 'application/json')
142
def parse_rpc_request(self, req, content_type):
143
""" Parse JSON-RPC requests"""
145
self.log.debug("RPC(json) call ignored (not available).")
146
raise JsonProtocolException("Error: JSON-RPC not available.\n")
148
data = json.load(req, cls=TracRpcJSONDecoder)
149
self.log.info("RPC(json) JSON-RPC request ID : %s.", data.get('id'))
150
if data.get('method') == 'system.multicall':
151
# Prepare for multicall
152
self.log.debug("RPC(json) Multicall request %s", data)
153
params = data.get('params', [])
154
for signature in params :
155
signature['methodName'] = signature.get('method', '')
156
data['params'] = [params]
159
# Abort with exception - no data can be read
160
self.log.error("RPC(json) decode error %s",
161
exception_to_unicode(e, traceback=True))
162
raise JsonProtocolException(e, -32700)
164
def send_rpc_result(self, req, result):
165
"""Send JSON-RPC response back to the caller."""
167
r_id = rpcreq.get('id')
169
if rpcreq.get('method') == 'system.multicall':
171
args = (rpcreq.get('params') or [[]])[0]
172
mcresults = [self._json_result(
173
isinstance(value, Exception) and \
175
sig.get('id') or r_id) \
176
for sig, value in izip(args, result)]
178
response = self._json_result(mcresults, r_id)
180
response = self._json_result(result, r_id)
182
self.log.debug("RPC(json) result: %s" % repr(response))
183
response = json.dumps(response, cls=TracRpcJSONEncoder)
185
response = json.dumps(self._json_error(e, r_id=r_id),
186
cls=TracRpcJSONEncoder)
188
self.log.error("RPC(json) error %s" % exception_to_unicode(e,
190
response = json.dumps(self._json_error(e, r_id=r_id),
191
cls=TracRpcJSONEncoder)
192
self._send_response(req, response + '\n', rpcreq['mimetype'])
194
def send_rpc_error(self, req, e):
195
"""Send a JSON-RPC fault message back to the caller. """
197
r_id = rpcreq.get('id')
198
response = json.dumps(self._json_error(e, r_id=r_id), \
199
cls=TracRpcJSONEncoder)
200
self._send_response(req, response + '\n', rpcreq['mimetype'])
204
def _send_response(self, req, response, content_type='application/json'):
205
self.log.debug("RPC(json) encoded response: %s" % response)
206
response = to_unicode(response).encode("utf-8")
207
req.send_response(200)
208
req.send_header('Content-Type', content_type)
209
req.send_header('Content-Length', len(response))
214
def _json_result(self, result, r_id=None):
215
""" Create JSON-RPC response dictionary. """
216
if not isinstance(result, Exception):
217
return {'result': result, 'error': None, 'id': r_id}
219
return self._json_error(result, r_id=r_id)
221
def _json_error(self, e, c=None, r_id=None):
222
""" Makes a response dictionary that is an error. """
223
if isinstance(e, MethodNotFound):
225
elif isinstance(e, PermissionError):
227
elif isinstance(e, ResourceNotFound):
230
c = c or hasattr(e, 'code') and e.code or -32603
231
return {'result': None, 'id': r_id, 'error': {
232
'name': hasattr(e, 'name') and e.name or 'JSONRPCError',
234
'message': to_unicode(e)}}