ai_api / src /app /api /settings /favicon /route.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
3.37 kB
import { NextResponse } from "next/server";
import { getSettings } from "@/lib/db/settings";
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
export const dynamic = "force-dynamic";
const ALLOWED_IMAGE_TYPES = [
"image/png",
"image/x-icon",
"image/svg+xml",
"image/gif",
"image/webp",
"image/jpeg",
];
const MAX_FAVICON_SIZE = 50 * 1024; // 50KB
const FETCH_TIMEOUT = 5000; // 5 seconds
const CACHE_DURATION = 300; // 5 minutes
function validateImageData(base64Data: string, contentType: string): boolean {
if (!ALLOWED_IMAGE_TYPES.includes(contentType)) {
console.error("Invalid content type:", contentType);
return false;
}
// Check for obvious image magic bytes
const matches = base64Data.match(/^data:[^;]+;base64,(.+)$/);
if (!matches) return false;
const binaryData = Buffer.from(matches[1], "base64");
if (binaryData.length > MAX_FAVICON_SIZE) {
console.error("Favicon too large:", binaryData.length);
return false;
}
return true;
}
export async function GET() {
try {
const settings = await getSettings();
const customFaviconBase64 = settings?.customFaviconBase64 as string | undefined;
const customFaviconUrl = settings?.customFaviconUrl as string | undefined;
let faviconData: string | null = null;
if (customFaviconBase64) {
// Validate stored Base64 data
const match = customFaviconBase64.match(/^data:([^;]+);base64,(.+)$/);
if (match && validateImageData(customFaviconBase64, match[1])) {
faviconData = customFaviconBase64;
}
} else if (customFaviconUrl) {
try {
const response = await safeOutboundFetch(customFaviconUrl, {
...SAFE_OUTBOUND_FETCH_PRESETS.validationRead,
guard: "public-only",
timeoutMs: FETCH_TIMEOUT,
headers: {
"User-Agent": "OmniRoute/2.0",
},
});
if (response.ok) {
const contentType = response.headers.get("content-type") || "";
const arrayBuffer = await response.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Validate size before processing
if (uint8Array.length > MAX_FAVICON_SIZE) {
console.error("Favicon exceeds max size:", uint8Array.length);
} else {
const base64 = Buffer.from(uint8Array).toString("base64");
const fullData = `data:${contentType};base64,${base64}`;
if (validateImageData(fullData, contentType)) {
faviconData = fullData;
}
}
}
} catch (error) {
console.error("Failed to fetch custom favicon:", error);
}
}
if (!faviconData) {
return NextResponse.redirect("/favicon.svg");
}
const match = faviconData.match(/^data:([^;]+);base64,(.+)$/);
if (!match) {
return NextResponse.redirect("/favicon.svg");
}
const contentType = match[1];
const base64Data = match[2];
const buffer = Buffer.from(base64Data, "base64");
return new NextResponse(buffer, {
headers: {
"Content-Type": contentType,
"Cache-Control": `public, max-age=${CACHE_DURATION}`,
},
});
} catch (error) {
console.error("Favicon API error:", error);
return NextResponse.redirect("/favicon.svg");
}
}