// 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([ '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(); 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); }