1
from __future__ import annotations
10
from itertools import chain
11
from test import ImportBlocker, ModuleStash, notBrotli, notZstd, onlyBrotli, onlyZstd
12
from unittest import mock
13
from unittest.mock import MagicMock, Mock, patch
14
from urllib.parse import urlparse
18
from urllib3 import add_stderr_logger, disable_warnings
19
from urllib3.connection import ProxyConfig
20
from urllib3.exceptions import (
21
InsecureRequestWarning,
24
UnrewindableBodyError,
26
from urllib3.util import is_fp_closed
27
from urllib3.util.connection import _has_ipv6, allowed_gai_family, create_connection
28
from urllib3.util.proxy import connection_requires_http_tunnel
29
from urllib3.util.request import _FAILEDTELL, make_headers, rewind_body
30
from urllib3.util.response import assert_header_parsing
31
from urllib3.util.ssl_ import (
33
_is_has_never_check_common_name_reliable,
38
from urllib3.util.timeout import _DEFAULT_TIMEOUT, Timeout
39
from urllib3.util.url import Url, _encode_invalid_chars, parse_url
40
from urllib3.util.util import to_bytes, to_str
42
from . import clear_warnings
44
# This number represents a time in seconds, it doesn't mean anything in
45
# isolation. Setting to a high-ish value to avoid conflicts with the smaller
46
# numbers used for timeouts
53
("http://google.com/mail", ("http", "google.com", None)),
54
("http://google.com/mail/", ("http", "google.com", None)),
55
("google.com/mail", ("http", "google.com", None)),
56
("http://google.com/", ("http", "google.com", None)),
57
("http://google.com", ("http", "google.com", None)),
58
("http://www.google.com", ("http", "www.google.com", None)),
59
("http://mail.google.com", ("http", "mail.google.com", None)),
60
("http://google.com:8000/mail/", ("http", "google.com", 8000)),
61
("http://google.com:8000", ("http", "google.com", 8000)),
62
("https://google.com", ("https", "google.com", None)),
63
("https://google.com:8000", ("https", "google.com", 8000)),
64
("http://user:password@127.0.0.1:1234", ("http", "127.0.0.1", 1234)),
65
("http://google.com/foo=http://bar:42/baz", ("http", "google.com", None)),
66
("http://google.com?foo=http://bar:42/baz", ("http", "google.com", None)),
67
("http://google.com#foo=http://bar:42/baz", ("http", "google.com", None)),
69
("173.194.35.7", ("http", "173.194.35.7", None)),
70
("http://173.194.35.7", ("http", "173.194.35.7", None)),
71
("http://173.194.35.7/test", ("http", "173.194.35.7", None)),
72
("http://173.194.35.7:80", ("http", "173.194.35.7", 80)),
73
("http://173.194.35.7:80/test", ("http", "173.194.35.7", 80)),
75
("[2a00:1450:4001:c01::67]", ("http", "[2a00:1450:4001:c01::67]", None)),
76
("http://[2a00:1450:4001:c01::67]", ("http", "[2a00:1450:4001:c01::67]", None)),
78
"http://[2a00:1450:4001:c01::67]/test",
79
("http", "[2a00:1450:4001:c01::67]", None),
82
"http://[2a00:1450:4001:c01::67]:80",
83
("http", "[2a00:1450:4001:c01::67]", 80),
86
"http://[2a00:1450:4001:c01::67]:80/test",
87
("http", "[2a00:1450:4001:c01::67]", 80),
89
# More IPv6 from http://www.ietf.org/rfc/rfc2732.txt
91
"http://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:8000/index.html",
92
("http", "[fedc:ba98:7654:3210:fedc:ba98:7654:3210]", 8000),
95
"http://[1080:0:0:0:8:800:200c:417a]/index.html",
96
("http", "[1080:0:0:0:8:800:200c:417a]", None),
98
("http://[3ffe:2a00:100:7031::1]", ("http", "[3ffe:2a00:100:7031::1]", None)),
100
"http://[1080::8:800:200c:417a]/foo",
101
("http", "[1080::8:800:200c:417a]", None),
103
("http://[::192.9.5.5]/ipng", ("http", "[::192.9.5.5]", None)),
105
"http://[::ffff:129.144.52.38]:42/index.html",
106
("http", "[::ffff:129.144.52.38]", 42),
109
"http://[2010:836b:4179::836b:4179]",
110
("http", "[2010:836b:4179::836b:4179]", None),
112
# Scoped IPv6 (with ZoneID), both RFC 6874 compliant and not.
113
("http://[a::b%25zone]", ("http", "[a::b%zone]", None)),
114
("http://[a::b%zone]", ("http", "[a::b%zone]", None)),
116
("HTTP://GOOGLE.COM/mail/", ("http", "google.com", None)),
117
("GOogle.COM/mail", ("http", "google.com", None)),
118
("HTTP://GoOgLe.CoM:8000/mail/", ("http", "google.com", 8000)),
119
("HTTP://user:password@EXAMPLE.COM:1234", ("http", "example.com", 1234)),
120
("173.194.35.7", ("http", "173.194.35.7", None)),
121
("HTTP://173.194.35.7", ("http", "173.194.35.7", None)),
123
"HTTP://[2a00:1450:4001:c01::67]:80/test",
124
("http", "[2a00:1450:4001:c01::67]", 80),
127
"HTTP://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8000/index.html",
128
("http", "[fedc:ba98:7654:3210:fedc:ba98:7654:3210]", 8000),
131
"HTTPS://[1080:0:0:0:8:800:200c:417A]/index.html",
132
("https", "[1080:0:0:0:8:800:200c:417a]", None),
134
("abOut://eXamPlE.com?info=1", ("about", "eXamPlE.com", None)),
136
"http+UNIX://%2fvar%2frun%2fSOCKET/path",
137
("http+unix", "%2fvar%2frun%2fSOCKET", None),
141
@pytest.mark.parametrize(["url", "scheme_host_port"], url_host_map)
142
def test_scheme_host_port(
143
self, url: str, scheme_host_port: tuple[str, str, int | None]
145
parsed_url = parse_url(url)
146
scheme, host, port = scheme_host_port
148
assert (parsed_url.scheme or "http") == scheme
149
assert parsed_url.hostname == parsed_url.host == host
150
assert parsed_url.port == port
152
def test_encode_invalid_chars_none(self) -> None:
153
assert _encode_invalid_chars(None, set()) is None
155
@pytest.mark.parametrize(
158
"http://google.com:foo",
161
"http://google.com:-80",
162
"http://google.com:65536",
163
"http://google.com:\xb2\xb2", # \xb2 = ^2
164
# Invalid IDNA labels
172
def test_invalid_url(self, url: str) -> None:
173
with pytest.raises(LocationParseError):
176
@pytest.mark.parametrize(
177
"url, expected_normalized_url",
179
("HTTP://GOOGLE.COM/MAIL/", "http://google.com/MAIL/"),
181
"http://user@domain.com:password@example.com/~tilde@?@",
182
"http://user%40domain.com:password@example.com/~tilde@?@",
185
"HTTP://JeremyCline:Hunter2@Example.com:8080/",
186
"http://JeremyCline:Hunter2@example.com:8080/",
188
("HTTPS://Example.Com/?Key=Value", "https://example.com/?Key=Value"),
189
("Https://Example.Com/#Fragment", "https://example.com/#Fragment"),
190
# IPv6 addresses with zone IDs. Both RFC 6874 (%25) as well as
191
# non-standard (unquoted %) variants.
192
("[::1%zone]", "[::1%zone]"),
193
("[::1%25zone]", "[::1%zone]"),
194
("[::1%25]", "[::1%25]"),
195
("[::Ff%etH0%Ff]/%ab%Af", "[::ff%etH0%FF]/%AB%AF"),
197
"http://user:pass@[AaAa::Ff%25etH0%Ff]/%ab%Af",
198
"http://user:pass@[aaaa::ff%etH0%FF]/%AB%AF",
200
# Invalid characters for the query/fragment getting encoded
202
'http://google.com/p[]?parameter[]="hello"#fragment#',
203
"http://google.com/p%5B%5D?parameter%5B%5D=%22hello%22#fragment%23",
205
# Percent encoding isn't applied twice despite '%' being invalid
206
# but the percent encoding is still normalized.
208
"http://google.com/p%5B%5d?parameter%5b%5D=%22hello%22#fragment%23",
209
"http://google.com/p%5B%5D?parameter%5B%5D=%22hello%22#fragment%23",
213
def test_parse_url_normalization(
214
self, url: str, expected_normalized_url: str
216
"""Assert parse_url normalizes the scheme/host, and only the scheme/host"""
217
actual_normalized_url = parse_url(url).url
218
assert actual_normalized_url == expected_normalized_url
220
@pytest.mark.parametrize("char", [chr(i) for i in range(0x00, 0x21)] + ["\x7F"])
221
def test_control_characters_are_percent_encoded(self, char: str) -> None:
222
percent_char = "%" + (hex(ord(char))[2:].zfill(2).upper())
224
f"http://user{char}@example.com/path{char}?query{char}#fragment{char}"
229
auth="user" + percent_char,
231
path="/path" + percent_char,
232
query="query" + percent_char,
233
fragment="fragment" + percent_char,
236
parse_url_host_map = [
237
("http://google.com/mail", Url("http", host="google.com", path="/mail")),
238
("http://google.com/mail/", Url("http", host="google.com", path="/mail/")),
239
("http://google.com/mail", Url("http", host="google.com", path="mail")),
240
("google.com/mail", Url(host="google.com", path="/mail")),
241
("http://google.com/", Url("http", host="google.com", path="/")),
242
("http://google.com", Url("http", host="google.com")),
243
("http://google.com?foo", Url("http", host="google.com", path="", query="foo")),
244
# Path/query/fragment
246
("/", Url(path="/")),
247
("#?/!google.com/?foo", Url(path="", fragment="?/!google.com/?foo")),
248
("/foo", Url(path="/foo")),
249
("/foo?bar=baz", Url(path="/foo", query="bar=baz")),
251
"/foo?bar=baz#banana?apple/orange",
252
Url(path="/foo", query="bar=baz", fragment="banana?apple/orange"),
255
"/redirect?target=http://localhost:61020/",
256
Url(path="redirect", query="target=http://localhost:61020/"),
259
("http://google.com/", Url("http", host="google.com", path="/")),
260
("http://google.com:80/", Url("http", host="google.com", port=80, path="/")),
261
("http://google.com:80", Url("http", host="google.com", port=80)),
264
"http://foo:bar@localhost/",
265
Url("http", auth="foo:bar", host="localhost", path="/"),
267
("http://foo@localhost/", Url("http", auth="foo", host="localhost", path="/")),
269
"http://foo:bar@localhost/",
270
Url("http", auth="foo:bar", host="localhost", path="/"),
274
non_round_tripping_parse_url_host_map = [
275
# Path/query/fragment
276
("?", Url(path="", query="")),
277
("#", Url(path="", fragment="")),
279
("/abc/../def", Url(path="/def")),
281
("http://google.com:", Url("http", host="google.com")),
282
("http://google.com:/", Url("http", host="google.com", path="/")),
285
"http://Königsgäßchen.de/straße",
286
Url("http", host="xn--knigsgchen-b4a3dun.de", path="/stra%C3%9Fe"),
288
# Percent-encode in userinfo
290
"http://user@email.com:password@example.com/",
291
Url("http", auth="user%40email.com:password", host="example.com", path="/"),
294
'http://user":quoted@example.com/',
295
Url("http", auth="user%22:quoted", host="example.com", path="/"),
298
("http://google.com/\uD800", Url("http", host="google.com", path="%ED%A0%80")),
300
"http://google.com?q=\uDC00",
301
Url("http", host="google.com", path="", query="q=%ED%B0%80"),
304
"http://google.com#\uDC00",
305
Url("http", host="google.com", path="", fragment="%ED%B0%80"),
309
@pytest.mark.parametrize(
311
chain(parse_url_host_map, non_round_tripping_parse_url_host_map),
313
def test_parse_url(self, url: str, expected_url: Url) -> None:
314
returned_url = parse_url(url)
315
assert returned_url == expected_url
316
assert returned_url.hostname == returned_url.host == expected_url.host
318
@pytest.mark.parametrize("url, expected_url", parse_url_host_map)
319
def test_unparse_url(self, url: str, expected_url: Url) -> None:
320
assert url == expected_url.url
322
@pytest.mark.parametrize(
323
["url", "expected_url"],
326
("/abc/../def", Url(path="/def")),
327
("/..", Url(path="/")),
328
("/./abc/./def/", Url(path="/abc/def/")),
329
("/.", Url(path="/")),
330
("/./", Url(path="/")),
331
("/abc/./.././d/././e/.././f/./../../ghi", Url(path="/ghi")),
334
def test_parse_and_normalize_url_paths(self, url: str, expected_url: Url) -> None:
335
actual_url = parse_url(url)
336
assert actual_url == expected_url
337
assert actual_url.url == expected_url.url
339
def test_parse_url_invalid_IPv6(self) -> None:
340
with pytest.raises(LocationParseError):
343
def test_parse_url_negative_port(self) -> None:
344
with pytest.raises(LocationParseError):
345
parse_url("https://www.google.com:-80/")
347
def test_parse_url_remove_leading_zeros(self) -> None:
348
url = parse_url("https://example.com:0000000000080")
349
assert url.port == 80
351
def test_parse_url_only_zeros(self) -> None:
352
url = parse_url("https://example.com:0")
355
url = parse_url("https://example.com:000000000000")
358
def test_Url_str(self) -> None:
359
U = Url("http", host="google.com")
360
assert str(U) == U.url
363
("http://google.com/mail", "/mail"),
364
("http://google.com/mail/", "/mail/"),
365
("http://google.com/", "/"),
366
("http://google.com", "/"),
371
("/foo?bar=baz", "/foo?bar=baz"),
374
@pytest.mark.parametrize("url, expected_request_uri", request_uri_map)
375
def test_request_uri(self, url: str, expected_request_uri: str) -> None:
376
returned_url = parse_url(url)
377
assert returned_url.request_uri == expected_request_uri
379
url_authority_map: list[tuple[str, str | None]] = [
380
("http://user:pass@google.com/mail", "user:pass@google.com"),
381
("http://user:pass@google.com:80/mail", "user:pass@google.com:80"),
382
("http://user@google.com:80/mail", "user@google.com:80"),
383
("http://user:pass@192.168.1.1/path", "user:pass@192.168.1.1"),
384
("http://user:pass@192.168.1.1:80/path", "user:pass@192.168.1.1:80"),
385
("http://user@192.168.1.1:80/path", "user@192.168.1.1:80"),
386
("http://user:pass@[::1]/path", "user:pass@[::1]"),
387
("http://user:pass@[::1]:80/path", "user:pass@[::1]:80"),
388
("http://user@[::1]:80/path", "user@[::1]:80"),
389
("http://user:pass@localhost/path", "user:pass@localhost"),
390
("http://user:pass@localhost:80/path", "user:pass@localhost:80"),
391
("http://user@localhost:80/path", "user@localhost:80"),
395
("http://google.com/mail", "google.com"),
396
("http://google.com:80/mail", "google.com:80"),
397
("http://192.168.0.1/path", "192.168.0.1"),
398
("http://192.168.0.1:80/path", "192.168.0.1:80"),
399
("http://[::1]/path", "[::1]"),
400
("http://[::1]:80/path", "[::1]:80"),
401
("http://localhost", "localhost"),
402
("http://localhost:80", "localhost:80"),
403
("google.com/foobar", "google.com"),
404
("google.com:12345", "google.com:12345"),
408
combined_netloc_authority_map = url_authority_map + url_netloc_map
410
# We compose this list due to variances between parse_url
411
# and urlparse when URIs don't provide a scheme.
412
url_authority_with_schemes_map = [
413
u for u in combined_netloc_authority_map if u[0].startswith("http")
416
@pytest.mark.parametrize("url, expected_authority", combined_netloc_authority_map)
417
def test_authority(self, url: str, expected_authority: str | None) -> None:
418
assert parse_url(url).authority == expected_authority
420
@pytest.mark.parametrize("url, expected_authority", url_authority_with_schemes_map)
421
def test_authority_matches_urllib_netloc(
422
self, url: str, expected_authority: str | None
424
"""Validate this matches the behavior of urlparse().netloc"""
425
assert urlparse(url).netloc == expected_authority
427
@pytest.mark.parametrize("url, expected_netloc", url_netloc_map)
428
def test_netloc(self, url: str, expected_netloc: str | None) -> None:
429
assert parse_url(url).netloc == expected_netloc
431
url_vulnerabilities = [
432
# urlparse doesn't follow RFC 3986 Section 3.2
434
"http://google.com#@evil.com/",
435
Url("http", host="google.com", path="", fragment="@evil.com/"),
439
"http://127.0.0.1%0d%0aConnection%3a%20keep-alive",
440
Url("http", host="127.0.0.1%0d%0aconnection%3a%20keep-alive"),
442
# NodeJS unicode -> double dot
444
"http://google.com/\uff2e\uff2e/abc",
445
Url("http", host="google.com", path="/%EF%BC%AE%EF%BC%AE/abc"),
449
"javascript:a='@google.com:12345/';alert(0)",
450
Url(scheme="javascript", path="a='@google.com:12345/';alert(0)"),
452
("//google.com/a/b/c", Url(host="google.com", path="/a/b/c")),
455
"http://ヒ:キ@ヒ.abc.ニ/ヒ?キ#ワ",
458
host="xn--pdk.abc.xn--idk",
459
auth="%E3%83%92:%E3%82%AD",
462
fragment="%E3%83%AF",
465
# Injected headers (CVE-2016-5699, CVE-2019-9740, CVE-2019-9947)
467
"10.251.0.83:7777?a=1 HTTP/1.1\r\nX-injected: header",
472
query="a=1%20HTTP/1.1%0D%0AX-injected:%20header",
476
"http://127.0.0.1:6379?\r\nSET test failure12\r\n:8080/test/?test=a",
482
query="%0D%0ASET%20test%20failure12%0D%0A:8080/test/?test=a",
485
# See https://bugs.xdavidhu.me/google/2020/03/08/the-unexpected-google-wide-domain-check-bypass/
487
"https://user:pass@xdavidhu.me\\test.corp.google.com:8080/path/to/something?param=value#hash",
492
path="/%5Ctest.corp.google.com:8080/path/to/something",
497
# Tons of '@' causing backtracking
499
"https://" + ("@" * 10000) + "[",
501
id="Tons of '@' causing backtracking 1",
504
"https://user:" + ("@" * 10000) + "example.com",
507
auth="user:" + ("%40" * 9999),
510
id="Tons of '@' causing backtracking 2",
514
@pytest.mark.parametrize("url, expected_url", url_vulnerabilities)
515
def test_url_vulnerabilities(
516
self, url: str, expected_url: typing.Literal[False] | Url
518
if expected_url is False:
519
with pytest.raises(LocationParseError):
522
assert parse_url(url) == expected_url
524
def test_parse_url_bytes_type_error(self) -> None:
525
with pytest.raises(TypeError):
526
parse_url(b"https://www.google.com/") # type: ignore[arg-type]
528
@pytest.mark.parametrize(
532
{"accept_encoding": True},
533
{"accept-encoding": "gzip,deflate,br,zstd"},
534
marks=[onlyBrotli(), onlyZstd()], # type: ignore[list-item]
537
{"accept_encoding": True},
538
{"accept-encoding": "gzip,deflate,br"},
539
marks=[onlyBrotli(), notZstd()], # type: ignore[list-item]
542
{"accept_encoding": True},
543
{"accept-encoding": "gzip,deflate,zstd"},
544
marks=[notBrotli(), onlyZstd()], # type: ignore[list-item]
547
{"accept_encoding": True},
548
{"accept-encoding": "gzip,deflate"},
549
marks=[notBrotli(), notZstd()], # type: ignore[list-item]
551
({"accept_encoding": "foo,bar"}, {"accept-encoding": "foo,bar"}),
552
({"accept_encoding": ["foo", "bar"]}, {"accept-encoding": "foo,bar"}),
554
{"accept_encoding": True, "user_agent": "banana"},
555
{"accept-encoding": "gzip,deflate,br,zstd", "user-agent": "banana"},
556
marks=[onlyBrotli(), onlyZstd()], # type: ignore[list-item]
559
{"accept_encoding": True, "user_agent": "banana"},
560
{"accept-encoding": "gzip,deflate,br", "user-agent": "banana"},
561
marks=[onlyBrotli(), notZstd()], # type: ignore[list-item]
564
{"accept_encoding": True, "user_agent": "banana"},
565
{"accept-encoding": "gzip,deflate,zstd", "user-agent": "banana"},
566
marks=[notBrotli(), onlyZstd()], # type: ignore[list-item]
569
{"accept_encoding": True, "user_agent": "banana"},
570
{"accept-encoding": "gzip,deflate", "user-agent": "banana"},
571
marks=[notBrotli(), notZstd()], # type: ignore[list-item]
573
({"user_agent": "banana"}, {"user-agent": "banana"}),
574
({"keep_alive": True}, {"connection": "keep-alive"}),
575
({"basic_auth": "foo:bar"}, {"authorization": "Basic Zm9vOmJhcg=="}),
577
{"proxy_basic_auth": "foo:bar"},
578
{"proxy-authorization": "Basic Zm9vOmJhcg=="},
580
({"disable_cache": True}, {"cache-control": "no-cache"}),
583
def test_make_headers(
584
self, kwargs: dict[str, bool | str], expected: dict[str, str]
586
assert make_headers(**kwargs) == expected # type: ignore[arg-type]
588
def test_rewind_body(self) -> None:
589
body = io.BytesIO(b"test data")
590
assert body.read() == b"test data"
592
# Assert the file object has been consumed
593
assert body.read() == b""
595
# Rewind it back to just be b'data'
597
assert body.read() == b"data"
599
def test_rewind_body_failed_tell(self) -> None:
600
body = io.BytesIO(b"test data")
601
body.read() # Consume body
603
# Simulate failed tell()
604
body_pos = _FAILEDTELL
605
with pytest.raises(UnrewindableBodyError):
606
rewind_body(body, body_pos)
608
def test_rewind_body_bad_position(self) -> None:
609
body = io.BytesIO(b"test data")
610
body.read() # Consume body
612
# Pass non-integer position
613
with pytest.raises(ValueError):
614
rewind_body(body, body_pos=None) # type: ignore[arg-type]
615
with pytest.raises(ValueError):
616
rewind_body(body, body_pos=object()) # type: ignore[arg-type]
618
def test_rewind_body_failed_seek(self) -> None:
619
class BadSeek(io.StringIO):
620
def seek(self, offset: int, whence: int = 0) -> typing.NoReturn:
623
with pytest.raises(UnrewindableBodyError):
624
rewind_body(BadSeek(), body_pos=2)
626
def test_add_stderr_logger(self) -> None:
627
handler = add_stderr_logger(level=logging.INFO) # Don't actually print debug
628
logger = logging.getLogger("urllib3")
629
assert handler in logger.handlers
631
logger.debug("Testing add_stderr_logger")
632
logger.removeHandler(handler)
634
def test_disable_warnings(self) -> None:
635
with warnings.catch_warnings(record=True) as w:
637
warnings.simplefilter("default", InsecureRequestWarning)
638
warnings.warn("This is a test.", InsecureRequestWarning)
641
warnings.warn("This is a test.", InsecureRequestWarning)
645
self, seconds: int, timeout: Timeout, time_mock: Mock
647
"""Make some time pass for the timeout object"""
648
time_mock.return_value = TIMEOUT_EPOCH
649
timeout.start_connect()
650
time_mock.return_value = TIMEOUT_EPOCH + seconds
653
@pytest.mark.parametrize(
656
({"total": -1}, "less than"),
657
({"connect": 2, "total": -1}, "less than"),
658
({"read": -1}, "less than"),
659
({"connect": False}, "cannot be a boolean"),
660
({"read": True}, "cannot be a boolean"),
661
({"connect": 0}, "less than or equal"),
662
({"read": "foo"}, "int, float or None"),
663
({"read": "1.0"}, "int, float or None"),
666
def test_invalid_timeouts(
667
self, kwargs: dict[str, int | bool], message: str
669
with pytest.raises(ValueError, match=message):
672
@patch("time.monotonic")
673
def test_timeout(self, time_monotonic: MagicMock) -> None:
674
timeout = Timeout(total=3)
676
# make 'no time' elapse
677
timeout = self._make_time_pass(
678
seconds=0, timeout=timeout, time_mock=time_monotonic
680
assert timeout.read_timeout == 3
681
assert timeout.connect_timeout == 3
683
timeout = Timeout(total=3, connect=2)
684
assert timeout.connect_timeout == 2
687
assert timeout.connect_timeout == _DEFAULT_TIMEOUT
689
# Connect takes 5 seconds, leaving 5 seconds for read
690
timeout = Timeout(total=10, read=7)
691
timeout = self._make_time_pass(
692
seconds=5, timeout=timeout, time_mock=time_monotonic
694
assert timeout.read_timeout == 5
696
# Connect takes 2 seconds, read timeout still 7 seconds
697
timeout = Timeout(total=10, read=7)
698
timeout = self._make_time_pass(
699
seconds=2, timeout=timeout, time_mock=time_monotonic
701
assert timeout.read_timeout == 7
703
timeout = Timeout(total=10, read=7)
704
assert timeout.read_timeout == 7
706
timeout = Timeout(total=None, read=None, connect=None)
707
assert timeout.connect_timeout is None
708
assert timeout.read_timeout is None
709
assert timeout.total is None
712
assert timeout.total == 5
714
def test_timeout_default_resolve(self) -> None:
715
"""The timeout default is resolved when read_timeout is accessed."""
717
with patch("urllib3.util.timeout.getdefaulttimeout", return_value=2):
718
assert timeout.read_timeout == 2
720
with patch("urllib3.util.timeout.getdefaulttimeout", return_value=3):
721
assert timeout.read_timeout == 3
723
def test_timeout_str(self) -> None:
724
timeout = Timeout(connect=1, read=2, total=3)
725
assert str(timeout) == "Timeout(connect=1, read=2, total=3)"
726
timeout = Timeout(connect=1, read=None, total=3)
727
assert str(timeout) == "Timeout(connect=1, read=None, total=3)"
729
@patch("time.monotonic")
730
def test_timeout_elapsed(self, time_monotonic: MagicMock) -> None:
731
time_monotonic.return_value = TIMEOUT_EPOCH
732
timeout = Timeout(total=3)
733
with pytest.raises(TimeoutStateError):
734
timeout.get_connect_duration()
736
timeout.start_connect()
737
with pytest.raises(TimeoutStateError):
738
timeout.start_connect()
740
time_monotonic.return_value = TIMEOUT_EPOCH + 2
741
assert timeout.get_connect_duration() == 2
742
time_monotonic.return_value = TIMEOUT_EPOCH + 37
743
assert timeout.get_connect_duration() == 37
745
def test_is_fp_closed_object_supports_closed(self) -> None:
748
def closed(self) -> typing.Literal[True]:
751
assert is_fp_closed(ClosedFile())
753
def test_is_fp_closed_object_has_none_fp(self) -> None:
756
def fp(self) -> None:
759
assert is_fp_closed(NoneFpFile())
761
def test_is_fp_closed_object_has_fp(self) -> None:
764
def fp(self) -> typing.Literal[True]:
767
assert not is_fp_closed(FpFile())
769
def test_is_fp_closed_object_has_neither_fp_nor_closed(self) -> None:
770
class NotReallyAFile:
773
with pytest.raises(ValueError):
774
is_fp_closed(NotReallyAFile())
776
def test_has_ipv6_disabled_on_compile(self) -> None:
777
with patch("socket.has_ipv6", False):
778
assert not _has_ipv6("::1")
780
def test_has_ipv6_enabled_but_fails(self) -> None:
781
with patch("socket.has_ipv6", True):
782
with patch("socket.socket") as mock:
783
instance = mock.return_value
784
instance.bind = Mock(side_effect=Exception("No IPv6 here!"))
785
assert not _has_ipv6("::1")
787
def test_has_ipv6_enabled_and_working(self) -> None:
788
with patch("socket.has_ipv6", True):
789
with patch("socket.socket") as mock:
790
instance = mock.return_value
791
instance.bind.return_value = True
792
assert _has_ipv6("::1")
794
def test_ip_family_ipv6_enabled(self) -> None:
795
with patch("urllib3.util.connection.HAS_IPV6", True):
796
assert allowed_gai_family() == socket.AF_UNSPEC
798
def test_ip_family_ipv6_disabled(self) -> None:
799
with patch("urllib3.util.connection.HAS_IPV6", False):
800
assert allowed_gai_family() == socket.AF_INET
802
@pytest.mark.parametrize("headers", [b"foo", None, object])
803
def test_assert_header_parsing_throws_typeerror_with_non_headers(
804
self, headers: bytes | object | None
806
with pytest.raises(TypeError):
807
assert_header_parsing(headers) # type: ignore[arg-type]
809
def test_connection_requires_http_tunnel_no_proxy(self) -> None:
810
assert not connection_requires_http_tunnel(
811
proxy_url=None, proxy_config=None, destination_scheme=None
814
def test_connection_requires_http_tunnel_http_proxy(self) -> None:
815
proxy = parse_url("http://proxy:8080")
816
proxy_config = ProxyConfig(
818
use_forwarding_for_https=False,
819
assert_hostname=None,
820
assert_fingerprint=None,
822
destination_scheme = "http"
823
assert not connection_requires_http_tunnel(
824
proxy, proxy_config, destination_scheme
827
destination_scheme = "https"
828
assert connection_requires_http_tunnel(proxy, proxy_config, destination_scheme)
830
def test_connection_requires_http_tunnel_https_proxy(self) -> None:
831
proxy = parse_url("https://proxy:8443")
832
proxy_config = ProxyConfig(
834
use_forwarding_for_https=False,
835
assert_hostname=None,
836
assert_fingerprint=None,
838
destination_scheme = "http"
839
assert not connection_requires_http_tunnel(
840
proxy, proxy_config, destination_scheme
843
def test_assert_header_parsing_no_error_on_multipart(self) -> None:
844
from http import client
846
header_msg = io.BytesIO()
848
b'Content-Type: multipart/encrypted;protocol="application/'
849
b'HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
850
b"\nServer: Microsoft-HTTPAPI/2.0\nDate: Fri, 16 Aug 2019 19:28:01 GMT"
851
b"\nContent-Length: 1895\n\n\n"
854
assert_header_parsing(client.parse_headers(header_msg))
856
@pytest.mark.parametrize("host", [".localhost", "...", "t" * 64])
857
def test_create_connection_with_invalid_idna_labels(self, host: str) -> None:
860
match=f"Failed to parse: '{host}', label empty or too long",
862
create_connection((host, 80))
864
@pytest.mark.parametrize(
874
@patch("socket.getaddrinfo")
875
@patch("socket.socket")
876
def test_create_connection_with_valid_idna_labels(
877
self, socket: MagicMock, getaddrinfo: MagicMock, host: str
879
getaddrinfo.return_value = [(None, None, None, None, None)]
880
socket.return_value = Mock()
881
create_connection((host, 80))
883
@patch("socket.getaddrinfo")
884
def test_create_connection_error(self, getaddrinfo: MagicMock) -> None:
885
getaddrinfo.return_value = []
886
with pytest.raises(OSError, match="getaddrinfo returns an empty list"):
887
create_connection(("example.com", 80))
889
@patch("socket.getaddrinfo")
890
def test_dnsresolver_forced_error(self, getaddrinfo: MagicMock) -> None:
891
getaddrinfo.side_effect = socket.gaierror()
892
with pytest.raises(socket.gaierror):
893
# dns is valid but we force the error just for the sake of the test
894
create_connection(("example.com", 80))
896
def test_dnsresolver_expected_error(self) -> None:
897
with pytest.raises(socket.gaierror):
898
# windows: [Errno 11001] getaddrinfo failed in windows
899
# linux: [Errno -2] Name or service not known
900
# macos: [Errno 8] nodename nor servname provided, or not known
901
create_connection(("badhost.invalid", 80))
903
@patch("socket.getaddrinfo")
904
@patch("socket.socket")
905
def test_create_connection_with_scoped_ipv6(
906
self, socket: MagicMock, getaddrinfo: MagicMock
908
# Check that providing create_connection with a scoped IPv6 address
909
# properly propagates the scope to getaddrinfo, and that the returned
910
# scoped ID makes it to the socket creation call.
911
fake_scoped_sa6 = ("a::b", 80, 0, 42)
912
getaddrinfo.return_value = [
921
socket.return_value = fake_sock = MagicMock()
923
create_connection(("a::b%iface", 80))
924
assert getaddrinfo.call_args[0][0] == "a::b%iface"
925
fake_sock.connect.assert_called_once_with(fake_scoped_sa6)
927
@pytest.mark.parametrize(
928
"input,params,expected",
930
("test", {}, "test"), # str input
931
(b"test", {}, "test"), # bytes input
932
(b"test", {"encoding": "utf-8"}, "test"), # bytes input with utf-8
933
(b"test", {"encoding": "ascii"}, "test"), # bytes input with ascii
937
self, input: bytes | str, params: dict[str, str], expected: str
939
assert to_str(input, **params) == expected
941
def test_to_str_error(self) -> None:
942
with pytest.raises(TypeError, match="not expecting type int"):
943
to_str(1) # type: ignore[arg-type]
945
@pytest.mark.parametrize(
946
"input,params,expected",
948
(b"test", {}, b"test"), # str input
949
("test", {}, b"test"), # bytes input
950
("é", {}, b"\xc3\xa9"), # bytes input
951
("test", {"encoding": "utf-8"}, b"test"), # bytes input with utf-8
952
("test", {"encoding": "ascii"}, b"test"), # bytes input with ascii
956
self, input: bytes | str, params: dict[str, str], expected: bytes
958
assert to_bytes(input, **params) == expected
960
def test_to_bytes_error(self) -> None:
961
with pytest.raises(TypeError, match="not expecting type int"):
962
to_bytes(1) # type: ignore[arg-type]
966
"""Test utils that use an SSL backend."""
968
@pytest.mark.parametrize(
969
"candidate, requirements",
971
(None, ssl.CERT_REQUIRED),
972
(ssl.CERT_NONE, ssl.CERT_NONE),
973
(ssl.CERT_REQUIRED, ssl.CERT_REQUIRED),
974
("REQUIRED", ssl.CERT_REQUIRED),
975
("CERT_REQUIRED", ssl.CERT_REQUIRED),
978
def test_resolve_cert_reqs(
979
self, candidate: int | str | None, requirements: int
981
assert resolve_cert_reqs(candidate) == requirements
983
@pytest.mark.parametrize(
984
"candidate, version",
986
(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1),
987
("PROTOCOL_TLSv1", ssl.PROTOCOL_TLSv1),
988
("TLSv1", ssl.PROTOCOL_TLSv1),
989
(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23),
992
def test_resolve_ssl_version(self, candidate: int | str, version: int) -> None:
993
assert resolve_ssl_version(candidate) == version
995
def test_ssl_wrap_socket_loads_the_cert_chain(self) -> None:
997
mock_context = Mock()
999
ssl_context=mock_context, sock=socket, certfile="/path/to/certfile"
1002
mock_context.load_cert_chain.assert_called_once_with("/path/to/certfile", None)
1004
@patch("urllib3.util.ssl_.create_urllib3_context")
1005
def test_ssl_wrap_socket_creates_new_context(
1006
self, create_urllib3_context: mock.MagicMock
1009
ssl_wrap_socket(socket, cert_reqs=ssl.CERT_REQUIRED)
1011
create_urllib3_context.assert_called_once_with(None, 2, ciphers=None)
1013
def test_ssl_wrap_socket_loads_verify_locations(self) -> None:
1015
mock_context = Mock()
1016
ssl_wrap_socket(ssl_context=mock_context, ca_certs="/path/to/pem", sock=socket)
1017
mock_context.load_verify_locations.assert_called_once_with(
1018
"/path/to/pem", None, None
1021
def test_ssl_wrap_socket_loads_certificate_directories(self) -> None:
1023
mock_context = Mock()
1025
ssl_context=mock_context, ca_cert_dir="/path/to/pems", sock=socket
1027
mock_context.load_verify_locations.assert_called_once_with(
1028
None, "/path/to/pems", None
1031
def test_ssl_wrap_socket_loads_certificate_data(self) -> None:
1033
mock_context = Mock()
1035
ssl_context=mock_context, ca_cert_data="TOTALLY PEM DATA", sock=socket
1037
mock_context.load_verify_locations.assert_called_once_with(
1038
None, None, "TOTALLY PEM DATA"
1041
def _wrap_socket_and_mock_warn(
1042
self, sock: socket.socket, server_hostname: str | None
1043
) -> tuple[Mock, MagicMock]:
1044
mock_context = Mock()
1045
with patch("warnings.warn") as warn:
1047
ssl_context=mock_context,
1049
server_hostname=server_hostname,
1051
return mock_context, warn
1053
def test_ssl_wrap_socket_sni_ip_address_no_warn(self) -> None:
1054
"""Test that a warning is not made if server_hostname is an IP address."""
1056
context, warn = self._wrap_socket_and_mock_warn(sock, "8.8.8.8")
1057
context.wrap_socket.assert_called_once_with(sock, server_hostname="8.8.8.8")
1058
warn.assert_not_called()
1060
def test_ssl_wrap_socket_sni_none_no_warn(self) -> None:
1061
"""Test that a warning is not made if server_hostname is not given."""
1063
context, warn = self._wrap_socket_and_mock_warn(sock, None)
1064
context.wrap_socket.assert_called_once_with(sock, server_hostname=None)
1065
warn.assert_not_called()
1067
@pytest.mark.parametrize(
1068
"openssl_version, openssl_version_number, implementation_name, version_info, pypy_version_info, reliable",
1070
# OpenSSL and Python OK -> reliable
1071
("OpenSSL 1.1.1", 0x101010CF, "cpython", (3, 9, 3), None, True),
1072
# Python OK -> reliable
1073
("OpenSSL 1.1.1", 0x10101000, "cpython", (3, 9, 3), None, True),
1074
# PyPy: depends on the version
1075
("OpenSSL 1.1.1", 0x10101000, "pypy", (3, 9, 9), (7, 3, 7), False),
1076
("OpenSSL 1.1.1", 0x101010CF, "pypy", (3, 8, 12), (7, 3, 8), True),
1077
# OpenSSL OK -> reliable
1078
("OpenSSL 1.1.1", 0x101010CF, "cpython", (3, 9, 2), None, True),
1079
# not OpenSSSL -> unreliable
1080
("LibreSSL 2.8.3", 0x101010CF, "cpython", (3, 10, 0), None, False),
1081
# old OpenSSL and old Python, unreliable
1082
("OpenSSL 1.1.0", 0x10101000, "cpython", (3, 9, 2), None, False),
1085
def test_is_has_never_check_common_name_reliable(
1087
openssl_version: str,
1088
openssl_version_number: int,
1089
implementation_name: str,
1090
version_info: _TYPE_VERSION_INFO,
1091
pypy_version_info: _TYPE_VERSION_INFO | None,
1095
_is_has_never_check_common_name_reliable(
1097
openssl_version_number,
1098
implementation_name,
1106
idna_blocker = ImportBlocker("idna")
1107
module_stash = ModuleStash("urllib3")
1110
class TestUtilWithoutIdna:
1112
def setup_class(cls) -> None:
1113
sys.modules.pop("idna", None)
1115
module_stash.stash()
1116
sys.meta_path.insert(0, idna_blocker)
1119
def teardown_class(cls) -> None:
1120
sys.meta_path.remove(idna_blocker)
1123
def test_parse_url_without_idna(self) -> None:
1124
url = "http://\uD7FF.com"
1125
with pytest.raises(LocationParseError, match=f"Failed to parse: {url}"):