icydns/src/modules/objects/dns/dns-cache.service.ts

141 lines
3.3 KiB
TypeScript

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<string, CachedZone> = {};
private ttl = this.config.get<number>('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<CachedZone | null> {
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<void> {
this.cached[name] = zone;
}
async load(name: string, file: string): Promise<CachedZone> {
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<void> {
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<void> {
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);
}
}
}
}