fix(api): parse client cookies with the dl cookie parser

This commit is contained in:
imSp4rky
2026-10-20 12:46:44 -06:00
parent ef75ea7d7a
commit 08a2257be3
3 changed files with 29 additions and 22 deletions
+8
View File
@@ -29,6 +29,14 @@ def test_load_client_cookies_reads_a_transport_blob():
assert [(c.name, c.value) for c in jar] == [("session-id", "123-456")]
def test_load_client_cookies_reads_what_local_dl_reads():
"""A file with no Netscape header, space-separated rows and an HttpOnly prefix loads as it does locally."""
text = "#HttpOnly_.example.com TRUE / TRUE 2000000000 session-id 123-456\n"
jar = load_client_cookies(transport_blob(text))
assert jar is not None
assert [(c.name, c.value) for c in jar] == [("session-id", "123-456")]
def test_load_client_cookies_without_a_blob():
assert load_client_cookies(None) is None
assert load_client_cookies("") is None
+13 -6
View File
@@ -5627,11 +5627,18 @@ class dl:
@staticmethod
def load_cookie_file(cookie_file: Path) -> MozillaCookieJar:
"""Load a Netscape cookie file with every expiry blanked, so no layer drops an expired cookie."""
cookie_jar = MozillaCookieJar(cookie_file)
cookie_data = html.unescape(cookie_file.read_text("utf-8-sig")).splitlines(keepends=False)
cookie_jar = dl.parse_cookie_text(cookie_file.read_text("utf-8-sig"), str(cookie_file))
cookie_jar.filename = str(cookie_file)
return cookie_jar
@staticmethod
def parse_cookie_text(text: str, source: str) -> MozillaCookieJar:
"""Parse Netscape cookie text the way :meth:`load_cookie_file` does; ``source`` names it in errors."""
cookie_jar = MozillaCookieJar()
cookie_data = html.unescape(text).splitlines(keepends=False)
head = next((line.lstrip() for line in cookie_data if line.strip()), "")
if head[:1] in ("[", "{"):
raise ValueError(f"{cookie_file} is a JSON export; export it again in Netscape format")
raise ValueError(f"{source} is a JSON export; export it again in Netscape format")
for i, line in enumerate(cookie_data):
body = line.lstrip()
prefix = "#HttpOnly_" if body.startswith("#HttpOnly_") else ""
@@ -5642,14 +5649,14 @@ class dl:
if len(line_data) == 6:
line_data.append("")
if len(line_data) != 7:
raise ValueError(f"{cookie_file} line {i + 1} is not a Netscape cookie row: {line!r}")
raise ValueError(f"{source} line {i + 1} is not a Netscape cookie row: {line!r}")
line_data[4] = ""
line_data[1] = str(line_data[0].startswith(".")).upper()
cookie_data[i] = prefix + "\t".join(line_data)
cookie_data.insert(0, "# Netscape HTTP Cookie File")
cookie_jar._really_load(StringIO("\n".join(cookie_data)), str(cookie_file), True, True) # type: ignore[attr-defined]
cookie_jar._really_load(StringIO("\n".join(cookie_data)), source, True, True) # type: ignore[attr-defined]
if not len(cookie_jar):
raise ValueError(f"{cookie_file} holds no cookies; export it again in Netscape format")
raise ValueError(f"{source} holds no cookies; export it again in Netscape format")
return cookie_jar
@staticmethod
+8 -16
View File
@@ -3,9 +3,7 @@ import base64
import enum
import json
import logging
import os
import re
import tempfile
import threading
import time
import uuid
@@ -15,7 +13,7 @@ from contextlib import suppress
from dataclasses import dataclass, field
from datetime import date as date_
from functools import partial
from http.cookiejar import CookieJar, MozillaCookieJar
from http.cookiejar import CookieJar
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, cast
@@ -1146,24 +1144,18 @@ def server_account_cookies(service: str, profile: Optional[str]) -> Any:
def load_client_cookies(cookie_text: Any) -> Optional[CookieJar]:
"""Read the cookie jar a client sent with its request, or None when it sent none.
The client sends a Netscape cookie file compressed and base64 encoded, and
``MozillaCookieJar`` reads from a path only, so the text goes through a temp file.
The client sends a Netscape cookie file compressed and base64 encoded. It is parsed
the way local ``dl`` parses a cookie file, so a file that works locally also works
over ``--remote``.
"""
from unshackle.commands.dl import dl
if not cookie_text or not isinstance(cookie_text, str):
return None
try:
cookie_str = safe_inflate(base64.b64decode(cookie_text)).decode("utf-8")
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
f.write(cookie_str)
tmp_path = f.name
try:
jar = MozillaCookieJar(tmp_path)
jar.load(ignore_discard=True, ignore_expires=True)
return jar
finally:
with suppress(OSError):
os.unlink(tmp_path)
cookie_str = safe_inflate(base64.b64decode(cookie_text)).decode("utf-8-sig")
return dl.parse_cookie_text(cookie_str, "client cookies")
except (ValueError, zlib.error, OSError) as e:
raise APIError(APIErrorCode.INVALID_INPUT, f"cookies is not a compressed Netscape cookie file: {e}")