mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
fix(remote): keep each session cookie's domain, path and secure flag
The client set server cookies with no domain and sent all of them to every host, so a CDN refused segment requests with 400 header too large. rnet sessions now send cookies too.
This commit is contained in:
@@ -1096,6 +1096,7 @@ Get tracks and chapters for a specific title within the remote session. Body: `{
|
|||||||
"track_manifests": [ { "track_id": "s-1", "type": "dash", "url": "https://...", "data": "<base64 zlib xml>" } ],
|
"track_manifests": [ { "track_id": "s-1", "type": "dash", "url": "https://...", "data": "<base64 zlib xml>" } ],
|
||||||
"session_headers": { "User-Agent": "..." },
|
"session_headers": { "User-Agent": "..." },
|
||||||
"session_cookies": { "sid": "..." },
|
"session_cookies": { "sid": "..." },
|
||||||
|
"session_cookie_jar": [ { "name": "sid", "value": "...", "domain": ".example.com", "path": "/", "secure": true } ],
|
||||||
"server_cdm_type": "widevine"
|
"server_cdm_type": "widevine"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1115,11 +1116,13 @@ Get the download URL, DRM info, headers, cookies, and raw track data for the sel
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"v-1": { "descriptor": "DASH", "url": "https://...", "drm": [ ... ], "headers": { ... }, "cookies": { ... }, "data": { ... } }
|
"v-1": { "descriptor": "DASH", "url": "https://...", "drm": [ ... ], "headers": { ... }, "cookies": { ... }, "cookie_jar": [ ... ], "data": { ... } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`cookies` gives names and values only. `cookie_jar` gives the same cookies with their domain, path and `secure` flag, in the `session_cookie_jar` form of the tracks response.
|
||||||
|
|
||||||
| Status | Error code | Meaning |
|
| Status | Error code | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `200` | - | Segment info returned. |
|
| `200` | - | Segment info returned. |
|
||||||
|
|||||||
@@ -214,8 +214,10 @@ hood the client walks a remote session through its lifecycle.
|
|||||||
downloading. A service can build an AdaptationSet of its own that the served
|
downloading. A service can build an AdaptationSet of its own that the served
|
||||||
manifest does not contain. Each such track gets a one-AdaptationSet MPD in
|
manifest does not contain. Each such track gets a one-AdaptationSet MPD in
|
||||||
`track_manifests`, and the client re-parses that instead. Any
|
`track_manifests`, and the client re-parses that instead. Any
|
||||||
`session_headers` / `session_cookies` the server used are merged into the
|
`session_headers` and cookies the server used are merged into the
|
||||||
client's local HTTP session.
|
client's local HTTP session. Each cookie in `session_cookie_jar` keeps its
|
||||||
|
domain, path and `secure` flag, so it goes only to its own hosts. `session_cookies` carries
|
||||||
|
the same cookies as names and values only, for older clients.
|
||||||
|
|
||||||
The tracks response repeats the title. A service can rewrite title fields inside
|
The tracks response repeats the title. A service can rewrite title fields inside
|
||||||
`get_tracks`, so the client takes this copy in place of the one from the title
|
`get_tracks`, so the client takes this copy in place of the one from the title
|
||||||
@@ -653,7 +655,7 @@ remote session API and a good template for any client.
|
|||||||
- **Flow.** `authenticate()` → `create` (+ poll `prompt` every 2s up to a 600s
|
- **Flow.** `authenticate()` → `create` (+ poll `prompt` every 2s up to a 600s
|
||||||
deadline, answering `pending_input` prompts. With stdin closed the client logs an
|
deadline, answering `pending_input` prompts. With stdin closed the client logs an
|
||||||
error and raises `SystemExit(1)` instead of posting an empty answer) → `get_titles()` → `get_tracks()`
|
error and raises `SystemExit(1)` instead of posting an empty answer) → `get_titles()` → `get_tracks()`
|
||||||
(merging returned `session_headers`/`session_cookies`, re-parsing `manifests`) →
|
(merging returned `session_headers`/`session_cookie_jar`, re-parsing `manifests`) →
|
||||||
get the licence in proxy mode or `server_cdm` mode → `close()` (`DELETE`, saving any returned
|
get the licence in proxy mode or `server_cdm` mode → `close()` (`DELETE`, saving any returned
|
||||||
`cache`).
|
`cache`).
|
||||||
- **Server resolution.** `resolve_server()` reads `config.remote_services.<name>`
|
- **Server resolution.** `resolve_server()` reads `config.remote_services.<name>`
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ def test_a_lent_device_session_keeps_its_auth_headers_and_track_secrets() -> Non
|
|||||||
headers, cookies = handlers.client_session_auth(session)
|
headers, cookies = handlers.client_session_auth(session)
|
||||||
|
|
||||||
assert headers == {"User-Agent": "ua"}
|
assert headers == {"User-Agent": "ua"}
|
||||||
assert cookies == {"account": "client"}
|
assert handlers.cookie_values(cookies) == {"account": "client"}
|
||||||
assert handlers.scrub_track_data({"token": "server", "id": 1}, session) == {"id": 1}
|
assert handlers.scrub_track_data({"token": "server", "id": 1}, session) == {"id": 1}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
from unshackle.core.api.errors import APIError
|
from unshackle.core.api.errors import APIError
|
||||||
from unshackle.core.api.handlers import client_session_auth, resolve_handler_proxy, scrub_track_data
|
from unshackle.core.api.handlers import client_session_auth, resolve_handler_proxy, scrub_track_data
|
||||||
|
from unshackle.core.remote_service import apply_session_cookies
|
||||||
|
|
||||||
|
|
||||||
def _session(client_auth):
|
def _session(client_auth):
|
||||||
cookie = SimpleNamespace(name="sid", value="secret")
|
cookie = SimpleNamespace(name="sid", value="secret", domain=".example.com", path="/")
|
||||||
svc = SimpleNamespace(headers={"Authorization": "Bearer x", "User-Agent": "ua"}, cookies=[cookie])
|
svc = SimpleNamespace(headers={"Authorization": "Bearer x", "User-Agent": "ua"}, cookies=[cookie])
|
||||||
return SimpleNamespace(service_instance=SimpleNamespace(session=svc), client_auth=client_auth)
|
return SimpleNamespace(service_instance=SimpleNamespace(session=svc), client_auth=client_auth)
|
||||||
|
|
||||||
@@ -15,14 +17,129 @@ def _session(client_auth):
|
|||||||
def test_client_login_keeps_auth():
|
def test_client_login_keeps_auth():
|
||||||
headers, cookies = client_session_auth(_session(True))
|
headers, cookies = client_session_auth(_session(True))
|
||||||
assert headers == {"Authorization": "Bearer x", "User-Agent": "ua"}
|
assert headers == {"Authorization": "Bearer x", "User-Agent": "ua"}
|
||||||
assert cookies == {"sid": "secret"}
|
assert cookies == [{"name": "sid", "value": "secret", "domain": ".example.com", "path": "/", "secure": False}]
|
||||||
assert scrub_track_data({"token": "t", "hls": {}}, _session(True)) == {"token": "t", "hls": {}}
|
assert scrub_track_data({"token": "t", "hls": {}}, _session(True)) == {"token": "t", "hls": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_cookies_keep_their_domain():
|
||||||
|
session = requests.Session()
|
||||||
|
_, cookies = client_session_auth(_session(True))
|
||||||
|
apply_session_cookies(session, {"session_cookie_jar": cookies})
|
||||||
|
assert session.cookies.get("sid", domain=".example.com") == "secret"
|
||||||
|
cdn = session.prepare_request(requests.Request("GET", "https://cdn.other.net/seg.mp4"))
|
||||||
|
assert "Cookie" not in cdn.headers
|
||||||
|
own = session.prepare_request(requests.Request("GET", "https://www.example.com/api"))
|
||||||
|
assert own.headers["Cookie"] == "sid=secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_takes_old_server_cookies_without_domain():
|
||||||
|
session = requests.Session()
|
||||||
|
apply_session_cookies(session, {"session_cookies": {"sid": "secret"}})
|
||||||
|
assert session.cookies.get("sid") == "secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_keeps_secure_cookies_off_http():
|
||||||
|
session = requests.Session()
|
||||||
|
jar = [{"name": "sid", "value": "s", "domain": ".example.com", "path": "/", "secure": True}]
|
||||||
|
apply_session_cookies(session, {"session_cookie_jar": jar})
|
||||||
|
plain = session.prepare_request(requests.Request("GET", "http://cdn.example.com/seg.mp4"))
|
||||||
|
assert "Cookie" not in plain.headers
|
||||||
|
tls = session.prepare_request(requests.Request("GET", "https://cdn.example.com/seg.mp4"))
|
||||||
|
assert tls.headers["Cookie"] == "sid=s"
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_skips_malformed_server_cookies():
|
||||||
|
session = requests.Session()
|
||||||
|
jar = [{"value": "v"}, "str", {"name": "n", "value": 1}, {"name": "ok", "value": "v", "domain": ".example.com"}]
|
||||||
|
apply_session_cookies(session, {"session_cookie_jar": jar})
|
||||||
|
assert [c.name for c in session.cookies] == ["ok"]
|
||||||
|
session = requests.Session()
|
||||||
|
apply_session_cookies(session, {"session_cookie_jar": 5, "session_cookies": ["x"]})
|
||||||
|
assert not session.cookies
|
||||||
|
|
||||||
|
|
||||||
|
def test_rnet_session_sends_client_and_set_cookies():
|
||||||
|
from unshackle.core.session import RnetSession
|
||||||
|
|
||||||
|
svc = RnetSession()
|
||||||
|
svc.cookies.set("sid", "stale", domain=".example.com")
|
||||||
|
svc.cookies.set("cdn", "c", domain="cdn.other.net")
|
||||||
|
svc.export_origin_cookies = lambda: {"https://www.example.com": "sid=live; cf=1"}
|
||||||
|
_, cookies = client_session_auth(SimpleNamespace(service_instance=SimpleNamespace(session=svc), client_auth=True))
|
||||||
|
assert {(c["name"], c["value"], c["domain"]) for c in cookies} == {
|
||||||
|
("sid", "live", "www.example.com"),
|
||||||
|
("cf", "1", "www.example.com"),
|
||||||
|
("cdn", "c", "cdn.other.net"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracks_response_carries_the_cookie_jar():
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from unshackle.core.api import session_store as store_mod
|
||||||
|
from unshackle.core.api.handlers import session_tracks_handler
|
||||||
|
from unshackle.core.tracks import Tracks
|
||||||
|
|
||||||
|
cookie = SimpleNamespace(name="sid", value="secret", domain=".example.com", path="/", secure=True)
|
||||||
|
|
||||||
|
class StubService:
|
||||||
|
session = SimpleNamespace(headers={}, cookies=[cookie])
|
||||||
|
|
||||||
|
def get_tracks(self, title):
|
||||||
|
return Tracks()
|
||||||
|
|
||||||
|
def get_chapters(self, title):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
store = store_mod.get_session_store()
|
||||||
|
entry = await store.create(
|
||||||
|
service_tag="EXAMPLE", service_instance=StubService(), session_id="jar", owner_key=None
|
||||||
|
)
|
||||||
|
entry.title_map = {"t1": "Example Title"}
|
||||||
|
entry.client_auth = True
|
||||||
|
try:
|
||||||
|
return json.loads((await session_tracks_handler({"title_id": "t1"}, "jar")).body)
|
||||||
|
finally:
|
||||||
|
await store.delete("jar")
|
||||||
|
|
||||||
|
body = asyncio.run(run())
|
||||||
|
assert body["session_cookies"] == {"sid": "secret"}
|
||||||
|
assert body["session_cookie_jar"] == [
|
||||||
|
{"name": "sid", "value": "secret", "domain": ".example.com", "path": "/", "secure": True}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_get_tracks_scopes_server_cookies():
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from unshackle.core.remote_service import RemoteService
|
||||||
|
|
||||||
|
jar = [{"name": "sid", "value": "secret", "domain": ".example.com", "path": "/"}]
|
||||||
|
service = object.__new__(RemoteService)
|
||||||
|
service.__dict__.update(
|
||||||
|
_tracks_by_title={},
|
||||||
|
_chapters_by_title={},
|
||||||
|
_sample_decrypt_tracks=set(),
|
||||||
|
_server_cdm=None,
|
||||||
|
_session=requests.Session(),
|
||||||
|
_session_id="s",
|
||||||
|
service_tag="EXAMPLE",
|
||||||
|
log=logging.getLogger("test"),
|
||||||
|
client=SimpleNamespace(post=lambda path, body: {"session_cookie_jar": jar}),
|
||||||
|
drain_server_logs=lambda: None,
|
||||||
|
)
|
||||||
|
service.get_tracks(SimpleNamespace(id="t1"))
|
||||||
|
cdn = service._session.prepare_request(requests.Request("GET", "https://cdn.other.net/seg.mp4"))
|
||||||
|
assert "Cookie" not in cdn.headers
|
||||||
|
assert service._session.cookies.get("sid", domain=".example.com") == "secret"
|
||||||
|
|
||||||
|
|
||||||
def test_server_login_strips_auth():
|
def test_server_login_strips_auth():
|
||||||
headers, cookies = client_session_auth(_session(False))
|
headers, cookies = client_session_auth(_session(False))
|
||||||
assert headers == {"User-Agent": "ua"}
|
assert headers == {"User-Agent": "ua"}
|
||||||
assert cookies == {}
|
assert cookies == []
|
||||||
assert scrub_track_data({"token": "t", "hls": {}}, _session(False)) == {"hls": {}}
|
assert scrub_track_data({"token": "t", "hls": {}}, _session(False)) == {"hls": {}}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from unshackle.core.drm import cbcs
|
|||||||
from unshackle.core.providers.anilist import parse_anilist_ref
|
from unshackle.core.providers.anilist import parse_anilist_ref
|
||||||
from unshackle.core.proxies.resolve import initialize_proxy_providers, resolve_proxy
|
from unshackle.core.proxies.resolve import initialize_proxy_providers, resolve_proxy
|
||||||
from unshackle.core.services import Services
|
from unshackle.core.services import Services
|
||||||
from unshackle.core.session import utf8_text
|
from unshackle.core.session import RnetSession, utf8_text
|
||||||
from unshackle.core.titles import Episode, Movie, Song, Title_T
|
from unshackle.core.titles import Episode, Movie, Song, Title_T
|
||||||
from unshackle.core.titles.music import SONG_FIELDS
|
from unshackle.core.titles.music import SONG_FIELDS
|
||||||
from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
|
from unshackle.core.tracks import Audio, Subtitle, Tracks, Video
|
||||||
@@ -4068,8 +4068,8 @@ async def session_titles_handler(session_id: str, request: Optional[web.Request]
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def client_session_auth(session: Any) -> tuple[Dict[str, str], Dict[str, str]]:
|
def client_session_auth(session: Any) -> tuple[Dict[str, str], list[Dict[str, Any]]]:
|
||||||
"""The service session headers and cookies a remote client may receive.
|
"""The service session headers and cookies (name, value, domain, path, secure) a remote client may receive.
|
||||||
|
|
||||||
A client whose own login succeeded gets them back. For a server account the cookie jar
|
A client whose own login succeeded gets them back. For a server account the cookie jar
|
||||||
stays on the server and the server drops the auth-bearing headers, so the client cannot
|
stays on the server and the server drops the auth-bearing headers, so the client cannot
|
||||||
@@ -4078,18 +4078,31 @@ def client_session_auth(session: Any) -> tuple[Dict[str, str], Dict[str, str]]:
|
|||||||
"""
|
"""
|
||||||
svc_session = session.service_instance.session
|
svc_session = session.service_instance.session
|
||||||
headers = dict(svc_session.headers) if hasattr(svc_session, "headers") and svc_session.headers else {}
|
headers = dict(svc_session.headers) if hasattr(svc_session, "headers") and svc_session.headers else {}
|
||||||
cookies: Dict[str, str] = {}
|
cookies: list[Dict[str, Any]] = []
|
||||||
if hasattr(svc_session, "cookies"):
|
jar = svc_session.scoped_cookies() if isinstance(svc_session, RnetSession) else getattr(svc_session, "cookies", [])
|
||||||
for cookie in svc_session.cookies:
|
for cookie in jar:
|
||||||
if hasattr(cookie, "name") and hasattr(cookie, "value"):
|
if hasattr(cookie, "name") and hasattr(cookie, "value"):
|
||||||
cookies[cookie.name] = cookie.value
|
cookies.append(
|
||||||
|
{
|
||||||
|
"name": cookie.name,
|
||||||
|
"value": cookie.value,
|
||||||
|
"domain": getattr(cookie, "domain", None) or "",
|
||||||
|
"path": getattr(cookie, "path", None) or "/",
|
||||||
|
"secure": bool(getattr(cookie, "secure", False)),
|
||||||
|
}
|
||||||
|
)
|
||||||
if not client_owns_login(session):
|
if not client_owns_login(session):
|
||||||
headers = {k: v for k, v in headers.items() if not CONFIG_SECRET_KEY_RE.search(str(k))}
|
headers = {k: v for k, v in headers.items() if not CONFIG_SECRET_KEY_RE.search(str(k))}
|
||||||
if not session.client_auth:
|
if not session.client_auth:
|
||||||
cookies = {}
|
cookies = []
|
||||||
return headers, cookies
|
return headers, cookies
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_values(cookies: list[Dict[str, Any]]) -> Dict[str, str]:
|
||||||
|
"""The ``{name: value}`` form of :func:`client_session_auth` cookies, for clients that predate the jar."""
|
||||||
|
return {c["name"]: c["value"] for c in cookies}
|
||||||
|
|
||||||
|
|
||||||
def client_owns_login(session: Any) -> bool:
|
def client_owns_login(session: Any) -> bool:
|
||||||
"""Whether the login state of ``session`` is the client's to take back.
|
"""Whether the login state of ``session`` is the client's to take back.
|
||||||
|
|
||||||
@@ -4232,7 +4245,8 @@ async def session_tracks_handler(
|
|||||||
"manifests": manifests,
|
"manifests": manifests,
|
||||||
"track_manifests": track_manifests,
|
"track_manifests": track_manifests,
|
||||||
"session_headers": session_headers,
|
"session_headers": session_headers,
|
||||||
"session_cookies": session_cookies,
|
"session_cookies": cookie_values(session_cookies),
|
||||||
|
"session_cookie_jar": session_cookies,
|
||||||
"server_cdm": session.server_cdm,
|
"server_cdm": session.server_cdm,
|
||||||
"server_cdm_type": server_cdm_type,
|
"server_cdm_type": server_cdm_type,
|
||||||
"sample_decrypt_tracks": [
|
"sample_decrypt_tracks": [
|
||||||
@@ -4269,6 +4283,7 @@ async def session_segments_handler(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result: Dict[str, Any] = {}
|
result: Dict[str, Any] = {}
|
||||||
|
headers, cookies = client_session_auth(session)
|
||||||
|
|
||||||
for track_id in track_ids:
|
for track_id in track_ids:
|
||||||
track = session.tracks.get(track_id)
|
track = session.tracks.get(track_id)
|
||||||
@@ -4285,10 +4300,11 @@ async def session_segments_handler(
|
|||||||
"descriptor": descriptor_name,
|
"descriptor": descriptor_name,
|
||||||
"url": str(track.url) if track.url else None,
|
"url": str(track.url) if track.url else None,
|
||||||
"drm": serialize_drm(track.drm) if hasattr(track, "drm") and track.drm else None,
|
"drm": serialize_drm(track.drm) if hasattr(track, "drm") and track.drm else None,
|
||||||
|
"headers": headers,
|
||||||
|
"cookies": cookie_values(cookies),
|
||||||
|
"cookie_jar": cookies,
|
||||||
}
|
}
|
||||||
|
|
||||||
track_info["headers"], track_info["cookies"] = client_session_auth(session)
|
|
||||||
|
|
||||||
# Include manifest-specific data for segment resolution. Round-trip through
|
# Include manifest-specific data for segment resolution. Round-trip through
|
||||||
# JSON so any non-serializable value becomes its str() (default=str).
|
# JSON so any non-serializable value becomes its str() (default=str).
|
||||||
if hasattr(track, "data") and track.data:
|
if hasattr(track, "data") and track.data:
|
||||||
|
|||||||
@@ -426,6 +426,27 @@ def apply_service_track_data(tracks: Tracks, data: Dict[str, Any]) -> None:
|
|||||||
track.data.update(extra)
|
track.data.update(extra)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_session_cookies(session: requests.Session, result: dict) -> None:
|
||||||
|
"""Copy the server's session cookies into ``session``, each scoped to its own domain.
|
||||||
|
|
||||||
|
An older server sends only ``session_cookies`` names and values, so those cookies go to every host.
|
||||||
|
It skips malformed entries.
|
||||||
|
"""
|
||||||
|
jar = result.get("session_cookie_jar")
|
||||||
|
if not isinstance(jar, list):
|
||||||
|
old = result.get("session_cookies")
|
||||||
|
jar = [{"name": k, "value": v} for k, v in old.items()] if isinstance(old, dict) else []
|
||||||
|
for c in jar:
|
||||||
|
if isinstance(c, dict) and isinstance(c.get("name"), str) and isinstance(c.get("value"), str):
|
||||||
|
session.cookies.set(
|
||||||
|
c["name"],
|
||||||
|
c["value"],
|
||||||
|
domain=str(c.get("domain") or ""),
|
||||||
|
path=str(c.get("path") or "/"),
|
||||||
|
secure=bool(c.get("secure")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_track_manifests(tracks: Tracks, track_manifests: list) -> None:
|
def resolve_track_manifests(tracks: Tracks, track_manifests: list) -> None:
|
||||||
"""Fill track.data from the per-track MPD fragments the server sent.
|
"""Fill track.data from the per-track MPD fragments the server sent.
|
||||||
|
|
||||||
@@ -1651,8 +1672,7 @@ class RemoteService:
|
|||||||
for k, v in result.get("session_headers", {}).items():
|
for k, v in result.get("session_headers", {}).items():
|
||||||
if k.lower() not in ("host", "content-length", "content-type"):
|
if k.lower() not in ("host", "content-length", "content-type"):
|
||||||
self._session.headers[k] = v
|
self._session.headers[k] = v
|
||||||
for k, v in result.get("session_cookies", {}).items():
|
apply_session_cookies(self._session, result)
|
||||||
self._session.cookies.set(k, v)
|
|
||||||
|
|
||||||
resolve_track_manifests(tracks, result.get("track_manifests", []))
|
resolve_track_manifests(tracks, result.get("track_manifests", []))
|
||||||
resolve_manifest_data(tracks, result.get("manifests", []))
|
resolve_manifest_data(tracks, result.get("manifests", []))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from urllib.parse import urlencode, urlparse, urlunparse
|
|||||||
|
|
||||||
import rnet
|
import rnet
|
||||||
from requests import HTTPError, Request, Response
|
from requests import HTTPError, Request, Response
|
||||||
|
from requests.cookies import create_cookie
|
||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
from unshackle.core.config import config
|
from unshackle.core.config import config
|
||||||
@@ -664,14 +665,19 @@ class RnetSession:
|
|||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_cookie_header(client: rnet.BlockingClient, origin: str, header: str) -> None:
|
def parse_cookie_header(header: str) -> Iterator[tuple[str, str]]:
|
||||||
"""Set every cookie in a ``name=value; ...`` header string on one origin of *client*."""
|
"""The ``(name, value)`` pairs of a ``name=value; ...`` header string."""
|
||||||
for pair in header.split(";"):
|
for pair in header.split(";"):
|
||||||
name, separator, value = pair.partition("=")
|
name, separator, value = pair.partition("=")
|
||||||
if not separator:
|
if separator:
|
||||||
continue
|
yield name.strip(), value.strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_cookie_header(client: rnet.BlockingClient, origin: str, header: str) -> None:
|
||||||
|
"""Set every cookie in a ``name=value; ...`` header string on one origin of *client*."""
|
||||||
|
for name, value in RnetSession.parse_cookie_header(header):
|
||||||
try:
|
try:
|
||||||
client.set_cookie(origin, rnet.Cookie(name.strip(), value.strip()))
|
client.set_cookie(origin, rnet.Cookie(name, value))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -705,6 +711,26 @@ class RnetSession:
|
|||||||
exported[origin] = raw.decode("utf-8", errors="replace")
|
exported[origin] = raw.decode("utf-8", errors="replace")
|
||||||
return exported
|
return exported
|
||||||
|
|
||||||
|
def scoped_cookies(self) -> CookieJar:
|
||||||
|
"""Every cookie this HTTP session sends, each scoped to its domain.
|
||||||
|
|
||||||
|
The live client's cookies of each requested origin come first, then the cookies set
|
||||||
|
through :attr:`cookies` for any other host.
|
||||||
|
"""
|
||||||
|
jar = CookieJar()
|
||||||
|
hosts: dict[str, set[str]] = {}
|
||||||
|
for origin, header in self.export_origin_cookies().items():
|
||||||
|
host = urlparse(origin).hostname or ""
|
||||||
|
for name, value in self.parse_cookie_header(header):
|
||||||
|
jar.set_cookie(create_cookie(name, value, domain=host))
|
||||||
|
hosts.setdefault(name, set()).add(host)
|
||||||
|
for domain, values in self.cookies.get_dict_by_domain().items():
|
||||||
|
bare = domain.lstrip(".")
|
||||||
|
for name, value in values.items():
|
||||||
|
if not any(h == bare or h.endswith("." + bare) for h in hosts.get(name, ())):
|
||||||
|
jar.set_cookie(create_cookie(name, value, domain=domain))
|
||||||
|
return jar
|
||||||
|
|
||||||
def import_origin_cookies(self, exported: dict[str, str]) -> None:
|
def import_origin_cookies(self, exported: dict[str, str]) -> None:
|
||||||
"""Load cookie headers from :meth:`export_origin_cookies` into this HTTP session's client."""
|
"""Load cookie headers from :meth:`export_origin_cookies` into this HTTP session's client."""
|
||||||
if not exported:
|
if not exported:
|
||||||
|
|||||||
Reference in New Issue
Block a user