Cookie heisst Cookie

„Keks", „Keksglas", „Kekse" — eingedeutscht, wo es nichts zu
verdeutschen gibt. Ein Cookie ist ein Cookie; wer danach sucht, sucht
nicht nach Keksen, und wer den Quelltext liest, liest daneben die
Schnittstelle, in der es `HTTPCookieStorage` heisst.

Betrifft Kommentare und Prüfungsnamen, kein Verhalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-10 20:42:29 +02:00
co-authored by Claude Opus 5
parent f789c40594
commit 693a6cc867
6 changed files with 32 additions and 32 deletions
@@ -135,14 +135,14 @@ public final class AVPlayerEngine: VideoPlayerEngine {
// //
// Serverneutral: wo kein Cookie liegt, ist die Liste leer und es // Serverneutral: wo kein Cookie liegt, ist die Liste leer und es
// ändert sich nichts. // ändert sich nichts.
let kekse = HTTPCookieStorage.shared.cookies(for: source.url) ?? [] let cookies = HTTPCookieStorage.shared.cookies(for: source.url) ?? []
let asset = AVURLAsset(url: source.url, options: [ let asset = AVURLAsset(url: source.url, options: [
// Keine genaue Dauer verlangen. Sonst durchsucht AVFoundation bei // Keine genaue Dauer verlangen. Sonst durchsucht AVFoundation bei
// manchen Formaten die ganze Datei, bevor es überhaupt anfängt — // manchen Formaten die ganze Datei, bevor es überhaupt anfängt —
// und „ganze Datei" heißt bei einem Remux vierzig Gigabyte. // und „ganze Datei" heißt bei einem Remux vierzig Gigabyte.
AVURLAssetPreferPreciseDurationAndTimingKey: false, AVURLAssetPreferPreciseDurationAndTimingKey: false,
AVURLAssetHTTPCookiesKey: kekse, AVURLAssetHTTPCookiesKey: cookies,
]) ])
let item = AVPlayerItem(asset: asset) let item = AVPlayerItem(asset: asset)
@@ -83,10 +83,10 @@ public actor StreamCache {
// **Die Anmeldung muss mit.** // **Die Anmeldung muss mit.**
// //
// Eine flüchtige Sitzung bringt ihren eigenen, leeren Keksspeicher // Eine flüchtige Sitzung bringt ihren eigenen, leeren Cookie-Speicher
// mit — jede Anfrage von hier ging also unangemeldet hinaus. Bei // mit — jede Anfrage von hier ging also unangemeldet hinaus. Bei
// Jellyfin fiel das nie auf: dort steht der Schlüssel in der Adresse. // Jellyfin fiel das nie auf: dort steht der Schlüssel in der Adresse.
// Flaro meldet über ein Keks an, und im Protokoll stand genau das: // Flaro meldet über ein Cookie an, und im Protokoll stand genau das:
// `FlaroToken was challenged`, einmal je Wiedergabe, gefolgt von // `FlaroToken was challenged`, einmal je Wiedergabe, gefolgt von
// zwölf Sekunden Stille und einer zweiten Bestellung. // zwölf Sekunden Stille und einer zweiten Bestellung.
configuration.httpCookieStorage = HTTPCookieStorage.shared configuration.httpCookieStorage = HTTPCookieStorage.shared
@@ -6,11 +6,11 @@ import MediaKit
/// Adressen und Zugang — alles, was ohne Netz entsteht. /// Adressen und Zugang — alles, was ohne Netz entsteht.
@Suite("Flaro: Adressen und Zugang") @Suite("Flaro: Adressen und Zugang")
struct AdressenTests { struct AdressenTests {
private func backend(_ transport: Attrappe, keksglas: Keksglas? = nil) -> FlaroBackend { private func backend(_ transport: Attrappe, cookieglas: Cookieglas? = nil) -> FlaroBackend {
FlaroBackend( FlaroBackend(
client: FlaroClient( client: FlaroClient(
baseURL: Proben.adresse, identity: Proben.kennung, token: "geheim-123", baseURL: Proben.adresse, identity: Proben.kennung, token: "geheim-123",
transport: transport, cookieJar: keksglas), transport: transport, cookieJar: cookieglas),
account: FlaroAccount( account: FlaroAccount(
baseURL: Proben.adresse, serverName: "Wohnzimmer", baseURL: Proben.adresse, serverName: "Wohnzimmer",
userID: "u1", userName: "guido", isAdministrator: true)) userID: "u1", userName: "guido", isAdministrator: true))
@@ -32,17 +32,17 @@ struct AdressenTests {
} }
@Test("Ein Zugang wird als Cookie abgelegt") @Test("Ein Zugang wird als Cookie abgelegt")
func tokenWirdZuKeks() async { func tokenWirdZuCookie() async {
let glas = Keksglas() let glas = Cookieglas()
_ = FlaroClient( _ = FlaroClient(
baseURL: Proben.adresse, identity: Proben.kennung, token: "geheim-123", baseURL: Proben.adresse, identity: Proben.kennung, token: "geheim-123",
transport: Attrappe(rumpf: "{}"), cookieJar: glas) transport: Attrappe(rumpf: "{}"), cookieJar: glas)
let keks = glas.kekse.first let cookie = glas.cookies.first
#expect(keks?.name == "flaro_session") #expect(cookie?.name == "flaro_session")
#expect(keks?.value == "geheim-123") #expect(cookie?.value == "geheim-123")
#expect(keks?.path == "/") #expect(cookie?.path == "/")
#expect(keks?.domain.hasSuffix("nas") == true) #expect(cookie?.domain.hasSuffix("nas") == true)
} }
/// **Nach einem Neustart meldet sich niemand neu an.** /// **Nach einem Neustart meldet sich niemand neu an.**
@@ -51,8 +51,8 @@ struct AdressenTests {
/// allein aus der Antwort des Servers, wäre keines da — und jedes Plakat /// allein aus der Antwort des Servers, wäre keines da — und jedes Plakat
/// bliebe grau, obwohl der Zugang gilt. /// bliebe grau, obwohl der Zugang gilt.
@Test("Auch ein wiederaufgenommenes Token legt das Cookie an") @Test("Auch ein wiederaufgenommenes Token legt das Cookie an")
func keksAuchOhneAnmeldung() async throws { func cookieAuchOhneAnmeldung() async throws {
let glas = Keksglas() let glas = Cookieglas()
let transport = Attrappe([ let transport = Attrappe([
("/api/v1/auth/me", 200, #"{"id":"u1","userName":"guido","isAdministrator":false}"#), ("/api/v1/auth/me", 200, #"{"id":"u1","userName":"guido","isAdministrator":false}"#),
("/api/v1/system/info", 200, Proben.info), ("/api/v1/system/info", 200, Proben.info),
@@ -62,7 +62,7 @@ struct AdressenTests {
baseURL: Proben.adresse, token: "geheim-123", baseURL: Proben.adresse, token: "geheim-123",
identity: Proben.kennung, transport: transport, cookieJar: glas) identity: Proben.kennung, transport: transport, cookieJar: glas)
#expect(glas.kekse.contains { $0.name == "flaro_session" && $0.value == "geheim-123" }) #expect(glas.cookies.contains { $0.name == "flaro_session" && $0.value == "geheim-123" })
} }
@Test("Die Download-Adresse zeigt auf die Datei, nicht auf den Titel") @Test("Die Download-Adresse zeigt auf die Datei, nicht auf den Titel")
@@ -104,7 +104,7 @@ struct AdressenTests {
let (token, konto, _) = try await FlaroSignIn.signIn( let (token, konto, _) = try await FlaroSignIn.signIn(
baseURL: Proben.adresse, userName: "guido", password: "geheim", baseURL: Proben.adresse, userName: "guido", password: "geheim",
identity: Proben.kennung, transport: transport, cookieJar: Keksglas()) identity: Proben.kennung, transport: transport, cookieJar: Cookieglas())
let rumpf = try #require(transport.rumpf(von: "/api/v1/auth/login")) let rumpf = try #require(transport.rumpf(von: "/api/v1/auth/login"))
@@ -46,15 +46,15 @@ final class Attrappe: FlaroTransport, @unchecked Sendable {
} }
} }
/// Ein Keksglas, das nur mitschreibt. /// Ein Cookieglas, das nur mitschreibt.
/// ///
/// Der Speicher des Systems anzufassen hiesse, dass eine Prüfung die nächste /// Der Speicher des Systems anzufassen hiesse, dass eine Prüfung die nächste
/// beeinflusst — und auf einem Entwicklerrechner auch den Browser. /// beeinflusst — und auf einem Entwicklerrechner auch den Browser.
final class Keksglas: FlaroCookieJar, @unchecked Sendable { final class Cookieglas: FlaroCookieJar, @unchecked Sendable {
private let sperre = NSLock() private let sperre = NSLock()
private var inhalt: [HTTPCookie] = [] private var inhalt: [HTTPCookie] = []
var kekse: [HTTPCookie] { sperre.withLock { inhalt } } var cookies: [HTTPCookie] { sperre.withLock { inhalt } }
func store(_ cookie: HTTPCookie) { sperre.withLock { inhalt.append(cookie) } } func store(_ cookie: HTTPCookie) { sperre.withLock { inhalt.append(cookie) } }
@@ -50,11 +50,11 @@ public sealed class WebInterfaceTests
var client = server.OhneGedaechtnis(); var client = server.OhneGedaechtnis();
var angemeldet = await client.PostAsJsonAsync("/api/v1/auth/login", var angemeldet = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("guido", "ein-langes-passwort", "Browser", "browser-1")); new LoginRequest("guido", "ein-langes-passwort", "Browser", "browser-1"));
var keks = Keks(angemeldet); var cookie = Cookie(angemeldet);
// Ohne Kopfzeile — genau so, wie ein <img> fragt. // Ohne Kopfzeile — genau so, wie ein <img> fragt.
var anfrage = new HttpRequestMessage(HttpMethod.Get, "/api/v1/movies"); var anfrage = new HttpRequestMessage(HttpMethod.Get, "/api/v1/movies");
anfrage.Headers.Add("Cookie", keks); anfrage.Headers.Add("Cookie", cookie);
var antwort = await client.SendAsync(anfrage); var antwort = await client.SendAsync(anfrage);
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
@@ -80,17 +80,17 @@ public sealed class WebInterfaceTests
var client = server.OhneGedaechtnis(); var client = server.OhneGedaechtnis();
var angemeldet = await client.PostAsJsonAsync("/api/v1/auth/login", var angemeldet = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("guido", "ein-langes-passwort", "Browser", "browser-1")); new LoginRequest("guido", "ein-langes-passwort", "Browser", "browser-1"));
var keks = Keks(angemeldet); var cookie = Cookie(angemeldet);
var abmelden = new HttpRequestMessage(HttpMethod.Post, "/api/v1/auth/logout"); var abmelden = new HttpRequestMessage(HttpMethod.Post, "/api/v1/auth/logout");
abmelden.Headers.Add("Cookie", keks); abmelden.Headers.Add("Cookie", cookie);
(await client.SendAsync(abmelden)).EnsureSuccessStatusCode(); (await client.SendAsync(abmelden)).EnsureSuccessStatusCode();
// **Das Token muss serverseitig weg sein, nicht nur das Cookie im // **Das Token muss serverseitig weg sein, nicht nur das Cookie im
// Browser.** Ein gelöschtes Cookie schützt niemanden, der den Wert // Browser.** Ein gelöschtes Cookie schützt niemanden, der den Wert
// vorher mitgeschrieben hat. // vorher mitgeschrieben hat.
var danach = new HttpRequestMessage(HttpMethod.Get, "/api/v1/movies"); var danach = new HttpRequestMessage(HttpMethod.Get, "/api/v1/movies");
danach.Headers.Add("Cookie", keks); danach.Headers.Add("Cookie", cookie);
Assert.That((await client.SendAsync(danach)).StatusCode, Assert.That((await client.SendAsync(danach)).StatusCode,
Is.EqualTo(HttpStatusCode.Unauthorized)); Is.EqualTo(HttpStatusCode.Unauthorized));
@@ -236,8 +236,8 @@ public sealed class WebInterfaceTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(GanzerKeks(dauerhaft), Does.Contain("max-age=31536000").IgnoreCase); Assert.That(GanzesCookie(dauerhaft), Does.Contain("max-age=31536000").IgnoreCase);
Assert.That(GanzerKeks(nurJetzt), Does.Not.Contain("max-age").IgnoreCase); Assert.That(GanzesCookie(nurJetzt), Does.Not.Contain("max-age").IgnoreCase);
}); });
} }
@@ -251,7 +251,7 @@ public sealed class WebInterfaceTests
var antwort = await server.OhneGedaechtnis().PostAsJsonAsync("/api/v1/auth/login", var antwort = await server.OhneGedaechtnis().PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("guido", "ein-langes-passwort", "Browser", "b1")); new LoginRequest("guido", "ein-langes-passwort", "Browser", "b1"));
Assert.That(GanzerKeks(antwort), Does.Contain("max-age=31536000").IgnoreCase); Assert.That(GanzesCookie(antwort), Does.Contain("max-age=31536000").IgnoreCase);
} }
/// <summary>Abmelden scheitert nie — auch nicht ohne gültiges Token.</summary> /// <summary>Abmelden scheitert nie — auch nicht ohne gültiges Token.</summary>
@@ -300,10 +300,10 @@ public sealed class WebInterfaceTests
} }
/// <summary>Nimmt den Namen und den Wert aus der Set-Cookie-Kopfzeile.</summary> /// <summary>Nimmt den Namen und den Wert aus der Set-Cookie-Kopfzeile.</summary>
private static string Keks(HttpResponseMessage antwort) => private static string Cookie(HttpResponseMessage antwort) =>
GanzerKeks(antwort).Split(';')[0]; GanzesCookie(antwort).Split(';')[0];
/// <summary>Die ganze Kopfzeile — samt Lebensdauer und Schutzangaben.</summary> /// <summary>Die ganze Kopfzeile — samt Lebensdauer und Schutzangaben.</summary>
private static string GanzerKeks(HttpResponseMessage antwort) => private static string GanzesCookie(HttpResponseMessage antwort) =>
antwort.Headers.GetValues("Set-Cookie").Single(); antwort.Headers.GetValues("Set-Cookie").Single();
} }
+1 -1
View File
@@ -720,7 +720,7 @@ export type Untertitelstand =
* einem Dreistünder zu ziehen heisst, die ganze Datei zu lesen, und das * einem Dreistünder zu ziehen heisst, die ganze Datei zu lesen, und das
* dauert beim ersten Mal Minuten. * dauert beim ersten Mal Minuten.
* *
* **Und mit Token, nicht mit Keks.** Ein `<track>` wies sich über das Cookie * **Und mit Token, nicht mit Cookie.** Ein `<track>` wies sich über das Cookie
* aus. Das geht, solange es eines gibt — gemessen fiel es auf, als keines da * aus. Das geht, solange es eines gibt — gemessen fiel es auf, als keines da
* war und jeder Abruf mit 401 zurückkam, ohne dass irgendwo etwas stand. */ * war und jeder Abruf mit 401 zurückkam, ohne dass irgendwo etwas stand. */
export async function untertitelHolen( export async function untertitelHolen(