Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1f14923

Browse files
committed
Integrate SSLTransport into urllib3.
For connections that will attempt to use an HTTPS proxy with an HTTPS destination, we'll use the TLS in TLS support provided by SSL Transport. HTTPS proxy and HTTP destinations will continue using a single TLS session as expected. HTTPS proxies and HTTPS destinations can continue to be insecure if indicated by the allow_insecure_proxies parameter.
1 parent 7667053 commit 1f14923

9 files changed

Lines changed: 244 additions & 62 deletions

File tree

src/urllib3/connection.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def __init__(self, *args, **kw):
111111
#: The socket options provided by the user. If no options are
112112
#: provided, we use the default options.
113113
self.socket_options = kw.pop("socket_options", self.default_socket_options)
114+
115+
# Proxy options provided by the user.
116+
self.proxy = kw.pop("proxy", None)
117+
self.proxy_config = kw.pop("proxy_config", None)
118+
114119
_HTTPConnection.__init__(self, *args, **kw)
115120

116121
@property
@@ -309,8 +314,13 @@ def connect(self):
309314
# Add certificate verification
310315
conn = self._new_conn()
311316
hostname = self.host
317+
tls_in_tls = False
312318

313319
if self._is_using_tunnel():
320+
if self._connection_requires_tls_in_tls():
321+
conn = self._connect_tls_proxy(hostname, conn)
322+
tls_in_tls = True
323+
314324
self.sock = conn
315325

316326
# Calls self._set_hostport(), so self.host is
@@ -370,6 +380,7 @@ def connect(self):
370380
ca_cert_data=self.ca_cert_data,
371381
server_hostname=server_hostname,
372382
ssl_context=context,
383+
tls_in_tls=tls_in_tls,
373384
)
374385

375386
if self.assert_fingerprint:
@@ -402,6 +413,56 @@ def connect(self):
402413
or self.assert_fingerprint is not None
403414
)
404415

416+
def _connection_requires_tls_in_tls(self):
417+
"""
418+
Indicates if the current connection requires two TLS connections, one to
419+
the proxy and one to the destination server.
420+
"""
421+
# Not supported in < py3.
422+
if not six.PY3:
423+
return False
424+
425+
if not self.proxy or not self.proxy_config:
426+
return False
427+
428+
if self.proxy_config.destination_scheme == "http":
429+
return False
430+
431+
return (
432+
self.proxy.scheme == "https" and not self.proxy_config.allow_insecure_proxy
433+
)
434+
435+
def _connect_tls_proxy(self, hostname, conn):
436+
"""
437+
Establish a TLS connection to the proxy using the provided SSL context.
438+
"""
439+
proxy_config = self.proxy_config
440+
ssl_context = proxy_config.ssl_context
441+
if not ssl_context:
442+
ssl_context = create_urllib3_context(
443+
ssl_version=resolve_ssl_version(self.ssl_version),
444+
cert_reqs=resolve_cert_reqs(self.cert_reqs),
445+
)
446+
if (
447+
not self.ca_certs
448+
and not self.ca_cert_dir
449+
and not self.ca_cert_data
450+
and hasattr(ssl_context, "load_default_certs")
451+
):
452+
ssl_context.load_default_certs()
453+
454+
return ssl_wrap_socket(
455+
sock=conn,
456+
keyfile=self.key_file,
457+
certfile=self.cert_file,
458+
key_password=self.key_password,
459+
ca_certs=self.ca_certs,
460+
ca_cert_dir=self.ca_cert_dir,
461+
ca_cert_data=self.ca_cert_data,
462+
server_hostname=hostname,
463+
ssl_context=ssl_context,
464+
)
465+
405466

406467
def _match_hostname(cert, asserted_hostname):
407468
try:

src/urllib3/connectionpool.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from .request import RequestMethods
3939
from .response import HTTPResponse
4040

