Skip to content

Commit 4c25370

Browse files
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 fa8fb38. * 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>
1 parent 684001f commit 4c25370

File tree

10 files changed

+395
-91
lines changed

10 files changed

+395
-91
lines changed

Lib/http/__init__.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ def is_server_error(self):
5454
CONTINUE = 100, 'Continue', 'Request received, please continue'
5555
SWITCHING_PROTOCOLS = (101, 'Switching Protocols',
5656
'Switching to new protocol; obey Upgrade header')
57-
PROCESSING = 102, 'Processing'
58-
EARLY_HINTS = 103, 'Early Hints'
57+
PROCESSING = 102, 'Processing', 'Server is processing the request'
58+
EARLY_HINTS = (103, 'Early Hints',
59+
'Headers sent to prepare for the response')
5960

6061
# success
6162
OK = 200, 'OK', 'Request fulfilled, document follows'
@@ -67,9 +68,11 @@ def is_server_error(self):
6768
NO_CONTENT = 204, 'No Content', 'Request fulfilled, nothing follows'
6869
RESET_CONTENT = 205, 'Reset Content', 'Clear input form for further input'
6970
PARTIAL_CONTENT = 206, 'Partial Content', 'Partial content follows'
70-
MULTI_STATUS = 207, 'Multi-Status'
71-
ALREADY_REPORTED = 208, 'Already Reported'
72-
IM_USED = 226, 'IM Used'
71+
MULTI_STATUS = (207, 'Multi-Status',
72+
'Response contains multiple statuses in the body')
73+
ALREADY_REPORTED = (208, 'Already Reported',
74+
'Operation has already been reported')
75+
IM_USED = 226, 'IM Used', 'Request completed using instance manipulations'
7376

7477
# redirection
7578
MULTIPLE_CHOICES = (300, 'Multiple Choices',
@@ -128,15 +131,19 @@ def is_server_error(self):
128131
EXPECTATION_FAILED = (417, 'Expectation Failed',
129132
'Expect condition could not be satisfied')
130133
IM_A_TEAPOT = (418, 'I\'m a Teapot',
131-
'Server refuses to brew coffee because it is a teapot.')
134+
'Server refuses to brew coffee because it is a teapot')
132135
MISDIRECTED_REQUEST = (421, 'Misdirected Request',
133136
'Server is not able to produce a response')
134-
UNPROCESSABLE_CONTENT = 422, 'Unprocessable Content'
137+
UNPROCESSABLE_CONTENT = (422, 'Unprocessable Content',
138+
'Server is not able to process the contained instructions')
135139
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
136-
LOCKED = 423, 'Locked'
137-
FAILED_DEPENDENCY = 424, 'Failed Dependency'
138-
TOO_EARLY = 425, 'Too Early'
139-
UPGRADE_REQUIRED = 426, 'Upgrade Required'
140+
LOCKED = 423, 'Locked', 'Resource of a method is locked'
141+
FAILED_DEPENDENCY = (424, 'Failed Dependency',
142+
'Dependent action of the request failed')
143+
TOO_EARLY = (425, 'Too Early',
144+
'Server refuses to process a request that might be replayed')
145+
UPGRADE_REQUIRED = (426, 'Upgrade Required',
146+
'Server refuses to perform the request using the current protocol')
140147
PRECONDITION_REQUIRED = (428, 'Precondition Required',
141148
'The origin server requires the request to be conditional')
142149
TOO_MANY_REQUESTS = (429, 'Too Many Requests',
@@ -164,10 +171,14 @@ def is_server_error(self):
164171
'The gateway server did not receive a timely response')
165172
HTTP_VERSION_NOT_SUPPORTED = (505, 'HTTP Version Not Supported',
166173
'Cannot fulfill request')
167-
VARIANT_ALSO_NEGOTIATES = 506, 'Variant Also Negotiates'
168-
INSUFFICIENT_STORAGE = 507, 'Insufficient Storage'
169-
LOOP_DETECTED = 508, 'Loop Detected'
170-
NOT_EXTENDED = 510, 'Not Extended'
174+
VARIANT_ALSO_NEGOTIATES = (506, 'Variant Also Negotiates',
175+
'Server has an internal configuration error')
176+
INSUFFICIENT_STORAGE = (507, 'Insufficient Storage',
177+
'Server is not able to store the representation')
178+
LOOP_DETECTED = (508, 'Loop Detected',
179+
'Server encountered an infinite loop while processing a request')
180+
NOT_EXTENDED = (510, 'Not Extended',
181+
'Request does not meet the resource access policy')
171182
NETWORK_AUTHENTICATION_REQUIRED = (511,
172183
'Network Authentication Required',
173184
'The client needs to authenticate to gain network access')

