141 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|
|
}
|