// ====== PENGATURAN UTAMA ======
const SCANNER_UUID = "1f37ac4f-fdd0-49df-9406-1eda70a1d512";
const FALLBACK_URLS = [];
// --- HELPER KV STORE ---
async function getUrls(env) {
try {
if (env && env.URL_STORE) {
const data = await env.URL_STORE.get("worker_urls");
if (data) {
const parsed = JSON.parse(data);
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
}
}
} catch (e) {}
return FALLBACK_URLS;
}
async function saveUrls(env, urls) {
try {
if (env && env.URL_STORE) await env.URL_STORE.put("worker_urls", JSON.stringify(urls));
} catch (e) {}
}
async function getAdminPassword(env) {
try {
if (env && env.URL_STORE) return await env.URL_STORE.get("admin_password");
} catch (e) {}
return null;
}
async function setAdminPassword(env, password) {
try {
if (env && env.URL_STORE) await env.URL_STORE.put("admin_password", password);
} catch (e) {}
}
async function getSessionToken(env) {
try {
if (env && env.URL_STORE) return await env.URL_STORE.get("admin_session");
} catch (e) {}
return null;
}
async function setSessionToken(env, token) {
try {
if (env && env.URL_STORE) await env.URL_STORE.put("admin_session", token, { expirationTtl: 86400 * 7 });
} catch (e) {}
}
async function getCfAccounts(env) {
try {
if (env && env.URL_STORE) {
const data = await env.URL_STORE.get("cf_accounts");
if (data) return JSON.parse(data);
}
} catch (e) {}
return [];
}
async function saveCfAccounts(env, accounts) {
try {
if (env && env.URL_STORE) await env.URL_STORE.put("cf_accounts", JSON.stringify(accounts));
} catch (e) {}
}
async function getTeleConfig(env) {
try {
if (env && env.URL_STORE) {
const data = await env.URL_STORE.get("tele_config");
if (data) return JSON.parse(data);
}
} catch (e) {}
return { token: "", chatId: "" };
}
async function saveTeleConfig(env, config) {
try {
if (env && env.URL_STORE) await env.URL_STORE.put("tele_config", JSON.stringify(config));
} catch (e) {}
}
function cleanUrlInput(rawUrl) {
if (!rawUrl) return '';
return rawUrl.trim().replace(/^https?:\/\//i, '').replace(/\/+$/, '');
}
async function isAuthenticated(request, env) {
try {
const cookieHeader = request.headers.get("Cookie") || "";
const match = cookieHeader.match(/session_token=([^;]+)/);
if (!match) return false;
const userToken = match[1];
const validToken = await getSessionToken(env);
return validToken && userToken === validToken;
} catch (e) {
return false;
}
}
// --- SCANNER STATUS ---
async function checkUrlStatus(url) {
const testPaths = ['/vmess', '/103.6.207.108-8080', '/vless', '/vless-argo', '/trojan', '/'];
const cleanHost = cleanUrlInput(url).split('/')[0];
let lastStatus = "DOWN";
for (const path of testPaths) {
try {
const res = await fetch(`https://${cleanHost}${path}`, {
method: 'GET',
headers: {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
'Sec-WebSocket-Version': '13',
'X-Sub-Id': SCANNER_UUID,
'Authorization': `Bearer ${SCANNER_UUID}`
},
redirect: 'manual'
});
lastStatus = res.status;
if (res.status === 101) return { isAlive: true, status: 101, path: path };
} catch (e) {
continue;
}
}
return { isAlive: false, status: lastStatus, path: "" };
}
// --- BOT TELEGRAM HELPER & HANDLER ---
async function sendTeleMessage(token, chatId, text, keyboard = null) {
if (!token || !chatId) return { ok: false };
const payload = { chat_id: chatId, text: text, parse_mode: "HTML" };
if (keyboard) payload.reply_markup = keyboard;
try {
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
return await res.json();
} catch(e) {
return { ok: false, description: e.message };
}
}
async function editTeleMessage(token, chatId, messageId, text, keyboard = null) {
if (!token || !chatId) return;
const payload = { chat_id: chatId, message_id: messageId, text: text, parse_mode: "HTML" };
if (keyboard) payload.reply_markup = keyboard;
try {
await fetch(`https://api.telegram.org/bot${token}/editMessageText`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
} catch(e) {}
}
function getTeleMainMenu() {
return {
inline_keyboard: [
[
{ text: "š Cek Status Tunnel", callback_data: "check_status" },
{ text: "š List Tunnel Server", callback_data: "list_urls" }
],
[
{ text: "ā Tambah Tunnel", callback_data: "ask_add_url" },
{ text: "šļø Hapus Tunnel", callback_data: "ask_del_url" }
],
[
{ text: "š List Custom Domain", callback_data: "list_domains" }
]
]
};
}
async function handleTelegramWebhook(request, env) {
const teleConfig = await getTeleConfig(env);
if (!teleConfig.token) return new Response("OK");
try {
const update = await request.json();
if (update.callback_query) {
const query = update.callback_query;
const chatId = query.message.chat.id;
const messageId = query.message.message_id;
const data = query.data;
if (data.startsWith("confirm_del:")) {
const urlToDel = data.replace("confirm_del:", "");
let urls = await getUrls(env);
const index = urls.indexOf(urlToDel);
if (index !== -1) {
urls.splice(index, 1);
await saveUrls(env, urls);
await editTeleMessage(teleConfig.token, chatId, messageId, `šļø URL Berhasil Dihapus:\n${urlToDel}`, getTeleMainMenu());
} else {
await editTeleMessage(teleConfig.token, chatId, messageId, `ā ļø URL sudah tidak ada di KV Store.`, getTeleMainMenu());
}
} else if (data === "cancel_del") {
await editTeleMessage(teleConfig.token, chatId, messageId, "ā Penghapusan dibatalkan.", getTeleMainMenu());
} else if (data === "check_status") {
await sendTeleMessage(teleConfig.token, chatId, "ā³ Sedang mengecek semua status tunnel...");
const urls = await getUrls(env);
if (urls.length === 0) {
await sendTeleMessage(teleConfig.token, chatId, "ā Belum ada URL tersimpan di list.");
} else {
let report = "š Laporan Status Tunnel Server:\n\n";
for (const u of urls) {
const st = await checkUrlStatus(u);
if (st.isAlive) {
report += `ā
${u} -> 101 ALIVE (${st.path})\n`;
} else {
report += `ā ${u} -> ERR: ${st.status}\n`;
}
}
await sendTeleMessage(teleConfig.token, chatId, report, getTeleMainMenu());
}
} else if (data === "list_urls") {
const urls = await getUrls(env);
let msg = "š List Tunnel Server: \n\n";
if (urls.length === 0) msg += "Belum ada URL tersimpan.";
else urls.forEach((u, i) => msg += `${i + 1}. ${u}\n`);
await sendTeleMessage(teleConfig.token, chatId, msg, getTeleMainMenu());
} else if (data === "ask_add_url") {
await sendTeleMessage(teleConfig.token, chatId, "āļø Kirimkan URL Tunnel Server Baru:\n\nContoh:\nmy-tunnel.trycloudflare.com\natau gunakan command:\n/add my-tunnel.trycloudflare.com");
} else if (data === "ask_del_url") {
const urls = await getUrls(env);
if (urls.length === 0) {
await sendTeleMessage(teleConfig.token, chatId, "ā Belum ada URL untuk dihapus.", getTeleMainMenu());
} else {
let msg = "šļø Pilih Nomor URL yang Ingin Dihapus:\n\nKetik /del <nomor>\n\nDaftar URL:\n";
urls.forEach((u, i) => msg += `${i + 1}. ${u}\n`);
await sendTeleMessage(teleConfig.token, chatId, msg);
}
} else if (data === "list_domains") {
await sendTeleMessage(teleConfig.token, chatId, "š Untuk kelola custom domain, silakan buka menu dashboard di browser.", getTeleMainMenu());
}
return new Response("OK");
}
if (update.message && update.message.text) {
const chatId = update.message.chat.id;
const text = update.message.text.trim();
if (teleConfig.chatId !== chatId.toString()) {
teleConfig.chatId = chatId.toString();
await saveTeleConfig(env, teleConfig);
}
if (text.startsWith("/start") || text.startsWith("/menu")) {
await sendTeleMessage(teleConfig.token, chatId, "ā” Selamat datang di Pages Manager Bot!\n\nSilakan pilih menu di bawah ini:", getTeleMainMenu());
} else if (text.startsWith("/add ")) {
const rawUrl = text.replace("/add ", "").trim();
const clean = cleanUrlInput(rawUrl);
if (clean) {
let urls = await getUrls(env);
if (!urls.includes(clean)) {
urls.push(clean);
await saveUrls(env, urls);
await sendTeleMessage(teleConfig.token, chatId, `ā
Berhasil menambahkan URL:\n${clean}`, getTeleMainMenu());
} else {
await sendTeleMessage(teleConfig.token, chatId, `ā ļø URL ${clean} sudah ada di dalam list!`, getTeleMainMenu());
}
}
} else if (text.startsWith("/del ")) {
const numStr = text.replace("/del ", "").trim();
const idx = parseInt(numStr, 10) - 1;
let urls = await getUrls(env);
if (!isNaN(idx) && idx >= 0 && idx < urls.length) {
const removed = urls.splice(idx, 1);
await saveUrls(env, urls);
await sendTeleMessage(teleConfig.token, chatId, `ā
Berhasil menghapus URL:\n${removed[0]}`, getTeleMainMenu());
} else {
await sendTeleMessage(teleConfig.token, chatId, "ā Nomor index URL tidak valid!");
}
} else if (text.includes("trycloudflare.com") || (text.includes(".") && !text.startsWith("/"))) {
const clean = cleanUrlInput(text);
if (clean) {
let urls = await getUrls(env);
if (!urls.includes(clean)) {
urls.push(clean);
await saveUrls(env, urls);
await sendTeleMessage(teleConfig.token, chatId, `ā
Berhasil menyimpan URL Tunnel Baru:\n${clean}`, getTeleMainMenu());
} else {
await sendTeleMessage(teleConfig.token, chatId, `ā ļø URL ${clean} sudah tersimpan sebelumnya.`, getTeleMainMenu());
}
}
} else {
await sendTeleMessage(teleConfig.token, chatId, "š¤ Menu Utama Worker Manager:", getTeleMainMenu());
}
}
} catch (e) {}
return new Response("OK");
}
// ---------------- DASHBOARD WEB UI HTML ----------------
function renderHTML(isSetup = false) {
if (isSetup) {
return `
Buat password admin pertama kali.
${url.hostname}`;
const resSend = await sendTeleMessage(body.token, body.chatId, msg, getTeleMainMenu());
if (resSend && resSend.ok) {
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
} else {
return new Response(JSON.stringify({ success: false, error: resSend.description || "Gagal mengirim pesan" }), { headers: { "Content-Type": "application/json" } });
}
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
if (path === "/api/cf-accounts") {
if (request.method === "GET") return new Response(JSON.stringify(await getCfAccounts(env)), { headers: { "Content-Type": "application/json" } });
if (request.method === "POST") {
const body = await request.json();
let accounts = await getCfAccounts(env);
const idx = accounts.findIndex(a => a.email === body.email && a.apiKey === body.apiKey);
if (idx === -1) accounts.push(body); else accounts[idx] = body;
await saveCfAccounts(env, accounts);
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
if (request.method === "DELETE") {
const index = parseInt(url.searchParams.get("index"), 10);
let accounts = await getCfAccounts(env);
if (!isNaN(index) && index >= 0 && index < accounts.length) { accounts.splice(index, 1); await saveCfAccounts(env, accounts); }
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
}
}
if (path === "/api/cf-init-data" && request.method === "POST") {
const body = await request.json();
const headers = {};
if (body.email && body.email.includes('@')) {
headers["X-Auth-Email"] = body.email;
headers["X-Auth-Key"] = body.apiKey;
} else {
headers["Authorization"] = "Bearer " + body.apiKey;
}
try {
const zRes = await fetch("https://api.cloudflare.com/client/v4/zones?per_page=50", { headers });
const zData = await zRes.json();
const zones = zData.success ? zData.result.map(z => ({ id: z.id, name: z.name })) : [];
let workers = [];
if (zData.success && zData.result.length > 0) {
const accId = zData.result[0].account.id;
try {
const wRes = await fetch("https://api.cloudflare.com/client/v4/accounts/" + accId + "/workers/scripts", { headers });
const wData = await wRes.json();
if (wData.success && Array.isArray(wData.result)) {
const sorted = wData.result.sort((a, b) => new Date(b.modified_on) - new Date(a.modified_on));
workers = sorted.map(w => ({ id: w.id, modified: w.modified_on }));
}
} catch(e) {}
}
return new Response(JSON.stringify({ success: true, zones, workers }), { headers: { "Content-Type": "application/json" } });
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
if (path === "/api/add-zone" && request.method === "POST") {
const body = await request.json();
const headers = { "Content-Type": "application/json" };
if (body.email && body.email.includes('@')) {
headers["X-Auth-Email"] = body.email;
headers["X-Auth-Key"] = body.apiKey;
} else {
headers["Authorization"] = "Bearer " + body.apiKey;
}
try {
const accRes = await fetch("https://api.cloudflare.com/client/v4/accounts", { headers });
const accData = await accRes.json();
if (!accData.success || accData.result.length === 0) throw new Error("Account ID tidak ditemukan");
const accountId = accData.result[0].id;
const res = await fetch("https://api.cloudflare.com/client/v4/zones", {
method: "POST",
headers,
body: JSON.stringify({
account: { id: accountId },
name: body.domain,
type: "full"
})
});
const data = await res.json();
if (data.success) {
const ns = data.result ? data.result.name_servers : [];
return new Response(JSON.stringify({ success: true, nameServers: ns }), { headers: { "Content-Type": "application/json" } });
} else {
const msg = data.errors && data.errors.length > 0 ? data.errors[0].message : "Gagal daftarkan zone";
return new Response(JSON.stringify({ success: false, error: msg }), { status: 400 });
}
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
if (path === "/api/attach-domain" && request.method === "POST") {
const body = await request.json();
const headers = { "Content-Type": "application/json" };
if (body.email && body.email.includes('@')) {
headers["X-Auth-Email"] = body.email;
headers["X-Auth-Key"] = body.apiKey;
} else {
headers["Authorization"] = "Bearer " + body.apiKey;
}
try {
const res = await fetch("https://api.cloudflare.com/client/v4/zones/" + body.zoneId + "/workers/domains", {
method: "PUT",
headers,
body: JSON.stringify({
environment: "production",
hostname: body.hostname,
service: body.workerName
})
});
const data = await res.json();
if (data.success) {
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
} else {
const msg = data.errors && data.errors.length > 0 ? data.errors[0].message : "Gagal attach domain ke worker";
return new Response(JSON.stringify({ success: false, error: msg }), { status: 400 });
}
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
if (path === "/api/list-attached-domains" && request.method === "POST") {
const body = await request.json();
const headers = {};
if (body.email && body.email.includes('@')) {
headers["X-Auth-Email"] = body.email;
headers["X-Auth-Key"] = body.apiKey;
} else {
headers["Authorization"] = "Bearer " + body.apiKey;
}
try {
const res = await fetch("https://api.cloudflare.com/client/v4/zones/" + body.zoneId + "/workers/domains", { headers });
const data = await res.json();
if (data.success) {
const domains = data.result.map(d => ({
id: d.id,
hostname: d.hostname,
service: d.service
}));
return new Response(JSON.stringify({ success: true, domains }), { headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({ success: false }), { status: 400 });
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
if (path === "/api/detach-domain" && request.method === "POST") {
const body = await request.json();
const headers = {};
if (body.email && body.email.includes('@')) {
headers["X-Auth-Email"] = body.email;
headers["X-Auth-Key"] = body.apiKey;
} else {
headers["Authorization"] = "Bearer " + body.apiKey;
}
try {
const res = await fetch("https://api.cloudflare.com/client/v4/zones/" + body.zoneId + "/workers/domains/" + body.domainId, {
method: "DELETE",
headers
});
const data = await res.json();
return new Response(JSON.stringify({ success: data.success }), { headers: { "Content-Type": "application/json" } });
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
return new Response("Not Found", { status: 404 });
}
// ---------------- HANDLER PROXY UTAMA ----------------
async function handleProxy(request, env) {
const WORKER_URLS = await getUrls(env);
if (WORKER_URLS.length === 0) return new Response("Belum ada target tunnel tersimpan di list.", { status: 503 });
const shuffled = [...WORKER_URLS].sort(() => Math.random() - 0.5);
const reqClone = request.clone();
for (const targetHost of shuffled) {
try {
const targetUrl = new URL(request.url);
const cleanHostStr = cleanUrlInput(targetHost);
if (!cleanHostStr) continue;
const parts = cleanHostStr.split('/');
const cleanHost = parts[0];
const basePath = parts.slice(1).join('/');
targetUrl.hostname = cleanHost;
targetUrl.protocol = 'https:';
if (basePath) targetUrl.pathname = '/' + basePath + (targetUrl.pathname === '/' ? '' : targetUrl.pathname);
const loopReq = reqClone.clone();
const response = await fetch(new Request(targetUrl, {
method: loopReq.method,
headers: loopReq.headers,
body: loopReq.body,
redirect: 'manual'
}));
if (response.status !== 429 && response.status !== 530) {
return response;
}
} catch (e) {
continue;
}
}
return new Response("Semua tunnel server tujuan tidak dapat dijangkau", { status: 503 });
}
// ---------------- PAGES ENTRY POINT ----------------
export async function onRequest(context) {
const { request, env } = context;
try {
const url = new URL(request.url);
// 1. Prioritaskan API Backend
if (url.pathname.startsWith("/api/")) {
return await handleApi(request, env);
}
if (url.pathname === "/telegram-webhook" && request.method === "POST") {
return await handleTelegramWebhook(request, env);
}
if (url.pathname === "/logout") {
return new Response("Logged out", {
status: 302,
headers: { "Location": "/ui", "Set-Cookie": "session_token=; Path=/; HttpOnly; Secure; Max-Age=0" }
});
}
if (url.pathname === "/login" && request.method === "POST") {
const formData = await request.formData();
const password = formData.get("password");
const savedPassword = await getAdminPassword(env);
if (savedPassword && password === savedPassword) {
const token = crypto.randomUUID();
await setSessionToken(env, token);
return new Response("OK", {
status: 302,
headers: { "Location": "/ui", "Set-Cookie": `session_token=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=604800` }
});
}
return new Response(renderLoginHTML("Password Salah!"), { headers: { "Content-Type": "text/html" } });
}
// 2. Khusus Dashboard UI & Setup Admin (hanya dibuka di /ui atau /admin)
if (url.pathname === "/ui" || url.pathname === "/admin") {
const savedPassword = await getAdminPassword(env);
if (!savedPassword) return new Response(renderHTML(true), { headers: { "Content-Type": "text/html" } });
if (await isAuthenticated(request, env)) return new Response(renderHTML(false), { headers: { "Content-Type": "text/html" } });
return new Response(renderLoginHTML(), { headers: { "Content-Type": "text/html" } });
}
// 3. Semua request lainnya (termasuk halaman utama / dan WebSocket/VPN traffic) langsung diarahkan ke server tujuan
return await handleProxy(request, env);
} catch (err) {
return new Response(`Pages Internal Error: ${err.message}`, { status: 500 });
}
}