3764 words
19 minutes
用Cloudflare Workers搭建Wikipedia镜像指南
当知识自由成为奢望,技术便是破局的利刃。
Workers免费限额
Worker免费套餐允许:
- 每日10万次请求
- 单请求CPU最多50ms
- 全局KV存储1GB
通常情况下少数人使用是完全够的,但是毕竟限额摆在那,不建议直接公开使用
具体步骤
- 进入
CloudFlare Workers
(https://workers.cloudflare.com/) - 点击
创建服务
- 修改
服务名称
这个将会决定到你网站的域名,并记住你设置的网址 - 最后点击
创建服务
来创建 Workers - 在接下来的网页中点击
快速编辑
,进入编辑界面 - 在左侧代码框中,把所有代码删掉,替换成以下我贴出的代码
- 点击
保存并部署
,就完成了
代码我已经写好了,如果有更改,我会更新
版本信息Ver 1.1
上次更新 2025/02/29
版本说明分为密码版和公共版
公共版没有密码,密码版需要配置
PROXY_PASSWORD
环境变量来设置密码
公共版
// 配置选项
const CONFIG = {
TARGET_HOST: 'zh.wikipedia.org',
ALLOWED_METHODS: new Set(['GET', 'HEAD', 'POST', 'OPTIONS']),
MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB
TIMEOUT: 30000, // 30秒
ALLOWED_CONTENT_TYPES: new Set(['application/json', 'text/plain', 'text/html']),
CACHE_TTL: 3600, // 响应缓存时间 1小时
// 跳过的请求头
SKIP_HEADERS_PREFIX: new Set([
'cf-',
'cdn-loop',
'x-forwarded-',
'x-real-ip',
'connection',
'keep-alive',
'upgrade',
'transfer-encoding',
]),
// 安全响应头
SECURITY_HEADERS: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin'
},
// 预编译正则表达式以提高性能
CONTENT_TYPE_REGEX: /^(application\/json|text\/plain|text\/html)(;|$)/
}
// 错误处理类
class ProxyError extends Error {
constructor(message, status = 500) {
super(message);
this.name = 'ProxyError';
this.status = status;
}
}
// 使用单一事件监听器处理所有请求
addEventListener('fetch', event => {
event.respondWith(
handleRequest(event.request)
.catch(handleError)
);
});
/**
* 主请求处理函数
* @param {Request} request 原始请求
* @returns {Promise<Response>} 响应
*/
async function handleRequest(request) {
// 验证请求方法 - 快速失败原则
if (!CONFIG.ALLOWED_METHODS.has(request.method)) {
throw new ProxyError('Method not allowed', 405);
}
// 处理POST请求的特殊验证
if (request.method === 'POST') {
const contentType = request.headers.get('Content-Type');
// 验证Content-Type - 使用预编译的正则表达式
if (contentType && !CONFIG.CONTENT_TYPE_REGEX.test(contentType)) {
throw new ProxyError('Unsupported Content-Type', 415);
}
// 验证请求大小
const contentLength = request.headers.get('content-length');
if (contentLength && parseInt(contentLength, 10) > CONFIG.MAX_BODY_SIZE) {
throw new ProxyError('Request entity too large', 413);
}
}
// 构建目标URL - 避免重复解析
const url = new URL(request.url);
const targetUrl = `https://${CONFIG.TARGET_HOST}${url.pathname}${url.search}`;
// 准备并发送请求
const proxyRequest = await createProxyRequest(request, targetUrl);
const response = await fetchWithTimeout(proxyRequest, CONFIG.TIMEOUT);
// 返回处理后的响应
return createProxyResponse(response);
}
/**
* 创建代理请求 - 优化头部处理
* @param {Request} originalRequest 原始请求
* @param {string} targetUrl 目标URL
*/
async function createProxyRequest(originalRequest, targetUrl) {
// 直接利用Headers API进行头部处理
const headers = new Headers();
const skipPrefixCheck = shouldSkipHeader;
// 使用迭代器一次性处理所有头部
for (const [key, value] of originalRequest.headers) {
if (!skipPrefixCheck(key)) {
headers.set(key, value);
}
}
// 设置必要的请求头
headers.set('Host', CONFIG.TARGET_HOST);
headers.set('Origin', `https://${CONFIG.TARGET_HOST}`);
headers.set('Referer', `https://${CONFIG.TARGET_HOST}`);
headers.set('X-Forwarded-Proto', 'https');
// 只在需要时获取CF连接IP
const cfIp = originalRequest.headers.get('cf-connecting-ip');
if (cfIp) headers.set('X-Real-IP', cfIp);
// 利用条件判断优化body处理
const needsBody = !['GET', 'HEAD'].includes(originalRequest.method);
// 构建新请求
return new Request(targetUrl, {
method: originalRequest.method,
headers,
body: needsBody ? originalRequest.body : undefined,
redirect: 'follow',
});
}
/**
* 创建代理响应
* @param {Response} originalResponse 原始响应
* @returns {Response} 修改后的响应
*/
function createProxyResponse(originalResponse) {
const headers = new Headers();
const skipPrefixCheck = shouldSkipHeader;
// 一次性处理所有头部
for (const [key, value] of originalResponse.headers) {
if (!skipPrefixCheck(key)) {
headers.set(key, value);
}
}
// 批量设置安全头
Object.entries(CONFIG.SECURITY_HEADERS).forEach(([key, value]) => {
headers.set(key, value);
});
// 设置缓存控制
headers.set('Cache-Control', `public, max-age=${CONFIG.CACHE_TTL}`);
// 创建新响应
return new Response(originalResponse.body, {
status: originalResponse.status,
statusText: originalResponse.statusText,
headers
});
}
/**
* 优化的头部跳过检查函数
* @param {string} headerName 头部名称
* @returns {boolean} 是否需要跳过
*/
function shouldSkipHeader(headerName) {
const lowerName = headerName.toLowerCase();
// 使用Set的has方法直接检查前缀开头
for (const prefix of CONFIG.SKIP_HEADERS_PREFIX) {
if (lowerName.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* 带超时的fetch
* @param {Request} request 请求
* @param {number} timeout 超时时间(ms)
* @returns {Promise<Response>} 响应
*/
async function fetchWithTimeout(request, timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(request, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new ProxyError('Request timeout', 504);
}
throw error;
}
}
/**
* 错误处理函数
* @param {Error} error 错误对象
* @returns {Response} 错误响应
*/
function handleError(error) {
const status = error instanceof ProxyError ? error.status : 500;
const message = error.message || 'Internal Server Error';
// 预定义错误响应头以减少对象创建
const errorHeaders = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
};
// 记录错误
console.error(`[ProxyError] ${status}: ${message}`, error.stack);
return new Response(
JSON.stringify({ error: message, status }),
{ status, headers: errorHeaders }
);
}
密码版
IMPORTANT需要设置环境变量
PROXY_PASSWORD
来设置密码
// 配置选项
const CONFIG = {
TARGET_HOST: 'zh.wikipedia.org',
ALLOWED_METHODS: new Set(['GET', 'HEAD', 'POST', 'OPTIONS']),
MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB
TIMEOUT: 30000, // 30秒
// 更新允许的内容类型,添加图片和其他资源类型
ALLOWED_CONTENT_TYPES: new Set([
'application/json',
'text/plain',
'text/html',
'image/',
'font/',
'application/javascript',
'text/css',
'application/xml',
'application/pdf'
]),
CACHE_TTL: 3600, // 响应缓存时间 1小时
// 图片等静态资源的缓存时间更长
STATIC_CACHE_TTL: 86400, // 24小时
// 跳过的请求头
SKIP_HEADERS_PREFIX: new Set([
'cf-',
'cdn-loop',
'x-forwarded-',
'x-real-ip',
'connection',
'keep-alive',
'upgrade',
'transfer-encoding',
]),
// 安全响应头 - 修改CSP以允许图片和其他资源
SECURITY_HEADERS: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'SAMEORIGIN',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Content-Security-Policy': "default-src 'self' https://*.wikimedia.org https://*.wikipedia.org; img-src 'self' https://*.wikimedia.org https://*.wikipedia.org data:; style-src 'self' 'unsafe-inline' https://*.wikimedia.org https://*.wikipedia.org; font-src 'self' https://*.wikimedia.org https://*.wikipedia.org; script-src 'self' 'unsafe-inline' https://*.wikimedia.org https://*.wikipedia.org; frame-ancestors 'none'"
},
// 内容类型检查 - 改为函数以支持更灵活的判断
isAllowedContentType: (contentType) => {
if (!contentType) return false;
const ct = contentType.toLowerCase();
for (const allowedType of CONFIG.ALLOWED_CONTENT_TYPES) {
if (ct.startsWith(allowedType)) return true;
}
return false;
},
// 身份验证配置
AUTH: {
COOKIE_NAME: 'auth_token',
TOKEN_TTL: 7 * 24 * 60 * 60, // 7天(秒)
TOKEN_SECRET_LENGTH: 32 // 密钥长度
}
};
// 错误处理类
class ProxyError extends Error {
constructor(message, status = 500) {
super(message);
this.name = 'ProxyError';
this.status = status;
}
}
// 主导出函数
export default {
async fetch(request, env, ctx) {
try {
// 验证必要的环境变量
if (!env.PROXY_PASSWORD) {
throw new ProxyError('Missing required environment variable: PROXY_PASSWORD', 500);
}
return await handleRequestWithAuth(request, env);
} catch (error) {
return handleError(error);
}
}
};
/**
* 身份验证和请求处理的入口
* @param {Request} request 原始请求
* @param {Object} env 环境变量
* @returns {Promise<Response>} 响应
*/
async function handleRequestWithAuth(request, env) {
const url = new URL(request.url);
// 为登录页面提供静态资源
if (url.pathname === '/login.css' || url.pathname === '/login.js') {
return handleStaticResource(url.pathname);
}
// 处理登录请求
if (url.pathname === '/login' && request.method === 'POST') {
return handleLogin(request, env);
}
// 检查cookie中的令牌
const token = getCookieValue(request, CONFIG.AUTH.COOKIE_NAME);
// 如果没有令牌或令牌无效,展示登录页面
if (!token || !await isValidToken(token, env)) {
return showLoginPage();
}
// 令牌有效,处理正常请求
return handleRequest(request, env);
}
/**
* 处理登录请求
* @param {Request} request 登录请求
* @param {Object} env 环境变量
* @returns {Promise<Response>} 登录响应
*/
async function handleLogin(request, env) {
// 确保是POST请求
if (request.method !== 'POST') {
throw new ProxyError('Method not allowed', 405);
}
// 验证内容类型
const contentType = request.headers.get('Content-Type');
if (!contentType || !contentType.includes('application/x-www-form-urlencoded')) {
throw new ProxyError('Unsupported Content-Type', 415);
}
let formData;
try {
formData = await request.formData();
} catch {
throw new ProxyError('Invalid form data', 400);
}
// 修复: 正确处理 formData.get('password') 返回的值,可能是 string 或 File
const passwordValue = formData.get('password');
// 确保密码是字符串
const password = typeof passwordValue === 'string' ? passwordValue : '';
if (!password) {
throw new ProxyError('Password is required', 400);
}
// 安全地验证密码 - 使用恒定时间比较避免计时攻击
if (await secureCompare(password, env.PROXY_PASSWORD)) {
// 生成安全令牌
const token = await generateSecureToken(env);
// 创建响应并设置cookie
const headers = new Headers({
'Location': '/',
'Set-Cookie': `${CONFIG.AUTH.COOKIE_NAME}=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${CONFIG.AUTH.TOKEN_TTL}`
});
return new Response(null, {
status: 302,
headers
});
} else {
// 使用延时以防止计时攻击
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 500));
// 记录失败的登录尝试
console.warn(`Failed login attempt from IP: ${request.headers.get('cf-connecting-ip')}`);
// 密码错误,显示登录页面并附带错误信息
return showLoginPage('密码不正确,请重试。');
}
}
/**
* 恒定时间比较函数 - 防止计时攻击
* @param {string} a 字符串1
* @param {string} b 字符串2
* @returns {Promise<boolean>} 比较结果
*/
async function secureCompare(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') {
return false;
}
// 如果长度不同,返回false但耗时相同
const aLen = a.length;
const bLen = b.length;
const maxLen = Math.max(aLen, bLen);
let result = aLen === bLen ? 1 : 0;
for (let i = 0; i < maxLen; i++) {
// 对超出范围的索引使用默认值0,确保恒定时间
const aChar = i < aLen ? a.charCodeAt(i) : 0;
const bChar = i < bLen ? b.charCodeAt(i) : 0;
result &= aChar === bChar ? 1 : 0;
}
return result === 1;
}
/**
* 生成安全令牌
* @param {Object} env 环境变量
* @returns {Promise<string>} 安全令牌
*/
async function generateSecureToken(env) {
// 创建随机数组
const randomBytes = new Uint8Array(CONFIG.AUTH.TOKEN_SECRET_LENGTH);
crypto.getRandomValues(randomBytes);
// 转换为base64,放入令牌
const randomStr = btoa(String.fromCharCode(...randomBytes));
const timestamp = Date.now().toString(36);
// 使用随机字符串和时间戳创建令牌
const tokenData = `${timestamp}.${randomStr}`;
// 创建签名以验证令牌
const signature = await createTokenSignature(tokenData, env.PROXY_PASSWORD);
return `${tokenData}.${signature}`;
}
/**
* 为令牌创建签名
* @param {string} tokenData 令牌数据
* @param {string} secret 密钥
* @returns {Promise<string>} 签名
*/
async function createTokenSignature(tokenData, secret) {
// 使用SubtleCrypto进行HMAC签名
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const messageData = encoder.encode(tokenData);
// 从密码创建密钥
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// 创建签名
const signature = await crypto.subtle.sign('HMAC', key, messageData);
// 转换为base64
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
/**
* 验证令牌是否有效
* @param {string} token 身份验证令牌
* @param {Object} env 环境变量
* @returns {Promise<boolean>} 是否有效
*/
async function isValidToken(token, env) {
try {
// 从令牌中提取时间戳和签名
const parts = token.split('.');
if (parts.length !== 3) return false;
const [timestamp, randomStr, signature] = parts;
const tokenData = `${timestamp}.${randomStr}`;
// 验证签名
const expectedSignature = await createTokenSignature(tokenData, env.PROXY_PASSWORD);
if (!await secureCompare(signature, expectedSignature)) {
return false;
}
// 验证时间戳
const tokenTime = parseInt(timestamp, 36);
const now = Date.now();
// 检查令牌是否过期
return (now - tokenTime) < (CONFIG.AUTH.TOKEN_TTL * 1000);
} catch {
return false;
}
}
/**
* 从请求中获取cookie值
* @param {Request} request 请求
* @param {string} name cookie名称
* @returns {string|null} cookie值或null
*/
function getCookieValue(request, name) {
const cookieHeader = request.headers.get('Cookie') || '';
const cookies = cookieHeader.split(';').map(c => c.trim());
for (const cookie of cookies) {
const [cookieName, ...valueParts] = cookie.split('=');
if (cookieName === name) {
// 使用join以处理cookie值中可能包含=的情况
return valueParts.join('=');
}
}
return null;
}
/**
* 处理静态资源
* @param {string} path 资源路径
* @returns {Response} 静态资源响应
*/
function handleStaticResource(path) {
if (path === '/login.css') {
return new Response(`
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
padding: 20px;
}
.login-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 100%;
max-width: 400px;
}
h1 {
margin-top: 0;
color: #333;
font-size: 24px;
text-align: center;
}
.error-message {
color: #dc3545;
margin-bottom: 15px;
text-align: center;
}
form {
display: flex;
flex-direction: column;
}
input[type="password"] {
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
margin-bottom: 15px;
font-size: 16px;
}
button {
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 12px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #0069d9;
}
`, {
headers: {
'Content-Type': 'text/css',
'Cache-Control': 'public, max-age=86400'
}
});
}
if (path === '/login.js') {
return new Response(`
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('login-form');
form.addEventListener('submit', function(e) {
const passwordInput = document.getElementById('password');
if (!passwordInput.value.trim()) {
e.preventDefault();
showError('请输入密码');
}
});
function showError(message) {
let errorDiv = document.querySelector('.error-message');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
const h1 = document.querySelector('h1');
h1.parentNode.insertBefore(errorDiv, h1.nextSibling);
}
errorDiv.textContent = message;
}
});
`, {
headers: {
'Content-Type': 'text/javascript',
'Cache-Control': 'public, max-age=86400'
}
});
}
throw new ProxyError('Not found', 404);
}
/**
* 显示登录页面
* @param {string} errorMessage 可选的错误消息
* @returns {Response} 登录页面响应
*/
function showLoginPage(errorMessage = '') {
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>请输入访问密码</title>
<link rel="stylesheet" href="/login.css">
<script src="/login.js"></script>
</head>
<body>
<div class="login-container">
<h1>请输入访问密码</h1>
${errorMessage ? `<div class="error-message">${errorMessage}</div>` : ''}
<form id="login-form" method="POST" action="/login">
<input type="password" id="password" name="password" placeholder="输入访问密码" required autofocus>
<button type="submit">确认</button>
</form>
</div>
</body>
</html>
`;
return new Response(html, {
status: 200,
headers: {
'Content-Type': 'text/html;charset=UTF-8',
'Cache-Control': 'no-store',
...CONFIG.SECURITY_HEADERS
}
});
}
/**
* 主请求处理函数
* @param {Request} request 原始请求
* @param {Object} env 环境变量
* @returns {Promise<Response>} 响应
*/
async function handleRequest(request, env) {
// 验证请求方法 - 快速失败原则
if (!CONFIG.ALLOWED_METHODS.has(request.method)) {
throw new ProxyError('Method not allowed', 405);
}
// 处理POST请求的特殊验证
if (request.method === 'POST') {
const contentType = request.headers.get('Content-Type');
// 验证Content-Type - 使用更灵活的检查
if (contentType && !CONFIG.isAllowedContentType(contentType)) {
throw new ProxyError('Unsupported Content-Type', 415);
}
// 验证请求大小
const contentLength = request.headers.get('content-length');
if (contentLength && parseInt(contentLength, 10) > CONFIG.MAX_BODY_SIZE) {
throw new ProxyError('Request entity too large', 413);
}
}
// 构建目标URL - 避免重复解析
const url = new URL(request.url);
// 排除登录路径和静态资源
if (url.pathname === '/login' || url.pathname === '/login.css' || url.pathname === '/login.js') {
throw new ProxyError('Not found', 404);
}
const targetUrl = `https://${CONFIG.TARGET_HOST}${url.pathname}${url.search}`;
// 准备并发送请求
const proxyRequest = await createProxyRequest(request, targetUrl);
try {
const response = await fetchWithTimeout(proxyRequest, CONFIG.TIMEOUT);
// 返回处理后的响应
return createProxyResponse(response, request);
} catch (error) {
// 更好的错误处理,包括重试逻辑
if (error.name === 'AbortError') {
// 对于超时错误,可以考虑重试
console.warn(`Request timed out for ${targetUrl}`);
throw new ProxyError('Gateway Timeout', 504);
}
throw error;
}
}
/**
* 创建代理请求 - 优化头部处理
* @param {Request} originalRequest 原始请求
* @param {string} targetUrl 目标URL
*/
async function createProxyRequest(originalRequest, targetUrl) {
// 直接利用Headers API进行头部处理
const headers = new Headers();
// 使用迭代器一次性处理所有头部
for (const [key, value] of originalRequest.headers) {
if (!shouldSkipHeader(key)) {
headers.set(key, value);
}
}
// 设置必要的请求头
headers.set('Host', CONFIG.TARGET_HOST);
headers.set('Origin', `https://${CONFIG.TARGET_HOST}`);
headers.set('Referer', `https://${CONFIG.TARGET_HOST}`);
// 用户代理可能也需要设置
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
}
// 只在需要时获取CF连接IP
const cfIp = originalRequest.headers.get('cf-connecting-ip');
if (cfIp) headers.set('X-Real-IP', cfIp);
// 利用条件判断优化body处理
const needsBody = !['GET', 'HEAD'].includes(originalRequest.method);
// 构建新请求
return new Request(targetUrl, {
method: originalRequest.method,
headers,
body: needsBody ? originalRequest.body : undefined,
redirect: 'follow',
});
}
/**
* 创建代理响应
* @param {Response} originalResponse 原始响应
* @param {Request} originalRequest 原始请求 - 用于根据请求类型确定缓存策略
* @returns {Response} 修改后的响应
*/
function createProxyResponse(originalResponse, originalRequest) {
const headers = new Headers();
// 一次性处理所有头部
for (const [key, value] of originalResponse.headers) {
if (!shouldSkipHeader(key)) {
headers.set(key, value);
}
}
// 设置安全头,但要注意保留原始内容类型和编码
Object.entries(CONFIG.SECURITY_HEADERS).forEach(([key, value]) => {
// 对于非HTML响应,避免设置过于严格的CSP
const contentType = originalResponse.headers.get('Content-Type') || '';
if (key === 'Content-Security-Policy' && !contentType.includes('text/html')) {
return; // 跳过非HTML内容的CSP
}
headers.set(key, value);
});
// 根据内容类型设置更合适的缓存控制
const contentType = originalResponse.headers.get('Content-Type') || '';
const url = new URL(originalRequest.url);
// 静态资源使用更长的缓存时间
if (isStaticResource(contentType, url.pathname)) {
headers.set('Cache-Control', `public, max-age=${CONFIG.STATIC_CACHE_TTL}`);
} else {
headers.set('Cache-Control', `public, max-age=${CONFIG.CACHE_TTL}`);
}
// 创建新响应
return new Response(originalResponse.body, {
status: originalResponse.status,
statusText: originalResponse.statusText,
headers
});
}
/**
* 判断是否为静态资源
* @param {string} contentType 内容类型
* @param {string} pathname 路径
* @returns {boolean} 是否为静态资源
*/
function isStaticResource(contentType, pathname) {
// 通过内容类型判断
if (contentType.includes('image/') ||
contentType.includes('font/') ||
contentType.includes('text/css') ||
contentType.includes('application/javascript')) {
return true;
}
// 通过文件扩展名判断
const staticExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',
'.css', '.js', '.woff', '.woff2', '.ttf', '.eot'];
return staticExtensions.some(ext => pathname.endsWith(ext));
}
/**
* 优化的头部跳过检查函数
* @param {string} headerName 头部名称
* @returns {boolean} 是否需要跳过
*/
function shouldSkipHeader(headerName) {
const lowerName = headerName.toLowerCase();
// 使用Set的has方法直接检查前缀开头
for (const prefix of CONFIG.SKIP_HEADERS_PREFIX) {
if (lowerName.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* 带超时的fetch
* @param {Request} request 请求
* @param {number} timeout 超时时间(ms)
* @returns {Promise<Response>} 响应
*/
async function fetchWithTimeout(request, timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(request, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error.name === 'AbortError'
? new ProxyError('Request timeout', 504)
: error;
}
}
/**
* 错误处理函数
* @param {Error} error 错误对象
* @returns {Response} 错误响应
*/
function handleError(error) {
const status = error instanceof ProxyError ? error.status : 500;
const message = error.message || 'Internal Server Error';
// 预定义错误响应头以减少对象创建
const errorHeaders = {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
...CONFIG.SECURITY_HEADERS
};
// 记录错误
console.error(`[ProxyError] ${status}: ${message}`, error.stack);
return new Response(
JSON.stringify({ error: message, status }),
{ status, headers: errorHeaders }
);
}
用Cloudflare Workers搭建Wikipedia镜像指南
https://www.noctiro.moe/posts/wikipedia-mirror/