wan-ip-guard/index.js
2026-08-31 15:17:50 +02:00

210 lines
5.9 KiB
JavaScript
Executable file

#!/usr/bin/env node
'use strict';
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const CONFIG = {
stateFile: process.env.STATE_FILE || '/var/lib/wan-ip-guard/current_ip',
daBaseUrl: process.env.DA_BASE_URL,
daUsername: process.env.DA_USERNAME,
daLoginKey: process.env.DA_LOGIN_KEY,
daDomain: process.env.DA_DOMAIN,
whatsappApi: process.env.WHATSAPP_API_ENDPOINT || 'http://localhost:3001',
whatsappSession: process.env.WHATSAPP_SESSION || 'EdGpt',
adminPhone: process.env.ADMIN_PHONE,
dnsRecords: (process.env.DNS_RECORDS || '').split(',').map(s => s.trim()).filter(Boolean),
};
function log(msg) {
console.log(`[${new Date().toISOString()}] ${msg}`);
}
function logError(msg) {
console.error(`[${new Date().toISOString()}] ERROR: ${msg}`);
}
async function fetchWithTimeout(url, options = {}, timeout = 15000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
async function getWanIp() {
const services = [
'https://ifconfig.me',
'https://api.ipify.org',
'https://icanhazip.com',
];
for (const url of services) {
try {
const resp = await fetchWithTimeout(url, {}, 10000);
if (resp.ok) {
const ip = (await resp.text()).trim();
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
log(`WAN IP obtained from ${url}: ${ip}`);
return ip;
}
}
} catch (e) {
log(`Could not get IP from ${url}: ${e.message}`);
}
}
throw new Error('Could not determine WAN IP from any service');
}
function readState() {
try {
return fs.readFileSync(CONFIG.stateFile, 'utf8').trim();
} catch {
return null;
}
}
function writeState(ip) {
const dir = path.dirname(CONFIG.stateFile);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(CONFIG.stateFile, ip + '\n');
}
async function daRequest(endpoint) {
const url = `${CONFIG.daBaseUrl}${endpoint}`;
const auth = Buffer.from(`${CONFIG.daUsername}:${CONFIG.daLoginKey}`).toString('base64');
const resp = await fetchWithTimeout(url, {
headers: { Authorization: `Basic ${auth}` },
});
const text = await resp.text();
if (!resp.ok) {
throw new Error(`DirectAdmin API ${resp.status}: ${text}`);
}
return text;
}
async function getDnsRecords() {
const text = await daRequest(`/CMD_API_DNS_CONTROL?domain=${encodeURIComponent(CONFIG.daDomain)}`);
const records = {};
for (const line of text.split('\n')) {
const match = line.match(/^(.+?)\s+\d+\s+IN\s+A\s+(\d+\.\d+\.\d+\.\d+)$/);
if (match) {
let name = match[1].trim();
if (name === `${CONFIG.daDomain}.` || name === CONFIG.daDomain) {
name = '';
}
if (!records[name]) records[name] = [];
records[name].push(match[2]);
}
}
return records;
}
async function deleteDnsRecord(name, ip) {
const recordValue = `${name}=${ip}`;
const endpoint = `/CMD_API_DNS_CONTROL?domain=${encodeURIComponent(CONFIG.daDomain)}&action=select&arecs0=${encodeURIComponent(recordValue)}`;
await daRequest(endpoint);
}
async function addDnsRecord(name, ip) {
const endpoint = `/CMD_API_DNS_CONTROL?domain=${encodeURIComponent(CONFIG.daDomain)}&action=add&type=A&name=${encodeURIComponent(name)}&value=${encodeURIComponent(ip)}`;
await daRequest(endpoint);
}
async function sendWhatsApp(message) {
const url = `${CONFIG.whatsappApi}/client/sendMessage/${CONFIG.whatsappSession}`;
const chatId = `${CONFIG.adminPhone}@c.us`;
try {
const resp = await fetchWithTimeout(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chatId, content: message, contentType: 'string' }),
}, 10000);
if (!resp.ok) {
logError(`WhatsApp send failed: ${resp.status}`);
} else {
log('WhatsApp notification sent.');
}
} catch (e) {
logError(`WhatsApp send error: ${e.message}`);
}
}
async function main() {
log('WAN IP Guard check starting.');
if (!CONFIG.daLoginKey || !CONFIG.daUsername || !CONFIG.daDomain) {
throw new Error('Missing required environment variables. Check .env file.');
}
const currentIp = await getWanIp();
const savedIp = readState();
if (savedIp === currentIp) {
log(`IP unchanged (${currentIp}). Nothing to do.`);
return;
}
log(`IP changed: ${savedIp || '(none)'} -> ${currentIp}`);
let dnsRecords = {};
try {
dnsRecords = await getDnsRecords();
log(`Current DNS A records: ${JSON.stringify(dnsRecords)}`);
} catch (e) {
logError(`Could not query DNS records: ${e.message}`);
}
const results = [];
for (const record of CONFIG.dnsRecords) {
const name = record === CONFIG.daDomain ? '' : record;
const label = name || '(apex)';
const oldIps = dnsRecords[name] || dnsRecords[record] || [];
try {
for (const oldIp of oldIps) {
await deleteDnsRecord(name, oldIp);
}
await addDnsRecord(name, currentIp);
results.push(`✅ ${label}`);
log(`Updated ${label}: ${oldIps.join(', ') || '(none)'} -> ${currentIp}`);
} catch (e) {
results.push(`❌ ${label}: ${e.message}`);
logError(`Failed to update ${label}: ${e.message}`);
}
}
const successCount = results.filter(r => r.startsWith('✅')).length;
const failCount = results.filter(r => r.startsWith('❌')).length;
const message = [
'🔄 *WAN IP Gewijzigd*',
'',
`*Oud:* ${savedIp || '(onbekend)'}`,
`*Nieuw:* ${currentIp}`,
'',
'*DNS Updates:*',
...results,
'',
`${successCount} gelukt, ${failCount} mislukt`,
].join('\n');
await sendWhatsApp(message);
if (failCount === 0) {
writeState(currentIp);
log(`State saved: ${currentIp}`);
} else {
log(`State NOT saved: ${failCount} DNS update(s) failed. Will retry on next run.`);
}
log('Done.');
}
main().catch(e => {
logError(`Fatal: ${e.message}`);
process.exit(1);
});