From fdcba26c7fa5ce41cfb9e80278b085ab9bfd1146 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 3 May 2026 23:01:00 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20proxy=20suit=20les=20redirects=20c=C3=B4?= =?UTF-8?q?t=C3=A9=20serveur=20(302=20=E2=86=92=20CORS=20bypass)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/audioRoutes.ts | 90 +++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/backend/src/routes/audioRoutes.ts b/backend/src/routes/audioRoutes.ts index b9ce86f..bbe8e1f 100644 --- a/backend/src/routes/audioRoutes.ts +++ b/backend/src/routes/audioRoutes.ts @@ -1,10 +1,10 @@ import { Router, Request, Response } from 'express'; import https from 'https'; import http from 'http'; +import { IncomingMessage } from 'http'; const router = Router(); -// Block private/loopback ranges to prevent SSRF const BLOCKED = [ /^127\./, /^10\./, @@ -18,6 +18,69 @@ function isBlocked(hostname: string): boolean { return BLOCKED.some(r => r.test(hostname)); } +const REDIRECT_CODES = new Set([301, 302, 303, 307, 308]); + +function fetchAudio( + url: string, + headers: Record, + res: Response, + depth: number, +): void { + if (depth > 8) { + if (!res.headersSent) res.status(502).json({ message: 'Too many redirects' }); + return; + } + + let parsed: URL; + try { + parsed = new URL(url); + } catch { + if (!res.headersSent) res.status(400).json({ message: 'Invalid redirect URL' }); + return; + } + + if (!['http:', 'https:'].includes(parsed.protocol)) { + if (!res.headersSent) res.status(400).json({ message: 'Protocol not allowed' }); + return; + } + if (isBlocked(parsed.hostname)) { + if (!res.headersSent) res.status(403).json({ message: 'Forbidden' }); + return; + } + + const lib = parsed.protocol === 'https:' ? https : http; + + const proxyReq = lib.request(url, { headers }, (proxyRes: IncomingMessage) => { + const status = proxyRes.statusCode ?? 200; + + // Follow redirects server-side so browser never sees non-CORS domains + if (REDIRECT_CODES.has(status) && proxyRes.headers.location) { + const next = new URL(proxyRes.headers.location, url).toString(); + proxyRes.resume(); // drain to free socket + fetchAudio(next, headers, res, depth + 1); + return; + } + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Content-Type, Accept-Ranges'); + + const forward = ['content-type', 'content-length', 'content-range', 'accept-ranges']; + for (const h of forward) { + const v = proxyRes.headers[h]; + if (v) res.setHeader(h, v as string); + } + + res.status(status); + proxyRes.pipe(res); + }); + + proxyReq.on('error', () => { + if (!res.headersSent) res.status(502).json({ message: 'Proxy error' }); + }); + + proxyReq.end(); +} + router.get('/proxy', (req: Request, res: Response) => { const raw = req.query.url as string; if (!raw) { res.status(400).json({ message: 'url required' }); return; } @@ -40,30 +103,11 @@ router.get('/proxy', (req: Request, res: Response) => { 'User-Agent': 'Mozilla/5.0 (compatible; Podcastic/1.0)', 'Accept': '*/*', }; - if (req.headers.range) upstream['Range'] = req.headers.range; + if (req.headers.range) upstream['Range'] = req.headers.range as string; - const lib = parsed.protocol === 'https:' ? https : http; + req.on('close', () => res.destroy()); - const proxyReq = lib.request(raw, { headers: upstream }, (proxyRes) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Content-Type, Accept-Ranges'); - - const forward = ['content-type', 'content-length', 'content-range', 'accept-ranges']; - for (const h of forward) { - const v = proxyRes.headers[h]; - if (v) res.setHeader(h, v); - } - - res.status(proxyRes.statusCode ?? 200); - proxyRes.pipe(res); - }); - - proxyReq.on('error', () => { - if (!res.headersSent) res.status(502).json({ message: 'Proxy error' }); - }); - - req.on('close', () => proxyReq.destroy()); - proxyReq.end(); + fetchAudio(raw, upstream, res, 0); }); export default router;