From 82012e9640431b173b446fa91a4e7efd568dea56 Mon Sep 17 00:00:00 2001 From: malmert Date: Sat, 10 Jan 2026 14:44:11 +0100 Subject: [PATCH] refactor: simplify API routes and add sniffer status functionality - 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. --- backend/src/api/sniffer_api.py | 6 +-- frontend/src/Layout/Sidebar.tsx | 3 +- frontend/src/api/apiClient.ts | 21 ++++++--- frontend/src/appRouter.tsx | 2 + .../hooks/{useNetwork.ts => useBackendAPI.ts} | 43 ++++++++++++++++--- frontend/src/pages/Home.tsx | 4 +- frontend/src/pages/Network.tsx | 4 +- frontend/src/pages/Sniffing.tsx | 32 ++++++++++++++ frontend/src/routes.ts | 1 + frontend/src/types/sniffer.ts | 21 +++++++++ 10 files changed, 115 insertions(+), 22 deletions(-) rename frontend/src/hooks/{useNetwork.ts => useBackendAPI.ts} (86%) create mode 100644 frontend/src/pages/Sniffing.tsx create mode 100644 frontend/src/types/sniffer.ts diff --git a/backend/src/api/sniffer_api.py b/backend/src/api/sniffer_api.py index 6aa5f71..4a2add6 100644 --- a/backend/src/api/sniffer_api.py +++ b/backend/src/api/sniffer_api.py @@ -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. diff --git a/frontend/src/Layout/Sidebar.tsx b/frontend/src/Layout/Sidebar.tsx index f1ed6bd..6127c0c 100644 --- a/frontend/src/Layout/Sidebar.tsx +++ b/frontend/src/Layout/Sidebar.tsx @@ -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: , label: 'Home' }, { key: PATHS.NETWORK, icon: , label: 'Network' }, + { key: PATHS.SNIFFING, icon: , label: 'Sniffing' }, { key: '/about', icon: , label: 'About' }, { key: '/settings', icon: , label: 'Settings' }, ]; diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index f5c9078..304ba77 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -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 => { return res.data; }; +export const fetchSnifferStatus = async (): Promise => { + const res = await api.get("/sniffer/status"); + return res.data; +}; + + // Mutations export const createBridge = async (req: BridgeCreateRequest) => { const res = await api.post("network/bridge/create", req); diff --git a/frontend/src/appRouter.tsx b/frontend/src/appRouter.tsx index c411d2f..57c46d8 100644 --- a/frontend/src/appRouter.tsx +++ b/frontend/src/appRouter.tsx @@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import App from './App'; // your layout component (has ) 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 */} } /> } /> + } /> {/* Fallback (renders inside layout too) */} } /> diff --git a/frontend/src/hooks/useNetwork.ts b/frontend/src/hooks/useBackendAPI.ts similarity index 86% rename from frontend/src/hooks/useNetwork.ts rename to frontend/src/hooks/useBackendAPI.ts index cf8ab8d..2e305d6 100644 --- a/frontend/src/hooks/useNetwork.ts +++ b/frontend/src/hooks/useBackendAPI.ts @@ -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({ + 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({ @@ -125,6 +133,16 @@ export function useNetwork() { return res; }, [qc]); + const fetchSnifferStatus = useCallback(async () => { + const res = await qc.fetchQuery({ + 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, }; } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index b91f6fc..2232bb9 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -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
Loading full state…
; diff --git a/frontend/src/pages/Network.tsx b/frontend/src/pages/Network.tsx index 80c9b0c..e01eeb0 100644 --- a/frontend/src/pages/Network.tsx +++ b/frontend/src/pages/Network.tsx @@ -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(); diff --git a/frontend/src/pages/Sniffing.tsx b/frontend/src/pages/Sniffing.tsx new file mode 100644 index 0000000..03e639c --- /dev/null +++ b/frontend/src/pages/Sniffing.tsx @@ -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 ( +
+

Sniffing

+
+ ); +} diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 034772d..1215334 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -2,4 +2,5 @@ export const PATHS = { ROOT: '/', HOME: '/home', NETWORK: '/network', + SNIFFING: '/sniffing', }; \ No newline at end of file diff --git a/frontend/src/types/sniffer.ts b/frontend/src/types/sniffer.ts new file mode 100644 index 0000000..9675df3 --- /dev/null +++ b/frontend/src/types/sniffer.ts @@ -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; +}