tr-ddns
Version:
Simple DDNS Server
260 lines (243 loc) • 7.44 kB
JavaScript
'use strict'
const EventEmitter = require('node:events');
const ipaddr = require('ipaddr.js');
const ApiSrv = require('tr-apisrv');
const packageJson = require('./package.json');
const nullish = require('./nullish');
const log = require('./log');
class ApiServer extends EventEmitter {
#server;
#debug;
#db;
#stat;
constructor(config) {
super();
this.#debug = config?.debug ? true : false;
this.#stat = { apiCalls: 0 };
let port = config?.port ?? 80;
let address = config?.address ?? '0.0.0.0';
if (! ipaddr.isValid(address)) {
throw new Error('Invalid TCP listen address');
}
if (! (Number.isSafeInteger(port) && (port >= 0x0001) && (port <= 0xffff))) {
throw new Error('Invalid TCP listen port');
}
if (! (typeof(config?.nameDB?.get) === 'function')) {
throw new Error('Invalid NameDB');
}
this.#db = config.nameDB;
this.#server = new ApiSrv({ port: port,
address: address,
callback: async function(r) { return this.#requestCb(r); }.bind(this),
authCallback: async function(r) { return this.#authCb(r); }.bind(this),
prettyPrintJsonResponses: this.#debug,
bodyReadTimeoutMs: 5000,
debug: this.#debug });
if (this.#debug) {
log('ApiServer: TCP:', address + ':' + port.toString());
}
}
async #authCb(r) {
return true;
}
async #requestCb(r) {
this.#stat.apiCalls++;
var res = r.res;
delete r.res;
switch (r.url) {
case '/':
r.jsonResponse({ status: 'ok', code: 200, data: { package: packageJson?.name ?? '?', version: packageJson?.version ?? '?' } }, 200);
return;
case '/domain':
return this.#domain(r);
case '/host':
return this.#host(r);
case '/dump':
return this.#dump(r);
case '/stats':
return this.#stats(r);
case '/flush':
return this.#flush(r);
}
if (this.#debug) {
log('API:', r.url, '(not found)');
}
r.jsonResponse({ status: 'error', code: 404, message: 'Not found' }, 404);
}
async #domain(r) {
try {
if (! this.#db.validDomain(r.params.domain)) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (domain)' }, 400);
return;
}
if ([ undefined, false, 'false' ].includes(r.params.remove)) {
try {
this.#db.addDomain(r.params.domain);
r.jsonResponse({ status: 'ok', code: 200 }, 200);
} catch (e) {
if (this.#debug) {
console.error(e);
}
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (unable to add domain)' }, 400);
return;
}
} else if ([ true, 'true' ].includes(r.params.remove)) {
try {
this.#db.removeDomain(r.params.domain);
r.jsonResponse({ status: 'ok', code: 200 }, 200);
} catch (e) {
if (this.#debug) {
console.error(e);
}
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (unable to remove domain)' }, 400);
return;
}
} else {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (remove)' }, 400);
return;
}
return;
} catch (e) {
if (this.#debug) {
console.error(e);
}
r.jsonResponse({ status: 'error', code: 500, message: 'Internal error' }, 500);
}
}
async #host(r) {
try {
if (! this.#db.valid(r.params.host)) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (host)' }, 400);
return;
}
let a, aaaa, txt, mx, ttlMs, remove, merge;
if (nullish(r.params.a)) {
a = undefined;
} else {
if (! (ipaddr.IPv4.isValid(r.params.a) || (r.params.a === ''))) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (a)' }, 400);
return;
}
a = r.params.a;
}
if (nullish(r.params.aaaa)) {
aaaa = undefined;
} else {
if (! (ipaddr.IPv6.isValid(r.params.aaaa) || (r.params.aaaa === ''))) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (aaaa)' }, 400);
return;
}
aaaa = r.params.aaaa;
}
if (nullish(r.params.txt)) {
txt = undefined;
} else {
if (! (typeof(r.params.txt) === 'string')) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (txt)' }, 400);
return;
}
txt = r.params.txt;
}
if (nullish(r.params.mx)) {
mx = undefined;
} else {
if (! (this.#db.valid(r.params.mx) || (r.params.mx === ''))) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (mx)' }, 400);
return;
}
mx = r.params.mx;
}
if (nullish(r.params.ttl)) {
ttlMs = undefined;
} else {
if (/^[1-9]\d{0,10}$/.test(r.params.ttl)) {
ttlMs = Number.parseInt(r.params.ttl) * 1000;
} else {
ttlMs = r.params.ttl * 1000;
}
if (! (Number.isSafeInteger(ttlMs) && (ttlMs >= 1) && (ttlMs <= 2147483647))) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (ttl)' }, 400);
return;
}
}
if (nullish(r.params.remove)) {
remove = ((typeof(a) === 'string') || (typeof(aaaa) === 'string') || (typeof(txt) === 'string') || (typeof(mx) === 'string')) ? false : true;
} else if ([ false, 'false' ].includes(r.params.remove)) {
remove = false;
} else if ([ true, 'true' ].includes(r.params.remove)) {
remove = true;
} else {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (remove)' }, 400);
return;
}
if ([ undefined, null, false, 'false' ].includes(r.params.merge)) {
merge = false;
} else if ([ true, 'true' ].includes(r.params.merge)) {
merge = true;
} else {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (merge)' }, 400);
return;
}
if (remove) {
if (a || aaaa || (typeof(txt) === 'string') || ttlMs) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (conflicting params)' }, 400);
return;
}
try {
let rv = this.#db.remove(r.params.host);
if (! rv) {
throw new Error('Unable to remove host');
}
r.jsonResponse({ status: 'ok', code: 200 }, 200);
} catch (e) {
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (unable to remove host)' }, 400);
return;
}
} else {
try {
let rv = this.#db.set(r.params.host, { a, aaaa, txt, mx }, ttlMs, merge);
if (! rv) {
throw new Error('Unable to add host');
}
r.jsonResponse({ status: 'ok', code: 200 }, 200);
} catch (e) {
log(e);
r.jsonResponse({ status: 'error', code: 400, message: 'Bad request (unable to add/update host)' }, 400);
return;
}
}
return;
} catch (e) {
console.error(e);
r.jsonResponse({ status: 'error', code: 500, message: 'Internal error' }, 500);
}
}
async #dump(r) {
try {
let d = this.#db.dump();
r.jsonResponse({ status: 'ok', code: 200, data: d }, 200);
} catch (e) {
console.error(e);
r.jsonResponse({ status: 'error', code: 500, message: 'Internal error' }, 500);
}
}
async #stats(r) {
try {
let d = Object.assign({}, this.#db.stats(), this.#stat);
r.jsonResponse({ status: 'ok', code: 200, data: d }, 200);
} catch (e) {
console.error(e);
r.jsonResponse({ status: 'error', code: 500, message: 'Internal error' }, 500);
}
}
async #flush(r) {
try {
this.#db.flush();
r.jsonResponse({ status: 'ok', code: 200 }, 200);
} catch (e) {
console.error(e);
r.jsonResponse({ status: 'error', code: 500, message: 'Internal error' }, 500);
}
}
}
module.exports = ApiServer;