Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 58 additions & 23 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@
_MAXLINE = 65536
_MAXHEADERS = 100

# maximal number of interim (1xx) responses tolerated before the final
# response. Real servers send at most a few; without a bound, a server
# streaming "100 Continue" responses would hang getresponse() forever.
# A socket timeout cannot detect that as data keeps arriving within every
# timeout window.
_MAXINTERIMRESPONSES = 100

# Data larger than this will be read in chunks, to prevent extreme
# overallocation.
_MIN_READ_BUF_SIZE = 1 << 20
Expand Down Expand Up @@ -186,11 +193,10 @@ def _strip_ipv6_iface(enc_name: bytes) -> bytes:
return enc_name

class HTTPMessage(email.message.Message):
# XXX The only usage of this method is in
# http.server.CGIHTTPRequestHandler. Maybe move the code there so
# that it doesn't need to be part of the public API. The API has
# never been defined so this could cause backwards compatibility
# issues.

# The getallmatchingheaders() method was only used by the CGI handler
# that was removed in Python 3.15. However, since the public API was not
# properly defined, it will be kept for backwards compatibility reasons.

def getallmatchingheaders(self, name):
"""Find all header lines matching a given header name.
Expand All @@ -215,22 +221,24 @@ def getallmatchingheaders(self, name):
lst.append(line)
return lst

def _read_headers(fp):
def _read_headers(fp, max_headers):
"""Reads potential header lines into a list from a file pointer.
Length of line is limited by _MAXLINE, and number of
headers is limited by _MAXHEADERS.
headers is limited by max_headers.
"""
headers = []
if max_headers is None:
max_headers = _MAXHEADERS
while True:
line = fp.readline(_MAXLINE + 1)
if len(line) > _MAXLINE:
raise LineTooLong("header line")
headers.append(line)
if len(headers) > _MAXHEADERS:
raise HTTPException("got more than %d headers" % _MAXHEADERS)
if line in (b'\r\n', b'\n', b''):
break
headers.append(line)
if len(headers) > max_headers:
raise HTTPException(f"got more than {max_headers} headers")
return headers

def _parse_header_lines(header_lines, _class=HTTPMessage):
Expand All @@ -247,10 +255,10 @@ def _parse_header_lines(header_lines, _class=HTTPMessage):
hstring = b''.join(header_lines).decode('iso-8859-1')
return email.parser.Parser(_class=_class).parsestr(hstring)

def parse_headers(fp, _class=HTTPMessage):
def parse_headers(fp, _class=HTTPMessage, *, _max_headers=None):
"""Parses only RFC 5322 headers from a file pointer."""

headers = _read_headers(fp)
headers = _read_headers(fp, _max_headers)
return _parse_header_lines(headers, _class)


Expand Down Expand Up @@ -292,6 +300,7 @@ def __init__(self, sock, debuglevel=0, method=None, url=None):
self.chunk_left = _UNKNOWN # bytes left to read in current chunk
self.length = _UNKNOWN # number of bytes left in response
self.will_close = _UNKNOWN # conn will close at end of response
self._max_headers = None # configured header count limit

def _read_status(self):
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
Expand Down Expand Up @@ -326,21 +335,29 @@ def _read_status(self):
raise BadStatusLine(line)
return version, status, reason

def begin(self):
def begin(self, *, _max_headers=None):
if self.headers is not None:
# we've already started reading the response
return

# Trailers of a chunked response are read by read() long after
# begin() returns, so remember the configured header count limit
# for _read_and_discard_trailer() to enforce.
self._max_headers = _max_headers

# read until we get a non-100 response
while True:
for _ in range(_MAXINTERIMRESPONSES):
version, status, reason = self._read_status()
if status != CONTINUE:
break
# skip the header from the 100 response
skipped_headers = _read_headers(self.fp)
skipped_headers = _read_headers(self.fp, _max_headers)
if self.debuglevel > 0:
print("headers:", skipped_headers)
del skipped_headers
else:
raise HTTPException(
f"got more than {_MAXINTERIMRESPONSES} interim responses")

self.code = self.status = status
self.reason = reason.strip()
Expand All @@ -352,7 +369,9 @@ def begin(self):
else:
raise UnknownProtocol(version)

self.headers = self.msg = parse_headers(self.fp)
self.headers = self.msg = parse_headers(
self.fp, _max_headers=_max_headers
)

if self.debuglevel > 0:
for hdr, val in self.headers.items():
Expand Down Expand Up @@ -558,6 +577,10 @@ def _read_next_chunk_size(self):
def _read_and_discard_trailer(self):
# read and discard trailer up to the CRLF terminator
### note: we shouldn't have any trailers!
max_trailers = self._max_headers
if max_trailers is None:
max_trailers = _MAXHEADERS
trailers_read = 0
while True:
line = self.fp.readline(_MAXLINE + 1)
if len(line) > _MAXLINE:
Expand All @@ -568,6 +591,13 @@ def _read_and_discard_trailer(self):
break
if line in (b'\r\n', b'\n', b''):
break
# Bound the trailer count just as response headers are bounded.
# A server streaming trailer lines forever would otherwise hang
# the client; a socket timeout cannot detect that as data keeps
# arriving within every timeout window.
trailers_read += 1
if trailers_read > max_trailers:
raise HTTPException(f"got more than {max_trailers} trailers")

def _get_chunk_left(self):
# return self.chunk_left, reading a new chunk if necessary.
Expand Down Expand Up @@ -885,7 +915,7 @@ def _get_content_length(body, method):
return None

def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None, blocksize=8192):
source_address=None, blocksize=8192, *, max_response_headers=None):
self.timeout = timeout
self.source_address = source_address
self.blocksize = blocksize
Expand All @@ -898,6 +928,7 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
self._tunnel_port = None
self._tunnel_headers = {}
self._raw_proxy_headers = None
self.max_response_headers = max_response_headers

(self.host, self.port) = self._get_hostport(host, port)

Expand Down Expand Up @@ -999,7 +1030,7 @@ def _tunnel(self):
try:
(version, code, message) = response._read_status()

self._raw_proxy_headers = _read_headers(response.fp)
self._raw_proxy_headers = _read_headers(response.fp, self.max_response_headers)

if self.debuglevel > 0:
for header in self._raw_proxy_headers:
Expand Down Expand Up @@ -1416,8 +1447,7 @@ def getresponse(self):
"""Get the response from the server.
If the HTTPConnection is in the correct state, returns an
instance of HTTPResponse or of whatever object is returned by
the response_class variable.
instance of HTTPResponse.
If a request has not been sent or if a previous response has
not be handled, ResponseNotReady is raised. If the HTTP
Expand Down Expand Up @@ -1456,7 +1486,10 @@ def getresponse(self):

