码桶

发现社区成员的开源项目

main
Cloudflare_Proxy/_worker.js
_worker.js9.6 KB
export default {
  async fetch(request, env, context) {
    const url = new URL(request.url);

    // ==========================================
    // OPTIONS 预检:CORS 全开放
    // ==========================================
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS, POST',
          'Access-Control-Allow-Headers': '*',
          'Access-Control-Max-Age': '86400',
        }
      });
    }

    const getErrorResponse = (statusCode, message) => {
      return new Response(message, {
        status: statusCode,
        headers: { 'Content-Type': 'text/plain; charset=utf-8' }
      });
    };

    // ==========================================
    // 1. 提取路径与兜底拦截
    // ==========================================
    let rawPath = url.pathname.slice(1);

    if (!rawPath.includes('http:/') && !rawPath.includes('https:/')) {
      return getErrorResponse(400, '目标 URL 解析失败,结构残缺。请检查请求地址。');
    }

    // ==========================================
    // 2. 提取目标 URL 与伪装域名
    // ==========================================
    let customRefererHost = '';
    const customHostRegex = /^([a-zA-Z0-9][-a-zA-Z0-9]{0,62}(?:\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+)\/(https?:\/.*)$/i;
    const match = rawPath.match(customHostRegex);

    let targetUrlStr;
    if (match) {
      customRefererHost = match[1];
      targetUrlStr = match[2] + url.search;
    } else {
      targetUrlStr = rawPath + url.search;
    }

    // 规范化双斜杠 (避免 https:/ 导致的 URL 解析失败)
    targetUrlStr = targetUrlStr.replace(/^(https?:\/)(?!\/)/i, '$1/');

    let targetUrl;
    try {
      targetUrl = new URL(targetUrlStr);
    } catch (e) {
      return getErrorResponse(400, '目标 URL 解析失败,请检查请求地址。');
    }

    // ==========================================
    // 3. 禁止代理自身
    // ==========================================
    if (targetUrl.hostname === url.hostname) {
      return getErrorResponse(403, '禁止代理自身域名,请勿套娃。');
    }

    // ==========================================
    // 4. 请求方法白名单
    // ==========================================
    if (!['GET', 'HEAD', 'POST'].includes(request.method)) {
      return new Response('Method Not Allowed', { status: 405 });
    }

    // ==========================================
    // 5. IP 提取与 Header 构建准备
    // ==========================================
    const clientIp =
      request.headers.get('CF-Connecting-IP') ||
      request.headers.get('X-Forwarded-For')?.split(',')[0].trim() ||
      request.headers.get('X-Real-IP') ||
      'Unknown_IP';
    const upstreamIp = clientIp !== 'Unknown_IP' ? `${clientIp}, 127.0.0.1` : 'Not Sent';

    const baseHeaders = {
      'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
      'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
      'Connection': 'keep-alive'
    };

    const passHeaders = ['accept', 'accept-encoding', 'accept-language', 'range', 'content-type'];

    const buildHeaders = (mode) => {
      const headers = new Headers(baseHeaders);
      const debug = { referer: '', ua: '' };

      const clientUA = request.headers.get('User-Agent');
      if (clientUA && clientUA.trim() !== '') {
        headers.set('User-Agent', clientUA);
        debug.ua = clientUA;
      } else {
        const fallback = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
        headers.set('User-Agent', fallback);
        debug.ua = fallback + ' (Fallback)';
      }

      for (const h of passHeaders) {
        const v = request.headers.get(h);
        if (v) headers.set(h, v);
      }

      if (clientIp !== 'Unknown_IP') {
        headers.set('X-Forwarded-For', upstreamIp);
        headers.set('X-Real-IP', clientIp);
      }

      if (customRefererHost) {
        debug.referer = `https://${customRefererHost}/ (Forced by User)`;
        headers.set('Referer', `https://${customRefererHost}/`);
        headers.set('Origin', `https://${customRefererHost}`);
      } else if (mode === 'disguise') {
        debug.referer = targetUrl.origin + '/ (Auto Disguise Mode)';
        headers.set('Referer', targetUrl.origin + '/');
        headers.set('Origin', targetUrl.origin);
      } else {
        debug.referer = 'None (Clean Mode)';
      }

      return { headers, debug };
    };

    // 修复:严谨的重定向 URL 构建逻辑(处理相对路径与绝对路径)
    const buildProxyUrl = (location, withDisguise) => {
      // 算出上游真正的绝对 URL
      const absoluteTarget = new URL(location, targetUrl).href;
      const prefix = (withDisguise && customRefererHost)
        ? url.origin + '/' + customRefererHost + '/'
        : url.origin + '/';
      return prefix + absoluteTarget;
    };

    // ==========================================
    // 6. 主请求逻辑(修复 POST 流重用问题)
    // ==========================================
    const FETCH_TIMEOUT = 30000;
    const ANTI_HOTLINK_CODES = [403, 401, 451, 404];

    // 如果是 POST 请求,提前 clone 一份流备用
    let bodyForClean = null;
    let bodyForDisguise = null;

    if (request.method === 'POST') {
      const [b1, b2] = request.body ? request.body.tee() : [null, null];
      bodyForClean = b1;
      bodyForDisguise = b2;
    }

    const fetchWithTimeout = async (bodyStream, headersDebug, timeoutMs) => {
      const ctrl = new AbortController();
      const tid = setTimeout(() => ctrl.abort(), timeoutMs);
      
      const opts = {
        method: request.method,
        redirect: 'manual',
        headers: headersDebug.headers,
        signal: ctrl.signal,
      };

      if (request.method === 'POST') {
        opts.body = bodyStream;
      }

      try {
        const resp = await fetch(targetUrl.href, opts);
        return { response: resp, debug: headersDebug.debug, aborted: false };
      } catch (err) {
        if (err.name === 'AbortError') {
          return { response: null, debug: headersDebug.debug, aborted: true };
        }
        throw err;
      } finally {
        clearTimeout(tid);
      }
    };

    try {
      // 第一步:Clean 模式
      const cleanHeaders = buildHeaders('clean');
      let result = await fetchWithTimeout(bodyForClean, cleanHeaders, FETCH_TIMEOUT);

      if (result.aborted) {
        return getErrorResponse(504, '请求超时,目标服务器响应过慢。');
      }

      let finalDebug = result.debug;
      let response = result.response;
      let usedMode = 'clean';

      // 第二步:智能重试(只在防盗链状态码且未自定义 Referer 时触发)
      if (!customRefererHost && ANTI_HOTLINK_CODES.includes(response.status)) {
        const disguiseHeaders = buildHeaders('disguise');
        result = await fetchWithTimeout(bodyForDisguise, disguiseHeaders, FETCH_TIMEOUT);

        if (result.aborted) {
          return getErrorResponse(504, '请求超时,目标服务器响应过慢。');
        }

        finalDebug = result.debug;
        response = result.response;
        usedMode = 'disguise';
      }

      // ==========================================
      // 7. 重定向改写
      // ==========================================
      if ([301, 302, 307, 308].includes(response.status)) {
        const location = response.headers.get('Location');
        if (location) {
          const rewritten = buildProxyUrl(location, usedMode === 'disguise');
          return new Response(null, {
            status: response.status,
            statusText: response.statusText,
            headers: {
              'Location': rewritten,
              'Access-Control-Allow-Origin': '*',
              'Access-Control-Expose-Headers': 'Location',
            }
          });
        }
      }

      // ==========================================
      // 8. 包装并输出最终响应
      // ==========================================
      const responseHeaders = new Headers();
      
      // 注意:去掉了 content-encoding,防止 Cloudflare 解压后导致乱码
      const headersToCopy = [
        'content-type', 'content-length', 'content-range',
        'accept-ranges', 'cache-control', 'expires',
        'last-modified', 'etag', 'content-disposition'
      ];

      for (const h of headersToCopy) {
        const v = response.headers.get(h);
        if (v) responseHeaders.set(h, v);
      }

      responseHeaders.set('Access-Control-Allow-Origin', '*');
      responseHeaders.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST');
      responseHeaders.set('Access-Control-Allow-Headers', '*');
      responseHeaders.delete('Set-Cookie');

      responseHeaders.set('X-Debug-Fake-Referer', finalDebug.referer);
      responseHeaders.set('X-Debug-Upstream-IP', upstreamIp);
      responseHeaders.set('X-Debug-Upstream-UA', finalDebug.ua);
      responseHeaders.set('X-Debug-Proxy-Status', `${usedMode} → Layer-1: ${response.status}`);
      responseHeaders.set('Access-Control-Expose-Headers',
        'X-Debug-Fake-Referer, X-Debug-Upstream-IP, X-Debug-Upstream-UA, X-Debug-Proxy-Status, Content-Range, Content-Disposition');

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: responseHeaders
      });

    } catch (error) {
      return getErrorResponse(500, `网关异常: ${error.message}`);
    }
  }
};