:3000

Oasis Proxy

Routes, templates & security headers

Mapa de Rutas — Arq. 6

RouteUpstreamContent-TypePropósito
GET /videoµStreamer :8080/streammultipart/x-mixed-replaceMJPEG stream continuo
GET /snapshotµStreamer :8080/snapshotimage/jpegFrame único para thumbnails / SSB blob
GET /audioMumble bridge (interno)audio/oggAudio downstream chunked
GET /camtext/htmlVista unificada (template webrtc_view)
GET /statusµStreamer :8080/stateapplication/jsonHealth check JSON (fps, encoder, clients)

Key Concept: ¿Por Qué Proxy y No Redirect?

µStreamer escucha en 127.0.0.1:8080. El browser del peer no tiene acceso directo a localhost del SNH. Oasis hace proxy del stream, exponiendo un solo puerto (:3000) con headers de seguridad controlados.
EnfoquePuertos expuestosCORS/CSP controlTor friendly
Redirect → :80802 (:3000 + :8080)✘ No✘ (2 hidden services)
Proxy (recomendado)1 (:3000)✔ Total✔ (1 hidden service)

Implementación Proxy — Node.js

GET /video — MJPEG stream proxy
// Proxy µStreamer MJPEG stream → browser const http = require('http'); app.get('/video', (req, res) => { const upstream = http.get( 'http://127.0.0.1:8080/stream', (mjpeg) => { res.writeHead(200, { 'Content-Type': mjpeg.headers['content-type'], 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' }); mjpeg.pipe(res); } ); upstream.on('error', (err) => { res.status(502).end('upstream down'); }); req.on('close', () => upstream.destroy()); });
req.on('close'): destruir el upstream cuando el browser cierra la pestaña. Sin esto, los sockets se acumulan y µStreamer cuenta "clientes fantasma".
GET /snapshot — JPEG single frame
app.get('/snapshot', (req, res) => { const upstream = http.get( 'http://127.0.0.1:8080/snapshot', (snap) => { res.writeHead(200, { 'Content-Type': 'image/jpeg', 'Cache-Control': 'no-store' }); snap.pipe(res); } ); upstream.on('error', () => res.status(502).end()); req.on('close', () => upstream.destroy()); });
GET /audio — Mumble bridge OGG
// audioBridge = readable stream del bridge Mumble→OGG app.get('/audio', (req, res) => { res.writeHead(200, { 'Content-Type': 'audio/ogg', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-store' }); audioBridge.pipe(res); req.on('close', () => audioBridge.unpipe(res)); });

Template: webrtc_view (cero JS)

<!-- views/webrtc_view.ejs — lo que sirve GET /cam --> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>SNH — Cámara</title> <style> body { margin:0; background:#111; color:#eee; font-family:monospace; text-align:center; } img { max-width:100%; height:auto; display:block; margin:0 auto; } audio { margin:12px auto; display:block; } .meta { font-size:0.7rem; opacity:0.5; margin:8px; } </style> </head> <body> <img src="/video" alt="MJPEG stream"> <audio src="/audio" controls autoplay></audio> <p class="meta"> µStreamer MJPEG · Mumble OGG · cero JavaScript </p> </body> </html>
Cero JS: el browser nativo del SNH (Oasis) renderiza HTML/CSS puro. <img> con MJPEG se actualiza automáticamente (el browser lo maneja nativamente). <audio> con OGG chunked hace autoplay sin scripts.

Security Headers — Oasis Middleware

// Helmet-style headers (sin dependencia externa) app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Referrer-Policy', 'no-referrer'); res.setHeader('Content-Security-Policy', "default-src 'self'; " + "img-src 'self'; " + "media-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + "script-src 'none'" // ← cero JS ); next(); });
script-src 'none': refuerza a nivel de CSP que no se ejecute JS. Doble seguridad: no lo servimos Y el browser no lo aceptaría.

Health Check Pattern

// GET /status — proxy al JSON de µStreamer + Mumble liveness app.get('/status', async (req, res) => { const [ustreamer, mumble] = await Promise.allSettled([ fetch('http://127.0.0.1:8080/state').then(r => r.json()), checkMumbleAlive(64738) ]); res.json({ video: ustreamer.status === 'fulfilled' ? { ok: true, ...ustreamer.value } : { ok: false }, audio: { ok: mumble.status === 'fulfilled' }, uptime: process.uptime() }); });

DRY Rules for Oasis Routes

1. Toda upstream es 127.0.0.1 — nunca exponer puertos internos al exterior.
2. Siempre destroy upstream on req close — evitar socket leaks.
3. Siempre Cache-Control: no-store en streams — no cachear datos en vuelo.
4. Un solo puerto público: :3000 — todo lo demás es localhost.
5. CSP con script-src 'none' — garantía doble de cero JS.
6. Para snapshots, considerar guardar como SSB blob (inmutable, direccionable por hash).

Keywords

proxy pipe(res) CSP no-store script-src 'none' SAMEORIGIN upstream.destroy() :3000 chunked OGG MJPEG SSB blob