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>
164 lines
7.1 KiB
TypeScript
164 lines
7.1 KiB
TypeScript
// 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();
|
|
});
|
|
});
|