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:
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. */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user