refactor: simplify API routes and add sniffer status functionality
Some checks failed
Build and Deploy MITM Webserver / build (push) Failing after 5s

- Updated API routes to remove redundant prefixes.
- Introduced sniffer status fetching in the backend API.
- Added Sniffing page and integrated it into the routing.
- Created a new hook for backend API interactions.
- Implemented sniffer status type definitions.
This commit is contained in:
2026-01-10 14:44:11 +01:00
parent a2a017f900
commit 82012e9640
10 changed files with 115 additions and 22 deletions

View File

@@ -58,7 +58,7 @@ class SnifferStatusResponse(BaseModel):
# Endpoints
# ------------------------------
@router.post("/sniffer/start", response_model=SnifferStartResponse)
@router.post("/start", response_model=SnifferStartResponse)
def sniffer_start(req: SnifferStartRequest):
"""
Start the AF_PACKET sniffer for the given bridge.
@@ -70,7 +70,7 @@ def sniffer_start(req: SnifferStartRequest):
raise HTTPException(status_code=500, detail=f"Failed to start sniffer: {exc}")
@router.post("/sniffer/stop", response_model=SnifferStopResponse)
@router.post("/stop", response_model=SnifferStopResponse)
def sniffer_stop():
"""
Stop the AF_PACKET sniffer (if running).
@@ -82,7 +82,7 @@ def sniffer_stop():
raise HTTPException(status_code=500, detail=f"Failed to stop sniffer: {exc}")
@router.get("/sniffer/status", response_model=SnifferStatusResponse)
@router.get("/status", response_model=SnifferStatusResponse)
def sniffer_status():
"""
Return the sniffer status information.

View File

@@ -1,5 +1,5 @@
// src/components/Sidebar.tsx
import { ApartmentOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons';
import { ApartmentOutlined, ApiOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -11,6 +11,7 @@ const { Sider } = Layout;
const menuItems = [
{ key: PATHS.HOME, icon: <HomeOutlined style={{ fontSize: '18px' }} />, label: 'Home' },
{ key: PATHS.NETWORK, icon: <ApartmentOutlined style={{ fontSize: '18px' }} />, label: 'Network' },
{ key: PATHS.SNIFFING, icon: <ApiOutlined style={{ fontSize: '18px' }} />, label: 'Sniffing' },
{ key: '/about', icon: <InfoCircleOutlined style={{ fontSize: '18px' }} />, label: 'About' },
{ key: '/settings', icon: <SettingOutlined style={{ fontSize: '18px' }} />, label: 'Settings' },
];

View File

@@ -1,13 +1,14 @@
// src/apiClient.ts
import axios from "axios";
import {
BridgeCreateRequest,
BridgeInfo,
BridgeRemoveRequest,
FullState,
InterfaceInfo,
RouteInfo,
BridgeCreateRequest,
BridgeInfo,
BridgeRemoveRequest,
FullState,
InterfaceInfo,
RouteInfo,
} from "../types/network";
import { SnifferStatusResponse } from "../types/sniffer";
const BASE = "http://mitm.lan/api";
@@ -17,7 +18,7 @@ export const api = axios.create({
timeout: 10000,
});
// 🔹 Normalize FastAPI errors here
// Normalize FastAPI errors here
api.interceptors.response.use(
(response) => response,
(error) => {
@@ -59,6 +60,12 @@ export const fetchFullState = async (): Promise<FullState> => {
return res.data;
};
export const fetchSnifferStatus = async (): Promise<SnifferStatusResponse> => {
const res = await api.get<SnifferStatusResponse>("/sniffer/status");
return res.data;
};
// Mutations
export const createBridge = async (req: BridgeCreateRequest) => {
const res = await api.post("network/bridge/create", req);

View File

@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import App from './App'; // your layout component (has <Outlet />)
import Home from './pages/Home';
import Network from './pages/Network';
import Sniffing from './pages/Sniffing';
import { PATHS } from './routes';
export default function AppRouter() {
@@ -16,6 +17,7 @@ export default function AppRouter() {
{/* Child routes - these render inside App's <Outlet /> */}
<Route path={PATHS.HOME.slice(1)} element={<Home />} />
<Route path={PATHS.NETWORK.slice(1)} element={<Network />} />
<Route path={PATHS.SNIFFING.slice(1)} element={<Sniffing />} />
{/* Fallback (renders inside layout too) */}
<Route path="*" element={<NotFound />} />

View File

@@ -3,13 +3,14 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import * as api from "../api/apiClient";
import type {
BridgeCreateRequest,
BridgeInfo,
BridgeRemoveRequest,
FullState,
InterfaceInfo,
RouteInfo,
BridgeCreateRequest,
BridgeInfo,
BridgeRemoveRequest,
FullState,
InterfaceInfo,
RouteInfo,
} from "../types/network";
import { SnifferStatusResponse } from "../types/sniffer";
/**
* useNetwork
@@ -22,7 +23,7 @@ import type {
*
* This gives "no initial auto-fetch" but "once fetched, auto-updates".
*/
export function useNetwork() {
export function useBackendAPI() {
const qc = useQueryClient();
// per-query enabled flags (start false => no automatic fetch)
@@ -74,6 +75,13 @@ export function useNetwork() {
...commonOptions,
});
const snifferStatusQuery = useQuery<SnifferStatusResponse>({
queryKey: ["sniffer-status"],
queryFn: api.fetchSnifferStatus,
enabled: fullStateEnabled,
...commonOptions,
});
// Imperative fetch helpers that also enable auto-refetch behavior
const fetchInterfaces = useCallback(async () => {
const res = await qc.fetchQuery<InterfaceInfo[]>({
@@ -125,6 +133,16 @@ export function useNetwork() {
return res;
}, [qc]);
const fetchSnifferStatus = useCallback(async () => {
const res = await qc.fetchQuery<SnifferStatusResponse>({
queryKey: ["sniffer-status"],
queryFn: api.fetchSnifferStatus,
staleTime: 1000 * 5
});
setFullStateEnabled(true);
return res;
}, [qc]);
// Local loading state for simple UI feedback
const [isCreating, setIsCreating] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
@@ -154,6 +172,7 @@ export function useNetwork() {
await qc.invalidateQueries({ queryKey: ["bridges"] });
await qc.invalidateQueries({ queryKey: ["full-state"] });
await qc.invalidateQueries({ queryKey: ["interfaces"] });
await qc.invalidateQueries({ queryKey: ["sniffer-status"] });
} catch (err) {
const message =
err instanceof Error ? err.message : typeof err === "string" ? err : "Remove bridge failed";
@@ -184,6 +203,10 @@ export function useNetwork() {
return qc.invalidateQueries({ queryKey: ["full-state"] });
}
function refreshSnifferStatus() {
return qc.invalidateQueries({ queryKey: ["sniffer-status"] });
}
// Convenience: refresh all queries
function refreshAll() {
refreshInterfaces();
@@ -191,6 +214,7 @@ export function useNetwork() {
refreshRoutes();
refreshBridges();
refreshFullState();
refreshSnifferStatus();
}
return {
@@ -200,6 +224,7 @@ export function useNetwork() {
routesQuery,
bridgesQuery,
fullStateQuery,
snifferStatusQuery,
// manual fetchers (fetch+enable auto-updates)
fetchInterfaces,
@@ -207,6 +232,7 @@ export function useNetwork() {
fetchRoutes,
fetchBridges,
fetchFullState,
fetchSnifferStatus,
// simple mutation functions (imperative)
createBridge,
@@ -222,6 +248,9 @@ export function useNetwork() {
refreshRoutes,
refreshBridges,
refreshFullState,
refreshSnifferStatus,
// refresh all
refreshAll,
};
}

View File

@@ -1,9 +1,9 @@
import { useEffect } from 'react';
import { fetchFullState } from '../api/apiClient';
import { useNetwork } from '../hooks/useNetwork';
import { useBackendAPI } from '../hooks/useBackendAPI';
export default function Home() {
const { fullStateQuery } = useNetwork();
const { fullStateQuery } = useBackendAPI();
const { data, isLoading, isError, error } = fullStateQuery;
if (isLoading) return <div>Loading full state…</div>;

View File

@@ -3,7 +3,7 @@ import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { Button, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useEffect, useMemo, useState } from 'react';
import { useNetwork } from '../hooks/useNetwork';
import { useBackendAPI } from '../hooks/useBackendAPI';
import type { BridgeInfo, InterfaceInfo } from '../types/network';
const { Title, Paragraph } = Typography;
@@ -20,7 +20,7 @@ export default function Network() {
refreshInterfaces,
isCreating,
isRemoving,
} = useNetwork();
} = useBackendAPI();
const [bridgeModalVisible, setBridgeModalVisible] = useState(false);
const [bridgeForm] = Form.useForm();

View File

@@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { useBackendAPI } from '../hooks/useBackendAPI';
import { fetchSnifferStatus } from '../api/apiClient';
export default function Sniffing() {
const {
interfacesQuery,
bridgesQuery,
createBridge,
removeBridge,
fetchBridges,
fetchInterfaces,
refreshBridges,
refreshInterfaces,
isCreating,
isRemoving,
} = useBackendAPI();
// fetch on mount (explicit, since queries are disabled by default)
useEffect(() => {
fetchInterfaces().catch(() => {});
fetchSnifferStatus().catch(() => {});
}, []);
const bridges = bridgesQuery.data ?? [];
const snifferStatus =
return (
<div className="sniffing-body">
<h1>Sniffing</h1>
</div>
);
}

View File

@@ -2,4 +2,5 @@ export const PATHS = {
ROOT: '/',
HOME: '/home',
NETWORK: '/network',
SNIFFING: '/sniffing',
};

View File

@@ -0,0 +1,21 @@
/**
* Status of an individual interface monitored by the AF_PACKET sniffer.
*/
export interface InterfaceSnifferStatus {
/** Whether the sniffer thread is active */
running: boolean;
/** Whether the interface exists in /sys/class/net */
exists: boolean;
/** Whether the interface is operationally UP */
up: boolean;
}
/**
* Response model for the sniffer status endpoint.
* Maps interface names to their sniffer status.
*/
export interface SnifferStatusResponse {
interfaces: Record<string, InterfaceSnifferStatus>;
}