feat: F4.6g2 the DNS server window, its records being /dns/hosts
Oracle DnsWindowRun uses Java's DNS server window as a person does and asks a computer's resolver after each step. The core's DNS server is a program its window changes: on and off (its socket on port 53), recursive resolution, and records that are /dns/hosts, read on every query as Java's server reads them (the reader moves from fls to the core). The snapshot carries every installed DNS server with whether it runs. The room takes the window's actions, sends its view and writes the server back after the run; the editor shows Java's window with its three tabs. Not taken from Java: a cell edit spilling into records of another type, an add losing what another program wrote into the file, an empty mail domain, a removal hitting the first record of the name. A table cell no longer takes the room's value over what is being typed (the web server's too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,141 +1,17 @@
|
||||
// Java's DNS server keeps its records in the text file /dns/hosts and reads
|
||||
// them on every query (DNSServer.leseRecordListe, ResourceRecord(String)).
|
||||
// Measured on test/dns-hosts.cases.txt against Java (dns-hosts.java.json)
|
||||
// and Swift (dns-hosts.swift.json). Java's format decides where the two
|
||||
// differ:
|
||||
// - fields are split at single spaces only: a tab or NBSP joins two fields;
|
||||
// - there are no comments, no "IN" and no zone-file order;
|
||||
// - a TTL that is not a number silently becomes 3600;
|
||||
// - a name that is not a domain name becomes the empty name, and the record
|
||||
// still counts - it just never answers, like one of another type;
|
||||
// - duplicates stay.
|
||||
// Java's errors are not copied (each named in dnsHosts.test.ts): a label may
|
||||
// start with a digit and holds no backslash (RFC 1123 - Java's pattern has
|
||||
// both wrong; see isDomainName), the type A, MX or NS is read in any case
|
||||
// (RFC 4343), and a negative TTL is invalid (RFC 2181).
|
||||
// Java's DNS server keeps its records in the text file /dns/hosts; reading
|
||||
// it lives in the core (runtime/dnsHosts.ts), whose engine answers from the
|
||||
// file as Java's server does. Here: finding the file in a project's tree.
|
||||
|
||||
import { javaIpCheck, javaParseInt, type DnsResourceRecord } from '@mdg-filius/core';
|
||||
import { isHostsDomainName } from '@mdg-filius/core';
|
||||
import { javaFileContent } from './javaFileSystem.js';
|
||||
import { javaTrim } from './javaString.js';
|
||||
import type { FlsFileTree } from './systemSoftware.js';
|
||||
|
||||
/** A record of /dns/hosts, as Java's ResourceRecord holds it (the getters). */
|
||||
export interface HostsRecord {
|
||||
readonly domainName: string;
|
||||
readonly type: string;
|
||||
readonly ttl: number;
|
||||
readonly rdata: string;
|
||||
}
|
||||
export { DNS_HOSTS_PATH, dnsRecordsOf, readDnsHosts, type HostsRecord } from '@mdg-filius/core';
|
||||
|
||||
export const DNS_HOSTS_PATH = '/dns/hosts';
|
||||
|
||||
// Labels of letters, digits and inner hyphens, an optional final dot; the
|
||||
// empty string counts too.
|
||||
const DOMAIN_LABELS = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)?\.?$/;
|
||||
/**
|
||||
* A domain name as EingabenUeberpruefung.musterDomain means it (the empty
|
||||
* string included). Java's pattern wants every label to start with a letter
|
||||
* and lets a backslash into the labels after the first; instead a label may
|
||||
* start with a digit (RFC 1123), but the last one must not be all digits
|
||||
* (RFC 3696), so that no IPv4 address passes for a name.
|
||||
*/
|
||||
export function isDomainName(text: string): boolean {
|
||||
if (!DOMAIN_LABELS.test(text)) return false;
|
||||
const labels = text.split('.').filter((label) => label !== '');
|
||||
return labels.length === 0 || !/^[0-9]+$/.test(labels[labels.length - 1]!);
|
||||
}
|
||||
|
||||
// EingabenUeberpruefung.musterIpAdresse.
|
||||
const JAVA_IP_ADDRESS =
|
||||
/^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
|
||||
/** A domain name as Java's patterns should mean it (core's isHostsDomainName). */
|
||||
export const isDomainName = isHostsDomainName;
|
||||
|
||||
/** The content of /dns/hosts as Java's DNS server finds it (see javaFileSystem.ts). */
|
||||
export function javaHostsFileContent(root: FlsFileTree | undefined): string | undefined {
|
||||
return javaFileContent(root, DNS_HOSTS_PATH);
|
||||
}
|
||||
|
||||
const ANSWERED_TYPES = new Set(['A', 'MX', 'NS']);
|
||||
|
||||
/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */
|
||||
export function readDnsHosts(content: string): HostsRecord[] {
|
||||
const records: HostsRecord[] = [];
|
||||
for (const token of content.split('\n')) {
|
||||
const line = javaTrim(token);
|
||||
// line.split(" ", 5).length < 4: fewer than three single spaces.
|
||||
if (line === '' || line.split(' ').length < 4) continue;
|
||||
const record = hostsRecord(line);
|
||||
if (isValidRecord(record)) records.push(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
/** `new ResourceRecord(line)`: up to four space-separated fields. */
|
||||
function hostsRecord(line: string): HostsRecord {
|
||||
const fields = line.split(' ').filter((field) => field !== '').map(javaTrim);
|
||||
let domainName = '';
|
||||
let type = 'A';
|
||||
let ttl = 3600;
|
||||
let rdata = '';
|
||||
if (fields[0] !== undefined && isDomainName(fields[0])) {
|
||||
domainName = fields[0].endsWith('.') ? fields[0] : `${fields[0]}.`;
|
||||
}
|
||||
if (fields[1] !== undefined) {
|
||||
const upper = fields[1].replace(/[a-z]/g, (letter) => letter.toUpperCase());
|
||||
type = ANSWERED_TYPES.has(upper) ? upper : fields[1];
|
||||
}
|
||||
if (fields[2] !== undefined) {
|
||||
const parsed = javaParseInt(fields[2]);
|
||||
if (parsed !== undefined && parsed >= 0) ttl = parsed;
|
||||
}
|
||||
if (fields[3] !== undefined) {
|
||||
// Java tests ".*\." (no line terminator before the dot); a target with
|
||||
// one fails the domain check anyway, so endsWith says the same.
|
||||
const needsDot = (type === 'NS' || type === 'MX') && !fields[3].endsWith('.');
|
||||
rdata = needsDot ? `${fields[3]}.` : fields[3];
|
||||
}
|
||||
return { domainName, type, ttl, rdata };
|
||||
}
|
||||
|
||||
function isValidRecord(record: HostsRecord): boolean {
|
||||
switch (record.type) {
|
||||
case 'A':
|
||||
return isDomainName(record.domainName) && JAVA_IP_ADDRESS.test(record.rdata);
|
||||
case 'MX':
|
||||
case 'NS':
|
||||
return isDomainName(record.domainName) && isDomainName(record.rdata);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The records Java answers with, in the model's form: Java looks them up by
|
||||
* type (A, MX, NS) and by name ignoring case, so a record with the empty
|
||||
* name or another type never answers and is left out. The file itself stays
|
||||
* as it is.
|
||||
*/
|
||||
export function dnsRecordsOf(records: readonly HostsRecord[], warn: (message: string) => void): DnsResourceRecord[] {
|
||||
const answered: DnsResourceRecord[] = [];
|
||||
for (const record of records) {
|
||||
if (record.domainName === '') continue;
|
||||
const name = record.domainName === '.' ? '.' : record.domainName.slice(0, -1).toLowerCase();
|
||||
switch (record.type) {
|
||||
case 'A': {
|
||||
const address = javaIpCheck(record.rdata);
|
||||
if (address === null) {
|
||||
warn(`DNS record '${name} A ${record.rdata}' has no usable IPv4 address, left out (it stays in ${DNS_HOSTS_PATH})`);
|
||||
continue;
|
||||
}
|
||||
answered.push({ name, type: 'A', ttlSeconds: record.ttl, target: address });
|
||||
break;
|
||||
}
|
||||
case 'MX':
|
||||
case 'NS':
|
||||
answered.push({ name, type: record.type, ttlSeconds: record.ttl, target: record.rdata.toLowerCase() });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return answered;
|
||||
return javaFileContent(root, '/dns/hosts');
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
// /mailserver/konten.txt at once), and an e-mail program's inbox and sent
|
||||
// mails.
|
||||
|
||||
import { portSnapshot, topologySnapshot, type DnsServerConfiguration, type EchoServerSnapshot, type EmailServerConfiguration, type EmailServerSnapshot, type Id, type WebServerSnapshot, type TopologySnapshot } from '@mdg-filius/core';
|
||||
import { portSnapshot, topologySnapshot, type DnsServerSnapshot, type EchoServerSnapshot, type EmailServerConfiguration, type EmailServerSnapshot, type Id, type WebServerSnapshot, type TopologySnapshot } from '@mdg-filius/core';
|
||||
import type { ImportedProject } from './importProject.js';
|
||||
|
||||
export function projectSnapshot(project: ImportedProject): TopologySnapshot {
|
||||
const dnsServers = new Map<Id, DnsServerConfiguration>();
|
||||
const dnsServers = new Map<Id, DnsServerSnapshot>();
|
||||
for (const [nodeId, configuration] of project.dnsServerConfigurations) {
|
||||
if (project.activePrograms.get(nodeId)?.has('dnsServer') === true) dnsServers.set(nodeId, configuration);
|
||||
dnsServers.set(nodeId, { ...configuration, isActive: project.activePrograms.get(nodeId)?.has('dnsServer') === true });
|
||||
}
|
||||
const echoServers = new Map<Id, EchoServerSnapshot>();
|
||||
for (const [nodeId, configuration] of project.echoServerConfigurations) {
|
||||
|
||||
@@ -714,7 +714,7 @@ describe('importProject rules', () => {
|
||||
expect(project.hostWirelessConfigurations.get(withoutNetwork!)).toEqual({ isEnabled: true, ssid: '' });
|
||||
});
|
||||
|
||||
it('runs only the DNS servers that are switched on (projectSnapshot)', () => {
|
||||
it('runs every DNS server, switched on or off, and says which is on (projectSnapshot)', () => {
|
||||
const dnsServer = (isActive: boolean): FlsApplication => ({ key: 'dns', javaClass: 'filius.software.dns.DNSServer', source: flsNode('Rechner').source, isActive });
|
||||
const project = importProject(
|
||||
topology({
|
||||
@@ -728,7 +728,10 @@ describe('importProject rules', () => {
|
||||
const [on, off] = project.graph.nodes.map((node) => node.id);
|
||||
|
||||
expect([...project.dnsServerConfigurations.keys()]).toEqual([on, off]);
|
||||
expect([...projectSnapshot(project).dnsServerConfigurationsByNodeId.keys()]).toEqual([on]);
|
||||
expect([...projectSnapshot(project).dnsServerConfigurationsByNodeId].map(([id, server]) => [id, server.isActive])).toEqual([
|
||||
[on, true],
|
||||
[off, false],
|
||||
]);
|
||||
});
|
||||
|
||||
it('runs every echo server, switched on or off, and every generic client (projectSnapshot)', () => {
|
||||
|
||||
Reference in New Issue
Block a user