feat(fls): system software settings as Java loads them
readSystemSoftware adds to each node what the topology needs from its system software: 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. All 23 corpus files match Java. - A router's firewall is one object its constructor hangs in two places; the corpus writes it under the web server's "konfig" plug-in. The places are merged. - The decoder keeps entries a constructor made in a map, reached by get(key) (installed applications), like the elements of a list. - The Java oracle loads every file twice and writes values that differ as null: what FILIUS rolls (MACs, a switch's SSID) without text search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
|||||||
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
|
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
|
||||||
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
|
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
|
||||||
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Ping über den Router im Kern |
|
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Ping über den Router im Kern |
|
||||||
|
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht); gegen Java geprüft |
|
||||||
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
|
||||||
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
|
| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter |
|
||||||
| `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java |
|
| `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java |
|
||||||
|
|||||||
+1437
-203
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,11 @@ export interface JavaObject {
|
|||||||
* result. `add`/`set` go to `calls`, in order.
|
* result. `add`/`set` go to `calls`, in order.
|
||||||
*/
|
*/
|
||||||
readonly presetElements: Map<number, JavaValue>;
|
readonly presetElements: Map<number, JavaValue>;
|
||||||
|
/**
|
||||||
|
* The same for a map: entries its constructor made (a router's installed
|
||||||
|
* applications), reached by `<void method="get"><string>key</string>`.
|
||||||
|
*/
|
||||||
|
readonly presetEntries: Map<string, JavaValue>;
|
||||||
/** True when the object was not in the file but returned by a getter. */
|
/** True when the object was not in the file but returned by a getter. */
|
||||||
readonly fromGetter: boolean;
|
readonly fromGetter: boolean;
|
||||||
}
|
}
|
||||||
@@ -139,12 +144,21 @@ function replayList(value: JavaObject, constructed: number): (JavaValue | undefi
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The entries of a map: `put` calls in order. */
|
/**
|
||||||
|
* The entries of a map: the ones its constructor made, then `put` calls in
|
||||||
|
* order — a later `put` of the same key replaces the value in place.
|
||||||
|
*/
|
||||||
export function mapEntries(value: JavaValue | undefined): [JavaValue, JavaValue][] {
|
export function mapEntries(value: JavaValue | undefined): [JavaValue, JavaValue][] {
|
||||||
if (!isJavaObject(value)) return [];
|
if (!isJavaObject(value)) return [];
|
||||||
return value.calls
|
const entries: [JavaValue, JavaValue][] = [...value.presetEntries];
|
||||||
.filter((call) => call.method === 'put')
|
for (const call of value.calls) {
|
||||||
.map((call) => [call.args[0] ?? null, call.args[1] ?? null] as [JavaValue, JavaValue]);
|
if (call.method !== 'put') continue;
|
||||||
|
const key = call.args[0] ?? null;
|
||||||
|
const existing = entries.find(([entryKey]) => entryKey === key);
|
||||||
|
if (existing === undefined) entries.push([key, call.args[1] ?? null]);
|
||||||
|
else existing[1] = call.args[1] ?? null;
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── XML tree ───────────────────────────────────────────────────────────────
|
// ── XML tree ───────────────────────────────────────────────────────────────
|
||||||
@@ -226,6 +240,7 @@ function newObject(fields: Partial<JavaObject> & { className: string }): JavaObj
|
|||||||
properties: new Map(),
|
properties: new Map(),
|
||||||
calls: [],
|
calls: [],
|
||||||
presetElements: new Map(),
|
presetElements: new Map(),
|
||||||
|
presetEntries: new Map(),
|
||||||
fromGetter: fields.fromGetter ?? false,
|
fromGetter: fields.fromGetter ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -412,6 +427,7 @@ class Decoder {
|
|||||||
if (isJavaObject(target)) {
|
if (isJavaObject(target)) {
|
||||||
target.calls.push({ method, args });
|
target.calls.push({ method, args });
|
||||||
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
|
if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id);
|
||||||
|
if (method === 'get' && args.length === 1 && typeof args[0] === 'string') return this.getEntry(target, args[0], id);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -463,6 +479,18 @@ class Decoder {
|
|||||||
return placeholder;
|
return placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `map.get(key)`: the value put before, else an entry the map held before
|
||||||
|
* the file touched it — a placeholder that later reads return.
|
||||||
|
*/
|
||||||
|
private getEntry(target: JavaObject, key: string, id: string | undefined): JavaValue {
|
||||||
|
const put = mapEntries(target).find(([entryKey]) => entryKey === key);
|
||||||
|
if (put !== undefined) return put[1];
|
||||||
|
const placeholder = newObject({ className: id?.replace(/\d+$/, '') ?? 'unknown', id, fromGetter: true });
|
||||||
|
target.presetEntries.set(key, placeholder);
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
private setIndex(target: JavaValue | undefined, index: number, value: JavaValue): JavaValue {
|
private setIndex(target: JavaValue | undefined, index: number, value: JavaValue): JavaValue {
|
||||||
if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') {
|
if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') {
|
||||||
(target as JavaArray).items[index] = value;
|
(target as JavaArray).items[index] = value;
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Checked against Java (corpus.java.json, `system`). DHCP, NAT, the modem,
|
||||||
|
// file systems and applications are later layers.
|
||||||
|
//
|
||||||
|
// 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 { isJavaObject, isUnresolved, listItems, mapEntries, 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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
break;
|
||||||
|
case 'Vermittlungsrechner':
|
||||||
|
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn);
|
||||||
|
break;
|
||||||
|
case 'Gateway':
|
||||||
|
case 'Modem':
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (javaClass !== 'Switch' && javaClass !== 'Modem') {
|
||||||
|
result.manualRoutes = manualRoutes(value('weiterleitungstabelle'), 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 listItems(table.properties.get('manuelleTabelle')).map((row) =>
|
||||||
|
listItems(row).map((cell) => (typeof cell === 'string' || cell === null ? cell : null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 {
|
||||||
|
const properties = new Map<string, JavaValue>();
|
||||||
|
for (const object of firewallObjects(software)) {
|
||||||
|
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'), false, '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);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// The network drawn in a .fls file — devices, cables, documentation items —
|
// The network drawn in a .fls file — devices, cables, documentation items —
|
||||||
// read from the decoded JavaBeans graph (javabeans.ts) the way Java FILIUS
|
// read from the decoded JavaBeans graph (javabeans.ts) the way Java FILIUS
|
||||||
// loads it (SzenarioVerwaltung.netzwerkLaden). Only the topology: system
|
// loads it (SzenarioVerwaltung.netzwerkLaden), with the system software
|
||||||
// software, file systems and applications are later layers, which start
|
// settings the topology needs (systemSoftware.ts). File systems and
|
||||||
// from `FlsNode.source`.
|
// applications are later layers, which start from `FlsNode.source`.
|
||||||
//
|
//
|
||||||
// The result is Java's view, indexed like Java's lists, and is checked
|
// The result is Java's view, indexed like Java's lists, and is checked
|
||||||
// against Java itself (corpus.java.json). The editor model (node kinds, UUIDs,
|
// against Java itself (corpus.java.json). The editor model (node kinds, UUIDs,
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import { isJavaObject, isUnresolved, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js';
|
import { isJavaObject, isUnresolved, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js';
|
||||||
import { javaIpCheck } from './javaIp.js';
|
import { javaIpCheck } from './javaIp.js';
|
||||||
|
import { readSystemSoftware, type FlsSystemSoftware } from './systemSoftware.js';
|
||||||
|
|
||||||
export type FlsNodeClass = 'Rechner' | 'Notebook' | 'Switch' | 'Vermittlungsrechner' | 'Gateway' | 'Modem';
|
export type FlsNodeClass = 'Rechner' | 'Notebook' | 'Switch' | 'Vermittlungsrechner' | 'Gateway' | 'Modem';
|
||||||
|
|
||||||
@@ -44,6 +45,8 @@ export interface FlsNode {
|
|||||||
interfaces: FlsInterface[];
|
interfaces: FlsInterface[];
|
||||||
/** Interfaces, or the ports of a Switch or Modem. */
|
/** Interfaces, or the ports of a Switch or Modem. */
|
||||||
portCount: number;
|
portCount: number;
|
||||||
|
/** SSIDs, routes, RIP, firewall (systemSoftware.ts). */
|
||||||
|
system: FlsSystemSoftware;
|
||||||
/** The node's JavaBean (`filius.hardware.knoten.*`), for the software layers. */
|
/** The node's JavaBean (`filius.hardware.knoten.*`), for the software layers. */
|
||||||
source: JavaObject;
|
source: JavaObject;
|
||||||
}
|
}
|
||||||
@@ -191,6 +194,9 @@ class Reader {
|
|||||||
useMacAsName: info.host && this.booleanProperty(device, 'useMACAsName', `node ${itemIndex + 1}`),
|
useMacAsName: info.host && this.booleanProperty(device, 'useMACAsName', `node ${itemIndex + 1}`),
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
portCount: 0,
|
portCount: 0,
|
||||||
|
system: readSystemSoftware(device, simpleName, (message) =>
|
||||||
|
this.warnings.push(`node ${itemIndex + 1}: ${message}`),
|
||||||
|
),
|
||||||
source: device,
|
source: device,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -214,8 +214,9 @@ function flsNode(javaClass: FlsNode['javaClass'], fields: Partial<FlsNode> = {})
|
|||||||
useMacAsName: false,
|
useMacAsName: false,
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
portCount: 0,
|
portCount: 0,
|
||||||
|
system: {},
|
||||||
source: { kind: 'object', className: javaClass, id: undefined, args: [], staticField: undefined,
|
source: { kind: 'object', className: javaClass, id: undefined, args: [], staticField: undefined,
|
||||||
factoryMethod: undefined, properties: new Map(), calls: [], presetElements: new Map(), fromGetter: false },
|
factoryMethod: undefined, properties: new Map(), calls: [], presetElements: new Map(), presetEntries: new Map(), fromGetter: false },
|
||||||
...fields,
|
...fields,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,33 @@ describe('XMLDecoder rules', () => {
|
|||||||
expect(listItems(list)).toHaveLength(3);
|
expect(listItems(list)).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the entries a constructor made in a map, reached by get(key), and shares them", () => {
|
||||||
|
// A router's operating system installs its web server in the constructor;
|
||||||
|
// the file reaches it through the map and changes it.
|
||||||
|
const [system] = decodeJavaBeans(
|
||||||
|
doc(`<object class="filius.software.system.VermittlungsrechnerBetriebssystem">
|
||||||
|
<void id="HashMap0" property="installierteAnwendungen">
|
||||||
|
<void id="WebServer0" method="get"><string>filius.software.www.WebServer</string>
|
||||||
|
<void property="aktiv"><boolean>true</boolean></void>
|
||||||
|
</void>
|
||||||
|
<void method="get"><string>filius.software.www.WebServer</string>
|
||||||
|
<void property="port"><int>8080</int></void>
|
||||||
|
</void>
|
||||||
|
<void method="put"><string>extra</string><int>1</int></void>
|
||||||
|
</void>
|
||||||
|
</object>`),
|
||||||
|
);
|
||||||
|
|
||||||
|
const entries = mapEntries(object(system).properties.get('installierteAnwendungen'));
|
||||||
|
expect(entries.map(([key]) => key)).toEqual(['filius.software.www.WebServer', 'extra']);
|
||||||
|
const server = object(entries[0]![1]);
|
||||||
|
expect(server).toMatchObject({ className: 'WebServer', id: 'WebServer0', fromGetter: true });
|
||||||
|
expect([...server.properties]).toEqual([
|
||||||
|
['aktiv', true],
|
||||||
|
['port', 8080],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('replays add at an index and set by index in order', () => {
|
it('replays add at an index and set by index in order', () => {
|
||||||
const [list] = decodeJavaBeans(
|
const [list] = decodeJavaBeans(
|
||||||
doc(`<object class="java.util.ArrayList">
|
doc(`<object class="java.util.ArrayList">
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
|
|||||||
node.javaClass === 'Switch' || node.javaClass === 'Modem'
|
node.javaClass === 'Switch' || node.javaClass === 'Modem'
|
||||||
? Array.from({ length: node.portCount }, () => ({}))
|
? Array.from({ length: node.portCount }, () => ({}))
|
||||||
: node.interfaces.map((nic) => ({ ...nic, mac: nic.mac ?? null })),
|
: node.interfaces.map((nic) => ({ ...nic, mac: nic.mac ?? null })),
|
||||||
|
system: {
|
||||||
|
...node.system,
|
||||||
|
...('ssid' in node.system ? { ssid: node.system.ssid ?? null } : {}),
|
||||||
|
...(node.system.firewall === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
firewall: {
|
||||||
|
...node.system.firewall,
|
||||||
|
rules: node.system.firewall.rules.map((rule) => [
|
||||||
|
rule.srcIP,
|
||||||
|
rule.srcMask,
|
||||||
|
rule.destIP,
|
||||||
|
rule.destMask,
|
||||||
|
rule.port,
|
||||||
|
rule.protocol,
|
||||||
|
rule.action,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
cables: topology.links.map((link) => ({
|
cables: topology.links.map((link) => ({
|
||||||
ends: link.ends.map((end) => [end.node, end.port]),
|
ends: link.ends.map((end) => [end.node, end.port]),
|
||||||
@@ -177,6 +197,44 @@ describe('readTopology rules', () => {
|
|||||||
expect(wrongFirst.nodes).toEqual([]);
|
expect(wrongFirst.nodes).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reads a router's firewall wherever the file reaches it, with Java's defaults for what it leaves out", () => {
|
||||||
|
// Settings under the installed application and under the web server's
|
||||||
|
// plug-in belong to the one firewall the constructor made.
|
||||||
|
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner">
|
||||||
|
<void property="systemSoftware"><void property="installierteAnwendungen">
|
||||||
|
<void method="get"><string>filius.software.firewall.Firewall</string>
|
||||||
|
<void property="allowRelatedPackets"><boolean>false</boolean></void>
|
||||||
|
<void property="ruleset">
|
||||||
|
<void method="add"><object class="filius.software.firewall.FirewallRule" id="FirewallRule0">
|
||||||
|
<void class="filius.software.firewall.FirewallRule" method="getField"><string>srcIP</string>
|
||||||
|
<void method="set"><object idref="FirewallRule0"/><string>10.0.0.0</string></void></void>
|
||||||
|
</object></void>
|
||||||
|
<void method="add"><object class="filius.software.firewall.FirewallRule">
|
||||||
|
<string>1.1.1.1</string><string>255.255.255.255</string><string></string><string></string>
|
||||||
|
<int>53</int><short>17</short><short>1</short>
|
||||||
|
</object></void>
|
||||||
|
</void>
|
||||||
|
</void>
|
||||||
|
<void method="get"><string>filius.software.www.WebServer</string>
|
||||||
|
<void property="plugins"><void method="get"><string>konfig</string>
|
||||||
|
<void property="firewall"><void property="activated"><boolean>true</boolean></void></void>
|
||||||
|
</void></void>
|
||||||
|
</void>
|
||||||
|
</void></void></object>`)}</object>`);
|
||||||
|
|
||||||
|
expect(nodes[0]!.system.firewall).toEqual({
|
||||||
|
activated: true,
|
||||||
|
defaultPolicy: 0,
|
||||||
|
dropICMP: false,
|
||||||
|
filterSYNSegmentsOnly: false,
|
||||||
|
filterUdp: true,
|
||||||
|
rules: [
|
||||||
|
{ srcIP: '10.0.0.0', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 6, action: 0 },
|
||||||
|
{ srcIP: '1.1.1.1', srcMask: '255.255.255.255', destIP: '', destMask: '', port: 53, protocol: 17, action: 1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("reads colors as Java's constructors do and keeps an invalid one unset", () => {
|
it("reads colors as Java's constructors do and keeps an invalid one unset", () => {
|
||||||
const colors = ['<int>16711935</int>', '<int>1</int><int>2</int><int>3</int>', '<int>1</int><int>2</int><int>300</int><int>4</int>'];
|
const colors = ['<int>16711935</int>', '<int>1</int><int>2</int><int>3</int>', '<int>1</int><int>2</int><int>300</int><int>4</int>'];
|
||||||
const { documentation, warnings } = topologyOf(`<object class="java.util.LinkedList"/><object class="java.util.LinkedList"/>
|
const { documentation, warnings } = topologyOf(`<object class="java.util.LinkedList"/><object class="java.util.LinkedList"/>
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ virtuellen Bildschirm statt „headless".
|
|||||||
`SzenarioVerwaltung.netzwerkLaden` (per Reflection) — Javas Weg, kein Nachbau.
|
`SzenarioVerwaltung.netzwerkLaden` (per Reflection) — Javas Weg, kein Nachbau.
|
||||||
* Stand: `gitlab.com/filius1/filius` @ `6640354` (2.14.0), wird beim ersten
|
* Stand: `gitlab.com/filius1/filius` @ `6640354` (2.14.0), wird beim ersten
|
||||||
Lauf nach `.upstream/` geholt und gebaut.
|
Lauf nach `.upstream/` geholt und gebaut.
|
||||||
* **MAC-Adressen**, die nicht in der Datei stehen, würfelt Filius beim Anlegen
|
* **Gewürfeltes:** Was nicht in der Datei steht, würfelt Filius zum Teil
|
||||||
der Netzwerkkarte. Sie erscheinen als `null`; zwei Läufe sind byte-gleich.
|
(MAC-Adressen, die SSID eines Switches). Jede Datei wird deshalb **zweimal**
|
||||||
* Ob eine MAC in der Datei steht, wird **ohne Rücksicht auf Groß-/Kleinschreibung**
|
geladen; jeder Wert, der sich zwischen beiden Läufen unterscheidet,
|
||||||
geprüft: `getMac()` schreibt groß, acht Korpus-Dateien speichern klein (bis
|
erscheint als `null`. Zwei Läufe des Werkzeugs sind byte-gleich. (Bis
|
||||||
29.09.2026 erschienen sie fälschlich als `null`).
|
29.09.2026 suchte es die MAC im Dateitext und hielt dabei acht
|
||||||
|
kleingeschriebene für gewürfelt.)
|
||||||
* Bisher ausgelesen: Geräte (Klasse, Name, Beschriftung, Position,
|
* Bisher ausgelesen: Geräte (Klasse, Name, Beschriftung, Position,
|
||||||
Schnittstellen, bei Rechner/Notebook `useIPAsName`/`useMACAsName`), Kabel
|
Schnittstellen, bei Rechner/Notebook `useIPAsName`/`useMACAsName`), unter
|
||||||
(Enden als Gerät/Schnittstelle), Doku-Elemente.
|
`system` SSID, Lernzeit des Switches, RIP, manuelle Routen und die Firewall
|
||||||
|
von Router und Heimrouter; Kabel (Enden als Gerät/Schnittstelle),
|
||||||
|
Doku-Elemente.
|
||||||
Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise.
|
Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise.
|
||||||
|
|
||||||
## Getter-Standardwerte
|
## Getter-Standardwerte
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import filius.hardware.knoten.InternetKnoten;
|
|||||||
import filius.hardware.knoten.Knoten;
|
import filius.hardware.knoten.Knoten;
|
||||||
import filius.hardware.knoten.LokalerKnoten;
|
import filius.hardware.knoten.LokalerKnoten;
|
||||||
import filius.rahmenprogramm.SzenarioVerwaltung;
|
import filius.rahmenprogramm.SzenarioVerwaltung;
|
||||||
|
import filius.software.firewall.Firewall;
|
||||||
|
import filius.software.firewall.FirewallRule;
|
||||||
|
import filius.software.system.Betriebssystem;
|
||||||
|
import filius.software.system.GatewayFirmware;
|
||||||
|
import filius.software.system.InternetKnotenBetriebssystem;
|
||||||
|
import filius.software.system.SwitchFirmware;
|
||||||
|
import filius.software.system.SystemSoftware;
|
||||||
|
import filius.software.system.VermittlungsrechnerBetriebssystem;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
@@ -27,12 +35,12 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
@@ -73,8 +81,17 @@ public class FlsOracle {
|
|||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the file twice. What FILIUS does not find in a file it rolls at
|
||||||
|
* random (MAC addresses, a switch's SSID) — such values differ between
|
||||||
|
* the two loads and are written as null, so that two runs are byte-equal.
|
||||||
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
static Map<String, Object> read(Path file) throws Exception {
|
static Map<String, Object> read(Path file) throws Exception {
|
||||||
|
return (Map<String, Object>) stable(load(file), load(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Object> load(Path file) throws Exception {
|
||||||
Path xml = Files.createTempFile("konfiguration", ".xml");
|
Path xml = Files.createTempFile("konfiguration", ".xml");
|
||||||
try {
|
try {
|
||||||
extractConfiguration(file, xml);
|
extractConfiguration(file, xml);
|
||||||
@@ -85,12 +102,27 @@ public class FlsOracle {
|
|||||||
List.class, List.class);
|
List.class, List.class);
|
||||||
load.setAccessible(true);
|
load.setAccessible(true);
|
||||||
boolean success = (Boolean) load.invoke(null, xml.toString(), nodes, cables, docs);
|
boolean success = (Boolean) load.invoke(null, xml.toString(), nodes, cables, docs);
|
||||||
return describe(success, nodes, cables, docs, Files.readString(xml, StandardCharsets.UTF_8));
|
return describe(success, nodes, cables, docs);
|
||||||
} finally {
|
} finally {
|
||||||
Files.deleteIfExists(xml);
|
Files.deleteIfExists(xml);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The two descriptions with every value that differs between them replaced by null. */
|
||||||
|
static Object stable(Object first, Object second) {
|
||||||
|
if (first instanceof Map<?, ?> left && second instanceof Map<?, ?> right && left.keySet().equals(right.keySet())) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
for (Object key : left.keySet()) result.put(key.toString(), stable(left.get(key), right.get(key)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (first instanceof List<?> left && second instanceof List<?> right && left.size() == right.size()) {
|
||||||
|
List<Object> result = new ArrayList<>();
|
||||||
|
for (int index = 0; index < left.size(); index++) result.add(stable(left.get(index), right.get(index)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return java.util.Objects.equals(first, second) ? first : null;
|
||||||
|
}
|
||||||
|
|
||||||
static void extractConfiguration(Path archive, Path target) throws Exception {
|
static void extractConfiguration(Path archive, Path target) throws Exception {
|
||||||
try (ZipInputStream zip = new ZipInputStream(Files.newInputStream(archive))) {
|
try (ZipInputStream zip = new ZipInputStream(Files.newInputStream(archive))) {
|
||||||
for (ZipEntry entry; (entry = zip.getNextEntry()) != null;) {
|
for (ZipEntry entry; (entry = zip.getNextEntry()) != null;) {
|
||||||
@@ -104,7 +136,7 @@ public class FlsOracle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, Object> describe(boolean success, List<GUIKnotenItem> nodes, List<GUIKabelItem> cables,
|
static Map<String, Object> describe(boolean success, List<GUIKnotenItem> nodes, List<GUIKabelItem> cables,
|
||||||
List<GUIDocuItem> docs, String xmlText) {
|
List<GUIDocuItem> docs) {
|
||||||
Map<Port, int[]> portOwners = new IdentityHashMap<>();
|
Map<Port, int[]> portOwners = new IdentityHashMap<>();
|
||||||
List<Object> nodeList = new ArrayList<>();
|
List<Object> nodeList = new ArrayList<>();
|
||||||
for (int nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) {
|
for (int nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) {
|
||||||
@@ -128,13 +160,8 @@ public class FlsOracle {
|
|||||||
Map<String, Object> port = new LinkedHashMap<>();
|
Map<String, Object> port = new LinkedHashMap<>();
|
||||||
port.put("ip", nic.getIp());
|
port.put("ip", nic.getIp());
|
||||||
port.put("mask", nic.getSubnetzMaske());
|
port.put("mask", nic.getSubnetzMaske());
|
||||||
// Without a MAC address in the file, FILIUS rolls a random one when it
|
// Random unless the file has one; see read().
|
||||||
// creates the interface; only a MAC that stands in the file is stable.
|
port.put("mac", nic.getMac());
|
||||||
String mac = nic.getMac();
|
|
||||||
// getMac() upper-cases; the file may not.
|
|
||||||
boolean macInFile = mac != null && Pattern
|
|
||||||
.compile(">" + Pattern.quote(mac) + "<", Pattern.CASE_INSENSITIVE).matcher(xmlText).find();
|
|
||||||
port.put("mac", macInFile ? mac : null);
|
|
||||||
port.put("gateway", nic.getGateway());
|
port.put("gateway", nic.getGateway());
|
||||||
port.put("dns", nic.getDns());
|
port.put("dns", nic.getDns());
|
||||||
ports.add(port);
|
ports.add(port);
|
||||||
@@ -148,6 +175,7 @@ public class FlsOracle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
json.put("ports", ports);
|
json.put("ports", ports);
|
||||||
|
json.put("system", systemSoftware(node));
|
||||||
nodeList.add(json);
|
nodeList.add(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +217,45 @@ public class FlsOracle {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The settings of a node's system software the topology layer reads. */
|
||||||
|
static Map<String, Object> systemSoftware(Knoten node) {
|
||||||
|
Map<String, Object> json = new LinkedHashMap<>();
|
||||||
|
SystemSoftware software = node.getSystemSoftware();
|
||||||
|
if (software instanceof SwitchFirmware firmware) {
|
||||||
|
json.put("ssid", firmware.getSSID());
|
||||||
|
json.put("retentionTime", firmware.getRetentionTime());
|
||||||
|
}
|
||||||
|
if (software instanceof Betriebssystem system) json.put("ssid", system.getSsid());
|
||||||
|
if (software instanceof VermittlungsrechnerBetriebssystem system) json.put("ripEnabled", system.isRipEnabled());
|
||||||
|
if (software instanceof InternetKnotenBetriebssystem system) {
|
||||||
|
List<Object> routes = new ArrayList<>();
|
||||||
|
if (system.getWeiterleitungstabelle() != null && system.getWeiterleitungstabelle().getManuelleTabelle() != null) {
|
||||||
|
for (String[] route : system.getWeiterleitungstabelle().getManuelleTabelle()) {
|
||||||
|
routes.add(route == null ? null : Arrays.asList(route));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.put("manualRoutes", routes);
|
||||||
|
}
|
||||||
|
Firewall firewall = software instanceof VermittlungsrechnerBetriebssystem system ? system.holeFirewall()
|
||||||
|
: software instanceof GatewayFirmware firmware ? firmware.holeFirewall() : null;
|
||||||
|
if (firewall != null) {
|
||||||
|
Map<String, Object> settings = new LinkedHashMap<>();
|
||||||
|
settings.put("activated", firewall.isActivated());
|
||||||
|
settings.put("defaultPolicy", (int) firewall.getDefaultPolicy());
|
||||||
|
settings.put("dropICMP", firewall.getDropICMP());
|
||||||
|
settings.put("filterSYNSegmentsOnly", firewall.getFilterSYNSegmentsOnly());
|
||||||
|
settings.put("filterUdp", firewall.getFilterUdp());
|
||||||
|
List<Object> rules = new ArrayList<>();
|
||||||
|
for (FirewallRule rule : firewall.getRuleset()) {
|
||||||
|
rules.add(Arrays.asList(rule.srcIP, rule.srcMask, rule.destIP, rule.destMask, rule.port,
|
||||||
|
(int) rule.protocol, (int) rule.action));
|
||||||
|
}
|
||||||
|
settings.put("rules", rules);
|
||||||
|
json.put("firewall", settings);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
/** Just enough JSON: maps, lists, strings, numbers, booleans, null. */
|
/** Just enough JSON: maps, lists, strings, numbers, booleans, null. */
|
||||||
static final class Json {
|
static final class Json {
|
||||||
static String write(Object value, String indent) {
|
static String write(Object value, String indent) {
|
||||||
|
|||||||
Reference in New Issue
Block a user