const DISALLOWED = new Set([ 'host', 'connection', 'content-length', 'transfer-encoding', 'upgrade', 'keep-alive', 'proxy-authorization', 'proxy-authentication', 'te', 'trailer', 'via', 'warning', 'expect', 'date', 'dnt', 'accept-encoding', ]); function corsHeaders(extra = {}) { return new Headers({ 'access-control-allow-origin': '*', 'cache-control': 'no-store', ...extra, }); } function jsonResponse(status, obj) { return new Response(JSON.stringify(obj), { status, headers: corsHeaders({ 'content-type': 'application/json; charset=utf-8' }), }); } function parseProxyHeaders(raw) { if (!raw) return {}; let parsed; try { parsed = JSON.parse(raw); } catch { return {}; } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}; const out = {}; for (const [name, value] of Object.entries(parsed)) { if (typeof value !== 'string' || !value) continue; if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) continue; if (DISALLOWED.has(name.toLowerCase())) continue; out[name] = value; } return out; } function proxyTarget(target, headers, token) { const query = new URLSearchParams({ url: target, h: JSON.stringify(headers || {}), }); if (token) query.set('token', token); return `media?${query.toString()}`; } function rewriteM3u8(text, baseUrl, headers, token) { return text .split(/\r?\n/) .map((line) => { const trimmed = line.trim(); if (!trimmed) return line; if (trimmed.startsWith('#')) { return line.replace(/(URI\s*=\s*)(?:"([^"]*)"|'([^']*)')/g, (match, prefix, double, single) => { const uri = double !== undefined ? double : single; try { return `${prefix}"${proxyTarget(new URL(uri, baseUrl).toString(), headers, token)}"`; } catch { return match; } }); } try { return proxyTarget(new URL(trimmed, baseUrl).toString(), headers, token); } catch { return line; } }) .join('\n'); } async function proxy(request, url, env) { const target = (url.searchParams.get('url') || '').trim(); const headers = parseProxyHeaders(url.searchParams.get('h')); const token = url.searchParams.get('token') || ''; let parsed; try { parsed = new URL(target); } catch { return jsonResponse(400, { error: 'missing or invalid ?url=' }); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { return jsonResponse(400, { error: 'proxy only supports http/https' }); } const upstreamHeaders = new Headers(); for (const [name, value] of Object.entries(headers)) { upstreamHeaders.set(name, value); } const range = request.headers.get('range'); if (range) upstreamHeaders.set('range', range); let upstream; try { upstream = await fetch(parsed.toString(), { headers: upstreamHeaders, redirect: 'follow', }); } catch (e) { return jsonResponse(502, { error: `proxy fetch failed: ${e.message}` }); } const contentType = upstream.headers.get('content-type') || ''; const looksLikeM3u8 = /mpegurl/i.test(contentType) || /\.m3u8(?:[?#].*)?$/i.test(parsed.pathname + parsed.search); if (looksLikeM3u8 && upstream.status < 400) { try { const text = await upstream.text(); if (/mpegurl/i.test(contentType) || text.trimStart().startsWith('#EXTM3U')) { const body = rewriteM3u8(text, parsed.toString(), headers, token); return new Response(body, { status: upstream.status, headers: corsHeaders({ 'content-type': 'application/vnd.apple.mpegurl; charset=utf-8' }), }); } return new Response(text, { status: upstream.status, headers: corsHeaders({ 'content-type': contentType || 'text/plain; charset=utf-8' }), }); } catch (e) { return jsonResponse(502, { error: `proxy playlist failed: ${e.message}` }); } } const out = corsHeaders(); for (const name of ['content-type', 'content-range', 'accept-ranges']) { const value = upstream.headers.get(name); if (value) out.set(name, value); } if (upstream.status === 206) { const value = upstream.headers.get('content-length'); if (value) out.set('content-length', value); } return new Response(upstream.body || null, { status: upstream.status, headers: out, }); } export default { async fetch(request, env) { const url = new URL(request.url); if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders({ 'access-control-allow-methods': 'GET, OPTIONS', 'access-control-allow-headers': '*', }), }); } if (request.method !== 'GET') { return jsonResponse(405, { error: 'method not allowed, use GET' }); } if (env.PROXY_TOKEN) { const token = url.searchParams.get('token') || request.headers.get('x-proxy-token') || ''; if (token !== env.PROXY_TOKEN) { return jsonResponse(403, { error: 'unauthorized' }); } } return await proxy(request, url, env); }, };