Jajal
Jajal
// if you want to use ipv6 or single proxyIP, please add comment at this line and
remove comment at the next line
let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)];
// use single proxyIP instead of random
// let proxyIP = 'cdn.xn--b6gac.eu.org';
// ipv6 proxyIP example remove comment to use
// let proxyIP = "[2a01:4f8:c2c:123f:64:5:6810:c55a]"
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) {
// 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;
const vlessSubConfig = createVLESSSub(userID,
request.headers.get('Host'));
// Construct and return response object
return new Response(btoa(vlessSubConfig), {
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 'ramdom
website' and return the original response, caching it in the process
const randomHostname =
cn_hostnames[Math.floor(Math.random() * cn_hostnames.length)];
const newHeaders = new
Headers(request.headers);
newHeaders.set('cf-connecting-ip', '1.2.3.4');
newHeaders.set('x-forwarded-for', '1.2.3.4');
newHeaders.set('x-real-ip', '1.2.3.4');
newHeaders.set('referer',
'https://www.google.com/search?q=edtunnel');
// Use fetch to proxy the request to 15
different domains
const proxyUrl = 'https://' + randomHostname +
url.pathname + url.search;
let modifiedRequest = new Request(proxyUrl, {
method: request.method,
headers: newHeaders,
body: request.body,
redirect: 'manual',
});
const proxyResponse = await
fetch(modifiedRequest, { redirect: 'manual' });
// Check for 302 or 301 redirect status and
return an error response
if ([301, 302].includes(proxyResponse.status))
{
return new Response(`Redirects to $
{randomHostname} are not allowed.`, {
status: 403,
statusText: 'Forbidden',
});
}
// Return the response from the proxy server
return proxyResponse;
}
} else {
return await vlessOverWSHandler(request);
}
} catch (err) {
/** @type {Error} */ let e = err;
return new Response(e.toString());
}
},
};
/**
* 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();
// 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
}
/**
* 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);
}
/**
* 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();
});
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',
};
}
console.log(`userID: ${slicedBufferString}`);
if (!isValidUser) {
return {
hasError: true,
message: 'invalid user',
};
}
// 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);
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);
});
/**
* 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);
}
}
/**
* 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) {
},
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) {
}
});
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 hashSeparator =
"################################################################";
<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>
const portSet_http = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]);
const portSet_https = new Set([443, 8443, 2053, 2096, 2087, 2083]);
return output.join('\n');
}
const cn_hostnames = [
'weibo.com', // Weibo - A popular social media platform
'www.baidu.com', // Baidu - The largest search engine in China
'www.qq.com', // QQ - A widely used instant messaging platform
'www.taobao.com', // Taobao - An e-commerce website owned by
Alibaba Group
'www.jd.com', // JD.com - One of the largest online retailers
in China
'www.sina.com.cn', // Sina - A Chinese online media company
'www.sohu.com', // Sohu - A Chinese internet service provider
'www.tmall.com', // Tmall - An online retail platform owned by
Alibaba Group
'www.163.com', // NetEase Mail - One of the major email
providers in China
'www.zhihu.com', // Zhihu - A popular question-and-answer website
'www.youku.com', // Youku - A Chinese video sharing platform
'www.xinhuanet.com', // Xinhua News Agency - Official news agency of
China
'www.douban.com', // Douban - A Chinese social networking service
'www.meituan.com', // Meituan - A Chinese group buying website for
local services
'www.toutiao.com', // Toutiao - A news and information content
platform
'www.ifeng.com', // iFeng - A popular news website in China
'www.autohome.com.cn', // Autohome - A leading Chinese automobile online
platform
'www.360.cn', // 360 - A Chinese internet security company
'www.douyin.com', // Douyin - A Chinese short video platform
'www.kuaidi100.com', // Kuaidi100 - A Chinese express delivery
tracking service
'www.wechat.com', // WeChat - A popular messaging and social media
app
'www.csdn.net', // CSDN - A Chinese technology community website
'www.imgo.tv', // ImgoTV - A Chinese live streaming platform
'www.aliyun.com', // Alibaba Cloud - A Chinese cloud computing
company
'www.eyny.com', // Eyny - A Chinese multimedia resource-sharing
website
'www.mgtv.com', // MGTV - A Chinese online video platform
'www.xunlei.com', // Xunlei - A Chinese download manager and
torrent client
'www.hao123.com', // Hao123 - A Chinese web directory service
'www.bilibili.com', // Bilibili - A Chinese video sharing and
streaming platform
'www.youth.cn', // Youth.cn - A China Youth Daily news portal
'www.hupu.com', // Hupu - A Chinese sports community and forum
'www.youzu.com', // Youzu Interactive - A Chinese game developer
and publisher
'www.panda.tv', // Panda TV - A Chinese live streaming platform
'www.tudou.com', // Tudou - A Chinese video-sharing website
'www.zol.com.cn', // ZOL - A Chinese electronics and gadgets
website
'www.toutiao.io', // Toutiao - A news and information app
'www.tiktok.com', // TikTok - A Chinese short-form video app
'www.netease.com', // NetEase - A Chinese internet technology
company
'www.cnki.net', // CNKI - China National Knowledge
Infrastructure, an information aggregator
'www.zhibo8.cc', // Zhibo8 - A website providing live sports
streams
'www.zhangzishi.cc', // Zhangzishi - Personal website of Zhang Zishi,
a public intellectual in China
'www.xueqiu.com', // Xueqiu - A Chinese online social platform for
investors and traders
'www.qqgongyi.com', // QQ Gongyi - Tencent's charitable foundation
platform
'www.ximalaya.com', // Ximalaya - A Chinese online audio platform
'www.dianping.com', // Dianping - A Chinese online platform for
finding and reviewing local businesses
'www.suning.com', // Suning - A leading Chinese online retailer
'www.zhaopin.com', // Zhaopin - A Chinese job recruitment platform
'www.jianshu.com', // Jianshu - A Chinese online writing platform
'www.mafengwo.cn', // Mafengwo - A Chinese travel information
sharing platform
'www.51cto.com', // 51CTO - A Chinese IT technical community
website
'www.qidian.com', // Qidian - A Chinese web novel platform
'www.ctrip.com', // Ctrip - A Chinese travel services provider
'www.pconline.com.cn', // PConline - A Chinese technology news and
review website
'www.cnzz.com', // CNZZ - A Chinese web analytics service
provider
'www.telegraph.co.uk', // The Telegraph - A British newspaper website
'www.ynet.com', // Ynet - A Chinese news portal
'www.ted.com', // TED - A platform for ideas worth spreading
'www.renren.com', // Renren - A Chinese social networking service
'www.pptv.com', // PPTV - A Chinese online video streaming
platform
'www.liepin.com', // Liepin - A Chinese online recruitment website
'www.881903.com', // 881903 - A Hong Kong radio station website
'www.aipai.com', // Aipai - A Chinese online video sharing
platform
'www.ttpaihang.com', // Ttpaihang - A Chinese celebrity popularity
ranking website
'www.quyaoya.com', // Quyaoya - A Chinese online ticketing platform
'www.91.com', // 91.com - A Chinese software download website
'www.dianyou.cn', // Dianyou - A Chinese game information website
'www.tmtpost.com', // TMTPost - A Chinese technology media platform
'www.douban.com', // Douban - A Chinese social networking service
'www.guancha.cn', // Guancha - A Chinese news and commentary
website
'www.so.com', // So.com - A Chinese search engine
'www.58.com', // 58.com - A Chinese classified advertising
website
'www.cnblogs.com', // Cnblogs - A Chinese technology blog community
'www.cntv.cn', // CCTV - China Central Television official
website
'www.secoo.com', // Secoo - A Chinese luxury e-commerce platform
];