From 4c2537010db852e42166cf7f8b95d02de117de4e Mon Sep 17 00:00:00 2001
From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com>
Date: Fri, 27 Feb 2026 06:44:37 +0100
Subject: [PATCH] Update `http` from 3.14.3 (#7137)
* Update `http` from 3.14.3
* Reapply patch
* Update `test/certdata` from 3.14.3
* Revert "Update `test/certdata` from 3.14.3"
This reverts commit fa8fb388b342185e901376374788bc833ba3655d.
* Update `test_httpservers.py`
* Reapply long test patch
* Mark failing tests
* Skip flaky test
* Allow password to be None
* Unmark passing test
* Fix error message
* Clippy
---------
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
---
Lib/http/__init__.py | 41 +++++++----
Lib/http/client.py | 10 +--
Lib/http/cookiejar.py | 2 +-
Lib/http/cookies.py | 33 +++++++--
Lib/http/server.py | 112 +++++++++++++++++++++++++---
Lib/test/test_http_cookiejar.py | 21 ++++--
Lib/test/test_http_cookies.py | 67 +++++++++++++++--
Lib/test/test_httplib.py | 58 +++++++++------
Lib/test/test_httpservers.py | 127 ++++++++++++++++++++++++++++++--
crates/stdlib/src/ssl.rs | 15 ++--
10 files changed, 395 insertions(+), 91 deletions(-)
diff --git a/Lib/http/__init__.py b/Lib/http/__init__.py
index 17a47b180..691b4a9a3 100644
--- a/Lib/http/__init__.py
+++ b/Lib/http/__init__.py
@@ -54,8 +54,9 @@ class HTTPStatus:
CONTINUE = 100, 'Continue', 'Request received, please continue'
SWITCHING_PROTOCOLS = (101, 'Switching Protocols',
'Switching to new protocol; obey Upgrade header')
- PROCESSING = 102, 'Processing'
- EARLY_HINTS = 103, 'Early Hints'
+ PROCESSING = 102, 'Processing', 'Server is processing the request'
+ EARLY_HINTS = (103, 'Early Hints',
+ 'Headers sent to prepare for the response')
# success
OK = 200, 'OK', 'Request fulfilled, document follows'
@@ -67,9 +68,11 @@ class HTTPStatus:
NO_CONTENT = 204, 'No Content', 'Request fulfilled, nothing follows'
RESET_CONTENT = 205, 'Reset Content', 'Clear input form for further input'
PARTIAL_CONTENT = 206, 'Partial Content', 'Partial content follows'
- MULTI_STATUS = 207, 'Multi-Status'
- ALREADY_REPORTED = 208, 'Already Reported'
- IM_USED = 226, 'IM Used'
+ MULTI_STATUS = (207, 'Multi-Status',
+ 'Response contains multiple statuses in the body')
+ ALREADY_REPORTED = (208, 'Already Reported',
+ 'Operation has already been reported')
+ IM_USED = 226, 'IM Used', 'Request completed using instance manipulations'
# redirection
MULTIPLE_CHOICES = (300, 'Multiple Choices',
@@ -128,15 +131,19 @@ class HTTPStatus:
EXPECTATION_FAILED = (417, 'Expectation Failed',
'Expect condition could not be satisfied')
IM_A_TEAPOT = (418, 'I\'m a Teapot',
- 'Server refuses to brew coffee because it is a teapot.')
+ 'Server refuses to brew coffee because it is a teapot')
MISDIRECTED_REQUEST = (421, 'Misdirected Request',
'Server is not able to produce a response')
- UNPROCESSABLE_CONTENT = 422, 'Unprocessable Content'
+ UNPROCESSABLE_CONTENT = (422, 'Unprocessable Content',
+ 'Server is not able to process the contained instructions')
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
- LOCKED = 423, 'Locked'
- FAILED_DEPENDENCY = 424, 'Failed Dependency'
- TOO_EARLY = 425, 'Too Early'
- UPGRADE_REQUIRED = 426, 'Upgrade Required'
+ LOCKED = 423, 'Locked', 'Resource of a method is locked'
+ FAILED_DEPENDENCY = (424, 'Failed Dependency',
+ 'Dependent action of the request failed')
+ TOO_EARLY = (425, 'Too Early',
+ 'Server refuses to process a request that might be replayed')
+ UPGRADE_REQUIRED = (426, 'Upgrade Required',
+ 'Server refuses to perform the request using the current protocol')
PRECONDITION_REQUIRED = (428, 'Precondition Required',
'The origin server requires the request to be conditional')
TOO_MANY_REQUESTS = (429, 'Too Many Requests',
@@ -164,10 +171,14 @@ class HTTPStatus:
'The gateway server did not receive a timely response')
HTTP_VERSION_NOT_SUPPORTED = (505, 'HTTP Version Not Supported',
'Cannot fulfill request')
- VARIANT_ALSO_NEGOTIATES = 506, 'Variant Also Negotiates'
- INSUFFICIENT_STORAGE = 507, 'Insufficient Storage'
- LOOP_DETECTED = 508, 'Loop Detected'
- NOT_EXTENDED = 510, 'Not Extended'
+ VARIANT_ALSO_NEGOTIATES = (506, 'Variant Also Negotiates',
+ 'Server has an internal configuration error')
+ INSUFFICIENT_STORAGE = (507, 'Insufficient Storage',
+ 'Server is not able to store the representation')
+ LOOP_DETECTED = (508, 'Loop Detected',
+ 'Server encountered an infinite loop while processing a request')
+ NOT_EXTENDED = (510, 'Not Extended',
+ 'Request does not meet the resource access policy')
NETWORK_AUTHENTICATION_REQUIRED = (511,
'Network Authentication Required',
'The client needs to authenticate to gain network access')
diff --git a/Lib/http/client.py b/Lib/http/client.py
index dd5f4136e..77f8d2629 100644
--- a/Lib/http/client.py
+++ b/Lib/http/client.py
@@ -1047,7 +1047,7 @@ class HTTPConnection:
response.close()
def send(self, data):
- """Send `data' to the server.
+ """Send 'data' to the server.
``data`` can be a string object, a bytes object, an array object, a
file-like object that supports a .read() method, or an iterable object.
"""
@@ -1159,10 +1159,10 @@ class HTTPConnection:
skip_accept_encoding=False):
"""Send a request to the server.
- `method' specifies an HTTP request method, e.g. 'GET'.
- `url' specifies the object being requested, e.g. '/index.html'.
- `skip_host' if True does not add automatically a 'Host:' header
- `skip_accept_encoding' if True does not add automatically an
+ 'method' specifies an HTTP request method, e.g. 'GET'.
+ 'url' specifies the object being requested, e.g. '/index.html'.
+ 'skip_host' if True does not add automatically a 'Host:' header
+ 'skip_accept_encoding' if True does not add automatically an
'Accept-Encoding:' header
"""
diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py
index 9a2f0fb85..68cf16c93 100644
--- a/Lib/http/cookiejar.py
+++ b/Lib/http/cookiejar.py
@@ -1987,7 +1987,7 @@ class MozillaCookieJar(FileCookieJar):
This class differs from CookieJar only in the format it uses to save and
load cookies to and from a file. This class uses the Mozilla/Netscape
- `cookies.txt' format. curl and lynx use this file format, too.
+ 'cookies.txt' format. curl and lynx use this file format, too.
Don't expect cookies saved while the browser is running to be noticed by
the browser (in fact, Mozilla on unix will overwrite your saved cookies if
diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
index 57791c6ab..e0e2cd4b6 100644
--- a/Lib/http/cookies.py
+++ b/Lib/http/cookies.py
@@ -87,9 +87,9 @@ within a string. Escaped quotation marks, nested semicolons, and other
such trickeries do not confuse it.
>>> C = cookies.SimpleCookie()
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
>>> print(C)
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
Each element of the Cookie also supports all of the RFC 2109
Cookie attributes. Here's an example which sets the Path
@@ -170,6 +170,15 @@ _Translator.update({
})
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
+_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
+
+
+def _has_control_character(*val):
+ """Detects control characters within a value.
+ Supports any type, as header values can be any type.
+ """
+ return any(_control_character_re.search(str(v)) for v in val)
+
def _quote(str):
r"""Quote a string for use in a cookie header.
@@ -264,17 +273,19 @@ class Morsel(dict):
"httponly" : "HttpOnly",
"version" : "Version",
"samesite" : "SameSite",
+ "partitioned": "Partitioned",
}
- _flags = {'secure', 'httponly'}
+ _reserved_defaults = dict.fromkeys(_reserved, "")
+
+ _flags = {'secure', 'httponly', 'partitioned'}
def __init__(self):
# Set defaults
self._key = self._value = self._coded_value = None
# Set default attributes
- for key in self._reserved:
- dict.__setitem__(self, key, "")
+ dict.update(self, self._reserved_defaults)
@property
def key(self):
@@ -292,12 +303,16 @@ class Morsel(dict):
K = K.lower()
if not K in self._reserved:
raise CookieError("Invalid attribute %r" % (K,))
+ if _has_control_character(K, V):
+ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
dict.__setitem__(self, K, V)
def setdefault(self, key, val=None):
key = key.lower()
if key not in self._reserved:
raise CookieError("Invalid attribute %r" % (key,))
+ if _has_control_character(key, val):
+ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
return dict.setdefault(self, key, val)
def __eq__(self, morsel):
@@ -333,6 +348,9 @@ class Morsel(dict):
raise CookieError('Attempt to set a reserved key %r' % (key,))
if not _is_legal_key(key):
raise CookieError('Illegal key %r' % (key,))
+ if _has_control_character(key, val, coded_val):
+ raise CookieError(
+ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))
# It's a good key, so save it.
self._key = key
@@ -486,7 +504,10 @@ class BaseCookie(dict):
result = []
items = sorted(self.items())
for key, value in items:
- result.append(value.output(attrs, header))
+ value_output = value.output(attrs, header)
+ if _has_control_character(value_output):
+ raise CookieError("Control characters are not allowed in cookies")
+ result.append(value_output)
return sep.join(result)
__str__ = output
diff --git a/Lib/http/server.py b/Lib/http/server.py
index 0ec479003..ac1f57c29 100644
--- a/Lib/http/server.py
+++ b/Lib/http/server.py
@@ -83,8 +83,10 @@ XXX To do:
__version__ = "0.6"
__all__ = [
- "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
- "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
+ "HTTPServer", "ThreadingHTTPServer",
+ "HTTPSServer", "ThreadingHTTPSServer",
+ "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
+ "CGIHTTPRequestHandler",
]
import copy
@@ -99,7 +101,7 @@ import os
import posixpath
import select
import shutil
-import socket # For gethostbyaddr()
+import socket
import socketserver
import sys
import time
@@ -114,6 +116,11 @@ DEFAULT_ERROR_MESSAGE = """\
+
Error response
@@ -133,7 +140,8 @@ _MIN_READ_BUF_SIZE = 1 << 20
class HTTPServer(socketserver.TCPServer):
- allow_reuse_address = 1 # Seems to make sense in testing environment
+ allow_reuse_address = True # Seems to make sense in testing environment
+ allow_reuse_port = False
def server_bind(self):
"""Override server_bind to store the server name."""
@@ -147,6 +155,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
+class HTTPSServer(HTTPServer):
+ def __init__(self, server_address, RequestHandlerClass,
+ bind_and_activate=True, *, certfile, keyfile=None,
+ password=None, alpn_protocols=None):
+ try:
+ import ssl
+ except ImportError:
+ raise RuntimeError("SSL module is missing; "
+ "HTTPS support is unavailable")
+
+ self.ssl = ssl
+ self.certfile = certfile
+ self.keyfile = keyfile
+ self.password = password
+ # Support by default HTTP/1.1
+ self.alpn_protocols = (
+ ["http/1.1"] if alpn_protocols is None else alpn_protocols
+ )
+
+ super().__init__(server_address,
+ RequestHandlerClass,
+ bind_and_activate)
+
+ def server_activate(self):
+ """Wrap the socket in SSLSocket."""
+ super().server_activate()
+ context = self._create_context()
+ self.socket = context.wrap_socket(self.socket, server_side=True)
+
+ def _create_context(self):
+ """Create a secure SSL context."""
+ context = self.ssl.create_default_context(self.ssl.Purpose.CLIENT_AUTH)
+ context.load_cert_chain(self.certfile, self.keyfile, self.password)
+ context.set_alpn_protocols(self.alpn_protocols)
+ return context
+
+
+class ThreadingHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer):
+ daemon_threads = True
+
+
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
"""HTTP request handler base class.
@@ -817,6 +866,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
r.append('')
r.append('')
r.append(f'')
+ r.append('')
r.append(f'{title}\n')
r.append(f'\n{title}
')
r.append('
\n')
@@ -1281,7 +1331,8 @@ def _get_best_family(*address):
def test(HandlerClass=BaseHTTPRequestHandler,
ServerClass=ThreadingHTTPServer,
- protocol="HTTP/1.0", port=8000, bind=None):
+ protocol="HTTP/1.0", port=8000, bind=None,
+ tls_cert=None, tls_key=None, tls_password=None):
"""Test the HTTP request handler class.
This runs an HTTP server on port 8000 (or the port argument).
@@ -1289,12 +1340,20 @@ def test(HandlerClass=BaseHTTPRequestHandler,
"""
ServerClass.address_family, addr = _get_best_family(bind, port)
HandlerClass.protocol_version = protocol
- with ServerClass(addr, HandlerClass) as httpd:
+
+ if tls_cert:
+ server = ServerClass(addr, HandlerClass, certfile=tls_cert,
+ keyfile=tls_key, password=tls_password)
+ else:
+ server = ServerClass(addr, HandlerClass)
+
+ with server as httpd:
host, port = httpd.socket.getsockname()[:2]
url_host = f'[{host}]' if ':' in host else host
+ protocol = 'HTTPS' if tls_cert else 'HTTP'
print(
- f"Serving HTTP on {host} port {port} "
- f"(http://{url_host}:{port}/) ..."
+ f"Serving {protocol} on {host} port {port} "
+ f"({protocol.lower()}://{url_host}:{port}/) ..."
)
try:
httpd.serve_forever()
@@ -1306,7 +1365,7 @@ if __name__ == '__main__':
import argparse
import contextlib
- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(color=True)
parser.add_argument('--cgi', action='store_true',
help='run as CGI server')
parser.add_argument('-b', '--bind', metavar='ADDRESS',
@@ -1319,17 +1378,38 @@ if __name__ == '__main__':
default='HTTP/1.0',
help='conform to this HTTP version '
'(default: %(default)s)')
+ parser.add_argument('--tls-cert', metavar='PATH',
+ help='path to the TLS certificate chain file')
+ parser.add_argument('--tls-key', metavar='PATH',
+ help='path to the TLS key file')
+ parser.add_argument('--tls-password-file', metavar='PATH',
+ help='path to the password file for the TLS key')
parser.add_argument('port', default=8000, type=int, nargs='?',
help='bind to this port '
'(default: %(default)s)')
args = parser.parse_args()
+
+ if not args.tls_cert and args.tls_key:
+ parser.error("--tls-key requires --tls-cert to be set")
+
+ tls_key_password = None
+ if args.tls_password_file:
+ if not args.tls_cert:
+ parser.error("--tls-password-file requires --tls-cert to be set")
+
+ try:
+ with open(args.tls_password_file, "r", encoding="utf-8") as f:
+ tls_key_password = f.read().strip()
+ except OSError as e:
+ parser.error(f"Failed to read TLS password file: {e}")
+
if args.cgi:
handler_class = CGIHTTPRequestHandler
else:
handler_class = SimpleHTTPRequestHandler
# ensure dual-stack is not disabled; ref #38907
- class DualStackServer(ThreadingHTTPServer):
+ class DualStackServerMixin:
def server_bind(self):
# suppress exception when protocol is IPv4
@@ -1342,10 +1422,20 @@ if __name__ == '__main__':
self.RequestHandlerClass(request, client_address, self,
directory=args.directory)
+ class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer):
+ pass
+ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer):
+ pass
+
+ ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer
+
test(
HandlerClass=handler_class,
- ServerClass=DualStackServer,
+ ServerClass=ServerClass,
port=args.port,
bind=args.bind,
protocol=args.protocol,
+ tls_cert=args.tls_cert,
+ tls_key=args.tls_key,
+ tls_password=tls_key_password,
)
diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py
index 51fa4a3d4..04cb440cd 100644
--- a/Lib/test/test_http_cookiejar.py
+++ b/Lib/test/test_http_cookiejar.py
@@ -7,7 +7,6 @@ import re
from test import support
from test.support import os_helper
from test.support import warnings_helper
-from test.support.testcase import ExtraAssertions
import time
import unittest
import urllib.request
@@ -222,11 +221,19 @@ class HeaderTests(unittest.TestCase):
expected = [[("expires", "01 Jan 2040 22:23:32 GMT"), ("version", "0")]]
self.assertEqual(parse_ns_headers([hdr]), expected)
- def test_join_header_words(self):
- joined = join_header_words([[("foo", None), ("bar", "baz")]])
- self.assertEqual(joined, "foo; bar=baz")
-
- self.assertEqual(join_header_words([[]]), "")
+ @support.subTests('src,expected', [
+ ([[("foo", None), ("bar", "baz")]], "foo; bar=baz"),
+ (([]), ""),
+ (([[]]), ""),
+ (([[("a", "_")]]), "a=_"),
+ (([[("a", ";")]]), 'a=";"'),
+ ([[("n", None), ("foo", "foo;_")], [("bar", "foo_bar")]],
+ 'n; foo="foo;_", bar=foo_bar'),
+ ([[("n", "m"), ("foo", None)], [("bar", "foo_bar")]],
+ 'n=m; foo, bar=foo_bar'),
+ ])
+ def test_join_header_words(self, src, expected):
+ self.assertEqual(join_header_words(src), expected)
@support.subTests('arg,expect', [
("foo", [[("foo", None)]]),
@@ -1427,7 +1434,7 @@ class CookieTests(unittest.TestCase):
self.assertIsNone(cookie.expires)
-class LWPCookieTests(unittest.TestCase, ExtraAssertions):
+class LWPCookieTests(unittest.TestCase):
# Tests taken from libwww-perl, with a few modifications and additions.
def test_netscape_example_1(self):
diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
index 3e0b4d1d5..11861b0e8 100644
--- a/Lib/test/test_http_cookies.py
+++ b/Lib/test/test_http_cookies.py
@@ -6,10 +6,9 @@ import doctest
from http import cookies
import pickle
from test import support
-from test.support.testcase import ExtraAssertions
-class CookieTests(unittest.TestCase, ExtraAssertions):
+class CookieTests(unittest.TestCase):
def test_basic(self):
cases = [
@@ -18,10 +17,10 @@ class CookieTests(unittest.TestCase, ExtraAssertions):
'repr': "",
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
- 'repr': '''''',
- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
+ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"',
+ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'},
+ 'repr': '''''',
+ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'},
# Check illegal cookies that have an '=' char in an unquoted value
{'data': 'keebler=E=mc2',
@@ -132,8 +131,8 @@ class CookieTests(unittest.TestCase, ExtraAssertions):
@support.requires_resource('cpu')
def test_unquote_large(self):
- #n = 10**6
- n = 10**4 # XXX: RUSTPYTHON; This takes more than 10 minutes to run. lower to 4
+ # n = 10**6
+ n = 10**4 # XXX: RUSTPYTHON; This takes more than 10 minutes to run. lower to 4
for encoded in r'\\', r'\134':
with self.subTest(encoded):
data = 'a="b=' + encoded*n + ';"'
@@ -207,6 +206,14 @@ class CookieTests(unittest.TestCase, ExtraAssertions):
self.assertEqual(C.output(),
'Set-Cookie: Customer="WILE_E_COYOTE"; HttpOnly; Secure')
+ def test_set_secure_httponly_partitioned_attrs(self):
+ C = cookies.SimpleCookie('Customer="WILE_E_COYOTE"')
+ C['Customer']['secure'] = True
+ C['Customer']['httponly'] = True
+ C['Customer']['partitioned'] = True
+ self.assertEqual(C.output(),
+ 'Set-Cookie: Customer="WILE_E_COYOTE"; HttpOnly; Partitioned; Secure')
+
def test_samesite_attrs(self):
samesite_values = ['Strict', 'Lax', 'strict', 'lax']
for val in samesite_values:
@@ -565,6 +572,50 @@ class MorselTests(unittest.TestCase):
r'Set-Cookie: key=coded_val; '
r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+')
+ def test_control_characters(self):
+ for c0 in support.control_characters_c0():
+ morsel = cookies.Morsel()
+
+ # .__setitem__()
+ with self.assertRaises(cookies.CookieError):
+ morsel[c0] = "val"
+ with self.assertRaises(cookies.CookieError):
+ morsel["path"] = c0
+
+ # .setdefault()
+ with self.assertRaises(cookies.CookieError):
+ morsel.setdefault("path", c0)
+ with self.assertRaises(cookies.CookieError):
+ morsel.setdefault(c0, "val")
+
+ # .set()
+ with self.assertRaises(cookies.CookieError):
+ morsel.set(c0, "val", "coded-value")
+ with self.assertRaises(cookies.CookieError):
+ morsel.set("path", c0, "coded-value")
+ with self.assertRaises(cookies.CookieError):
+ morsel.set("path", "val", c0)
+
+ def test_control_characters_output(self):
+ # Tests that even if the internals of Morsel are modified
+ # that a call to .output() has control character safeguards.
+ for c0 in support.control_characters_c0():
+ morsel = cookies.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel._key = c0 # Override private variable.
+ cookie = cookies.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(cookies.CookieError):
+ cookie.output()
+
+ morsel = cookies.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel._coded_value = c0 # Override private variable.
+ cookie = cookies.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(cookies.CookieError):
+ cookie.output()
+
def load_tests(loader, tests, pattern):
tests.addTest(doctest.DocTestSuite(cookies))
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
index 5267d2fe0..8ce5f853b 100644
--- a/Lib/test/test_httplib.py
+++ b/Lib/test/test_httplib.py
@@ -16,7 +16,6 @@ TestCase = unittest.TestCase
from test import support
from test.support import os_helper
from test.support import socket_helper
-from test.support.testcase import ExtraAssertions
support.requires_working_socket(module=True)
@@ -135,7 +134,7 @@ class FakeSocketHTTPConnection(client.HTTPConnection):
def create_connection(self, *pos, **kw):
return FakeSocket(*self.fake_socket_args)
-class HeaderTests(TestCase, ExtraAssertions):
+class HeaderTests(TestCase):
def test_auto_headers(self):
# Some headers are added automatically, but should not be added by
# .request() if they are explicitly set.
@@ -538,7 +537,7 @@ class TransferEncodingTest(TestCase):
return b''.join(body)
-class BasicTest(TestCase, ExtraAssertions):
+class BasicTest(TestCase):
def test_dir_with_added_behavior_on_status(self):
# see issue40084
self.assertTrue({'description', 'name', 'phrase', 'value'} <= set(dir(HTTPStatus(404))))
@@ -595,8 +594,9 @@ class BasicTest(TestCase, ExtraAssertions):
CONTINUE = 100, 'Continue', 'Request received, please continue'
SWITCHING_PROTOCOLS = (101, 'Switching Protocols',
'Switching to new protocol; obey Upgrade header')
- PROCESSING = 102, 'Processing'
- EARLY_HINTS = 103, 'Early Hints'
+ PROCESSING = 102, 'Processing', 'Server is processing the request'
+ EARLY_HINTS = (103, 'Early Hints',
+ 'Headers sent to prepare for the response')
# success
OK = 200, 'OK', 'Request fulfilled, document follows'
CREATED = 201, 'Created', 'Document created, URL follows'
@@ -607,9 +607,11 @@ class BasicTest(TestCase, ExtraAssertions):
NO_CONTENT = 204, 'No Content', 'Request fulfilled, nothing follows'
RESET_CONTENT = 205, 'Reset Content', 'Clear input form for further input'
PARTIAL_CONTENT = 206, 'Partial Content', 'Partial content follows'
- MULTI_STATUS = 207, 'Multi-Status'
- ALREADY_REPORTED = 208, 'Already Reported'
- IM_USED = 226, 'IM Used'
+ MULTI_STATUS = (207, 'Multi-Status',
+ 'Response contains multiple statuses in the body')
+ ALREADY_REPORTED = (208, 'Already Reported',
+ 'Operation has already been reported')
+ IM_USED = 226, 'IM Used', 'Request completed using instance manipulations'
# redirection
MULTIPLE_CHOICES = (300, 'Multiple Choices',
'Object has several resources -- see URI list')
@@ -666,15 +668,19 @@ class BasicTest(TestCase, ExtraAssertions):
EXPECTATION_FAILED = (417, 'Expectation Failed',
'Expect condition could not be satisfied')
IM_A_TEAPOT = (418, 'I\'m a Teapot',
- 'Server refuses to brew coffee because it is a teapot.')
+ 'Server refuses to brew coffee because it is a teapot')
MISDIRECTED_REQUEST = (421, 'Misdirected Request',
'Server is not able to produce a response')
- UNPROCESSABLE_CONTENT = 422, 'Unprocessable Content'
+ UNPROCESSABLE_CONTENT = (422, 'Unprocessable Content',
+ 'Server is not able to process the contained instructions')
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
- LOCKED = 423, 'Locked'
- FAILED_DEPENDENCY = 424, 'Failed Dependency'
- TOO_EARLY = 425, 'Too Early'
- UPGRADE_REQUIRED = 426, 'Upgrade Required'
+ LOCKED = 423, 'Locked', 'Resource of a method is locked'
+ FAILED_DEPENDENCY = (424, 'Failed Dependency',
+ 'Dependent action of the request failed')
+ TOO_EARLY = (425, 'Too Early',
+ 'Server refuses to process a request that might be replayed')
+ UPGRADE_REQUIRED = (426, 'Upgrade Required',
+ 'Server refuses to perform the request using the current protocol')
PRECONDITION_REQUIRED = (428, 'Precondition Required',
'The origin server requires the request to be conditional')
TOO_MANY_REQUESTS = (429, 'Too Many Requests',
@@ -701,10 +707,14 @@ class BasicTest(TestCase, ExtraAssertions):
'The gateway server did not receive a timely response')
HTTP_VERSION_NOT_SUPPORTED = (505, 'HTTP Version Not Supported',
'Cannot fulfill request')
- VARIANT_ALSO_NEGOTIATES = 506, 'Variant Also Negotiates'
- INSUFFICIENT_STORAGE = 507, 'Insufficient Storage'
- LOOP_DETECTED = 508, 'Loop Detected'
- NOT_EXTENDED = 510, 'Not Extended'
+ VARIANT_ALSO_NEGOTIATES = (506, 'Variant Also Negotiates',
+ 'Server has an internal configuration error')
+ INSUFFICIENT_STORAGE = (507, 'Insufficient Storage',
+ 'Server is not able to store the representation')
+ LOOP_DETECTED = (508, 'Loop Detected',
+ 'Server encountered an infinite loop while processing a request')
+ NOT_EXTENDED = (510, 'Not Extended',
+ 'Request does not meet the resource access policy')
NETWORK_AUTHENTICATION_REQUIRED = (511,
'Network Authentication Required',
'The client needs to authenticate to gain network access')
@@ -1560,7 +1570,7 @@ class BasicTest(TestCase, ExtraAssertions):
conn.putrequest('GET', '/☃')
-class ExtendedReadTest(TestCase, ExtraAssertions):
+class ExtendedReadTest(TestCase):
"""
Test peek(), read1(), readline()
"""
@@ -1753,7 +1763,7 @@ class Readliner:
raise
-class OfflineTest(TestCase, ExtraAssertions):
+class OfflineTest(TestCase):
def test_all(self):
# Documented objects defined in the module should be in __all__
expected = {"responses"} # Allowlist documented dict() object
@@ -2111,6 +2121,7 @@ class HTTPSTest(TestCase):
self.addCleanup(resp.close)
self.assertEqual(resp.status, 404)
+ @unittest.skip("TODO: RUSTPYTHON; Flaky on CI")
def test_local_bad_hostname(self):
# The (valid) cert doesn't validate the HTTPS hostname
import ssl
@@ -2156,10 +2167,11 @@ class HTTPSTest(TestCase):
self.assertEqual(h, c.host)
self.assertEqual(p, c.port)
+ @unittest.expectedFailure # TODO: RUSTPYTHON
def test_tls13_pha(self):
import ssl
- if not ssl.HAS_TLSv1_3:
- self.skipTest('TLS 1.3 support required')
+ if not ssl.HAS_TLSv1_3 or not ssl.HAS_PHA:
+ self.skipTest('TLS 1.3 PHA support required')
# just check status of PHA flag
h = client.HTTPSConnection('localhost', 443)
self.assertTrue(h._context.post_handshake_auth)
@@ -2307,7 +2319,7 @@ class HTTPResponseTest(TestCase):
header = self.resp.getheader('No-Such-Header',default=42)
self.assertEqual(header, 42)
-class TunnelTests(TestCase, ExtraAssertions):
+class TunnelTests(TestCase):
def setUp(self):
response_text = (
'HTTP/1.1 200 OK\r\n\r\n' # Reply to CONNECT
diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py
index 951be1e24..b7885f5d2 100644
--- a/Lib/test/test_httpservers.py
+++ b/Lib/test/test_httpservers.py
@@ -4,7 +4,7 @@ Written by Cody A.W. Somerville ,
Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
"""
from collections import OrderedDict
-from http.server import BaseHTTPRequestHandler, HTTPServer, \
+from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
from http import server, HTTPStatus
@@ -32,9 +32,13 @@ from io import BytesIO, StringIO
import unittest
from test import support
from test.support import (
- is_apple, os_helper, requires_subprocess, threading_helper
+ is_apple, import_helper, os_helper, requires_subprocess, threading_helper
)
-from test.support.testcase import ExtraAssertions
+
+try:
+ import ssl
+except ImportError:
+ ssl = None
support.requires_working_socket(module=True)
@@ -47,14 +51,49 @@ class NoLogRequestHandler:
return ''
+class DummyRequestHandler(NoLogRequestHandler, SimpleHTTPRequestHandler):
+ pass
+
+
+def create_https_server(
+ certfile,
+ keyfile=None,
+ password=None,
+ *,
+ address=('localhost', 0),
+ request_handler=DummyRequestHandler,
+):
+ return HTTPSServer(
+ address, request_handler,
+ certfile=certfile, keyfile=keyfile, password=password
+ )
+
+
+class TestSSLDisabled(unittest.TestCase):
+ def test_https_server_raises_runtime_error(self):
+ with import_helper.isolated_modules():
+ sys.modules['ssl'] = None
+ certfile = certdata_file("keycert.pem")
+ with self.assertRaises(RuntimeError):
+ create_https_server(certfile)
+
+
class TestServerThread(threading.Thread):
- def __init__(self, test_object, request_handler):
+ def __init__(self, test_object, request_handler, tls=None):
threading.Thread.__init__(self)
self.request_handler = request_handler
self.test_object = test_object
+ self.tls = tls
def run(self):
- self.server = HTTPServer(('localhost', 0), self.request_handler)
+ if self.tls:
+ certfile, keyfile, password = self.tls
+ self.server = create_https_server(
+ certfile, keyfile, password,
+ request_handler=self.request_handler,
+ )
+ else:
+ self.server = HTTPServer(('localhost', 0), self.request_handler)
self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname()
self.test_object.server_started.set()
self.test_object = None
@@ -68,12 +107,16 @@ class TestServerThread(threading.Thread):
self.join()
-class BaseTestCase(unittest.TestCase, ExtraAssertions):
+class BaseTestCase(unittest.TestCase):
+
+ # Optional tuple (certfile, keyfile, password) to use for HTTPS servers.
+ tls = None
+
def setUp(self):
self._threads = threading_helper.threading_setup()
os.environ = os_helper.EnvironmentVarGuard()
self.server_started = threading.Event()
- self.thread = TestServerThread(self, self.request_handler)
+ self.thread = TestServerThread(self, self.request_handler, self.tls)
self.thread.start()
self.server_started.wait()
@@ -355,6 +398,74 @@ class HTTP09ServerTestCase(BaseTestCase):
self.assertEqual(res, b'')
+def certdata_file(*path):
+ return os.path.join(os.path.dirname(__file__), "certdata", *path)
+
+
+@unittest.skipIf(ssl is None, "requires ssl")
+class BaseHTTPSServerTestCase(BaseTestCase):
+ CERTFILE = certdata_file("keycert.pem")
+ ONLYCERT = certdata_file("ssl_cert.pem")
+ ONLYKEY = certdata_file("ssl_key.pem")
+ CERTFILE_PROTECTED = certdata_file("keycert.passwd.pem")
+ ONLYKEY_PROTECTED = certdata_file("ssl_key.passwd.pem")
+ EMPTYCERT = certdata_file("nullcert.pem")
+ BADCERT = certdata_file("badcert.pem")
+ KEY_PASSWORD = "somepass"
+ BADPASSWORD = "badpass"
+
+ tls = (ONLYCERT, ONLYKEY, None) # values by default
+
+ request_handler = DummyRequestHandler
+
+ def test_get(self):
+ response = self.request('/')
+ self.assertEqual(response.status, HTTPStatus.OK)
+
+ def request(self, uri, method='GET', body=None, headers={}):
+ context = ssl._create_unverified_context()
+ self.connection = http.client.HTTPSConnection(
+ self.HOST, self.PORT, context=context
+ )
+ self.connection.request(method, uri, body, headers)
+ return self.connection.getresponse()
+
+ def test_valid_certdata(self):
+ valid_certdata= [
+ (self.CERTFILE, None, None),
+ (self.CERTFILE, self.CERTFILE, None),
+ (self.CERTFILE_PROTECTED, None, self.KEY_PASSWORD),
+ (self.ONLYCERT, self.ONLYKEY_PROTECTED, self.KEY_PASSWORD),
+ ]
+ for certfile, keyfile, password in valid_certdata:
+ with self.subTest(
+ certfile=certfile, keyfile=keyfile, password=password
+ ):
+ server = create_https_server(certfile, keyfile, password)
+ self.assertIsInstance(server, HTTPSServer)
+ server.server_close()
+
+ def test_invalid_certdata(self):
+ invalid_certdata = [
+ (self.BADCERT, None, None),
+ (self.EMPTYCERT, None, None),
+ (self.ONLYCERT, None, None),
+ (self.ONLYKEY, None, None),
+ (self.ONLYKEY, self.ONLYCERT, None),
+ (self.CERTFILE_PROTECTED, None, self.BADPASSWORD),
+ # TODO: test the next case and add same case to test_ssl (We
+ # specify a cert and a password-protected file, but no password):
+ # (self.CERTFILE_PROTECTED, None, None),
+ # see issue #132102
+ ]
+ for certfile, keyfile, password in invalid_certdata:
+ with self.subTest(
+ certfile=certfile, keyfile=keyfile, password=password
+ ):
+ with self.assertRaises(ssl.SSLError):
+ create_https_server(certfile, keyfile, password)
+
+
class RequestHandlerLoggingTestCase(BaseTestCase):
class request_handler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
@@ -1153,7 +1264,7 @@ class AuditableBytesIO:
return len(self.datas)
-class BaseHTTPRequestHandlerTestCase(unittest.TestCase, ExtraAssertions):
+class BaseHTTPRequestHandlerTestCase(unittest.TestCase):
"""Test the functionality of the BaseHTTPServer.
Test the support for the Expect 100-continue header.
diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs
index fefca0444..20db9b6e0 100644
--- a/crates/stdlib/src/ssl.rs
+++ b/crates/stdlib/src/ssl.rs
@@ -2018,25 +2018,26 @@ mod _ssl {
) -> PyResult<(Option, Option)> {
match password {
OptionalArg::Present(p) => {
- // Try string first
+ if vm.is_none(p) {
+ return Ok((None, None));
+ }
+
+ // Try string
if let Ok(pwd_str) = PyUtf8StrRef::try_from_object(vm, p.clone()) {
Ok((Some(pwd_str.as_str().to_owned()), None))
}
// Try bytes-like
else if let Ok(pwd_bytes_like) = ArgBytesLike::try_from_object(vm, p.clone())
{
- let pwd = String::from_utf8(pwd_bytes_like.borrow_buf().to_vec()).map_err(
- |_| vm.new_type_error("password bytes must be valid UTF-8".to_owned()),
- )?;
+ let pwd = String::from_utf8(pwd_bytes_like.borrow_buf().to_vec())
+ .map_err(|_| vm.new_type_error("password bytes must be valid UTF-8"))?;
Ok((Some(pwd), None))
}
// Try callable
else if p.is_callable() {
Ok((None, Some(p.clone())))
} else {
- Err(vm.new_type_error(
- "password should be a string, bytes, or callable".to_owned(),
- ))
+ Err(vm.new_type_error("password should be a string or callable"))
}
}
_ => Ok((None, None)),