From beda52ed36e701e45f22903fc4d3bec0d085b25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 19 Feb 2019 17:18:50 +0100 Subject: [PATCH] bpo-35153: Add headers parameter to xmlrpc.client.ServerProxy (GH-10308) Allow to add HTTP headers to XML-RPC requests sent to the server. --- Doc/library/xmlrpc.client.rst | 14 +++-- Lib/test/test_xmlrpc.py | 63 ++++++++++++++++++- Lib/xmlrpc/client.py | 17 +++-- .../2018-11-03-12-38-03.bpo-35153.009pdF.rst | 3 + 4 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-11-03-12-38-03.bpo-35153.009pdF.rst diff --git a/Doc/library/xmlrpc.client.rst b/Doc/library/xmlrpc.client.rst index 27d92e32472..32403819531 100644 --- a/Doc/library/xmlrpc.client.rst +++ b/Doc/library/xmlrpc.client.rst @@ -34,10 +34,7 @@ between conformable Python objects and XML on the wire. .. class:: ServerProxy(uri, transport=None, encoding=None, verbose=False, \ allow_none=False, use_datetime=False, \ - use_builtin_types=False, *, context=None) - - .. versionchanged:: 3.3 - The *use_builtin_types* flag was added. + use_builtin_types=False, *, headers=(), context=None) A :class:`ServerProxy` instance is an object that manages communication with a remote XML-RPC server. The required first argument is a URI (Uniform Resource @@ -59,9 +56,18 @@ between conformable Python objects and XML on the wire. presented as :class:`bytes` objects; this flag is false by default. :class:`datetime.datetime`, :class:`bytes` and :class:`bytearray` objects may be passed to calls. + The *headers* parameter is an optional sequence of HTTP headers to send with + each request, expressed as a sequence of 2-tuples representing the header + name and value. (e.g. `[('Header-Name', 'value')]`). The obsolete *use_datetime* flag is similar to *use_builtin_types* but it applies only to date/time values. +.. versionchanged:: 3.3 + The *use_builtin_types* flag was added. + +.. versionchanged:: 3.8 + The *headers* parameter was added. + Both the HTTP and HTTPS transports support the URL syntax extension for HTTP Basic Authentication: ``http://user:pass@host:port/path``. The ``user:pass`` portion will be base64-encoded as an HTTP 'Authorization' header, and sent to diff --git a/Lib/test/test_xmlrpc.py b/Lib/test/test_xmlrpc.py index 32263f7f0b3..916e9c49a97 100644 --- a/Lib/test/test_xmlrpc.py +++ b/Lib/test/test_xmlrpc.py @@ -1170,6 +1170,67 @@ class GzipUtilTestCase(unittest.TestCase): xmlrpclib.gzip_decode(encoded, max_decode=-1) +class HeadersServerTestCase(BaseServerTestCase): + class RequestHandler(xmlrpc.server.SimpleXMLRPCRequestHandler): + test_headers = None + + def do_POST(self): + self.__class__.test_headers = self.headers + return super().do_POST() + requestHandler = RequestHandler + standard_headers = [ + 'Host', 'Accept-Encoding', 'Content-Type', 'User-Agent', + 'Content-Length'] + + def setUp(self): + self.RequestHandler.test_headers = None + return super().setUp() + + def assertContainsAdditionalHeaders(self, headers, additional): + expected_keys = sorted(self.standard_headers + list(additional.keys())) + self.assertListEqual(sorted(headers.keys()), expected_keys) + + for key, value in additional.items(): + self.assertEqual(headers.get(key), value) + + def test_header(self): + p = xmlrpclib.ServerProxy(URL, headers=[('X-Test', 'foo')]) + self.assertEqual(p.pow(6, 8), 6**8) + + headers = self.RequestHandler.test_headers + self.assertContainsAdditionalHeaders(headers, {'X-Test': 'foo'}) + + def test_header_many(self): + p = xmlrpclib.ServerProxy( + URL, headers=[('X-Test', 'foo'), ('X-Test-Second', 'bar')]) + self.assertEqual(p.pow(6, 8), 6**8) + + headers = self.RequestHandler.test_headers + self.assertContainsAdditionalHeaders( + headers, {'X-Test': 'foo', 'X-Test-Second': 'bar'}) + + def test_header_empty(self): + p = xmlrpclib.ServerProxy(URL, headers=[]) + self.assertEqual(p.pow(6, 8), 6**8) + + headers = self.RequestHandler.test_headers + self.assertContainsAdditionalHeaders(headers, {}) + + def test_header_tuple(self): + p = xmlrpclib.ServerProxy(URL, headers=(('X-Test', 'foo'),)) + self.assertEqual(p.pow(6, 8), 6**8) + + headers = self.RequestHandler.test_headers + self.assertContainsAdditionalHeaders(headers, {'X-Test': 'foo'}) + + def test_header_items(self): + p = xmlrpclib.ServerProxy(URL, headers={'X-Test': 'foo'}.items()) + self.assertEqual(p.pow(6, 8), 6**8) + + headers = self.RequestHandler.test_headers + self.assertContainsAdditionalHeaders(headers, {'X-Test': 'foo'}) + + #Test special attributes of the ServerProxy object class ServerProxyTestCase(unittest.TestCase): def setUp(self): @@ -1396,7 +1457,7 @@ def test_main(): BinaryTestCase, FaultTestCase, UseBuiltinTypesTestCase, SimpleServerTestCase, SimpleServerEncodingTestCase, KeepaliveServerTestCase1, KeepaliveServerTestCase2, - GzipServerTestCase, GzipUtilTestCase, + GzipServerTestCase, GzipUtilTestCase, HeadersServerTestCase, MultiPathServerTestCase, ServerProxyTestCase, FailingServerTestCase, CGIHandlerTestCase, SimpleXMLRPCDispatcherTestCase) diff --git a/Lib/xmlrpc/client.py b/Lib/xmlrpc/client.py index ddab76ffbe8..a0e923a2032 100644 --- a/Lib/xmlrpc/client.py +++ b/Lib/xmlrpc/client.py @@ -1131,10 +1131,12 @@ class Transport: # that they can decode such a request encode_threshold = None #None = don't encode - def __init__(self, use_datetime=False, use_builtin_types=False): + def __init__(self, use_datetime=False, use_builtin_types=False, + *, headers=()): self._use_datetime = use_datetime self._use_builtin_types = use_builtin_types self._connection = (None, None) + self._headers = list(headers) self._extra_headers = [] ## @@ -1265,7 +1267,7 @@ class Transport: def send_request(self, host, handler, request_body, debug): connection = self.make_connection(host) - headers = self._extra_headers[:] + headers = self._headers + self._extra_headers if debug: connection.set_debuglevel(1) if self.accept_gzip_encoding and gzip: @@ -1347,9 +1349,11 @@ class Transport: class SafeTransport(Transport): """Handles an HTTPS transaction to an XML-RPC server.""" - def __init__(self, use_datetime=False, use_builtin_types=False, *, - context=None): - super().__init__(use_datetime=use_datetime, use_builtin_types=use_builtin_types) + def __init__(self, use_datetime=False, use_builtin_types=False, + *, headers=(), context=None): + super().__init__(use_datetime=use_datetime, + use_builtin_types=use_builtin_types, + headers=headers) self.context = context # FIXME: mostly untested @@ -1409,7 +1413,7 @@ class ServerProxy: def __init__(self, uri, transport=None, encoding=None, verbose=False, allow_none=False, use_datetime=False, use_builtin_types=False, - *, context=None): + *, headers=(), context=None): # establish a "logical" server connection # get the url @@ -1429,6 +1433,7 @@ class ServerProxy: extra_kwargs = {} transport = handler(use_datetime=use_datetime, use_builtin_types=use_builtin_types, + headers=headers, **extra_kwargs) self.__transport = transport diff --git a/Misc/NEWS.d/next/Library/2018-11-03-12-38-03.bpo-35153.009pdF.rst b/Misc/NEWS.d/next/Library/2018-11-03-12-38-03.bpo-35153.009pdF.rst new file mode 100644 index 00000000000..7926024a67b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-11-03-12-38-03.bpo-35153.009pdF.rst @@ -0,0 +1,3 @@ +Add *headers* optional keyword-only parameter to +:class:`xmlrpc.client.ServerProxy`, :class:`xmlrpc.client.Transport` and +:class:`xmlrpc.client.SafeTransport`. Patch by Cédric Krier.