diff --git a/_worker.js b/_worker.js
index f48627684..b9d566110 100644
--- a/_worker.js
+++ b/_worker.js
@@ -48,6 +48,7 @@ export default {
guestToken = env.GUESTTOKEN || env.GUEST || guestToken;
if (!guestToken) guestToken = await MD5MD5(mytoken);
const 访客订阅 = guestToken;
+ const 订阅中转路径 = `/${await MD5MD5(fakeToken)}?token=${fakeToken}`;
//console.log(`${fakeUserID}\n${fakeHostName}`); // 打印fakeID
let UD = Math.floor(((timestamp - Date.now()) / timestamp * total * 1099511627776) / 2);
@@ -55,8 +56,8 @@ export default {
let expire = Math.floor(timestamp / 1000);
SUBUpdateTime = env.SUBUPTIME || SUBUpdateTime;
- if (!([mytoken, fakeToken, 访客订阅].includes(token) || url.pathname == ("/" + mytoken) || url.pathname.includes("/" + mytoken + "?"))) {
- if (TG == 1 && url.pathname !== "/" && url.pathname !== "/favicon.ico") await sendMessage(`#异常访问 ${FileName}`, request.headers.get('CF-Connecting-IP'), `UA: ${userAgent}\n域名: ${url.hostname}\n入口: ${url.pathname + url.search}`);
+ if (!([mytoken, fakeToken, 访客订阅].includes(token) || url.pathname === ("/" + mytoken) || url.pathname.includes("/" + mytoken + "?"))) {
+ if (TG === 1 && url.pathname !== "/" && url.pathname !== "/favicon.ico") await sendMessage(`#异常访问 ${FileName}`, request.headers.get('CF-Connecting-IP'), `UA: ${userAgent}\n域名: ${url.hostname}\n入口: ${url.pathname + url.search}`);
if (env.URL302) return Response.redirect(env.URL302, 302);
else if (env.URL) return await proxyURL(env.URL, url);
else return new Response(await nginx(), {
@@ -66,6 +67,27 @@ export default {
},
});
} else {
+ if (token === fakeToken && url.searchParams.has('url')) {
+ const 远程订阅链接 = url.searchParams.get('url');
+ const 追加UA参数 = url.searchParams.get('ua') || 'v2rayn';
+ const 原始UA参数 = url.searchParams.get('uafull') || userAgentHeader || '';
+ if (!远程订阅链接 || !/^https?:\/\//i.test(远程订阅链接)) {
+ return new Response('无效的远程订阅链接', { status: 400 });
+ }
+ try {
+ const 中转响应 = await getUrl(request, 远程订阅链接, 追加UA参数, 原始UA参数);
+ const 中转头 = new Headers(中转响应.headers);
+ if (!中转头.get('content-type')) 中转头.set('content-type', 'text/plain; charset=utf-8');
+ return new Response(await 中转响应.text(), {
+ status: 中转响应.status,
+ headers: 中转头,
+ });
+ } catch (error) {
+ console.error('远程订阅中转失败:', error);
+ return new Response('远程订阅中转失败', { status: 502 });
+ }
+ }
+
if (env.KV) {
await 迁移地址列表(env, 'LINK.txt');
if (userAgent.includes('mozilla') && !url.search) {
@@ -108,7 +130,7 @@ export default {
}
let subConverterUrl;
- let 订阅转换URL = `${url.origin}/${await MD5MD5(fakeToken)}?token=${fakeToken}`;
+ let 订阅转换URL = `${url.origin}${订阅中转路径}`;
//console.log(订阅转换URL);
let req_data = MainData;
@@ -121,12 +143,14 @@ export default {
else if (url.searchParams.has('loon')) 追加UA = 'Loon';
const 订阅链接数组 = [...new Set(urls)].filter(item => item?.trim?.()); // 去重
+ let 第三方Clash配置 = [];
if (订阅链接数组.length > 0) {
- const 请求订阅响应内容 = await getSUB(订阅链接数组, request, 追加UA, userAgentHeader);
+ const 请求订阅响应内容 = await getSUB(订阅链接数组, request, 追加UA, userAgentHeader, `${url.origin}${订阅中转路径}`);
console.log(请求订阅响应内容);
req_data += 请求订阅响应内容[0].join('\n');
订阅转换URL += "|" + 请求订阅响应内容[1];
- if (订阅格式 == 'base64' && !isSubConverterRequest && 请求订阅响应内容[1].includes('://')) {
+ 第三方Clash配置 = 请求订阅响应内容[2] || [];
+ if (订阅格式 === 'base64' && !isSubConverterRequest && 请求订阅响应内容[1].includes('://')) {
subConverterUrl = `${subProtocol}://${subConverter}/sub?target=mixed&url=${encodeURIComponent(请求订阅响应内容[1])}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;
try {
const subConverterResponse = await fetch(subConverterUrl, { headers: { 'User-Agent': 'v2rayN/CF-Workers-SUB (https://github.com/cmliu/CF-Workers-SUB)' } });
@@ -188,17 +212,17 @@ export default {
//"Subscription-Userinfo": `upload=${UD}; download=${UD}; total=${total}; expire=${expire}`,
};
- if (订阅格式 == 'base64' || token == fakeToken) {
+ if (订阅格式 === 'base64' || token === fakeToken) {
return new Response(base64Data, { headers: responseHeaders });
- } else if (订阅格式 == 'clash') {
+ } else if (订阅格式 === 'clash') {
subConverterUrl = `${subProtocol}://${subConverter}/sub?target=clash&url=${encodeURIComponent(订阅转换URL)}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;
- } else if (订阅格式 == 'singbox') {
+ } else if (订阅格式 === 'singbox') {
subConverterUrl = `${subProtocol}://${subConverter}/sub?target=singbox&url=${encodeURIComponent(订阅转换URL)}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;
- } else if (订阅格式 == 'surge') {
+ } else if (订阅格式 === 'surge') {
subConverterUrl = `${subProtocol}://${subConverter}/sub?target=surge&ver=4&url=${encodeURIComponent(订阅转换URL)}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;
- } else if (订阅格式 == 'quanx') {
+ } else if (订阅格式 === 'quanx') {
subConverterUrl = `${subProtocol}://${subConverter}/sub?target=quanx&url=${encodeURIComponent(订阅转换URL)}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&udp=true`;
- } else if (订阅格式 == 'loon') {
+ } else if (订阅格式 === 'loon') {
subConverterUrl = `${subProtocol}://${subConverter}/sub?target=loon&url=${encodeURIComponent(订阅转换URL)}&insert=false&config=${encodeURIComponent(subConfig)}&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false`;
}
//console.log(订阅转换URL);
@@ -206,7 +230,10 @@ export default {
const subConverterResponse = await fetch(subConverterUrl, { headers: { 'User-Agent': userAgentHeader } });//订阅转换
if (!subConverterResponse.ok) return new Response(base64Data, { headers: responseHeaders });
let subConverterContent = await subConverterResponse.text();
- if (订阅格式 == 'clash') subConverterContent = await clashFix(subConverterContent);
+ if (订阅格式 === 'clash') {
+ subConverterContent = 合并Clash订阅(subConverterContent, 第三方Clash配置);
+ subConverterContent = await clashFix(subConverterContent);
+ }
// 只有非浏览器订阅才会返回SUBNAME
if (!userAgent.includes('mozilla')) responseHeaders["Content-Disposition"] = `attachment; filename*=utf-8''${encodeURIComponent(FileName)}`;
return new Response(subConverterContent, { headers: responseHeaders });
@@ -218,10 +245,10 @@ export default {
};
async function ADD(envadd) {
- var addtext = envadd.replace(/[ "'|\r\n]+/g, '\n').replace(/\n+/g, '\n'); // 替换为换行
+ var addtext = envadd.replace(/[\t"'|\r\n]+/g, '\n').replace(/\n+/g, '\n'); // 替换为换行
//console.log(addtext);
- if (addtext.charAt(0) == '\n') addtext = addtext.slice(1);
- if (addtext.charAt(addtext.length - 1) == '\n') addtext = addtext.slice(0, addtext.length - 1);
+ if (addtext.charAt(0) === '\n') addtext = addtext.slice(1);
+ if (addtext.charAt(addtext.length - 1) === '\n') addtext = addtext.slice(0, addtext.length - 1);
const add = addtext.split('\n');
//console.log(add);
return add;
@@ -262,7 +289,7 @@ async function sendMessage(type, ip, add_data = "") {
if (BotToken !== '' && ChatID !== '') {
let msg = "";
const response = await fetch(`http://ip-api.com/json/${ip}?lang=zh-CN`);
- if (response.status == 200) {
+ if (response.status === 200) {
const ipInfo = await response.json();
msg = `${type}\nIP: ${ip}\n国家: ${ipInfo.country}\n城市: ${ipInfo.city}\n组织: ${ipInfo.org}\nASN: ${ipInfo.as}\n${add_data}`;
} else {
@@ -326,6 +353,54 @@ function clashFix(content) {
return content;
}
+function 提取Clash代理内容(content) {
+ const lines = content.includes('\r\n') ? content.split('\r\n') : content.split('\n');
+ const proxiesIndex = lines.findIndex(line => line.trim() === 'proxies:');
+ if (proxiesIndex === -1) return '';
+
+ const proxyLines = [];
+ for (let i = proxiesIndex + 1; i < lines.length; i++) {
+ const line = lines[i];
+ if (line && !/^\s/.test(line) && /^[^#\s][^:]*:/.test(line)) break;
+ proxyLines.push(line);
+ }
+
+ while (proxyLines.length > 0 && !proxyLines[0].trim()) proxyLines.shift();
+ while (proxyLines.length > 0 && !proxyLines[proxyLines.length - 1].trim()) proxyLines.pop();
+
+ return proxyLines.join('\n');
+}
+
+function 合并Clash订阅(主配置, 第三方Clash配置 = []) {
+ const 额外代理内容 = [...new Set(第三方Clash配置.map(提取Clash代理内容).filter(Boolean))].join('\n');
+ if (!额外代理内容) return 主配置;
+
+ if (主配置.includes('\nproxy-groups:')) {
+ return 主配置.replace('\nproxy-groups:', `\n${额外代理内容}\nproxy-groups:`);
+ }
+
+ if (主配置.includes('proxies:')) {
+ return `${主配置}\n${额外代理内容}`;
+ }
+
+ return 主配置;
+}
+
+async function 获取Clash订阅配置(api, request, 追加UA, userAgentHeader) {
+ const clash配置 = [];
+ for (const apiUrl of [...new Set(api)].filter(item => item?.trim?.())) {
+ try {
+ const response = await getUrl(request, apiUrl, 'clash', 'Clash Verge/2.0');
+ if (!response.ok) continue;
+ const content = await response.text();
+ if (content.includes('proxies:')) clash配置.push(content);
+ } catch (error) {
+ console.log('获取Clash订阅配置失败: ' + apiUrl);
+ }
+ }
+ return clash配置;
+}
+
async function proxyURL(proxyURL, url) {
const URLs = await ADD(proxyURL);
const fullURL = URLs[Math.floor(Math.random() * URLs.length)];
@@ -340,7 +415,7 @@ async function proxyURL(proxyURL, url) {
let URLSearch = parsedURL.search;
// 处理 pathname
- if (URLPathname.charAt(URLPathname.length - 1) == '/') {
+ if (URLPathname.charAt(URLPathname.length - 1) === '/') {
URLPathname = URLPathname.slice(0, -1);
}
URLPathname += url.pathname;
@@ -366,12 +441,17 @@ async function proxyURL(proxyURL, url) {
return newResponse;
}
-async function getSUB(api, request, 追加UA, userAgentHeader) {
+function 创建订阅中转链接(订阅中转地址, 原始订阅链接, 追加UA, 原始UA = '') {
+ return `${订阅中转地址}&url=${encodeURIComponent(原始订阅链接)}&ua=${encodeURIComponent(追加UA || 'v2rayn')}&uafull=${encodeURIComponent(原始UA)}`;
+}
+
+async function getSUB(api, request, 追加UA, userAgentHeader, 订阅中转地址 = '') {
if (!api || api.length === 0) {
return [];
} else api = [...new Set(api)]; // 去重
let newapi = "";
let 订阅转换URLs = "";
+ let 第三方Clash配置 = [];
let 异常订阅 = "";
const controller = new AbortController(); // 创建一个AbortController实例,用于取消请求
const timeout = setTimeout(() => {
@@ -415,22 +495,33 @@ async function getSUB(api, request, 追加UA, userAgentHeader) {
if (response.status === 'fulfilled') {
const content = await response.value || 'null'; // 获取响应的内容
if (content.includes('proxies:')) {
- //console.log('Clash订阅: ' + response.apiUrl);
- 订阅转换URLs += "|" + response.apiUrl; // Clash 配置
+ // clash 请求直接复用已抓取到的配置,其它格式交给订阅转换器处理,避免重复加载。
+ if (追加UA === 'clash') {
+ 第三方Clash配置.push(content);
+ } else {
+ 订阅转换URLs += "|" + (订阅中转地址 ? 创建订阅中转链接(订阅中转地址, response.apiUrl, 追加UA, userAgentHeader || '') : response.apiUrl);
+ }
} else if (content.includes('outbounds"') && content.includes('inbounds"')) {
//console.log('Singbox订阅: ' + response.apiUrl);
- 订阅转换URLs += "|" + response.apiUrl; // Singbox 配置
+ 订阅转换URLs += "|" + (订阅中转地址 ? 创建订阅中转链接(订阅中转地址, response.apiUrl, 追加UA, userAgentHeader || '') : response.apiUrl); // Singbox 配置
} else if (content.includes('://')) {
//console.log('明文订阅: ' + response.apiUrl);
newapi += content + '\n'; // 追加内容
} else if (isValidBase64(content)) {
//console.log('Base64订阅: ' + response.apiUrl);
- newapi += base64Decode(content) + '\n'; // 解码并追加内容
+ try {
+ newapi += base64Decode(content) + '\n'; // 解码并追加内容
+ } catch (error) {
+ console.log('Base64订阅解码失败,转交订阅转换器: ' + response.apiUrl);
+ 订阅转换URLs += "|" + response.apiUrl;
+ }
} else {
- const 异常订阅LINK = `trojan://CMLiussss@127.0.0.1:8888?security=tls&allowInsecure=1&type=tcp&headerType=none#%E5%BC%82%E5%B8%B8%E8%AE%A2%E9%98%85%20${response.apiUrl.split('://')[1].split('/')[0]}`;
- console.log('异常订阅: ' + 异常订阅LINK);
- 异常订阅 += `${异常订阅LINK}\n`;
+ console.log('未识别订阅格式,转交订阅转换器: ' + response.apiUrl);
+ 订阅转换URLs += "|" + response.apiUrl;
}
+ } else {
+ console.log('订阅请求失败,转交订阅转换器: ' + response.apiUrl);
+ 订阅转换URLs += "|" + response.apiUrl;
}
}
} catch (error) {
@@ -441,13 +532,13 @@ async function getSUB(api, request, 追加UA, userAgentHeader) {
const 订阅内容 = await ADD(newapi + 异常订阅); // 将处理后的内容转换为数组
// 返回处理后的结果
- return [订阅内容, 订阅转换URLs];
+ return [订阅内容, 订阅转换URLs, 第三方Clash配置];
}
async function getUrl(request, targetUrl, 追加UA, userAgentHeader) {
// 设置自定义 User-Agent
const newHeaders = new Headers(request.headers);
- newHeaders.set("User-Agent", `${atob('djJyYXlOLzYuNDU=')} cmliu/CF-Workers-SUB ${追加UA}(${userAgentHeader})`);
+ newHeaders.set("User-Agent", 获取订阅UA(追加UA, userAgentHeader));
// 构建新的请求对象
const modifiedRequest = new Request(targetUrl, {
@@ -475,11 +566,30 @@ async function getUrl(request, targetUrl, 追加UA, userAgentHeader) {
return fetch(modifiedRequest);
}
+function 获取订阅UA(追加UA, userAgentHeader = '') {
+ if (userAgentHeader && !userAgentHeader.toLowerCase().includes('mozilla') && !userAgentHeader.toLowerCase().includes('subconverter')) {
+ return userAgentHeader;
+ }
+ if (追加UA === 'clash') return 'Clash Verge/2.0';
+ if (追加UA === 'singbox') return 'sing-box';
+ if (追加UA === 'surge') return 'Surge';
+ if (追加UA === 'Quantumult%20X') return 'Quantumult X';
+ if (追加UA === 'Loon') return 'Loon';
+ return `${atob('djJyYXlOLzYuNDU=')} cmliu/CF-Workers-SUB`;
+}
+
function isValidBase64(str) {
// 先移除所有空白字符(空格、换行、回车等)
const cleanStr = str.replace(/\s/g, '');
- const base64Regex = /^[A-Za-z0-9+/=]+$/;
- return base64Regex.test(cleanStr);
+ if (!cleanStr || cleanStr.length < 16) return false;
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
+ if (!base64Regex.test(cleanStr)) return false;
+ try {
+ atob(cleanStr);
+ return true;
+ } catch {
+ return false;
+ }
}
async function 迁移地址列表(env, txt = 'ADD.txt') {
@@ -825,4 +935,4 @@ async function KV(request, env, txt = 'ADD.txt', guest) {
headers: { "Content-Type": "text/plain;charset=utf-8" }
});
}
-}
\ No newline at end of file
+}