import * as fs from 'fs/promises'; import { DNSRecordType } from 'src/types/dns.enum'; import { SOARecord, DNSRecord, DNSZone } from 'src/types/dns.interfaces'; 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[] { const name = record.name.padEnd(padI, ' '); const type = record.type.toString().padEnd(padJ, ' '); const padK = ' '.padStart(padI + magicPadding); const padL = ['serial', 'refresh', 'retry', 'expire', 'minimum'].reduce( (previous, current) => { const len = `${record[current]}`.length; return previous > len ? previous : len; }, 0, ) + 1; return [ `${name} IN ${type} ${record.nameserver} ${record.email} (`, `${padK} ${record.serial.toString().padEnd(padL, ' ')} ; Serial`, `${padK} ${record.refresh.toString().padEnd(padL, ' ')} ; Refresh`, `${padK} ${record.retry.toString().padEnd(padL, ' ')} ; Retry`, `${padK} ${record.expire.toString().padEnd(padL, ' ')} ; Expire`, `${padK} ${record.minimum.toString().padEnd(padL, ' ')} ; Minimum`, `)`, ]; } /** * 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[] = []; file.push(`$TTL ${zone.ttl}`); file.push(`; GENERATED BY ICYDNS`); let longestName = 0; let longestType = 0; // First pass: for nice alignments zone.records.forEach((record) => { if (record.name.length > longestName) { longestName = record.name.length; } if (record.type.toString().length > longestType) { longestType = record.type.toString().length; } }); zone.records.forEach((record) => { if (record.type === DNSRecordType.SOA) { file.push( ...createSOAString(record as SOARecord, longestName, longestType), ); return; } if (record.type === DNSRecordType.TXT) { file.push(...splitTXTString(record, longestName, longestType)); return; } const name = record.name.padEnd(longestName, ' '); const type = record.type.toString().padEnd(longestType, ' '); file.push(`${name} IN ${type} ${record.value}`); }); zone.includes.forEach((include) => { file.push(`$INCLUDE ${include}`); }); file.push(''); return file; } export async function writeZoneFile( zone: DNSZone, file: string, ): Promise { const fullText = createZoneFile(zone); await fs.writeFile(file, fullText.join('\n')); return fullText; }