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:
co-authored by
Claude Opus 5
parent
f789c40594
commit
693a6cc867
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user