import { parseURL } from "./urlParser.js"; import { isValidHostName } from "./utils.js"; import { withCORS } from "./cors.js"; import { onRequestStart, onRequestEnd, onError, renderPrometheus } from "./metrics.js"; /** * 创建代理处理器 * @param {object} options * @param {import('http-proxy').default} proxy * @returns {(req, res) => void} */ export function createProxyHandler(options, proxy) { const { originBlacklist = [], originWhitelist = [], requireHeader = [], removeHeaders = [], setHeaders = {}, checkRateLimit = null, redirectSameOrigin = false, } = options || {}; // 根路径文档页渲染(KISS/YAGNI:仅提供必要说明) const renderLandingHTML = (req) => { const host = req.headers.host || `localhost:${process.env.PORT || 4399}`; const base = `http://${host}`; return `\n\n\n \n \n CORS Proxy 服务\n \n\n\n

CORS Proxy Server

\n

通过在目标 URL 前加上代理前缀来解决跨域。

\n

用法

\n
${base}/<目标URL>
\n

示例

\n
GET ${base}/https://api.github.com/users/octocat\nGET ${base}/https://jsonplaceholder.typicode.com/posts/1
\n

说明

\n \n

将真实 API 地址拼接在 ${base}/ 后即可。

\n\n`; }; return (req, res) => { const start = Date.now(); const reqId = Math.random().toString(36).slice(2, 10); onRequestStart(); const log = (level, data) => { try { console.log(JSON.stringify({ level, reqId, method: req.method, path: req.url, origin: req.headers.origin || null, ...data })); } catch {} }; // 健康检查与指标 if (req.url === "/healthz") { const headers = withCORS({ "Content-Type": "application/json" }, req); res.writeHead(200, headers); res.end(JSON.stringify({ status: "ok", uptime: process.uptime() })); onRequestEnd(); return; } if (req.url === "/metrics") { const text = renderPrometheus(); res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" }); res.end(text); onRequestEnd(); return; } // 0. 处理 OPTIONS 预检请求 if (req.method === "OPTIONS") { const headers = withCORS({}, req); res.writeHead(200, headers); res.end(); return; } // 0.1 检查来源黑/白名单 const origin = req.headers.origin || ""; if (originBlacklist.length && originBlacklist.includes(origin)) { onError(); res.writeHead(403, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "origin_blacklisted" })); onRequestEnd(); return; } if (originWhitelist.length && !originWhitelist.includes(origin)) { onError(); res.writeHead(403, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "origin_not_whitelisted" })); onRequestEnd(); return; } // 0.2 检查必需头部 if ( requireHeader.length && !requireHeader.every((h) => req.headers[h.toLowerCase()]) ) { onError(); res.writeHead(400, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "missing_required_header" })); onRequestEnd(); return; } // 0.3 检查限流 if (typeof checkRateLimit === "function" && !checkRateLimit(req)) { onError(); res.writeHead(429, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "rate_limit_exceeded" })); onRequestEnd(); return; } // 1. 提取目标 URL const url = decodeURIComponent(req.url.substring(1)); if (!url || req.url === "/") { const headers = withCORS({ "Content-Type": "text/html; charset=utf-8" }, req); res.writeHead(200, headers); res.end(renderLandingHTML(req)); onRequestEnd(); return; } const targetLocation = parseURL(url); if (!targetLocation) { onError(); res.writeHead(400, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "invalid_target_url" })); onRequestEnd(); return; } // 2. 主机名校验 if (!isValidHostName(targetLocation.hostname)) { onError(); res.writeHead(404, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "invalid_host" })); onRequestEnd(); return; } // 2.1 移除指定头部 removeHeaders.forEach((h) => { delete req.headers[h.toLowerCase()]; }); // 2.2 设置指定头部 Object.entries(setHeaders).forEach(([k, v]) => { req.headers[k.toLowerCase()] = v; }); // 3. 代理请求 proxy.once("proxyRes", (proxyRes, req2, res2) => { // 处理重定向 if ([301, 302, 303, 307, 308].includes(proxyRes.statusCode)) { const loc = proxyRes.headers["location"]; if (loc) { const newUrl = new URL(loc, targetLocation); // redirectSameOrigin: 只重定向同源 if (!redirectSameOrigin || newUrl.origin === targetLocation.origin) { proxyRes.headers["location"] = "/" + newUrl.href; } } } // 添加 CORS 头 Object.entries(withCORS({}, req2)).forEach(([k, v]) => { res2.setHeader(k, v); }); // 记录成功日志 try { console.log(JSON.stringify({ level: "info", reqId, status: proxyRes.statusCode, target: targetLocation.href, duration_ms: Date.now() - start })); } catch {} }); proxy.web(req, res, { target: targetLocation.href, ignorePath: true, changeOrigin: true, }); res.on("close", () => { onRequestEnd(); }); }; } // 基础可靠性:socket 超时(15s 默认) try { req.socket.setTimeout(15000, () => { onError(); try { res.writeHead(504, withCORS({ "Content-Type": "application/json" }, req)); res.end(JSON.stringify({ error: "client_timeout" })); } catch {} try { req.destroy(); } catch {} onRequestEnd(); }); } catch {}