| 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; |
| const FETCH_TIMEOUT = 30000; |
| const SECURITY_HEADERS: HeadersInit = { |
| |
| "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 { |
| |
| 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}`); |
| } |
|
|
| |
| const contentLength = response.headers.get("content-length"); |
| if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) { |
| throw error(413, "File too large (max 10MB)"); |
| } |
|
|
| |
| const originalContentType = response.headers.get("content-type") || "application/octet-stream"; |
| |
| 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, |
| }; |
|
|
| |
| 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."); |
| } |
| } |
|
|