feat: F4.6g3 a computer's firewall window, measured against Java's with traffic from two networks
The firewall-pc scenario and FirewallWindowRun use Java's window as a person does and send a ping, a TCP connection and a DNS question from the server's network and from behind the router after each step. The core adds and removes port rules (personalFirewall.ts) and writes both into the firewall's log, which now carries each line's time; the room takes the window's actions and sends its view and the log as lines; the editor shows Java's window with its two tabs. Not adopted from Java: any integer as a port, a port after a space refused without a word, a log one can type into. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// A computer's firewall on its desktop in Chromium (F4.6g3), on FILIUS'
|
||||
// example email_komplett with the firewall installed on its web server:
|
||||
// Java's boxes, a port refused and one added, a notebook's browser let
|
||||
// through by the rule, ICMP no longer allowed and a ping dropped (both in the
|
||||
// log), the firewall switched off (the boxes beneath it can no longer be
|
||||
// changed), the rule chosen and removed.
|
||||
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
|
||||
|
||||
let server: { readonly base: string; readonly close: () => Promise<void> };
|
||||
|
||||
test.beforeAll(async () => {
|
||||
server = await serveEditor();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
async function desktopOf(page: Page, name: string) {
|
||||
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
|
||||
const at = await onDevice(page, id);
|
||||
await page.mouse.click(at.x, at.y);
|
||||
return page.locator(`section[data-desktop="${id}"]`);
|
||||
}
|
||||
|
||||
async function install(page: Page, desktop: ReturnType<Page['locator']>, program: string) {
|
||||
await desktop.locator('[data-program="installation"]').click();
|
||||
const installation = desktop.locator('[data-program-window="installation"]');
|
||||
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: program }).click();
|
||||
await installation.getByRole('button', { name: 'Installieren' }).click();
|
||||
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||
}
|
||||
|
||||
test('keeps a computer’s firewall as Java’s window does, and the firewall lets through what its rules open', async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await openEditor(page, server.base, 'email_komplett.fls');
|
||||
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
|
||||
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||
const desktop = await desktopOf(page, 'Web-Server');
|
||||
await install(page, desktop, 'Firewall');
|
||||
await desktop.locator('[data-program="firewall"]').click();
|
||||
const window = desktop.locator('[data-program-window="firewall"]');
|
||||
await expect(window.locator('.program-titlebar h3')).toHaveText('Firewall');
|
||||
await expect(window.getByRole('tab')).toHaveText(['Konfiguration', 'Log']);
|
||||
const active = window.getByRole('checkbox', { name: 'Firewall aktivieren' });
|
||||
const icmp = window.getByRole('checkbox', { name: 'Internet Control Message Protocol (Ping etc.) erlauben' });
|
||||
await expect(active).toBeChecked();
|
||||
await expect(icmp).toBeChecked();
|
||||
await expect(window.getByRole('checkbox', { name: 'Aktiviere UDP-Datenverkehrfilter' })).toBeChecked();
|
||||
await expect(window.getByText('Die Firewall blockiert alle eingehenden IP-Pakete')).toBeVisible();
|
||||
|
||||
// A port that is none is marked and not handed over; 80 is added and the field emptied.
|
||||
const port = window.getByRole('textbox', { name: 'Zulässiger Port/Protokoll:' });
|
||||
const add = window.getByRole('button', { name: 'Hinzufügen' });
|
||||
await port.fill('70000');
|
||||
await expect(port).toHaveAttribute('aria-invalid', 'true');
|
||||
await expect(add).toBeDisabled();
|
||||
await port.fill('80');
|
||||
await add.click();
|
||||
const rows = window.getByRole('table', { name: 'Regeln' }).locator('tbody tr');
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.first().locator('td')).toHaveText(['TCP', '80', 'Alle Absender']);
|
||||
await expect(port).toHaveValue('');
|
||||
|
||||
// A notebook's browser is let through by the rule; the log says so.
|
||||
await desktop.locator('.desktop-close').click();
|
||||
const notebook = await desktopOf(page, 'Rechner 1');
|
||||
await notebook.locator('[data-program="webBrowser"]').click();
|
||||
const browser = notebook.locator('[data-program-window="webBrowser"]');
|
||||
const address = browser.getByRole('textbox', { name: 'Adresse' });
|
||||
await address.fill('141.99.50.231');
|
||||
await address.press('Enter');
|
||||
await expect(browser.frameLocator('iframe[title="Seite"]').locator('body')).not.toBeEmpty({ timeout: 30_000 });
|
||||
|
||||
// ICMP no longer allowed: the notebook's ping is dropped.
|
||||
await notebook.locator('.desktop-close').click();
|
||||
await desktopOf(page, 'Web-Server');
|
||||
await desktop.locator('[data-program="firewall"]').click();
|
||||
await icmp.click();
|
||||
await expect(icmp).not.toBeChecked();
|
||||
await desktop.locator('.desktop-close').click();
|
||||
await desktopOf(page, 'Rechner 1');
|
||||
await install(page, notebook, 'Befehlszeile');
|
||||
await notebook.locator('[data-program="commandPrompt"]').click();
|
||||
const input = notebook.locator('input[name="terminalInput"]');
|
||||
await input.fill('ping 141.99.50.231');
|
||||
await input.press('Enter');
|
||||
await expect(notebook.locator('.terminal-text')).toContainText('0 Paket(e) empfangen', { timeout: 60_000 });
|
||||
await notebook.locator('.desktop-close').click();
|
||||
await desktopOf(page, 'Web-Server');
|
||||
await desktop.locator('[data-program="firewall"]').click();
|
||||
await window.getByRole('tab', { name: 'Log' }).click();
|
||||
const log = window.getByRole('tabpanel', { name: 'Log' });
|
||||
await expect(log).toContainText('Portregel wurde hinzugefügt: 80/TCP\nFirewall-Regel hat gegriffen: #1 (*/* -> */*; TCP:80 => ACCEPT) -> akzeptieren');
|
||||
await expect(log).toContainText('ICMP-Paket verworfen: 141.99.50.181 -> 141.99.50.231 (code: 0, type: 8)');
|
||||
|
||||
// Off: the two boxes beneath it can no longer be changed; the rule chosen and removed.
|
||||
await window.getByRole('tab', { name: 'Konfiguration' }).click();
|
||||
await active.click();
|
||||
await expect(active).not.toBeChecked();
|
||||
await expect(icmp).toBeDisabled();
|
||||
await expect(window.getByRole('button', { name: 'Löschen' })).toBeDisabled();
|
||||
await rows.first().dispatchEvent('pointerdown');
|
||||
await window.getByRole('button', { name: 'Löschen' }).click();
|
||||
await expect(rows).toHaveCount(0);
|
||||
await window.getByRole('tab', { name: 'Log' }).click();
|
||||
await expect(log).toContainText('Portregel wurde entfernt.');
|
||||
});
|
||||
@@ -41,6 +41,7 @@ import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js';
|
||||
import { FileExplorerPanel } from './FileExplorerPanel.js';
|
||||
import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js';
|
||||
import { DnsServerPanel } from './DnsServerPanel.js';
|
||||
import { PersonalFirewallPanel } from './PersonalFirewallPanel.js';
|
||||
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
|
||||
import { WebServerPanel } from './WebServerPanel.js';
|
||||
import './desktop.css';
|
||||
@@ -71,7 +72,7 @@ export interface DesktopWindowProps {
|
||||
export type DesktopWindowName = DesktopProgram | 'installation';
|
||||
|
||||
/** The programs whose windows are built so far. */
|
||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'dnsServer']);
|
||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'dnsServer', 'firewall']);
|
||||
|
||||
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
|
||||
function useDesktopView(doc: Y.Doc, id: string) {
|
||||
@@ -189,6 +190,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
{shown === 'echoServer' ? <EchoServerPanel node={id} simulation={simulation} /> : null}
|
||||
{shown === 'simpleClient' ? <SimpleClientPanel node={id} simulation={simulation} /> : null}
|
||||
{shown === 'dnsServer' ? <DnsServerPanel node={id} simulation={simulation} /> : null}
|
||||
{shown === 'firewall' ? <PersonalFirewallPanel node={id} simulation={simulation} /> : null}
|
||||
</ProgramFrame>
|
||||
)}
|
||||
{info ? (
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// A computer's firewall, after Java's GUIApplicationFirewallWindow (measured
|
||||
// with tools/java-oracle's FirewallWindowRun): two tabs. The first holds the
|
||||
// three boxes — the firewall on, ICMP allowed, UDP filtered; the last two
|
||||
// only while it is on —, the port with its protocol (TCP, UDP, "*") and its
|
||||
// senders (all, or those of the same network), "Hinzufügen" (the field is
|
||||
// emptied after it), "Löschen" of the row chosen, and the table of rules.
|
||||
// The second holds the log: the firewall's lines and the rules added or
|
||||
// removed. The firewall is the room's: the panel shows its view and lines
|
||||
// and hands over what is done.
|
||||
//
|
||||
// Better than Java:
|
||||
// - a port that is none (Java takes any number, and refuses one after a
|
||||
// space without a word) is marked red and not handed over.
|
||||
// - Java writes the firewall's explanation but never shows it; here it
|
||||
// stands under the boxes.
|
||||
// - a rule the window cannot make (from a file: one that drops, or names an
|
||||
// address) shows what it is in the sender column; Java shows it as a port
|
||||
// opened for all senders.
|
||||
// - the log cannot be typed into (Java's can).
|
||||
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react';
|
||||
import { firewallPortOfText, type FirewallLogEntry, type LogLine, type PortRuleProtocol } from '@mdg-filius/core';
|
||||
import type { FirewallView, FirewallViewRule } from '@mdg-filius/protocol';
|
||||
import { useTranslate, type Translate } from '../i18n/i18n.js';
|
||||
import type { Simulation } from '../simulation/useSimulation.js';
|
||||
import { firewallLine } from './serverLines.js';
|
||||
|
||||
const NO_LINES: readonly LogLine<FirewallLogEntry>[] = [];
|
||||
const PROTOCOL_NAMES: Readonly<Record<FirewallViewRule['protocol'], string>> = { tcp: 'TCP', udp: 'UDP', all: '*', icmp: 'ICMP' };
|
||||
/** Java's list: TCP, UDP, "*". */
|
||||
const PROTOCOLS: readonly PortRuleProtocol[] = ['tcp', 'udp', 'all'];
|
||||
|
||||
/** A rule's cells as Java's ruleToVector writes them: protocol, port (none for every port), sender. */
|
||||
export function firewallRow(rule: FirewallViewRule, t: Translate): readonly [string, string, string] {
|
||||
const sender = rule.sender === 'all' ? t('personalFirewall.allSenders') : rule.sender === 'sameNetwork' ? t('personalFirewall.localNetwork') : rule.text;
|
||||
return [PROTOCOL_NAMES[rule.protocol], rule.port >= 0 ? String(rule.port) : '', sender];
|
||||
}
|
||||
|
||||
export function PersonalFirewallPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) {
|
||||
const t = useTranslate();
|
||||
const topic = `app:${node}/firewall` as const;
|
||||
const { watch, act } = simulation;
|
||||
useEffect(() => watch([topic]), [watch, topic]);
|
||||
const view = simulation.views.get(topic) as FirewallView | null | undefined;
|
||||
const [tab, setTab] = useState<'setup' | 'log'>('setup');
|
||||
const [port, setPort] = useState('');
|
||||
const [protocol, setProtocol] = useState<PortRuleProtocol>('tcp');
|
||||
const [sameNetworkOnly, setSameNetworkOnly] = useState(false);
|
||||
const [chosen, setChosen] = useState<number | null>(null);
|
||||
const lines = (simulation.apps.get(topic as never)?.lines as readonly LogLine<FirewallLogEntry>[] | undefined) ?? NO_LINES;
|
||||
const log = lines.map((each) => `${firewallLine(each, t)}\n`).join('');
|
||||
const logElement = useRef<HTMLPreElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (logElement.current !== null) logElement.current.scrollTop = logElement.current.scrollHeight;
|
||||
}, [log, tab]);
|
||||
if (view === null || view === undefined) return <div className="web-server" />;
|
||||
const typed = firewallPortOfText(port);
|
||||
const chosenRow = chosen !== null && chosen < view.rules.length ? chosen : null;
|
||||
|
||||
const add = () => {
|
||||
if (typed === undefined) return;
|
||||
act(node, 'firewall', 'addRule', { port: typed, protocol, sameNetworkOnly });
|
||||
setPort('');
|
||||
};
|
||||
const remove = () => {
|
||||
if (chosenRow === null) return;
|
||||
act(node, 'firewall', 'removeRule', { row: chosenRow });
|
||||
setChosen(null);
|
||||
};
|
||||
const choose = (row: number) => (event: KeyboardEvent<HTMLTableRowElement>) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
setChosen(row);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="web-server personal-firewall">
|
||||
<div className="mail-tabs" role="tablist">
|
||||
{(['setup', 'log'] as const).map((each) => (
|
||||
<button key={each} type="button" role="tab" aria-selected={tab === each} onClick={() => setTab(each)}>
|
||||
{t(`personalFirewall.${each}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{tab === 'log' ? (
|
||||
<pre className="web-server-log" ref={logElement} role="tabpanel" aria-label={t('personalFirewall.log')} tabIndex={0}>
|
||||
{log}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="personal-firewall-page" role="tabpanel" aria-label={t('personalFirewall.setup')}>
|
||||
<div className="personal-firewall-boxes">
|
||||
<label>
|
||||
<input type="checkbox" checked={view.active} onChange={(event) => act(node, 'firewall', 'setActive', { active: event.target.checked })} />
|
||||
{t('firewall.active')}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" checked={view.icmpAllowed} disabled={!view.active} onChange={(event) => act(node, 'firewall', 'setIcmpAllowed', { allowed: event.target.checked })} />
|
||||
{t('personalFirewall.allowIcmp')}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" checked={view.filterUdp} disabled={!view.active} onChange={(event) => act(node, 'firewall', 'setFilterUdp', { enabled: event.target.checked })} />
|
||||
{t('personalFirewall.filterUdp')}
|
||||
</label>
|
||||
</div>
|
||||
<p className="personal-firewall-hint">{t('personalFirewall.hint')}</p>
|
||||
<div className="web-server-bar">
|
||||
<label>
|
||||
{t('personalFirewall.port')}
|
||||
<input
|
||||
value={port}
|
||||
inputMode="numeric"
|
||||
aria-invalid={port !== '' && typed === undefined ? 'true' : undefined}
|
||||
onChange={(event) => setPort(event.target.value)}
|
||||
onKeyDown={(event) => event.key === 'Enter' && add()}
|
||||
/>
|
||||
</label>
|
||||
<select aria-label={t('firewall.protocol')} value={protocol} onChange={(event) => setProtocol(event.target.value as PortRuleProtocol)}>
|
||||
{PROTOCOLS.map((each) => (
|
||||
<option key={each} value={each}>
|
||||
{PROTOCOL_NAMES[each]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select aria-label={t('personalFirewall.sender')} value={sameNetworkOnly ? 'sameNetwork' : 'all'} onChange={(event) => setSameNetworkOnly(event.target.value === 'sameNetwork')}>
|
||||
<option value="all">{t('personalFirewall.allSenders')}</option>
|
||||
<option value="sameNetwork">{t('personalFirewall.sameNetwork')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="web-server-bar">
|
||||
<button type="button" disabled={typed === undefined} onClick={add}>
|
||||
{t('personalFirewall.add')}
|
||||
</button>
|
||||
<button type="button" disabled={chosenRow === null} onClick={remove}>
|
||||
{t('personalFirewall.delete')}
|
||||
</button>
|
||||
</div>
|
||||
<table className="dns-server-table personal-firewall-table" aria-label={t('personalFirewall.rules')}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('firewall.protocol')}</th>
|
||||
<th>{t('personalFirewall.portColumn')}</th>
|
||||
<th>{t('personalFirewall.sender')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{view.rules.map((rule, row) => (
|
||||
<tr key={row} tabIndex={0} aria-selected={row === chosenRow} onPointerDown={() => setChosen(row)} onKeyDown={choose(row)}>
|
||||
{firewallRow(rule, t).map((cell, column) => (
|
||||
<td key={column}>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1140,3 +1140,65 @@
|
||||
.dns-server-table tr[aria-selected='true'] input {
|
||||
background: #b8cfe5;
|
||||
}
|
||||
|
||||
/* A computer's firewall (GUIApplicationFirewallWindow): two tabs, the boxes,
|
||||
* the port with its two lists, two buttons and the table of rules; the log. */
|
||||
.personal-firewall-page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.personal-firewall-boxes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.personal-firewall-boxes label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 44px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.personal-firewall-boxes input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.personal-firewall-hint {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.personal-firewall input:not([type='checkbox']),
|
||||
.personal-firewall select {
|
||||
min-height: 32px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #a0a0a0;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
font: 16px Dialog, sans-serif;
|
||||
}
|
||||
|
||||
.personal-firewall input:not([type='checkbox']) {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.personal-firewall [aria-invalid='true'] {
|
||||
border-color: #ff0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.personal-firewall-table td {
|
||||
min-height: 32px;
|
||||
padding: 6px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.personal-firewall-table tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// ServerMitarbeiter, TCPSocket's failures), and the generic client's as its
|
||||
// window writes them (ClientBaustein, Socket, TCPSocket), in the person's
|
||||
// language: one line each (a failed connection two), as Java appends them to
|
||||
// its window's text area.
|
||||
// its window's text area. And a computer's firewall's log as its window
|
||||
// writes it (Firewall.benachrichtigeBeobachter, GUIApplicationFirewallWindow).
|
||||
|
||||
import type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import { firewallRuleText, PORT_RULE_PROTOCOL_NAMES, type FirewallLogEntry, type ServerLogEntry, type SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import type { Translate } from '../i18n/i18n.js';
|
||||
|
||||
export function serverLine(entry: ServerLogEntry, t: Translate): string {
|
||||
@@ -45,3 +46,20 @@ export function simpleClientLine(entry: SimpleClientLogEntry, t: Translate): str
|
||||
return t(`server.sendFailed.${entry.failure}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function firewallLine(entry: FirewallLogEntry, t: Translate): string {
|
||||
switch (entry.kind) {
|
||||
case 'ruleMatched':
|
||||
return t('personalFirewall.ruleMatched', {
|
||||
number: entry.ruleNumber,
|
||||
rule: firewallRuleText(entry.rule, entry.firstInterface),
|
||||
action: t(entry.accepted ? 'firewall.accept' : 'firewall.drop'),
|
||||
});
|
||||
case 'icmpDropped':
|
||||
return t('personalFirewall.icmpDropped', { sender: entry.senderIPAddress, receiver: entry.receiverIPAddress, code: entry.code, type: entry.type });
|
||||
case 'portRuleAdded':
|
||||
return t('personalFirewall.portRuleAdded', { port: entry.port, protocol: PORT_RULE_PROTOCOL_NAMES[entry.protocol] });
|
||||
case 'portRuleRemoved':
|
||||
return t('personalFirewall.portRuleRemoved');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,24 @@
|
||||
"network.ipAddress": "IP-Adresse:",
|
||||
"network.macAddress": "MAC-Adresse:",
|
||||
"network.subnetMask": "Netzmaske:",
|
||||
"personalFirewall.add": "Hinzufügen",
|
||||
"personalFirewall.allSenders": "Alle Absender",
|
||||
"personalFirewall.allowIcmp": "Internet Control Message Protocol (Ping etc.) erlauben",
|
||||
"personalFirewall.delete": "Löschen",
|
||||
"personalFirewall.filterUdp": "Aktiviere UDP-Datenverkehrfilter",
|
||||
"personalFirewall.hint": "Die Firewall blockiert alle eingehenden IP-Pakete, wenn sie aktiviert wurde. Ausnahmen müssen explizit angegeben werden. Regeln bestimmen TCP-/UDP-Ports, für die Daten zugelassen werden und ob die Ausnahme nur für alle Absender oder nur für Absender im lokalen Rechnernetz gilt.",
|
||||
"personalFirewall.icmpDropped": "ICMP-Paket verworfen: {sender} -> {receiver} (code: {code}, type: {type})",
|
||||
"personalFirewall.localNetwork": "Nur lokales Rechnernetz",
|
||||
"personalFirewall.log": "Log",
|
||||
"personalFirewall.port": "Zulässiger Port/Protokoll:",
|
||||
"personalFirewall.portColumn": "Port",
|
||||
"personalFirewall.portRuleAdded": "Portregel wurde hinzugefügt: {port}/{protocol}",
|
||||
"personalFirewall.portRuleRemoved": "Portregel wurde entfernt.",
|
||||
"personalFirewall.ruleMatched": "Firewall-Regel hat gegriffen: #{number} ({rule}) -> {action}",
|
||||
"personalFirewall.rules": "Regeln",
|
||||
"personalFirewall.sameNetwork": "Absender im gleichen Rechnernetz",
|
||||
"personalFirewall.sender": "Absender",
|
||||
"personalFirewall.setup": "Konfiguration",
|
||||
"program.commandPrompt": "Befehlszeile",
|
||||
"program.dnsServer": "DNS-Server",
|
||||
"program.echoServer": "Echo-Server",
|
||||
|
||||
@@ -248,6 +248,24 @@
|
||||
"network.ipAddress": "IP address:",
|
||||
"network.macAddress": "MAC address:",
|
||||
"network.subnetMask": "Netmask:",
|
||||
"personalFirewall.add": "Add",
|
||||
"personalFirewall.allSenders": "All sender",
|
||||
"personalFirewall.allowIcmp": "Allow Internet Control Message Protocol (Ping etc.)",
|
||||
"personalFirewall.delete": "Delete",
|
||||
"personalFirewall.filterUdp": "Activate UDP traffic filter",
|
||||
"personalFirewall.hint": "The firewall blocks incoming IP packets, if it is activated. Exceptions must be declared. Rules define opened ports and whether they are opened for global or local sender.",
|
||||
"personalFirewall.icmpDropped": "Dropped ICMP packet: {sender} -> {receiver} (code: {code}, type: {type})",
|
||||
"personalFirewall.localNetwork": "Local network",
|
||||
"personalFirewall.log": "Log",
|
||||
"personalFirewall.port": "Opened port/protocol:",
|
||||
"personalFirewall.portColumn": "Port",
|
||||
"personalFirewall.portRuleAdded": "Port rule is added: {port}/{protocol}",
|
||||
"personalFirewall.portRuleRemoved": "Delete port rule.",
|
||||
"personalFirewall.ruleMatched": "Firewall rule matched: #{number} ({rule}) -> {action}",
|
||||
"personalFirewall.rules": "Rules",
|
||||
"personalFirewall.sameNetwork": "Local sender",
|
||||
"personalFirewall.sender": "Sender",
|
||||
"personalFirewall.setup": "Setup",
|
||||
"program.commandPrompt": "Command Line",
|
||||
"program.dnsServer": "DNS server",
|
||||
"program.echoServer": "Echo server",
|
||||
|
||||
@@ -175,6 +175,20 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||
'firewall.dropIcmp': 'jfirewalldialog_msg40',
|
||||
'firewall.dropIcmpHint': 'jfirewalldialog_msg41',
|
||||
'firewall.filterSyn': 'jfirewalldialog_msg42',
|
||||
'personalFirewall.hint': 'firewall_msg2',
|
||||
'personalFirewall.port': 'firewall_msg3',
|
||||
'personalFirewall.allSenders': 'firewall_msg4',
|
||||
'personalFirewall.sameNetwork': 'firewall_msg5',
|
||||
'personalFirewall.add': 'firewall_msg6',
|
||||
'personalFirewall.delete': 'firewall_msg7',
|
||||
'personalFirewall.portColumn': 'firewall_msg8',
|
||||
'personalFirewall.sender': 'firewall_msg9',
|
||||
'personalFirewall.setup': 'firewall_msg10',
|
||||
'personalFirewall.log': 'firewall_msg11',
|
||||
'personalFirewall.localNetwork': 'firewall_msg12',
|
||||
'personalFirewall.allowIcmp': 'firewall_msg13',
|
||||
'personalFirewall.filterUdp': 'firewall_msg14',
|
||||
'personalFirewall.portRuleRemoved': 'sw_firewall_msg7',
|
||||
'sheet.connections': 'jvermittlungsrechnerkonfiguration_msg23',
|
||||
'connections.local': 'jvermittlungsrechnerkonfiguration_msg22',
|
||||
'connections.add': 'jvermittlungsrechnerkonfiguration_msg24',
|
||||
@@ -319,6 +333,9 @@ const FROM_JAVA_JOINED: Readonly<Record<string, readonly [string, string, string
|
||||
|
||||
/** Java's words before a value, joined as its programs join them (SMTPMitarbeiter, SMTPClient, the server window's question). */
|
||||
const FROM_JAVA_BEFORE: Readonly<Record<string, readonly [string, string, string?]>> = {
|
||||
'personalFirewall.ruleMatched': ['sw_firewall_msg8', ' #{number} ({rule}) -> {action}'],
|
||||
'personalFirewall.icmpDropped': ['firewallthread_msg1', ' {sender} -> {receiver} (code: {code}, type: {type})'],
|
||||
'personalFirewall.portRuleAdded': ['sw_firewall_msg5', ' {port}/{protocol}'],
|
||||
'mail.sendFailed': ['sw_smtpmitarbeiter_msg14', ' {address}'],
|
||||
'mail.mailSent': ['sw_smtpclient_msg4', ' {recipients}'],
|
||||
// "'" + name + "'", the question's two halves written against it.
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// A server's lines as Java's web server window wrote them
|
||||
// (packages/sim/test/browserWindow.java.json, BrowserWindowRun): each kind
|
||||
// the window got there, in Java's words.
|
||||
// the window got there, in Java's words. And a computer's firewall's log and
|
||||
// table as Java's firewall window wrote them
|
||||
// (packages/core/test/firewallWindow.java.json, FirewallWindowRun).
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import { serverLine, simpleClientLine } from '../src/desktop/serverLines.js';
|
||||
import { DIRECTLY_CONNECTED_SOURCE_MARKER, firewallRule, type FirewallLogEntry, type ServerLogEntry, type SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import type { FirewallViewRule } from '@mdg-filius/protocol';
|
||||
import { firewallRow } from '../src/desktop/PersonalFirewallPanel.js';
|
||||
import { firewallLine, serverLine, simpleClientLine } from '../src/desktop/serverLines.js';
|
||||
import { translate } from '../src/i18n/i18n.js';
|
||||
|
||||
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../sim/test/browserWindow.java.json'), 'utf8')) as { steps: { step: string; log?: string }[] };
|
||||
@@ -78,3 +82,43 @@ describe("the echo windows' lines", () => {
|
||||
]).toEqual(['Fehler beim Verbindungsaufbau', 'Fehler beim Verbindungsaufbau\nAktives Öffnen des Sockets fehlgeschlagen', 'Keine Verbindung hergestellt!', 'Verbindung nicht aufgebaut!']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a computer's firewall's lines", () => {
|
||||
const firewallJava = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../core/test/firewallWindow.java.json'), 'utf8')) as { steps: { step: string; log: string }[] };
|
||||
const javaLog = firewallJava.steps.find((step) => step.step === 'delete row 0')!.log.split('\n');
|
||||
|
||||
it("are Java's, line by line", () => {
|
||||
const network = { ipAddress: '10.0.0.10', subnetMask: '255.255.255.0' };
|
||||
const entries: FirewallLogEntry[] = [
|
||||
{ kind: 'portRuleAdded', port: 55555, protocol: 'tcp' },
|
||||
{ kind: 'ruleMatched', ruleNumber: 1, rule: firewallRule({ port: 55555, protocol: 'tcp', action: 'accept' }), firstInterface: network, accepted: true },
|
||||
{ kind: 'ruleMatched', ruleNumber: 2, rule: firewallRule({ port: 53, protocol: 'udp', action: 'accept', sourceIPAddress: DIRECTLY_CONNECTED_SOURCE_MARKER }), firstInterface: network, accepted: true },
|
||||
{ kind: 'icmpDropped', senderIPAddress: '10.0.0.2', receiverIPAddress: '10.0.0.10', type: 8, code: 0 },
|
||||
{ kind: 'portRuleAdded', port: -1, protocol: 'all' },
|
||||
{ kind: 'portRuleRemoved' },
|
||||
];
|
||||
const lines = entries.map((entry) => firewallLine(entry, t));
|
||||
for (const line of lines) expect(javaLog, line).toContain(line);
|
||||
expect(firewallLine({ kind: 'ruleMatched', ruleNumber: 3, rule: firewallRule({ port: 80 }), firstInterface: undefined, accepted: false }, t)).toBe(
|
||||
'Firewall-Regel hat gegriffen: #3 (*/* -> */*; TCP:80 => DROP) -> verwerfen',
|
||||
);
|
||||
});
|
||||
|
||||
it("are the table's rows as Java's ruleToVector writes them, and name a rule the window cannot make", () => {
|
||||
expect(
|
||||
[
|
||||
{ protocol: 'tcp', port: 55555, sender: 'all', text: '' },
|
||||
{ protocol: 'udp', port: 53, sender: 'sameNetwork', text: '' },
|
||||
{ protocol: 'all', port: -1, sender: 'all', text: '' },
|
||||
{ protocol: 'tcp', port: 0, sender: 'all', text: '' },
|
||||
{ protocol: 'icmp', port: -1, sender: null, text: '10.0.0.0/255.0.0.0 -> */*; ?:* => DROP' },
|
||||
].map((rule) => firewallRow(rule as FirewallViewRule, t)),
|
||||
).toEqual([
|
||||
['TCP', '55555', 'Alle Absender'],
|
||||
['UDP', '53', 'Nur lokales Rechnernetz'],
|
||||
['*', '', 'Alle Absender'],
|
||||
['TCP', '0', 'Alle Absender'],
|
||||
['ICMP', '', '10.0.0.0/255.0.0.0 -> */*; ?:* => DROP'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user