41-
from .util.connection import is_connection_dropped
41+
from .util.connection import is_connection_dropped, connection_requires_http_tunnel
4242
from .util.request import set_file_position
4343
from .util.response import assert_header_parsing
4444
from .util.retry import Retry
@@ -181,6 +181,7 @@ def __init__(
181181
retries=None,
182182
_proxy=None,
183183
_proxy_headers=None,
184+
_proxy_config=None,
184185
**conn_kw
185186
):
186187
ConnectionPool.__init__(self, host, port)
@@ -202,6 +203,7 @@ def __init__(
202203

203204
self.proxy = _proxy
204205
self.proxy_headers = _proxy_headers or {}
206+
self.proxy_config = _proxy_config
205207

206208
# Fill the queue up so that doing get() on it will block properly
207209
for _ in xrange(maxsize):
@@ -218,6 +220,9 @@ def __init__(
218220
# list.
219221
self.conn_kw.setdefault("socket_options", [])
220222

223+
self.conn_kw["proxy"] = self.proxy
224+
self.conn_kw["proxy_config"] = self.proxy_config
225+
221226
def _new_conn(self):
222227
"""
223228
Return a fresh :class:`HTTPConnection`.
@@ -637,7 +642,7 @@ def urlopen(
637642
# Merge the proxy headers. Only done when not using HTTP CONNECT. We
638643
# have to copy the headers dict so we can safely change it without those
639644
# changes being reflected in anyone else's copy.
640-
if self.scheme == "http" or (self.proxy and self.proxy.scheme == "https"):
645+
if not connection_requires_http_tunnel(self.proxy, self.proxy_config):
641646
headers = headers.copy()
642647
headers.update(self.proxy_headers)
643648

@@ -952,7 +957,7 @@ def _prepare_proxy(self, conn):
952957
improperly set Host: header to proxy's IP:port.
953958
"""
954959

955-
if self.proxy.scheme != "https":
960+
if connection_requires_http_tunnel(self.proxy, self.proxy_config):
956961
conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers)
957962

958963
conn.connect()

src/urllib3/poolmanager.py

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,13 @@
1919
from .request import RequestMethods
2020
from .util.url import parse_url
2121
from .util.retry import Retry
22+
from .util.connection import connection_requires_http_tunnel
23+
from .packages.six import PY3
2224

2325

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

2628

27-
class InvalidProxyConfigurationWarning(HTTPWarning):
28-
"""Raised when a user has an HTTPS proxy without enabling HTTPS proxies."""
29-
30-
pass
31-
32-
3329
log = logging.getLogger(__name__)
3430

3531
SSL_KEYWORDS = (
@@ -66,6 +62,7 @@ class InvalidProxyConfigurationWarning(HTTPWarning):
6662
"key_headers", # dict
6763
"key__proxy", # parsed proxy url
6864
"key__proxy_headers", # dict
65+
"key__proxy_config", # class
6966
"key_socket_options", # list of (level (int), optname (int), value (int or str)) tuples
7067
"key__socks_options", # dict
7168
"key_assert_hostname", # bool or string
@@ -76,6 +73,18 @@ class InvalidProxyConfigurationWarning(HTTPWarning):
7673
#: The namedtuple class used to construct keys for the connection pool.
7774
#: All custom key schemes should include the fields in this key at a minimum.
7875
PoolKey = collections.namedtuple("PoolKey", _key_fields)
76+
_proxy_config_fields = ("ssl_context", "allow_insecure_proxy", "destination_scheme")
77+
78+
79+
class ProxyConfig:
80+
"""
81+
Configuration class for proxies.
82+
"""
83+
84+
def __init__(self, ssl_context, allow_insecure_proxy, destination_scheme=None):
85+
self.ssl_context = ssl_context
86+
self.allow_insecure_proxy = allow_insecure_proxy
87+
self.destination_scheme = destination_scheme
7988

8089

8190
def _default_key_normalizer(key_class, request_context):
@@ -168,6 +177,7 @@ class PoolManager(RequestMethods):
168177
"""
169178

170179
proxy = None
180+
proxy_config = None
171181

172182
def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
173183
RequestMethods.__init__(self, headers)
@@ -322,14 +332,26 @@ def _merge_pool_kwargs(self, override):
322332
def _proxy_requires_url_absolute_form(self, parsed_url):
323333
"""
324334
Indicates if the proxy requires the complete destination URL in the
325-
request.
326-
327-
Normally this is only needed when not using an HTTP CONNECT tunnel.
335+
request. Normally this is only needed when not using an HTTP CONNECT
336+
tunnel.
328337
"""
329338
if self.proxy is None:
330339
return False
331340

332-
return parsed_url.scheme == "http" or self.proxy.scheme == "https"
341+
return not connection_requires_http_tunnel(self.proxy, self.proxy_config)
342+
343+
def _validate_proxy_scheme_url_selection(self, url_scheme):
344+
if (
345+
not PY3
346+
and url_scheme == "https"
347+
and self.proxy is not None
348+
and self.proxy.scheme == "https"
349+
and not self.proxy_config.allow_insecure_proxy
350+
):
351+
352+
raise ProxySchemeUnsupported(
353+
"Contacting HTTPS destinations through HTTPS proxies is not supported on py2."
354+
)
333355

334356
def urlopen(self, method, url, redirect=True, **kw):
335357
"""
@@ -341,6 +363,8 @@ def urlopen(self, method, url, redirect=True, **kw):
341363
:class:`urllib3.connectionpool.ConnectionPool` can be chosen for it.
342364
"""
343365
u = parse_url(url)
366+
self._validate_proxy_scheme_url_selection(u.scheme)
367+
344368
conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
345369

346370
kw["assert_same_host"] = False
@@ -408,6 +432,10 @@ class ProxyManager(PoolManager):
408432
HTTPS/CONNECT case they are sent only once. Could be used for proxy
409433
authentication.
410434
435+
:param proxy_ssl_context:
436+
The proxy SSL context is used to establish the TLS connection to the
437+
proxy when using HTTPS proxies.
438+
411439
:param _allow_https_proxy_to_see_traffic:
412440
Allows forwarding of HTTPS requests to HTTPS proxies. The proxy will
413441
have visibility of all the traffic sent. ONLY USE IF YOU KNOW WHAT
@@ -433,6 +461,7 @@ def __init__(
433461
num_pools=10,
434462
headers=None,
435463
proxy_headers=None,
464+
proxy_ssl_context=None,
436465
_allow_https_proxy_to_see_traffic=False,
437466
**connection_pool_kw
438467
):
@@ -454,15 +483,21 @@ def __init__(
454483

455484
self.proxy = proxy
456485
self.proxy_headers = proxy_headers or {}
486+
self.proxy_ssl_context = proxy_ssl_context
487+
self.proxy_config = ProxyConfig(
488+
proxy_ssl_context, _allow_https_proxy_to_see_traffic
489+
)
457490

458491
connection_pool_kw["_proxy"] = self.proxy
459492
connection_pool_kw["_proxy_headers"] = self.proxy_headers
460-
461-
self.allow_insecure_proxy = _allow_https_proxy_to_see_traffic
493+
connection_pool_kw["_proxy_config"] = self.proxy_config
462494

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

465497
def connection_from_host(self, host, port=None, scheme="http", pool_kwargs=None):
498+
# Original destination is needed to identify if we need TLS within TLS.
499+
self.proxy_config.destination_scheme = scheme
500+
466501
if scheme == "https":
467502
return super(ProxyManager, self).connection_from_host(
468503
host, port, scheme, pool_kwargs=pool_kwargs
@@ -487,31 +522,14 @@ def _set_proxy_headers(self, url, headers=None):
487522
headers_.update(headers)
488523
return headers_
489524

490-
def _validate_proxy_scheme_url_selection(self, url_scheme):
491-
if (
492-
url_scheme == "https"
493-
and self.proxy.scheme == "https"
494-
and not self.allow_insecure_proxy
495-
):
496-
warnings.warn(
497-
"Your proxy configuration specified an HTTPS scheme for the proxy. "
498-
"Are you sure you want to use HTTPS to contact the proxy? "
499-
"This most likely indicates an error in your configuration."
500-
"If you are sure you want use HTTPS to contact the proxy, enable "
501-
"the _allow_https_proxy_to_see_traffic.",
502-
InvalidProxyConfigurationWarning,
503-
)
504-
505-
raise ProxySchemeUnsupported(
506-
"Contacting HTTPS destinations through HTTPS proxies is not supported."
507-
)
508-
509525
def urlopen(self, method, url, redirect=True, **kw):
510526
"Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute."
511527
u = parse_url(url)
512-
self._validate_proxy_scheme_url_selection(u.scheme)
513528

514-
if u.scheme == "http" or self.proxy.scheme == "https":
529+
# Original destination is needed to identify if we need TLS within TLS.
530+
self.proxy_config.destination_scheme = u.scheme
531+
532+
if not connection_requires_http_tunnel(self.proxy, self.proxy_config):
515533
# For connections using HTTP CONNECT, httplib sets the necessary
516534
# headers on the CONNECT to the proxy. For HTTP or when talking
517535
# HTTPS to the proxy, we'll definitely need to set 'Host' at the

src/urllib3/util/connection.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
from .wait import NoWayToWaitForSocketError, wait_for_read
44
from ..contrib import _appengine_environ
55

6+
hasSSL = False
7+
try:
8+
import ssl
9+
10+
hasSSL = True
11+
except ImportError:
12+
pass
13+
614

715
def is_connection_dropped(conn): # Platform-specific
816
"""
@@ -26,6 +34,29 @@ def is_connection_dropped(conn): # Platform-specific
2634
return False
2735

2836

37+
def connection_requires_http_tunnel(proxy_url, proxy_config):
38+
"""
39+
Returns True if the connection requires an HTTP CONNECT through the proxy.
40+
41+
:param destination_url:
42+
:URL URL of the destination.
43+
:param proxy_url:
44+
:URL URL of the proxy.
45+
:param proxy_config:
46+
:class:`PoolManager.ProxyConfig` proxy configuration
47+
"""
48+
if proxy_url is None or proxy_config is None:
49+
return False
50+
51+
if proxy_config.destination_scheme == "http":
52+
return False
53+
54+
if proxy_url.scheme == "http":
55+
return True
56+
57+
return not proxy_config.allow_insecure_proxy
58+
59+
2960
# This function is copied from socket.py in the Python 2.7 standard
3061
# library test suite. Added to its signature is only `socket_options`.
3162
# One additional modification is that we avoid binding to IPv6 servers

0 commit comments

Comments
 (0)