diff -r a1cd431a71c6 Doc/library/xmlrpc.server.rst --- a/Doc/library/xmlrpc.server.rst Sun Oct 28 11:13:51 2012 -0700 +++ b/Doc/library/xmlrpc.server.rst Sun Oct 28 23:34:55 2012 -0400 @@ -18,7 +18,8 @@ .. class:: SimpleXMLRPCServer(addr, requestHandler=SimpleXMLRPCRequestHandler,\ logRequests=True, allow_none=False, encoding=None,\ - bind_and_activate=True, use_builtin_types=False) + bind_and_activate=True, use_builtin_types=False, + ssl_certfile=None, ssl_context=None) Create a new server instance. This class provides methods for registration of functions that can be called by the XML-RPC protocol. The *requestHandler* @@ -34,7 +35,11 @@ the *allow_reuse_address* class variable before the address is bound. The *use_builtin_types* parameter is passed to the :func:`~xmlrpc.client.loads` function and controls which types are processed - when date/times values or binary data are received; it defaults to false. + when date/times values or binary data are received; it defaults to false. If + *ssl_context* is passed, then :func:`wrap_socket` is called on the created + socket object. If *ssl_certfile* is passed, then a context is created using + that file. Note that *ssl_certfile* should contain both the key and the + certificate, in that exact order. .. versionchanged:: 3.3 The *use_builtin_types* flag was added. @@ -250,7 +255,8 @@ .. class:: DocXMLRPCServer(addr, requestHandler=DocXMLRPCRequestHandler,\ logRequests=True, allow_none=False, encoding=None,\ - bind_and_activate=True, use_builtin_types=True) + bind_and_activate=True, use_builtin_types=True, + ssl_certfile=None, ssl_context=None) Create a new server instance. All parameters have the same meaning as for :class:`SimpleXMLRPCServer`; *requestHandler* defaults to diff -r a1cd431a71c6 Lib/test/test_xmlrpc.py --- a/Lib/test/test_xmlrpc.py Sun Oct 28 11:13:51 2012 -0700 +++ b/Lib/test/test_xmlrpc.py Sun Oct 28 23:34:55 2012 -0400 @@ -18,6 +18,12 @@ except ImportError: threading = None +# For SSL Server Checks. +try: + import ssl +except ImportError: + ssl = None + alist = [{'astring': 'foo@bar.baz.spam', 'afloat': 7283.43, 'anint': 2**20, @@ -36,6 +42,8 @@ datetime.datetime(2005, 2, 10, 11, 41, 23)), }] +CERTFILE = os.path.join(os.path.dirname(__file__), "keycert.pem") + class XMLRPCTestCase(unittest.TestCase): def test_dump_load(self): @@ -414,6 +422,63 @@ PORT = None evt.set() +# Start up a server with an SSL Context. +def ssl_http_server(evt, numrequests, requestHandler=None): + class TestInstanceClass: + def div(self, x, y): + return x // y + + def _methodHelp(self, name): + if name == 'div': + return 'This is the div function' + + def my_function(): + '''This is my function''' + return True + + class MyXMLRPCServer(xmlrpc.server.SimpleXMLRPCServer): + def get_request(self): + # Ensure socket is blocking, but rely on the super class + # get_request as we'll wrap in an SSL context. + s, port = super().get_request() + s.setblocking(True) + return s, port + + if not requestHandler: + requestHandler = xmlrpc.server.SimpleXMLRPCRequestHandler + serv = MyXMLRPCServer(("localhost", 0), requestHandler, + logRequests=False, bind_and_activate=False, + ssl_certfile=CERTFILE) + try: + serv.server_bind() + global ADDR, PORT, URL + ADDR, PORT = serv.socket.getsockname() + #connect to IP address directly. This avoids socket.create_connection() + #trying to connect to "localhost" using all address families, which + #causes slowdown e.g. on vista which supports AF_INET6. The server listens + #on AF_INET only. + URL = "https://%s:%d"%(ADDR, PORT) + serv.server_activate() + serv.register_introspection_functions() + serv.register_multicall_functions() + serv.register_function(pow) + serv.register_function(lambda x,y: x+y, 'add') + serv.register_function(my_function) + serv.register_instance(TestInstanceClass()) + evt.set() + + # handle up to 'numrequests' requests + while numrequests > 0: + serv.handle_request() + numrequests -= 1 + + except socket.timeout: + pass + finally: + serv.socket.close() + PORT = None + evt.set() + def http_multi_server(evt, numrequests, requestHandler=None): class TestInstanceClass: def div(self, x, y): @@ -479,6 +544,7 @@ PORT = None evt.set() + # This function prevents errors like: # def is_unavailable_exception(e): @@ -540,6 +606,31 @@ # disable traceback reporting xmlrpc.server.SimpleXMLRPCServer._send_traceback_header = False +@unittest.skipUnless(ssl, 'SSL required for this test.') +class SSLServerTestCase(BaseServerTestCase): + threadFunc = staticmethod(ssl_http_server) + + def test_simplecall(self): + # Try the basic RPC functionality to ensure the call is + # dispatched as it should be. + p = xmlrpclib.ServerProxy(URL) + self.assertEqual(6**8, p.pow(6, 8)) + + def test_multicall(self): + # Run a multicall to ensure SSL doesn't interfere with + # dispatching. + multicall = xmlrpclib.MultiCall(xmlrpclib.ServerProxy(URL)) + multicall.pow(2,3) + multicall.div(8,4) + power_func, div_func = multicall() + self.assertEqual(power_func, 8) + self.assertEqual(div_func, 2) + + def test_nossl(self): + # Connect w/o SSL availability. + p = xmlrpclib.ServerProxy(URL.replace('https', 'http')) + self.assertRaises(ConnectionRefusedError, p.pow, 1, 1) + class SimpleServerTestCase(BaseServerTestCase): def test_simple1(self): try: @@ -1091,7 +1182,7 @@ xmlrpc_tests.append(ServerProxyTestCase) xmlrpc_tests.append(FailingServerTestCase) xmlrpc_tests.append(CGIHandlerTestCase) - + xmlrpc_tests.append(SSLServerTestCase) support.run_unittest(*xmlrpc_tests) if __name__ == "__main__": diff -r a1cd431a71c6 Lib/xmlrpc/server.py --- a/Lib/xmlrpc/server.py Sun Oct 28 11:13:51 2012 -0700 +++ b/Lib/xmlrpc/server.py Sun Oct 28 23:34:55 2012 -0400 @@ -119,6 +119,13 @@ except ImportError: fcntl = None +try: + import ssl + _have_ssl = True +except ImportError: + _have_ssl = False + + def resolve_dotted_attribute(obj, attr, allow_dotted_names=True): """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d @@ -578,9 +585,26 @@ def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, logRequests=True, allow_none=False, encoding=None, - bind_and_activate=True, use_builtin_types=False): + bind_and_activate=True, use_builtin_types=False, + ssl_certfile=None, ssl_context=None): self.logRequests = logRequests + # Setup SSL context here if we were passed a cert path. Or just + # default to a context. We do this here to allow errors before a socket.bind(). + if _have_ssl: + + # These are mutually exclusive. We only want to support a simple certfile or + # a more inclusive context. Don't give the opportunity to introduce any + # non-obvious behavior with SSL. + if ssl_certfile and ssl_context: + raise ValueError("'ssl_certfile' and 'ssl_context' are mutually exclusive'") + + if ssl_certfile: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_context.load_cert_chain(ssl_certfile) + + self._ssl_context = ssl_context + SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate) @@ -592,6 +616,16 @@ flags |= fcntl.FD_CLOEXEC fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) + def get_request(self): + """Accepts an incoming socket connection.""" + sock, addrinfo = self.socket.accept() + # If we're using SSL, wrap the incoming connection using our context, + # if one exists. + if _have_ssl and self._ssl_context: + sock = self._ssl_context.wrap_socket(sock, server_side=True) + return sock, addrinfo + + class MultiPathXMLRPCServer(SimpleXMLRPCServer): """Multipath XML-RPC Server This specialization of SimpleXMLRPCServer allows the user to create @@ -931,10 +965,11 @@ def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler, logRequests=True, allow_none=False, encoding=None, - bind_and_activate=True, use_builtin_types=False): + bind_and_activate=True, use_builtin_types=False, + ssl_certfile=None, ssl_context=None): SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none, encoding, bind_and_activate, - use_builtin_types) + use_builtin_types, ssl_certfile, ssl_context) XMLRPCDocGenerator.__init__(self) class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler,