summaryrefslogtreecommitdiff
path: root/app/routes
diff options
context:
space:
mode:
Diffstat (limited to 'app/routes')
-rw-r--r--app/routes/api.image.ts25
1 files changed, 20 insertions, 5 deletions
diff --git a/app/routes/api.image.ts b/app/routes/api.image.ts
index 814cd1c..fee6401 100644
--- a/app/routes/api.image.ts
+++ b/app/routes/api.image.ts
@@ -12,6 +12,19 @@ function isPrivateHost(hostname: string): boolean {
return PRIVATE_IP_RE.test(hostname) || hostname === "localhost";
}
+function resolveContentType(declared: string, data: ArrayBuffer): string | null {
+ if (declared.startsWith("image/")) return declared;
+ if (declared.startsWith("application/octet-stream") || declared === "") {
+ const b = new Uint8Array(data, 0, 12);
+ if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return "image/jpeg";
+ if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return "image/png";
+ if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return "image/gif";
+ if (b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
+ b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50) return "image/webp";
+ }
+ return null;
+}
+
export async function loader({ request }: LoaderFunctionArgs) {
const raw = new URL(request.url).searchParams.get("url");
if (!raw) return new Response("Missing url", { status: 400 });
@@ -54,21 +67,23 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (!res.ok) return new Response("Upstream error", { status: 502 });
const contentType = res.headers.get("content-type") ?? "";
- if (!contentType.startsWith("image/")) {
+ const data = await res.arrayBuffer();
+
+ const resolvedType = resolveContentType(contentType, data);
+ if (!resolvedType) {
return new Response("Not an image", { status: 400 });
}
- const data = await res.arrayBuffer();
-
if (cache.size >= MAX_CACHE_ENTRIES) {
cache.delete(cache.keys().next().value!);
}
- cache.set(raw, { data, contentType, expiresAt: now + CACHE_TTL_MS });
+ cache.set(raw, { data, contentType: resolvedType, expiresAt: now + CACHE_TTL_MS });
return new Response(data.slice(0), {
headers: {
- "Content-Type": contentType,
+ "Content-Type": resolvedType,
"Cache-Control": "public, max-age=86400",
+ "X-Content-Type-Options": "nosniff",
},
});
}