From 094c47c15d03bc80df52856047d7c0ab79c59f47 Mon Sep 17 00:00:00 2001 From: oxpoe Date: Fri, 6 Nov 2026 05:49:15 +0700 Subject: [PATCH] feat(isolir): generator script cloud sederhana - tab Cloud di modal Script Isolir - resolve semua A record, warn Cloudflare/CDN - hook PPPoE, DNS redirect, NAT HTTP, filter allow/reject/drop - API /admin/api/isolir-cloud-script - versi 0.6.7 --- CHANGELOG.md | 9 +++ README.md | 2 +- package.json | 2 +- routes/adminPortal.js | 18 +++++- services/mikrotikService.js | 107 ++++++++++++++++++++++++++++++++++++ version.txt | 2 +- views/admin/routers.ejs | 88 +++++++++++++++++++++++------ 7 files changed, 207 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37fd8a8..1084887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.6.7] — 2026-11-06 — "isolir-cloud" + +### Ditambahkan +- Tab **Cloud (Sederhana)** pada modal Script Isolir (Manajemen Router): generate script RouterOS siap tempel untuk billing yang berjalan di cloud. + - Input domain/IP publik billing, port redirector, dan profil PPPoE isolir. + - Berisi: address-list BILLING (semua hasil resolve A), hook profil PPPoE, redirect DNS, NAT HTTP ke redirector, serta filter allow/reject/drop. + - Deteksi Cloudflare/CDN + peringatan bila port redirector tidak ikut diproxy. +- Endpoint `GET /admin/api/isolir-cloud-script`. + ## [0.6.6] — 2026-11-06 — "qr-custom-bulk" ### Ditambahkan diff --git a/README.md b/README.md index 1afeba6..ad6edcb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Lisensi: ISC - Versi 0.6.6 (qr-custom-bulk) + Versi 0.6.7 (isolir-cloud) Node.js >= 20 Docker Demo diff --git a/package.json b/package.json index e27f087..bae36e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-isp", - "version": "0.6.6", + "version": "0.6.7", "description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "main": "app-customer.js", "scripts": { diff --git a/routes/adminPortal.js b/routes/adminPortal.js index 64976d1..5dc264c 100644 --- a/routes/adminPortal.js +++ b/routes/adminPortal.js @@ -7673,9 +7673,12 @@ router.post('/whatsapp/repair-keys', requireAdminSession, async (req, res) => { // ─── ROUTERS (MULTI-ROUTER) ────────────────────────────────────────────────── router.get('/routers', requireAdminSession, requireSidebarMenuAccess('mikrotik'), (req, res) => { + const _settings = getSettings(); + const defaultPublicBase = String(_settings.public_base_url || _settings.app_url || ((req.headers['x-forwarded-proto'] || req.protocol) + '://' + req.get('host'))).replace(/\/+$/, ''); res.render('admin/routers', { title: 'Manajemen Router', company: company(), activePage: 'mikrotik', - routers: mikrotikService.getAllRouters(), msg: flashMsg(req) + routers: mikrotikService.getAllRouters(), msg: flashMsg(req), + defaultPublicBase }); }); @@ -7987,6 +7990,19 @@ router.post('/api/routers/:id/setup-firewall', requireAdmin, async (req, res) => } }); +router.get('/api/isolir-cloud-script', requireAdmin, async (req, res) => { + try { + const data = await mikrotikService.generateIsolirCloudScript({ + host: req.query.host, + port: req.query.port, + profile: req.query.profile + }); + res.json({ success: true, ...data }); + } catch (e) { + res.json({ success: false, error: e.message }); + } +}); + router.get('/api/isolir-portal-script', requireAdmin, async (req, res) => { try { const data = await mikrotikService.generateIsolirPortalScript(); diff --git a/services/mikrotikService.js b/services/mikrotikService.js index a4f76a8..1a79efd 100644 --- a/services/mikrotikService.js +++ b/services/mikrotikService.js @@ -1632,6 +1632,112 @@ async function generateIsolirPortalScript() { }; } +/** + * Script isolir sederhana untuk billing yang berjalan di CLOUD. + * Asumsi: redirector billing (port isolir_redirect_port) terbuka dari publik. + */ +async function generateIsolirCloudScript(opts = {}) { + const settings = getSettingsWithCache() || {}; + const rawHost = String(opts.host || settings.public_base_url || settings.app_url || '').trim(); + if (!rawHost) throw new Error('Isi domain/IP publik billing terlebih dahulu.'); + + let url = null; + try { + url = new URL(/^https?:\/\//i.test(rawHost) ? rawHost : ('https://' + rawHost)); + } catch (e) { + throw new Error('Domain/IP billing tidak valid.'); + } + const hostname = url.hostname; + const baseUrl = url.origin; + let billingIps = []; + if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) { + billingIps = [hostname]; + } else { + try { + const list = await dns.promises.resolve4(hostname); + billingIps = (list || []).filter(Boolean); + } catch (e) {} + if (!billingIps.length) { + try { billingIps = [await resolveIpv4(hostname)]; } catch (e) { billingIps = []; } + } + } + if (!billingIps.length) throw new Error('IP publik billing tidak bisa di-resolve. Masukkan IP origin langsung.'); + const billingIp = billingIps[0]; + const cfRanges = ['104.16.', '104.17.', '104.18.', '104.19.', '104.20.', '104.21.', '104.22.', '104.23.', '104.24.', '104.25.', '104.26.', '104.27.', '172.64.', '172.65.', '172.66.', '172.67.', '162.158.', '173.245.', '103.21.', '103.22.', '103.31.', '141.101.', '108.162.', '190.93.', '188.114.', '197.234.', '198.41.', '131.0.72.']; + const isCdn = billingIps.some(ip => cfRanges.some(p => ip.startsWith(p))); + const redirectPort = Math.max(1, Math.min(65535, parseInt(opts.port, 10) || parseInt(settings.isolir_redirect_port, 10) || 8099)); + const profile = String(opts.profile || 'isolir').trim() || 'isolir'; + const isolirUrl = String(settings.isolir_portal_url || (baseUrl + '/isolated')).trim(); + + const onUp = `/ip firewall address-list remove [find list=LIST_ISOLIR address=$\\"remote-address\\"]; /ip firewall address-list add list=LIST_ISOLIR address=$\\"remote-address\\" comment=$user timeout=23h`; + const onDown = `/ip firewall address-list remove [find list=LIST_ISOLIR address=$\\"remote-address\\"]`; + + const lines = [ + '# ============================================================', + '# ISOLIR PAGE — BILLING CLOUD (script sederhana)', + '# Tempel di Terminal RouterOS. Ubah variabel di bawah bila perlu.', + '# ============================================================', + ...(isCdn ? [ + '# PERINGATAN: ' + hostname + ' memakai Cloudflare/CDN.', + '# Port ' + redirectPort + ' biasanya TIDAK ikut diproxy Cloudflare.', + '# Untuk NAT redirector, gunakan IP ORIGIN (DNS-only) atau pastikan', + '# port ' + redirectPort + ' terbuka langsung di server origin.', + '# ============================================================', + '' + ] : []), + ':local billingIp "' + billingIp + '"', + ':local isolirPort ' + redirectPort, + ':local isolirProfile "' + profile + '"', + '', + '# --- Bersihkan rule lama buatan script ini ---', + '/ip firewall nat remove [find comment~"ISOLIR_CLOUD_"]', + '/ip firewall filter remove [find comment~"ISOLIR_CLOUD_"]', + '/ip firewall address-list remove [find list=BILLING comment="BILLING_ISOLIR"]', + '', + '# --- Izinkan billing (semua IP hasil resolve) ---', + ...billingIps.map(function (ip) { return '/ip firewall address-list add list=BILLING address=' + ip + ' comment="BILLING_ISOLIR"'; }), + '', + '# --- Hook profil PPPoE isolir ---', + '/ppp profile set [find name=$isolirProfile] on-up="' + onUp + '" on-down="' + onDown + '"', + '', + '# --- DNS isolir diarahkan ke router ---', + '/ip firewall nat add chain=dstnat protocol=udp dst-port=53 src-address-list=LIST_ISOLIR action=redirect to-ports=53 comment="ISOLIR_CLOUD_DNS_UDP"', + '/ip firewall nat add chain=dstnat protocol=tcp dst-port=53 src-address-list=LIST_ISOLIR action=redirect to-ports=53 comment="ISOLIR_CLOUD_DNS_TCP"', + '', + '# --- HTTP pelanggan isolir -> redirector cloud ---', + '/ip firewall nat add chain=dstnat protocol=tcp dst-port=80 src-address-list=LIST_ISOLIR dst-address-list=!BILLING action=dst-nat to-addresses=$billingIp to-ports=$isolirPort comment="ISOLIR_CLOUD_HTTP"', + '', + '# --- Filter ---', + '/ip firewall filter add chain=forward src-address-list=LIST_ISOLIR protocol=udp dst-port=53 action=accept comment="ISOLIR_CLOUD_DNS"', + '/ip firewall filter add chain=forward src-address-list=LIST_ISOLIR protocol=tcp dst-port=53 action=accept comment="ISOLIR_CLOUD_DNS_TCP"', + '/ip firewall filter add chain=forward src-address-list=LIST_ISOLIR dst-address-list=BILLING action=accept comment="ISOLIR_CLOUD_ALLOW_BILLING"', + '/ip firewall filter add chain=forward src-address-list=LIST_ISOLIR protocol=tcp dst-port=443 action=reject comment="ISOLIR_CLOUD_REJECT_HTTPS"', + '/ip firewall filter add chain=forward src-address-list=LIST_ISOLIR action=drop comment="ISOLIR_CLOUD_DROP"', + '', + '# ============================================================', + '# CATATAN BILLING CLOUD', + '# 1. Redirector billing harus bisa diakses: http://' + billingIp + ':' + redirectPort, + '# (buka port ' + redirectPort + ' di firewall/security group cloud).', + '# 2. Di billing set ISOLIR_REDIRECT_URL=' + isolirUrl, + '# atau setting isolir_portal_url.', + '# 3. Halaman isolir: ' + isolirUrl, + '# 4. Jika pakai Cloudflare/CDN, pastikan IP edge yang dipakai ada di', + '# address-list BILLING, atau gunakan IP origin langsung.', + '# ============================================================', + ]; + return { + script: lines.join('\n'), + billingHost: hostname, + billingIp, + billingIps, + isCdn, + redirectPort, + profile, + isolirUrl, + warning: isCdn ? ('Domain memakai Cloudflare/CDN. NAT redirector ke ' + billingIp + ':' + redirectPort + ' biasanya gagal — gunakan IP origin (DNS-only) atau buka port redirector di origin.') : '' + }; +} + const ISOLIR_ADDR_LIST = 'LIST_ISOLIR'; /** Nama profil PPPoE isolir yang dipakai pelanggan di router ini (distinct dari DB). */ @@ -2220,6 +2326,7 @@ module.exports = { setupIsolirFirewall, ensurePppProfileIsolirAddressListHook, generateIsolirPortalScript, + generateIsolirCloudScript, manageStaticIp, removeStaticIp }; diff --git a/version.txt b/version.txt index bf21f52..8b707c6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.6.6 \ No newline at end of file +0.6.7 \ No newline at end of file diff --git a/views/admin/routers.ejs b/views/admin/routers.ejs index 90fbade..af1e808 100644 --- a/views/admin/routers.ejs +++ b/views/admin/routers.ejs @@ -527,6 +527,9 @@ + @@ -554,6 +557,23 @@

