move example to different port
[multitaskhttpd.git] / SimpleJSONRPCServer.py
1
2 # Originally taken from:
3 # http://code.activestate.com/recipes/552751/
4 # thanks to david decotigny
5
6 # Heavily based on the XML-RPC implementation in python.
7 # Based on the json-rpc specs: http://json-rpc.org/wiki/specification
8 # The main deviation is on the error treatment. The official spec
9 # would set the 'error' attribute to a string. This implementation
10 # sets it to a dictionary with keys: message/traceback/type
11
12 import cjson
13 import SocketServer
14 import SimpleAppHTTPServer
15 #import BaseHTTPServer
16 import sys
17 import traceback
18 try:
19 import fcntl
20 except ImportError:
21 fcntl = None
22
23
24 ###
25 ### Server code
26 ###
27 import SimpleXMLRPCServer
28
29
30 class SimpleJSONRPCRequestHandler(SimpleAppHTTPServer.SimpleAppHTTPRequestHandler):
31 """Simple JSONRPC request handler class and HTTP GET Server
32
33 Handles all HTTP POST requests and attempts to decode them as
34 JSONRPC requests.
35
36 Handles all HTTP GET requests and serves the content from the
37 current directory.
38
39 """
40
41 # Class attribute listing the accessible path components;
42 # paths not on this list will result in a 404 error.
43 rpc_paths = ('/', '/JSON')
44
45 def __init__(self):
46
47 self.funcs = {}
48
49 def is_rpc_path_valid(self):
50 return True
51 if self.rpc_paths:
52 return self.path in self.rpc_paths
53 else:
54 # If .rpc_paths is empty, just assume all paths are legal
55 return True
56
57 def onPOST(self, client, *args):
58 """Handles the HTTP POST request.
59
60 Attempts to interpret all HTTP POST requests as XML-RPC calls,
61 which are forwarded to the server's _dispatch method for handling.
62 """
63 print "onPost", client, args
64 self.client = client
65 self.hr = args[0]
66
67 # Check that the path is legal
68 if not self.is_rpc_path_valid():
69 self.report_404()
70 return
71
72 print "about to read data"
73 try:
74 # Get arguments by reading body of request.
75 # We read this in chunks to avoid straining
76 # socket.read(); around the 10 or 15Mb mark, some platforms
77 # begin to have problems (bug #792570).
78 max_chunk_size = 10*1024*1024
79 size_remaining = int(self.hr.headers["content-length"])
80 L = []
81 print "size_remaining", size_remaining
82 while size_remaining:
83 chunk_size = min(size_remaining, max_chunk_size)
84 data = self.hr.rfile.read(chunk_size)
85 L.append(data)
86 size_remaining -= len(L[-1])
87 data = ''.join(L)
88
89 # In previous versions of SimpleXMLRPCServer, _dispatch
90 # could be overridden in this class, instead of in
91 # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
92 # check to see if a subclass implements _dispatch and dispatch
93 # using that method if present.
94 response = self._marshaled_dispatch(
95 data, getattr(self, '_dispatch', None)
96 )
97 except: # This should only happen if the module is buggy
98 # internal error, report as HTTP server error
99 self.hr.send_response(500)
100 self.hr.add_cookies()
101 self.hr.end_headers()
102 else:
103 # got a valid JSONRPC response
104 self.hr.send_response(200)
105 self.hr.send_header("Content-type", "text/x-json")
106 self.hr.send_header("Content-length", str(len(response)))
107 self.hr.add_cookies()
108 self.hr.end_headers()
109 self.hr.wfile.write(response)
110
111 # shut down the connection
112 #self.wfile.flush()
113 #self.connection.shutdown(1)
114
115 def report_404 (self):
116 # Report a 404 error
117 self.hr.send_response(404)
118 response = 'No such page'
119 self.hr.send_header("Content-type", "text/plain")
120 self.hr.send_header("Content-length", str(len(response)))
121 self.hr.add_cookies()
122 self.hr.end_headers()
123 self.hr.wfile.write(response)
124 # shut down the connection
125 self.hr.wfile.flush()
126 self.hr.connection.shutdown(1)
127
128 def register_function(self, function, name = None):
129 """Registers a function to respond to XML-RPC requests.
130
131 The optional name argument can be used to set a Unicode name
132 for the function.
133 """
134
135 if name is None:
136 name = function.__name__
137 self.funcs[name] = function
138
139
140 def _marshaled_dispatch(self, data, dispatch_method = None):
141 id = None
142 try:
143 req = cjson.decode(data)
144 method = req['method']
145 params = req['params'] or ()
146 id = req['id']
147
148 if dispatch_method is not None:
149 result = dispatch_method(method, params)
150 else:
151 result = self._dispatch(method, params)
152 response = dict(id=id, result=result, error=None)
153 except:
154 extpe, exv, extrc = sys.exc_info()
155 err = dict(type=str(extpe),
156 message=str(exv),
157 traceback=''.join(traceback.format_tb(extrc)))
158 response = dict(id=id, result=None, error=err)
159 try:
160 return cjson.encode(response)
161 except:
162 extpe, exv, extrc = sys.exc_info()
163 err = dict(type=str(extpe),
164 message=str(exv),
165 traceback=''.join(traceback.format_tb(extrc)))
166 response = dict(id=id, result=None, error=err)
167 return cjson.encode(response)
168
169 def _dispatch(self, method, params):
170 """Dispatches the XML-RPC method.
171
172 XML-RPC calls are forwarded to a registered function that
173 matches the called XML-RPC method name. If no such function
174 exists then the call is forwarded to the registered instance,
175 if available.
176
177 If the registered instance has a _dispatch method then that
178 method will be called with the name of the XML-RPC method and
179 its parameters as a tuple
180 e.g. instance._dispatch('add',(2,3))
181
182 If the registered instance does not have a _dispatch method
183 then the instance will be searched to find a matching method
184 and, if found, will be called.
185
186 Methods beginning with an '_' are considered private and will
187 not be called.
188 """
189
190 func = self.funcs.get(method, None)
191
192 if func is not None:
193 print "params", params
194 return func(*params)
195 else:
196 raise Exception('method "%s" is not supported' % method)
197
198
199 #def log_request(self, code='-', size='-'):
200 # """Selectively log an accepted request."""
201
202 # if self.server.logRequests:
203 # BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
204
205
206
207 class SimpleJSONRPCServer:
208 """Simple JSON-RPC server.
209
210 Simple JSON-RPC server that allows functions and a single instance
211 to be installed to handle requests. The default implementation
212 attempts to dispatch JSON-RPC calls to the functions or instance
213 installed in the server. Override the _dispatch method inhereted
214 from SimpleJSONRPCDispatcher to change this behavior.
215 """
216
217 allow_reuse_address = True
218
219 def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
220 logRequests=True):
221 self.logRequests = logRequests
222
223 # [Bug #1222790] If possible, set close-on-exec flag; if a
224 # method spawns a subprocess, the subprocess shouldn't have
225 # the listening socket open.
226 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
227 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
228 flags |= fcntl.FD_CLOEXEC
229 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
230
231
232 ###
233 ### Client code
234 ###
235 import xmlrpclib
236
237 class ResponseError(xmlrpclib.ResponseError):
238 pass
239 class Fault(xmlrpclib.ResponseError):
240 pass
241
242 def _get_response(file, sock):
243 data = ""
244 while 1:
245 if sock:
246 response = sock.recv(1024)
247 else:
248 response = file.read(1024)
249 if not response:
250 break
251 data += response
252
253 file.close()
254
255 return data
256
257 class Transport(xmlrpclib.Transport):
258 def _parse_response(self, file, sock):
259 return _get_response(file, sock)
260
261 class SafeTransport(xmlrpclib.SafeTransport):
262 def _parse_response(self, file, sock):
263 return _get_response(file, sock)
264
265 class ServerProxy:
266 def __init__(self, uri, id=None, transport=None, use_datetime=0):
267 # establish a "logical" server connection
268
269 # get the url
270 import urllib
271 type, uri = urllib.splittype(uri)
272 if type not in ("http", "https"):
273 raise IOError, "unsupported JSON-RPC protocol"
274 self.__host, self.__handler = urllib.splithost(uri)
275 if not self.__handler:
276 self.__handler = "/JSON"
277
278 if transport is None:
279 if type == "https":
280 transport = SafeTransport(use_datetime=use_datetime)
281 else:
282 transport = Transport(use_datetime=use_datetime)
283
284 self.__transport = transport
285 self.__id = id
286
287 def __request(self, methodname, params):
288 # call a method on the remote server
289
290 request = cjson.encode(dict(id=self.__id, method=methodname,
291 params=params))
292
293 data = self.__transport.request(
294 self.__host,
295 self.__handler,
296 request,
297 verbose=False
298 )
299
300 response = cjson.decode(data)
301
302 if response["id"] != self.__id:
303 raise ResponseError("Invalid request id (is: %s, expected: %s)" \
304 % (response["id"], self.__id))
305 if response["error"] is not None:
306 raise Fault("JSON Error", response["error"])
307 return response["result"]
308
309 def __repr__(self):
310 return (
311 "<ServerProxy for %s%s>" %
312 (self.__host, self.__handler)
313 )
314
315 __str__ = __repr__
316
317 def __getattr__(self, name):
318 # magic method dispatcher
319 return xmlrpclib._Method(self.__request, name)
320
321
322 def jsonremote(service):
323 """Make JSONRPCService a decorator so that you can write :
324
325 chatservice = SimpleJSONRPCServer()
326
327 @jsonremote(chatservice, 'login')
328 def login(request, user_name):
329 (...)
330 """
331 def remotify(func):
332 if isinstance(service, SimpleJSONRPCServer):
333 service.register_function(func, func.__name__)
334 else:
335 emsg = 'Service "%s" not found' % str(service.__name__)
336 raise NotImplementedError, emsg
337 return func
338 return remotify
339
340
341 if __name__ == '__main__':
342 if not len(sys.argv) > 1:
343 import socket
344 print 'Running JSON-RPC server on port 8000'
345 server = SimpleJSONRPCServer(("localhost", 8000))
346 server.register_function(pow)
347 server.register_function(lambda x,y: x+y, 'add')
348 server.register_function(lambda x: x, 'echo')
349 server.serve_forever()
350 else:
351 remote = ServerProxy(sys.argv[1])
352 print 'Using connection', remote
353
354 print repr(remote.add(1, 2))
355 aaa = remote.add
356 print repr(remote.pow(2, 4))
357 print aaa(5, 6)
358
359 try:
360 # Invalid parameters
361 aaa(5, "toto")
362 print "Successful execution of invalid code"
363 except Fault:
364 pass
365
366 try:
367 # Invalid parameters
368 aaa(5, 6, 7)
369 print "Successful execution of invalid code"
370 except Fault:
371 pass
372
373 try:
374 # Invalid method name
375 print repr(remote.powx(2, 4))
376 print "Successful execution of invalid code"
377 except Fault:
378 pass