split long TXT records
This commit is contained in:
parent
10ac1b0d80
commit
8caefdbf13
@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "icy-dyndns",
|
"name": "icydns",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc -w",
|
||||||
|
"start": "node src/index.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CachedZone, SOARecord } from '../models/interfaces';
|
import { CachedZone, DNSRecord, SOARecord } from '../models/interfaces';
|
||||||
import { readZoneFile } from './reader';
|
import { readZoneFile } from './reader';
|
||||||
import { DNSRecordType } from './records';
|
import { DNSRecordType } from './records';
|
||||||
import { ReloadExecutor } from './rndc';
|
import { ReloadExecutor } from './rndc';
|
||||||
@ -17,6 +17,37 @@ export class DNSCache {
|
|||||||
return this.cached[name] != null;
|
return this.cached[name] != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search(
|
||||||
|
cached: CachedZone,
|
||||||
|
name?: string,
|
||||||
|
type?: DNSRecordType,
|
||||||
|
value?: string,
|
||||||
|
strict = false
|
||||||
|
): DNSRecord[] {
|
||||||
|
return cached.zone.records.filter((zone) => {
|
||||||
|
if (type && zone.type !== type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name && zone.name !== name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && ((!strict && !zone.value.includes(value as string)) ||
|
||||||
|
(strict && zone.value !== value))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).map((record) => {
|
||||||
|
const inx = cached.zone.records.indexOf(record);
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
index: inx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async get(name: string): Promise<CachedZone | null> {
|
async get(name: string): Promise<CachedZone | null> {
|
||||||
const cached = this.cached[name];
|
const cached = this.cached[name];
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import { DNSZone, SOARecord } from '../models/interfaces';
|
import { DNSRecord, DNSZone, SOARecord } from '../models/interfaces';
|
||||||
import { DNSRecordType } from './records';
|
import { DNSRecordType } from './records';
|
||||||
|
|
||||||
|
const maxStrLength = 255;
|
||||||
|
const magicPadding = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits and comments SOA record
|
||||||
|
* @param record
|
||||||
|
* @param padI
|
||||||
|
* @param padJ
|
||||||
|
* @returns new lines
|
||||||
|
*/
|
||||||
function createSOAString(record: SOARecord, padI: number, padJ: number): string[] {
|
function createSOAString(record: SOARecord, padI: number, padJ: number): string[] {
|
||||||
const name = record.name.padEnd(padI, ' ');
|
const name = record.name.padEnd(padI, ' ');
|
||||||
const type = record.type.toString().padEnd(padJ, ' ');
|
const type = record.type.toString().padEnd(padJ, ' ');
|
||||||
const padK = ' '.padStart(
|
const padK = ' '.padStart(padI + magicPadding);
|
||||||
padI + 3
|
|
||||||
);
|
|
||||||
const padL = ['serial', 'refresh', 'retry', 'expire', 'minimum']
|
const padL = ['serial', 'refresh', 'retry', 'expire', 'minimum']
|
||||||
.reduce((previous, current) => {
|
.reduce((previous, current) => {
|
||||||
const len = `${record[current]}`.length;
|
const len = `${record[current]}`.length;
|
||||||
@ -24,10 +32,44 @@ function createSOAString(record: SOARecord, padI: number, padJ: number): string[
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createZoneFile(zone: DNSZone, preferredLineLength = 120): string[] {
|
/**
|
||||||
|
* Splits very long TXT records into multiple lines.
|
||||||
|
* Mandatory for DKIM keys, for example.
|
||||||
|
* @param record
|
||||||
|
* @param padI
|
||||||
|
* @param padJ
|
||||||
|
* @returns new lines
|
||||||
|
*/
|
||||||
|
function splitTXTString(record: DNSRecord, padI: number, padJ: number): string[] {
|
||||||
|
const name = record.name.padEnd(padI, ' ');
|
||||||
|
const type = record.type.toString().padEnd(padJ, ' ');
|
||||||
|
const strLen = maxStrLength - padI - magicPadding;
|
||||||
|
const padK = ' '.padStart(padI + magicPadding);
|
||||||
|
const splitStrings = [];
|
||||||
|
|
||||||
|
if (record.value.length < strLen) {
|
||||||
|
return [`${name} IN ${type} ${record.value}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
let temporary = record.value.replace(/"/g, '');
|
||||||
|
while (temporary.length > strLen) {
|
||||||
|
splitStrings.push(temporary.substr(0, strLen));
|
||||||
|
temporary = temporary.substr(strLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
splitStrings.push(temporary);
|
||||||
|
|
||||||
|
return [
|
||||||
|
`${name} IN ${type} (`,
|
||||||
|
...splitStrings.map((str) => `${padK} "${str}"`),
|
||||||
|
`)`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createZoneFile(zone: DNSZone): string[] {
|
||||||
const file: string[] = [];
|
const file: string[] = [];
|
||||||
file.push(`$TTL ${zone.ttl}`);
|
file.push(`$TTL ${zone.ttl}`);
|
||||||
file.push(`; GENERATED BY icy-dyndns`);
|
file.push(`; GENERATED BY ICYDNS`);
|
||||||
|
|
||||||
let longestName = 0;
|
let longestName = 0;
|
||||||
let longestType = 0;
|
let longestType = 0;
|
||||||
@ -49,6 +91,11 @@ export function createZoneFile(zone: DNSZone, preferredLineLength = 120): string
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.type === DNSRecordType.TXT) {
|
||||||
|
file.push(...splitTXTString(record, longestName, longestType));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const name = record.name.padEnd(longestName, ' ');
|
const name = record.name.padEnd(longestName, ' ');
|
||||||
const type = record.type.toString().padEnd(longestType, ' ');
|
const type = record.type.toString().padEnd(longestType, ' ');
|
||||||
file.push(`${name} IN ${type} ${record.value}`);
|
file.push(`${name} IN ${type} ${record.value}`);
|
||||||
|
35
src/index.ts
35
src/index.ts
@ -18,6 +18,7 @@ const app = express();
|
|||||||
const api = express.Router();
|
const api = express.Router();
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.enable('trust proxy');
|
||||||
|
|
||||||
const keys = new Keys();
|
const keys = new Keys();
|
||||||
const rndc = ReloadExecutor.fromEnvironment();
|
const rndc = ReloadExecutor.fromEnvironment();
|
||||||
@ -84,35 +85,13 @@ api.get('/zone/records/:domain', domainAuthorization, async (req, res) => {
|
|||||||
const domain = req.params.domain;
|
const domain = req.params.domain;
|
||||||
const cached = await getOrLoad(domain);
|
const cached = await getOrLoad(domain);
|
||||||
|
|
||||||
const type = req.query.type;
|
const type = req.query.type as DNSRecordType;
|
||||||
const name = req.query.name;
|
const name = req.query.name as string;
|
||||||
const value = req.query.value;
|
const value = req.query.value as string;
|
||||||
|
|
||||||
if (type || name || 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({
|
res.json({
|
||||||
records: results,
|
records: cache.search(cached, name, type, value),
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -241,6 +220,10 @@ api.put('/zone/records/:domain', domainAuthorization, async (req, res) => {
|
|||||||
throw new Error('Validation error: Invalid characters');
|
throw new Error('Validation error: Invalid characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cache.search(cached, name, upperType, value, true).length) {
|
||||||
|
throw new Error('Exact same record already exists. No need to duplicate records!');
|
||||||
|
}
|
||||||
|
|
||||||
zone.records.push(newRecord);
|
zone.records.push(newRecord);
|
||||||
|
|
||||||
await cache.update(domain, cached);
|
await cache.update(domain, cached);
|
||||||
|
Reference in New Issue
Block a user