mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: enhance website icon loading mechanism; implement icon loading state management and error handling
This commit is contained in:
+55
-22
@@ -142,6 +142,17 @@ function normalizeIconHost(rawHost: string): string | null {
|
||||
}
|
||||
|
||||
const ICON_UPSTREAM_TIMEOUT_MS = 2500;
|
||||
const BITWARDEN_DEFAULT_GLOBE_ICON_BYTES = 500;
|
||||
const BITWARDEN_DEFAULT_GLOBE_ICON_SHA256 = 'aaa64871332ad5b7d28fe8874efb19c2d9cc2f1e6de75d52b080b438225a0783';
|
||||
|
||||
type IconSource = {
|
||||
url: string;
|
||||
rejectImage?: {
|
||||
byteLength: number;
|
||||
sha256: string;
|
||||
};
|
||||
headers?: HeadersInit;
|
||||
};
|
||||
|
||||
async function fetchIconSource(source: { url: string; headers?: HeadersInit }): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
@@ -161,48 +172,70 @@ async function fetchIconSource(source: { url: string; headers?: HeadersInit }):
|
||||
}
|
||||
}
|
||||
|
||||
async function sha256Hex(bytes: ArrayBuffer): Promise<string> {
|
||||
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function iconResponse(body: BodyInit | null, contentType: string | null): Response {
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentType || 'image/png',
|
||||
'Cache-Control': `public, max-age=${LIMITS.cache.iconTtlSeconds}, immutable`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleWebsiteIcon(host: string, fallbackMode: 'default' | 'not-found' = 'default'): Promise<Response> {
|
||||
const normalizedHost = normalizeIconHost(host);
|
||||
if (!normalizedHost) return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon();
|
||||
|
||||
const encodedHost = encodeURIComponent(normalizedHost);
|
||||
const requestHeaders = { 'User-Agent': 'NodeWarden/1.0' };
|
||||
const upstreamSources: Array<{ url: string; headers?: HeadersInit }> = [
|
||||
const upstreamSources: IconSource[] = [
|
||||
{
|
||||
url: `https://favicon.im/zh/${encodedHost}?larger=true&throw-error-on-404=true`,
|
||||
headers: requestHeaders,
|
||||
},
|
||||
{
|
||||
url: `https://icons.bitwarden.net/${encodedHost}/icon.png`,
|
||||
headers: requestHeaders,
|
||||
},
|
||||
{
|
||||
url: `https://favicon.im/${encodedHost}`,
|
||||
headers: requestHeaders,
|
||||
},
|
||||
{
|
||||
url: `https://icons.duckduckgo.com/ip3/${encodedHost}.ico`,
|
||||
rejectImage: {
|
||||
byteLength: BITWARDEN_DEFAULT_GLOBE_ICON_BYTES,
|
||||
sha256: BITWARDEN_DEFAULT_GLOBE_ICON_SHA256,
|
||||
},
|
||||
headers: requestHeaders,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
for (const source of upstreamSources) {
|
||||
for (const source of upstreamSources) {
|
||||
try {
|
||||
const resp = await fetchIconSource(source);
|
||||
|
||||
if (!resp.ok) continue;
|
||||
const contentType = String(resp.headers.get('Content-Type') || '').toLowerCase();
|
||||
if (!contentType.startsWith('image/')) continue;
|
||||
|
||||
return new Response(resp.body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': resp.headers.get('Content-Type') || 'image/png',
|
||||
'Cache-Control': `public, max-age=${LIMITS.cache.iconTtlSeconds}, immutable`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!source.rejectImage) {
|
||||
return iconResponse(resp.body, resp.headers.get('Content-Type'));
|
||||
}
|
||||
|
||||
return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon();
|
||||
} catch {
|
||||
return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon();
|
||||
const contentLength = Number(resp.headers.get('Content-Length') || '');
|
||||
if (Number.isFinite(contentLength) && contentLength > 0 && contentLength !== source.rejectImage.byteLength) {
|
||||
return iconResponse(resp.body, resp.headers.get('Content-Type'));
|
||||
}
|
||||
|
||||
const bytes = await resp.arrayBuffer();
|
||||
if (bytes.byteLength === 0) continue;
|
||||
if (bytes.byteLength === source.rejectImage.byteLength && (await sha256Hex(bytes)) === source.rejectImage.sha256) continue;
|
||||
|
||||
return iconResponse(bytes, resp.headers.get('Content-Type'));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon();
|
||||
}
|
||||
|
||||
export function buildWebBootstrapResponse(env: Env): WebBootstrapResponse {
|
||||
|
||||
Reference in New Issue
Block a user