// FILIUS' DNS server as its window changes it (DNSServer, // GUIApplicationDNSServerWindow, DNSConfigTable; measured with // tools/java-oracle's DnsWindowRun): switched on and off (its socket on port // 53), recursive resolution, and the records — which are /dns/hosts, read on // every query as Java's server reads them, so that a record another program // wrote into the file answers at once. A server without the file (a core // scenario) answers from its settings until its window writes one. // // Departures from Java (dnsWindow.test.ts names each): // - a record added keeps what another program wrote into the file meanwhile // (Java writes back the list it read before: the file's new lines are lost); // - a cell changed changes that record only (Java counts only the records of // the table's type and, past the one changed, writes the value into each // record of another type up to the next of its own: editing an MX record's // server turned the following A record's address into that name); // - a row removed is that record (Java removes the first of the name: with // two of one name, the other); // - a record needs a name (Java's pattern takes the empty one: ". MX 3600 x."). import type { DnsRecordType, DnsResourceRecord, DnsServerConfiguration } from '../config.js'; import { DNS_HOSTS_PATH, dnsHostsText, dnsRecordsOf, hostsRecordsOf, isHostsDomainName, newHostsRecord, readDnsHosts, type HostsRecord } from '../dnsHosts.js'; import type { VirtualFileSystem } from '../../model/virtualFileSystem.js'; import { javaIpCheck } from '../../util/javaIp.js'; import { IP_ADDRESS_PATTERN } from './web.js'; /** What the server gets from the engine: its files (none yet, or made to write), and its socket opened and closed. */ export interface DnsServerHost { readonly files: () => VirtualFileSystem | undefined; readonly filesToWrite: () => VirtualFileSystem; readonly open: () => void; readonly close: () => void; } /** EingabenUeberpruefung.musterIpAdresse with IP.ipCheck, as the window checks an address. */ const isAddress = (text: string) => IP_ADDRESS_PATTERN.test(text) && javaIpCheck(text) !== null; /** The window's check of a record's two parts: a name, and an address (A) or a name (MX, NS). */ export function validDnsRecord(type: DnsRecordType, domainName: string, rdata: string): boolean { return domainName !== '' && isHostsDomainName(domainName) && (type === 'A' ? isAddress(rdata) : rdata !== '' && isHostsDomainName(rdata)); } export class DnsServer { private active: boolean; private recursive: boolean; constructor( private readonly host: DnsServerHost, private readonly configuration: DnsServerConfiguration, isActive: boolean, ) { this.active = isActive; this.recursive = configuration.recursiveResolutionEnabled; } get isActive(): boolean { return this.active; } get recursiveResolutionEnabled(): boolean { return this.recursive; } /** The start button: the socket on port 53 opened or closed. */ setActive(active: boolean): void { // Once more alike: the port is taken by its own socket, or there is none to close. this.active = active; if (active) this.host.open(); else this.host.close(); } setRecursiveResolutionEnabled(enabled: boolean): void { this.recursive = enabled; } private file(): string | undefined { const files = this.host.files(); return files !== undefined && files.contains(DNS_HOSTS_PATH) && files.entry(DNS_HOSTS_PATH).content.kind === 'text' ? files.textFile(DNS_HOSTS_PATH) : undefined; } /** The records as the window lists them: the valid lines of /dns/hosts (holeResourceRecords), else the settings'. */ get records(): HostsRecord[] { const text = this.file(); return text === undefined ? hostsRecordsOf(this.configuration.records) : readDnsHosts(text); } /** The records a query is answered from. */ get answering(): DnsResourceRecord[] { const text = this.file(); return text === undefined ? [...this.configuration.records] : dnsRecordsOf(readDnsHosts(text), () => {}); } /** The records of a type in the order of the file, each with its place among all (a row of the window's table). */ rows(type: DnsRecordType): { readonly record: HostsRecord; readonly index: number }[] { return this.records.flatMap((record, index) => (record.type === type ? [{ record, index }] : [])); } private write(records: readonly HostsRecord[]): void { const files = this.host.filesToWrite(); if (!files.contains('/dns')) files.createDirectory('/dns', true); files.writeTextFile(DNS_HOSTS_PATH, dnsHostsText(records)); } /** "Hinzufügen" (hinzuRecord): at the end of the file as it is now; false when the window's check fails. */ addRecord(type: DnsRecordType, domainName: string, rdata: string): boolean { if (!validDnsRecord(type, domainName, rdata)) return false; this.write([...this.records, newHostsRecord(domainName, type, rdata)]); return true; } /** "Auswahl entfernen": the table's row of the type; false when there is none. */ removeRecord(type: DnsRecordType, row: number): boolean { const target = this.rows(type)[row]; if (target === undefined) return false; this.write(this.records.filter((_, index) => index !== target.index)); return true; } /** A cell of the type's table written (changeSingleEntry): the name (column 0) or the data (column 1) of that record only; false when the value fails the check. */ changeRecord(type: DnsRecordType, row: number, column: 0 | 1, value: string): boolean { const target = this.rows(type)[row]; if (target === undefined) return false; const trimmed = value.trim(); const domainName = column === 0 ? trimmed : target.record.domainName; const rdata = column === 1 ? trimmed : target.record.rdata; if (!validDnsRecord(type, domainName, rdata)) return false; const changed = { ...newHostsRecord(domainName, type, rdata), ttl: target.record.ttl }; this.write(this.records.map((record, index) => (index === target.index ? changed : record))); return true; } }