+ + +
+
@@ -891,27 +912,60 @@ async function testConn(id) { } function switchIsolirTab(tab) { - const autoTab = document.getElementById('isolir-tab-auto'); - const scriptTab = document.getElementById('isolir-tab-script'); - const autoBtn = document.getElementById('tab-btn-auto'); - const scriptBtn = document.getElementById('tab-btn-script'); - const copyBtn = document.getElementById('btn-copy-script-modal'); + const panels = { + auto: document.getElementById('isolir-tab-auto'), + script: document.getElementById('isolir-tab-script'), + cloud: document.getElementById('isolir-tab-cloud') + }; + const btns = { + auto: document.getElementById('tab-btn-auto'), + script: document.getElementById('tab-btn-script'), + cloud: document.getElementById('tab-btn-cloud') + }; + const copyScript = document.getElementById('btn-copy-script-modal'); + const copyCloud = document.getElementById('btn-copy-cloud-modal'); + Object.keys(panels).forEach(function (k) { + if (panels[k]) panels[k].style.display = (k === tab) ? 'block' : 'none'; + if (btns[k]) btns[k].className = 'btn btn-sm ' + (k === tab ? 'btn-p' : 'btn-g'); + }); + if (copyScript) copyScript.style.display = tab === 'script' ? 'inline-block' : 'none'; + if (copyCloud) copyCloud.style.display = tab === 'cloud' ? 'inline-block' : 'none'; +} - if (tab === 'auto') { - autoTab.style.display = 'block'; - scriptTab.style.display = 'none'; - autoBtn.className = 'btn btn-sm btn-p'; - scriptBtn.className = 'btn btn-sm btn-g'; - if (copyBtn) copyBtn.style.display = 'none'; - } else { - autoTab.style.display = 'none'; - scriptTab.style.display = 'block'; - autoBtn.className = 'btn btn-sm btn-g'; - scriptBtn.className = 'btn btn-sm btn-p'; - if (copyBtn) copyBtn.style.display = 'inline-block'; +async function loadIsolirCloudScript(btn) { + const hostEl = document.getElementById('ic-host'); + const portEl = document.getElementById('ic-port'); + const profEl = document.getElementById('ic-profile'); + const body = document.getElementById('isolir-cloud-body'); + const meta = document.getElementById('isolir-cloud-meta'); + const old = btn ? btn.innerHTML : ''; + if (btn) { btn.disabled = true; btn.innerHTML = ' Membuat...'; } + if (body) body.value = ''; + if (meta) meta.textContent = 'Membuat script...'; + try { + const url = '/admin/api/isolir-cloud-script?host=' + encodeURIComponent(hostEl ? hostEl.value : '') + + '&port=' + encodeURIComponent(portEl ? portEl.value : '') + + '&profile=' + encodeURIComponent(profEl ? profEl.value : ''); + const res = await fetch(url); + const data = await res.json(); + if (!data.success) throw new Error(data.error || 'Gagal membuat script.'); + if (body) body.value = data.script || ''; + if (meta) meta.textContent = 'Billing: ' + (data.billingHost || '-') + ' → ' + ((data.billingIps || [data.billingIp]).join(', ')) + ' | redirect port ' + data.redirectPort + ' | ' + (data.isolirUrl || '') + (data.warning ? ' — ' + data.warning : ''); + } catch (e) { + if (meta) meta.textContent = 'Error: ' + e.message; + } finally { + if (btn) { btn.disabled = false; btn.innerHTML = old; } } } +function copyIsolirCloudScript() { + const ta = document.getElementById('isolir-cloud-body'); + if (!ta || !ta.value) return; + ta.select(); + try { document.execCommand('copy'); } catch (e) {} + alert('Script cloud disalin ke clipboard.'); +} + async function setupFirewall(routerId) { openIsolirScriptModal(routerId); switchIsolirTab('auto');