All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
// src/components/Sniffing.tsx
|
|
import {
|
|
CheckCircleOutlined,
|
|
ExclamationCircleOutlined,
|
|
PlayCircleOutlined,
|
|
ReloadOutlined,
|
|
StopOutlined,
|
|
SyncOutlined,
|
|
} from '@ant-design/icons';
|
|
import { Button, Card, Col, Divider, List, message, Row, Select, Space, Spin, Tag, Tooltip, Typography } from 'antd';
|
|
import { ReactElement, useCallback, useEffect, useState } from 'react';
|
|
import { fetchSnifferStatus as fetchSnifferStatusApi, startSniffer, stopSniffer } from '../api/apiClient';
|
|
import { useBackendAPI } from '../hooks/useBackendAPI';
|
|
import { InterfaceSnifferStatus } from '../types/sniffer';
|
|
|
|
const { Title, Text } = Typography;
|
|
const { Option } = Select;
|
|
|
|
/** Local type for the interface objects returned by your backend/hook.
|
|
* Adjust fields if your backend has different shape.
|
|
*/
|
|
type NetworkInterface = {
|
|
ifindex: number;
|
|
name: string;
|
|
state?: string;
|
|
mac?: string;
|
|
mtu?: number;
|
|
flags?: string[];
|
|
addresses?: string[];
|
|
};
|
|
|
|
export default function Sniffing(): ReactElement {
|
|
const {
|
|
interfacesQuery,
|
|
bridgesQuery,
|
|
fetchInterfaces,
|
|
fetchBridges,
|
|
refreshInterfaces,
|
|
refreshBridges,
|
|
snifferStatusQuery,
|
|
} = useBackendAPI();
|
|
|
|
// local UI state
|
|
const [selectedInterface, setSelectedInterface] = useState<string | undefined>(undefined);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
// ensure data is fetched on mount
|
|
useEffect(() => {
|
|
fetchInterfaces().catch(() => {});
|
|
fetchSnifferStatusApi().catch(() => {});
|
|
if (fetchBridges) fetchBridges().catch(() => {});
|
|
}, [fetchInterfaces, fetchBridges]);
|
|
|
|
// Properly type interfaces as array of objects (not string[])
|
|
const interfaces = (interfacesQuery?.data ?? []) as NetworkInterface[];
|
|
const bridges = (bridgesQuery?.data ?? []) as Array<{ name: string }>;
|
|
|
|
const statusMap = (snifferStatusQuery?.data?.interfaces ?? {}) as Record<string, InterfaceSnifferStatus>;
|
|
|
|
// Explicitly type entries so renderItem knows the types
|
|
const statusEntries = Object.entries(statusMap) as [string, InterfaceSnifferStatus][];
|
|
|
|
const refreshAll = useCallback(async () => {
|
|
try {
|
|
if (refreshInterfaces) await refreshInterfaces();
|
|
if (snifferStatusQuery?.refetch) await snifferStatusQuery.refetch();
|
|
if (refreshBridges) await refreshBridges();
|
|
} catch {
|
|
// ignore — queries will surface errors
|
|
}
|
|
}, [refreshInterfaces, snifferStatusQuery, refreshBridges]);
|
|
|
|
// actions
|
|
const handleStart = async () => {
|
|
if (!selectedInterface) {
|
|
message.warning('Please select an interface to start sniffing on.');
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
await startSniffer({ bridge: selectedInterface });
|
|
message.success(`Sniffer started on ${selectedInterface}`);
|
|
await refreshAll();
|
|
} catch (err: any) {
|
|
console.error('startSniffer error', err);
|
|
message.error(err?.message ?? 'Failed to start sniffer');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const handleStop = async () => {
|
|
setBusy(true);
|
|
try {
|
|
await stopSniffer();
|
|
message.success('Sniffer stopped');
|
|
await refreshAll();
|
|
} catch (err: any) {
|
|
console.error('stopSniffer error', err);
|
|
message.error(err?.message ?? 'Failed to stop sniffer');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
// Quick action per-interface (start/stop)
|
|
const handleStartOn = async (ifaceName: string) => {
|
|
setBusy(true);
|
|
try {
|
|
await startSniffer({ bridge: ifaceName });
|
|
message.success(`Started sniffer on ${ifaceName}`);
|
|
await refreshAll();
|
|
} catch (err: any) {
|
|
console.error('handleStartOn error', err);
|
|
message.error(err?.message ?? `Failed to start on ${ifaceName}`);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const handleStopFromList = async (ifaceName?: string) => {
|
|
setBusy(true);
|
|
try {
|
|
await stopSniffer();
|
|
message.success(`Stopped sniffer${ifaceName ? ` (requested from ${ifaceName})` : ''}`);
|
|
await refreshAll();
|
|
} catch (err: any) {
|
|
console.error('handleStopFromList error', err);
|
|
message.error(err?.message ?? 'Failed to stop sniffer');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={{ padding: 16 }}>
|
|
<Row gutter={[16, 16]}>
|
|
<Col span={16}>
|
|
<Card>
|
|
<Row justify="space-between" align="middle">
|
|
<Col>
|
|
<Title level={4} style={{ margin: 0 }}>
|
|
Sniffing
|
|
</Title>
|
|
<Text type="secondary">Start, stop and view AF_PACKET sniffer status</Text>
|
|
</Col>
|
|
|
|
<Col>
|
|
<Space>
|
|
<Tooltip title="Refresh interfaces & status">
|
|
<Button icon={<ReloadOutlined />} onClick={() => refreshAll()} disabled={busy} />
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Refresh sniffer status">
|
|
<Button
|
|
icon={<SyncOutlined spin={snifferStatusQuery?.isFetching} />}
|
|
onClick={() => snifferStatusQuery?.refetch?.()}
|
|
disabled={busy}
|
|
/>
|
|
</Tooltip>
|
|
</Space>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Divider />
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<Row gutter={[12, 12]} align="middle">
|
|
<Col>
|
|
<Text strong>Select interface / bridge</Text>
|
|
</Col>
|
|
|
|
<Col flex="1">
|
|
<Select
|
|
showSearch
|
|
placeholder="Choose interface or bridge"
|
|
style={{ width: '100%' }}
|
|
value={selectedInterface}
|
|
onChange={(val) => setSelectedInterface(val as string | undefined)}
|
|
loading={interfacesQuery?.isLoading}
|
|
allowClear
|
|
optionFilterProp="children"
|
|
filterOption={(input, option) =>
|
|
(option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())
|
|
}
|
|
>
|
|
{/* interfaces from the system: use iface.name */}
|
|
{interfaces.map((iface) => (
|
|
<Option key={`if:${iface.name}`} value={iface.name}>
|
|
{iface.name}
|
|
</Option>
|
|
))}
|
|
|
|
{/* optional bridges (if present) */}
|
|
{bridges && bridges.length > 0 && (
|
|
<>
|
|
<Option key="__divider__" value="__divider__" disabled>
|
|
────────── bridges ──────────
|
|
</Option>
|
|
{bridges.map((b) => (
|
|
<Option key={`br:${b.name}`} value={b.name}>
|
|
{b.name}
|
|
</Option>
|
|
))}
|
|
</>
|
|
)}
|
|
</Select>
|
|
</Col>
|
|
|
|
<Col>
|
|
<Space>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={handleStart}
|
|
loading={busy && !snifferStatusQuery?.isFetching}
|
|
disabled={
|
|
!selectedInterface || busy || (selectedInterface && statusMap[selectedInterface]?.running)
|
|
}
|
|
>
|
|
Start
|
|
</Button>
|
|
|
|
<Button
|
|
danger
|
|
icon={<StopOutlined />}
|
|
onClick={handleStop}
|
|
loading={busy && !snifferStatusQuery?.isFetching}
|
|
disabled={busy}
|
|
>
|
|
Stop
|
|
</Button>
|
|
</Space>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row>
|
|
<Col span={24}>
|
|
<Text type="secondary">Tip: you can also start/stop directly from the list below.</Text>
|
|
</Col>
|
|
</Row>
|
|
</Space>
|
|
</Card>
|
|
|
|
<Divider />
|
|
|
|
<Card title="Sniffer status" bordered>
|
|
{snifferStatusQuery?.isLoading ? (
|
|
<div style={{ textAlign: 'center', padding: 24 }}>
|
|
<Spin />
|
|
</div>
|
|
) : snifferStatusQuery?.isError ? (
|
|
<div style={{ padding: 12 }}>
|
|
<Space direction="vertical">
|
|
<Text type="danger">Failed to load sniffer status.</Text>
|
|
<Text type="secondary">
|
|
{(snifferStatusQuery?.error as any)?.message ?? String(snifferStatusQuery?.error)}
|
|
</Text>
|
|
</Space>
|
|
</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;
|
|
return (
|
|
<List.Item
|
|
actions={[
|
|
running ? (
|
|
<Button
|
|
key="stop"
|
|
size="small"
|
|
icon={<StopOutlined />}
|
|
onClick={() => handleStopFromList(name)}
|
|
disabled={busy}
|
|
>
|
|
Stop
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
key="start"
|
|
size="small"
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={() => handleStartOn(name)}
|
|
disabled={busy}
|
|
>
|
|
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>}
|
|
</Space>
|
|
}
|
|
description={
|
|
<Space size="small">
|
|
<Text type="secondary">interface: {name}</Text>
|
|
</Space>
|
|
}
|
|
/>
|
|
</List.Item>
|
|
);
|
|
}}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
|
|
<Col span={8}>
|
|
<Card title="Quick actions" size="small" bordered>
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
<Button icon={<ReloadOutlined />} onClick={() => refreshAll()} block disabled={busy}>
|
|
Refresh all
|
|
</Button>
|
|
|
|
<Button icon={<SyncOutlined />} onClick={() => snifferStatusQuery?.refetch?.()} block disabled={busy}>
|
|
Refresh status
|
|
</Button>
|
|
|
|
<Divider />
|
|
|
|
<Button
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={() => {
|
|
// start on selected, fallback to first available interface
|
|
const target = selectedInterface ?? interfaces[0]?.name;
|
|
if (!target) {
|
|
message.warning('No interface selected / available');
|
|
return;
|
|
}
|
|
setSelectedInterface(target);
|
|
handleStartOn(target);
|
|
}}
|
|
block
|
|
disabled={busy || interfaces.length === 0}
|
|
>
|
|
Start on selected / first
|
|
</Button>
|
|
|
|
<Button danger icon={<StopOutlined />} onClick={() => handleStop()} block disabled={busy}>
|
|
Stop sniffer
|
|
</Button>
|
|
</Space>
|
|
</Card>
|
|
|
|
<Card title="Available interfaces" size="small" style={{ marginTop: 12 }}>
|
|
{interfacesQuery?.isLoading ? (
|
|
<Spin />
|
|
) : interfaces.length === 0 ? (
|
|
<Text type="secondary">No interfaces found.</Text>
|
|
) : (
|
|
<List
|
|
size="small"
|
|
dataSource={interfaces}
|
|
renderItem={(iface: NetworkInterface) => <List.Item>{iface.name}</List.Item>}
|
|
/>
|
|
)}
|
|
</Card>
|
|
|
|
<Card title="Bridges" size="small" style={{ marginTop: 12 }}>
|
|
{bridgesQuery?.isLoading ? (
|
|
<Spin />
|
|
) : bridges.length === 0 ? (
|
|
<Text type="secondary">No bridges found.</Text>
|
|
) : (
|
|
<List
|
|
size="small"
|
|
dataSource={bridges}
|
|
renderItem={(b: { name: string }) => <List.Item>{b.name}</List.Item>}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
);
|
|
}
|