Go Live: MediaMTX credentials via X-Stream-Auth -> Authorization on the Caddy->MediaMTX hop (WebRTC ignores query params); fix WHIP session DELETE path

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 00:56:56 -05:00
parent dff31b0b37
commit 4b4c7eaedf
2 changed files with 14 additions and 4 deletions

View File

@@ -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/* {{

View File

@@ -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())
},