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
40 changes: 26 additions & 14 deletions Lib/http/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,18 @@ def _debug(*args):
logger = logging.getLogger("http.cookiejar")
return logger.debug(*args)


HTTPONLY_ATTR = "HTTPOnly"

@dlenski dlenski Sep 26, 2023

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is similar to my alternative approach in #22798 (and see bpo-2190), except:

  • This version:
    • Check if a cookie is "HTTP-only" with cookie.has_nonstandard_attr(http.cookiejar.HTTPONLY_ATTR))
    • Set a cookie as "HTTP-only" with cookie.set_nonstandard_attr("HTTPOnly", "")
  • My version:
    • Check with cookie.httponly.
    • Set with cookie.httponly = True

HTTPONLY_PREFIX = "#HttpOnly_"
DEFAULT_HTTP_PORT = str(http.client.HTTP_PORT)
NETSCAPE_MAGIC_RGX = re.compile("#( Netscape)? HTTP Cookie File")
MISSING_FILENAME_TEXT = ("a filename was not supplied (nor was the CookieJar "
"instance initialised with one)")
NETSCAPE_HEADER_TEXT = """\
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit.

"""

def _warn_unhandled_exception():
# There are a few catch-all except: statements in this module, for
Expand Down Expand Up @@ -2004,28 +2012,29 @@ class MozillaCookieJar(FileCookieJar):
header by default (Mozilla can cope with that).

"""
magic_re = re.compile("#( Netscape)? HTTP Cookie File")
header = """\
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit.

"""

def _really_load(self, f, filename, ignore_discard, ignore_expires):
now = time.time()

magic = f.readline()
if not self.magic_re.search(magic):
if not NETSCAPE_MAGIC_RGX.match(f.readline()):
raise LoadError(
"%r does not look like a Netscape format cookies file" %
filename)

try:
while 1:
line = f.readline()
rest = {}

if line == "": break

# httponly is a cookie flag as defined in rfc6265
# when encoded in a netscape cookie file,
# the line is prepended with "#HttpOnly_"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if line.startswith(HTTPONLY_PREFIX):
rest[HTTPONLY_ATTR] = ""
line = line[len(HTTPONLY_PREFIX):]

# last field may be absent, so keep any trailing tab
if line.endswith("\n"): line = line[:-1]

Expand Down Expand Up @@ -2063,7 +2072,7 @@ def _really_load(self, f, filename, ignore_discard, ignore_expires):
discard,
None,
None,
{})
rest)
if not ignore_discard and c.discard:
continue
if not ignore_expires and c.is_expired(now):
Expand All @@ -2083,16 +2092,17 @@ def save(self, filename=None, ignore_discard=False, ignore_expires=False):
else: raise ValueError(MISSING_FILENAME_TEXT)

with open(filename, "w") as f:
f.write(self.header)
f.write(NETSCAPE_HEADER_TEXT)
now = time.time()
for cookie in self:
domain = cookie.domain
if not ignore_discard and cookie.discard:
continue
if not ignore_expires and cookie.is_expired(now):
continue
if cookie.secure: secure = "TRUE"
else: secure = "FALSE"
if cookie.domain.startswith("."): initial_dot = "TRUE"
if domain.startswith("."): initial_dot = "TRUE"
else: initial_dot = "FALSE"
if cookie.expires is not None:
expires = str(cookie.expires)
Expand All @@ -2107,7 +2117,9 @@ def save(self, filename=None, ignore_discard=False, ignore_expires=False):
else:
name = cookie.name
value = cookie.value
if cookie.has_nonstandard_attr(HTTPONLY_ATTR):
domain = HTTPONLY_PREFIX + domain
f.write(
"\t".join([cookie.domain, initial_dot, cookie.path,
"\t".join([domain, initial_dot, cookie.path,
secure, expires, name, value])+
"\n")
5 changes: 5 additions & 0 deletions Lib/test/test_http_cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,10 @@ def test_mozilla(self):
interact_netscape(c, "http://www.foo.com/",
"fooc=bar; Domain=www.foo.com; %s" % expires)

for cookie in c:
if cookie.name == "foo1":
cookie.set_nonstandard_attr("HTTPOnly", "")

def save_and_restore(cj, ignore_discard):
try:
cj.save(ignore_discard=ignore_discard)
Expand All @@ -1785,6 +1789,7 @@ def save_and_restore(cj, ignore_discard):
new_c = save_and_restore(c, True)
self.assertEqual(len(new_c), 6) # none discarded
self.assertIn("name='foo1', value='bar'", repr(new_c))
self.assertIn("rest={'HTTPOnly': ''}", repr(new_c))

new_c = save_and_restore(c, False)
self.assertEqual(len(new_c), 4) # 2 of them discarded on save
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The :mod:`http.cookiejar` module now supports the parsing of cookies in CURL-style cookiejar files through MozillaCookieJar
on all platforms. Previously, such cookie entries would be silently ignored when loading a cookiejar with such entries.

Additionally, the HTTP Only attribute is persisted in the object, and will be correctly written to file if the MozillaCookieJar object is subsequently dumped.