import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DNSRecordType } from 'src/types/dns.enum'; import { CachedZone, DNSRecord, SOARecord } from 'src/types/dns.interfaces'; import { readZoneFile } from 'src/utility/dns/reader'; import * as validator from 'src/utility/dns/validator'; import { RNDCService } from './rndc.service'; @Injectable() export class DNSCacheService { private cached: Record = {}; private ttl = this.config.get('cacheTTL'); constructor(private rndc: RNDCService, private config: ConfigService) {} has(name: string): boolean { 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 { const cached = this.cached[name]; if (!cached) { return null; } if (cached.changed.getTime() < new Date().getTime() - this.ttl * 1000) { return this.load(name, cached.file); } return this.cached[name]; } async set(name: string, zone: CachedZone): Promise { this.cached[name] = zone; } async load(name: string, file: string): Promise { const zoneFile = await readZoneFile(file); const cache = { name, file, zone: zoneFile, added: new Date(), changed: new Date(), }; this.cached[name] = cache; return cache; } async save(name: string): Promise { const zone = await this.get(name); if (!zone) { throw new Error('No such cached zone file!'); } try { await validator.validateAndSave(name, zone); } catch (e: any) { // Reload previous state await this.load(name, zone.file); throw e as Error; } } async update( name: string, newZone?: CachedZone, skipReload = false, ): Promise { let zone: CachedZone | null; if (newZone) { zone = newZone; } else { zone = await this.get(name); } if (!zone) { throw new Error('No such cached zone file!'); } // Delete marked-for-deletion records zone.zone.records = zone.zone.records.filter( (record) => record.forDeletion !== true, ); // Set new serial const soa = zone.zone.records.find( (record) => record.type === DNSRecordType.SOA, ) as SOARecord; soa.serial = Math.floor(Date.now() / 1000); zone.changed = new Date(); this.set(name, zone); await this.save(name); if (!skipReload) { try { await this.rndc.reload(name); } catch (e: unknown) { // logger.warn('%s automatic zone reload failed:', name, (e as Error).stack); } } } }