test backend changes api snivver interfaces
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-03-01 14:40:52 +01:00
parent 153bd4e7c8
commit 2ec5e21e94
6 changed files with 596 additions and 201 deletions

View File

@@ -1,33 +1,400 @@
import { useEffect } from 'react';
import { fetchSnifferStatus } from '../api/apiClient';
// 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';
export default function Sniffing() {
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,
createBridge,
removeBridge,
fetchBridges,
fetchInterfaces,
refreshBridges,
fetchBridges,
refreshInterfaces,
isCreating,
isRemoving,
refreshBridges,
snifferStatusQuery,
} = useBackendAPI();
// fetch on mount (explicit, since queries are disabled by default)
// local UI state
const [selectedInterface, setSelectedInterface] = useState<string | undefined>(undefined);
const [busy, setBusy] = useState(false);
// ensure data is fetched on mount
useEffect(() => {
fetchInterfaces().catch(() => {});
fetchSnifferStatus().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);
}
};
const bridges = bridgesQuery.data ?? [];
const snifferStatus = snifferStatusQuery.data;
return (
<div className="sniffing-body">
<h1>Sniffing</h1>
<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>
);
}