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

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