| import { NextRequest, NextResponse } from "next/server";
|
| import {
|
| verify,
|
| mintStreamToken,
|
| mintDashBase,
|
| StreamClaim,
|
| } from "@/lib/security/tokens";
|
|
|
| export const runtime = "nodejs";
|
| export const dynamic = "force-dynamic";
|
|
|
| const UA =
|
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
|
|
|
|
|
| function rewriteManifest(
|
| text: string,
|
| baseUrl: string,
|
| origin: string,
|
| ref?: string
|
| ): string {
|
| const base = new URL(baseUrl);
|
| const wrap = (u: string) => {
|
| let abs: string;
|
| try {
|
| abs = new URL(u, base).toString();
|
| } catch {
|
| return u;
|
| }
|
| return `${origin}/api/stream?t=${encodeURIComponent(
|
| mintStreamToken(abs, ref)
|
| )}`;
|
| };
|
| return text
|
| .split("\n")
|
| .map((line) => {
|
| const t = line.trim();
|
| if (!t) return line;
|
| if (t.startsWith("#")) {
|
|
|
| return line.replace(/URI="([^"]+)"/g, (_m, u) => `URI="${wrap(u)}"`);
|
| }
|
| return wrap(t);
|
| })
|
| .join("\n");
|
| }
|
|
|
|
|
|
|
| function rewriteMpd(
|
| xml: string,
|
| mpdUrl: string,
|
| origin: string,
|
| ref?: string
|
| ): string {
|
| const bases = [...xml.matchAll(/<BaseURL>([^<]+)<\/BaseURL>/g)].map((m) =>
|
| m[1].trim()
|
| );
|
| let real = bases.find((u) => !u.includes("x-bc")) || bases[0];
|
| if (!real) {
|
|
|
| real = mpdUrl.split("?")[0].replace(/[^/]*$/, "");
|
| } else {
|
| real = new URL(real, mpdUrl).toString();
|
| }
|
| if (!real.endsWith("/")) real += "/";
|
|
|
| const proxied = `${origin}/api/dseg/${mintDashBase(real, ref)}/`;
|
|
|
| let first = true;
|
| let out = xml.replace(/<BaseURL>[^<]*<\/BaseURL>/g, () =>
|
| first ? ((first = false), `<BaseURL>${proxied}</BaseURL>`) : ""
|
| );
|
|
|
| if (first) {
|
| out = out.replace(/(<MPD\b[^>]*>)/, `$1<BaseURL>${proxied}</BaseURL>`);
|
| }
|
| return out;
|
| }
|
|
|
| export async function GET(req: NextRequest) {
|
| const { searchParams, origin } = new URL(req.url);
|
| const claim = verify<StreamClaim>(searchParams.get("t"));
|
| if (!claim || claim.k !== "stream") {
|
| return new NextResponse("forbidden", { status: 403 });
|
| }
|
|
|
| const target = claim.url;
|
| const upstreamHeaders: Record<string, string> = { "User-Agent": UA };
|
| if (claim.ref) {
|
| upstreamHeaders["Referer"] = claim.ref;
|
| upstreamHeaders["Origin"] = new URL(claim.ref).origin;
|
| }
|
| const range = req.headers.get("range");
|
| if (range) upstreamHeaders["Range"] = range;
|
|
|
| let upstream: Response;
|
| try {
|
| upstream = await fetch(target, {
|
| headers: upstreamHeaders,
|
| redirect: "follow",
|
| cache: "no-store",
|
| });
|
| } catch {
|
| return new NextResponse("bad gateway", { status: 502 });
|
| }
|
|
|
| const ct = (upstream.headers.get("content-type") || "").toLowerCase();
|
| const path0 = target.split("?")[0].toLowerCase();
|
| const isManifest = path0.endsWith(".m3u8") || ct.includes("mpegurl");
|
| const isMpd = path0.endsWith(".mpd") || ct.includes("dash+xml");
|
|
|
| if (isMpd) {
|
| const xml = await upstream.text();
|
| const finalUrl = (upstream as any).url || target;
|
| const body = rewriteMpd(xml, finalUrl, origin, claim.ref);
|
| return new NextResponse(body, {
|
| status: upstream.status,
|
| headers: {
|
| "Content-Type": "application/dash+xml",
|
| "Cache-Control": "no-store",
|
| },
|
| });
|
| }
|
|
|
| if (isManifest) {
|
| const text = await upstream.text();
|
|
|
| const finalUrl = (upstream as any).url || target;
|
| const body = rewriteManifest(text, finalUrl, origin, claim.ref);
|
| return new NextResponse(body, {
|
| status: upstream.status,
|
| headers: {
|
| "Content-Type": "application/vnd.apple.mpegurl",
|
| "Cache-Control": "no-store",
|
| },
|
| });
|
| }
|
|
|
|
|
| const headers = new Headers();
|
| for (const h of [
|
| "content-type",
|
| "content-length",
|
| "accept-ranges",
|
| "content-range",
|
| "cache-control",
|
| ]) {
|
| const v = upstream.headers.get(h);
|
| if (v) headers.set(h, v);
|
| }
|
| if (!headers.has("cache-control")) headers.set("Cache-Control", "no-store");
|
|
|
| return new NextResponse(upstream.body, {
|
| status: upstream.status,
|
| headers,
|
| });
|
| }
|
|
|