// ====== 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 ` Setup Password Admin

šŸ” Setup Password Admin

Buat password admin pertama kali.

`; } return ` Pages Manager

⚔ Pages Manager

šŸ¤– Bot Telegram Integration
šŸ”‘ Setting Cloudflare API Account
🌐 Custom Domain Terhubung ke Worker
šŸ”— List Tunnel Server
`; } function renderLoginHTML(error = '') { return ` Login Admin Pages

šŸ”‘ Login Admin

${error ? `
${error}
` : ''}
`; } // ---------------- API HANDLER UTAMA ---------------- async function handleApi(request, env) { const url = new URL(request.url); const path = url.pathname; if (path === "/api/setup" && request.method === "POST") { if (!env || !env.URL_STORE) { return new Response("Error: KV URL_STORE belum terhubung di Pages Settings.", { status: 500 }); } const savedPassword = await getAdminPassword(env); if (!savedPassword) { const formData = await request.formData(); const newPassword = formData.get("password"); if (newPassword && newPassword.trim().length >= 4) { await setAdminPassword(env, newPassword.trim()); 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("Setup complete", { status: 302, headers: { "Location": "/ui" } }); } if (path === "/api/check") { const targetUrl = url.searchParams.get("url"); if (!targetUrl) return new Response("URL required", { status: 400 }); const result = await checkUrlStatus(targetUrl); return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" } }); } if (!(await isAuthenticated(request, env))) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } }); } if (path === "/api/urls") { if (request.method === "GET") return new Response(JSON.stringify(await getUrls(env)), { headers: { "Content-Type": "application/json" } }); if (request.method === "POST") { const body = await request.json(); let urls = await getUrls(env); let clean = cleanUrlInput(body.url); if (clean && !urls.includes(clean)) { urls.push(clean); await saveUrls(env, urls); } 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 urls = await getUrls(env); if (!isNaN(index) && index >= 0 && index < urls.length) { urls.splice(index, 1); await saveUrls(env, urls); } return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } }); } } if (path === "/api/tele-config" && request.method === "GET") { return new Response(JSON.stringify(await getTeleConfig(env)), { headers: { "Content-Type": "application/json" } }); } if (path === "/api/tele-setup" && request.method === "POST") { const body = await request.json(); await saveTeleConfig(env, { token: body.token, chatId: body.chatId }); const webhookUrl = `https://${url.hostname}/telegram-webhook`; try { const teleRes = await fetch(`https://api.telegram.org/bot${body.token}/setWebhook?url=${encodeURIComponent(webhookUrl)}`); const teleData = await teleRes.json(); return new Response(JSON.stringify({ success: teleData.ok, error: teleData.description }), { headers: { "Content-Type": "application/json" } }); } catch (e) { return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 }); } } if (path === "/api/tele-test" && request.method === "POST") { const body = await request.json(); await saveTeleConfig(env, { token: body.token, chatId: body.chatId }); try { const msg = `⚔ Pages Manager Test\n\nBot Telegram berhasil terhubung ke Pages: ${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 }); } }