From ef9453cbc01a0585dd07560d263a6f62baaf02b1 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Thu, 5 Feb 2026 04:29:44 -0800 Subject: [PATCH] sucess pt. 9 proxy amq cdn --- src/routes/anime/[annId]/+page.svelte | 2 +- src/routes/cdn/[filename]/+server.ts | 193 ++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/routes/cdn/[filename]/+server.ts diff --git a/src/routes/anime/[annId]/+page.svelte b/src/routes/anime/[annId]/+page.svelte index ab9addb..f89b6f3 100644 --- a/src/routes/anime/[annId]/+page.svelte +++ b/src/routes/anime/[annId]/+page.svelte @@ -39,7 +39,7 @@ } function audioUrl(fileName: string) { - return `https://nawdist.animemusicquiz.com/${fileName}`; + return `/cdn/${encodeURIComponent(fileName)}`; } async function load() { diff --git a/src/routes/cdn/[filename]/+server.ts b/src/routes/cdn/[filename]/+server.ts new file mode 100644 index 0000000..cb5b56a --- /dev/null +++ b/src/routes/cdn/[filename]/+server.ts @@ -0,0 +1,193 @@ +import { error } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; + +const UPSTREAM_ORIGIN = "https://nawdist.animemusicquiz.com"; +const ALLOWED_METHODS = ["GET", "HEAD"] as const; + +// Basic filename hygiene: avoid path traversal and weird encodings. +// If you need to support subdirectories later, relax this and validate segments. +function isSafeFilename(name: string) { + // Disallow slashes/backslashes and any ".." + if ( + !name || + name.includes("/") || + name.includes("\\") || + name.includes("..") + ) { + return false; + } + + // Keep it reasonably strict; CDN filenames are typically simple. + // Allow common audio/container extensions and typical characters. + return /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(name); +} + +function pickForwardHeader(request: Request, headerName: string) { + const v = request.headers.get(headerName); + return v ?? undefined; +} + +export const GET: RequestHandler = async ({ + params, + request, + fetch, + setHeaders, +}) => { + const filename = params.filename; + + if (!isSafeFilename(filename)) { + throw error(400, "Invalid filename"); + } + + // Forward Range so audio seeking works + const range = pickForwardHeader(request, "range"); + const ifNoneMatch = pickForwardHeader(request, "if-none-match"); + const ifModifiedSince = pickForwardHeader(request, "if-modified-since"); + + const upstreamUrl = `${UPSTREAM_ORIGIN}/${encodeURIComponent(filename)}`; + + const upstreamRes = await fetch(upstreamUrl, { + method: "GET", + headers: { + ...(range ? { range } : {}), + ...(ifNoneMatch ? { "if-none-match": ifNoneMatch } : {}), + ...(ifModifiedSince ? { "if-modified-since": ifModifiedSince } : {}), + }, + }); + + // Map some upstream statuses sensibly + if (upstreamRes.status === 404) throw error(404, "File not found"); + if (upstreamRes.status === 416) { + // Range Not Satisfiable (e.g. stale range) should pass through + return new Response(upstreamRes.body, { + status: upstreamRes.status, + statusText: upstreamRes.statusText, + headers: upstreamRes.headers, + }); + } + if ( + !upstreamRes.ok && + upstreamRes.status !== 206 && + upstreamRes.status !== 304 + ) { + throw error(upstreamRes.status, `Upstream error (${upstreamRes.status})`); + } + + // Pass through headers that matter for media + caching. + // Avoid copying hop-by-hop headers. + const passthrough = new Headers(); + + const contentType = upstreamRes.headers.get("content-type"); + if (contentType) passthrough.set("content-type", contentType); + + const contentLength = upstreamRes.headers.get("content-length"); + if (contentLength) passthrough.set("content-length", contentLength); + + const acceptRanges = upstreamRes.headers.get("accept-ranges"); + if (acceptRanges) passthrough.set("accept-ranges", acceptRanges); + + const contentRange = upstreamRes.headers.get("content-range"); + if (contentRange) passthrough.set("content-range", contentRange); + + const etag = upstreamRes.headers.get("etag"); + if (etag) passthrough.set("etag", etag); + + const lastModified = upstreamRes.headers.get("last-modified"); + if (lastModified) passthrough.set("last-modified", lastModified); + + const cacheControl = upstreamRes.headers.get("cache-control"); + if (cacheControl) passthrough.set("cache-control", cacheControl); + else passthrough.set("cache-control", "public, max-age=86400"); + + // CORS: allow your app to fetch this endpoint from anywhere you deploy it + passthrough.set("access-control-allow-origin", "*"); + passthrough.set("access-control-allow-methods", ALLOWED_METHODS.join(", ")); + passthrough.set( + "access-control-allow-headers", + "range, if-none-match, if-modified-since", + ); + + // Tell browsers/proxies that Range affects the response + passthrough.set("vary", "range"); + + // Also apply via SvelteKit helper so adapters that manage headers behave + setHeaders(Object.fromEntries(passthrough.entries())); + + // 304 responses have no body + if (upstreamRes.status === 304) { + return new Response(null, { status: 304, headers: passthrough }); + } + + return new Response(upstreamRes.body, { + status: upstreamRes.status, + statusText: upstreamRes.statusText, + headers: passthrough, + }); +}; + +// HEAD support for players/browsers that probe first +export const HEAD: RequestHandler = async (event) => { + // Reuse GET logic but avoid streaming a body. + // We still want upstream to evaluate Range/ETag, so call upstream with HEAD. + const { params, request, fetch, setHeaders } = event; + const filename = params.filename; + + if (!isSafeFilename(filename)) { + throw error(400, "Invalid filename"); + } + + const range = pickForwardHeader(request, "range"); + const ifNoneMatch = pickForwardHeader(request, "if-none-match"); + const ifModifiedSince = pickForwardHeader(request, "if-modified-since"); + + const upstreamUrl = `${UPSTREAM_ORIGIN}/${encodeURIComponent(filename)}`; + + const upstreamRes = await fetch(upstreamUrl, { + method: "HEAD", + headers: { + ...(range ? { range } : {}), + ...(ifNoneMatch ? { "if-none-match": ifNoneMatch } : {}), + ...(ifModifiedSince ? { "if-modified-since": ifModifiedSince } : {}), + }, + }); + + if (upstreamRes.status === 404) throw error(404, "File not found"); + if ( + !upstreamRes.ok && + upstreamRes.status !== 206 && + upstreamRes.status !== 304 + ) { + throw error(upstreamRes.status, `Upstream error (${upstreamRes.status})`); + } + + const headers = new Headers(); + + const copy = (h: string) => { + const v = upstreamRes.headers.get(h); + if (v) headers.set(h, v); + }; + + copy("content-type"); + copy("content-length"); + copy("accept-ranges"); + copy("content-range"); + copy("etag"); + copy("last-modified"); + copy("cache-control"); + + if (!headers.has("cache-control")) { + headers.set("cache-control", "public, max-age=86400"); + } + + headers.set("access-control-allow-origin", "*"); + headers.set("access-control-allow-methods", ALLOWED_METHODS.join(", ")); + headers.set( + "access-control-allow-headers", + "range, if-none-match, if-modified-since", + ); + headers.set("vary", "range"); + + setHeaders(Object.fromEntries(headers.entries())); + + return new Response(null, { status: upstreamRes.status, headers }); +};