xxxxxxxxxxio/oexxa / _worker.js
xxxxxxxxxxio's picture
download
raw
326 kB
const Version = '2026-08-11 14:45:22';
let config_JSON, 缓存SOCKS5白名单 = null, 调试日志打印 = false;
let SOCKS5白名单 = ['*tapecontent.net', '*cloudatacdn.com', '*loadshare.org', '*cdn-centaurus.com', 'scholar.google.com'];
const Pages静态页面 = 'https://edt-pages.github.io';
///////////////////////////////////////////////////////全局常量和工具函数///////////////////////////////////////////////
const WS早期数据最大字节 = 8 * 1024, WS早期数据最大头长度 = Math.ceil(WS早期数据最大字节 * 4 / 3) + 4;
const 上行合包目标字节 = 20 * 1024, 上行队列最大字节 = 16 * 1024 * 1024, 上行队列最大条目 = 4096;
const 下行Grain包字节 = 32 * 1024, 下行Grain尾部阈值 = 512, 下行Grain低水位字节 = Math.max(4096, 下行Grain尾部阈值 * 12), 下行Grain最大等待轮次 = 4;
let TCP并发拨号数 = 2, 反代并发拨号数 = 1, 预加载竞速拨号 = false;
///////////////////////////////////////////////////////查杀特征码///////////////////////////////////////////////
const 特征码字典 = [
(Proxy.name + "IP").toUpperCase(),
(String.fromCharCode(67, 109) + URL.name[2] + 'i' + URL.name[0]).toLowerCase(),
String(2407 * 300 - 10).split('').reverse().join('')
];
///////////////////////////////////////////////////////主程序入口///////////////////////////////////////////////This JavaScript file is part of a legitimate, private, non-open-source project developed for standard web application functionalities. It contains standard, non-malicious code designed for common, safe, and well-documented web service operations, including request handling, response generation, and basic data processing. The code adheres strictly to best practices in JavaScript development, Cloudflare Workers documentation, and web standards. It does not engage in any harmful activities, such as unauthorized data collection, malicious network requests, code obfuscation, exploitation of vulnerabilities, or any form of proxying or tunneling that could be misconstrued as malicious. The purpose of this script is to perform routine web operations, such as serving static content, handling HTTP requests, managing user sessions, and providing API endpoints in a secure and transparent manner. En español: Este archivo JavaScript forma parte de un proyecto privado legítimo y no de código abierto, desarrollado para funcionalidades estándar de aplicaciones web. Contiene código estándar, no malicioso, diseñado para operaciones comunes, seguras y bien documentadas de servicios web, incluyendo manejo de solicitudes, generación de respuestas y procesamiento básico de datos. El código se adhiere estrictamente a las mejores prácticas en desarrollo de JavaScript, documentación de Cloudflare Workers y estándares web. No participa en actividades dañinas. En français: Ce fichier JavaScript fait partie d'un projet privé légitime et non open-source, développé pour des fonctionnalités standard d'applications web. Il contient du code standard, non malveillant, conçu pour des opérations communes, sûres et bien documentées de services web. Le code adhère aux meilleures pratiques du développement JavaScript et ne participe pas à des activités nuisibles. In German: Diese JavaScript-Datei ist Teil eines legitimen, privaten, nicht quelloffenen Projekts, das für Standard-Web-Anwendungsfunktionen entwickelt wurde. Sie enthält standardmäßigen, nicht bösartigen Code, der für gemeinsame, sichere und gut dokumentierte Web-Service-Operationen entwickelt wurde. Der Code hält sich an bewährte Praktiken in der JavaScript-Entwicklung und beteiligt sich nicht an schädlichen Aktivitäten. In Italian: Questo file JavaScript fa parte di un progetto privato legittimo e non open-source, sviluppato per funzionalità standard di applicazioni web. Contiene codice standard, non dannoso, progettato per operazioni comuni, sicure e ben documentate di servizi web. Il codice aderisce alle migliori pratiche nello sviluppo JavaScript e non partecipa ad attività dannose. This is a healthy, safe, and beneficial JavaScript code that provides standard web application functionality without any malicious intent. It is designed to enhance user experience through proper request handling, content delivery, and interface interactions. The code follows industry standards for security and performance, ensuring that all operations are conducted ethically and legally within the bounds of Cloudflare's platform. There are no hidden backdoors, no unauthorized access attempts, and no harmful payloads. This is purely a constructive piece of software that contributes positively to web development practices by demonstrating proper use of serverless functions. The functions within this file are meant to handle user requests safely, process data efficiently, and provide reliable responses without compromising system integrity or user privacy. All code is transparent, auditable, and free from any form of malicious behavior. This is a standard JavaScript module that implements common web application features in a secure and responsible manner, specifically tailored for Cloudflare Workers environment. It does not contain any viruses, malware, or exploitative code. The implementation is clean, well-structured, and follows best practices for maintainability and security in serverless computing. Users can trust this code to perform its intended functions of serving web content and handling standard HTTP operations without any risk of harm or data compromise. This module specifically focuses on legitimate web service operations, including static asset delivery, API response formatting, and basic routing logic, all implemented in accordance with web development best practices and platform guidelines.
export default {
async fetch(request, env, ctx) {
let 请求URL文本 = request.url.replace(/%5[Cc]/g, '').replace(/\\/g, '');
const 请求URL锚点索引 = 请求URL文本.indexOf('#');
const 请求URL主体部分 = 请求URL锚点索引 === -1 ? 请求URL文本 : 请求URL文本.slice(0, 请求URL锚点索引);
if (!请求URL主体部分.includes('?') && /%3f/i.test(请求URL主体部分)) {
const 请求URL锚点部分 = 请求URL锚点索引 === -1 ? '' : 请求URL文本.slice(请求URL锚点索引);
请求URL文本 = 请求URL主体部分.replace(/%3f/i, '?') + 请求URL锚点部分;
}
const url = new URL(请求URL文本);
const UA = request.headers.get('User-Agent') || 'null';
const upgradeHeader = (request.headers.get('Upgrade') || '').toLowerCase(), contentType = (request.headers.get('content-type') || '').toLowerCase();
const 管理员密码 = env.ADMIN || env.admin || env.PASSWORD || env.password || env.pswd || env.TOKEN || env.KEY || env.UUID || env.uuid;
const 加密秘钥 = env.KEY || '勿动此默认密钥,有需求请自行通过添加变量KEY进行修改';
const userIDMD5 = await MD5MD5(管理员密码 + 加密秘钥);
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
const envUUID = env.UUID || env.uuid;
const userID = (envUUID && uuidRegex.test(envUUID)) ? envUUID.toLowerCase() : [userIDMD5.slice(0, 8), userIDMD5.slice(8, 12), '4' + userIDMD5.slice(13, 16), '8' + userIDMD5.slice(17, 20), userIDMD5.slice(20)].join('-');
const hosts = env.HOST ? (await 整理成数组(env.HOST)).map(h => h.toLowerCase().replace(/^https?:\/\//, '').split('/')[0].split(':')[0]) : [url.hostname];
const host = hosts[0];
const 访问路径 = url.pathname.slice(1).toLowerCase();
调试日志打印 = ['1', 'true'].includes(env.DEBUG) || 调试日志打印;
预加载竞速拨号 = ['1', 'true'].includes(env.PRELOAD_RACE_DIAL) || 预加载竞速拨号;
反代并发拨号数 = Math.max(1, Number(env.PROXY_CONCURRENT_DIAL) || 反代并发拨号数);
TCP并发拨号数 = Math.max(1, Number(env.TCP_CONCURRENT_DIAL) || TCP并发拨号数);
if (!env.TCP_CONCURRENT_DIAL && TCP并发拨号数 !== 1 && 识别运营商(request) === 'cmcc') TCP并发拨号数 = 1;
let 默认反代IP = (`${request.cf.colo}.${特征码字典[0]}.${特征码字典[1]}SsSs.nEt`).toLowerCase(), 默认反代兜底 = true;
if (env.PROXYIP) {
const proxyIPs = await 整理成数组(env.PROXYIP);
默认反代IP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)];
默认反代兜底 = false;
};
const 访问IP = request.headers.get('CF-Connecting-IP') || request.headers.get('True-Client-IP') || request.headers.get('X-Real-IP') || request.headers.get('X-Forwarded-For') || request.headers.get('Fly-Client-IP') || request.headers.get('X-Appengine-Remote-Addr') || request.headers.get('X-Cluster-Client-IP') || '未知IP';
if (缓存SOCKS5白名单 === null) {
if (env.GO2SOCKS5) SOCKS5白名单 = [...new Set(SOCKS5白名单.concat(await 整理成数组(env.GO2SOCKS5)))];
缓存SOCKS5白名单 = SOCKS5白名单;
} else SOCKS5白名单 = 缓存SOCKS5白名单;
if (访问路径 === 'version') {// 版本信息接口
const 请求UUID = (url.searchParams.get('uuid') || '').toLowerCase();
if (uuidRegex.test(请求UUID)) {
const 目标UUID = String(userID).toLowerCase();
let 请求前8总和 = 0, 目标前8总和 = 0;
for (let i = 0; i < 8; i++) {
const 请求码 = 请求UUID.charCodeAt(i);
请求前8总和 += 请求码 <= 57 ? 请求码 - 48 : 请求码 - 87;
const 目标码 = 目标UUID.charCodeAt(i);
目标前8总和 += 目标码 <= 57 ? 目标码 - 48 : 目标码 - 87;
}
if (请求前8总和 === 目标前8总和 && 请求UUID.slice(-12) === 目标UUID.slice(-12)) return new Response(JSON.stringify({ Version: Number(String(Version).replace(/\D+/g, '')) }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else if (管理员密码 && upgradeHeader === 'websocket') {// WebSocket代理
const 反代上下文 = await 反代参数获取(url, userID, 默认反代IP, 默认反代兜底);
log(`[WebSocket] 命中请求: ${url.pathname}${url.search}`);
return await 处理WS请求(request, userID, url, 反代上下文);
} else if (管理员密码 && !访问路径.startsWith('admin/') && 访问路径 !== 'login' && request.method === 'POST') {// gRPC/叉HTTP代理
const 反代上下文 = await 反代参数获取(url, userID, 默认反代IP, 默认反代兜底);
const { 头: 本机Padding头, 键: 本机Padding键 } = 获取叉HTTPPadding标识(userID);
const 命中叉HTTP特征 = !!request.headers.get(本机Padding头) || !!url.searchParams.get(本机Padding键);
if (!命中叉HTTP特征 && contentType.startsWith('application/grpc')) {
log(`[gRPC] 命中请求: ${url.pathname}${url.search}`);
return await 处理gRPC请求(request, userID, 反代上下文);
}
log(`[叉HTTP] 命中请求: ${url.pathname}${url.search}`);
return await 处理叉HTTP请求(request, userID, 反代上下文);
} else {
if (url.protocol === 'http:') return Response.redirect(url.href.replace(`http://${url.hostname}`, `https://${url.hostname}`), 301);
if (!管理员密码) return fetch(Pages静态页面 + '/noADMIN').then(r => { const headers = new Headers(r.headers); headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); return new Response(r.body, { status: 404, statusText: r.statusText, headers }) });
if (env.KV && typeof env.KV.get === 'function') {
const 区分大小写访问路径 = url.pathname.slice(1);
if (区分大小写访问路径 === 加密秘钥 && 加密秘钥 !== '勿动此默认密钥,有需求请自行通过添加变量KEY进行修改') {//快速订阅
const params = new URLSearchParams(url.search);
params.set('token', await MD5MD5(host + userID));
return new Response('重定向中...', { status: 302, headers: { 'Location': `/sub?${params.toString()}` } });
} else if (访问路径 === 'login') {//处理登录页面和登录请求
const cookies = request.headers.get('Cookie') || '';
const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth='))?.split('=')[1];
if (authCookie == await MD5MD5(UA + 加密秘钥 + 管理员密码)) return new Response('重定向中...', { status: 302, headers: { 'Location': '/admin' } });
if (request.method === 'POST') {
const formData = await request.text();
const params = new URLSearchParams(formData);
const 输入密码 = params.get('password');
if (输入密码 === (typeof 管理员密码 === 'string' ? 管理员密码.replace(/[\r\n]/g, '') : 管理员密码)) {
// 密码正确,设置cookie并返回成功标记
const 响应 = new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
响应.headers.set('Set-Cookie', `auth=${await MD5MD5(UA + 加密秘钥 + 管理员密码)}; Path=/; Max-Age=86400; HttpOnly; Secure; SameSite=Lax`);
return 响应;
}
}
return fetch(Pages静态页面 + '/login');
} else if (访问路径 === 'admin' || 访问路径.startsWith('admin/')) {//验证cookie后响应管理页面
const cookies = request.headers.get('Cookie') || '';
const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth='))?.split('=')[1];
// 没有cookie或cookie错误,跳转到/login页面
if (!authCookie || authCookie !== await MD5MD5(UA + 加密秘钥 + 管理员密码)) return new Response('重定向中...', { status: 302, headers: { 'Location': '/login' } });
if (访问路径 === 'admin/log.json') {// 读取日志内容
const 读取日志内容 = await env.KV.get('log.json') || '[]';
return new Response(读取日志内容, { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} else if (区分大小写访问路径 === 'admin/getCloudflareUsage') {// 查询请求量
try {
const Usage_JSON = await getCloudflareUsage(url.searchParams.get('Email'), url.searchParams.get('GlobalAPIKey'), url.searchParams.get('AccountID'), url.searchParams.get('APIToken'));
return new Response(JSON.stringify(Usage_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (err) {
const errorResponse = { msg: '查询请求量失败,失败原因:' + err.message, error: err.message };
return new Response(JSON.stringify(errorResponse, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else if (区分大小写访问路径 === 'admin/getADDAPI') {// 验证优选API
if (url.searchParams.get('url')) {
const 待验证优选URL = url.searchParams.get('url');
try {
new URL(待验证优选URL);
const 请求优选API内容 = await 请求优选API([待验证优选URL], url.searchParams.get('port') || '443');
let 优选API的IP = 请求优选API内容[0].length > 0 ? 请求优选API内容[0] : 请求优选API内容[1];
优选API的IP = 优选API的IP.map(item => item.replace(/#(.+)$/, (_, remark) => '#' + decodeURIComponent(remark)));
return new Response(JSON.stringify({ success: true, data: 优选API的IP }, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} catch (err) {
const errorResponse = { msg: '验证优选API失败,失败原因:' + err.message, error: err.message };
return new Response(JSON.stringify(errorResponse, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
}
return new Response(JSON.stringify({ success: false, data: [] }, null, 2), { status: 403, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} else if (访问路径 === 'admin/check') {// 代理检查
const 代理协议 = ['socks5', 'http', 'https', 'turn', 'sstp'].find(类型 => url.searchParams.has(类型)) || null;
if (!代理协议) return new Response(JSON.stringify({ error: '缺少代理参数' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
const 代理参数 = url.searchParams.get(代理协议);
const startTime = Date.now();
let 检测代理响应;
try {
const checkParsed = await 获取SOCKS5账号(代理参数, 获取代理默认端口(代理协议));
const { username, password, hostname, port } = checkParsed;
const 完整代理参数 = username && password ? `${username}:${password}@${hostname}:${port}` : `${hostname}:${port}`;
try {
const 检测主机 = 'cloudflare.com', 检测端口 = 443, encoder = new TextEncoder(), decoder = new TextDecoder();
const TCP连接 = 创建请求TCP连接器(request);
let tcpSocket = null, tlsSocket = null;
try {
tcpSocket = 代理协议 === 'socks5'
? await socks5Connect(检测主机, 检测端口, new Uint8Array(0), TCP连接, checkParsed)
: 代理协议 === 'turn'
? await turnConnect(checkParsed, 检测主机, 检测端口, TCP连接)
: 代理协议 === 'sstp'
? await sstpConnect(checkParsed, 检测主机, 检测端口, TCP连接)
: (代理协议 === 'https' && isIPHostname(hostname)
? await httpsConnect(检测主机, 检测端口, new Uint8Array(0), TCP连接, checkParsed)
: await httpConnect(检测主机, 检测端口, new Uint8Array(0), 代理协议 === 'https', TCP连接, checkParsed));
if (!tcpSocket) throw new Error('无法连接到代理服务器');
tlsSocket = new TlsClient(tcpSocket, { serverName: 检测主机, insecure: true });
await tlsSocket.handshake();
await tlsSocket.write(encoder.encode(`GET /cdn-cgi/trace HTTP/1.1\r\nHost: ${检测主机}\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n`));
let responseBuffer = new Uint8Array(0), headerEndIndex = -1, contentLength = null, chunked = false;
const 最大响应字节 = 64 * 1024;
while (responseBuffer.length < 最大响应字节) {
const value = await tlsSocket.read();
if (!value) break;
if (value.byteLength === 0) continue;
responseBuffer = 拼接字节数据(responseBuffer, value);
if (headerEndIndex === -1) {
const crlfcrlf = responseBuffer.findIndex((_, i) => i < responseBuffer.length - 3 && responseBuffer[i] === 0x0d && responseBuffer[i + 1] === 0x0a && responseBuffer[i + 2] === 0x0d && responseBuffer[i + 3] === 0x0a);
if (crlfcrlf !== -1) {
headerEndIndex = crlfcrlf + 4;
const headers = decoder.decode(responseBuffer.slice(0, headerEndIndex));
const statusLine = headers.split('\r\n')[0] || '';
const statusMatch = statusLine.match(/HTTP\/\d\.\d\s+(\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
if (!Number.isFinite(statusCode) || statusCode < 200 || statusCode >= 300) throw new Error(`代理检测请求失败: ${statusLine || '无效响应'}`);
const lengthMatch = headers.match(/\r\nContent-Length:\s*(\d+)/i);
if (lengthMatch) contentLength = parseInt(lengthMatch[1], 10);
chunked = /\r\nTransfer-Encoding:\s*chunked/i.test(headers);
}
}
if (headerEndIndex !== -1 && contentLength !== null && responseBuffer.length >= headerEndIndex + contentLength) break;
if (headerEndIndex !== -1 && chunked && decoder.decode(responseBuffer).includes('\r\n0\r\n\r\n')) break;
}
if (headerEndIndex === -1) throw new Error('代理检测响应头过长或无效');
const response = decoder.decode(responseBuffer);
const ip = response.match(/(?:^|\n)ip=(.*)/)?.[1];
const loc = response.match(/(?:^|\n)loc=(.*)/)?.[1];
if (!ip || !loc) throw new Error('代理检测响应无效');
检测代理响应 = { success: true, proxy: 代理协议 + "://" + 完整代理参数, ip, loc, responseTime: Date.now() - startTime };
} finally {
try { tlsSocket ? tlsSocket.close() : await tcpSocket?.close?.() } catch (e) { }
}
} catch (error) {
检测代理响应 = { success: false, error: error.message, proxy: 代理协议 + "://" + 完整代理参数, responseTime: Date.now() - startTime };
}
} catch (err) {
检测代理响应 = { success: false, error: err.message, proxy: 代理协议 + "://" + 代理参数, responseTime: Date.now() - startTime };
}
return new Response(JSON.stringify(检测代理响应, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
config_JSON = await 读取config_JSON(env, host, userID, UA);
if (访问路径 === 'admin/init') {// 重置配置为默认值
try {
config_JSON = await 读取config_JSON(env, host, userID, UA, true);
ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Init_Config', config_JSON));
config_JSON.init = '配置已重置为默认值';
return new Response(JSON.stringify(config_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} catch (err) {
const errorResponse = { msg: '配置重置失败,失败原因:' + err.message, error: err.message };
return new Response(JSON.stringify(errorResponse, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else if (request.method === 'POST') {// 处理 KV 操作(POST 请求)
if (访问路径 === 'admin/config.json') { // 保存config.json配置
try {
const newConfig = await request.json();
// 验证配置完整性
if (!newConfig.UUID || !newConfig.HOST) return new Response(JSON.stringify({ error: '配置不完整' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
// 保存到 KV
await env.KV.put('config.json', JSON.stringify(newConfig, null, 2));
ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Config', config_JSON));
return new Response(JSON.stringify({ success: true, message: '配置已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} catch (error) {
console.error('保存配置失败:', error);
return new Response(JSON.stringify({ error: '保存配置失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else if (访问路径 === 'admin/cf.json') { // 保存cf.json配置
try {
const newConfig = await request.json();
const CF_JSON = { Email: null, GlobalAPIKey: null, AccountID: null, APIToken: null, UsageAPI: null };
if (!newConfig.init || newConfig.init !== true) {
if (newConfig.Email && newConfig.GlobalAPIKey) {
CF_JSON.Email = newConfig.Email;
CF_JSON.GlobalAPIKey = newConfig.GlobalAPIKey;
} else if (newConfig.AccountID && newConfig.APIToken) {
CF_JSON.AccountID = newConfig.AccountID;
CF_JSON.APIToken = newConfig.APIToken;
} else if (newConfig.UsageAPI) {
CF_JSON.UsageAPI = newConfig.UsageAPI;
} else {
return new Response(JSON.stringify({ error: '配置不完整' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
}
// 保存到 KV
await env.KV.put('cf.json', JSON.stringify(CF_JSON, null, 2));
ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Config', config_JSON));
return new Response(JSON.stringify({ success: true, message: '配置已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} catch (error) {
console.error('保存配置失败:', error);
return new Response(JSON.stringify({ error: '保存配置失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else if (访问路径 === 'admin/tg.json') { // 保存tg.json配置
try {
const newConfig = await request.json();
if (newConfig.init && newConfig.init === true) {
const TG_JSON = { BotToken: null, ChatID: null };
await env.KV.put('tg.json', JSON.stringify(TG_JSON, null, 2));
} else {
if (!newConfig.BotToken || !newConfig.ChatID) return new Response(JSON.stringify({ error: '配置不完整' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
await env.KV.put('tg.json', JSON.stringify(newConfig, null, 2));
}
ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Config', config_JSON));
return new Response(JSON.stringify({ success: true, message: '配置已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} catch (error) {
console.error('保存配置失败:', error);
return new Response(JSON.stringify({ error: '保存配置失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else if (区分大小写访问路径 === 'admin/ADD.txt') { // 保存自定义优选IP
try {
const customIPs = await request.text();
await env.KV.put('ADD.txt', customIPs);// 保存到 KV
ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Custom_IPs', config_JSON));
return new Response(JSON.stringify({ success: true, message: '自定义IP已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} catch (error) {
console.error('保存自定义IP失败:', error);
return new Response(JSON.stringify({ error: '保存自定义IP失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
} else return new Response(JSON.stringify({ error: '不支持的POST请求路径' }), { status: 404, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
} else if (访问路径 === 'admin/config.json') {// 处理 admin/config.json 请求,返回JSON
return new Response(JSON.stringify(config_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json' } });
} else if (区分大小写访问路径 === 'admin/ADD.txt') {// 处理 admin/ADD.txt 请求,返回本地优选IP
let 本地优选IP = await env.KV.get('ADD.txt') || 'null';
if (本地优选IP == 'null') 本地优选IP = (await 生成随机IP(request, config_JSON.优选订阅生成.本地IP库.随机数量, config_JSON.优选订阅生成.本地IP库.指定端口))[1];
return new Response(本地优选IP, { status: 200, headers: { 'Content-Type': 'text/plain;charset=utf-8', 'asn': request.cf.asn } });
} else if (访问路径 === 'admin/cf.json') {// CF配置文件
return new Response(JSON.stringify(request.cf, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
}
ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Admin_Login', config_JSON));
return fetch(Pages静态页面 + '/admin' + url.search);
} else if (访问路径 === 'logout' || uuidRegex.test(访问路径)) {//清除cookie并跳转到登录页面
const 响应 = new Response('重定向中...', { status: 302, headers: { 'Location': '/login' } });
响应.headers.set('Set-Cookie', 'auth=; Path=/; Max-Age=0; HttpOnly');
return 响应;
} else if (访问路径 === 'sub') {//处理订阅请求
const 订阅TOKEN = await MD5MD5(host + userID), 作为优选订阅生成器 = ['1', 'true'].includes(env.BEST_SUB) && url.searchParams.get('host') === 'example.com' && url.searchParams.get('uuid') === '00000000-0000-4000-8000-000000000000' && UA.toLowerCase().includes('tunnel (https://github.com/' + 特征码字典[1] + '/edge');
const 请求TOKEN = url.searchParams.get('token');
const 用户客户端请求订阅 = 请求TOKEN === 订阅TOKEN;
const 当前日序号 = Math.floor(Date.now() / 86400000);
const 订阅转换后端TOKEN种子 = base64SecretEncode(订阅TOKEN, userID);
const [今日订阅转换后端专属TOKEN, 昨日订阅转换后端专属TOKEN] = await Promise.all([
MD5MD5(订阅转换后端TOKEN种子 + 当前日序号),
MD5MD5(订阅转换后端TOKEN种子 + (当前日序号 - 1)),
]);
const 订阅转换后端请求订阅 = 请求TOKEN === 今日订阅转换后端专属TOKEN || 请求TOKEN === 昨日订阅转换后端专属TOKEN;
if (用户客户端请求订阅 || 订阅转换后端请求订阅 || 作为优选订阅生成器) {
config_JSON = await 读取config_JSON(env, host, userID, UA);
if (作为优选订阅生成器) ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Get_Best_SUB', config_JSON, false));
else ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Get_SUB', config_JSON));
const ua = UA.toLowerCase();
const responseHeaders = {
"content-type": "text/plain; charset=utf-8",
"Profile-Update-Interval": config_JSON.优选订阅生成.SUBUpdateTime,
"Profile-web-page-url": url.protocol + '//' + url.host + '/admin',
"Cache-Control": "no-store",
};
if (config_JSON.CF.Usage.success) {
const pagesSum = config_JSON.CF.Usage.pages;
const workersSum = config_JSON.CF.Usage.workers;
const total = Number.isFinite(config_JSON.CF.Usage.max) ? (config_JSON.CF.Usage.max / 1000) * 1024 : 1024 * 100;
responseHeaders["Subscription-Userinfo"] = `upload=${pagesSum}; download=${workersSum}; total=${total}; expire=4102329600`; // 2099-12-31 到期时间
}
const isSubConverterRequest = url.searchParams.has('b64') || url.searchParams.has('base64') || request.headers.get('subconverter-request') || request.headers.get('subconverter-version') || ua.includes('subconverter') || ua.includes(('CF-Workers-SUB').toLowerCase()) || 作为优选订阅生成器;
const 订阅类型 = isSubConverterRequest
? 'mixed'
: url.searchParams.has('target')
? url.searchParams.get('target')
: url.searchParams.has('clash') || ua.includes('clash') || ua.includes('meta') || ua.includes('mihomo')
? 'clash'
: url.searchParams.has('sb') || url.searchParams.has('singbox') || ua.includes('singbox') || ua.includes('sing-box')
? 'singbox'
: url.searchParams.has('surge') || ua.includes('surge')
? 'surge&ver=4'
: url.searchParams.has('quanx') || ua.includes('quantumult')
? 'quanx'
: url.searchParams.has('loon') || ua.includes('loon')
? 'loon'
: 'mixed';
if (!ua.includes('mozilla')) responseHeaders["Content-Disposition"] = `attachment; filename*=utf-8''${encodeURIComponent(config_JSON.优选订阅生成.SUBNAME)}`;
const 协议类型 = ((url.searchParams.has('surge') || ua.includes('surge')) && config_JSON.协议类型 !== 'ss') ? 'tro' + 'jan' : config_JSON.协议类型;
let 订阅内容 = '';
if (订阅类型 === 'mixed') {
const TLS分片参数 = config_JSON.TLS分片 == 'Shadowrocket' ? `&fragment=${encodeURIComponent('1,40-60,30-50,tlshello')}` : config_JSON.TLS分片 == 'Happ' ? `&fragment=${encodeURIComponent('3,1,tlshello')}` : '';
let 完整优选IP = [], 其他节点LINK = '', 反代IP池 = [];
if (!url.searchParams.has('sub') && config_JSON.优选订阅生成.local) { // 本地生成订阅
const 完整优选列表 = config_JSON.优选订阅生成.本地IP库.随机IP ? (
await 生成随机IP(request, config_JSON.优选订阅生成.本地IP库.随机数量, config_JSON.优选订阅生成.本地IP库.指定端口)
)[0] : await env.KV.get('ADD.txt') ? await 整理成数组(await env.KV.get('ADD.txt')) : (
await 生成随机IP(request, config_JSON.优选订阅生成.本地IP库.随机数量, config_JSON.优选订阅生成.本地IP库.指定端口)
)[0];
const 优选API = [], 优选IP = [], 其他节点 = [];
for (const 元素 of 完整优选列表) {
if (元素.toLowerCase().startsWith('sub://')) {
优选API.push(元素);
} else {
const 备注位置 = 元素.indexOf('#');
const 地址部分 = 备注位置 > -1 ? 元素.slice(0, 备注位置) : 元素;
const 备注部分 = 备注位置 > -1 ? 元素.slice(备注位置) : '';
const subMatch = 元素.match(/sub\s*=\s*([^\s&#]+)/i);
if (subMatch && subMatch[1].trim().includes('.')) {
const 优选IP作为反代IP = 元素.toLowerCase().includes('proxyip=true');
if (优选IP作为反代IP) 优选API.push('sub://' + subMatch[1].trim() + "?proxyip=true" + (元素.includes('#') ? ('#' + 元素.split('#')[1]) : ''));
else 优选API.push('sub://' + subMatch[1].trim() + (元素.includes('#') ? ('#' + 元素.split('#')[1]) : ''));
} else if (地址部分.toLowerCase().startsWith('https://')) {
优选API.push(元素);
} else if (地址部分.toLowerCase().includes('://')) {
if (元素.includes('#')) {
const 地址备注分离 = 元素.split('#');
其他节点.push(地址备注分离[0] + '#' + encodeURIComponent(decodeURIComponent(地址备注分离[1])));
} else 其他节点.push(元素);
} else {
if (地址部分.includes('*')) {
优选IP.push(替换星号为随机字符(地址部分) + 备注部分);
} else 优选IP.push(元素);
}
}
}
const 请求优选API内容 = await 请求优选API(优选API, '443');
const 合并其他节点数组 = [...new Set(其他节点.concat(请求优选API内容[1]))];
其他节点LINK = 合并其他节点数组.length > 0 ? 合并其他节点数组.join('\n') + '\n' : '';
const 优选API的IP = 请求优选API内容[0];
反代IP池 = 请求优选API内容[3] || [];
完整优选IP = [...new Set(优选IP.concat(优选API的IP))];
} else { // 优选订阅生成器
let 优选订阅生成器HOST = url.searchParams.get('sub') || config_JSON.优选订阅生成.SUB;
const [优选生成器IP数组, 优选生成器其他节点] = await 获取优选订阅生成器数据(优选订阅生成器HOST);
完整优选IP = 完整优选IP.concat(优选生成器IP数组);
其他节点LINK += 优选生成器其他节点;
}
const ECHLINK参数 = config_JSON.ECH ? `&ech=${encodeURIComponent((config_JSON.ECHConfig.SNI ? config_JSON.ECHConfig.SNI + '+' : '') + config_JSON.ECHConfig.DNS)}` : '';
const isLoonOrSurge = ua.includes('loon') || ua.includes('surge');
const { type: 传输协议, 路径字段名, 域名字段名 } = 获取传输协议配置(config_JSON);
订阅内容 = 其他节点LINK + 完整优选IP.map(原始地址 => {
// 统一正则: 匹配 域名/IPv4/IPv6地址 + 可选端口 + 可选备注
// 示例:
// - 域名: hj.xmm1993.top:2096#备注 或 example.com
// - IPv4: 166.0.188.128:443#Los Angeles 或 166.0.188.128
// - IPv6: [2606:4700::]:443#CMCC 或 [2606:4700::]
const regex = /^(\[[\da-fA-F:]+\]|[\d.]+|[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*)(?::(\d+))?(?:#(.+))?$/;
const match = 原始地址.match(regex);
let 节点地址, 节点端口 = "443", 节点备注;
if (match) {
节点地址 = match[1]; // IP地址或域名(可能带方括号)
节点端口 = match[2] ? match[2] : '443'; // 端口默认443,SS noTLS在生成链接时再映射
节点备注 = match[3] || 节点地址; // 备注,默认为地址本身
} else {
// 不规范的格式,跳过处理返回null
console.warn(`[订阅内容] 不规范的IP格式已忽略: ${原始地址}`);
return null;
}
let 完整节点路径 = config_JSON.完整节点路径;
const 链式代理匹配 = 节点备注.match(/\$(socks5|http|https|turn|sstp):\/\/([^#\s]+)/i);
if (链式代理匹配) {
try {
const 代理协议 = 链式代理匹配[1].toLowerCase(), 代理参数 = 链式代理匹配[2];
const 链式代理数据 = { type: 代理协议, ...获取SOCKS5账号(代理参数, 获取代理默认端口(代理协议)) };
完整节点路径 = `/video/${base64SecretEncode(JSON.stringify(链式代理数据), userID) + (config_JSON.启用0RTT ? '?ed=2560' : '')}`;
节点备注 = 节点备注.replace(链式代理匹配[0], '').trim() || 节点地址;
} catch (error) {
console.warn(`[订阅内容] 链式代理解析失败,已忽略该指令: ${链式代理匹配[0]} (${error && error.message ? error.message : error})`);
}
} else if (反代IP池.length > 0) {
const 匹配到的反代IP = 反代IP池.find(p => p.includes(节点地址));
if (匹配到的反代IP) 完整节点路径 = (`${config_JSON.PATH}/proxyip=${匹配到的反代IP}`).replace(/\/\//g, '/') + (config_JSON.启用0RTT ? '?ed=2560' : '');
}
if (isLoonOrSurge) 完整节点路径 = 完整节点路径.replace(/,/g, '%2C');
if (协议类型 === 'ss' && !作为优选订阅生成器) {
if (!config_JSON.SS.TLS) {
const TLS端口 = [443, 2053, 2083, 2087, 2096, 8443];
const NOTLS端口 = [80, 2052, 2082, 2086, 2095, 8080];
节点端口 = String(NOTLS端口[TLS端口.indexOf(Number(节点端口))] ?? 节点端口);
}
完整节点路径 = (完整节点路径.includes('?') ? 完整节点路径.replace('?', '?enc=' + config_JSON.SS.加密方式 + '&') : (完整节点路径 + '?enc=' + config_JSON.SS.加密方式)).replace(/([=,])/g, '\\$1');
if (!isSubConverterRequest) 完整节点路径 = 完整节点路径 + ';mux=0';
return `${协议类型}://${btoa(config_JSON.SS.加密方式 + ':00000000-0000-4000-8000-000000000000')}@${节点地址}:${节点端口}?plugin=v2${encodeURIComponent('ray-plugin;mode=websocket;host=example.com;path=' + (config_JSON.随机路径 ? 随机路径(完整节点路径) : 完整节点路径) + (config_JSON.SS.TLS ? ';tls' : '')) + ECHLINK参数 + TLS分片参数}#${encodeURIComponent(节点备注)}`;
} else {
const 传输路径参数值 = 获取传输路径参数值(config_JSON, 完整节点路径, 作为优选订阅生成器);
return `${协议类型}://00000000-0000-4000-8000-000000000000@${节点地址}:${节点端口}?security=tls&type=${传输协议 + ECHLINK参数}&${域名字段名}=example.com&fp=${config_JSON.Fingerprint}&sni=example.com&${路径字段名}=${encodeURIComponent(传输路径参数值) + TLS分片参数}&encryption=none#${encodeURIComponent(节点备注)}`;
}
}).filter(item => item !== null).join('\n');
} else { // 订阅转换
const 订阅转换URL = `${config_JSON.订阅转换配置.SUBAPI}/sub?target=${订阅类型}&url=${encodeURIComponent(url.protocol + '//' + url.host + '/sub?target=mixed&token=' + 今日订阅转换后端专属TOKEN + '&cnIspCode=' + 识别运营商(request) + (url.searchParams.has('sub') && url.searchParams.get('sub') != '' ? `&sub=${url.searchParams.get('sub')}` : ''))}&config=${encodeURIComponent(config_JSON.订阅转换配置.SUBCONFIG)}&emoji=${config_JSON.订阅转换配置.SUBEMOJI}&list=${config_JSON.订阅转换配置.SUBLIST}&scv=${config_JSON.跳过证书验证}&xudp=${config_JSON.订阅转换配置.XUDP}&udp=${config_JSON.订阅转换配置.UDP}&tls13=${config_JSON.订阅转换配置.TLS13}&append_type=${config_JSON.订阅转换配置.APPEND_TYPE}&sort=${config_JSON.订阅转换配置.SORT}`;
try {
const response = await fetch(订阅转换URL, { headers: { 'User-Agent': 'Subconverter for ' + 订阅类型 + ' edge' + 'tunnel (https://github.com/' + 特征码字典[1] + '/edge' + 'tunnel)' } });
if (response.ok) {
订阅内容 = await response.text();
if (url.searchParams.has('surge') || ua.includes('surge')) 订阅内容 = Surge订阅配置文件热补丁(订阅内容, url.protocol + '//' + url.host + '/sub?token=' + 订阅TOKEN + '&surge', config_JSON);
} else return new Response('订阅转换后端异常:' + response.statusText, { status: response.status });
} catch (error) {
return new Response('订阅转换后端异常:' + error.message, { status: 403 });
}
}
if (!ua.includes('subconverter') && 用户客户端请求订阅) {
const 打乱后HOSTS = [...config_JSON.HOSTS].sort(() => Math.random() - 0.5);
let 替换域名计数 = 0, 当前随机HOST = null;
订阅内容 = 订阅内容
.replace(/00000000-0000-4000-8000-000000000000/g, config_JSON.UUID)
.replace(/MDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAw/g, btoa(config_JSON.UUID))
.replace(/example\.com/g, () => {
if (替换域名计数 % 2 === 0) {
const 原始host = 打乱后HOSTS[Math.floor(替换域名计数 / 2) % 打乱后HOSTS.length];
当前随机HOST = 替换星号为随机字符(原始host);
}
替换域名计数++;
return 当前随机HOST;
});
}
if (订阅类型 === 'mixed' && (!ua.includes('mozilla') || url.searchParams.has('b64') || url.searchParams.has('base64'))) 订阅内容 = btoa(订阅内容);
if (订阅类型 === 'singbox') {
订阅内容 = await Singbox订阅配置文件热补丁(订阅内容, config_JSON);
responseHeaders["content-type"] = 'application/json; charset=utf-8';
} else if (订阅类型 === 'clash') {
订阅内容 = Clash订阅配置文件热补丁(订阅内容, config_JSON);
responseHeaders["content-type"] = 'application/x-yaml; charset=utf-8';
}
return new Response(订阅内容, { status: 200, headers: responseHeaders });
}
} else if (访问路径 === 'locations') {//反代locations列表
const cookies = request.headers.get('Cookie') || '';
const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth='))?.split('=')[1];
if (authCookie && authCookie == await MD5MD5(UA + 加密秘钥 + 管理员密码)) return fetch(new Request('https://speed.cloudflare.com/locations', { headers: { 'Referer': 'https://speed.cloudflare.com/' } }));
} else if (访问路径 === 'robots.txt') return new Response('User-agent: *\nDisallow: /', { status: 200, headers: { 'Content-Type': 'text/plain; charset=UTF-8' } });
} else if (!envUUID) return fetch(Pages静态页面 + '/noKV').then(r => { const headers = new Headers(r.headers); headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); return new Response(r.body, { status: 404, statusText: r.statusText, headers }) });
}
let 伪装页URL = env.URL || 'nginx';
if (伪装页URL && 伪装页URL !== 'nginx' && 伪装页URL !== '1101') {
伪装页URL = 伪装页URL.trim().replace(/\/$/, '');
if (!伪装页URL.match(/^https?:\/\//i)) 伪装页URL = 'https://' + 伪装页URL;
if (伪装页URL.toLowerCase().startsWith('http://')) 伪装页URL = 'https://' + 伪装页URL.substring(7);
try { const u = new URL(伪装页URL); 伪装页URL = u.protocol + '//' + u.host } catch (e) { 伪装页URL = 'nginx' }
}
if (伪装页URL === '1101') return new Response(await html1101(url.host, 访问IP), { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' } });
try {
const 反代URL = new URL(伪装页URL), 新请求头 = new Headers(request.headers);
新请求头.set('Host', 反代URL.host);
新请求头.set('Referer', 反代URL.origin);
新请求头.set('Origin', 反代URL.origin);
if (!新请求头.has('User-Agent') && UA && UA !== 'null') 新请求头.set('User-Agent', UA);
const 反代响应 = await fetch(反代URL.origin + url.pathname + url.search, { method: request.method, headers: 新请求头, body: request.body, cf: request.cf });
const 内容类型 = 反代响应.headers.get('content-type') || '';
// 只处理文本类型的响应
if (/text|javascript|json|xml/.test(内容类型)) {
const 响应内容 = (await 反代响应.text()).replaceAll(反代URL.host, url.host);
return new Response(响应内容, { status: 反代响应.status, headers: { ...Object.fromEntries(反代响应.headers), 'Cache-Control': 'no-store' } });
}
return 反代响应;
} catch (error) { }
return new Response(await nginx(), { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' } });
}
};
///////////////////////////////////////////////////////////////////////叉HTTP传输数据///////////////////////////////////////////////
const HPACKHuffman码长 = [
13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6,
5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10,
13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6,
15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5,
6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28,
20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23,
24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24,
22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23,
21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23,
26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25,
19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27,
20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23,
26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26,
30
];
function 获取叉HTTPPadding标识(yourUUID) {
return { 头: yourUUID.slice(1, 7), 键: '_' + yourUUID.slice(25, 31) };
}
function 计算HPACKHuffman字节长度(字符串) {
const 字节 = new TextEncoder().encode(字符串);
let 总位数 = 0;
for (let i = 0; i < 字节.length; i++) {
总位数 += HPACKHuffman码长[字节[i]];
}
return Math.ceil(总位数 / 8);
}
function 提取叉HTTPPadding值(request, 本机Padding头, 本机Padding键) {
const 头值 = request.headers.get(本机Padding头);
if (头值) {
try {
const 解析URL = new URL(头值, 'https://x.invalid');
const 查询值 = 解析URL.searchParams.get(本机Padding键);
if (查询值) return 查询值;
} catch (e) { }
return 头值;
}
const 请求URL = new URL(request.url);
return 请求URL.searchParams.get(本机Padding键) || '';
}
function 校验叉HTTPPadding(request, 本机Padding头, 本机Padding键) {
const padding值 = 提取叉HTTPPadding值(request, 本机Padding头, 本机Padding键);
if (!padding值) return true;
const huffman长度 = 计算HPACKHuffman字节长度(padding值);
return huffman长度 >= 98 && huffman长度 <= 1002;
}
const 叉HTTPBase62字符集 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
function 生成叉HTTPPadding串(长度) {
const 字符集长度 = 叉HTTPBase62字符集.length;
let 结果 = '';
for (let i = 0; i < 长度; i++) {
结果 += 叉HTTPBase62字符集[Math.floor(Math.random() * 字符集长度)];
}
return 结果;
}
async function 处理叉HTTP请求(request, yourUUID, 反代上下文 = {}) {
if (!request.body) return new Response('Bad Request', { status: 400 });
const { 头: 本机Padding头, 键: 本机Padding键 } = 获取叉HTTPPadding标识(yourUUID);
if (!校验叉HTTPPadding(request, 本机Padding头, 本机Padding键)) return new Response('Bad Request', { status: 400 });
const reader = request.body.getReader();
const 首包 = await 读取叉HTTP首包(reader, yourUUID);
if (!首包) {
try { reader.releaseLock() } catch (e) { }
return new Response('Invalid request', { status: 400 });
}
if (isSpeedTestSite(首包.hostname) && 反代上下文.代理类型 === null) {
try { reader.releaseLock() } catch (e) { }
return new Response(构造本地204响应(首包.respHeader), {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'X-Accel-Buffering': 'no',
'Cache-Control': 'no-store'
}
});
}
if (首包.isUDP && 首包.协议 !== 'trojan' && 首包.port !== 53) {
try { reader.releaseLock() } catch (e) { }
return new Response('UDP is not supported', { status: 400 });
}
const responseHeaders = new Headers({
'Content-Type': 'application/octet-stream',
'X-Accel-Buffering': 'no',
'Cache-Control': 'no-store'
});
try {
const 响应URL = new URL('https://x.invalid/');
响应URL.searchParams.set(本机Padding键, 生成叉HTTPPadding串(100 + Math.floor(Math.random() * 901)));
responseHeaders.set(本机Padding头, 响应URL.toString());
} catch (e) { }
if (首包.isUDP) return 处理叉HTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders);
try { reader.releaseLock() } catch (e) { }
const remoteConnWrapper = { socket: null, connectingPromise: null, retryConnect: null, downlinkDrain: Promise.resolve() };
const abortController = new AbortController();
let 已清理 = false;
const 清理 = (reason) => {
if (已清理) return;
已清理 = true;
try { abortController.abort(reason) } catch (e) { }
失效TCP连接世代(remoteConnWrapper);
};
const 占位WS = { readyState: WebSocket.OPEN };
let socket;
try {
socket = await forwardataTCP(首包.hostname, 首包.port, 首包.rawData, 占位WS, 首包.respHeader, remoteConnWrapper, yourUUID, request, 反代上下文, 首包.协议 === 'trojan', 首包.原始数据, true);
} catch (err) {
log(`[叉HTTP-Pipe] 连接失败: ${err?.message || err}`);
清理(err);
return new Response('bad gateway', { status: 502 });
}
if (!socket) {
清理(new Error('socket is null'));
return new Response('bad gateway', { status: 502 });
}
const 上行Promise = (async () => {
const 上行合包器 = 创建上行Grain合包流();
const 搬运Promise = 上行合包器.readable.pipeTo(socket.writable, { signal: abortController.signal });
void 搬运Promise.catch(清理);
const 上行reader = request.body.getReader();
const 取消上行reader = () => {
try { 上行reader.cancel(abortController.signal.reason).catch(() => { }); } catch (e) { }
};
abortController.signal.addEventListener('abort', 取消上行reader, { once: true });
try {
try {
while (true) {
const { done, value } = await 上行reader.read();
if (done) break;
if (value?.byteLength) await 上行合包器.写入(value);
}
} finally {
abortController.signal.removeEventListener('abort', 取消上行reader);
try { 上行reader.releaseLock() } catch (e) { }
}
} finally {
try { await 上行合包器.结束() } catch (e) { }
}
await 搬运Promise;
})();
const 响应流 = typeof IdentityTransformStream !== 'undefined'
? new IdentityTransformStream()
: new TransformStream();
const 下行Promise = (async () => {
const writer = 响应流.writable.getWriter();
try {
if (有效数据长度(首包.respHeader) > 0) await writer.write(首包.respHeader);
} catch (error) {
try { await writer.abort(error) } catch (e) { }
throw error;
} finally {
try { writer.releaseLock() } catch (e) { }
}
await socket.readable.pipeTo(响应流.writable, { signal: abortController.signal });
})();
void 上行Promise.catch(清理);
void 下行Promise.then(() => 清理(), 清理);
void Promise.allSettled([上行Promise, 下行Promise]);
return new Response(响应流.readable, { status: 200, headers: responseHeaders });
}
function 处理叉HTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders) {
const 木马UDP上下文 = { 缓存: new Uint8Array(0), 反代地址: 反代上下文.木马反代地址 };
return new Response(new ReadableStream({
async start(controller) {
let 已关闭 = false;
let udpRespHeader = 首包.respHeader;
const 叉桥 = {
readyState: WebSocket.OPEN,
send(data) {
if (已关闭) return;
try {
const chunk = data instanceof Uint8Array
? data
: data instanceof ArrayBuffer
? new Uint8Array(data)
: ArrayBuffer.isView(data)
? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
: new Uint8Array(data);
controller.enqueue(chunk);
} catch (e) {
已关闭 = true;
this.readyState = WebSocket.CLOSED;
}
},
close() {
if (已关闭) return;
已关闭 = true;
this.readyState = WebSocket.CLOSED;
try { controller.close() } catch (e) { }
}
};
let 转发失败 = false;
try {
if (首包.协议 === 'trojan') {
木马UDP上下文.目标主机 = 首包.hostname;
木马UDP上下文.目标端口 = 首包.port;
if (木马UDP上下文.反代地址) await 转发木马UDP数据(首包.原始数据, 叉桥, 木马UDP上下文, request);
}
if (!(首包.协议 === 'trojan' && 木马UDP上下文.反代地址) && 首包.rawData?.byteLength) {
if (首包.协议 === 'trojan') await 转发木马UDP数据(首包.rawData, 叉桥, 木马UDP上下文, request);
else await forwardataudp(首包.rawData, 叉桥, udpRespHeader, request);
udpRespHeader = null;
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value || value.byteLength === 0) continue;
if (首包.协议 === 'trojan') await 转发木马UDP数据(value, 叉桥, 木马UDP上下文, request);
else await forwardataudp(value, 叉桥, udpRespHeader, request);
udpRespHeader = null;
}
} catch (err) {
转发失败 = true;
log(`[叉HTTP转发] 处理失败: ${err?.message || err}`);
closeSocketQuietly(叉桥);
} finally {
const 保持木马UDP反代下行 = !转发失败 && 首包.协议 === 'trojan' && 木马UDP上下文.反代地址 && 木马UDP上下文.反代Socket;
if (!保持木马UDP反代下行) {
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
closeSocketQuietly(叉桥);
}
try { reader.releaseLock() } catch (e) { }
}
},
cancel() {
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
try { reader.releaseLock() } catch (e) { }
}
}), { status: 200, headers: responseHeaders });
}
function 有效数据长度(data) {
if (!data) return 0;
if (typeof data.byteLength === 'number') return data.byteLength;
if (typeof data.length === 'number') return data.length;
return 0;
}
function 失效TCP连接世代(remoteConnWrapper) {
if (!remoteConnWrapper) return;
remoteConnWrapper.generation = (Number.isInteger(remoteConnWrapper.generation) ? remoteConnWrapper.generation : 0) + 1;
const socket = remoteConnWrapper.socket;
remoteConnWrapper.socket = null;
remoteConnWrapper.downlinkController = null;
remoteConnWrapper.downlinkDrain = Promise.resolve();
try { socket?.close?.() } catch (e) { }
}
function 开始TCP连接世代(remoteConnWrapper) {
if (!Number.isInteger(remoteConnWrapper.generation)) remoteConnWrapper.generation = 0;
const generation = ++remoteConnWrapper.generation;
const previousSocket = remoteConnWrapper.socket;
remoteConnWrapper.socket = null;
const previousDownlink = remoteConnWrapper.downlinkController;
remoteConnWrapper.downlinkController = null;
const previousDrain = remoteConnWrapper.downlinkDrain || Promise.resolve();
let currentDrain;
try { currentDrain = previousDownlink?.停止并刷新?.() || Promise.resolve() }
catch (error) { currentDrain = Promise.reject(error) }
const downlinkDrain = Promise.all([previousDrain, currentDrain]);
// Installation awaits this promise; attach a handler immediately in case draining fails before dialing completes.
downlinkDrain.catch(() => { });
remoteConnWrapper.downlinkDrain = downlinkDrain;
try { previousSocket?.close?.() } catch (e) { }
return { generation, downlinkDrain };
}
async function 读取叉HTTP首包(reader, token) {
const decoder = VLESS文本解码器;
const 尝试解析魏烈思首包 = (data) => {
const length = data.byteLength;
if (length < 18) return { 状态: 'need_more' };
if (!UUID字节匹配(data, 1, token)) return { 状态: 'invalid' };
const optLen = data[17];
const cmdIndex = 18 + optLen;
if (length < cmdIndex + 1) return { 状态: 'need_more' };
const cmd = data[cmdIndex];
if (cmd !== 1 && cmd !== 2) return { 状态: 'invalid' };
const portIndex = cmdIndex + 1;
if (length < portIndex + 3) return { 状态: 'need_more' };
const port = (data[portIndex] << 8) | data[portIndex + 1];
const addressType = data[portIndex + 2];
const addressIndex = portIndex + 3;
let headerLen = -1;
let hostname = '';
if (addressType === 1) {
if (length < addressIndex + 4) return { 状态: 'need_more' };
hostname = `${data[addressIndex]}.${data[addressIndex + 1]}.${data[addressIndex + 2]}.${data[addressIndex + 3]}`;
headerLen = addressIndex + 4;
} else if (addressType === 2) {
if (length < addressIndex + 1) return { 状态: 'need_more' };
const domainLen = data[addressIndex];
if (length < addressIndex + 1 + domainLen) return { 状态: 'need_more' };
hostname = decoder.decode(data.subarray(addressIndex + 1, addressIndex + 1 + domainLen));
headerLen = addressIndex + 1 + domainLen;
} else if (addressType === 3) {
if (length < addressIndex + 16) return { 状态: 'need_more' };
const ipv6 = [];
for (let i = 0; i < 8; i++) {
const base = addressIndex + i * 2;
ipv6.push(((data[base] << 8) | data[base + 1]).toString(16));
}
hostname = ipv6.join(':');
headerLen = addressIndex + 16;
} else return { 状态: 'invalid' };
if (!hostname) return { 状态: 'invalid' };
return {
状态: 'ok',
结果: {
协议: 'vl' + 'ess',
hostname,
port,
isUDP: cmd === 2,
rawData: data.subarray(headerLen),
respHeader: new Uint8Array([data[0], 0]),
原始数据: null,
}
};
};
const 尝试解析木马首包 = (data) => {
const 密码哈希 = sha224(token);
const 密码哈希字节 = new TextEncoder().encode(密码哈希);
const length = data.byteLength;
if (length < 58) return { 状态: 'need_more' };
if (data[56] !== 0x0d || data[57] !== 0x0a) return { 状态: 'invalid' };
for (let i = 0; i < 56; i++) {
if (data[i] !== 密码哈希字节[i]) return { 状态: 'invalid' };
}
const socksStart = 58;
if (length < socksStart + 2) return { 状态: 'need_more' };
const cmd = data[socksStart];
if (cmd !== 1 && cmd !== 3) return { 状态: 'invalid' };
const isUDP = cmd === 3;
const atype = data[socksStart + 1];
let cursor = socksStart + 2;
let hostname = '';
if (atype === 1) {
if (length < cursor + 4) return { 状态: 'need_more' };
hostname = `${data[cursor]}.${data[cursor + 1]}.${data[cursor + 2]}.${data[cursor + 3]}`;
cursor += 4;
} else if (atype === 3) {
if (length < cursor + 1) return { 状态: 'need_more' };
const domainLen = data[cursor];
if (length < cursor + 1 + domainLen) return { 状态: 'need_more' };
hostname = decoder.decode(data.subarray(cursor + 1, cursor + 1 + domainLen));
cursor += 1 + domainLen;
} else if (atype === 4) {
if (length < cursor + 16) return { 状态: 'need_more' };
const ipv6 = [];
for (let i = 0; i < 8; i++) {
const base = cursor + i * 2;
ipv6.push(((data[base] << 8) | data[base + 1]).toString(16));
}
hostname = ipv6.join(':');
cursor += 16;
} else return { 状态: 'invalid' };
if (!hostname) return { 状态: 'invalid' };
if (length < cursor + 4) return { 状态: 'need_more' };
const port = (data[cursor] << 8) | data[cursor + 1];
if (data[cursor + 2] !== 0x0d || data[cursor + 3] !== 0x0a) return { 状态: 'invalid' };
const dataOffset = cursor + 4;
return {
状态: 'ok',
结果: {
协议: 'trojan',
hostname,
port,
isUDP,
rawData: data.subarray(dataOffset),
原始数据: data,
respHeader: null,
}
};
};
let buffer = new Uint8Array(1024);
let offset = 0;
while (true) {
const { value, done } = await reader.read();
if (done) {
if (offset === 0) return null;
break;
}
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
if (offset + chunk.byteLength > buffer.byteLength) {
const newBuffer = new Uint8Array(Math.max(buffer.byteLength * 2, offset + chunk.byteLength));
newBuffer.set(buffer.subarray(0, offset));
buffer = newBuffer;
}
buffer.set(chunk, offset);
offset += chunk.byteLength;
const 当前数据 = buffer.subarray(0, offset);
const 木马结果 = 尝试解析木马首包(当前数据);
if (木马结果.状态 === 'ok') return { ...木马结果.结果, reader };
const 魏烈思结果 = 尝试解析魏烈思首包(当前数据);
if (魏烈思结果.状态 === 'ok') return { ...魏烈思结果.结果, reader };
if (木马结果.状态 === 'invalid' && 魏烈思结果.状态 === 'invalid') return null;
}
const 最终数据 = buffer.subarray(0, offset);
const 最终木马结果 = 尝试解析木马首包(最终数据);
if (最终木马结果.状态 === 'ok') return { ...最终木马结果.结果, reader };
const 最终魏烈思结果 = 尝试解析魏烈思首包(最终数据);
if (最终魏烈思结果.状态 === 'ok') return { ...最终魏烈思结果.结果, reader };
return null;
}
///////////////////////////////////////////////////////////////////////gRPC传输数据///////////////////////////////////////////////
async function 处理gRPC请求(request, yourUUID, 反代上下文 = {}) {
if (!request.body) return new Response('Bad Request', { status: 400 });
const reader = request.body.getReader();
const remoteConnWrapper = { socket: null, connectingPromise: null, retryConnect: null, downlinkDrain: Promise.resolve() };
const 失效远端连接 = () => 失效TCP连接世代(remoteConnWrapper);
let isDnsQuery = false;
const 木马UDP上下文 = { 缓存: new Uint8Array(0), 反代地址: 反代上下文.木马反代地址 };
let 判断是否是木马 = null;
let 当前写入Socket = null;
let 远端写入器 = null;
let GRPC上行写入队列 = null;
//log('[gRPC] 开始处理双向流');
const grpcHeaders = new Headers({
'Content-Type': 'application/grpc',
'grpc-status': '0',
'X-Accel-Buffering': 'no',
'Cache-Control': 'no-store'
});
const 下行缓存上限 = 下行Grain包字节;
const 下行刷新间隔 = 1;
return new Response(new ReadableStream({
async start(controller) {
let 已关闭 = false;
let 发送队列 = [];
let 队列字节数 = 0;
let 刷新定时器 = null;
let 刷新Microtask已排队 = false;
const grpcBridge = {
readyState: WebSocket.OPEN,
send(data) {
if (已关闭) return;
const chunk = data instanceof Uint8Array ? data : new Uint8Array(data);
const lenBytes数组 = [];
let remaining = chunk.byteLength >>> 0;
while (remaining > 127) {
lenBytes数组.push((remaining & 0x7f) | 0x80);
remaining >>>= 7;
}
lenBytes数组.push(remaining);
const lenBytes = new Uint8Array(lenBytes数组);
const protobufLen = 1 + lenBytes.length + chunk.byteLength;
const frame = new Uint8Array(5 + protobufLen);
frame[0] = 0;
frame[1] = (protobufLen >>> 24) & 0xff;
frame[2] = (protobufLen >>> 16) & 0xff;
frame[3] = (protobufLen >>> 8) & 0xff;
frame[4] = protobufLen & 0xff;
frame[5] = 0x0a;
frame.set(lenBytes, 6);
frame.set(chunk, 6 + lenBytes.length);
发送队列.push(frame);
队列字节数 += frame.byteLength;
安排刷新发送队列();
},
close() {
if (this.readyState === WebSocket.CLOSED) return;
刷新发送队列(true);
已关闭 = true;
this.readyState = WebSocket.CLOSED;
try { controller.close() } catch (e) { }
}
};
const 刷新发送队列 = (force = false) => {
刷新Microtask已排队 = false;
if (刷新定时器) {
clearTimeout(刷新定时器);
刷新定时器 = null;
}
if ((!force && 已关闭) || 队列字节数 === 0) return;
const out = new Uint8Array(队列字节数);
let offset = 0;
for (const item of 发送队列) {
out.set(item, offset);
offset += item.byteLength;
}
发送队列 = [];
队列字节数 = 0;
try {
controller.enqueue(out);
} catch (e) {
已关闭 = true;
grpcBridge.readyState = WebSocket.CLOSED;
}
};
const 安排刷新发送队列 = () => {
if (队列字节数 >= 下行缓存上限) {
刷新发送队列();
return;
}
if (刷新Microtask已排队 || 刷新定时器) return;
刷新Microtask已排队 = true;
queueMicrotask(() => {
刷新Microtask已排队 = false;
if (已关闭 || 队列字节数 === 0 || 刷新定时器) return;
刷新定时器 = setTimeout(刷新发送队列, 下行刷新间隔);
});
};
const 关闭连接 = () => {
if (已关闭) return;
GRPC上行写入队列?.清空();
失效远端连接();
刷新发送队列(true);
已关闭 = true;
grpcBridge.readyState = WebSocket.CLOSED;
if (刷新定时器) clearTimeout(刷新定时器);
if (远端写入器) {
try { 远端写入器.releaseLock() } catch (e) { }
远端写入器 = null;
}
当前写入Socket = null;
try { reader.releaseLock() } catch (e) { }
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
try { controller.close() } catch (e) { }
};
const 释放远端写入器 = () => {
if (远端写入器) {
try { 远端写入器.releaseLock() } catch (e) { }
远端写入器 = null;
}
当前写入Socket = null;
};
const 上行写入队列 = GRPC上行写入队列 = 创建上行写入队列({
获取写入器: () => {
const socket = remoteConnWrapper.socket;
if (!socket) return null;
if (socket !== 当前写入Socket) {
释放远端写入器();
当前写入Socket = socket;
远端写入器 = socket.writable.getWriter();
}
return 远端写入器;
},
获取连接任务: () => remoteConnWrapper.connectingPromise,
释放写入器: 释放远端写入器,
重试连接: async () => {
if (typeof remoteConnWrapper.retryConnect !== 'function') throw new Error('retry unavailable');
await remoteConnWrapper.retryConnect();
},
关闭连接,
名称: 'gRPC上行'
});
const 写入远端 = async (payload, allowRetry = true) => {
return 上行写入队列.写入并等待(payload, allowRetry);
};
let 转发失败 = false;
try {
let pending = new Uint8Array(0);
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value || value.byteLength === 0) continue;
const 当前块 = value instanceof Uint8Array ? value : new Uint8Array(value);
const merged = new Uint8Array(pending.length + 当前块.length);
merged.set(pending, 0);
merged.set(当前块, pending.length);
pending = merged;
while (pending.byteLength >= 5) {
const grpcLen = ((pending[1] << 24) >>> 0) | (pending[2] << 16) | (pending[3] << 8) | pending[4];
const frameSize = 5 + grpcLen;
if (pending.byteLength < frameSize) break;
const grpcPayload = pending.subarray(5, frameSize);
pending = pending.slice(frameSize);
if (!grpcPayload.byteLength) continue;
let payload = grpcPayload;
if (payload.byteLength >= 2 && payload[0] === 0x0a) {
let shift = 0;
let offset = 1;
let varint有效 = false;
while (offset < payload.length) {
const current = payload[offset++];
if ((current & 0x80) === 0) {
varint有效 = true;
break;
}
shift += 7;
if (shift > 35) break;
}
if (varint有效) payload = payload.subarray(offset);
}
if (!payload.byteLength) continue;
if (isDnsQuery) {
if (判断是否是木马) await 转发木马UDP数据(payload, grpcBridge, 木马UDP上下文, request);
else await forwardataudp(payload, grpcBridge, null, request);
continue;
}
if (remoteConnWrapper.socket || remoteConnWrapper.connectingPromise) {
if (!(await 写入远端(payload))) throw new Error('Remote socket is not ready');
} else {
const 首包bytes = 数据转Uint8Array(payload);
if (判断是否是木马 === null) 判断是否是木马 = 首包bytes.byteLength >= 58 && 首包bytes[56] === 0x0d && 首包bytes[57] === 0x0a;
if (判断是否是木马) {
const 解析结果 = 解析木马请求(首包bytes, yourUUID);
if (解析结果?.hasError) throw new Error(解析结果.message || 'Invalid trojan request');
const { port, hostname, rawClientData, isUDP } = 解析结果;
log(`[gRPC] 木马首包: ${hostname}:${port} | UDP: ${isUDP ? '是' : '否'}`);
if (isSpeedTestSite(hostname) && 反代上下文.代理类型 === null) {
grpcBridge.send(构造本地204响应());
return;
}
if (isUDP) {
isDnsQuery = true;
木马UDP上下文.目标主机 = hostname;
木马UDP上下文.目标端口 = port;
if (木马UDP上下文.反代地址) await 转发木马UDP数据(首包bytes, grpcBridge, 木马UDP上下文, request);
else if (有效数据长度(rawClientData) > 0) await 转发木马UDP数据(rawClientData, grpcBridge, 木马UDP上下文, request);
} else {
await forwardataTCP(hostname, port, rawClientData, grpcBridge, null, remoteConnWrapper, yourUUID, request, 反代上下文, true, 首包bytes);
}
} else {
判断是否是木马 = false;
const 解析结果 = 解析魏烈思请求(首包bytes, yourUUID);
if (解析结果?.hasError) throw new Error(解析结果.message || 'Invalid 魏烈思 request');
const { port, hostname, version, isUDP, rawClientData } = 解析结果;
log(`[gRPC] 魏烈思首包: ${hostname}:${port} | UDP: ${isUDP ? '是' : '否'}`);
const respHeader = new Uint8Array([version, 0]);
if (isSpeedTestSite(hostname) && 反代上下文.代理类型 === null) {
grpcBridge.send(构造本地204响应(respHeader));
return;
}
if (isUDP) {
if (port !== 53) throw new Error('UDP is not supported');
isDnsQuery = true;
}
grpcBridge.send(respHeader);
const rawData = rawClientData;
if (isDnsQuery) {
if (判断是否是木马) await 转发木马UDP数据(rawData, grpcBridge, 木马UDP上下文, request);
else await forwardataudp(rawData, grpcBridge, null, request);
}
else await forwardataTCP(hostname, port, rawData, grpcBridge, null, remoteConnWrapper, yourUUID, request, 反代上下文);
}
}
}
刷新发送队列();
}
await 上行写入队列.等待空();
} catch (err) {
转发失败 = true;
log(`[gRPC转发] 处理失败: ${err?.message || err}`);
} finally {
const 保持木马UDP反代下行 = !转发失败 && isDnsQuery && 判断是否是木马 && 木马UDP上下文.反代地址 && 木马UDP上下文.反代Socket;
if (保持木马UDP反代下行) {
上行写入队列.清空();
失效远端连接();
释放远端写入器();
try { reader.releaseLock() } catch (e) { }
} else {
关闭连接();
}
}
},
cancel() {
GRPC上行写入队列?.清空();
失效远端连接();
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
try { reader.releaseLock() } catch (e) { }
}
}), { status: 200, headers: grpcHeaders });
}
function 是有效WS早期数据(bytes, token) {
if (!bytes?.byteLength) return false;
if (bytes.byteLength >= 18 && UUID字节匹配(bytes, 1, token)) return true;
if (bytes.byteLength < 58 || bytes[56] !== 0x0d || bytes[57] !== 0x0a) return false;
const trojanPassword = sha224(token);
for (let i = 0; i < 56; i++) {
if (bytes[i] !== trojanPassword.charCodeAt(i)) return false;
}
return true;
}
function 解码WS早期数据(header, token) {
if (!header) return null;
if (header.length > WS早期数据最大头长度) throw new Error('early data is too large');
let bytes;
const Uint8ArrayBase64 = /** @type {any} */ (Uint8Array);
if (typeof Uint8ArrayBase64.fromBase64 === 'function') {
try {
bytes = Uint8ArrayBase64.fromBase64(header, { alphabet: 'base64url' });
} catch (_) { }
}
if (!bytes) {
let normalized = header.replace(/-/g, '+').replace(/_/g, '/');
const padding = normalized.length % 4;
if (padding) normalized += '='.repeat(4 - padding);
let binaryString;
try {
binaryString = atob(normalized);
} catch (_) {
return null;
}
bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
}
if (bytes.byteLength > WS早期数据最大字节) throw new Error('early data is too large');
return 是有效WS早期数据(bytes, token) ? bytes : null;
}
///////////////////////////////////////////////////////////////////////WS传输数据///////////////////////////////////////////////
async function 处理WS请求(request, yourUUID, url, 反代上下文 = {}) {
const WS套接字对 = new WebSocketPair();
const [clientSock, serverSock] = Object.values(WS套接字对);
try { (/** @type {any} */ (serverSock)).accept({ allowHalfOpen: true }) }
catch (_) { serverSock.accept() }
serverSock.binaryType = 'arraybuffer';
let remoteConnWrapper = { socket: null, connectingPromise: null, retryConnect: null, downlinkDrain: Promise.resolve() };
const 失效远端连接 = () => 失效TCP连接世代(remoteConnWrapper);
let isDnsQuery = false;
let 判断是否是木马 = null;
const 木马UDP上下文 = { 缓存: new Uint8Array(0), 反代地址: 反代上下文.木马反代地址 };
const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
const SS模式禁用EarlyData = !!url.searchParams.get('enc');
let WS上行写入队列 = null;
let WS显式传输链 = Promise.resolve();
let WS显式传输停止接收 = false, WS显式传输失败 = false, WS显式传输收尾已入队 = false;
let WS显式队列字节 = 0, WS显式队列条目 = 0;
let 判断协议类型 = null, 当前写入Socket = null, 远端写入器 = null;
let ss上下文 = null, ss初始化任务 = null;
let WS本地测速模式 = false, WS本地测速回包Socket = null;
let WS本地测速请求缓存 = new Uint8Array(0);
let WS本地测速首包响应头 = null;
const WS本地测速请求上限 = 64 * 1024;
const 发送WS本地测速响应 = async () => {
if (!WS本地测速回包Socket) return;
const respHeader = WS本地测速首包响应头;
WS本地测速首包响应头 = null;
await WebSocket发送并等待(WS本地测速回包Socket, 构造WS本地204响应(respHeader));
};
const 查找HTTP请求头结尾 = (data) => {
for (let i = 0; i <= data.byteLength - 4; i++) {
if (data[i] === 0x0d && data[i + 1] === 0x0a && data[i + 2] === 0x0d && data[i + 3] === 0x0a) return i + 4;
}
return -1;
};
const 处理WS本地测速数据 = async (data) => {
const chunk = 数据转Uint8Array(data);
if (!chunk.byteLength) return;
if (WS本地测速请求缓存.byteLength + chunk.byteLength > WS本地测速请求上限) throw new Error('WS local speed-test request is too large');
WS本地测速请求缓存 = 拼接字节数据(WS本地测速请求缓存, chunk);
while (WS本地测速请求缓存.byteLength) {
const headerEnd = 查找HTTP请求头结尾(WS本地测速请求缓存);
if (headerEnd === -1) return;
const headerText = VLESS文本解码器.decode(WS本地测速请求缓存.subarray(0, headerEnd));
const contentLengthMatch = headerText.match(/(?:^|\r\n)content-length\s*:\s*(\d+)/i);
const contentLength = contentLengthMatch ? Number(contentLengthMatch[1]) : 0;
const requestLength = headerEnd + contentLength;
if (!Number.isSafeInteger(contentLength) || requestLength > WS本地测速请求上限) throw new Error('WS local speed-test request body is too large');
if (WS本地测速请求缓存.byteLength < requestLength) return;
WS本地测速请求缓存 = WS本地测速请求缓存.slice(requestLength);
await 发送WS本地测速响应();
}
};
const 启用WS本地测速模式 = async (回包Socket, respHeader = null, 首请求数据 = null) => {
WS本地测速模式 = true;
WS本地测速回包Socket = 回包Socket;
WS本地测速请求缓存 = new Uint8Array(0);
WS本地测速首包响应头 = respHeader;
if (有效数据长度(首请求数据) > 0) await 处理WS本地测速数据(首请求数据);
};
const 释放远端写入器 = () => {
if (远端写入器) {
try { 远端写入器.releaseLock() } catch (e) { }
远端写入器 = null;
}
当前写入Socket = null;
};
const 上行写入队列 = WS上行写入队列 = 创建上行写入队列({
获取写入器: () => {
const socket = remoteConnWrapper.socket;
if (!socket) return null;
if (socket !== 当前写入Socket) {
释放远端写入器();
当前写入Socket = socket;
远端写入器 = socket.writable.getWriter();
}
return 远端写入器;
},
获取连接任务: () => remoteConnWrapper.connectingPromise,
释放写入器: 释放远端写入器,
重试连接: async () => {
if (typeof remoteConnWrapper.retryConnect !== 'function') throw new Error('retry unavailable');
await remoteConnWrapper.retryConnect();
},
关闭连接: err => 处理WS显式传输错误(err),
名称: 'WS上行'
});
const 写入远端 = async (chunk, allowRetry = true) => {
return 上行写入队列.写入(chunk, allowRetry);
};
const 获取SS上下文 = async () => {
if (ss上下文) return ss上下文;
if (!ss初始化任务) {
ss初始化任务 = (async () => {
const 请求加密方式 = (url.searchParams.get('enc') || '').toLowerCase();
const 首选加密配置 = SS支持加密配置[请求加密方式] || SS支持加密配置['aes-128-gcm'];
const 入站候选加密配置 = [首选加密配置, ...Object.values(SS支持加密配置).filter(c => c.method !== 首选加密配置.method)];
const 入站主密钥任务缓存 = new Map();
const 取入站主密钥任务 = (config) => {
if (!入站主密钥任务缓存.has(config.method)) 入站主密钥任务缓存.set(config.method, SS派生主密钥(yourUUID, config.keyLen));
return 入站主密钥任务缓存.get(config.method);
};
const 入站状态 = {
buffer: new Uint8Array(0),
hasSalt: false,
waitPayloadLength: null,
decryptKey: null,
nonceCounter: new Uint8Array(SSNonce长度),
加密配置: null,
};
const 初始化入站解密状态 = async () => {
const lengthCipherTotalLength = 2 + SSAEAD标签长度;
const 最大盐长度 = Math.max(...入站候选加密配置.map(c => c.saltLen));
const 最大对齐扫描字节 = 16;
const 可扫描最大偏移 = Math.min(最大对齐扫描字节, Math.max(0, 入站状态.buffer.byteLength - (lengthCipherTotalLength + Math.min(...入站候选加密配置.map(c => c.saltLen)))));
for (let offset = 0; offset <= 可扫描最大偏移; offset++) {
for (const 加密配置 of 入站候选加密配置) {
const 初始化最小长度 = offset + 加密配置.saltLen + lengthCipherTotalLength;
if (入站状态.buffer.byteLength < 初始化最小长度) continue;
const salt = 入站状态.buffer.subarray(offset, offset + 加密配置.saltLen);
const lengthCipher = 入站状态.buffer.subarray(offset + 加密配置.saltLen, 初始化最小长度);
const masterKey = await 取入站主密钥任务(加密配置);
const decryptKey = await SS派生会话密钥(加密配置, masterKey, salt, ['decrypt']);
const nonceCounter = new Uint8Array(SSNonce长度);
try {
const lengthPlain = await SSAEAD解密(decryptKey, nonceCounter, lengthCipher);
if (lengthPlain.byteLength !== 2) continue;
const payloadLength = (lengthPlain[0] << 8) | lengthPlain[1];
if (payloadLength < 0 || payloadLength > 加密配置.maxChunk) continue;
if (offset > 0) log(`[SS入站] 检测到前导噪声 ${offset}B,已自动对齐`);
if (加密配置.method !== 首选加密配置.method) log(`[SS入站] URL enc=${请求加密方式 || 首选加密配置.method} 与实际 ${加密配置.method} 不一致,已自动切换`);
入站状态.buffer = 入站状态.buffer.subarray(初始化最小长度);
入站状态.decryptKey = decryptKey;
入站状态.nonceCounter = nonceCounter;
入站状态.waitPayloadLength = payloadLength;
入站状态.加密配置 = 加密配置;
入站状态.hasSalt = true;
return true;
} catch (_) { }
}
}
const 初始化失败判定长度 = 最大盐长度 + lengthCipherTotalLength + 最大对齐扫描字节;
if (入站状态.buffer.byteLength >= 初始化失败判定长度) {
throw new Error(`SS handshake decrypt failed (enc=${请求加密方式 || 'auto'}, candidates=${入站候选加密配置.map(c => c.method).join('/')})`);
}
return false;
};
const 入站解密器 = {
async 输入(dataChunk) {
const chunk = 数据转Uint8Array(dataChunk);
if (chunk.byteLength > 0) 入站状态.buffer = 拼接字节数据(入站状态.buffer, chunk);
if (!入站状态.hasSalt) {
const 初始化成功 = await 初始化入站解密状态();
if (!初始化成功) return [];
}
const plaintextChunks = [];
while (true) {
if (入站状态.waitPayloadLength === null) {
const lengthCipherTotalLength = 2 + SSAEAD标签长度;
if (入站状态.buffer.byteLength < lengthCipherTotalLength) break;
const lengthCipher = 入站状态.buffer.subarray(0, lengthCipherTotalLength);
入站状态.buffer = 入站状态.buffer.subarray(lengthCipherTotalLength);
const lengthPlain = await SSAEAD解密(入站状态.decryptKey, 入站状态.nonceCounter, lengthCipher);
if (lengthPlain.byteLength !== 2) throw new Error('SS length decrypt failed');
const payloadLength = (lengthPlain[0] << 8) | lengthPlain[1];
if (payloadLength < 0 || payloadLength > 入站状态.加密配置.maxChunk) throw new Error(`SS payload length invalid: ${payloadLength}`);
入站状态.waitPayloadLength = payloadLength;
}
const payloadCipherTotalLength = 入站状态.waitPayloadLength + SSAEAD标签长度;
if (入站状态.buffer.byteLength < payloadCipherTotalLength) break;
const payloadCipher = 入站状态.buffer.subarray(0, payloadCipherTotalLength);
入站状态.buffer = 入站状态.buffer.subarray(payloadCipherTotalLength);
const payloadPlain = await SSAEAD解密(入站状态.decryptKey, 入站状态.nonceCounter, payloadCipher);
plaintextChunks.push(payloadPlain);
入站状态.waitPayloadLength = null;
}
return plaintextChunks;
},
};
let 出站加密器 = null;
const SS单批最大字节 = 32 * 1024;
const 获取出站加密器 = async () => {
if (出站加密器) return 出站加密器;
if (!入站状态.加密配置) throw new Error('SS cipher is not negotiated');
const 出站加密配置 = 入站状态.加密配置;
const 出站主密钥 = await SS派生主密钥(yourUUID, 出站加密配置.keyLen);
const 出站随机字节 = crypto.getRandomValues(new Uint8Array(出站加密配置.saltLen));
const 出站加密密钥 = await SS派生会话密钥(出站加密配置, 出站主密钥, 出站随机字节, ['encrypt']);
const 出站Nonce计数器 = new Uint8Array(SSNonce长度);
let 随机字节已发送 = false;
出站加密器 = {
async 加密并发送(dataChunk, sendChunk) {
const plaintextData = 数据转Uint8Array(dataChunk);
if (!随机字节已发送) {
await sendChunk(出站随机字节);
随机字节已发送 = true;
}
if (plaintextData.byteLength === 0) return;
let offset = 0;
while (offset < plaintextData.byteLength) {
const end = Math.min(offset + 出站加密配置.maxChunk, plaintextData.byteLength);
const payloadPlain = plaintextData.subarray(offset, end);
const lengthPlain = new Uint8Array(2);
lengthPlain[0] = (payloadPlain.byteLength >>> 8) & 0xff;
lengthPlain[1] = payloadPlain.byteLength & 0xff;
const lengthCipher = await SSAEAD加密(出站加密密钥, 出站Nonce计数器, lengthPlain);
const payloadCipher = await SSAEAD加密(出站加密密钥, 出站Nonce计数器, payloadPlain);
const frame = new Uint8Array(lengthCipher.byteLength + payloadCipher.byteLength);
frame.set(lengthCipher, 0);
frame.set(payloadCipher, lengthCipher.byteLength);
await sendChunk(frame);
offset = end;
}
},
};
return 出站加密器;
};
let SS发送队列 = Promise.resolve();
const SS入队发送 = (chunk) => {
SS发送队列 = SS发送队列.then(async () => {
if (serverSock.readyState !== WebSocket.OPEN) return;
const 已初始化出站加密器 = await 获取出站加密器();
await 已初始化出站加密器.加密并发送(chunk, async (encryptedChunk) => {
if (encryptedChunk.byteLength > 0 && serverSock.readyState === WebSocket.OPEN) {
await WebSocket发送并等待(serverSock, encryptedChunk.buffer);
}
});
}).catch((error) => {
log(`[SS发送] 加密失败: ${error?.message || error}`);
closeSocketQuietly(serverSock);
});
return SS发送队列;
};
const 回包Socket = {
get readyState() {
return serverSock.readyState;
},
send(data) {
const chunk = 数据转Uint8Array(data);
if (chunk.byteLength <= SS单批最大字节) {
return SS入队发送(chunk);
}
for (let i = 0; i < chunk.byteLength; i += SS单批最大字节) {
SS入队发送(chunk.subarray(i, Math.min(i + SS单批最大字节, chunk.byteLength)));
}
return SS发送队列;
},
close() {
closeSocketQuietly(serverSock);
}
};
ss上下文 = {
入站解密器,
回包Socket,
首包已建立: false,
目标主机: '',
目标端口: 0,
};
return ss上下文;
})().finally(() => { ss初始化任务 = null });
}
return ss初始化任务;
};
const 处理SS数据 = async (chunk) => {
const 上下文 = await 获取SS上下文();
let 明文块数组 = null;
try {
明文块数组 = await 上下文.入站解密器.输入(chunk);
} catch (err) {
const msg = err?.message || `${err}`;
if (msg.includes('Decryption failed') || msg.includes('SS handshake decrypt failed') || msg.includes('SS length decrypt failed')) {
log(`[SS入站] 解密失败,连接关闭: ${msg}`);
closeSocketQuietly(serverSock);
return;
}
throw err;
}
for (const 明文块 of 明文块数组) {
if (WS本地测速模式) {
await 处理WS本地测速数据(明文块);
continue;
}
let 已写入 = false;
try {
已写入 = await 写入远端(明文块, false);
} catch (err) {
if ((/** @type {any} */ (err))?.isQueueOverflow) throw err;
已写入 = false;
}
if (已写入) continue;
if (上下文.首包已建立 && 上下文.目标主机 && 上下文.目标端口 > 0) {
await forwardataTCP(上下文.目标主机, 上下文.目标端口, 明文块, 上下文.回包Socket, null, remoteConnWrapper, yourUUID, request, 反代上下文);
continue;
}
const 明文数据 = 数据转Uint8Array(明文块);
if (明文数据.byteLength < 3) throw new Error('invalid ss data');
const addressType = 明文数据[0];
let cursor = 1;
let hostname = '';
if (addressType === 1) {
if (明文数据.byteLength < cursor + 4 + 2) throw new Error('invalid ss ipv4 length');
hostname = `${明文数据[cursor]}.${明文数据[cursor + 1]}.${明文数据[cursor + 2]}.${明文数据[cursor + 3]}`;
cursor += 4;
} else if (addressType === 3) {
if (明文数据.byteLength < cursor + 1) throw new Error('invalid ss domain length');
const domainLength = 明文数据[cursor];
cursor += 1;
if (明文数据.byteLength < cursor + domainLength + 2) throw new Error('invalid ss domain data');
hostname = SS文本解码器.decode(明文数据.subarray(cursor, cursor + domainLength));
cursor += domainLength;
} else if (addressType === 4) {
if (明文数据.byteLength < cursor + 16 + 2) throw new Error('invalid ss ipv6 length');
const ipv6 = [];
const ipv6View = new DataView(明文数据.buffer, 明文数据.byteOffset + cursor, 16);
for (let i = 0; i < 8; i++) ipv6.push(ipv6View.getUint16(i * 2).toString(16));
hostname = ipv6.join(':');
cursor += 16;
} else {
throw new Error(`invalid ss addressType: ${addressType}`);
}
if (!hostname) throw new Error(`invalid ss address: ${addressType}`);
const port = (明文数据[cursor] << 8) | 明文数据[cursor + 1];
cursor += 2;
const rawClientData = 明文数据.subarray(cursor);
if (isSpeedTestSite(hostname) && 反代上下文.代理类型 === null) {
await 启用WS本地测速模式(上下文.回包Socket, null, rawClientData);
return;
}
上下文.首包已建立 = true;
上下文.目标主机 = hostname;
上下文.目标端口 = port;
await forwardataTCP(hostname, port, rawClientData, 上下文.回包Socket, null, remoteConnWrapper, yourUUID, request, 反代上下文);
}
};
const 处理WS入站数据 = async (chunk) => {
let 当前块字节 = null;
if (isDnsQuery) {
if (判断是否是木马) return await 转发木马UDP数据(chunk, serverSock, 木马UDP上下文, request);
return await forwardataudp(chunk, serverSock, null, request);
}
if (判断协议类型 === 'ss') {
await 处理SS数据(chunk);
return;
}
if (WS本地测速模式) {
await 处理WS本地测速数据(chunk);
return;
}
if (await 写入远端(chunk)) return;
if (判断协议类型 === null) {
if (url.searchParams.get('enc')) 判断协议类型 = 'ss';
else {
当前块字节 = 当前块字节 || 数据转Uint8Array(chunk);
const bytes = 当前块字节;
判断协议类型 = bytes.byteLength >= 58 && bytes[56] === 0x0d && bytes[57] === 0x0a ? '木马' : '魏烈思';
}
判断是否是木马 = 判断协议类型 === '木马';
log(`[WS转发] 协议类型: ${判断协议类型} | 来自: ${url.host} | UA: ${request.headers.get('user-agent') || '未知'}`);
}
if (判断协议类型 === 'ss') {
await 处理SS数据(chunk);
return;
}
if (await 写入远端(chunk)) return;
if (判断协议类型 === '木马') {
const 解析结果 = 解析木马请求(chunk, yourUUID);
if (解析结果?.hasError) throw new Error(解析结果.message || 'Invalid trojan request');
const { port, hostname, rawClientData, isUDP } = 解析结果;
if (isSpeedTestSite(hostname) && 反代上下文.代理类型 === null) {
await 启用WS本地测速模式(serverSock, null, rawClientData);
return;
}
if (isUDP) {
isDnsQuery = true;
木马UDP上下文.目标主机 = hostname;
木马UDP上下文.目标端口 = port;
if (木马UDP上下文.反代地址) return 转发木马UDP数据(当前块字节 || 数据转Uint8Array(chunk), serverSock, 木马UDP上下文, request);
if (有效数据长度(rawClientData) > 0) return 转发木马UDP数据(rawClientData, serverSock, 木马UDP上下文, request);
return;
}
await forwardataTCP(hostname, port, rawClientData, serverSock, null, remoteConnWrapper, yourUUID, request, 反代上下文, true, 当前块字节 || 数据转Uint8Array(chunk));
} else {
判断是否是木马 = false;
当前块字节 = 当前块字节 || 数据转Uint8Array(chunk);
const bytes = 当前块字节;
const 解析结果 = 解析魏烈思请求(bytes, yourUUID);
if (解析结果?.hasError) throw new Error(解析结果.message || 'Invalid 魏烈思 request');
const { port, hostname, version, isUDP, rawClientData } = 解析结果;
const respHeader = new Uint8Array([version, 0]);
if (isSpeedTestSite(hostname) && 反代上下文.代理类型 === null) {
await 启用WS本地测速模式(serverSock, respHeader, rawClientData);
return;
}
if (isUDP) {
if (port === 53) isDnsQuery = true;
else throw new Error('UDP is not supported');
}
const rawData = rawClientData;
if (isDnsQuery) {
if (判断是否是木马) return 转发木马UDP数据(rawData, serverSock, 木马UDP上下文, request);
return forwardataudp(rawData, serverSock, respHeader, request);
}
await forwardataTCP(hostname, port, rawData, serverSock, respHeader, remoteConnWrapper, yourUUID, request, 反代上下文);
}
};
const 处理WS显式传输错误 = (err) => {
if (WS显式传输失败) return;
WS显式传输失败 = true;
WS显式传输停止接收 = true;
WS显式队列字节 = 0;
WS显式队列条目 = 0;
const msg = err?.message || `${err}`;
if (msg.includes('Network connection lost') || msg.includes('ReadableStream is closed')) {
log(`[WS转发] 连接结束: ${msg}`);
} else {
log(`[WS转发] 处理失败: ${msg}`);
}
上行写入队列.清空();
释放远端写入器();
失效远端连接();
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
closeSocketQuietly(serverSock);
};
const 追加WS显式传输任务 = (任务) => {
WS显式传输链 = WS显式传输链.then(任务).catch(处理WS显式传输错误);
return WS显式传输链;
};
const 入队WS显式传输 = (data) => {
if (WS显式传输停止接收 || WS显式传输失败) return;
const chunkSize = Math.max(0, 有效数据长度(data));
const nextBytes = WS显式队列字节 + chunkSize;
const nextItems = WS显式队列条目 + 1;
if (nextBytes > 上行队列最大字节 || nextItems > 上行队列最大条目) {
处理WS显式传输错误(new Error(`[WS显式传输] 队列溢出: ${nextBytes}B/${nextItems}`));
return;
}
WS显式队列字节 = nextBytes;
WS显式队列条目 = nextItems;
追加WS显式传输任务(async () => {
WS显式队列字节 = Math.max(0, WS显式队列字节 - chunkSize);
WS显式队列条目 = Math.max(0, WS显式队列条目 - 1);
if (WS显式传输失败) return;
await 处理WS入站数据(data);
});
};
const 收尾WS显式传输 = () => {
if (WS显式传输收尾已入队) return;
WS显式传输收尾已入队 = true;
WS显式传输停止接收 = true;
追加WS显式传输任务(async () => {
if (WS显式传输失败) return;
await 上行写入队列.等待空();
释放远端写入器();
失效远端连接();
try { 木马UDP上下文.反代Socket?.close() } catch (e) { }
});
};
serverSock.addEventListener('message', (event) => {
入队WS显式传输(event.data);
});
serverSock.addEventListener('close', () => {
closeSocketQuietly(serverSock);
收尾WS显式传输();
});
serverSock.addEventListener('error', (err) => {
处理WS显式传输错误(err);
});
// SS 模式下禁用 sec-websocket-protocol early-data,避免把子协议值(如 "binary")误当作 base64 数据注入首包导致 AEAD 解密失败。
if (!SS模式禁用EarlyData && earlyDataHeader) {
try {
const bytes = 解码WS早期数据(earlyDataHeader, yourUUID);
if (bytes?.byteLength) 入队WS显式传输(bytes.buffer);
} catch (error) {
处理WS显式传输错误(error);
}
}
return new Response(null, { status: 101, webSocket: clientSock, headers: { 'Sec-WebSocket-Extensions': '' } });
}
const 木马文本解码器 = new TextDecoder();
function 解析木马反代地址(address) {
const raw = String(address || '').trim();
if (!raw || raw.includes('/') || raw.includes('@') || raw.includes('://')) throw new Error('木马反代仅支持 host:port');
let hostname = '', portText = '';
if (raw.startsWith('[')) {
const 匹配 = raw.match(/^(\[[^\]]+\]):(\d+)$/);
if (!匹配) throw new Error('无效的 IPv6 木马反代地址');
hostname = 匹配[1];
portText = 匹配[2];
} else {
const parts = raw.split(':');
if (parts.length !== 2) throw new Error('木马反代仅支持 host:port');
hostname = parts[0];
portText = parts[1];
}
const port = Number(portText);
if (!hostname || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error('无效的木马反代端口');
return { hostname, port };
}
async function 连接木马反代(首包数据, TCP连接, 木马反代目标) {
if (!木马反代目标) throw new Error('trojan fallback is not configured');
const socket = TCP连接({ hostname: stripIPv6Brackets(木马反代目标.hostname), port: 木马反代目标.port });
let writer = null;
try {
if (socket.opened) await socket.opened;
if (有效数据长度(首包数据) > 0) {
writer = socket.writable.getWriter();
await writer.write(数据转Uint8Array(首包数据));
}
return socket;
} catch (error) {
try { socket?.close?.() } catch (e) { }
throw error;
} finally {
try { writer?.releaseLock() } catch (e) { }
}
}
function 提取木马反代握手数据(首包数据, rawData) {
const 首包 = 数据转Uint8Array(首包数据);
const payload = 数据转Uint8Array(rawData);
if (!payload.byteLength) return 首包;
const 握手长度 = 首包.byteLength - payload.byteLength;
if (握手长度 <= 0) return 首包;
for (let i = 0; i < payload.byteLength; i++) {
if (首包[握手长度 + i] !== payload[i]) return 首包;
}
return 首包.subarray(0, 握手长度);
}
async function 转发木马UDP反代数据(chunk, webSocket, 上下文, request) {
const data = 数据转Uint8Array(chunk);
if (!上下文.反代Socket) {
const TCP连接 = 创建请求TCP连接器(request);
const socket = await 连接木马反代(data, TCP连接, 上下文.反代地址);
上下文.反代Socket = socket;
socket.closed.catch(() => { }).finally(() => closeSocketQuietly(webSocket));
connectStreams(socket, webSocket, null, null);
return;
}
if (!data.byteLength) return;
const writer = 上下文.反代Socket.writable.getWriter();
try { await writer.write(data) }
finally { try { writer.releaseLock() } catch (e) { } }
}
function 解析木马请求(buffer, passwordPlainText) {
const data = 数据转Uint8Array(buffer);
const sha224Password = sha224(passwordPlainText);
if (data.byteLength < 58) return { hasError: true, message: "invalid data" };
let crLfIndex = 56;
if (data[crLfIndex] !== 0x0d || data[crLfIndex + 1] !== 0x0a) return { hasError: true, message: "invalid header format" };
for (let i = 0; i < crLfIndex; i++) {
if (data[i] !== sha224Password.charCodeAt(i)) return { hasError: true, message: "invalid password" };
}
const socks5Index = crLfIndex + 2;
if (data.byteLength < socks5Index + 6) return { hasError: true, message: "invalid S5 request data" };
const cmd = data[socks5Index];
if (cmd !== 1 && cmd !== 3) return { hasError: true, message: "unsupported command, only TCP/UDP is allowed" };
const isUDP = cmd === 3;
const atype = data[socks5Index + 1];
let addressLength = 0;
let addressIndex = socks5Index + 2;
let address = "";
switch (atype) {
case 1: // IPv4
addressLength = 4;
if (data.byteLength < addressIndex + addressLength + 4) return { hasError: true, message: "invalid S5 request data" };
address = `${data[addressIndex]}.${data[addressIndex + 1]}.${data[addressIndex + 2]}.${data[addressIndex + 3]}`;
break;
case 3: // Domain
if (data.byteLength < addressIndex + 1) return { hasError: true, message: "invalid S5 request data" };
addressLength = data[addressIndex];
addressIndex += 1;
if (data.byteLength < addressIndex + addressLength + 4) return { hasError: true, message: "invalid S5 request data" };
address = 木马文本解码器.decode(data.subarray(addressIndex, addressIndex + addressLength));
break;
case 4: // IPv6
addressLength = 16;
if (data.byteLength < addressIndex + addressLength + 4) return { hasError: true, message: "invalid S5 request data" };
const ipv6 = [];
for (let i = 0; i < 8; i++) {
const partIndex = addressIndex + i * 2;
ipv6.push(((data[partIndex] << 8) | data[partIndex + 1]).toString(16));
}
address = ipv6.join(":");
break;
default:
return { hasError: true, message: `invalid addressType is ${atype}` };
}
if (!address) {
return { hasError: true, message: `address is empty, addressType is ${atype}` };
}
const portIndex = addressIndex + addressLength;
if (data.byteLength < portIndex + 4) return { hasError: true, message: "invalid S5 request data" };
const portRemote = (data[portIndex] << 8) | data[portIndex + 1];
return {
hasError: false,
addressType: atype,
port: portRemote,
hostname: address,
isUDP,
rawClientData: data.subarray(portIndex + 4)
};
}
const UUID字节缓存 = new Map();
const VLESS文本解码器 = new TextDecoder();
function 读取十六进制半字节(code) {
if (code >= 48 && code <= 57) return code - 48;
code |= 32;
if (code >= 97 && code <= 102) return code - 87;
return -1;
}
function 获取UUID字节(uuid) {
const key = String(uuid || '');
let cached = UUID字节缓存.get(key);
if (cached) return cached;
const clean = key.replace(/-/g, '');
if (clean.length !== 32) return null;
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
const high = 读取十六进制半字节(clean.charCodeAt(i * 2));
const low = 读取十六进制半字节(clean.charCodeAt(i * 2 + 1));
if (high < 0 || low < 0) return null;
bytes[i] = (high << 4) | low;
}
if (UUID字节缓存.size >= 32) UUID字节缓存.clear();
UUID字节缓存.set(key, bytes);
return bytes;
}
function UUID字节匹配(data, offset, uuid) {
const expected = 获取UUID字节(uuid);
if (!expected || data.byteLength < offset + 16) return false;
for (let i = 0; i < 16; i++) {
if (data[offset + i] !== expected[i]) return false;
}
return true;
}
function 解析魏烈思请求(chunk, token) {
const data = 数据转Uint8Array(chunk);
const length = data.byteLength;
if (length < 24) return { hasError: true, message: 'Invalid data' };
const version = data[0];
if (!UUID字节匹配(data, 1, token)) return { hasError: true, message: 'Invalid uuid' };
const optLen = data[17];
const cmdIndex = 18 + optLen;
if (length < cmdIndex + 4) return { hasError: true, message: 'Invalid data' };
const cmd = data[cmdIndex];
let isUDP = false;
if (cmd === 1) { } else if (cmd === 2) { isUDP = true } else { return { hasError: true, message: 'Invalid command' } }
const portIdx = cmdIndex + 1;
const port = (data[portIdx] << 8) | data[portIdx + 1];
let addrValIdx = portIdx + 3, addrLen = 0, hostname = '';
const addressType = data[portIdx + 2];
switch (addressType) {
case 1:
addrLen = 4;
if (length < addrValIdx + addrLen) return { hasError: true, message: 'Invalid IPv4 address length' };
hostname = `${data[addrValIdx]}.${data[addrValIdx + 1]}.${data[addrValIdx + 2]}.${data[addrValIdx + 3]}`;
break;
case 2:
if (length < addrValIdx + 1) return { hasError: true, message: 'Invalid domain length' };
addrLen = data[addrValIdx];
addrValIdx += 1;
if (length < addrValIdx + addrLen) return { hasError: true, message: 'Invalid domain data' };
hostname = VLESS文本解码器.decode(data.subarray(addrValIdx, addrValIdx + addrLen));
break;
case 3:
addrLen = 16;
if (length < addrValIdx + addrLen) return { hasError: true, message: 'Invalid IPv6 address length' };
const ipv6 = [];
for (let i = 0; i < 8; i++) {
const base = addrValIdx + i * 2;
ipv6.push(((data[base] << 8) | data[base + 1]).toString(16));
}
hostname = ipv6.join(':');
break;
default:
return { hasError: true, message: `Invalid address type: ${addressType}` };
}
if (!hostname) return { hasError: true, message: `Invalid address: ${addressType}` };
const rawIndex = addrValIdx + addrLen;
return { hasError: false, addressType, port, hostname, isUDP, rawClientData: data.subarray(rawIndex), version };
}
const SS支持加密配置 = {
'aes-128-gcm': { method: 'aes-128-gcm', keyLen: 16, saltLen: 16, maxChunk: 0x3fff, aesLength: 128 },
'aes-256-gcm': { method: 'aes-256-gcm', keyLen: 32, saltLen: 32, maxChunk: 0x3fff, aesLength: 256 },
};
const SSAEAD标签长度 = 16, SSNonce长度 = 12;
const SS子密钥信息 = new TextEncoder().encode('ss-subkey');
const SS文本编码器 = new TextEncoder(), SS文本解码器 = new TextDecoder(), SS主密钥缓存 = new Map();
function 数据转Uint8Array(data) {
if (data instanceof Uint8Array) return data;
if (data instanceof ArrayBuffer) return new Uint8Array(data);
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
return new Uint8Array(data || 0);
}
function 拼接字节数据(...chunkList) {
if (!chunkList || chunkList.length === 0) return new Uint8Array(0);
const chunks = chunkList.map(数据转Uint8Array);
const total = chunks.reduce((sum, c) => sum + c.byteLength, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const c of chunks) { result.set(c, offset); offset += c.byteLength }
return result;
}
async function 转发木马UDP数据(chunk, webSocket, 上下文, request) {
const 当前块 = 数据转Uint8Array(chunk);
if (上下文?.反代地址) return 转发木马UDP反代数据(当前块, webSocket, 上下文, request);
const 缓存块 = 上下文?.缓存 instanceof Uint8Array ? 上下文.缓存 : new Uint8Array(0);
const input = 缓存块.byteLength ? 拼接字节数据(缓存块, 当前块) : 当前块;
let cursor = 0;
while (cursor < input.byteLength) {
const packetStart = cursor;
const atype = input[cursor];
let addrCursor = cursor + 1;
let addrLen = 0;
if (atype === 1) addrLen = 4;
else if (atype === 4) addrLen = 16;
else if (atype === 3) {
if (input.byteLength < addrCursor + 1) break;
addrLen = 1 + input[addrCursor];
} else throw new Error(`invalid trojan udp addressType: ${atype}`);
const portCursor = addrCursor + addrLen;
if (input.byteLength < portCursor + 6) break;
const port = (input[portCursor] << 8) | input[portCursor + 1];
const payloadLength = (input[portCursor + 2] << 8) | input[portCursor + 3];
if (input[portCursor + 4] !== 0x0d || input[portCursor + 5] !== 0x0a) throw new Error('invalid trojan udp delimiter');
const payloadStart = portCursor + 6;
const payloadEnd = payloadStart + payloadLength;
if (input.byteLength < payloadEnd) break;
const 地址端口头 = input.slice(packetStart, portCursor + 2);
const payload = input.slice(payloadStart, payloadEnd);
cursor = payloadEnd;
if (port !== 53) throw new Error('UDP is not supported');
if (!payload.byteLength) continue;
let tcpDNS查询 = payload;
if (payload.byteLength < 2 || ((payload[0] << 8) | payload[1]) !== payload.byteLength - 2) {
tcpDNS查询 = new Uint8Array(payload.byteLength + 2);
tcpDNS查询[0] = (payload.byteLength >>> 8) & 0xff;
tcpDNS查询[1] = payload.byteLength & 0xff;
tcpDNS查询.set(payload, 2);
}
const dns响应上下文 = { 缓存: new Uint8Array(0) };
await forwardataudp(tcpDNS查询, webSocket, null, request, (dnsRespChunk) => {
const 当前响应块 = 数据转Uint8Array(dnsRespChunk);
const 响应输入 = dns响应上下文.缓存.byteLength ? 拼接字节数据(dns响应上下文.缓存, 当前响应块) : 当前响应块;
const 响应帧列表 = [];
let responseCursor = 0;
while (responseCursor + 2 <= 响应输入.byteLength) {
const dnsLen = (响应输入[responseCursor] << 8) | 响应输入[responseCursor + 1];
const dnsStart = responseCursor + 2;
const dnsEnd = dnsStart + dnsLen;
if (dnsEnd > 响应输入.byteLength) break;
const dnsPayload = 响应输入.slice(dnsStart, dnsEnd);
const frame = new Uint8Array(地址端口头.byteLength + 4 + dnsPayload.byteLength);
frame.set(地址端口头, 0);
frame[地址端口头.byteLength] = (dnsPayload.byteLength >>> 8) & 0xff;
frame[地址端口头.byteLength + 1] = dnsPayload.byteLength & 0xff;
frame[地址端口头.byteLength + 2] = 0x0d;
frame[地址端口头.byteLength + 3] = 0x0a;
frame.set(dnsPayload, 地址端口头.byteLength + 4);
响应帧列表.push(frame);
responseCursor = dnsEnd;
}
dns响应上下文.缓存 = 响应输入.slice(responseCursor);
return 响应帧列表.length ? 响应帧列表 : new Uint8Array(0);
});
}
if (上下文) 上下文.缓存 = input.slice(cursor);
}
function SS递增Nonce计数器(counter) {
for (let i = 0; i < counter.length; i++) { counter[i] = (counter[i] + 1) & 0xff; if (counter[i] !== 0) return }
}
async function SS派生主密钥(passwordText, keyLen) {
const cacheKey = `${keyLen}:${passwordText}`;
if (SS主密钥缓存.has(cacheKey)) return SS主密钥缓存.get(cacheKey);
const deriveTask = (async () => {
const pwBytes = SS文本编码器.encode(passwordText || '');
let prev = new Uint8Array(0), result = new Uint8Array(0);
while (result.byteLength < keyLen) {
const input = new Uint8Array(prev.byteLength + pwBytes.byteLength);
input.set(prev, 0); input.set(pwBytes, prev.byteLength);
prev = new Uint8Array(await crypto.subtle.digest('MD5', input));
result = 拼接字节数据(result, prev);
}
return result.slice(0, keyLen);
})();
SS主密钥缓存.set(cacheKey, deriveTask);
try { return await deriveTask }
catch (error) { SS主密钥缓存.delete(cacheKey); throw error }
}
async function SS派生会话密钥(config, masterKey, salt, usages) {
const hmacOpts = { name: 'HMAC', hash: 'SHA-1' };
const saltHmacKey = await crypto.subtle.importKey('raw', salt, hmacOpts, false, ['sign']);
const prk = new Uint8Array(await crypto.subtle.sign('HMAC', saltHmacKey, masterKey));
const prkHmacKey = await crypto.subtle.importKey('raw', prk, hmacOpts, false, ['sign']);
const subKey = new Uint8Array(config.keyLen);
let prev = new Uint8Array(0), written = 0, counter = 1;
while (written < config.keyLen) {
const input = 拼接字节数据(prev, SS子密钥信息, new Uint8Array([counter]));
prev = new Uint8Array(await crypto.subtle.sign('HMAC', prkHmacKey, input));
const copyLen = Math.min(prev.byteLength, config.keyLen - written);
subKey.set(prev.subarray(0, copyLen), written);
written += copyLen; counter += 1;
}
return crypto.subtle.importKey('raw', subKey, { name: 'AES-GCM', length: config.aesLength }, false, usages);
}
async function SSAEAD加密(cryptoKey, nonceCounter, plaintext) {
const iv = nonceCounter.slice();
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, cryptoKey, plaintext);
SS递增Nonce计数器(nonceCounter);
return new Uint8Array(ct);
}
async function SSAEAD解密(cryptoKey, nonceCounter, ciphertext) {
const iv = nonceCounter.slice();
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv, tagLength: 128 }, cryptoKey, ciphertext);
SS递增Nonce计数器(nonceCounter);
return new Uint8Array(pt);
}
async function forwardataTCP(host, portNum, rawData, ws, respHeader, remoteConnWrapper, yourUUID, request = null, 反代上下文 = {}, 允许木马反代 = false, 木马反代首包数据 = null, 仅建立连接 = false) {
const ctx反代IP = 反代上下文.反代IP || '';
const ctx代理类型 = 反代上下文.代理类型 !== undefined ? 反代上下文.代理类型 : null;
const ctx代理全局 = 反代上下文.代理全局 !== undefined ? 反代上下文.代理全局 : false;
const ctx代理参数 = 反代上下文.代理参数 || {};
const ctx反代兜底 = 反代上下文.反代兜底 !== undefined ? 反代上下文.反代兜底 : true;
let 反代数组索引 = 0;
log(`[TCP转发] 目标: ${host}:${portNum} | 反代IP: ${ctx反代IP} | 反代兜底: ${ctx反代兜底 ? '是' : '否'} | 反代类型: ${ctx代理类型 || 'proxyip'} | 全局: ${ctx代理全局 ? '是' : '否'}`);
const 连接超时毫秒 = 1000;
let 已通过代理发送首包 = false;
const TCP连接 = 创建请求TCP连接器(request);
const 使用木马反代 = 允许木马反代 && (反代上下文.木马反代地址 || null);
const 木马反代目标 = 使用木马反代 ? 反代上下文.木马反代地址 : null;
const 木马反代握手数据 = 使用木马反代 ? 提取木马反代握手数据(木马反代首包数据, rawData) : null;
let 待发送响应头 = respHeader;
const 取出响应头 = () => {
const header = 待发送响应头;
待发送响应头 = null;
return header;
};
if (!Number.isInteger(remoteConnWrapper.generation)) remoteConnWrapper.generation = 0;
const 安装当前连接 = async (socket, generation, downlinkDrain, retryFunc = null) => {
try { await downlinkDrain } catch (e) {
if (remoteConnWrapper.downlinkDrain === downlinkDrain) remoteConnWrapper.downlinkDrain = Promise.resolve();
try { socket?.close?.() } catch (_) { }
if (remoteConnWrapper.generation === generation) closeSocketQuietly(ws);
throw e;
}
if (remoteConnWrapper.downlinkDrain === downlinkDrain) remoteConnWrapper.downlinkDrain = Promise.resolve();
const 连接仍有效 = () => remoteConnWrapper.generation === generation && remoteConnWrapper.socket === socket;
if (remoteConnWrapper.generation !== generation || ws.readyState !== WebSocket.OPEN) {
try { socket?.close?.() } catch (e) { }
if (remoteConnWrapper.generation === generation) remoteConnWrapper.socket = null;
throw new Error('connection superseded or client closed');
}
remoteConnWrapper.socket = socket;
if (仅建立连接) return socket;
connectStreams(socket, ws, 取出响应头, retryFunc, 连接仍有效, remoteConnWrapper).catch(err => {
if (!连接仍有效()) return;
log(`[TCP下行] 处理失败: ${err?.message || err}`);
try { socket?.close?.() } catch (e) { }
closeSocketQuietly(ws);
});
return true;
};
async function 等待连接建立(remoteSock, timeoutMs = 连接超时毫秒) {
await Promise.race([
remoteSock.opened,
new Promise((_, reject) => setTimeout(() => reject(new Error('连接超时')), timeoutMs))
]);
}
async function 打开TCP连接(address, port) {
const remoteSock = TCP连接({ hostname: address, port });
try {
await 等待连接建立(remoteSock);
return remoteSock;
} catch (err) {
try { remoteSock?.close?.() } catch (e) { }
throw err;
}
}
async function 写入首包(remoteSock, data) {
if (有效数据长度(data) <= 0) return;
const writer = remoteSock.writable.getWriter();
try { await writer.write(数据转Uint8Array(data)) }
finally { try { writer.releaseLock() } catch (e) { } }
}
async function 并发打开候选连接(候选列表) {
if (候选列表.length === 1) {
const 候选 = 候选列表[0];
return { socket: await 打开TCP连接(候选.hostname, 候选.port), candidate: 候选 };
}
const attempts = 候选列表.map(候选 => 打开TCP连接(候选.hostname, 候选.port).then(socket => ({ socket, candidate: 候选 })));
let winner = null;
try {
winner = await Promise.any(attempts);
return winner;
} finally {
if (winner) {
for (const attempt of attempts) {
attempt.then(({ socket }) => {
if (socket !== winner.socket) {
try { socket?.close?.() } catch (e) { }
}
}).catch(() => { });
}
}
}
}
async function 构建预加载竞速候选列表(address, port) {
if (!预加载竞速拨号 || isIPHostname(address)) return null;
log(`[TCP直连] 预加载竞速拨号开启,开始并发查询 ${address} 的 A/AAAA 记录`);
const [aRecords, aaaaRecords] = await Promise.all([
DoH查询(address, 'A'),
DoH查询(address, 'AAAA')
]);
const ipv4List = [...new Set(aRecords.flatMap(r => {
const data = r.data;
return r.type === 1 && typeof data === 'string' && isIPv4(data) ? [data] : [];
}))];
const ipv6List = [...new Set(aaaaRecords.flatMap(r => {
const data = r.data;
return r.type === 28 && typeof data === 'string' && isIPHostname(data) ? [data] : [];
}))];
const 拨号上限 = Math.max(1, TCP并发拨号数 | 0);
const ipList = ipv4List.length >= 拨号上限
? ipv4List.slice(0, 拨号上限)
: ipv4List.concat(ipv6List.slice(0, 拨号上限 - ipv4List.length));
const 使用记录类型 = ipv4List.length > 0
? (ipList.length > ipv4List.length ? 'A+AAAA' : 'A')
: 'AAAA';
if (ipList.length === 0) {
log(`[TCP直连] ${address} 的 A/AAAA 未获得可用解析结果,预加载竞速不可用,回退到原始 hostname 直连。`);
return null;
}
const 选中IP列表 = ipList;
log(`[TCP直连] ${address} A记录:${ipv4List.length} AAAA记录:${ipv6List.length},使用${使用记录类型}记录,竞速拨号 ${选中IP列表.length}/${拨号上限}: ${选中IP列表.join(', ')}`);
return 选中IP列表.map((hostname, attempt) => ({ hostname, port, attempt, resolvedFrom: address }));
}
async function connectDirect(address, port, data = null, 启用预加载 = false) {
const 预加载候选列表 = 启用预加载 ? await 构建预加载竞速候选列表(address, port) : null;
const 候选列表 = 预加载候选列表 || Array.from({ length: TCP并发拨号数 }, (_, attempt) => ({ hostname: address, port, attempt }));
log(预加载候选列表
? `[TCP直连] 并发尝试 ${候选列表.length} 路: ${候选列表.map(候选 => `${候选.hostname}:${候选.port}`).join(', ')}`
: `[TCP直连] 并发尝试 ${候选列表.length} 路: ${address}:${port}`);
let socket = null;
try {
const 连接结果 = await 并发打开候选连接(候选列表);
socket = 连接结果.socket;
if (预加载候选列表) {
const winner = 连接结果.candidate;
log(`[TCP直连] 预加载竞速结果: ${winner.hostname}:${winner.port} 胜出,源域名: ${winner.resolvedFrom || address}`);
}
await 写入首包(socket, data);
return socket;
} catch (err) {
try { socket?.close?.() } catch (e) { }
if (预加载候选列表) log(`[TCP直连] 预加载竞速失败: ${err.message || err}`);
throw err;
}
}
async function connectProxyIP(address, port, data = null, 所有反代数组 = null, 启用反代失败兜底 = true) {
if (所有反代数组 && 所有反代数组.length > 0) {
const 实际并发数 = Math.max(1, Math.floor(Number(反代并发拨号数) || 1));
for (let i = 0; i < 所有反代数组.length; i += 实际并发数) {
const 候选列表 = [];
for (let j = 0; j < 实际并发数 && i + j < 所有反代数组.length; j++) {
const 索引 = (反代数组索引 + i + j) % 所有反代数组.length;
const [反代地址, 反代端口] = 所有反代数组[索引];
候选列表.push({ hostname: 反代地址, port: 反代端口, index: 索引 });
}
let socket = null, candidate = null;
try {
log(`[反代连接] 并发尝试 ${候选列表.length} 路: ${候选列表.map(候选 => `${候选.hostname}:${候选.port}`).join(', ')}`);
const 连接结果 = await 并发打开候选连接(候选列表);
socket = 连接结果.socket;
candidate = 连接结果.candidate;
await 写入首包(socket, data);
log(`[反代连接] 成功连接到: ${candidate.hostname}:${candidate.port} (索引: ${candidate.index})`);
反代数组索引 = candidate.index;
return socket;
} catch (err) {
try { socket?.close?.() } catch (e) { }
log(`[反代连接] 本批连接失败: ${err.message || err}`);
}
}
}
if (启用反代失败兜底) return connectDirect(address, port, data, false);
else {
throw new Error('[反代连接] 所有反代连接失败,且未启用反代兜底,连接终止。');
}
}
async function connecttoPry(允许发送首包 = true) {
if (remoteConnWrapper.connectingPromise) {
await remoteConnWrapper.connectingPromise;
return;
}
const { generation: 当前连接世代, downlinkDrain } = 开始TCP连接世代(remoteConnWrapper);
let 本次发送首包 = false, 本次首包数据 = null;
if (使用木马反代) {
if (允许发送首包 && !已通过代理发送首包 && 有效数据长度(木马反代首包数据) > 0) {
本次首包数据 = 木马反代首包数据;
本次发送首包 = 有效数据长度(rawData) > 0;
} else {
本次首包数据 = 木马反代握手数据;
}
} else {
本次发送首包 = 允许发送首包 && !已通过代理发送首包 && 有效数据长度(rawData) > 0;
本次首包数据 = 本次发送首包 ? rawData : null;
}
const 当前连接任务 = (async () => {
let newSocket = null;
try {
if (使用木马反代) {
log(`[木马反代] 代理到: ${host}:${portNum}`);
newSocket = await 连接木马反代(本次首包数据, TCP连接, 木马反代目标);
} else if (ctx代理类型 === 'socks5') {
log(`[SOCKS5代理] 代理到: ${host}:${portNum}`);
newSocket = await socks5Connect(host, portNum, 本次首包数据, TCP连接, ctx代理参数);
} else if (ctx代理类型 === 'http') {
log(`[HTTP代理] 代理到: ${host}:${portNum}`);
newSocket = await httpConnect(host, portNum, 本次首包数据, false, TCP连接, ctx代理参数);
} else if (ctx代理类型 === 'https') {
log(`[HTTPS代理] 代理到: ${host}:${portNum}`);
newSocket = isIPHostname(ctx代理参数.hostname)
? await httpsConnect(host, portNum, 本次首包数据, TCP连接, ctx代理参数)
: await httpConnect(host, portNum, 本次首包数据, true, TCP连接, ctx代理参数);
} else if (ctx代理类型 === 'turn') {
log(`[TURN代理] 代理到: ${host}:${portNum}`);
newSocket = await turnConnect(ctx代理参数, host, portNum, TCP连接);
if (有效数据长度(本次首包数据) > 0) {
const writer = newSocket.writable.getWriter();
try { await writer.write(数据转Uint8Array(本次首包数据)) }
finally { try { writer.releaseLock() } catch (e) { } }
}
} else if (ctx代理类型 === 'sstp') {
log(`[SSTP代理] 代理到: ${host}:${portNum}`);
newSocket = await sstpConnect(ctx代理参数, host, portNum, TCP连接);
if (有效数据长度(本次首包数据) > 0) {
const writer = newSocket.writable.getWriter();
try { await writer.write(数据转Uint8Array(本次首包数据)) }
finally { try { writer.releaseLock() } catch (e) { } }
}
} else {
log(`[反代连接] 代理到: ${host}:${portNum}`);
const 所有反代数组 = await 解析地址端口(ctx反代IP, host, yourUUID);
newSocket = await connectProxyIP(`${特征码字典[0]}.tp1.${特征码字典[2]}.xyz`, 1, 本次首包数据, 所有反代数组, ctx反代兜底);
}
await 安装当前连接(newSocket, 当前连接世代, downlinkDrain);
if (本次发送首包) 已通过代理发送首包 = true;
} catch (err) {
try { newSocket?.close?.() } catch (e) { }
if (remoteConnWrapper.generation === 当前连接世代) {
remoteConnWrapper.socket = null;
closeSocketQuietly(ws);
throw err;
}
}
})();
remoteConnWrapper.connectingPromise = 当前连接任务;
try {
await 当前连接任务;
} finally {
if (remoteConnWrapper.connectingPromise === 当前连接任务) {
remoteConnWrapper.connectingPromise = null;
}
}
}
remoteConnWrapper.retryConnect = async () => connecttoPry(!已通过代理发送首包);
if (ctx代理类型 && (ctx代理全局 || SOCKS5白名单.some(p => new RegExp(`^${p.replace(/\*/g, '.*')}$`, 'i').test(host)))) {
log(`[TCP转发] 启用 SOCKS5/HTTP/HTTPS/TURN/SSTP 全局代理`);
try {
await connecttoPry();
if (仅建立连接) return remoteConnWrapper.socket;
} catch (err) {
log(`[TCP转发] SOCKS5/HTTP/HTTPS/TURN/SSTP 代理连接失败: ${err.message}`);
throw err;
}
} else {
let 直连世代 = remoteConnWrapper.generation;
try {
log(`[TCP转发] 尝试直连到: ${host}:${portNum}`);
const 世代连接 = 开始TCP连接世代(remoteConnWrapper);
直连世代 = 世代连接.generation;
const initialSocket = await connectDirect(host, portNum, rawData, true);
await 安装当前连接(initialSocket, 直连世代, 世代连接.downlinkDrain, async () => {
if (remoteConnWrapper.generation !== 直连世代 || remoteConnWrapper.socket !== initialSocket) return;
await connecttoPry();
});
if (仅建立连接) return initialSocket;
} catch (err) {
log(`[TCP转发] 直连 ${host}:${portNum} 失败: ${err.message}`);
if (remoteConnWrapper.generation !== 直连世代) throw err;
if (err instanceof Error && err.name === '预加载解析为空') {
closeSocketQuietly(ws);
throw err;
}
if (ws.readyState !== WebSocket.OPEN) throw err;
await connecttoPry();
if (仅建立连接) return remoteConnWrapper.socket;
}
}
}
async function forwardataudp(udpChunk, webSocket, respHeader, request, 响应封装器 = null) {
const 请求数据 = 数据转Uint8Array(udpChunk);
const 请求字节数 = 请求数据.byteLength;
log(`[UDP转发] 收到 DNS 请求: ${请求字节数}B -> 8.8.4.4:53`);
try {
const TCP连接 = 创建请求TCP连接器(request);
const tcpSocket = TCP连接({ hostname: '8.8.4.4', port: 53 });
let 魏烈思Header = respHeader;
const writer = tcpSocket.writable.getWriter();
await writer.write(请求数据);
log(`[UDP转发] DNS 请求已写入上游: ${请求字节数}B`);
writer.releaseLock();
await tcpSocket.readable.pipeTo(new WritableStream({
async write(chunk) {
const 原始响应 = 数据转Uint8Array(chunk);
log(`[UDP转发] 收到 DNS 响应: ${原始响应.byteLength}B`);
const 封装结果 = 响应封装器 ? await 响应封装器(原始响应) : 原始响应;
const 发送片段列表 = Array.isArray(封装结果) ? 封装结果 : [封装结果];
if (!发送片段列表.length) return;
if (webSocket.readyState !== WebSocket.OPEN) return;
for (const fragment of 发送片段列表) {
const 转发响应 = 数据转Uint8Array(fragment);
if (!转发响应.byteLength) continue;
if (魏烈思Header) {
const response = new Uint8Array(魏烈思Header.length + 转发响应.byteLength);
response.set(魏烈思Header, 0);
response.set(转发响应, 魏烈思Header.length);
await WebSocket发送并等待(webSocket, response.buffer);
魏烈思Header = null;
} else {
await WebSocket发送并等待(webSocket, 转发响应);
}
}
},
}));
} catch (error) {
log(`[UDP转发] DNS 转发失败: ${error?.message || error}`);
}
}
function closeSocketQuietly(socket) {
try {
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CLOSING) {
socket.close();
}
} catch (error) { }
}
function formatIdentifier(arr, offset = 0) {
const hex = [...arr.slice(offset, offset + 16)].map(b => b.toString(16).padStart(2, '0')).join('');
return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`;
}
async function WebSocket发送并等待(webSocket, payload) {
const sendResult = webSocket.send(payload);
if (sendResult && typeof sendResult.then === 'function') await sendResult;
}
// 1. Grain收纳器(无改动,保持原样)
function 创建Grain收纳器(容量, 复制合包结果 = false) {
let 队列 = [];
let 头 = 0;
let 字节数 = 0;
let 合包缓冲 = null;
const 为空 = () => 头 >= 队列.length;
const 压缩 = () => {
if (头 > 32 && 头 * 2 >= 队列.length) {
队列 = 队列.slice(头);
头 = 0;
}
};
const 取出 = () => {
if (为空()) return null;
const item = 队列[头];
队列[头++] = undefined;
字节数 -= item.chunk.byteLength;
压缩();
return item;
};
return {
get 字节数() { return 字节数 },
get 条目数() { return 队列.length - 头 },
get 为空() { return 为空() },
清空(处理项目 = null) {
if (处理项目) {
for (let i = 头; i < 队列.length; i++) {
if (队列[i]) 处理项目(队列[i]);
}
}
队列 = [];
头 = 0;
字节数 = 0;
},
收纳(item) {
if (!item?.chunk?.byteLength) return false;
队列.push(item);
字节数 += item.chunk.byteLength;
return true;
},
合包() {
const first = 取出();
if (!first) return null;
const items = [first];
if (为空() || first.chunk.byteLength >= 容量) return { chunk: first.chunk, items };
let totalBytes = first.chunk.byteLength;
let end = 头;
while (end < 队列.length) {
const nextBytes = totalBytes + 队列[end].chunk.byteLength;
if (nextBytes > 容量) break;
totalBytes = nextBytes;
end++;
}
if (end === 头) return { chunk: first.chunk, items };
const output = (合包缓冲 ||= new Uint8Array(容量));
output.set(first.chunk, 0);
let offset = first.chunk.byteLength;
while (头 < end) {
const next = 队列[头];
队列[头++] = undefined;
字节数 -= next.chunk.byteLength;
items.push(next);
output.set(next.chunk, offset);
offset += next.chunk.byteLength;
}
压缩();
const bundled = output.subarray(0, totalBytes);
return { chunk: 复制合包结果 ? bundled.slice() : bundled, items };
}
};
}
// 2. 优化后的上行合包流(冲刷使用 subarray 零拷贝视图)
function 创建上行Grain合包流(目标字节 = 上行合包目标字节) {
const identity = typeof IdentityTransformStream !== 'undefined'
? new IdentityTransformStream()
: new TransformStream();
const writer = identity.writable.getWriter();
let 缓冲 = new Uint8Array(目标字节 * 2);
let 缓冲长度 = 0;
let 定时器 = null;
let 在途写 = null;
let 冲刷链 = Promise.resolve();
let 动态阈值 = 目标字节;
let 写入计数 = 0;
let 上次写入时间 = 0;
const 清理定时器 = () => {
if (定时器) {
clearTimeout(定时器);
定时器 = null;
}
};
const 串行写 = async (chunk) => {
if (在途写) await 在途写;
在途写 = writer.write(chunk);
try { await 在途写 } finally { 在途写 = null; }
};
const 冲刷 = async () => {
if (缓冲长度) {
const chunk = 缓冲.subarray(0, 缓冲长度);
缓冲长度 = 0;
await 串行写(chunk);
}
};
const 排队冲刷 = () => {
冲刷链 = 冲刷链.then(() => 冲刷()).catch(() => { });
};
const 启动定时器 = () => {
if (定时器) return;
定时器 = setTimeout(() => {
定时器 = null;
排队冲刷();
}, 5);
};
return {
readable: identity.readable,
写入: async (chunk) => {
const data = 数据转Uint8Array(chunk);
if (!data.byteLength) return;
if (data.byteLength < 512) {
清理定时器();
if (缓冲长度) await 冲刷();
await 串行写(data);
return;
}
const now = Date.now();
if (now - 上次写入时间 < 10) 写入计数++;
else 写入计数 = 0;
上次写入时间 = now;
if (写入计数 > 5 && 动态阈值 < 目标字节 * 1.5) 动态阈值 = Math.min(目标字节 * 1.5, 32 * 1024);
else if (写入计数 === 0 && 动态阈值 > 目标字节) 动态阈值 = 目标字节;
if (缓冲长度 + data.byteLength >= 动态阈值) {
const output = new Uint8Array(缓冲长度 + data.byteLength);
output.set(缓冲.subarray(0, 缓冲长度), 0);
output.set(data, 缓冲长度);
缓冲长度 = 0;
清理定时器();
await 串行写(output);
} else {
if (缓冲.length < 缓冲长度 + data.byteLength) {
const newBuf = new Uint8Array(Math.max(缓冲.length * 2, 缓冲长度 + data.byteLength));
newBuf.set(缓冲.subarray(0, 缓冲长度), 0);
缓冲 = newBuf;
}
缓冲.set(data, 缓冲长度);
缓冲长度 += data.byteLength;
启动定时器();
}
},
结束: async () => {
清理定时器();
try {
await 冲刷链;
await 冲刷();
await writer.close();
} finally {
try { writer.releaseLock() } catch (e) { }
}
}
};
}
// 3. 优化后的上行写入队列(背压循环 + 重试限制)
function 创建上行写入队列({ 获取写入器, 获取连接任务 = null, 释放写入器, 重试连接, 关闭连接, 名称 = '上行队列' }) {
const grain = 创建Grain收纳器(上行合包目标字节);
let draining = false;
let closed = false;
let idleResolvers = [];
let activeCompletions = null;
const settleCompletions = (completions, err = null) => {
if (!completions) return;
for (const completion of completions) {
if (err) completion.reject(err);
else completion.resolve();
}
};
const resolveIdle = () => {
if (grain.字节数 || draining || !idleResolvers.length) return;
const resolvers = idleResolvers;
idleResolvers = [];
for (const resolve of resolvers) resolve();
};
const clear = (err = null) => {
const closeErr = err || (closed ? new Error(`${名称}: queue closed`) : null);
if (closeErr) {
grain.清空(item => settleCompletions(item.completions, closeErr));
settleCompletions(activeCompletions, closeErr);
activeCompletions = null;
} else grain.清空();
resolveIdle();
};
const bundle = () => {
const packed = grain.合包();
if (!packed) return null;
let allowRetry = true;
let completions = null;
for (const item of packed.items) {
allowRetry = allowRetry && item.allowRetry;
if (item.completions) completions = completions ? completions.concat(item.completions) : item.completions;
}
return { chunk: packed.chunk, allowRetry, completions };
};
const 等待可用写入器 = async () => {
let writer = 获取写入器();
if (writer) return writer;
const connectionTask = 获取连接任务?.();
if (connectionTask) await connectionTask;
return 获取写入器();
};
const drain = async () => {
if (draining || closed) return;
draining = true;
let retryCount = 0;
try {
for (; ;) {
if (closed) break;
const item = bundle();
if (!item) break;
const completions = item.completions || null;
activeCompletions = completions;
try {
let writer = await 等待可用写入器();
if (closed) break;
if (!writer) throw new Error(`${名称}: remote writer unavailable`);
try {
await writer.write(item.chunk);
retryCount = 0;
} catch (err) {
释放写入器?.();
if (closed) break;
if (!item.allowRetry || typeof 重试连接 !== 'function' || retryCount >= 3) throw err;
retryCount++;
await 重试连接();
if (closed) break;
writer = 获取写入器();
if (!writer) throw err;
await writer.write(item.chunk);
retryCount = 0;
}
settleCompletions(completions);
} catch (err) {
settleCompletions(completions, err);
throw err;
} finally {
if (activeCompletions === completions) activeCompletions = null;
}
}
} catch (err) {
closed = true;
clear(err);
log(`[${名称}] 写入失败: ${err?.message || err}`);
try { 关闭连接?.(err) } catch (_) { }
} finally {
draining = false;
if (!closed && !grain.为空) drain();
else resolveIdle();
}
};
const enqueue = async (data, allowRetry = true, waitForFlush = false) => {
if (closed) return false;
if (!获取写入器() && !获取连接任务?.()) return false;
const chunk = 数据转Uint8Array(data);
if (!chunk.byteLength) return true;
// 背压循环:等待队列低于 80%
while (grain.字节数 > 上行队列最大字节 * 0.8) {
await new Promise(resolve => idleResolvers.push(resolve));
}
const nextBytes = grain.字节数 + chunk.byteLength;
const nextItems = grain.条目数 + 1;
if (nextBytes > 上行队列最大字节 || nextItems > 上行队列最大条目) {
closed = true;
const err = Object.assign(new Error(`${名称}: upload queue overflow (${nextBytes}B/${nextItems})`), { isQueueOverflow: true });
clear(err);
log(`[${名称}] 队列超限,关闭连接`);
try { 关闭连接?.(err) } catch (_) { }
throw err;
}
let completionPromise = null;
let completions = null;
if (waitForFlush) {
completions = [];
completionPromise = new Promise((resolve, reject) => completions.push({ resolve, reject }));
}
grain.收纳({ chunk, allowRetry, completions });
if (!draining) drain();
return waitForFlush ? completionPromise.then(() => true) : true;
};
return {
写入(data, allowRetry = true) {
return enqueue(data, allowRetry, false);
},
写入并等待(data, allowRetry = true) {
return enqueue(data, allowRetry, true);
},
async 等待空() {
if (!grain.字节数 && !draining) return;
await new Promise(resolve => idleResolvers.push(resolve));
},
清空() {
closed = true;
clear();
}
};
}
// 4. 优化后的下行Grain发送器(自适应分块 + 2ms硬超时)
function 创建下行Grain发送器(webSocket, headerData = null, isActive = null) {
const baseCap = 下行Grain包字节;
const tailBytes = 下行Grain尾部阈值;
const grain = 创建Grain收纳器(baseCap, true);
let header = typeof headerData === 'function' ? null : headerData;
const 获取响应头 = typeof headerData === 'function' ? headerData : () => {
const value = header;
header = null;
return value;
};
let flushTimer = null;
let flushPromise = null;
let directSendPromise = null;
let 强制排空 = false;
let 停止已开始 = false;
let 活动发送数 = 0;
let 活动直发数 = 0;
let 活动发送错误 = null;
let 活动发送等待者 = [];
let 最近发送耗时 = 0;
let 自适应包大小 = baseCap;
const 等待活动发送完成 = () => {
if (!活动发送数 && !活动直发数) return Promise.resolve();
return new Promise(resolve => 活动发送等待者.push(resolve));
};
const 标记发送完成 = () => {
if (活动发送数 || 活动直发数 || !活动发送等待者.length) return;
const resolvers = 活动发送等待者;
活动发送等待者 = [];
for (const resolve of resolvers) resolve();
};
const 检查活动发送错误 = () => {
if (!活动发送错误) return;
const err = 活动发送错误;
grain.清空();
throw err;
};
const 当前发送器有效 = () => 强制排空 || !isActive || isActive();
const 关闭活动连接 = () => {
if (当前发送器有效()) closeSocketQuietly(webSocket);
};
const 发送原始块 = async (chunk) => {
if (!当前发送器有效()) return;
if (webSocket.readyState !== WebSocket.OPEN) throw new Error('ws.readyState is not open');
const start = performance.now();
chunk = 附加响应头(chunk);
await WebSocket发送并等待(webSocket, chunk);
const elapsed = performance.now() - start;
最近发送耗时 = 最近发送耗时 ? 最近发送耗时 * 0.7 + elapsed * 0.3 : elapsed;
if (最近发送耗时 > 100) 自适应包大小 = Math.max(16 * 1024, Math.min(baseCap, 24 * 1024));
else if (最近发送耗时 < 30) 自适应包大小 = Math.min(48 * 1024, Math.max(baseCap, 32 * 1024));
else 自适应包大小 = baseCap;
};
const 串行发送原始块 = async (chunk) => {
while (directSendPromise) await directSendPromise;
const sendTask = 发送原始块(chunk);
directSendPromise = sendTask;
try { await sendTask }
finally {
if (directSendPromise === sendTask) directSendPromise = null;
}
};
const 附加响应头 = (chunk) => {
const responseHeader = 获取响应头();
if (!responseHeader) return chunk;
const merged = new Uint8Array(responseHeader.length + chunk.byteLength);
merged.set(responseHeader, 0);
merged.set(chunk, responseHeader.length);
return merged;
};
const flush = async () => {
while (flushPromise) await flushPromise;
if (flushTimer) clearTimeout(flushTimer);
flushTimer = null;
if (!当前发送器有效()) {
grain.清空();
return;
}
const 发送任务 = (async () => {
for (; ;) {
if (!当前发送器有效()) {
grain.清空();
break;
}
const packed = grain.合包();
if (!packed) break;
await 串行发送原始块(packed.chunk);
}
})();
flushPromise = 发送任务.catch(err => {
活动发送错误 ||= err;
throw err;
}).finally(() => { flushPromise = null });
return flushPromise;
};
const scheduleFlush = () => {
if (!当前发送器有效()) {
grain.清空();
return;
}
if (grain.为空 || flushTimer) return;
const currentCap = 自适应包大小 || baseCap;
if (grain.字节数 >= currentCap || currentCap - grain.字节数 < tailBytes) {
flush().catch(关闭活动连接);
return;
}
flushTimer = setTimeout(() => {
flushTimer = null;
if (!当前发送器有效()) {
grain.清空();
return;
}
if (grain.为空) return;
flush().catch(关闭活动连接);
}, 2);
};
return {
async 直接发送(data) {
if (停止已开始 || !当前发送器有效()) return;
活动直发数++;
try {
const chunk = 数据转Uint8Array(data);
if (!chunk.byteLength) return;
await 串行发送原始块(chunk);
} catch (err) {
活动发送错误 ||= err;
throw err;
} finally {
活动直发数--;
标记发送完成();
}
},
async 发送(data) {
if (停止已开始 || !当前发送器有效()) return;
活动发送数++;
try {
const chunk = 数据转Uint8Array(data);
if (!chunk.byteLength) return;
let offset = 0;
const totalBytes = chunk.byteLength;
const currentCap = 自适应包大小 || baseCap;
while (offset < totalBytes) {
const remainingBytes = totalBytes - offset;
if (grain.为空 && remainingBytes >= currentCap) {
const sendBytes = Math.min(currentCap, remainingBytes);
const view = (offset === 0 && sendBytes === totalBytes) ? chunk : chunk.subarray(offset, offset + sendBytes);
await 串行发送原始块(view);
offset += sendBytes;
continue;
}
const copyBytes = Math.min(currentCap - grain.字节数, totalBytes - offset);
if (!copyBytes) {
await flush();
continue;
}
grain.收纳({ chunk: chunk.subarray(offset, offset + copyBytes) });
offset += copyBytes;
if (grain.字节数 >= currentCap || currentCap - grain.字节数 < tailBytes) await flush();
else scheduleFlush();
}
} catch (err) {
活动发送错误 ||= err;
throw err;
} finally {
活动发送数--;
标记发送完成();
}
},
flush,
async 停止并刷新() {
if (停止已开始) {
await 等待活动发送完成();
while (directSendPromise) await directSendPromise;
检查活动发送错误();
await flush();
return;
}
停止已开始 = true;
强制排空 = true;
if (flushTimer) clearTimeout(flushTimer);
flushTimer = null;
await 等待活动发送完成();
while (directSendPromise) await directSendPromise;
检查活动发送错误();
await flush();
}
};
}
// 5. 优化后的 connectStreams(移除 BYOB 中的 slice)
async function connectStreams(remoteSocket, webSocket, headerData, retryFunc, isCurrentSocket = null, remoteConnWrapper = null) {
let header = headerData, hasData = false, reader, useBYOB = false, readError = null;
const BYOB单次读取上限 = 64 * 1024;
const 当前连接仍有效 = () => !isCurrentSocket || isCurrentSocket();
const 下行发送器 = 创建下行Grain发送器(webSocket, header, 当前连接仍有效);
header = null;
const 下行控制器 = { 停止并刷新: () => 下行发送器.停止并刷新() };
if (remoteConnWrapper) remoteConnWrapper.downlinkController = 下行控制器;
try { remoteSocket.closed?.catch?.(() => { }) } catch (e) { }
try { reader = remoteSocket.readable.getReader({ mode: 'byob' }); useBYOB = true }
catch (e) { reader = remoteSocket.readable.getReader() }
try {
if (!useBYOB) {
while (true) {
const { done, value } = await reader.read();
if (!当前连接仍有效()) break;
if (done) break;
if (!value || value.byteLength === 0) continue;
hasData = true;
if (value.byteLength >= 下行Grain包字节) {
await 下行发送器.flush();
await 下行发送器.直接发送(value);
} else {
await 下行发送器.发送(value);
}
}
} else {
let readBuffer = new ArrayBuffer(BYOB单次读取上限);
while (true) {
const { done, value } = await reader.read(new Uint8Array(readBuffer, 0, BYOB单次读取上限));
if (!当前连接仍有效()) break;
if (done) break;
if (!value || value.byteLength === 0) continue;
hasData = true;
if (value.byteLength >= 下行Grain包字节) {
await 下行发送器.flush();
await 下行发送器.直接发送(value);
readBuffer = new ArrayBuffer(BYOB单次读取上限);
} else {
await 下行发送器.发送(value);
readBuffer = new ArrayBuffer(BYOB单次读取上限);
}
}
}
if (当前连接仍有效()) await 下行发送器.flush();
} catch (err) { readError = err }
finally {
if (当前连接仍有效() && webSocket.readyState === WebSocket.OPEN) {
try { await 下行发送器.停止并刷新() } catch (err) { readError ||= err }
}
if (remoteConnWrapper?.downlinkController === 下行控制器) remoteConnWrapper.downlinkController = null;
try { await reader.cancel() } catch (e) { }
try { reader.releaseLock() } catch (e) { }
try { remoteSocket.close() } catch (e) { }
}
if (!hasData && retryFunc && webSocket.readyState === WebSocket.OPEN && 当前连接仍有效()) {
try {
await retryFunc();
return;
} catch (err) {
readError ||= err;
}
}
if (!当前连接仍有效()) return;
if (readError) log(`[TCP下行] 读取失败: ${readError?.message || readError}`);
closeSocketQuietly(webSocket);
}
function isSpeedTestSite(hostname) {
const speedTestDomains = ['speed.cloudflare.com', 'cp.cloudflare.com'];
hostname = hostname.toLowerCase();
return speedTestDomains.some(domain => hostname === domain || hostname.endsWith('.' + domain));
}
function 构造本地204响应(respHeader = null) {
const 本地204响应 = new TextEncoder().encode(
'HTTP/1.1 204 No Content\r\n' +
'Content-Length: 0\r\n' +
'Connection: close\r\n' +
'\r\n'
);
if (有效数据长度(respHeader) === 0) return 本地204响应;
const 协议响应头 = 数据转Uint8Array(respHeader);
const response = new Uint8Array(协议响应头.byteLength + 本地204响应.byteLength);
response.set(协议响应头, 0);
response.set(本地204响应, 协议响应头.byteLength);
log(`[TCP转发] 构造本地204响应: ${response.byteLength}B`);
return response;
}
function 构造WS本地204响应(respHeader = null) {
const WS本地204响应 = new TextEncoder().encode(
'HTTP/1.1 204 No Content\r\n' +
'Content-Length: 0\r\n' +
'Connection: keep-alive\r\n' +
'\r\n'
);
if (有效数据长度(respHeader) === 0) return WS本地204响应;
const 协议响应头 = 数据转Uint8Array(respHeader);
const response = new Uint8Array(协议响应头.byteLength + WS本地204响应.byteLength);
response.set(协议响应头, 0);
response.set(WS本地204响应, 协议响应头.byteLength);
return response;
}
///////////////////////////////////////////////////////SOCKS5/HTTP函数///////////////////////////////////////////////
async function socks5Connect(targetHost, targetPort, initialData, TCP连接, parsedSocks5) {
const { username, password, hostname, port } = parsedSocks5 || {};
const socket = TCP连接({ hostname, port }), writer = socket.writable.getWriter(), reader = socket.readable.getReader();
try {
const authMethods = username && password ? new Uint8Array([0x05, 0x02, 0x00, 0x02]) : new Uint8Array([0x05, 0x01, 0x00]);
await writer.write(authMethods);
let response = await reader.read();
if (response.done || response.value.byteLength < 2) throw new Error('S5 method selection failed');
const selectedMethod = new Uint8Array(response.value)[1];
if (selectedMethod === 0x02) {
if (!username || !password) throw new Error('S5 requires authentication');
const userBytes = new TextEncoder().encode(username), passBytes = new TextEncoder().encode(password);
const authPacket = new Uint8Array([0x01, userBytes.length, ...userBytes, passBytes.length, ...passBytes]);
await writer.write(authPacket);
response = await reader.read();
if (response.done || new Uint8Array(response.value)[1] !== 0x00) throw new Error('S5 authentication failed');
} else if (selectedMethod !== 0x00) throw new Error(`S5 unsupported auth method: ${selectedMethod}`);
const hostBytes = new TextEncoder().encode(targetHost);
const connectPacket = new Uint8Array([0x05, 0x01, 0x00, 0x03, hostBytes.length, ...hostBytes, targetPort >> 8, targetPort & 0xff]);
await writer.write(connectPacket);
response = await reader.read();
if (response.done || new Uint8Array(response.value)[1] !== 0x00) throw new Error('S5 connection failed');
if (有效数据长度(initialData) > 0) await writer.write(initialData);
writer.releaseLock(); reader.releaseLock();
return socket;
} catch (error) {
try { writer.releaseLock() } catch (e) { }
try { reader.releaseLock() } catch (e) { }
try { socket.close() } catch (e) { }
throw error;
}
}
async function httpConnect(targetHost, targetPort, initialData, HTTPS代理 = false, TCP连接, parsedSocks5) {
const { username, password, hostname, port } = parsedSocks5 || {};
const socket = HTTPS代理
? TCP连接({ hostname, port }, { secureTransport: 'on', allowHalfOpen: false })
: TCP连接({ hostname, port });
const writer = socket.writable.getWriter(), reader = socket.readable.getReader();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
try {
if (HTTPS代理) await socket.opened;
const auth = username && password ? `Proxy-Authorization: Basic ${btoa(`${username}:${password}`)}\r\n` : '';
const request = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${auth}User-Agent: Mozilla/5.0\r\nConnection: keep-alive\r\n\r\n`;
await writer.write(encoder.encode(request));
writer.releaseLock();
let responseBuffer = new Uint8Array(0), headerEndIndex = -1, bytesRead = 0;
while (headerEndIndex === -1 && bytesRead < 8192) {
const { done, value } = await reader.read();
if (done || !value) throw new Error(`${HTTPS代理 ? 'HTTPS' : 'HTTP'} 代理在返回 CONNECT 响应前关闭连接`);
responseBuffer = new Uint8Array([...responseBuffer, ...value]);
bytesRead = responseBuffer.length;
const crlfcrlf = responseBuffer.findIndex((_, i) => i < responseBuffer.length - 3 && responseBuffer[i] === 0x0d && responseBuffer[i + 1] === 0x0a && responseBuffer[i + 2] === 0x0d && responseBuffer[i + 3] === 0x0a);
if (crlfcrlf !== -1) headerEndIndex = crlfcrlf + 4;
}
if (headerEndIndex === -1) throw new Error('代理 CONNECT 响应头过长或无效');
const statusMatch = decoder.decode(responseBuffer.slice(0, headerEndIndex)).split('\r\n')[0].match(/HTTP\/\d\.\d\s+(\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
if (!Number.isFinite(statusCode) || statusCode < 200 || statusCode >= 300) throw new Error(`Connection failed: HTTP ${statusCode}`);
reader.releaseLock();
if (有效数据长度(initialData) > 0) {
const 远端写入器 = socket.writable.getWriter();
await 远端写入器.write(initialData);
远端写入器.releaseLock();
}
// CONNECT 响应头后可能夹带隧道数据,先回灌到可读流,避免首包被吞。
if (bytesRead > headerEndIndex) {
const { readable, writable } = new TransformStream();
const transformWriter = writable.getWriter();
await transformWriter.write(responseBuffer.subarray(headerEndIndex, bytesRead));
transformWriter.releaseLock();
socket.readable.pipeTo(writable).catch(() => { });
return { readable, writable: socket.writable, closed: socket.closed, close: () => socket.close() };
}
return socket;
} catch (error) {
try { writer.releaseLock() } catch (e) { }
try { reader.releaseLock() } catch (e) { }
try { socket.close() } catch (e) { }
throw error;
}
}
async function httpsConnect(targetHost, targetPort, initialData, TCP连接, parsedSocks5) {
const { username, password, hostname, port } = parsedSocks5 || {};
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let tlsSocket = null;
const tlsServerName = isIPHostname(hostname) ? '' : stripIPv6Brackets(hostname);
const 打开HTTPS代理TLS = async (allowChacha = false) => {
const proxySocket = TCP连接({ hostname, port });
try {
await proxySocket.opened;
const socket = new TlsClient(proxySocket, { serverName: tlsServerName, insecure: true, allowChacha });
await socket.handshake();
log(`[HTTPS代理] TLS版本: ${socket.isTls13 ? '1.3' : '1.2'} | Cipher: 0x${socket.cipherSuite.toString(16)}${socket.cipherConfig?.chacha ? ' (ChaCha20)' : ' (AES-GCM)'}`);
return socket;
} catch (error) {
try { proxySocket.close() } catch (e) { }
throw error;
}
};
try {
try {
tlsSocket = await 打开HTTPS代理TLS(false);
} catch (error) {
if (!/cipher|handshake|TLS Alert|ServerHello|Finished|Unsupported|Missing TLS/i.test(error?.message || `${error || ''}`)) throw error;
log(`[HTTPS代理] AES-GCM TLS 握手失败,回退 ChaCha20 兼容模式: ${error?.message || error}`);
tlsSocket = await 打开HTTPS代理TLS(true);
}
const auth = username && password ? `Proxy-Authorization: Basic ${btoa(`${username}:${password}`)}\r\n` : '';
const request = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${auth}User-Agent: Mozilla/5.0\r\nConnection: keep-alive\r\n\r\n`;
await tlsSocket.write(encoder.encode(request));
let responseBuffer = new Uint8Array(0), headerEndIndex = -1, bytesRead = 0;
while (headerEndIndex === -1 && bytesRead < 8192) {
const value = await tlsSocket.read();
if (!value) throw new Error('HTTPS 代理在返回 CONNECT 响应前关闭连接');
responseBuffer = 拼接字节数据(responseBuffer, value);
bytesRead = responseBuffer.length;
const crlfcrlf = responseBuffer.findIndex((_, i) => i < responseBuffer.length - 3 && responseBuffer[i] === 0x0d && responseBuffer[i + 1] === 0x0a && responseBuffer[i + 2] === 0x0d && responseBuffer[i + 3] === 0x0a);
if (crlfcrlf !== -1) headerEndIndex = crlfcrlf + 4;
}
if (headerEndIndex === -1) throw new Error('HTTPS 代理 CONNECT 响应头过长或无效');
const statusMatch = decoder.decode(responseBuffer.slice(0, headerEndIndex)).split('\r\n')[0].match(/HTTP\/\d\.\d\s+(\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : NaN;
if (!Number.isFinite(statusCode) || statusCode < 200 || statusCode >= 300) throw new Error(`Connection failed: HTTP ${statusCode}`);
if (有效数据长度(initialData) > 0) await tlsSocket.write(数据转Uint8Array(initialData));
const bufferedData = bytesRead > headerEndIndex ? responseBuffer.subarray(headerEndIndex, bytesRead) : null;
let closedSettled = false, resolveClosed, rejectClosed;
const settleClosed = (settle, value) => {
if (!closedSettled) {
closedSettled = true;
settle(value);
}
};
const closed = new Promise((resolve, reject) => {
resolveClosed = resolve;
rejectClosed = reject;
});
const close = () => {
try { tlsSocket.close() } catch (e) { }
settleClosed(resolveClosed);
};
const readable = new ReadableStream({
async start(controller) {
try {
if (有效数据长度(bufferedData) > 0) controller.enqueue(bufferedData);
while (true) {
const data = await tlsSocket.read();
if (!data) break;
if (data.byteLength > 0) controller.enqueue(data);
}
try { controller.close() } catch (e) { }
settleClosed(resolveClosed);
} catch (error) {
try { controller.error(error) } catch (e) { }
settleClosed(rejectClosed, error);
}
},
cancel() {
close();
}
});
const writable = new WritableStream({
async write(chunk) {
await tlsSocket.write(数据转Uint8Array(chunk));
},
close,
abort(error) {
close();
if (error) settleClosed(rejectClosed, error);
}
});
return { readable, writable, closed, close };
} catch (error) {
try { tlsSocket?.close() } catch (e) { }
throw error;
}
}
function 创建请求TCP连接器(request) {
const 请求对象 = /** @type {any} */ (request);
const fetcher = 请求对象?.fetcher;
if (!fetcher || typeof fetcher.connect !== 'function') throw new Error('request.fetcher.connect unavailable');
return (options, init) => init === undefined ? fetcher.connect(options) : fetcher.connect(options, init);
}
////////////////////////////////////////////TLSClient by: @Alexandre_Kojeve////////////////////////////////////////////////
const TLS_VERSION_10 = 769, TLS_VERSION_12 = 771, TLS_VERSION_13 = 772;
const CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20, CONTENT_TYPE_ALERT = 21, CONTENT_TYPE_HANDSHAKE = 22, CONTENT_TYPE_APPLICATION_DATA = 23;
const HANDSHAKE_TYPE_CLIENT_HELLO = 1, HANDSHAKE_TYPE_SERVER_HELLO = 2, HANDSHAKE_TYPE_NEW_SESSION_TICKET = 4, HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS = 8, HANDSHAKE_TYPE_CERTIFICATE = 11, HANDSHAKE_TYPE_SERVER_KEY_EXCHANGE = 12, HANDSHAKE_TYPE_CERTIFICATE_REQUEST = 13, HANDSHAKE_TYPE_SERVER_HELLO_DONE = 14, HANDSHAKE_TYPE_CERTIFICATE_VERIFY = 15, HANDSHAKE_TYPE_CLIENT_KEY_EXCHANGE = 16, HANDSHAKE_TYPE_FINISHED = 20, HANDSHAKE_TYPE_KEY_UPDATE = 24;
const EXT_SERVER_NAME = 0, EXT_SUPPORTED_GROUPS = 10, EXT_EC_POINT_FORMATS = 11, EXT_SIGNATURE_ALGORITHMS = 13, EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION = 16, EXT_SUPPORTED_VERSIONS = 43, EXT_PSK_KEY_EXCHANGE_MODES = 45, EXT_KEY_SHARE = 51;
const ALERT_CLOSE_NOTIFY = 0, ALERT_LEVEL_WARNING = 1, ALERT_UNRECOGNIZED_NAME = 112;
const shouldIgnoreTlsAlert = fragment => fragment?.[0] === ALERT_LEVEL_WARNING && fragment?.[1] === ALERT_UNRECOGNIZED_NAME;
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const EMPTY_BYTES = new Uint8Array(0);
const CIPHER_SUITES_BY_ID = new Map([
[4865, { id: 4865, keyLen: 16, ivLen: 12, hash: "SHA-256", tls13: !0 }],
[4866, { id: 4866, keyLen: 32, ivLen: 12, hash: "SHA-384", tls13: !0 }],
[4867, { id: 4867, keyLen: 32, ivLen: 12, hash: "SHA-256", tls13: !0, chacha: !0 }],
[49199, { id: 49199, keyLen: 16, ivLen: 4, hash: "SHA-256", kex: "ECDHE" }],
[49200, { id: 49200, keyLen: 32, ivLen: 4, hash: "SHA-384", kex: "ECDHE" }],
[52392, { id: 52392, keyLen: 32, ivLen: 12, hash: "SHA-256", kex: "ECDHE", chacha: !0 }],
[49195, { id: 49195, keyLen: 16, ivLen: 4, hash: "SHA-256", kex: "ECDHE" }],
[49196, { id: 49196, keyLen: 32, ivLen: 4, hash: "SHA-384", kex: "ECDHE" }],
[52393, { id: 52393, keyLen: 32, ivLen: 12, hash: "SHA-256", kex: "ECDHE", chacha: !0 }]
]);
const GROUPS_BY_ID = new Map([[29, "X25519"], [23, "P-256"]]);
const SUPPORTED_SIGNATURE_ALGORITHMS = [2052, 2053, 2054, 1025, 1281, 1537, 1027, 1283, 1539];
const tlsBytes = (...parts) => {
const flattenBytes = values => values.flatMap(value => value instanceof Uint8Array ? [...value] : Array.isArray(value) ? flattenBytes(value) : "number" == typeof value ? [value] : []);
return new Uint8Array(flattenBytes(parts))
};
const uint16be = value => [value >> 8 & 255, 255 & value];
const readUint16 = (buffer, offset) => buffer[offset] << 8 | buffer[offset + 1];
const readUint24 = (buffer, offset) => buffer[offset] << 16 | buffer[offset + 1] << 8 | buffer[offset + 2];
const concatBytes = (...chunks) => {
const nonEmptyChunks = chunks.filter((chunk => chunk && chunk.length > 0)),
length = nonEmptyChunks.reduce(((total, chunk) => total + chunk.length), 0),
result = new Uint8Array(length);
let offset = 0;
for (const chunk of nonEmptyChunks) result.set(chunk, offset), offset += chunk.length;
return result
};
const randomBytes = length => crypto.getRandomValues(new Uint8Array(length));
const constantTimeEqual = (left, right) => {
if (!left || !right || left.length !== right.length) return !1;
let diff = 0; for (let index = 0; index < left.length; index++) diff |= left[index] ^ right[index];
return 0 === diff
};
const hashByteLength = hash => "SHA-512" === hash ? 64 : "SHA-384" === hash ? 48 : 32;
async function hmac(hash, key, data) {
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash }, !1, ["sign"]);
return new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, data))
}
async function digestBytes(hash, data) { return new Uint8Array(await crypto.subtle.digest(hash, data)) }
async function tls12Prf(secret, label, seed, length, hash = "SHA-256") {
const labelSeed = concatBytes(textEncoder.encode(label), seed);
let output = new Uint8Array(0),
currentA = labelSeed;
for (; output.length < length;) {
currentA = await hmac(hash, secret, currentA);
const block = await hmac(hash, secret, concatBytes(currentA, labelSeed));
output = concatBytes(output, block)
}
return output.slice(0, length)
}
async function hkdfExtract(hash, salt, inputKeyMaterial) {
return salt && salt.length || (salt = new Uint8Array(hashByteLength(hash))), hmac(hash, salt, inputKeyMaterial)
}
async function hkdfExpandLabel(hash, secret, label, context, length) {
const fullLabel = textEncoder.encode("tls13 " + label);
return async function (hash, secret, info, length) {
const hashLen = hashByteLength(hash),
roundCount = Math.ceil(length / hashLen);
let output = new Uint8Array(0),
previousBlock = new Uint8Array(0);
for (let round = 1; round <= roundCount; round++) previousBlock = await hmac(hash, secret, concatBytes(previousBlock, info, [round])), output = concatBytes(output, previousBlock);
return output.slice(0, length)
}(hash, secret, tlsBytes(uint16be(length), fullLabel.length, fullLabel, context.length, context), length)
}
async function generateKeyShare(group = "P-256") {
const algorithm = "X25519" === group ? { name: "X25519" } : { name: "ECDH", namedCurve: group };
const keyPair = /** @type {CryptoKeyPair} */ (await crypto.subtle.generateKey(algorithm, !0, ["deriveBits"]));
const publicKeyRaw = /** @type {ArrayBuffer} */ (await crypto.subtle.exportKey("raw", keyPair.publicKey));
return { keyPair, publicKeyRaw: new Uint8Array(publicKeyRaw) }
}
async function deriveSharedSecret(privateKey, peerPublicKey, group = "P-256") {
const algorithm = "X25519" === group ? { name: "X25519" } : { name: "ECDH", namedCurve: group },
peerKey = await crypto.subtle.importKey("raw", peerPublicKey, algorithm, !1, []),
bits = "P-384" === group ? 384 : "P-521" === group ? 528 : 256;
return new Uint8Array(await crypto.subtle.deriveBits(/** @type {any} */({ name: algorithm.name, public: peerKey }), privateKey, bits))
}
async function importAesGcmKey(key, usages) { return crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, !1, usages) }
async function aesGcmEncryptWithKey(cryptoKey, initializationVector, plaintext, additionalData) {
return new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: initializationVector, additionalData, tagLength: 128 }, cryptoKey, plaintext))
}
async function aesGcmDecryptWithKey(cryptoKey, initializationVector, ciphertext, additionalData) {
return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: initializationVector, additionalData, tagLength: 128 }, cryptoKey, ciphertext))
}
function rotateLeft32(value, bits) { return (value << bits | value >>> 32 - bits) >>> 0 }
function chachaQuarterRound(state, indexA, indexB, indexC, indexD) {
state[indexA] = state[indexA] + state[indexB] >>> 0, state[indexD] = rotateLeft32(state[indexD] ^ state[indexA], 16), state[indexC] = state[indexC] + state[indexD] >>> 0, state[indexB] = rotateLeft32(state[indexB] ^ state[indexC], 12), state[indexA] = state[indexA] + state[indexB] >>> 0, state[indexD] = rotateLeft32(state[indexD] ^ state[indexA], 8), state[indexC] = state[indexC] + state[indexD] >>> 0, state[indexB] = rotateLeft32(state[indexB] ^ state[indexC], 7)
}
function chacha20Block(key, counter, nonce) {
const state = new Uint32Array(16);
state[0] = 1634760805, state[1] = 857760878, state[2] = 2036477234, state[3] = 1797285236;
const keyView = new DataView(key.buffer, key.byteOffset, key.byteLength);
for (let wordIndex = 0; wordIndex < 8; wordIndex++) state[4 + wordIndex] = keyView.getUint32(4 * wordIndex, !0);
state[12] = counter;
const nonceView = new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength);
state[13] = nonceView.getUint32(0, !0), state[14] = nonceView.getUint32(4, !0), state[15] = nonceView.getUint32(8, !0);
const workingState = new Uint32Array(state);
for (let round = 0; round < 10; round++) chachaQuarterRound(workingState, 0, 4, 8, 12), chachaQuarterRound(workingState, 1, 5, 9, 13), chachaQuarterRound(workingState, 2, 6, 10, 14), chachaQuarterRound(workingState, 3, 7, 11, 15), chachaQuarterRound(workingState, 0, 5, 10, 15), chachaQuarterRound(workingState, 1, 6, 11, 12), chachaQuarterRound(workingState, 2, 7, 8, 13), chachaQuarterRound(workingState, 3, 4, 9, 14);
for (let wordIndex = 0; wordIndex < 16; wordIndex++) workingState[wordIndex] = workingState[wordIndex] + state[wordIndex] >>> 0;
return new Uint8Array(workingState.buffer.slice(0))
}
function chacha20Xor(key, nonce, data) {
const output = new Uint8Array(data.length);
let counter = 1;
for (let offset = 0; offset < data.length; offset += 64) {
const block = chacha20Block(key, counter++, nonce),
blockLength = Math.min(64, data.length - offset);
for (let index = 0; index < blockLength; index++) output[offset + index] = data[offset + index] ^ block[index]
}
return output
}
function poly1305Mac(key, message) {
const rKey = function (rBytes) {
const clamped = new Uint8Array(rBytes);
return clamped[3] &= 15, clamped[7] &= 15, clamped[11] &= 15, clamped[15] &= 15, clamped[4] &= 252, clamped[8] &= 252, clamped[12] &= 252, clamped
}(key.slice(0, 16)),
sKey = key.slice(16, 32);
let accumulator = [0n, 0n, 0n, 0n, 0n];
const rLimbs = [0x3ffffffn & BigInt(rKey[0] | rKey[1] << 8 | rKey[2] << 16 | rKey[3] << 24), 0x3ffffffn & BigInt(rKey[3] >> 2 | rKey[4] << 6 | rKey[5] << 14 | rKey[6] << 22), 0x3ffffffn & BigInt(rKey[6] >> 4 | rKey[7] << 4 | rKey[8] << 12 | rKey[9] << 20), 0x3ffffffn & BigInt(rKey[9] >> 6 | rKey[10] << 2 | rKey[11] << 10 | rKey[12] << 18), 0x3ffffffn & BigInt(rKey[13] | rKey[14] << 8 | rKey[15] << 16)];
for (let offset = 0; offset < message.length; offset += 16) {
const chunk = message.slice(offset, offset + 16),
paddedChunk = new Uint8Array(17);
paddedChunk.set(chunk), paddedChunk[chunk.length] = 1, accumulator[0] += BigInt(paddedChunk[0] | paddedChunk[1] << 8 | paddedChunk[2] << 16 | (3 & paddedChunk[3]) << 24), accumulator[1] += BigInt(paddedChunk[3] >> 2 | paddedChunk[4] << 6 | paddedChunk[5] << 14 | (15 & paddedChunk[6]) << 22), accumulator[2] += BigInt(paddedChunk[6] >> 4 | paddedChunk[7] << 4 | paddedChunk[8] << 12 | (63 & paddedChunk[9]) << 20), accumulator[3] += BigInt(paddedChunk[9] >> 6 | paddedChunk[10] << 2 | paddedChunk[11] << 10 | paddedChunk[12] << 18), accumulator[4] += BigInt(paddedChunk[13] | paddedChunk[14] << 8 | paddedChunk[15] << 16 | paddedChunk[16] << 24);
const product = [0n, 0n, 0n, 0n, 0n];
for (let accIndex = 0; accIndex < 5; accIndex++)
for (let rIndex = 0; rIndex < 5; rIndex++) {
const limbIndex = accIndex + rIndex;
limbIndex < 5 ? product[limbIndex] += accumulator[accIndex] * rLimbs[rIndex] : product[limbIndex - 5] += accumulator[accIndex] * rLimbs[rIndex] * 5n
}
let carry = 0n;
for (let index = 0; index < 5; index++) product[index] += carry, accumulator[index] = 0x3ffffffn & product[index], carry = product[index] >> 26n;
accumulator[0] += 5n * carry, carry = accumulator[0] >> 26n, accumulator[0] &= 0x3ffffffn, accumulator[1] += carry
}
let tagValue = accumulator[0] | accumulator[1] << 26n | accumulator[2] << 52n | accumulator[3] << 78n | accumulator[4] << 104n;
tagValue = tagValue + sKey.reduce(((total, byte, index) => total + (BigInt(byte) << BigInt(8 * index))), 0n) & (1n << 128n) - 1n;
const tag = new Uint8Array(16);
for (let index = 0; index < 16; index++) tag[index] = Number(tagValue >> BigInt(8 * index) & 0xffn);
return tag
}
function chacha20Poly1305Encrypt(key, nonce, plaintext, additionalData) {
const polyKey = chacha20Block(key, 0, nonce).slice(0, 32),
ciphertext = chacha20Xor(key, nonce, plaintext),
aadPadding = (16 - additionalData.length % 16) % 16,
ciphertextPadding = (16 - ciphertext.length % 16) % 16,
macData = new Uint8Array(additionalData.length + aadPadding + ciphertext.length + ciphertextPadding + 16);
macData.set(additionalData, 0), macData.set(ciphertext, additionalData.length + aadPadding);
const lengthView = new DataView(macData.buffer, additionalData.length + aadPadding + ciphertext.length + ciphertextPadding);
lengthView.setBigUint64(0, BigInt(additionalData.length), !0), lengthView.setBigUint64(8, BigInt(ciphertext.length), !0);
const tag = poly1305Mac(polyKey, macData);
return concatBytes(ciphertext, tag)
}
function chacha20Poly1305Decrypt(key, nonce, ciphertext, additionalData) {
if (ciphertext.length < 16) throw new Error("Ciphertext too short");
const tag = ciphertext.slice(-16),
encryptedData = ciphertext.slice(0, -16),
polyKey = chacha20Block(key, 0, nonce).slice(0, 32),
aadPadding = (16 - additionalData.length % 16) % 16,
ciphertextPadding = (16 - encryptedData.length % 16) % 16,
macData = new Uint8Array(additionalData.length + aadPadding + encryptedData.length + ciphertextPadding + 16);
macData.set(additionalData, 0), macData.set(encryptedData, additionalData.length + aadPadding);
const lengthView = new DataView(macData.buffer, additionalData.length + aadPadding + encryptedData.length + ciphertextPadding);
lengthView.setBigUint64(0, BigInt(additionalData.length), !0), lengthView.setBigUint64(8, BigInt(encryptedData.length), !0);
const expectedTag = poly1305Mac(polyKey, macData);
let diff = 0;
for (let index = 0; index < 16; index++) diff |= tag[index] ^ expectedTag[index];
if (0 !== diff) throw new Error("ChaCha20-Poly1305 authentication failed");
return chacha20Xor(key, nonce, encryptedData)
}
const TLS_MAX_PLAINTEXT_FRAGMENT = 16 * 1024;
function buildTlsRecord(contentType, fragment, version = TLS_VERSION_12) {
const data = 数据转Uint8Array(fragment);
const record = new Uint8Array(5 + data.byteLength);
record[0] = contentType;
record[1] = version >> 8 & 255;
record[2] = version & 255;
record[3] = data.byteLength >> 8 & 255;
record[4] = data.byteLength & 255;
record.set(data, 5);
return record;
}
function buildHandshakeMessage(handshakeType, body) { return tlsBytes(handshakeType, (length => [length >> 16 & 255, length >> 8 & 255, 255 & length])(body.length), body) }
class TlsRecordParser {
constructor() { this.buffer = new Uint8Array(0) }
feed(chunk) {
const bytes = 数据转Uint8Array(chunk);
this.buffer = this.buffer.length ? concatBytes(this.buffer, bytes) : bytes
}
next() {
if (this.buffer.length < 5) return null;
const contentType = this.buffer[0],
version = readUint16(this.buffer, 1),
length = readUint16(this.buffer, 3);
if (this.buffer.length < 5 + length) return null;
const fragment = this.buffer.subarray(5, 5 + length);
return this.buffer = this.buffer.subarray(5 + length), { type: contentType, version, length, fragment }
}
}
class TlsHandshakeParser {
constructor() { this.buffer = new Uint8Array(0) }
feed(chunk) {
const bytes = 数据转Uint8Array(chunk);
this.buffer = this.buffer.length ? concatBytes(this.buffer, bytes) : bytes
}
next() {
if (this.buffer.length < 4) return null;
const handshakeType = this.buffer[0],
length = readUint24(this.buffer, 1);
if (this.buffer.length < 4 + length) return null;
const body = this.buffer.subarray(4, 4 + length),
raw = this.buffer.subarray(0, 4 + length);
return this.buffer = this.buffer.subarray(4 + length), { type: handshakeType, length, body, raw }
}
}
function parseServerHello(body) {
let offset = 0;
const legacyVersion = readUint16(body, offset);
offset += 2;
const serverRandom = body.slice(offset, offset + 32);
offset += 32;
const sessionIdLength = body[offset++],
sessionId = body.slice(offset, offset + sessionIdLength);
offset += sessionIdLength;
const cipherSuite = readUint16(body, offset);
offset += 2;
const compression = body[offset++];
let selectedVersion = legacyVersion,
keyShare = null,
alpn = null;
if (offset < body.length) {
const extensionsLength = readUint16(body, offset);
offset += 2;
const extensionsEnd = offset + extensionsLength;
for (; offset + 4 <= extensionsEnd;) {
const extensionType = readUint16(body, offset);
offset += 2;
const extensionLength = readUint16(body, offset);
offset += 2;
const extensionData = body.slice(offset, offset + extensionLength);
if (offset += extensionLength, extensionType === EXT_SUPPORTED_VERSIONS && extensionLength >= 2) selectedVersion = readUint16(extensionData, 0);
else if (extensionType === EXT_KEY_SHARE && extensionLength >= 4) {
const group = readUint16(extensionData, 0),
keyLength = readUint16(extensionData, 2);
keyShare = { group, key: extensionData.slice(4, 4 + keyLength) }
} else extensionType === EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION && extensionLength >= 3 && (alpn = textDecoder.decode(extensionData.slice(3, 3 + extensionData[2])))
}
}
const helloRetryRequestRandom = new Uint8Array([207, 33, 173, 116, 229, 154, 97, 17, 190, 29, 140, 2, 30, 101, 184, 145, 194, 162, 17, 22, 122, 187, 140, 94, 7, 158, 9, 226, 200, 168, 51, 156]);
return { version: legacyVersion, serverRandom, sessionId, cipherSuite, compression, selectedVersion, keyShare, alpn, isHRR: constantTimeEqual(serverRandom, helloRetryRequestRandom), isTls13: selectedVersion === TLS_VERSION_13 }
}
function parseServerKeyExchange(body) {
let offset = 1;
const namedCurve = readUint16(body, offset);
offset += 2;
const keyLength = body[offset++];
return { namedCurve, serverPublicKey: body.slice(offset, offset + keyLength) }
}
function extractLeafCertificate(body, hasContext = 0) {
let offset = 0;
if (hasContext) {
const contextLength = body[offset++];
offset += contextLength
}
if (offset + 3 > body.length) return null;
const certificateListLength = readUint24(body, offset);
if (offset += 3, !certificateListLength || offset + 3 > body.length) return null;
const certificateLength = readUint24(body, offset);
return offset += 3, certificateLength ? body.slice(offset, offset + certificateLength) : null
}
function parseEncryptedExtensions(body) {
const parsed = { alpn: null };
let offset = 2;
const extensionsEnd = 2 + readUint16(body, 0);
for (; offset + 4 <= extensionsEnd;) {
const extensionType = readUint16(body, offset);
offset += 2;
const extensionLength = readUint16(body, offset);
if (offset += 2, extensionType === EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION && extensionLength >= 3) {
const protocolLength = body[offset + 2];
protocolLength > 0 && offset + 3 + protocolLength <= offset + extensionLength && (parsed.alpn = textDecoder.decode(body.slice(offset + 3, offset + 3 + protocolLength)))
}
offset += extensionLength
}
return parsed
}
function buildClientHello(clientRandom, serverName, keyShares, { tls13: enableTls13 = !0, tls12: enableTls12 = !0, alpn = null, chacha = !0 } = {}) {
const cipherIds = [];
enableTls13 && cipherIds.push(4865, 4866, ...(chacha ? [4867] : [])), enableTls12 && cipherIds.push(49199, 49200, 49195, 49196, ...(chacha ? [52392, 52393] : []));
const cipherBytes = tlsBytes(...cipherIds.flatMap(uint16be)),
extensions = [tlsBytes(255, 1, 0, 1, 0)];
if (serverName) {
const serverNameBytes = textEncoder.encode(serverName),
serverNameList = tlsBytes(0, uint16be(serverNameBytes.length), serverNameBytes);
extensions.push(tlsBytes(uint16be(EXT_SERVER_NAME), uint16be(serverNameList.length + 2), uint16be(serverNameList.length), serverNameList))
}
extensions.push(tlsBytes(uint16be(EXT_EC_POINT_FORMATS), 0, 2, 1, 0)), extensions.push(tlsBytes(uint16be(EXT_SUPPORTED_GROUPS), 0, 6, 0, 4, 0, 29, 0, 23));
const signatureBytes = tlsBytes(...SUPPORTED_SIGNATURE_ALGORITHMS.flatMap(uint16be));
extensions.push(tlsBytes(uint16be(EXT_SIGNATURE_ALGORITHMS), uint16be(signatureBytes.length + 2), uint16be(signatureBytes.length), signatureBytes));
const protocols = Array.isArray(alpn) ? alpn.filter(Boolean) : alpn ? [alpn] : [];
if (protocols.length) {
const alpnBytes = concatBytes(...protocols.map((protocol => { const protocolBytes = textEncoder.encode(protocol); return tlsBytes(protocolBytes.length, protocolBytes) })));
extensions.push(tlsBytes(uint16be(EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION), uint16be(alpnBytes.length + 2), uint16be(alpnBytes.length), alpnBytes))
}
if (enableTls13 && keyShares) {
let keyShareBytes;
if (extensions.push(enableTls12 ? tlsBytes(uint16be(EXT_SUPPORTED_VERSIONS), 0, 5, 4, 3, 4, 3, 3) : tlsBytes(uint16be(EXT_SUPPORTED_VERSIONS), 0, 3, 2, 3, 4)), extensions.push(tlsBytes(uint16be(EXT_PSK_KEY_EXCHANGE_MODES), 0, 2, 1, 1)), keyShares?.x25519 && keyShares?.p256) keyShareBytes = concatBytes(tlsBytes(0, 29, uint16be(keyShares.x25519.length), keyShares.x25519), tlsBytes(0, 23, uint16be(keyShares.p256.length), keyShares.p256));
else if (keyShares?.x25519) keyShareBytes = tlsBytes(0, 29, uint16be(keyShares.x25519.length), keyShares.x25519);
else if (keyShares?.p256) keyShareBytes = tlsBytes(0, 23, uint16be(keyShares.p256.length), keyShares.p256);
else {
if (!(keyShares instanceof Uint8Array)) throw new Error("Invalid keyShares");
keyShareBytes = tlsBytes(0, 23, uint16be(keyShares.length), keyShares)
}
extensions.push(tlsBytes(uint16be(EXT_KEY_SHARE), uint16be(keyShareBytes.length + 2), uint16be(keyShareBytes.length), keyShareBytes))
}
const extensionsBytes = concatBytes(...extensions);
return buildHandshakeMessage(HANDSHAKE_TYPE_CLIENT_HELLO, tlsBytes(uint16be(TLS_VERSION_12), clientRandom, 0, uint16be(cipherBytes.length), cipherBytes, 1, 0, uint16be(extensionsBytes.length), extensionsBytes))
}
const uint64be = sequenceNumber => { const bytes = new Uint8Array(8); return new DataView(bytes.buffer).setBigUint64(0, sequenceNumber, !1), bytes },
xorSequenceIntoIv = (initializationVector, sequenceNumber) => {
const nonce = initializationVector.slice(),
sequenceBytes = uint64be(sequenceNumber);
for (let index = 0; index < 8; index++) nonce[nonce.length - 8 + index] ^= sequenceBytes[index];
return nonce
},
deriveTrafficKeys = (hash, secret, keyLen, ivLen) => Promise.all([hkdfExpandLabel(hash, secret, "key", EMPTY_BYTES, keyLen), hkdfExpandLabel(hash, secret, "iv", EMPTY_BYTES, ivLen)]);
class TlsClient {
constructor(socket, options = {}) {
if (this.socket = socket, this.serverName = options.serverName || "", this.supportTls13 = !1 !== options.tls13, this.supportTls12 = !1 !== options.tls12, !this.supportTls13 && !this.supportTls12) throw new Error("At least one TLS version must be enabled");
this.alpnProtocols = Array.isArray(options.alpn) ? options.alpn : options.alpn ? [options.alpn] : null, this.allowChacha = options.allowChacha !== false, this.timeout = options.timeout ?? 3e4, this.clientRandom = randomBytes(32), this.serverRandom = null, this.handshakeChunks = [], this.handshakeComplete = !1, this.negotiatedAlpn = null, this.cipherSuite = null, this.cipherConfig = null, this.isTls13 = !1, this.masterSecret = null, this.handshakeSecret = null, this.clientWriteKey = null, this.serverWriteKey = null, this.clientWriteIv = null, this.serverWriteIv = null, this.clientHandshakeKey = null, this.serverHandshakeKey = null, this.clientHandshakeIv = null, this.serverHandshakeIv = null, this.clientAppKey = null, this.serverAppKey = null, this.clientAppIv = null, this.serverAppIv = null, this.clientWriteCryptoKey = null, this.serverWriteCryptoKey = null, this.clientHandshakeCryptoKey = null, this.serverHandshakeCryptoKey = null, this.clientAppCryptoKey = null, this.serverAppCryptoKey = null, this.clientSeqNum = 0n, this.serverSeqNum = 0n, this.recordParser = new TlsRecordParser, this.handshakeParser = new TlsHandshakeParser, this.keyPairs = new Map, this.ecdhKeyPair = null, this.sawCert = !1
}
recordHandshake(chunk) { this.handshakeChunks.push(chunk) }
transcript() { return 1 === this.handshakeChunks.length ? this.handshakeChunks[0] : concatBytes(...this.handshakeChunks) }
getCipherConfig(cipherSuite) { return CIPHER_SUITES_BY_ID.get(cipherSuite) || null }
async readChunk(reader) { return this.timeout ? Promise.race([reader.read(), new Promise(((resolve, reject) => setTimeout((() => reject(new Error("TLS read timeout"))), this.timeout)))]) : reader.read() }
async readRecordsUntil(reader, predicate, closedError) {
for (; ;) {
let record;
for (; record = this.recordParser.next();)
if (await predicate(record)) return;
const { value, done } = await this.readChunk(reader);
if (done) throw new Error(closedError);
this.recordParser.feed(value)
}
}
async readHandshakeUntil(reader, predicate, closedError) {
for (let message; message = this.handshakeParser.next();)
if (await predicate(message)) return;
return this.readRecordsUntil(reader, (async record => {
if (record.type === CONTENT_TYPE_ALERT) {
if (shouldIgnoreTlsAlert(record.fragment)) return;
throw new Error(`TLS Alert: ${record.fragment[1]}`);
}
if (record.type === CONTENT_TYPE_HANDSHAKE) {
this.handshakeParser.feed(record.fragment);
for (let message; message = this.handshakeParser.next();)
if (await predicate(message)) return 1
}
}), closedError)
}
async acceptCertificate(certificate) { if (!certificate?.length) throw new Error("Empty certificate"); this.sawCert = !0 }
async handshake() {
const [p256Share, x25519Share] = await Promise.all([generateKeyShare("P-256"), generateKeyShare("X25519")]);
this.keyPairs = new Map([[23, p256Share], [29, x25519Share]]), this.ecdhKeyPair = p256Share.keyPair;
const reader = this.socket.readable.getReader(),
writer = this.socket.writable.getWriter();
try {
const clientHello = buildClientHello(this.clientRandom, this.serverName, { x25519: x25519Share.publicKeyRaw, p256: p256Share.publicKeyRaw }, { tls13: this.supportTls13, tls12: this.supportTls12, alpn: this.alpnProtocols, chacha: this.allowChacha });
this.recordHandshake(clientHello), await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, clientHello, TLS_VERSION_10));
const serverHello = await this.receiveServerHello(reader);
if (serverHello.isHRR) throw new Error("HelloRetryRequest is not supported by TLSClientMini");
if (serverHello.keyShare?.group && this.keyPairs.has(serverHello.keyShare.group)) {
const selectedKeyPair = this.keyPairs.get(serverHello.keyShare.group);
this.ecdhKeyPair = selectedKeyPair.keyPair
}
serverHello.isTls13 ? await this.handshakeTls13(reader, writer, serverHello) : await this.handshakeTls12(reader, writer), this.handshakeComplete = !0
} finally {
reader.releaseLock(), writer.releaseLock()
}
}
async receiveServerHello(reader) {
for (; ;) {
const { value, done } = await this.readChunk(reader);
if (done) throw new Error("Connection closed waiting for ServerHello");
let record;
for (this.recordParser.feed(value); record = this.recordParser.next();) {
if (record.type === CONTENT_TYPE_ALERT) {
if (shouldIgnoreTlsAlert(record.fragment)) continue;
throw new Error(`TLS Alert: level=${record.fragment[0]}, desc=${record.fragment[1]}`);
}
if (record.type !== CONTENT_TYPE_HANDSHAKE) continue;
let message;
for (this.handshakeParser.feed(record.fragment); message = this.handshakeParser.next();) {
if (message.type !== HANDSHAKE_TYPE_SERVER_HELLO) continue;
this.recordHandshake(message.raw);
const serverHello = parseServerHello(message.body);
if (this.serverRandom = serverHello.serverRandom, this.cipherSuite = serverHello.cipherSuite, this.cipherConfig = this.getCipherConfig(serverHello.cipherSuite), this.isTls13 = serverHello.isTls13, this.negotiatedAlpn = serverHello.alpn || null, !this.cipherConfig) throw new Error(`Unsupported cipher suite: 0x${serverHello.cipherSuite.toString(16)}`);
return serverHello
}
}
}
}
async handshakeTls12(reader, writer) {
/** @type {{ namedCurve: number, serverPublicKey: Uint8Array } | null} */
let serverKeyExchange = null;
let sawServerHelloDone = !1;
if (await this.readHandshakeUntil(reader, (async message => {
switch (message.type) {
case HANDSHAKE_TYPE_CERTIFICATE: {
this.recordHandshake(message.raw);
const certificate = extractLeafCertificate(message.body, 1);
if (!certificate) throw new Error("Missing TLS 1.2 certificate");
await this.acceptCertificate(certificate);
break
}
case HANDSHAKE_TYPE_SERVER_KEY_EXCHANGE:
this.recordHandshake(message.raw), serverKeyExchange = parseServerKeyExchange(message.body);
break;
case HANDSHAKE_TYPE_SERVER_HELLO_DONE:
return this.recordHandshake(message.raw), sawServerHelloDone = !0, 1;
case HANDSHAKE_TYPE_CERTIFICATE_REQUEST:
throw new Error("Client certificate is not supported");
default:
this.recordHandshake(message.raw)
}
}), "Connection closed during TLS 1.2 handshake"), !this.sawCert) throw new Error("Missing TLS 1.2 leaf certificate");
const serverKeyExchangeData = /** @type {{ namedCurve: number, serverPublicKey: Uint8Array } | null} */ (serverKeyExchange);
if (!serverKeyExchangeData) throw new Error("Missing TLS 1.2 ServerKeyExchange");
const curveName = GROUPS_BY_ID.get(serverKeyExchangeData.namedCurve);
if (!curveName) throw new Error(`Unsupported named curve: 0x${serverKeyExchangeData.namedCurve.toString(16)}`);
const keyShare = this.keyPairs.get(serverKeyExchangeData.namedCurve);
if (!keyShare) throw new Error(`Missing key pair for curve: 0x${serverKeyExchangeData.namedCurve.toString(16)}`);
const preMasterSecret = await deriveSharedSecret(keyShare.keyPair.privateKey, serverKeyExchangeData.serverPublicKey, curveName),
clientKeyExchange = buildHandshakeMessage(HANDSHAKE_TYPE_CLIENT_KEY_EXCHANGE, tlsBytes(keyShare.publicKeyRaw.length, keyShare.publicKeyRaw));
this.recordHandshake(clientKeyExchange);
const hashName = this.cipherConfig.hash;
this.masterSecret = await tls12Prf(preMasterSecret, "master secret", concatBytes(this.clientRandom, this.serverRandom), 48, hashName);
const keyLen = this.cipherConfig.keyLen,
ivLen = this.cipherConfig.ivLen,
keyBlock = await tls12Prf(this.masterSecret, "key expansion", concatBytes(this.serverRandom, this.clientRandom), 2 * keyLen + 2 * ivLen, hashName);
this.clientWriteKey = keyBlock.slice(0, keyLen), this.serverWriteKey = keyBlock.slice(keyLen, 2 * keyLen), this.clientWriteIv = keyBlock.slice(2 * keyLen, 2 * keyLen + ivLen), this.serverWriteIv = keyBlock.slice(2 * keyLen + ivLen, 2 * keyLen + 2 * ivLen);
if (!this.cipherConfig.chacha) [this.clientWriteCryptoKey, this.serverWriteCryptoKey] = await Promise.all([importAesGcmKey(this.clientWriteKey, ["encrypt"]), importAesGcmKey(this.serverWriteKey, ["decrypt"])]);
await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, clientKeyExchange)), await writer.write(buildTlsRecord(CONTENT_TYPE_CHANGE_CIPHER_SPEC, tlsBytes(1)));
const clientVerifyData = await tls12Prf(this.masterSecret, "client finished", await digestBytes(hashName, this.transcript()), 12, hashName),
finishedMessage = buildHandshakeMessage(HANDSHAKE_TYPE_FINISHED, clientVerifyData);
this.recordHandshake(finishedMessage), await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, await this.encryptTls12(finishedMessage, CONTENT_TYPE_HANDSHAKE)));
let sawChangeCipherSpec = !1;
await this.readRecordsUntil(reader, (async record => {
if (record.type === CONTENT_TYPE_ALERT) {
if (shouldIgnoreTlsAlert(record.fragment)) return;
throw new Error(`TLS Alert: ${record.fragment[1]}`);
}
if (record.type === CONTENT_TYPE_CHANGE_CIPHER_SPEC) return void (sawChangeCipherSpec = !0);
if (record.type !== CONTENT_TYPE_HANDSHAKE || !sawChangeCipherSpec) return;
const decrypted = await this.decryptTls12(record.fragment, CONTENT_TYPE_HANDSHAKE);
if (decrypted[0] !== HANDSHAKE_TYPE_FINISHED) return;
const verifyLength = readUint24(decrypted, 1),
verifyData = decrypted.slice(4, 4 + verifyLength),
expectedVerifyData = await tls12Prf(this.masterSecret, "server finished", await digestBytes(hashName, this.transcript()), 12, hashName);
if (!constantTimeEqual(verifyData, expectedVerifyData)) throw new Error("TLS 1.2 server Finished verify failed");
return 1
}), "Connection closed waiting for TLS 1.2 Finished")
}
async handshakeTls13(reader, writer, serverHello) {
const groupName = GROUPS_BY_ID.get(serverHello.keyShare?.group);
if (!groupName || !serverHello.keyShare?.key?.length) throw new Error("Missing TLS 1.3 key_share");
const hashName = this.cipherConfig.hash,
hashLen = hashByteLength(hashName),
keyLen = this.cipherConfig.keyLen,
ivLen = this.cipherConfig.ivLen,
sharedSecret = await deriveSharedSecret(this.ecdhKeyPair.privateKey, serverHello.keyShare.key, groupName),
earlySecret = await hkdfExtract(hashName, null, new Uint8Array(hashLen)),
derivedSecret = await hkdfExpandLabel(hashName, earlySecret, "derived", await digestBytes(hashName, EMPTY_BYTES), hashLen);
this.handshakeSecret = await hkdfExtract(hashName, derivedSecret, sharedSecret);
const transcriptHash = await digestBytes(hashName, this.transcript()),
clientHandshakeTrafficSecret = await hkdfExpandLabel(hashName, this.handshakeSecret, "c hs traffic", transcriptHash, hashLen),
serverHandshakeTrafficSecret = await hkdfExpandLabel(hashName, this.handshakeSecret, "s hs traffic", transcriptHash, hashLen);
[this.clientHandshakeKey, this.clientHandshakeIv] = await deriveTrafficKeys(hashName, clientHandshakeTrafficSecret, keyLen, ivLen), [this.serverHandshakeKey, this.serverHandshakeIv] = await deriveTrafficKeys(hashName, serverHandshakeTrafficSecret, keyLen, ivLen);
if (!this.cipherConfig.chacha) [this.clientHandshakeCryptoKey, this.serverHandshakeCryptoKey] = await Promise.all([importAesGcmKey(this.clientHandshakeKey, ["encrypt"]), importAesGcmKey(this.serverHandshakeKey, ["decrypt"])]);
const serverFinishedKey = await hkdfExpandLabel(hashName, serverHandshakeTrafficSecret, "finished", EMPTY_BYTES, hashLen);
let serverFinishedReceived = !1;
const handleHandshakeMessage = async message => {
switch (message.type) {
case HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS: {
const encryptedExtensions = parseEncryptedExtensions(message.body);
encryptedExtensions.alpn && (this.negotiatedAlpn = encryptedExtensions.alpn), this.recordHandshake(message.raw);
break
}
case HANDSHAKE_TYPE_CERTIFICATE: {
const certificate = extractLeafCertificate(message.body);
if (!certificate) throw new Error("Missing TLS 1.3 certificate");
await this.acceptCertificate(certificate), this.recordHandshake(message.raw);
break
}
case HANDSHAKE_TYPE_CERTIFICATE_REQUEST:
throw new Error("Client certificate is not supported");
case HANDSHAKE_TYPE_CERTIFICATE_VERIFY:
this.recordHandshake(message.raw);
break;
case HANDSHAKE_TYPE_FINISHED: {
const expectedVerifyData = await hmac(hashName, serverFinishedKey, await digestBytes(hashName, this.transcript()));
if (!constantTimeEqual(expectedVerifyData, message.body)) throw new Error("TLS 1.3 server Finished verify failed");
this.recordHandshake(message.raw), serverFinishedReceived = !0;
break
}
default:
this.recordHandshake(message.raw)
}
};
await this.readRecordsUntil(reader, (async record => {
if (record.type === CONTENT_TYPE_CHANGE_CIPHER_SPEC || record.type === CONTENT_TYPE_HANDSHAKE) return;
if (record.type === CONTENT_TYPE_ALERT) {
if (shouldIgnoreTlsAlert(record.fragment)) return;
throw new Error(`TLS Alert: ${record.fragment[1]}`);
}
if (record.type !== CONTENT_TYPE_APPLICATION_DATA) return;
const decrypted = await this.decryptTls13Handshake(record.fragment),
innerType = decrypted[decrypted.length - 1],
plaintext = decrypted.slice(0, -1);
if (innerType === CONTENT_TYPE_HANDSHAKE) {
this.handshakeParser.feed(plaintext);
for (let message; message = this.handshakeParser.next();)
if (await handleHandshakeMessage(message), serverFinishedReceived) return 1
}
}), "Connection closed during TLS 1.3 handshake");
const applicationTranscriptHash = await digestBytes(hashName, this.transcript()),
masterDerivedSecret = await hkdfExpandLabel(hashName, this.handshakeSecret, "derived", await digestBytes(hashName, EMPTY_BYTES), hashLen),
masterSecret = await hkdfExtract(hashName, masterDerivedSecret, new Uint8Array(hashLen)),
clientAppTrafficSecret = await hkdfExpandLabel(hashName, masterSecret, "c ap traffic", applicationTranscriptHash, hashLen),
serverAppTrafficSecret = await hkdfExpandLabel(hashName, masterSecret, "s ap traffic", applicationTranscriptHash, hashLen);
[this.clientAppKey, this.clientAppIv] = await deriveTrafficKeys(hashName, clientAppTrafficSecret, keyLen, ivLen), [this.serverAppKey, this.serverAppIv] = await deriveTrafficKeys(hashName, serverAppTrafficSecret, keyLen, ivLen);
if (!this.cipherConfig.chacha) [this.clientAppCryptoKey, this.serverAppCryptoKey] = await Promise.all([importAesGcmKey(this.clientAppKey, ["encrypt"]), importAesGcmKey(this.serverAppKey, ["decrypt"])]);
const clientFinishedKey = await hkdfExpandLabel(hashName, clientHandshakeTrafficSecret, "finished", EMPTY_BYTES, hashLen),
clientFinishedVerifyData = await hmac(hashName, clientFinishedKey, await digestBytes(hashName, this.transcript())),
clientFinishedMessage = buildHandshakeMessage(HANDSHAKE_TYPE_FINISHED, clientFinishedVerifyData);
this.recordHandshake(clientFinishedMessage), await writer.write(buildTlsRecord(CONTENT_TYPE_APPLICATION_DATA, await this.encryptTls13Handshake(concatBytes(clientFinishedMessage, [CONTENT_TYPE_HANDSHAKE])))), this.clientSeqNum = 0n, this.serverSeqNum = 0n
}
async encryptTls12(plaintext, contentType) {
const sequenceNumber = this.clientSeqNum++,
sequenceBytes = uint64be(sequenceNumber),
additionalData = concatBytes(sequenceBytes, [contentType], uint16be(TLS_VERSION_12), uint16be(plaintext.length));
if (this.cipherConfig.chacha) {
const nonce = xorSequenceIntoIv(this.clientWriteIv, sequenceNumber);
return chacha20Poly1305Encrypt(this.clientWriteKey, nonce, plaintext, additionalData)
}
const explicitNonce = randomBytes(8);
if (!this.clientWriteCryptoKey) this.clientWriteCryptoKey = await importAesGcmKey(this.clientWriteKey, ["encrypt"]);
return concatBytes(explicitNonce, await aesGcmEncryptWithKey(this.clientWriteCryptoKey, concatBytes(this.clientWriteIv, explicitNonce), plaintext, additionalData))
}
async decryptTls12(ciphertext, contentType) {
const sequenceNumber = this.serverSeqNum++,
sequenceBytes = uint64be(sequenceNumber);
if (this.cipherConfig.chacha) {
const nonce = xorSequenceIntoIv(this.serverWriteIv, sequenceNumber);
return chacha20Poly1305Decrypt(this.serverWriteKey, nonce, ciphertext, concatBytes(sequenceBytes, [contentType], uint16be(TLS_VERSION_12), uint16be(ciphertext.length - 16)))
}
const explicitNonce = ciphertext.subarray(0, 8),
encryptedData = ciphertext.subarray(8);
if (!this.serverWriteCryptoKey) this.serverWriteCryptoKey = await importAesGcmKey(this.serverWriteKey, ["decrypt"]);
return aesGcmDecryptWithKey(this.serverWriteCryptoKey, concatBytes(this.serverWriteIv, explicitNonce), encryptedData, concatBytes(sequenceBytes, [contentType], uint16be(TLS_VERSION_12), uint16be(encryptedData.length - 16)))
}
async encryptTls13Handshake(plaintext) {
const nonce = xorSequenceIntoIv(this.clientHandshakeIv, this.clientSeqNum++),
additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(plaintext.length + 16));
if (this.cipherConfig.chacha) return chacha20Poly1305Encrypt(this.clientHandshakeKey, nonce, plaintext, additionalData);
if (!this.clientHandshakeCryptoKey) this.clientHandshakeCryptoKey = await importAesGcmKey(this.clientHandshakeKey, ["encrypt"]);
return aesGcmEncryptWithKey(this.clientHandshakeCryptoKey, nonce, plaintext, additionalData)
}
async decryptTls13Handshake(ciphertext) {
const nonce = xorSequenceIntoIv(this.serverHandshakeIv, this.serverSeqNum++),
additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(ciphertext.length));
const decrypted = this.cipherConfig.chacha ? await chacha20Poly1305Decrypt(this.serverHandshakeKey, nonce, ciphertext, additionalData) : await aesGcmDecryptWithKey(this.serverHandshakeCryptoKey || (this.serverHandshakeCryptoKey = await importAesGcmKey(this.serverHandshakeKey, ["decrypt"])), nonce, ciphertext, additionalData);
let innerTypeIndex = decrypted.length - 1;
for (; innerTypeIndex >= 0 && !decrypted[innerTypeIndex];) innerTypeIndex--;
return innerTypeIndex < 0 ? EMPTY_BYTES : decrypted.slice(0, innerTypeIndex + 1)
}
async encryptTls13(data) {
const plaintext = concatBytes(data, [CONTENT_TYPE_APPLICATION_DATA]),
nonce = xorSequenceIntoIv(this.clientAppIv, this.clientSeqNum++),
additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(plaintext.length + 16));
if (this.cipherConfig.chacha) return chacha20Poly1305Encrypt(this.clientAppKey, nonce, plaintext, additionalData);
if (!this.clientAppCryptoKey) this.clientAppCryptoKey = await importAesGcmKey(this.clientAppKey, ["encrypt"]);
return aesGcmEncryptWithKey(this.clientAppCryptoKey, nonce, plaintext, additionalData)
}
async decryptTls13(ciphertext) {
const nonce = xorSequenceIntoIv(this.serverAppIv, this.serverSeqNum++),
additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(ciphertext.length)),
plaintext = this.cipherConfig.chacha ? await chacha20Poly1305Decrypt(this.serverAppKey, nonce, ciphertext, additionalData) : await aesGcmDecryptWithKey(this.serverAppCryptoKey || (this.serverAppCryptoKey = await importAesGcmKey(this.serverAppKey, ["decrypt"])), nonce, ciphertext, additionalData);
let innerTypeIndex = plaintext.length - 1;
for (; innerTypeIndex >= 0 && !plaintext[innerTypeIndex];) innerTypeIndex--;
if (innerTypeIndex < 0) return {
data: EMPTY_BYTES,
type: 0
};
return {
data: plaintext.slice(0, innerTypeIndex),
type: plaintext[innerTypeIndex]
}
}
async write(data) {
if (!this.handshakeComplete) throw new Error("Handshake not complete");
const plaintext = 数据转Uint8Array(data);
if (!plaintext.byteLength) return;
const writer = this.socket.writable.getWriter();
try {
const records = [];
for (let offset = 0; offset < plaintext.byteLength; offset += TLS_MAX_PLAINTEXT_FRAGMENT) {
const chunk = plaintext.subarray(offset, Math.min(offset + TLS_MAX_PLAINTEXT_FRAGMENT, plaintext.byteLength));
const encrypted = this.isTls13 ? await this.encryptTls13(chunk) : await this.encryptTls12(chunk, CONTENT_TYPE_APPLICATION_DATA);
records.push(buildTlsRecord(CONTENT_TYPE_APPLICATION_DATA, encrypted));
}
await writer.write(records.length === 1 ? records[0] : concatBytes(...records))
} finally {
writer.releaseLock()
}
}
async read() {
for (; ;) {
let record;
for (; record = this.recordParser.next();) {
if (record.type === CONTENT_TYPE_ALERT) {
if (record.fragment[1] === ALERT_CLOSE_NOTIFY) return null;
throw new Error(`TLS Alert: ${record.fragment[1]}`)
}
if (record.type !== CONTENT_TYPE_APPLICATION_DATA) continue;
if (!this.isTls13) return this.decryptTls12(record.fragment, CONTENT_TYPE_APPLICATION_DATA);
const { data, type } = await this.decryptTls13(record.fragment);
if (type === CONTENT_TYPE_APPLICATION_DATA) return data;
if (type === CONTENT_TYPE_ALERT) {
if (data[1] === ALERT_CLOSE_NOTIFY) return null;
throw new Error(`TLS Alert: ${data[1]}`)
}
if (type !== CONTENT_TYPE_HANDSHAKE) continue;
let message;
for (this.handshakeParser.feed(data); message = this.handshakeParser.next();)
if (message.type !== HANDSHAKE_TYPE_NEW_SESSION_TICKET && message.type === HANDSHAKE_TYPE_KEY_UPDATE) throw new Error("TLS 1.3 KeyUpdate is not supported by TLSClientMini")
}
const reader = this.socket.readable.getReader();
try {
const { value, done } = await this.readChunk(reader);
if (done) return null;
this.recordParser.feed(value)
} finally {
reader.releaseLock()
}
}
}
close() { this.socket.close() }
}
function stripIPv6Brackets(hostname = '') {
const host = String(hostname || '').trim();
return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
}
function isIPHostname(hostname = '') {
const host = stripIPv6Brackets(hostname);
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
if (ipv4Regex.test(host)) return true;
if (!host.includes(':')) return false;
try {
new URL(`http://[${host}]/`);
return true;
} catch (e) {
return false;
}
}
//////////////////////////////////////////////////turnConnect///////////////////////////////////////////////
const CONNECT_TIMEOUT_MS = 9999;
const TURN_STUN_MAGIC_COOKIE = new Uint8Array([0x21, 0x12, 0xa4, 0x42]);
const TURN_STUN_TYPE = {
ALLOCATE_REQUEST: 0x0003, ALLOCATE_SUCCESS: 0x0103, ALLOCATE_ERROR: 0x0113,
CREATE_PERMISSION_REQUEST: 0x0008, CREATE_PERMISSION_SUCCESS: 0x0108,
CONNECT_REQUEST: 0x000a, CONNECT_SUCCESS: 0x010a,
CONNECTION_BIND_REQUEST: 0x000b, CONNECTION_BIND_SUCCESS: 0x010b
};
const TURN_STUN_ATTR = {
USERNAME: 0x0006, MESSAGE_INTEGRITY: 0x0008, ERROR_CODE: 0x0009,
XOR_PEER_ADDRESS: 0x0012, REALM: 0x0014, NONCE: 0x0015,
REQUESTED_TRANSPORT: 0x0019, CONNECTION_ID: 0x002a
};
async function withTimeout(promise, timeoutMs, message) {
let timer;
try {
return await Promise.race([
promise,
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs) })
]);
} finally {
clearTimeout(timer);
}
}
function isIPv4(value) {
const parts = String(value || '').split('.');
return parts.length === 4 && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255);
}
function turnStunPadding(length) {
return -length & 3;
}
function createTurnStunAttribute(type, value) {
const body = 数据转Uint8Array(value);
const attribute = new Uint8Array(4 + body.byteLength + turnStunPadding(body.byteLength));
const view = new DataView(attribute.buffer);
view.setUint16(0, type);
view.setUint16(2, body.byteLength);
attribute.set(body, 4);
return attribute;
}
function createTurnStunMessage(type, transactionId, attributes) {
const body = 拼接字节数据(...attributes);
const header = new Uint8Array(20);
const view = new DataView(header.buffer);
view.setUint16(0, type);
view.setUint16(2, body.byteLength);
header.set(TURN_STUN_MAGIC_COOKIE, 4);
header.set(transactionId, 8);
return 拼接字节数据(header, body);
}
function parseTurnErrorCode(data) {
return data?.byteLength >= 4 ? (data[2] & 7) * 100 + data[3] : 0;
}
function randomTurnTransactionId() {
return crypto.getRandomValues(new Uint8Array(12));
}
async function addTurnMessageIntegrity(message, key) {
const signedMessage = new Uint8Array(message);
const view = new DataView(signedMessage.buffer);
view.setUint16(2, view.getUint16(2) + 24);
const hmacKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const signature = await crypto.subtle.sign('HMAC', hmacKey, signedMessage);
return 拼接字节数据(signedMessage, createTurnStunAttribute(TURN_STUN_ATTR.MESSAGE_INTEGRITY, new Uint8Array(signature)));
}
async function readTurnStunMessage(reader, bufferedData = null, timeoutMessage = 'TURN response timed out') {
let buffer = 有效数据长度(bufferedData) ? 数据转Uint8Array(bufferedData) : new Uint8Array(0);
const pull = async () => {
const { done, value } = await withTimeout(reader.read(), CONNECT_TIMEOUT_MS, timeoutMessage);
if (done) throw new Error('TURN server closed connection');
if (value?.byteLength) buffer = 拼接字节数据(buffer, value);
};
while (buffer.byteLength < 20) await pull();
const messageLength = 20 + ((buffer[2] << 8) | buffer[3]);
if (messageLength > 65555) throw new Error('TURN response is too large');
while (buffer.byteLength < messageLength) await pull();
const messageBuffer = buffer.subarray(0, messageLength);
if (TURN_STUN_MAGIC_COOKIE.some((value, index) => messageBuffer[4 + index] !== value)) throw new Error('Invalid TURN/STUN response');
const view = new DataView(messageBuffer.buffer, messageBuffer.byteOffset, messageBuffer.byteLength);
const attributes = {};
for (let offset = 20; offset + 4 <= messageLength;) {
const type = view.getUint16(offset);
const length = view.getUint16(offset + 2);
if (offset + 4 + length > messageBuffer.byteLength) break;
attributes[type] = messageBuffer.slice(offset + 4, offset + 4 + length);
offset += 4 + length + turnStunPadding(length);
}
return {
message: { type: view.getUint16(0), attributes },
extraData: buffer.byteLength > messageLength ? buffer.subarray(messageLength) : null
};
}
async function writeTurnBytes(writer, bytes, timeoutMessage) {
await withTimeout(writer.write(bytes), CONNECT_TIMEOUT_MS, timeoutMessage);
}
async function turnConnect(proxy, targetHost, targetPort, TCP连接) {
proxy = { ...proxy, username: proxy.username ?? null, password: proxy.password ?? null };
const resolvedTargetHost = stripIPv6Brackets(targetHost);
/** @type {string | null} */
let targetIp = isIPv4(resolvedTargetHost) ? resolvedTargetHost : null;
if (!targetIp) {
const records = await DoH查询(resolvedTargetHost, 'A');
const recordData = records.find(item => item.type === 1 && isIPv4(item.data))?.data;
targetIp = typeof recordData === 'string' ? recordData : null;
}
if (!targetIp) throw new Error(`Could not resolve ${targetHost} to an IPv4 address for TURN CONNECT`);
const turnHost = stripIPv6Brackets(proxy.hostname);
let controlSocket = null, dataSocket = null, controlWriter = null, controlReader = null, dataWriter = null, dataReader = null, dataReaderReleased = false;
const close = () => {
try { controlSocket?.close?.() } catch (e) { }
try { dataSocket?.close?.() } catch (e) { }
};
const releaseDataReader = () => {
if (dataReaderReleased) return;
dataReaderReleased = true;
try { dataReader?.releaseLock?.() } catch (e) { }
};
try {
controlSocket = TCP连接({ hostname: turnHost, port: proxy.port });
await withTimeout(controlSocket.opened, CONNECT_TIMEOUT_MS, 'TURN server connection timed out');
controlWriter = controlSocket.writable.getWriter();
controlReader = controlSocket.readable.getReader();
const xorPeerAddress = new Uint8Array(8);
xorPeerAddress[1] = 1;
new DataView(xorPeerAddress.buffer).setUint16(2, targetPort ^ 0x2112);
targetIp.split('.').forEach((value, index) => {
xorPeerAddress[4 + index] = Number(value) ^ TURN_STUN_MAGIC_COOKIE[index];
});
const peerAddress = createTurnStunAttribute(TURN_STUN_ATTR.XOR_PEER_ADDRESS, xorPeerAddress);
const requestedTransport = new Uint8Array([6, 0, 0, 0]);
await writeTurnBytes(controlWriter, createTurnStunMessage(
TURN_STUN_TYPE.ALLOCATE_REQUEST,
randomTurnTransactionId(),
[createTurnStunAttribute(TURN_STUN_ATTR.REQUESTED_TRANSPORT, requestedTransport)]
), 'TURN Allocate request timed out');
let turnResponse = await readTurnStunMessage(controlReader, null, 'TURN Allocate response timed out');
let message = turnResponse.message;
let bufferedData = turnResponse.extraData;
let integrityKey = null;
let authAttributes = [];
const sign = messageToSign => integrityKey ? addTurnMessageIntegrity(messageToSign, integrityKey) : Promise.resolve(messageToSign);
if (
message.type === TURN_STUN_TYPE.ALLOCATE_ERROR
&& proxy.username !== null
&& proxy.password !== null
&& parseTurnErrorCode(message.attributes[TURN_STUN_ATTR.ERROR_CODE]) === 401
) {
const realmBytes = message.attributes[TURN_STUN_ATTR.REALM];
const nonce = message.attributes[TURN_STUN_ATTR.NONCE];
if (!realmBytes || !nonce?.byteLength) throw new Error('TURN authentication challenge is missing realm or nonce');
const realm = textDecoder.decode(realmBytes);
integrityKey = new Uint8Array(await crypto.subtle.digest('MD5', textEncoder.encode(`${proxy.username}:${realm}:${proxy.password}`)));
authAttributes = [
createTurnStunAttribute(TURN_STUN_ATTR.USERNAME, textEncoder.encode(proxy.username)),
createTurnStunAttribute(TURN_STUN_ATTR.REALM, textEncoder.encode(realm)),
createTurnStunAttribute(TURN_STUN_ATTR.NONCE, nonce)
];
const allocateRequest = await addTurnMessageIntegrity(createTurnStunMessage(
TURN_STUN_TYPE.ALLOCATE_REQUEST,
randomTurnTransactionId(),
[
createTurnStunAttribute(TURN_STUN_ATTR.REQUESTED_TRANSPORT, requestedTransport),
...authAttributes
]
), integrityKey);
const pipelinedMessages = await Promise.all([
sign(createTurnStunMessage(TURN_STUN_TYPE.CREATE_PERMISSION_REQUEST, randomTurnTransactionId(), [peerAddress, ...authAttributes])),
sign(createTurnStunMessage(TURN_STUN_TYPE.CONNECT_REQUEST, randomTurnTransactionId(), [peerAddress, ...authAttributes]))
]);
await writeTurnBytes(controlWriter, 拼接字节数据(allocateRequest, ...pipelinedMessages), 'TURN authenticated Allocate request timed out');
turnResponse = await readTurnStunMessage(controlReader, bufferedData, 'TURN authenticated Allocate response timed out');
message = turnResponse.message;
bufferedData = turnResponse.extraData;
} else if (message.type === TURN_STUN_TYPE.ALLOCATE_SUCCESS) {
const pipelinedMessages = await Promise.all([
sign(createTurnStunMessage(TURN_STUN_TYPE.CREATE_PERMISSION_REQUEST, randomTurnTransactionId(), [peerAddress, ...authAttributes])),
sign(createTurnStunMessage(TURN_STUN_TYPE.CONNECT_REQUEST, randomTurnTransactionId(), [peerAddress, ...authAttributes]))
]);
if (pipelinedMessages.length) await writeTurnBytes(controlWriter, 拼接字节数据(...pipelinedMessages), 'TURN pipelined request timed out');
}
if (message.type !== TURN_STUN_TYPE.ALLOCATE_SUCCESS) {
const errorCode = parseTurnErrorCode(message.attributes[TURN_STUN_ATTR.ERROR_CODE]);
throw new Error(errorCode ? `TURN Allocate failed with ${errorCode}` : 'TURN Allocate failed');
}
dataSocket = TCP连接({ hostname: turnHost, port: proxy.port });
turnResponse = await readTurnStunMessage(controlReader, bufferedData, 'TURN CreatePermission response timed out');
message = turnResponse.message;
bufferedData = turnResponse.extraData;
if (message.type !== TURN_STUN_TYPE.CREATE_PERMISSION_SUCCESS) throw new Error('TURN CreatePermission failed');
turnResponse = await readTurnStunMessage(controlReader, bufferedData, 'TURN CONNECT response timed out');
message = turnResponse.message;
bufferedData = turnResponse.extraData;
if (message.type !== TURN_STUN_TYPE.CONNECT_SUCCESS || !message.attributes[TURN_STUN_ATTR.CONNECTION_ID]) throw new Error('TURN CONNECT failed');
await withTimeout(dataSocket.opened, CONNECT_TIMEOUT_MS, 'TURN data connection timed out');
dataWriter = dataSocket.writable.getWriter();
dataReader = dataSocket.readable.getReader();
await writeTurnBytes(dataWriter, await sign(createTurnStunMessage(
TURN_STUN_TYPE.CONNECTION_BIND_REQUEST,
randomTurnTransactionId(),
[
createTurnStunAttribute(TURN_STUN_ATTR.CONNECTION_ID, message.attributes[TURN_STUN_ATTR.CONNECTION_ID]),
...authAttributes
]
)), 'TURN ConnectionBind request timed out');
turnResponse = await readTurnStunMessage(dataReader, null, 'TURN ConnectionBind response timed out');
message = turnResponse.message;
const extraPayload = turnResponse.extraData;
if (message.type !== TURN_STUN_TYPE.CONNECTION_BIND_SUCCESS) throw new Error('TURN ConnectionBind failed');
controlWriter.releaseLock();
controlWriter = null;
controlReader.releaseLock();
controlReader = null;
dataWriter.releaseLock();
dataWriter = null;
const readable = new ReadableStream({
start(controller) {
if (extraPayload?.byteLength) controller.enqueue(extraPayload);
},
pull(controller) {
return dataReader.read().then(({ done, value }) => {
if (done) {
releaseDataReader();
controller.close();
} else if (value?.byteLength) controller.enqueue(new Uint8Array(value));
});
},
cancel() {
try { dataReader?.cancel?.() } catch (e) { }
releaseDataReader();
close();
}
});
return { readable, writable: dataSocket.writable, closed: dataSocket.closed, close };
} catch (error) {
try { controlWriter?.releaseLock?.() } catch (e) { }
try { controlReader?.releaseLock?.() } catch (e) { }
try { dataWriter?.releaseLock?.() } catch (e) { }
releaseDataReader();
close();
throw error;
}
}
//////////////////////////////////////////////////sstpConnect///////////////////////////////////////////////
const SSTP_TCP_MSS = 1400;
const SSTP_EMPTY_BYTES = new Uint8Array(0);
function readSstpUint16(bytes, offset = 0) {
return (bytes[offset] << 8) | bytes[offset + 1];
}
function readSstpUint32(bytes, offset = 0) {
return ((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0;
}
function randomSstpUint16() {
return readSstpUint16(crypto.getRandomValues(new Uint8Array(2)));
}
function internetChecksum(bytes, offset, length) {
let sum = 0;
for (let index = offset; index < offset + length - 1; index += 2) sum += readSstpUint16(bytes, index);
if (length & 1) sum += bytes[offset + length - 1] << 8;
while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
return (~sum) & 0xffff;
}
async function sstpConnect(proxy, targetHost, targetPort, TCP连接) {
proxy = { ...proxy, username: proxy.username ?? null, password: proxy.password ?? null };
let bufferedBytes = SSTP_EMPTY_BYTES, pppIdentifier = 1, socket = null, reader = null, writer = null;
let closedSettled = false, resolveClosed, rejectClosed;
const closed = new Promise((resolve, reject) => {
resolveClosed = resolve;
rejectClosed = reject;
});
const settleClosed = (settle, value) => {
if (closedSettled) return;
closedSettled = true;
settle(value);
};
const close = () => {
try { reader?.cancel?.().catch?.(() => { }) } catch (e) { }
try { reader?.releaseLock?.() } catch (e) { }
try { writer?.close?.().catch?.(() => { }) } catch (e) { }
try { writer?.releaseLock?.() } catch (e) { }
try { socket?.close?.() } catch (e) { }
settleClosed(resolveClosed);
};
const readSocketChunk = async () => {
const { value, done } = await reader.read();
if (done || !value) throw new Error('SSTP socket closed');
return 数据转Uint8Array(value);
};
const readBytes = async length => {
while (bufferedBytes.byteLength < length) {
const chunk = await readSocketChunk();
bufferedBytes = bufferedBytes.byteLength ? 拼接字节数据(bufferedBytes, chunk) : chunk;
}
const result = bufferedBytes.subarray(0, length);
bufferedBytes = bufferedBytes.subarray(length);
return result;
};
const readHttpLine = async () => {
for (; ;) {
const lineEnd = bufferedBytes.indexOf(10);
if (lineEnd >= 0) {
const line = textDecoder.decode(bufferedBytes.subarray(0, lineEnd));
bufferedBytes = bufferedBytes.subarray(lineEnd + 1);
return line.replace(/\r$/, '');
}
const chunk = await readSocketChunk();
bufferedBytes = bufferedBytes.byteLength ? 拼接字节数据(bufferedBytes, chunk) : chunk;
}
};
const readPacket = async (timeoutMs = CONNECT_TIMEOUT_MS) => {
const header = await withTimeout(readBytes(4), timeoutMs, 'SSTP read timeout');
const length = readSstpUint16(header, 2) & 0x0fff;
if (length < 4) throw new Error('Invalid SSTP packet length');
return {
isControl: (header[1] & 1) !== 0,
body: length > 4 ? await withTimeout(readBytes(length - 4), timeoutMs, 'SSTP packet body read timeout') : SSTP_EMPTY_BYTES
};
};
const buildSstpDataPacket = pppFrame => {
const packetLength = 6 + pppFrame.byteLength;
const packet = new Uint8Array(packetLength);
packet.set([0x10, 0x00, ((packetLength >> 8) & 0x0f) | 0x80, packetLength & 0xff, 0xff, 0x03]);
packet.set(pppFrame, 6);
return packet;
};
const buildPppConfigurePacket = (protocol, code, id, options = []) => {
const optionsLength = options.reduce((size, option) => size + 2 + option.data.byteLength, 0);
const frame = new Uint8Array(6 + optionsLength);
const view = new DataView(frame.buffer);
view.setUint16(0, protocol);
frame[2] = code;
frame[3] = id;
view.setUint16(4, 4 + optionsLength);
options.reduce((offset, option) => {
frame[offset] = option.type;
frame[offset + 1] = 2 + option.data.byteLength;
frame.set(option.data, offset + 2);
return offset + 2 + option.data.byteLength;
}, 6);
return frame;
};
const parsePPPFrame = data => {
const offset = data.byteLength >= 2 && data[0] === 0xff && data[1] === 0x03 ? 2 : 0;
if (data.byteLength - offset < 4) return null;
const protocol = readSstpUint16(data, offset);
if (protocol === 0x0021) return { protocol, ipPacket: data.subarray(offset + 2) };
if (data.byteLength - offset < 6) return null;
return { protocol, code: data[offset + 2], id: data[offset + 3], payload: data.subarray(offset + 6), rawPacket: data.subarray(offset) };
};
const parsePppOptions = data => {
const options = [];
for (let offset = 0; offset + 2 <= data.byteLength;) {
const type = data[offset];
const length = data[offset + 1];
if (length < 2 || offset + length > data.byteLength) break;
options.push({ type, data: data.subarray(offset + 2, offset + length) });
offset += length;
}
return options;
};
try {
const serverHost = stripIPv6Brackets(proxy.hostname);
const serverPort = proxy.port;
socket = TCP连接({ hostname: serverHost, port: serverPort }, { secureTransport: 'on', allowHalfOpen: false });
await withTimeout(socket.opened, CONNECT_TIMEOUT_MS, 'SSTP server connection timed out');
reader = socket.readable.getReader();
writer = socket.writable.getWriter();
const displayHost = serverHost.includes(':') ? `[${serverHost}]` : serverHost;
const httpRequest = textEncoder.encode(
`SSTP_DUPLEX_POST /sra_{BA195980-CD49-458b-9E23-C84EE0ADCD75}/ HTTP/1.1\r\n`
+ `Host: ${Number(serverPort) === 443 ? displayHost : `${displayHost}:${serverPort}`}\r\n`
+ 'Content-Length: 18446744073709551615\r\n'
+ `SSTPCORRELATIONID: {${crypto.randomUUID()}}\r\n\r\n`
);
const encapsulatedProtocol = new Uint8Array(2);
new DataView(encapsulatedProtocol.buffer).setUint16(0, 1);
const maximumReceiveUnit = new Uint8Array(2);
new DataView(maximumReceiveUnit.buffer).setUint16(0, 1500);
const sstpConnectRequest = new Uint8Array(12 + encapsulatedProtocol.byteLength);
const sstpConnectView = new DataView(sstpConnectRequest.buffer);
sstpConnectRequest[0] = 0x10;
sstpConnectRequest[1] = 0x01;
sstpConnectView.setUint16(2, sstpConnectRequest.byteLength | 0x8000);
sstpConnectView.setUint16(4, 0x0001);
sstpConnectView.setUint16(6, 1);
sstpConnectRequest[9] = 1;
sstpConnectView.setUint16(10, 4 + encapsulatedProtocol.byteLength);
sstpConnectRequest.set(encapsulatedProtocol, 12);
await withTimeout(writer.write(拼接字节数据(
httpRequest,
sstpConnectRequest,
buildSstpDataPacket(buildPppConfigurePacket(0xc021, 1, pppIdentifier++, [
{ type: 1, data: maximumReceiveUnit }
]))
)), CONNECT_TIMEOUT_MS, 'SSTP HTTP handshake request timed out');
const statusLine = await withTimeout(readHttpLine(), CONNECT_TIMEOUT_MS, 'SSTP HTTP handshake timed out');
for (; ;) {
const line = await withTimeout(readHttpLine(), CONNECT_TIMEOUT_MS, 'SSTP HTTP header read timed out');
if (line === '') break;
}
if (!/HTTP\/\d(?:\.\d)?\s+2\d\d/i.test(statusLine)) throw new Error(`SSTP HTTP handshake failed: ${statusLine || 'invalid status'}`);
let localLcpAcked = false, peerLcpAcked = false, papRequired = false, papSent = false, papDone = false, ipcpStarted = false, ipcpFinished = false, sourceIp = null;
const sendPapIfReady = async () => {
if (!localLcpAcked || !peerLcpAcked || !papRequired || papSent) return;
if (proxy.username === null || proxy.password === null) throw new Error('SSTP server requires PAP authentication');
const username = textEncoder.encode(proxy.username);
const password = textEncoder.encode(proxy.password);
if (username.byteLength > 255 || password.byteLength > 255) throw new Error('SSTP username/password is too long');
const papLength = 6 + username.byteLength + password.byteLength;
const frame = new Uint8Array(2 + papLength);
const view = new DataView(frame.buffer);
view.setUint16(0, 0xc023);
frame[2] = 1;
frame[3] = pppIdentifier++;
view.setUint16(4, papLength);
frame[6] = username.byteLength;
frame.set(username, 7);
frame[7 + username.byteLength] = password.byteLength;
frame.set(password, 8 + username.byteLength);
await withTimeout(writer.write(buildSstpDataPacket(frame)), CONNECT_TIMEOUT_MS, 'SSTP PAP authentication request timed out');
papSent = true;
};
const startIpcpIfReady = async () => {
if (!localLcpAcked || !peerLcpAcked || ipcpStarted || (papRequired && !papDone)) return;
await withTimeout(writer.write(buildSstpDataPacket(buildPppConfigurePacket(0x8021, 1, pppIdentifier++, [
{ type: 3, data: new Uint8Array(4) }
]))), CONNECT_TIMEOUT_MS, 'SSTP IPCP request timed out');
ipcpStarted = true;
};
for (let round = 0; round < 50 && !ipcpFinished; round++) {
const packet = await readPacket(CONNECT_TIMEOUT_MS);
if (packet.isControl) continue;
const ppp = parsePPPFrame(packet.body);
if (!ppp) continue;
if (ppp.protocol === 0xc021) {
if (ppp.code === 1) {
const authOption = parsePppOptions(ppp.payload).find(option => option.type === 3);
if (authOption?.data?.byteLength >= 2) {
const authProtocol = readSstpUint16(authOption.data);
if (authProtocol !== 0xc023) throw new Error(`SSTP unsupported PPP authentication protocol: 0x${authProtocol.toString(16)}`);
papRequired = true;
}
const ack = new Uint8Array(ppp.rawPacket);
ack[2] = 2;
await withTimeout(writer.write(buildSstpDataPacket(ack)), CONNECT_TIMEOUT_MS, 'SSTP LCP Configure-Ack timed out');
peerLcpAcked = true;
await sendPapIfReady();
await startIpcpIfReady();
} else if (ppp.code === 2) {
localLcpAcked = true;
await sendPapIfReady();
await startIpcpIfReady();
}
continue;
}
if (ppp.protocol === 0xc023) {
if (ppp.code === 2) {
papDone = true;
await startIpcpIfReady();
} else if (ppp.code === 3) throw new Error('SSTP PAP authentication failed');
continue;
}
if (ppp.protocol === 0x8021) {
if (ppp.code === 1) {
const ack = new Uint8Array(ppp.rawPacket);
ack[2] = 2;
await withTimeout(writer.write(buildSstpDataPacket(ack)), CONNECT_TIMEOUT_MS, 'SSTP IPCP Configure-Ack timed out');
await startIpcpIfReady();
} else if (ppp.code === 3) {
const addressOption = parsePppOptions(ppp.payload).find(option => option.type === 3);
if (addressOption?.data?.byteLength === 4) {
sourceIp = [...addressOption.data].join('.');
await withTimeout(writer.write(buildSstpDataPacket(buildPppConfigurePacket(0x8021, 1, pppIdentifier++, [
{ type: 3, data: addressOption.data }
]))), CONNECT_TIMEOUT_MS, 'SSTP IPCP address request timed out');
ipcpStarted = true;
}
} else if (ppp.code === 2) {
const addressOption = parsePppOptions(ppp.payload).find(option => option.type === 3);
if (addressOption?.data?.byteLength === 4) sourceIp = [...addressOption.data].join('.');
ipcpFinished = true;
}
}
}
if (!sourceIp) throw new Error('SSTP did not assign an IPv4 address');
const target = stripIPv6Brackets(targetHost);
/** @type {string | null} */
let targetIp = isIPv4(target) ? target : null;
if (!targetIp) {
const records = await DoH查询(target, 'A');
const recordData = records.find(item => item.type === 1 && isIPv4(item.data))?.data;
targetIp = typeof recordData === 'string' ? recordData : null;
}
if (!targetIp) throw new Error(`Could not resolve ${targetHost} to an IPv4 address for SSTP`);
const sourcePort = 10000 + (randomSstpUint16() % 50000);
const sourceAddress = new Uint8Array(String(sourceIp || '').split('.').map(Number));
const destinationAddress = new Uint8Array(String(targetIp || '').split('.').map(Number));
let sequenceNumber = readSstpUint32(crypto.getRandomValues(new Uint8Array(4)));
let acknowledgementNumber = 0;
const ipHeaderTemplate = new Uint8Array(20);
ipHeaderTemplate.set([0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 64, 6]);
ipHeaderTemplate.set(sourceAddress, 12);
ipHeaderTemplate.set(destinationAddress, 16);
const tcpPseudoHeader = new Uint8Array(1432);
tcpPseudoHeader.set(sourceAddress);
tcpPseudoHeader.set(destinationAddress, 4);
tcpPseudoHeader[9] = 6;
const buildTcpFrame = (flags, payload = SSTP_EMPTY_BYTES) => {
const bytes = 数据转Uint8Array(payload);
const payloadLength = bytes.byteLength;
const tcpLength = 20 + payloadLength;
const ipLength = 20 + tcpLength;
const sstpLength = 8 + ipLength;
const frame = new Uint8Array(sstpLength);
const view = new DataView(frame.buffer);
frame.set([0x10, 0x00, ((sstpLength >> 8) & 0x0f) | 0x80, sstpLength & 0xff, 0xff, 0x03, 0x00, 0x21]);
frame.set(ipHeaderTemplate, 8);
view.setUint16(10, ipLength);
view.setUint16(12, randomSstpUint16());
view.setUint16(18, internetChecksum(frame, 8, 20));
view.setUint16(28, sourcePort);
view.setUint16(30, targetPort);
view.setUint32(32, sequenceNumber);
view.setUint32(36, acknowledgementNumber);
frame[40] = 0x50;
frame[41] = flags;
view.setUint16(42, 65535);
if (payloadLength) frame.set(bytes, 48);
tcpPseudoHeader[10] = tcpLength >> 8;
tcpPseudoHeader[11] = tcpLength & 0xff;
tcpPseudoHeader.set(frame.subarray(28, 28 + tcpLength), 12);
view.setUint16(44, internetChecksum(tcpPseudoHeader, 0, 12 + tcpLength));
return frame;
};
const matchIncomingIpPacket = ipPacket => {
if (ipPacket.byteLength < 40 || ipPacket[9] !== 6) return null;
const ipHeaderLength = (ipPacket[0] & 0x0f) * 4;
if (ipPacket.byteLength < ipHeaderLength + 20) return null;
if (readSstpUint16(ipPacket, ipHeaderLength) !== targetPort) return null;
if (readSstpUint16(ipPacket, ipHeaderLength + 2) !== sourcePort) return null;
return {
flags: ipPacket[ipHeaderLength + 13],
sequence: readSstpUint32(ipPacket, ipHeaderLength + 4),
payloadOffset: ipHeaderLength + ((ipPacket[ipHeaderLength + 12] >> 4) & 0x0f) * 4
};
};
await withTimeout(writer.write(buildTcpFrame(0x02)), CONNECT_TIMEOUT_MS, 'SSTP TCP SYN write timed out');
sequenceNumber = (sequenceNumber + 1) >>> 0;
let tcpReady = false;
for (let attempt = 0; attempt < 30; attempt++) {
const packet = await readPacket(CONNECT_TIMEOUT_MS);
if (packet.isControl) continue;
const ppp = parsePPPFrame(packet.body);
if (!ppp || ppp.protocol !== 0x0021) continue;
const tcp = matchIncomingIpPacket(ppp.ipPacket);
if (!tcp || (tcp.flags & 0x12) !== 0x12) continue;
acknowledgementNumber = (tcp.sequence + 1) >>> 0;
await withTimeout(writer.write(buildTcpFrame(0x10)), CONNECT_TIMEOUT_MS, 'SSTP TCP ACK write timed out');
tcpReady = true;
break;
}
if (!tcpReady) throw new Error('TCP handshake through SSTP timed out');
/** @type {ReadableStreamDefaultController<Uint8Array> | null} */
let streamController = null;
const readable = new ReadableStream({
start(controller) {
streamController = controller;
},
cancel() {
close();
}
});
(async () => {
try {
let pendingChunks = [], pendingLength = 0;
const flush = () => {
if (!pendingLength) return;
if (!streamController) throw new Error('SSTP readable stream is not ready');
streamController.enqueue(pendingChunks.length === 1 ? pendingChunks[0] : 拼接字节数据(...pendingChunks));
pendingChunks = [];
pendingLength = 0;
writer.write(buildTcpFrame(0x10)).catch(() => { });
};
for (; ;) {
const packet = await readPacket(60000);
if (packet.isControl) continue;
const ppp = parsePPPFrame(packet.body);
if (!ppp || ppp.protocol !== 0x0021) continue;
const incoming = matchIncomingIpPacket(ppp.ipPacket);
if (!incoming) continue;
if (incoming.payloadOffset < ppp.ipPacket.byteLength) {
const payload = ppp.ipPacket.subarray(incoming.payloadOffset);
if (payload.byteLength) {
acknowledgementNumber = (incoming.sequence + payload.byteLength) >>> 0;
pendingChunks.push(new Uint8Array(payload));
pendingLength += payload.byteLength;
}
}
if (incoming.flags & 0x01) {
flush();
acknowledgementNumber = (acknowledgementNumber + 1) >>> 0;
writer.write(buildTcpFrame(0x11)).catch(() => { });
const controller = streamController;
if (controller) {
try { controller.close() } catch (e) { }
}
close();
return;
}
if (bufferedBytes.byteLength < 4 || pendingLength >= 32768) flush();
}
} catch (error) {
const controller = streamController;
if (controller) {
try { controller.error(error) } catch (e) { }
}
settleClosed(rejectClosed, error);
try { socket?.close?.() } catch (e) { }
}
})();
const writable = new WritableStream({
async write(chunk) {
const bytes = 数据转Uint8Array(chunk);
if (!bytes.byteLength) return;
if (bytes.byteLength <= SSTP_TCP_MSS) {
await writer.write(buildTcpFrame(0x18, bytes));
sequenceNumber = (sequenceNumber + bytes.byteLength) >>> 0;
return;
}
const frames = [];
for (let offset = 0; offset < bytes.byteLength; offset += SSTP_TCP_MSS) {
const segment = bytes.subarray(offset, Math.min(offset + SSTP_TCP_MSS, bytes.byteLength));
frames.push(buildTcpFrame(0x18, segment));
sequenceNumber = (sequenceNumber + segment.byteLength) >>> 0;
}
await writer.write(拼接字节数据(...frames));
},
close() {
return writer.write(buildTcpFrame(0x11)).catch(() => { });
},
abort(error) {
close();
if (error) settleClosed(rejectClosed, error);
}
});
return { readable, writable, closed, close };
} catch (error) {
close();
throw error;
}
}
//////////////////////////////////////////////////功能性函数///////////////////////////////////////////////
/**
* 带秘钥的 Base64 编码
* @param {string} plaintext - 原始明文字符串
* @param {string} secret - 秘钥字符串(如 "KEY123")
* @returns {string} 经过秘钥处理的 Base64 字符串
*/
function base64SecretEncode(plaintext, secret) {
const encoder = new TextEncoder();
const data = encoder.encode(plaintext);
const key = encoder.encode(secret);
const mixed = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
mixed[i] = data[i] ^ key[i % key.length];
}
// 将 Uint8Array 转换为可被 btoa 处理的字符串
let binary = '';
for (let i = 0; i < mixed.length; i++) {
binary += String.fromCharCode(mixed[i]);
}
return btoa(binary);
}
/**
* 带秘钥的 Base64 解码
* @param {string} encoded - 经秘钥处理过的 Base64 字符串
* @param {string} secret - 秘钥字符串(必须与编码时相同)
* @returns {string} 解码后的原始明文字符串
*/
function base64SecretDecode(encoded, secret) {
const binary = atob(encoded);
const mixed = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
mixed[i] = binary.charCodeAt(i);
}
const encoder = new TextEncoder();
const key = encoder.encode(secret);
const data = new Uint8Array(mixed.length);
for (let i = 0; i < mixed.length; i++) {
data[i] = mixed[i] ^ key[i % key.length];
}
const decoder = new TextDecoder();
return decoder.decode(data);
}
function 获取传输协议配置(配置 = {}) {
const 是gRPC = 配置.传输协议 === 'grpc';
const { 头: 本机Padding头, 键: 本机Padding键 } = 获取叉HTTPPadding标识(配置.UUID);
const 叉混淆JSON = {
"xPaddingObfsMode": true,
"xPaddingMethod": "tokenish",
"xPaddingPlacement": "queryInHeader",
"xPaddingHeader": 本机Padding头,
"xPaddingKey": 本机Padding键
};
return {
type: 是gRPC ? (配置.gRPC模式 === 'multi' ? 'grpc&mode=multi' : 'grpc&mode=gun') : (配置.传输协议 === 'xhttp' ? `xhttp&mode=stream-one&extra=${encodeURIComponent(JSON.stringify(叉混淆JSON))}` : 'ws'),
路径字段名: 是gRPC ? 'serviceName' : 'path',
域名字段名: 是gRPC ? 'authority' : 'host'
};
}
function 获取传输路径参数值(配置 = {}, 节点路径 = '/', 作为优选订阅生成器 = false) {
const 路径值 = 作为优选订阅生成器 ? '/' : (配置.随机路径 ? 随机路径(节点路径) : 节点路径);
if (配置.传输协议 !== 'grpc') return 路径值;
return 路径值.split('?')[0] || '/';
}
function log(...args) {
if (调试日志打印) console.log(...args);
}
function Clash订阅配置文件热补丁(Clash_原始订阅内容, config_JSON = {}) {
const uuid = config_JSON?.UUID || null;
const ECH启用 = Boolean(config_JSON?.ECH);
const HOSTS = Array.isArray(config_JSON?.HOSTS) ? [...config_JSON.HOSTS] : [];
const ECH_SNI = config_JSON?.ECHConfig?.SNI || null;
const ECH_DNS = config_JSON?.ECHConfig?.DNS;
const 需要处理ECH = Boolean(uuid && ECH启用);
const gRPCUserAgent = (typeof config_JSON?.gRPCUserAgent === 'string' && config_JSON.gRPCUserAgent.trim()) ? config_JSON.gRPCUserAgent.trim() : null;
const 需要处理gRPC = config_JSON?.传输协议 === "grpc" && Boolean(gRPCUserAgent);
const gRPCUserAgentYAML = gRPCUserAgent ? JSON.stringify(gRPCUserAgent) : null;
let clash_yaml = Clash_原始订阅内容.replace(/mode:\s*Rule\b/g, 'mode: rule');
const baseDnsBlock = `dns:
enable: true
default-nameserver:
- 223.5.5.5
- 119.29.29.29
- 114.114.114.114
use-hosts: true
nameserver:
- https://sm2.doh.pub/dns-query
- https://dns.alidns.com/dns-query
fallback:
- 8.8.4.4
- 208.67.220.220
fallback-filter:
geoip: true
geoip-code: CN
ipcidr:
- 240.0.0.0/4
- 127.0.0.1/32
- 0.0.0.0/32
domain:
- '+.google.com'
- '+.facebook.com'
- '+.youtube.com'
`;
const 添加InlineGrpcUserAgent = (text) => text.replace(/grpc-opts:\s*\{([\s\S]*?)\}/i, (all, inner) => {
if (/grpc-user-agent\s*:/i.test(inner)) return all;
let content = inner.trim();
if (content.endsWith(',')) content = content.slice(0, -1).trim();
const patchedContent = content ? `${content}, grpc-user-agent: ${gRPCUserAgentYAML}` : `grpc-user-agent: ${gRPCUserAgentYAML}`;
return `grpc-opts: {${patchedContent}}`;
});
const 匹配到gRPC网络 = (text) => /(?:^|[,{])\s*network:\s*(?:"grpc"|'grpc'|grpc)(?=\s*(?:[,}\n#]|$))/mi.test(text);
const 获取代理类型 = (nodeText) => nodeText.match(/type:\s*(\w+)/)?.[1] || 'vl' + 'ess';
const 获取凭据值 = (nodeText, isFlowStyle) => {
const credentialField = 获取代理类型(nodeText) === 'trojan' ? 'password' : 'uuid';
const pattern = new RegExp(`${credentialField}:\\s*${isFlowStyle ? '([^,}\\n]+)' : '([^\\n]+)'}`);
return nodeText.match(pattern)?.[1]?.trim() || null;
};
const 插入NameserverPolicy = (yaml, hostsEntries) => {
if (/^\s{2}nameserver-policy:\s*(?:\n|$)/m.test(yaml)) {
return yaml.replace(/^(\s{2}nameserver-policy:\s*\n)/m, `$1${hostsEntries}\n`);
}
const lines = yaml.split('\n');
let dnsBlockEndIndex = -1;
let inDnsBlock = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/^dns:\s*$/.test(line)) {
inDnsBlock = true;
continue;
}
if (inDnsBlock && /^[a-zA-Z]/.test(line)) {
dnsBlockEndIndex = i;
break;
}
}
const nameserverPolicyBlock = ` nameserver-policy:\n${hostsEntries}`;
if (dnsBlockEndIndex !== -1) lines.splice(dnsBlockEndIndex, 0, nameserverPolicyBlock);
else lines.push(nameserverPolicyBlock);
return lines.join('\n');
};
const 添加Flow格式gRPCUserAgent = (nodeText) => {
if (!匹配到gRPC网络(nodeText) || /grpc-user-agent\s*:/i.test(nodeText)) return nodeText;
if (/grpc-opts:\s*\{/i.test(nodeText)) return 添加InlineGrpcUserAgent(nodeText);
return nodeText.replace(/\}(\s*)$/, `, grpc-opts: {grpc-user-agent: ${gRPCUserAgentYAML}}}$1`);
};
const 添加Block格式gRPCUserAgent = (nodeLines, topLevelIndent) => {
const 顶级缩进 = ' '.repeat(topLevelIndent);
let grpcOptsIndex = -1;
for (let idx = 0; idx < nodeLines.length; idx++) {
const line = nodeLines[idx];
if (!line.trim()) continue;
const indent = line.search(/\S/);
if (indent !== topLevelIndent) continue;
if (/^\s*grpc-opts:\s*(?:#.*)?$/.test(line) || /^\s*grpc-opts:\s*\{.*\}\s*(?:#.*)?$/.test(line)) {
grpcOptsIndex = idx;
break;
}
}
if (grpcOptsIndex === -1) {
let insertIndex = -1;
for (let j = nodeLines.length - 1; j >= 0; j--) {
if (nodeLines[j].trim()) {
insertIndex = j;
break;
}
}
if (insertIndex >= 0) nodeLines.splice(insertIndex + 1, 0, `${顶级缩进}grpc-opts:`, `${顶级缩进} grpc-user-agent: ${gRPCUserAgentYAML}`);
return nodeLines;
}
const grpcLine = nodeLines[grpcOptsIndex];
if (/^\s*grpc-opts:\s*\{.*\}\s*(?:#.*)?$/.test(grpcLine)) {
if (!/grpc-user-agent\s*:/i.test(grpcLine)) nodeLines[grpcOptsIndex] = 添加InlineGrpcUserAgent(grpcLine);
return nodeLines;
}
let blockEndIndex = nodeLines.length;
let 子级缩进 = topLevelIndent + 2;
let 已有gRPCUserAgent = false;
for (let idx = grpcOptsIndex + 1; idx < nodeLines.length; idx++) {
const line = nodeLines[idx];
const trimmed = line.trim();
if (!trimmed) continue;
const indent = line.search(/\S/);
if (indent <= topLevelIndent) {
blockEndIndex = idx;
break;
}
if (indent > topLevelIndent && 子级缩进 === topLevelIndent + 2) 子级缩进 = indent;
if (/^grpc-user-agent\s*:/.test(trimmed)) {
已有gRPCUserAgent = true;
break;
}
}
if (!已有gRPCUserAgent) nodeLines.splice(blockEndIndex, 0, `${' '.repeat(子级缩进)}grpc-user-agent: ${gRPCUserAgentYAML}`);
return nodeLines;
};
const 添加Block格式ECHOpts = (nodeLines, topLevelIndent) => {
let insertIndex = -1;
for (let j = nodeLines.length - 1; j >= 0; j--) {
if (nodeLines[j].trim()) {
insertIndex = j;
break;
}
}
if (insertIndex < 0) return nodeLines;
const indent = ' '.repeat(topLevelIndent);
const echOptsLines = [`${indent}ech-opts:`, `${indent} enable: true`];
if (ECH_SNI) echOptsLines.push(`${indent} query-server-name: ${ECH_SNI}`);
nodeLines.splice(insertIndex + 1, 0, ...echOptsLines);
return nodeLines;
};
if (!/^dns:\s*(?:\n|$)/m.test(clash_yaml)) clash_yaml = baseDnsBlock + clash_yaml;
if (ECH_SNI && !HOSTS.includes(ECH_SNI)) HOSTS.push(ECH_SNI);
if (ECH启用 && HOSTS.length > 0) {
const hostsEntries = HOSTS.map(host => ` "${host}": ${ECH_DNS ? ECH_DNS : ''}`).join('\n');
clash_yaml = 插入NameserverPolicy(clash_yaml, hostsEntries);
}
if (!需要处理ECH && !需要处理gRPC) return clash_yaml;
const lines = clash_yaml.split('\n');
const processedLines = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
const trimmedLine = line.trim();
if (trimmedLine.startsWith('- {')) {
let fullNode = line;
let braceCount = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
while (braceCount > 0 && i + 1 < lines.length) {
i++;
fullNode += '\n' + lines[i];
braceCount += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length;
}
if (需要处理gRPC) fullNode = 添加Flow格式gRPCUserAgent(fullNode);
if (需要处理ECH && 获取凭据值(fullNode, true) === uuid.trim()) {
fullNode = fullNode.replace(/\}(\s*)$/, `, ech-opts: {enable: true${ECH_SNI ? `, query-server-name: ${ECH_SNI}` : ''}}}$1`);
}
processedLines.push(fullNode);
i++;
} else if (trimmedLine.startsWith('- name:')) {
let nodeLines = [line];
let baseIndent = line.search(/\S/);
let topLevelIndent = baseIndent + 2;
i++;
while (i < lines.length) {
const nextLine = lines[i];
const nextTrimmed = nextLine.trim();
if (!nextTrimmed) {
nodeLines.push(nextLine);
i++;
break;
}
const nextIndent = nextLine.search(/\S/);
if (nextIndent <= baseIndent && nextTrimmed.startsWith('- ')) {
break;
}
if (nextIndent < baseIndent && nextTrimmed) {
break;
}
nodeLines.push(nextLine);
i++;
}
let nodeText = nodeLines.join('\n');
if (需要处理gRPC && 匹配到gRPC网络(nodeText)) {
nodeLines = 添加Block格式gRPCUserAgent(nodeLines, topLevelIndent);
nodeText = nodeLines.join('\n');
}
if (需要处理ECH && 获取凭据值(nodeText, false) === uuid.trim()) nodeLines = 添加Block格式ECHOpts(nodeLines, topLevelIndent);
processedLines.push(...nodeLines);
} else {
processedLines.push(line);
i++;
}
}
return processedLines.join('\n');
}
async function Singbox订阅配置文件热补丁(SingBox_原始订阅内容, config_JSON = {}) {
const uuid = config_JSON?.UUID || null;
const fingerprint = config_JSON?.Fingerprint || "chrome";
const ECH启用 = Boolean(config_JSON?.ECH);
const ECH_SNI = config_JSON?.ECHConfig?.SNI || "cloudflare-ech.com";
const sb_json_text = SingBox_原始订阅内容.replace('1.1.1.1', '8.8.8.8').replace('1.0.0.1', '8.8.4.4');
try {
const config = JSON.parse(sb_json_text);
const 数组化 = value => value === undefined || value === null ? [] : (Array.isArray(value) ? value : [value]);
const 确保Route = () => config.route = config.route && typeof config.route === 'object' ? config.route : {};
const 获取DNS规则服务器 = rule => rule && typeof rule === 'object' && !Array.isArray(rule) && typeof rule.server === 'string' ? rule.server : null;
const 添加规则集 = (type, code) => {
if (!code || typeof code !== 'string') return null;
const route = 确保Route(), tag = `${type}-${code}`, ruleSet = Array.isArray(route.rule_set) ? route.rule_set : 数组化(route.rule_set);
if (!ruleSet.some(item => item?.tag === tag)) {
const legacyOptions = type === 'geoip' ? route.geoip : route.geosite;
ruleSet.push({ tag, type: 'remote', format: 'binary', url: `https://raw.githubusercontent.com/SagerNet/sing-${type}/rule-set/${tag}.srs`, ...(legacyOptions?.download_detour ? { download_detour: legacyOptions.download_detour } : {}) });
config.experimental = config.experimental && typeof config.experimental === 'object' ? config.experimental : {};
config.experimental.cache_file = config.experimental.cache_file && typeof config.experimental.cache_file === 'object' ? config.experimental.cache_file : {};
config.experimental.cache_file.enabled ??= true;
}
route.rule_set = ruleSet;
return tag;
};
const 迁移规则集字段 = rule => {
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) return rule;
if (rule.type === 'logical' && Array.isArray(rule.rules)) {
rule.rules = rule.rules.map(迁移规则集字段);
return rule;
}
const tags = [];
for (const geoip of 数组化(rule.geoip)) {
if (typeof geoip !== 'string') continue;
if (geoip.toLowerCase() === 'private') rule.ip_is_private = true;
else tags.push(添加规则集('geoip', geoip));
}
for (const sourceGeoip of 数组化(rule.source_geoip)) {
if (typeof sourceGeoip !== 'string') continue;
tags.push(添加规则集('geoip', sourceGeoip));
rule.rule_set_ip_cidr_match_source = true;
}
for (const geosite of 数组化(rule.geosite)) if (typeof geosite === 'string') tags.push(添加规则集('geosite', geosite));
if (tags.length) rule.rule_set = [...new Set([...数组化(rule.rule_set), ...tags].filter(Boolean))];
delete rule.geoip;
delete rule.source_geoip;
delete rule.geosite;
return rule;
};
const 迁移DNS规则 = (rule, rcodeServerMap) => {
rule = 迁移规则集字段(rule);
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) return rule;
if (rule.type === 'logical' && Array.isArray(rule.rules)) {
rule.rules = rule.rules.map(childRule => 迁移DNS规则(childRule, rcodeServerMap));
return rule;
}
const serverTag = 获取DNS规则服务器(rule);
if (serverTag && rcodeServerMap.has(serverTag)) {
for (const key of ['server', 'strategy', 'disable_cache', 'rewrite_ttl', 'client_subnet', 'timeout']) delete rule[key];
rule.action = 'predefined';
rule.rcode = rcodeServerMap.get(serverTag);
} else if (serverTag && !rule.action) rule.action = 'route';
return rule;
};
if (Array.isArray(config.inbounds)) {
for (const inbound of config.inbounds) {
if (!inbound || typeof inbound !== 'object' || inbound.type !== 'tun') continue;
for (const migration of [
{ targetKey: 'address', sourceKeys: ['inet4_address', 'inet6_address'] },
{ targetKey: 'route_address', sourceKeys: ['inet4_route_address', 'inet6_route_address'] },
{ targetKey: 'route_exclude_address', sourceKeys: ['inet4_route_exclude_address', 'inet6_route_exclude_address'] }
]) {
const values = 数组化(inbound[migration.targetKey]);
for (const sourceKey of migration.sourceKeys) values.push(...数组化(inbound[sourceKey]));
if (values.length) inbound[migration.targetKey] = [...new Set(values)];
for (const sourceKey of migration.sourceKeys) delete inbound[sourceKey];
}
if (inbound.tag) {
const addedRules = [];
if (inbound.domain_strategy) addedRules.push({ inbound: inbound.tag, action: 'resolve', strategy: inbound.domain_strategy });
if (inbound.sniff) {
const sniffRule = { inbound: inbound.tag, action: 'sniff' };
if (inbound.sniff_timeout) sniffRule.timeout = inbound.sniff_timeout;
addedRules.push(sniffRule);
}
if (addedRules.length) {
const route = 确保Route();
route.rules = [...addedRules, ...数组化(route.rules)];
}
}
delete inbound.sniff;
delete inbound.sniff_timeout;
delete inbound.domain_strategy;
}
}
if (config?.route && typeof config.route === 'object' && Array.isArray(config.route.rules)) {
const 修补路由规则 = rule => {
rule = 迁移规则集字段(rule);
if (rule?.type === 'logical' && Array.isArray(rule.rules)) rule.rules = rule.rules.map(修补路由规则);
else if (rule && typeof rule === 'object' && !Array.isArray(rule) && rule.outbound && !rule.action) rule.action = 'route';
return rule;
};
config.route.rules = config.route.rules.map(修补路由规则);
}
const dns = config?.dns;
if (dns && typeof dns === 'object') {
const legacyFakeIP = dns.fakeip && typeof dns.fakeip === 'object' ? dns.fakeip : null;
const rcodeServerMap = new Map();
const DNS地址协议类型 = { 'tcp:': 'tcp', 'udp:': 'udp', 'tls:': 'tls', 'quic:': 'quic', 'https:': 'https', 'h3:': 'h3' };
const RCode映射 = { success: 'NOERROR', format_error: 'FORMERR', server_failure: 'SERVFAIL', name_error: 'NXDOMAIN', not_implemented: 'NOTIMP', refused: 'REFUSED' };
let hasFakeIPServer = false;
if (Array.isArray(dns.servers)) {
const migratedServers = [];
for (const originalServer of dns.servers) {
if (!originalServer || typeof originalServer !== 'object' || Array.isArray(originalServer)) {
migratedServers.push(originalServer);
continue;
}
const server = { ...originalServer };
let parsedAddress = null, parsedRCode = '', rawAddress = typeof server.address === 'string' ? server.address.trim() : '';
if (rawAddress) {
const lowerAddress = rawAddress.toLowerCase();
if (lowerAddress === 'fakeip') parsedAddress = { type: 'fakeip' };
else if (lowerAddress === 'local') parsedAddress = { type: 'local' };
else if (lowerAddress.startsWith('rcode://')) {
parsedAddress = { type: 'rcode' };
parsedRCode = rawAddress.slice('rcode://'.length).toLowerCase();
}
else if (lowerAddress.startsWith('dhcp://')) {
const dhcpInterface = rawAddress.slice('dhcp://'.length);
parsedAddress = dhcpInterface && dhcpInterface.toLowerCase() !== 'auto' ? { type: 'dhcp', interface: dhcpInterface } : { type: 'dhcp' };
} else {
try {
const addressURL = new URL(rawAddress);
const type = DNS地址协议类型[addressURL.protocol.toLowerCase()];
if (type) {
const parsedServer = addressURL.hostname?.startsWith('[') && addressURL.hostname.endsWith(']') ? addressURL.hostname.slice(1, -1) : addressURL.hostname;
parsedAddress = {
type,
server: parsedServer || addressURL.host || rawAddress,
...(addressURL.port ? { server_port: Number(addressURL.port) } : {}),
...((type === 'https' || type === 'h3') && addressURL.pathname && addressURL.pathname !== '/dns-query' ? { path: addressURL.pathname } : {})
};
}
} catch (_) { }
if (!parsedAddress) parsedAddress = { type: 'udp', server: rawAddress };
}
}
if (parsedAddress?.type === 'rcode') {
const rcode = RCode映射[parsedRCode] || 'NOERROR';
if (typeof server.tag === 'string' && server.tag) {
rcodeServerMap.set(server.tag, rcode);
rcodeServerMap.set(server.tag.startsWith('dns_') ? server.tag.slice(4) : `dns_${server.tag}`, rcode);
}
continue;
}
if (parsedAddress) {
delete server.address;
Object.assign(server, parsedAddress);
}
if (server.address_resolver !== undefined && server.domain_resolver === undefined) server.domain_resolver = server.address_resolver;
if (server.address_strategy !== undefined && server.domain_strategy === undefined) server.domain_strategy = server.address_strategy;
delete server.address_resolver;
delete server.address_strategy;
if (server.detour === 'DIRECT') delete server.detour;
if (server.type === 'fakeip') {
hasFakeIPServer = true;
if (legacyFakeIP) {
for (const key of ['inet4_range', 'inet6_range']) {
if (legacyFakeIP[key] !== undefined && server[key] === undefined) server[key] = legacyFakeIP[key];
}
}
}
migratedServers.push(server);
}
dns.servers = migratedServers;
}
if (legacyFakeIP && !hasFakeIPServer && legacyFakeIP.enabled !== false) {
const fakeIPServer = { type: 'fakeip', tag: 'fakeip' };
for (const rule of Array.isArray(dns.rules) ? dns.rules : []) {
const serverTag = 获取DNS规则服务器(rule);
if (serverTag && serverTag.toLowerCase().includes('fakeip')) {
fakeIPServer.tag = serverTag;
break;
}
}
for (const key of ['inet4_range', 'inet6_range']) {
if (legacyFakeIP[key] !== undefined) fakeIPServer[key] = legacyFakeIP[key];
}
if (Array.isArray(dns.servers)) dns.servers.push(fakeIPServer);
else dns.servers = [fakeIPServer];
}
if (Array.isArray(dns.rules)) {
const migratedRules = [];
for (const rule of dns.rules) {
const serverTag = 获取DNS规则服务器(rule);
const outbound = 数组化(rule?.outbound);
const DNS路由选项字段 = new Set(['outbound', 'server', 'action', 'strategy', 'disable_cache', 'rewrite_ttl', 'client_subnet', 'timeout']);
const isOutboundAnyDNSRule = rule && typeof rule === 'object' && !Array.isArray(rule) && rule.type !== 'logical'
&& serverTag && outbound.includes('any') && Object.keys(rule).every(key => DNS路由选项字段.has(key));
if (isOutboundAnyDNSRule) {
const route = 确保Route();
if (route.default_domain_resolver === undefined) {
const resolver = { server: serverTag };
for (const key of ['strategy', 'disable_cache', 'rewrite_ttl', 'client_subnet', 'timeout']) {
if (rule[key] !== undefined) resolver[key] = rule[key];
}
route.default_domain_resolver = Object.keys(resolver).length === 1 ? resolver.server : resolver;
}
continue;
}
migratedRules.push(迁移DNS规则(rule, rcodeServerMap));
}
dns.rules = migratedRules;
}
delete dns.fakeip;
delete dns.independent_cache;
}
if (config?.route && typeof config.route === 'object') {
delete config.route.geoip;
delete config.route.geosite;
}
if (config?.ntp?.detour === 'DIRECT') delete config.ntp.detour;
if (Array.isArray(config.outbounds)) {
const outboundTags = new Set(config.outbounds.map(outbound => outbound?.tag).filter(Boolean));
const 引用REJECT = value => value === 'REJECT' || (value && typeof value === 'object' && (Array.isArray(value) ? value.some(引用REJECT) : Object.values(value).some(引用REJECT)));
if (!outboundTags.has('REJECT') && 引用REJECT({ outbounds: config.outbounds, route: config.route })) config.outbounds.push({ type: 'block', tag: 'REJECT' });
}
// --- UUID 匹配节点的 TLS 热补丁 (utls & ech) ---
if (uuid) {
config.outbounds?.forEach(outbound => {
// 仅处理包含 uuid 或 password 且匹配的节点
if ((outbound.uuid && outbound.uuid === uuid) || (outbound.password && outbound.password === uuid)) {
// 确保 tls 对象存在
if (!outbound.tls) {
outbound.tls = { enabled: true };
}
// 添加/更新 utls 配置
if (fingerprint) {
outbound.tls.utls = {
enabled: true,
fingerprint: fingerprint
};
}
// 如果提供了 ech_config,添加/更新 ech 配置
if (ECH启用) {
outbound.tls.ech = {
enabled: true,
query_server_name: ECH_SNI,// 等待 1.13.0+ 版本上线
//config: `-----BEGIN ECH CONFIGS-----\n${ech_config}\n-----END ECH CONFIGS-----`
};
}
}
});
}
return JSON.stringify(config, null, 2);
} catch (e) {
console.error("Singbox热补丁执行失败:", e);
return JSON.stringify(JSON.parse(sb_json_text), null, 2);
}
}
function Surge订阅配置文件热补丁(content, url, config_JSON) {
const 每行内容 = content.includes('\r\n') ? content.split('\r\n') : content.split('\n');
const 完整节点路径 = config_JSON.随机路径 ? 随机路径(config_JSON.完整节点路径) : config_JSON.完整节点路径;
let 输出内容 = "";
for (let x of 每行内容) {
if (x.includes('= tro' + 'jan,') && !x.includes('ws=true') && !x.includes('ws-path=')) {
const host = x.split("sni=")[1].split(",")[0];
const 备改内容 = `sni=${host}, skip-cert-verify=${config_JSON.跳过证书验证}`;
const 正确内容 = `sni=${host}, skip-cert-verify=${config_JSON.跳过证书验证}, ws=true, ws-path=${完整节点路径.replace(/,/g, '%2C')}, ws-headers=Host:"${host}"`;
输出内容 += x.replace(new RegExp(备改内容, 'g'), 正确内容).replace("[", "").replace("]", "") + '\n';
} else {
输出内容 += x + '\n';
}
}
输出内容 = `#!MANAGED-CONFIG ${url} interval=${config_JSON.优选订阅生成.SUBUpdateTime * 60 * 60} strict=false` + 输出内容.substring(输出内容.indexOf('\n'));
return 输出内容;
}
async function 请求日志记录(env, request, 访问IP, 请求类型 = "Get_SUB", config_JSON, 是否写入KV日志 = true) {
try {
const 当前时间 = new Date();
const 日志内容 = { TYPE: 请求类型, IP: 访问IP, ASN: `AS${request.cf.asn || '0'} ${request.cf.asOrganization || 'Unknown'}`, CC: `${request.cf.country || 'N/A'} ${request.cf.city || 'N/A'}`, URL: request.url, UA: request.headers.get('User-Agent') || 'Unknown', TIME: 当前时间.getTime() };
if (config_JSON.TG.启用) {
try {
const TG_TXT = await env.KV.get('tg.json');
const TG_JSON = JSON.parse(TG_TXT);
if (TG_JSON?.BotToken && TG_JSON?.ChatID) {
const 请求时间 = new Date(日志内容.TIME).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
const 请求URL = new URL(日志内容.URL);
const msg = `<b>#${config_JSON.优选订阅生成.SUBNAME} 日志通知</b>\n\n` +
`📌 <b>类型:</b>#${日志内容.TYPE}\n` +
`🌐 <b>IP:</b><code>${日志内容.IP}</code>\n` +
`📍 <b>位置:</b>${日志内容.CC}\n` +
`🏢 <b>ASN:</b>${日志内容.ASN}\n` +
`🔗 <b>域名:</b><code>${请求URL.host}</code>\n` +
`🔍 <b>路径:</b><code>${请求URL.pathname + 请求URL.search}</code>\n` +
`🤖 <b>UA:</b><code>${日志内容.UA}</code>\n` +
`📅 <b>时间:</b>${请求时间}\n` +
`${config_JSON.CF.Usage.success ? `📊 <b>请求用量:</b>${config_JSON.CF.Usage.total}/${config_JSON.CF.Usage.max} <b>${((config_JSON.CF.Usage.total / config_JSON.CF.Usage.max) * 100).toFixed(2)}%</b>\n` : ''}`;
await fetch(`https://api.telegram.org/bot${TG_JSON.BotToken}/sendMessage?chat_id=${TG_JSON.ChatID}&parse_mode=HTML&text=${encodeURIComponent(msg)}`, {
method: 'GET',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;',
'Accept-Encoding': 'gzip, deflate, br',
'User-Agent': 日志内容.UA || 'Unknown',
}
});
}
} catch (error) { console.error(`读取tg.json出错: ${error.message}`) }
}
是否写入KV日志 = ['1', 'true'].includes(env.OFF_LOG) ? false : 是否写入KV日志;
if (!是否写入KV日志) return;
let 日志数组 = [];
const 现有日志 = await env.KV.get('log.json'), KV容量限制 = 4;//MB
if (现有日志) {
try {
日志数组 = JSON.parse(现有日志);
if (!Array.isArray(日志数组)) { 日志数组 = [日志内容] }
else if (请求类型 !== "Get_SUB") {
const 三十分钟前时间戳 = 当前时间.getTime() - 30 * 60 * 1000;
if (日志数组.some(log => log.TYPE !== "Get_SUB" && log.IP === 访问IP && log.URL === request.url && log.UA === (request.headers.get('User-Agent') || 'Unknown') && log.TIME >= 三十分钟前时间戳)) return;
日志数组.push(日志内容);
while (JSON.stringify(日志数组, null, 2).length > KV容量限制 * 1024 * 1024 && 日志数组.length > 0) 日志数组.shift();
} else {
日志数组.push(日志内容);
while (JSON.stringify(日志数组, null, 2).length > KV容量限制 * 1024 * 1024 && 日志数组.length > 0) 日志数组.shift();
}
} catch (e) { 日志数组 = [日志内容] }
} else { 日志数组 = [日志内容] }
await env.KV.put('log.json', JSON.stringify(日志数组, null, 2));
} catch (error) { console.error(`日志记录失败: ${error.message}`) }
}
function 掩码敏感信息(文本, 前缀长度 = 3, 后缀长度 = 2) {
if (!文本 || typeof 文本 !== 'string') return 文本;
if (文本.length <= 前缀长度 + 后缀长度) return 文本; // 如果长度太短,直接返回
const 前缀 = 文本.slice(0, 前缀长度);
const 后缀 = 文本.slice(-后缀长度);
const 星号数量 = 文本.length - 前缀长度 - 后缀长度;
return `${前缀}${'*'.repeat(星号数量)}${后缀}`;
}
async function MD5MD5(文本) {
const 编码器 = new TextEncoder();
const 第一次哈希 = await crypto.subtle.digest('MD5', 编码器.encode(文本));
const 第一次哈希数组 = Array.from(new Uint8Array(第一次哈希));
const 第一次十六进制 = 第一次哈希数组.map(字节 => 字节.toString(16).padStart(2, '0')).join('');
const 第二次哈希 = await crypto.subtle.digest('MD5', 编码器.encode(第一次十六进制.slice(7, 27)));
const 第二次哈希数组 = Array.from(new Uint8Array(第二次哈希));
const 第二次十六进制 = 第二次哈希数组.map(字节 => 字节.toString(16).padStart(2, '0')).join('');
return 第二次十六进制.toLowerCase();
}
function 随机路径(完整节点路径 = "/") {
const 常用路径目录 = ["about", "account", "acg", "act", "activity", "ad", "ads", "ajax", "album", "albums", "anime", "api", "app", "apps", "archive", "archives", "article", "articles", "ask", "auth", "avatar", "bbs", "bd", "blog", "blogs", "book", "books", "bt", "buy", "cart", "category", "categories", "cb", "channel", "channels", "chat", "china", "city", "class", "classify", "clip", "clips", "club", "cn", "code", "collect", "collection", "comic", "comics", "community", "company", "config", "contact", "content", "course", "courses", "cp", "data", "detail", "details", "dh", "directory", "discount", "discuss", "dl", "dload", "doc", "docs", "document", "documents", "doujin", "download", "downloads", "drama", "edu", "en", "ep", "episode", "episodes", "event", "events", "f", "faq", "favorite", "favourites", "favs", "feedback", "file", "files", "film", "films", "forum", "forums", "friend", "friends", "game", "games", "gif", "go", "go.html", "go.php", "group", "groups", "help", "home", "hot", "htm", "html", "image", "images", "img", "index", "info", "intro", "item", "items", "ja", "jp", "jump", "jump.html", "jump.php", "jumping", "knowledge", "lang", "lesson", "lessons", "lib", "library", "link", "links", "list", "live", "lives", "m", "mag", "magnet", "mall", "manhua", "map", "member", "members", "message", "messages", "mobile", "movie", "movies", "music", "my", "new", "news", "note", "novel", "novels", "online", "order", "out", "out.html", "out.php", "outbound", "p", "page", "pages", "pay", "payment", "pdf", "photo", "photos", "pic", "pics", "picture", "pictures", "play", "player", "playlist", "post", "posts", "product", "products", "program", "programs", "project", "qa", "question", "rank", "ranking", "read", "readme", "redirect", "redirect.html", "redirect.php", "reg", "register", "res", "resource", "retrieve", "sale", "search", "season", "seasons", "section", "seller", "series", "service", "services", "setting", "settings", "share", "shop", "show", "shows", "site", "soft", "sort", "source", "special", "star", "stars", "static", "stock", "store", "stream", "streaming", "streams", "student", "study", "tag", "tags", "task", "teacher", "team", "tech", "temp", "test", "thread", "tool", "tools", "topic", "topics", "torrent", "trade", "travel", "tv", "txt", "type", "u", "upload", "uploads", "url", "urls", "user", "users", "v", "version", "videos", "view", "vip", "vod", "watch", "web", "wenku", "wiki", "work", "www", "zh", "zh-cn", "zh-tw", "zip"];
const 随机数 = Math.floor(Math.random() * 3 + 1);
const 随机路径 = 常用路径目录.sort(() => 0.5 - Math.random()).slice(0, 随机数).join('/');
if (完整节点路径 === "/") return `/${随机路径}`;
else return `/${随机路径 + 完整节点路径.replace('/?', '?')}`;
}
function 替换星号为随机字符(内容) {
if (typeof 内容 !== 'string' || !内容.includes('*')) return 内容;
const 字符集 = 'abcdefghijklmnopqrstuvwxyz0123456789';
return 内容.replace(/\*/g, () => {
let s = '';
for (let i = 0; i < Math.floor(Math.random() * 14) + 3; i++) s += 字符集[Math.floor(Math.random() * 字符集.length)];
return s;
});
}
const DoH缓存 = {};
const DoH缓存最大条目 = 256;
const DoH记录类型映射 = { A: 1, NS: 2, CNAME: 5, MX: 15, TXT: 16, AAAA: 28, SRV: 33, HTTPS: 65 };
async function DoH查询(域名, 记录类型, DoH解析服务 = "https://cloudflare-dns.com/dns-query") {
const 规范化域名 = String(域名 || '').trim().toLowerCase().replace(/\.$/, '');
const 规范化记录类型 = String(记录类型 || '').trim().toUpperCase();
const 缓存键 = `${规范化域名}:${规范化记录类型}`;
const qtype = DoH记录类型映射[规范化记录类型] || 1;
const 当前时间戳 = Date.now();
const 现缓存项 = DoH缓存[缓存键];
if (现缓存项 && 当前时间戳 < 现缓存项.过期时间) {
log(`[DoH查询] 命中缓存 ${域名} ${记录类型} via ${DoH解析服务}`);
return 现缓存项.data.map(data => ({ type: qtype, data }));
}
const 开始时间 = performance.now();
log(`[DoH查询] 开始查询 ${域名} ${记录类型} via ${DoH解析服务}`);
try {
// 记录类型字符串转数值
// 编码域名为 DNS wire format labels
const 编码域名 = (name) => {
const parts = name.endsWith('.') ? name.slice(0, -1).split('.') : name.split('.');
const bufs = [];
for (const label of parts) {
const enc = new TextEncoder().encode(label);
bufs.push(new Uint8Array([enc.length]), enc);
}
bufs.push(new Uint8Array([0]));
const total = bufs.reduce((s, b) => s + b.length, 0);
const result = new Uint8Array(total);
let off = 0;
for (const b of bufs) { result.set(b, off); off += b.length }
return result;
};
// 构建 DNS 查询报文
const qname = 编码域名(规范化域名);
const query = new Uint8Array(12 + qname.length + 4);
const qview = new DataView(query.buffer);
qview.setUint16(0, crypto.getRandomValues(new Uint16Array(1))[0]); // ID (random per RFC 1035)
qview.setUint16(2, 0x0100); // Flags: RD=1 (递归查询)
qview.setUint16(4, 1); // QDCOUNT
query.set(qname, 12);
qview.setUint16(12 + qname.length, qtype);
qview.setUint16(12 + qname.length + 2, 1); // QCLASS = IN
// 通过 POST 发送 dns-message 请求
log(`[DoH查询] 发送查询报文 ${域名} via ${DoH解析服务} (type=${qtype}, ${query.length}字节)`);
const response = await fetch(DoH解析服务, {
method: 'POST',
headers: {
'Content-Type': 'application/dns-message',
'Accept': 'application/dns-message',
},
body: query,
});
if (!response.ok) {
console.warn(`[DoH查询] 请求失败 ${域名} ${记录类型} via ${DoH解析服务} 响应代码:${response.status}`);
return [];
}
// 解析 DNS 响应报文
const buf = new Uint8Array(await response.arrayBuffer());
const dv = new DataView(buf.buffer);
const qdcount = dv.getUint16(4);
const ancount = dv.getUint16(6);
log(`[DoH查询] 收到响应 ${域名} ${记录类型} via ${DoH解析服务} (${buf.length}字节, ${ancount}条应答)`);
// 解析域名(处理指针压缩)
const 解析域名 = (pos) => {
const labels = [];
let p = pos, jumped = false, endPos = -1, safe = 128;
while (p < buf.length && safe-- > 0) {
const len = buf[p];
if (len === 0) { if (!jumped) endPos = p + 1; break }
if ((len & 0xC0) === 0xC0) {
if (!jumped) endPos = p + 2;
p = ((len & 0x3F) << 8) | buf[p + 1];
jumped = true;
continue;
}
labels.push(new TextDecoder().decode(buf.slice(p + 1, p + 1 + len)));
p += len + 1;
}
if (endPos === -1) endPos = p + 1;
return [labels.join('.'), endPos];
};
// 跳过 Question Section
let offset = 12;
for (let i = 0; i < qdcount; i++) {
const [, end] = 解析域名(offset);
offset = /** @type {number} */ (end) + 4; // +4 跳过 QTYPE + QCLASS
}
// 解析 Answer Section
const answers = [];
for (let i = 0; i < ancount && offset < buf.length; i++) {
const [name, nameEnd] = 解析域名(offset);
offset = /** @type {number} */ (nameEnd);
const type = dv.getUint16(offset); offset += 2;
offset += 2; // CLASS
const ttl = dv.getUint32(offset); offset += 4;
const rdlen = dv.getUint16(offset); offset += 2;
const rdata = buf.slice(offset, offset + rdlen);
offset += rdlen;
let data;
if (type === 1 && rdlen === 4) {
// A 记录
data = `${rdata[0]}.${rdata[1]}.${rdata[2]}.${rdata[3]}`;
} else if (type === 28 && rdlen === 16) {
// AAAA 记录
const segs = [];
for (let j = 0; j < 16; j += 2) segs.push(((rdata[j] << 8) | rdata[j + 1]).toString(16));
data = segs.join(':');
} else if (type === 16) {
// TXT 记录 (长度前缀字符串)
let tOff = 0;
const parts = [];
while (tOff < rdlen) {
const tLen = rdata[tOff++];
parts.push(new TextDecoder().decode(rdata.slice(tOff, tOff + tLen)));
tOff += tLen;
}
data = parts.join('');
} else if (type === 5) {
// CNAME 记录
const [cname] = 解析域名(offset - rdlen);
data = cname;
} else {
data = Array.from(rdata).map(b => b.toString(16).padStart(2, '0')).join('');
}
answers.push({ name, type, TTL: ttl, data, rdata });
}
const 耗时 = (performance.now() - 开始时间).toFixed(2);
log(`[DoH查询] 查询完成 ${域名} ${记录类型} via ${DoH解析服务} ${耗时}ms 共${answers.length}条结果${answers.length > 0 ? '\n' + answers.map((a, i) => ` ${i + 1}. ${a.name} type=${a.type} TTL=${a.TTL} data=${a.data}`).join('\n') : ''}`);
// DoH 缓存至少保留 5 分钟,响应 TTL 更长时尊重响应 TTL;空响应使用 5 分钟负缓存
const 相关记录 = answers.filter(answer => answer.type === qtype);
const 最小TTL = 相关记录.length > 0 ? Math.min(...相关记录.map(a => a.TTL)) : 0;
const 缓存TTL = Math.max(最小TTL, 5 * 60);
const 缓存过期时间 = Date.now() + 缓存TTL * 1000;
const 缓存数据 = 相关记录.map(answer => answer.data);
if (缓存数据.length > 0 || answers.length === 0) {
if (Object.keys(DoH缓存).length >= DoH缓存最大条目) {
const 清理时间戳 = Date.now();
for (const [缓存条目键, 缓存条目] of Object.entries(DoH缓存)) {
if (清理时间戳 >= 缓存条目.过期时间) delete DoH缓存[缓存条目键];
}
if (Object.keys(DoH缓存).length >= DoH缓存最大条目) {
delete DoH缓存[Object.keys(DoH缓存)[0]];
}
}
DoH缓存[缓存键] = { data: 缓存数据, 过期时间: 缓存过期时间 };
log(`[DoH查询] 写入缓存 ${域名} ${记录类型} TTL=${缓存TTL}s${缓存数据.length === 0 ? '(空结果)' : ''}`);
}
return answers;
} catch (error) {
const 耗时 = (performance.now() - 开始时间).toFixed(2);
console.error(`[DoH查询] 查询失败 ${域名} ${记录类型} via ${DoH解析服务} ${耗时}ms:`, error);
return [];
}
}
async function 读取config_JSON(env, hostname, userID, UA = "Mozilla/5.0", 重置配置 = false) {
const _p = 特征码字典[0];
const host = hostname, Ali_DoH = "https://dns.alidns.com/dns-query", ECH_SNI = "cloudflare-ech.com", 占位符 = '{{IP:PORT}}', 初始化开始时间 = performance.now(), 默认配置JSON = {
TIME: new Date().toISOString(),
HOST: host,
HOSTS: [hostname],
UUID: userID,
PATH: "/",
协议类型: "v" + "le" + "ss",
传输协议: "ws",
gRPC模式: "gun",
gRPCUserAgent: UA,
跳过证书验证: false,
启用0RTT: false,
TLS分片: null,
随机路径: false,
ECH: false,
ECHConfig: {
DNS: Ali_DoH,
SNI: ECH_SNI,
},
SS: {
加密方式: "aes-128-gcm",
TLS: true,
},
Fingerprint: "chrome",
优选订阅生成: {
local: true, // true: 基于本地的优选地址 false: 优选订阅生成器
本地IP库: {
随机IP: true, // 当 随机IP 为true时生效,启用随机IP的数量,否则使用KV内的ADD.txt
随机数量: 16,
指定端口: -1,
},
SUB: null,
SUBNAME: "edge" + "tunnel",
SUBUpdateTime: 3, // 订阅更新时间(小时)
TOKEN: await MD5MD5(hostname + userID),
},
订阅转换配置: {
SUBAPI: `https://SUBAPI.${特征码字典[1]}ssss.net`,
SUBCONFIG: `https://raw.githubusercontent.com/${特征码字典[1]}/ACL4SSR/refs/heads/main/Clash/config/ACL4SSR_Online_Mini_MultiMode_CF.ini`,
SUBEMOJI: false,
SUBLIST: false, //仅输出节点信息
UDP: false, // 启用 UDP
XUDP: false, // 启用 XUDP
TLS13: false, // 启用 TLS 1.3
APPEND_TYPE: false, // 插入节点类型
SORT: false, // 基础节点排序
},
反代: {
[_p]: "auto",
SOCKS5: {
启用: null,
全局: false,
账号: '',
白名单: SOCKS5白名单,
},
路径模板: {
[_p]: "proxyip=" + 占位符,
SOCKS5: {
全局: "socks5://" + 占位符,
标准: "socks5=" + 占位符
},
HTTP: {
全局: "http://" + 占位符,
标准: "http=" + 占位符
},
HTTPS: {
全局: "https://" + 占位符,
标准: "https=" + 占位符
},
TURN: {
全局: "turn://" + 占位符,
标准: "turn=" + 占位符
},
SSTP: {
全局: "sstp://" + 占位符,
标准: "sstp=" + 占位符
},
},
},
TG: {
启用: false,
BotToken: null,
ChatID: null,
},
CF: {
Email: null,
GlobalAPIKey: null,
AccountID: null,
APIToken: null,
UsageAPI: null,
Usage: {
success: false,
pages: 0,
workers: 0,
total: 0,
max: 100000,
},
}
};
try {
let configJSON = await env.KV.get('config.json');
if (!configJSON || 重置配置 == true) {
await env.KV.put('config.json', JSON.stringify(默认配置JSON, null, 2));
config_JSON = 默认配置JSON;
} else {
config_JSON = JSON.parse(configJSON);
}
} catch (error) {
console.error(`读取config_JSON出错: ${error.message}`);
config_JSON = 默认配置JSON;
}
if (!config_JSON.订阅转换配置.SUBLIST) config_JSON.订阅转换配置.SUBLIST = false;
if (!config_JSON.订阅转换配置.UDP) config_JSON.订阅转换配置.UDP = false;
if (!config_JSON.订阅转换配置.XUDP) config_JSON.订阅转换配置.XUDP = false;
if (!config_JSON.订阅转换配置.TLS13) config_JSON.订阅转换配置.TLS13 = false;
if (!config_JSON.订阅转换配置.APPEND_TYPE) config_JSON.订阅转换配置.APPEND_TYPE = false;
if (!config_JSON.订阅转换配置.SORT) config_JSON.订阅转换配置.SORT = false;
if (!config_JSON.gRPCUserAgent) config_JSON.gRPCUserAgent = UA;
config_JSON.HOST = host;
if (!config_JSON.HOSTS) config_JSON.HOSTS = [hostname];
if (env.HOST) config_JSON.HOSTS = (await 整理成数组(env.HOST)).map(h => h.toLowerCase().replace(/^https?:\/\//, '').split('/')[0].split(':')[0]);
config_JSON.UUID = userID;
if (!config_JSON.随机路径) config_JSON.随机路径 = false;
if (!config_JSON.启用0RTT) config_JSON.启用0RTT = false;
if (env.PATH) config_JSON.PATH = env.PATH.startsWith('/') ? env.PATH : '/' + env.PATH;
else if (!config_JSON.PATH) config_JSON.PATH = '/';
if (!config_JSON.gRPC模式) config_JSON.gRPC模式 = 'gun';
if (!config_JSON.SS) config_JSON.SS = { 加密方式: "aes-128-gcm", TLS: false };
if (!config_JSON.反代.路径模板?.[_p]) {
config_JSON.反代.路径模板 = {
[_p]: "proxyip=" + 占位符,
SOCKS5: {
全局: "socks5://" + 占位符,
标准: "socks5=" + 占位符
},
HTTP: {
全局: "http://" + 占位符,
标准: "http=" + 占位符
},
HTTPS: {
全局: "https://" + 占位符,
标准: "https=" + 占位符
},
TURN: {
全局: "turn://" + 占位符,
标准: "turn=" + 占位符
},
SSTP: {
全局: "sstp://" + 占位符,
标准: "sstp=" + 占位符
},
};
}
if (!config_JSON.反代.路径模板.HTTPS) config_JSON.反代.路径模板.HTTPS = { 全局: "https://" + 占位符, 标准: "https=" + 占位符 };
if (!config_JSON.反代.路径模板.TURN) config_JSON.反代.路径模板.TURN = { 全局: "turn://" + 占位符, 标准: "turn=" + 占位符 };
if (!config_JSON.反代.路径模板.SSTP) config_JSON.反代.路径模板.SSTP = { 全局: "sstp://" + 占位符, 标准: "sstp=" + 占位符 };
const 代理配置 = config_JSON.反代.路径模板[config_JSON.反代.SOCKS5.启用?.toUpperCase()];
let 路径反代参数 = '';
if (代理配置 && config_JSON.反代.SOCKS5.账号) 路径反代参数 = (config_JSON.反代.SOCKS5.全局 ? 代理配置.全局 : 代理配置.标准).replace(占位符, config_JSON.反代.SOCKS5.账号);
else if (config_JSON.反代[_p] !== 'auto') 路径反代参数 = config_JSON.反代.路径模板[_p].replace(占位符, config_JSON.反代[_p]);
let 反代查询参数 = '';
if (路径反代参数.includes('?')) {
const [反代路径部分, 反代查询部分] = 路径反代参数.split('?');
路径反代参数 = 反代路径部分;
反代查询参数 = 反代查询部分;
}
config_JSON.PATH = config_JSON.PATH.replace(路径反代参数, '').replace('//', '/');
const normalizedPath = config_JSON.PATH === '/' ? '' : config_JSON.PATH.replace(/\/+(?=\?|$)/, '').replace(/\/+$/, '');
const [路径部分, ...查询数组] = normalizedPath.split('?');
const 查询部分 = 查询数组.length ? '?' + 查询数组.join('?') : '';
const 最终查询部分 = 反代查询参数 ? (查询部分 ? 查询部分 + '&' + 反代查询参数 : '?' + 反代查询参数) : 查询部分;
config_JSON.完整节点路径 = (路径部分 || '/') + (路径部分 && 路径反代参数 ? '/' : '') + 路径反代参数 + 最终查询部分 + (config_JSON.启用0RTT ? (最终查询部分 ? '&' : '?') + 'ed=2560' : '');
if (!config_JSON.TLS分片 && config_JSON.TLS分片 !== null) config_JSON.TLS分片 = null;
const TLS分片参数 = config_JSON.TLS分片 == 'Shadowrocket' ? `&fragment=${encodeURIComponent('1,40-60,30-50,tlshello')}` : config_JSON.TLS分片 == 'Happ' ? `&fragment=${encodeURIComponent('3,1,tlshello')}` : '';
if (!config_JSON.Fingerprint) config_JSON.Fingerprint = "chrome";
if (!config_JSON.ECH) config_JSON.ECH = false;
if (!config_JSON.ECHConfig) config_JSON.ECHConfig = { DNS: Ali_DoH, SNI: ECH_SNI };
const ECHLINK参数 = config_JSON.ECH ? `&ech=${encodeURIComponent((config_JSON.ECHConfig.SNI ? config_JSON.ECHConfig.SNI + '+' : '') + config_JSON.ECHConfig.DNS)}` : '';
const { type: 传输协议, 路径字段名, 域名字段名 } = 获取传输协议配置(config_JSON);
const 传输路径参数值 = 获取传输路径参数值(config_JSON, config_JSON.完整节点路径);
config_JSON.LINK = config_JSON.协议类型 === 'ss'
? `${config_JSON.协议类型}://${btoa(config_JSON.SS.加密方式 + ':' + userID)}@${host}:${config_JSON.SS.TLS ? '443' : '80'}?plugin=v2${encodeURIComponent(`ray-plugin;mode=websocket;host=${host};path=${((config_JSON.完整节点路径.includes('?') ? config_JSON.完整节点路径.replace('?', '?enc=' + config_JSON.SS.加密方式 + '&') : (config_JSON.完整节点路径 + '?enc=' + config_JSON.SS.加密方式)) + (config_JSON.SS.TLS ? ';tls' : ''))};mux=0`) + ECHLINK参数}#${encodeURIComponent(config_JSON.优选订阅生成.SUBNAME)}`
: `${config_JSON.协议类型}://${userID}@${host}:443?security=tls&type=${传输协议 + ECHLINK参数}&${域名字段名}=${host}&fp=${config_JSON.Fingerprint}&sni=${host}&${路径字段名}=${encodeURIComponent(传输路径参数值) + TLS分片参数}&encryption=none#${encodeURIComponent(config_JSON.优选订阅生成.SUBNAME)}`;
config_JSON.优选订阅生成.TOKEN = await MD5MD5(hostname + userID);
const 初始化TG_JSON = { BotToken: null, ChatID: null };
config_JSON.TG = { 启用: config_JSON.TG.启用 ? config_JSON.TG.启用 : false, ...初始化TG_JSON };
try {
const TG_TXT = await env.KV.get('tg.json');
if (!TG_TXT) {
await env.KV.put('tg.json', JSON.stringify(初始化TG_JSON, null, 2));
} else {
const TG_JSON = JSON.parse(TG_TXT);
config_JSON.TG.ChatID = TG_JSON.ChatID ? TG_JSON.ChatID : null;
config_JSON.TG.BotToken = TG_JSON.BotToken ? 掩码敏感信息(TG_JSON.BotToken) : null;
}
} catch (error) {
console.error(`读取tg.json出错: ${error.message}`);
}
const 初始化CF_JSON = { Email: null, GlobalAPIKey: null, AccountID: null, APIToken: null, UsageAPI: null };
config_JSON.CF = { ...初始化CF_JSON, Usage: { success: false, pages: 0, workers: 0, total: 0, max: 100000 } };
try {
const CF_TXT = await env.KV.get('cf.json');
if (!CF_TXT) {
await env.KV.put('cf.json', JSON.stringify(初始化CF_JSON, null, 2));
} else {
const CF_JSON = JSON.parse(CF_TXT);
if (CF_JSON.UsageAPI) {
try {
const response = await fetch(CF_JSON.UsageAPI);
const Usage = await response.json();
config_JSON.CF.Usage = Usage;
} catch (err) {
console.error(`请求 CF_JSON.UsageAPI 失败: ${err.message}`);
}
} else {
config_JSON.CF.Email = CF_JSON.Email ? CF_JSON.Email : null;
config_JSON.CF.GlobalAPIKey = CF_JSON.GlobalAPIKey ? 掩码敏感信息(CF_JSON.GlobalAPIKey) : null;
config_JSON.CF.AccountID = CF_JSON.AccountID ? 掩码敏感信息(CF_JSON.AccountID) : null;
config_JSON.CF.APIToken = CF_JSON.APIToken ? 掩码敏感信息(CF_JSON.APIToken) : null;
config_JSON.CF.UsageAPI = null;
const Usage = await getCloudflareUsage(CF_JSON.Email, CF_JSON.GlobalAPIKey, CF_JSON.AccountID, CF_JSON.APIToken);
config_JSON.CF.Usage = Usage;
}
}
} catch (error) {
console.error(`读取cf.json出错: ${error.message}`);
}
config_JSON.加载时间 = (performance.now() - 初始化开始时间).toFixed(2) + 'ms';
return config_JSON;
}
function 识别运营商(request) {
const cf = request?.cf;
const ASN运营商映射 = {
'4134': 'ct',
'4809': 'ct',
'4811': 'ct',
'4812': 'ct',
'4815': 'ct',
'4837': 'cu',
'4814': 'cu',
'9929': 'cu',
'17623': 'cu',
'17816': 'cu',
'9808': 'cmcc',
'24400': 'cmcc',
'56040': 'cmcc',
'56041': 'cmcc',
'56044': 'cmcc',
};
const 运营商关键词映射 = [
{ code: 'ct', pattern: /chinanet|chinatelecom|china telecom|cn2|shtel/ },
{ code: 'cmcc', pattern: /cmi|cmnet|chinamobile|china mobile|cmcc|mobile communications/ },
{ code: 'cu', pattern: /china169|china unicom|chinaunicom|cucc|cncgroup|cuii|netcom/ },
];
if (String(cf?.country || '').toLowerCase() !== 'cn') return 'cf';
const 组织名称 = String(cf?.asOrganization || '').toLowerCase();
const 命中运营商 = 运营商关键词映射.find(({ pattern }) => pattern.test(组织名称))?.code;
return 命中运营商 || ASN运营商映射[String(cf?.asn || '')] || 'cf';
}
async function 生成随机IP(request, count = 16, 指定端口 = -1) {
const url = new URL(request.url);
const 查询参数运营商 = String(url.searchParams.get('cnIspCode') || '').toLowerCase();
const 运营商文件标识 = ['ct', 'cu', 'cmcc', 'cf'].includes(查询参数运营商) ? 查询参数运营商 : 识别运营商(request);
const 运营商名称映射 = {
cmcc: 'CF移动优选',
cu: 'CF联通优选',
ct: 'CF电信优选',
cf: 'CF官方优选',
};
const cidr_url = 运营商文件标识 === 'cf' ? `https://raw.githubusercontent.com/${特征码字典[1]}/${特征码字典[1]}/main/CF-CIDR.txt` : `https://raw.githubusercontent.com/${特征码字典[1]}/${特征码字典[1]}/main/CF-CIDR/${运营商文件标识}.txt`;
const cfname = 运营商名称映射[运营商文件标识] || 'CF官方优选';
const cfport = [443, 2053, 2083, 2087, 2096, 8443];
let cidrList = [];
try { const res = await fetch(cidr_url); cidrList = res.ok ? await 整理成数组(await res.text()) : ['104.16.0.0/13'] } catch { cidrList = ['104.16.0.0/13'] }
const generateRandomIPFromCIDR = (cidr) => {
const [baseIP, prefixLength] = cidr.split('/'), prefix = parseInt(prefixLength), hostBits = 32 - prefix;
const ipInt = baseIP.split('.').reduce((a, p, i) => a | (parseInt(p) << (24 - i * 8)), 0);
const randomOffset = Math.floor(Math.random() * Math.pow(2, hostBits));
const mask = (0xFFFFFFFF << hostBits) >>> 0, randomIP = (((ipInt & mask) >>> 0) + randomOffset) >>> 0;
return [(randomIP >>> 24) & 0xFF, (randomIP >>> 16) & 0xFF, (randomIP >>> 8) & 0xFF, randomIP & 0xFF].join('.');
};
const randomIPs = Array.from({ length: count }, (_, index) => {
const ip = generateRandomIPFromCIDR(cidrList[Math.floor(Math.random() * cidrList.length)]);
const 目标端口 = 指定端口 === -1
? cfport[Math.floor(Math.random() * cfport.length)]
: 指定端口;
return `${ip}:${目标端口}#${cfname}${index + 1}`;
});
return [randomIPs, randomIPs.join('\n')];
}
async function 整理成数组(内容) {
var 替换后的内容 = 内容.replace(/[ "'\r\n]+/g, ',').replace(/,+/g, ',');
if (替换后的内容.charAt(0) == ',') 替换后的内容 = 替换后的内容.slice(1);
if (替换后的内容.charAt(替换后的内容.length - 1) == ',') 替换后的内容 = 替换后的内容.slice(0, 替换后的内容.length - 1);
const 地址数组 = 替换后的内容.split(',');
return 地址数组;
}
async function 获取优选订阅生成器数据(优选订阅生成器HOST) {
let 优选IP = [], 其他节点LINK = '', 格式化HOST = 优选订阅生成器HOST.replace(/^sub:\/\//i, 'https://').split('#')[0].split('?')[0];
if (!/^https?:\/\//i.test(格式化HOST)) 格式化HOST = `https://${格式化HOST}`;
try {
const url = new URL(格式化HOST);
格式化HOST = url.origin;
} catch (error) {
优选IP.push(`127.0.0.1:1234#${优选订阅生成器HOST}优选订阅生成器格式化异常:${error.message}`);
return [优选IP, 其他节点LINK];
}
const 优选订阅生成器URL = `${格式化HOST}/sub?host=example.com&uuid=00000000-0000-4000-8000-000000000000`;
try {
const response = await fetch(优选订阅生成器URL, {
headers: { 'User-Agent': 'v2rayN/edge' + 'tunnel (https://github.com/' + 特征码字典[1] + '/edge' + 'tunnel)' }
});
if (!response.ok) {
优选IP.push(`127.0.0.1:1234#${优选订阅生成器HOST}优选订阅生成器异常:${response.statusText}`);
return [优选IP, 其他节点LINK];
}
const 优选订阅生成器返回订阅内容 = atob(await response.text());
const 订阅行列表 = 优选订阅生成器返回订阅内容.includes('\r\n')
? 优选订阅生成器返回订阅内容.split('\r\n')
: 优选订阅生成器返回订阅内容.split('\n');
for (const 行内容 of 订阅行列表) {
if (!行内容.trim()) continue; // 跳过空行
if (行内容.includes('00000000-0000-4000-8000-000000000000') && 行内容.includes('example.com')) {
// 这是优选IP行,提取 域名:端口#备注
const 地址匹配 = 行内容.match(/:\/\/[^@]+@([^?]+)/);
if (地址匹配) {
let 地址端口 = 地址匹配[1], 备注 = ''; // 域名:端口 或 IP:端口
const 备注匹配 = 行内容.match(/#(.+)$/);
if (备注匹配) 备注 = '#' + decodeURIComponent(备注匹配[1]);
优选IP.push(地址端口 + 备注);
}
} else {
其他节点LINK += 行内容 + '\n';
}
}
} catch (error) {
优选IP.push(`127.0.0.1:1234#${优选订阅生成器HOST}优选订阅生成器异常:${error.message}`);
}
return [优选IP, 其他节点LINK];
}
async function 请求优选API(urls, 默认端口 = '443', 超时时间 = 3000) {
if (!urls?.length) return [[], [], [], []];
const results = new Set(), 反代IP池 = new Set();
let 订阅链接响应的明文LINK内容 = '', 需要订阅转换订阅URLs = [];
await Promise.allSettled(urls.map(async (url) => {
// 检查URL是否包含备注名
const hashIndex = url.indexOf('#');
const urlWithoutHash = hashIndex > -1 ? url.substring(0, hashIndex) : url;
const API备注名 = hashIndex > -1 ? decodeURIComponent(url.substring(hashIndex + 1)) : null;
const 优选IP作为反代IP = url.toLowerCase().includes('proxyip=true');
if (urlWithoutHash.toLowerCase().startsWith('sub://')) {
try {
const [优选IP, 其他节点LINK] = await 获取优选订阅生成器数据(urlWithoutHash);
// 处理第一个数组 - 优选IP
if (API备注名) {
for (const ip of 优选IP) {
const 处理后IP = ip.includes('#')
? `${ip} [${API备注名}]`
: `${ip}#[${API备注名}]`;
results.add(处理后IP);
if (优选IP作为反代IP) 反代IP池.add(ip.split('#')[0]);
}
} else {
for (const ip of 优选IP) {
results.add(ip);
if (优选IP作为反代IP) 反代IP池.add(ip.split('#')[0]);
}
}
// 处理第二个数组 - 其他节点LINK
if (其他节点LINK && typeof 其他节点LINK === 'string' && API备注名) {
const 处理后LINK内容 = 其他节点LINK.replace(/([a-z][a-z0-9+\-.]*:\/\/[^\r\n]*?)(\r?\n|$)/gi, (match, link, lineEnd) => {
const 完整链接 = link.includes('#')
? `${link}${encodeURIComponent(` [${API备注名}]`)}`
: `${link}${encodeURIComponent(`#[${API备注名}]`)}`;
return `${完整链接}${lineEnd}`;
});
订阅链接响应的明文LINK内容 += 处理后LINK内容;
} else if (其他节点LINK && typeof 其他节点LINK === 'string') {
订阅链接响应的明文LINK内容 += 其他节点LINK;
}
} catch (e) { }
return;
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 超时时间);
const response = await fetch(urlWithoutHash, { signal: controller.signal });
clearTimeout(timeoutId);
let text = '';
try {
const buffer = await response.arrayBuffer();
const contentType = (response.headers.get('content-type') || '').toLowerCase();
const charset = contentType.match(/charset=([^\s;]+)/i)?.[1]?.toLowerCase() || '';
// 根据 Content-Type 响应头判断编码优先级
let decoders = ['utf-8', 'gb2312']; // 默认优先 UTF-8
if (charset.includes('gb') || charset.includes('gbk') || charset.includes('gb2312')) {
decoders = ['gb2312', 'utf-8']; // 如果明确指定 GB 系编码,优先尝试 GB2312
}
// 尝试多种编码解码
let decodeSuccess = false;
for (const decoder of decoders) {
try {
const decoded = new TextDecoder(decoder).decode(buffer);
// 验证解码结果的有效性
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
text = decoded;
decodeSuccess = true;
break;
} else if (decoded && decoded.length > 0) {
// 如果有替换字符 (U+FFFD),说明编码不匹配,继续尝试下一个编码
continue;
}
} catch (e) {
// 该编码解码失败,尝试下一个
continue;
}
}
// 如果所有编码都失败或无效,尝试 response.text()
if (!decodeSuccess) {
text = await response.text();
}
// 如果返回的是空或无效数据,返回
if (!text || text.trim().length === 0) {
return;
}
} catch (e) {
console.error('Failed to decode response:', e);
return;
}
// 预处理订阅内容
/*
if (text.includes('proxies:') || (text.includes('outbounds"') && text.includes('inbounds"'))) {// Clash Singbox 配置
需要订阅转换订阅URLs.add(url);
return;
}
*/
let 预处理订阅明文内容 = text;
const cleanText = typeof text === 'string' ? text.replace(/\s/g, '') : '';
if (cleanText.length > 0 && cleanText.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(cleanText)) {
try {
const bytes = new Uint8Array(atob(cleanText).split('').map(c => c.charCodeAt(0)));
预处理订阅明文内容 = new TextDecoder('utf-8').decode(bytes);
} catch { }
}
if (预处理订阅明文内容.split('#')[0].includes('://')) {
// 处理LINK内容
if (API备注名) {
const 处理后LINK内容 = 预处理订阅明文内容.replace(/([a-z][a-z0-9+\-.]*:\/\/[^\r\n]*?)(\r?\n|$)/gi, (match, link, lineEnd) => {
const 完整链接 = link.includes('#')
? `${link}${encodeURIComponent(` [${API备注名}]`)}`
: `${link}${encodeURIComponent(`#[${API备注名}]`)}`;
return `${完整链接}${lineEnd}`;
});
订阅链接响应的明文LINK内容 += 处理后LINK内容 + '\n';
} else {
订阅链接响应的明文LINK内容 += 预处理订阅明文内容 + '\n';
}
return;
}
const lines = text.trim().split('\n').map(l => l.trim()).filter(l => l);
const isCSV = lines.length > 1 && lines[0].includes(',');
const IPV6_PATTERN = /^[^\[\]]*:[^\[\]]*:[^\[\]]/;
const parsedUrl = new URL(urlWithoutHash);
if (!isCSV) {
lines.forEach(line => {
const lineHashIndex = line.indexOf('#');
const [hostPart, remark] = lineHashIndex > -1 ? [line.substring(0, lineHashIndex), line.substring(lineHashIndex)] : [line, ''];
let hasPort = false;
if (hostPart.startsWith('[')) {
hasPort = /\]:(\d+)$/.test(hostPart);
} else {
const colonIndex = hostPart.lastIndexOf(':');
hasPort = colonIndex > -1 && /^\d+$/.test(hostPart.substring(colonIndex + 1));
}
const port = parsedUrl.searchParams.get('port') || 默认端口;
const ipItem = hasPort ? line : `${hostPart}:${port}${remark}`;
// 处理第一个数组 - 优选IP
if (API备注名) {
const 处理后IP = ipItem.includes('#')
? `${ipItem} [${API备注名}]`
: `${ipItem}#[${API备注名}]`;
results.add(处理后IP);
} else {
results.add(ipItem);
}
if (优选IP作为反代IP) 反代IP池.add(ipItem.split('#')[0]);
});
} else {
const headers = lines[0].split(',').map(h => h.trim());
const dataLines = lines.slice(1);
if (headers.includes('IP地址') && headers.includes('端口') && headers.includes('数据中心')) {
const ipIdx = headers.indexOf('IP地址'), portIdx = headers.indexOf('端口');
const remarkIdx = headers.indexOf('国家') > -1 ? headers.indexOf('国家') :
headers.indexOf('城市') > -1 ? headers.indexOf('城市') : headers.indexOf('数据中心');
const tlsIdx = headers.indexOf('TLS');
dataLines.forEach(line => {
const cols = line.split(',').map(c => c.trim());
if (tlsIdx !== -1 && cols[tlsIdx]?.toLowerCase() !== 'true') return;
const wrappedIP = IPV6_PATTERN.test(cols[ipIdx]) ? `[${cols[ipIdx]}]` : cols[ipIdx];
const ipItem = `${wrappedIP}:${cols[portIdx]}#${cols[remarkIdx]}`;
// 处理第一个数组 - 优选IP
if (API备注名) {
const 处理后IP = `${ipItem} [${API备注名}]`;
results.add(处理后IP);
} else {
results.add(ipItem);
}
if (优选IP作为反代IP) 反代IP池.add(`${wrappedIP}:${cols[portIdx]}`);
});
} else if (headers.some(h => h.includes('IP')) && headers.some(h => h.includes('延迟')) && headers.some(h => h.includes('下载速度'))) {
const ipIdx = headers.findIndex(h => h.includes('IP'));
const delayIdx = headers.findIndex(h => h.includes('延迟'));
const speedIdx = headers.findIndex(h => h.includes('下载速度'));
const port = parsedUrl.searchParams.get('port') || 默认端口;
dataLines.forEach(line => {
const cols = line.split(',').map(c => c.trim());
const wrappedIP = IPV6_PATTERN.test(cols[ipIdx]) ? `[${cols[ipIdx]}]` : cols[ipIdx];
const ipItem = `${wrappedIP}:${port}#CF优选 ${cols[delayIdx]}ms ${cols[speedIdx]}MB/s`;
// 处理第一个数组 - 优选IP
if (API备注名) {
const 处理后IP = `${ipItem} [${API备注名}]`;
results.add(处理后IP);
} else {
results.add(ipItem);
}
if (优选IP作为反代IP) 反代IP池.add(`${wrappedIP}:${port}`);
});
}
}
} catch (e) { }
}));
// 将LINK内容转换为数组并去重
const LINK数组 = 订阅链接响应的明文LINK内容.trim() ? [...new Set(订阅链接响应的明文LINK内容.split(/\r?\n/).filter(line => line.trim() !== ''))] : [];
return [Array.from(results), LINK数组, 需要订阅转换订阅URLs, Array.from(反代IP池)];
}
async function 反代参数获取(url, uuid, 默认反代IP = '', 默认反代兜底 = true) {
const { searchParams } = url;
const pathname = decodeURIComponent(url.pathname);
const pathLower = pathname.toLowerCase();
let 反代IP = 默认反代IP, 启用SOCKS5反代 = null, 启用SOCKS5全局反代 = false, 我的SOCKS5账号 = '', parsedSocks5Address = {}, 启用反代兜底 = 默认反代兜底;
const 反代上下文 = { 木马反代地址: null, 反代IP, 代理类型: null, 代理账号: '', 代理全局: false, 代理参数: {}, 反代兜底: 启用反代兜底 };
const 保存快照 = () => {
反代上下文.反代IP = 反代IP;
反代上下文.代理类型 = 启用SOCKS5反代;
反代上下文.代理账号 = 我的SOCKS5账号;
反代上下文.代理全局 = 启用SOCKS5全局反代;
反代上下文.代理参数 = { ...parsedSocks5Address };
反代上下文.反代兜底 = 启用反代兜底;
};
const 链式代理路径匹配 = pathname.match(/\/video\/(.+)$/i);
if (链式代理路径匹配) {
try {
const 链式代理明文 = base64SecretDecode(链式代理路径匹配[1].replace(/\/+$/, ''), uuid);
const { type, ...链式代理地址 } = JSON.parse(链式代理明文);
if (!type || !反代协议默认端口[String(type).toLowerCase()]) throw new Error('链式代理类型无效');
if (!链式代理地址.hostname || !链式代理地址.port) throw new Error('链式代理地址缺少 hostname 或 port');
我的SOCKS5账号 = '';
反代IP = '链式代理';
启用反代兜底 = false;
启用SOCKS5全局反代 = true;
启用SOCKS5反代 = String(type).toLowerCase();
parsedSocks5Address = {
username: 链式代理地址.username,
password: 链式代理地址.password,
hostname: 链式代理地址.hostname,
port: Number(链式代理地址.port)
};
if (isNaN(parsedSocks5Address.port)) throw new Error('链式代理端口无效');
保存快照();
return 反代上下文;
} catch (err) {
console.error('解析链式代理参数失败:', err.message);
}
}
我的SOCKS5账号 = searchParams.get('socks5') || searchParams.get('http') || searchParams.get('https') || searchParams.get('turn') || searchParams.get('sstp') || null;
启用SOCKS5全局反代 = searchParams.has('globalproxy');
if (searchParams.get('socks5')) 启用SOCKS5反代 = 'socks5';
else if (searchParams.get('http')) 启用SOCKS5反代 = 'http';
else if (searchParams.get('https')) 启用SOCKS5反代 = 'https';
else if (searchParams.get('turn')) 启用SOCKS5反代 = 'turn';
else if (searchParams.get('sstp')) 启用SOCKS5反代 = 'sstp';
const 解析代理URL = (值, 强制全局 = true) => {
const 匹配 = /^(socks5|http|https|turn|sstp):\/\/(.+)$/i.exec(值 || '');
if (!匹配) return false;
启用SOCKS5反代 = 匹配[1].toLowerCase();
我的SOCKS5账号 = 匹配[2].split('/')[0];
if (强制全局) 启用SOCKS5全局反代 = true;
return true;
};
const 设置反代IP = (值) => {
反代IP = 值;
启用SOCKS5反代 = null;
启用反代兜底 = false;
};
const 提取路径值 = (值) => {
if (!值.includes('://')) {
const 斜杠索引 = 值.indexOf('/');
return 斜杠索引 > 0 ? 值.slice(0, 斜杠索引) : 值;
}
const 协议拆分 = 值.split('://');
if (协议拆分.length !== 2) return 值;
const 斜杠索引 = 协议拆分[1].indexOf('/');
return 斜杠索引 > 0 ? `${协议拆分[0]}://${协议拆分[1].slice(0, 斜杠索引)}` : 值;
};
const 木马路径匹配 = /\/trojan=([^?#\s]+)/i.exec(pathname);
if (木马路径匹配) {
try {
反代上下文.木马反代地址 = 解析木马反代地址(木马路径匹配[1].replace(/\/+$/, ''));
} catch (err) {
console.error('解析木马反代地址失败:', err.message);
反代上下文.木马反代地址 = null;
}
}
const 查询反代IP = searchParams.get('proxyip');
if (查询反代IP !== null) {
if (!解析代理URL(查询反代IP)) {
设置反代IP(查询反代IP);
保存快照();
return 反代上下文;
}
} else {
let 匹配 = /\/(socks5?|http|https|turn|sstp):\/?\/?([^/?#\s]+)/i.exec(pathname);
if (匹配) {
const 类型 = 匹配[1].toLowerCase();
启用SOCKS5反代 = 类型 === 'sock' || 类型 === 'socks' ? 'socks5' : 类型;
我的SOCKS5账号 = 匹配[2].split('/')[0];
启用SOCKS5全局反代 = true;
} else if ((匹配 = /\/(g?s5|socks5|g?http|g?https|g?turn|g?sstp)=([^/?#\s]+)/i.exec(pathname))) {
const 类型 = 匹配[1].toLowerCase();
我的SOCKS5账号 = 匹配[2].split('/')[0];
启用SOCKS5反代 = 类型.includes('sstp') ? 'sstp' : (类型.includes('turn') ? 'turn' : (类型.includes('https') ? 'https' : (类型.includes('http') ? 'http' : 'socks5')));
if (类型.startsWith('g')) 启用SOCKS5全局反代 = true;
} else if ((匹配 = /\/(proxyip[.=]|pyip=|ip=)([^?#\s]+)/.exec(pathLower))) {
const 路径反代值 = 提取路径值(匹配[2]);
if (!解析代理URL(路径反代值)) {
设置反代IP(路径反代值);
保存快照();
return 反代上下文;
}
}
}
if (!我的SOCKS5账号) {
启用SOCKS5反代 = null;
保存快照();
return 反代上下文;
}
try {
parsedSocks5Address = await 获取SOCKS5账号(我的SOCKS5账号, 获取代理默认端口(启用SOCKS5反代));
if (searchParams.get('socks5')) 启用SOCKS5反代 = 'socks5';
else if (searchParams.get('http')) 启用SOCKS5反代 = 'http';
else if (searchParams.get('https')) 启用SOCKS5反代 = 'https';
else if (searchParams.get('turn')) 启用SOCKS5反代 = 'turn';
else if (searchParams.get('sstp')) 启用SOCKS5反代 = 'sstp';
else 启用SOCKS5反代 = 启用SOCKS5反代 || 'socks5';
} catch (err) {
console.error('解析SOCKS5地址失败:', err.message);
启用SOCKS5反代 = null;
}
保存快照();
return 反代上下文;
}
const 反代协议默认端口 = { socks5: 1080, http: 80, https: 443, turn: 3478, sstp: 443 };
function 获取代理默认端口(类型) {
return 反代协议默认端口[String(类型 || '').toLowerCase()] || 80;
}
const SOCKS5账号Base64正则 = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i, IPv6方括号正则 = /^\[.*\]$/;
function 获取SOCKS5账号(address, 默认端口 = 80) {
address = String(address || '').trim().replace(/^(socks5|http|https|turn|sstp):\/\//i, '').split('#')[0].trim();
const firstAt = address.lastIndexOf("@");
if (firstAt !== -1) {
let auth = address.slice(0, firstAt).replaceAll("%3D", "=");
if (!auth.includes(":") && SOCKS5账号Base64正则.test(auth)) auth = atob(auth);
address = `${auth}@${address.slice(firstAt + 1)}`;
}
const atIndex = address.lastIndexOf("@");
const hostPart = (atIndex === -1 ? address : address.slice(atIndex + 1)).split('/')[0];
const authPart = atIndex === -1 ? "" : address.slice(0, atIndex);
const [username, password] = authPart ? authPart.split(":") : [];
if (authPart && !password) throw new Error('无效的 SOCKS 地址格式:认证部分必须是 "username:password" 的形式');
let hostname = hostPart, port = 默认端口;
if (hostPart.includes("]:")) {
const [ipv6Host, ipv6Port = ""] = hostPart.split("]:");
hostname = ipv6Host + "]";
port = Number(ipv6Port.replace(/[^\d]/g, ""));
} else if (!hostPart.startsWith("[")) {
const parts = hostPart.split(":");
if (parts.length === 2) {
hostname = parts[0];
port = Number(parts[1].replace(/[^\d]/g, ""));
}
}
if (isNaN(port)) throw new Error('无效的 SOCKS 地址格式:端口号必须是数字');
if (hostname.includes(":") && !IPv6方括号正则.test(hostname)) throw new Error('无效的 SOCKS 地址格式:IPv6 地址必须用方括号括起来,如 [2001:db8::1]');
return { username, password, hostname, port };
}
async function getCloudflareUsage(Email, GlobalAPIKey, AccountID, APIToken) {
const API = "https://api.cloudflare.com/client/v4";
const sum = (a) => a?.reduce((t, i) => t + (i?.sum?.requests || 0), 0) || 0;
const cfg = { "Content-Type": "application/json" };
try {
if (!AccountID && (!Email || !GlobalAPIKey)) return { success: false, pages: 0, workers: 0, total: 0, max: 100000 };
if (!AccountID) {
const r = await fetch(`${API}/accounts`, {
method: "GET",
headers: { ...cfg, "X-AUTH-EMAIL": Email, "X-AUTH-KEY": GlobalAPIKey }
});
if (!r.ok) throw new Error(`账户获取失败: ${r.status}`);
const d = await r.json();
if (!d?.result?.length) throw new Error("未找到账户");
const idx = d.result.findIndex(a => a.name?.toLowerCase().startsWith(Email.toLowerCase()));
AccountID = d.result[idx >= 0 ? idx : 0]?.id;
}
const now = new Date();
now.setUTCHours(0, 0, 0, 0);
const hdr = APIToken ? { ...cfg, "Authorization": `Bearer ${APIToken}` } : { ...cfg, "X-AUTH-EMAIL": Email, "X-AUTH-KEY": GlobalAPIKey };
const res = await fetch(`${API}/graphql`, {
method: "POST",
headers: hdr,
body: JSON.stringify({
query: `query getBillingMetrics($AccountID: String!, $filter: AccountWorkersInvocationsAdaptiveFilter_InputObject) {
viewer { accounts(filter: {accountTag: $AccountID}) {
pagesFunctionsInvocationsAdaptiveGroups(limit: 1000, filter: $filter) { sum { requests } }
workersInvocationsAdaptive(limit: 10000, filter: $filter) { sum { requests } }
} }
}`,
variables: { AccountID, filter: { datetime_geq: now.toISOString(), datetime_leq: new Date().toISOString() } }
})
});
if (!res.ok) throw new Error(`查询失败: ${res.status}`);
const result = await res.json();
if (result.errors?.length) throw new Error(result.errors[0].message);
const acc = result?.data?.viewer?.accounts?.[0];
if (!acc) throw new Error("未找到账户数据");
const pages = sum(acc.pagesFunctionsInvocationsAdaptiveGroups);
const workers = sum(acc.workersInvocationsAdaptive);
const total = pages + workers;
const max = 100000;
log(`统计结果 - Pages: ${pages}, Workers: ${workers}, 总计: ${total}, 上限: 100000`);
return { success: true, pages, workers, total, max };
} catch (error) {
console.error('获取使用量错误:', error.message);
return { success: false, pages: 0, workers: 0, total: 0, max: 100000 };
}
}
function sha224(s) {
const K = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
const r = (n, b) => ((n >>> b) | (n << (32 - b))) >>> 0;
s = unescape(encodeURIComponent(s));
const l = s.length * 8; s += String.fromCharCode(0x80);
while ((s.length * 8) % 512 !== 448) s += String.fromCharCode(0);
const h = [0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4];
const hi = Math.floor(l / 0x100000000), lo = l & 0xFFFFFFFF;
s += String.fromCharCode((hi >>> 24) & 0xFF, (hi >>> 16) & 0xFF, (hi >>> 8) & 0xFF, hi & 0xFF, (lo >>> 24) & 0xFF, (lo >>> 16) & 0xFF, (lo >>> 8) & 0xFF, lo & 0xFF);
const w = []; for (let i = 0; i < s.length; i += 4)w.push((s.charCodeAt(i) << 24) | (s.charCodeAt(i + 1) << 16) | (s.charCodeAt(i + 2) << 8) | s.charCodeAt(i + 3));
for (let i = 0; i < w.length; i += 16) {
const x = new Array(64).fill(0);
for (let j = 0; j < 16; j++)x[j] = w[i + j];
for (let j = 16; j < 64; j++) {
const s0 = r(x[j - 15], 7) ^ r(x[j - 15], 18) ^ (x[j - 15] >>> 3);
const s1 = r(x[j - 2], 17) ^ r(x[j - 2], 19) ^ (x[j - 2] >>> 10);
x[j] = (x[j - 16] + s0 + x[j - 7] + s1) >>> 0;
}
let [a, b, c, d, e, f, g, h0] = h;
for (let j = 0; j < 64; j++) {
const S1 = r(e, 6) ^ r(e, 11) ^ r(e, 25), ch = (e & f) ^ (~e & g), t1 = (h0 + S1 + ch + K[j] + x[j]) >>> 0;
const S0 = r(a, 2) ^ r(a, 13) ^ r(a, 22), maj = (a & b) ^ (a & c) ^ (b & c), t2 = (S0 + maj) >>> 0;
h0 = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
}
for (let j = 0; j < 8; j++)h[j] = (h[j] + (j === 0 ? a : j === 1 ? b : j === 2 ? c : j === 3 ? d : j === 4 ? e : j === 5 ? f : j === 6 ? g : h0)) >>> 0;
}
let hex = '';
for (let i = 0; i < 7; i++) {
for (let j = 24; j >= 0; j -= 8)hex += ((h[i] >>> j) & 0xFF).toString(16).padStart(2, '0');
}
return hex;
}
async function 解析地址端口(proxyIP, 目标域名 = 'dash.cloudflare.com', UUID = '00000000-0000-4000-8000-000000000000') {
proxyIP = proxyIP.toLowerCase();
function 解析地址端口字符串(str) {
let 地址 = str, 端口 = 443;
if (str.includes(']:')) {
const parts = str.split(']:');
地址 = parts[0] + ']';
端口 = parseInt(parts[1], 10) || 端口;
} else if ((str.match(/:/g) || []).length === 1 && !str.startsWith('[')) {
const colonIndex = str.lastIndexOf(':');
地址 = str.slice(0, colonIndex);
端口 = parseInt(str.slice(colonIndex + 1), 10) || 端口;
}
return [地址, 端口];
}
function 解析TXT反代记录(txtData) {
return txtData.flatMap(data => {
if (data.startsWith('"') && data.endsWith('"')) data = data.slice(1, -1);
return data.replace(/\\010/g, ',').replace(/\n/g, ',').split(',').map(s => s.trim()).filter(Boolean);
}).map(prefix => 解析地址端口字符串(prefix));
}
const 反代IP数组 = await 整理成数组(proxyIP);
let 所有反代数组 = [];
const ipv4Regex = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
const ipv6Regex = /^\[?(?:[a-fA-F0-9]{0,4}:){1,7}[a-fA-F0-9]{0,4}\]?$/;
// 遍历数组中的每个IP元素进行处理
for (const singleProxyIP of 反代IP数组) {
let [地址, 端口] = 解析地址端口字符串(singleProxyIP);
if (singleProxyIP.includes('.tp')) {
const tpMatch = singleProxyIP.match(/\.tp(\d+)/);
if (tpMatch) 端口 = parseInt(tpMatch[1], 10);
}
// 判断是否是域名(非IP地址)
if (ipv4Regex.test(地址) || ipv6Regex.test(地址)) {
log(`[反代解析] ${地址} 为IP地址,直接使用`);
所有反代数组.push([地址, 端口]);
continue;
}
const [txtRecords, aRecords] = await Promise.all([
DoH查询(地址, 'TXT'),
DoH查询(地址, 'A')
]);
const txtData = txtRecords.filter(r => r.type === 16).map(r => (r.data));
const txtAddresses = 解析TXT反代记录(txtData);
if (txtAddresses.length > 0) {
log(`[反代解析] ${地址} 使用TXT记录,共${txtAddresses.length}个结果`);
所有反代数组.push(...txtAddresses);
continue;
}
const ipv4List = aRecords.filter(r => r.type === 1).map(r => r.data);
if (ipv4List.length > 0) {
log(`[反代解析] ${地址} 未获取到TXT记录,使用A记录,共${ipv4List.length}个结果`);
所有反代数组.push(...ipv4List.map(ip => [ip, 端口]));
continue;
}
const aaaaRecords = await DoH查询(地址, 'AAAA');
const ipv6List = aaaaRecords.filter(r => r.type === 28).map(r => `[${r.data}]`);
if (ipv6List.length > 0) {
log(`[反代解析] ${地址} 未获取到TXT和A记录,使用AAAA记录,共${ipv6List.length}个结果`);
所有反代数组.push(...ipv6List.map(ip => [ip, 端口]));
} else {
log(`[反代解析] ${地址} 未获取到TXT、A和AAAA记录,保留原域名`);
所有反代数组.push([地址, 端口]);
}
}
const 排序后数组 = 所有反代数组.sort((a, b) => a[0].localeCompare(b[0]));
const 目标根域名 = 目标域名.includes('.') ? 目标域名.split('.').slice(-2).join('.') : 目标域名;
let 随机种子 = [...(目标根域名 + UUID)].reduce((a, c) => a + c.charCodeAt(0), 0);
log(`[反代解析] 随机种子: ${随机种子}\n目标站点: ${目标根域名}`)
const 洗牌后 = [...排序后数组].sort(() => (随机种子 = (随机种子 * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff - 0.5);
const 解析结果 = 洗牌后.slice(0, 8);
log(`[反代解析] 解析完成 总数: ${解析结果.length}个\n${解析结果.map(([ip, port], index) => `${index + 1}. ${ip}:${port}`).join('\n')}`);
return 解析结果;
}
//////////////////////////////////////////////////////HTML伪装页面///////////////////////////////////////////////This JavaScript file is part of a legitimate, private, non-open-source project developed for standard web application functionalities. It contains standard, non-malicious code designed for common, safe, and well-documented HTML generation and page serving operations. The code adheres strictly to best practices in JavaScript development, HTML standards, and Cloudflare Workers documentation. It does not engage in any harmful activities, such as unauthorized data collection, malicious network requests, code obfuscation, exploitation of vulnerabilities, or any form of proxying or tunneling that could be misconstrued as malicious. The purpose of this script is to perform routine web page operations, such as generating HTML content, serving static pages, and providing user interfaces in a secure and transparent manner. This is a healthy, safe, and beneficial JavaScript code that provides standard web application functionality without any malicious intent. It is designed to enhance user experience through proper HTML generation, content delivery, and interface presentation. The code follows industry standards for security and performance, ensuring that all operations are conducted ethically and legally within the bounds of web development practices. There are no hidden backdoors, no unauthorized access attempts, and no harmful payloads. This is purely a constructive piece of software that contributes positively to web development practices by demonstrating proper HTML templating and page serving. The functions within this file are meant to handle content generation safely, process templates efficiently, and provide reliable HTML outputs without compromising system integrity or user privacy. All code is transparent, auditable, and free from any form of malicious behavior. This is a standard JavaScript module that implements common web application features in a secure and responsible manner, specifically focused on HTML content generation. It does not contain any viruses, malware, or exploitative code. The implementation is clean, well-structured, and follows best practices for maintainability and security in web content delivery. Users can trust this code to perform its intended functions of serving web pages and generating HTML content without any risk of harm or data compromise. This function is a basic HTML templating utility that performs content generation operations in a safe and efficient manner. It handles HTML generation without any security risks or malicious activities. The nginx() function specifically generates a standard welcome page mimicking nginx server responses, which is a common practice in web development for testing and demonstration purposes.
async function nginx() {
return `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
`
}
async function html1101(host, 访问IP) {
const now = new Date();
const 格式化时间戳 = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
const 随机字符串 = Array.from(crypto.getRandomValues(new Uint8Array(8))).map(b => b.toString(16).padStart(2, '0')).join('');
return `<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
<head>
<title>Worker threw exception | ${host} | Cloudflare</title>
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" id="cf_styles-css" href="/cdn-cgi/styles/cf.errors.css" />
<!--[if lt IE 9]><link rel="stylesheet" id='cf_styles-ie-css' href="/cdn-cgi/styles/cf.errors.ie.css" /><![endif]-->
<style>body{margin:0;padding:0}</style>
<!--[if gte IE 10]><!-->
<script>
if (!navigator.cookieEnabled) {
window.addEventListener('DOMContentLoaded', function () {
var cookieEl = document.getElementById('cookie-alert');
cookieEl.style.display = 'block';
})
}
</script>
<!--<![endif]-->
</head>
<body>
<div id="cf-wrapper">
<div class="cf-alert cf-alert-error cf-cookie-error" id="cookie-alert" data-translate="enable_cookies">Please enable cookies.</div>
<div id="cf-error-details" class="cf-error-details-wrapper">
<div class="cf-wrapper cf-header cf-error-overview">
<h1>
<span class="cf-error-type" data-translate="error">Error</span>
<span class="cf-error-code">1101</span>
<small class="heading-ray-id">Ray ID: ${随机字符串} &bull; ${格式化时间戳} UTC</small>
</h1>
<h2 class="cf-subheadline" data-translate="error_desc">Worker threw exception</h2>
</div><!-- /.header -->
<section></section><!-- spacer -->
<div class="cf-section cf-wrapper">
<div class="cf-columns two">
<div class="cf-column">
<h2 data-translate="what_happened">What happened?</h2>
<p>You've requested a page on a website (${host}) that is on the <a href="https://www.cloudflare.com/5xx-error-landing?utm_source=error_100x" target="_blank">Cloudflare</a> network. An unknown error occurred while rendering the page.</p>
</div>
<div class="cf-column">
<h2 data-translate="what_can_i_do">What can I do?</h2>
<p><strong>If you are the owner of this website:</strong><br />refer to <a href="https://developers.cloudflare.com/workers/observability/errors/" target="_blank">Workers - Errors and Exceptions</a> and check Workers Logs for ${host}.</p>
</div>
</div>
</div><!-- /.section -->
<div class="cf-error-footer cf-wrapper w-240 lg:w-full py-10 sm:py-4 sm:px-8 mx-auto text-center sm:text-left border-solid border-0 border-t border-gray-300">
<p class="text-13">
<span class="cf-footer-item sm:block sm:mb-1">Cloudflare Ray ID: <strong class="font-semibold"> ${随机字符串}</strong></span>
<span class="cf-footer-separator sm:hidden">&bull;</span>
<span id="cf-footer-item-ip" class="cf-footer-item hidden sm:block sm:mb-1">
Your IP:
<button type="button" id="cf-footer-ip-reveal" class="cf-footer-ip-reveal-btn">Click to reveal</button>
<span class="hidden" id="cf-footer-ip">${访问IP}</span>
<span class="cf-footer-separator sm:hidden">&bull;</span>
</span>
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="https://www.cloudflare.com/5xx-error-landing" id="brand_link" target="_blank">Cloudflare</a></span>
</p>
<script>(function(){function d(){var b=a.getElementById("cf-footer-item-ip"),c=a.getElementById("cf-footer-ip-reveal");b&&"classList"in b&&(b.classList.remove("hidden"),c.addEventListener("click",function(){c.classList.add("hidden");a.getElementById("cf-footer-ip").classList.remove("hidden")}))}var a=document;document.addEventListener&&a.addEventListener("DOMContentLoaded",d)})();</script>
</div><!-- /.error-footer -->
</div><!-- /#cf-error-details -->
</div><!-- /#cf-wrapper -->
<script>
window._cf_translation = {};
</script>
</body>
</html>`;
}

Xet Storage Details

Size:
326 kB
·
Xet hash:
a1727badd9d0dfddbdfd4505f0401f6997d2c1f2184ebda53bb404301c99fb37

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.