From 7b78d0b7781effb94f25ea3adfc5ab56038cc62e Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Wed, 27 May 2026 10:06:54 +0200 Subject: [PATCH 01/28] fix(cors): allow play from .to / staging / onion mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daemon CORS allowlist was hardcoded to torrentclaw.com + localhost. Browsers playing from any other official mirror (.to, onion, www., staging.) received 200 + body from the daemon's HLS server but no Access-Control-Allow-Origin header, so the response was dropped client-side. Probe loop treated every candidate as a failure and surfaced "No se puede conectar con tu agente — 404 todos los canales" even though the tunnel + ffmpeg were healthy. Static baseline now includes the full known mirror set (.com / www / app / staging / .to / www.to / built-in onion). At startup the daemon also fetches /api/mirrors with IPFS fallback and merges the live origins, so a future mirror addition does not require a CLI rebuild. --- internal/cmd/daemon.go | 55 ++++++++++++++++++++++++++++++++++++- internal/engine/validate.go | 15 ++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/internal/cmd/daemon.go b/internal/cmd/daemon.go index 19c4b7c..7cd1023 100644 --- a/internal/cmd/daemon.go +++ b/internal/cmd/daemon.go @@ -293,7 +293,15 @@ func runDaemonStart() error { // Create persistent stream server streamSrv := engine.NewStreamServer(cfg.Download.StreamPort) streamSrv.SetUPnPEnabled(cfg.Download.EnableUPnP) - streamSrv.SetCORSAllowedOrigins(cfg.Download.CORSExtraOrigins) + // CORS extras = operator config + dynamic mirror list from /api/mirrors. + // Without the mirror merge, a user playing from `torrentclaw.to` (or any + // future mirror) hits the daemon, gets 200 + body, but no + // `Access-Control-Allow-Origin` → browser drops the response → player + // reports "404 todos los canales". Fetching /api/mirrors at startup + // future-proofs against mirror additions without a CLI rebuild. + corsExtras := append([]string(nil), cfg.Download.CORSExtraOrigins...) + corsExtras = append(corsExtras, mirrorCORSOrigins(ctx, cfg, userAgent)...) + streamSrv.SetCORSAllowedOrigins(corsExtras) // Reap HLS tmpdirs left over from a previous daemon run before we start // accepting new sessions. The in-memory registry doesn't survive a // restart, so without this disk usage grows unbounded across restarts. @@ -862,3 +870,48 @@ func superviseFunnel(ctx context.Context, d *agent.Daemon, port int) { backoff = min(backoff*2, maxBackoff) } } + +// mirrorCORSOrigins fetches /api/mirrors from the configured primary (+ extra +// mirror candidates + static IPFS fallback) and returns the discovered URLs as +// Origin strings. Best-effort: any failure logs a warning and returns an empty +// slice; the static defaultCORSAllowedOrigins in validate.go covers the known +// mirrors (.com / .to / built-in onion) so the daemon still accepts the +// official surfaces when this call fails. +// +// Bounded to a short timeout so a slow /api/mirrors response can't delay +// daemon startup — every second here is a second the user can't play. +func mirrorCORSOrigins(parent context.Context, cfg config.Config, userAgent string) []string { + ctx, cancel := context.WithTimeout(parent, 10*time.Second) + defer cancel() + + candidates := append([]string{cfg.Auth.APIURL}, cfg.Auth.Mirrors...) + resp, err := agent.FetchMirrorsWithFallback(ctx, candidates, userAgent) + if err != nil { + log.Printf("[cors] mirror discovery failed (%v) — using static allowlist only", err) + return nil + } + + seen := make(map[string]struct{}) + out := make([]string, 0, len(resp.Mirrors)) + add := func(rawURL string) { + if rawURL == "" { + return + } + origin := strings.TrimRight(rawURL, "/") + if _, dup := seen[origin]; dup { + return + } + seen[origin] = struct{}{} + out = append(out, origin) + } + for _, m := range resp.Mirrors { + add(m.URL) + } + if resp.Tor != nil { + add(resp.Tor.URL) + } + if len(out) > 0 { + log.Printf("[cors] merged %d mirror origins from /api/mirrors", len(out)) + } + return out +} diff --git a/internal/engine/validate.go b/internal/engine/validate.go index dd07516..0efd4de 100644 --- a/internal/engine/validate.go +++ b/internal/engine/validate.go @@ -21,12 +21,27 @@ var validSessionID = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,128}$`) // 127.0.0.1 is listed in addition to localhost because some browsers treat // them as distinct origins for CORS. // +// Mirrors (`.to`, `staging.torrentclaw.com`, `www.`) are listed so a user +// playing from any official mirror succeeds the HEAD probe; without these +// the browser drops the response for "missing ACAO" and the player reports +// "404 todos los canales" even though the daemon returned 200. +// // Note: media tags (