diff --git a/src/dns/reader.ts b/src/dns/reader.ts index 56c1690..32cf8fc 100644 --- a/src/dns/reader.ts +++ b/src/dns/reader.ts @@ -25,7 +25,7 @@ function parseRecordLine(line: string, index: number, lines: string[]): DNSRecor actualLine = clean; } - const split = actualLine.split(' '); + const split = actualLine.replace(/"\s"/g, '').split(' '); if (split[0] === 'IN' && split[1] === 'NS') { return { name: '', diff --git a/src/dns/writer.ts b/src/dns/writer.ts index c9d06bc..d25569e 100644 --- a/src/dns/writer.ts +++ b/src/dns/writer.ts @@ -27,6 +27,7 @@ function createSOAString(record: SOARecord, padI: number, padJ: number): string[ export function createZoneFile(zone: DNSZone, preferredLineLength = 120): string[] { const file: string[] = []; file.push(`$TTL ${zone.ttl}`); + file.push(`; GENERATED BY icy-dyndns`); let longestName = 0; let longestType = 0; @@ -57,6 +58,8 @@ export function createZoneFile(zone: DNSZone, preferredLineLength = 120): string file.push(`$INCLUDE ${include}`); }); + file.push(''); + return file; } diff --git a/src/index.ts b/src/index.ts index 82b15c6..1598b70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import express, { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; +import express, { ErrorRequestHandler, NextFunction, Request, RequestHandler, 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'; +import { CachedZone } from './models/interfaces'; const port = parseInt(process.env.PORT || '9129', 10); const dir = process.env.ZONEFILES || '.'; @@ -16,11 +16,14 @@ const api = express.Router(); app.use(express.json()); const cache = new DNSCache(); -const weHave = ['lunasqu.ee']; +const authentic: {[x: string]: string} = { + 'testing-token-auth-aaaa': 'lunasqu.ee', + 'aaa': 'lol' +}; async function getOrLoad(domain: string): Promise { if (!cache.has(domain)) { - if (!weHave.includes(domain)) { + if (!Object.values(authentic).includes(domain)) { throw new Error('Invalid domain.'); } return cache.load(domain, path.resolve(dir, `${domain}.zone`)); @@ -35,13 +38,43 @@ async function getOrLoad(domain: string): Promise { 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.use((req, res, next) => { + const authHeader = req.get('authorization'); + if (!authHeader) { + res.status(400).json({ success: false, message: 'Missing Authorization header' }); + return; + } + + const parts = authHeader.split(' '); + if (parts[0].toLowerCase() !== 'bearer') { + res.status(400).json({ success: false, message: 'Invalid Authorization header' }); + return; + } + + if (!parts[1] || !authentic[parts[1]]) { + res.status(401).json({ success: false, message: 'Unauthorized' }); + return; + } + + res.locals.token = parts[1]; + next(); }); -api.get('/records/:domain', async (req, res) => { +const domainAuthorization: RequestHandler = (req, res, next) => { + if (!req.params.domain || !res.locals.token) { + next(new Error('Unexpected bad request')); + return; + } + + if (authentic[res.locals.token] !== req.params.domain) { + res.status(401).json({ success: false, message: 'Unauthorized access to domain' }); + return; + } + + next(); +} + +api.get('/zone/records/:domain', domainAuthorization, async (req, res) => { const domain = req.params.domain; const cached = await getOrLoad(domain); @@ -79,10 +112,10 @@ api.get('/records/:domain', async (req, res) => { return; } - res.json(cached.zone); + res.json(cached.zone.records); }); -api.post('/records/:domain', async (req, res) => { +api.post('/zone/records/:domain', domainAuthorization, async (req, res) => { const domain = req.params.domain; const index = parseInt(req.body.index, 10); const setters = req.body.record; @@ -122,7 +155,7 @@ api.post('/records/:domain', async (req, res) => { res.json({ success: true, message: 'Record changed successfully.', record }); }); -api.delete('/records/:domain', async (req, res) => { +api.delete('/zone/records/:domain', domainAuthorization, async (req, res) => { const domain = req.params.domain; const index = parseInt(req.body.index, 10); @@ -143,7 +176,7 @@ api.delete('/records/:domain', async (req, res) => { res.json({ success: true, message: 'Record deleted successfully.', record }); }); -api.put('/records/:domain', async (req, res) => { +api.put('/zone/records/:domain', domainAuthorization, async (req, res) => { const domain = req.params.domain; const setter = req.body.record; @@ -172,13 +205,45 @@ api.put('/records/:domain', async (req, res) => { 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) => { +api.get('/zone/:domain/download', domainAuthorization, async (req, res) => { + const domain = req.params.domain; + const cached = await getOrLoad(domain); + res.send(createZoneFile(cached.zone).join('\n')); +}); + +api.get('/zone/:domain', domainAuthorization, async (req, res) => { + const domain = req.params.domain; + const cached = await getOrLoad(domain); + res.json(cached.zone); +}); + +api.post('/zone/:domain', domainAuthorization, async (req, res) => { + const domain = req.params.domain; + const cached = await getOrLoad(domain); + + if (!req.body.ttl) { + res.json({ success: true, message: 'Nothing was changed.' }); + return; + } + + const numTTL = parseInt(req.body.TTL, 10); + if (!isNaN(numTTL)) { + cached.zone.ttl = numTTL; + } + + await cache.update(domain, cached); + + res.json({ success: true, message: 'TTL changed successfully.', ttl: cached.zone.ttl }); +}); + +api.post('/dyndns/:domain', domainAuthorization, async (req, res) => { const domain = req.params.domain; const subdomain = req.body.subdomain || '@'; const waitPartial = req.body.dualRequest === true;