import { connect } from "cloudflare:sockets";
// Variables
let serviceName = "";
let APP_DOMAIN = "";
let prxIP = "";
let cachedPrxList = [];
// IN-MEMORY CACHE STORAGE
let memoryProxyCache = {
data: [],
lastUpdated: 0,
ttl: 10 * 60 * 1000
};
// 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
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",
ZERO_COPY_MODE: "ON",
EARLY_FLUSH_MODE: "ON",
STREAM_BUFFER_MODE: "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==");
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:", 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) {
try {
const kvPrx = await getKVPrxList(config);
if (kvPrx && kvPrx["SG"] && Array.isArray(kvPrx["SG"]) && kvPrx["SG"].length > 0) {
const sgPool = kvPrx["SG"];
return sgPool[Math.floor(Math.random() * sgPool.length)];
}
} catch (e) {}
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";
return `${targetList[0].prxIP}:${targetList[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";
return allProxies[Math.floor(Math.random() * allProxies.length)];
}
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
Smart Routing (/MULTI) & Zero-Copy Active
➕ Daftarkan Custom Proxy Baru
📋 Daftar Custom Proxy Tersimpan
`;
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(/^\//, "");
// 1. /MULTI -> Smart Auto Fallback
if (cleanPath.toUpperCase() === "MULTI") {
return await websocketHandler(request, currentConfig, "MULTI", "");
}
// 2. /DIRECT -> Pure Direct
if (cleanPath.toUpperCase() === "DIRECT") {
return await websocketHandler(request, currentConfig, "DIRECT", "");
}
// 3. /SEMUA
if (cleanPath.toUpperCase() === "SEMUA") {
const picked = await getBestInMemoryProxy(currentConfig);
return await websocketHandler(request, currentConfig, "PROXY", picked);
}
// 4. IP:Port atau Custom Proxy
const proxyAuthMatch = cleanPath.match(/^([a-zA-Z0-9.-]+:\d+(@[a-zA-Z0-9._~%!$&'()*+,;=-]+:[a-zA-Z0-9._~%!$&'()*+,;=-]+)?)$/);
if (proxyAuthMatch) {
return await websocketHandler(request, currentConfig, "PROXY", proxyAuthMatch[1]);
}
const prxMatch = rawPath.match(/^\/(.+[:=-]\d+(@.+:.+)?)$/);
if (prxMatch) {
return await websocketHandler(request, currentConfig, "PROXY", prxMatch[1]);
}
if (/^[A-Z0-9_-]+$/.test(cleanPath)) {
const prxKeys = cleanPath.split(",");
const prxKey = prxKeys[Math.floor(Math.random() * prxKeys.length)];
let 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);
}
return await websocketHandler(request, currentConfig, "PROXY", selectedIp);
}
if (/[a-z]/.test(cleanPath)) {
const picked = await getBestSgProxyInternal(currentConfig);
return await websocketHandler(request, currentConfig, "PROXY", picked);
}
}
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(`Error: ${err.toString()}`, { status: 500, headers: CORS_HEADER_OPTIONS });
}
}
};
async function websocketHandler(request, config, routingMode = "PROXY", assignedProxy = "") {
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,
routingMode,
assignedProxy
);
},
close() { log(`readableWebSocketStream is closed`); },
abort(reason) { log(`readableWebSocketStream is aborted`, reason); }
})
).catch(err => log("Stream pipe error", err));
return new Response(null, { status: 101, webSocket: client });
}
async function handleTCPOutBound(
remoteSocket,
addressRemote,
portRemote,
rawClientData,
webSocket,
responseHeader,
log,
config,
routingMode,
assignedProxy
) {
// Fast Connect dengan Timeout (Default 450ms)
async function connectAndWrite(address, port, timeoutMs = 450) {
const tcpSocket = connect({ hostname: address, port: parseInt(port, 10) });
remoteSocket.value = tcpSocket;
log(`Connecting Direct to ${address}:${port}`);
// Batas waktu paksa agar tidak nunggu timeout bawaan Cloudflare (3-5 detik)
if (timeoutMs > 0) {
await Promise.race([
tcpSocket.opened,
new Promise((_, reject) => setTimeout(() => reject(new Error("FastConnect Timeout")), timeoutMs))
]);
}
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 Proxy -> ${parsedPrx.host}:${parsedPrx.port}`);
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 tunneling failed: ${responseText.split("\r\n")[0]}`);
}
if (rawClientData && rawClientData.byteLength > 0) {
await writer.write(rawClientData);
}
writer.releaseLock();
return tcpSocket;
}
async function retryProxyFallback() {
try {
const backupPrx = assignedProxy || (await getBestSgProxyInternal(config));
const parsed = parseProxy(backupPrx);
if (!parsed) return safeCloseWebSocket(webSocket);
log(`Fallback through Proxy: ${parsed.host}:${parsed.port}`);
let tcpSocket = (parsed.user && parsed.pass)
? await connectViaHttpProxy(parsed, addressRemote, portRemote)
: await connectAndWrite(parsed.host, parsed.port, 0); // Proxy fallback tanpa limit agresif
tcpSocket.closed.catch(() => {}).finally(() => safeCloseWebSocket(webSocket));
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log);
} catch (e) {
log(`Fallback error: ${e.message}`);
safeCloseWebSocket(webSocket);
}
}
// 1. Jika mode PROXY eksplisit dengan auth
if (routingMode === "PROXY" && assignedProxy) {
const parsedPrx = parseProxy(assignedProxy);
if (parsedPrx && parsedPrx.user && parsedPrx.pass) {
try {
const tcpSocket = await connectViaHttpProxy(parsedPrx, addressRemote, portRemote);
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log);
return;
} catch (e) {
log(`Tunneling failed: ${e.message}`);
safeCloseWebSocket(webSocket);
return;
}
}
}
// 2. Mode MULTI & DIRECT: Batasi Direct maksimal 450ms, jika macet langsung fallback ke SG
try {
const timeoutLimit = (routingMode === "MULTI") ? 450 : 0;
const tcpSocket = await connectAndWrite(addressRemote, portRemote, timeoutLimit);
remoteSocketToWSZeroCopy(
tcpSocket,
webSocket,
responseHeader,
(routingMode === "MULTI" || routingMode === "PROXY") ? retryProxyFallback : null,
log
);
} catch (err) {
log(`Direct connect timeout/gagal (${err.message}). Langsung beralih ke Proxy...`);
if (routingMode === "MULTI" || routingMode === "PROXY") {
await retryProxyFallback();
} else {
safeCloseWebSocket(webSocket);
}
}
}
// 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(`remoteSocket closed`); },
abort(reason) { console.error(`remoteSocket abort`, reason); }
})
).catch(() => 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: ${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 to ${targetAddress} closed`); },
abort(reason) { console.error(`UDP abort: ${reason}`); },
}),
);
} catch (e) {
console.error(`Error UDP: ${e.message}`);
}
}
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
let readableStreamCancel = false;
return new ReadableStream({
start(controller) {
webSocketServer.addEventListener("message", (event) => {
if (readableStreamCancel) return;
controller.enqueue(event.data);
});
webSocketServer.addEventListener("close", () => {
safeCloseWebSocket(webSocketServer);
if (readableStreamCancel) return;
controller.close();
});
webSocketServer.addEventListener("error", (err) => {
log("webSocketServer 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;
readableStreamCancel = true;
safeCloseWebSocket(webSocketServer);
},
});
}
function safeCloseWebSocket(socket) {
try {
if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
socket.close();
}
} catch (error) {}
}
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) {
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++);
if (version !== 1) return { hasError: true, message: `Invalid protocol: ${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++);
const isUDP = cmd !== 0x01;
const portRemote = view.getUint16(offset, false); offset += 2;
const addressType = view.getUint8(offset++);
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++);
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,
encIv,
};
} catch (e) {
return { hasError: true, message: "Stream 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: ${addressType}` };
}
const portIndex = addressValueIndex + addressLength;
const portRemote = new DataView(ssBuffer.slice(portIndex, portIndex + 2)).getUint16(0);
return {
hasError: false,
addressRemote: addressValue,
addressType,
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 === 2) isUDP = true;
else if (cmd !== 1) return { hasError: true, message: `cmd ${cmd} unsupported` };
const portIndex = 18 + optLength + 1;
const portRemote = new DataView(buffer.slice(portIndex, portIndex + 2)).getUint16(0);
let addressIndex = portIndex + 2;
const addressType = new Uint8Array(buffer.slice(addressIndex, addressIndex + 1))[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: `invalid addressType ${addressType}` };
}
return {
hasError: false,
addressRemote: addressValue,
addressType,
portRemote,
rawDataIndex: addressValueIndex + addressLength,
rawClientData: buffer.slice(addressValueIndex + addressLength),
version: new Uint8Array([version[0], 0]),
isUDP,
};
}
function readHorseHeader(buffer) {
const dataBuffer = buffer.slice(58);
if (dataBuffer.byteLength < 6) return { hasError: true, message: "invalid 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 cmd");
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 ${addressType}` };
}
const portIndex = addressValueIndex + addressLength;
const portRemote = new DataView(dataBuffer.slice(portIndex, portIndex + 2)).getUint16(0);
return {
hasError: false,
addressRemote: addressValue,
addressType,
portRemote,
rawDataIndex: portIndex + 4,
rawClientData: dataBuffer.slice(portIndex + 4),
version: null,
isUDP,
};
}
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("");
}
// 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 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) => 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 decrypt error: " + 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);
}