Skip to content
Merged
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
7 changes: 5 additions & 2 deletions Lib/http/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2056,7 +2056,8 @@ def _really_load(self, f, filename, ignore_discard, ignore_expires):
assert domain_specified == initial_dot

discard = False
if expires == "":
# curl and Wget set expires to 0 for session cookies.
if expires == "0" or expires == "":
expires = None
discard = True

Expand Down Expand Up @@ -2108,7 +2109,9 @@ def save(self, filename=None, ignore_discard=False, ignore_expires=False):
if cookie.expires is not None:
expires = str(cookie.expires)
else:
expires = ""
# curl and Wget use 0 for session cookies, and ignore
# the line if this field is empty.
expires = "0"
if cookie.value is None:
# cookies.txt regards 'Set-Cookie: foo' as a cookie
# with no name, whereas http.cookiejar regards it as a
Expand Down
13 changes: 12 additions & 1 deletion Lib/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,18 @@ def makefile(self, mode="r", buffering=None, *,
rawmode += "w"
raw = SocketIO(self, rawmode)
self._io_refs += 1
line_buffering = False
if buffering is None:
buffering = -1
if buffering == 1:
if binary:
import warnings
warnings.warn("line buffering (buffering=1) isn't supported "
"in binary mode, the default buffer size will "
"be used", RuntimeWarning, 2)
else:
line_buffering = True
buffering = -1
if buffering < 0:
buffering = io.DEFAULT_BUFFER_SIZE
if buffering == 0:
Expand All @@ -345,7 +355,8 @@ def makefile(self, mode="r", buffering=None, *,
if binary:
return buffer
encoding = io.text_encoding(encoding)
text = io.TextIOWrapper(buffer, encoding, errors, newline)
text = io.TextIOWrapper(
buffer, encoding, errors, newline, line_buffering)
text.mode = mode
return text

Expand Down
59 changes: 58 additions & 1 deletion Lib/test/test_http_cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
CookieJar, DefaultCookiePolicy, LWPCookieJar, MozillaCookieJar,
LoadError, lwp_cookie_str, DEFAULT_HTTP_PORT, escape_path,
reach, is_HDN, domain_match, user_domain_match, request_path,
request_port, request_host)
request_port, request_host, NETSCAPE_HEADER_TEXT)

mswindows = (sys.platform == "win32")

Expand Down Expand Up @@ -2048,6 +2048,63 @@ def test_session_cookies(self):
# we didn't have session cookies in the first place
self.assertNotEqual(counter["session_before"], 0)

def test_save_session_cookies(self):
# Session cookies are saved with 0 in the expiration time field,
# as curl and Wget do (gh-61366).
filename = os_helper.TESTFN
self.addCleanup(os_helper.unlink, filename)
expires = int(time.time() + 3600)
c = MozillaCookieJar()
c.set_cookie(Cookie(0, "perm", "bar", None, False,
"www.foo.com", True, False, "/", False, False,
expires, False, None, None, {}))
c.set_cookie(Cookie(0, "session", "bar", None, False,
"www.foo.com", True, False, "/", False, False,
None, True, None, None, {}))
c.save(filename, ignore_discard=True)

saved = {}
with open(filename) as f:
for line in f:
if line.strip() and not line.startswith("#"):
fields = line.split("\t")
saved[fields[5]] = fields[4]
self.assertEqual(saved, {"perm": str(expires), "session": "0"})

# The saved file can be read back.
c = MozillaCookieJar()
c.revert(filename, ignore_discard=True)
self.assertEqual(sorted(cookie.name for cookie in c),
["perm", "session"])

def test_load_session_cookies(self):
# curl and Wget write 0 in the expires field for session cookies,
# while we write an empty field. Both should be read (gh-61366).
filename = os_helper.TESTFN
self.addCleanup(os_helper.unlink, filename)
expires = int(time.time() + 3600)
with open(filename, "w") as f:
f.write(NETSCAPE_HEADER_TEXT)
f.write("www.foo.com\tFALSE\t/\tFALSE\t%u\tperm\tbar\n" % expires)
f.write("www.foo.com\tFALSE\t/\tFALSE\t0\tcurl_session\tbar\n")
f.write("www.foo.com\tFALSE\t/\tFALSE\t\tour_session\tbar\n")

