Cloudflare 部署 | 最新BPB代理面板

搭建流程

Cloudflare官网地址: 【点我前往

Github项目地址: 【点我前往

详细设置

KV空间变量名称: bpb

面板网址后缀: /panel /login

原始默认密码: admin

部署代码

_worker.js
// @ts-nocheck
// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->.
// @ts-ignore
// https://github.com/bia-pain-bache/BPB-Worker-Panel

import { connect } from 'cloudflare:sockets';

// How to generate your own UUID:
// https://www.uuidgenerator.net/
let userID = '89b3cbba-e6ac-485a-9481-976a0415eab9';

// https://www.nslookup.io/domains/cdn.xn--b6gac.eu.org/dns-records/
// https://www.nslookup.io/domains/cdn-all.xn--b6gac.eu.org/dns-records/
const proxyIPs= ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org', 'edgetunnel.anycast.eu.org'];

const defaultHttpPorts = ['80', '8080', '2052', '2082', '2086', '2095', '8880'];
const defaultHttpsPorts = ['443', '8443', '2053', '2083', '2087', '2096'];

let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)];

let dohURL = 'https://cloudflare-dns.com/dns-query';

let panelVersion = '2.4.3';

if (!isValidUUID(userID)) {
    throw new Error('uuid is not valid');
}

export default {
    /**
     * @param {import("@cloudflare/workers-types").Request} request
     * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: 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;
            const upgradeHeader = request.headers.get('Upgrade');
            
            if (!upgradeHeader || upgradeHeader !== 'websocket') {
                
                const url = new URL(request.url);
                const searchParams = new URLSearchParams(url.search);
                const host = request.headers.get('Host');
                const client = searchParams.get('app');

                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 `/sub/${userID}`:

                        if (client === 'sfa') {
                            const BestPingSFA = await getSingboxConfig(env, host);
                            return new Response(`${JSON.stringify(BestPingSFA, null, 4)}`, { status: 200 });                            
                        }
                        const normalConfigs = await getNormalConfigs(env, host, client);
                        return new Response(normalConfigs, { status: 200 });                        

                    case `/fragsub/${userID}`:

                        let fragConfigs = await getFragmentConfigs(env, host, 'v2ray');
                        fragConfigs = fragConfigs.map(config => config.config);

                        return new Response(`${JSON.stringify(fragConfigs, null, 4)}`, { status: 200 });

                    case `/warpsub/${userID}`:

                        const wowConfig = await getWarpConfigs(env, client);
                        return new Response(`${JSON.stringify(wowConfig, null, 4)}`, { status: 200 });

                    case '/panel':

                        if (typeof env.bpb !== 'object') {
                            const errorPage = renderErrorPage('KV Dataset is not properly set!', null, true);
                            return new Response(errorPage, { status: 200, headers: {'Content-Type': 'text/html'}});
                        }

                        const isAuth = await Authenticate(request, env); 
                        
                        if (request.method === 'POST') {
                            
                            if (!isAuth) return new Response('Unauthorized', { status: 401 });             
                            const formData = await request.formData();
                            await updateDataset(env, formData);

                            return new Response('Success', { status: 200 });
                        }
                        
                        if (!isAuth) return Response.redirect(`${url.origin}/login`, 302);
                        if (! await env.bpb.get('proxySettings')) await updateDataset(env);
                        const fragConfs = await getFragmentConfigs(env, host, 'nekoray');
                        const homePage = await renderHomePage(env, host, fragConfs);

                        return new Response(homePage, {
                            status: 200,
                            headers: {
                                'Content-Type': 'text/html',
                                'Access-Control-Allow-Origin': url.origin,
                                'Access-Control-Allow-Methods': 'GET, POST',
                                'Access-Control-Allow-Headers': 'Content-Type, Authorization',
                                'X-Content-Type-Options': 'nosniff',
                                'X-Frame-Options': 'DENY',
                                'Referrer-Policy': 'strict-origin-when-cross-origin'
                            }
                        });
                                                      
                    case '/login':

                        if (typeof env.bpb !== 'object') {
                            const errorPage = renderErrorPage('KV Dataset is not properly set!', null, true);
                            return new Response(errorPage, { status: 200, headers: {'Content-Type': 'text/html'}});
                        }

                        const loginAuth = await Authenticate(request, env);
                        if (loginAuth) return Response.redirect(`${url.origin}/panel`, 302);

                        let secretKey = await env.bpb.get('secretKey');
                        const pwd = await env.bpb.get('pwd');
                        if (!pwd) await env.bpb.put('pwd', 'admin');

                        if (!secretKey) {
                            secretKey = generateSecretKey();
                            await env.bpb.put('secretKey', secretKey);
                        }

                        if (request.method === 'POST') {
                            const password = await request.text();
                            const savedPass = await env.bpb.get('pwd');

                            if (password === savedPass) {
                                const jwtToken = generateJWTToken(secretKey, password);
                                const cookieHeader = `jwtToken=${jwtToken}; HttpOnly; Secure; Max-Age=${7 * 24 * 60 * 60}; Path=/; SameSite=Strict`;
                                
                                return new Response('Success', {
                                    status: 200,
                                    headers: {
                                      'Set-Cookie': cookieHeader,
                                      'Content-Type': 'text/plain',
                                    }
                                });        
                            } else {
                                return new Response('Method Not Allowed', { status: 405 });
                            }
                        }
                        
                        const loginPage = await renderLoginPage();

                        return new Response(loginPage, {
                            status: 200,
                            headers: {
                                'Content-Type': 'text/html',
                                'Access-Control-Allow-Origin': url.origin,
                                'Access-Control-Allow-Methods': 'GET, POST',
                                'Access-Control-Allow-Headers': 'Content-Type, Authorization',
                                'X-Content-Type-Options': 'nosniff',
                                'X-Frame-Options': 'DENY',
                                'Referrer-Policy': 'strict-origin-when-cross-origin'
                            }
                        });
                    
                    case '/logout':
                                    
                        return new Response('Success', {
                            status: 200,
                            headers: {
                                'Set-Cookie': 'jwtToken=; Secure; SameSite=None; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
                                'Content-Type': 'text/plain'
                            }
                        });        

                    case '/panel/password':

                        let passAuth = await Authenticate(request, env);
                        if (!passAuth) return new Response('Unauthorized!', { status: 401 });           
                        const newPwd = await request.text();
                        const oldPwd = await env.bpb.get('pwd');
                        if (newPwd === oldPwd) return new Response('Please enter a new Password!', { status: 400 });
                        await env.bpb.put('pwd', newPwd);

                        return new Response('Success', {
                            status: 200,
                            headers: {
                                'Set-Cookie': 'jwtToken=; Path=/; Secure; SameSite=None; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
                                'Content-Type': 'text/plain',
                            }
                        });

                    default:
                        // return new Response('Not found', { status: 404 });
                        url.hostname = 'www.speedtest.net';
                        url.protocol = 'https:';
                        request = new Request(url, request);
                        return await fetch(request);
                }
            } else {
                return await vlessOverWSHandler(request);
            }
        } catch (err) {
            /** @type {Error} */ let e = err;
            const errorPage = renderErrorPage('Something went wrong!', e.message.toString(), false);
            return new Response(errorPage, { status: 200, headers: {'Content-Type': 'text/html'}});
        }
    },
};

/**
 * 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 = '';
	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(request, 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<void>} The remote socket.
 */
async function handleTCPOutBound(request, 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 { pathname } = new URL(request.url);
        let panelProxyIP = pathname.split('/')[2];
        panelProxyIP = panelProxyIP ? atob(panelProxyIP) : undefined;
		const tcpSocket = await connectAndWrite(panelProxyIP || 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<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
 * @param {string | null} hostName
 * @returns {string}
 */

const getNormalConfigs = async (env, hostName, client) => {
    let proxySettings = {};
    let vlessWsTls = '';

    try {
        proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while getting normal configs - ${error}`);
    }

    const { cleanIPs, proxyIP, ports } = proxySettings;
    const resolved = await resolveDNS(hostName);
    const Addresses = [
        hostName,
        'www.speedtest.net',
        ...resolved.ipv4,
        ...resolved.ipv6.map((ip) => `[${ip}]`),
        ...(cleanIPs ? cleanIPs.split(',') : [])
    ];

    ports.forEach(port => {
        Addresses.forEach((addr, index) => {

            vlessWsTls += 'vless' + `://${userID}@${addr}:${port}?encryption=none&type=ws&host=${
                randomUpperCase(hostName)}${
                defaultHttpsPorts.includes(port) 
                    ? `&security=tls&sni=${
                        randomUpperCase(hostName)
                    }&fp=randomized&alpn=${
                        client === 'singbox' ? 'http/1.1' : 'h2,http/1.1'
                    }`
                    : ''}&path=${`/${getRandomPath(16)}${proxyIP ? `/${encodeURIComponent(btoa(proxyIP))}` : ''}`}${
                        client === 'singbox' 
                            ? '&eh=Sec-WebSocket-Protocol&ed=2560' 
                            : encodeURIComponent('?ed=2560')
                    }#${encodeURIComponent(generateRemark(index, port))}\n`;
        });
    });

    return btoa(vlessWsTls);
}

const generateRemark = (index, port) => {
    let remark = '';
    switch (index) {
        case 0:
        case 1:
            remark = `💦 BPB - Domain_${index + 1} : ${port}`;
            break;
        case 2:
        case 3:
            remark = `💦 BPB - IPv4_${index - 1} : ${port}`;
            break;
        case 4:
        case 5:
            remark = `💦 BPB - IPv6_${index - 3} : ${port}`;
            break;
        default:
            remark = `💦 BPB - Clean IP_${index - 5} : ${port}`;
            break;
    }

    return remark;
}

const extractVlessParams = async (vlessConfig) => {
    const url = new URL(vlessConfig.replace('vless', 'http'));
    const params = new URLSearchParams(url.search);
    let configParams = {
        uuid : url.username,
        hostName : url.hostname,
        port : url.port
    };

    params.forEach( (value, key) => {
        configParams[key] = value;
    })

    return JSON.stringify(configParams);
}

const buildProxyOutbound = async (proxyParams) => {
    const { hostName, port, uuid, flow, security, type, sni, fp, alpn, pbk, sid, spx, headerType, host, path, authority, serviceName, mode } = proxyParams;
    let proxyOutbound = structuredClone(xrayOutboundTemp);   
    proxyOutbound.settings.vnext[0].address = hostName;
    proxyOutbound.settings.vnext[0].port = +port;
    proxyOutbound.settings.vnext[0].users[0].id = uuid;
    proxyOutbound.settings.vnext[0].users[0].flow = flow;
    proxyOutbound.streamSettings.security = security;
    proxyOutbound.streamSettings.network = type;
    proxyOutbound.tag = "out";
    proxyOutbound.streamSettings.sockopt.dialerProxy = "proxy";

    switch (security) {

        case 'tls':
            proxyOutbound.streamSettings.tlsSettings.serverName = sni;
            proxyOutbound.streamSettings.tlsSettings.fingerprint = fp;
            proxyOutbound.streamSettings.tlsSettings.alpn = alpn ? alpn?.split(',') : [];
            delete proxyOutbound.streamSettings.realitySettings;         
            break;

        case 'reality':
            proxyOutbound.streamSettings.realitySettings.publicKey = pbk;
            proxyOutbound.streamSettings.realitySettings.shortId = sid;
            proxyOutbound.streamSettings.realitySettings.serverName = sni;
            proxyOutbound.streamSettings.realitySettings.fingerprint = fp;
            proxyOutbound.streamSettings.realitySettings.spiderX = spx;
            delete proxyOutbound.mux;
            delete proxyOutbound.streamSettings.tlsSettings;
            break;

        default:
            delete proxyOutbound.streamSettings.tlsSettings;
            delete proxyOutbound.streamSettings.realitySettings;         
            break;
    }
 
    switch (type) {

        case 'tcp':
            delete proxyOutbound.streamSettings.grpcSettings;
            delete proxyOutbound.streamSettings.wsSettings;
            
            if (security === 'reality' && (!headerType || headerType === 'none')) {
                delete proxyOutbound.streamSettings.tcpSettings;
                break;
            }

            if (headerType === 'http') {
                proxyOutbound.streamSettings.tcpSettings.header.request.headers.Host = host?.split(',');
                proxyOutbound.streamSettings.tcpSettings.header.request.path = path?.split(',');
            } 
            
            if (!headerType) {
                proxyOutbound.streamSettings.tcpSettings.header.type = 'none';
                delete proxyOutbound.streamSettings.tcpSettings.header.request;
                delete proxyOutbound.streamSettings.tcpSettings.header.response;
            }
            
            break;

        case 'ws':
            proxyOutbound.streamSettings.wsSettings.headers.Host = host;
            proxyOutbound.streamSettings.wsSettings.path = path;
            delete proxyOutbound.streamSettings.grpcSettings;
            delete proxyOutbound.streamSettings.tcpSettings;
            break;

        case 'grpc':  
            proxyOutbound.streamSettings.grpcSettings.authority = authority;
            proxyOutbound.streamSettings.grpcSettings.serviceName = serviceName;
            proxyOutbound.streamSettings.grpcSettings.multiMode = mode === 'multi';
            delete proxyOutbound.mux;
            delete proxyOutbound.streamSettings.tcpSettings;
            delete proxyOutbound.streamSettings.wsSettings;
            break;

        default:
            break;
    }
    
    return proxyOutbound;
}

const buildWorkerLessConfig = async (env, client) => {
    let proxySettings = {};

    try {
        proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while generating WorkerLess config - ${error}`);
    }

    const { remoteDNS, localDNS, lengthMin,  lengthMax,  intervalMin,  intervalMax, blockAds, bypassIran, blockPorn, bypassLAN } = proxySettings;  
    let fakeOutbound = structuredClone(xrayOutboundTemp);
    delete fakeOutbound.mux;
    fakeOutbound.settings.vnext[0].address = 'google.com';
    fakeOutbound.settings.vnext[0].users[0].id = userID;
    delete fakeOutbound.streamSettings.sockopt;
    fakeOutbound.streamSettings.tlsSettings.serverName = 'google.com';
    fakeOutbound.streamSettings.wsSettings.headers.Host = 'google.com';
    fakeOutbound.streamSettings.wsSettings.path = '/';
    delete fakeOutbound.streamSettings.grpcSettings;
    delete fakeOutbound.streamSettings.tcpSettings;
    delete fakeOutbound.streamSettings.realitySettings;
    fakeOutbound.tag = 'fake-outbound';

    let fragConfig = structuredClone(xrayConfigTemp);
    fragConfig.remarks  = '💦 BPB Frag - WorkerLess ⭐'
    fragConfig.dns = await buildDNSObject(remoteDNS, localDNS, blockAds, bypassIran, blockPorn, true);
    fragConfig.outbounds[0].settings.domainStrategy = 'UseIP';
    fragConfig.outbounds[0].settings.fragment.length = `${lengthMin}-${lengthMax}`;
    fragConfig.outbounds[0].settings.fragment.interval = `${intervalMin}-${intervalMax}`;
    fragConfig.outbounds = [
        {...fragConfig.outbounds[0]}, 
        {...fakeOutbound}, 
        {...fragConfig.outbounds[1]}, 
        {...fragConfig.outbounds[2]}, 
        {...fragConfig.outbounds[3]}
    ];
    fragConfig.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, false, false, true);
    delete fragConfig.routing.balancers;
    delete fragConfig.observatory;

    if (client === 'nekoray') {
        fragConfig.inbounds[0].port = 2080;
        fragConfig.inbounds[1].port = 2081;
    }

    return fragConfig;
}

const getFragmentConfigs = async (env, hostName, client) => {
    let Configs = [];
    let outbounds = [];
    let proxySettings = {};
    let proxyOutbound;
    let proxyIndex = 1;
    const bestFragValues = ['10-20', '20-30', '30-40', '40-50', '50-60', '60-70', 
                            '70-80', '80-90', '90-100', '10-30', '20-40', '30-50', 
                            '40-60', '50-70', '60-80', '70-90', '80-100', '100-200']

    try {
        proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while getting fragment configs - ${error}`);
    }

    const {
        remoteDNS, 
        localDNS, 
        lengthMin, 
        lengthMax, 
        intervalMin, 
        intervalMax,
        blockAds,
        bypassIran,
        blockPorn,
        bypassLAN, 
        cleanIPs,
        proxyIP,
        outProxy,
        outProxyParams,
        ports = ['443']
    } = proxySettings;

    const resolved = await resolveDNS(hostName);
    const Addresses = [
        hostName,
        "www.speedtest.net",
        ...resolved.ipv4,
        ...resolved.ipv6.map((ip) => `[${ip}]`),
        ...(cleanIPs ? cleanIPs.split(",") : [])
    ];

    if (outProxy) {
        const proxyParams = JSON.parse(outProxyParams);
        try {
            proxyOutbound = await buildProxyOutbound(proxyParams);
        } catch (error) {
            console.log('An error occured while parsing chain proxy: ', error);
            proxyOutbound = undefined;
            await env.bpb.put("proxySettings", JSON.stringify({
                ...proxySettings, 
                outProxy: '',
                outProxyParams: ''}));
        }
    }

    for (let portIndex in ports.filter(port => defaultHttpsPorts.includes(port))) {
        let port = +ports[portIndex];
        for (let index in Addresses) {            
            let remark = generateRemark(+index, port);
            let addr = Addresses[index];
            let fragConfig = structuredClone(xrayConfigTemp);
            let outbound = structuredClone(xrayOutboundTemp);
            delete outbound.mux;
            delete outbound.streamSettings.grpcSettings;
            delete outbound.streamSettings.realitySettings;
            delete outbound.streamSettings.tcpSettings;
            outbound.settings.vnext[0].address = addr;
            outbound.settings.vnext[0].port = port;
            outbound.settings.vnext[0].users[0].id = userID;
            outbound.streamSettings.tlsSettings.serverName = randomUpperCase(hostName);
            outbound.streamSettings.wsSettings.headers.Host = randomUpperCase(hostName);
            outbound.streamSettings.wsSettings.path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}?ed=2560`;
            fragConfig.remarks = remark;
            fragConfig.dns = await buildDNSObject(remoteDNS, localDNS, blockAds, bypassIran, blockPorn);
            fragConfig.outbounds[0].settings.fragment.length = `${lengthMin}-${lengthMax}`;
            fragConfig.outbounds[0].settings.fragment.interval = `${intervalMin}-${intervalMax}`;
            
            if (proxyOutbound) {
                fragConfig.outbounds = [{...proxyOutbound}, { ...outbound}, ...fragConfig.outbounds];
                fragConfig.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, true, false);
            } else {
                fragConfig.outbounds = [{ ...outbound}, ...fragConfig.outbounds];
                fragConfig.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, false, false);
            }
            
            delete fragConfig.observatory;
            delete fragConfig.routing.balancers;

            if (client === 'nekoray') {
                fragConfig.inbounds[0].port = 2080;
                fragConfig.inbounds[1].port = 2081;
                fragConfig.inbounds[2].port = 6450;
            }
                        
            Configs.push({
                address: remark,
                config: fragConfig
            }); 

            outbound.tag = `prox_${proxyIndex}`;
        
            if (proxyOutbound) {
                let proxyOut = structuredClone(proxyOutbound);
                proxyOut.tag = `out_${proxyIndex}`;
                proxyOut.streamSettings.sockopt.dialerProxy = `prox_${proxyIndex}`;
                outbounds.push({...proxyOut}, {...outbound});
            } else {
                outbounds.push({...outbound});
            }

            proxyIndex++;
        };
    };

    let bestPing = structuredClone(xrayConfigTemp);
    bestPing.remarks = '💦 BPB Frag - Best Ping 💥';
    bestPing.dns = await buildDNSObject(remoteDNS, localDNS, blockAds, bypassIran, blockPorn);
    bestPing.outbounds[0].settings.fragment.length = `${lengthMin}-${lengthMax}`;
    bestPing.outbounds[0].settings.fragment.interval = `${intervalMin}-${intervalMax}`;
    bestPing.outbounds = [...outbounds, ...bestPing.outbounds];
    
    if (proxyOutbound) {
        bestPing.observatory.subjectSelector = ["out"];
        bestPing.routing.balancers[0].selector = ["out"];
        bestPing.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, true, true);
    } else {
        bestPing.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, false, true);
    }

    if (client === 'nekoray') {
        bestPing.inbounds[0].port = 2080;
        bestPing.inbounds[1].port = 2081;
        bestPing.inbounds[2].port = 6450;
    }

    let bestFragment = structuredClone(xrayConfigTemp);
    bestFragment.remarks = '💦 BPB Frag - Best Fragment 😎';
    bestFragment.dns = await buildDNSObject(remoteDNS, localDNS, blockAds, bypassIran, blockPorn);
    bestFragment.outbounds.splice(0,1);
    bestFragValues.forEach( (fragLength, index) => {
        bestFragment.outbounds.push({
            tag: `frag_${index + 1}`,
            protocol: "freedom",
            settings: {
                fragment: {
                    packets: "tlshello",
                    length: fragLength,
                    interval: "1-1"
                }
            },
            proxySettings: {
                tag: proxyOutbound ? "out" : "proxy"
            }
        });
    });

    let bestFragmentOutbounds = structuredClone([{...outbounds[0]}, {...outbounds[1]}]);  
    bestFragmentOutbounds[0].settings.vnext[0].port = 443;
    
    if (proxyOutbound) {
        bestFragmentOutbounds[0].streamSettings.sockopt.dialerProxy = 'proxy';
        delete bestFragmentOutbounds[1].streamSettings.sockopt.dialerProxy;
        bestFragmentOutbounds[0].tag = 'out';
        bestFragmentOutbounds[1].tag = 'proxy';
        bestFragment.outbounds = [bestFragmentOutbounds[0], bestFragmentOutbounds[1], ...bestFragment.outbounds];
        bestFragment.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, true, true);
    } else {
        delete bestFragmentOutbounds[0].streamSettings.sockopt.dialerProxy;
        bestFragmentOutbounds[0].tag = 'proxy';
        bestFragment.outbounds = [bestFragmentOutbounds[0], ...bestFragment.outbounds];
        bestFragment.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, false, true);
    }

    bestFragment.observatory.subjectSelector = ["frag"];
    bestFragment.observatory.probeInterval = '1m';
    bestFragment.routing.balancers[0].selector = ["frag"];

    if (client === 'nekoray') {
        bestFragment.inbounds[0].port = 2080;
        bestFragment.inbounds[1].port = 2081;
        bestFragment.inbounds[2].port = 6450;
    }

    const workerLessConfig = await buildWorkerLessConfig(env, client); 
    Configs.push(
        { address: 'Best-Ping', config: bestPing}, 
        { address: 'Best-Fragment', config: bestFragment}, 
        { address: 'WorkerLess', config: workerLessConfig}
    );

    return Configs;
}

const getSingboxConfig = async (env, hostName) => {
    let proxySettings = {};
    
    try {
        proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while getting sing-box configs - ${error}`);
    }

    const { remoteDNS,  localDNS, cleanIPs, proxyIP, ports } = proxySettings
    let config = structuredClone(singboxConfigTemp);
    config.dns.servers[0].address = remoteDNS;
    config.dns.servers[1].address = localDNS;
    const resolved = await resolveDNS(hostName);
    const Addresses = [
        hostName,
        "www.speedtest.net",
        ...resolved.ipv4,
        ...resolved.ipv6.map((ip) => `[${ip}]`),
        ...(cleanIPs ? cleanIPs.split(",") : [])
    ];

    ports.forEach(port => {
        Addresses.forEach((addr, index) => {

            let remark = generateRemark(index, port);
            let outbound = structuredClone(singboxOutboundTemp);
            outbound.server = addr;
            outbound.tag = remark;
            outbound.uuid = userID;
            outbound.server_port = +port;
            outbound.transport.headers.Host = randomUpperCase(hostName);
            outbound.transport.path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`;
            defaultHttpsPorts.includes(port)
                ? outbound.tls.server_name = randomUpperCase(hostName)
                : delete outbound.tls;
            config.outbounds.push(outbound);
            config.outbounds[0].outbounds.push(remark);
            config.outbounds[1].outbounds.push(remark);
        });
    });

    return config;
}

const getWarpConfigs = async (env, client) => {
    let proxySettings = {};
    let xrayOutbounds = [], singboxOutbounds = [];

    try {
        proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while getting fragment configs - ${error}`);
    }

    const {
        remoteDNS, 
        localDNS,
        blockAds,
        bypassIran,
        blockPorn,
        bypassLAN,
        wowEndpoint,
        warpEndpoints
    } = proxySettings;

    const ipv6Regex = /\[(.*?)\]/;
    const portRegex = /[^:]*$/;
    let xrayWoWConfig = structuredClone(xrayConfigTemp);
    let singboxWarpConfig = structuredClone(singboxConfigTemp);
    singboxWarpConfig.outbounds[0].outbounds = ['💦 Warp Best Ping 🚀'];
    singboxWarpConfig.outbounds[1].tag = '💦 Warp Best Ping 🚀';

    for (let i = 0; i < 2; i++) {
        let wgConfig = await fetchWgConfig();
        let xrayOutbound = structuredClone(xrayWgOutboundTemp);
        let singboxOutbound = structuredClone(singboxWgOutboundTemp);
        xrayOutbound.settings.address = [
            `${wgConfig.account.config.interface.addresses.v4}/32`,
            `${wgConfig.account.config.interface.addresses.v6}/128`
        ];

        if (wowEndpoint) xrayOutbound.settings.peers[0].endpoint = wowEndpoint;
        xrayOutbound.settings.peers[0].publicKey = wgConfig.account.config.peers[0].public_key;
        xrayOutbound.settings.reserved = base64ToDecimal(wgConfig.account.config.client_id);
        xrayOutbound.settings.secretKey = wgConfig.privateKey;
        xrayOutbound.tag = i === 1 ? 'warp-ir' : 'warp-out';    
        
        if (i === 1) {
            delete xrayOutbound.streamSettings;
        } else {
            xrayOutbound.streamSettings.sockopt.dialerProxy = 'warp-ir';
        }

        xrayOutbounds.push(xrayOutbound);

        singboxOutbound.local_address = [
            `${wgConfig.account.config.interface.addresses.v4}/32`,
            `${wgConfig.account.config.interface.addresses.v6}/128`
        ];

        if (wowEndpoint) {
            singboxOutbound.server = wowEndpoint.includes('[') ? wowEndpoint.match(ipv6Regex)[1] : wowEndpoint.split(':')[0];
            singboxOutbound.server_port = wowEndpoint.includes('[') ? +wowEndpoint.match(portRegex)[0] : +wowEndpoint.split(':')[1];
        }

        singboxOutbound.peer_public_key = wgConfig.account.config.peers[0].public_key;
        singboxOutbound.reserved = wgConfig.account.config.client_id;
        singboxOutbound.private_key = wgConfig.privateKey;
        singboxOutbound.tag = i === 1 ? '💦 Warp-ir' : '💦 WoW 🌍';    
        
        if (i === 0) {
            singboxOutbound.detour = '💦 Warp-ir';
            singboxWarpConfig.outbounds[0].outbounds.push('💦 WoW 🌍');
        } else {
            delete singboxOutbound.detour;
        }

        singboxOutbounds.push(singboxOutbound);
    }

    xrayWoWConfig.remarks = '💦 BPB - WoW 🌍';
    xrayWoWConfig.dns = await buildDNSObject(remoteDNS, localDNS, blockAds, bypassIran, blockPorn);
    xrayWoWConfig.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, false, false);
    xrayWoWConfig.outbounds.splice(0,1);
    delete xrayWoWConfig.observatory;
    delete xrayWoWConfig.routing.balancers;
    xrayWoWConfig.outbounds = [...xrayOutbounds, ...xrayWoWConfig.outbounds];
    xrayWoWConfig.routing.rules[xrayWoWConfig.routing.rules.length - 1].outboundTag = 'warp-out';
    
    let xrayWarpConfigs = [];
    let xrayWarpOutbounds = [];
    
    warpEndpoints.split(',').forEach((endpoint, index) => {
        let xrayWarpConfig = structuredClone(xrayWoWConfig);
        xrayWarpConfig.outbounds.splice(0,1);
        xrayWarpConfig.outbounds[0].settings.peers[0].endpoint = endpoint;
        xrayWarpConfig.outbounds[0].tag = 'warp';
        xrayWarpConfig.routing.rules[xrayWarpConfig.routing.rules.length - 1].outboundTag = 'warp';
        xrayWarpConfig.remarks = `💦 BPB - Warp ${index + 1} 🇮🇷`;
        xrayWarpConfigs.push(xrayWarpConfig);
        let xrayWarpOutbound = structuredClone(xrayWarpConfig.outbounds[0]);
        xrayWarpOutbound.tag = `warp_${index + 1}`;
        xrayWarpOutbounds.push(xrayWarpOutbound);

        let singboxWarpOutbound = structuredClone(singboxOutbounds[singboxOutbounds.length - 1]);
        singboxWarpOutbound.server = endpoint.includes('[') ? endpoint.match(ipv6Regex)[1] : endpoint.split(':')[0];
        singboxWarpOutbound.server_port = endpoint.includes('[') ? +endpoint.match(portRegex)[0] : +endpoint.split(':')[1];
        singboxWarpOutbound.tag = `💦 Warp ${index + 1} 🇮🇷`;
        singboxWarpOutbound
        singboxOutbounds.push(singboxWarpOutbound);
        singboxWarpConfig.outbounds[0].outbounds.push(singboxWarpOutbound.tag);
        singboxWarpConfig.outbounds[1].outbounds.push(singboxWarpOutbound.tag);
    });

    let xrayWarpBestPing = structuredClone(xrayConfigTemp);
    xrayWarpBestPing.remarks = '💦 BPB - Warp Best Ping 🚀';
    xrayWarpBestPing.dns = await buildDNSObject(remoteDNS, localDNS, blockAds, bypassIran, blockPorn);
    xrayWarpBestPing.routing.rules = buildRoutingRules(localDNS, blockAds, bypassIran, blockPorn, bypassLAN, false, true);
    xrayWarpBestPing.outbounds.splice(0,1);
    xrayWarpBestPing.outbounds = [...xrayWarpOutbounds, ...xrayWarpBestPing.outbounds];
    xrayWarpBestPing.routing.balancers[0].selector = 'warp';
    xrayWarpBestPing.observatory.subjectSelector = ['warp'];

    singboxWarpConfig.dns.servers[0].address = remoteDNS;
    singboxWarpConfig.dns.servers[1].address = localDNS;
    singboxWarpConfig.dns.rules[0].domain = 'engage.cloudflareclient.com';
    singboxWarpConfig.outbounds = [...singboxWarpConfig.outbounds, ...singboxOutbounds];

    return client === 'singbox' 
        ? singboxWarpConfig 
        : [{...xrayWoWConfig}, {...xrayWarpBestPing}, ...xrayWarpConfigs];
}

const fetchWgConfig = async () => {
    const wgResponse = await fetch('https://fscarmen.cloudflare.now.cc/wg');
    const wgDataText = await wgResponse.text();
    const { publicKey, privateKey } = extractWgKeys(wgDataText);
    const wgData = { PublicKey: publicKey, PrivateKey: privateKey };
    const accountResponse = await fetch('https://api.cloudflareclient.com/v0a2158/reg', {
        method: 'POST',
        headers: {
            'User-Agent': 'okhttp/3.12.1',
            'CF-Client-Version': 'a-6.10-2158',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            key: wgData.PublicKey,
            install_id: wgData.install_id,
            fcm_token: wgData.fcm_token,
            tos: new Date().toISOString(),
            model: 'PC',
            serial_number: wgData.install_id,
            locale: 'en_US'
        })
    });
    const accountData = await accountResponse.json();

    return {
        privateKey: wgData.PrivateKey,
        account: accountData
    };
}

const buildDNSObject = async (remoteDNS, localDNS, blockAds, bypassIran, blockPorn, isWorkerLess) => {
    let dnsObject = {
        hosts: {},
        servers: [
          isWorkerLess ? "https://cloudflare-dns.com/dns-query" : remoteDNS,
          {
            address: localDNS,
            domains: ["geosite:category-ir", "domain:.ir"],
            expectIPs: ["geoip:ir"],
            port: 53,
          },
        ],
        tag: "dns",
    };

    if (isWorkerLess) {
        const resolvedDOH = await resolveDNS('cloudflare-dns.com');
        const resolvedCloudflare = await resolveDNS('cloudflare.com');
        const resolvedCLDomain = await resolveDNS('www.speedtest.net.cdn.cloudflare.net');
        const resolvedCFNS_1 = await resolveDNS('ben.ns.cloudflare.com');
        const resolvedCFNS_2 = await resolveDNS('lara.ns.cloudflare.com');
        dnsObject.hosts['cloudflare-dns.com'] = [
            ...resolvedDOH.ipv4, 
            ...resolvedCloudflare.ipv4, 
            ...resolvedCLDomain.ipv4,
            ...resolvedCFNS_1.ipv4,
            ...resolvedCFNS_2.ipv4
        ];
    }

    if (blockAds) {
        dnsObject.hosts["geosite:category-ads-all"] = "127.0.0.1";
        dnsObject.hosts["geosite:category-ads-ir"] = "127.0.0.1";
    }

    if (blockPorn) {
        dnsObject.hosts["geosite:category-porn"] = "127.0.0.1";
    }

    if (!bypassIran || localDNS === 'localhost' || isWorkerLess) {
        dnsObject.servers.pop();
    }

    return dnsObject;
}

const buildRoutingRules = (localDNS, blockAds, bypassIran, blockPorn, bypassLAN, isChain, isBalancer, isWorkerLess) => {
    let rules = [
        {
            inboundTag: ["dns-in"],
            outboundTag: "dns-out",
            type: "field"
        },
        {
          ip: [localDNS],
          outboundTag: "direct",
          port: "53",
          type: "field",
        }
    ];

    if (localDNS === 'localhost' || isWorkerLess) {
        rules.pop();
    }

    if (bypassIran || bypassLAN) {
        let rule = {
            ip: [],
            outboundTag: "direct",
            type: "field",
        };
        
        if (bypassIran && !isWorkerLess) {
            rules.push({
                domain: ["geosite:category-ir", "domain:.ir"],
                outboundTag: "direct",
                type: "field",
            });
            rule.ip.push("geoip:ir");
        }

        bypassLAN && rule.ip.push("geoip:private");
        rules.push(rule);
    }

    if (blockAds || blockPorn) {
        let rule = {
            domain: [],
            outboundTag: "block",
            type: "field",
        };

        blockAds && rule.domain.push("geosite:category-ads-all", "geosite:category-ads-ir");
        blockPorn && rule.domain.push("geosite:category-porn");
        rules.push(rule);
    }
   
    if (isBalancer) {
        rules.push({
            balancerTag: "all",
            type: "field",
            network: "tcp,udp",
        });
    } else  {
        rules.push({
            outboundTag: isChain ? "out" : isWorkerLess ? "fragment" : "proxy",
            type: "field",
            network: "tcp,udp"
        });
    }

    return rules;
}

const extractWgKeys = (textData) => {
    const lines = textData.trim().split("\n");
    const publicKey = lines[0].split(":")[1].trim();
    const privateKey = lines[1].split(":")[1].trim();
    return { publicKey, privateKey };
}

const base64ToDecimal = (base64) => {
    const binaryString = atob(base64);
    const hexString = Array.from(binaryString).map(char => char.charCodeAt(0).toString(16).padStart(2, '0')).join('');
    const decimalArray = hexString.match(/.{2}/g).map(hex => parseInt(hex, 16));
    return decimalArray;
}

const updateDataset = async (env, Settings) => {

    const vlessConfig = Settings?.get('outProxy');
    const proxySettings = {
        remoteDNS: Settings?.get('remoteDNS') || 'https://94.140.14.14/dns-query',
        localDNS: Settings?.get('localDNS') || '8.8.8.8',
        lengthMin: Settings?.get('fragmentLengthMin') || '100',
        lengthMax: Settings?.get('fragmentLengthMax') || '200',
        intervalMin: Settings?.get('fragmentIntervalMin') || '5',
        intervalMax: Settings?.get('fragmentIntervalMax') || '10',
        blockAds: Settings?.get('block-ads') || false,
        bypassIran: Settings?.get('bypass-iran') || false,
        blockPorn: Settings?.get('block-porn') || false,
        bypassLAN: Settings?.get('bypass-lan') || false,
        cleanIPs: Settings?.get('cleanIPs')?.replaceAll(' ', '') || '',
        proxyIP: Settings?.get('proxyIP') || '',
        ports: Settings?.getAll('ports[]') || ['443'],
        outProxy: vlessConfig || '',
        outProxyParams: vlessConfig ? await extractVlessParams(vlessConfig) : '',
        wowEndpoint: Settings?.get('wowEndpoint')?.replaceAll(' ', '') || 'engage.cloudflareclient.com:2408',
        warpEndpoints: Settings?.get('warpEndpoints')?.replaceAll(' ', '') || 'engage.cloudflareclient.com:2408'
    };

    try {    
        await env.bpb.put("proxySettings", JSON.stringify(proxySettings));          
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while updating KV - ${error}`);
    }
}

const randomUpperCase = (str) => {
    let result = '';
    for (let i = 0; i < str.length; i++) {
        result += Math.random() < 0.5 ? str[i].toUpperCase() : str[i];
    }
    return result;
}

const getRandomPath = (length) => {
    let result = '';
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const charactersLength = characters.length;
    for (let i = 0; i < length; i++) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
}

const resolveDNS = async (domain) => {
    const dohURLv4 = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=A`;
    const dohURLv6 = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=AAAA`;

    try {
        const [ipv4Response, ipv6Response] = await Promise.all([
            fetch(dohURLv4, { headers: { accept: 'application/dns-json' } }),
            fetch(dohURLv6, { headers: { accept: 'application/dns-json' } }),
        ]);

        const ipv4Addresses = await ipv4Response.json();
        const ipv6Addresses = await ipv6Response.json();

        const ipv4 = ipv4Addresses.Answer
            ? ipv4Addresses.Answer.map((record) => record.data)
            : [];
        const ipv6 = ipv6Addresses.Answer
            ? ipv6Addresses.Answer.map((record) => record.data)
            : [];

        return { ipv4, ipv6 };
    } catch (error) {
        console.error('Error resolving DNS:', error);
        throw new Error(`An error occurred while resolving DNS - ${error}`);
    }
}

const generateJWTToken = (password, secretKey) => {
    const header = {
        alg: 'HS256',
        typ: 'JWT'
    };

    const payload = {
        exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60),
        data: { password }
    };
    const encodedHeader = btoa(JSON.stringify(header));
    const encodedPayload = btoa(JSON.stringify(payload));
    const signature = btoa(crypto.subtle.digest('SHA-256', new TextEncoder().encode(`${encodedHeader}.${encodedPayload}.${secretKey}`)));

    return `Bearer ${encodedHeader}.${encodedPayload}.${signature}`;
}

const generateSecretKey = () => {
    const bytes = new Uint8Array(32);
    crypto.getRandomValues(bytes);
    return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
}
  
const Authenticate = async (request, env) => {
    
    try {
        const secretKey = await env.bpb.get('secretKey');
        const cookie = request.headers.get('Cookie');
        const cookieMatch = cookie ? cookie.match(/(^|;\s*)jwtToken=([^;]*)/) : null;
        const token = cookieMatch ? cookieMatch.pop() : null;

        if (!token) {
            console.log('token');
            return false;
        }

        const tokenWithoutBearer = token.startsWith('Bearer ') ? token.slice(7) : token;
        const [encodedHeader, encodedPayload, signature] = tokenWithoutBearer.split('.');
        const payload = JSON.parse(atob(encodedPayload));

        const expectedSignature = btoa(crypto.subtle.digest(
            'SHA-256',
            new TextEncoder().encode(`${encodedHeader}.${encodedPayload}.${secretKey}`)
        ));

        if (signature !== expectedSignature) return false;

        const now = Math.floor(Date.now() / 1000);
        if (payload.exp < now) return false;

        return true;
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while authentication - ${error}`);
    }
}

const renderHomePage = async (env, hostName, fragConfigs) => {
    let proxySettings = {};
    
    try {
        proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
    } catch (error) {
        console.log(error);
        throw new Error(`An error occurred while rendering home page - ${error}`);
    }

    const {
        remoteDNS = '', 
        localDNS = '', 
        lengthMin = '', 
        lengthMax = '', 
        intervalMin = '', 
        intervalMax = '', 
        blockAds = false, 
        bypassIran = false,
        blockPorn = false,
        bypassLAN = false,
        cleanIPs = '', 
        proxyIP = '', 
        outProxy = '',
        ports = ['443'],
        wowEndpoint = 'engage.cloudflareclient.com:2408',
        warpEndpoints = 'engage.cloudflareclient.com:2408'
    } = proxySettings;

    const genCustomConfRow = async (configs) => {
        let tableBlock = "";
        configs.forEach(config => {
            tableBlock += `
            <tr>
                <td>
                    ${config.address === 'Best-Ping' 
                        ? `<div  style="justify-content: center;"><span><b>💦 Best-Ping 💥</b></span></div>` 
                        : config.address === 'WorkerLess'
                            ? `<div  style="justify-content: center;"><span><b>💦 WorkerLess ⭐</b></span></div>`
                            : config.address === 'Best-Fragment'
                                ? `<div  style="justify-content: center;"><span><b>💦 Best-Fragment 😎</b></span></div>`
                                : config.address
                    }
                </td>
                <td>
                    <button onclick="copyToClipboard('${encodeURIComponent(JSON.stringify(config.config, null, 4))}', true)">
                        Copy Config 
                        <span class="material-symbols-outlined">copy_all</span>
                    </button>
                </td>
            </tr>`;
        });

        return tableBlock;
    }

    const buildPortsBlock = async () => {
        let httpPortsBlock = '';
        let httpsPortsBlock = '';
        [...defaultHttpPorts, ...defaultHttpsPorts].forEach(port => {
            let id = `port-${port}`;
            let portBlock = `
                <div class="routing" style="grid-template-columns: 1fr 2fr; margin-right: 10px;">
                    <input type="checkbox" id=${id} name=${id} onchange="handlePortChange(event)" value="true" ${ports.includes(port) ? 'checked' : ''}>
                    <label style="margin-bottom: 3px;" for=${id}>${port}</label>
                </div>`;
            defaultHttpPorts.includes(port) ? httpPortsBlock += portBlock : httpsPortsBlock += portBlock;
        });

        return {httpPortsBlock, httpsPortsBlock};
    }

    const html = `
    <!DOCTYPE html>
    <html lang="en">

	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>BPB Panel ${panelVersion}</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" />
		<style>
			body { font-family: system-ui; }
            .material-symbols-outlined {
                margin-left: 5px;
                font-variation-settings:
                'FILL' 0,
                'wght' 400,
                'GRAD' 0,
                'opsz' 24
            }
            h1 { font-size: 2.5em; text-align: center; color: #09639f; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25); }
			h2 { margin: 30px 0; text-align: center; color: #3b3b3b; }
			hr { border: 1px solid #ddd; margin: 20px 0; }
            .footer {
                display: flex;
                font-weight: 600;
                margin: 10px auto 0 auto;
                justify-content: center;
                align-items: center;
            }
            .footer button {margin: 0 20px; background: #212121; max-width: fit-content;}
            .footer button:hover, .footer button:focus { background: #3b3b3b;}
            .form-control a, a.link { text-decoration: none; }
			.form-control {
				margin-bottom: 15px;
				display: grid;
				grid-template-columns: 1fr 1fr;
				align-items: baseline;
				justify-content: flex-end;
				font-family: Arial, sans-serif;
			}
            .form-control button {
                background-color: white;
                font-size: 1.1rem;
                font-weight: 600;
                color: #09639f;
                border-color: #09639f;
                border: 2px solid;
            }
            #apply {display: block; margin-top: 30px;}
            input.button {font-weight: 600; padding: 15px 0; font-size: 1.1rem;}
			label {
				display: block;
				margin-bottom: 5px;
				font-size: 110%;
				font-weight: 600;
				color: #333;
			}
			input[type="text"],
			input[type="number"],
			input[type="url"],
			textarea,
			select {
				width: 100%;
				text-align: center;
				padding: 10px;
				border: 1px solid #ddd;
				border-radius: 5px;
				font-size: 16px;
				color: #333;
				background-color: #fff;
				box-sizing: border-box;
				margin-bottom: 15px;
				transition: border-color 0.3s ease;
			}	
			input[type="text"]:focus,
			input[type="number"]:focus,
			input[type="url"]:focus,
			textarea:focus,
			select:focus { border-color: #3498db; outline: none; }
			.button,
			table button {
				display: flex;
                align-items: center;
                justify-content: center;
                width: 100%;
				white-space: nowrap;
				padding: 10px 15px;
				font-size: 16px;
                font-weight: 600;
				letter-spacing: 1px;
				border: none;
				border-radius: 5px;
				color: #fff;
				background-color: #09639f;
				cursor: pointer;
				outline: none;
				box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
				transition: all 0.3s ease;
			}
            table button { margin: auto; width: auto; }
            .button.disabled {
                background-color: #ccc;
                cursor: not-allowed;
                box-shadow: none;
                pointer-events: none;
            }
			.button:hover,
			.button:focus,
			table button:hover,
			table button:focus {
				background-color: #2980b9;
				box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3);
				transform: translateY(-2px);
			}
            button.button:hover { color: white; }
			.button:active,
			table button:active { transform: translateY(1px); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); }
			.form-container {
				max-width: 90%;
				margin: 0 auto;
				padding: 20px;
				background: #f9f9f9;
				border: 1px solid #eaeaea;
				border-radius: 10px;
				box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
			}
			.table-container { margin-top: 20px; overflow-x: auto; }
			table { 
                width: 100%; 
                border-collapse: collapse; 
                margin-bottom: 20px;
                border-radius: 7px;
                overflow: hidden;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }
			th, td { padding: 8px 15px; border-bottom: 1px solid #ddd; }
            td div { display: flex; align-items: center; }
			th { background-color: #3498db; color: white; font-weight: bold; font-size: 1.1rem; width: 50%;}
			tr:nth-child(odd) { background-color: #f2f2f2; }
            #custom-configs-table td { text-align: center; text-wrap: nowrap; }
			tr:hover { background-color: #f1f1f1; }
            .modal {
                display: none;
                position: fixed;
                z-index: 1;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                overflow: auto;
                background-color: rgba(0, 0, 0, 0.4);
            }
            .modal-content {
                background-color: #f9f9f9;
                margin: auto;
                padding: 10px 20px 20px;
                border: 1px solid #eaeaea;
                border-radius: 10px;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                width: 80%;
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
            }
            .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; }
            .close:hover,
            .close:focus { color: black; text-decoration: none; cursor: pointer; }
            .form-control label {
                display: block;
                margin-bottom: 5px;
                font-size: 110%;
                font-weight: 600;
                color: #333;
            }
            .form-control input[type="password"] {
                width: 100%;
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                font-size: 16px;
                color: #333;
                background-color: #fff;
                box-sizing: border-box;
                margin-bottom: 15px;
                transition: border-color 0.3s ease;
            }
            .routing { 
                display: grid;
                grid-template-columns: 1fr 3fr 8fr 1fr;
                justify-content: center;
                margin-bottom: 15px;
            }
            .routing label {
                text-align: left;
                margin: 0;
                font-weight: 400;
                font-size: 100%;
                text-wrap: nowrap;
            }
            .form-control input[type="password"]:focus { border-color: #3498db; outline: none; }
            #passwordError { color: red; margin-bottom: 10px; }
            .symbol { margin-right: 8px; }
            .modalQR {
                display: none;
                position: fixed;
                z-index: 1;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                overflow: auto;
                background-color: rgba(0, 0, 0, 0.4);
            }
            @media only screen and (min-width: 768px) {
                .form-container { max-width: 70%; }
                #apply { display: block; margin: 30px auto 0 auto; max-width: 50%; }
                .modal-content { width: 30% }
                .routing { grid-template-columns: 4fr 2fr 6fr 4fr; }
            }
		</style>
	</head>
	
	<body>
		<h1>BPB Panel <span style="font-size: smaller;">${panelVersion}</span> 💦</h1>
		<div class="form-container">
            <h2>FRAGMENT SETTINGS ⚙️</h2>
			<form id="configForm">
				<div class="form-control">
					<label for="remoteDNS">🌏 Remote DNS</label>
					<input type="url" id="remoteDNS" name="remoteDNS" value="${remoteDNS}" required>
				</div>
				<div class="form-control">
					<label for="localDNS">🏚️ Local DNS</label>
					<input type="text" id="localDNS" name="localDNS" value="${localDNS}"
						pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|localhost$"
						title="Please enter a valid DNS IP Address or localhost!"  required>
				</div>	
				<div class="form-control">
					<label for="fragmentLengthMin">📐 Length</label>
					<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: baseline;">
						<input type="number" id="fragmentLengthMin" name="fragmentLengthMin" value="${lengthMin}" min="10" required>
						<span style="text-align: center; white-space: pre;"> - </span>
						<input type="number" id="fragmentLengthMax" name="fragmentLengthMax" value="${lengthMax}" max="500" required>
					</div>
				</div>
				<div class="form-control">
					<label for="fragmentIntervalMin">🕞 Interval</label>
					<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: baseline;">
						<input type="number" id="fragmentIntervalMin" name="fragmentIntervalMin"
    						value="${intervalMin}" max="30" required>
						<span style="text-align: center; white-space: pre;"> - </span>
						<input type="number" id="fragmentIntervalMax" name="fragmentIntervalMax"
    						value="${intervalMax}" max="30" required>
					</div>
				</div>
				<div class="form-control">
					<label for="outProxy">✈️ Chain Proxy</label>
					<input type="text" id="outProxy" name="outProxy" value="${outProxy}">
				</div>
                <h2>FRAGMENT ROUTING ⚙️</h2>
				<div class="form-control" style="margin-bottom: 20px;">			
                    <div class="routing">
                        <input type="checkbox" id="block-ads" name="block-ads" style="margin: 0; grid-column: 2;" value="true" ${blockAds ? 'checked' : ''}>
                        <label for="block-ads">Block Ads.</label>
                    </div>
                    <div class="routing">
						<input type="checkbox" id="bypass-iran" name="bypass-iran" style="margin: 0; grid-column: 2;" value="true" ${bypassIran ? 'checked' : ''}>
                        <label for="bypass-iran">Bypass Iran</label>
					</div>
                    <div class="routing">
						<input type="checkbox" id="block-porn" name="block-porn" style="margin: 0; grid-column: 2;" value="true" ${blockPorn ? 'checked' : ''}>
                        <label for="block-porn">Block Porn</label>
					</div>
                    <div class="routing">
						<input type="checkbox" id="bypass-lan" name="bypass-lan" style="margin: 0; grid-column: 2;" value="true" ${bypassLAN ? 'checked' : ''}>
                        <label for="bypass-lan">Bypass LAN</label>
					</div>
				</div>
                <h2>PROXY IP ⚙️</h2>
				<div class="form-control">
					<label for="proxyIP">📍 IP or Domain</label>
					<input type="text" id="proxyIP" name="proxyIP" value="${proxyIP}">
				</div>
                <h2>CLEAN IP ⚙️</h2>
				<div class="form-control">
					<label for="cleanIPs">✨ Clean IPs</label>
					<input type="text" id="cleanIPs" name="cleanIPs" value="${cleanIPs.replaceAll(",", " , ")}">
				</div>
                <div class="form-control">
                    <label>🔎 Online Scanner</label>
                    <a href="https://scanner.github1.cloud/" id="scanner" name="scanner" target="_blank">
                        <button type="button" class="button">
                            Scan now
                            <span class="material-symbols-outlined" style="margin-left: 5px;">open_in_new</span>
                        </button>
                    </a>
                </div>
                <h2>PORTS ⚙️</h2>
                <div class="table-container">
                    <table id="frag-sub-table">
                        <tr>
                            <th style="text-wrap: nowrap; background-color: gray;">Config type</th>
                            <th style="text-wrap: nowrap; background-color: gray;">Ports</th>
                        </tr>
                        <tr>
                            <td style="text-align: center; font-size: larger;"><b>TLS</b></td>
                            <td style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr;">${(await buildPortsBlock()).httpsPortsBlock}</td>    
                        </tr>
                        ${hostName.includes('pages.dev') ? '' : `<tr>
                            <td style="text-align: center; font-size: larger;"><b>Non TLS</b></td>
                            <td style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr;">${(await buildPortsBlock()).httpPortsBlock}</td>    
                        </tr>`}        
                    </table>
                </div>
                <h2>WARP ENDPOINT ⚙️</h2>
				<div class="form-control">
                    <label for="wowEndpoint">✨ WoW Endpoint</label>
                    <input type="text" id="wowEndpoint" name="wowEndpoint" value="${wowEndpoint.replaceAll(",", " , ")}">
				</div>
				<div class="form-control">
                    <label for="warpEndpoints">✨ Warp Endpoints</label>
                    <input type="text" id="warpEndpoints" name="warpEndpoints" value="${warpEndpoints.replaceAll(",", " , ")}">
				</div>
                <div class="form-control">
                    <label>🔎 Scanner Script</label>
                    <button type="button" class="button" style="padding: 10px 0;" onclick="copyToClipboard('bash <(curl -fsSL https://raw.githubusercontent.com/Ptechgithub/warp/main/endip/install.sh)', false)">
                        Copy Script<span class="material-symbols-outlined">terminal</span>
                    </button>
                </div>
				<div id="apply" class="form-control">
					<div style="grid-column: 2; width: 100%;">
						<input type="submit" id="applyButton" class="button disabled" value="APPLY SETTINGS 💥" form="configForm">
					</div>
				</div>
			</form>
            <hr>            
			<h2>NORMAL CONFIGS 🔗</h2>
			<div class="table-container">
				<table id="normal-configs-table">
					<tr>
						<th>Application</th>
						<th>Subscription</th>
					</tr>
					<tr>
                        <td>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>v2rayNG</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>v2rayN</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Shadowrocket</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Streisand</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Hiddify</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Nekoray (Xray)</span>
                            </div>
                        </td>
						<td>
                            <button onclick="openQR('https://${hostName}/sub/${userID}#BPB-Normal', 'Normal Subscription')" style="margin-bottom: 8px;">
                                QR Code&nbsp;<span class="material-symbols-outlined">qr_code</span>
                            </button>
                            <button onclick="copyToClipboard('https://${hostName}/sub/${userID}#BPB-Normal', false)">
                                Copy Sub<span class="material-symbols-outlined">format_list_bulleted</span>
                            </button>
                        </td>
					</tr>
					<tr>
                        <td>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Nekobox</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Nekoray (Sing-Box)</span>
                            </div>
                        </td>
						<td>
                            <button onclick="copyToClipboard('https://${hostName}/sub/${userID}?app=singbox#BPB-Normal', false)">
                                Copy Sub<span class="material-symbols-outlined">format_list_bulleted</span>
                            </button>
						</td>
					</tr>
                    <tr>
                        <td>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Sing-box - <b>Best Ping</b></span>
                            </div>
                        </td>
                        <td>
                            <button onclick="openQR('sing-box://import-remote-profile?url=https://${hostName}/sub/${userID}?app=sfa#BPB-Normal', 'Normal Subscription')" style="margin-bottom: 8px;">
                                QR Code&nbsp;<span class="material-symbols-outlined">qr_code</span>
                            </button>
                            <button onclick="copyToClipboard('https://${hostName}/sub/${userID}?app=sfa#BPB-Normal', false)">
                                Copy Sub<span class="material-symbols-outlined">format_list_bulleted</span>
                            </button>
                        </td>
                    </tr>
				</table>
			</div>
			<h2>FRAGMENT SUB ⛓️</h2>
			<div class="table-container">
                <table id="frag-sub-table">
                    <tr>
                        <th style="text-wrap: nowrap;">Application</th>
                        <th style="text-wrap: nowrap;">Fragment Subscription</th>
                    </tr>
                    <tr>
                        <td style="text-wrap: nowrap;">
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>v2rayNG</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>v2rayN</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>MahsaNG</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Streisand</span>
                            </div>
                        </td>
                        <td>
                            <button onclick="openQR('https://${hostName}/fragsub/${userID}#BPB Fragment', 'Fragment Subscription')" style="margin-bottom: 8px;">
                                QR Code&nbsp;<span class="material-symbols-outlined">qr_code</span>
                            </button>
                            <button onclick="copyToClipboard('https://${hostName}/fragsub/${userID}#BPB Fragment', true)">
                                Copy Sub<span class="material-symbols-outlined">format_list_bulleted</span>
                            </button>
                        </td>
                    </tr>
                </table>
            </div>
            <h2>WARP SUB 🔗</h2>
			<div class="table-container">
				<table id="normal-configs-table">
					<tr>
						<th>Application</th>
						<th>Subscription</th>
					</tr>
					<tr>
                        <td>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>v2rayNG</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>v2rayN</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>MahsaNG</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Streisand</span>
                            </div>
                        </td>
						<td>
                            <button onclick="openQR('https://${hostName}/warpsub/${userID}#BPB-Warp', 'Warp Subscription')" style="margin-bottom: 8px;">
                                QR Code&nbsp;<span class="material-symbols-outlined">qr_code</span>
                            </button>
                            <button onclick="copyToClipboard('https://${hostName}/warpsub/${userID}#BPB-Warp', false)">
                                Copy Sub<span class="material-symbols-outlined">format_list_bulleted</span>
                            </button>
                        </td>
					</tr>
					<tr>
                        <td>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Hiddify</span>
                            </div>
                            <div>
                                <span class="material-symbols-outlined symbol">verified</span>
                                <span>Singbox</span>
                            </div>
                        </td>
						<td>
                            <button onclick="openQR('sing-box://import-remote-profile?url=https://${hostName}/warpsub/${userID}?app=singbox#BPB-Warp', 'Warp Subscription')" style="margin-bottom: 8px;">
                                QR Code&nbsp;<span class="material-symbols-outlined">qr_code</span>
                            </button>
                            <button onclick="copyToClipboard('https://${hostName}/warpsub/${userID}?app=singbox#BPB-Warp', false)">
                                Copy Sub<span class="material-symbols-outlined">format_list_bulleted</span>
                            </button>
						</td>
					</tr>
				</table>
			</div>
            <h2>FRAGMENT - NEKORAY ⛓️</h2>
            <div class="table-container">
				<table id="custom-configs-table">
					<tr style="text-wrap: nowrap;">
						<th>Config Address</th>
						<th>Fragment Config</th>
					</tr>					
					${await genCustomConfRow(fragConfigs)}
				</table>
			</div>
            <div id="myModal" class="modal">
                <div class="modal-content">
                    <span class="close">&times;</span>
                    <form id="passwordChangeForm">
                        <h2>Change Password</h2>
                        <div class="form-control">
                            <label for="newPassword">New Password</label>
                            <input type="password" id="newPassword" name="newPassword" required>
                            </div>
                        <div class="form-control">
                            <label for="confirmPassword">Confirm Password</label>
                            <input type="password" id="confirmPassword" name="confirmPassword" required>
                        </div>
                        <div id="passwordError" style="color: red; margin-bottom: 10px;"></div>
                        <button id="changePasswordBtn" type="submit" class="button">Change Password</button>
                    </form>
                </div>
            </div>
            <div id="myQRModal" class="modalQR">
                <div class="modal-content" style="width: auto; text-align: center;">
                    <div style="display: flex; flex-direction: column; align-items: center; margin-bottom: 10px;">
                        <span id="closeQRModal" class="close" style="align-self: flex-end;">&times;</span>
                        <span id="qrcodeTitle" style="align-self: center; font-weight: bold;"></span>
                    </div>
                    <div id="qrcode-container"></div>
                </div>
            </div>
            <hr>
            <div class="footer">
                <i class="fa fa-github" style="font-size:36px; margin-right: 10px;"></i>
                <a class="link" href="https://github.com/bia-pain-bache/BPB-Worker-Panel" target="_blank">Github</a>
                <button id="openModalBtn" class="button">Change Password</button>
                <button type="button" id="logout" style="background: none; margin: 0; border: none; cursor: pointer;">
                    <i class="fa fa-power-off fa-2x" aria-hidden="true"></i>
                </button>
            </div>
        </div>
        
    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
	<script>
    let activePortsNo = ${ports.length};
		document.addEventListener('DOMContentLoaded', () => {
            let regionNames = new Intl.DisplayNames(['en'], {type: 'region'});
            const configForm = document.getElementById('configForm');            
            const modal = document.getElementById('myModal');
            const changePass = document.getElementById("openModalBtn");
            const closeBtn = document.querySelector(".close");
            const passwordChangeForm = document.getElementById('passwordChangeForm');            
            const applyBtn = document.getElementById('applyButton');         
            const initialFormData = new FormData(configForm);
            const closeQR = document.getElementById("closeQRModal");
            let modalQR = document.getElementById("myQRModal");
            let qrcodeContainer = document.getElementById("qrcode-container");
          
            const hasFormDataChanged = () => {
                const currentFormData = new FormData(configForm);
                const currentFormDataEntries = [...currentFormData.entries()];

                const nonCheckboxFieldsChanged = currentFormDataEntries.some(
                    ([key, value]) => !initialFormData.has(key) || initialFormData.get(key) !== value
                );

                const checkboxFieldsChanged = Array.from(configForm.elements)
                    .filter((element) => element.type === 'checkbox')
                    .some((checkbox) => {
                    const initialValue = initialFormData.has(checkbox.name)
                        ? initialFormData.get(checkbox.name)
                        : false;
                    const currentValue = currentFormDataEntries.find(([key]) => key === checkbox.name)?.[1] || false;
                    return initialValue !== currentValue;
                });

                return nonCheckboxFieldsChanged || checkboxFieldsChanged;
            };
          
            const enableApplyButton = () => {
                const isChanged = hasFormDataChanged();
                applyButton.disabled = !isChanged;
                applyButton.classList.toggle('disabled', !isChanged);
            };
                      
            passwordChangeForm.addEventListener('submit', event => resetPassword(event));
            document.getElementById('logout').addEventListener('click', event => logout(event));
			configForm.addEventListener('submit', (event) => applySettings(event, configForm));
            configForm.addEventListener('input', enableApplyButton);
            configForm.addEventListener('change', enableApplyButton);
            changePass.addEventListener('click', () => {
                modal.style.display = "block";
                document.body.style.overflow = "hidden";
            });        
            closeBtn.addEventListener('click', () => {
                modal.style.display = "none";
                document.body.style.overflow = "";
            });
            closeQR.addEventListener('click', () => {
                modalQR.style.display = "none";
                qrcodeContainer.lastElementChild.remove();
            });
            window.onclick = (event) => {
                if (event.target == modalQR) {
                    modalQR.style.display = "none";
                    qrcodeContainer.lastElementChild.remove();
                }
            }
		});

        const handlePortChange = (event) => {
            event.target.checked ? activePortsNo++ : activePortsNo--;
            if (activePortsNo === 0) {
                event.preventDefault();
                event.target.checked = !event.target.checked;
                alert("⛔ At least one port should be selected! 🫤");
                activePortsNo = 1;
                return false;
            }
        }

        const openQR = (url, title) => {
            let qrcodeContainer = document.getElementById("qrcode-container");
            let qrcodeTitle = document.getElementById("qrcodeTitle");
            const modalQR = document.getElementById("myQRModal");
            qrcodeTitle.textContent = title;
            modalQR.style.display = "block";
            let qrcodeDiv = document.createElement("div");
            qrcodeDiv.className = "qrcode";
            new QRCode(qrcodeDiv, {
                text: url,
                width: 256,
                height: 256,
                colorDark: "#000000",
                colorLight: "#ffffff",
                correctLevel: QRCode.CorrectLevel.H
            });
            qrcodeContainer.appendChild(qrcodeDiv);
        }

		const copyToClipboard = (text, decode) => {
            const textarea = document.createElement('textarea');
            const value = decode ? decodeURIComponent(text) : text;
			textarea.value = value;
			document.body.appendChild(textarea);
			textarea.select();
			document.execCommand('copy');
			document.body.removeChild(textarea);
			alert('📋 Copied to clipboard:\\n\\n' +  value);
		}

        const applySettings = async (event, configForm) => {
            event.preventDefault();
            event.stopPropagation();
            const applyButton = document.getElementById('applyButton');
            const getValue = (id) => parseInt(document.getElementById(id).value, 10);              
            const lengthMin = getValue('fragmentLengthMin');
            const lengthMax = getValue('fragmentLengthMax');
            const intervalMin = getValue('fragmentIntervalMin');
            const intervalMax = getValue('fragmentIntervalMax');
            const proxyIP = document.getElementById('proxyIP').value?.trim();
            const cleanIP = document.getElementById('cleanIPs');
            const wowEndpoint = document.getElementById('wowEndpoint').value?.trim();
            const warpEndpoints = document.getElementById('warpEndpoints').value?.replaceAll(' ', '').split(',');
            const cleanIPs = cleanIP.value?.split(',');
            const chainProxy = document.getElementById('outProxy').value?.trim();                    
            const formData = new FormData(configForm);
            const isVless = /vless:\\/\\/[^\s@]+@[^\\s:]+:[^\\s]+/.test(chainProxy);
            const hasSecurity = /security=/.test(chainProxy);
            const validSecurityType = /security=(tls|none|reality)/.test(chainProxy);
            const validTransmission = /type=(tcp|grpc|ws)/.test(chainProxy);
            const validIPDomain = /^((?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,})|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|\\[(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,7}:\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}\\]|\\[[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6}\\]|\\[:(?::[a-fA-F0-9]{1,4}){1,7}\\]|\\[\\](?:::[a-fA-F0-9]{1,4}){1,7}\\])$/i;
            const checkedPorts = Array.from(document.querySelectorAll('input[name^="port-"]:checked')).map(input => input.name.split('-')[1]);
            const validEndpoint = /^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|\\[(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,7}:\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}\\]|\\[(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}\\]|\\[[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6}\\]|\\[:(?::[a-fA-F0-9]{1,4}){1,7}\\]|\\[::(?::[a-fA-F0-9]{1,4}){0,7}\\]):(?:[0-9]{1,5})$/;
            checkedPorts.forEach(port => formData.append('ports[]', port));

            const invalidIPs = [...cleanIPs, proxyIP]?.filter(value => {
                if (value !== "") {
                    const trimmedValue = value.trim();
                    return !validIPDomain.test(trimmedValue);
                }
            });

            const invalidEndpoints = [wowEndpoint, ...warpEndpoints]?.filter(value => {
                if (value !== "") {
                    const trimmedValue = value.trim();
                    return !validEndpoint.test(trimmedValue);
                }
            });
    
            if (invalidIPs.length) {
                alert('⛔ Invalid IPs or Domains 🫤\\n\\n' + invalidIPs.map(ip => '⚠️ ' + ip).join('\\n'));
                return false;
            }
            
            if (invalidEndpoints.length) {
                alert('⛔ Invalid endpoint 🫤\\n\\n' + invalidEndpoints.map(endpoint => '⚠️ ' + endpoint).join('\\n'));
                return false;
            }

            if (lengthMin >= lengthMax || intervalMin > intervalMax) {
                alert('⛔ Minimum should be smaller or equal to Maximum! 🫤');               
                return false;
            }

            if (!(isVless && (hasSecurity && validSecurityType || !hasSecurity) && validTransmission) && chainProxy) {
                alert('⛔ Invalid Config! 🫤 \\n - The chain proxy should be VLESS!\\n - Transmission should be GRPC,WS or TCP\\n - Security should be TLS,Reality or None');               
                return false;
            }

            try {
                document.body.style.cursor = 'wait';
                const applyButtonVal = applyButton.value;
                applyButton.value = '⌛ Loading...';

                const response = await fetch('/panel', {
                    method: 'POST',
                    body: formData,
                    credentials: 'include'
                });

                document.body.style.cursor = 'default';
                applyButton.value = applyButtonVal;

                if (response.ok) {
                    alert('Parameters applied successfully 😎');
                    window.location.reload(true);
                } else {
                    const errorMessage = await response.text();
                    console.error(errorMessage, response.status);
                    alert('⚠️ Session expired! Please login again.');
                    window.location.href = '/login';
                }           
            } catch (error) {
                console.error('Error:', error);
            }
        }

        const logout = async (event) => {
            event.preventDefault();

            try {
                const response = await fetch('/logout', {
                    method: 'GET',
                    credentials: 'same-origin'
                });
            
                if (response.ok) {
                    window.location.href = '/login';
                } else {
                    console.error('Failed to log out:', response.status);
                }
            } catch (error) {
                console.error('Error:', error);
            }
        }

        const resetPassword = async (event) => {
            event.preventDefault();
            const modal = document.getElementById('myModal');
            const newPasswordInput = document.getElementById('newPassword');
            const confirmPasswordInput = document.getElementById('confirmPassword');
            const passwordError = document.getElementById('passwordError');             
            const newPassword = newPasswordInput.value;
            const confirmPassword = confirmPasswordInput.value;
    
            if (newPassword !== confirmPassword) {
                passwordError.textContent = "Passwords do not match";
                return false;
            }

            const hasCapitalLetter = /[A-Z]/.test(newPassword);
            const hasNumber = /[0-9]/.test(newPassword);
            const isLongEnough = newPassword.length >= 8;

            if (!(hasCapitalLetter && hasNumber && isLongEnough)) {
                passwordError.textContent = '⚠️ Password must contain at least one capital letter, one number, and be at least 8 characters long.';
                return false;
            }
                    
            try {
                const response = await fetch('/panel/password', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'text/plain'
                    },
                    body: newPassword,
                    credentials: 'same-origin'
                });
            
                if (response.ok) {
                    modal.style.display = "none";
                    document.body.style.overflow = "";
                    alert("Password changed successfully! 👍");
                    window.location.href = '/login';
                } else if (response.status === 401) {
                    const errorMessage = await response.text();
                    passwordError.textContent = '⚠️ ' + errorMessage;
                    console.error(errorMessage, response.status);
                    alert('⚠️ Session expired! Please login again.');
                    window.location.href = '/login';
                } else {
                    const errorMessage = await response.text();
                    passwordError.textContent = '⚠️ ' + errorMessage;
                    console.error(errorMessage, response.status);
                    return false;
                }
            } catch (error) {
                console.error('Error:', error);
            }
        }
	</script>
	</body>	
	</html>`;

    return html;
}

