2021-05-15 09:22:46 +00:00
|
|
|
import express, { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response } from 'express';
|
2021-05-21 13:41:06 +00:00
|
|
|
import cors from 'cors';
|
2021-05-15 08:45:12 +00:00
|
|
|
import 'express-async-errors';
|
|
|
|
import path from 'path';
|
2021-07-08 10:57:20 +00:00
|
|
|
import fs from 'fs/promises';
|
2021-05-15 08:45:12 +00:00
|
|
|
import { DNSCache } from './dns/cache';
|
|
|
|
import { DNSRecordType } from './dns/records';
|
2021-05-15 16:29:38 +00:00
|
|
|
import { ReloadExecutor } from './dns/rndc';
|
|
|
|
import { ValidatorExecutor } from './dns/validator';
|
2021-05-15 08:45:12 +00:00
|
|
|
import { createZoneFile } from './dns/writer';
|
|
|
|
import { fromRequest } from './ip/from-request';
|
2021-05-15 16:29:38 +00:00
|
|
|
import { Keys } from './keys';
|
2021-05-15 09:22:46 +00:00
|
|
|
import { CachedZone } from './models/interfaces';
|
2021-07-08 10:57:20 +00:00
|
|
|
import { logger } from './log/Logger';
|
2021-05-15 08:45:12 +00:00
|
|
|
|
|
|
|
const port = parseInt(process.env.PORT || '9129', 10);
|
2021-05-15 16:29:38 +00:00
|
|
|
const cacheTTL = parseInt(process.env.CACHE_TTL || '2629746', 10);
|
2021-05-15 08:45:12 +00:00
|
|
|
const dir = process.env.ZONEFILES || '.';
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
const api = express.Router();
|
|
|
|
|
|
|
|
app.use(express.json());
|
2021-05-15 17:33:29 +00:00
|
|
|
app.enable('trust proxy');
|
2021-05-15 08:45:12 +00:00
|
|
|
|
2021-05-21 13:41:06 +00:00
|
|
|
app.use(cors({
|
|
|
|
origin: '*',
|
|
|
|
credentials: true
|
|
|
|
}));
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
const keys = new Keys();
|
|
|
|
const rndc = ReloadExecutor.fromEnvironment();
|
|
|
|
const validator = new ValidatorExecutor();
|
|
|
|
const cache = new DNSCache(rndc, validator, cacheTTL);
|
2021-05-15 08:45:12 +00:00
|
|
|
|
|
|
|
async function getOrLoad(domain: string): Promise<CachedZone> {
|
|
|
|
if (!cache.has(domain)) {
|
2021-05-15 16:29:38 +00:00
|
|
|
if (!keys.getKey(domain)) {
|
2021-05-15 08:45:12 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-05-15 09:22:46 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
if (!parts[1] || !keys.getDomain(parts[1])) {
|
2021-05-15 09:22:46 +00:00
|
|
|
res.status(401).json({ success: false, message: 'Unauthorized' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
res.locals.token = parts[1];
|
|
|
|
next();
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
2021-05-15 09:22:46 +00:00
|
|
|
const domainAuthorization: RequestHandler = (req, res, next) => {
|
|
|
|
if (!req.params.domain || !res.locals.token) {
|
|
|
|
next(new Error('Unexpected bad request'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
if (keys.getDomain(res.locals.token) !== req.params.domain) {
|
2021-05-15 09:22:46 +00:00
|
|
|
res.status(401).json({ success: false, message: 'Unauthorized access to domain' });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Get zone records
|
|
|
|
*/
|
2021-05-15 09:22:46 +00:00
|
|
|
api.get('/zone/records/:domain', domainAuthorization, async (req, res) => {
|
2021-05-15 08:45:12 +00:00
|
|
|
const domain = req.params.domain;
|
|
|
|
const cached = await getOrLoad(domain);
|
|
|
|
|
2021-05-15 17:33:29 +00:00
|
|
|
const type = req.query.type as DNSRecordType;
|
|
|
|
const name = req.query.name as string;
|
|
|
|
const value = req.query.value as string;
|
2021-05-15 08:45:12 +00:00
|
|
|
|
|
|
|
if (type || name || value) {
|
|
|
|
res.json({
|
2021-05-15 17:33:29 +00:00
|
|
|
records: cache.search(cached, name, type, value),
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-05-15 09:22:46 +00:00
|
|
|
res.json(cached.zone.records);
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
2021-05-16 14:21:49 +00:00
|
|
|
* Update records by their index in the zone file
|
2021-05-15 16:29:38 +00:00
|
|
|
* record: {
|
2021-05-16 14:21:49 +00:00
|
|
|
* index: number;
|
2021-05-15 16:29:38 +00:00
|
|
|
* name?: string;
|
|
|
|
* type?: DNSRecordType;
|
|
|
|
* value?: string;
|
2021-05-16 16:18:07 +00:00
|
|
|
* setIndex?: number;
|
2021-05-16 14:40:48 +00:00
|
|
|
* forDeletion?: boolean;
|
2021-05-16 14:21:49 +00:00
|
|
|
* }[];
|
2021-05-15 16:29:38 +00:00
|
|
|
*/
|
2021-05-21 13:41:06 +00:00
|
|
|
api.patch('/zone/records/:domain', domainAuthorization, async (req, res) => {
|
2021-05-15 08:45:12 +00:00
|
|
|
const domain = req.params.domain;
|
2021-05-16 14:21:49 +00:00
|
|
|
let setters = req.body.record;
|
2021-05-15 08:45:12 +00:00
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
if (!setters) {
|
|
|
|
res.json({ success: true, message: 'Nothing was changed.' });
|
|
|
|
return;
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
if (!Array.isArray(setters)) {
|
|
|
|
setters = [setters];
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
const cached = await getOrLoad(domain);
|
|
|
|
const { zone } = cached;
|
|
|
|
|
|
|
|
const changed = [];
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
|
for (const setter of setters) {
|
|
|
|
const index = parseInt(setter.index, 10);
|
|
|
|
if (index == null || isNaN(index) || !zone.records[index]) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Invalid record index.'
|
|
|
|
});
|
|
|
|
continue;
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
const keys = Object.keys(setter);
|
|
|
|
const record = { ...zone.records[index] };
|
|
|
|
if (!setter || keys.length === 0) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Nothing was changed.',
|
|
|
|
record,
|
|
|
|
})
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (setter.type) {
|
|
|
|
const upperType = setter.type.toUpperCase();
|
|
|
|
if (upperType === 'SOA' && record.type !== DNSRecordType.SOA) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Cannot change type to Start Of Authority.',
|
|
|
|
record
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!DNSRecordType[<keyof typeof DNSRecordType>upperType] && upperType !== '*') {
|
|
|
|
errors.push({
|
|
|
|
message: 'Unsupported record type.',
|
|
|
|
record
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 14:40:48 +00:00
|
|
|
if (record.type === DNSRecordType.SOA && setter.forDeletion) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Cannot delete the Start Of Authority record.',
|
|
|
|
record
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
keys.forEach((key) => {
|
2021-05-16 14:40:48 +00:00
|
|
|
if (key === 'forDeletion') {
|
|
|
|
record.forDeletion = true;
|
|
|
|
} else if (record[key]) {
|
2021-05-16 14:21:49 +00:00
|
|
|
record[key] = setter[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!validator.validateRecord(record)) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Validation error: Invalid characters',
|
|
|
|
record
|
|
|
|
});
|
|
|
|
continue;
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 16:18:07 +00:00
|
|
|
if (setter.setIndex) {
|
|
|
|
const afterI = parseInt(setter.setIndex, 10);
|
|
|
|
if (isNaN(afterI) || afterI > zone.records.length) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Invalid insertion location!',
|
|
|
|
record
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
zone.records[index] = { ...record, forDeletion: true };
|
|
|
|
zone.records.splice(afterI, 0, record);
|
|
|
|
changed.push({ ...record, index: afterI });
|
|
|
|
} else {
|
|
|
|
zone.records[index] = record;
|
|
|
|
changed.push(record);
|
|
|
|
}
|
2021-05-15 16:29:38 +00:00
|
|
|
}
|
|
|
|
|
2021-05-15 08:45:12 +00:00
|
|
|
await cache.update(domain, cached);
|
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
if (!changed.length && errors.length) {
|
|
|
|
res.status(400).json({ success: false, message: 'Updating record(s) failed.', changed, errors });
|
|
|
|
} else if (changed.length) {
|
|
|
|
res.json({ success: true, message: 'Record(s) changed successfully.', changed, errors });
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('zone %s changed records from %s', domain, req.ip);
|
|
|
|
logger.debug(changed);
|
2021-05-16 14:21:49 +00:00
|
|
|
} else {
|
|
|
|
res.json({ success: true, message: 'Nothing was changed.', changed, errors });
|
|
|
|
}
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Delete a record from zone by its index in the zone file
|
|
|
|
* index: number;
|
|
|
|
*/
|
2021-05-15 09:22:46 +00:00
|
|
|
api.delete('/zone/records/:domain', domainAuthorization, async (req, res) => {
|
2021-05-15 08:45:12 +00:00
|
|
|
const domain = req.params.domain;
|
2021-05-16 14:40:48 +00:00
|
|
|
let indexes = req.body.index;
|
2021-05-15 08:45:12 +00:00
|
|
|
|
|
|
|
const cached = await getOrLoad(domain);
|
|
|
|
const { zone } = cached;
|
2021-05-16 14:40:48 +00:00
|
|
|
|
|
|
|
if (!Array.isArray(indexes)) {
|
|
|
|
indexes = [indexes];
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
2021-05-16 14:40:48 +00:00
|
|
|
const deleted = [];
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
|
for (const number of indexes) {
|
|
|
|
const index = parseInt(number, 10);
|
|
|
|
if (index == null || isNaN(index) || !zone.records[index]) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Invalid record index.',
|
|
|
|
record: { index }
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const record = zone.records[index];
|
|
|
|
if (record.type === DNSRecordType.SOA) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Cannot delete the Start Of Authority record.',
|
|
|
|
record
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
zone.records[index].forDeletion = true;
|
|
|
|
deleted.push(record);
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await cache.update(domain, cached);
|
|
|
|
|
2021-05-16 14:40:48 +00:00
|
|
|
if (!deleted.length && errors.length) {
|
|
|
|
res.status(400).json({ success: false, message: 'Deleting record(s) failed.', deleted, errors });
|
|
|
|
} else if (deleted.length) {
|
|
|
|
res.json({ success: true, message: 'Record(s) deleted successfully.', deleted, errors });
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('zone %s deleted records from %s', domain, req.ip);
|
|
|
|
logger.debug(deleted);
|
2021-05-16 14:40:48 +00:00
|
|
|
} else {
|
|
|
|
res.json({ success: true, message: 'Nothing was deleted.', deleted, errors });
|
|
|
|
}
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Create a new record in zone
|
|
|
|
* record: {
|
|
|
|
* name: string;
|
|
|
|
* type: DNSRecordType;
|
|
|
|
* value: string;
|
2021-05-16 16:18:07 +00:00
|
|
|
* index?: number;
|
2021-05-16 14:21:49 +00:00
|
|
|
* }[];
|
2021-05-15 16:29:38 +00:00
|
|
|
*/
|
2021-05-21 13:41:06 +00:00
|
|
|
api.post('/zone/records/:domain', domainAuthorization, async (req, res) => {
|
2021-05-15 08:45:12 +00:00
|
|
|
const domain = req.params.domain;
|
2021-05-16 14:21:49 +00:00
|
|
|
let setters = req.body.record;
|
2021-05-15 08:45:12 +00:00
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
if (!setters) {
|
2021-05-15 08:45:12 +00:00
|
|
|
throw new Error('New record is missing!');
|
|
|
|
}
|
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
if (!Array.isArray(setters)) {
|
|
|
|
setters = [setters];
|
2021-05-15 08:45:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const cached = await getOrLoad(domain);
|
|
|
|
const { zone } = cached;
|
2021-05-15 09:22:46 +00:00
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
const created = [];
|
|
|
|
const errors = [];
|
2021-05-15 16:29:38 +00:00
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
for (const setter of setters) {
|
|
|
|
const missing = ['name', 'type', 'value'].reduce<string[]>(
|
|
|
|
(list, entry) => (setter[entry] == null ? [...list, entry] : list)
|
|
|
|
, []);
|
|
|
|
|
|
|
|
if (missing.length) {
|
|
|
|
errors.push({
|
|
|
|
message: `${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`,
|
|
|
|
record: setter
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { name, type, value } = setter;
|
|
|
|
const upperType = type.toUpperCase();
|
|
|
|
if (upperType === 'SOA') {
|
|
|
|
errors.push({
|
|
|
|
message: 'Cannot add another Start Of Authority record. Please use POST method to modify the existing record.',
|
|
|
|
record: setter
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!DNSRecordType[<keyof typeof DNSRecordType>upperType] && upperType !== '*') {
|
|
|
|
errors.push({
|
|
|
|
message: 'Unsupported record type.',
|
|
|
|
record: setter
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newRecord = { name, type: upperType, value };
|
|
|
|
|
|
|
|
if (!validator.validateRecord(newRecord)) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Validation error: Invalid characters',
|
2021-05-16 16:18:07 +00:00
|
|
|
record: newRecord
|
2021-05-16 14:21:49 +00:00
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
2021-05-15 17:33:29 +00:00
|
|
|
|
2021-05-16 14:21:49 +00:00
|
|
|
if (cache.search(cached, name, upperType, value, true).length) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Exact same record already exists. No need to duplicate records!',
|
2021-05-16 16:18:07 +00:00
|
|
|
record: newRecord
|
2021-05-16 14:21:49 +00:00
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-16 16:18:07 +00:00
|
|
|
if (setter.index) {
|
|
|
|
const afterI = parseInt(setter.index, 10);
|
|
|
|
if (isNaN(afterI) || afterI > zone.records.length) {
|
|
|
|
errors.push({
|
|
|
|
message: 'Invalid insertion location!',
|
|
|
|
record: newRecord
|
|
|
|
});
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
zone.records.splice(afterI, 0, newRecord);
|
|
|
|
created.push({ ...newRecord, index: afterI });
|
|
|
|
} else {
|
|
|
|
const index = zone.records.push(newRecord) - 1;
|
|
|
|
created.push({ ...newRecord, index });
|
|
|
|
}
|
2021-05-16 14:21:49 +00:00
|
|
|
}
|
2021-05-15 08:45:12 +00:00
|
|
|
|
|
|
|
await cache.update(domain, cached);
|
2021-05-16 14:21:49 +00:00
|
|
|
|
|
|
|
if (!created.length && errors.length) {
|
|
|
|
res.status(400).json({ success: false, message: 'Creating record(s) failed.', created, errors });
|
|
|
|
} else if (created.length) {
|
|
|
|
res.status(201).json({ success: true, message: 'Record(s) created successfully.', created, errors });
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('zone %s created records from %s', domain, req.ip);
|
|
|
|
logger.debug(created);
|
2021-05-16 14:21:49 +00:00
|
|
|
} else {
|
|
|
|
res.json({ success: true, message: 'Nothing was created.', created, errors });
|
|
|
|
}
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Get full zone as file
|
|
|
|
*/
|
2021-05-15 09:22:46 +00:00
|
|
|
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'));
|
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Get full zone
|
|
|
|
*/
|
2021-05-15 09:22:46 +00:00
|
|
|
api.get('/zone/:domain', domainAuthorization, async (req, res) => {
|
|
|
|
const domain = req.params.domain;
|
|
|
|
const cached = await getOrLoad(domain);
|
|
|
|
res.json(cached.zone);
|
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Reload zone or change TTL
|
|
|
|
* ttl?: number
|
|
|
|
*/
|
2021-05-15 09:22:46 +00:00
|
|
|
api.post('/zone/:domain', domainAuthorization, async (req, res) => {
|
|
|
|
const domain = req.params.domain;
|
|
|
|
const cached = await getOrLoad(domain);
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
if (req.body.ttl) {
|
|
|
|
const numTTL = parseInt(req.body.TTL, 10);
|
|
|
|
if (isNaN(numTTL)) {
|
|
|
|
throw new Error('Invalid number for TTL');
|
|
|
|
}
|
2021-05-15 09:22:46 +00:00
|
|
|
cached.zone.ttl = numTTL;
|
|
|
|
}
|
|
|
|
|
|
|
|
await cache.update(domain, cached);
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
if (req.body.ttl) {
|
|
|
|
res.json({ success: true, message: 'TTL changed successfully.', ttl: cached.zone.ttl });
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('zone %s set ttl: %d from %s', domain, cached.zone.ttl, req.ip);
|
2021-05-15 16:29:38 +00:00
|
|
|
} else {
|
|
|
|
res.json({ success: true, message: 'Zone reloaded successfully.' });
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('zone %s reload from %s', domain, req.ip);
|
2021-05-15 16:29:38 +00:00
|
|
|
}
|
2021-05-15 09:22:46 +00:00
|
|
|
});
|
|
|
|
|
2021-05-15 16:29:38 +00:00
|
|
|
/**
|
|
|
|
* Quickly set IP address for the zone.
|
|
|
|
* Either for root or a subdomain of user choice.
|
|
|
|
* ipv4?: string;
|
|
|
|
* ipv6?: string;
|
|
|
|
* subdomain?: string;
|
|
|
|
* dualRequest?: boolean;
|
|
|
|
*/
|
|
|
|
api.post('/set-ip/:domain', domainAuthorization, async (req, res) => {
|
2021-05-15 08:45:12 +00:00
|
|
|
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
|
|
|
|
});
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('set-ip (partial) from %s: %s', req.ip, actions.join('\n'));
|
2021-05-15 08:45:12 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await cache.update(domain, cached);
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
message: 'Successfully updated zone file.',
|
|
|
|
actions
|
|
|
|
});
|
2021-07-08 10:57:20 +00:00
|
|
|
logger.info('set-ip from %s: %s', req.ip, actions.join('\n'));
|
2021-05-15 08:45:12 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2021-07-08 10:57:20 +00:00
|
|
|
async function load() {
|
|
|
|
await keys.load();
|
|
|
|
|
|
|
|
if (logger.logToFile) {
|
|
|
|
try {
|
|
|
|
await fs.stat(logger.logDir);
|
|
|
|
} catch {
|
|
|
|
await fs.mkdir(logger.logDir);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
app.listen(port, () => logger.info(`listening on ${port}`));
|
|
|
|
}
|
|
|
|
|
|
|
|
load().catch((e) => console.error(e.stack));
|