c = MozillaCookieJar()
c.revert(filename)
self.assertEqual([cookie.name for cookie in c], ["perm"])

c = MozillaCookieJar()
c.revert(filename, ignore_discard=True)
self.assertEqual(sorted(cookie.name for cookie in c),
["curl_session", "our_session", "perm"])
for cookie in c:
if cookie.name == "perm":
self.assertEqual(cookie.expires, expires)
self.assertFalse(cookie.discard)
else:
self.assertIsNone(cookie.expires)
self.assertTrue(cookie.discard)


if __name__ == "__main__":
unittest.main()
46 changes: 40 additions & 6 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,25 @@ def test_makefile_mode(self):
with sock.makefile(mode, encoding=encoding) as fp:
self.assertEqual(fp.mode, mode)

def test_makefile_line_buffering(self):
with socket.socket() as sock:
for mode in 'r', 'w':
with self.subTest(mode=mode):
with sock.makefile(mode, buffering=1,
encoding="utf-8") as fp:
self.assertTrue(fp.line_buffering)

def test_makefile_line_buffering_binary(self):
# Line buffering is not supported in binary mode, as in open().
with socket.socket() as sock:
for mode in 'rb', 'wb':
with self.subTest(mode=mode):
with self.assertWarnsRegex(
RuntimeWarning,
"line buffering .* isn't supported in binary "
"mode"):
sock.makefile(mode, buffering=1).close()

def test_makefile_invalid_mode(self):
for mode in 'rt', 'x', '+', 'a':
with self.subTest(mode=mode):
Expand Down Expand Up @@ -5761,10 +5780,20 @@ def testReadline(self):
# Performing file readline test
line = self.read_file.readline()
self.assertEqual(line, self.read_msg)
# Readline mode
if self.bufsize == 1 and self.read_mode == "r":
self.assertTrue(self.read_file.line_buffering)

def _testReadline(self):
self.write_file.write(self.write_msg)
self.write_file.flush()
# Readline mode: no need to flush
if self.bufsize == 1 and self.write_mode == "w":
self.assertTrue(self.write_file.line_buffering)
else:
self.write_file.flush()
# Prevent garbage collection from flushing
# until the server has finished
self.assertTrue(self.serv_finished.wait(5.0))

def testCloseAfterMakefile(self):
# The file returned by makefile should keep the socket open.
Expand Down Expand Up @@ -5922,11 +5951,6 @@ def _testWriteNonBlocking(self):
self.serv_skipped = "failed to saturate the socket buffer"


class LineBufferedFileObjectClassTestCase(FileObjectClassTestCase):

bufsize = 1 # Default-buffered for reading; line-buffered for writing


class SmallBufferedFileObjectClassTestCase(FileObjectClassTestCase):

bufsize = 2 # Exercise the buffering code
Expand Down Expand Up @@ -5962,6 +5986,16 @@ class UnicodeReadWriteFileObjectClassTestCase(FileObjectClassTestCase):
newline = ''


class UnicodeLineBufferedFileObjectClassTestCase(FileObjectClassTestCase):

bufsize = 1 # Default-buffered for reading; line-buffered for writing
read_mode = 'r'
read_msg = MSG.decode('utf-8')
write_mode = 'w'
write_msg = MSG.decode('utf-8')
newline = ''


class NetworkConnectionTest(object):
"""Prove network connection."""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:class:`http.cookiejar.MozillaCookieJar` now reads session cookies written
by curl and Wget, which use ``0`` in the expiration time field.
Contributed by J茅r茅mie Detrey.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:meth:`socket.socket.makefile` now supports line buffering (``buffering=1``)
in text mode, as :func:`open` does and as it worked in Python 2. Previously
it silently used block buffering. In binary mode it now emits
a :exc:`RuntimeWarning` and uses the default buffer size, also as :func:`open`
does.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:meth:`!http.cookiejar.MozillaCookieJar.save` now writes ``0`` in the
expiration time field for session cookies, as curl and Wget do. Previously
this field was left empty, and such lines were ignored by curl and Wget.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update bundled `libexpat <https://libexpat.github.io/>`_ to version 2.8.3
for the fix to :cve:`2026-72522`.
Loading
Loading