diff --git a/botocore/args.py b/botocore/args.py index 741ca77886..96f7f73131 100644 --- a/botocore/args.py +++ b/botocore/args.py @@ -147,6 +147,7 @@ def get_client_args( socket_options=socket_options, client_cert=new_config.client_cert, proxies_config=new_config.proxies_config, + source_address=new_config.source_address, ) serializer = botocore.serialize.create_serializer( @@ -271,6 +272,7 @@ def compute_client_args( sigv4a_signing_region_set=( client_config.sigv4a_signing_region_set ), + source_address=client_config.source_address, ) self._compute_retry_config(config_kwargs) self._compute_connect_timeout(config_kwargs) diff --git a/botocore/config.py b/botocore/config.py index 587dc95ad8..cb03b14ee5 100644 --- a/botocore/config.py +++ b/botocore/config.py @@ -234,6 +234,13 @@ class Config: documentation. Invalid parameters or ones that are not used by the specified service will be ignored. + Defaults to None. + + :type source_address: tuple + :param source_address: A tuple with 2 values (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 behaviour will be used. + Defaults to None. """ @@ -264,6 +271,7 @@ class Config: ('disable_request_compression', None), ('client_context_params', None), ('sigv4a_signing_region_set', None), + ('source_address', None), ] ) diff --git a/botocore/endpoint.py b/botocore/endpoint.py index 1c2cee068b..ac08e5ef14 100644 --- a/botocore/endpoint.py +++ b/botocore/endpoint.py @@ -401,6 +401,7 @@ def create_endpoint( socket_options=None, client_cert=None, proxies_config=None, + source_address=None, ): if not is_valid_endpoint_url( endpoint_url @@ -420,6 +421,7 @@ def create_endpoint( socket_options=socket_options, client_cert=client_cert, proxies_config=proxies_config, + source_address=source_address, ) return Endpoint( diff --git a/botocore/httpsession.py b/botocore/httpsession.py index bd8b82fafa..11ed84228e 100644 --- a/botocore/httpsession.py +++ b/botocore/httpsession.py @@ -293,6 +293,7 @@ def __init__( socket_options=None, client_cert=None, proxies_config=None, + source_address=None, ): self._verify = verify self._proxy_config = ProxyConfiguration( @@ -319,6 +320,7 @@ def __init__( self._socket_options = socket_options if socket_options is None: self._socket_options = [] + self._source_address = source_address self._proxy_managers = {} self._manager = PoolManager(**self._get_pool_manager_kwargs()) self._manager.pool_classes_by_scheme = self._pool_classes_by_scheme @@ -342,6 +344,8 @@ def _get_pool_manager_kwargs(self, **extra_kwargs): 'cert_file': self._cert_file, 'key_file': self._key_file, } + if self._source_address: + pool_manager_kwargs['source_address'] = self._source_address pool_manager_kwargs.update(**extra_kwargs) return pool_manager_kwargs diff --git a/tests/unit/test_args.py b/tests/unit/test_args.py index 9338b05371..c410dacd8c 100644 --- a/tests/unit/test_args.py +++ b/tests/unit/test_args.py @@ -117,6 +117,7 @@ def assert_create_endpoint_call(self, mock_endpoint, **override_kwargs): 'proxies_config': None, 'socket_options': self.default_socket_options, 'client_cert': None, + 'source_address': None, } call_kwargs.update(**override_kwargs) mock_endpoint.return_value.create_endpoint.assert_called_with( diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py index 00790e4ecb..f959d8f2e7 100644 --- a/tests/unit/test_endpoint.py +++ b/tests/unit/test_endpoint.py @@ -488,3 +488,15 @@ def test_socket_options(self): ) session_args = self.mock_session.call_args[1] self.assertEqual(session_args.get('socket_options'), socket_options) + + def test_source_address(self): + source_address = ('192.168.1.1', 1234) + self.creator.create_endpoint( + self.service_model, + region_name='us-west-2', + endpoint_url='https://example.com', + http_session_cls=self.mock_session, + source_address=source_address, + ) + session_args = self.mock_session.call_args[1] + self.assertEqual(session_args.get('source_address'), source_address) diff --git a/tests/unit/test_http_session.py b/tests/unit/test_http_session.py index 8fb081e1ab..ca8f6d9905 100644 --- a/tests/unit/test_http_session.py +++ b/tests/unit/test_http_session.py @@ -447,6 +447,25 @@ def test_session_forwards_socket_options_to_proxy_manager(self): socket_options=socket_options, ) + def test_session_forwards_source_address_to_pool_manager(self): + source_address = ('192.168.1.1', 1234) + URLLib3Session(source_address=source_address) + self.assert_pool_manager_call(source_address=source_address) + + def test_session_forwards_source_address_to_proxy_manager(self): + proxies = {'http': 'http://proxy.com'} + source_address = ('192.168.1.1', 1234) + session = URLLib3Session( + proxies=proxies, + source_address=source_address, + ) + session.send(self.request.prepare()) + self.assert_proxy_manager_call( + proxies['http'], + proxy_headers={}, + source_address=source_address, + ) + def make_request_with_error(self, error): self.connection.urlopen.side_effect = error session = URLLib3Session()