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>
169 lines
10 KiB
TypeScript
169 lines
10 KiB
TypeScript
// 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ä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ä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ültigkeit</th>\n <th colspan=2>Nä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>Ö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ä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);
|
|
}
|