// 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-.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; 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 = { 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 (sw_firewallweblog_msg1); * the engine writes . */ const JAVA_BUGS: Record JavaRun> = { routerLog: (java) => { const fixed = (text: string | null) => text?.replace('', '') ?? null; const rows = (byCard: Record | 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> = { ripRoutes: (text) => text.replace(/-?\d+<\/td>([\d.]+<\/td>validity$1'), }; type Lines = Record; 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 = (log: readonly LogLine[], 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); } } }); } });