Move ssl_match_hostname to urllib3.utils
MODIFY
noxfile.py
noxfile.py
@@ -14,14 +14,12 @@ TYPED_FILES = {
"src/urllib3/_collections.py",
"src/urllib3/fields.py",
"src/urllib3/filepost.py",
- "src/urllib3/packages/__init__.py",
- "src/urllib3/packages/ssl_match_hostname/__init__.py",
- "src/urllib3/packages/ssl_match_hostname/_implementation.py",
"src/urllib3/util/connection.py",
"src/urllib3/util/proxy.py",
"src/urllib3/util/queue.py",
"src/urllib3/util/response.py",
"src/urllib3/util/ssl_.py",
+ "src/urllib3/util/ssl_match_hostname.py",
"src/urllib3/util/ssltransport.py",
"src/urllib3/util/url.py",
"src/urllib3/util/wait.py",

MODIFY
setup.cfg
setup.cfg
[flake8]
ignore = E501, E203, W503, W504
-exclude=./docs/conf.py,./src/urllib3/packages/*
+exclude=./docs/conf.py
max-line-length=99
[metadata]

MODIFY
setup.py
setup.py
@@ -99,8 +99,6 @@ setup(
license="MIT",
packages=[
"urllib3",
- "urllib3.packages",
- "urllib3.packages.ssl_match_hostname",
"urllib3.contrib",
"urllib3.contrib._securetransport",
"urllib3.util",

MODIFY
src/urllib3/connection.py
src/urllib3/connection.py
@@ -47,7 +47,6 @@ from .exceptions import (
NewConnectionError,
SystemTimeWarning,
)
-from .packages.ssl_match_hostname import CertificateError, match_hostname
from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection, ssl_
from .util.ssl_ import (
PeerCertRetType,
@@ -57,6 +56,7 @@ from .util.ssl_ import (
resolve_ssl_version,
ssl_wrap_socket,
)
+from .util.ssl_match_hostname import CertificateError, match_hostname
# Not a no-op, we're adding this to the namespace so it can be imported.
ConnectionError = ConnectionError

MODIFY
src/urllib3/connectionpool.py
src/urllib3/connectionpool.py
@@ -32,7 +32,6 @@ from .exceptions import (
SSLError,
TimeoutError,
)
-from .packages.ssl_match_hostname import CertificateError
from .request import RequestMethods
from .response import BaseHTTPResponse, HTTPResponse
from .util.connection import is_connection_dropped
@@ -40,6 +39,7 @@ 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
+from .util.ssl_match_hostname import CertificateError
from .util.timeout import Timeout
from .util.url import Url, _encode_target
from .util.url import _normalize_host as normalize_host

DELETE
src/urllib3/packages/__init__.py
-from . import ssl_match_hostname
-
-__all__ = ("ssl_match_hostname",)

DELETE
src/urllib3/packages/__init__.pyi
src/urllib3/packages/__init__.pyi
DELETE
src/urllib3/packages/ssl_match_hostname/__init__.py
-from ._implementation import CertificateError, match_hostname
-
-__all__ = ("CertificateError", "match_hostname")

DELETE
src/urllib3/packages/ssl_match_hostname/_implementation.pyi
-from typing import Dict, Tuple, Union
-
-# https://github.com/python/typeshed/blob/master/stdlib/2and3/ssl.pyi
-_PCTRTT = Tuple[Tuple[str, str], ...]
-_PCTRTTT = Tuple[_PCTRTT, ...]
-_PeerCertRetDictType = Dict[str, Union[str, _PCTRTTT, _PCTRTT]]
-_PeerCertRetType = Union[_PeerCertRetDictType, bytes, None]
-
-class CertificateError(ValueError): ...
-
-def match_hostname(cert: _PeerCertRetType, hostname: str) -> None: ...

RENAME
src/urllib3/packages/ssl_match_hostname/_implementation.py
src/urllib3/util/ssl_match_hostname.py
import ipaddress
import re
-import sys
+from typing import Any, Match, Optional, Union
+
+from .ssl_ import PeerCertRetType
__version__ = "3.5.0.1"
@@ -14,7 +16,9 @@ class CertificateError(ValueError):
pass
-def _dnsname_match(dn, hostname, max_wildcards=1):
+def _dnsname_match(
+ dn: Any, hostname: str, max_wildcards: int = 1
+) -> Union[Optional[Match[str]], bool]:
"""Matching according to RFC 6125, section 6.4.3
http://tools.ietf.org/html/rfc6125#section-6.4.3
@@ -41,7 +45,7 @@ def _dnsname_match(dn, hostname, max_wildcards=1):
# speed up common case w/o wildcards
if not wildcards:
- return dn.lower() == hostname.lower()
+ return bool(dn.lower() == hostname.lower())
# RFC 6125, section 6.4.3, subitem 1.
# The client SHOULD NOT attempt to match a presented identifier in which
@@ -68,7 +72,7 @@ def _dnsname_match(dn, hostname, max_wildcards=1):
return pat.match(hostname)
-def _ipaddress_match(ipname, host_ip):
+def _ipaddress_match(ipname: Any, host_ip: str) -> bool:
"""Exact matching of IP addresses.
RFC 6125 explicitly doesn't define an algorithm for this
@@ -77,10 +81,10 @@ def _ipaddress_match(ipname, host_ip):
# OpenSSL may add a trailing newline to a subjectAltName's IP address
# Divergence from upstream: ipaddress can't handle byte str
ip = ipaddress.ip_address(ipname.rstrip())
- return ip == host_ip
+ return bool(ip == host_ip)
-def match_hostname(cert, hostname):
+def match_hostname(cert: PeerCertRetType, hostname: str) -> None:
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
rules are followed, but IP addresses are not accepted for *hostname*.
@@ -101,8 +105,8 @@ def match_hostname(cert, hostname):
# Not an IP address (common case)
host_ip = None
dnsnames = []
- san = cert.get("subjectAltName", ())
- for key, value in san:
+ san = cert.get("subjectAltName", ()) # type: ignore
+ for key, value in san: # type: ignore
if key == "DNS":
if host_ip is None and _dnsname_match(value, hostname):
return

MODIFY
test/test_connection.py
test/test_connection.py
@@ -4,10 +4,10 @@ from unittest import mock
import pytest
from urllib3.connection import RECENT_DATE, CertificateError, _match_hostname
-from urllib3.packages.ssl_match_hostname._implementation import (
+from urllib3.util.ssl_match_hostname import (
CertificateError as ImplementationCertificateError,
)
-from urllib3.packages.ssl_match_hostname._implementation import match_hostname
+from urllib3.util.ssl_match_hostname import match_hostname
class TestConnection:
@@ -47,6 +47,36 @@ class TestConnection:
)
assert e._peer_cert == cert
+ def test_match_hostname_no_dns(self):
+ cert = {"subjectAltName": [("DNS", "")]}
+ asserted_hostname = "bar"
+ try:
+ with mock.patch("urllib3.connection.log.warning") as mock_log:
+ _match_hostname(cert, asserted_hostname)
+ except CertificateError as e:
+ assert "hostname 'bar' doesn't match ''" in str(e)
+ mock_log.assert_called_once_with(
+ "Certificate did not match expected hostname: %s. Certificate: %s",
+ "bar",
+ {"subjectAltName": [("DNS", "")]},
+ )
+ assert e._peer_cert == cert
+
+ def test_match_hostname_startwith_wildcard(self):
+ cert = {"subjectAltName": [("DNS", "*")]}
+ asserted_hostname = "foo"
+ _match_hostname(cert, asserted_hostname)
+
+ def test_match_hostname_dnsname(self):
+ cert = {"subjectAltName": [("DNS", "xn--p1b6ci4b4b3a*.xn--11b5bs8d")]}
+ asserted_hostname = "xn--p1b6ci4b4b3a*.xn--11b5bs8d"
+ _match_hostname(cert, asserted_hostname)
+
+ def test_match_hostname_include_wildcard(self):
+ cert = {"subjectAltName": [("DNS", "foo*")]}
+ asserted_hostname = "foobar"
+ _match_hostname(cert, asserted_hostname)
+
def test_match_hostname_ignore_common_name(self):
cert = {"subject": [("commonName", "foo")]}
asserted_hostname = "foo"
@@ -56,6 +86,21 @@ class TestConnection:
):
match_hostname(cert, asserted_hostname)
+ def test_match_hostname_ip_address(self):
+ cert = {"subjectAltName": [("IP Address", "1.1.1.1")]}
+ asserted_hostname = "1.1.1.2"
+ try:
+ with mock.patch("urllib3.connection.log.warning") as mock_log:
+ _match_hostname(cert, asserted_hostname)
+ except CertificateError as e:
+ assert "hostname '1.1.1.2' doesn't match '1.1.1.1'" in str(e)
+ mock_log.assert_called_once_with(
+ "Certificate did not match expected hostname: %s. Certificate: %s",
+ "1.1.1.2",
+ {"subjectAltName": [("IP Address", "1.1.1.1")]},
+ )
+ assert e._peer_cert == cert
+
def test_recent_date(self):
# This test is to make sure that the RECENT_DATE value
# doesn't get too far behind what the current date is.

MODIFY
test/test_connectionpool.py
test/test_connectionpool.py
@@ -27,8 +27,8 @@ from urllib3.exceptions import (
SSLError,
TimeoutError,
)
-from urllib3.packages.ssl_match_hostname import CertificateError
from urllib3.response import HTTPResponse
+from urllib3.util.ssl_match_hostname import CertificateError
from urllib3.util.timeout import Timeout
from .test_response import MockChunkedEncodingResponse, MockSock

Move ssl_match_hostname to urllib3.utils