代码展示
/**
* ═══════════════════════════════════════════════════════════════════════════
* www.chzi.eu.org — Cloudflare Workers 图片反向代理与 CDN 边缘加速
* 版本:v1.4(稳定增强版)
*
* 用途:为中国及全球用户提供 Blogger 博客图片的稳定访问与 WebP/AVIF CDN 加速
* 架构:CF Workers Edge → Blogger CDN(bp.blogspot.com / googleusercontent)
* 调用:https://www.chzi.eu.org/?url=<Blogger图片URL>
* ═══════════════════════════════════════════════════════════════════════════
*/
// ─────────────────────────────────────────────────────────────────────────────
// CONFIG:所有可调参数统一管理,业务代码中无魔法常量
// ─────────────────────────────────────────────────────────────────────────────
const CONFIG = Object.freeze({
// 节点服务域名与主站地址
SERVICE_DOMAIN: "www.chzi.eu.org",
MAIN_SITE_URL: "https://www.chzi.eu.org",
// 上游请求超时(毫秒)
TIMEOUT_PRIMARY: 5_000, // 主源 GET 超时
TIMEOUT_FALLBACK: 4_000, // 备用源并发竞争超时
TIMEOUT_HEAD_PROBE: 3_000, // HEAD 探测超时(轻量探测)
// 请求去重 TTL(毫秒)
PENDING_TTL: 13_000, // 5s + 4s + 3s + 1s 余量
// pendingRequests Map 大小上限,超出时返回 503 过载占位图
MAX_PENDING: 200,
// 响应体大小上限(字节)
MAX_BYTES_THUMBNAIL: 2 * 1024 * 1024, // 缩略图:2 MB
MAX_BYTES_ORIGINAL: 12 * 1024 * 1024, // 原图:12 MB
// 缓存时效(秒)
CACHE_TTL: 31_536_000, // 主源图片:1 年(immutable 强缓存)
FALLBACK_CACHE_TTL: 300, // fallback 降级图:5 分钟(主源恢复后自动回正)
PLACEHOLDER_TTL: 60, // 占位图:60 秒
// 上游请求伪装头
UPSTREAM_UA:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/135.0.0.0 Safari/537.36",
UPSTREAM_REFERER: "https://www.google.com/",
// 允许代理的上游域名白名单
ALLOWED_UPSTREAM_HOSTS: [
"bp.blogspot.com",
"blogger.googleusercontent.com",
],
// Referer 防盗链白名单(允许本域、主站、常见搜索引擎与社交平台)
ALLOWED_REFERER_HOSTS: [
"chzi.eu.org",
"blogspot.com",
"google.com",
"bing.com",
"t.me",
"x.com",
"twitter.com",
"facebook.com",
"baidu.com",
"duckduckgo.com",
"yandex.com",
],
// 占位图 SVG 画布尺寸(像素)
PLACEHOLDER_W: 640,
PLACEHOLDER_H: 360,
});
// ─────────────────────────────────────────────────────────────────────────────
// 客户端格式能力分级
// ─────────────────────────────────────────────────────────────────────────────
function resolveClientFmt(acceptHeader) {
if (!acceptHeader) return "base";
const supported = new Set();
for (const item of acceptHeader.split(",")) {
const parts = item.trim().split(";");
const mime = parts[0].trim().toLowerCase();
let q = 1;
for (const p of parts.slice(1)) {
const [k, v] = p.trim().split("=");
if (k.trim() === "q") { q = parseFloat(v) || 0; break; }
}
if (q > 0) supported.add(mime);
}
if (supported.has("image/avif")) return "avif";
if (supported.has("image/webp")) return "webp";
return "base";
}
const FMT_ACCEPT = Object.freeze({
avif: "image/avif,image/webp,image/*,*/*;q=0.8",
webp: "image/webp,image/*,*/*;q=0.8",
base: "image/jpeg,image/png,image/gif,image/*,*/*;q=0.8",
});
// ─────────────────────────────────────────────────────────────────────────────
// pendingRequests:请求去重表
// ─────────────────────────────────────────────────────────────────────────────
const pendingRequests = new Map();
// ─────────────────────────────────────────────────────────────────────────────
// Worker 入口
// ─────────────────────────────────────────────────────────────────────────────
export default {
async fetch(request, env, ctx) {
try {
return await handleRequest(request, ctx);
} catch (err) {
console.error("[proxy][FATAL]", err?.message ?? String(err));
return plainTextResponse("Internal Server Error", 500);
}
},
};
// ─────────────────────────────────────────────────────────────────────────────
// handleRequest:主请求处理管道
// ─────────────────────────────────────────────────────────────────────────────
async function handleRequest(request, ctx) {
const reqUrl = new URL(request.url);
const method = request.method;
// Step 1:HTTP 方法校验
if (method !== "GET" && method !== "HEAD") {
return plainTextResponse("Method Not Allowed", 405, { Allow: "GET, HEAD" });
}
// Step 2:解析 ?url= 参数
const rawTarget = reqUrl.searchParams.get("url");
if (!rawTarget) return landingPageResponse();
const parsedTarget = normalizeUrl(rawTarget);
if (!parsedTarget) return plainTextResponse("Bad Request: invalid URL", 400);
const target = parsedTarget.href;
// Step 3:防环路(防止自身回环调用)
if (parsedTarget.hostname === reqUrl.hostname) {
return plainTextResponse("Forbidden: loop detected", 403);
}
// Step 4:上游域名白名单
if (!isAllowedUpstreamHost(parsedTarget.hostname)) {
return plainTextResponse("Forbidden: upstream host not allowed", 403);
}
// Step 4.5:强制 HTTPS 协议
if (parsedTarget.protocol !== "https:") {
return plainTextResponse("Bad Request: only HTTPS upstream allowed", 400);
}
// Step 5:Referer 防盗链校验
const referer = request.headers.get("Referer") || "";
if (referer && !isAllowedRefererHost(getHostname(referer))) {
return makePlaceholderResponse(403, "不允许盗链引用");
}
// Step 6:格式分级与 CacheKey 构造(直接生成规范 URL 字符串,降低内存开销)
const fmt = resolveClientFmt(request.headers.get("Accept") || "");
const cache = caches.default;
const reqBase = `${reqUrl.origin}${reqUrl.pathname}`;
const cacheKey = buildCacheKey(reqBase, target, fmt);
// Step 7:Worker Cache 命中查询
const cached = await cache.match(cacheKey);
if (cached) {
const h = new Headers(cached.headers);
h.set("X-Cache-Status", "HIT");
return new Response(method === "HEAD" ? null : cached.body, {
status: cached.status, headers: h,
});
}
// Step 8:HEAD 方法——轻量探测
if (method === "HEAD") {
return await probeUpstreamHead(target, fmt);
}
// Step 9:请求去重合并
const dedupeKey = `${target}::${fmt}`;
const existing = pendingRequests.get(dedupeKey);
if (existing) {
const recipe = await existing.promise;
return buildResponseFromRecipe(recipe);
}
// Step 10:过载熔断
if (pendingRequests.size >= CONFIG.MAX_PENDING) {
console.warn("[proxy][OVERLOAD] pending size =", pendingRequests.size);
return makePlaceholderResponse(503, "服务繁忙");
}
// Step 11:首次请求,回源执行
const abortCtrl = new AbortController();
const recipePromise = fetchImageRecipe(
reqBase, target, parsedTarget, fmt, cacheKey, cache, ctx, abortCtrl.signal
);
const entry = { promise: recipePromise, abort: abortCtrl, timer: null };
pendingRequests.set(dedupeKey, entry);
entry.timer = setTimeout(() => {
if (pendingRequests.get(dedupeKey) === entry) {
entry.abort.abort();
pendingRequests.delete(dedupeKey);
console.warn("[proxy][TTL] aborted:", dedupeKey);
}
}, CONFIG.PENDING_TTL);
recipePromise.finally(() => {
if (pendingRequests.get(dedupeKey) === entry) {
clearTimeout(entry.timer);
pendingRequests.delete(dedupeKey);
}
});
const recipe = await recipePromise;
return buildResponseFromRecipe(recipe);
}
// ─────────────────────────────────────────────────────────────────────────────
// probeUpstreamHead:HEAD 方法探测
// ─────────────────────────────────────────────────────────────────────────────
async function probeUpstreamHead(target, fmt) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), CONFIG.TIMEOUT_HEAD_PROBE);
try {
const res = await fetch(target, {
method: "HEAD",
headers: buildUpstreamHeaders(fmt),
signal: ctrl.signal,
});
const h = proxyBaseHeaders("MISS");
// 保留上游关键元信息
for (const key of ["content-type", "content-length", "etag", "last-modified"]) {
const val = res.headers.get(key);
if (val) h.set(key, val);
}
return new Response(null, {
status: res.ok ? 200 : res.status,
headers: h,
});
} catch {
return new Response(null, { status: 503, headers: proxyBaseHeaders("MISS") });
} finally {
clearTimeout(timer);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// fetchImageRecipe:核心回源与安全读取
// ─────────────────────────────────────────────────────────────────────────────
async function fetchImageRecipe(reqBase, target, parsedTarget, fmt, cacheKey, cache, ctx, abortSignal) {
if (abortSignal.aborted) {
return { kind: "placeholder", proxyStatus: 503, message: "请求已取消" };
}
// 尺寸判定:支持传统路径 /s800/ 与现代后缀 =s800 两种 Blogger 规范
const isThumbnail = isThumbnailUrl(parsedTarget.pathname);
const maxBytes = isThumbnail ? CONFIG.MAX_BYTES_THUMBNAIL : CONFIG.MAX_BYTES_ORIGINAL;
const upstreamHdrs = buildUpstreamHeaders(fmt);
const sources = buildSources(parsedTarget, target);
// 阶段一:串行请求主源
const t0 = Date.now();
const res0 = await fetchUpstream(sources[0], upstreamHdrs, CONFIG.TIMEOUT_PRIMARY, abortSignal);
const lat = Date.now() - t0;
if (res0?._rateLimited) {
console.warn("[proxy][429]", sources[0]);
return { kind: "placeholder", proxyStatus: 429, message: "请求过于频繁" };
}
// 阶段二:主源失败时并发竞争备用源(原图降级策略)
let usedFallback = false;
let fallbackSource = null;
let res = res0;
if ((!res || res._permanent) && sources.length > 1) {
const fb = await fetchAny(sources.slice(1), upstreamHdrs, CONFIG.TIMEOUT_FALLBACK, abortSignal);
if (fb?._rateLimited) {
console.warn("[proxy][429] fallback limit");
return { kind: "placeholder", proxyStatus: 429, message: "请求过于频繁" };
}
if (fb) {
res = fb.res;
fallbackSource = fb.sourceUrl;
usedFallback = true;
}
}
if (!res || res._permanent) {
console.error("[proxy][FAIL]", target);
return { kind: "placeholder", proxyStatus: 502, message: "图片加载失败" };
}
// Content-Type 宽容过滤
const upstreamCT = (res.headers.get("content-type") || "").toLowerCase();
if (upstreamCT && !upstreamCT.startsWith("image/") && upstreamCT !== "application/octet-stream") {
res.body?.cancel();
console.warn("[proxy][CT] invalid:", upstreamCT, target);
return { kind: "forbidden", text: "非法内容类型" };
}
// 安全流式读取 body(带内存防御阈值,防 Chunked 无上限流 OOM)
const buffer = await readSafeBuffer(res, maxBytes);
if (!buffer) {
console.warn("[proxy][SIZE] exceeded limit:", maxBytes, target);
return { kind: "placeholder", proxyStatus: 413, message: "图片文件过大" };
}
// 二进制魔数检测(100% 确认真实格式)
const detectedCT = detectMimeType(buffer);
if (usedFallback) {
if (!detectedCT || buffer.byteLength < 100) {
console.warn("[proxy][FALLBACK] invalid image body:", fallbackSource);
return { kind: "placeholder", proxyStatus: 502, message: "图片加载失败" };
}
}
const finalCT = detectedCT ?? (upstreamCT || "application/octet-stream");
if (!finalCT.startsWith("image/")) {
console.warn("[proxy][CT] final rejected:", finalCT, target);
return { kind: "forbidden", text: "非法内容类型" };
}
// 构造规范化响应头
const h = new Headers(res.headers);
h.delete("Set-Cookie");
h.delete("Content-Security-Policy");
h.delete("Content-Disposition");
const ttl = usedFallback ? CONFIG.FALLBACK_CACHE_TTL : CONFIG.CACHE_TTL;
h.set("Content-Type", finalCT);
h.set("Cache-Control", `public, max-age=${ttl}${ttl === CONFIG.CACHE_TTL ? ", immutable" : ""}`);
h.set("Vary", "Accept");
h.set("Access-Control-Allow-Origin", "*");
h.set("Cross-Origin-Resource-Policy","cross-origin");
h.set("Timing-Allow-Origin", "*");
h.set("X-Content-Type-Options", "nosniff");
h.set("X-Proxy-Version", "v1.4");
h.set("X-Cache-Status", "MISS");
if (!usedFallback) h.set("X-Upstream-Latency", `${lat}ms`);
h.set("X-Proxy-Source", usedFallback ? "fallback" : "primary");
if (detectedCT && detectedCT !== upstreamCT && upstreamCT) {
h.set("X-Content-Type-Fixed", `upstream=${upstreamCT};detected=${detectedCT}`);
}
// 写入 Worker 缓存(Fallback 采用并行双写策略)
ctx.waitUntil(
(async () => {
try {
const cacheOps = [
cache.put(cacheKey, new Response(buffer, { status: 200, headers: new Headers(h) }))
];
if (usedFallback && fallbackSource) {
const fbKey = buildCacheKey(reqBase, fallbackSource, fmt);
cacheOps.push(cache.put(fbKey, new Response(buffer, { status: 200, headers: new Headers(h) })));
}
await Promise.allSettled(cacheOps);
} catch (e) {
console.error("[proxy][CACHE]", e?.message);
}
})()
);
return {
kind: "ok",
headers: [...h.entries()],
buffer,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// readSafeBuffer:带上限的安全响应体读取(防内存耗尽)
// ─────────────────────────────────────────────────────────────────────────────
async function readSafeBuffer(res, maxBytes) {
const declaredLen = parseInt(res.headers.get("content-length") || "0", 10);
if (declaredLen > 0) {
if (declaredLen > maxBytes) {
res.body?.cancel();
return null;
}
// declaredLen 正常时采用高效底层原生读取
const buf = await res.arrayBuffer();
return buf.byteLength <= maxBytes ? buf : null;
}
// 针对 chunked / 无 Content-Length 的响应流,边读边计算防止 OOM
if (!res.body) return new ArrayBuffer(0);
const reader = res.body.getReader();
const chunks = [];
let received = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.byteLength;
if (received > maxBytes) {
await reader.cancel();
return null;
}
chunks.push(value);
}
} catch {
return null;
}
// 拼装 Uint8Array
const merged = new Uint8Array(received);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
return merged.buffer;
}
// ─────────────────────────────────────────────────────────────────────────────
// buildResponseFromRecipe:Recipe -> Response
// ─────────────────────────────────────────────────────────────────────────────
function buildResponseFromRecipe(recipe) {
switch (recipe.kind) {
case "ok":
return new Response(recipe.buffer, { status: 200, headers: new Headers(recipe.headers) });
case "placeholder":
return makePlaceholderResponse(recipe.proxyStatus, recipe.message);
case "forbidden":
return plainTextResponse(recipe.text, 403);
default:
return plainTextResponse("Internal Error", 500);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// buildSources:构建有序回源列表(兼容双规则尺寸降级)
// ─────────────────────────────────────────────────────────────────────────────
function buildSources(parsedTarget, originalUrl) {
const sources = [originalUrl];
try {
const u = new URL(originalUrl);
let modified = false;
// 规则 1:传统路径型 /s1600/ -> /s0/
const pathPattern = /(\/)(s\d+(?:-[ch]\d+)*|w\d+(?:-h\d+)*|h\d+)(\/)/i;
if (pathPattern.test(u.pathname)) {
u.pathname = u.pathname.replace(pathPattern, "$1s0$3");
modified = true;
}
// 规则 2:现代 Blogger / Google UserContent 后缀型 =s1600 -> =s0
const paramPattern = /(=)(s\d+(?:-[ch]\d+)*|w\d+(?:-h\d+)*|h\d+)(.*)$/i;
if (!modified && paramPattern.test(u.pathname)) {
u.pathname = u.pathname.replace(paramPattern, "$1s0$3");
modified = true;
}
if (modified && u.toString() !== originalUrl) {
sources.push(u.toString());
}
} catch { /* 静默降级 */ }
return sources;
}
function isThumbnailUrl(pathname) {
const p1 = /\/(s\d+(?:-[ch]\d+)*|w\d+(?:-h\d+)*|h\d+)\//i;
const p2 = /=(s\d+(?:-[ch]\d+)*|w\d+(?:-h\d+)*|h\d+)/i;
// 排除 /s0/ 与 =s0(原图标识)
if (/\/(s0)\//i.test(pathname) || /=(s0)($|[^0-9])/i.test(pathname)) return false;
return p1.test(pathname) || p2.test(pathname);
}
// ─────────────────────────────────────────────────────────────────────────────
// fetchUpstream & fetchAny:出站请求与并发竞速
// ─────────────────────────────────────────────────────────────────────────────
async function fetchUpstream(url, headers, timeoutMs, externalSignal = null) {
const localCtrl = new AbortController();
const timer = setTimeout(() => localCtrl.abort(), timeoutMs);
const signal = combineSignals(localCtrl.signal, externalSignal);
try {
const res = await fetch(url, { method: "GET", headers, signal });
if (res.ok) return res;
res.body?.cancel();
if (res.status === 404 || res.status === 410) return { _permanent: true };
if (res.status === 429) return { _rateLimited: true };
return null;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
async function fetchAny(urls, headers, timeoutMs, externalSignal = null) {
if (!urls.length) return null;
const raceCtrls = urls.map(() => new AbortController());
let winnerIdx = -1;
try {
const result = await Promise.any(
urls.map((url, i) => {
const extSig = combineSignals(raceCtrls[i].signal, externalSignal);
return fetchUpstream(url, headers, timeoutMs, extSig).then(res => {
if (res && !res._permanent && !res._rateLimited) {
raceCtrls.forEach((c, j) => { if (j !== i) c.abort(); });
winnerIdx = i;
return { res, sourceUrl: url };
}
if (res?._rateLimited) throw Object.assign(new Error("rate-limited"), { _rateLimited: true });
throw new Error("not ok");
});
})
);
return result;
} catch (err) {
if (err instanceof AggregateError && err.errors?.some(e => e._rateLimited)) {
return { _rateLimited: true };
}
return null;
} finally {
raceCtrls.forEach((c, i) => { if (i !== winnerIdx) c.abort(); });
}
}
// ─────────────────────────────────────────────────────────────────────────────
// detectMimeType:魔数(Magic Bytes)深度检测
// ─────────────────────────────────────────────────────────────────────────────
function detectMimeType(buffer) {
if (!buffer || buffer.byteLength < 12) return null;
const b = new Uint8Array(buffer, 0, 16);
// WebP:RIFF....WEBP
if (b[0]===0x52 && b[1]===0x49 && b[2]===0x46 && b[3]===0x46 &&
b[8]===0x57 && b[9]===0x45 && b[10]===0x42 && b[11]===0x50) return "image/webp";
// JPEG:FF D8 FF
if (b[0]===0xFF && b[1]===0xD8 && b[2]===0xFF) return "image/jpeg";
// PNG:89 50 4E 47
if (b[0]===0x89 && b[1]===0x50 && b[2]===0x4E && b[3]===0x47) return "image/png";
// GIF:GIF87a / GIF89a
if (b[0]===0x47 && b[1]===0x49 && b[2]===0x46) return "image/gif";
// ISO BMFF (AVIF)
if (b[4]===0x66 && b[5]===0x74 && b[6]===0x79 && b[7]===0x70) {
const brand = String.fromCharCode(b[8], b[9], b[10], b[11]);
if (brand === "avif" || brand === "avis" || brand === "mif1") return "image/avif";
return null;
}
return null;
}
// ─────────────────────────────────────────────────────────────────────────────
// makePlaceholderResponse:SVG 占位图生成
// ─────────────────────────────────────────────────────────────────────────────
function makePlaceholderResponse(status, message = "图片暂时无法显示") {
const safeMsg = String(message)
.replace(/&/g,"&").replace(/</g,"<")
.replace(/>/g,">").replace(/"/g,""");
const safeCode = String(status).replace(/[<>&]/g,"");
const { PLACEHOLDER_W: w, PLACEHOLDER_H: h } = CONFIG;
const svg = [
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`,
`<rect width="${w}" height="${h}" fill="#f8fafc"/>`,
`<circle cx="${w/2}" cy="${h*0.36}" r="24" fill="#e2e8f0"/>`,
`<path d="M${w/2 - 8} ${h*0.36 - 8}l16 16m0-16l-16 16" stroke="#94a3b8" stroke-width="2" stroke-linecap="round"/>`,
`<text x="${w/2}" y="${h*0.58}" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,sans-serif" font-size="15" font-weight="500" fill="#475569">${safeMsg}</text>`,
`<text x="${w/2}" y="${h*0.70}" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,sans-serif" font-size="12" fill="#94a3b8">HTTP ${safeCode} · CHZI.EU.ORG</text>`,
`</svg>`,
].join("");
return new Response(svg, {
status: 200,
headers: {
"Content-Type": "image/svg+xml; charset=utf-8",
"Cache-Control": `public, max-age=${CONFIG.PLACEHOLDER_TTL}`,
"X-Proxy-Status": String(status),
"X-Proxy-Version": "v1.4",
"X-Robots-Tag": "noindex",
"Access-Control-Allow-Origin": "*",
"X-Content-Type-Options": "nosniff",
},
});
}
// ─────────────────────────────────────────────────────────────────────────────
// combineSignals:AbortSignal 合并
// ─────────────────────────────────────────────────────────────────────────────
function combineSignals(a, b) {
if (!b) return a;
if (typeof AbortSignal.any === "function") return AbortSignal.any([a, b]);
const ctrl = new AbortController();
if (a.aborted || b.aborted) { ctrl.abort(); return ctrl.signal; }
const onAbort = () => {
a.removeEventListener("abort", onAbort);
b.removeEventListener("abort", onAbort);
ctrl.abort();
};
a.addEventListener("abort", onAbort);
b.addEventListener("abort", onAbort);
return ctrl.signal;
}
// ─────────────────────────────────────────────────────────────────────────────
// 工具函数
// ─────────────────────────────────────────────────────────────────────────────
function buildCacheKey(reqBase, target, fmt) {
return `${reqBase}?url=${encodeURIComponent(target)}&fmt=${fmt}`;
}
function buildUpstreamHeaders(fmt) {
return new Headers({
"User-Agent": CONFIG.UPSTREAM_UA,
"Referer": CONFIG.UPSTREAM_REFERER,
"Accept": FMT_ACCEPT[fmt],
"Accept-Language": "en-US,en;q=0.9",
});
}
function proxyBaseHeaders(cacheStatus) {
return new Headers({
"X-Cache-Status": cacheStatus,
"X-Proxy-Version": "v1.4",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*",
});
}
function normalizeUrl(raw) {
if (!raw || typeof raw !== "string") return null;
let trimmed = raw.trim();
try {
try { trimmed = decodeURIComponent(trimmed); } catch { /* 避免非法 escape 报错 */ }
if (!/^https?:\/\//i.test(trimmed)) {
trimmed = trimmed.startsWith("//") ? "https:" + trimmed : "https://" + trimmed;
}
return new URL(trimmed);
} catch {
return null;
}
}
function isAllowedUpstreamHost(host) {
const h = host.toLowerCase();
return CONFIG.ALLOWED_UPSTREAM_HOSTS.some(d => h === d || h.endsWith("." + d));
}
function isAllowedRefererHost(host) {
if (!host) return true;
const h = host.toLowerCase();
return CONFIG.ALLOWED_REFERER_HOSTS.some(d => h === d || h.endsWith("." + d));
}
function getHostname(url) {
try { return new URL(url).hostname.toLowerCase(); }
catch { return ""; }
}
function plainTextResponse(text, status, extraHeaders = {}) {
return new Response(text, {
status,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"X-Proxy-Version": "v1.4",
...extraHeaders,
},
});
}
function landingPageResponse() {
const site = CONFIG.MAIN_SITE_URL;
return new Response(
`<!DOCTYPE html><html lang="zh-CN"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="10;url=${site}">
<meta name="robots" content="noindex">
<title>CHZI.EU.ORG 图片 CDN 加速节点</title>
<style>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;padding:30px 20px;max-width:640px;margin:auto;background:#fafafa;color:#1e293b;}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:24px;box-shadow:0 1px 3px rgba(0,0,0,0.05);}
h2{margin-top:0;color:#0f172a;font-size:1.4em;border-bottom:1px solid #f1f5f9;padding-bottom:12px;}
p{font-size:.95em;color:#334155;}
a{color:#2563eb;text-decoration:none;}a:hover{text-decoration:underline;}
code{background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:.9em;color:#e11d48;}
.countdown-box{margin-top:20px;padding:12px;background:#eff6ff;border-radius:8px;color:#1d4ed8;font-size:.9em;text-align:center;}
</style>
</head><body>
<div class="card">
<h2>CHZI.EU.ORG 图片反代加速节点</h2>
<p>当前节点 <code>${CONFIG.SERVICE_DOMAIN}</code> 专用于 Blogger 博客图片的全球 CDN 边缘加速与格式协商。</p>
<p>依托 Cloudflare Edge 网络,自动将上游图片转码并分发为 AVIF/WebP 格式,保障国内及海外极速访问体验。</p>
<p><strong>接口调用格式:</strong><br><code>https://${CONFIG.SERVICE_DOMAIN}/?url=<Blogger图片完整URL></code></p>
<div class="countdown-box">页面将在 10 秒后自动前往主站:<a href="${site}">${CONFIG.SERVICE_DOMAIN}</a></div>
</div>
</body></html>`,
{ status: 200, headers: {
"Content-Type": "text/html; charset=utf-8",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"Referrer-Policy": "no-referrer",
}});
}