const express = require('express') const util = require('util') const exec = util.promisify(require('child_process').exec) const path = require('path') const fs = require('fs') const fsp = fs.promises const strs = [ '%sdmn IN A %ip', '%sdmn IN AAAA %ip' ] const router = express.Router() const cfgLoader = require(path.join('..', '..', 'config-loader'))(path.join(__dirname, 'config.json'), { named: '/var/named/', tokens: {} }) let memcache = {} function fmtip (i, sd, ip) { return strs[i].replace('%sdmn', sd).replace('%ip', ip) } async function updateSerial (zonefile, contents) { let returnContents = contents != null if (!contents) { contents = await fsp.readFile(zonefile, {encoding: 'utf8'}) } // Split the file into lines let lines = contents.split('\n') // Find 'serial' let prv = false let found = false for (let i in lines) { if (found) break let line = lines[i] if (line.indexOf('SOA') !== -1) { prv = true continue } if (prv && line.toLowerCase().indexOf('serial') !== -1) { // Ladies and gentlemen, we got 'em found = true line = line.replace(/\d+/, Math.floor(Date.now() / 1000)) lines[i] = line break } if (prv) { if (line.indexOf(',') === -1 && line.indexOf('(') === -1 && line.indexOf(')') === -1 && line.indexOf('IN') === -1) { found = true line = line.replace(/\d+/, Math.floor(Date.now() / 1000)) lines[i] = line break } else { // Just give up, not in this script's readability scope.. break } } prv = false } contents = lines.join('\n') if (!found) return contents if (returnContents) return contents await fsp.writeFile(zonefile, contents) } async function updateZone (cfg, v4, v6) { let zone = cfg.domain + '.zone' let zfile = path.join(cfg.root, zone) await fsp.access(zfile, fs.constants.F_OK | fs.constants.W_OK) if (!memcache[cfg.token]) { memcache[cfg.token] = {} } else if (memcache[cfg.token]['v4'] === v4 && memcache[cfg.token]['v6'] === v6) { // Don't update when it's identical return false } memcache[cfg.token].v6 = v6 memcache[cfg.token].v4 = v4 // If subdomain exists, use that file instead and update serial on primary if (cfg.subdomain && cfg.subdomain !== '@') { let zone2 = cfg.subdomain + '.' + zone let zfile2 = path.join(cfg.root, zone2) await fsp.access(zfile2, fs.constants.F_OK | fs.constants.W_OK) let file = '; GENERATED BY DYNDNS' if (v4) { file += '\n' + fmtip(0, cfg.subdomain, v4) } if (v6) { file += '\n' + fmtip(1, cfg.subdomain, v6) } await fsp.writeFile(zfile2, file) await updateSerial(zfile) await exec('rndc reload ' + cfg.domain) return true } // Subdomain not set, just update '@' target in main zone file let zoneFile = await fsp.readFile(zfile, {encoding: 'utf8'}) let atlines = [] let lines = zoneFile.split('\n') for (let i in lines) { let line = lines[i] if (line.indexOf('@') === 0 && line.indexOf('SOA') === -1 && line.indexOf('A') !== -1) { atlines.push(i) } } if (atlines.length === 0) throw new Error('I dont know what to do with this zone file.') let set6 = false let set4 = false for (let j in atlines) { let line = lines[atlines[j]] if (line.indexOf('AAAA') !== -1) { if (set6 || v6 === null) { lines.splice(atlines[j], 1) continue } if (v6) { lines[atlines[j]] = fmtip(1, '@', v6) set6 = true } else if (v4 && atlines.length !== 1) { lines[atlines[j]] = fmtip(0, '@', v4) set4 = true } if (v4 && atlines.length === 1) { lines.splice(atlines[j], 0, fmtip(0, '@', v4)) set4 = true } } else { if (set4 || v4 === null) { lines.splice(atlines[j], 1) continue } if (v4) { lines[atlines[j]] = fmtip(0, '@', v4) set4 = true } else if (v6 && atlines.length !== 1) { lines[atlines[j]] = fmtip(1, '@', v6) set6 = true } if (v6 && atlines.length === 1) { lines.splice(atlines[j], 0, fmtip(1, '@', v6)) set6 = true } } } zoneFile = await updateSerial(null, lines.join('\n')) if (!zoneFile) throw new Error('I dont know what to do with this zone file.') await fsp.writeFile(zfile, zoneFile) await exec('rndc reload ' + cfg.domain) } function validv4 (ipaddress) { if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) { return true } return false } function validv6 (value) { // See https://blogs.msdn.microsoft.com/oldnewthing/20060522-08/?p=31113 and // https://4sysops.com/archives/ipv6-tutorial-part-4-ipv6-address-syntax/ const components = value.split(':') if (components.length < 2 || components.length > 8) { return false } if (components[0] !== '' || components[1] !== '') { // Address does not begin with a zero compression ("::") if (!components[0].match(/^[\da-f]{1,4}/i)) { // Component must contain 1-4 hex characters return false } } let numberOfZeroCompressions = 0 for (let i = 1; i < components.length; ++i) { if (components[i] === '') { // We're inside a zero compression ("::") ++numberOfZeroCompressions if (numberOfZeroCompressions > 1) { // Zero compression can only occur once in an address return false } continue } if (!components[i].match(/^[\da-f]{1,4}/i)) { // Component must contain 1-4 hex characters return false } } return true } async function init () { let config = await cfgLoader await fsp.access(config.named, fs.constants.F_OK) router.post('/', async (req, res, next) => { let token = req.header('token') || req.body.token // Check token if (!token || !config.tokens[token]) return res.status(402).send('Forbidden') let v4 = null let qv4 = req.query.ipv4 || req.body.ipv4 let v6 = null let qv6 = req.query.ipv6 || req.body.ipv6 // Lets begin our trials // Determine Address from request headers if (req.header('x-forwarded-for')) { v4 = req.header('x-forwarded-for') } else { v4 = req.connection.remoteAddress } if (!validv4(v4)) { v6 = v4 v4 = null } // IPv4 if (qv4 && validv4(qv4)) { v4 = qv4 } if (qv4 === 'ignore') { v4 = null } // IPv6 if (qv6 && validv6(qv6)) { v6 = qv6 } if (qv6 === 'ignore') { v6 = null } if (v4 === null && v6 === null) { res.send('Nothing to do') } // Remove subnet masks if (v6) v6 = v6.replace(/\/(\d+)$/, '') if (v4) v4 = v4.replace(/\/(\d+)$/, '') try { await updateZone(Object.assign({ token: token, root: config.named, }, config.tokens[token]), v4, v6) } catch (e) { console.error(e.stack) return res.status(500).send('Internal Server Error') } res.status(204).end() }) return router } module.exports = init