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.");
	}
}