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/
Author
Noctiro
Published at
2025-02-28