用 CF Workers 代理国外 大模型 API url

2025年3月6日 163点热度 0人点赞 0条评论

Shadowfetch 是 Cloudflare Workers 内置的 fetch 的替代方案。它旨在在不泄露不必要的用户信息的情况下,最大限度地控制请求和响应处理。由于 Cloudflare 的内置 fetch 可能会披露如下细节:

用户 IP 地址
地理位置元数据(通过 CF-IPCountry 获取)

其他工人的特定头信息,
Shadowfetch 通过剥离或仅转发必要的标头来充当隐私保护盾。

 

 

项目地址:https://github.com/tysak/shadowfetch

1:创建一个CF WORKERS

放入以下代码(自行修改url地址),并部署,添加个人域名即可

import { connect } from "cloudflare:sockets";

// 全局配置,包括目标 URL 和调试模式
const CONFIG = {
  AUTH_TOKEN: "optional-auth-token", // 可选的认证 token,若不需要可留空或删除相关逻辑
  DEFAULT_DST_URL: "https://generativelanguage.googleapis.com/", // 目标 API 地址,这里以gemini为例
  DEBUG_MODE: false, // 调试模式,设为 true 可查看日志
};

// 从环境变量更新全局配置(优先使用环境变量值)
function updateConfigFromEnv(env) {
  if (!env) return;
  for (const key of Object.keys(CONFIG)) {
    if (key in env) {
      if (typeof CONFIG[key] === 'boolean') {
        CONFIG[key] = env[key] === 'true';
      } else {
        CONFIG[key] = env[key];
      }
    }
  }
}

// 定义文本编码器和解码器,用于字符串与字节数组的转换
const encoder = new TextEncoder();
const decoder = new TextDecoder();

// 过滤不应该转发的 HTTP 头部(如 host、accept-encoding、cf-*)
const HEADER_FILTER_RE = /^(host|accept-encoding|cf-)/i;

// 根据调试模式定义日志输出函数
const log = CONFIG.DEBUG_MODE
  ? (message, data = "") => console.log(`[DEBUG] ${message}`, data)
  : () => {};

// 拼接多个 Uint8Array
function concatUint8Arrays(...arrays) {
  const total = arrays.reduce((sum, arr) => sum + arr.length, 0);
  const result = new Uint8Array(total);
  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}

// 解析 HTTP 响应头部,返回状态码、状态文本、头部和头部结束位置
function parseHttpHeaders(buff) {
  const text = decoder.decode(buff);
  const headerEnd = text.indexOf("\r\n\r\n");
  if (headerEnd === -1) return null;
  const headerSection = text.slice(0, headerEnd).split("\r\n");
  const statusLine = headerSection[0];
  const statusMatch = statusLine.match(/HTTP\/1\.[01] (\d+) (.*)/);
  if (!statusMatch) throw new Error(`Invalid status line: ${statusLine}`);
  const headers = new Headers();
  for (let i = 1; i < headerSection.length; i++) {
    const line = headerSection[i];
    const idx = line.indexOf(": ");
    if (idx !== -1) {
      headers.append(line.slice(0, idx), line.slice(idx + 2));
    }
  }
  return { status: Number(statusMatch[1]), statusText: statusMatch[2], headers, headerEnd };
}

// 读取数据直到遇到双 CRLF(HTTP 头部结束)
async function readUntilDoubleCRLF(reader) {
  let respText = "";
  while (true) {
    const { value, done } = await reader.read();
    if (value) {
      respText += decoder.decode(value, { stream: true });
      if (respText.includes("\r\n\r\n")) break;
    }
    if (done) break;
  }
  return respText;
}

// 异步生成器:读取 chunked 编码的 HTTP 响应数据块
async function* readChunks(reader, buff = new Uint8Array()) {
  while (true) {
    let pos = -1;
    for (let i = 0; i < buff.length - 1; i++) {
      if (buff[i] === 13 && buff[i + 1] === 10) {
        pos = i;
        break;
      }
    }
    if (pos === -1) {
      const { value, done } = await reader.read();
      if (done) break;
      buff = concatUint8Arrays(buff, value);
      continue;
    }
    const size = parseInt(decoder.decode(buff.slice(0, pos)), 16);
    log("Read chunk size", size);
    if (!size) break;
    buff = buff.slice(pos + 2);
    while (buff.length < size + 2) {
      const { value, done } = await reader.read();
      if (done) throw new Error("Unexpected EOF in chunked encoding");
      buff = concatUint8Arrays(buff, value);
    }
    yield buff.slice(0, size);
    buff = buff.slice(size + 2);
  }
}