Lib/http/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ def close(self):
10471047
response.close()
10481048

10491049
def send(self, data):
1050-
"""Send `data' to the server.
1050+
"""Send 'data' to the server.
10511051
``data`` can be a string object, a bytes object, an array object, a
10521052
file-like object that supports a .read() method, or an iterable object.
10531053
"""
@@ -1159,10 +1159,10 @@ def putrequest(self, method, url, skip_host=False,
11591159
skip_accept_encoding=False):
11601160
"""Send a request to the server.
11611161
1162-
`method' specifies an HTTP request method, e.g. 'GET'.
1163-
`url' specifies the object being requested, e.g. '/index.html'.
1164-
`skip_host' if True does not add automatically a 'Host:' header
1165-
`skip_accept_encoding' if True does not add automatically an
1162+
'method' specifies an HTTP request method, e.g. 'GET'.
1163+
'url' specifies the object being requested, e.g. '/index.html'.
1164+
'skip_host' if True does not add automatically a 'Host:' header
1165+
'skip_accept_encoding' if True does not add automatically an
11661166
'Accept-Encoding:' header
11671167
"""
11681168

Lib/http/cookiejar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1987,7 +1987,7 @@ class MozillaCookieJar(FileCookieJar):
19871987
19881988
This class differs from CookieJar only in the format it uses to save and
19891989
load cookies to and from a file. This class uses the Mozilla/Netscape
1990-
`cookies.txt' format. curl and lynx use this file format, too.
1990+
'cookies.txt' format. curl and lynx use this file format, too.
19911991
19921992
Don't expect cookies saved while the browser is running to be noticed by
19931993
the browser (in fact, Mozilla on unix will overwrite your saved cookies if

Lib/http/cookies.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@
8787
such trickeries do not confuse it.
8888
8989
>>> C = cookies.SimpleCookie()
90-
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
90+
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
9191
>>> print(C)
92-
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
92+
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
9393
9494
Each element of the Cookie also supports all of the RFC 2109
9595
Cookie attributes. Here's an example which sets the Path
@@ -170,6 +170,15 @@ class CookieError(Exception):
170170
})
171171

172172
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
173+
_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
174+
175+
176+
def _has_control_character(*val):
177+
"""Detects control characters within a value.
178+
Supports any type, as header values can be any type.
179+
"""
180+
return any(_control_character_re.search(str(v)) for v in val)
181+
173182

174183
def _quote(str):
175184
r"""Quote a string for use in a cookie header.
@@ -264,17 +273,19 @@ class Morsel(dict):
264273
"httponly" : "HttpOnly",
265274
"version" : "Version",
266275
"samesite" : "SameSite",
276+
"partitioned": "Partitioned",
267277
}
268278

269-
_flags = {'secure', 'httponly'}
279+
_reserved_defaults = dict.fromkeys(_reserved, "")
280+
281+
_flags = {'secure', 'httponly', 'partitioned'}
270282

271283
def __init__(self):
272284
# Set defaults
273285
self._key = self._value = self._coded_value = None
274286

275287
# Set default attributes
276-
for key in self._reserved:
277-
dict.__setitem__(self, key, "")
288+
dict.update(self, self._reserved_defaults)
278289

279290
@property
280291
def key(self):
@@ -292,12 +303,16 @@ def __setitem__(self, K, V):
292303
K = K.lower()
293304
if not K in self._reserved:
294305
raise CookieError("Invalid attribute %r" % (K,))
306+
if _has_control_character(K, V):
307+
raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
295308
dict.__setitem__(self, K, V)
296309

297310
def setdefault(self, key, val=None):
298311
key = key.lower()
299312
if key not in self._reserved:
300313
raise CookieError("Invalid attribute %r" % (key,))
314+
if _has_control_character(key, val):
315+
raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
301316
return dict.setdefault(self, key, val)
302317

