Files
open-isp/scripts/olt-probe.js
T
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
2026-10-09 05:51:21 +07:00

346 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* OLT Probe — deteksi cepat kompatibilitas OLT baru (multi-vendor).
*
* Tujuan: sebelum/saat membeli OLT merk baru, jalankan probe ini untuk tahu:
* 1. SNMP hidup? vendor-nya siapa (dari sysObjectID / enterprise OID)?
* 2. Profil brand bawaan mana yang cocok (hioso/hsgq/cdata/vsol/generic/...)?
* 3. Telnet/SSH/Web UI terbuka? keluarga CLI-nya apa?
* 4. Rekomendasi: brand yang dipilih di aplikasi + apakah rename perlu adapter.
*
* HANYA perintah BACA (read-only). Tidak mengubah konfigurasi apa pun.
*
* Contoh:
* node scripts/olt-probe.js --from-db 1
* node scripts/olt-probe.js --host 192.168.16.2 --user admin --pass admin
* node scripts/olt-probe.js --host 10.0.0.9 --community public --cmds "show version,show onu"
* node scripts/olt-probe.js --from-db 1 --json /tmp/probe-olt1.json
*/
const net = require('net');
const http = require('http');
const https = require('https');
const snmp = require('net-snmp');
const oltSvc = require('../services/oltService');
/* ── Pemetaan enterprise OID → vendor (untuk tebakan cepat) ───────────── */
const ENTERPRISE_VENDOR = {
'25355': 'Hioso',
'3320': 'CTC-style EPON (umum di OLT China)',
'3902': 'ZTE',
'2011': 'Huawei',
'34592': 'C-Data',
'37950': 'V-SOL',
'27332': 'FiberHome',
'35048': 'BDCOM?',
'13158': 'Cisco (umum)',
'8072': 'Net-SNMP generik — vendor tidak menyetel sysObjectID (banyak OLT embedded Linux); penentuan pakai hasil "Profil brand" di bawah',
};
/* ── util ─────────────────────────────────────────────────────────────── */
function arg(name, def = null) {
const i = process.argv.indexOf('--' + name);
if (i === -1) return def;
const v = process.argv[i + 1];
return (v && !v.startsWith('--')) ? v : true;
}
function tcpCheck(host, port, timeout = 3000) {
return new Promise((resolve) => {
const s = new net.Socket();
const done = (ok) => { try { s.destroy(); } catch (e) {} resolve(ok); };
s.setTimeout(timeout);
s.once('connect', () => done(true));
s.once('timeout', () => done(false));
s.once('error', () => done(false));
s.connect(port, host);
});
}
function snmpValueToString(value) {
try {
let v = value;
if (Buffer.isBuffer(v)) v = v.toString('utf8');
v = String(v == null ? '' : v).replace(/\0/g, '').trim();
// beberapa perangkat mengembalikan string ber-quote
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1).trim();
return v;
} catch (e) { return String(value); }
}
function looksLikeOidString(s) {
return /^\d+(\.\d+){4,}$/.test(String(s || '').trim());
}
function decodeOid(buf) {
try {
const b = Buffer.from(buf);
if (!b.length) return '';
const out = [Math.floor(b[0] / 40), b[0] % 40];
let val = 0;
for (let i = 1; i < b.length; i++) {
val = (val << 7) | (b[i] & 0x7f);
if (!(b[i] & 0x80)) { out.push(val); val = 0; }
}
return out.join('.');
} catch (e) { return String(buf); }
}
function snmpGet(session, oids, timeout = 4000) {
return new Promise((resolve) => {
let finished = false;
const t = setTimeout(() => { if (!finished) { finished = true; resolve(null); } }, timeout);
try {
session.get(oids, (err, vbs) => {
if (finished) return;
finished = true; clearTimeout(t);
resolve(err ? null : vbs);
});
} catch (e) { if (!finished) { finished = true; clearTimeout(t); resolve(null); } }
});
}
function snmpSubtreeCount(session, oid, limit = 8, timeout = 5000) {
return new Promise((resolve) => {
const rows = [];
let finished = false;
const done = () => { if (!finished) { finished = true; resolve(rows); } };
const t = setTimeout(done, timeout);
try {
session.subtree(oid, 20, (vbs) => {
for (const vb of vbs) {
if (rows.length >= limit) break;
let val = vb.value;
if (Buffer.isBuffer(val)) {
const s = val.toString('utf8').replace(/\0/g, '').trim();
val = /^[\x20-\x7E]+$/.test(s) && s.length >= 2 ? s : val.toString('hex').toUpperCase();
}
rows.push({ oid: vb.oid, value: String(val).slice(0, 60) });
}
if (rows.length >= limit) { try { session.close(); } catch (e) {} }
}, () => { clearTimeout(t); done(); });
} catch (e) { clearTimeout(t); done(); }
});
}
function httpProbe(url, timeout = 5000, allowSelfSigned = false) {
return new Promise((resolve) => {
try {
const u = new URL(url);
const lib = u.protocol === 'https:' ? https : http;
const req = lib.request({
host: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80), path: u.pathname,
method: 'GET', timeout,
rejectUnauthorized: false,
headers: { 'User-Agent': 'OPEN-ISP-OLT-Probe/1.0' },
}, (res) => {
let body = '';
res.on('data', (d) => { if (body.length < 4000) body += d.toString('utf8'); });
res.on('end', () => {
const title = (body.match(/<title[^>]*>([^<]{0,80})</i) || [])[1] || '';
resolve({ ok: true, status: res.statusCode, server: res.headers.server || '', title: title.trim() });
});
});
req.on('timeout', () => { req.destroy(); resolve({ ok: false, why: 'timeout' }); });
req.on('error', (e) => resolve({ ok: false, why: e.code || e.message }));
req.end();
} catch (e) { resolve({ ok: false, why: e.message }); }
});
}
/* Fallback telnet yang tahan terhadap prompt aneh (mis. '#' tanpa newline) */
async function rawTelnetRun(host, port, user, pass, cmds, waitMs = 2200) {
const sock = new net.Socket();
let buf = '';
sock.setTimeout(15000);
sock.on('data', (d) => { buf += d.toString('utf8'); });
await new Promise((resolve, reject) => {
sock.connect(port, host, resolve);
sock.once('error', reject);
});
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const drain = () => { const x = buf; buf = ''; return x; };
const clean = (t) => String(t).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/\0/g, '').replace(/[^\x20-\x7E\r\n]/g, '');
await wait(2500);
let banner = clean(drain());
if (/(login|username)\s*[:>]/i.test(banner)) { sock.write(String(user || 'admin') + '\r\n'); await wait(1600); banner += clean(drain()); }
if (/(password|passwd)\s*[:>]/i.test(banner)) { sock.write(String(pass || 'admin') + '\r\n'); await wait(1800); banner += clean(drain()); }
sock.write('\r\n'); await wait(800); banner += clean(drain());
const outputs = [];
for (const cmd of cmds) {
sock.write(cmd + '\r\n');
await wait(waitMs);
outputs.push(clean(drain()));
}
try { sock.end(); sock.destroy(); } catch (e) {}
return { banner: banner.trim(), outputs };
}
function truncate(s, n) { s = String(s || ''); return s.length > n ? (s.slice(0, n) + '\n…[dipotong]') : s; }
/* ── main ─────────────────────────────────────────────────────────────── */
(async () => {
const fromDb = arg('from-db');
let host = arg('host');
let community = arg('community', 'public');
let snmpPort = Number(arg('snmp-port', 161));
let user = arg('user', 'admin');
let pass = arg('pass', 'admin');
let telnetPort = Number(arg('telnet-port', 23));
let cmds = String(arg('cmds', 'show version')).split(',').map(s => s.trim()).filter(Boolean);
const jsonOut = arg('json');
const enablePassword = arg('enable-password');
if (fromDb) {
const db = require('../config/database');
const olt = db.prepare('SELECT * FROM olts WHERE id = ?').get(Number(fromDb));
if (!olt) { console.error('OLT id=' + fromDb + ' tidak ditemukan di database'); process.exit(1); }
host = host || olt.host;
community = (community === 'public') ? (olt.snmp_community || community) : community;
snmpPort = snmpPort || Number(olt.snmp_port || 161);
user = (user === 'admin') ? (olt.web_user || user) : user;
pass = (pass === 'admin') ? (olt.web_password || pass) : pass;
telnetPort = telnetPort || Number(olt.telnet_port || 23);
console.log(`[i] Memakai data OLT dari DB: id=${olt.id} "${olt.name}" brand=${olt.brand} host=${host}`);
}
if (!host) {
console.log('Penggunaan: node scripts/olt-probe.js --host <ip> [--community public] [--user admin --pass admin]\n' +
' node scripts/olt-probe.js --from-db <id-olt> [--json hasil.json]');
process.exit(1);
}
const result = { host, at: new Date().toISOString(), snmp: null, tcp: {}, profiles: [], telnet: null, web: null, verdict: [] };
console.log(`\n=== OLT PROBE — ${host} ===\n`);
/* 1) cek port TCP */
console.log('-- Konektivitas --');
for (const p of [23, 22, 80, 443]) {
const ok = await tcpCheck(host, p);
result.tcp[p] = ok;
console.log(` port ${String(p).padEnd(3)} : ${ok ? 'TERBUKA' : 'tertutup/tidak merespons'}`);
}
/* 2) SNMP: identitas */
console.log('\n-- SNMP --');
const session = snmp.createSession(host, community, { port: snmpPort, version: snmp.Version2c, timeout: 4000, retries: 1 });
const idVbs = await snmpGet(session, ['1.3.6.1.2.1.1.1.0', '1.3.6.1.2.1.1.2.0', '1.3.6.1.2.1.1.5.0', '1.3.6.1.2.1.1.3.0'], 5000);
if (!idVbs) {
console.log(' SNMP: TIDAK MERESPONS (community salah / SNMP mati / port beda)');
result.snmp = { ok: false };
} else {
const sysDescr = idVbs[0] && idVbs[0].value != null ? snmpValueToString(idVbs[0].value) : '-';
const sysObjRaw = idVbs[1] && idVbs[1].value != null ? snmpValueToString(idVbs[1].value) : '';
const sysObjFromBuf = idVbs[1] && idVbs[1].value != null ? decodeOid(idVbs[1].value) : '-';
const sysObj = looksLikeOidString(sysObjRaw) ? sysObjRaw : (looksLikeOidString(sysObjFromBuf) ? sysObjFromBuf : sysObjFromBuf);
const sysName = idVbs[2] && idVbs[2].value != null ? snmpValueToString(idVbs[2].value) : '-';
const uptime = idVbs[3] && idVbs[3].value != null ? String(idVbs[3].value) : '-';
const ent = (sysObj.split('.')[6] || '');
let vendor = ENTERPRISE_VENDOR[ent] || '';
if (!vendor) {
// fallback: cari enterprise di daftar berdasarkan kemunculan substring OID
for (const [k, v] of Object.entries(ENTERPRISE_VENDOR)) {
if (sysObj.includes('4.1.' + k + '.')) { vendor = v + ' (terdeteksi dari OID)'; break; }
}
}
if (!vendor) vendor = ent ? `(enterprise ${ent} — belum terdaftar, laporkan ke developer)` : '-';
result.snmp = { ok: true, sysDescr, sysObjectID: sysObj, sysName, uptime, enterprise: ent, vendorGuess: vendor };
console.log(` sysDescr : ${truncate(sysDescr, 120)}`);
console.log(` sysObjectID : ${sysObj}`);
console.log(` enterprise : ${ent} → tebakan vendor: ${vendor}`);
console.log(` sysName : ${sysName}`);
}
/* 3) probe semua profil brand bawaan */
if (result.snmp && result.snmp.ok) {
console.log('\n-- Profil brand bawaan (uji OID probe) --');
for (const brandKey of Object.keys(oltSvc.BRAND_PROFILES || {})) {
for (const prof of (oltSvc.BRAND_PROFILES[brandKey] || [])) {
const probe = prof.probe_oid;
if (!probe) continue;
const vbs = await snmpGet(session, [probe], 3500);
if (vbs) {
const sample = await snmpSubtreeCount(session, prof.status_table || probe, 5, 5000);
result.profiles.push({ brandKey, profile: prof.name, ok: true, sample: sample.slice(0, 5) });
console.log(` ✅ ${brandKey.toUpperCase()} / ${prof.name} — HIDUP (contoh ${sample.length} entri: ${sample[0] ? sample[0].value : '-'} ...)`);
} else {
result.profiles.push({ brandKey, profile: prof.name, ok: false });
}
}
}
if (!result.profiles.some(p => p.ok)) console.log(' (tidak ada profil yang cocok — kemungkinan MIB privat; kirim hasil JSON ini ke developer)');
}
try { session.close(); } catch (e) {}
/* 4) Telnet (read-only) */
console.log('\n-- Telnet CLI --');
try {
const out = await oltSvc.telnetLoginAndRun(host, user, pass, cmds, {
port: telnetPort,
enablePassword: enablePassword !== null ? String(enablePassword) : null,
});
result.telnet = { ok: true, cmds, output: truncate(out, 2500) };
const head = String(out).split('\n').map(l => l.trim()).filter(Boolean).slice(0, 12).join('\n ');
console.log(` ✅ Login telnet BERHASIL (${cmds.join(' | ')}) — cuplikan:\n ${head}`);
const hay = String(out).toLowerCase();
const fam = hay.includes('hioso') ? 'Hioso' : hay.includes('hsgq') ? 'HSGQ' : hay.includes('c-data') || hay.includes('cdata') ? 'C-Data' : hay.includes('vsol') ? 'V-SOL' : null;
if (fam) console.log(` [i] Keluarga CLI terdeteksi: ${fam}`);
} catch (e) {
result.telnet = { ok: false, error: e.message || String(e) };
console.log(' ✖ Telnet gagal: ' + (e.message || String(e)));
// Diagnostik: tangkap banner mentah 4 detik agar terlihat prompt/flow-nya
try {
const raw = await new Promise((resolve) => {
const sock = new net.Socket();
let buf = '';
const t = setTimeout(() => { try { sock.destroy(); } catch (e2) {} resolve(buf); }, 4000);
sock.setTimeout(4000);
sock.on('data', (d) => { buf += d.toString('utf8').replace(/\0/g, ''); });
sock.once('error', () => { clearTimeout(t); resolve(buf); });
sock.once('timeout', () => { clearTimeout(t); try { sock.destroy(); } catch (e2) {} resolve(buf); });
sock.connect(telnetPort, host, () => {});
});
const clean = String(raw).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/[^\x20-\x7E\r\n]/g, '').trim();
result.telnet.rawBanner = truncate(clean, 600);
console.log(' [i] Banner telnet mentah:\n ' + truncate(clean, 400).split('\n').join('\n '));
} catch (e2) {}
// Fallback: mode raw (prompt longgar) — tetap READ-ONLY
try {
console.log(' [i] Mencoba mode telnet raw (prompt longgar)…');
const raw = await rawTelnetRun(host, telnetPort, user, pass, cmds);
result.telnetRaw = { ok: true, banner: truncate(raw.banner, 500), outputs: raw.outputs.map(o => truncate(o, 1200)) };
const joined = (raw.banner + '\n' + raw.outputs.join('\n'));
const head = String(joined).split('\n').map(l => l.trim()).filter(Boolean).slice(0, 10).join('\n ');
console.log(' ✅ Telnet RAW berhasil — cuplikan:\n ' + head);
const hay = joined.toLowerCase();
const fam = hay.includes('hioso') ? 'Hioso' : hay.includes('hsgq') ? 'HSGQ' : hay.includes('c-data') || hay.includes('cdata') ? 'C-Data' : hay.includes('vsol') ? 'V-SOL' : hay.includes('gpon') ? 'GPON-generik' : hay.includes('epon') ? 'EPON-generik' : null;
if (fam) console.log(' [i] Keluarga CLI terdeteksi (raw): ' + fam);
} catch (e3) {
console.log(' ✖ Telnet raw juga gagal: ' + (e3.message || String(e3)));
}
}
/* 5) Web UI */
console.log('\n-- Web UI --');
for (const url of [`http://${host}/`, `https://${host}/`]) {
const w = await httpProbe(url, 5000);
if (w.ok) {
result.web = { url, ...w };
console.log(` ${url} → HTTP ${w.status} | Server: ${w.server || '-'} | Title: ${w.title || '-'}`);
break;
} else {
console.log(` ${url} → ${w.why}`);
}
}
/* 6) kesimpulan */
console.log('\n=== KESIMPULAN ===');
const live = [...new Set(result.profiles.filter(p => p.ok).map(p => p.brandKey))];
if (result.snmp && result.snmp.ok) {
if (live.length) console.log(`✅ Pilih brand di aplikasi: ${live.map(s => s.toUpperCase()).join(' / ')} (monitoring SNMP siap)`);
else console.log('⚠️ SNMP hidup tapi tidak ada profil cocok → perlu profil baru (kirim JSON hasil ini).');
} else {
console.log('⚠️ SNMP mati/kosong. Jika telnet hidup, monitoring bisa dibuat via driver CLI (kirim cuplikan telnet ini).');
}
if (result.telnet && result.telnet.ok) console.log('ℹ️ Telnet hidup → jalur rename/authorize CLI memungkinkan (tergantung keluarga CLI).');
console.log('ℹ️ Rename nama pelanggan→OLT saat ini: hioso/hsgq (SNMP) & zte/huawei (CLI). Brand lain perlu adapter.');
if (jsonOut) {
require('fs').writeFileSync(String(jsonOut), JSON.stringify(result, null, 2));
console.log(`\n[i] Hasil lengkap disimpan: ${jsonOut} (kirim file ini untuk analisis lanjutan)`);
}
process.exit(0);
})().catch((e) => { console.error('Probe error:', e); process.exit(1); });