coyotte508
A new start
fc69895
import { error } from "@sveltejs/kit";
import { logger } from "$lib/server/logger.js";
import { fetch } from "undici";
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const FETCH_TIMEOUT = 30000; // 30 seconds
// Validate URL safety - HTTPS only
function isValidUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
// Only allow HTTPS protocol
if (url.protocol !== "https:") {
return false;
}
// Prevent localhost/private IPs (basic check)
const hostname = url.hostname.toLowerCase();
if (
hostname === "localhost" ||
hostname.startsWith("127.") ||
hostname.startsWith("192.168.") ||
hostname.startsWith("172.16.") ||
hostname === "[::1]" ||
hostname === "0.0.0.0"
) {
return false;
}
return true;
} catch {
return false;
}
}
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 contentType = response.headers.get("content-type") || "application/octet-stream";
const contentDisposition = response.headers.get("content-disposition");
const headers: HeadersInit = {
"Content-Type": contentType,
"Cache-Control": "public, max-age=3600",
};
if (contentDisposition) {
headers["Content-Disposition"] = contentDisposition;
}
// 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.");
}
}