303318
def __eq__(self, morsel):
@@ -333,6 +348,9 @@ def set(self, key, val, coded_val):
333348
raise CookieError('Attempt to set a reserved key %r' % (key,))
334349
if not _is_legal_key(key):
335350
raise CookieError('Illegal key %r' % (key,))
351+
if _has_control_character(key, val, coded_val):
352+
raise CookieError(
353+
"Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))
336354

337355
# It's a good key, so save it.
338356
self._key = key
@@ -486,7 +504,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
486504
result = []
487505
items = sorted(self.items())
488506
for key, value in items:
489-
result.append(value.output(attrs, header))
507+
value_output = value.output(attrs, header)
508+
if _has_control_character(value_output):
509+
raise CookieError("Control characters are not allowed in cookies")
510+
result.append(value_output)
490511
return sep.join(result)
491512

492513
__str__ = output

Lib/http/server.py

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@
8383
__version__ = "0.6"
8484

8585
__all__ = [
86-
"HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
87-
"SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
86+
"HTTPServer", "ThreadingHTTPServer",
87+
"HTTPSServer", "ThreadingHTTPSServer",
88+
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
89+
"CGIHTTPRequestHandler",
8890
]
8991

9092
import copy
@@ -99,7 +101,7 @@
99101
import posixpath
100102
import select
101103
import shutil
102-
import socket # For gethostbyaddr()
104+
import socket
103105
import socketserver
104106
import sys
105107
import time
@@ -114,6 +116,11 @@
114116
<html lang="en">
115117
<head>
116118
<meta charset="utf-8">
119+
<style type="text/css">
120+
:root {
121+
color-scheme: light dark;
122+
}
123+
</style>
117124
<title>Error response</title>
118125
</head>
119126
<body>
@@ -133,7 +140,8 @@
133140

134141
class HTTPServer(socketserver.TCPServer):
135142

136-
allow_reuse_address = 1 # Seems to make sense in testing environment
143+
allow_reuse_address = True # Seems to make sense in testing environment
144+
allow_reuse_port = False
137145

138146
def server_bind(self):
139147
"""Override server_bind to store the server name."""
@@ -147,6 +155,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
147155
daemon_threads = True
148156

149157

158+
class HTTPSServer(HTTPServer):
159+
def __init__(self, server_address, RequestHandlerClass,
160+
bind_and_activate=True, *, certfile, keyfile=None,
161+
password=None, alpn_protocols=None):
162+
try:
163+
import ssl
164+
except ImportError:
165+
raise RuntimeError("SSL module is missing; "
166+
"HTTPS support is unavailable")
167+
168+
self.ssl = ssl
169+
self.certfile = certfile
170+
self.keyfile = keyfile
171+
self.password = password
172+
# Support by default HTTP/1.1
173+
self.alpn_protocols = (
174+
["http/1.1"] if alpn_protocols is None else alpn_protocols
175+
)
176+
177+
super().__init__(server_address,
178+
RequestHandlerClass,
179+
bind_and_activate)
180+
181+
def server_activate(self):
182+
"""Wrap the socket in SSLSocket."""
183+
super().server_activate()
184+
context = self._create_context()
185+
self.socket = context.wrap_socket(self.socket, server_side=True)
186+
187+
def _create_context(self):
188+
"""Create a secure SSL context."""
189+
context = self.ssl.create_default_context(self.ssl.Purpose.CLIENT_AUTH)
190+
context.load_cert_chain(self.certfile, self.keyfile, self.password)
191+
context.set_alpn_protocols(self.alpn_protocols)
192+
return context
193+
194+
195+
class ThreadingHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer):
196+
daemon_threads = True
197+
198+
150199
class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
151200

