Last active
August 15, 2023 17:39
-
-
Save 3yed-61/d8d55654b4acb7119ce2e4def0584838 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->. | |
// @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()" | |
let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; | |
const proxyIPs = ['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) | |
// now deprecated, please use planetscale.com instead | |
let nodeId = ''; // 1 | |
let apiToken = ''; //abcdefghijklmnopqrstuvwxyz123456 | |
let apiHost = ''; // api.v2board.com | |
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<Response>} | |
*/ | |
async fetch(request, env, ctx) { | |
try { | |
userID = env.UUID || userID; | |
proxyIP = env.PROXYIP || proxyIP; | |
dohURL = env.DNS_RESOLVER_URL || dohURL; | |
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); | |
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 '/connect': // for test connect to cf socket | |
const [hostname, port] = ['cloudflare.com', '80']; | |
console.log(`Connecting to ${hostname}:${port}...`); | |
try { | |
const socket = await connect({ | |
hostname: hostname, | |
port: parseInt(port, 10), | |
}); | |
const writer = socket.writable.getWriter(); | |
try { | |
await writer.write(new TextEncoder().encode('GET / HTTP/1.1\r\nHost: ' + hostname + '\r\n\r\n')); | |
} catch (writeError) { | |
writer.releaseLock(); | |
await socket.close(); | |
return new Response(writeError.message, { status: 500 }); | |
} | |
writer.releaseLock(); | |
const reader = socket.readable.getReader(); | |
let value; | |
try { | |
const result = await reader.read(); | |
value = result.value; | |
} catch (readError) { | |
await reader.releaseLock(); | |
await socket.close(); | |
return new Response(readError.message, { status: 500 }); | |
} | |
await reader.releaseLock(); | |
await socket.close(); | |
return new Response(new TextDecoder().decode(value), { status: 200 }); | |
} catch (connectError) { | |
return new Response(connectError.message, { status: 500 }); | |
} | |
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", | |
} | |
}); | |
} | |
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) { | |
// if not in cache, get response from origin | |
// send client ip to origin server to get right ip | |
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()); | |
} | |
}, | |
}; | |
/** | |
* 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<object>} 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) | |
} | |
/** | |
* 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<Response>} 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 = ''; | |
const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { | |
console.log(`[${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, | |
}); | |
} | |
let apiResponseCache = null; | |
let cacheTimeout = null; | |
/** | |
* Fetches the API response from the server and caches it for future use. | |
* @returns {Promise<object|null>} A Promise that resolves to the API response object or null if there was an error. | |
*/ | |
async function fetchApiResponse() { | |
const requestOptions = { | |
method: 'GET', | |
redirect: 'follow' | |
}; | |
try { | |
const response = await fetch(`https://${apiHost}/api/v1/server/UniProxy/user?node_id=${nodeId}&node_type=v2ray&token=${apiToken}`, requestOptions); | |
if (!response.ok) { | |
console.error('Error: Network response was not ok'); | |
return null; | |
} | |
const apiResponse = await response.json(); | |
apiResponseCache = apiResponse; | |
// Refresh the cache every 5 minutes (300000 milliseconds) | |
if (cacheTimeout) { | |
clearTimeout(cacheTimeout); | |
} | |
cacheTimeout = setTimeout(() => fetchApiResponse(), 300000); | |
return apiResponse; | |
} catch (error) { | |
console.error('Error:', error); | |
return null; | |
} | |
} | |
/** | |
* Returns the cached API response if it exists, otherwise fetches the API response from the server and caches it for future use. | |
* @returns {Promise<object|null>} A Promise that resolves to the cached API response object or the fetched API response object, or null if there was an error. | |
*/ | |
async function getApiResponse() { | |
if (!apiResponseCache) { | |
return await fetchApiResponse(); | |
} | |
return apiResponseCache; | |
} | |
/** | |
* Checks if a given UUID is present in the API response. | |
* @param {string} targetUuid The UUID to search for. | |
* @returns {Promise<boolean>} A Promise that resolves to true if the UUID is present in the API response, false otherwise. | |
*/ | |
async function checkUuidInApiResponse(targetUuid) { | |
// Check if any of the environment variables are empty | |
if (!nodeId || !apiToken || !apiHost) { | |
return false; | |
} | |
try { | |
const apiResponse = await getApiResponse(); | |
if (!apiResponse) { | |
return false; | |
} | |
const isUuidInResponse = apiResponse.users.some(user => user.uuid === targetUuid); | |
return isUuidInResponse; | |
} catch (error) { | |
console.error('Error:', error); | |
return false; | |
} | |
} | |
// Usage example: | |
// const targetUuid = "65590e04-a94c-4c59-a1f2-571bce925aad"; | |
// checkUuidInApiResponse(targetUuid).then(result => console.log(result)); | |
/** | |
* 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<void>} 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<import("@cloudflare/workers-types").Socket>} 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<void>} 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]; | |
console.log(slicedBufferString, uuids); | |
// 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<void>) | null} retry The function to retry the connection if it fails. | |
* @param {(info: string) => void} log The logging function. | |
* @returns {Promise<void>} 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 = []; | |
header.push(`\n<p align="center"> | |
<img src="https://cloudflare-ipfs.com/ipfs/bafybeigd6i5aavwpr6wvnwuyayklq3omonggta4x2q7kpmgafj357nkcky" alt="图片描述" style="margin-bottom: -50px;"> | |
`); | |
header.push(`\n<b style=" font-size: 15px;" >Welcome! This function generates configuration for VLESS protocol. If you found this useful, please check our GitHub project for more:</b>\n`); | |
header.push(`<b style=" font-size: 15px;" >欢迎!这是生成 VLESS 协议的配置。如果您发现这个项目很好用,请查看我们的 GitHub 项目给我一个star:</b>\n`); | |
header.push(`\n<a href="https://github.com/3Kmfi6HP/EDtunnel" target="_blank">EDtunnel - https://github.com/3Kmfi6HP/EDtunnel</a>\n`); | |
header.push(`\n<iframe src="https://ghbtns.com/github-btn.html?user=USERNAME&repo=REPOSITORY&type=star&count=true&size=large" frameborder="0" scrolling="0" width="170" height="30" title="GitHub"></iframe>\n\n`.replace(/USERNAME/g, "3Kmfi6HP").replace(/REPOSITORY/g, "EDtunnel")); | |
header.push(`<a href="//${hostName}/sub/${userIDArray[0]}" target="_blank">VLESS 节点订阅连接</a>\n<a href="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" target="_blank">Clash 节点订阅连接</a></p>\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 = ` | |
<head> | |
<title>EDtunnel: VLESS configuration</title> | |
<meta name="description" content="This is a tool for generating VLESS protocol configurations. Give us a star on GitHub https://github.com/3Kmfi6HP/EDtunnel if you found it useful!"> | |
<meta name="keywords" content="EDtunnel, cloudflare pages, cloudflare worker, severless"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<meta property="og:site_name" content="EDtunnel: VLESS configuration" /> | |
<meta property="og:type" content="website" /> | |
<meta property="og:title" content="EDtunnel - VLESS configuration and subscribe output" /> | |
<meta property="og:description" content="Use cloudflare pages and worker severless to implement vless protocol" /> | |
<meta property="og:url" content="https://${hostName}/" /> | |
<meta property="og:image" content="https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${encodeURIComponent(`vless://${userIDs.split(',')[0]}@${hostName}${commonUrlPart}`)}" /> | |
<meta name="twitter:card" content="summary_large_image" /> | |
<meta name="twitter:title" content="EDtunnel - VLESS configuration and subscribe output" /> | |
<meta name="twitter:description" content="Use cloudflare pages and worker severless to implement vless protocol" /> | |
<meta name="twitter:url" content="https://${hostName}/" /> | |
<meta name="twitter:image" content="https://cloudflare-ipfs.com/ipfs/bafybeigd6i5aavwpr6wvnwuyayklq3omonggta4x2q7kpmgafj357nkcky" /> | |
<meta property="og:image:width" content="1500" /> | |
<meta property="og:image:height" content="1500" /> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
background-color: #f0f0f0; | |
color: #333; | |
padding: 10px; | |
} | |
a { | |
color: #1a0dab; | |
text-decoration: none; | |
} | |
img { | |
max-width: 100%; | |
height: auto; | |
} | |
pre { | |
white-space: pre-wrap; | |
word-wrap: break-word; | |
background-color: #fff; | |
border: 1px solid #ddd; | |
padding: 15px; | |
margin: 10px 0; | |
} | |
/* Dark mode */ | |
@media (prefers-color-scheme: dark) { | |
body { | |
background-color: #333; | |
color: #f0f0f0; | |
} | |
a { | |
color: #9db4ff; | |
} | |
pre { | |
background-color: #282a36; | |
border-color: #6272a4; | |
} | |
} | |
</style> | |
</head> | |
`; | |
// Join output with newlines, wrap inside <html> and <body> | |
return ` | |
<html> | |
${htmlHead} | |
<body> | |
<pre style=" | |
background-color: transparent; | |
border: none; | |
">${header.join('')}</pre><pre>${output.join('\n')}</pre> | |
</body> | |
</html>`; | |
} | |
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) => { | |
// 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`; | |
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`; | |
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'); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment