import { connect } from "cloudflare:sockets"; // Variables let serviceName = ""; let APP_DOMAIN = ""; let prxIP = ""; let cachedPrxList = []; // IN-MEMORY CACHE STORAGE (Menyimpan hasil proxy terbaik di memori) let memoryProxyCache = { data: [], lastUpdated: 0, ttl: 10 * 60 * 1000 // Cache berlaku selama 10 menit }; // Constant & Default Values const horse = "dHJvamFu"; const flash = "dm1lc3M="; const neko = "dmxlc3M="; const v2 = "djJyYXk="; const PORTS = [443, 80]; const PROTOCOLS = [atob(horse), atob(flash), atob(neko), "ss"]; const SUB_PAGE_URL = "https://foolvpn.web.id/nautica"; // Default Configs Sesuai Foto & Optimal (Default DoH Cloudflare) const DEFAULT_CONFIG = { DNS_TYPE: "DOH", DOH_URL: "https://cloudflare-dns.com/dns-query", DNS_SERVER_ADDRESS: "1.1.1.1", DNS_SERVER_PORT: "53", UDP_MODE: "RELAY", UDP_RELAY_HOST: "udp-relay.hobihaus.space", UDP_RELAY_PORT: "7300", KV_PRX_URL: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.json", PRX_BANK_URL: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.txt", REVERSE_PRX_TARGET: "", RAW_KV_JSON_DATA: "", RAW_KV_TXT_DATA: "", TCP_OPTIMIZE_MODE: "OFF", // Default MATI ZERO_COPY_MODE: "ON", EARLY_FLUSH_MODE: "ON", STREAM_BUFFER_MODE: "HIGH_THROUGHPUT", // Default HIGH_THROUGHPUT DNS_HISTORY: [ { type: "DOH", label: "Cloudflare DoH", dohUrl: "https://cloudflare-dns.com/dns-query", isDefault: true }, { type: "DOH", label: "Google DoH", dohUrl: "https://dns.google/dns-query", isDefault: true }, { type: "DOH", label: "NextDNS DoH", dohUrl: "https://dns.nextdns.io/dns-query", isDefault: true }, { type: "UDP", label: "Google DNS (8.8.8.8:53)", addr: "8.8.8.8", port: "53", isDefault: true }, { type: "UDP", label: "Cloudflare DNS (1.1.1.1:53)", addr: "1.1.1.1", port: "53", isDefault: true } ], RELAY_HISTORY: [ { label: "Hobihaus Relay (udp-relay.hobihaus.space:7300)", host: "udp-relay.hobihaus.space", port: "7300", isDefault: true }, { label: "Railway Proxy (altaria.proxy.rlwy.net:57280)", host: "altaria.proxy.rlwy.net", port: "57280", isDefault: true } ], PRX_BANK_HISTORY: [ { label: "Default Proxy Bank TXT", url: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.txt", isDefault: true } ], KV_PRX_HISTORY: [ { label: "Default Proxy JSON URL", url: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.json", isDefault: true } ], CUSTOM_PRX_LIST: [] }; const CONVERTER_URL = "https://api.foolvpn.web.id/convert"; const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; const CORS_HEADER_OPTIONS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", }; // Encrypted Stream Constants const SALT_A1 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgS2V5X0xlbmd0aA=="); const SALT_A2 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgTm9uY2VfTGVuZ3Ro"); const SALT_A3 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgS2V5"); const SALT_A4 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgTm9uY2U="); const SALT_B1 = atob("QUVBRCBSZXNwIEhlYWRlciBMZW4gSVY="); const SALT_B2 = atob("QUVBRCBSZXNwIEhlYWRlciBMZW4gSVY="); const SALT_B3 = atob("QUVBRCBSZXNwIEhlYWRlciBLZXk="); const SALT_B4 = atob("QUVBRCBSZXNwIEhlYWRlciBJVg=="); // HELPER PARSER PROXY: IP:PORT atau IP:PORT@USER:PASS function parseProxy(proxyStr) { if (!proxyStr) return null; const atSplit = proxyStr.split("@"); const hostPort = atSplit[0]; const auth = atSplit[1] || null; let [host, port] = hostPort.split(":"); port = parseInt(port, 10) || 443; let user = null; let pass = null; if (auth && auth.includes(":")) { const authParts = auth.split(":"); user = authParts[0]; pass = authParts.slice(1).join(":"); } return { host, port, user, pass }; } async function getConfig(env) { let kvConfig = {}; if (env && env.CONFIG_KV) { try { const stored = await env.CONFIG_KV.get("APP_CONFIG", { type: "json" }); if (stored) kvConfig = stored; } catch (e) { console.error("Gagal membaca KV:", e); } } return { DNS_TYPE: kvConfig.DNS_TYPE || (env && env.DNS_TYPE) || DEFAULT_CONFIG.DNS_TYPE, DOH_URL: kvConfig.DOH_URL || (env && env.DOH_URL) || DEFAULT_CONFIG.DOH_URL, DNS_SERVER_ADDRESS: kvConfig.DNS_SERVER_ADDRESS || (env && env.DNS_SERVER_ADDRESS) || DEFAULT_CONFIG.DNS_SERVER_ADDRESS, DNS_SERVER_PORT: parseInt(kvConfig.DNS_SERVER_PORT || (env && env.DNS_SERVER_PORT) || DEFAULT_CONFIG.DNS_SERVER_PORT), UDP_MODE: kvConfig.UDP_MODE || (env && env.UDP_MODE) || DEFAULT_CONFIG.UDP_MODE, UDP_RELAY_HOST: kvConfig.UDP_RELAY_HOST || (env && env.UDP_RELAY_HOST) || DEFAULT_CONFIG.UDP_RELAY_HOST, UDP_RELAY_PORT: parseInt(kvConfig.UDP_RELAY_PORT || (env && env.UDP_RELAY_PORT) || DEFAULT_CONFIG.UDP_RELAY_PORT), KV_PRX_URL: kvConfig.KV_PRX_URL || (env && env.KV_PRX_URL) || DEFAULT_CONFIG.KV_PRX_URL, PRX_BANK_URL: kvConfig.PRX_BANK_URL || (env && env.PRX_BANK_URL) || DEFAULT_CONFIG.PRX_BANK_URL, REVERSE_PRX_TARGET: kvConfig.REVERSE_PRX_TARGET || DEFAULT_CONFIG.REVERSE_PRX_TARGET, RAW_KV_JSON_DATA: kvConfig.RAW_KV_JSON_DATA || DEFAULT_CONFIG.RAW_KV_JSON_DATA, RAW_KV_TXT_DATA: kvConfig.RAW_KV_TXT_DATA || DEFAULT_CONFIG.RAW_KV_TXT_DATA, TCP_OPTIMIZE_MODE: kvConfig.TCP_OPTIMIZE_MODE !== undefined ? kvConfig.TCP_OPTIMIZE_MODE : DEFAULT_CONFIG.TCP_OPTIMIZE_MODE, ZERO_COPY_MODE: kvConfig.ZERO_COPY_MODE !== undefined ? kvConfig.ZERO_COPY_MODE : DEFAULT_CONFIG.ZERO_COPY_MODE, EARLY_FLUSH_MODE: kvConfig.EARLY_FLUSH_MODE !== undefined ? kvConfig.EARLY_FLUSH_MODE : DEFAULT_CONFIG.EARLY_FLUSH_MODE, STREAM_BUFFER_MODE: kvConfig.STREAM_BUFFER_MODE !== undefined ? kvConfig.STREAM_BUFFER_MODE : DEFAULT_CONFIG.STREAM_BUFFER_MODE, DNS_HISTORY: kvConfig.DNS_HISTORY || DEFAULT_CONFIG.DNS_HISTORY, RELAY_HISTORY: kvConfig.RELAY_HISTORY || DEFAULT_CONFIG.RELAY_HISTORY, PRX_BANK_HISTORY: kvConfig.PRX_BANK_HISTORY || DEFAULT_CONFIG.PRX_BANK_HISTORY, KV_PRX_HISTORY: kvConfig.KV_PRX_HISTORY || DEFAULT_CONFIG.KV_PRX_HISTORY, CUSTOM_PRX_LIST: kvConfig.CUSTOM_PRX_LIST || DEFAULT_CONFIG.CUSTOM_PRX_LIST }; } async function getKVPrxList(config) { if (config.RAW_KV_JSON_DATA && config.RAW_KV_JSON_DATA.trim() !== "") { try { return JSON.parse(config.RAW_KV_JSON_DATA); } catch (e) { console.error("Gagal parsing RAW_KV_JSON_DATA dari KV:", e); } } if (!config.KV_PRX_URL) throw new Error("No URL Provided!"); const kvPrx = await fetch(config.KV_PRX_URL); return kvPrx.status == 200 ? await kvPrx.json() : {}; } async function getPrxList(config) { let kvParsedList = []; let urlParsedList = []; if (config.RAW_KV_TXT_DATA && config.RAW_KV_TXT_DATA.trim() !== "") { const lines = config.RAW_KV_TXT_DATA.split("\n").filter(Boolean); kvParsedList = lines.map((entry) => { const [prxIP, prxPort, country, org] = entry.split(","); return { prxIP: prxIP ? prxIP.trim() : "Unknown", prxPort: prxPort ? prxPort.trim() : "Unknown", country: country ? country.trim().toUpperCase() : "Unknown", org: org ? org.trim() : "Unknown Org", }; }).filter(Boolean); } if (config.PRX_BANK_URL) { try { const prxBank = await fetch(config.PRX_BANK_URL); if (prxBank.status == 200) { const text = (await prxBank.text()) || ""; const prxString = text.split("\n").filter(Boolean); urlParsedList = prxString.map((entry) => { const [prxIP, prxPort, country, org] = entry.split(","); return { prxIP: prxIP ? prxIP.trim() : "Unknown", prxPort: prxPort ? prxPort.trim() : "Unknown", country: country ? country.trim().toUpperCase() : "Unknown", org: org ? org.trim() : "Unknown Org", }; }).filter(Boolean); } } catch (e) { console.error("Gagal mengambil Proxy Bank TXT dari URL:", e); } } let customList = config.CUSTOM_PRX_LIST || []; const existingIdentifiers = new Set([ ...kvParsedList.map(p => `${p.prxIP}:${p.prxPort}`), ...customList.map(p => `${p.prxIP}:${p.prxPort}`) ]); const filteredUrlList = urlParsedList.filter(p => !existingIdentifiers.has(`${p.prxIP}:${p.prxPort}`)); cachedPrxList = [...customList, ...kvParsedList, ...filteredUrlList]; return cachedPrxList; } async function checkProxyInternal(host, port, timeoutMs = 800) { const startTime = Date.now(); try { const socket = connect({ hostname: host, port: parseInt(port) || 443 }); await Promise.race([ socket.opened, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs)) ]); const latency = Date.now() - startTime; try { socket.close(); } catch (e) {} return { status: "active", latency: latency }; } catch (err) { return { status: "dead", latency: 99999 }; } } async function getBestInMemoryProxy(config) { const now = Date.now(); if (memoryProxyCache.data.length > 0 && (now - memoryProxyCache.lastUpdated < memoryProxyCache.ttl)) { const randomCached = memoryProxyCache.data[Math.floor(Math.random() * memoryProxyCache.data.length)]; return `${randomCached.prxIP}:${randomCached.prxPort}`; } const list = await getPrxList(config); if (list.length === 0) return "172.232.249.224:2053"; const prioritized = list.filter(p => p.country === "SG" || p.country === "ID"); const targetPool = prioritized.length > 0 ? prioritized : list; const samples = targetPool.sort(() => 0.5 - Math.random()).slice(0, 6); const testResults = await Promise.all( samples.map(async (p) => { const res = await checkProxyInternal(p.prxIP, parseInt(p.prxPort) || 443, 600); return { ...p, latency: res.latency }; }) ); testResults.sort((a, b) => a.latency - b.latency); const healthyOnly = testResults.filter(p => p.latency < 400); if (healthyOnly.length > 0) { memoryProxyCache.data = healthyOnly; memoryProxyCache.lastUpdated = now; return `${healthyOnly[0].prxIP}:${healthyOnly[0].prxPort}`; } const activeOnly = testResults.filter(p => p.latency < 99999); if (activeOnly.length > 0) { memoryProxyCache.data = activeOnly; memoryProxyCache.lastUpdated = now; return `${activeOnly[0].prxIP}:${activeOnly[0].prxPort}`; } return `${samples[0].prxIP}:${samples[0].prxPort}`; } async function getBestSgProxyInternal(config) { const list = await getPrxList(config); const sgList = list.filter(p => p.country && p.country.toUpperCase() === "SG"); const targetList = sgList.length > 0 ? sgList : list; if (targetList.length === 0) return "172.232.249.224:2053"; const samples = targetList.sort(() => 0.5 - Math.random()).slice(0, 4); const testResults = await Promise.all( samples.map(async (p) => { const res = await checkProxyInternal(p.prxIP, parseInt(p.prxPort) || 443, 800); return { ipPort: `${p.prxIP}:${p.prxPort}`, latency: res.latency }; }) ); testResults.sort((a, b) => a.latency - b.latency); return testResults[0].ipPort || `${samples[0].prxIP}:${samples[0].prxPort}`; } async function getBestKvJsonProxyInternal(kvPrx) { let allProxies = []; for (const key in kvPrx) { if (Array.isArray(kvPrx[key])) { allProxies.push(...kvPrx[key]); } } if (allProxies.length === 0) return "172.232.249.224:2053"; const samples = allProxies.sort(() => 0.5 - Math.random()).slice(0, 4); const testResults = await Promise.all( samples.map(async (p) => { const hostPort = p.split("@")[0]; const [ip, port] = hostPort.split(":"); const res = await checkProxyInternal(ip, parseInt(port) || 443, 800); return { ipPort: p, latency: res.latency }; }) ); testResults.sort((a, b) => a.latency - b.latency); return testResults[0].ipPort || samples[0]; } async function reverseWeb(request, target, targetPath) { if (!target || target.trim() === "") target = "example.com"; const targetUrl = new URL(request.url); const targetChunk = target.split(":"); targetUrl.hostname = targetChunk[0]; targetUrl.port = targetChunk[1]?.toString() || "443"; targetUrl.pathname = targetPath || targetUrl.pathname; const modifiedHeaders = new Headers(request.headers); modifiedHeaders.set("Host", targetUrl.hostname); modifiedHeaders.set("X-Forwarded-Host", request.headers.get("Host")); const modifiedRequest = new Request(targetUrl, { method: request.method, headers: modifiedHeaders, body: request.body, redirect: "follow" }); const response = await fetch(modifiedRequest); const newResponse = new Response(response.body, response); for (const [key, value] of Object.entries(CORS_HEADER_OPTIONS)) { newResponse.headers.set(key, value); } newResponse.headers.set("X-Proxied-By", "Cloudflare Worker"); return newResponse; } export default { async fetch(request, env, ctx) { try { const url = new URL(request.url); APP_DOMAIN = url.hostname; serviceName = APP_DOMAIN.split(".")[0]; const currentConfig = await getConfig(env); if (url.pathname === "/api/get-proxy-list") { const list = await getPrxList(currentConfig); return new Response(JSON.stringify(list), { headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json", "Cache-Control": "no-store, no-cache, must-revalidate" } }); } if (url.pathname.startsWith("/api/check")) { const target = url.searchParams.get("ip"); if (!target || !target.includes(":")) { return new Response(JSON.stringify({ status: "error", message: "Gunakan parameter ?ip=IP:PORT" }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } }); } const hostPort = target.split("@")[0]; const [ip, port] = hostPort.split(":"); const res = await checkProxyInternal(ip, parseInt(port) || 443, 2500); return new Response(JSON.stringify({ status: res.status, ip: ip, port: port, latency: res.status === "active" ? `${res.latency}ms` : "timeout" }), { status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json", "Cache-Control": "no-store, no-cache, must-revalidate" } }); } const htmlContent = ` Nautica Worker Dashboard

⚡ DDFATHUVLES ULTRA SPEED

Full-Speed Pipeline Active (Zero-Lag)

▌ BUG SNI (NORMAL / STANDAR)

▌ BUG SNI (REVERSE)

▌ BUG CDN (PROXY PORT)

`; if (url.pathname === "/ui" || url.pathname === "/ui/") { return new Response(htmlContent, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0", ...CORS_HEADER_OPTIONS }, }); } const targetReversePrx = currentConfig.REVERSE_PRX_TARGET; if (url.pathname === "/") { if (!targetReversePrx || targetReversePrx === "example.com" || targetReversePrx.trim() === "") { return new Response(htmlContent, { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0", ...CORS_HEADER_OPTIONS }, }); } } if (url.pathname === "/api/save-config" && request.method === "POST") { if (!env || !env.CONFIG_KV) { return new Response( JSON.stringify({ success: false, message: "Binding CONFIG_KV belum diset di Cloudflare Worker!" }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } } ); } const body = await request.json(); let oldConfig = {}; try { const stored = await env.CONFIG_KV.get("APP_CONFIG", { type: "json" }); if (stored) oldConfig = stored; } catch (e) {} const mergedConfig = { ...DEFAULT_CONFIG, ...oldConfig, ...body }; await env.CONFIG_KV.put("APP_CONFIG", JSON.stringify(mergedConfig)); return new Response( JSON.stringify({ success: true, message: "Pengaturan berhasil disimpan ke KV." }), { status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } } ); } const upgradeHeader = request.headers.get("Upgrade"); if (upgradeHeader === "websocket") { const rawPath = decodeURIComponent(url.pathname); const cleanPath = rawPath.replace(/^\//, ""); if (cleanPath.toUpperCase() === "SEMUA") { prxIP = await getBestInMemoryProxy(currentConfig); return await websocketHandler(request, currentConfig); } // Support ip:port atau ip:port@user:pass const proxyAuthMatch = cleanPath.match(/^([a-zA-Z0-9.-]+:\d+(@[a-zA-Z0-9._~%!$&'()*+,;=-]+:[a-zA-Z0-9._~%!$&'()*+,;=-]+)?)$/); if (proxyAuthMatch) { prxIP = proxyAuthMatch[1]; return await websocketHandler(request, currentConfig); } const prxMatch = rawPath.match(/^\/(.+[:=-]\d+(@.+:.+)?)$/); if (prxMatch) { prxIP = prxMatch[1]; return await websocketHandler(request, currentConfig); } if (/^[A-Z0-9_-]+$/.test(cleanPath)) { const prxKeys = cleanPath.split(","); const prxKey = prxKeys[Math.floor(Math.random() * prxKeys.length)]; let selectedIp = ""; if (currentConfig.CUSTOM_PRX_LIST && currentConfig.CUSTOM_PRX_LIST.length > 0) { const matchedCustom = currentConfig.CUSTOM_PRX_LIST.find( p => p.org.toUpperCase() === prxKey || p.country.toUpperCase() === prxKey ); if (matchedCustom) { selectedIp = `${matchedCustom.prxIP}:${matchedCustom.prxPort}`; } } if (!selectedIp) { const kvPrx = await getKVPrxList(currentConfig); if (kvPrx[prxKey] && kvPrx[prxKey].length > 0) { selectedIp = kvPrx[prxKey][Math.floor(Math.random() * kvPrx[prxKey].length)]; } else { selectedIp = await getBestKvJsonProxyInternal(kvPrx); } } prxIP = selectedIp; return await websocketHandler(request, currentConfig); } if (/[a-z]/.test(cleanPath)) { prxIP = await getBestSgProxyInternal(currentConfig); return await websocketHandler(request, currentConfig); } } if (url.pathname.startsWith("/sub")) { return Response.redirect(SUB_PAGE_URL + `?host=${APP_DOMAIN}`, 301); } else if (url.pathname.startsWith("/check")) { const target = url.searchParams.get("target").split(":"); const result = await checkPrxHealth(target[0], target[1] || "443"); return new Response(JSON.stringify(result), { status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json", }, }); } else if (url.pathname.startsWith("/api/v1")) { const apiPath = url.pathname.replace("/api/v1", ""); if (apiPath.startsWith("/sub")) { const filterCC = url.searchParams.get("cc")?.split(",") || []; const filterPort = url.searchParams.get("port")?.split(",") || PORTS; const filterVPN = url.searchParams.get("vpn")?.split(",") || PROTOCOLS; const filterLimit = parseInt(url.searchParams.get("limit")) || 10; const filterFormat = url.searchParams.get("format") || "raw"; const fillerDomain = url.searchParams.get("domain") || APP_DOMAIN; const prxList = await getPrxList(currentConfig) .then((prxs) => { if (filterCC.length) { return prxs.filter((prx) => filterCC.includes(prx.country)); } return prxs; }) .then((prxs) => { shuffleArray(prxs); return prxs; }); const uuid = crypto.randomUUID(); const result = []; for (const prx of prxList) { const uri = new URL(`${atob(horse)}://${fillerDomain}`); uri.searchParams.set("encryption", "none"); uri.searchParams.set("type", "ws"); uri.searchParams.set("host", APP_DOMAIN); for (const port of filterPort) { for (const protocol of filterVPN) { if (result.length >= filterLimit) break; uri.protocol = protocol; uri.port = port.toString(); if (protocol == "ss") { uri.username = btoa(`none:${uuid}`); uri.searchParams.set( "plugin", `${atob(v2)}-plugin${port == 80 ? "" : ";tls"};mux=0;mode=websocket;path=/${prx.prxIP}-${ prx.prxPort };host=${APP_DOMAIN}`, ); } else { uri.username = uuid; } uri.searchParams.set("security", port == 443 ? "tls" : "none"); uri.searchParams.set("sni", port == 80 && protocol == atob(neko) ? "" : APP_DOMAIN); uri.searchParams.set("path", `/${prx.prxIP}-${prx.prxPort}`); uri.hash = `${result.length + 1} ${getFlagEmoji(prx.country)} ${prx.org} WS ${ port == 443 ? "TLS" : "NTLS" } [${serviceName}]`; result.push(uri.toString()); } } } let finalResult = ""; switch (filterFormat) { case "raw": finalResult = result.join("\n"); break; case atob(v2): finalResult = btoa(result.join("\n")); break; case atob(neko): case "sfa": case "bfr": const res = await fetch(CONVERTER_URL, { method: "POST", body: JSON.stringify({ url: result.join(","), format: filterFormat, template: "cf", }), }); if (res.status == 200) { finalResult = await res.text(); } else { return new Response(res.statusText, { status: res.status, headers: { ...CORS_HEADER_OPTIONS, }, }); } break; } return new Response(finalResult, { status: 200, headers: { ...CORS_HEADER_OPTIONS, }, }); } else if (apiPath.startsWith("/myip")) { return new Response( JSON.stringify({ ip: request.headers.get("cf-connecting-ipv6") || request.headers.get("cf-connecting-ip") || request.headers.get("x-real-ip"), colo: request.headers.get("cf-ray")?.split("-")[1], ...request.cf, }), { headers: { ...CORS_HEADER_OPTIONS, }, }, ); } } return await reverseWeb(request, targetReversePrx); } catch (err) { return new Response(`An error occurred: ${err.toString()}`, { status: 500, headers: { ...CORS_HEADER_OPTIONS, }, }); } }, }; async function websocketHandler(request, config) { const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept(); let addressLog = ""; let portLog = ""; const log = (info, event) => { console.log(`[${addressLog}:${portLog}] ${info}`, event || ""); }; const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); let remoteSocketWrapper = { value: null, }; let isDNS = false; readableWebSocketStream .pipeTo( new WritableStream({ async write(chunk, controller) { if (isDNS) { if (config.DNS_TYPE === "DOH") { return handleDoHOutbound(config.DOH_URL, chunk, webSocket, null, log); } return handleUDPOutbound( config.DNS_SERVER_ADDRESS, config.DNS_SERVER_PORT, chunk, webSocket, null, log, config ); } if (remoteSocketWrapper.value) { const writer = remoteSocketWrapper.value.writable.getWriter(); await writer.write(chunk); writer.releaseLock(); return; } const protocol = await protocolSniffer(chunk); let protocolHeader; if (protocol === atob(horse)) { protocolHeader = readHorseHeader(chunk); } else if (protocol === atob(flash)) { protocolHeader = await readStreamHeader(chunk); } else if (protocol === atob(neko)) { protocolHeader = readNekoHeader(chunk); } else if (protocol === "ss") { protocolHeader = readSsHeader(chunk); } else { throw new Error("Unknown Protocol!"); } addressLog = protocolHeader.addressRemote; portLog = `${protocolHeader.portRemote} -> ${protocolHeader.isUDP ? "UDP" : "TCP"}`; if (protocolHeader.hasError) { throw new Error(protocolHeader.message); } let responseHeader = protocolHeader.version; if (protocol === atob(flash) && protocolHeader.needsResponse) { responseHeader = await generateStreamResponseHeader( protocolHeader.responseOptions, protocolHeader.encKey, protocolHeader.encIv, ); } if (protocolHeader.isUDP) { if (protocolHeader.portRemote === 53) { isDNS = true; if (config.DNS_TYPE === "DOH") { return handleDoHOutbound(config.DOH_URL, chunk, webSocket, responseHeader, log); } return handleUDPOutbound( config.DNS_SERVER_ADDRESS, config.DNS_SERVER_PORT, chunk, webSocket, responseHeader, log, config ); } return handleUDPOutbound( protocolHeader.addressRemote, protocolHeader.portRemote, chunk, webSocket, responseHeader, log, config ); } handleTCPOutBound( remoteSocketWrapper, protocolHeader.addressRemote, protocolHeader.portRemote, protocolHeader.rawClientData, webSocket, responseHeader, log, config, ); }, 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, }); } async function protocolSniffer(buffer) { if (buffer.byteLength >= 62) { const horseDelimiter = new Uint8Array(buffer.slice(56, 60)); if (horseDelimiter[0] === 0x0d && horseDelimiter[1] === 0x0a) { if (horseDelimiter[2] === 0x01 || horseDelimiter[2] === 0x03 || horseDelimiter[2] === 0x7f) { if (horseDelimiter[3] === 0x01 || horseDelimiter[3] === 0x03 || horseDelimiter[3] === 0x04) { return atob(horse); } } } } if (buffer.byteLength >= 18) { const version = new Uint8Array(buffer.slice(0, 1))[0]; if (version === 0) { const protocolUuid = new Uint8Array(buffer.slice(1, 17)); if (arrayBufferToHex(protocolUuid).match(/^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$/i)) { return atob(neko); } } } if (buffer.byteLength >= 42) { const firstByte = new Uint8Array(buffer.slice(0, 1))[0]; if (firstByte === 0x01 || firstByte === 0x03 || firstByte === 0x04) { return "ss"; } return atob(flash); } return "ss"; } async function generateStreamResponseHeader(responseOptions, encKey, encIv) { try { const key = (await sha256(encKey)).slice(0, 16); const lengthKey = (await kdf(key, [SALT_B1])).slice(0, 16); const lengthIv = (await kdf(key, [SALT_B2])).slice(0, 12); const lengthData = new Uint8Array([0, 4]); const encryptedLength = await aesGcmEncrypt(lengthKey, lengthIv, lengthData, new Uint8Array(0)); const headerPayload = new Uint8Array([responseOptions[0], 0x00, 0x00, 0x00]); const payloadKey = (await kdf(key, [SALT_B3])).slice(0, 16); const payloadIv = (await kdf(key, [SALT_B4])).slice(0, 12); const encryptedPayload = await aesGcmEncrypt(payloadKey, payloadIv, headerPayload, new Uint8Array(0)); const response = new Uint8Array(encryptedLength.length + encryptedPayload.length); response.set(encryptedLength, 0); response.set(encryptedPayload, encryptedLength.length); return response; } catch (e) { console.error("VMess Response Header Error:", e); return new Uint8Array(0); } } async function readStreamHeader(buffer) { try { const uuidString = "00000000-0000-0000-0000-000000000000"; const uuidBytes = new Uint8Array( uuidString .replace(/-/g, "") .match(/.{1,2}/g) .map((byte) => parseInt(byte, 16)), ); const authKey = await md5( uuidBytes, new TextEncoder().encode(atob("YzQ4NjE5ZmUtOGYwMi00OWUwLWI5ZTktZWRmNzYzZTE3ZTIx")), ); const authId = new Uint8Array(buffer.slice(0, 16)); const encryptedLength = new Uint8Array(buffer.slice(16, 34)); const nonce = new Uint8Array(buffer.slice(34, 42)); const lengthKey = (await kdf(authKey, [SALT_A1, authId, nonce])).slice(0, 16); const lengthIv = (await kdf(authKey, [SALT_A2, authId, nonce])).slice(0, 12); const lengthBytes = await aesGcmDecrypt(lengthKey, lengthIv, encryptedLength, authId); const headerLength = (lengthBytes[0] << 8) | lengthBytes[1]; const encryptedHeader = new Uint8Array(buffer.slice(42, 42 + headerLength + 16)); const payloadKey = (await kdf(authKey, [SALT_A3, authId, nonce])).slice(0, 16); const payloadIv = (await kdf(authKey, [SALT_A4, authId, nonce])).slice(0, 12); const headerPayload = await aesGcmDecrypt(payloadKey, payloadIv, encryptedHeader, authId); const view = new DataView(headerPayload.buffer); let offset = 0; const version = view.getUint8(offset); offset += 1; if (version !== 1) { return { hasError: true, message: `Invalid protocol version: ${version}` }; } const encIv = new Uint8Array(headerPayload.slice(offset, offset + 16)); offset += 16; const encKey = new Uint8Array(headerPayload.slice(offset, offset + 16)); offset += 16; const options = new Uint8Array(headerPayload.slice(offset, offset + 4)); offset += 4; const cmd = view.getUint8(offset); offset += 1; const isUDP = cmd !== 0x01; const portRemote = view.getUint16(offset, false); offset += 2; const addressType = view.getUint8(offset); offset += 1; let addressRemote = ""; switch (addressType) { case 1: addressRemote = `${view.getUint8(offset)}.${view.getUint8(offset + 1)}.${view.getUint8(offset + 2)}.${view.getUint8(offset + 3)}`; offset += 4; break; case 2: case 3: const domainLength = view.getUint8(offset); offset += 1; addressRemote = new TextDecoder().decode(headerPayload.slice(offset, offset + domainLength)); offset += domainLength; break; case 4: const ipv6Parts = []; for (let i = 0; i < 8; i++) { ipv6Parts.push(view.getUint16(offset + i * 2, false).toString(16)); } addressRemote = ipv6Parts.join(":"); offset += 16; break; default: return { hasError: true, message: `Invalid address type: ${addressType}` }; } const rawDataIndex = 42 + headerLength + 16; return { hasError: false, addressRemote, addressType, portRemote, rawDataIndex, rawClientData: buffer.slice(rawDataIndex), version: new Uint8Array([options[0], 0]), isUDP, needsResponse: true, responseOptions: options, encKey: encKey, encIv: encIv, }; } catch (e) { return { hasError: true, message: "Stream header parsing failed: " + e.message, }; } } function readSsHeader(ssBuffer) { const view = new DataView(ssBuffer); const addressType = view.getUint8(0); let addressLength = 0; let addressValueIndex = 1; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 3: addressLength = new Uint8Array(ssBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 4: addressLength = 16; const dataView = new DataView(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `Invalid addressType for SS: ${addressType}`, }; } if (!addressValue) { return { hasError: true, message: `Destination address empty, address type is: ${addressType}`, }; } const portIndex = addressValueIndex + addressLength; const portBuffer = ssBuffer.slice(portIndex, portIndex + 2); const portRemote = new DataView(portBuffer).getUint16(0); return { hasError: false, addressRemote: addressValue, addressType: addressType, portRemote: portRemote, rawDataIndex: portIndex + 2, rawClientData: ssBuffer.slice(portIndex + 2), version: null, isUDP: portRemote == 53, }; } function readNekoHeader(buffer) { const version = new Uint8Array(buffer.slice(0, 1)); let isUDP = false; const optLength = new Uint8Array(buffer.slice(17, 18))[0]; const cmd = new Uint8Array(buffer.slice(18 + optLength, 18 + optLength + 1))[0]; if (cmd === 1) { } else if (cmd === 2) { isUDP = true; } else { return { hasError: true, message: `command ${cmd} is not supported`, }; } const portIndex = 18 + optLength + 1; const portBuffer = buffer.slice(portIndex, portIndex + 2); const portRemote = new DataView(portBuffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array(buffer.slice(addressIndex, addressIndex + 1)); const addressType = addressBuffer[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(buffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 2: addressLength = new Uint8Array(buffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(buffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 3: addressLength = 16; const dataView = new DataView(buffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(":"); 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: addressType, portRemote: portRemote, rawDataIndex: addressValueIndex + addressLength, rawClientData: buffer.slice(addressValueIndex + addressLength), version: new Uint8Array([version[0], 0]), isUDP: isUDP, }; } function readHorseHeader(buffer) { const dataBuffer = buffer.slice(58); if (dataBuffer.byteLength < 6) { return { hasError: true, message: "invalid request data", }; } let isUDP = false; const view = new DataView(dataBuffer); const cmd = view.getUint8(0); if (cmd == 3) { isUDP = true; } else if (cmd != 1) { throw new Error("Unsupported command type!"); } let addressType = view.getUint8(1); let addressLength = 0; let addressValueIndex = 2; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 3: addressLength = new Uint8Array(dataBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 4: addressLength = 16; const dataView = new DataView(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `invalid addressType is ${addressType}`, }; } if (!addressValue) { return { hasError: true, message: `address is empty, addressType is ${addressType}`, }; } const portIndex = addressValueIndex + addressLength; const portBuffer = dataBuffer.slice(portIndex, portIndex + 2); const portRemote = new DataView(portBuffer).getUint16(0); return { hasError: false, addressRemote: addressValue, addressType: addressType, portRemote: portRemote, rawDataIndex: portIndex + 4, rawClientData: dataBuffer.slice(portIndex + 4), version: null, isUDP: isUDP, }; } async function handleTCPOutBound( remoteSocket, addressRemote, portRemote, rawClientData, webSocket, responseHeader, log, config, ) { async function connectAndWrite(address, port) { const tcpSocket = connect({ hostname: address, port: parseInt(port, 10), }); remoteSocket.value = tcpSocket; log(`Connecting to ${address}:${port}`); if (rawClientData && rawClientData.byteLength > 0) { const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); writer.releaseLock(); } return tcpSocket; } async function connectViaHttpProxy(parsedPrx, targetHost, targetPort) { const tcpSocket = connect({ hostname: parsedPrx.host, port: parsedPrx.port, }); remoteSocket.value = tcpSocket; log(`Tunneling via Upstream Proxy -> ${parsedPrx.host}:${parsedPrx.port} (Auth: ${parsedPrx.user ? 'YES' : 'NO'})`); const writer = tcpSocket.writable.getWriter(); let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`; if (parsedPrx.user && parsedPrx.pass) { const authBase64 = btoa(`${parsedPrx.user}:${parsedPrx.pass}`); connectReq += `Proxy-Authorization: Basic ${authBase64}\r\n`; } connectReq += `User-Agent: Cloudflare-Worker\r\nProxy-Connection: Keep-Alive\r\n\r\n`; await writer.write(new TextEncoder().encode(connectReq)); const reader = tcpSocket.readable.getReader(); const { value: resChunk } = await reader.read(); reader.releaseLock(); const responseText = new TextDecoder().decode(resChunk || new Uint8Array(0)); if (!responseText.includes(" 200 ")) { throw new Error(`Proxy authentication/tunneling failed: ${responseText.split("\r\n")[0]}`); } if (rawClientData && rawClientData.byteLength > 0) { await writer.write(rawClientData); } writer.releaseLock(); return tcpSocket; } async function retry() { if (!prxIP) { safeCloseWebSocket(webSocket); return; } const parsed = parseProxy(prxIP); if (!parsed) { safeCloseWebSocket(webSocket); return; } try { let tcpSocket; if (parsed.user && parsed.pass) { tcpSocket = await connectViaHttpProxy(parsed, addressRemote, portRemote); } else { tcpSocket = await connectAndWrite(parsed.host, parsed.port); } tcpSocket.closed .catch((error) => console.log("retry tcpSocket closed error", error)) .finally(() => safeCloseWebSocket(webSocket)); if (config.ZERO_COPY_MODE === "ON") { remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log); } else if (config.TCP_OPTIMIZE_MODE === "OFF") { remoteSocketToWSStandard(tcpSocket, webSocket, responseHeader, null, log); } else { remoteSocketToWSOptimized(tcpSocket, webSocket, responseHeader, null, log); } } catch (e) { log(`Upstream proxy error: ${e.message}`); safeCloseWebSocket(webSocket); } } // Jika format proxy mengandung user:pass, langsung lewat upstream tunnel const parsedPrx = parseProxy(prxIP); if (parsedPrx && parsedPrx.user && parsedPrx.pass) { try { const tcpSocket = await connectViaHttpProxy(parsedPrx, addressRemote, portRemote); if (config.ZERO_COPY_MODE === "ON") { remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log); } else if (config.TCP_OPTIMIZE_MODE === "OFF") { remoteSocketToWSStandard(tcpSocket, webSocket, responseHeader, null, log); } else { remoteSocketToWSOptimized(tcpSocket, webSocket, responseHeader, null, log); } } catch (err) { log(`Upstream proxy direct attempt error: ${err.message}`); safeCloseWebSocket(webSocket); } return; } // Mode Standard / Bypass try { const tcpSocket = await connectAndWrite(addressRemote, portRemote); if (config.ZERO_COPY_MODE === "ON") { remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, retry, log); } else if (config.TCP_OPTIMIZE_MODE === "OFF") { remoteSocketToWSStandard(tcpSocket, webSocket, responseHeader, retry, log); } else { remoteSocketToWSOptimized(tcpSocket, webSocket, responseHeader, retry, log); } } catch (err) { log(`Direct connect failed to ${addressRemote}:${portRemote}, switching to ProxyIP...`); await retry(); } } // PIPELINE ZERO-COPY NATIVE DIRECT STREAM async function remoteSocketToWSZeroCopy(remoteSocket, webSocket, responseHeader, retry, log) { let header = responseHeader; let hasIncomingData = false; await remoteSocket.readable .pipeTo( new WritableStream({ write(chunk) { hasIncomingData = true; if (webSocket.readyState !== WS_READY_STATE_OPEN) return; if (header && header.byteLength > 0) { const combined = new Uint8Array(header.byteLength + chunk.byteLength); combined.set(new Uint8Array(header), 0); combined.set(new Uint8Array(chunk), header.byteLength); webSocket.send(combined.buffer); header = null; } else { webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable closed (Zero-Copy)`); }, abort(reason) { console.error(`remoteConnection!.readable abort (Zero-Copy)`, reason); } }) ) .catch((error) => { safeCloseWebSocket(webSocket); }); if (!hasIncomingData && retry) { retry(); } } async function remoteSocketToWSStandard(remoteSocket, webSocket, responseHeader, retry, log) { let header = responseHeader; let hasIncomingData = false; await remoteSocket.readable .pipeTo( new WritableStream({ async write(chunk, controller) { hasIncomingData = true; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error("webSocket.readyState is not open"); return; } if (header && header.byteLength > 0) { const combined = new Uint8Array(header.byteLength + chunk.byteLength); combined.set(new Uint8Array(header), 0); combined.set(new Uint8Array(chunk), header.byteLength); webSocket.send(combined.buffer); header = null; } else { webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable is close`); }, abort(reason) { console.error(`remoteConnection!.readable abort`, reason); }, }) ) .catch((error) => { console.error(`remoteSocketToWS standard exception`, error); safeCloseWebSocket(webSocket); }); if (!hasIncomingData && retry) { retry(); } } async function remoteSocketToWSOptimized(remoteSocket, webSocket, responseHeader, retry, log) { let header = responseHeader; let hasIncomingData = false; const optimizedWriter = new WritableStream({ async write(chunk, controller) { hasIncomingData = true; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error("webSocket.readyState is not open"); return; } if (header && header.byteLength > 0) { const combined = new Uint8Array(header.byteLength + chunk.byteLength); combined.set(new Uint8Array(header), 0); combined.set(new Uint8Array(chunk), header.byteLength); webSocket.send(combined.buffer); header = null; } else { webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable is close (optimized)`); }, abort(reason) { console.error(`remoteConnection!.readable abort (optimized)`, reason); }, }); try { await remoteSocket.readable.pipeTo(optimizedWriter); } catch (error) { console.error(`remoteSocketToWS optimized exception`, error); safeCloseWebSocket(webSocket); } if (!hasIncomingData && retry) { retry(); } } async function handleDoHOutbound(dohUrl, dataChunk, webSocket, responseHeader, log) { try { let protocolHeader = responseHeader; const dohResponse = await fetch(dohUrl, { method: "POST", headers: { "Content-Type": "application/dns-message", "Accept": "application/dns-message", }, body: dataChunk, }); if (dohResponse.ok && webSocket.readyState === WS_READY_STATE_OPEN) { const dnsResponseBuffer = await dohResponse.arrayBuffer(); if (protocolHeader) { webSocket.send(await new Blob([protocolHeader, dnsResponseBuffer]).arrayBuffer()); } else { webSocket.send(dnsResponseBuffer); } } } catch (e) { console.error(`Error handling DoH Outbound: ${e.message}`); } } async function handleUDPOutbound(targetAddress, targetPort, dataChunk, webSocket, responseHeader, log, config) { try { let protocolHeader = responseHeader; let tcpSocket; if (config.UDP_MODE === "NATIVE") { tcpSocket = connect({ hostname: targetAddress, port: targetPort, }); const writer = tcpSocket.writable.getWriter(); await writer.write(dataChunk); writer.releaseLock(); } else { const relayHost = config.UDP_RELAY_HOST || "udp-relay.hobihaus.space"; const relayPort = config.UDP_RELAY_PORT || 7300; tcpSocket = connect({ hostname: relayHost, port: relayPort, }); const header = `udp:${targetAddress}:${targetPort}`; const headerBuffer = new TextEncoder().encode(header); const separator = new Uint8Array([0x7c]); const relayMessage = new Uint8Array(headerBuffer.length + separator.length + dataChunk.byteLength); relayMessage.set(headerBuffer, 0); relayMessage.set(separator, headerBuffer.length); relayMessage.set(new Uint8Array(dataChunk), headerBuffer.length + separator.length); const writer = tcpSocket.writable.getWriter(); await writer.write(relayMessage); writer.releaseLock(); } await tcpSocket.readable.pipeTo( new WritableStream({ async write(chunk) { if (webSocket.readyState === WS_READY_STATE_OPEN) { if (protocolHeader) { webSocket.send(await new Blob([protocolHeader, chunk]).arrayBuffer()); protocolHeader = null; } else { webSocket.send(chunk); } } }, close() { log(`UDP connection to ${targetAddress} closed`); }, abort(reason) { console.error(`UDP connection aborted due to ${reason}`); }, }), ); } catch (e) { console.error(`Error while handling UDP outbound: ${e.message}`); } } function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { webSocketServer.addEventListener("message", (event) => { if (readableStreamCancel) { return; } const message = event.data; controller.enqueue(message); }); webSocketServer.addEventListener("close", () => { safeCloseWebSocket(webSocketServer); if (readableStreamCancel) { return; } 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) {}, cancel(reason) { if (readableStreamCancel) { return; } log(`ReadableStream was canceled, due to ${reason}`); readableStreamCancel = true; safeCloseWebSocket(webSocketServer); }, }); return stream; } 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); } } async function checkPrxHealth(prxIP, prxPort) { const req = await fetch(`https://id1.foolvpn.web.id/api/v1/check?ip=${prxIP}:${prxPort}`); return await req.json(); } function base64ToArrayBuffer(base64Str) { if (!base64Str) { return { error: null }; } try { 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 { error }; } } function arrayBufferToHex(buffer) { return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join(""); } function shuffleArray(array) { let currentIndex = array.length; while (currentIndex != 0) { let randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; } } function getFlagEmoji(isoCode) { const codePoints = isoCode .toUpperCase() .split("") .map((char) => 127397 + char.charCodeAt(0)); return String.fromCodePoint(...codePoints); } // CRYPTO HELPERS async function md5(...inputs) { const combined = new Uint8Array(inputs.reduce((acc, input) => acc + input.length, 0)); let offset = 0; for (const input of inputs) { combined.set(new Uint8Array(input), offset); offset += input.length; } const hashBuffer = await crypto.subtle.digest("MD5", combined); return new Uint8Array(hashBuffer); } async function sha256(input) { const hashBuffer = await crypto.subtle.digest("SHA-256", input); return new Uint8Array(hashBuffer); } async function kdf(key, path) { async function hmacSha256(key, data) { const hmacKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const signature = await crypto.subtle.sign("HMAC", hmacKey, data); return new Uint8Array(signature); } async function recursiveHash(keyBytes, innerHashFn) { return async (data) => { const ipad = new Uint8Array(64); const opad = new Uint8Array(64); ipad.set(keyBytes.slice(0, Math.min(64, keyBytes.length))); opad.set(keyBytes.slice(0, Math.min(64, keyBytes.length))); for (let i = 0; i < 64; i++) { ipad[i] ^= 0x36; opad[i] ^= 0x5c; } const innerData = new Uint8Array(ipad.length + data.length); innerData.set(ipad); innerData.set(data, ipad.length); const innerResult = await innerHashFn(innerData); const outerData = new Uint8Array(opad.length + innerResult.length); outerData.set(opad); outerData.set(innerResult, opad.length); return await innerHashFn(outerData); }; } const sha256Hash = async (data) => { return new Uint8Array(await crypto.subtle.digest("SHA-256", data)); }; let currentHashFn = await recursiveHash(new TextEncoder().encode("VMess AEAD KDF"), sha256Hash); for (const salt of path) { const saltBytes = typeof salt === "string" ? new TextEncoder().encode(salt) : new Uint8Array(salt); currentHashFn = await recursiveHash(saltBytes, currentHashFn); } return await currentHashFn(key); } async function aesGcmDecrypt(key, nonce, data, aad) { const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["decrypt"]); try { const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce, additionalData: aad }, cryptoKey, data); return new Uint8Array(decrypted); } catch (e) { throw new Error("AEAD decryption failed: " + e.message); } } async function aesGcmEncrypt(key, nonce, data, aad) { const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt"]); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, additionalData: aad }, cryptoKey, data); return new Uint8Array(encrypted); }