285 lines
7.1 KiB
JavaScript
285 lines
7.1 KiB
JavaScript
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
|