// version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC. // @ts-ignore import { connect } from 'cloudflare:sockets'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; const proxyIPs = ['cdn-all.xn--b6gac.eu.org', 'cdn.xn--b6gac.eu.org', 'cdn-b100.xn--b6gac.eu.org', 'edgetunnel.anycast.eu.org', 'cdn.anycast.eu.org']; let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query // v2board api environment variables (optional) deprecated, please use planetscale.com instead if (!isValidUUID(userID)) { throw new Error('uuid is invalid'); } export default { /** * @param {import("@cloudflare/workers-types").Request} request * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env * @param {import("@cloudflare/workers-types").ExecutionContext} ctx * @returns {Promise} */ async fetch(request, env, ctx) { // uuid_validator(request); try { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; dohURL = env.DNS_RESOLVER_URL || dohURL; let userID_Path = userID; if (userID.includes(',')) { userID_Path = userID.split(',')[0]; } const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); switch (url.pathname) { case '/cf': return new Response(JSON.stringify(request.cf, null, 4), { status: 200, headers: { "Content-Type": "application/json;charset=utf-8", }, }); case `/${userID_Path}`: { const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); return new Response(`${vlessConfig}`, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", } }); } case `/sub/${userID_Path}`: { const url = new URL(request.url); const searchParams = url.searchParams; let vlessConfig = createVLESSSub(userID, request.headers.get('Host')); // If 'format' query param equals to 'clash', convert config to base64 if (searchParams.get('format') === 'clash') { vlessConfig = btoa(vlessConfig); } // Construct and return response object return new Response(vlessConfig, { status: 200, headers: { "Content-Type": "text/plain;charset=utf-8", } }); } case `/bestip/${userID_Path}`: { const bestiplink = `https://sub.xf.free.hr/auto?host=${request.headers.get('Host')}&uuid=${userID_Path}` const reqHeaders = new Headers(request.headers); const bestipresponse = await fetch(bestiplink, { redirect: 'manual', headers: reqHeaders, }); // Construct and return response object return bestipresponse } default: // return new Response('Not found', { status: 404 }); // For any other path, reverse proxy to 'www.fmprc.gov.cn' and return the original response, caching it in the process const hostnames = ['www.fmprc.gov.cn', 'www.xuexi.cn', 'www.gov.cn', 'mail.gov.cn', 'www.mofcom.gov.cn', 'www.gfbzb.gov.cn', 'www.miit.gov.cn', 'www.12377.cn']; url.hostname = hostnames[Math.floor(Math.random() * hostnames.length)]; url.protocol = 'https:'; const newHeaders = new Headers(request.headers); newHeaders.set('cf-connecting-ip', newHeaders.get('x-forwarded-for') || newHeaders.get('cf-connecting-ip')); newHeaders.set('x-forwarded-for', newHeaders.get('cf-connecting-ip')); newHeaders.set('x-real-ip', newHeaders.get('cf-connecting-ip')); newHeaders.set('referer', 'https://www.google.com/q=edtunnel'); request = new Request(url, { method: request.method, headers: newHeaders, body: request.body, redirect: request.redirect, }); const cache = caches.default; let response = await cache.match(request); if (!response) { try { response = await fetch(request, { redirect: 'manual' }); } catch (err) { url.protocol = 'http:'; url.hostname = hostnames[Math.floor(Math.random() * hostnames.length)]; request = new Request(url, { method: request.method, headers: newHeaders, body: request.body, redirect: request.redirect, }); response = await fetch(request, { redirect: 'manual' }); } const cloneResponse = response.clone(); ctx.waitUntil(cache.put(request, cloneResponse)); } return response; } } else { return await vlessOverWSHandler(request); } } catch (err) { /** @type {Error} */ let e = err; return new Response(e.toString()); } }, }; export async function uuid_validator(request) { const hostname = request.headers.get('Host'); const currentDate = new Date(); const subdomain = hostname.split('.')[0]; const year = currentDate.getFullYear(); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); const day = String(currentDate.getDate()).padStart(2, '0'); const formattedDate = `${year}-${month}-${day}`; // const daliy_sub = formattedDate + subdomain const hashHex = await hashHex_f(subdomain); // subdomain string contains timestamps utc and uuid string TODO. console.log(hashHex, subdomain, formattedDate); } export async function hashHex_f(string) { const encoder = new TextEncoder(); const data = encoder.encode(string); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); return hashHex; } /** * Handles VLESS over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the VLESS header. * @param {import("@cloudflare/workers-types").Request} request The incoming request object. * @returns {Promise} A Promise that resolves to a WebSocket response object. */ async function vlessOverWSHandler(request) { const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept(); let address = ''; let portWithRandomLog = ''; let currentDate = new Date(); const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || ''); }; const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ let remoteSocketWapper = { value: null, }; let udpStreamWrite = null; let isDns = false; // ws --> remote readableWebSocketStream.pipeTo(new WritableStream({ async write(chunk, controller) { if (isDns && udpStreamWrite) { return udpStreamWrite(chunk); } if (remoteSocketWapper.value) { const writer = remoteSocketWapper.value.writable.getWriter() await writer.write(chunk); writer.releaseLock(); return; } const { hasError, message, portRemote = 443, addressRemote = '', rawDataIndex, vlessVersion = new Uint8Array([0, 0]), isUDP, } = processVlessHeader(chunk, userID); address = addressRemote; portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `; if (hasError) { // controller.error(message); throw new Error(message); // cf seems has bug, controller.error will not end stream // webSocket.close(1000, message); return; } // If UDP and not DNS port, close it if (isUDP && portRemote !== 53) { throw new Error('UDP proxy only enabled for DNS which is port 53'); // cf seems has bug, controller.error will not end stream } if (isUDP && portRemote === 53) { isDns = true; } // ["version", "附加信息长度 N"] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); const rawClientData = chunk.slice(rawDataIndex); // TODO: support udp here when cf runtime has udp support if (isDns) { const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); udpStreamWrite = write; udpStreamWrite(rawClientData); return; } handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); }, close() { log(`readableWebSocketStream is close`); }, abort(reason) { log(`readableWebSocketStream is abort`, JSON.stringify(reason)); }, })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); return new Response(null, { status: 101, webSocket: client, }); } /** * Handles outbound TCP connections. * * @param {any} remoteSocket * @param {string} addressRemote The remote address to connect to. * @param {number} portRemote The remote port to connect to. * @param {Uint8Array} rawClientData The raw client data to write. * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. * @param {Uint8Array} vlessResponseHeader The VLESS response header. * @param {function} log The logging function. * @returns {Promise} The remote socket. */ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { /** * Connects to a given address and port and writes data to the socket. * @param {string} address The address to connect to. * @param {number} port The port to connect to. * @returns {Promise} A Promise that resolves to the connected socket. */ async function connectAndWrite(address, port) { /** @type {import("@cloudflare/workers-types").Socket} */ const tcpSocket = connect({ hostname: address, port: port, }); remoteSocket.value = tcpSocket; log(`connected to ${address}:${port}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // first write, nomal is tls client hello writer.releaseLock(); return tcpSocket; } /** * Retries connecting to the remote address and port if the Cloudflare socket has no incoming data. * @returns {Promise} A Promise that resolves when the retry is complete. */ async function retry() { const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) tcpSocket.closed.catch(error => { console.log('retry tcpSocket closed error', error); }).finally(() => { safeCloseWebSocket(webSocket); }) remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); } const tcpSocket = await connectAndWrite(addressRemote, portRemote); // when remoteSocket is ready, pass to websocket // remote--> ws remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); } /** * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. * @param {(info: string)=> void} log The logging function. * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. */ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { webSocketServer.addEventListener('message', (event) => { const message = event.data; controller.enqueue(message); }); webSocketServer.addEventListener('close', () => { safeCloseWebSocket(webSocketServer); controller.close(); }); webSocketServer.addEventListener('error', (err) => { log('webSocketServer has error'); controller.error(err); }); const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); if (error) { controller.error(error); } else if (earlyData) { controller.enqueue(earlyData); } }, pull(controller) { // if ws can stop read if stream is full, we can implement backpressure // https://streams.spec.whatwg.org/#example-rs-push-backpressure }, cancel(reason) { log(`ReadableStream was canceled, due to ${reason}`) readableStreamCancel = true; safeCloseWebSocket(webSocketServer); } }); return stream; } // https://xtls.github.io/development/protocols/vless.html // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw /** * Processes the VLESS header buffer and returns an object with the relevant information. * @param {ArrayBuffer} vlessBuffer The VLESS header buffer to process. * @param {string} userID The user ID to validate against the UUID in the VLESS header. * @returns {{ * hasError: boolean, * message?: string, * addressRemote?: string, * addressType?: number, * portRemote?: number, * rawDataIndex?: number, * vlessVersion?: Uint8Array, * isUDP?: boolean * }} An object with the relevant information extracted from the VLESS header buffer. */ function processVlessHeader(vlessBuffer, userID) { if (vlessBuffer.byteLength < 24) { return { hasError: true, message: 'invalid data', }; } const version = new Uint8Array(vlessBuffer.slice(0, 1)); let isValidUser = false; let isUDP = false; const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); const slicedBufferString = stringify(slicedBuffer); // check if userID is valid uuid or uuids split by , and contains userID in it otherwise return error message to console const uuids = userID.includes(',') ? userID.split(",") : [userID]; // uuid_validator(hostName, slicedBufferString); // isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()); isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim(); console.log(`userID: ${slicedBufferString}`); if (!isValidUser) { return { hasError: true, message: 'invalid user', }; } const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; //skip opt for now const command = new Uint8Array( vlessBuffer.slice(18 + optLength, 18 + optLength + 1) )[0]; // 0x01 TCP // 0x02 UDP // 0x03 MUX if (command === 1) { isUDP = false; } else if (command === 2) { isUDP = true; } else { return { hasError: true, message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, }; } const portIndex = 18 + optLength + 1; const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); // port is big-Endian in raw data etc 80 == 0x005d const portRemote = new DataView(portBuffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array( vlessBuffer.slice(addressIndex, addressIndex + 1) ); // 1--> ipv4 addressLength =4 // 2--> domain name addressLength=addressBuffer[1] // 3--> ipv6 addressLength =16 const addressType = addressBuffer[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ''; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ).join('.'); break; case 2: addressLength = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) )[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; case 3: addressLength = 16; const dataView = new DataView( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(':'); // seems no need add [] for ipv6 break; default: return { hasError: true, message: `invild addressType is ${addressType}`, }; } if (!addressValue) { return { hasError: true, message: `addressValue is empty, addressType is ${addressType}`, }; } return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: addressValueIndex + addressLength, vlessVersion: version, isUDP, }; } /** * Converts a remote socket to a WebSocket connection. * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert. * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to. * @param {ArrayBuffer | null} vlessResponseHeader The VLESS response header. * @param {(() => Promise) | null} retry The function to retry the connection if it fails. * @param {(info: string) => void} log The logging function. * @returns {Promise} A Promise that resolves when the conversion is complete. */ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { // remote--> ws let remoteChunkCount = 0; let chunks = []; /** @type {ArrayBuffer | null} */ let vlessHeader = vlessResponseHeader; let hasIncomingData = false; // check if remoteSocket has incoming data await remoteSocket.readable .pipeTo( new WritableStream({ start() { }, /** * * @param {Uint8Array} chunk * @param {*} controller */ async write(chunk, controller) { hasIncomingData = true; remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( 'webSocket.readyState is not open, maybe close' ); } if (vlessHeader) { webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); vlessHeader = null; } else { // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`); // seems no need rate limit this, CF seems fix this??.. // if (remoteChunkCount > 20000) { // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M // await delay(1); // } webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. }, abort(reason) { console.error(`remoteConnection!.readable abort`, reason); }, }) ) .catch((error) => { console.error( `remoteSocketToWS has exception `, error.stack || error ); safeCloseWebSocket(webSocket); }); // seems is cf connect socket have error, // 1. Socket.closed will have error // 2. Socket.readable will be close without any data coming if (hasIncomingData === false && retry) { log(`retry`) retry(); } } /** * Decodes a base64 string into an ArrayBuffer. * @param {string} base64Str The base64 string to decode. * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error. */ function base64ToArrayBuffer(base64Str) { if (!base64Str) { return { earlyData: null, error: null }; } try { // go use modified Base64 for URL rfc4648 which js atob not support base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const decode = atob(base64Str); const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); return { earlyData: arryBuffer.buffer, error: null }; } catch (error) { return { earlyData: null, error }; } } /** * Checks if a given string is a valid UUID. * Note: This is not a real UUID validation. * @param {string} uuid The string to validate as a UUID. * @returns {boolean} True if the string is a valid UUID, false otherwise. */ function isValidUUID(uuid) { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(uuid); } const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; /** * Closes a WebSocket connection safely without throwing exceptions. * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close. */ function safeCloseWebSocket(socket) { try { if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { socket.close(); } } catch (error) { console.error('safeCloseWebSocket error', error); } } const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 256).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } function stringify(arr, offset = 0) { const uuid = unsafeStringify(arr, offset); if (!isValidUUID(uuid)) { throw TypeError("Stringified UUID is invalid"); } return uuid; } /** * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection. * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over. * @param {ArrayBuffer} vlessResponseHeader The VLESS response header. * @param {(string) => void} log The logging function. * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream. */ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { let isVlessHeaderSent = false; const transformStream = new TransformStream({ start(controller) { }, transform(chunk, controller) { // udp message 2 byte is the the length of udp data // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message for (let index = 0; index < chunk.byteLength;) { const lengthBuffer = chunk.slice(index, index + 2); const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); const udpData = new Uint8Array( chunk.slice(index + 2, index + 2 + udpPakcetLength) ); index = index + 2 + udpPakcetLength; controller.enqueue(udpData); } }, flush(controller) { } }); // only handle dns udp for now transformStream.readable.pipeTo(new WritableStream({ async write(chunk) { const resp = await fetch(dohURL, // dns server url { method: 'POST', headers: { 'content-type': 'application/dns-message', }, body: chunk, }) const dnsQueryResult = await resp.arrayBuffer(); const udpSize = dnsQueryResult.byteLength; // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); if (webSocket.readyState === WS_READY_STATE_OPEN) { log(`doh success and dns message length is ${udpSize}`); if (isVlessHeaderSent) { webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); } else { webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); isVlessHeaderSent = true; } } } })).catch((error) => { log('dns udp has error' + error) }); const writer = transformStream.writable.getWriter(); return { /** * * @param {Uint8Array} chunk */ write(chunk) { writer.write(chunk); } }; } /** * * @param {string} userID - single or comma separated userIDs * @param {string | null} hostName * @returns {string} */ function getVLESSConfig(userIDs, hostName) { const commonUrlPart = `:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`; const separator = "---------------------------------------------------------------"; const hashSeparator = "################################################################"; // Split the userIDs into an array let userIDArray = userIDs.split(','); // Prepare output array let output = []; let header = []; const clash_link = `https://subconverter.do.xn--b6gac.eu.org/sub?target=clash&url=https://${hostName}/sub/${userIDArray[0]}?format=clash&insert=false&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`; header.push(`\n

