// src/apiClient.ts import axios from 'axios'; import { CreateRuleRequest, ExecResult, RulesetModel } from '../types/firewall'; import { BridgeCreateRequest, BridgeInfo, BridgeRemoveRequest, FullState, InterfaceInfo, RouteInfo, } from '../types/network'; import { DeleteResult, EnableRequest, OperationResult, RequirementsDeleteResult, RequirementsUploadResult, ScriptInfo, ScriptUploadResponse, ScriptWithStatus, StatusForNameResponse, } from '../types/scripting'; import { SnifferStartRequest, SnifferStartResponse, SnifferStatusResponse, SnifferStopRequest, SnifferStopResponse, } from '../types/sniffer'; const BASE = 'http://mitm.lan/api'; export const api = axios.create({ baseURL: BASE, headers: { 'Content-Type': 'application/json' }, timeout: 20000, }); // Normalize FastAPI errors here api.interceptors.response.use( (response) => response, (error) => { // FastAPI HTTPException format const detail = error?.response?.data?.detail ?? error?.response?.data?.message ?? error.message ?? 'Unknown error'; // Always reject with a standard Error return Promise.reject(new Error(detail)); }, ); /* ------------------------- Basic endpoints ------------------------- */ export const fetchHello = async (): Promise => { const res = await api.get('/hello'); return res.data; }; export const fetchVersions = async (): Promise => { const res = await api.get('/versions'); return res.data; }; /* ------------------------- Network queries (existing) ------------------------- */ export const fetchInterfaces = async (): Promise => { const res = await api.get('/network/interfaces'); return res.data; }; export const fetchLinks = async (): Promise => { const res = await api.get('/network/links'); return res.data; }; export const fetchRoutes = async (): Promise => { const res = await api.get('/network/routes'); return res.data; }; export const fetchBridges = async (): Promise => { const res = await api.get('/network/bridges'); return res.data; }; export const fetchFullState = async (): Promise => { const res = await api.get('/network/full-state'); return res.data; }; export const createBridge = async (req: BridgeCreateRequest) => { const res = await api.post('/network/bridge/create', req); return res.data; }; export const removeBridge = async (req: BridgeRemoveRequest) => { const res = await api.post('/network/bridge/remove', req); return res.data; }; /* ------------------------- Sniffer ------------------------- */ /** * Start a sniffer session. Provide exactly one of { bridge, interface }. * Returns a session_id that you can use to stop the session later. */ export const startSniffer = async (payload: SnifferStartRequest): Promise => { const res = await api.post('/sniffer/start', payload); return res.data; }; /** * Stop sniffer(s). * - If you pass a body with { session_id }, it will stop that specific session. * - If you call without body and without query params, it will stop all sessions. */ export const stopSniffer = async (body?: SnifferStopRequest): Promise => { const res = await api.post('/sniffer/stop', body ?? {}); return res.data; }; /** * Stop sniffing for a specific interface across sessions. * Calls: POST /sniffer/stop?interface=eth0 (empty body) */ export const stopSnifferByInterface = async (iface: string): Promise => { const res = await api.post(`/sniffer/stop?interface=${encodeURIComponent(iface)}`, {}); return res.data; }; /** * Stop sniffing for a specific bridge across sessions. * Calls: POST /sniffer/stop?bridge=br0 (empty body) */ export const stopSnifferByBridge = async (bridge: string): Promise => { const res = await api.post(`/sniffer/stop?bridge=${encodeURIComponent(bridge)}`, {}); return res.data; }; /** * Fetch the sniffer status (per-interface). */ export const fetchSnifferStatus = async (): Promise => { const res = await api.get('/sniffer/status'); return res.data; }; /* ------------------------- Packets ------------------------- */ export const fetchPackets = async (limit = 100): Promise => { // limit default mirrors OpenAPI default const res = await api.get('/packets/packets', { params: { limit } }); return res.data; }; export const clearPackets = async (): Promise => { // limit default mirrors OpenAPI default const res = await api.delete('/packets/packets'); return res.data; }; /* ------------------------- Firewall ------------------------- */ /** * GET /firewall/rules * Returns: { ruleset: RulesetModel | string | null } * - If the server returns a raw textual fallback (string), the caller should handle it. */ export const fetchRuleset = async (): Promise<{ ruleset: RulesetModel }> => { const res = await api.get<{ ruleset: RulesetModel }>('/firewall/rules'); return res.data; }; /** * DELETE /firewall/rules/{handle}?family=...&table=...&chain=... * On success the backend returns 204 No Content. This function resolves to void. */ export const deleteRule = async (handle: number, family: string, table: string, chain: string): Promise => { const res = await api.delete(`/firewall/rules/${encodeURIComponent(String(handle))}`, { params: { family, table, chain }, }); // axios resolves non-2xx as reject; server uses 204 No Content so nothing to return return res.data; }; /** * createRuleJson - POST /firewall/rules * Body: CreateRuleRequest (must include expr) */ export const createRuleJson = async (req: CreateRuleRequest): Promise => { const res = await api.post('/firewall/rules', req); return res.data; }; export const execFirewallRaw = async (cmd: string): Promise => { const res = await api.post('/firewall/raw', { cmd: cmd }); return res.data; }; /* ------------------------- Scripts ------------------------- */ /** * Fetch the combined scripts + status endpoint. * Returns a list of ScriptWithStatus entries. */ export const fetchScriptsAll = async (): Promise => { const res = await api.get('/scripts/scripts'); return res.data; }; /** * Convenience: list only the basic ScriptInfo items (no mappings). * This uses the combined endpoint and maps to ScriptInfo[]. */ export const listScripts = async (): Promise => { const all = await fetchScriptsAll(); return all.map((s) => ({ name: s.name, path: s.path })); }; /** * Get status/mappings for a single script by name. * Because the backend merged status into /scripts, we fetch that and filter. */ export const fetchScriptStatusForName = async (name: string): Promise => { const all = await fetchScriptsAll(); const found = all.find((s) => s.name === name); if (!found) { // If script not found we still return an empty mapping structure return { name, mappings: [] }; } return { name: found.name, mappings: found.mappings || [] }; }; /** * Upload a script (multipart). If a requirements file is provided the backend * will run pip install and return pip output in the response (mandatory install). */ export const uploadScript = async (opts: { name: string; script: File | Blob; requirements?: File | Blob | null; }): Promise => { const fd = new FormData(); fd.append('name', opts.name); fd.append('script', opts.script); if (opts.requirements) fd.append('requirements', opts.requirements as Blob); const res = await api.post('/scripts/scripts', fd, { headers: { 'Content-Type': 'multipart/form-data' }, }); return res.data; }; /** * Download the script file as a Blob. Use this blob to create an object URL or read text. */ export const downloadScript = async (name: string): Promise => { const res = await api.get(`/scripts/scripts/${encodeURIComponent(name)}`, { responseType: 'blob' }); return res.data as Blob; }; /** * Download the requirements.txt for a script as a Blob. * Returns 404 if not present (axios will throw). */ export const downloadRequirements = async (name: string): Promise => { const res = await api.get(`/scripts/scripts/${encodeURIComponent(name)}/requirements`, { responseType: 'blob' }); return res.data as Blob; }; /** * Replace / upload requirements for a script. This endpoint ALWAYS runs pip install * and will return pip stdout/stderr on success. On pip failure the backend returns 500. * * Use multipart/form-data so Swagger UI shows a file picker (backend expects UploadFile). */ export const uploadRequirements = async ( name: string, requirements: File | Blob, ): Promise => { const fd = new FormData(); fd.append('requirements', requirements); const res = await api.put(`/scripts/scripts/${encodeURIComponent(name)}/requirements`, fd, { headers: { 'Content-Type': 'multipart/form-data' }, }); return res.data; }; /** * Delete only the requirements file and attempt to clean up the venv. * Returns summary `{ removed: { requirements_removed, venv_removed }, errors? }`. */ export const deleteRequirements = async (name: string): Promise => { const res = await api.delete(`/scripts/scripts/${encodeURIComponent(name)}/requirements`); return res.data; }; /** * Delete a script (and its files / venv). If qnum provided only remove that unit. */ export const deleteScript = async (name: string, qnum?: number | null): Promise => { const params: Record = {}; if (typeof qnum !== 'undefined' && qnum !== null) params.qnum = qnum; const res = await api.delete(`/scripts/scripts/${encodeURIComponent(name)}`, { params }); return res.data; }; /** * Enable a script (creates + starts a systemd unit). Returns OperationResult including service name. */ export const enableScript = async (name: string, req: EnableRequest): Promise => { const res = await api.post(`/scripts/scripts/${encodeURIComponent(name)}/enable`, req); return res.data; }; /** * Disable a script unit (POST with qnum as query param). Returns OperationResult. */ export const disableScript = async (name: string, qnum: number): Promise => { const res = await api.post(`/scripts/scripts/${encodeURIComponent(name)}/disable`, null, { params: { qnum }, }); return res.data; }; export default api;