feat(editor): F4.5d the firewall in the device sheet
Measured first: SheetRun drives FILIUS' firewall dialog down to its table editor (Swing's own end of an edit). Java's table writes a cell into the rule before it checks it, so that the firewall holds "abc", a mask of 255.0.255.0 or port 0 while the table shows the old value, and reads a port that is no number as all ports. Here nothing the check refuses reaches the rule. - doc: the three switches, the default action, rules (new as Java's, cells by Java's checks: masks of ones then zeros, the directly connected marker as a source only, an empty port for all ports; protocol, action, moved up or down, removed) - editor: the firewall as a page of the router's and home router's sheet, with Java's explanations; the general tab named so (Java: "network interfaces") Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -348,6 +348,66 @@ test("keeps a home router's port forwarding on its own page, and shows its DHCP
|
||||
await expect(sheet(page).getByRole('tab', { name: 'LAN: 192.168.0.10' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
test("sets up a router's firewall on its own page: the switches, the default, rules edited, moved and removed", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'new');
|
||||
await page.locator('[data-palette="router"]').click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: '2', exact: true }).click();
|
||||
const id = (await drawing(page)).nodes[0]!.id;
|
||||
const firewall = async () => ((await sheetOf(page, id)) as RouterSheet).firewall;
|
||||
await sheet(page).getByRole('button', { name: 'Firewall einrichten' }).click();
|
||||
await expect(sheet(page).getByRole('heading')).toHaveText('Firewall einrichten');
|
||||
await expect(sheet(page).getByRole('tab')).toHaveText(['Allgemein', 'Firewall-Regeln']);
|
||||
await sheet(page).getByLabel('Firewall aktivieren').check();
|
||||
await sheet(page).getByLabel('nur SYN-Pakete verwerfen').uncheck();
|
||||
await expect.poll(firewall).toMatchObject({ isActive: true, filterSynSegmentsOnly: false });
|
||||
|
||||
await sheet(page).getByRole('tab', { name: 'Firewall-Regeln' }).click();
|
||||
await sheet(page).locator('select[name="defaultPolicy"]').selectOption('accept');
|
||||
await sheet(page).getByRole('button', { name: 'Neue Regel' }).click();
|
||||
const rows = sheet(page).locator('table[data-rows="firewall"] tbody tr');
|
||||
await expect(rows.first().locator('td')).toHaveText(['1', '', '', '', '', 'TCP', '', 'verwerfen']);
|
||||
await expect(sheet(page).getByRole('button', { name: 'rauf' })).toBeDisabled();
|
||||
await field(page, 'firewall-sourceIPAddress').fill('abc');
|
||||
await field(page, 'firewall-sourceIPAddress').press('Enter');
|
||||
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin (leer).');
|
||||
await field(page, 'firewall-sourceIPAddress').fill('10.0.0.0');
|
||||
await field(page, 'firewall-sourceIPAddress').press('Enter');
|
||||
await field(page, 'firewall-sourceSubnetMask').fill('255.0.255.0');
|
||||
await field(page, 'firewall-sourceSubnetMask').press('Enter');
|
||||
await expect(field(page, 'firewall-sourceSubnetMask')).toHaveAttribute('aria-invalid', 'true');
|
||||
await field(page, 'firewall-sourceSubnetMask').fill('255.0.0.0');
|
||||
await field(page, 'firewall-sourceSubnetMask').press('Enter');
|
||||
await field(page, 'firewall-port').fill('80');
|
||||
await field(page, 'firewall-port').press('Enter');
|
||||
await sheet(page).locator('select[name="firewall-protocol"]').selectOption('udp');
|
||||
await sheet(page).locator('select[name="firewall-action"]').selectOption('accept');
|
||||
await expect(rows.first().locator('td')).toHaveText(['1', '10.0.0.0', '255.0.0.0', '', '', 'UDP', '80', 'akzeptieren']);
|
||||
|
||||
await sheet(page).getByRole('button', { name: 'Neue Regel' }).click();
|
||||
await field(page, 'firewall-destinationIPAddress').fill('192.168.0.1');
|
||||
await field(page, 'firewall-destinationIPAddress').press('Enter');
|
||||
await expect(sheet(page).getByRole('button', { name: 'runter' })).toBeDisabled();
|
||||
await sheet(page).getByRole('button', { name: 'rauf' }).click();
|
||||
await expect.poll(async () => (await firewall()).rules.map((rule) => rule.destinationIPAddress)).toEqual(['192.168.0.1', '']);
|
||||
await expect(rows.first()).toHaveAttribute('aria-selected', 'true');
|
||||
await sheet(page).getByRole('button', { name: 'Regel löschen' }).click();
|
||||
await expect.poll(async () => (await firewall()).rules.map((rule) => rule.sourceIPAddress)).toEqual(['10.0.0.0']);
|
||||
// Java keeps a row chosen: the one that took the removed one's place.
|
||||
await expect(rows.first()).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(field(page, 'firewall-sourceIPAddress')).toHaveValue('10.0.0.0');
|
||||
await expect(await firewall()).toMatchObject({ defaultPolicy: 'accept' });
|
||||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||||
await expect(sheet(page).getByRole('heading')).toHaveText('Vermittlungsrechner');
|
||||
});
|
||||
|
||||
test("opens a home router's firewall from its general tab", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'new');
|
||||
await page.locator('[data-palette="gateway"]').click();
|
||||
await sheet(page).getByRole('button', { name: 'Firewall einrichten' }).click();
|
||||
await expect(sheet(page).getByRole('heading')).toHaveText('Firewall einrichten');
|
||||
await expect(sheet(page).getByLabel('Firewall aktivieren')).not.toBeChecked();
|
||||
});
|
||||
|
||||
test.describe('on a phone', () => {
|
||||
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });
|
||||
|
||||
|
||||
@@ -42,6 +42,29 @@
|
||||
"dhcp.static": "Statische Adresszuweisung",
|
||||
"dhcp.subnetMask": "Netzmaske",
|
||||
"dhcp.upperBound": "Adress-Obergrenze",
|
||||
"firewall.accept": "akzeptieren",
|
||||
"firewall.action": "Aktion",
|
||||
"firewall.active": "Firewall aktivieren",
|
||||
"firewall.activeHint": "Erst durch Aktivieren der Firewall werden die Firewall-Regeln angewendet. Bei deaktivierter Firewall findet keine Filterung statt.",
|
||||
"firewall.add": "Neue Regel",
|
||||
"firewall.defaultPolicy": "Standardaktion, wenn keine Regel greift",
|
||||
"firewall.delete": "Regel löschen",
|
||||
"firewall.destinationIPAddress": "Ziel-IP",
|
||||
"firewall.destinationSubnetMask": "Netzmaske",
|
||||
"firewall.down": "runter",
|
||||
"firewall.drop": "verwerfen",
|
||||
"firewall.dropIcmp": "ICMP-Pakete filtern",
|
||||
"firewall.dropIcmpHint": "Alle ICMP Pakete, z.B. Ping-Anfragen, werden von der Firewall verworfen.",
|
||||
"firewall.filterSyn": "nur SYN-Pakete verwerfen",
|
||||
"firewall.filterSynHint": "Neue Verbindungsanfragen zu gesperrten Ports werden verworfen.\nDer Rückkanal für erlaubte Verbindungsanfragen wird akzeptiert.",
|
||||
"firewall.intro": "Die folgenden Regeln werden von oben nach unten durchlaufen. Sobald eine Regel greift, wird die gewählte Aktion ausgeführt.\nAlle Angaben (außer Protokoll und Aktion) in einer Regel sind optional. Leere Felder werden zur Filterung nicht ausgewertet.\nWenn eine IP-Adresse angegeben wird, muss auch die Netzwerkmaske angegeben sein! Mit einer Maske ungleich 255.255.255.255 lassen sich ganze Netzbereiche adressieren.\nÜber die Standard-Aktion kann zwischen Blacklisting- und Whitelisting-Ansatz gewechselt werden.",
|
||||
"firewall.number": "ID",
|
||||
"firewall.port": "Port",
|
||||
"firewall.protocol": "Protokoll",
|
||||
"firewall.rules": "Firewall-Regeln",
|
||||
"firewall.sourceIPAddress": "Quell-IP",
|
||||
"firewall.sourceSubnetMask": "Netzmaske",
|
||||
"firewall.up": "rauf",
|
||||
"forwarding.lanIPAddress": "LAN-Adresse",
|
||||
"forwarding.lanPortValue": "LAN-Port",
|
||||
"forwarding.protocolValue": "Protokoll",
|
||||
@@ -71,6 +94,7 @@
|
||||
"sheet.dhcpServer": "DHCP-Server einrichten",
|
||||
"sheet.dnsServer": "Domain Name Server",
|
||||
"sheet.empty": "(leer)",
|
||||
"sheet.firewall": "Firewall einrichten",
|
||||
"sheet.forwardingTable": "Weiterleitungstabelle",
|
||||
"sheet.general": "Allgemein",
|
||||
"sheet.invalidValue": "Dieser Wert ist ungültig.",
|
||||
|
||||
@@ -42,6 +42,29 @@
|
||||
"dhcp.static": "Static Address Assignment",
|
||||
"dhcp.subnetMask": "Netmask",
|
||||
"dhcp.upperBound": "Upper bound of address",
|
||||
"firewall.accept": "accept",
|
||||
"firewall.action": "Action",
|
||||
"firewall.active": "Activate firewall",
|
||||
"firewall.activeHint": "Firewall rules will only be applied if the firewall is activated. In the case of a not activated firewall no packet filtering will take place.",
|
||||
"firewall.add": "add new rule",
|
||||
"firewall.defaultPolicy": "Default action, if no rule matches",
|
||||
"firewall.delete": "delete rule",
|
||||
"firewall.destinationIPAddress": "Dest. IP",
|
||||
"firewall.destinationSubnetMask": "Netmask",
|
||||
"firewall.down": "move down",
|
||||
"firewall.drop": "drop",
|
||||
"firewall.dropIcmp": "Filter ICMP packets",
|
||||
"firewall.dropIcmpHint": "All ICMP packets, e.g., ping requests, will be dropped.",
|
||||
"firewall.filterSyn": "only drop SYN packets",
|
||||
"firewall.filterSynHint": "New connection requests to blocked ports will be dropped.\nBack channels for allowed connection requests will be accepted.",
|
||||
"firewall.intro": "The following rules are evaluated from top to bottom. For the first matching rule the given action will be used.\nAll fields (except protocol and action) are optional. Empty fields will not be considered for packet filtering.\nIf an IP address is used, then the subnet mask has to be filled in as well! A subnet mask different to 255.255.255.255 can be used to address whole subnet ranges.\nUsing the default policy switch the approach can be toggled from blacklisting to whitelisting and vice versa.",
|
||||
"firewall.number": "ID",
|
||||
"firewall.port": "Port",
|
||||
"firewall.protocol": "Protocol",
|
||||
"firewall.rules": "Firewall Rules",
|
||||
"firewall.sourceIPAddress": "Source IP",
|
||||
"firewall.sourceSubnetMask": "Netmask",
|
||||
"firewall.up": "move up",
|
||||
"forwarding.lanIPAddress": "LAN Address",
|
||||
"forwarding.lanPortValue": "LAN Port",
|
||||
"forwarding.protocolValue": "Protocol",
|
||||
@@ -71,6 +94,7 @@
|
||||
"sheet.dhcpServer": "DHCP server setup",
|
||||
"sheet.dnsServer": "Domain Name Server",
|
||||
"sheet.empty": "(empty)",
|
||||
"sheet.firewall": "Firewall settings",
|
||||
"sheet.forwardingTable": "Forwarding table",
|
||||
"sheet.general": "General",
|
||||
"sheet.invalidValue": "This value is invalid.",
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ICON_URLS } from '../canvas/icons.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { NameField, type Change } from './NameField.js';
|
||||
import { HomeRouterFields, RouterFields } from './RouterSheets.js';
|
||||
import { FirewallPanel } from './FirewallPanel.js';
|
||||
import { DhcpServerPanel, PortForwardingPanel } from './ServicePanels.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
import './sheet.css';
|
||||
@@ -61,7 +62,7 @@ export interface DeviceSheetProps {
|
||||
}
|
||||
|
||||
/** A page of the sheet that Java opens as a dialog. */
|
||||
export type SheetPage = 'dhcp' | 'forwarding';
|
||||
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall';
|
||||
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight }: DeviceSheetProps) {
|
||||
const t = useTranslate();
|
||||
@@ -91,7 +92,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
‹
|
||||
</button>
|
||||
)}
|
||||
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : title}</h2>
|
||||
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : open === 'firewall' ? t('sheet.firewall') : title}</h2>
|
||||
<button type="button" className="sheet-close" onClick={onClose} aria-label={t('sheet.close')} title={t('sheet.close')}>
|
||||
×
|
||||
</button>
|
||||
@@ -105,7 +106,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
) : sheet.kind === 'remoteLink' ? (
|
||||
<ModemFields sheet={sheet} change={change} />
|
||||
) : sheet.kind === 'router' ? (
|
||||
<RouterFields sheet={sheet} change={change} onHighlight={onHighlight} />
|
||||
<RouterFields sheet={sheet} change={change} onHighlight={onHighlight} onOpen={onOpen} />
|
||||
) : sheet.kind === 'gateway' ? (
|
||||
<HomeRouterFields sheet={sheet} change={change} onHighlight={onHighlight} onOpen={onOpen} />
|
||||
) : (
|
||||
@@ -116,6 +117,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
<div key={`${id}-dhcp`}>
|
||||
<DhcpServerPanel server={sheet.dhcpServer} change={change} />
|
||||
</div>
|
||||
) : open === 'firewall' && (sheet.kind === 'router' || sheet.kind === 'gateway') ? (
|
||||
<div key={`${id}-firewall`}>
|
||||
<FirewallPanel firewall={sheet.firewall} change={change} />
|
||||
</div>
|
||||
) : open === 'forwarding' && sheet.kind === 'gateway' ? (
|
||||
<div key={`${id}-forwarding`}>
|
||||
<PortForwardingPanel sheet={sheet} change={change} />
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// A router's or home router's firewall as a page of the device sheet, after
|
||||
// Java's dialog (JFirewallDialog): a general tab with the three switches and
|
||||
// their explanations, a tab with the default action and the rules — a rule
|
||||
// chosen to edit its cells, moved up or down, a new rule at the end as
|
||||
// Java's (TCP, all ports, drop), a rule removed. Java's first tab says
|
||||
// "network interfaces" though it holds the general switches; here it says so.
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
addFirewallRule,
|
||||
FIREWALL_RULE_COLUMNS,
|
||||
firewallCellRule,
|
||||
moveFirewallRule,
|
||||
removeFirewallRule,
|
||||
setFirewallDefaultPolicy,
|
||||
setFirewallFlag,
|
||||
setFirewallRuleCell,
|
||||
setFirewallRuleChoice,
|
||||
checkField,
|
||||
type FirewallSheet,
|
||||
} from '@mdg-filius/doc';
|
||||
import type { FirewallAction, FirewallProtocol, FirewallRule } from '@mdg-filius/core';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Change } from './NameField.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
import { SheetTabs } from './SheetTabs.js';
|
||||
|
||||
/** Java's protocol names in the rule table (ruleAsVector): "*" for all. */
|
||||
const PROTOCOL_NAMES: Readonly<Record<FirewallProtocol, string>> = { all: '*', tcp: 'TCP', udp: 'UDP', icmp: 'ICMP' };
|
||||
/** Java's list offers all, TCP and UDP; ICMP comes only from a file, and is offered where a rule has it. */
|
||||
const OFFERED: readonly FirewallProtocol[] = ['all', 'tcp', 'udp'];
|
||||
|
||||
const cellText = (rule: FirewallRule, column: (typeof FIREWALL_RULE_COLUMNS)[number]) => (column === 'port' ? (rule.port < 0 ? '' : String(rule.port)) : rule[column]);
|
||||
|
||||
/** A text with Java's line breaks as paragraphs. */
|
||||
function Note({ text }: { readonly text: string }) {
|
||||
return (
|
||||
<>
|
||||
{text.split('\n').map((line, index) => (
|
||||
<p key={index} className="sheet-note">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FirewallPanel({ firewall, change }: { readonly firewall: FirewallSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
const [chosen, setChosen] = useState<number | null>(null);
|
||||
const rule = chosen === null ? undefined : firewall.rules[chosen];
|
||||
const flag = (name: 'isActive' | 'dropIcmp' | 'filterSynSegmentsOnly') => (on: boolean) => change.run(() => setFirewallFlag(change.doc, change.id, name, on, change.origin));
|
||||
const move = (step: -1 | 1) => {
|
||||
if (chosen !== null && change.run(() => moveFirewallRule(change.doc, change.id, chosen, step, change.origin))) setChosen(chosen + step);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SheetTabs
|
||||
tabs={[
|
||||
{ key: 'general', label: t('sheet.general') },
|
||||
{ key: 'rules', label: t('firewall.rules') },
|
||||
]}
|
||||
selected={tab}
|
||||
onSelect={setTab}
|
||||
/>
|
||||
<div role="tabpanel" key={tab}>
|
||||
{tab === 'general' ? (
|
||||
<>
|
||||
<SheetCheck name="firewallActive" label={t('firewall.active')} checked={firewall.isActive} onChange={flag('isActive')} />
|
||||
<Note text={t('firewall.activeHint')} />
|
||||
<SheetCheck name="dropIcmp" label={t('firewall.dropIcmp')} checked={firewall.dropIcmp} onChange={flag('dropIcmp')} />
|
||||
<Note text={t('firewall.dropIcmpHint')} />
|
||||
<SheetCheck name="filterSyn" label={t('firewall.filterSyn')} checked={firewall.filterSynSegmentsOnly} onChange={flag('filterSynSegmentsOnly')} />
|
||||
<Note text={t('firewall.filterSynHint')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Note text={t('firewall.intro')} />
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.defaultPolicy')}</span>
|
||||
<select
|
||||
name="defaultPolicy"
|
||||
value={firewall.defaultPolicy}
|
||||
onChange={(event) => change.run(() => setFirewallDefaultPolicy(change.doc, change.id, event.target.value as FirewallAction, change.origin))}
|
||||
>
|
||||
<option value="accept">{t('firewall.accept')}</option>
|
||||
<option value="drop">{t('firewall.drop')}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="route-table-frame">
|
||||
<table className="route-table" data-rows="firewall">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{t('firewall.number')}</th>
|
||||
{FIREWALL_RULE_COLUMNS.slice(0, 4).map((column) => (
|
||||
<th key={column} scope="col">
|
||||
{t(`firewall.${column}`)}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col">{t('firewall.protocol')}</th>
|
||||
<th scope="col">{t('firewall.port')}</th>
|
||||
<th scope="col">{t('firewall.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{firewall.rules.map((each, index) => (
|
||||
<tr key={index} data-manual={String(index)} aria-selected={chosen === index} onClick={() => setChosen(index)}>
|
||||
<td>{index + 1}</td>
|
||||
{FIREWALL_RULE_COLUMNS.slice(0, 4).map((column) => (
|
||||
// A cell its rule refuses (from a file) is marked, not mended.
|
||||
<td key={column} data-invalid={'invalid' in checkField(firewallCellRule(column), cellText(each, column)) ? 'true' : undefined}>
|
||||
{cellText(each, column)}
|
||||
</td>
|
||||
))}
|
||||
<td>{PROTOCOL_NAMES[each.protocol]}</td>
|
||||
<td data-invalid={'invalid' in checkField('portOrEmpty', cellText(each, 'port')) ? 'true' : undefined}>{cellText(each, 'port')}</td>
|
||||
<td>{t(each.action === 'accept' ? 'firewall.accept' : 'firewall.drop')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" disabled={chosen === null || chosen === 0} onClick={() => move(-1)}>
|
||||
{t('firewall.up')}
|
||||
</button>
|
||||
<button type="button" disabled={chosen === null || chosen === firewall.rules.length - 1} onClick={() => move(1)}>
|
||||
{t('firewall.down')}
|
||||
</button>
|
||||
<button type="button" onClick={() => setChosen(change.run(() => addFirewallRule(change.doc, change.id, change.origin)) ?? null)}>
|
||||
{t('firewall.add')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={chosen === null}
|
||||
onClick={() => {
|
||||
if (chosen === null) return;
|
||||
change.run(() => removeFirewallRule(change.doc, change.id, chosen, change.origin));
|
||||
// Java keeps the row chosen, or the last one when the last went.
|
||||
const left = firewall.rules.length - 1;
|
||||
setChosen(left === 0 ? null : Math.min(chosen, left - 1));
|
||||
}}
|
||||
>
|
||||
{t('firewall.delete')}
|
||||
</button>
|
||||
</div>
|
||||
{rule === undefined ? null : (
|
||||
<fieldset className="sheet-group" key={`rule-${String(chosen)}`}>
|
||||
{FIREWALL_RULE_COLUMNS.map((column) => (
|
||||
<SheetField
|
||||
key={column}
|
||||
name={`firewall-${column}`}
|
||||
label={t(`firewall.${column}`)}
|
||||
value={cellText(rule, column)}
|
||||
rule={firewallCellRule(column)}
|
||||
inputMode={column === 'port' ? 'numeric' : 'decimal'}
|
||||
onCommit={(text) => change.run(() => setFirewallRuleCell(change.doc, change.id, chosen!, column, text, change.origin))}
|
||||
/>
|
||||
))}
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.protocol')}</span>
|
||||
<select
|
||||
name="firewall-protocol"
|
||||
value={rule.protocol}
|
||||
onChange={(event) => change.run(() => setFirewallRuleChoice(change.doc, change.id, chosen!, { protocol: event.target.value as FirewallProtocol }, change.origin))}
|
||||
>
|
||||
{(OFFERED.includes(rule.protocol) ? OFFERED : [...OFFERED, rule.protocol]).map((protocol) => (
|
||||
<option key={protocol} value={protocol}>
|
||||
{PROTOCOL_NAMES[protocol]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.action')}</span>
|
||||
<select
|
||||
name="firewall-action"
|
||||
value={rule.action}
|
||||
onChange={(event) => change.run(() => setFirewallRuleChoice(change.doc, change.id, chosen!, { action: event.target.value as FirewallAction }, change.origin))}
|
||||
>
|
||||
<option value="accept">{t('firewall.accept')}</option>
|
||||
<option value="drop">{t('firewall.drop')}</option>
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +65,17 @@ function GatewayField({ value, change }: { readonly value: string; readonly chan
|
||||
);
|
||||
}
|
||||
|
||||
export function RouterFields({ sheet, change, onHighlight }: { readonly sheet: RouterSheet; readonly change: Change; readonly onHighlight: (link: string | null) => void }) {
|
||||
export function RouterFields({
|
||||
sheet,
|
||||
change,
|
||||
onHighlight,
|
||||
onOpen,
|
||||
}: {
|
||||
readonly sheet: RouterSheet;
|
||||
readonly change: Change;
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
readonly onOpen: (page: 'firewall') => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
// Java greys the table out while RIP runs; an open table goes back to the general tab.
|
||||
@@ -89,6 +99,11 @@ export function RouterFields({ sheet, change, onHighlight }: { readonly sheet: R
|
||||
<GatewayField value={sheet.gateway} change={change} />
|
||||
<SheetCheck name="rip" label={t('sheet.rip')} checked={sheet.rip} onChange={(on) => change.run(() => setRip(change.doc, change.id, on, change.origin))} />
|
||||
<SheetCheck name="ipForwarding" label={t('sheet.ipForwarding')} checked={sheet.ipForwarding} disabled onChange={() => undefined} />
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={() => onOpen('firewall')}>
|
||||
{t('sheet.firewall')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : card !== undefined ? (
|
||||
<InterfaceFields card={card} change={change} />
|
||||
@@ -109,7 +124,7 @@ export function HomeRouterFields({
|
||||
readonly sheet: HomeRouterSheet;
|
||||
readonly change: Change;
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
readonly onOpen: (page: 'dhcp' | 'forwarding') => void;
|
||||
readonly onOpen: (page: 'dhcp' | 'forwarding' | 'firewall') => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
@@ -131,6 +146,9 @@ export function HomeRouterFields({
|
||||
<GatewayField value={sheet.gateway} change={change} />
|
||||
<SheetCheck name="ipForwarding" label={t('sheet.ipForwarding')} checked={sheet.ipForwarding} disabled onChange={() => undefined} />
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={() => onOpen('firewall')}>
|
||||
{t('sheet.firewall')}
|
||||
</button>
|
||||
<button type="button" onClick={() => onOpen('forwarding')}>
|
||||
{t('sheet.portForwarding')}
|
||||
</button>
|
||||
|
||||
@@ -85,6 +85,26 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||
'forwarding.publicPortValue': 'jportforwarding_msg5',
|
||||
'forwarding.lanIPAddress': 'jportforwarding_msg6',
|
||||
'forwarding.lanPortValue': 'jportforwarding_msg7',
|
||||
'sheet.firewall': 'jvermittlungsrechnerkonfiguration_msg4',
|
||||
'firewall.rules': 'jfirewalldialog_msg21',
|
||||
'firewall.up': 'jfirewalldialog_msg22',
|
||||
'firewall.down': 'jfirewalldialog_msg23',
|
||||
'firewall.add': 'jfirewalldialog_msg24',
|
||||
'firewall.delete': 'jfirewalldialog_msg25',
|
||||
'firewall.number': 'jfirewalldialog_msg26',
|
||||
'firewall.sourceIPAddress': 'jfirewalldialog_msg27',
|
||||
'firewall.sourceSubnetMask': 'jfirewalldialog_msg28',
|
||||
'firewall.destinationIPAddress': 'jfirewalldialog_msg29',
|
||||
'firewall.destinationSubnetMask': 'jfirewalldialog_msg30',
|
||||
'firewall.protocol': 'jfirewalldialog_msg31',
|
||||
'firewall.accept': 'jfirewalldialog_msg33',
|
||||
'firewall.drop': 'jfirewalldialog_msg34',
|
||||
'firewall.port': 'jfirewalldialog_msg35',
|
||||
'firewall.active': 'jfirewalldialog_msg38',
|
||||
'firewall.activeHint': 'jfirewalldialog_msg39',
|
||||
'firewall.dropIcmp': 'jfirewalldialog_msg40',
|
||||
'firewall.dropIcmpHint': 'jfirewalldialog_msg41',
|
||||
'firewall.filterSyn': 'jfirewalldialog_msg42',
|
||||
};
|
||||
|
||||
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
|
||||
@@ -97,6 +117,13 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
|
||||
// Java's word, then what it joins or which card.
|
||||
'sheet.connectedTo': ['jvermittlungsrechnerkonfiguration_msg6', (java) => `${java} {device}`],
|
||||
'sheet.nic': ['jvermittlungsrechnerkonfiguration_msg10', (java) => `${java} {number}`],
|
||||
// A column's title without Java's question mark ("Aktion?").
|
||||
'firewall.action': ['jfirewalldialog_msg32', (java) => java.replace(/\?$/, '')],
|
||||
// A label above its list, without the colon.
|
||||
'firewall.defaultPolicy': ['jfirewalldialog_msg36', (java) => java.replace(/:$/, '')],
|
||||
// Java's "\n" as line breaks; the last sentence of the German starts with a small letter there.
|
||||
'firewall.intro': ['jfirewalldialog_msg37', (java) => java.replaceAll('\\n', '\n').replace('\nüber', '\nÜber')],
|
||||
'firewall.filterSynHint': ['jfirewalldialog_msg43', (java) => java.replaceAll('\\n', '\n')],
|
||||
};
|
||||
|
||||
describe('the catalogs', () => {
|
||||
|
||||
Reference in New Issue
Block a user