import express, { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; import 'express-async-errors'; import path from 'path'; import { DNSCache } from './dns/cache'; import { DNSRecordType } from './dns/records'; import { createZoneFile } from './dns/writer'; import { fromRequest } from './ip/from-request'; import { CachedZone, DNSRecord } from './models/interfaces'; const port = parseInt(process.env.PORT || '9129', 10); const dir = process.env.ZONEFILES || '.'; const app = express(); const api = express.Router(); app.use(express.json()); const cache = new DNSCache(); const weHave = ['lunasqu.ee']; async function getOrLoad(domain: string): Promise { if (!cache.has(domain)) { if (!weHave.includes(domain)) { throw new Error('Invalid domain.'); } return cache.load(domain, path.resolve(dir, `${domain}.zone`)); } const get = await cache.get(domain); if (!get) { throw new Error('Misconfigured domain zone file.'); } return get; } api.get('/records/:domain/download', async (req, res) => { const domain = req.params.domain; const cached = await getOrLoad(domain); res.send(createZoneFile(cached.zone).join('\n')); }); api.get('/records/:domain', async (req, res) => { const domain = req.params.domain; const cached = await getOrLoad(domain); const type = req.query.type; const name = req.query.name; const value = req.query.value; if (type || name || value) { const results = cached.zone.records.filter((zone) => { if (type && zone.type !== type) { return false; } if (name && zone.name !== name) { return false; } if (value && !zone.value.includes(value as string)) { return false; } return true; }).map((record) => { const inx = cached.zone.records.indexOf(record); return { ...record, index: inx } }); res.json({ records: results, }); return; } res.json(cached.zone); }); api.post('/records/:domain', async (req, res) => { const domain = req.params.domain; const index = parseInt(req.body.index, 10); const setters = req.body.record; const cached = await getOrLoad(domain); const { zone } = cached; if (index == null || isNaN(index) || !zone.records[index]) { throw new Error('Invalid record index.'); } const keys = Object.keys(setters); const record = zone.records[index]; if (!setters || keys.length === 0) { res.json({ success: true, message: 'Nothing was changed.', record }); return; } if (setters.type) { const upperType = setters.type.toUpperCase(); if (upperType === 'SOA' && record.type !== DNSRecordType.SOA) { throw new Error('Cannot change type to Start Of Authority.'); } if (!DNSRecordType[upperType] && upperType !== '*') { throw new Error('Unsupported record type.'); } } keys.forEach((key) => { if (record[key]) { record[key] = setters[key]; } }); await cache.update(domain, cached); res.json({ success: true, message: 'Record changed successfully.', record }); }); api.delete('/records/:domain', async (req, res) => { const domain = req.params.domain; const index = parseInt(req.body.index, 10); const cached = await getOrLoad(domain); const { zone } = cached; if (!index || isNaN(index) || !zone.records[index]) { throw new Error('Invalid record index.'); } const record = zone.records[index]; if (record.type === DNSRecordType.SOA) { throw new Error('Cannot delete the Start Of Authority record.'); } zone.records.splice(index, 1); await cache.update(domain, cached); res.json({ success: true, message: 'Record deleted successfully.', record }); }); api.put('/records/:domain', async (req, res) => { const domain = req.params.domain; const setter = req.body.record; if (!setter) { throw new Error('New record is missing!'); } const missing = ['name', 'type', 'value'].reduce( (list, entry) => (setter[entry] == null ? [...list, entry] : list) , []); if (missing.length) { throw new Error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`); } const { name, type, value } = setter; const upperType = type.toUpperCase(); if (upperType === 'SOA') { throw new Error('Cannot add another Start Of Authority record. Please use POST method to modify the existing record.'); } if (!DNSRecordType[upperType] && upperType !== '*') { throw new Error('Unsupported record type.'); } const cached = await getOrLoad(domain); const { zone } = cached; const newRecord = { name, type: upperType, value }; zone.records.push(newRecord); await cache.update(domain, cached); res.status(201).json({ success: true, message: 'Record added.', record: newRecord }); }); api.post('/dyndns/:domain', async (req, res) => { const domain = req.params.domain; const subdomain = req.body.subdomain || '@'; const waitPartial = req.body.dualRequest === true; const { v4, v6 } = fromRequest(req); if (!v4 && !v6) { res.json({ success: true, message: 'Nothing to do.' }); } const cached = await getOrLoad(domain); const { zone } = cached; const actions: string[] = []; if (v4) { const findFirstA = zone.records.find((record) => record.type === DNSRecordType.A && record.name === subdomain ); if (!findFirstA) { zone.records.push({ name: subdomain, type: DNSRecordType.A, value: v4 }); actions.push(`created A record ${v4}`); } else { if (findFirstA.value !== v4) { findFirstA.value = v4; actions.push(`updated A record with ${v4}`); } } } if (v6) { const findFirstAAAA = zone.records.find((record) => record.type === DNSRecordType.AAAA && record.name === subdomain ); if (!findFirstAAAA) { zone.records.push({ name: subdomain, type: DNSRecordType.AAAA, value: v6 }); actions.push(`created AAAA record ${v6}`); } else { if (findFirstAAAA.value !== v6) { findFirstAAAA.value = v6; actions.push(`updated AAAA record with ${v6}`); } } } if (!actions.length) { res.json({ success: true, message: 'Up to date.', actions }); return; } if (waitPartial && ((v4 && !v6) || (!v4 && v6))) { res.status(202).json({ success: true, message: 'Waiting for next request..', actions }); return; } await cache.update(domain, cached); res.json({ success: true, message: 'Successfully updated zone file.', actions }); }); const errorHandler: ErrorRequestHandler = (err: any, req: Request, res: Response, next: NextFunction) => { res.status(400).json({ success: false, message: err.message }); } api.use(errorHandler); app.use('/api/v1', api); app.listen(port, () => console.log(`listening on ${port}`));