All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 1m39s
311 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|