diff --git a/configure.py b/configure.py index 4028c2c..2d45ab7 100755 --- a/configure.py +++ b/configure.py @@ -164,6 +164,13 @@ def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", l reverse_proxy 127.0.0.1:9997 }} handle_path /webrtc/* {{ + # Go Live sends MediaMTX credentials in X-Stream-Auth so Authentik's forward-auth never sees an + # Authorization header; Caddy turns it into Authorization only on the hop to MediaMTX. + @creds header X-Stream-Auth * + reverse_proxy @creds 127.0.0.1:8889 {{ + header_up Authorization {{http.request.header.X-Stream-Auth}} + header_up -X-Stream-Auth + }} reverse_proxy 127.0.0.1:8889 }} handle_path /hls/* {{ diff --git a/live/src/broadcast.js b/live/src/broadcast.js index 6b9b0b4..a2c7767 100644 --- a/live/src/broadcast.js +++ b/live/src/broadcast.js @@ -56,11 +56,14 @@ export async function publish({ stream, path, user, pass, onState }) { const offer = await pc.createOffer() await pc.setLocalDescription(offer) await new Promise(res => { if (pc.iceGatheringState === 'complete') return res(); const t = setTimeout(res, 2000); pc.onicegatheringstatechange = () => { if (pc.iceGatheringState === 'complete') { clearTimeout(t); res() } } }) - const url = `/webrtc/${path}/whip?user=${encodeURIComponent(user)}&pass=${encodeURIComponent(pass)}` - const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp, credentials: 'same-origin' }) + // MediaMTX takes WebRTC credentials only in an Authorization header ("Bearer user:pass"). We send them as + // X-Stream-Auth and Caddy rewrites the header on the MediaMTX hop, so Authentik's gate never sees it. + const auth = { 'X-Stream-Auth': `Bearer ${user}:${pass}` } + const r = await fetch(`/webrtc/${path}/whip`, { method: 'POST', headers: { 'Content-Type': 'application/sdp', ...auth }, body: pc.localDescription.sdp, credentials: 'same-origin' }) if (!r.ok) { pc.close(); throw new Error(r.status === 401 || r.status === 403 ? 'stream key rejected' : r.status === 400 && /busy|already/i.test(await r.text().catch(() => '')) ? 'that stream name is already live (OBS running?)' : `publish failed (${r.status})`) } await pc.setRemoteDescription({ type: 'answer', sdp: await r.text() }) - const session = r.headers.get('Location') + let session = r.headers.get('Location') // MediaMTX answers relative to its own root + if (session && session.startsWith('/') && !session.startsWith('/webrtc/')) session = '/webrtc' + session return { pc, stats: async () => { @@ -71,7 +74,7 @@ export async function publish({ stream, path, user, pass, onState }) { return { kbps, fps, w, h } }, stop: async () => { - try { if (session) await fetch(session.startsWith('http') ? new URL(session).pathname : session, { method: 'DELETE', credentials: 'same-origin' }) } catch {} + try { if (session) await fetch(session.startsWith('http') ? '/webrtc' + new URL(session).pathname : session, { method: 'DELETE', headers: auth, credentials: 'same-origin' }) } catch {} pc.close() stream.getTracks().forEach(t => t.stop()) },