const VERSION = "2.1.4"; const ADMIN_DID = "did:plc:l37td5yhxl2irrzrgvei4qay"; const CONTACT_EMAIL = "danny@danielmorrisey.com"; const SERVICE_NAME = "BlueAT Network Favicon Fetcher"; 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); if (url.pathname === "/xrpc/_health") { return Response.json({ status: "ok", version: VERSION, admin_did: ADMIN_DID, admin_contact_email: CONTACT_EMAIL, service_name: SERVICE_NAME, cache_ttl: CACHE_TTL, fallback_url: DEFAULT_FAVICON_URL, ts: Date.now(), }); } 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; // 1. Local PoP cache const cached = await cache.match(cacheKey); if (cached) return cached; // 2. Global KV cache const kvKey = `favicon:${domain}:${larger}`; if (env.FAVICON_KV) { const kvEntry = await env.FAVICON_KV.getWithMetadata(kvKey, "arrayBuffer"); if (kvEntry.value) { const { contentType, source } = kvEntry.metadata ?? {}; const kvResponse = buildResponse(kvEntry.value, contentType, source, "kv"); ctx.waitUntil(cache.put(cacheKey, kvResponse.clone())); return kvResponse; } } // 3. Full fetch pipeline 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 = buildResponse(result.body, result.contentType, result.source, result.source); ctx.waitUntil(Promise.all([ env.FAVICON_KV?.put(kvKey, result.body, { expirationTtl: CACHE_TTL, metadata: { contentType: result.contentType, source: result.source }, }), cache.put(cacheKey, response.clone()), ])); return response; }, }; function buildResponse(body, contentType, source, cacheLayer) { return new Response(body, { headers: { "Content-Type": contentType ?? "image/x-icon", "Cache-Control": `public, max-age=${CACHE_TTL}, s-maxage=${CACHE_TTL}`, "Access-Control-Allow-Origin": "*", "X-Favicon-Source": source ?? "unknown", "X-Cache-Layer": cacheLayer, "CDN-Cache-Control": `max-age=${CACHE_TTL}`, }, cf: { cacheTtl: CACHE_TTL, cacheEverything: true, }, }); } 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); }