A project's own applications (classes under projekt/anwendungen, installed on a host) are no longer left out. The decoder now keeps the Java type of every primitive — at setters, constructor and call arguments, also through shared ids like Integer0 — and which properties an object's own getter made. beanCapture.ts turns a decoded bean back into what the writer writes, with the node's system software as a mark the writer replaces by the system software it writes; what the format cannot say is refused, not guessed (an unresolved value, a Field, a factory-made object, an object a getter reaches elsewhere in the file). The reader keeps own applications as captured beans; the corpus no longer warns. The capture first took a setter with another object's getter result for an own getter; the round-trip test found it. The Java oracle did not show whether an application runs on its node's system software, so a mutation writing the mark instead survived; it now reports onItsSystem (true for all 192 applications of the corpus). Checked by Java: from the files written again, FILIUS loads Accumulator and Broadcast installed, switched on and on their node's system software, as in the original. The round-trip test covers every primitive, null, a class, an array, shared objects, getter chains, constructed elements, map reads and static constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
588 lines
25 KiB
TypeScript
588 lines
25 KiB
TypeScript
// The settings of a node's system software that the topology needs, read as
|
|
// Java FILIUS holds them after loading: a switch's SSID and learning time, a
|
|
// host's wireless SSID, a router's RIP switch, the manual routes, and the
|
|
// firewall of a router or home router.
|
|
// Java counterparts: filius.software.system.SwitchFirmware, Betriebssystem,
|
|
// VermittlungsrechnerBetriebssystem, GatewayFirmware, InternetKnotenBetriebssystem,
|
|
// filius.software.vermittlungsschicht.Weiterleitungstabelle,
|
|
// filius.software.firewall.Firewall and FirewallRule.
|
|
//
|
|
// Also DHCP (client switch and server settings), a home router's static NAT
|
|
// table (port forwarding) and a modem's connection settings.
|
|
// Java counterparts as well: filius.software.dhcp.DHCPServer, ModemFirmware.
|
|
//
|
|
// And the file system: Java's tree of DefaultMutableTreeNodes, a
|
|
// directory's user object its name, a file's a filius.software.system.Datei.
|
|
// A PC's or notebook's constructor leaves the root empty. A router's or home
|
|
// router's makes files from its own templates (the start page of the
|
|
// firewall's web interface), which are not in the file; what the file adds
|
|
// to that root (a web server's www.conf) comes after them.
|
|
//
|
|
// Checked against Java (corpus.java.json, `system`). Applications are a
|
|
// later layer.
|
|
//
|
|
// A router's firewall is one object its constructor makes and hangs in two
|
|
// places: the installed applications and the configuration plug-ins of its
|
|
// web server. XMLEncoder writes its settings wherever it meets it first —
|
|
// in the corpus, under the web server's `konfig` plug-in. The decoder does
|
|
// not know Java's constructors and sees a placeholder per path; they are
|
|
// merged here.
|
|
|
|
import { captureBean } from './beanCapture.js';
|
|
import { isDomainName } from './dnsHosts.js';
|
|
import type { BeanObject } from './javaBeansWriter.js';
|
|
import {
|
|
EMAIL_CLIENT_FILE,
|
|
EMAIL_SERVER_FILE,
|
|
javaContacts,
|
|
javaEmailAccounts,
|
|
javaEmails,
|
|
javaLegacyEmailAccounts,
|
|
readEmailClientFile,
|
|
readEmailServerFile,
|
|
type JavaContact,
|
|
type JavaEmail,
|
|
type JavaEmailAccount,
|
|
} from './javaEmail.js';
|
|
import { javaFileContent } from './javaFileSystem.js';
|
|
import {
|
|
isJavaObject,
|
|
isUnresolved,
|
|
javaDefault,
|
|
listItems,
|
|
mapEntries,
|
|
simpleClassName,
|
|
type JavaObject,
|
|
type JavaValue,
|
|
} from './javabeans.js';
|
|
import type { FlsNodeClass } from './topology.js';
|
|
|
|
export interface FlsFirewallRule {
|
|
srcIP: string | null;
|
|
srcMask: string | null;
|
|
destIP: string | null;
|
|
destMask: string | null;
|
|
/** A port, or -1 for all. */
|
|
port: number;
|
|
/** -1 all, 1 ICMP, 6 TCP, 17 UDP. */
|
|
protocol: number;
|
|
/** 0 drop, 1 accept. */
|
|
action: number;
|
|
}
|
|
|
|
export interface FlsFirewall {
|
|
activated: boolean;
|
|
/** 0 drop, 1 accept. */
|
|
defaultPolicy: number;
|
|
dropICMP: boolean;
|
|
filterSYNSegmentsOnly: boolean;
|
|
filterUdp: boolean;
|
|
rules: FlsFirewallRule[];
|
|
}
|
|
|
|
/** The settings of Java's DHCPServer under English names (Java's own: javaDhcpServer in the topology test). */
|
|
export interface FlsDhcpServer {
|
|
isActive: boolean;
|
|
lowerBound: string | null;
|
|
upperBound: string | null;
|
|
gateway: string | null;
|
|
dnsServer: string | null;
|
|
useOwnSettings: boolean;
|
|
/** As Java keeps them after setStaticAssignedAddresses: MAC and IP of each entry. */
|
|
staticAssignedAddresses: { mac: string; ip: string }[];
|
|
}
|
|
|
|
export interface FlsModem {
|
|
/** 1 server, 2 client. */
|
|
mode: number;
|
|
port: number;
|
|
/** Java's ipAdresse: the host a client modem connects to. */
|
|
ipAddress: string | null;
|
|
}
|
|
|
|
export type FlsFileTree =
|
|
| { kind: 'directory'; name: string | null; children: FlsFileTree[] }
|
|
| { kind: 'file'; name: string | null; type: string | null; content: string | null; children: FlsFileTree[] }
|
|
/** A node whose user object is neither a name nor a file. */
|
|
| { kind: 'other'; className: string; children: FlsFileTree[] };
|
|
|
|
/** The applications FILIUS 2.14 can install (config/Desktop_*.txt). */
|
|
export const FILIUS_APPLICATION_CLASSES = [
|
|
'filius.software.clientserver.ClientBaustein',
|
|
'filius.software.clientserver.ServerBaustein',
|
|
'filius.software.dateiaustausch.PeerToPeerAnwendung',
|
|
'filius.software.dns.DNSServer',
|
|
'filius.software.email.EmailAnwendung',
|
|
'filius.software.email.EmailServer',
|
|
'filius.software.firewall.Firewall',
|
|
'filius.software.lokal.FileExplorer',
|
|
'filius.software.lokal.ImageViewer',
|
|
'filius.software.lokal.Terminal',
|
|
'filius.software.lokal.TextEditor',
|
|
'filius.software.www.WebBrowser',
|
|
'filius.software.www.WebServer',
|
|
] as const;
|
|
|
|
export type FlsApplicationClass = (typeof FILIUS_APPLICATION_CLASSES)[number];
|
|
|
|
/** Server applications (ServerAnwendung): they listen on a port and can be switched on. */
|
|
const SERVER_APPLICATIONS = new Set<string>([
|
|
'filius.software.clientserver.ServerBaustein',
|
|
'filius.software.dns.DNSServer',
|
|
'filius.software.www.WebServer',
|
|
]);
|
|
|
|
/** An application of the project's own: its key, class and bean, which refers to its node's system software by a mark. */
|
|
export interface FlsOwnApplication {
|
|
key: string | null;
|
|
javaClass: string;
|
|
bean: BeanObject;
|
|
}
|
|
|
|
/** An installed application with the settings the model reads. */
|
|
export interface FlsApplication {
|
|
/** The key in installierteAnwendungen — the class name in every corpus file. */
|
|
key: string | null;
|
|
javaClass: FlsApplicationClass;
|
|
/** Server applications: switched on (Java's aktiv) and the port; EmailServer: switched on. */
|
|
isActive?: boolean;
|
|
port?: number;
|
|
/** ClientBaustein: the server it talks to (Java's zielIPAdresse, zielPort). */
|
|
targetAddress?: string | null;
|
|
targetPort?: number;
|
|
/** DNSServer: whether it asks other servers for names it does not know. */
|
|
recursiveResolutionEnabled?: boolean;
|
|
/** WebServer: serve by host name (Java's useVHost). */
|
|
useVirtualHosts?: boolean;
|
|
/**
|
|
* PeerToPeerAnwendung: how many peers it keeps (Java's maxTeilnehmerZahl);
|
|
* null when the file has none — Java's constructor rolls 3, 4 or 5.
|
|
*/
|
|
maxPeers?: number | null;
|
|
/** Firewall: the personal firewall. */
|
|
firewall?: FlsFirewall;
|
|
/** EmailServer: its domain ("filius.de" for one that is not a domain name, as Java's setter keeps it). */
|
|
mailDomain?: string;
|
|
/** EmailServer: the accounts of the bean list. */
|
|
beanAccounts?: JavaEmailAccount[];
|
|
/**
|
|
* EmailServer: the accounts of /mailserver/konten.txt, which Java reads at
|
|
* every simulation start — null without the file (Java then starts with
|
|
* none).
|
|
*/
|
|
fileAccounts?: JavaEmailAccount[] | null;
|
|
/** EmailAnwendung: its folders (Java's empfangene, gesendete, erstellte Nachrichten). */
|
|
received?: JavaEmail[];
|
|
sent?: JavaEmail[];
|
|
drafts?: JavaEmail[];
|
|
addressBook?: JavaContact[];
|
|
/** EmailAnwendung: the account of /konten.txt, null without one. */
|
|
fileAccount?: JavaEmailAccount | null;
|
|
/** EmailAnwendung: the accounts of an older file's kontoListe, which FILIUS 2.14 ignores. */
|
|
legacyAccounts?: JavaEmailAccount[];
|
|
/** The application's JavaBean, for the e-mail layer. */
|
|
source: JavaObject;
|
|
}
|
|
|
|
export interface FlsSystemSoftware {
|
|
/**
|
|
* Switch: its SSID, undefined when the file has none (Java rolls one).
|
|
* Rechner, Notebook: the wireless network it joins, null for none.
|
|
*/
|
|
ssid?: string | null | undefined;
|
|
/** Switch: how long it keeps a learned MAC address, in milliseconds. */
|
|
retentionTime?: number;
|
|
/** Vermittlungsrechner. */
|
|
ripEnabled?: boolean;
|
|
/** Every node with an IP stack: rows of destination, mask, gateway, interface address — as the file has them. */
|
|
manualRoutes?: (string | null)[][];
|
|
/** Vermittlungsrechner, Gateway. */
|
|
firewall?: FlsFirewall;
|
|
/** Every node with an IP stack: whether it asks a DHCP server for its address. */
|
|
dhcpClient?: boolean;
|
|
/** Every node with an IP stack. */
|
|
dhcpServer?: FlsDhcpServer;
|
|
/** Gateway: rows of protocol, public port, LAN address, LAN port — as the file has them. */
|
|
staticNAT?: (string | null)[][];
|
|
/** Modem. */
|
|
modem?: FlsModem;
|
|
/** Rechner, Notebook: the installed applications, in the file's order. */
|
|
applications?: FlsApplication[];
|
|
/**
|
|
* Rechner, Notebook: the installed applications FILIUS does not ship — a
|
|
* project's own, compiled into projekt/anwendungen — kept as their beans,
|
|
* to be written again as the file had them.
|
|
*/
|
|
ownApplications?: FlsOwnApplication[];
|
|
/**
|
|
* Every node with an IP stack: the root directory (named "root" by Java's
|
|
* constructor) as the file has it. A router's or home router's holds only
|
|
* what the file adds to the constructor's, see `templateFiles`.
|
|
*/
|
|
fileSystem?: FlsFileTree;
|
|
/**
|
|
* Vermittlungsrechner, Gateway: whether the files the constructor makes
|
|
* stay in front of the file's — false when the file sets a root of its own.
|
|
*/
|
|
templateFiles?: boolean;
|
|
}
|
|
|
|
type Warn = (message: string) => void;
|
|
|
|
const FIREWALL_CLASS = 'filius.software.firewall.Firewall';
|
|
const WEB_SERVER_CLASS = 'filius.software.www.WebServer';
|
|
|
|
export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass, warn: Warn): FlsSystemSoftware {
|
|
const system = device.properties.get('systemSoftware');
|
|
const software = isJavaObject(system) ? system : undefined;
|
|
const value = (property: string) => software?.properties.get(property);
|
|
const result: FlsSystemSoftware = {};
|
|
|
|
switch (javaClass) {
|
|
case 'Switch':
|
|
result.ssid = optionalString(value('SSID'), 'SSID', warn);
|
|
result.retentionTime = integer(value('retentionTime'), 300_000, 'retentionTime', warn);
|
|
break;
|
|
case 'Rechner':
|
|
case 'Notebook':
|
|
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
|
|
result.fileSystem = fileSystem(value('dateisystem'), warn);
|
|
[result.applications, result.ownApplications] = applications(value('installierteAnwendungen'), result.fileSystem, software, warn);
|
|
break;
|
|
case 'Vermittlungsrechner':
|
|
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
|
|
break;
|
|
case 'Gateway':
|
|
case 'Modem':
|
|
break;
|
|
}
|
|
if (javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway') {
|
|
result.fileSystem = fileSystem(value('dateisystem'), warn);
|
|
result.templateFiles = constructedRoot(value('dateisystem'));
|
|
}
|
|
if (javaClass !== 'Switch' && javaClass !== 'Modem') {
|
|
result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), warn);
|
|
result.dhcpClient = boolean(value('DHCPKonfiguration'), false, 'DHCPKonfiguration', warn);
|
|
result.dhcpServer = dhcpServer(value('DHCPServer'), warn);
|
|
}
|
|
if (javaClass === 'Gateway') result.staticNAT = rowsOfStrings(value('staticNAT'));
|
|
if (javaClass === 'Modem') {
|
|
result.modem = {
|
|
mode: integer(value('mode'), 2, 'modem mode', warn),
|
|
port: integer(value('port'), 12_345, 'modem port', warn),
|
|
ipAddress: modemAddress(optionalString(value('ipAdresse'), 'modem ipAdresse', warn)),
|
|
};
|
|
}
|
|
if (javaClass === 'Vermittlungsrechner' || javaClass === 'Gateway') {
|
|
result.firewall = firewall(software, warn);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function manualRoutes(table: JavaValue | undefined, warn: Warn): (string | null)[][] {
|
|
if (table === undefined || table === null) return [];
|
|
if (!isJavaObject(table)) {
|
|
warn(`weiterleitungstabelle is ${describe(table)}, ignored`);
|
|
return [];
|
|
}
|
|
return rowsOfStrings(table.properties.get('manuelleTabelle'));
|
|
}
|
|
|
|
function modemAddress(value: string | null | undefined): string | null {
|
|
return value === undefined ? 'localhost' : value;
|
|
}
|
|
|
|
/**
|
|
* installierteAnwendungen: key → application. A class FILIUS does not ship —
|
|
* a project's own application, compiled into projekt/anwendungen — cannot
|
|
* run here; it is kept as its bean (beanCapture.ts), or left out with a
|
|
* warning where the bean cannot be written again.
|
|
*/
|
|
function applications(
|
|
map: JavaValue | undefined,
|
|
root: FlsFileTree,
|
|
software: JavaObject | undefined,
|
|
warn: Warn,
|
|
): [FlsApplication[], FlsOwnApplication[]] {
|
|
const result: FlsApplication[] = [];
|
|
const own: FlsOwnApplication[] = [];
|
|
for (const [key, application] of mapEntries(map)) {
|
|
if (!isJavaObject(application)) {
|
|
warn(`application ${describe(key)} is ${describe(application)}, left out`);
|
|
continue;
|
|
}
|
|
const javaClass = application.className;
|
|
if (!(FILIUS_APPLICATION_CLASSES as readonly string[]).includes(javaClass)) {
|
|
const captured = captureBean(application, software);
|
|
if ('bean' in captured) own.push({ key: typeof key === 'string' ? key : null, javaClass, bean: captured.bean });
|
|
else warn(`application ${javaClass}, a project's own, cannot be written again: ${captured.failure}; left out`);
|
|
continue;
|
|
}
|
|
const simple = simpleClassName(javaClass);
|
|
const at = (property: string) => application.properties.get(property);
|
|
const measured = (property: string) => javaDefault(simple, property);
|
|
const entry: FlsApplication = {
|
|
key: typeof key === 'string' ? key : null,
|
|
javaClass: javaClass as FlsApplicationClass,
|
|
source: application,
|
|
};
|
|
if (SERVER_APPLICATIONS.has(javaClass)) {
|
|
entry.isActive = boolean(at('aktiv'), measured('aktiv') === true, `${simple} aktiv`, warn);
|
|
entry.port = integer(at('port'), Number(measured('port')), `${simple} port`, warn);
|
|
}
|
|
switch (javaClass) {
|
|
case 'filius.software.clientserver.ClientBaustein': {
|
|
const target = optionalString(at('zielIPAdresse'), 'ClientBaustein zielIPAdresse', warn);
|
|
entry.targetAddress = target === undefined ? null : target;
|
|
entry.targetPort = integer(at('zielPort'), Number(measured('zielPort')), 'ClientBaustein zielPort', warn);
|
|
break;
|
|
}
|
|
case 'filius.software.dns.DNSServer':
|
|
entry.recursiveResolutionEnabled = boolean(at('recursiveResolutionEnabled'), false, 'DNSServer recursion', warn);
|
|
break;
|
|
case 'filius.software.www.WebServer':
|
|
entry.useVirtualHosts = boolean(at('useVHost'), measured('useVHost') === true, 'WebServer useVHost', warn);
|
|
break;
|
|
case 'filius.software.dateiaustausch.PeerToPeerAnwendung':
|
|
entry.maxPeers =
|
|
at('maxTeilnehmerZahl') === undefined ? null : integer(at('maxTeilnehmerZahl'), 0, 'Gnutella peers', warn);
|
|
break;
|
|
case 'filius.software.firewall.Firewall':
|
|
entry.firewall = firewallOf([application], true, warn);
|
|
break;
|
|
case 'filius.software.email.EmailServer': {
|
|
entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn);
|
|
const domain = optionalString(at('mailDomain'), 'EmailServer mailDomain', warn);
|
|
entry.mailDomain = typeof domain === 'string' && isDomainName(domain) ? domain : 'filius.de';
|
|
entry.beanAccounts = javaEmailAccounts(at('listeBenutzerkonten'), warn);
|
|
const file = javaFileContent(root, EMAIL_SERVER_FILE);
|
|
entry.fileAccounts = file === undefined ? null : readEmailServerFile(file, warn);
|
|
break;
|
|
}
|
|
case 'filius.software.email.EmailAnwendung': {
|
|
entry.received = javaEmails(at('empfangeneNachrichten'), warn);
|
|
entry.sent = javaEmails(at('gesendeteNachrichten'), warn);
|
|
entry.drafts = javaEmails(at('erstellteNachrichten'), warn);
|
|
entry.addressBook = javaContacts(at('adressbuch'), warn);
|
|
const file = javaFileContent(root, EMAIL_CLIENT_FILE);
|
|
entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn);
|
|
entry.legacyAccounts = javaLegacyEmailAccounts(at('kontoListe'), warn);
|
|
break;
|
|
}
|
|
}
|
|
result.push(entry);
|
|
}
|
|
return [result, own];
|
|
}
|
|
|
|
/**
|
|
* Dateisystem: its root is `arbeitsVerzeichnis` in files up to 1.10 and
|
|
* `root` after. The constructor's root is a node named "root"; a node the
|
|
* file makes with `new DefaultMutableTreeNode()` has no user object.
|
|
*/
|
|
function fileSystem(system: JavaValue | undefined, warn: Warn): FlsFileTree {
|
|
const root = fileSystemRoot(system);
|
|
if (root === undefined) return { kind: 'directory', name: 'root', children: [] };
|
|
return fileTreeNode(root, true, warn);
|
|
}
|
|
|
|
function fileSystemRoot(system: JavaValue | undefined): JavaValue | undefined {
|
|
const at = (property: string) => (isJavaObject(system) ? system.properties.get(property) : undefined);
|
|
return at('root') ?? at('arbeitsVerzeichnis');
|
|
}
|
|
|
|
/** Whether the root is the one the constructor made: none set, or reached through the getter. */
|
|
function constructedRoot(system: JavaValue | undefined): boolean {
|
|
if (system !== undefined && !(isJavaObject(system) && system.fromGetter)) return false;
|
|
const root = fileSystemRoot(system);
|
|
return root === undefined || (isJavaObject(root) && root.fromGetter);
|
|
}
|
|
|
|
function fileTreeNode(node: JavaValue, constructorRoot: boolean, warn: Warn): FlsFileTree {
|
|
if (!isJavaObject(node)) {
|
|
warn(`file system: a node is ${describe(node)}, left empty`);
|
|
return { kind: 'other', className: 'unknown', children: [] };
|
|
}
|
|
for (const call of node.calls) {
|
|
if (call.method !== 'add') warn(`file system: ${call.method} on a node is not read`);
|
|
}
|
|
const children = listItems(node).map((child) => fileTreeNode(child, false, warn));
|
|
const userObject = node.properties.has('userObject')
|
|
? node.properties.get('userObject')!
|
|
: node.fromGetter && constructorRoot
|
|
? 'root'
|
|
: (node.args[0] ?? null);
|
|
if (userObject === null || typeof userObject === 'string') return { kind: 'directory', name: userObject, children };
|
|
if (isJavaObject(userObject) && userObject.className === 'filius.software.system.Datei') {
|
|
// new Datei(name, typ, inhalt), then the setters.
|
|
const [name, type, content] = userObject.args.length === 3 ? userObject.args : [];
|
|
const text = (property: string, fromConstructor: JavaValue | undefined) => {
|
|
const value = userObject.properties.get(property) ?? fromConstructor ?? null;
|
|
if (typeof value === 'string' || value === null) return value;
|
|
warn(`file system: file ${property} is ${describe(value)}, ignored`);
|
|
return null;
|
|
};
|
|
return {
|
|
kind: 'file',
|
|
name: text('name', name),
|
|
type: text('dateiTyp', type),
|
|
content: text('dateiInhalt', content),
|
|
children,
|
|
};
|
|
}
|
|
return { kind: 'other', className: isJavaObject(userObject) ? userObject.className : typeof userObject, children };
|
|
}
|
|
|
|
/** A list of String arrays (routes, static NAT); anything but a string is null. */
|
|
function rowsOfStrings(list: JavaValue | undefined): (string | null)[][] {
|
|
return listItems(list).map((row) => listItems(row).map((cell) => (typeof cell === 'string' ? cell : null)));
|
|
}
|
|
|
|
/** The DHCP server every IP stack's constructor makes: off, all addresses 0.0.0.0. */
|
|
function dhcpServer(server: JavaValue | undefined, warn: Warn): FlsDhcpServer {
|
|
const at = (property: string) => (isJavaObject(server) ? server.properties.get(property) : undefined);
|
|
const address = (property: string) => {
|
|
const value = optionalString(at(property), `DHCPServer ${property}`, warn);
|
|
return value === undefined ? '0.0.0.0' : value;
|
|
};
|
|
return {
|
|
isActive: boolean(at('aktiv'), false, 'DHCPServer aktiv', warn),
|
|
lowerBound: address('untergrenze'),
|
|
upperBound: address('obergrenze'),
|
|
gateway: address('gatewayip'),
|
|
dnsServer: address('dnsserverip'),
|
|
useOwnSettings: boolean(at('ownSettings'), false, 'DHCPServer ownSettings', warn),
|
|
staticAssignedAddresses: staticAssignments(at('staticAssignedAddresses'), warn),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
|
|
* MAC and IP from the first two parts. An entry with fewer makes Java throw
|
|
* inside the loop — XMLDecoder swallows it, and the entries after it are
|
|
* lost. That error is not copied: such an entry alone is left out.
|
|
*/
|
|
function staticAssignments(list: JavaValue | undefined, warn: Warn): { mac: string; ip: string }[] {
|
|
const result: { mac: string; ip: string }[] = [];
|
|
for (const entry of listItems(list)) {
|
|
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
|
|
if (parts.length < 2) {
|
|
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP, left out`);
|
|
continue;
|
|
}
|
|
result.push({ mac: parts[0]!, ip: parts[1]! });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Character.isWhitespace: space, line and paragraph separators except the
|
|
* no-break ones (U+00A0, U+2007, U+202F), and the controls TAB, LF, VT, FF,
|
|
* CR, FS, GS, RS, US.
|
|
*/
|
|
const JAVA_WHITESPACE = /(?:(?![\u00A0\u2007\u202F])[\p{Zs}\p{Zl}\p{Zp}]|[\t\n\u000B\f\r\u001C-\u001F])+/u;
|
|
|
|
function javaWhitespaceSplit(text: string): string[] {
|
|
return text.split(JAVA_WHITESPACE).filter((part) => part !== '');
|
|
}
|
|
|
|
/** Every object the file reaches the router's one firewall by. */
|
|
function firewallObjects(software: JavaObject | undefined): JavaObject[] {
|
|
const found: JavaObject[] = [];
|
|
const applications = new Map(mapEntries(software?.properties.get('installierteAnwendungen')));
|
|
const installed = applications.get(FIREWALL_CLASS);
|
|
if (isJavaObject(installed)) found.push(installed);
|
|
const webServer = applications.get(WEB_SERVER_CLASS);
|
|
if (isJavaObject(webServer)) {
|
|
for (const [, plugin] of mapEntries(webServer.properties.get('plugins'))) {
|
|
const pluginFirewall = isJavaObject(plugin) ? plugin.properties.get('firewall') : undefined;
|
|
if (isJavaObject(pluginFirewall) && !found.includes(pluginFirewall)) found.push(pluginFirewall);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/** The firewall as the router's constructor leaves it (off, drop), then the file's settings. */
|
|
function firewall(software: JavaObject | undefined, warn: Warn): FlsFirewall {
|
|
return firewallOf(firewallObjects(software), false, warn);
|
|
}
|
|
|
|
/**
|
|
* A firewall's settings from every object the file reaches it by. `activated`
|
|
* starts as the constructor leaves it: a router's is switched off, a PC's
|
|
* personal firewall (new Firewall()) is on.
|
|
*/
|
|
function firewallOf(objects: JavaObject[], activatedByDefault: boolean, warn: Warn): FlsFirewall {
|
|
const properties = new Map<string, JavaValue>();
|
|
for (const object of objects) {
|
|
for (const [property, value] of object.properties) {
|
|
if (properties.has(property) && properties.get(property) !== value) {
|
|
warn(`firewall: ${property} is set in two places, the later one is used`);
|
|
}
|
|
properties.set(property, value);
|
|
}
|
|
}
|
|
const at = (property: string) => properties.get(property);
|
|
// setAllowRelatedPackets is the old name of setFilterSYNSegmentsOnly.
|
|
const filterSyn = at('filterSYNSegmentsOnly') ?? at('allowRelatedPackets');
|
|
return {
|
|
activated: boolean(at('activated'), activatedByDefault, 'firewall activated', warn),
|
|
defaultPolicy: integer(at('defaultPolicy'), 0, 'firewall defaultPolicy', warn),
|
|
dropICMP: boolean(at('dropICMP'), false, 'firewall dropICMP', warn),
|
|
filterSYNSegmentsOnly: boolean(filterSyn, true, 'firewall filterSYNSegmentsOnly', warn),
|
|
filterUdp: boolean(at('filterUdp'), true, 'firewall filterUdp', warn),
|
|
rules: listItems(at('ruleset')).flatMap((rule) => (isJavaObject(rule) ? [firewallRule(rule, warn)] : [])),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* FirewallRule: public fields set one by one (`getField(name).set`), or the
|
|
* seven-argument constructor. Defaults: all addresses empty, all ports, TCP,
|
|
* drop.
|
|
*/
|
|
function firewallRule(rule: JavaObject, warn: Warn): FlsFirewallRule {
|
|
const [srcIP, srcMask, destIP, destMask, port, protocol, action] = rule.args.length === 7 ? rule.args : [];
|
|
const field = (name: string, fromConstructor: JavaValue | undefined) => rule.properties.get(name) ?? fromConstructor;
|
|
const text = (name: string, fromConstructor: JavaValue | undefined) => {
|
|
const value = field(name, fromConstructor) ?? '';
|
|
if (typeof value === 'string' || value === null) return value;
|
|
warn(`firewall rule: ${name} is ${describe(value)}, used ""`);
|
|
return '';
|
|
};
|
|
return {
|
|
srcIP: text('srcIP', srcIP),
|
|
srcMask: text('srcMask', srcMask),
|
|
destIP: text('destIP', destIP),
|
|
destMask: text('destMask', destMask),
|
|
port: integer(field('port', port), -1, 'firewall rule port', warn),
|
|
protocol: integer(field('protocol', protocol), 6, 'firewall rule protocol', warn),
|
|
action: integer(field('action', action), 0, 'firewall rule action', warn),
|
|
};
|
|
}
|
|
|
|
function optionalString(value: JavaValue | undefined, name: string, warn: Warn): string | null | undefined {
|
|
if (value === undefined || value === null || typeof value === 'string') return value;
|
|
warn(`${name} is ${describe(value)}, ignored`);
|
|
return undefined;
|
|
}
|
|
|
|
function boolean(value: JavaValue | undefined, fallback: boolean, name: string, warn: Warn): boolean {
|
|
if (value === undefined) return fallback;
|
|
if (typeof value === 'boolean') return value;
|
|
warn(`${name} is ${describe(value)}, used ${fallback}`);
|
|
return fallback;
|
|
}
|
|
|
|
function integer(value: JavaValue | undefined, fallback: number, name: string, warn: Warn): number {
|
|
if (value === undefined) return fallback;
|
|
if (typeof value === 'number' && Number.isInteger(value)) return value;
|
|
warn(`${name} is ${describe(value)}, used ${fallback}`);
|
|
return fallback;
|
|
}
|
|
|
|
function describe(value: JavaValue): string {
|
|
if (isUnresolved(value)) return `unresolved (${value.reason})`;
|
|
if (isJavaObject(value)) return `an object of ${value.className}`;
|
|
return typeof value === 'bigint' ? `${value}` : JSON.stringify(value);
|
|
}
|