const DEFAULT_FAVICON_URL = "https://cdn.madebydanny.uk/user-content/2026-04-20/cb70f504-07fa-4f02-942f-45f1ce80255a.svg"; const CACHE_TTL = 86400; export default { async fetch(request, env, ctx) { const url = new URL(request.url); let domain = url.pathname.slice(1); if (!domain) return new Response("Usage: /{domain}", { status: 400 }); domain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").toLowerCase().trim(); if (!isValidDomain(domain)) return new Response("Invalid domain", { status: 400 }); const larger = url.searchParams.get("larger") === "true"; const defaultAvatar = url.searchParams.get("default-avatar"); const throwOn404 = url.searchParams.get("throw-error-on-404") === "true"; const cacheKey = new Request(`https://favicon-cache/${domain}?larger=${larger}`, request); const cache = caches.default; const cached = await cache.match(cacheKey); if (cached) return cached; const result = await fetchFavicon(domain, larger); if (!result) { if (throwOn404) return new Response("No favicon found", { status: 404 }); if (defaultAvatar) return Response.redirect(defaultAvatar, 302); return Response.redirect(DEFAULT_FAVICON_URL, 302); } const response = new Response(result.body, { headers: { "Content-Type": result.contentType, "Cache-Control": `public, max-age=${CACHE_TTL}`, "Access-Control-Allow-Origin": "*", "X-Favicon-Source": result.source, }, }); ctx.waitUntil(cache.put(cacheKey, response.clone())); return response; }, }; async function fetchFavicon(domain, larger) { if (larger) { const r = await tryFetch(`https://www.google.com/s2/favicons?domain=${domain}&sz=128`); if (r) return { ...r, source: "google-s2-128" }; } const htmlResult = await fetchFromHTML(domain, larger); if (htmlResult) return htmlResult; const ico = await tryFetch(`https://${domain}/favicon.ico`); if (ico) return { ...ico, source: "favicon.ico" }; const small = await tryFetch(`https://www.google.com/s2/favicons?domain=${domain}&sz=32`); if (small) return { ...small, source: "google-s2-32" }; return null; } async function fetchFromHTML(domain, larger) { let html; try { const res = await fetchWithTimeout(`https://${domain}`, 5000); if (!res?.ok) { const www = await fetchWithTimeout(`https://www.${domain}`, 5000); if (!www?.ok) return null; html = await www.text(); } else { html = await res.text(); } } catch { return null; } const icons = extractIcons(html, domain); const best = pickBestIcon(icons, larger); if (!best) return null; const result = await tryFetch(best); return result ? { ...result, source: best } : null; } function extractIcons(html, domain) { const icons = []; const patterns = [ /]+rel=["'][^"']*icon[^"']*["'][^>]*href=["']([^"']+)["'][^>]*>/gi, /]+href=["']([^"']+)["'][^>]*rel=["'][^"']*icon[^"']*["'][^>]*>/gi, /]+rel=["'][^"']*apple-touch-icon[^"']*["'][^>]*href=["']([^"']+)["'][^>]*>/gi, ]; for (const re of patterns) { let m; while ((m = re.exec(html)) !== null) { const href = m[1].trim(); if (href && !href.startsWith("data:")) icons.push(resolveUrl(href, domain)); } } return [...new Set(icons)]; } function pickBestIcon(icons, larger) { if (!icons.length) return null; if (larger) { const svg = icons.find(u => u.match(/\.svg(\?|$)/i)); if (svg) return svg; const big = icons.find(u => u.match(/\b(512|256|192|180|152|144|128|96)\b/)); if (big) return big; } return icons.find(u => u.match(/\.ico(\?|$)/i)) || icons.find(u => u.match(/\.png(\?|$)/i)) || icons[0]; } function resolveUrl(href, domain) { if (href.startsWith("http://") || href.startsWith("https://")) return href; if (href.startsWith("//")) return `https:${href}`; if (href.startsWith("/")) return `https://${domain}${href}`; return `https://${domain}/${href}`; } async function tryFetch(url) { try { const res = await fetchWithTimeout(url, 5000); if (!res?.ok) return null; const ct = res.headers.get("content-type") || ""; if (!ct.includes("image") && !ct.includes("octet-stream") && !ct.includes("svg") && !ct.includes("icon")) return null; const body = await res.arrayBuffer(); if (!body.byteLength) return null; return { body, contentType: sanitizeContentType(ct, url) }; } catch { return null; } } async function fetchWithTimeout(url, ms) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), ms); try { return await fetch(url, { signal: controller.signal, headers: { "User-Agent": "Mozilla/5.0 (compatible; FaviconBot/1.0; +https://favicon.blueat.net)", Accept: "image/*,*/*;q=0.8", }, redirect: "follow", cf: { cacheTtl: CACHE_TTL, cacheEverything: true }, }); } finally { clearTimeout(id); } } function sanitizeContentType(ct, url) { if (ct.includes("svg")) return "image/svg+xml"; if (ct.includes("png")) return "image/png"; if (ct.includes("jpeg") || ct.includes("jpg")) return "image/jpeg"; if (ct.includes("gif")) return "image/gif"; if (ct.includes("webp")) return "image/webp"; if (ct.includes("icon") || ct.includes("ico") || url?.endsWith(".ico")) return "image/x-icon"; return "image/x-icon"; } function isValidDomain(domain) { if (!domain.includes(".")) return false; const BLOCKED_EXT = [".php",".xml",".json",".yml",".yaml",".txt",".html",".htm",".asp",".aspx",".env",".ini",".cfg",".conf",".bak",".sql",".sh",".py",".rb",".pl",".cgi",".exe",".dll",".log"]; if (BLOCKED_EXT.some(ext => domain.endsWith(ext))) return false; const BLOCKED_NAMES = ["phpinfo","adminer","phpmyadmin","swagger","docker-compose","sitemap","robots","wp-login","xmlrpc","config","setup","install","readme","license","changelog"]; if (BLOCKED_NAMES.some(n => domain === n || domain.startsWith(n + "."))) return false; const tld = domain.split(".").pop(); if (!/^[a-zA-Z]{2,63}$/.test(tld)) return false; return /^[a-zA-Z0-9][a-zA-Z0-9\-\.]{0,251}[a-zA-Z0-9]$/.test(domain); }