try:
try:
response.begin()
if self.max_response_headers is None:
response.begin()
else:
response.begin(_max_headers=self.max_response_headers)
except ConnectionError:
self.close()
raise
Expand Down Expand Up @@ -1487,10 +1520,12 @@ class HTTPSConnection(HTTPConnection):

def __init__(self, host, port=None,
*, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None, context=None, blocksize=8192):
source_address=None, context=None, blocksize=8192,
max_response_headers=None):
super(HTTPSConnection, self).__init__(host, port, timeout,
source_address,
blocksize=blocksize)
blocksize=blocksize,
max_response_headers=max_response_headers)
if context is None:
context = _create_https_context(self._http_vsn)
self._context = context
Expand Down
11 changes: 7 additions & 4 deletions Lib/http/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,10 +1032,13 @@ def set_ok_domain(self, cookie, request):
if j == 0: # domain like .foo.bar
tld = domain[i+1:]
sld = domain[j+1:i]
if sld.lower() in ("co", "ac", "com", "edu", "org", "net",
"gov", "mil", "int", "aero", "biz", "cat", "coop",
"info", "jobs", "mobi", "museum", "name", "pro",
"travel", "eu") and len(tld) == 2:
known_slds = (
"co", "ac", "com", "edu", "org", "net",
"gov", "mil", "int", "aero", "biz", "cat", "coop",
"info", "jobs", "mobi", "museum", "name", "pro",
"travel", "eu", "tv", "or", "nom", "sch", "web",
)
if sld.lower() in known_slds and len(tld) == 2:
# domain like .co.uk
_debug(" country-code second level domain %s", domain)
return False
Expand Down
19 changes: 17 additions & 2 deletions Lib/http/cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
import re
import string
import types
lazy import warnings

__all__ = ["CookieError", "BaseCookie", "SimpleCookie"]

Expand Down Expand Up @@ -390,7 +391,8 @@ def output(self, attrs=None, header="Set-Cookie:"):
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.OutputString())

def js_output(self, attrs=None):
def _js_output(self, attrs=None):
"""Internal implementation without deprecation warning."""
import urllib.parse
# Print javascript
output_string = self.OutputString(attrs)
Expand All @@ -407,6 +409,14 @@ def js_output(self, attrs=None):
</script>
""" % (output_encoded,)

def js_output(self, attrs=None):
warnings._deprecated(
"http.cookies.Morsel.js_output",
message=warnings._DEPRECATED_MSG + "; use output() instead",
remove=(3, 19),
)
return self._js_output(attrs)

def OutputString(self, attrs=None):
# Build up our result
#
Expand Down Expand Up @@ -541,10 +551,15 @@ def __repr__(self):

def js_output(self, attrs=None):
"""Return a string suitable for JavaScript."""
warnings._deprecated(
"http.cookies.BaseCookie.js_output",
message=warnings._DEPRECATED_MSG + "; use output() instead",
remove=(3, 19),
)
result = []
items = sorted(self.items())
for key, value in items:
result.append(value.js_output(attrs))
result.append(value._js_output(attrs))
return _nulljoin(result)

def load(self, rawdata):
Expand Down
Loading
Loading