210 lines
6 KiB
JavaScript
Executable file
210 lines
6 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=${name}&value=${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);
|
|
});
|