generated from Dicken/dickendock
Kritische Bugfixes: Export/CA-Download (Service-Worker), Speichern-Bug bei Diensten/Lesezeichen, Dark-Mode-Direktlink, Favicon-Mixed-Content-Proxy, Live-Update Zuletzt-besucht, Kategorie-Farb-Picker, Startseite-Nav
This commit is contained in:
90
apps/backend/src/routes/faviconProxy.ts
Normal file
90
apps/backend/src/routes/faviconProxy.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
|
||||
const MAX_BYTES = 2 * 1024 * 1024; // 2 MB reicht für jedes realistische Favicon
|
||||
const TIMEOUT_MS = 4000;
|
||||
|
||||
function fetchImage(
|
||||
targetUrl: string
|
||||
): Promise<{ statusCode: number; contentType: string; body: Buffer }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(targetUrl);
|
||||
} catch {
|
||||
reject(new Error("Ungültige URL"));
|
||||
return;
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
reject(new Error("Nur http/https erlaubt"));
|
||||
return;
|
||||
}
|
||||
|
||||
const client = parsed.protocol === "https:" ? https : http;
|
||||
const req = client.get(
|
||||
parsed,
|
||||
{
|
||||
timeout: TIMEOUT_MS,
|
||||
// Homelab-Geräte haben oft selbstsignierte Zertifikate.
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
(res) => {
|
||||
const statusCode = res.statusCode ?? 0;
|
||||
if (statusCode >= 300 && statusCode < 400 && res.headers.location) {
|
||||
res.resume();
|
||||
fetchImage(new URL(res.headers.location, targetUrl).toString())
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
res.on("data", (chunk: Buffer) => {
|
||||
size += chunk.length;
|
||||
if (size > MAX_BYTES) {
|
||||
req.destroy();
|
||||
reject(new Error("Bild zu groß"));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
statusCode,
|
||||
contentType: res.headers["content-type"] ?? "image/x-icon",
|
||||
body: Buffer.concat(chunks),
|
||||
});
|
||||
});
|
||||
res.on("error", reject);
|
||||
}
|
||||
);
|
||||
|
||||
req.on("timeout", () => req.destroy(new Error("Zeitüberschreitung")));
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
export async function faviconProxyRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get("/api/favicon-proxy", async (request, reply) => {
|
||||
const { url } = request.query as { url?: string };
|
||||
if (!url) {
|
||||
return reply.code(400).send({ error: "url erforderlich" });
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await fetchImage(url);
|
||||
if (image.statusCode >= 400 || image.body.length === 0) {
|
||||
return reply.code(404).send();
|
||||
}
|
||||
reply.header("Cache-Control", "public, max-age=86400");
|
||||
reply.type(image.contentType);
|
||||
return reply.send(image.body);
|
||||
} catch {
|
||||
// Icon nicht ladbar (Gerät offline, kaputte URL, ...) - 404 statt 500,
|
||||
// damit das Frontend sauber auf den Buchstaben-Fallback zurückfällt.
|
||||
return reply.code(404).send();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user