Musik-Knopf repariert: Inhaltsrichtlinie blockierte die Spotify-Player

Gemeldet am 27.08.2026 ("unten rechts laeuft was nicht richtig mit der
Musik"). Ursache lag nicht in der Seite, sondern in der am 26.08.2026
eingefuehrten Inhaltsrichtlinie: "frame-src 'none'" verbietet dem
Dokument jede Einbettung -- und die beiden Player (Hasidog, Van-Van)
sind genau das. Der Knopf reagierte, das Feld ging auf, die Player
blieben leer. Kein Absturz, keine Fehlermeldung auf der Seite; die
Begruendung stand nur in der Browser-Konsole.

- frame-src erlaubt jetzt genau eine Quelle: https://open.spotify.com.
  Kein Sternchen, kein 'unsafe-*'. Was im Spotify-Rahmen passiert,
  regelt Spotifys eigene Richtlinie.
- Neuer Test pruef-musik.mjs (17 Pruefungen). Er startet bewusst den
  ECHTEN server/index.js statt eines Datei-Servers -- ein einfacher
  Datei-Server erzeugt die Kopfzeile gar nicht und haette den Fehler
  nie gesehen. Geprueft wird im echten Browser, ob die Rahmen wirklich
  von open.spotify.com laden (statt einer Fehlerseite), dazu Tippziel,
  Position, Schliessen per Knopf und Escape, Handy-Layout.

Beim Nachsehen aufgefallen und NOCH OFFEN: PayPal-Kasse und
Google-Anmeldung auf abonnieren.html laden ihre Skripte und Rahmen
ebenfalls von fremden Adressen. Beide sind derzeit nicht scharf
(googleClientId/paypalClientId sind null), wuerden aber mit derselben
Richtlinie an derselben Stelle still ausfallen, sobald Dogi seine
Zugangsdaten eintraegt. Wird gesondert mit ihm besprochen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-27 13:52:36 +02:00
co-authored by Claude Opus 5
parent 887da7b09b
commit 1d99da8ef8
2 changed files with 179 additions and 1 deletions
+154
View File
@@ -0,0 +1,154 @@
/* =====================================================================
Musik-Knopf unten rechts — läuft gegen den ECHTEN Server
WARUM NICHT GEGEN EINEN SIMPLEN DATEI-SERVER
Der Fehler, um den es hier geht, stand in keiner HTML-Datei und in
keiner Zeile JavaScript: Die Seite war fehlerfrei, aber der Server
schickte "frame-src 'none'" mit — und der Browser warf die beiden
Spotify-Player still weg (gemeldet 27.08.2026). Ein Test, der die
Dateien nur ausliefert, hätte das nie gesehen, weil er die Kopfzeile
gar nicht erst erzeugt.
Deshalb startet dieser Test server/index.js selbst auf einem eigenen
Port und prüft im echten Browser, ob die Player wirklich ankommen.
Geprüft wird:
1. Der Knopf ist da und groß genug zum Antippen.
2. Klick klappt das Feld auf, beide Player werden nachgeladen.
3. Die Rahmen laden WIRKLICH von open.spotify.com (nicht Fehlerseite).
4. Kein einziger Verstoß gegen die Inhaltsrichtlinie in der Konsole.
5. Die Richtlinie bleibt eng: nur open.spotify.com, kein Sternchen.
6. Schließen über ✕ und über Escape.
7. Am Handy passt das Feld in den Bildschirm.
===================================================================== */
import { chromium } from "playwright";
import { spawn } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const HIER = dirname(fileURLToPath(import.meta.url));
const PORT = 4433;
const BASIS = `http://127.0.0.1:${PORT}`;
let ok = 0, fehl = 0;
const funde = [];
const pruefe = (name, gut, info) => {
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); }
};
const server = spawn(process.execPath, ["index.js"], {
cwd: join(HIER, "server"),
env: { ...process.env, PORT: String(PORT) },
stdio: ["ignore", "pipe", "pipe"],
});
async function warteAufServer() {
for (let i = 0; i < 40; i++) {
try {
const res = await fetch(BASIS + "/index.html");
if (res.ok) return res;
} catch { /* noch nicht oben */ }
await new Promise((r) => setTimeout(r, 250));
}
throw new Error("Server ist nicht hochgekommen");
}
(async () => {
const antwort = await warteAufServer();
const richtlinie = antwort.headers.get("content-security-policy") || "";
console.log("\n== Inhaltsrichtlinie ==");
const frameRegel = (richtlinie.split(";").find((t) => t.trim().startsWith("frame-src")) || "").trim();
pruefe("frame-src erlaubt Spotify", /https:\/\/open\.spotify\.com/.test(frameRegel), frameRegel || "(keine frame-src-Regel)");
pruefe("frame-src bleibt eng (kein *)", !/[\s:]\*/.test(frameRegel) && !/'unsafe/.test(frameRegel), frameRegel);
const browser = await chromium.launch();
/* ---------- Computer ---------- */
console.log("\n== Computer ==");
const seite = await browser.newPage({ viewport: { width: 1366, height: 800 } });
const verstoesse = new Set();
seite.on("console", (m) => {
const text = m.text();
if (/Content Security Policy|violates the following/i.test(text)) verstoesse.add(text.replace(/\s+/g, " ").slice(0, 160));
});
await seite.goto(BASIS + "/index.html", { waitUntil: "domcontentloaded" });
await seite.waitForSelector("#music-toggle", { timeout: 10000 });
const knopf = await seite.evaluate(() => {
const r = document.getElementById("music-toggle").getBoundingClientRect();
return { breite: Math.round(r.width), hoehe: Math.round(r.height), rechts: Math.round(innerWidth - r.right), unten: Math.round(innerHeight - r.bottom) };
});
pruefe("Knopf ist groß genug zum Antippen", knopf.breite >= 44 && knopf.hoehe >= 44, knopf.breite + "x" + knopf.hoehe + "px");
pruefe("Knopf sitzt unten rechts", knopf.rechts >= 0 && knopf.rechts < 80 && knopf.unten >= 0 && knopf.unten < 80, "Abstand rechts " + knopf.rechts + "px, unten " + knopf.unten + "px");
pruefe("Feld ist zu Beginn zu", await seite.evaluate(() => document.getElementById("music-panel").hidden), "");
await seite.click("#music-toggle");
await seite.waitForTimeout(500);
const nachKlick = await seite.evaluate(() => {
const p = document.getElementById("music-panel");
return {
offen: !p.hidden,
quellen: [...p.querySelectorAll("iframe")].map((f) => f.getAttribute("src") || ""),
nochUngeladen: p.querySelectorAll("iframe[data-src]").length,
};
});
pruefe("Feld klappt auf", nachKlick.offen, "");
pruefe("Beide Player werden nachgeladen", nachKlick.quellen.length === 2 && nachKlick.quellen.every((q) => q.startsWith("https://open.spotify.com/embed/artist/")), nachKlick.quellen.length + " Player");
pruefe("Kein Player bleibt ungeladen", nachKlick.nochUngeladen === 0, nachKlick.nochUngeladen + " ohne src");
/* Der eigentliche Punkt: Kommt der Rahmen wirklich an? Wird er von der
Richtlinie geblockt, steht in seiner Adresse chrome-error://. */
await seite.waitForTimeout(6000);
const rahmen = seite.frames().map((f) => f.url());
const spotifyRahmen = rahmen.filter((u) => u.startsWith("https://open.spotify.com/"));
const kaputteRahmen = rahmen.filter((u) => u.startsWith("chrome-error://"));
pruefe("Beide Spotify-Rahmen sind wirklich geladen", spotifyRahmen.length === 2, spotifyRahmen.length + " geladen: " + spotifyRahmen.map((u) => u.slice(38, 60)).join(", "));
pruefe("Kein Rahmen ist eine Fehlerseite", kaputteRahmen.length === 0, kaputteRahmen.length + " Fehlerseiten");
pruefe("Kein Verstoß gegen die Inhaltsrichtlinie", verstoesse.size === 0, [...verstoesse].join(" | ") || "keine");
await seite.screenshot({ path: "bild-musik-computer.png" });
/* ---------- Schließen ---------- */
console.log("\n== Schließen ==");
await seite.click("#music-close");
await seite.waitForTimeout(300);
pruefe("✕ schließt das Feld", await seite.evaluate(() => document.getElementById("music-panel").hidden), "");
await seite.click("#music-toggle");
await seite.waitForTimeout(300);
await seite.keyboard.press("Escape");
await seite.waitForTimeout(300);
pruefe("Escape schließt das Feld", await seite.evaluate(() => document.getElementById("music-panel").hidden), "");
/* ---------- Handy ---------- */
console.log("\n== Handy (390x844) ==");
const handy = await browser.newPage({ viewport: { width: 390, height: 844 } });
await handy.goto(BASIS + "/index.html", { waitUntil: "domcontentloaded" });
await handy.waitForSelector("#music-toggle", { timeout: 10000 });
await handy.click("#music-toggle");
await handy.waitForTimeout(4000);
const h = await handy.evaluate(() => {
const r = document.getElementById("music-panel").getBoundingClientRect();
return { links: Math.round(r.left), rechts: Math.round(innerWidth - r.right), oben: Math.round(r.top), unten: Math.round(r.bottom),
fensterHoehe: innerHeight, ueberlauf: document.documentElement.scrollWidth - document.documentElement.clientWidth };
});
pruefe("Feld passt seitlich in den Bildschirm", h.links >= 0 && h.rechts >= 0, "links " + h.links + "px, rechts " + h.rechts + "px");
pruefe("Feld ragt nicht oben heraus", h.oben >= 0, "oben " + h.oben + "px");
pruefe("Seite läuft nicht seitlich über", h.ueberlauf <= 1, h.ueberlauf + "px");
const handyRahmen = handy.frames().filter((f) => f.url().startsWith("https://open.spotify.com/")).length;
pruefe("Am Handy laden die Player auch", handyRahmen === 2, handyRahmen + " geladen");
await handy.screenshot({ path: "bild-musik-handy.png" });
await browser.close();
server.kill();
console.log("\n===== " + ok + " ok, " + fehl + " Fehler =====");
if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); }
process.exit(fehl ? 1 : 0);
})().catch((e) => {
console.error("Testlauf abgebrochen:", e.message);
server.kill();
process.exit(1);
});
+25 -1
View File
@@ -186,7 +186,31 @@ export function inhaltsrichtlinie(wurzel) {
"form-action 'self'",
/* Ersetzt X-Frame-Options; wirkt gegen Klickfallen. */
"frame-ancestors 'self'",
"frame-src 'none'",
/* ⚠️ HIER STAND "frame-src 'none'" — DAS HAT DEN MUSIK-KNOPF
STILLGELEGT (gemeldet 27.08.2026).
Der Knopf unten rechts klappt ein Feld mit zwei Spotify-Playern
auf (Hasidog und Van-Van, siehe main.js). Genau die sind
<iframe>s, und 'none' verbietet dem Dokument JEDE Einbettung.
Sichtbar war das denkbar undankbar: Der Knopf reagierte, das
Feld ging auf, die Überschriften standen da — nur wo die Player
sein sollten, blieb es leer. Nichts stürzte ab, nichts meldete
einen Fehler; die Begründung stand ausschließlich in der
Browser-Konsole.
Erlaubt ist deshalb genau eine Quelle: open.spotify.com. Das ist
kein Aufweichen der Richtlinie -- 'none' war schlicht zu eng für
eine Seite, die diese Player seit jeher einbettet. Was INNERHALB
des Spotify-Rahmens passiert, regelt Spotifys eigene Richtlinie,
nicht unsere; ein eingeschleustes Skript gewinnt hier also nichts
außer der Möglichkeit, ausgerechnet Spotify einzubetten.
Kommt später ein weiterer Anbieter dazu (PayPal-Kasse und
Google-Anmeldung auf abonnieren.html arbeiten ebenfalls mit
Rahmen), muss er hier NAMENTLICH ergänzt werden -- sonst
wiederholt sich exakt dieser stille Ausfall an der Kasse.
pruef-musik.mjs prüft das im echten Browser mit. */
"frame-src https://open.spotify.com",
"worker-src 'self'",
/* Hebt gemischte Inhalte auf HTTPS, statt sie zu blockieren. */
"upgrade-insecure-requests",