码桶
发现社区成员的开源项目
[[path]].js12.1 KB
export async function onRequest(context) {
const { request } = 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',
}
});
}
// ==========================================
// 📜 封装:异常通道的 HTML 错误提示
// ==========================================
const getErrorResponse = (statusCode = 400, errorMessage = '') => {
const htmlContent = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>EdgeOne Proxy Gateway - Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background: #1e1e1e; color: #ced4da; font-family: 'Courier New', Courier, monospace; padding: 20px; line-height: 1.5; }
.container { max-width: 800px; margin: 40px auto; background: #252526; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
.error-box { color: #e74c3c; font-weight: bold; font-size: 16px; margin-bottom: 20px; }
pre { white-space: pre-wrap; word-wrap: break-word; color: #f1c40f; font-size: 14px; margin: 0; }
</style>
</head>
<body>
<div class="container">
<div class="error-box">⚠️ 网关异常提示</div>
<pre>${errorMessage}</pre>
</div>
</body>
</html>`;
return new Response(htmlContent, {
status: statusCode,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate',
'X-Frame-Options': 'DENY'
}
});
};
// ==========================================
// 1. 提取目标 URL 与自定义伪装域名逻辑
// ==========================================
let rawPath = url.pathname.slice(1);
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;
}
// 修复非标准双斜杠
if (targetUrlStr.startsWith('https:/') && !targetUrlStr.startsWith('https://')) {
targetUrlStr = targetUrlStr.replace('https:/', 'https://');
} else if (targetUrlStr.startsWith('http:/') && !targetUrlStr.startsWith('http://')) {
targetUrlStr = targetUrlStr.replace('http:/', 'http://');
}
let targetUrl;
try {
targetUrl = new URL(targetUrlStr);
} catch (e) {
return getErrorResponse(400, '目标 URL 解析失败,结构残缺。请检查请求地址。');
}
// ==========================================
// 2. 禁止代理自身(防无限递归)
// ==========================================
if (targetUrl.hostname === url.hostname) {
return getErrorResponse(403, '禁止代理自身域名,请勿套娃。');
}
// ==========================================
// 3. 请求方法白名单
// ==========================================
if (!['GET', 'HEAD', 'POST'].includes(request.method)) {
return new Response('Method Not Allowed By Proxy Gateway', { status: 405 });
}
// ==========================================
// 3. 多层级 IP 嗅探
// ==========================================
let foundIp = 'Unknown_IP';
try {
if (request.eo && request.eo.clientIp) {
foundIp = request.eo.clientIp;
} else {
// 回退到通用的代理头嗅探
const xff = request.headers.get('X-Forwarded-For');
const realIp = request.headers.get('X-Real-IP');
if (xff) foundIp = xff.split(',')[0].trim();
else if (realIp) foundIp = realIp.trim();
}
} catch (ipError) {
foundIp = 'Unknown_IP';
}
const clientIp = foundIp;
const upstreamIp = clientIp !== 'Unknown_IP' ? `${clientIp}, 127.0.0.1` : 'Not Sent';
// 基础请求头模板(不设 Cache-Control,让上游 CDN 正常缓存视频等大文件)
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,en-GB;q=0.7,en-US;q=0.6',
'Connection': 'keep-alive'
};
const passHeaders = ['accept', 'accept-encoding', 'accept-language', 'range'];
// ==========================================
// 4. 构建上游请求头(重构:返回 { headers, debug } 消除副作用)
// ==========================================
const buildHeaders = (mode) => {
const headers = new Headers(baseHeaders);
const debug = { referer: '', ua: '' };
// UA 透传
const clientUserAgent = request.headers.get('User-Agent');
if (clientUserAgent && clientUserAgent.trim() !== '') {
headers.set('User-Agent', clientUserAgent);
debug.ua = clientUserAgent;
} else {
const fallbackUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
headers.set('User-Agent', fallbackUA);
debug.ua = fallbackUA + ' (Fallback)';
}
// 透传关键头
for (const header of passHeaders) {
const value = request.headers.get(header);
if (value) headers.set(header, value);
}
// IP 伪装头
if (clientIp && clientIp !== 'Unknown_IP') {
headers.set('X-Forwarded-For', upstreamIp);
headers.set('X-Real-IP', clientIp);
headers.set('Client-Ip', clientIp);
headers.set('True-Client-Ip', clientIp);
}
// 三种头策略
if (customRefererHost) {
// 用户指定伪装域名模式
debug.referer = `https://${customRefererHost}/ (Forced by User)`;
headers.set('Referer', `https://${customRefererHost}/`);
headers.set('Origin', `https://${customRefererHost}`);
headers.set('Host', targetUrl.host);
} else if (mode === 'disguise') {
// 自动伪装模式:伪装为目标源自身
debug.referer = targetUrl.origin + '/ (Auto Disguise Mode)';
headers.set('Referer', targetUrl.origin + '/');
headers.set('Origin', targetUrl.origin);
headers.set('Host', targetUrl.host);
} else {
// Clean 模式:无伪装
debug.referer = 'None (Clean Mode)';
headers.set('Host', targetUrl.host);
}
return { headers, debug };
};
// ==========================================
// 5. 构建构造代理 URL 的辅助函数(用于重定向改写)
// ==========================================
const buildProxyUrl = (locationUrl, withDisguise) => {
let prefix;
if (withDisguise && customRefererHost) {
prefix = url.origin + '/' + customRefererHost + '/';
} else {
prefix = url.origin + '/';
}
return prefix + locationUrl;
};
// ==========================================
// 6. 主请求 + 智能重试
// ==========================================
const FETCH_TIMEOUT_CLEAN = 30000; // Clean 模式超时(30秒,兼容慢速视频源)
const FETCH_TIMEOUT_DISGUISE = 30000; // Disguise 模式超时
const ANTI_HOTLINK_CODES = [403, 401, 451, 404]; // 防盗链状态码(含 404 伪装不存在)
const doFetch = () => {
const options = {
method: request.method,
redirect: 'manual',
};
// POST 请求体透传
if (request.method === 'POST') {
options.body = request.body;
}
return options;
};
const fetchWithTimeout = async (fetchOptions, headersDebug, timeoutMs, label) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(targetUrl.href, {
...fetchOptions,
headers: headersDebug.headers,
signal: controller.signal,
});
return { response, debug: headersDebug.debug, aborted: false };
} catch (err) {
if (err.name === 'AbortError') {
return { response: null, debug: headersDebug.debug, aborted: true, label };
}
throw err;
} finally {
clearTimeout(timeoutId);
}
};
try {
const fetchOptions = doFetch();
// 第一步:Clean 模式(带超时)
const cleanHeaders = buildHeaders('clean');
let result = await fetchWithTimeout(fetchOptions, cleanHeaders, FETCH_TIMEOUT_CLEAN, 'clean');
let finalDebug;
let usedMode = 'clean';
if (result.aborted) {
return getErrorResponse(504, 'Clean 模式请求超时,目标服务器响应过慢。');
}
finalDebug = result.debug;
let response = result.response;
// 智能重试:仅在非伪装域名模式 + 防盗链状态码时,切换到 Disguise 模式
if (!customRefererHost && ANTI_HOTLINK_CODES.includes(response.status)) {
const disguiseHeaders = buildHeaders('disguise');
const disguiseFetchOptions = doFetch();
result = await fetchWithTimeout(disguiseFetchOptions, disguiseHeaders, FETCH_TIMEOUT_DISGUISE, 'disguise');
if (result.aborted) {
return getErrorResponse(504, 'Disguise 模式请求超时,目标服务器响应过慢。');
}
finalDebug = result.debug;
response = result.response;
usedMode = 'disguise';
}
// ==========================================
// 7. 301/302 重定向改写:隐藏真实目标
// ==========================================
const redirectStatuses = [301, 302, 307, 308];
if (redirectStatuses.includes(response.status)) {
const location = response.headers.get('Location');
if (location) {
const rewritten = buildProxyUrl(location, usedMode === 'disguise');
const redirectHeaders = new Headers();
redirectHeaders.set('Location', rewritten);
redirectHeaders.set('Access-Control-Allow-Origin', '*');
redirectHeaders.set('Access-Control-Expose-Headers', 'Location');
return new Response(null, {
status: response.status,
statusText: response.statusText,
headers: redirectHeaders,
});
}
}
// ==========================================
// 8. 包装并输出最终响应
// ==========================================
const responseHeaders = new Headers();
const headersToCopy = [
'content-type', 'content-length', 'content-range',
'accept-ranges', 'cache-control', 'expires',
'last-modified', 'etag', 'content-disposition',
'content-encoding',
];
for (const header of headersToCopy) {
const value = response.headers.get(header);
if (value) responseHeaders.set(header, value);
}
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}`);
}
}