Files
mitm-webserver/frontend/src/components/FireWallAddTableModal.tsx
malmert b60a1d3118
Some checks failed
Build and Deploy MITM Webserver / build (push) Failing after 3s
file structure and comments unified
2026-03-06 19:03:26 +01:00

277 lines
8.2 KiB
TypeScript

import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Typography, notification } from 'antd';
import { ReactElement, useEffect, useState } from 'react';
import { execFirewallRaw, fetchRuleset } from '../api/apiClient';
const { Paragraph, Text } = Typography;
const { Option } = Select;
type ExecResult = {
rc: number;
stdout?: string | null;
stderr?: string | null;
};
type CmdResult = {
cmd: string;
out?: ExecResult;
err?: string;
};
type Props = {
open: boolean;
onClose?: (created?: boolean) => void; // created === true when a table was created
startOnPreview?: boolean;
};
const FAMILY_DESCRIPTIONS: Record<string, string> = {
ip: 'IPv4 packets.',
ip6: 'IPv6 packets.',
inet: 'Both IPv4 and IPv6.',
arp: 'ARP packets.',
bridge: 'Packets passing through a bridge.',
netdev: 'Direct filtering on a network device (ingress).',
};
export default function FirewallAddTableModal({ open, onClose, startOnPreview = false }: Props): ReactElement {
const [form] = Form.useForm();
const [localLoadingRuleset, setLocalLoadingRuleset] = useState(false);
const [rulesetEmpty, setRulesetEmpty] = useState<boolean>(false);
const [step, setStep] = useState<number>(startOnPreview ? 1 : 0);
const [running, setRunning] = useState(false);
const [results, setResults] = useState<CmdResult[]>([]);
const [selectedFamily, setSelectedFamily] = useState<string>('bridge');
useEffect(() => {
form.setFieldsValue({
family: 'bridge',
tableName: 'filter',
});
setSelectedFamily('bridge');
if (open) {
refreshRuleset();
setResults([]);
setRunning(false);
setStep(startOnPreview ? 1 : 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
async function refreshRuleset() {
setLocalLoadingRuleset(true);
try {
const res = await fetchRuleset();
const ruleset = (res as any).ruleset;
if (ruleset && Array.isArray(ruleset.tables)) {
setRulesetEmpty(ruleset.tables.length === 0);
} else {
setRulesetEmpty(false);
}
} catch (err: any) {
notification.warning({
message: 'Could not load ruleset',
description: err?.message ?? String(err),
duration: 6,
});
setRulesetEmpty(false);
} finally {
setLocalLoadingRuleset(false);
}
}
function buildCommands(values: any): string[] {
const family = (values.family ?? 'bridge').trim();
const table = (values.tableName ?? 'filter').trim();
return [`add table ${family} ${table}`];
}
async function executeCommands(cmds: string[]) {
setRunning(true);
setResults([]);
const acc: CmdResult[] = [];
for (const cmd of cmds) {
try {
const out = (await execFirewallRaw(cmd)) as ExecResult;
const success = out && (out.rc === 0 || out.rc === -1);
if (success) acc.push({ cmd, out });
else acc.push({ cmd, out, err: `rc: ${out.rc} stderr: ${out.stderr ?? ''}` });
} catch (err: any) {
acc.push({ cmd, err: err?.message ?? String(err) });
}
}
setResults(acc);
setRunning(false);
try {
await refreshRuleset();
} catch {
}
const hadError = acc.some((r) => r.err);
if (!hadError) {
notification.success({
message: 'Table created',
description: 'Table was created and ruleset has been refreshed locally in the modal.',
duration: 4,
});
onClose?.(true); // signal parent to refresh and close modal
} else {
notification.error({
message: 'Some commands returned errors',
description: 'See execution results below for details.',
duration: 6,
});
setStep(1);
}
}
function renderFooter() {
return (
<Space>
<Button
onClick={() => {
if (step === 0) onClose?.(false);
else setStep(0);
}}
>
{step === 0 ? 'Cancel' : 'Back'}
</Button>
{step === 0 ? (
<Space>
<Button
type="primary"
onClick={() => {
form
.validateFields()
.then(() => setStep(1))
.catch(() =>
notification.warning({
message: 'Validation',
description: 'Please fill required fields before preview.',
}),
);
}}
>
Preview
</Button>
</Space>
) : (
<Button
type="primary"
loading={running}
onClick={() => executeCommands(buildCommands(form.getFieldsValue()))}
>
Execute
</Button>
)}
</Space>
);
}
if (localLoadingRuleset) {
return (
<Modal title="Create Table" open={open} onCancel={() => onClose?.(false)} footer={null} width={700}>
<div style={{ textAlign: 'center', padding: 28 }}>
<Spin />
</div>
</Modal>
);
}
return (
<Modal
title="Create Table"
open={open}
onCancel={() => onClose?.(false)}
width={700}
footer={renderFooter()}
destroyOnClose
>
{step === 0 && (
<div>
<Form
form={form}
layout="vertical"
onValuesChange={(changed) => {
if (changed.family) setSelectedFamily(changed.family);
}}
>
<Row gutter={12}>
<Col span={8}>
<Form.Item name="family" label="Family" rules={[{ required: true }]}>
<Select defaultValue="bridge" value={selectedFamily}>
<Option value="ip">ip</Option>
<Option value="ip6">ip6</Option>
<Option value="inet">inet</Option>
<Option value="arp">arp</Option>
<Option value="bridge">bridge</Option>
<Option value="netdev">netdev</Option>
</Select>
</Form.Item>
<Paragraph type="secondary" style={{ marginTop: 8 }}>
{FAMILY_DESCRIPTIONS[selectedFamily]}
</Paragraph>
</Col>
<Col span={8}>
<Form.Item name="tableName" label="Table Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
</Row>
</Form>
</div>
)}
{step === 1 && (
<>
<Card title="Command preview" style={{ marginBottom: 12 }}>
<Paragraph>
The command below will be executed on the server (server will prefix with <Text code>nft</Text>):
</Paragraph>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fafafa', padding: 12 }}>
{buildCommands(form.getFieldsValue()).join('\n')}
</pre>
</Card>
{results.length > 0 && (
<Card title="Execution results" style={{ marginTop: 12 }}>
{results.map((r, i) => (
<div key={i} style={{ marginBottom: 12 }}>
<Text strong>{r.cmd}</Text>
{r.err ? (
<Paragraph type="danger">{r.err}</Paragraph>
) : (
<>
<Paragraph>
<Text type="secondary">rc:</Text> {r.out?.rc}
</Paragraph>
{r.out?.stdout ? (
<>
<Text type="secondary">stdout:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fff', padding: 8 }}>{r.out.stdout}</pre>
</>
) : null}
{r.out?.stderr ? (
<>
<Text type="secondary">stderr:</Text>
<pre style={{ whiteSpace: 'pre-wrap', background: '#fff', padding: 8 }}>{r.out.stderr}</pre>
</>
) : null}
</>
)}
</div>
))}
</Card>
)}
</>
)}
</Modal>
);
}