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:
+10
-8
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@ export * from './runtime/apps/server.js';
|
||||
export * from './runtime/apps/echo.js';
|
||||
export * from './runtime/apps/web.js';
|
||||
export * from './runtime/apps/routerPages.js';
|
||||
export * from './runtime/apps/firewallSettings.js';
|
||||
export * from './runtime/http.js';
|
||||
export * from './util/equal.js';
|
||||
export * from './util/javaBase64.js';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Cases for tools/java-oracle's KonfigCases: FILIUS' firewall settings page
|
||||
# (konfig) fed with what a browser posts. Read by firewallSettings.test.ts.
|
||||
case getEmpty empty
|
||||
GET
|
||||
case getThree three
|
||||
GET
|
||||
POST
|
||||
case deactivate three
|
||||
POST btnSave=speichern
|
||||
POST firewallActivate=1&dropICMP=1&onlySYN=1&btnSave=speichern
|
||||
case defaultPolicy three
|
||||
POST defaultPolicy=accept&defPolSet=speichern
|
||||
POST defaultPolicy=xyz&defPolSet=speichern
|
||||
POST defaultPolicy=accept
|
||||
POST defaultPolicy=accept&defPolSet=speichern
|
||||
POST defPolSet=speichern
|
||||
case moveUp three
|
||||
POST radioRule=2&moveup=rauf
|
||||
POST radioRule=1&moveup=rauf
|
||||
POST radioRule=4&moveup=rauf
|
||||
POST moveup=rauf
|
||||
POST radioRule=3&radioRule=2&moveup=rauf
|
||||
POST radioRule=2&radioRule=abc&moveup=rauf
|
||||
POST radioRule=%2B3&moveup=rauf
|
||||
case moveDown three
|
||||
POST radioRule=1&movedown=runter
|
||||
POST radioRule=3&movedown=runter
|
||||
POST movedown=runter
|
||||
case moveDownZero three
|
||||
POST radioRule=0&movedown=runter
|
||||
case delete three
|
||||
POST radioRule=2&delrule=l%C3%B6schen
|
||||
POST radioRule=0&delrule=l%C3%B6schen
|
||||
POST radioRule=abc&delrule=l%C3%B6schen
|
||||
POST radioRule=9&delrule=l%C3%B6schen
|
||||
case addEmpty empty
|
||||
POST srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=80&action=accept&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=&destIP=&destMask=&protocol=UDP&port=-1&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=&destIP=&destMask=&protocol=tcp&port=+53+&action=ACCEPT&addrule=hinzuf%C3%BCgen
|
||||
POST addrule=x
|
||||
case addSource empty
|
||||
POST srcIP=10.0.0.0&srcMask=255.255.255.0&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen
|
||||
case addSourceMaskFirst empty
|
||||
POST srcMask=255.255.255.0&srcIP=10.0.0.0&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen
|
||||
case sourceRule sourceRule
|
||||
GET
|
||||
case addDestination empty
|
||||
POST srcIP=&srcMask=&destIP=20.0.0.0&destMask=255.0.0.0&protocol=UDP&port=&action=accept&addrule=hinzuf%C3%BCgen
|
||||
case addDestinationMaskFirst empty
|
||||
POST srcIP=&srcMask=&destMask=255.0.0.0&destIP=20.0.0.0&protocol=UDP&port=&action=accept&addrule=hinzuf%C3%BCgen
|
||||
case destinationRule destinationRule
|
||||
GET
|
||||
case errors three
|
||||
POST srcIP=10.0.0.300&srcMask=&destIP=&destMask=255.0.255.0&protocol=UDP&port=abc&action=accept&addrule=hinzuf%C3%BCgen
|
||||
GET
|
||||
POST radioRule=1&moveup=rauf
|
||||
POST srcIP=10.0.0.1&srcMask=1.2.3.4&destIP=&destMask=255.0.0.0&protocol=TCP&port=0&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=0.0.0.0&destIP=1.2.3.4&destMask=0.0.0.0&protocol=*&port=65536&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=080&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=%2B80&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=255.255.255.0.&destIP=&destMask=255.255.255&protocol=*&port=65535&action=drop&addrule=hinzuf%C3%BCgen
|
||||
case errorAndSave three
|
||||
POST srcIP=1.2.3&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&btnSave=speichern&addrule=hinzuf%C3%BCgen
|
||||
case markup empty
|
||||
POST srcIP=%3Cb%3E%22&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
|
||||
case dollar empty
|
||||
POST srcIP=%241&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
|
||||
case encoding empty
|
||||
POST srcIP=+&srcMask=&destIP=&destMask=&protocol=%2A&port=%38&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST \u000abtnSave=speichern&firewallActivate=1
|
||||
POST BTNSAVE=x&firewallActivate=1
|
||||
POST =x&btnSave
|
||||
case masks empty
|
||||
POST srcIP=&srcMask=%2B255.255.255.0&destIP=1.2.3&destMask=255.255.255.255.0&protocol=TCP&protocol=UDP&port=&action=drop&addrule=hinzuf%C3%BCgen
|
||||
POST srcIP=&srcMask=255.255.255.256&destIP=&destMask=255.255.255.255&protocol=*&port=&action=accept&action=drop&addrule=hinzuf%C3%BCgen
|
||||
case portZero portZero
|
||||
GET
|
||||
case ampersand empty
|
||||
POST srcIP=%26&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop&addrule=hinzuf%C3%BCgen
|
||||
# What Java's browser window posts (Swing's form: every field, the button pressed).
|
||||
case browserForm three
|
||||
POST firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=drop&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
|
||||
POST onlySYN=1&defaultPolicy=accept&defPolSet=speichern&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
|
||||
POST firewallActivate=1&onlySYN=1&defaultPolicy=drop&radioRule=3&moveup=rauf&srcIP=10.0.0.0&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
|
||||
POST firewallActivate=1&onlySYN=1&defaultPolicy=drop&srcIP=&srcMask=&destIP=&destMask=&protocol=UDP&port=53&action=accept&addrule=hinzuf%C3%BCgen
|
||||
POST firewallActivate=1&onlySYN=1&defaultPolicy=drop&radioRule=1&delrule=l%C3%B6schen&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop
|
||||
case malformed three
|
||||
POST radioRule=2&moveup=rauf&x=%zz
|
||||
POST radioRule=2&moveup=rauf&x=%
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,163 @@
|
||||
// The firewall settings page of routers and home routers (konfig,
|
||||
// apps/firewallSettings.ts) against Java FILIUS' own: tools/java-oracle's
|
||||
// KonfigCases fed Java's page with the steps of firewall-settings.cases.txt
|
||||
// (firewall-settings.<de|en>.java.json). Each step's page and the firewall
|
||||
// after it must be Java's, in both languages, except where Java errs.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
applyFirewallSettings,
|
||||
firewallConfiguration,
|
||||
firewallRule,
|
||||
firewallSettingsPage,
|
||||
javaUrlDecode,
|
||||
type FirewallConfiguration,
|
||||
type FirewallRule,
|
||||
type PageLanguage,
|
||||
} from '../src/index.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type JavaRule = [string, string, string, string, number, string, string];
|
||||
|
||||
interface JavaFirewall {
|
||||
isActive: boolean;
|
||||
dropIcmp: boolean;
|
||||
filterSynSegmentsOnly: boolean;
|
||||
defaultPolicy: string;
|
||||
rules: JavaRule[];
|
||||
}
|
||||
|
||||
interface JavaStep {
|
||||
post: string | null;
|
||||
page: string | null;
|
||||
firewall: JavaFirewall;
|
||||
}
|
||||
|
||||
interface JavaCase {
|
||||
name: string;
|
||||
start: string;
|
||||
steps: JavaStep[];
|
||||
}
|
||||
|
||||
function javaRule(rule: FirewallRule): JavaRule {
|
||||
return [rule.sourceIPAddress, rule.sourceSubnetMask, rule.destinationIPAddress, rule.destinationSubnetMask, rule.port, rule.protocol, rule.action];
|
||||
}
|
||||
|
||||
function asJava(configuration: FirewallConfiguration): JavaFirewall {
|
||||
return {
|
||||
isActive: configuration.isActive,
|
||||
dropIcmp: configuration.dropIcmp,
|
||||
filterSynSegmentsOnly: configuration.filterSynSegmentsOnly,
|
||||
defaultPolicy: configuration.defaultPolicy,
|
||||
rules: configuration.rules.map(javaRule),
|
||||
};
|
||||
}
|
||||
|
||||
/** KonfigCases' start states. */
|
||||
const STARTS: Record<string, FirewallConfiguration> = {
|
||||
empty: firewallConfiguration(),
|
||||
three: firewallConfiguration({
|
||||
isActive: true,
|
||||
rules: [
|
||||
firewallRule({ port: 80, protocol: 'tcp', action: 'accept' }),
|
||||
firewallRule({ sourceIPAddress: '999.999.999.999', destinationIPAddress: '20.0.0.0', destinationSubnetMask: '255.0.0.0', protocol: 'udp', action: 'drop' }),
|
||||
firewallRule({ sourceIPAddress: '10.0.0.0', sourceSubnetMask: '255.0.0.0', protocol: 'all', action: 'drop' }),
|
||||
],
|
||||
}),
|
||||
sourceRule: firewallConfiguration({ rules: [firewallRule({ sourceIPAddress: '10.0.0.0', sourceSubnetMask: '255.255.255.0', port: 80, protocol: 'tcp', action: 'drop' })] }),
|
||||
portZero: firewallConfiguration({ rules: [firewallRule({ port: 0, protocol: 'icmp', action: 'accept' })] }),
|
||||
destinationRule: firewallConfiguration({ rules: [firewallRule({ destinationIPAddress: '20.0.0.0', destinationSubnetMask: '255.0.0.0', protocol: 'udp', action: 'accept' })] }),
|
||||
};
|
||||
|
||||
type Cases = Record<string, JavaCase>;
|
||||
|
||||
const step = (cases: Cases, name: string, index: number) => cases[name]!.steps[index]!;
|
||||
|
||||
/** A step as Java means it: another step's page and firewall, which Java answers for the same state. */
|
||||
const asStep =
|
||||
(name: string, index: number) =>
|
||||
(cases: Cases, measured: JavaStep): JavaStep => ({ ...step(cases, name, index), post: measured.post });
|
||||
|
||||
/**
|
||||
* Where Java errs, each step as Java means it (the core's departures, see
|
||||
* firewallSettings.ts).
|
||||
*/
|
||||
const JAVA_BUGS: Record<string, Record<number, (cases: Cases, measured: JavaStep) => JavaStep>> = {
|
||||
// Java checks the source IP against the mask not read yet, and the mask
|
||||
// against the IP not read yet: it refuses every rule with an address,
|
||||
// whichever field comes first. The core takes the rule.
|
||||
addSource: { 0: asStep('sourceRule', 0) },
|
||||
addSourceMaskFirst: { 0: asStep('sourceRule', 0) },
|
||||
addDestination: { 0: asStep('destinationRule', 0) },
|
||||
addDestinationMaskFirst: { 0: asStep('destinationRule', 0) },
|
||||
// Java keeps the error of a failed add for every later page.
|
||||
errors: { 1: asStep('getThree', 0) },
|
||||
// Save and add at once: Java saves, but writes the add's errors as a bare
|
||||
// list and fills the form in. The core answers the saved firewall.
|
||||
errorAndSave: { 0: asStep('deactivate', 0) },
|
||||
// Java writes the form's text into the page as it came.
|
||||
markup: { 0: (_, measured) => ({ ...measured, page: measured.page!.replace('value="<b>""', 'value="<b>""') }) },
|
||||
// A "$" in the form's text: Java's replaceAll reads it as a group, throws,
|
||||
// and the page fails (500). The core answers as for any other bad address.
|
||||
dollar: { 0: (cases, measured) => ({ ...measured, page: step(cases, 'markup', 0).page!.replace('value="<b>""', 'value="$1"') }) },
|
||||
// An "&" typed into a field: Java decodes the data before it splits it,
|
||||
// reads an empty source and adds a rule. The core keeps the "&" in its
|
||||
// field and answers as for any other bad address.
|
||||
ampersand: { 0: (cases) => ({ ...step(cases, 'markup', 0), page: step(cases, 'markup', 0).page!.replace('value="<b>""', 'value="&"') }) },
|
||||
// Moving rule 0 down: Java throws (its worker thread ends, no page comes).
|
||||
moveDownZero: { 0: asStep('getThree', 0) },
|
||||
// An escape that is not one: Java's decoder throws. The core reads the
|
||||
// data as it came, as Java's code means to.
|
||||
malformed: { 0: asStep('moveUp', 0), 1: asStep('getThree', 0) },
|
||||
};
|
||||
|
||||
function javaCases(language: PageLanguage): Cases {
|
||||
const list = JSON.parse(readFileSync(join(here, `firewall-settings.${language}.java.json`), 'utf8')) as JavaCase[];
|
||||
return Object.fromEntries(list.map((each) => [each.name, each]));
|
||||
}
|
||||
|
||||
describe('the firewall settings page as Java FILIUS answers it', () => {
|
||||
for (const language of ['de', 'en'] as const) {
|
||||
const cases = javaCases(language);
|
||||
|
||||
it(`corrects Java only where it errs (${language})`, () => {
|
||||
for (const [name, steps] of Object.entries(JAVA_BUGS)) {
|
||||
for (const [index, correct] of Object.entries(steps)) {
|
||||
const measured = step(cases, name, Number(index));
|
||||
expect(correct(cases, measured), `${name} ${index}`).not.toEqual(measured);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const [name, javaCase] of Object.entries(cases)) {
|
||||
it(`${name} (${language})`, () => {
|
||||
let configuration = STARTS[javaCase.start]!;
|
||||
for (const [index, measured] of javaCase.steps.entries()) {
|
||||
const expected = JAVA_BUGS[name]?.[index]?.(cases, measured) ?? measured;
|
||||
const result = applyFirewallSettings(configuration, measured.post ?? undefined);
|
||||
configuration = result.configuration;
|
||||
expect(asJava(configuration), `step ${index}: the firewall`).toEqual(expected.firewall);
|
||||
expect(firewallSettingsPage(configuration, language, result.rejected), `step ${index}: the page`).toBe(expected.page);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("a form's data as Java's URLDecoder reads it", () => {
|
||||
it('turns + into spaces and runs of escapes into UTF-8 characters', () => {
|
||||
expect(javaUrlDecode('a+b%C3%BC%E2%82%AC%41')).toBe('a bü€A');
|
||||
expect(javaUrlDecode('%+f%-0x')).toBe('\u000f\u0000x');
|
||||
});
|
||||
|
||||
it('fails where Java throws: an escape cut off, not hexadecimal, or negative', () => {
|
||||
expect(javaUrlDecode('%zz')).toBeUndefined();
|
||||
expect(javaUrlDecode('%4')).toBeUndefined();
|
||||
expect(javaUrlDecode('%41%')).toBeUndefined();
|
||||
expect(javaUrlDecode('%-1')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
firewallLogPage,
|
||||
firewallRule,
|
||||
firewallRuleText,
|
||||
firewallSettingsPage,
|
||||
forwardingTablePage,
|
||||
interfaceKey,
|
||||
javaTextLength,
|
||||
@@ -140,6 +141,42 @@ describe("a router's firewall log", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a router's settings page (konfig)", () => {
|
||||
const pageOf = (engine: NetworkRuntimeEngine) => {
|
||||
const pages = engine.webBrowser(LEFT)!.log.all.flatMap((line) => (line.kind === 'page' ? [line] : []));
|
||||
return pages[pages.length - 1];
|
||||
};
|
||||
const ask = (engine: NetworkRuntimeEngine, data?: string) => {
|
||||
engine.webBrowser(LEFT)!.open('10.0.0.1/konfig', data);
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 5_000 });
|
||||
return pageOf(engine);
|
||||
};
|
||||
|
||||
it('changes the firewall at run time, into the snapshot; a page without data changes nothing', () => {
|
||||
const engine = startedEngine({ ...network(), webBrowserNodeIds: new Set([LEFT]) });
|
||||
const before = engine.state.topologySnapshot;
|
||||
expect(ask(engine)).toMatchObject({ statusCode: 200, contentType: 'text/html' });
|
||||
expect(engine.state.topologySnapshot).toBe(before);
|
||||
const page = ask(engine, 'firewallActivate=1&btnSave=speichern&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop');
|
||||
expect(engine.state.topologySnapshot.firewallConfigurationsByNodeId.get(ROUTER)).toEqual(
|
||||
firewallConfiguration({ isActive: true, defaultPolicy: 'accept', filterSynSegmentsOnly: false, rules: [firewallRule({ protocol: 'udp', port: 7, action: 'drop' })] }),
|
||||
);
|
||||
// The browser reads the answer as HTTPNachricht does: without its last empty lines.
|
||||
expect(page?.body).toBe(firewallSettingsPage(engine.firewallConfiguration(ROUTER), 'de').replace(/\n+$/, ''));
|
||||
});
|
||||
|
||||
it("answers in the project's language, and on a home router as on a router", () => {
|
||||
const gateway = network({ language: 'en', webBrowserNodeIds: new Set([LEFT]) });
|
||||
const engine = startedEngine({ ...gateway, nodes: gateway.nodes.map((node) => (node.id === ROUTER ? { ...node, kind: 'gateway' as const } : node)) });
|
||||
const page = ask(engine, 'srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=22&action=accept&addrule=add');
|
||||
expect(page?.body).toContain('<title>Router Firewall Configuration</title>');
|
||||
expect(engine.firewallConfiguration(ROUTER).rules).toEqual([firewallRule({ protocol: 'udp', port: 7, action: 'drop' }), firewallRule({ protocol: 'tcp', port: 22, action: 'accept' })]);
|
||||
const rejected = ask(engine, 'srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=0&action=accept&addrule=add');
|
||||
expect(rejected?.body).toContain('<font color="red">Erroneous input(s):<ul><li>Port invalid</li></ul></font><br>');
|
||||
expect(engine.firewallConfiguration(ROUTER).rules).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a RIP route's public address (hopPublicIp)", () => {
|
||||
it('is the telling router\'s, also when a shorter way replaces the route', () => {
|
||||
const engine = startedEngine(network({ ripEnabledByNodeId: new Map([[ROUTER, true]]), firewallConfigurationsByNodeId: new Map() }));
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
// not compared. Java's router server sometimes never listens (setAktiv wakes
|
||||
// its thread only when it already waits), and takes no initial sequence
|
||||
// number then; the engine's always listens: sequence numbers are compared
|
||||
// below the millions. The settings page (konfig) comes with F3.2c.
|
||||
// below the millions. The settings page (konfig) changes the firewall at run
|
||||
// time: a ping passes once "drop ICMP" is saved off; a rule moved above the
|
||||
// one that accepts the browser shuts it out.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
@@ -46,15 +48,53 @@ const open =
|
||||
engine.webBrowser(browser)!.open(typed);
|
||||
};
|
||||
|
||||
const post =
|
||||
(typed: string, data: string) =>
|
||||
(engine: NetworkRuntimeEngine, browser: string): void => {
|
||||
engine.webBrowser(browser)!.open(typed, data);
|
||||
};
|
||||
|
||||
const ping = (engine: NetworkRuntimeEngine, browser: string): void => {
|
||||
engine.sendIcmpEcho(browser, '20.0.0.2', { sequenceNumber: 1 });
|
||||
};
|
||||
|
||||
/** The form as Java's browser window posts it: every field, then the button pressed. */
|
||||
const FORM = {
|
||||
saveWithoutIcmp: 'firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop',
|
||||
addTcpDrop: 'firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen',
|
||||
moveSecondUp: 'firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&radioRule=2&moveup=rauf&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop',
|
||||
};
|
||||
|
||||
const STORIES: Record<string, Story> = {
|
||||
routerIndex: { scenario: 'router-web', browser: id(301), router: id(302), acts: [[500, open('10.0.0.1')]] },
|
||||
routerRoutes: { scenario: 'router-web', browser: id(301), router: id(302), acts: [[500, open('10.0.0.1/routes')]] },
|
||||
routerKonfig: { scenario: 'router-web', browser: id(301), router: id(302), acts: [[500, open('10.0.0.1/konfig')]] },
|
||||
konfigIcmp: {
|
||||
scenario: 'router-web',
|
||||
browser: id(301),
|
||||
router: id(302),
|
||||
acts: [
|
||||
[500, ping],
|
||||
[3500, post('10.0.0.1/konfig', FORM.saveWithoutIcmp)],
|
||||
[7000, ping],
|
||||
],
|
||||
},
|
||||
konfigBlock: {
|
||||
scenario: 'router-web',
|
||||
browser: id(301),
|
||||
router: id(302),
|
||||
acts: [
|
||||
[500, post('10.0.0.1/konfig', FORM.addTcpDrop)],
|
||||
[4000, post('10.0.0.1/konfig', FORM.moveSecondUp)],
|
||||
[8000, open('10.0.0.1')],
|
||||
],
|
||||
},
|
||||
routerLog: {
|
||||
scenario: 'router-web',
|
||||
browser: id(301),
|
||||
router: id(302),
|
||||
acts: [
|
||||
[500, (engine, browser) => engine.sendIcmpEcho(browser, '20.0.0.2', { sequenceNumber: 1 })],
|
||||
[500, ping],
|
||||
[3500, open('10.0.0.1')],
|
||||
[7000, open('10.0.0.1/log')],
|
||||
],
|
||||
@@ -78,12 +118,21 @@ const withoutInitialNumbers = (text: string) =>
|
||||
.replace(/(seq|ack)=(\d+)/g, (_, name: string, value: string) => `${name}=${Number(value) % 1_000_000}`)
|
||||
.replace(/(SEQ|ACK): \d+\.(\d{3}\.\d{3})/g, '$1: n.$2');
|
||||
|
||||
/** Java's wait for a SYN's answer (TCPSocket: three times 2.75 s, its timeout here). */
|
||||
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
|
||||
|
||||
/**
|
||||
* Where Java errs: its run corrected, as Java means it. routerLog: the
|
||||
* German log page closes its title with </titel> (sw_firewallweblog_msg1);
|
||||
* the engine writes </title>.
|
||||
* the engine writes </title>. konfigBlock: the browser's SYN, dropped by the
|
||||
* firewall, goes once in Java, not three times (webJava.test.ts, noServer);
|
||||
* the page "server not reachable" comes two attempts later in the engine.
|
||||
*/
|
||||
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
konfigBlock: (java) => ({
|
||||
...java,
|
||||
events: java.events.map(([time, who, text]) => (who === 'browser' && text.startsWith('page 0') ? [time + 2 * JAVA_TIMEOUT_MILLISECONDS, who, text] : [time, who, text])),
|
||||
}),
|
||||
routerLog: (java) => {
|
||||
const fixed = (text: string | null) => text?.replace('</titel>', '</title>') ?? null;
|
||||
const rows = (byCard: Record<string, (string | null)[][]> | undefined) =>
|
||||
@@ -103,6 +152,11 @@ const PAGE_PLACEHOLDERS: Record<string, (text: string) => string> = {
|
||||
ripRoutes: (text) => text.replace(/<td>-?\d+<\/td>(<td>[\d.]+<\/td><td><a )/g, '<td>validity</td>$1'),
|
||||
};
|
||||
|
||||
/** Where the frames are compared only up to a time (Java's). konfigBlock: the engine's second SYN, which Java does not send. */
|
||||
const UNTIL: Record<string, (java: JavaRun) => number> = {
|
||||
konfigBlock: (java) => java.events.find(([, who, text]) => who === 'browser' && text.startsWith('page 0'))![0] - 250,
|
||||
};
|
||||
|
||||
type Lines = Record<Program, [number, string][]>;
|
||||
|
||||
function javaLines(java: JavaRun): Lines {
|
||||
@@ -162,6 +216,7 @@ describe('the pages of routers and home routers as Java FILIUS serves them', ()
|
||||
cell: (text) => placeholders(withoutInitialNumbers(text)),
|
||||
tables: java,
|
||||
from: firstCall,
|
||||
until: UNTIL[name]?.(measured),
|
||||
starts: { java: 0, engine: 0 },
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -137,6 +137,11 @@ describe('the web server', () => {
|
||||
expect(ask(engine, 'HALLO', 'GET', 'GET /')).toEqual(['HTTP/1.1 501 Not Implemented\n', 'HTTP/1.1 400 Bad Request\n', 'HTTP/1.1 200 OK\nContent-type: text/html\n\nStart']);
|
||||
});
|
||||
|
||||
it('answers only GET and POST as Java writes them: a method in small letters is read, then answered 501 (WebServerMitarbeiter)', () => {
|
||||
const engine = startedEngine(network(files({ '/webserver/index.html': 'Start' })));
|
||||
expect(ask(engine, 'get /', 'Post /\n\nx=1')).toEqual(['HTTP/1.1 501 Not Implemented\n', 'HTTP/1.1 501 Not Implemented\n']);
|
||||
});
|
||||
|
||||
it('serves nothing outside its document root (Java serves any file of the host), and no folder', () => {
|
||||
const engine = startedEngine(network(files({ '/geheim.txt': 'geheim', '/webserver/ordner/a.txt': 'a' })));
|
||||
expect(ask(engine, 'GET /../geheim.txt HTTP/1.1', 'GET /ordner/../../geheim.txt HTTP/1.1', 'GET /ordner HTTP/1.1', 'GET /ordner/./a.txt?x HTTP/1.1')).toEqual([
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
// echo or web server is there either way, switched on or off (it can be
|
||||
// switched at run time). The programs read the nodes' files (a web server its
|
||||
// pages).
|
||||
//
|
||||
// Back the other way: what the simulation changes that Java keeps in the
|
||||
// project (its objects are the project's), so a project saved after it holds
|
||||
// it. That is a router's or home router's firewall, set on its page konfig.
|
||||
|
||||
import { portSnapshot, topologySnapshot, type DnsServerConfiguration, type EchoServerSnapshot, type Id, type WebServerSnapshot, type TopologySnapshot } from '@mdg-filius/core';
|
||||
import type { ImportedProject } from './importProject.js';
|
||||
@@ -51,3 +55,8 @@ export function projectSnapshot(project: ImportedProject): TopologySnapshot {
|
||||
fileSystemsByNodeId: project.virtualFileSystems,
|
||||
});
|
||||
}
|
||||
|
||||
/** The project with what the simulation changed that Java keeps: the firewalls, as the run's snapshot holds them. */
|
||||
export function projectWithSimulationChanges(project: ImportedProject, snapshot: TopologySnapshot): ImportedProject {
|
||||
return { ...project, firewallConfigurationsByNodeId: new Map(snapshot.firewallConfigurationsByNodeId) };
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
defaultDisplayName,
|
||||
defaultSwitchConfiguration,
|
||||
firewallConfiguration,
|
||||
firewallRule,
|
||||
FIREWALL_PROTOCOL,
|
||||
interfaceKey,
|
||||
deepEqual,
|
||||
@@ -28,8 +29,11 @@ import {
|
||||
import {
|
||||
decodeJavaBeans,
|
||||
importProject,
|
||||
openProjectArchive,
|
||||
projectSnapshot,
|
||||
projectWithSimulationChanges,
|
||||
readFlsArchive,
|
||||
saveProjectArchive,
|
||||
readTopology,
|
||||
type FlsApplication,
|
||||
type JavaEmail,
|
||||
@@ -773,6 +777,30 @@ describe('importProject rules', () => {
|
||||
expect(snapshot.fileSystemsByNodeId).toBe(project.virtualFileSystems);
|
||||
});
|
||||
|
||||
it("keeps a router's firewall set on its page konfig in the project saved after the run (projectWithSimulationChanges)", () => {
|
||||
const project = openProjectArchive(new Uint8Array(readFileSync(join(corpusRoot, 'java-examples/internetwork_komplett.fls'))), counter());
|
||||
const node = (name: string) => project.graph.nodes.find((each) => each.displayName === name)!.id;
|
||||
const router = project.graph.nodes.find((each) => each.kind === 'router' && project.deviceConfigurations.get(each.id)?.ipAddress === '141.99.1.1')!.id;
|
||||
const notebook = node('Notebook 1.10');
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot: { ...projectSnapshot(project), webBrowserNodeIds: new Set([notebook]) }, seed: 14, initialTimeMilliseconds: 0 });
|
||||
const form = 'srcIP=&srcMask=&destIP=&destMask=&protocol=UDP&port=53&action=accept';
|
||||
// Added first: once switched on, the firewall drops the browser too (by default).
|
||||
engine.webBrowser(notebook)!.open('141.99.1.1/konfig', `onlySYN=1&defaultPolicy=drop&${form}&addrule=hinzuf%C3%BCgen`);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 5_000 });
|
||||
engine.webBrowser(notebook)!.open('141.99.1.1/konfig', `firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=drop&${form}`);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
|
||||
const expected = firewallConfiguration({ isActive: true, rules: [firewallRule({ port: 53, protocol: 'udp', action: 'accept' })] });
|
||||
expect(engine.firewallConfiguration(router)).toEqual(expected);
|
||||
// The project itself is as it was opened, until the run's changes go back into it.
|
||||
expect(project.firewallConfigurationsByNodeId.get(router)).toEqual(firewallConfiguration());
|
||||
|
||||
const saved = saveProjectArchive(projectWithSimulationChanges(project, engine.state.topologySnapshot)).archive;
|
||||
const reopened = openProjectArchive(saved, counter());
|
||||
const reopenedRouter = reopened.graph.nodes.find((each) => each.kind === 'router' && reopened.deviceConfigurations.get(each.id)?.ipAddress === '141.99.1.1')!.id;
|
||||
expect(reopened.firewallConfigurationsByNodeId.get(reopenedRouter)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("keeps a node's IP forwarding where it differs from its kind's: a host that forwards, a router that does not", () => {
|
||||
const system = (ipForwardingEnabled: boolean) => ({ manualRoutes: [], ipForwardingEnabled });
|
||||
const project = importProject(
|
||||
|
||||
@@ -79,3 +79,5 @@ packages/core/test/programLines.ts::tcpsocket
|
||||
# Java's German firewall log page closes its title with the typo </titel>; quoted where the core names the departure.
|
||||
packages/core/src/runtime/apps/routerPages.ts::titel
|
||||
packages/core/test/routerWebJava.test.ts::titel
|
||||
# konfig: the path of Java's firewall settings plug-in (FirewallWebKonfig); the stories that use it are named after it, as in RouterWebRun.
|
||||
packages/core/test/routerWebJava.test.ts::konfig
|
||||
|
||||
@@ -646,3 +646,41 @@ Gemessen (01.10.2026):
|
||||
* Java zählt TCP-Sequenznummern und Segmente in Zeichen seiner Strings: Ein
|
||||
„ä“ auf der Routen-Seite ist eine Stelle, nicht zwei Bytes.
|
||||
|
||||
|
||||
## Firewall-Einstellungen der Router (F3.2c)
|
||||
|
||||
`KonfigCases` füttert Javas Seite `konfig` (`FirewallWebKonfig`) direkt mit
|
||||
den Schritten aus `packages/core/test/firewall-settings.cases.txt` (eine
|
||||
Anfrage ohne Daten oder die Daten eines Formulars) und schreibt je Schritt
|
||||
die Seite und die Firewall danach, auf Deutsch und auf Englisch
|
||||
(`firewall-settings.{de,en}.java.json`; das Gebietsschema wird vor dem
|
||||
ersten Lesen des Nachrichtenbündels gesetzt). Jeder Fall beginnt mit einem
|
||||
neuen Router, ohne Regeln, mit drei oder mit der einen Regel, die ein Fall
|
||||
anlegt (die Seite, die Java zeigen würde, hätte es sie angenommen).
|
||||
`RouterWebRun` spielt dazu auf `router-web` `konfigIcmp` und `konfigBlock`
|
||||
über das Netz.
|
||||
|
||||
Gemessen (01.10.2026):
|
||||
|
||||
* Javas Browserfenster (Swing, `FormSubmitEvent.getData`) schickt jedes Feld
|
||||
des Formulars in seiner Reihenfolge, UTF-8-kodiert, angehakte Kästchen und
|
||||
die gewählte Option, dazu nur den gedrückten Knopf.
|
||||
* Java prüft die Felder beim Lesen: die Quell-IP gegen die Maske, die noch
|
||||
nicht gelesen ist (also leer), die Maske gegen die IP, ebenso. Eine Regel
|
||||
mit Quell- oder Zieladresse nimmt die Seite deshalb nie an, in keiner
|
||||
Reihenfolge der Felder.
|
||||
* Ein Fehler bleibt im Plug-in stehen und erscheint auf jeder späteren Seite,
|
||||
auch ohne Daten. Mit zwei Knöpfen zugleich (Speichern und Hinzufügen)
|
||||
speichert Java und schreibt die Fehler als nackte Liste.
|
||||
* Java dekodiert die Daten, bevor es sie an `&` und `=` zerlegt: Ein
|
||||
getipptes `&` zerreißt das Formular. Eine kaputte Kodierung (`%zz`, `%` am
|
||||
Ende) wirft im Dekodierer, Regel 0 nach unten wirft
|
||||
(`ArrayIndexOutOfBoundsException`), ein `$` im wieder gefüllten Formular
|
||||
lässt `replaceAll` scheitern (die Seite ist `null`, also 500).
|
||||
* Die Maske prüft `isValidSubnetmask`: vier Zahlen (eine leere am Ende fällt
|
||||
bei Javas `split` weg), `parseInt` mit Vorzeichen, die Bits Einsen, dann
|
||||
Nullen, mindestens eine Eins (0.0.0.0 ist keine Maske).
|
||||
* Über das Netz: „ICMP filtern“ aus, und der nächste Ping kommt durch; eine
|
||||
Regel „TCP 80 verwerfen“ über die erlaubende geschoben, und das SYN des
|
||||
Browsers bleibt an der Firewall hängen. Javas Router-Server lauschte auch
|
||||
hier in einem Lauf nie (`run.sh` wiederholt).
|
||||
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
||||
fi
|
||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||
mkdir -p "$here/.build"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java"
|
||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||
mkdir -p "$here/.build/home"
|
||||
# Removed first: a crashed run must not leave the previous result standing.
|
||||
@@ -224,6 +224,7 @@ done
|
||||
# sometimes never listens (a race in setAktiv): a run without a page is
|
||||
# repeated.
|
||||
for run in router-web:routerIndex:6000 router-web:routerRoutes:6000 router-web:routerLog:12000 router-web:routerKonfig:6000 \
|
||||
router-web:konfigIcmp:12000 router-web:konfigBlock:30000 \
|
||||
nat-home:gatewayIndex:9000 nat-home:gatewayFromWan:15000 rip-chain:ripRoutes:46000; do
|
||||
network="${run%%:*}"; rest="${run#*:}"; story="${rest%%:*}"
|
||||
output="$root/packages/core/test/scenarios/routerweb-java-$story.java.json"
|
||||
@@ -236,6 +237,16 @@ for run in router-web:routerIndex:6000 router-web:routerRoutes:6000 router-web:r
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# The firewall settings page (konfig) fed with what a browser posts, in both
|
||||
# languages (KonfigCases; packages/core/test/firewallSettings.test.ts).
|
||||
for language in de en; do
|
||||
output="$root/packages/core/test/firewall-settings.$language.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" KonfigCases "$language" \
|
||||
"$root/packages/core/test/firewall-settings.cases.txt" "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
|
||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts).
|
||||
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Feeds FILIUS' firewall settings page (FirewallWebKonfig, the router's web
|
||||
// server's plug-in konfig) with what a browser posts, step after step, and
|
||||
// writes each page and the firewall after it: the reference for the core's
|
||||
// konfig page (packages/core/src/runtime/apps/firewallSettings.ts). Each case
|
||||
// starts from a new router; its firewall holds no rules ("empty"), three
|
||||
// ("three"), one only a file can bring ("portZero": port 0, ICMP), or the
|
||||
// one rule a case adds ("sourceRule", "destinationRule":
|
||||
// the page Java would answer, had it taken the rule). The cases file is
|
||||
// ASCII, with Java-style unicode escapes (backslash, u, four hex digits) for
|
||||
// anything else:
|
||||
//
|
||||
// case <name> <start>
|
||||
// GET the page without data (Java: holeHtmlSeite(null))
|
||||
// POST <data> the page with the data a form posts ("POST" alone: empty)
|
||||
//
|
||||
// Usage: java -cp filius.jar:lib/*:classes KonfigCases <de|en> <cases txt> <output json>
|
||||
|
||||
import filius.hardware.knoten.Vermittlungsrechner;
|
||||
import filius.rahmenprogramm.Information;
|
||||
import filius.software.firewall.Firewall;
|
||||
import filius.software.firewall.FirewallRule;
|
||||
import filius.software.system.VermittlungsrechnerBetriebssystem;
|
||||
import filius.software.www.WebServerPlugIn;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class KonfigCases {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// Before any class reads the message bundle (I18n.messages is read once).
|
||||
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
|
||||
List<Object> cases = new ArrayList<>();
|
||||
Map<String, Object> current = null;
|
||||
List<Object> steps = null;
|
||||
WebServerPlugIn page = null;
|
||||
Firewall firewall = null;
|
||||
for (String line : Files.readAllLines(Path.of(args[1]), StandardCharsets.US_ASCII)) {
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
if (line.startsWith("case ")) {
|
||||
String[] words = line.split(" ");
|
||||
current = new LinkedHashMap<>();
|
||||
steps = new ArrayList<>();
|
||||
current.put("name", words[1]);
|
||||
current.put("start", words[2]);
|
||||
current.put("steps", steps);
|
||||
cases.add(current);
|
||||
VermittlungsrechnerBetriebssystem os = (VermittlungsrechnerBetriebssystem) new Vermittlungsrechner().getSystemSoftware();
|
||||
firewall = os.holeFirewall();
|
||||
page = os.holeWebServer().holePlugin("konfig");
|
||||
if (words[2].equals("three")) {
|
||||
firewall.setActivated(true);
|
||||
firewall.addRule(new FirewallRule("", "", "", "", 80, FirewallRule.TCP, FirewallRule.ACCEPT));
|
||||
firewall.addRule(new FirewallRule("999.999.999.999", "", "20.0.0.0", "255.0.0.0", FirewallRule.ALL_PORTS, FirewallRule.UDP, FirewallRule.DROP));
|
||||
firewall.addRule(new FirewallRule("10.0.0.0", "255.0.0.0", "", "", FirewallRule.ALL_PORTS, FirewallRule.ALL_PROTOCOLS, FirewallRule.DROP));
|
||||
} else if (words[2].equals("portZero")) {
|
||||
firewall.addRule(new FirewallRule("", "", "", "", 0, FirewallRule.ICMP, FirewallRule.ACCEPT));
|
||||
} else if (words[2].equals("sourceRule")) {
|
||||
firewall.addRule(new FirewallRule("10.0.0.0", "255.255.255.0", "", "", 80, FirewallRule.TCP, FirewallRule.DROP));
|
||||
} else if (words[2].equals("destinationRule")) {
|
||||
firewall.addRule(new FirewallRule("", "", "20.0.0.0", "255.0.0.0", FirewallRule.ALL_PORTS, FirewallRule.UDP, FirewallRule.ACCEPT));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String post = line.equals("GET") ? null : line.equals("POST") ? "" : IpCheckCases.unescape(line.substring("POST ".length()));
|
||||
Map<String, Object> step = new LinkedHashMap<>();
|
||||
step.put("post", post);
|
||||
try {
|
||||
step.put("page", page.holeHtmlSeite(post));
|
||||
} catch (RuntimeException error) {
|
||||
step.put("page", "!" + error.getClass().getSimpleName());
|
||||
}
|
||||
step.put("firewall", state(firewall));
|
||||
steps.add(step);
|
||||
}
|
||||
try (FileOutputStream out = new FileOutputStream(args[2])) {
|
||||
out.write((FlsOracle.Json.write(cases, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
System.out.println("konfig cases (" + args[0] + "): " + cases.size());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
static Map<String, Object> state(Firewall firewall) {
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
state.put("isActive", firewall.isActivated());
|
||||
state.put("dropIcmp", firewall.getDropICMP());
|
||||
state.put("filterSynSegmentsOnly", firewall.getFilterSYNSegmentsOnly());
|
||||
state.put("defaultPolicy", firewall.getDefaultPolicy() == FirewallRule.ACCEPT ? "accept" : "drop");
|
||||
List<Object> rules = new ArrayList<>();
|
||||
for (FirewallRule rule : firewall.getRuleset()) {
|
||||
String protocol = rule.protocol == FirewallRule.TCP ? "tcp" : rule.protocol == FirewallRule.UDP ? "udp" : rule.protocol == FirewallRule.ICMP ? "icmp" : rule.protocol == FirewallRule.ALL_PROTOCOLS ? "all" : String.valueOf(rule.protocol);
|
||||
rules.add(Arrays.asList(rule.srcIP, rule.srcMask, rule.destIP, rule.destMask, rule.port, protocol, rule.action == FirewallRule.ACCEPT ? "accept" : "drop"));
|
||||
}
|
||||
state.put("rules", rules);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@
|
||||
// routerLog a ping to 20.0.0.2 (dropped), the index, then
|
||||
// 10.0.0.1/log
|
||||
// routerKonfig 10.0.0.1/konfig
|
||||
// konfigIcmp a ping to 20.0.0.2 (dropped), the settings saved
|
||||
// without "drop ICMP" (the form as Java's browser window
|
||||
// posts it), the ping again
|
||||
// konfigBlock a rule added (TCP 80: drop), moved up above the rule
|
||||
// that accepts the browser, then the index again
|
||||
// nat-home (home router 42.0.0.10/192.168.0.1):
|
||||
// gatewayIndex from the LAN PC 192.168.0.10: 192.168.0.1, then
|
||||
// 192.168.0.1/routes (no such page on a home router)
|
||||
@@ -61,6 +66,22 @@ public class RouterWebRun {
|
||||
browser.holeWebseite(new URL("http", parts[0], path.isEmpty() ? "/" : path));
|
||||
}
|
||||
|
||||
/** What the browser window posts when a button of the settings form is pressed (Swing: every field, the button). */
|
||||
static void post(String typed, String data) throws Exception {
|
||||
event("browser", "call holeWebseite " + typed + " " + data);
|
||||
String[] parts = typed.split("/", 2);
|
||||
browser.holeWebseite(new URL("http", parts[0], "/" + parts[1]), data);
|
||||
}
|
||||
|
||||
static void ping(InternetKnotenBetriebssystem host) {
|
||||
Thread ping = new Thread(() -> {
|
||||
event("ping", "call sendProbe 20.0.0.2");
|
||||
Object answer = host.holeICMP().sendProbe("20.0.0.2", 64, 1, 1);
|
||||
event("ping", "returned " + (answer == null ? "null" : "answer"));
|
||||
});
|
||||
ping.start();
|
||||
}
|
||||
|
||||
static String id(int suffix) {
|
||||
return String.format("00000000-0000-0000-0000-%012d", suffix);
|
||||
}
|
||||
@@ -124,12 +145,7 @@ public class RouterWebRun {
|
||||
open("10.0.0.1/routes");
|
||||
break;
|
||||
case "routerLog": {
|
||||
Thread ping = new Thread(() -> {
|
||||
event("ping", "call sendProbe 20.0.0.2");
|
||||
Object answer = host.holeICMP().sendProbe("20.0.0.2", 64, 1, 1);
|
||||
event("ping", "returned " + (answer == null ? "null" : "answer"));
|
||||
});
|
||||
ping.start();
|
||||
ping(host);
|
||||
sleepUntil(3500);
|
||||
open("10.0.0.1");
|
||||
sleepUntil(7000);
|
||||
@@ -139,6 +155,20 @@ public class RouterWebRun {
|
||||
case "routerKonfig":
|
||||
open("10.0.0.1/konfig");
|
||||
break;
|
||||
case "konfigIcmp":
|
||||
ping(host);
|
||||
sleepUntil(3500);
|
||||
post("10.0.0.1/konfig", "firewallActivate=1&onlySYN=1&btnSave=speichern&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop");
|
||||
sleepUntil(7000);
|
||||
ping(host);
|
||||
break;
|
||||
case "konfigBlock":
|
||||
post("10.0.0.1/konfig", "firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&srcIP=&srcMask=&destIP=&destMask=&protocol=TCP&port=80&action=drop&addrule=hinzuf%C3%BCgen");
|
||||
sleepUntil(4000);
|
||||
post("10.0.0.1/konfig", "firewallActivate=1&dropICMP=1&onlySYN=1&defaultPolicy=accept&radioRule=2&moveup=rauf&srcIP=&srcMask=&destIP=&destMask=&protocol=*&port=&action=drop");
|
||||
sleepUntil(8000);
|
||||
open("10.0.0.1");
|
||||
break;
|
||||
case "gatewayIndex":
|
||||
open("192.168.0.1");
|
||||
sleepUntil(4000);
|
||||
|
||||
Reference in New Issue
Block a user