serve-lunasqu.ee/applications/dyndns/index.js

283 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('http_x_forwarded_for')) {
v4 = req.header('http_x_forwarded_for')
} else if (req.header('remote_addr')) {
v4 = req.header('remote_addr')
} 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')
}
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