Skip to content

Commit

Permalink
Integrate TLS-in-TLS support into urllib3 (urllib3#1923)
Browse files Browse the repository at this point in the history
  • Loading branch information
jalopezsilva committed Sep 28, 2020
1 parent 831abda commit 79ec81d
Show file tree
Hide file tree
Showing 12 changed files with 405 additions and 66 deletions.
47 changes: 47 additions & 0 deletions src/urllib3/connection.py
Expand Up @@ -9,6 +9,7 @@
from .packages import six
from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection
from .packages.six.moves.http_client import HTTPException # noqa: F401
from .util.proxy import create_proxy_ssl_context

try: # Compiled with SSL?
import ssl
Expand Down Expand Up @@ -117,6 +118,11 @@ def __init__(self, *args, **kw):
#: The socket options provided by the user. If no options are
#: provided, we use the default options.
self.socket_options = kw.pop("socket_options", self.default_socket_options)

# Proxy options provided by the user.
self.proxy = kw.pop("proxy", None)
self.proxy_config = kw.pop("proxy_config", None)

_HTTPConnection.__init__(self, *args, **kw)

@property
Expand Down Expand Up @@ -271,6 +277,7 @@ class HTTPSConnection(HTTPConnection):
ca_cert_data = None
ssl_version = None
assert_fingerprint = None
tls_in_tls_required = False

def __init__(
self,
Expand Down Expand Up @@ -335,8 +342,13 @@ def connect(self):
# Add certificate verification
conn = self._new_conn()
hostname = self.host
tls_in_tls = False

if self._is_using_tunnel():
if self.tls_in_tls_required:
conn = self._connect_tls_proxy(hostname, conn)
tls_in_tls = True

self.sock = conn

# Calls self._set_hostport(), so self.host is
Expand Down Expand Up @@ -396,6 +408,7 @@ def connect(self):
ca_cert_data=self.ca_cert_data,
server_hostname=server_hostname,
ssl_context=context,
tls_in_tls=tls_in_tls,
)

if self.assert_fingerprint:
Expand Down Expand Up @@ -428,6 +441,40 @@ def connect(self):
or self.assert_fingerprint is not None
)

def _connect_tls_proxy(self, hostname, conn):
"""
Establish a TLS connection to the proxy using the provided SSL context.
"""
proxy_config = self.proxy_config
ssl_context = proxy_config.ssl_context
if ssl_context:
# If the user provided a proxy context, we assume CA and client
# certificates have already been set
return ssl_wrap_socket(
sock=conn,
server_hostname=hostname,
ssl_context=ssl_context,
)

ssl_context = create_proxy_ssl_context(
self.ssl_version,
self.cert_reqs,
self.ca_certs,
self.ca_cert_dir,
self.ca_cert_data,
)

# If no cert was provided, use only the default options for server
# certificate validation
return ssl_wrap_socket(
sock=conn,
ca_certs=self.ca_certs,
ca_cert_dir=self.ca_cert_dir,
ca_cert_data=self.ca_cert_data,
server_hostname=hostname,
ssl_context=ssl_context,
)


def _match_hostname(cert, asserted_hostname):
try:
Expand Down
26 changes: 21 additions & 5 deletions src/urllib3/connectionpool.py
Expand Up @@ -40,6 +40,7 @@
from .response import HTTPResponse