图片描述`); header.push(`\nWelcome! This function generates configuration for VLESS protocol. If you found this useful, please check our GitHub project for more:\n`); header.push(`欢迎!这是生成 VLESS 协议的配置。如果您发现这个项目很好用,请查看我们的 GitHub 项目给我一个star:\n`); header.push(`\nEDtunnel - https://github.com/3Kmfi6HP/EDtunnel\n`); header.push(`\n\n\n`.replace(/USERNAME/g, "3Kmfi6HP").replace(/REPOSITORY/g, "EDtunnel")); header.push(`VLESS 节点订阅连接\nClash 节点订阅连接\nClash 节点订阅连接2

\n`); header.push(``); // Generate output string for each userID userIDArray.forEach((userID) => { const vlessMain = `vless://${userID}@${hostName}${commonUrlPart}`; const vlessSec = `vless://${userID}@${proxyIP}${commonUrlPart}`; output.push(`UUID: ${userID}`); output.push(`${hashSeparator}\nv2ray default ip\n${separator}\n${vlessMain}\n${separator}`); output.push(`${hashSeparator}\nv2ray with best ip\n${separator}\n${vlessSec}\n${separator}`); }); output.push(`${hashSeparator}\n# Clash Proxy Provider 配置格式(configuration format)\nproxy-groups:\n - name: UseProvider\n type: select\n use:\n - provider1\n proxies:\n - Proxy\n - DIRECT\nproxy-providers:\n provider1:\n type: http\n url: https://${hostName}/sub/${userIDArray[0]}?format=clash\n interval: 3600\n path: ./provider1.yaml\n health-check:\n enable: true\n interval: 600\n # lazy: true\n url: http://www.gstatic.com/generate_204\n\n${hashSeparator}`); // HTML Head with CSS const htmlHead = ` EDtunnel: VLESS configuration `; // Join output with newlines, wrap inside and return ` ${htmlHead}
${header.join('')}
${output.join('\n')}
`; } function createVLESSSub(userID_Path, hostName) { let portArray_http = [80, 8080, 8880, 2052, 2086, 2095, 2082]; let portArray_https = [443, 8443, 2053, 2096, 2087, 2083]; // Split the userIDs into an array let userIDArray = userID_Path.includes(',') ? userID_Path.split(',') : [userID_Path]; // Prepare output array let output = []; // Generate output string for each userID userIDArray.forEach((userID) => { // Check if the hostName is a Cloudflare Pages domain, if not, generate HTTP configurations // reasons: pages.dev not support http only https if (!hostName.includes('pages.dev')) { // Iterate over all ports for http portArray_http.forEach((port) => { const commonUrlPart_http = `:${port}?encryption=none&security=none&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}-HTTP-${port}`; const vlessMainHttp = `vless://${userID}@${hostName}${commonUrlPart_http}`; // For each proxy IP, generate a VLESS configuration and add to output proxyIPs.forEach((proxyIP) => { const vlessSecHttp = `vless://${userID}@${proxyIP}${commonUrlPart_http}-${proxyIP}-EDtunnel`; output.push(`${vlessMainHttp}`); output.push(`${vlessSecHttp}`); }); }); } // Iterate over all ports for https portArray_https.forEach((port) => { const commonUrlPart_https = `:${port}?encryption=none&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}-HTTPS-${port}`; const vlessMainHttps = `vless://${userID}@${hostName}${commonUrlPart_https}`; // For each proxy IP, generate a VLESS configuration and add to output proxyIPs.forEach((proxyIP) => { const vlessSecHttps = `vless://${userID}@${proxyIP}${commonUrlPart_https}-${proxyIP}-EDtunnel`; output.push(`${vlessMainHttps}`); output.push(`${vlessSecHttps}`); }); }); }); // Join output with newlines return output.join('\n'); }