sucess pt. 9 proxy amq cdn
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
}
|
||||
|
||||
function audioUrl(fileName: string) {
|
||||
return `https://nawdist.animemusicquiz.com/${fileName}`;
|
||||
return `/cdn/${encodeURIComponent(fileName)}`;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
|
||||
193
src/routes/cdn/[filename]/+server.ts
Normal file
193
src/routes/cdn/[filename]/+server.ts
Normal file
@@ -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 });
|
||||
};
|
||||
Reference in New Issue
Block a user