Files
mitm-webserver/frontend/src/components/SnifferManager.tsx
malmert 7812f8715f
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 1m39s
cleanup frontend notifications
2026-03-01 20:47:29 +01:00

311 lines
10 KiB
TypeScript

// src/components/Sniffing.tsx
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
PlayCircleOutlined,
PlusOutlined,
ReloadOutlined,
StopOutlined,
} from '@ant-design/icons';
import {
Button,
Card,
Form,
List,
Modal,
notification,
Radio,
Select,
Space,
Spin,
Tag,
Tooltip,
Typography,
} from 'antd';
import { ReactElement, useMemo, useState } from 'react';
import { startSniffer, stopSniffer, stopSnifferByInterface } from '../api/apiClient';
import { BridgeInfo, InterfaceInfo } from '../types/network';
import { InterfaceSnifferStatus } from '../types/sniffer';
const { Text } = Typography;
const { Option } = Select;
interface SnifferManagerProps {
interfaces: InterfaceInfo[];
bridges: BridgeInfo[];
statusMap: Record<string, InterfaceSnifferStatus>;
refreshAll: () => Promise<void>;
refreshStatus: () => Promise<void>;
loading: boolean;
statusLoading: boolean;
}
export default function SnifferManager(props: SnifferManagerProps): ReactElement {
// local UI state
// modal / form
const [isModalOpen, setIsModalOpen] = useState(false);
const [startMode, setStartMode] = useState<'interface' | 'bridge'>('interface');
const [form] = Form.useForm();
// derived entries
const statusEntries = useMemo(
() => Object.entries(props.statusMap) as [string, InterfaceSnifferStatus][],
[props.statusMap],
);
// open/close modal
const onOpenStartModal = () => {
form.resetFields();
setStartMode('interface');
setIsModalOpen(true);
};
const onCloseModal = () => setIsModalOpen(false);
// start submit
const handleStartSubmit = async (values: any) => {
const { target } = values;
if (!target) {
notification.warning({ message: 'Warning', description: 'Please select a target to start sniffing on.' });
return;
}
try {
const payload = startMode === 'interface' ? { interface: target } : { bridge: target };
const res = await startSniffer(payload);
notification.success({
message: 'Sniffer started',
description: `Sniffer started on ${target} (session ${res.session_id})`,
});
await props.refreshAll();
setIsModalOpen(false);
} catch (err: any) {
console.error('startSniffer error', err);
notification.error(err?.message ?? 'Failed to start sniffer');
} finally {
}
};
// stop all
const handleStopAll = async () => {
try {
await stopSniffer();
notification.success({
message: 'All sniffers stopped',
});
await props.refreshStatus();
} catch (err: any) {
console.error('stopSniffer error', err);
notification.error(err?.message ?? 'Failed to stop sniffers');
} finally {
}
};
// stop per-interface
const handleStopFromList = async (ifaceName: string, session_id?: string | null) => {
try {
// prefer stop by interface
await stopSnifferByInterface(ifaceName);
notification.success({
message: 'Sniffer stopped',
description: `Sniffer stopped on interface ${ifaceName}`,
});
await props.refreshStatus();
return;
} catch (err: any) {
console.error('stopSnifferByInterface error', err);
// try stop by session as fallback
if (session_id) {
try {
await stopSniffer({ session_id });
notification.success({
message: 'Sniffer stopped',
description: `Sniffer stopped on interface ${ifaceName} (session ${session_id})`,
});
await props.refreshStatus();
return;
} catch (err2: any) {
console.error('stopSniffer by session fallback failed', err2);
}
}
notification.error({
message: 'Failed to stop sniffer',
description: err?.message ?? 'Failed to stop sniffer',
});
} finally {
}
};
return (
<div className="sniffing-manager">
<Card
title="Sniffer sessions"
style={{ marginBottom: 16 }}
extra={
<Space>
<Tooltip title="Start a new sniffer session">
<Button icon={<PlusOutlined />} onClick={onOpenStartModal} />
</Tooltip>
<Tooltip title="Refresh status">
<Button icon={<ReloadOutlined />} onClick={() => props.refreshStatus()} />
</Tooltip>
<Tooltip title="Stop all sniffers">
<Button danger icon={<StopOutlined />} onClick={handleStopAll} />
</Tooltip>
</Space>
}
>
{props.statusLoading ? (
<div style={{ textAlign: 'center', padding: 24 }}>
<Spin />
</div>
) : statusEntries.length === 0 ? (
<div style={{ padding: 12 }}>
<Text type="secondary">No status information available.</Text>
</div>
) : (
<List
dataSource={statusEntries}
renderItem={([name, st]: [string, InterfaceSnifferStatus]) => {
const running = st.running;
const exists = st.exists;
const up = st.up;
const session_id = st.session_id ?? null;
const session_label = st.session_label ?? null;
return (
<List.Item
actions={[
running ? (
<Button
key="stop"
size="small"
icon={<StopOutlined />}
onClick={() => handleStopFromList(name, session_id)}
disabled={props.loading}
>
Stop
</Button>
) : (
<Button
key="start"
size="small"
type="primary"
icon={<PlayCircleOutlined />}
onClick={async () => {
// start on this interface
try {
const res = await startSniffer({ interface: name });
notification.success({
message: 'Sniffer started',
description: `Sniffer started on ${name} (session ${res.session_id})`,
});
await props.refreshStatus();
} catch (err: any) {
console.error('startSniffer quick', err);
notification.error({
message: 'Failed to start sniffer',
description: err?.message ?? 'Failed to start sniffer',
});
} finally {
}
}}
>
Start
</Button>
),
]}
>
<List.Item.Meta
title={
<Space>
<Text strong>{name}</Text>
{running ? (
<Tag icon={<CheckCircleOutlined />} color="success">
running
</Tag>
) : (
<Tag icon={<ExclamationCircleOutlined />} color="default">
stopped
</Tag>
)}
{!exists && <Tag color="error">missing</Tag>}
{exists && !up && <Tag color="warning">down</Tag>}
{exists && up && <Tag color="processing">up</Tag>}
{session_id && (
<Tag>
{session_label ? `${session_label}` : 'session'}:{' '}
<Text code copyable={{ text: session_id }}>
{session_id.slice(0, 8)}
</Text>
</Tag>
)}
</Space>
}
description={<Text type="secondary">interface: {name}</Text>}
/>
</List.Item>
);
}}
/>
)}
</Card>
{/* Start sniffer modal */}
<Modal
title="Start sniffer session"
open={isModalOpen}
onCancel={onCloseModal}
onOk={() => form.submit()}
confirmLoading={props.loading}
okText="Start"
>
<Form form={form} layout="vertical" onFinish={handleStartSubmit} initialValues={{ target: undefined }}>
<Form.Item label="Mode" name="mode">
<Radio.Group
value={startMode}
onChange={(e) => {
setStartMode(e.target.value);
form.setFieldsValue({ target: undefined });
}}
>
<Radio value="interface">Interface</Radio>
<Radio value="bridge">Bridge</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={startMode === 'interface' ? 'Interface' : 'Bridge'}
name="target"
rules={[{ required: true, message: 'Please select a target' }]}
>
<Select
showSearch
placeholder={startMode === 'interface' ? 'Select interface' : 'Select bridge'}
optionFilterProp="children"
filterOption={(input, option) =>
(option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())
}
>
{startMode === 'interface'
? props.interfaces.map((i) => (
<Option key={`if:${i.name}`} value={i.name}>
{i.name}
</Option>
))
: props.bridges.map((b) => (
<Option key={`br:${b.ifname}`} value={b.ifname}>
{b.ifname} ({b.members.map((m) => m.ifname).join(', ')})
</Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
}