├── README.md └── _worker.js /README.md: -------------------------------------------------------------------------------- 1 | # 项目略述 2 | * 免费读取50个高速时效Vless节点,并且一个小时更新一次API数据!(如果节点失效请等待更新。) 3 | * 时效的意义:有时效的SNI和HOST能够更好的保护原始服务器被墙的风险(时效的SNI和HOST重定向到原始服务器),实际上有能力固定SNI和HOST即相对更长时间让节点正常使用,不过出于保护必要就不公开了。 4 | * ### 少测速,少测速,少测速!以免节点失效太快! 5 | * 很好的主(备)用节点,ip比较干净(能上GPT)。 6 | * 因为是Vless协议,实际上可以修改Address部分为优选IP或者域名,以提高上网速度或者解决节点不通问题! 7 | * `注意:本项目获取到的节点名称并不一定对应实际出口IP地址,因为Vless协议无法像SS协议一样通过IP修改名称,比如写着“Germany-1”实际上是法国的IP。` 8 | * 已经修改成使用订阅转换器(三合一),在代码部分修改`WebToken: 'sub'//此处修改登录密码token`,默认( https://你的域名地址/sub )。 9 | 10 | * 本版本在测试阶段,任何可能、不可能的Bug都有可能发生! 11 | 12 | * 预计更新:Vless自定义优选订阅器。 13 | 14 | # 部署教程 15 | * 将_worker.js里的代码直接复制到Cloudflare进行部署(或者fork之后pages部署`自行尝试`),默认Token为sub。 16 | * Pages部署诺要修改Token,增加变量名为`TOKEN`并自行修改,重新部署即可。 17 | * 代码部署到网页之后显示base64编码内容表示搭建成功,网页地址均为订阅链接(不需要复制网页内的base64编码导入软件)。 18 | * 代码已使用订阅转换器,可允许在Nekobox、V2ray、Clash、Singbox等软件进行订阅,其他软件可自行测试。 19 | * `注意:本项目一定要绑自定义域名,否则会获取不到节点信息!!` 20 | 21 | ## 更多问题请联系: 22 | * [Telegram](https://t.me/Enkelte_bot) 23 | -------------------------------------------------------------------------------- /_worker.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | WebToken: 'sub',//此处修改登录密码token 3 | FileName: 'Vless',MainData: '',urls: [],subconverter: "SUBAPI.CMLiussss.net",subconfig: "https://raw.githubusercontent.com/cmliu/ACL4SSR/main/Clash/config/ACL4SSR_Online_MultiCountry.ini", subProtocol: 'https', 4 | }; 5 | export default { 6 | async fetch(request, env) { 7 | const userAgent = request.headers.get('User-Agent')?.toLowerCase() || "null"; 8 | const url = new URL(request.url); 9 | const token = url.searchParams.get('token'); 10 | config.WebToken = env.TOKEN || config.WebToken; 11 | config.subconverter = env.SUBAPI || config.subconverter; 12 | config.subconfig = env.SUBCONFIG || config.subconfig; 13 | config.FileName = env.SUBNAME || config.FileName; 14 | config.MainData = env.LINK || config.MainData; 15 | if (env.LINKSUB) config.urls = await addLinks(env.LINKSUB); 16 | await fetchAndDecryptData(); 17 | const currentDate = new Date(); 18 | currentDate.setHours(0, 0, 0, 0); 19 | const fakeToken = await MD5MD5(`${config.WebToken}${Math.ceil(currentDate.getTime() / 1000)}`); 20 | let allLinks = await addLinks(config.MainData + '\n' + config.urls.join('\n')); 21 | let selfHostedNodes = "", subscriptionLinks = ""; 22 | allLinks.forEach(x => x.toLowerCase().startsWith('http') ? subscriptionLinks += x + '\n' : selfHostedNodes += x + '\n'); 23 | config.MainData = selfHostedNodes; 24 | config.urls = await addLinks(subscriptionLinks); 25 | if (![config.WebToken, fakeToken].includes(token) && !url.pathname.includes("/" + config.WebToken)) { 26 | return new Response(await forbiddenPage(), { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' } }); 27 | } 28 | const subscriptionFormat = determineSubscriptionFormat(userAgent, url); 29 | let subscriptionConversionUrl = `${url.origin}/${await MD5MD5(fakeToken)}?token=${fakeToken}`; 30 | let req_data = config.MainData + (await getSubscription(config.urls, "v2rayn", request.headers.get('User-Agent')))[0].join('\n'); 31 | subscriptionConversionUrl += `|${(await getSubscription(config.urls, "v2rayn", request.headers.get('User-Agent')))[1]}`; 32 | if (env.WARP) subscriptionConversionUrl += `|${(await addLinks(env.WARP)).join("|")}`; 33 | const base64Data = btoa(req_data); 34 | if (subscriptionFormat === 'base64' || token === fakeToken) { 35 | return new Response(base64Data, { headers: { "content-type": "text/plain; charset=utf-8" } }); 36 | } 37 | try { 38 | const subconverterResponse = await fetch(buildSubconverterUrl(subscriptionFormat, subscriptionConversionUrl)); 39 | if (!subconverterResponse.ok) throw new Error(); 40 | let subconverterContent = await subconverterResponse.text(); 41 | if (subscriptionFormat === 'clash') subconverterContent = await clashFix(subconverterContent); 42 | return new Response(subconverterContent, { 43 | headers: { 44 | "Content-Disposition": `attachment; filename*=utf-8''${encodeURIComponent(config.FileName)}; filename=${config.FileName}`, 45 | "content-type": "text/plain; charset=utf-8", 46 | }, 47 | }); 48 | } catch { 49 | return new Response(base64Data, { headers: { "content-type": "text/plain; charset=utf-8" } }); 50 | } 51 | } 52 | }; 53 | function formatVlessLink({ protocol = '', uuid = '', address = '', port = '', encryption = '', security = '', sni = '', fingerprint = '', path = '', type = '', publicKey = '', shortId = '', flow = '', hostname = '', additionalParams: { host = '' } = {}}) { 54 | return `${protocol}://${uuid}@${address}:${port}?security=${security}&sni=${sni}&fp=${fingerprint}&type=${type}&path=${path}&host=${host}&pbk=${publicKey}&sid=${shortId}&flow=${flow}&encryption=${encryption}#${encodeURIComponent(hostname)}`; 55 | } 56 | async function fetchAndDecryptData() { 57 | const apiUrl = 'https://vless.enkelte.ggff.net/vless_list'; 58 | const keyApiUrl = 'https://key.enkelte.ggff.net/'; 59 | try { 60 | const { key, iv } = await (await fetch(keyApiUrl)).json(); 61 | const decodedKey = atob(key); 62 | const decodedIv = atob(iv); 63 | const encryptedData = await (await fetch(apiUrl)).text(); 64 | const decryptedJson = await decryptAES(atob(encryptedData), decodedKey, decodedIv); 65 | const data = JSON.parse(decryptedJson).data; 66 | config.MainData = data.map(formatVlessLink).join('\n'); 67 | } catch (error) { 68 | throw new Error('Error fetching or decrypting data: ' + error.message); 69 | } 70 | } 71 | async function decryptAES(data, key, iv) { 72 | const decoder = new TextDecoder('utf-8'); 73 | const encoder = new TextEncoder(); 74 | const keyBuffer = encoder.encode(key); 75 | const ivBuffer = encoder.encode(iv); 76 | const dataBuffer = hexToUint8Array(data); 77 | const cryptoKey = await crypto.subtle.importKey('raw', keyBuffer, { name: 'AES-CBC' }, false, ['decrypt']); 78 | const decryptedData = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivBuffer }, cryptoKey, dataBuffer); 79 | return decoder.decode(decryptedData); 80 | } 81 | function hexToUint8Array(hex) { 82 | return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); 83 | } 84 | function determineSubscriptionFormat(userAgent, url) { 85 | if (userAgent.includes('null') || userAgent.includes('subconverter')) return 'base64'; 86 | if (userAgent.includes('clash') || url.searchParams.has('clash')) return 'clash'; 87 | if (userAgent.includes('sing-box') || url.searchParams.has('sb') || url.searchParams.has('singbox')) return 'singbox'; 88 | if (userAgent.includes('surge') || url.searchParams.has('surge')) return 'surge'; 89 | return 'base64'; 90 | } 91 | function buildSubconverterUrl(subscriptionFormat, subscriptionConversionUrl) { 92 | return `${config.subProtocol}://${config.subconverter}/sub?target=${subscriptionFormat}&url=${encodeURIComponent(subscriptionConversionUrl)}&config=${encodeURIComponent(config.subconfig)}`; 93 | } 94 | async function addLinks(data) { 95 | return data.split("\n").filter(e => e.trim() !== ""); 96 | } 97 | async function getSubscription(urls, UA, userAgentHeader) { 98 | const headers = { "User-Agent": userAgentHeader || UA }; 99 | let subscriptionContent = [], unconvertedLinks = []; 100 | for (const url of urls) { 101 | try { 102 | const response = await fetch(url, { headers }); 103 | if (response.status === 200) { 104 | subscriptionContent.push((await response.text()).split("\n")); 105 | } else { 106 | unconvertedLinks.push(url); 107 | } 108 | } catch { 109 | unconvertedLinks.push(url); 110 | } 111 | } 112 | return [subscriptionContent.flat(), unconvertedLinks]; 113 | } 114 | async function clashFix(content) { 115 | return content.split("\n").reduce((acc, line) => { 116 | if (line.startsWith(" - name: ")) acc += ` - name: ${line.split("name: ")[1]}\n`; 117 | else acc += line + "\n"; 118 | return acc; 119 | }, ''); 120 | } 121 | async function forbiddenPage() { 122 | return `
Access Denied
`; 123 | } 124 | async function MD5MD5(value) { 125 | const encoded = new TextEncoder().encode(value); 126 | const buffer = await crypto.subtle.digest("MD5", await crypto.subtle.digest("MD5", encoded)); 127 | return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, "0")).join(""); 128 | } 129 | async function aes128cbcDecrypt(encryptedText, key, iv) { 130 | const encryptedBuffer = hexStringToUint8Array(encryptedText); 131 | const algorithm = { name: 'AES-CBC', iv }; 132 | const keyObj = await crypto.subtle.importKey('raw', key, algorithm, false, ['decrypt']); 133 | try { 134 | const decryptedBuffer = await crypto.subtle.decrypt(algorithm, keyObj, encryptedBuffer); 135 | return new TextDecoder().decode(decryptedBuffer).replace(/\0+$/, ''); 136 | } catch { 137 | throw new Error('Decryption failed'); 138 | } 139 | } 140 | function hexStringToUint8Array(hexString) { 141 | return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); 142 | } 143 | --------------------------------------------------------------------------------