feat(core): the pages of routers and home routers after Java, measured against Java

Routers and home routers start a web server on port 80 with Java's index,
log and routes pages in the project's language (routerPages.ts); the
firewall keeps a log per node that outlives a new start; RIP routes carry
the telling router's public address. TCP counts sequence numbers and
segments in Java's characters, not bytes. RouterWebRun records six stories,
with firewalls now built by ScenarioNetwork. Not taken from Java: its router
server sometimes never listening (a race in setAktiv), and </titel>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 17:41:52 +00:00
co-authored by Claude Opus 5.5
parent 4cd5749350
commit 7dac57cbbe
29 changed files with 20398 additions and 39 deletions
@@ -0,0 +1,168 @@
// The pages of FILIUS' routers and home routers (filius.software.system:
// VermittlungsrechnerBetriebssystem, GatewayFirmware; the web server's
// plug-ins FirewallWebLog and VermittlungWeb), written as Java writes them:
// from its templates (tmpl/vermittlung_index, gateway_index, routing) and the
// texts of its message bundle, in the project's language. The pages go over
// the simulated network as they are, so they are written here, not by the
// editor.
//
// Departure from Java: the German log page closes its title with </title>
// (Java's bundle writes </titel>).
import type { FirewallRule } from '../config.js';
import type { FirewallLogEntry } from '../engine.js';
import { formatIPv4, maskAddress, parseIPv4 } from '../ipv4.js';
import type { RouteRow } from '../routeTable.js';
import { DIRECTLY_CONNECTED_SOURCE_MARKER } from '../config.js';
/** The languages FILIUS' pages come in (Java also has French). */
export type PageLanguage = 'de' | 'en';
/** Java's templates as its WebServer and WebServerPlugIn read them: each line ended by "\n". */
const TEMPLATES: Record<'routerIndex' | 'gatewayIndex' | 'routing', Record<PageLanguage, string>> = {
routerIndex: {
de: "<html>\n <head>\n <title>Administration des Vermittlungsrechners</title>\n </head>\n <body>\n\n <h1>Firewall-Administration</h1>\n <p> <a href=\"konfig\">Konfiguration</a> </p>\n <p> <a href=\"log\">Log-Eintr&auml;ge anzeigen</a> </p>\n\n <h1> Weiterleitung/Routing </h1>\n <p> <a href=\"routes\"> Anzeige des aktuellen Status der Weiterleitungs-/Routing-Tabelle </a></p>\n </body>\n</html>\n",
en: "<html>\n <head>\n <title>Router Administration</title>\n </head>\n <body>\n <h1>Firewall Administration</h1>\n <p> <a href=\"konfig\">Configuration</a> </p>\n <p> <a href=\"log\">Show Log</a> </p>\n\n <h1> Routing/Forwarding </h1>\n <p> <a href=\"routes\"> Show current routing/forwarding table </a> </p>\n \n </body>\n</html>\n",
},
gatewayIndex: {
de: "<html>\n <head>\n <title>Administration des Gateways</title>\n </head>\n <body>\n <h1>Firewall-Administration</h1>\n <p> <a href=\"konfig\">Konfiguration</a> </p>\n <p> <a href=\"log\">Log-Eintr&auml;ge anzeigen</a> </p>\n </body>\n</html>\n",
en: "<html>\n <head>\n <title>Gateway Administration</title>\n </head>\n <body>\n <h1>Firewall Administration</h1>\n <p> <a href=\"konfig\">Configuration</a> </p>\n <p> <a href=\"log\">Show Log</a> </p> \n </body>\n</html>\n",
},
routing: {
de: "<html>\n <head>\n <title>\n:title:\n </title>\n </head>\n <body>\n <h1>\n:title:\n </h1>\n\n <table border=1>\n <tr>\n <th colspan=2>Rechnernetz</th>\n <th>Hops</th>\n <th>G&uuml;ltigkeit</th>\n <th colspan=2>N&auml;chster Hop</th>\n </tr>\n <tr>\n <th>Adresse</th>\n <th>Netzmaske</th>\n <th></th>\n <th>(sec)</th>\n <th>Ziel</th>\n <th>&Ouml;ffentliche Adresse</th>\n </tr>\n\n:routing_entries:\n\n </table>\n <p>\n:hint:\n </p>\n </body>\n</html>\n",
en: "<html>\n <head>\n <title>\n:title:\n </title>\n </head>\n <body>\n <h1>\n:title:\n </h1>\n\n <table border=1>\n <tr>\n <th colspan=2>Network</th>\n <th>Hops</th>\n <th>Validity</th>\n <th colspan=2>Next Hop</th>\n </tr>\n <tr>\n <th>Address</th>\n <th>Netmask</th>\n <th></th>\n <th>(sec)</th>\n <th>Target</th>\n <th>Public Address</th>\n </tr>\n\n:routing_entries:\n\n </table>\n <p>\n:hint:\n </p>\n </body>\n</html>\n",
},
};
/** The texts of Java's message bundle the pages use. */
interface PageTexts {
readonly logHead: string;
readonly logBody: string;
readonly ruleMatched: string;
readonly icmpDropped: string;
readonly accept: string;
readonly drop: string;
readonly ripHint: string;
readonly unlimited: string;
readonly forwardingHint: string;
readonly forwardingTitle: string;
readonly ripTitle: string;
}
const TEXTS: Record<PageLanguage, PageTexts> = {
de: {
/** sw_firewallweblog_msg1, msg2 (</title>, where Java writes </titel>). */
logHead: '<html>\n\t<head>\n\t\t<title>Log-Einträge der Firewall</title>',
logBody: '\n\t</head>\n\t<body>\n\t\t<h2>Log-Eintr&auml;ge der Firewall</h2>',
/** sw_firewall_msg8, firewallthread_msg1, jfirewalldialog_msg33, msg34. */
ruleMatched: 'Firewall-Regel hat gegriffen:',
icmpDropped: 'ICMP-Paket verworfen:',
accept: 'akzeptieren',
drop: 'verwerfen',
/** sw_vermittlungweb_msg1, msg3, msg4, msg5, msg6. */
ripHint: 'Diese Tabelle wird durch das Routing Information Protocol (RIP) aufgebaut und aktualisiert.',
unlimited: 'unbegrenzt',
forwardingHint: 'Diese Tabelle wurde aus der IP-Konfiguration des Vermittlungsrechners und manuellen Einträgen erstellt.',
forwardingTitle: 'Routing-/Weiterleitungs-Tabelle',
ripTitle: 'Routing-Tabelle',
},
en: {
logHead: '<html>\n\t<head>\n\t\t<title>Log-data set entry of the firewall</title>',
logBody: '\n\t</head>\n\t<body>\n\t\t<h2>Log-data set entry of the firewall</h2>',
ruleMatched: 'Firewall rule matched:',
icmpDropped: 'Dropped ICMP packet:',
accept: 'accept',
drop: 'drop',
ripHint: 'This table is generated and updated by the Routing Information Protocol (RIP).',
unlimited: 'unlimited',
forwardingHint: 'This table was created from the IP configuration of the router and manually added entries of the forwarding table.',
forwardingTitle: 'Routing/Forwarding Table',
ripTitle: 'Routing Table',
},
};
/** The index page a router or home router keeps in /webserver (WebServer.erzeugeIndexDatei). */
export function routerIndexPage(kind: 'router' | 'gateway', language: PageLanguage): string {
return TEMPLATES[kind === 'router' ? 'routerIndex' : 'gatewayIndex'][language];
}
/** FirewallRule.toString(nics): the rule as the log names it. */
export function firewallRuleText(rule: FirewallRule, firstInterface: { readonly ipAddress: string; readonly subnetMask: string } | undefined): string {
const sameNetwork = rule.sourceIPAddress === DIRECTLY_CONNECTED_SOURCE_MARKER && firstInterface !== undefined;
let text = '';
if (rule.sourceIPAddress === '') {
text += '*/';
} else if (sameNetwork) {
const address = parseIPv4(firstInterface.ipAddress);
const mask = parseIPv4(firstInterface.subnetMask);
const network = address === undefined || mask === undefined ? firstInterface.ipAddress : formatIPv4(maskAddress(address, mask));
text += `${network}/${firstInterface.subnetMask} -> `;
} else {
text += `${rule.sourceIPAddress}/`;
}
if (!sameNetwork) text += rule.sourceSubnetMask === '' ? '* -> ' : `${rule.sourceSubnetMask} -> `;
text += rule.destinationIPAddress === '' ? '*/' : `${rule.destinationIPAddress}/`;
text += rule.destinationSubnetMask === '' ? '*; ' : `${rule.destinationSubnetMask}; `;
text += rule.protocol === 'all' ? '*:' : rule.protocol === 'tcp' ? 'TCP:' : rule.protocol === 'udp' ? 'UDP:' : '?:';
text += rule.port >= 0 ? `${rule.port} => ` : '* => ';
return text + (rule.action === 'accept' ? 'ACCEPT' : 'DROP');
}
/** A line of the firewall's log, as Firewall tells its observers. */
export function firewallLogText(entry: FirewallLogEntry, language: PageLanguage): string {
const texts = TEXTS[language];
if (entry.kind === 'icmpDropped') {
return `${texts.icmpDropped} ${entry.senderIPAddress} -> ${entry.receiverIPAddress} (code: ${entry.code}, type: ${entry.type})`;
}
return `${texts.ruleMatched} #${entry.ruleNumber} (${firewallRuleText(entry.rule, entry.firstInterface)}) -> ${entry.accepted ? texts.accept : texts.drop}`;
}
/** FirewallWebLog.holeHtmlSeite: the log since the engine was made. */
export function firewallLogPage(entries: readonly FirewallLogEntry[], language: PageLanguage): string {
const texts = TEXTS[language];
const lines = entries.map((entry) => `<br /> \n${firewallLogText(entry, language)}`).join('');
return `${texts.logHead}${texts.logBody}\n\n${lines}\n\n\t</body>\n</html>`;
}
function routingPage(title: string, entries: string, hint: string, language: PageLanguage): string {
return TEMPLATES.routing[language].replaceAll(':title:', title).replaceAll(':routing_entries:', entries).replaceAll(':hint:', hint);
}
/** VermittlungWeb.createForwardingTablePage: a router without RIP. */
export function forwardingTablePage(rows: readonly RouteRow[], language: PageLanguage): string {
const texts = TEXTS[language];
const entries = rows
.map((row) => {
const direct = row.nextHop === row.interfaceIPAddress;
const localhost = row.interfaceIPAddress === '127.0.0.1';
const cells =
`<td>${row.destinationNetwork}</td><td>${row.subnetMask}</td>` +
(direct || localhost ? '<td> 0 </td>' : '<td> ? </td>') +
`<td>${texts.unlimited}</td>` +
`<td><a href="http://${row.nextHop}/routes">${row.nextHop}</a></td><td> - </td>`;
if (direct || localhost) return `<tr style='background-color:#aaffaa'>${cells}</tr>`;
if (row.subnetMask === '0.0.0.0') return `<tr style='background-color:#aaaaff'>${cells}</tr>`;
return `<tr>${cells}</tr>`;
})
.join('');
return routingPage(texts.forwardingTitle, entries, texts.forwardingHint, language);
}
/** VermittlungWeb.createRoutingTablePage: a router with RIP; how long each learned route has left, in whole seconds. */
export function ripTablePage(rows: readonly RouteRow[], nowMilliseconds: number, language: PageLanguage): string {
const texts = TEXTS[language];
const entries = rows
.map((row) => {
const hops = row.metric ?? 16;
const validity = row.expiresAtMilliseconds === undefined ? texts.unlimited : String(Math.trunc((row.expiresAtMilliseconds - nowMilliseconds) / 1_000));
const publicAddress = row.hopPublicIPAddress ?? '';
const cells =
`<td>${row.destinationNetwork}</td><td>${row.subnetMask}</td><td>${hops}</td><td>${validity}</td><td>${row.nextHop}</td>` +
`<td><a href="http://${publicAddress}/routes">${publicAddress}</a></td>`;
if (hops === 0) return `<tr style='background-color:#aaffaa'>${cells}</tr>`;
if (hops === 16) return `<tr style='background-color:#ffaaaa'>${cells}</tr>`;
return `<tr>${cells}</tr>`;
})
.join('');
return routingPage(texts.ripTitle, entries, texts.ripHint, language);
}