diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst index 7a6717b5cf8..9f906df4ffe 100644 --- a/Doc/library/http.client.rst +++ b/Doc/library/http.client.rst @@ -23,7 +23,7 @@ HTTPS protocols. It is normally not used directly --- the module The module provides the following classes: -.. class:: HTTPConnection(host, port=None, strict=None[, timeout]) +.. class:: HTTPConnection(host, port=None, strict=None[, timeout[, source_address]]) An :class:`HTTPConnection` instance represents one transaction with an HTTP server. It should be instantiated passing it a host and optional port @@ -35,6 +35,8 @@ The module provides the following classes: status line. If the optional *timeout* parameter is given, blocking operations (like connection attempts) will timeout after that many seconds (if it is not given, the global default timeout setting is used). + The optional *source_address* parameter may be a typle of a (host, port) + to use as the source address the HTTP connection is made from. For example, the following calls all create instances that connect to the server at the same host and port:: @@ -44,8 +46,11 @@ The module provides the following classes: >>> h3 = http.client.HTTPConnection('www.cwi.nl', 80) >>> h3 = http.client.HTTPConnection('www.cwi.nl', 80, timeout=10) + .. versionchanged:: 3.2 + *source_address* was added. -.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout]) + +.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout[, source_address]]) A subclass of :class:`HTTPConnection` that uses SSL for communication with secure servers. Default port is ``443``. *key_file* is the name of a PEM @@ -56,6 +61,9 @@ The module provides the following classes: This does not do any certificate verification. + .. versionchanged:: 3.2 + *source_address* was added. + .. class:: HTTPResponse(sock, debuglevel=0, strict=0, method=None, url=None) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 79a49649932..ec8ff3d3b39 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -194,7 +194,7 @@ The module :mod:`socket` exports the following constants and functions: this platform. -.. function:: create_connection(address[, timeout]) +.. function:: create_connection(address[, timeout[, source_address]]) Convenience function. Connect to *address* (a 2-tuple ``(host, port)``), and return the socket object. Passing the optional *timeout* parameter will @@ -202,6 +202,13 @@ The module :mod:`socket` exports the following constants and functions: *timeout* is supplied, the global default timeout setting returned by :func:`getdefaulttimeout` is used. + If supplied, *source_address* must be a 2-tuple ``(host, port)`` for the + socket to bind to as its source address before connecting. If host or port + are '' or 0 respectively the OS default behavior will be used. + + .. versionchanged:: 3.2 + *source_address* was added. + .. function:: getaddrinfo(host, port[, family[, socktype[, proto[, flags]]]]) diff --git a/Lib/http/client.py b/Lib/http/client.py index 55819030593..d35f245d26c 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -634,8 +634,9 @@ class HTTPConnection: strict = 0 def __init__(self, host, port=None, strict=None, - timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None): self.timeout = timeout + self.source_address = source_address self.sock = None self._buffer = [] self.__response = None @@ -707,7 +708,7 @@ class HTTPConnection: def connect(self): """Connect to the host and port specified in __init__.""" self.sock = socket.create_connection((self.host,self.port), - self.timeout) + self.timeout, self.source_address) if self._tunnel_host: self._tunnel() @@ -1042,8 +1043,10 @@ else: default_port = HTTPS_PORT def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): - HTTPConnection.__init__(self, host, port, strict, timeout) + strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + super(HTTPSConnection, self).__init__(host, port, strict, timeout, + source_address) self.key_file = key_file self.cert_file = cert_file @@ -1051,7 +1054,7 @@ else: "Connect to a host on a given (SSL) port." sock = socket.create_connection((self.host, self.port), - self.timeout) + self.timeout, self.source_address) if self._tunnel_host: self.sock = sock diff --git a/Lib/socket.py b/Lib/socket.py index be019dbc51a..9133411c312 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -23,7 +23,8 @@ inet_aton() -- convert IP addr string (123.45.67.89) to 32-bit packed format inet_ntoa() -- convert 32-bit packed format IP to string (123.45.67.89) socket.getdefaulttimeout() -- get the default timeout value socket.setdefaulttimeout() -- set the default timeout value -create_connection() -- connects to an address, with an optional timeout +create_connection() -- connects to an address, with an optional timeout and + optional source address. [*] not available on all platforms! @@ -276,7 +277,8 @@ def getfqdn(name=''): _GLOBAL_DEFAULT_TIMEOUT = object() -def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT): +def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, + source_address=None): """Connect to *address* and return the socket object. Convenience function. Connect to *address* (a 2-tuple ``(host, @@ -284,7 +286,9 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT): *timeout* parameter will set the timeout on the socket instance before attempting to connect. If no *timeout* is supplied, the global default timeout setting returned by :func:`getdefaulttimeout` - is used. + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. """ msg = "getaddrinfo returns an empty list" @@ -296,6 +300,8 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT): sock = socket(af, socktype, proto) if timeout is not _GLOBAL_DEFAULT_TIMEOUT: sock.settimeout(timeout) + if source_address: + sock.bind(source_address) sock.connect(sa) return sock diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 37cda5d19be..705ceecb3d7 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -4,7 +4,8 @@ import io import array import socket -from unittest import TestCase +import unittest +TestCase = unittest.TestCase from test import support @@ -263,6 +264,38 @@ class OfflineTest(TestCase): def test_responses(self): self.assertEquals(client.responses[client.NOT_FOUND], "Not Found") + +class SourceAddressTest(TestCase): + def setUp(self): + self.serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.port = support.bind_port(self.serv) + self.source_port = support.find_unused_port() + self.serv.listen(5) + self.conn = None + + def tearDown(self): + if self.conn: + self.conn.close() + self.conn = None + self.serv.close() + self.serv = None + + def testHTTPConnectionSourceAddress(self): + self.conn = client.HTTPConnection(HOST, self.port, + source_address=('', self.source_port)) + self.conn.connect() + self.assertEqual(self.conn.sock.getsockname()[1], self.source_port) + + @unittest.skipIf(not hasattr(client, 'HTTPSConnection'), + 'http.client.HTTPSConnection not defined') + def testHTTPSConnectionSourceAddress(self): + self.conn = client.HTTPSConnection(HOST, self.port, + source_address=('', self.source_port)) + # We don't test anything here other the constructor not barfing as + # this code doesn't deal with setting up an active running SSL server + # for an ssl_wrapped connect() to actually return from. + + class TimeoutTest(TestCase): PORT = None @@ -390,7 +423,7 @@ class RequestBodyTest(TestCase): def test_main(verbose=None): support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, - HTTPSTimeoutTest, RequestBodyTest) + HTTPSTimeoutTest, RequestBodyTest, SourceAddressTest) if __name__ == '__main__': test_main() diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index db32335bb1e..62c2e253c8f 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -993,7 +993,7 @@ class NetworkConnectionAttributesTest(SocketTCPTest, ThreadableTest): ThreadableTest.__init__(self) def clientSetUp(self): - pass + self.source_port = support.find_unused_port() def clientTearDown(self): self.cli.close() @@ -1008,6 +1008,19 @@ class NetworkConnectionAttributesTest(SocketTCPTest, ThreadableTest): self.cli = socket.create_connection((HOST, self.port), timeout=30) self.assertEqual(self.cli.family, 2) + testSourcePort = _justAccept + def _testSourcePort(self): + self.cli = socket.create_connection((HOST, self.port), timeout=30, + source_address=('', self.source_port)) + self.assertEqual(self.cli.getsockname()[1], self.source_port) + + testSourceAddress = _justAccept + def _testSourceAddress(self): + self.cli = socket.create_connection( + (HOST, self.port), 30, ('127.0.0.1', self.source_port)) + self.assertEqual(self.cli.getsockname(), + ('127.0.0.1', self.source_port)) + testTimeoutDefault = _justAccept def _testTimeoutDefault(self): # passing no explicit timeout uses socket's global default diff --git a/Misc/NEWS b/Misc/NEWS index 81d01e4c477..451a2a06d4a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -191,6 +191,11 @@ C-API Library ------- +_ Issue #3972: http.client.HTTPConnection now accepts an optional source_address + parameter to allow specifying where your connections come from. + +- socket.create_connection now accepts an optional source_address parameter. + - Issue #5511: now zipfile.ZipFile can be used as a context manager. Initial patch by Brian Curtin.