| import logging |
| import os |
| import os.path |
| import socket |
| import sys |
| import warnings |
| from base64 import b64encode |
| from concurrent.futures import CancelledError |
|
|
| from urllib3 import PoolManager, Timeout, proxy_from_url |
| from urllib3.exceptions import ( |
| ConnectTimeoutError as URLLib3ConnectTimeoutError, |
| ) |
| from urllib3.exceptions import ( |
| LocationParseError, |
| NewConnectionError, |
| ProtocolError, |
| ProxyError, |
| ) |
| from urllib3.exceptions import ReadTimeoutError as URLLib3ReadTimeoutError |
| from urllib3.exceptions import SSLError as URLLib3SSLError |
| from urllib3.util.retry import Retry |
| from urllib3.util.ssl_ import ( |
| OP_NO_COMPRESSION, |
| PROTOCOL_TLS, |
| OP_NO_SSLv2, |
| OP_NO_SSLv3, |
| is_ipaddress, |
| ssl, |
| ) |
| from urllib3.util.url import parse_url |
|
|
| try: |
| from urllib3.util.ssl_ import OP_NO_TICKET, PROTOCOL_TLS_CLIENT |
| except ImportError: |
| |
| |
| from ssl import OP_NO_TICKET, PROTOCOL_TLS_CLIENT |
|
|
| try: |
| |
| |
| with warnings.catch_warnings(): |
| warnings.simplefilter("ignore", category=DeprecationWarning) |
| |
| from urllib3.contrib.pyopenssl import ( |
| orig_util_SSLContext as SSLContext, |
| ) |
| except (AttributeError, ImportError): |
| from urllib3.util.ssl_ import SSLContext |
|
|
| try: |
| from urllib3.util.ssl_ import DEFAULT_CIPHERS |
| except ImportError: |
| |
| |
| |
| DEFAULT_CIPHERS = None |
|
|
| import botocore.awsrequest |
| from botocore.compat import ( |
| IPV6_ADDRZ_RE, |
| ensure_bytes, |
| filter_ssl_warnings, |
| unquote, |
| urlparse, |
| ) |
| from botocore.exceptions import ( |
| ConnectionClosedError, |
| ConnectTimeoutError, |
| EndpointConnectionError, |
| HTTPClientError, |
| InvalidProxiesConfigError, |
| ProxyConnectionError, |
| ReadTimeoutError, |
| SSLError, |
| ) |
|
|
| filter_ssl_warnings() |
| logger = logging.getLogger(__name__) |
| DEFAULT_TIMEOUT = 60 |
| MAX_POOL_CONNECTIONS = 10 |
| DEFAULT_CA_BUNDLE = os.path.join(os.path.dirname(__file__), 'cacert.pem') |
|
|
| try: |
| from certifi import where |
| except ImportError: |
|
|
| def where(): |
| return DEFAULT_CA_BUNDLE |
|
|
|
|
| def get_cert_path(verify): |
| if verify is not True: |
| return verify |
|
|
| cert_path = where() |
| logger.debug("Certificate path: %s", cert_path) |
|
|
| return cert_path |
|
|
|
|
| def create_urllib3_context( |
| ssl_version=None, cert_reqs=None, options=None, ciphers=None |
| ): |
| """This function is a vendored version of the same function in urllib3 |
| |
| We vendor this function to ensure that the SSL contexts we construct |
| always use the std lib SSLContext instead of pyopenssl. |
| """ |
| |
| if not ssl_version or ssl_version == PROTOCOL_TLS: |
| ssl_version = PROTOCOL_TLS_CLIENT |
|
|
| context = SSLContext(ssl_version) |
|
|
| if ciphers: |
| context.set_ciphers(ciphers) |
| elif DEFAULT_CIPHERS: |
| context.set_ciphers(DEFAULT_CIPHERS) |
|
|
| |
| cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs |
|
|
| if options is None: |
| options = 0 |
| |
| options |= OP_NO_SSLv2 |
| |
| options |= OP_NO_SSLv3 |
| |
| |
| options |= OP_NO_COMPRESSION |
| |
| |
| |
| |
| options |= OP_NO_TICKET |
|
|
| context.options |= options |
|
|
| |
| |
| |
| |
| |
| |
| if ( |
| cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4) |
| ) and getattr(context, "post_handshake_auth", None) is not None: |
| context.post_handshake_auth = True |
|
|
| def disable_check_hostname(): |
| if ( |
| getattr(context, "check_hostname", None) is not None |
| ): |
| |
| |
| context.check_hostname = False |
|
|
| |
| |
| |
| |
| |
| if cert_reqs == ssl.CERT_REQUIRED: |
| context.verify_mode = cert_reqs |
| disable_check_hostname() |
| else: |
| disable_check_hostname() |
| context.verify_mode = cert_reqs |
|
|
| |
| |
| if hasattr(context, "keylog_filename"): |
| sslkeylogfile = os.environ.get("SSLKEYLOGFILE") |
| if sslkeylogfile and not sys.flags.ignore_environment: |
| context.keylog_filename = sslkeylogfile |
|
|
| return context |
|
|
|
|
| def ensure_boolean(val): |
| """Ensures a boolean value if a string or boolean is provided |
| |
| For strings, the value for True/False is case insensitive |
| """ |
| if isinstance(val, bool): |
| return val |
| else: |
| return val.lower() == 'true' |
|
|
|
|
| def mask_proxy_url(proxy_url): |
| """ |
| Mask proxy url credentials. |
| |
| :type proxy_url: str |
| :param proxy_url: The proxy url, i.e. https://username:password@proxy.com |
| |
| :return: Masked proxy url, i.e. https://***:***@proxy.com |
| """ |
| mask = '*' * 3 |
| parsed_url = urlparse(proxy_url) |
| if parsed_url.username: |
| proxy_url = proxy_url.replace(parsed_url.username, mask, 1) |
| if parsed_url.password: |
| proxy_url = proxy_url.replace(parsed_url.password, mask, 1) |
| return proxy_url |
|
|
|
|
| def _is_ipaddress(host): |
| """Wrap urllib3's is_ipaddress to support bracketed IPv6 addresses.""" |
| return is_ipaddress(host) or bool(IPV6_ADDRZ_RE.match(host)) |
|
|
|
|
| class ProxyConfiguration: |
| """Represents a proxy configuration dictionary and additional settings. |
| |
| This class represents a proxy configuration dictionary and provides utility |
| functions to retrieve well structured proxy urls and proxy headers from the |
| proxy configuration dictionary. |
| """ |
|
|
| def __init__(self, proxies=None, proxies_settings=None): |
| if proxies is None: |
| proxies = {} |
| if proxies_settings is None: |
| proxies_settings = {} |
|
|
| self._proxies = proxies |
| self._proxies_settings = proxies_settings |
|
|
| def proxy_url_for(self, url): |
| """Retrieves the corresponding proxy url for a given url.""" |
| parsed_url = urlparse(url) |
| proxy = self._proxies.get(parsed_url.scheme) |
| if proxy: |
| proxy = self._fix_proxy_url(proxy) |
| return proxy |
|
|
| def proxy_headers_for(self, proxy_url): |
| """Retrieves the corresponding proxy headers for a given proxy url.""" |
| headers = {} |
| username, password = self._get_auth_from_url(proxy_url) |
| if username and password: |
| basic_auth = self._construct_basic_auth(username, password) |
| headers['Proxy-Authorization'] = basic_auth |
| return headers |
|
|
| @property |
| def settings(self): |
| return self._proxies_settings |
|
|
| def _fix_proxy_url(self, proxy_url): |
| if proxy_url.startswith('http:') or proxy_url.startswith('https:'): |
| return proxy_url |
| elif proxy_url.startswith('//'): |
| return 'http:' + proxy_url |
| else: |
| return 'http://' + proxy_url |
|
|
| def _construct_basic_auth(self, username, password): |
| auth_str = f'{username}:{password}' |
| encoded_str = b64encode(auth_str.encode('ascii')).strip().decode() |
| return f'Basic {encoded_str}' |
|
|
| def _get_auth_from_url(self, url): |
| parsed_url = urlparse(url) |
| try: |
| return unquote(parsed_url.username), unquote(parsed_url.password) |
| except (AttributeError, TypeError): |
| return None, None |
|
|
|
|
| class URLLib3Session: |
| """A basic HTTP client that supports connection pooling and proxies. |
| |
| This class is inspired by requests.adapters.HTTPAdapter, but has been |
| boiled down to meet the use cases needed by botocore. For the most part |
| this classes matches the functionality of HTTPAdapter in requests v2.7.0 |
| (the same as our vendored version). The only major difference of note is |
| that we currently do not support sending chunked requests. While requests |
| v2.7.0 implemented this themselves, later version urllib3 support this |
| directly via a flag to urlopen so enabling it if needed should be trivial. |
| """ |
|
|
| def __init__( |
| self, |
| verify=True, |
| proxies=None, |
| timeout=None, |
| max_pool_connections=MAX_POOL_CONNECTIONS, |
| socket_options=None, |
| client_cert=None, |
| proxies_config=None, |
| ): |
| self._verify = verify |
| self._proxy_config = ProxyConfiguration( |
| proxies=proxies, proxies_settings=proxies_config |
| ) |
| self._pool_classes_by_scheme = { |
| 'http': botocore.awsrequest.AWSHTTPConnectionPool, |
| 'https': botocore.awsrequest.AWSHTTPSConnectionPool, |
| } |
| if timeout is None: |
| timeout = DEFAULT_TIMEOUT |
| if not isinstance(timeout, (int, float)): |
| timeout = Timeout(connect=timeout[0], read=timeout[1]) |
|
|
| self._cert_file = None |
| self._key_file = None |
| if isinstance(client_cert, str): |
| self._cert_file = client_cert |
| elif isinstance(client_cert, tuple): |
| self._cert_file, self._key_file = client_cert |
|
|
| self._timeout = timeout |
| self._max_pool_connections = max_pool_connections |
| self._socket_options = socket_options |
| if socket_options is None: |
| self._socket_options = [] |
| self._proxy_managers = {} |
| self._manager = PoolManager(**self._get_pool_manager_kwargs()) |
| self._manager.pool_classes_by_scheme = self._pool_classes_by_scheme |
|
|
| def _proxies_kwargs(self, **kwargs): |
| proxies_settings = self._proxy_config.settings |
| proxies_kwargs = { |
| 'use_forwarding_for_https': proxies_settings.get( |
| 'proxy_use_forwarding_for_https' |
| ), |
| **kwargs, |
| } |
| return {k: v for k, v in proxies_kwargs.items() if v is not None} |
|
|
| def _get_pool_manager_kwargs(self, **extra_kwargs): |
| pool_manager_kwargs = { |
| 'timeout': self._timeout, |
| 'maxsize': self._max_pool_connections, |
| 'ssl_context': self._get_ssl_context(), |
| 'socket_options': self._socket_options, |
| 'cert_file': self._cert_file, |
| 'key_file': self._key_file, |
| } |
| pool_manager_kwargs.update(**extra_kwargs) |
| return pool_manager_kwargs |
|
|
| def _get_ssl_context(self): |
| return create_urllib3_context() |
|
|
| def _get_proxy_manager(self, proxy_url): |
| if proxy_url not in self._proxy_managers: |
| proxy_headers = self._proxy_config.proxy_headers_for(proxy_url) |
| proxy_ssl_context = self._setup_proxy_ssl_context(proxy_url) |
| proxy_manager_kwargs = self._get_pool_manager_kwargs( |
| proxy_headers=proxy_headers |
| ) |
| proxy_manager_kwargs.update( |
| self._proxies_kwargs(proxy_ssl_context=proxy_ssl_context) |
| ) |
| proxy_manager = proxy_from_url(proxy_url, **proxy_manager_kwargs) |
| proxy_manager.pool_classes_by_scheme = self._pool_classes_by_scheme |
| self._proxy_managers[proxy_url] = proxy_manager |
|
|
| return self._proxy_managers[proxy_url] |
|
|
| def _path_url(self, url): |
| parsed_url = urlparse(url) |
| path = parsed_url.path |
| if not path: |
| path = '/' |
| if parsed_url.query: |
| path = path + '?' + parsed_url.query |
| return path |
|
|
| def _setup_ssl_cert(self, conn, url, verify): |
| if url.lower().startswith('https') and verify: |
| conn.cert_reqs = 'CERT_REQUIRED' |
| conn.ca_certs = get_cert_path(verify) |
| else: |
| conn.cert_reqs = 'CERT_NONE' |
| conn.ca_certs = None |
|
|
| def _setup_proxy_ssl_context(self, proxy_url): |
| proxies_settings = self._proxy_config.settings |
| proxy_ca_bundle = proxies_settings.get('proxy_ca_bundle') |
| proxy_cert = proxies_settings.get('proxy_client_cert') |
| if proxy_ca_bundle is None and proxy_cert is None: |
| return None |
|
|
| context = self._get_ssl_context() |
| try: |
| url = parse_url(proxy_url) |
| |
| |
| if not _is_ipaddress(url.host): |
| context.check_hostname = True |
| if proxy_ca_bundle is not None: |
| context.load_verify_locations(cafile=proxy_ca_bundle) |
|
|
| if isinstance(proxy_cert, tuple): |
| context.load_cert_chain(proxy_cert[0], keyfile=proxy_cert[1]) |
| elif isinstance(proxy_cert, str): |
| context.load_cert_chain(proxy_cert) |
|
|
| return context |
| except (OSError, URLLib3SSLError, LocationParseError) as e: |
| raise InvalidProxiesConfigError(error=e) |
|
|
| def _get_connection_manager(self, url, proxy_url=None): |
| if proxy_url: |
| manager = self._get_proxy_manager(proxy_url) |
| else: |
| manager = self._manager |
| return manager |
|
|
| def _get_request_target(self, url, proxy_url): |
| has_proxy = proxy_url is not None |
|
|
| if not has_proxy: |
| return self._path_url(url) |
|
|
| |
| |
| |
| proxy_scheme = urlparse(proxy_url).scheme |
| using_https_forwarding_proxy = ( |
| proxy_scheme == 'https' |
| and self._proxies_kwargs().get('use_forwarding_for_https', False) |
| ) |
|
|
| if using_https_forwarding_proxy or url.startswith('http:'): |
| return url |
| else: |
| return self._path_url(url) |
|
|
| def _chunked(self, headers): |
| transfer_encoding = headers.get('Transfer-Encoding', b'') |
| transfer_encoding = ensure_bytes(transfer_encoding) |
| return transfer_encoding.lower() == b'chunked' |
|
|
| def close(self): |
| self._manager.clear() |
| for manager in self._proxy_managers.values(): |
| manager.clear() |
|
|
| def send(self, request): |
| try: |
| proxy_url = self._proxy_config.proxy_url_for(request.url) |
| manager = self._get_connection_manager(request.url, proxy_url) |
| conn = manager.connection_from_url(request.url) |
| self._setup_ssl_cert(conn, request.url, self._verify) |
| if ensure_boolean( |
| os.environ.get('BOTO_EXPERIMENTAL__ADD_PROXY_HOST_HEADER', '') |
| ): |
| |
| |
| |
| |
| host = urlparse(request.url).hostname |
| conn.proxy_headers['host'] = host |
|
|
| request_target = self._get_request_target(request.url, proxy_url) |
| urllib_response = conn.urlopen( |
| method=request.method, |
| url=request_target, |
| body=request.body, |
| headers=request.headers, |
| retries=Retry(False), |
| assert_same_host=False, |
| preload_content=False, |
| decode_content=False, |
| chunked=self._chunked(request.headers), |
| ) |
|
|
| http_response = botocore.awsrequest.AWSResponse( |
| request.url, |
| urllib_response.status, |
| urllib_response.headers, |
| urllib_response, |
| ) |
|
|
| if not request.stream_output: |
| |
| |
| |
| http_response.content |
|
|
| return http_response |
| except URLLib3SSLError as e: |
| raise SSLError(endpoint_url=request.url, error=e) |
| except (NewConnectionError, socket.gaierror) as e: |
| raise EndpointConnectionError(endpoint_url=request.url, error=e) |
| except ProxyError as e: |
| raise ProxyConnectionError( |
| proxy_url=mask_proxy_url(proxy_url), error=e |
| ) |
| except URLLib3ConnectTimeoutError as e: |
| raise ConnectTimeoutError(endpoint_url=request.url, error=e) |
| except URLLib3ReadTimeoutError as e: |
| raise ReadTimeoutError(endpoint_url=request.url, error=e) |
| except ProtocolError as e: |
| raise ConnectionClosedError( |
| error=e, request=request, endpoint_url=request.url |
| ) |
| except CancelledError: |
| raise |
| except Exception as e: |
| message = 'Exception received when sending urllib3 HTTP request' |
| logger.debug(message, exc_info=True) |
| raise HTTPClientError(error=e) |
|
|