feat(core): the firewall settings page of routers after Java, measured against Java

The page konfig changes the firewall at run time (into the snapshot, back
into the project through projectWithSimulationChanges). KonfigCases feeds
Java's plug-in directly (25 cases, de and en); RouterWebRun plays two
stories over the network. Ten Java bugs named, not adopted. The web server
answers a method not written GET or POST with 501, as Java does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 18:13:31 +00:00
co-authored by Claude Opus 5.5
parent 7dac57cbbe
commit 8438b42018
23 changed files with 13229 additions and 24 deletions
File diff suppressed because one or more lines are too long
@@ -81,6 +81,11 @@ const TEXTS: Record<PageLanguage, PageTexts> = {
},
};
/** jfirewalldialog_msg33, msg34: a rule's action as the pages name it. */
export function firewallActionText(action: 'accept' | 'drop', language: PageLanguage): string {
return action === 'accept' ? TEXTS[language].accept : TEXTS[language].drop;
}
/** 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];
@@ -114,7 +119,7 @@ export function firewallLogText(entry: FirewallLogEntry, language: PageLanguage)
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}`;
return `${texts.ruleMatched} #${entry.ruleNumber} (${firewallRuleText(entry.rule, entry.firstInterface)}) -> ${firewallActionText(entry.accepted ? 'accept' : 'drop', language)}`;
}
/** FirewallWebLog.holeHtmlSeite: the log since the engine was made. */
+3 -1
View File
@@ -129,6 +129,8 @@ export class WebServer extends TcpServerProgram {
});
const parsed = parseHttpRequest(message);
if (parsed.type === 'invalid') return response(parsed.statusCode);
// HTTPNachricht reads the method in any case; the server answers only GET and POST as written.
if (parsed.request.method !== HTTP_GET && parsed.request.method !== HTTP_POST) return response(501);
const path = httpPath(parsed.request.path);
let file = path === '/' ? 'index.html' : path.startsWith('/') ? path.slice(1) : path;
let folder = this.virtualHostFolder(parsed.request.host);
@@ -189,7 +191,7 @@ function javaSplit(text: string, separator: string): string[] {
/** EingabenUeberpruefung.musterDomain and musterIpAdresse. */
const DOMAIN_PATTERN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9]){0,1}(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]){0,1})*){0,1}\.{0,1}$/;
const IP_ADDRESS_PATTERN =
export const IP_ADDRESS_PATTERN =
/^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
function isHostName(text: string): boolean {
+18 -3
View File
@@ -116,6 +116,7 @@ import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.
import { EchoServer, SimpleClient } from './apps/echo.js';
import { WebBrowser, WebServer, type WebServerPage } from './apps/web.js';
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
import { VirtualFileSystem } from '../model/virtualFileSystem.js';
import {
PRODUCTION_TRACE_RETENTION,
@@ -1152,7 +1153,17 @@ export class NetworkRuntimeEngine {
const files = new VirtualFileSystem();
files.createDirectory('/webserver', true);
files.writeTextFile('/webserver/index.html', routerIndexPage(kind, language));
const pages = new Map<string, WebServerPage>([['log', () => firewallLogPage(this.firewallLog(nodeId), language)]]);
const pages = new Map<string, WebServerPage>([
['log', () => firewallLogPage(this.firewallLog(nodeId), language)],
[
'konfig',
(postData) => {
const result = applyFirewallSettings(this.firewallConfiguration(nodeId), postData);
if (result.configuration !== this.firewallConfiguration(nodeId)) this.setFirewallConfiguration(nodeId, result.configuration);
return firewallSettingsPage(result.configuration, language, result.rejected);
},
],
]);
if (kind === 'router') {
pages.set('routes', () => {
const rip = this.ripTable(nodeId);
@@ -5096,6 +5107,11 @@ export class NetworkRuntimeEngine {
// ── Firewall ─────────────────────────────────────────────────────────────
/** The node's firewall as it is now: changed at run time (the page konfig), it is in the snapshot, and so in the project saved from it. */
firewallConfiguration(nodeId: Id): FirewallConfiguration {
return this.stateValue.topologySnapshot.firewallConfigurationsByNodeId.get(nodeId) ?? firewallConfiguration();
}
setFirewallConfiguration(nodeId: Id, configuration: FirewallConfiguration): void {
const configurations = new Map(this.stateValue.topologySnapshot.firewallConfigurationsByNodeId);
configurations.set(nodeId, configuration);
@@ -5111,8 +5127,7 @@ export class NetworkRuntimeEngine {
* recorded, including "firewall inactive".
*/
evaluateFirewall(packet: IPv4Packet, nodeId: Id, incomingInterfaceId?: Id, frameIdentity?: number): FirewallDecision {
const configuration =
this.stateValue.topologySnapshot.firewallConfigurationsByNodeId.get(nodeId) ?? firewallConfiguration();
const configuration = this.firewallConfiguration(nodeId);
const decide = (accepted: boolean, ruleIndex: number | undefined, detail: string): FirewallDecision =>
this.recordFirewallDecision(packet, frameIdentity, nodeId, incomingInterfaceId, accepted, ruleIndex, detail);
+35 -1
View File
@@ -41,7 +41,7 @@ function words(line: string): string[] {
}
/** Java's String.trim. */
function javaTrim(text: string): string {
export function javaTrim(text: string): string {
let start = 0;
let end = text.length;
while (start < end && text.charCodeAt(start) <= 0x20) start += 1;
@@ -211,3 +211,37 @@ export function decodeUriPath(path: string): string | undefined {
}
return new TextDecoder().decode(new Uint8Array(bytes));
}
/**
* URLDecoder.decode(text, "UTF-8"), as a form's data is read: "+" a space,
* each run of "%xy" the characters of those UTF-8 bytes; undefined where
* Java throws (an escape cut off or not hexadecimal; Java's parseInt also
* takes a sign there, and refuses only a negative value).
*/
export function javaUrlDecode(text: string): string | undefined {
let decoded = '';
let index = 0;
while (index < text.length) {
const character = text[index]!;
if (character === '+') {
decoded += ' ';
index += 1;
} else if (character === '%') {
const bytes: number[] = [];
while (index + 2 < text.length && text[index] === '%') {
const hex = text.slice(index + 1, index + 3);
if (!/^[+-]?[0-9A-Fa-f]+$/.test(hex)) return undefined;
const value = Number.parseInt(hex, 16);
if (value < 0) return undefined;
bytes.push(value);
index += 3;
}
if (index < text.length && text[index] === '%') return undefined;
decoded += new TextDecoder().decode(new Uint8Array(bytes));
} else {
decoded += character;
index += 1;
}
}
return decoded;
}