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
+163
View File
@@ -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="&lt;b&gt;&quot;"') }) },
// 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="&amp;"') }) },
// 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();
});
});