the code used at ico.madebydanny.uk
worker.js
edited
1const DEFAULT_FAVICON_URL =
2 "https://cdn.madebydanny.uk/user-content/2026-03-05/843fc448-e9fb-40dd-bbb7-17db9f6471b5.png";
3
4const CACHE_TTL = 86400;
5
6export 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
48async 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
66async 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
87function 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
104function 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
117function 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
124async 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
136async 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
152function 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
162function 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}