diff --git a/_worker.js b/_worker.js index 7b57eb7..6d439d7 100644 --- a/_worker.js +++ b/_worker.js @@ -1,6 +1,7 @@ // version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC. // @ts-ignore import { connect } from 'cloudflare:sockets'; +// import { connectdb } from '@planetscale/database'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" @@ -11,7 +12,8 @@ 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 +// v2board api environment variables (optional) +// now deprecated, please use planetscale.com instead let nodeId = ''; // 1 let apiToken = ''; //abcdefghijklmnopqrstuvwxyz123456 @@ -19,7 +21,7 @@ let apiToken = ''; //abcdefghijklmnopqrstuvwxyz123456 let apiHost = ''; // api.v2board.com if (!isValidUUID(userID)) { - throw new Error('uuid is not valid'); + throw new Error('uuid is invalid'); } export default { @@ -37,6 +39,10 @@ export default { nodeId = env.NODE_ID || nodeId; apiToken = env.API_TOKEN || apiToken; apiHost = env.API_HOST || apiHost; + 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); @@ -89,7 +95,7 @@ export default { } catch (connectError) { return new Response(connectError.message, { status: 500 }); } - case `/${userID}`: { + case `/${userID_Path}`: { const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); return new Response(`${vlessConfig}`, { status: 200, @@ -98,6 +104,15 @@ export default { } }); } + case `/sub/${userID_Path}`: { + const vlessConfig = createVLESSSub(userID, request.headers.get('Host')); + return new Response(`${vlessConfig}`, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } default: // return new Response('Not found', { status: 404 }); // For any other path, reverse proxy to 'www.fmprc.gov.cn' and return the original response @@ -116,20 +131,32 @@ export default { }, }; - - +/** + * Creates a PlanetScale connection object and returns it. + * @param {{DATABASE_HOST: string, DATABASE_USERNAME: string, DATABASE_PASSWORD: string}} env The environment variables containing the database connection information. + * @returns {Promise} A Promise that resolves to the PlanetScale connection object. + */ +function getPlanetScaleConnection(env) { + const config = { + host: env.DATABASE_HOST, + username: env.DATABASE_USERNAME, + password: env.DATABASE_PASSWORD, + fetch: (url, init) => { + delete (init)["cache"]; + return fetch(url, init); + } + } + return connectdb(config) +} /** - * - * @param {import("@cloudflare/workers-types").Request} request + * 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) { - - /** @type {import("@cloudflare/workers-types").WebSocket[]} */ - // @ts-ignore const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); - webSocket.accept(); let address = ''; @@ -171,24 +198,24 @@ async function vlessOverWSHandler(request) { isUDP, } = processVlessHeader(chunk, userID); address = addressRemote; - portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' - } `; + 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 but port not DNS port, close it - if (isUDP) { - if (portRemote === 53) { - isDns = true; - } else { - // controller.error('UDP proxy only enable for DNS which is port 53'); - throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream - 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); @@ -214,7 +241,6 @@ async function vlessOverWSHandler(request) { return new Response(null, { status: 101, - // @ts-ignore webSocket: client, }); } @@ -307,6 +333,13 @@ async function checkUuidInApiResponse(targetUuid) { * @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({ @@ -321,10 +354,12 @@ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawCli return tcpSocket; } - // if the cf connect tcp socket have no incoming data, we retry to redirect ip + /** + * 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) - // no matter retry success or not, close websocket tcpSocket.closed.catch(error => { console.log('retry tcpSocket closed error', error); }).finally(() => { @@ -341,42 +376,30 @@ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawCli } /** - * - * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer - * @param {string} earlyDataHeader for ws 0rtt - * @param {(info: string)=> void} log for ws 0rtt + * 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) => { - if (readableStreamCancel) { - return; - } const message = event.data; controller.enqueue(message); }); - // The event means that the client closed the client -> server stream. - // However, the server -> client stream is still open until you call close() on the server side. - // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. webSocketServer.addEventListener('close', () => { - // client send close, need close server - // if stream is cancel, skip controller.close safeCloseWebSocket(webSocketServer); - if (readableStreamCancel) { - return; - } controller.close(); - } - ); + }); + webSocketServer.addEventListener('error', (err) => { log('webSocketServer has error'); controller.error(err); - } - ); - // for ws 0rtt + }); const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); if (error) { controller.error(error); @@ -389,13 +412,8 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { // if ws can stop read if stream is full, we can implement backpressure // https://streams.spec.whatwg.org/#example-rs-push-backpressure }, + cancel(reason) { - // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here - // 2. if readableStream is cancel, all controller.close/enqueue need skip, - // 3. but from testing controller.error still work even if readableStream is cancel - if (readableStreamCancel) { - return; - } log(`ReadableStream was canceled, due to ${reason}`) readableStreamCancel = true; safeCloseWebSocket(webSocketServer); @@ -403,22 +421,27 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { }); return stream; - } // https://xtls.github.io/development/protocols/vless.html // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw /** - * - * @param { ArrayBuffer} vlessBuffer - * @param {string} userID - * @returns + * 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 -) { +function processVlessHeader(vlessBuffer, userID) { if (vlessBuffer.byteLength < 24) { return { hasError: true, @@ -430,10 +453,12 @@ function processVlessHeader( 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]; + console.log(slicedBufferString, uuids); - isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()); + // 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}`); @@ -455,6 +480,7 @@ function processVlessHeader( // 0x02 UDP // 0x03 MUX if (command === 1) { + isUDP = false; } else if (command === 2) { isUDP = true; } else { @@ -535,12 +561,13 @@ function processVlessHeader( /** - * - * @param {import("@cloudflare/workers-types").Socket} remoteSocket - * @param {import("@cloudflare/workers-types").WebSocket} webSocket - * @param {ArrayBuffer} vlessResponseHeader - * @param {(() => Promise) | null} retry - * @param {*} log + * 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 @@ -561,7 +588,7 @@ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, re */ async write(chunk, controller) { hasIncomingData = true; - // remoteChunkCount++; + remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( 'webSocket.readyState is not open, maybe close' @@ -571,6 +598,7 @@ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, re 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 @@ -606,13 +634,13 @@ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, re } /** - * - * @param {string} base64Str - * @returns + * 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 { error: null }; + return { earlyData: null, error: null }; } try { // go use modified Base64 for URL rfc4648 which js atob not support @@ -621,13 +649,15 @@ function base64ToArrayBuffer(base64Str) { const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); return { earlyData: arryBuffer.buffer, error: null }; } catch (error) { - return { error }; + return { earlyData: null, error }; } } /** - * This is not real UUID validation - * @param {string} uuid + * 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; @@ -637,8 +667,8 @@ function isValidUUID(uuid) { const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; /** - * Normally, WebSocket will not has exceptions when close. - * @param {import("@cloudflare/workers-types").WebSocket} socket + * Closes a WebSocket connection safely without throwing exceptions. + * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close. */ function safeCloseWebSocket(socket) { try { @@ -651,12 +681,15 @@ function safeCloseWebSocket(socket) { } 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)) { @@ -667,10 +700,11 @@ function stringify(arr, offset = 0) { /** - * - * @param {import("@cloudflare/workers-types").WebSocket} webSocket - * @param {ArrayBuffer} vlessResponseHeader - * @param {(string)=> void} log + * 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) { @@ -739,43 +773,77 @@ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { } /** - * - * @param {string} userID + * + * @param {string} userID - single or comma separated userIDs * @param {string | null} hostName * @returns {string} */ -function getVLESSConfig(userID, hostName) { - const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` - const vlessSec = `vless://${userID}@${proxyIP}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` - return ` -################################################################ -v2ray default ip ---------------------------------------------------------------- -${vlessMain} ---------------------------------------------------------------- -################################################################ -v2ray with best ip ---------------------------------------------------------------- -${vlessSec} ---------------------------------------------------------------- -################################################################ -clash-meta ---------------------------------------------------------------- -- type: vless - name: ${hostName} - server: ${hostName} - port: 443 - uuid: ${userID} - network: ws - tls: true - udp: false - sni: ${hostName} - client-fingerprint: chrome - ws-opts: - path: "/?ed=2048" - headers: - host: ${hostName} ---------------------------------------------------------------- -################################################################ -`; +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 = []; + + // Generate output string for each userID + userIDArray.forEach((userID) => { + + const vlessMain = `vless://${userID}@${hostName}${commonUrlPart}`; + const vlessSec = `vless://${userID}@${proxyIP}${commonUrlPart}`; + + 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}\nclash-meta\n${separator}\n- type: vless\n name: ${hostName}\n server: ${hostName}\n port: 443\n uuid: ${userID}\n network: ws\n tls: true\n udp: false\n sni: ${hostName}\n client-fingerprint: chrome\n ws-opts:\n path: "/?ed=2048"\n headers:\n host: ${hostName}\n${separator}\n${hashSeparator}`); + }); + + // Join output with newlines + return output.join('\n'); +} +function createVLESSSub(userID_Path, hostName) { + let portArray_http = [80, 8080, 8880, 2052, 2086, 2095]; + 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) => { + // 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`; + 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}`; + 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`; + 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}`; + output.push(`${vlessMainHttps}`); + output.push(`${vlessSecHttps}`); + }); + }); + }); + + // Join output with newlines + return output.join('\n'); } + diff --git a/wrangler.toml b/wrangler.toml index 8e28ef2..0bc94a4 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -6,10 +6,10 @@ main = "_worker.js" compatibility_date = "2023-05-26" [vars] -UUID = "65590e04-a94c-4c59-a1f2-571bce925aad" +# UUID = "d342d11e-d424-4583-b36e-524ab1f0afa4" # PROXYIP = "1.2.3.4" # DNS_RESOLVER_URL = "https://cloudflare-dns.com/dns-query" # NODE_ID = "1" # API_TOKEN = "example_dev_token" # API_HOST = "api.v2board.com" -# UUID = "1b6c1745-992e-4aac-8685-266c090e50ea,89b64978-6244-4bf3-bf64-67ade4ce5c8f" +UUID = "1b6c1745-992e-4aac-8685-266c090e50ea,89b64978-6244-4bf3-bf64-67ade4ce5c8f,d342d11e-d424-4583-b36e-524ab1f0afa4"