152201
"""HTTP request handler base class.
@@ -817,6 +866,7 @@ def list_directory(self, path):
817866
r.append('<html lang="en">')
818867
r.append('<head>')
819868
r.append(f'<meta charset="{enc}">')
869+
r.append('<style type="text/css">\n:root {\ncolor-scheme: light dark;\n}\n</style>')
820870
r.append(f'<title>{title}</title>\n</head>')
821871
r.append(f'<body>\n<h1>{title}</h1>')
822872
r.append('<hr>\n<ul>')
@@ -1281,20 +1331,29 @@ def _get_best_family(*address):
12811331

12821332
def test(HandlerClass=BaseHTTPRequestHandler,
12831333
ServerClass=ThreadingHTTPServer,
1284-
protocol="HTTP/1.0", port=8000, bind=None):
1334+
protocol="HTTP/1.0", port=8000, bind=None,
1335+
tls_cert=None, tls_key=None, tls_password=None):
12851336
"""Test the HTTP request handler class.
12861337
12871338
This runs an HTTP server on port 8000 (or the port argument).
12881339
12891340
"""
12901341
ServerClass.address_family, addr = _get_best_family(bind, port)
12911342
HandlerClass.protocol_version = protocol
1292-
with ServerClass(addr, HandlerClass) as httpd:
1343+
1344+
if tls_cert:
1345+
server = ServerClass(addr, HandlerClass, certfile=tls_cert,
1346+
keyfile=tls_key, password=tls_password)
1347+
else:
1348+
server = ServerClass(addr, HandlerClass)
1349+
1350+
with server as httpd:
12931351
host, port = httpd.socket.getsockname()[:2]
12941352
url_host = f'[{host}]' if ':' in host else host
1353+
protocol = 'HTTPS' if tls_cert else 'HTTP'
12951354
print(
1296-
f"Serving HTTP on {host} port {port} "
1297-
f"(http://{url_host}:{port}/) ..."
1355+
f"Serving {protocol} on {host} port {port} "
1356+
f"({protocol.lower()}://{url_host}:{port}/) ..."
12981357
)
12991358
try:
13001359
httpd.serve_forever()
@@ -1306,7 +1365,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
13061365
import argparse
13071366
import contextlib
13081367

1309-
parser = argparse.ArgumentParser()
1368+
parser = argparse.ArgumentParser(color=True)
13101369
parser.add_argument('--cgi', action='store_true',
13111370
help='run as CGI server')
13121371
parser.add_argument('-b', '--bind', metavar='ADDRESS',
@@ -1319,17 +1378,38 @@ def test(HandlerClass=BaseHTTPRequestHandler,
13191378
default='HTTP/1.0',
13201379
help='conform to this HTTP version '
13211380
'(default: %(default)s)')
1381+
parser.add_argument('--tls-cert', metavar='PATH',
1382+
help='path to the TLS certificate chain file')
1383+
parser.add_argument('--tls-key', metavar='PATH',
1384+
help='path to the TLS key file')
1385+
parser.add_argument('--tls-password-file', metavar='PATH',
1386+
help='path to the password file for the TLS key')
13221387
parser.add_argument('port', default=8000, type=int, nargs='?',
13231388
help='bind to this port '
13241389
'(default: %(default)s)')
13251390
args = parser.parse_args()
1391+
1392+
if not args.tls_cert and args.tls_key:
1393+
parser.error("--tls-key requires --tls-cert to be set")
1394+
1395+
tls_key_password = None
1396+
if args.tls_password_file:
1397+
if not args.tls_cert:
1398+
parser.error("--tls-password-file requires --tls-cert to be set")
1399+
1400+
try:
1401+
with open(args.tls_password_file, "r", encoding="utf-8") as f:
1402+
tls_key_password = f.read().strip()
1403+
except OSError as e:
1404+
parser.error(f"Failed to read TLS password file: {e}")
1405+
13261406
if args.cgi:
13271407
handler_class = CGIHTTPRequestHandler
13281408
else:
13291409
handler_class = SimpleHTTPRequestHandler
13301410

13311411
# ensure dual-stack is not disabled; ref #38907
1332-
class DualStackServer(ThreadingHTTPServer):
1412+
class DualStackServerMixin:
13331413

13341414
def server_bind(self):
13351415
# suppress exception when protocol is IPv4
@@ -1342,10 +1422,20 @@ def finish_request(self, request, client_address):
13421422
self.RequestHandlerClass(request, client_address, self,
13431423
directory=args.directory)
13441424

1425+
class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer):
1426+
pass
1427+
class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer):
1428+
pass
1429+
1430+
ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer
1431+
13451432
test(
13461433
HandlerClass=handler_class,
1347-
ServerClass=DualStackServer,
1434+
ServerClass=ServerClass,
13481435
port=args.port,
13491436
bind=args.bind,
13501437
protocol=args.protocol,
1438+
tls_cert=args.tls_cert,
1439+
tls_key=args.tls_key,
1440+
tls_password=tls_key_password,
13511441
)

0 commit comments

Comments
 (0)