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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user