1  r"""XML-RPC Servers.
       2  
       3  This module can be used to create simple XML-RPC servers
       4  by creating a server and either installing functions, a
       5  class instance, or by extending the SimpleXMLRPCServer
       6  class.
       7  
       8  It can also be used to handle XML-RPC requests in a CGI
       9  environment using CGIXMLRPCRequestHandler.
      10  
      11  The Doc* classes can be used to create XML-RPC servers that
      12  serve pydoc-style documentation in response to HTTP
      13  GET requests. This documentation is dynamically generated
      14  based on the functions and methods registered with the
      15  server.
      16  
      17  A list of possible usage patterns follows:
      18  
      19  1. Install functions:
      20  
      21  server = SimpleXMLRPCServer(("localhost", 8000))
      22  server.register_function(pow)
      23  server.register_function(lambda x,y: x+y, 'add')
      24  server.serve_forever()
      25  
      26  2. Install an instance:
      27  
      28  class MyFuncs:
      29      def __init__(self):
      30          # make all of the sys functions available through sys.func_name
      31          import sys
      32          self.sys = sys
      33      def _listMethods(self):
      34          # implement this method so that system.listMethods
      35          # knows to advertise the sys methods
      36          return list_public_methods(self) + \
      37                  ['sys.' + method for method in list_public_methods(self.sys)]
      38      def pow(self, x, y): return pow(x, y)
      39      def add(self, x, y) : return x + y
      40  
      41  server = SimpleXMLRPCServer(("localhost", 8000))
      42  server.register_introspection_functions()
      43  server.register_instance(MyFuncs())
      44  server.serve_forever()
      45  
      46  3. Install an instance with custom dispatch method:
      47  
      48  class Math:
      49      def _listMethods(self):
      50          # this method must be present for system.listMethods
      51          # to work
      52          return ['add', 'pow']
      53      def _methodHelp(self, method):
      54          # this method must be present for system.methodHelp
      55          # to work
      56          if method == 'add':
      57              return "add(2,3) => 5"
      58          elif method == 'pow':
      59              return "pow(x, y[, z]) => number"
      60          else:
      61              # By convention, return empty
      62              # string if no help is available
      63              return ""
      64      def _dispatch(self, method, params):
      65          if method == 'pow':
      66              return pow(*params)
      67          elif method == 'add':
      68              return params[0] + params[1]
      69          else:
      70              raise ValueError('bad method')
      71  
      72  server = SimpleXMLRPCServer(("localhost", 8000))
      73  server.register_introspection_functions()
      74  server.register_instance(Math())
      75  server.serve_forever()
      76  
      77  4. Subclass SimpleXMLRPCServer:
      78  
      79  class MathServer(SimpleXMLRPCServer):
      80      def _dispatch(self, method, params):
      81          try:
      82              # We are forcing the 'export_' prefix on methods that are
      83              # callable through XML-RPC to prevent potential security
      84              # problems
      85              func = getattr(self, 'export_' + method)
      86          except AttributeError:
      87              raise Exception('method "%s" is not supported' % method)
      88          else:
      89              return func(*params)
      90  
      91      def export_add(self, x, y):
      92          return x + y
      93  
      94  server = MathServer(("localhost", 8000))
      95  server.serve_forever()
      96  
      97  5. CGI script:
      98  
      99  server = CGIXMLRPCRequestHandler()
     100  server.register_function(pow)
     101  server.handle_request()
     102  """
     103  
     104  # Written by Brian Quinlan (brian@sweetapp.com).
     105  # Based on code written by Fredrik Lundh.
     106  
     107  from xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode
     108  from http.server import BaseHTTPRequestHandler
     109  from functools import partial
     110  from inspect import signature
     111  import html
     112  import http.server
     113  import socketserver
     114  import sys
     115  import os
     116  import re
     117  import pydoc
     118  import traceback
     119  try:
     120      import fcntl
     121  except ImportError:
     122      fcntl = None
     123  
     124  def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
     125      """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
     126  
     127      Resolves a dotted attribute name to an object.  Raises
     128      an AttributeError if any attribute in the chain starts with a '_'.
     129  
     130      If the optional allow_dotted_names argument is false, dots are not
     131      supported and this function operates similar to getattr(obj, attr).
     132      """
     133  
     134      if allow_dotted_names:
     135          attrs = attr.split('.')
     136      else:
     137          attrs = [attr]
     138  
     139      for i in attrs:
     140          if i.startswith('_'):
     141              raise AttributeError(
     142                  'attempt to access private attribute "%s"' % i
     143                  )
     144          else:
     145              obj = getattr(obj,i)
     146      return obj
     147  
     148  def list_public_methods(obj):
     149      """Returns a list of attribute strings, found in the specified
     150      object, which represent callable attributes"""
     151  
     152      return [member for member in dir(obj)
     153                  if not member.startswith('_') and
     154                      callable(getattr(obj, member))]
     155  
     156  class ESC[4;38;5;81mSimpleXMLRPCDispatcher:
     157      """Mix-in class that dispatches XML-RPC requests.
     158  
     159      This class is used to register XML-RPC method handlers
     160      and then to dispatch them. This class doesn't need to be
     161      instanced directly when used by SimpleXMLRPCServer but it
     162      can be instanced when used by the MultiPathXMLRPCServer
     163      """
     164  
     165      def __init__(self, allow_none=False, encoding=None,
     166                   use_builtin_types=False):
     167          self.funcs = {}
     168          self.instance = None
     169          self.allow_none = allow_none
     170          self.encoding = encoding or 'utf-8'
     171          self.use_builtin_types = use_builtin_types
     172  
     173      def register_instance(self, instance, allow_dotted_names=False):
     174          """Registers an instance to respond to XML-RPC requests.
     175  
     176          Only one instance can be installed at a time.
     177  
     178          If the registered instance has a _dispatch method then that
     179          method will be called with the name of the XML-RPC method and
     180          its parameters as a tuple
     181          e.g. instance._dispatch('add',(2,3))
     182  
     183          If the registered instance does not have a _dispatch method
     184          then the instance will be searched to find a matching method
     185          and, if found, will be called. Methods beginning with an '_'
     186          are considered private and will not be called by
     187          SimpleXMLRPCServer.
     188  
     189          If a registered function matches an XML-RPC request, then it
     190          will be called instead of the registered instance.
     191  
     192          If the optional allow_dotted_names argument is true and the
     193          instance does not have a _dispatch method, method names
     194          containing dots are supported and resolved, as long as none of
     195          the name segments start with an '_'.
     196  
     197              *** SECURITY WARNING: ***
     198  
     199              Enabling the allow_dotted_names options allows intruders
     200              to access your module's global variables and may allow
     201              intruders to execute arbitrary code on your machine.  Only
     202              use this option on a secure, closed network.
     203  
     204          """
     205  
     206          self.instance = instance
     207          self.allow_dotted_names = allow_dotted_names
     208  
     209      def register_function(self, function=None, name=None):
     210          """Registers a function to respond to XML-RPC requests.
     211  
     212          The optional name argument can be used to set a Unicode name
     213          for the function.
     214          """
     215          # decorator factory
     216          if function is None:
     217              return partial(self.register_function, name=name)
     218  
     219          if name is None:
     220              name = function.__name__
     221          self.funcs[name] = function
     222  
     223          return function
     224  
     225      def register_introspection_functions(self):
     226          """Registers the XML-RPC introspection methods in the system
     227          namespace.
     228  
     229          see http://xmlrpc.usefulinc.com/doc/reserved.html
     230          """
     231  
     232          self.funcs.update({'system.listMethods' : self.system_listMethods,
     233                        'system.methodSignature' : self.system_methodSignature,
     234                        'system.methodHelp' : self.system_methodHelp})
     235  
     236      def register_multicall_functions(self):
     237          """Registers the XML-RPC multicall method in the system
     238          namespace.
     239  
     240          see http://www.xmlrpc.com/discuss/msgReader$1208"""
     241  
     242          self.funcs.update({'system.multicall' : self.system_multicall})
     243  
     244      def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
     245          """Dispatches an XML-RPC method from marshalled (XML) data.
     246  
     247          XML-RPC methods are dispatched from the marshalled (XML) data
     248          using the _dispatch method and the result is returned as
     249          marshalled data. For backwards compatibility, a dispatch
     250          function can be provided as an argument (see comment in
     251          SimpleXMLRPCRequestHandler.do_POST) but overriding the
     252          existing method through subclassing is the preferred means
     253          of changing method dispatch behavior.
     254          """
     255  
     256          try:
     257              params, method = loads(data, use_builtin_types=self.use_builtin_types)
     258  
     259              # generate response
     260              if dispatch_method is not None:
     261                  response = dispatch_method(method, params)
     262              else:
     263                  response = self._dispatch(method, params)
     264              # wrap response in a singleton tuple
     265              response = (response,)
     266              response = dumps(response, methodresponse=1,
     267                               allow_none=self.allow_none, encoding=self.encoding)
     268          except Fault as fault:
     269              response = dumps(fault, allow_none=self.allow_none,
     270                               encoding=self.encoding)
     271          except BaseException as exc:
     272              response = dumps(
     273                  Fault(1, "%s:%s" % (type(exc), exc)),
     274                  encoding=self.encoding, allow_none=self.allow_none,
     275                  )
     276  
     277          return response.encode(self.encoding, 'xmlcharrefreplace')
     278  
     279      def system_listMethods(self):
     280          """system.listMethods() => ['add', 'subtract', 'multiple']
     281  
     282          Returns a list of the methods supported by the server."""
     283  
     284          methods = set(self.funcs.keys())
     285          if self.instance is not None:
     286              # Instance can implement _listMethod to return a list of
     287              # methods
     288              if hasattr(self.instance, '_listMethods'):
     289                  methods |= set(self.instance._listMethods())
     290              # if the instance has a _dispatch method then we
     291              # don't have enough information to provide a list
     292              # of methods
     293              elif not hasattr(self.instance, '_dispatch'):
     294                  methods |= set(list_public_methods(self.instance))
     295          return sorted(methods)
     296  
     297      def system_methodSignature(self, method_name):
     298          """system.methodSignature('add') => [double, int, int]
     299  
     300          Returns a list describing the signature of the method. In the
     301          above example, the add method takes two integers as arguments
     302          and returns a double result.
     303  
     304          This server does NOT support system.methodSignature."""
     305  
     306          # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
     307  
     308          return 'signatures not supported'
     309  
     310      def system_methodHelp(self, method_name):
     311          """system.methodHelp('add') => "Adds two integers together"
     312  
     313          Returns a string containing documentation for the specified method."""
     314  
     315          method = None
     316          if method_name in self.funcs:
     317              method = self.funcs[method_name]
     318          elif self.instance is not None:
     319              # Instance can implement _methodHelp to return help for a method
     320              if hasattr(self.instance, '_methodHelp'):
     321                  return self.instance._methodHelp(method_name)
     322              # if the instance has a _dispatch method then we
     323              # don't have enough information to provide help
     324              elif not hasattr(self.instance, '_dispatch'):
     325                  try:
     326                      method = resolve_dotted_attribute(
     327                                  self.instance,
     328                                  method_name,
     329                                  self.allow_dotted_names
     330                                  )
     331                  except AttributeError:
     332                      pass
     333  
     334          # Note that we aren't checking that the method actually
     335          # be a callable object of some kind
     336          if method is None:
     337              return ""
     338          else:
     339              return pydoc.getdoc(method)
     340  
     341      def system_multicall(self, call_list):
     342          """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
     343  [[4], ...]
     344  
     345          Allows the caller to package multiple XML-RPC calls into a single
     346          request.
     347  
     348          See http://www.xmlrpc.com/discuss/msgReader$1208
     349          """
     350  
     351          results = []
     352          for call in call_list:
     353              method_name = call['methodName']
     354              params = call['params']
     355  
     356              try:
     357                  # XXX A marshalling error in any response will fail the entire
     358                  # multicall. If someone cares they should fix this.
     359                  results.append([self._dispatch(method_name, params)])
     360              except Fault as fault:
     361                  results.append(
     362                      {'faultCode' : fault.faultCode,
     363                       'faultString' : fault.faultString}
     364                      )
     365              except BaseException as exc:
     366                  results.append(
     367                      {'faultCode' : 1,
     368                       'faultString' : "%s:%s" % (type(exc), exc)}
     369                      )
     370          return results
     371  
     372      def _dispatch(self, method, params):
     373          """Dispatches the XML-RPC method.
     374  
     375          XML-RPC calls are forwarded to a registered function that
     376          matches the called XML-RPC method name. If no such function
     377          exists then the call is forwarded to the registered instance,
     378          if available.
     379  
     380          If the registered instance has a _dispatch method then that
     381          method will be called with the name of the XML-RPC method and
     382          its parameters as a tuple
     383          e.g. instance._dispatch('add',(2,3))
     384  
     385          If the registered instance does not have a _dispatch method
     386          then the instance will be searched to find a matching method
     387          and, if found, will be called.
     388  
     389          Methods beginning with an '_' are considered private and will
     390          not be called.
     391          """
     392  
     393          try:
     394              # call the matching registered function
     395              func = self.funcs[method]
     396          except KeyError:
     397              pass
     398          else:
     399              if func is not None:
     400                  return func(*params)
     401              raise Exception('method "%s" is not supported' % method)
     402  
     403          if self.instance is not None:
     404              if hasattr(self.instance, '_dispatch'):
     405                  # call the `_dispatch` method on the instance
     406                  return self.instance._dispatch(method, params)
     407  
     408              # call the instance's method directly
     409              try:
     410                  func = resolve_dotted_attribute(
     411                      self.instance,
     412                      method,
     413                      self.allow_dotted_names
     414                  )
     415              except AttributeError:
     416                  pass
     417              else:
     418                  if func is not None:
     419                      return func(*params)
     420  
     421          raise Exception('method "%s" is not supported' % method)
     422  
     423  class ESC[4;38;5;81mSimpleXMLRPCRequestHandler(ESC[4;38;5;149mBaseHTTPRequestHandler):
     424      """Simple XML-RPC request handler class.
     425  
     426      Handles all HTTP POST requests and attempts to decode them as
     427      XML-RPC requests.
     428      """
     429  
     430      # Class attribute listing the accessible path components;
     431      # paths not on this list will result in a 404 error.
     432      rpc_paths = ('/', '/RPC2', '/pydoc.css')
     433  
     434      #if not None, encode responses larger than this, if possible
     435      encode_threshold = 1400 #a common MTU
     436  
     437      #Override form StreamRequestHandler: full buffering of output
     438      #and no Nagle.
     439      wbufsize = -1
     440      disable_nagle_algorithm = True
     441  
     442      # a re to match a gzip Accept-Encoding
     443      aepattern = re.compile(r"""
     444                              \s* ([^\s;]+) \s*            #content-coding
     445                              (;\s* q \s*=\s* ([0-9\.]+))? #q
     446                              """, re.VERBOSE | re.IGNORECASE)
     447  
     448      def accept_encodings(self):
     449          r = {}
     450          ae = self.headers.get("Accept-Encoding", "")
     451          for e in ae.split(","):
     452              match = self.aepattern.match(e)
     453              if match:
     454                  v = match.group(3)
     455                  v = float(v) if v else 1.0
     456                  r[match.group(1)] = v
     457          return r
     458  
     459      def is_rpc_path_valid(self):
     460          if self.rpc_paths:
     461              return self.path in self.rpc_paths
     462          else:
     463              # If .rpc_paths is empty, just assume all paths are legal
     464              return True
     465  
     466      def do_POST(self):
     467          """Handles the HTTP POST request.
     468  
     469          Attempts to interpret all HTTP POST requests as XML-RPC calls,
     470          which are forwarded to the server's _dispatch method for handling.
     471          """
     472  
     473          # Check that the path is legal
     474          if not self.is_rpc_path_valid():
     475              self.report_404()
     476              return
     477  
     478          try:
     479              # Get arguments by reading body of request.
     480              # We read this in chunks to avoid straining
     481              # socket.read(); around the 10 or 15Mb mark, some platforms
     482              # begin to have problems (bug #792570).
     483              max_chunk_size = 10*1024*1024
     484              size_remaining = int(self.headers["content-length"])
     485              L = []
     486              while size_remaining:
     487                  chunk_size = min(size_remaining, max_chunk_size)
     488                  chunk = self.rfile.read(chunk_size)
     489                  if not chunk:
     490                      break
     491                  L.append(chunk)
     492                  size_remaining -= len(L[-1])
     493              data = b''.join(L)
     494  
     495              data = self.decode_request_content(data)
     496              if data is None:
     497                  return #response has been sent
     498  
     499              # In previous versions of SimpleXMLRPCServer, _dispatch
     500              # could be overridden in this class, instead of in
     501              # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
     502              # check to see if a subclass implements _dispatch and dispatch
     503              # using that method if present.
     504              response = self.server._marshaled_dispatch(
     505                      data, getattr(self, '_dispatch', None), self.path
     506                  )
     507          except Exception as e: # This should only happen if the module is buggy
     508              # internal error, report as HTTP server error
     509              self.send_response(500)
     510  
     511              # Send information about the exception if requested
     512              if hasattr(self.server, '_send_traceback_header') and \
     513                      self.server._send_traceback_header:
     514                  self.send_header("X-exception", str(e))
     515                  trace = traceback.format_exc()
     516                  trace = str(trace.encode('ASCII', 'backslashreplace'), 'ASCII')
     517                  self.send_header("X-traceback", trace)
     518  
     519              self.send_header("Content-length", "0")
     520              self.end_headers()
     521          else:
     522              self.send_response(200)
     523              self.send_header("Content-type", "text/xml")
     524              if self.encode_threshold is not None:
     525                  if len(response) > self.encode_threshold:
     526                      q = self.accept_encodings().get("gzip", 0)
     527                      if q:
     528                          try:
     529                              response = gzip_encode(response)
     530                              self.send_header("Content-Encoding", "gzip")
     531                          except NotImplementedError:
     532                              pass
     533              self.send_header("Content-length", str(len(response)))
     534              self.end_headers()
     535              self.wfile.write(response)
     536  
     537      def decode_request_content(self, data):
     538          #support gzip encoding of request
     539          encoding = self.headers.get("content-encoding", "identity").lower()
     540          if encoding == "identity":
     541              return data
     542          if encoding == "gzip":
     543              try:
     544                  return gzip_decode(data)
     545              except NotImplementedError:
     546                  self.send_response(501, "encoding %r not supported" % encoding)
     547              except ValueError:
     548                  self.send_response(400, "error decoding gzip content")
     549          else:
     550              self.send_response(501, "encoding %r not supported" % encoding)
     551          self.send_header("Content-length", "0")
     552          self.end_headers()
     553  
     554      def report_404 (self):
     555              # Report a 404 error
     556          self.send_response(404)
     557          response = b'No such page'
     558          self.send_header("Content-type", "text/plain")
     559          self.send_header("Content-length", str(len(response)))
     560          self.end_headers()
     561          self.wfile.write(response)
     562  
     563      def log_request(self, code='-', size='-'):
     564          """Selectively log an accepted request."""
     565  
     566          if self.server.logRequests:
     567              BaseHTTPRequestHandler.log_request(self, code, size)
     568  
     569  class ESC[4;38;5;81mSimpleXMLRPCServer(ESC[4;38;5;149msocketserverESC[4;38;5;149m.ESC[4;38;5;149mTCPServer,
     570                           ESC[4;38;5;149mSimpleXMLRPCDispatcher):
     571      """Simple XML-RPC server.
     572  
     573      Simple XML-RPC server that allows functions and a single instance
     574      to be installed to handle requests. The default implementation
     575      attempts to dispatch XML-RPC calls to the functions or instance
     576      installed in the server. Override the _dispatch method inherited
     577      from SimpleXMLRPCDispatcher to change this behavior.
     578      """
     579  
     580      allow_reuse_address = True
     581  
     582      # Warning: this is for debugging purposes only! Never set this to True in
     583      # production code, as will be sending out sensitive information (exception
     584      # and stack trace details) when exceptions are raised inside
     585      # SimpleXMLRPCRequestHandler.do_POST
     586      _send_traceback_header = False
     587  
     588      def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
     589                   logRequests=True, allow_none=False, encoding=None,
     590                   bind_and_activate=True, use_builtin_types=False):
     591          self.logRequests = logRequests
     592  
     593          SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
     594          socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
     595  
     596  
     597  class ESC[4;38;5;81mMultiPathXMLRPCServer(ESC[4;38;5;149mSimpleXMLRPCServer):
     598      """Multipath XML-RPC Server
     599      This specialization of SimpleXMLRPCServer allows the user to create
     600      multiple Dispatcher instances and assign them to different
     601      HTTP request paths.  This makes it possible to run two or more
     602      'virtual XML-RPC servers' at the same port.
     603      Make sure that the requestHandler accepts the paths in question.
     604      """
     605      def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
     606                   logRequests=True, allow_none=False, encoding=None,
     607                   bind_and_activate=True, use_builtin_types=False):
     608  
     609          SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none,
     610                                      encoding, bind_and_activate, use_builtin_types)
     611          self.dispatchers = {}
     612          self.allow_none = allow_none
     613          self.encoding = encoding or 'utf-8'
     614  
     615      def add_dispatcher(self, path, dispatcher):
     616          self.dispatchers[path] = dispatcher
     617          return dispatcher
     618  
     619      def get_dispatcher(self, path):
     620          return self.dispatchers[path]
     621  
     622      def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
     623          try:
     624              response = self.dispatchers[path]._marshaled_dispatch(
     625                 data, dispatch_method, path)
     626          except BaseException as exc:
     627              # report low level exception back to server
     628              # (each dispatcher should have handled their own
     629              # exceptions)
     630              response = dumps(
     631                  Fault(1, "%s:%s" % (type(exc), exc)),
     632                  encoding=self.encoding, allow_none=self.allow_none)
     633              response = response.encode(self.encoding, 'xmlcharrefreplace')
     634          return response
     635  
     636  class ESC[4;38;5;81mCGIXMLRPCRequestHandler(ESC[4;38;5;149mSimpleXMLRPCDispatcher):
     637      """Simple handler for XML-RPC data passed through CGI."""
     638  
     639      def __init__(self, allow_none=False, encoding=None, use_builtin_types=False):
     640          SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
     641  
     642      def handle_xmlrpc(self, request_text):
     643          """Handle a single XML-RPC request"""
     644  
     645          response = self._marshaled_dispatch(request_text)
     646  
     647          print('Content-Type: text/xml')
     648          print('Content-Length: %d' % len(response))
     649          print()
     650          sys.stdout.flush()
     651          sys.stdout.buffer.write(response)
     652          sys.stdout.buffer.flush()
     653  
     654      def handle_get(self):
     655          """Handle a single HTTP GET request.
     656  
     657          Default implementation indicates an error because
     658          XML-RPC uses the POST method.
     659          """
     660  
     661          code = 400
     662          message, explain = BaseHTTPRequestHandler.responses[code]
     663  
     664          response = http.server.DEFAULT_ERROR_MESSAGE % \
     665              {
     666               'code' : code,
     667               'message' : message,
     668               'explain' : explain
     669              }
     670          response = response.encode('utf-8')
     671          print('Status: %d %s' % (code, message))
     672          print('Content-Type: %s' % http.server.DEFAULT_ERROR_CONTENT_TYPE)
     673          print('Content-Length: %d' % len(response))
     674          print()
     675          sys.stdout.flush()
     676          sys.stdout.buffer.write(response)
     677          sys.stdout.buffer.flush()
     678  
     679      def handle_request(self, request_text=None):
     680          """Handle a single XML-RPC request passed through a CGI post method.
     681  
     682          If no XML data is given then it is read from stdin. The resulting
     683          XML-RPC response is printed to stdout along with the correct HTTP
     684          headers.
     685          """
     686  
     687          if request_text is None and \
     688              os.environ.get('REQUEST_METHOD', None) == 'GET':
     689              self.handle_get()
     690          else:
     691              # POST data is normally available through stdin
     692              try:
     693                  length = int(os.environ.get('CONTENT_LENGTH', None))
     694              except (ValueError, TypeError):
     695                  length = -1
     696              if request_text is None:
     697                  request_text = sys.stdin.read(length)
     698  
     699              self.handle_xmlrpc(request_text)
     700  
     701  
     702  # -----------------------------------------------------------------------------
     703  # Self documenting XML-RPC Server.
     704  
     705  class ESC[4;38;5;81mServerHTMLDoc(ESC[4;38;5;149mpydocESC[4;38;5;149m.ESC[4;38;5;149mHTMLDoc):
     706      """Class used to generate pydoc HTML document for a server"""
     707  
     708      def markup(self, text, escape=None, funcs={}, classes={}, methods={}):
     709          """Mark up some plain text, given a context of symbols to look for.
     710          Each context dictionary maps object names to anchor names."""
     711          escape = escape or self.escape
     712          results = []
     713          here = 0
     714  
     715          # XXX Note that this regular expression does not allow for the
     716          # hyperlinking of arbitrary strings being used as method
     717          # names. Only methods with names consisting of word characters
     718          # and '.'s are hyperlinked.
     719          pattern = re.compile(r'\b((http|https|ftp)://\S+[\w/]|'
     720                                  r'RFC[- ]?(\d+)|'
     721                                  r'PEP[- ]?(\d+)|'
     722                                  r'(self\.)?((?:\w|\.)+))\b')
     723          while match := pattern.search(text, here):
     724              start, end = match.span()
     725              results.append(escape(text[here:start]))
     726  
     727              all, scheme, rfc, pep, selfdot, name = match.groups()
     728              if scheme:
     729                  url = escape(all).replace('"', '"')
     730                  results.append('<a href="%s">%s</a>' % (url, url))
     731              elif rfc:
     732                  url = 'https://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc)
     733                  results.append('<a href="%s">%s</a>' % (url, escape(all)))
     734              elif pep:
     735                  url = 'https://peps.python.org/pep-%04d/' % int(pep)
     736                  results.append('<a href="%s">%s</a>' % (url, escape(all)))
     737              elif text[end:end+1] == '(':
     738                  results.append(self.namelink(name, methods, funcs, classes))
     739              elif selfdot:
     740                  results.append('self.<strong>%s</strong>' % name)
     741              else:
     742                  results.append(self.namelink(name, classes))
     743              here = end
     744          results.append(escape(text[here:]))
     745          return ''.join(results)
     746  
     747      def docroutine(self, object, name, mod=None,
     748                     funcs={}, classes={}, methods={}, cl=None):
     749          """Produce HTML documentation for a function or method object."""
     750  
     751          anchor = (cl and cl.__name__ or '') + '-' + name
     752          note = ''
     753  
     754          title = '<a name="%s"><strong>%s</strong></a>' % (
     755              self.escape(anchor), self.escape(name))
     756  
     757          if callable(object):
     758              argspec = str(signature(object))
     759          else:
     760              argspec = '(...)'
     761  
     762          if isinstance(object, tuple):
     763              argspec = object[0] or argspec
     764              docstring = object[1] or ""
     765          else:
     766              docstring = pydoc.getdoc(object)
     767  
     768          decl = title + argspec + (note and self.grey(
     769                 '<font face="helvetica, arial">%s</font>' % note))
     770  
     771          doc = self.markup(
     772              docstring, self.preformat, funcs, classes, methods)
     773          doc = doc and '<dd><tt>%s</tt></dd>' % doc
     774          return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
     775  
     776      def docserver(self, server_name, package_documentation, methods):
     777          """Produce HTML documentation for an XML-RPC server."""
     778  
     779          fdict = {}
     780          for key, value in methods.items():
     781              fdict[key] = '#-' + key
     782              fdict[value] = fdict[key]
     783  
     784          server_name = self.escape(server_name)
     785          head = '<big><big><strong>%s</strong></big></big>' % server_name
     786          result = self.heading(head)
     787  
     788          doc = self.markup(package_documentation, self.preformat, fdict)
     789          doc = doc and '<tt>%s</tt>' % doc
     790          result = result + '<p>%s</p>\n' % doc
     791  
     792          contents = []
     793          method_items = sorted(methods.items())
     794          for key, value in method_items:
     795              contents.append(self.docroutine(value, key, funcs=fdict))
     796          result = result + self.bigsection(
     797              'Methods', 'functions', ''.join(contents))
     798  
     799          return result
     800  
     801  
     802      def page(self, title, contents):
     803          """Format an HTML page."""
     804          css_path = "/pydoc.css"
     805          css_link = (
     806              '<link rel="stylesheet" type="text/css" href="%s">' %
     807              css_path)
     808          return '''\
     809  <!DOCTYPE>
     810  <html lang="en">
     811  <head>
     812  <meta charset="utf-8">
     813  <title>Python: %s</title>
     814  %s</head><body>%s</body></html>''' % (title, css_link, contents)
     815  
     816  class ESC[4;38;5;81mXMLRPCDocGenerator:
     817      """Generates documentation for an XML-RPC server.
     818  
     819      This class is designed as mix-in and should not
     820      be constructed directly.
     821      """
     822  
     823      def __init__(self):
     824          # setup variables used for HTML documentation
     825          self.server_name = 'XML-RPC Server Documentation'
     826          self.server_documentation = \
     827              "This server exports the following methods through the XML-RPC "\
     828              "protocol."
     829          self.server_title = 'XML-RPC Server Documentation'
     830  
     831      def set_server_title(self, server_title):
     832          """Set the HTML title of the generated server documentation"""
     833  
     834          self.server_title = server_title
     835  
     836      def set_server_name(self, server_name):
     837          """Set the name of the generated HTML server documentation"""
     838  
     839          self.server_name = server_name
     840  
     841      def set_server_documentation(self, server_documentation):
     842          """Set the documentation string for the entire server."""
     843  
     844          self.server_documentation = server_documentation
     845  
     846      def generate_html_documentation(self):
     847          """generate_html_documentation() => html documentation for the server
     848  
     849          Generates HTML documentation for the server using introspection for
     850          installed functions and instances that do not implement the
     851          _dispatch method. Alternatively, instances can choose to implement
     852          the _get_method_argstring(method_name) method to provide the
     853          argument string used in the documentation and the
     854          _methodHelp(method_name) method to provide the help text used
     855          in the documentation."""
     856  
     857          methods = {}
     858  
     859          for method_name in self.system_listMethods():
     860              if method_name in self.funcs:
     861                  method = self.funcs[method_name]
     862              elif self.instance is not None:
     863                  method_info = [None, None] # argspec, documentation
     864                  if hasattr(self.instance, '_get_method_argstring'):
     865                      method_info[0] = self.instance._get_method_argstring(method_name)
     866                  if hasattr(self.instance, '_methodHelp'):
     867                      method_info[1] = self.instance._methodHelp(method_name)
     868  
     869                  method_info = tuple(method_info)
     870                  if method_info != (None, None):
     871                      method = method_info
     872                  elif not hasattr(self.instance, '_dispatch'):
     873                      try:
     874                          method = resolve_dotted_attribute(
     875                                      self.instance,
     876                                      method_name
     877                                      )
     878                      except AttributeError:
     879                          method = method_info
     880                  else:
     881                      method = method_info
     882              else:
     883                  assert 0, "Could not find method in self.functions and no "\
     884                            "instance installed"
     885  
     886              methods[method_name] = method
     887  
     888          documenter = ServerHTMLDoc()
     889          documentation = documenter.docserver(
     890                                  self.server_name,
     891                                  self.server_documentation,
     892                                  methods
     893                              )
     894  
     895          return documenter.page(html.escape(self.server_title), documentation)
     896  
     897  class ESC[4;38;5;81mDocXMLRPCRequestHandler(ESC[4;38;5;149mSimpleXMLRPCRequestHandler):
     898      """XML-RPC and documentation request handler class.
     899  
     900      Handles all HTTP POST requests and attempts to decode them as
     901      XML-RPC requests.
     902  
     903      Handles all HTTP GET requests and interprets them as requests
     904      for documentation.
     905      """
     906  
     907      def _get_css(self, url):
     908          path_here = os.path.dirname(os.path.realpath(__file__))
     909          css_path = os.path.join(path_here, "..", "pydoc_data", "_pydoc.css")
     910          with open(css_path, mode="rb") as fp:
     911              return fp.read()
     912  
     913      def do_GET(self):
     914          """Handles the HTTP GET request.
     915  
     916          Interpret all HTTP GET requests as requests for server
     917          documentation.
     918          """
     919          # Check that the path is legal
     920          if not self.is_rpc_path_valid():
     921              self.report_404()
     922              return
     923  
     924          if self.path.endswith('.css'):
     925              content_type = 'text/css'
     926              response = self._get_css(self.path)
     927          else:
     928              content_type = 'text/html'
     929              response = self.server.generate_html_documentation().encode('utf-8')
     930  
     931          self.send_response(200)
     932          self.send_header('Content-Type', '%s; charset=UTF-8' % content_type)
     933          self.send_header("Content-length", str(len(response)))
     934          self.end_headers()
     935          self.wfile.write(response)
     936  
     937  class ESC[4;38;5;81mDocXMLRPCServer(  ESC[4;38;5;149mSimpleXMLRPCServer,
     938                          ESC[4;38;5;149mXMLRPCDocGenerator):
     939      """XML-RPC and HTML documentation server.
     940  
     941      Adds the ability to serve server documentation to the capabilities
     942      of SimpleXMLRPCServer.
     943      """
     944  
     945      def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
     946                   logRequests=True, allow_none=False, encoding=None,
     947                   bind_and_activate=True, use_builtin_types=False):
     948          SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
     949                                      allow_none, encoding, bind_and_activate,
     950                                      use_builtin_types)
     951          XMLRPCDocGenerator.__init__(self)
     952  
     953  class ESC[4;38;5;81mDocCGIXMLRPCRequestHandler(   ESC[4;38;5;149mCGIXMLRPCRequestHandler,
     954                                      ESC[4;38;5;149mXMLRPCDocGenerator):
     955      """Handler for XML-RPC data and documentation requests passed through
     956      CGI"""
     957  
     958      def handle_get(self):
     959          """Handles the HTTP GET request.
     960  
     961          Interpret all HTTP GET requests as requests for server
     962          documentation.
     963          """
     964  
     965          response = self.generate_html_documentation().encode('utf-8')
     966  
     967          print('Content-Type: text/html')
     968          print('Content-Length: %d' % len(response))
     969          print()
     970          sys.stdout.flush()
     971          sys.stdout.buffer.write(response)
     972          sys.stdout.buffer.flush()
     973  
     974      def __init__(self):
     975          CGIXMLRPCRequestHandler.__init__(self)
     976          XMLRPCDocGenerator.__init__(self)
     977  
     978  
     979  if __name__ == '__main__':
     980      import datetime
     981  
     982      class ESC[4;38;5;81mExampleService:
     983          def getData(self):
     984              return '42'
     985  
     986          class ESC[4;38;5;81mcurrentTime:
     987              @staticmethod
     988              def getCurrentTime():
     989                  return datetime.datetime.now()
     990  
     991      with SimpleXMLRPCServer(("localhost", 8000)) as server:
     992          server.register_function(pow)
     993          server.register_function(lambda x,y: x+y, 'add')
     994          server.register_instance(ExampleService(), allow_dotted_names=True)
     995          server.register_multicall_functions()
     996          print('Serving XML-RPC on localhost port 8000')
     997          print('It is advisable to run this example server within a secure, closed network.')
     998          try:
     999              server.serve_forever()
    1000          except KeyboardInterrupt:
    1001              print("\nKeyboard interrupt received, exiting.")
    1002              sys.exit(0)