refactor: simplify API routes and add sniffer status functionality
Some checks failed
Build and Deploy MITM Webserver / build (push) Failing after 5s
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:
@@ -58,7 +58,7 @@ class SnifferStatusResponse(BaseModel):
|
|||||||
# Endpoints
|
# Endpoints
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
|
||||||
@router.post("/sniffer/start", response_model=SnifferStartResponse)
|
@router.post("/start", response_model=SnifferStartResponse)
|
||||||
def sniffer_start(req: SnifferStartRequest):
|
def sniffer_start(req: SnifferStartRequest):
|
||||||
"""
|
"""
|
||||||
Start the AF_PACKET sniffer for the given bridge.
|
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}")
|
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():
|
def sniffer_stop():
|
||||||
"""
|
"""
|
||||||
Stop the AF_PACKET sniffer (if running).
|
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}")
|
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():
|
def sniffer_status():
|
||||||
"""
|
"""
|
||||||
Return the sniffer status information.
|
Return the sniffer status information.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/components/Sidebar.tsx
|
// 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 { Layout, Menu } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
@@ -11,6 +11,7 @@ const { Sider } = Layout;
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ key: PATHS.HOME, icon: <HomeOutlined style={{ fontSize: '18px' }} />, label: 'Home' },
|
{ key: PATHS.HOME, icon: <HomeOutlined style={{ fontSize: '18px' }} />, label: 'Home' },
|
||||||
{ key: PATHS.NETWORK, icon: <ApartmentOutlined style={{ fontSize: '18px' }} />, label: 'Network' },
|
{ 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: '/about', icon: <InfoCircleOutlined style={{ fontSize: '18px' }} />, label: 'About' },
|
||||||
{ key: '/settings', icon: <SettingOutlined style={{ fontSize: '18px' }} />, label: 'Settings' },
|
{ key: '/settings', icon: <SettingOutlined style={{ fontSize: '18px' }} />, label: 'Settings' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
InterfaceInfo,
|
InterfaceInfo,
|
||||||
RouteInfo,
|
RouteInfo,
|
||||||
} from "../types/network";
|
} from "../types/network";
|
||||||
|
import { SnifferStatusResponse } from "../types/sniffer";
|
||||||
|
|
||||||
const BASE = "http://mitm.lan/api";
|
const BASE = "http://mitm.lan/api";
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export const api = axios.create({
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔹 Normalize FastAPI errors here
|
// Normalize FastAPI errors here
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -59,6 +60,12 @@ export const fetchFullState = async (): Promise<FullState> => {
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchSnifferStatus = async (): Promise<SnifferStatusResponse> => {
|
||||||
|
const res = await api.get<SnifferStatusResponse>("/sniffer/status");
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
export const createBridge = async (req: BridgeCreateRequest) => {
|
export const createBridge = async (req: BridgeCreateRequest) => {
|
||||||
const res = await api.post("network/bridge/create", req);
|
const res = await api.post("network/bridge/create", req);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
|||||||
import App from './App'; // your layout component (has <Outlet />)
|
import App from './App'; // your layout component (has <Outlet />)
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Network from './pages/Network';
|
import Network from './pages/Network';
|
||||||
|
import Sniffing from './pages/Sniffing';
|
||||||
import { PATHS } from './routes';
|
import { PATHS } from './routes';
|
||||||
|
|
||||||
export default function AppRouter() {
|
export default function AppRouter() {
|
||||||
@@ -16,6 +17,7 @@ export default function AppRouter() {
|
|||||||
{/* Child routes - these render inside App's <Outlet /> */}
|
{/* Child routes - these render inside App's <Outlet /> */}
|
||||||
<Route path={PATHS.HOME.slice(1)} element={<Home />} />
|
<Route path={PATHS.HOME.slice(1)} element={<Home />} />
|
||||||
<Route path={PATHS.NETWORK.slice(1)} element={<Network />} />
|
<Route path={PATHS.NETWORK.slice(1)} element={<Network />} />
|
||||||
|
<Route path={PATHS.SNIFFING.slice(1)} element={<Sniffing />} />
|
||||||
|
|
||||||
{/* Fallback (renders inside layout too) */}
|
{/* Fallback (renders inside layout too) */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
InterfaceInfo,
|
InterfaceInfo,
|
||||||
RouteInfo,
|
RouteInfo,
|
||||||
} from "../types/network";
|
} from "../types/network";
|
||||||
|
import { SnifferStatusResponse } from "../types/sniffer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useNetwork
|
* useNetwork
|
||||||
@@ -22,7 +23,7 @@ import type {
|
|||||||
*
|
*
|
||||||
* This gives "no initial auto-fetch" but "once fetched, auto-updates".
|
* This gives "no initial auto-fetch" but "once fetched, auto-updates".
|
||||||
*/
|
*/
|
||||||
export function useNetwork() {
|
export function useBackendAPI() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|
||||||
// per-query enabled flags (start false => no automatic fetch)
|
// per-query enabled flags (start false => no automatic fetch)
|
||||||
@@ -74,6 +75,13 @@ export function useNetwork() {
|
|||||||
...commonOptions,
|
...commonOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const snifferStatusQuery = useQuery<SnifferStatusResponse>({
|
||||||
|
queryKey: ["sniffer-status"],
|
||||||
|
queryFn: api.fetchSnifferStatus,
|
||||||
|
enabled: fullStateEnabled,
|
||||||
|
...commonOptions,
|
||||||
|
});
|
||||||
|
|
||||||
// Imperative fetch helpers that also enable auto-refetch behavior
|
// Imperative fetch helpers that also enable auto-refetch behavior
|
||||||
const fetchInterfaces = useCallback(async () => {
|
const fetchInterfaces = useCallback(async () => {
|
||||||
const res = await qc.fetchQuery<InterfaceInfo[]>({
|
const res = await qc.fetchQuery<InterfaceInfo[]>({
|
||||||
@@ -125,6 +133,16 @@ export function useNetwork() {
|
|||||||
return res;
|
return res;
|
||||||
}, [qc]);
|
}, [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
|
// Local loading state for simple UI feedback
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [isRemoving, setIsRemoving] = useState(false);
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
@@ -154,6 +172,7 @@ export function useNetwork() {
|
|||||||
await qc.invalidateQueries({ queryKey: ["bridges"] });
|
await qc.invalidateQueries({ queryKey: ["bridges"] });
|
||||||
await qc.invalidateQueries({ queryKey: ["full-state"] });
|
await qc.invalidateQueries({ queryKey: ["full-state"] });
|
||||||
await qc.invalidateQueries({ queryKey: ["interfaces"] });
|
await qc.invalidateQueries({ queryKey: ["interfaces"] });
|
||||||
|
await qc.invalidateQueries({ queryKey: ["sniffer-status"] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message =
|
||||||
err instanceof Error ? err.message : typeof err === "string" ? err : "Remove bridge failed";
|
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"] });
|
return qc.invalidateQueries({ queryKey: ["full-state"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshSnifferStatus() {
|
||||||
|
return qc.invalidateQueries({ queryKey: ["sniffer-status"] });
|
||||||
|
}
|
||||||
|
|
||||||
// Convenience: refresh all queries
|
// Convenience: refresh all queries
|
||||||
function refreshAll() {
|
function refreshAll() {
|
||||||
refreshInterfaces();
|
refreshInterfaces();
|
||||||
@@ -191,6 +214,7 @@ export function useNetwork() {
|
|||||||
refreshRoutes();
|
refreshRoutes();
|
||||||
refreshBridges();
|
refreshBridges();
|
||||||
refreshFullState();
|
refreshFullState();
|
||||||
|
refreshSnifferStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -200,6 +224,7 @@ export function useNetwork() {
|
|||||||
routesQuery,
|
routesQuery,
|
||||||
bridgesQuery,
|
bridgesQuery,
|
||||||
fullStateQuery,
|
fullStateQuery,
|
||||||
|
snifferStatusQuery,
|
||||||
|
|
||||||
// manual fetchers (fetch+enable auto-updates)
|
// manual fetchers (fetch+enable auto-updates)
|
||||||
fetchInterfaces,
|
fetchInterfaces,
|
||||||
@@ -207,6 +232,7 @@ export function useNetwork() {
|
|||||||
fetchRoutes,
|
fetchRoutes,
|
||||||
fetchBridges,
|
fetchBridges,
|
||||||
fetchFullState,
|
fetchFullState,
|
||||||
|
fetchSnifferStatus,
|
||||||
|
|
||||||
// simple mutation functions (imperative)
|
// simple mutation functions (imperative)
|
||||||
createBridge,
|
createBridge,
|
||||||
@@ -222,6 +248,9 @@ export function useNetwork() {
|
|||||||
refreshRoutes,
|
refreshRoutes,
|
||||||
refreshBridges,
|
refreshBridges,
|
||||||
refreshFullState,
|
refreshFullState,
|
||||||
|
refreshSnifferStatus,
|
||||||
|
|
||||||
|
// refresh all
|
||||||
refreshAll,
|
refreshAll,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { fetchFullState } from '../api/apiClient';
|
import { fetchFullState } from '../api/apiClient';
|
||||||
import { useNetwork } from '../hooks/useNetwork';
|
import { useBackendAPI } from '../hooks/useBackendAPI';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { fullStateQuery } = useNetwork();
|
const { fullStateQuery } = useBackendAPI();
|
||||||
const { data, isLoading, isError, error } = fullStateQuery;
|
const { data, isLoading, isError, error } = fullStateQuery;
|
||||||
|
|
||||||
if (isLoading) return <div>Loading full state…</div>;
|
if (isLoading) return <div>Loading full state…</div>;
|
||||||
|
|||||||
@@ -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 { Button, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNetwork } from '../hooks/useNetwork';
|
import { useBackendAPI } from '../hooks/useBackendAPI';
|
||||||
import type { BridgeInfo, InterfaceInfo } from '../types/network';
|
import type { BridgeInfo, InterfaceInfo } from '../types/network';
|
||||||
|
|
||||||
const { Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
@@ -20,7 +20,7 @@ export default function Network() {
|
|||||||
refreshInterfaces,
|
refreshInterfaces,
|
||||||
isCreating,
|
isCreating,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
} = useNetwork();
|
} = useBackendAPI();
|
||||||
|
|
||||||
const [bridgeModalVisible, setBridgeModalVisible] = useState(false);
|
const [bridgeModalVisible, setBridgeModalVisible] = useState(false);
|
||||||
const [bridgeForm] = Form.useForm();
|
const [bridgeForm] = Form.useForm();
|
||||||
|
|||||||
32
frontend/src/pages/Sniffing.tsx
Normal file
32
frontend/src/pages/Sniffing.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,4 +2,5 @@ export const PATHS = {
|
|||||||
ROOT: '/',
|
ROOT: '/',
|
||||||
HOME: '/home',
|
HOME: '/home',
|
||||||
NETWORK: '/network',
|
NETWORK: '/network',
|
||||||
|
SNIFFING: '/sniffing',
|
||||||
};
|
};
|
||||||
21
frontend/src/types/sniffer.ts
Normal file
21
frontend/src/types/sniffer.ts
Normal 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>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user