from .util.connection import is_connection_dropped
from .util.proxy import connection_requires_http_tunnel
from .util.request import set_file_position
from .util.response import assert_header_parsing
from .util.retry import Retry
Expand Down Expand Up @@ -182,6 +183,7 @@ def __init__(
retries=None,
_proxy=None,
_proxy_headers=None,
_proxy_config=None,
**conn_kw
):
ConnectionPool.__init__(self, host, port)
Expand All @@ -203,6 +205,7 @@ def __init__(

self.proxy = _proxy
self.proxy_headers = _proxy_headers or {}
self.proxy_config = _proxy_config

# Fill the queue up so that doing get() on it will block properly
for _ in xrange(maxsize):
Expand All @@ -219,6 +222,9 @@ def __init__(
# list.
self.conn_kw.setdefault("socket_options", [])

self.conn_kw["proxy"] = self.proxy
self.conn_kw["proxy_config"] = self.proxy_config

def _new_conn(self):
"""
Return a fresh :class:`HTTPConnection`.
Expand Down Expand Up @@ -621,6 +627,10 @@ def urlopen(
Additional parameters are passed to
:meth:`urllib3.response.HTTPResponse.from_httplib`
"""

parsed_url = parse_url(url)
destination_scheme = parsed_url.scheme

if headers is None:
headers = self.headers

Expand All @@ -638,7 +648,7 @@ def urlopen(
if url.startswith("/"):
url = six.ensure_str(_encode_target(url))
else:
url = six.ensure_str(parse_url(url).url)
url = six.ensure_str(parsed_url.url)

conn = None

Expand All @@ -653,10 +663,14 @@ def urlopen(
# [1] <https://github.com/urllib3/urllib3/issues/651>
release_this_conn = release_conn

http_tunnel_required = connection_requires_http_tunnel(
self.proxy, self.proxy_config, destination_scheme
)

# Merge the proxy headers. Only done when not using HTTP CONNECT. We
# have to copy the headers dict so we can safely change it without those
# changes being reflected in anyone else's copy.
if self.scheme == "http" or (self.proxy and self.proxy.scheme == "https"):
if not http_tunnel_required:
headers = headers.copy()
headers.update(self.proxy_headers)

Expand All @@ -682,7 +696,7 @@ def urlopen(
is_new_proxy_conn = self.proxy is not None and not getattr(
conn, "sock", None
)
if is_new_proxy_conn:
if is_new_proxy_conn and http_tunnel_required:
self._prepare_proxy(conn)

# Make the request on the httplib connection object.
Expand Down Expand Up @@ -946,8 +960,10 @@ def _prepare_proxy(self, conn):
improperly set Host: header to proxy's IP:port.
"""

if self.proxy.scheme != "https":
conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers)
conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers)

if self.proxy.scheme == "https":
conn.tls_in_tls_required = True

conn.connect()

Expand Down
96 changes: 51 additions & 45 deletions src/urllib3/poolmanager.py
Expand Up @@ -2,14 +2,12 @@
import collections
import functools
import logging
import warnings

from ._collections import RecentlyUsedContainer
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from .connectionpool import port_by_scheme

from .exceptions import (
HTTPWarning,
LocationValueError,
MaxRetryError,
ProxySchemeUnknown,
Expand All @@ -21,17 +19,13 @@
from .request import RequestMethods
from .util.url import parse_url
from .util.retry import Retry
from .util.proxy import connection_requires_http_tunnel
from .packages.six import PY3


__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"]


class InvalidProxyConfigurationWarning(HTTPWarning):
"""Raised when a user has an HTTPS proxy without enabling HTTPS proxies."""

pass


log = logging.getLogger(__name__)

SSL_KEYWORDS = (
Expand Down Expand Up @@ -68,6 +62,7 @@ class InvalidProxyConfigurationWarning(HTTPWarning):
"key_headers", # dict
"key__proxy", # parsed proxy url
"key__proxy_headers", # dict
"key__proxy_config", # class
"key_socket_options", # list of (level (int), optname (int), value (int or str)) tuples
"key__socks_options", # dict
"key_assert_hostname", # bool or string
Expand All @@ -79,6 +74,9 @@ class InvalidProxyConfigurationWarning(HTTPWarning):
#: All custom key schemes should include the fields in this key at a minimum.
PoolKey = collections.namedtuple("PoolKey", _key_fields)

_proxy_config_fields = ("ssl_context", "use_forwarding_for_https")
ProxyConfig = collections.namedtuple("ProxyConfig", _proxy_config_fields)


def _default_key_normalizer(key_class, request_context):
"""
Expand Down Expand Up @@ -170,6 +168,7 @@ class PoolManager(RequestMethods):
"""

proxy = None
proxy_config = None

def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
RequestMethods.__init__(self, headers)
Expand Down Expand Up @@ -326,14 +325,32 @@ def _merge_pool_kwargs(self, override):
def _proxy_requires_url_absolute_form(self, parsed_url):
"""
Indicates if the proxy requires the complete destination URL in the
request.
Normally this is only needed when not using an HTTP CONNECT tunnel.
request. Normally this is only needed when not using an HTTP CONNECT
tunnel.
"""
if self.proxy is None:
return False

return parsed_url.scheme == "http" or self.proxy.scheme == "https"
return not connection_requires_http_tunnel(
self.proxy, self.proxy_config, parsed_url.scheme
)

def _validate_proxy_scheme_url_selection(self, url_scheme):
"""
Validates that were not attempting to do TLS in TLS connections on
Python2 or with unsupported SSL implementations.
"""
if self.proxy is None or url_scheme != "https":
return

if self.proxy.scheme != "https":
return

if not PY3 and not self.proxy_config.use_forwarding_for_https:
raise ProxySchemeUnsupported(
"Contacting HTTPS destinations through HTTPS proxies "
"'via CONNECT tunnels' is not supported in Python 2"
)

def urlopen(self, method, url, redirect=True, **kw):
"""
Expand All @@ -345,6 +362,8 @@ def urlopen(self, method, url, redirect=True, **kw):
:class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
"""
u = parse_url(url)
self._validate_proxy_scheme_url_selection(u.scheme)

conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)

kw["assert_same_host"] = False
Expand Down Expand Up @@ -415,11 +434,18 @@ class ProxyManager(PoolManager):
HTTPS/CONNECT case they are sent only once. Could be used for proxy
authentication.
:param _allow_https_proxy_to_see_traffic:
Allows forwarding of HTTPS requests to HTTPS proxies. The proxy will
have visibility of all the traffic sent. ONLY USE IF YOU KNOW WHAT
YOU'RE DOING. This flag might be removed at any time in any future
update.
:param proxy_ssl_context:
The proxy SSL context is used to establish the TLS connection to the
proxy when using HTTPS proxies.
:param use_forwarding_for_https:
(Defaults to False) If set to True will forward requests to the HTTPS
proxy to be made on behalf of the client instead of creating a TLS
tunnel via the CONNECT method. **Enabling this flag means that request
and response headers and content will be visible from the HTTPS proxy**
whereas tunneling keeps request and response headers and content
private. IP address, target hostname, SNI, and port are always visible
to an HTTPS proxy even when this flag is disabled.
Example:
>>> proxy = urllib3.ProxyManager('http://localhost:3128/')
Expand All @@ -440,7 +466,8 @@ def __init__(
num_pools=10,
headers=None,
proxy_headers=None,
_allow_https_proxy_to_see_traffic=False,
proxy_ssl_context=None,
use_forwarding_for_https=False,
**connection_pool_kw
):

Expand All @@ -461,11 +488,12 @@ def __init__(

self.proxy = proxy
self.proxy_headers = proxy_headers or {}
self.proxy_ssl_context = proxy_ssl_context
self.proxy_config = ProxyConfig(proxy_ssl_context, use_forwarding_for_https)

connection_pool_kw["_proxy"] = self.proxy
connection_pool_kw["_proxy_headers"] = self.proxy_headers

self.allow_insecure_proxy = _allow_https_proxy_to_see_traffic
connection_pool_kw["_proxy_config"] = self.proxy_config

super(ProxyManager, self).__init__(num_pools, headers, **connection_pool_kw)

Expand Down Expand Up @@ -494,35 +522,13 @@ def _set_proxy_headers(self, url, headers=None):
headers_.update(headers)
return headers_

def _validate_proxy_scheme_url_selection(self, url_scheme):
if (
url_scheme == "https"
and self.proxy.scheme == "https"
and not self.allow_insecure_proxy
):
warnings.warn(
"Your proxy configuration specified an HTTPS scheme for the proxy. "
"Are you sure you want to use HTTPS to contact the proxy? "
"This most likely indicates an error in your configuration."
"If you are sure you want use HTTPS to contact the proxy, enable "
"the _allow_https_proxy_to_see_traffic.",
InvalidProxyConfigurationWarning,
)

raise ProxySchemeUnsupported(
"Contacting HTTPS destinations through HTTPS proxies is not supported."
)

def urlopen(self, method, url, redirect=True, **kw):
"Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute."
u = parse_url(url)
self._validate_proxy_scheme_url_selection(u.scheme)

if u.scheme == "http" or self.proxy.scheme == "https":
if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme):
# For connections using HTTP CONNECT, httplib sets the necessary
# headers on the CONNECT to the proxy. For HTTP or when talking
# HTTPS to the proxy, we'll definitely need to set 'Host' at the
# very least.
# headers on the CONNECT to the proxy. If we're not using CONNECT,
# we'll definitely need to set 'Host' at the very least.
headers = kw.get("headers", self.headers)
kw["headers"] = self._set_proxy_headers(url, headers)

Expand Down

0 comments on commit 79ec81d

Please sign in to comment.