// 解析完整的 HTTP 响应,处理 chunked 或固定长度的响应体
async function parseResponse(reader) {
  let buff = new Uint8Array();
  while (true) {
    const { value, done } = await reader.read();
    if (value) {
      buff = concatUint8Arrays(buff, value);
      const parsed = parseHttpHeaders(buff);
      if (parsed) {
        const { status, statusText, headers, headerEnd } = parsed;
        const isChunked = headers.get("transfer-encoding")?.includes("chunked");
        const contentLength = parseInt(headers.get("content-length") || "0", 10);
        const data = buff.slice(headerEnd + 4);
        return new Response(
          new ReadableStream({
            async start(ctrl) {
              try {
                if (isChunked) {
                  log("Using chunked transfer mode");
                  for await (const chunk of readChunks(reader, data)) {
                    ctrl.enqueue(chunk);
                  }
                } else {
                  log("Using fixed-length transfer mode", { contentLength });
                  let received = data.length;
                  if (data.length) ctrl.enqueue(data);
                  while (received < contentLength) {
                    const { value, done } = await reader.read();
                    if (done) break;
                    received += value.length;
                    ctrl.enqueue(value);
                  }
                }
                ctrl.close();
              } catch (err) {
                log("Error parsing response", err);
                ctrl.error(err);
              }
            },
          }),
          { status, statusText, headers }
        );
      }
    }
    if (done) break;
  }
  throw new Error("Unable to parse response headers");
}

// 处理 HTTP 请求的转发
async function nativeFetch(req, dstUrl) {
  // 清理头部,过滤不需要的头部
  const cleanedHeaders = new Headers();
  for (const [k, v] of req.headers) {
    if (!HEADER_FILTER_RE.test(k)) {
      cleanedHeaders.set(k, v);
    }
  }

  const targetUrl = new URL(dstUrl);
  cleanedHeaders.set("Host", targetUrl.hostname);
  cleanedHeaders.set("accept-encoding", "identity"); // 禁用压缩

  // 如果需要 API Key,可以在这里添加
  // cleanedHeaders.set('Authorization', 'Bearer YOUR_API_KEY');

  const port = targetUrl.protocol === "https:" ? 443 : 80;
  const socket = await connect(
    { hostname: targetUrl.hostname, port },
    { secureTransport: targetUrl.protocol === "https:" ? "on" : "off" }
  );
  const writer = socket.writable.getWriter();

  // 构造请求行和头部
  const requestLine =
    `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` +
    Array.from(cleanedHeaders.entries())
      .map(([k, v]) => `${k}: ${v}`)
      .join("\r\n") +
    "\r\n\r\n";
  log("Sending request", requestLine);
  await writer.write(encoder.encode(requestLine));

  // 转发请求体(如果有)
  if (req.body) {
    log("Forwarding request body");
    for await (const chunk of req.body) {
      await writer.write(chunk);
    }
  }

  // 解析并返回目标服务器的响应
  return await parseResponse(socket.readable.getReader());
}

// 请求处理入口
async function handleRequest(req, env) {
  updateConfigFromEnv(env);
  CONFIG.DEBUG_MODE = CONFIG.DEBUG_MODE;

  const url = new URL(req.url);
  const path = url.pathname + url.search; // 获取路径和查询参数
  const targetUrl = `${CONFIG.DEFAULT_DST_URL}${path.startsWith('/') ? path.slice(1) : path}`;

  log("Target URL", targetUrl);
  return await nativeFetch(req, targetUrl);
}

// 导出 Cloudflare Workers 的 fetch 事件处理器
export default { fetch: handleRequest };
export const onRequest = (ctx) => handleRequest(ctx.request, ctx.env);

 

以上即可使用

下面是其他使用方法。

 

使用方法

1. HTTP 代理

你只需要把目标 URL 加到你部署的 Worker地址 的后面,用以下格式:

https://你的_Cloudflare_Worker_域名/密码/https://目标网站/其他路径

举个例子:

https://bequiet.pages.dev/fonts/https://destination.example.com/dns_query

访问该链接,就能把请求转发到 https://destination.example.com/dns_query,并且不再暴露自己的真实IP!

2. WebSocket 代理

想要代理 WebSocket?只需要把目标协议换成以 wss:// 开头即可。
格式依然是:

wss://你的_Cloudflare_Worker_域名/密码/wss://目标网站/其他路径

例如:

wss://bequiet.pages.dev/fonts/wss://destination.example.com/chatroom

一键即可中转到 wss://destination.example.com/chatroom

admin

这个人很懒,什么都没留下