File size: 3,114 Bytes
17d4d70 4adcf97 90b890a e67ab0e 17d4d70 cfa04c3 17d4d70 90b890a 17d4d70 3ac3ba4 17d4d70 3ac3ba4 17d4d70 2f40f2a 17d4d70 2f40f2a 17d4d70 3050b83 17d4d70 3050b83 17d4d70 cfa04c3 17d4d70 4adcf97 17d4d70 4adcf97 17d4d70 4adcf97 2f40f2a 17d4d70 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | import { error } from "@sveltejs/kit";
import { logger } from "$lib/server/logger.js";
import { fetch } from "undici";
import { isValidUrl } from "$lib/server/urlSafety";
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const FETCH_TIMEOUT = 30000; // 30 seconds
const SECURITY_HEADERS: HeadersInit = {
// Prevent any active content from executing if someone navigates directly to this endpoint.
"Content-Security-Policy":
"default-src 'none'; frame-ancestors 'none'; sandbox; script-src 'none'; img-src 'none'; style-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer",
};
export async function GET({ url }) {
const targetUrl = url.searchParams.get("url");
if (!targetUrl) {
logger.warn("Missing 'url' parameter");
throw error(400, "Missing 'url' parameter");
}
if (!isValidUrl(targetUrl)) {
logger.warn({ targetUrl }, "Invalid or unsafe URL (only HTTPS is supported)");
throw error(400, "Invalid or unsafe URL (only HTTPS is supported)");
}
try {
// Fetch with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
const response = await fetch(targetUrl, {
signal: controller.signal,
headers: {
"User-Agent": "HuggingChat-Attachment-Fetcher/1.0",
},
}).finally(() => clearTimeout(timeoutId));
if (!response.ok) {
logger.error({ targetUrl, response }, `Error fetching URL. Response not ok.`);
throw error(response.status, `Failed to fetch: ${response.statusText}`);
}
// Check content length if available
const contentLength = response.headers.get("content-length");
if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) {
throw error(413, "File too large (max 10MB)");
}
// Stream the response back
const originalContentType = response.headers.get("content-type") || "application/octet-stream";
// Send as text/plain for safety; expose the original type via secondary header
const safeContentType = "text/plain; charset=utf-8";
const contentDisposition = response.headers.get("content-disposition");
const headers: HeadersInit = {
"Content-Type": safeContentType,
"X-Forwarded-Content-Type": originalContentType,
"Cache-Control": "public, max-age=3600",
...(contentDisposition ? { "Content-Disposition": contentDisposition } : {}),
...SECURITY_HEADERS,
};
// Get the body as array buffer to check size
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength > MAX_FILE_SIZE) {
throw error(413, "File too large (max 10MB)");
}
return new Response(arrayBuffer, { headers });
} catch (err) {
if (err instanceof Error) {
if (err.name === "AbortError") {
logger.error(err, `Request timeout`);
throw error(504, "Request timeout");
}
logger.error(err, `Error fetching URL`);
throw error(500, `Failed to fetch URL: ${err.message}`);
}
logger.error(err, `Error fetching URL`);
throw error(500, "Failed to fetch URL.");
}
}
|