Routers and home routers start a web server on port 80 with Java's index, log and routes pages in the project's language (routerPages.ts); the firewall keeps a log per node that outlives a new start; RIP routes carry the telling router's public address. TCP counts sequence numbers and segments in Java's characters, not bytes. RouterWebRun records six stories, with firewalls now built by ScenarioNetwork. Not taken from Java: its router server sometimes never listening (a race in setAktiv), and </titel>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
8.9 KiB
TypeScript
186 lines
8.9 KiB
TypeScript
// The pages of routers and home routers as Java FILIUS serves them.
|
|
// tools/java-oracle's RouterWebRun let a host's browser ask a router's or
|
|
// home router's web server for its index page, its routing table and its
|
|
// firewall log (routerweb-java-<story>.java.json). The engine plays the same
|
|
// stories: each card's frames must come in the same order, with the same
|
|
// content and rows, at about the same time; the server's lines and the
|
|
// browser's pages likewise. The pages are Java's German ones.
|
|
//
|
|
// The router's server switches itself on when the simulation starts (Java
|
|
// writes so into its window): lines from before the story's first call are
|
|
// 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.
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { LogLine, NetworkRuntimeEngine } from '../src/index.js';
|
|
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js';
|
|
import { browserText, isPolled, POLL_MILLISECONDS, serverText } from './programLines.js';
|
|
import { snapshotOf, type Scenario } from './scenario.js';
|
|
|
|
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
|
|
|
|
const id = (suffix: number) => `00000000-0000-0000-0000-${String(suffix).padStart(12, '0')}`;
|
|
|
|
type Program = 'server' | 'browser';
|
|
|
|
interface JavaRun extends JavaTables {
|
|
frames: Record<string, JavaFrame[]>;
|
|
events: [number, string, string][];
|
|
}
|
|
|
|
interface Story {
|
|
readonly scenario: string;
|
|
readonly browser: string;
|
|
readonly router: string;
|
|
readonly acts: readonly [number, (engine: NetworkRuntimeEngine, browser: string) => void][];
|
|
}
|
|
|
|
const open =
|
|
(typed: string) =>
|
|
(engine: NetworkRuntimeEngine, browser: string): void => {
|
|
engine.webBrowser(browser)!.open(typed);
|
|
};
|
|
|
|
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')]] },
|
|
routerLog: {
|
|
scenario: 'router-web',
|
|
browser: id(301),
|
|
router: id(302),
|
|
acts: [
|
|
[500, (engine, browser) => engine.sendIcmpEcho(browser, '20.0.0.2', { sequenceNumber: 1 })],
|
|
[3500, open('10.0.0.1')],
|
|
[7000, open('10.0.0.1/log')],
|
|
],
|
|
},
|
|
gatewayIndex: {
|
|
scenario: 'nat-home',
|
|
browser: id(501),
|
|
router: id(502),
|
|
acts: [
|
|
[500, open('192.168.0.1')],
|
|
[4000, open('192.168.0.1/routes')],
|
|
],
|
|
},
|
|
gatewayFromWan: { scenario: 'nat-home', browser: id(504), router: id(502), acts: [[500, open('42.0.0.10')]] },
|
|
ripRoutes: { scenario: 'rip-chain', browser: id(401), router: id(402), acts: [[40_000, open('10.0.1.1/routes')]] },
|
|
};
|
|
|
|
/** Sequence numbers below the millions: which socket drew which initial number differs (see above). */
|
|
const withoutInitialNumbers = (text: string) =>
|
|
withClientPortText(text)
|
|
.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');
|
|
|
|
/**
|
|
* 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>.
|
|
*/
|
|
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
|
routerLog: (java) => {
|
|
const fixed = (text: string | null) => text?.replace('</titel>', '</title>') ?? null;
|
|
const rows = (byCard: Record<string, (string | null)[][]> | undefined) =>
|
|
Object.fromEntries(Object.entries(byCard ?? {}).map(([card, cardRows]) => [card, cardRows.map((row) => row.map(fixed))]));
|
|
return { ...java, events: java.events.map(([time, who, text]) => [time, who, fixed(text)!]), rows: rows(java.rows), layeredRows: rows(java.layeredRows) };
|
|
},
|
|
};
|
|
|
|
/**
|
|
* ripRoutes: how long a learned route has left. Java's routers start one
|
|
* after the other and draw their beacons' pauses in another order (rip-chain
|
|
* in ripJava.test.ts: R2 sends three times at the start, not twice); a
|
|
* neighbour's beacon refreshes the route some seconds apart. Compared as a
|
|
* placeholder; the page's other cells exactly.
|
|
*/
|
|
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'),
|
|
};
|
|
|
|
type Lines = Record<Program, [number, string][]>;
|
|
|
|
function javaLines(java: JavaRun): Lines {
|
|
const lines: Lines = { server: [], browser: [] };
|
|
const firstCall = Math.min(...java.events.filter(([, , text]) => text.startsWith('call ')).map(([time]) => time));
|
|
for (const [time, program, text] of java.events) {
|
|
if (program !== 'server' && program !== 'browser') continue;
|
|
if (time < firstCall || text === 'null' || text.startsWith('call ')) continue;
|
|
const page = /^page (\d+) (\S+)\n/.exec(text);
|
|
lines[program].push([time, withClientPortText(page !== null && page[1] !== '200' ? `page ${page[1]} ${page[2]}` : text)]);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function engineLines(engine: NetworkRuntimeEngine, story: Story, firstCall: number): Lines {
|
|
const lines = <E>(log: readonly LogLine<E>[], text: (entry: E) => string | undefined): [number, string][] =>
|
|
log.flatMap((line) => {
|
|
const each = line.timeMilliseconds < firstCall ? undefined : text(line);
|
|
return each === undefined ? [] : [[line.timeMilliseconds, withClientPortText(each)] as [number, string]];
|
|
});
|
|
return {
|
|
server: lines(engine.webServer(story.router)!.log.all, serverText),
|
|
browser: lines(engine.webBrowser(story.browser)!.log.all, browserText),
|
|
};
|
|
}
|
|
|
|
describe('the pages of routers and home routers as Java FILIUS serves them', () => {
|
|
it('corrects Java only where it errs, and puts in placeholders only where they change something', () => {
|
|
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
|
|
const java = JSON.parse(readFileSync(join(scenarioDirectory, `routerweb-java-${name}.java.json`), 'utf8')) as JavaRun;
|
|
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
|
|
}
|
|
for (const [name, placeholders] of Object.entries(PAGE_PLACEHOLDERS)) {
|
|
const text = readFileSync(join(scenarioDirectory, `routerweb-java-${name}.java.json`), 'utf8');
|
|
expect(placeholders(text), name).not.toBe(text);
|
|
}
|
|
});
|
|
|
|
for (const [name, story] of Object.entries(STORIES)) {
|
|
it(name, () => {
|
|
const measured = JSON.parse(readFileSync(join(scenarioDirectory, `routerweb-java-${name}.java.json`), 'utf8')) as JavaRun;
|
|
const java = JAVA_BUGS[name]?.(measured) ?? measured;
|
|
const placeholders = PAGE_PLACEHOLDERS[name] ?? ((text: string) => text);
|
|
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, `${story.scenario}.scenario.json`), 'utf8')) as Scenario;
|
|
const snapshot = { ...snapshotOf(scenario), webBrowserNodeIds: new Set([story.browser]) };
|
|
const { engine } = watchedEngine(scenario, snapshot);
|
|
for (const [time, act] of story.acts) {
|
|
engine.handle({ type: 'advance', toMilliseconds: time });
|
|
act(engine, story.browser);
|
|
}
|
|
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 });
|
|
|
|
const firstCall = story.acts[0]![0];
|
|
// Before the story, only what the network does by itself (RIP); compared from the first call.
|
|
expectSameCaptures(engine, scenario, java.frames, {
|
|
text: withoutInitialNumbers,
|
|
cell: (text) => placeholders(withoutInitialNumbers(text)),
|
|
tables: java,
|
|
from: firstCall,
|
|
starts: { java: 0, engine: 0 },
|
|
});
|
|
|
|
const withPlaceholders = (lines: Lines): Lines => ({
|
|
server: lines.server.map(([time, text]) => [time, placeholders(text)]),
|
|
browser: lines.browser.map(([time, text]) => [time, placeholders(text)]),
|
|
});
|
|
const expected = withPlaceholders(javaLines(java));
|
|
const actual = withPlaceholders(engineLines(engine, story, firstCall));
|
|
for (const program of ['server', 'browser'] as const) {
|
|
expect(actual[program].map(([, text]) => text), `${program}'s lines`).toEqual(expected[program].map(([, text]) => text));
|
|
for (const [index, [javaTime, text]] of expected[program].entries()) {
|
|
const late = javaTime - actual[program][index]![0];
|
|
const most = (isPolled(text) ? POLL_MILLISECONDS : 0) + GAP_TOLERANCE_MILLISECONDS;
|
|
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeGreaterThanOrEqual(-GAP_TOLERANCE_MILLISECONDS);
|
|
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeLessThanOrEqual(most);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|