Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate TLS-in-TLS support into urllib3 #1923

Merged
merged 13 commits into from Sep 28, 2020
50 changes: 50 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 generate_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 @@ -269,6 +275,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 @@ -333,8 +340,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._connection_requires_tls_in_tls():
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 @@ -394,6 +406,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 @@ -426,6 +439,43 @@ def connect(self):
or self.assert_fingerprint is not None
)

def set_tls_in_tls_required(self):
sethmlarson marked this conversation as resolved.
Show resolved Hide resolved
self.tls_in_tls_required = True

def _connection_requires_tls_in_tls(self):
"""
Indicates if the current connection requires two TLS connections, one to
the proxy and one to the destination server.
"""
return self.tls_in_tls_required

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 not ssl_context:
ssl_context = generate_proxy_ssl_context(
sethmlarson marked this conversation as resolved.
Show resolved Hide resolved
self.ssl_version,
self.cert_reqs,
self.ca_certs,
self.ca_cert_dir,
self.ca_cert_data,
)

return ssl_wrap_socket(
sock=conn,
keyfile=self.key_file,
certfile=self.cert_file,
key_password=self.key_password,
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
29 changes: 23 additions & 6 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 @@ -313,7 +319,7 @@ def _validate_conn(self, conn):
"""
pass

def _prepare_proxy(self, conn):
def _prepare_proxy(self, conn, destination_scheme):
sethmlarson marked this conversation as resolved.
Show resolved Hide resolved
# Nothing to do for HTTP connections.
pass

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 @@ -656,7 +666,9 @@ def urlopen(
# 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 connection_requires_http_tunnel(
self.proxy, self.proxy_config, destination_scheme
):
headers = headers.copy()
headers.update(self.proxy_headers)

Expand All @@ -683,7 +695,7 @@ def urlopen(
conn, "sock", None
)
if is_new_proxy_conn:
self._prepare_proxy(conn)
self._prepare_proxy(conn, destination_scheme)

# Make the request on the httplib connection object.
httplib_response = self._make_request(
Expand Down Expand Up @@ -942,17 +954,22 @@ def _prepare_conn(self, conn):
conn.ssl_version = self.ssl_version
return conn

def _prepare_proxy(self, conn):
def _prepare_proxy(self, conn, destination_scheme):
"""
Establishes a tunnel connection through HTTP CONNECT.

Tunnel connection is established early because otherwise httplib would
improperly set Host: header to proxy's IP:port.
"""

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

if self.proxy.scheme == "https":
conn.set_tls_in_tls_required()

conn.connect()

def _new_conn(self):
Expand Down