add demo README
[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.end_headers()
101 else:
102 # got a valid JSONRPC response
103 self.hr.send_response(200)
104 self.hr.send_header("Content-type", "text/x-json")
105 self.hr.send_header("Content-length", str(len(response)))
106 self.hr.end_headers()
107 self.hr.wfile.write(response)
108
109 # shut down the connection
110 #self.wfile.flush()
111 #self.connection.shutdown(1)
112
113 def report_404 (self):
114 # Report a 404 error
115 self.hr.send_response(404)
116 response = 'No such page'
117 self.hr.send_header("Content-type", "text/plain")
118 self.hr.send_header("Content-length", str(len(response)))
119 self.hr.end_headers()
120 self.hr.wfile.write(response)
121 # shut down the connection
122 self.hr.wfile.flush()
123 self.hr.connection.shutdown(1)
124
125 def register_function(self, function, name = None):
126 """Registers a function to respond to XML-RPC requests.
127
128 The optional name argument can be used to set a Unicode name
129 for the function.
130 """
131
132 if name is None:
133 name = function.__name__
134 self.funcs[name] = function
135
136
137 def _marshaled_dispatch(self, data, dispatch_method = None):
138 id = None
139 try:
140 req = cjson.decode(data)
141 method = req['method']
142 params = req['params']
143 id = req['id']
144
145 if dispatch_method is not None:
146 result = dispatch_method(method, params)
147 else:
148 result = self._dispatch(method, params)
149 response = dict(id=id, result=result, error=None)
150 except:
151 extpe, exv, extrc = sys.exc_info()
152 err = dict(type=str(extpe),
153 message=str(exv),
154 traceback=''.join(traceback.format_tb(extrc)))
155 response = dict(id=id, result=None, error=err)
156 try:
157 return cjson.encode(response)
158 except:
159 extpe, exv, extrc = sys.exc_info()
160 err = dict(type=str(extpe),
161 message=str(exv),
162 traceback=''.join(traceback.format_tb(extrc)))
163 response = dict(id=id, result=None, error=err)
164 return cjson.encode(response)
165
166 def _dispatch(self, method, params):
167 """Dispatches the XML-RPC method.
168
169 XML-RPC calls are forwarded to a registered function that
170 matches the called XML-RPC method name. If no such function
171 exists then the call is forwarded to the registered instance,
172 if available.
173
174 If the registered instance has a _dispatch method then that
175 method will be called with the name of the XML-RPC method and
176 its parameters as a tuple
177 e.g. instance._dispatch('add',(2,3))
178
179 If the registered instance does not have a _dispatch method
180 then the instance will be searched to find a matching method
181 and, if found, will be called.
182
183 Methods beginning with an '_' are considered private and will
184 not be called.
185 """
186
187 func = self.funcs.get(method, None)
188
189 if func is not None:
190 print "params", params
191 return func(*params)
192 else:
193 raise Exception('method "%s" is not supported' % method)
194
195
196 #def log_request(self, code='-', size='-'):
197 # """Selectively log an accepted request."""
198
199 # if self.server.logRequests:
200 # BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
201
202
203
204 class SimpleJSONRPCServer:
205 """Simple JSON-RPC server.
206
207 Simple JSON-RPC server that allows functions and a single instance
208 to be installed to handle requests. The default implementation
209 attempts to dispatch JSON-RPC calls to the functions or instance
210 installed in the server. Override the _dispatch method inhereted
211 from SimpleJSONRPCDispatcher to change this behavior.
212 """
213
214 allow_reuse_address = True
215
216 def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
217 logRequests=True):
218 self.logRequests = logRequests
219
220 # [Bug #1222790] If possible, set close-on-exec flag; if a
221 # method spawns a subprocess, the subprocess shouldn't have
222 # the listening socket open.
223 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
224 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
225 flags |= fcntl.FD_CLOEXEC
226 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
227
228
229 ###
230 ### Client code
231 ###
232 import xmlrpclib
233
234 class ResponseError(xmlrpclib.ResponseError):
235 pass
236 class Fault(xmlrpclib.ResponseError):
237 pass
238
239 def _get_response(file, sock):
240 data = ""
241 while 1:
242 if sock:
243 response = sock.recv(1024)
244 else:
245 response = file.read(1024)
246 if not response:
247 break
248 data += response
249
250 file.close()
251
252 return data
253
254 class Transport(xmlrpclib.Transport):
255 def _parse_response(self, file, sock):
256 return _get_response(file, sock)
257
258 class SafeTransport(xmlrpclib.SafeTransport):
259 def _parse_response(self, file, sock):
260 return _get_response(file, sock)
261
262 class ServerProxy:
263 def __init__(self, uri, id=None, transport=None, use_datetime=0):
264 # establish a "logical" server connection
265
266 # get the url
267 import urllib
268 type, uri = urllib.splittype(uri)
269 if type not in ("http", "https"):
270 raise IOError, "unsupported JSON-RPC protocol"
271 self.__host, self.__handler = urllib.splithost(uri)
272 if not self.__handler:
273 self.__handler = "/JSON"
274
275 if transport is None:
276 if type == "https":
277 transport = SafeTransport(use_datetime=use_datetime)
278 else:
279 transport = Transport(use_datetime=use_datetime)
280
281 self.__transport = transport
282 self.__id = id
283
284 def __request(self, methodname, params):
285 # call a method on the remote server
286
287 request = cjson.encode(dict(id=self.__id, method=methodname,
288 params=params))
289
290 data = self.__transport.request(
291 self.__host,
292 self.__handler,
293 request,
294 verbose=False
295 )
296
297 response = cjson.decode(data)
298
299 if response["id"] != self.__id:
300 raise ResponseError("Invalid request id (is: %s, expected: %s)" \
301 % (response["id"], self.__id))
302 if response["error"] is not None:
303 raise Fault("JSON Error", response["error"])
304 return response["result"]
305
306 def __repr__(self):
307 return (
308 "<ServerProxy for %s%s>" %
309 (self.__host, self.__handler)
310 )
311
312 __str__ = __repr__
313
314 def __getattr__(self, name):
315 # magic method dispatcher
316 return xmlrpclib._Method(self.__request, name)
317
318
319 def jsonremote(service):
320 """Make JSONRPCService a decorator so that you can write :
321
322 chatservice = SimpleJSONRPCServer()
323
324 @jsonremote(chatservice, 'login')
325 def login(request, user_name):
326 (...)
327 """
328 def remotify(func):
329 if isinstance(service, SimpleJSONRPCServer):
330 service.register_function(func, func.__name__)
331 else:
332 emsg = 'Service "%s" not found' % str(service.__name__)
333 raise NotImplementedError, emsg
334 return func
335 return remotify
336
337
338 if __name__ == '__main__':
339 if not len(sys.argv) > 1:
340 import socket
341 print 'Running JSON-RPC server on port 8000'
342 server = SimpleJSONRPCServer(("localhost", 8000))
343 server.register_function(pow)
344 server.register_function(lambda x,y: x+y, 'add')
345 server.register_function(lambda x: x, 'echo')
346 server.serve_forever()
347 else:
348 remote = ServerProxy(sys.argv[1])
349 print 'Using connection', remote
350
351 print repr(remote.add(1, 2))
352 aaa = remote.add
353 print repr(remote.pow(2, 4))
354 print aaa(5, 6)
355
356 try:
357 # Invalid parameters
358 aaa(5, "toto")
359 print "Successful execution of invalid code"
360 except Fault:
361 pass
362
363 try:
364 # Invalid parameters
365 aaa(5, 6, 7)
366 print "Successful execution of invalid code"
367 except Fault:
368 pass
369
370 try:
371 # Invalid method name
372 print repr(remote.powx(2, 4))
373 print "Successful execution of invalid code"
374 except Fault:
375 pass