#!/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); });