The Cloudflare Worker code behind favicon.blueat.net favicon.blueat.net
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

first commit

Daniel Morrisey 62520ac1

+171
+171
worker.js
··· 1 + const DEFAULT_FAVICON_URL = 2 + "https://cdn.madebydanny.uk/user-content/2026-03-05/843fc448-e9fb-40dd-bbb7-17db9f6471b5.png"; 3 + 4 + const CACHE_TTL = 86400; 5 + 6 + export default { 7 + async fetch(request, env, ctx) { 8 + const url = new URL(request.url); 9 + let domain = url.pathname.slice(1); 10 + 11 + if (!domain) return new Response("Usage: /{domain}", { status: 400 }); 12 + 13 + domain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").toLowerCase().trim(); 14 + 15 + if (!isValidDomain(domain)) return new Response("Invalid domain", { status: 400 }); 16 + 17 + const larger = url.searchParams.get("larger") === "true"; 18 + const defaultAvatar = url.searchParams.get("default-avatar"); 19 + const throwOn404 = url.searchParams.get("throw-error-on-404") === "true"; 20 + 21 + const cacheKey = new Request(`https://favicon-cache/${domain}?larger=${larger}`, request); 22 + const cache = caches.default; 23 + const cached = await cache.match(cacheKey); 24 + if (cached) return cached; 25 + 26 + const result = await fetchFavicon(domain, larger); 27 + 28 + if (!result) { 29 + if (throwOn404) return new Response("No favicon found", { status: 404 }); 30 + if (defaultAvatar) return Response.redirect(defaultAvatar, 302); 31 + return Response.redirect(DEFAULT_FAVICON_URL, 302); 32 + } 33 + 34 + const response = new Response(result.body, { 35 + headers: { 36 + "Content-Type": result.contentType, 37 + "Cache-Control": `public, max-age=${CACHE_TTL}`, 38 + "Access-Control-Allow-Origin": "*", 39 + "X-Favicon-Source": result.source, 40 + }, 41 + }); 42 + 43 + ctx.waitUntil(cache.put(cacheKey, response.clone())); 44 + return response; 45 + }, 46 + }; 47 + 48 + async function fetchFavicon(domain, larger) { 49 + if (larger) { 50 + const r = await tryFetch(`https://www.google.com/s2/favicons?domain=${domain}&sz=128`); 51 + if (r) return { ...r, source: "google-s2-128" }; 52 + } 53 + 54 + const htmlResult = await fetchFromHTML(domain, larger); 55 + if (htmlResult) return htmlResult; 56 + 57 + const ico = await tryFetch(`https://${domain}/favicon.ico`); 58 + if (ico) return { ...ico, source: "favicon.ico" }; 59 + 60 + const small = await tryFetch(`https://www.google.com/s2/favicons?domain=${domain}&sz=32`); 61 + if (small) return { ...small, source: "google-s2-32" }; 62 + 63 + return null; 64 + } 65 + 66 + async function fetchFromHTML(domain, larger) { 67 + let html; 68 + try { 69 + const res = await fetchWithTimeout(`https://${domain}`, 5000); 70 + if (!res?.ok) { 71 + const www = await fetchWithTimeout(`https://www.${domain}`, 5000); 72 + if (!www?.ok) return null; 73 + html = await www.text(); 74 + } else { 75 + html = await res.text(); 76 + } 77 + } catch { return null; } 78 + 79 + const icons = extractIcons(html, domain); 80 + const best = pickBestIcon(icons, larger); 81 + if (!best) return null; 82 + 83 + const result = await tryFetch(best); 84 + return result ? { ...result, source: best } : null; 85 + } 86 + 87 + function extractIcons(html, domain) { 88 + const icons = []; 89 + const patterns = [ 90 + /<link[^>]+rel=["'][^"']*icon[^"']*["'][^>]*href=["']([^"']+)["'][^>]*>/gi, 91 + /<link[^>]+href=["']([^"']+)["'][^>]*rel=["'][^"']*icon[^"']*["'][^>]*>/gi, 92 + /<link[^>]+rel=["'][^"']*apple-touch-icon[^"']*["'][^>]*href=["']([^"']+)["'][^>]*>/gi, 93 + ]; 94 + for (const re of patterns) { 95 + let m; 96 + while ((m = re.exec(html)) !== null) { 97 + const href = m[1].trim(); 98 + if (href && !href.startsWith("data:")) icons.push(resolveUrl(href, domain)); 99 + } 100 + } 101 + return [...new Set(icons)]; 102 + } 103 + 104 + function pickBestIcon(icons, larger) { 105 + if (!icons.length) return null; 106 + if (larger) { 107 + const svg = icons.find(u => u.match(/\.svg(\?|$)/i)); 108 + if (svg) return svg; 109 + const big = icons.find(u => u.match(/\b(512|256|192|180|152|144|128|96)\b/)); 110 + if (big) return big; 111 + } 112 + return icons.find(u => u.match(/\.ico(\?|$)/i)) 113 + || icons.find(u => u.match(/\.png(\?|$)/i)) 114 + || icons[0]; 115 + } 116 + 117 + function resolveUrl(href, domain) { 118 + if (href.startsWith("http://") || href.startsWith("https://")) return href; 119 + if (href.startsWith("//")) return `https:${href}`; 120 + if (href.startsWith("/")) return `https://${domain}${href}`; 121 + return `https://${domain}/${href}`; 122 + } 123 + 124 + async function tryFetch(url) { 125 + try { 126 + const res = await fetchWithTimeout(url, 5000); 127 + if (!res?.ok) return null; 128 + const ct = res.headers.get("content-type") || ""; 129 + if (!ct.includes("image") && !ct.includes("octet-stream") && !ct.includes("svg") && !ct.includes("icon")) return null; 130 + const body = await res.arrayBuffer(); 131 + if (!body.byteLength) return null; 132 + return { body, contentType: sanitizeContentType(ct, url) }; 133 + } catch { return null; } 134 + } 135 + 136 + async function fetchWithTimeout(url, ms) { 137 + const controller = new AbortController(); 138 + const id = setTimeout(() => controller.abort(), ms); 139 + try { 140 + return await fetch(url, { 141 + signal: controller.signal, 142 + headers: { 143 + "User-Agent": "Mozilla/5.0 (compatible; FaviconBot/1.0; +https://favicon.blueat.net)", 144 + Accept: "image/*,*/*;q=0.8", 145 + }, 146 + redirect: "follow", 147 + cf: { cacheTtl: CACHE_TTL, cacheEverything: true }, 148 + }); 149 + } finally { clearTimeout(id); } 150 + } 151 + 152 + function sanitizeContentType(ct, url) { 153 + if (ct.includes("svg")) return "image/svg+xml"; 154 + if (ct.includes("png")) return "image/png"; 155 + if (ct.includes("jpeg") || ct.includes("jpg")) return "image/jpeg"; 156 + if (ct.includes("gif")) return "image/gif"; 157 + if (ct.includes("webp")) return "image/webp"; 158 + if (ct.includes("icon") || ct.includes("ico") || url?.endsWith(".ico")) return "image/x-icon"; 159 + return "image/x-icon"; 160 + } 161 + 162 + function isValidDomain(domain) { 163 + if (!domain.includes(".")) return false; 164 + 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"]; 165 + if (BLOCKED_EXT.some(ext => domain.endsWith(ext))) return false; 166 + const BLOCKED_NAMES = ["phpinfo","adminer","phpmyadmin","swagger","docker-compose","sitemap","robots","wp-login","xmlrpc","config","setup","install","readme","license","changelog"]; 167 + if (BLOCKED_NAMES.some(n => domain === n || domain.startsWith(n + "."))) return false; 168 + const tld = domain.split(".").pop(); 169 + if (!/^[a-zA-Z]{2,63}$/.test(tld)) return false; 170 + return /^[a-zA-Z0-9][a-zA-Z0-9\-\.]{0,251}[a-zA-Z0-9]$/.test(domain); 171 + }