Package Gnumed :: Package ProxiedWeb :: Module jsonserver
[frames] | no frames]

Source Code for Module Gnumed.ProxiedWeb.jsonserver

  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 SimpleHTTPServer 
 15  import BaseHTTPServer 
 16  import sys 
 17  import traceback 
 18  import socket 
 19  import os 
 20  import cgi 
 21  import urllib 
 22  try: 
 23      import fcntl 
 24  except ImportError: 
 25      fcntl = None 
 26  try: 
 27      from cStringIO import StringIO 
 28  except ImportError: 
 29      from StringIO import StringIO 
 30   
 31   
 32  ### 
 33  ### Server code 
 34  ### 
 35  import SimpleXMLRPCServer 
 36   
37 -def _quote_html(html):
38 return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
39
40 -class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
41 - def _marshaled_dispatch(self, data, dispatch_method = None):
42 id = None 43 try: 44 print "about to decode. pid:", os.getpid(), repr(data) 45 req = cjson.decode(data) 46 method = req['method'] 47 params = req['params'] or () 48 id = req['id'] 49 print "decoded and about to call. pid:", os.getpid(), method, params 50 51 if dispatch_method is not None: 52 result = dispatch_method(method, params) 53 else: 54 result = self._dispatch(method, params) 55 response = dict(id=id, result=result, error=None) 56 except: 57 extpe, exv, extrc = sys.exc_info() 58 err = dict(type=str(extpe), 59 message=str(exv), 60 traceback=''.join(traceback.format_tb(extrc))) 61 response = dict(id=id, result=None, error=err) 62 try: 63 return cjson.encode(response) 64 except: 65 extpe, exv, extrc = sys.exc_info() 66 err = dict(type=str(extpe), 67 message=str(exv), 68 traceback=''.join(traceback.format_tb(extrc))) 69 response = dict(id=id, result=None, error=err) 70 return cjson.encode(response)
71 72
73 -class SimpleJSONRPCRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
74 """Simple JSONRPC request handler class and HTTP GET Server 75 76 Handles all HTTP POST requests and attempts to decode them as 77 JSONRPC requests. 78 79 Handles all HTTP GET requests and serves the content from the 80 current directory. 81 82 """ 83 84 # Class attribute listing the accessible path components; 85 # paths not on this list will result in a 404 error. 86 rpc_paths = ('/', '/JSON') 87
88 - def is_rpc_path_valid(self):
89 if self.rpc_paths: 90 return self.path in self.rpc_paths 91 else: 92 # If .rpc_paths is empty, just assume all paths are legal 93 return True
94
95 - def send_head(self):
96 """Common code for GET and HEAD commands. 97 98 This sends the response code and MIME headers. 99 100 Return value is either a file object (which has to be copied 101 to the outputfile by the caller unless the command was HEAD, 102 and must be closed by the caller under all circumstances), or 103 None, in which case the caller has nothing further to do. 104 105 """ 106 print "send_head. pid:", os.getpid() 107 path = self.translate_path(self.path) 108 f = None 109 if os.path.isdir(path): 110 if not self.path.endswith('/'): 111 # redirect browser - doing basically what apache does 112 self.send_response(301) 113 self.send_header("Location", self.path + "/") 114 self.end_headers() 115 return None 116 for index in "index.html", "index.htm": 117 index = os.path.join(path, index) 118 if os.path.exists(index): 119 path = index 120 break 121 else: 122 return self.list_directory(path) 123 ctype = self.guess_type(path) 124 if ctype.startswith('text/'): 125 mode = 'r' 126 else: 127 mode = 'rb' 128 try: 129 f = open(path, mode) 130 except IOError: 131 self.send_error(404, "File not found") 132 return None 133 self.send_response(200) 134 self.send_header("Content-type", ctype) 135 fs = os.fstat(f.fileno()) 136 self.send_header("Content-Length", str(fs[6])) 137 self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 138 if self.request_version == "HTTP/1.1": 139 self.send_header("Connection", "keep-alive") 140 141 self.end_headers() 142 return f
143
144 - def list_directory(self, path):
145 """Helper to produce a directory listing (absent index.html). 146 147 Return value is either a file object, or None (indicating an 148 error). In either case, the headers are sent, making the 149 interface the same as for send_head(). 150 151 """ 152 try: 153 list = os.listdir(path) 154 except os.error: 155 self.send_error(404, "No permission to list directory") 156 return None 157 list.sort(key=lambda a: a.lower()) 158 f = StringIO() 159 displaypath = cgi.escape(urllib.unquote(self.path)) 160 f.write("<title>Directory listing for %s</title>\n" % displaypath) 161 f.write("<h2>Directory listing for %s</h2>\n" % displaypath) 162 f.write("<hr>\n<ul>\n") 163 for name in list: 164 fullname = os.path.join(path, name) 165 displayname = linkname = name 166 # Append / for directories or @ for symbolic links 167 if os.path.isdir(fullname): 168 displayname = name + "/" 169 linkname = name + "/" 170 if os.path.islink(fullname): 171 displayname = name + "@" 172 # Note: a link to a directory displays with @ and links with / 173 f.write('<li><a href="%s">%s</a>\n' 174 % (urllib.quote(linkname), cgi.escape(displayname))) 175 f.write("</ul>\n<hr>\n") 176 f.write("\n<hr>\nCookies: %s\n<hr>\n" % str(self.headers.get("Cookie", None))) 177 length = f.tell() 178 f.seek(0) 179 self.send_response(200) 180 self.send_header("Content-type", "text/html") 181 if self.request_version == "HTTP/1.1": 182 self.send_header("Connection", "keep-alive") 183 self.send_header("Content-Length", str(length)) 184 self.end_headers() 185 return f
186
187 - def send_error(self, code, message=None):
188 """Send and log an error reply. 189 190 Arguments are the error code, and a detailed message. 191 The detailed message defaults to the short entry matching the 192 response code. 193 194 This sends an error response (so it must be called before any 195 output has been generated), logs the error, and finally sends 196 a piece of HTML explaining the error to the user. 197 198 """ 199 200 try: 201 short, long = self.responses[code] 202 except KeyError: 203 short, long = '???', '???' 204 if message is None: 205 message = short 206 explain = long 207 self.log_error("code %d, message %s", code, message) 208 # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201) 209 content = (self.error_message_format % 210 {'code': code, 'message': _quote_html(message), 'explain': explain}) 211 self.send_response(code, message) 212 self.send_header("Content-Type", "text/html") 213 if self.command != 'HEAD' and code >= 200 and code not in (204, 304): 214 self.send_header("Content-Length", str(len(content))) 215 if self.request_version == "HTTP/1.1": 216 self.send_header("Connection", "keep-alive") 217 else: 218 self.send_header('Connection', 'close') 219 self.end_headers() 220 if self.command != 'HEAD' and code >= 200 and code not in (204, 304): 221 self.wfile.write(content)
222
223 - def do_POST(self):
224 """Handles the HTTP POST request. 225 226 Attempts to interpret all HTTP POST requests as XML-RPC calls, 227 which are forwarded to the server's _dispatch method for handling. 228 """ 229 230 self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 231 232 # Check that the path is legal 233 if not self.is_rpc_path_valid(): 234 self.report_404() 235 return 236 237 try: 238 # Get arguments by reading body of request. 239 # We read this in chunks to avoid straining 240 # socket.read(); around the 10 or 15Mb mark, some platforms 241 # begin to have problems (bug #792570). 242 max_chunk_size = 10*1024*1024 243 size_remaining = int(self.headers["content-length"]) 244 L = [] 245 while size_remaining: 246 chunk_size = min(size_remaining, max_chunk_size) 247 L.append(self.rfile.read(chunk_size)) 248 size_remaining -= len(L[-1]) 249 data = ''.join(L) 250 251 # In previous versions of SimpleXMLRPCServer, _dispatch 252 # could be overridden in this class, instead of in 253 # SimpleXMLRPCDispatcher. To maintain backwards compatibility, 254 # check to see if a subclass implements _dispatch and dispatch 255 # using that method if present. 256 response = self.server._marshaled_dispatch( 257 data, getattr(self, '_dispatch', None) 258 ) 259 except: # This should only happen if the module is buggy 260 # internal error, report as HTTP server error 261 self.send_response(500) 262 self.end_headers() 263 else: 264 # got a valid JSONRPC response 265 self.send_response(200) 266 self.send_header("Content-type", "text/x-json") 267 self.send_header("Connection", "keep-alive") 268 self.send_header("Content-length", str(len(response))) 269 self.end_headers() 270 print "response", repr(response) 271 self.wfile.write(response) 272 273 self.wfile.flush() 274 print "flushed"
275
276 - def report_404 (self):
277 # Report a 404 error 278 self.send_response(404) 279 response = 'No such page' 280 self.send_header("Content-type", "text/plain") 281 self.send_header("Content-length", str(len(response))) 282 self.end_headers() 283 self.wfile.write(response) 284 # shut down the connection 285 self.wfile.flush() 286 self.connection.shutdown(1)
287
288 - def log_request(self, code='-', size='-'):
289 """Selectively log an accepted request.""" 290 291 if self.server.logRequests: 292 BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
293 294
295 -class SimpleForkingJSONRPCServer(SocketServer.ForkingTCPServer, 296 SimpleJSONRPCDispatcher):
297 """Simple JSON-RPC server. 298 299 Simple JSON-RPC server that allows functions and a single instance 300 to be installed to handle requests. The default implementation 301 attempts to dispatch JSON-RPC calls to the functions or instance 302 installed in the server. Override the _dispatch method inhereted 303 from SimpleJSONRPCDispatcher to change this behavior. 304 """ 305 306 allow_reuse_address = True 307
308 - def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, 309 logRequests=True):
310 self.logRequests = logRequests 311 312 SimpleJSONRPCDispatcher.__init__(self, allow_none=True, encoding=None) 313 SocketServer.ForkingTCPServer.__init__(self, addr, requestHandler) 314 315 # [Bug #1222790] If possible, set close-on-exec flag; if a 316 # method spawns a subprocess, the subprocess shouldn't have 317 # the listening socket open. 318 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): 319 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) 320 flags |= fcntl.FD_CLOEXEC 321 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
322
323 -class SimpleJSONRPCServer(SocketServer.ThreadingTCPServer, 324 SimpleJSONRPCDispatcher):
325 """Simple JSON-RPC server. 326 327 Simple JSON-RPC server that allows functions and a single instance 328 to be installed to handle requests. The default implementation 329 attempts to dispatch JSON-RPC calls to the functions or instance 330 installed in the server. Override the _dispatch method inhereted 331 from SimpleJSONRPCDispatcher to change this behavior. 332 """ 333 334 allow_reuse_address = True 335
336 - def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, 337 logRequests=True):
338 self.logRequests = logRequests 339 340 SimpleJSONRPCDispatcher.__init__(self, allow_none=True, encoding=None) 341 SocketServer.ThreadingTCPServer.__init__(self, addr, requestHandler) 342 343 # [Bug #1222790] If possible, set close-on-exec flag; if a 344 # method spawns a subprocess, the subprocess shouldn't have 345 # the listening socket open. 346 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): 347 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) 348 flags |= fcntl.FD_CLOEXEC 349 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
350 351 352 ### 353 ### Client code 354 ### 355 import xmlrpclib 356
357 -class ResponseError(xmlrpclib.ResponseError):
358 pass
359 -class Fault(xmlrpclib.ResponseError):
360 pass
361
362 -def _get_response(file, sock):
363 data = "" 364 while 1: 365 if sock: 366 response = sock.recv(1024) 367 else: 368 response = file.read(1024) 369 if not response: 370 break 371 data += response 372 373 file.close() 374 375 return data
376
377 -class Transport(xmlrpclib.Transport):
378 - def _parse_response(self, file, sock):
379 return _get_response(file, sock)
380
381 -class SafeTransport(xmlrpclib.SafeTransport):
382 - def _parse_response(self, file, sock):
383 return _get_response(file, sock)
384
385 -class ServerProxy:
386 - def __init__(self, uri, id=None, transport=None, use_datetime=0):
387 # establish a "logical" server connection 388 389 # get the url 390 import urllib 391 type, uri = urllib.splittype(uri) 392 if type not in ("http", "https"): 393 raise IOError, "unsupported JSON-RPC protocol" 394 self.__host, self.__handler = urllib.splithost(uri) 395 if not self.__handler: 396 self.__handler = "/JSON" 397 398 if transport is None: 399 if type == "https": 400 transport = SafeTransport(use_datetime=use_datetime) 401 else: 402 transport = Transport(use_datetime=use_datetime) 403 404 self.__transport = transport 405 self.__id = id
406
407 - def __request(self, methodname, params):
408 # call a method on the remote server 409 410 request = cjson.encode(dict(id=self.__id, method=methodname, 411 params=params)) 412 413 data = self.__transport.request( 414 self.__host, 415 self.__handler, 416 request, 417 verbose=False 418 ) 419 420 response = cjson.decode(data) 421 422 if response["id"] != self.__id: 423 raise ResponseError("Invalid request id (is: %s, expected: %s)" \ 424 % (response["id"], self.__id)) 425 if response["error"] is not None: 426 raise Fault("JSON Error", response["error"]) 427 return response["result"]
428
429 - def __repr__(self):
430 return ( 431 "<ServerProxy for %s%s>" % 432 (self.__host, self.__handler) 433 )
434 435 __str__ = __repr__ 436
437 - def __getattr__(self, name):
438 # magic method dispatcher 439 return xmlrpclib._Method(self.__request, name)
440 441 442 if __name__ == '__main__': 443 if not len(sys.argv) > 1: 444 import socket 445 print 'Running JSON-RPC server on port 8000' 446 server = SimpleJSONRPCServer(("localhost", 8000)) 447 server.register_function(pow) 448 server.register_function(lambda x,y: x+y, 'add') 449 server.register_function(lambda x: x, 'echo') 450 server.serve_forever() 451 else: 452 remote = ServerProxy(sys.argv[1]) 453 print 'Using connection', remote 454 455 print repr(remote.add(1, 2)) 456 aaa = remote.add 457 print repr(remote.pow(2, 4)) 458 print aaa(5, 6) 459 460 try: 461 # Invalid parameters 462 aaa(5, "toto") 463 print "Successful execution of invalid code" 464 except Fault: 465 pass 466 467 try: 468 # Invalid parameters 469 aaa(5, 6, 7) 470 print "Successful execution of invalid code" 471 except Fault: 472 pass 473 474 try: 475 # Invalid method name 476 print repr(remote.powx(2, 4)) 477 print "Successful execution of invalid code" 478 except Fault: 479 pass 480