const renderLoginPage = async () => {
    const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Login</title>
    <style>

        html, body { height: 100%; margin: 0; }
        body {
            font-family: system-ui;
            background-color: #f9f9f9;
            position: relative;
            overflow: hidden;
        }
        .container {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 90%;
        }
        h1 { font-size: 2.5rem; text-align: center; color: #09639f; margin: 0 auto 30px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25); }        
        h2 {text-align: center;}
        .form-container {
            background: #f9f9f9;
            border: 1px solid #eaeaea;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .form-control { margin-bottom: 15px; display: flex; align-items: center; }
        label {
            display: block;
            margin-bottom: 5px;
            padding-right: 20px;
            font-size: 110%;
            font-weight: 600;
            color: #333;
        }
        input[type="text"],
        input[type="password"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            color: #333;
        }
        button {
            display: block;
            width: 100%;
            padding: 10px;
            font-size: 16px;
            font-weight: 600;
            border: none;
            border-radius: 5px;
            color: #fff;
            background-color: #09639f;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }
        button:hover {background-color: #2980b9;}
        @media only screen and (min-width: 768px) {
            .container { width: 30%; }
        }
    </style>
    </head>
    <body>
        <div class="container">
            <h1>BPB Panel <span style="font-size: smaller;">${panelVersion}</span> 💦</h1>
            <div class="form-container">
                <h2>User Login</h2>
                <form id="loginForm">
                    <div class="form-control">
                        <label for="password">Password</label>
                        <input type="password" id="password" name="password" required>
                    </div>
                    <div id="passwordError" style="color: red; margin-bottom: 10px;"></div>
                    <button type="submit" class="button">Login</button>
                </form>
            </div>
        </div>
    <script>
        document.getElementById('loginForm').addEventListener('submit', async (event) => {
            event.preventDefault();
            const password = document.getElementById('password').value;

            try {
                const response = await fetch('/login', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'text/plain'
                    },
                    body: password
                });
            
                if (response.ok) {
                    window.location.href = '/panel';
                } else {
                    passwordError.textContent = '⚠️ Wrong Password!';
                    const errorMessage = await response.text();
                    console.error('Login failed:', errorMessage);
                }
            } catch (error) {
                console.error('Error during login:', error);
            }
        });
    </script>
    </body>
    </html>`;

    return html;
}

const renderErrorPage = (message, error, refer) => {
    return `
    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Error Page</title>
        <style>
            body,
            html {
                height: 100%;
                margin: 0;
                display: flex;
                justify-content: center;
                align-items: center;
                font-family: system-ui;
            }
            h1 { font-size: 2.5rem; text-align: center; color: #09639f; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25); }
            #error-container { text-align: center; }
        </style>
    </head>

    <body>
        <div id="error-container">
            <h1>BPB Panel <span style="font-size: smaller;">${panelVersion}</span> 💦</h1>
            <div id="error-message">
                <h2>${message} ${refer 
                    ? 'Please try again or refer to <a href="https://github.com/bia-pain-bache/BPB-Worker-Panel/blob/main/README.md">documents</a>' 
                    : ''}
                </h2>
                <p><b>${error ? `⚠️ ${error}` : ''}</b></p>
            </div>
        </div>
    </body>

    </html>`;
}

const xrayConfigTemp = {
    remarks: "",
    log: {
        loglevel: "warning",
    },
    dns: {},
    inbounds: [
        {
            port: 10808,
            protocol: "socks",
            settings: {
                auth: "noauth",
                udp: true,
                userLevel: 8,
            },
            sniffing: {
                destOverride: ["http", "tls"],
                enabled: true,
                routeOnly: true
            },
            tag: "socks-in",
        },
        {
            port: 10809,
            protocol: "http",
            settings: {
                auth: "noauth",
                udp: true,
                userLevel: 8,
            },
            sniffing: {
                destOverride: ["http", "tls"],
                enabled: true,
                routeOnly: true
            },
            tag: "http-in",
        },
        {
            listen: "127.0.0.1",
            port: 10853,
            protocol: "dokodemo-door",
            settings: {
              address: "1.1.1.1",
              network: "tcp,udp",
              port: 53
            },
            tag: "dns-in"
        }
    ],
    outbounds: [
        {
            tag: "fragment",
            protocol: "freedom",
            settings: {
                fragment: {
                    packets: "tlshello",
                    length: "",
                    interval: "",
                },
            },
            streamSettings: {
                sockopt: {
                    tcpKeepAliveIdle: 100,
                    tcpNoDelay: true,
                },
            },
        },
        {
            protocol: "dns",
            tag: "dns-out"
        },
        {
            protocol: "freedom",
            settings: {},
            tag: "direct",
        },
        {
            protocol: "blackhole",
            settings: {
                response: {
                    type: "http",
                },
            },
            tag: "block",
        },
    ],
    policy: {
        levels: {
            8: {
                connIdle: 300,
                downlinkOnly: 1,
                handshake: 4,
                uplinkOnly: 1,
            }
        },
        system: {
            statsOutboundUplink: true,
            statsOutboundDownlink: true,
        }
    },
    routing: {
        domainStrategy: "IPIfNonMatch",
        rules: [],
        balancers: [
            {
                tag: "all",
                selector: ["prox"],
                strategy: {
                    type: "leastPing",
                },
            }
        ]
    },
    observatory: {
        probeInterval: "30s",
        probeURL: "https://api.github.com/_private/browser/stats",
        subjectSelector: ["prox"],
        EnableConcurrency: true,
    },
    stats: {},
};
  
const xrayOutboundTemp = 
{
    mux: {
        concurrency: 8,
        enabled: true,
        xudpConcurrency: 8,
        xudpProxyUDP443: "reject"
    },
    protocol: "vless",
    settings: {
        vnext: [
            {
                address: "",
                port: 443,
                users: [
                    {
                        encryption: "none",
                        flow: "",
                        id: "",
                        level: 8,
                        security: "auto"
                    }
                ]
            }
        ]
    },
    streamSettings: {
        network: "ws",
        security: "tls",
        sockopt: {
            dialerProxy: "fragment",
            tcpKeepAliveIdle: 100,
            tcpNoDelay: true
        },
        tlsSettings: {
            allowInsecure: false,
            fingerprint: "chrome",
            alpn: ["h2", "http/1.1"],
            serverName: ""
        },
        wsSettings: {
            headers: {Host: ""},
            path: ""
        },
        grpcSettings: {
            authority: "",
            multiMode: false,
            serviceName: ""
        },
        realitySettings: {
            fingerprint: "",
            publicKey: "",
            serverName: "",
            shortId: "",
            spiderX: ""
        },
        tcpSettings: {
            header: {
                request: {
                    headers: {
                        Host: []
                    },
                    method: "GET",
                    path: [],
                    version: "1.1"
                },
                response: {
                    headers: {
                        "Content-Type": ["application/octet-stream"]
                    },
                    reason: "OK",
                    status: "200",
                    version: "1.1"
                },
                type: "http"
            }
        }
    },
    tag: "proxy"
};

const singboxConfigTemp = {
    log: {
        level: "warn",
        timestamp: true
    },
    dns: {
        servers: [
            {
                tag: "dns-remote",
                address: "https://8.8.8.8/dns-query",
                address_resolver: "dns-direct"
            },
            {
                tag: "dns-direct",
                address: "8.8.8.8",
                address_resolver: "dns-local",
                detour: "direct"
            },
            {
                tag: "dns-local",
                address: "local",
                detour: "direct"
            },
            {
                tag: "dns-block",
                address: "rcode://success"
            }
        ],
        rules: [
            {
                domain_suffix: [".ir"],
                server: "dns-direct"
            },
            {
                outbound: "direct",
                server: "dns-direct"
            }
        ],
        independent_cache: true
    },
    inbounds: [
        {
            type: "direct",
            tag: "dns-in",
            listen: "127.0.0.1",
            listen_port: 6450,
            override_address: "8.8.8.8",
            override_port: 53
        },
        {
            type: "tun",
            tag: "tun-in",
            mtu: 9000,
            inet4_address: "172.19.0.1/28",
            auto_route: true,
            strict_route: true,
            endpoint_independent_nat: true,
            stack: "mixed",
            sniff: true,
            sniff_override_destination: true
        },
        {
            type: "mixed",
            tag: "mixed-in",
            listen: "127.0.0.1",
            listen_port: 2080,
            sniff: true,
            sniff_override_destination: true
        }
    ],
    outbounds: [
        {
            type: "selector",
            tag: "proxy",
            outbounds: ["💦 Best-Ping 💥"]
        },
        {
            type: "urltest",
            tag: "💦 Best-Ping 💥",
            outbounds: [],
            url: "https://www.gstatic.com/generate_204",
            interval: "30s",
            tolerance: 50
        },
        {
            type: "direct",
            tag: "direct"
        },
        {
            type: "block",
            tag: "block"
        },
        {
            type: "dns",
            tag: "dns-out"
        }
    ],
    route: {
        rules: [
            {
                port: 53,
                outbound: "dns-out"
            },
            {
                inbound: "dns-in",
                outbound: "dns-out"
            },
            {
                network: "udp",
                port: 443,
                port_range: [],
                outbound: "block"
            },
            {
                ip_is_private: true,
                outbound: "direct"
            },
            {
                rule_set: [
                    "geosite-category-ads-all",
                    "geosite-malware",
                    "geosite-phishing",
                    "geosite-cryptominers",
                    "geoip-malware",
                    "geoip-phishing"
                ],
                outbound: "block"
            },
            {
                rule_set: ["geosite-ir", "geoip-ir"],
                outbound: "direct"
            },
            {
                ip_cidr: ["224.0.0.0/3", "ff00::/8"],
                source_ip_cidr: ["224.0.0.0/3", "ff00::/8"],
                outbound: "block"
            }
        ],
        rule_set: [
            {
                type: "remote",
                tag: "geosite-ir",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geosite-category-ads-all",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geosite-malware",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-malware.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geosite-phishing",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-phishing.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geosite-cryptominers",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-cryptominers.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geoip-ir",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geoip-malware",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-malware.srs",
                download_detour: "direct"
            },
            {
                type: "remote",
                tag: "geoip-phishing",
                format: "binary",
                url: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-phishing.srs",
                download_detour: "direct"
            }
        ],
        auto_detect_interface: true,
        override_android_vpn: true,
        final: "proxy"
    },
    experimental: {
        clash_api: {
            external_controller: "0.0.0.0:9090",
            external_ui: "yacd",
            external_ui_download_url: "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
            external_ui_download_detour: "direct",
            secret: "",
            default_mode: "rule"
        }
    }
};

const singboxOutboundTemp = {
    type: "vless",
    server: "",
    server_port: 443,
    uuid: "",
    domain_strategy: "prefer_ipv6",
    packet_encoding: "",
    tls: {
        alpn: [
            "http/1.1"
        ],
        enabled: true,
        insecure: false,
        server_name: "",
        utls: {
            enabled: true,
            fingerprint: "randomized"
        }
    },
    transport: {
        early_data_header_name: "Sec-WebSocket-Protocol",
        max_early_data: 2560,
        headers: {
            Host: ""
        },
        path: "/",
        type: "ws"
    },
    tag: ""
};

const xrayWgOutboundTemp = {
    protocol: "wireguard",
    settings: {
        address: [],
        mtu: 1280,
        peers: [
            {
                endpoint: "engage.cloudflareclient.com:2408",
                publicKey: ""
            }
        ],
        reserved: [],
        secretKey: "",
        keepAlive: 10
    },
    streamSettings: {
        sockopt: {
            dialerProxy: ""
        }
    },
    tag: "proxy"
};

const singboxWgOutboundTemp = {
    local_address: [],
    mtu: 1280,
    peer_public_key: "",
    pre_shared_key: "",
    private_key: "",
    reserved: "",
    server: "engage.cloudflareclient.com",
    server_port: 2408,
    type: "wireguard",
    domain_strategy: "prefer_ipv6",
    detour: "",
    tag: ""
};// Some code

相关功能

🌏 Remote DNS 远程DNS:远程DNS服务器地址,例如: https://94.140.14.14/dns-query

🏚️ Local DNS 本地DNS:本地DNS服务器地址,例如 8.8.8.8

📐 Length 长度: 数据包的最小和最大长度,范围为 100 - 200

🕞 Interval 间隔: 发送数据包的最小和最大间隔时间,范围为 5 - 10 秒

✈️ Chain Proxy 链代理: 用于输入代理链地址

该面板提供两种部署选项

Worker

创建BPB-Worker-Panel 分支并启动 Fork 进程。

Cloudflare KV

创建一个KV空间

Workers and Pages

打开Cloudflare → Workers and Pages → Create Application → Pages

Connect to Git

选择 Fork 的 BPB-Worker-Panel 项目

依次点击 ⚙ SettingsFunctionsKV namespace bindings

填写 Variable name bpb Save

点击🔧Deployment 👉 Production 👉 View details

环境变量

UUID=

打开Windows PowerShell new-guid 生成UUID,或v2Ray 生成UUID

PROXYIP=

优选IP

功能解释

远程DNS和本地DNS:指定使用的DNS服务器。远程DNS通常用于提高隐私性和安全性,而本地DNS则用于本地解析。

长度和间隔:设置数据包的大小和发送间隔,以优化流量和性能。

链代理:允许用户通过多个代理服务器进行链式代理,以增强隐私并绕过地域限制。

碎片路由:提供广告屏蔽、色情内容屏蔽以及绕过特定国家或局域网限制的选项,以提升网络体验。

代理IP:配置代理服务器的IP地址或域名,用于流量转发和隐藏真实IP。

清洁IP:用于配置和扫描清洁的IP地址,以避免IP被列入黑名单或受到限制。

端口:选择使用的端口,以确保与远程服务器的连接和通信。

WARP端点:配置WARP(WireGuard协议的一种实现)端点,以提供安全的网络连接。

应用设置:保存并应用所有配置选项,以使设置生效。

常规配置、碎片订阅和WARP订阅:提供不同应用程序的配置和订阅选项,用户可以根据需要选择合适的配置方式。

该面板用于管理和配置网络代理服务,通过碎片化数据、DNS配置和代理服务器,提供安全、私密和高效的网络连接。

支持客户机

客户端版本状态

v2rayNG

1.8.19 or higher

✔️

v2rayN

6.42 or higher

✔️

Nekobox

Sing-box

1.8.10 or higher

Streisand

✔️

V2Box

Shadowrocket

Nekoray

✔️

Hiddify

Last updated