Files
mitm-webserver/frontend/src/api/apiClient.ts
malmert 0f2a342b75
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 1m4s
test database privilige and index + delete ep packets
2026-03-01 18:23:08 +01:00

337 lines
11 KiB
TypeScript

// 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<any> => {
const res = await api.get('/hello');
return res.data;
};
export const fetchVersions = async (): Promise<any> => {
const res = await api.get('/versions');
return res.data;
};
/* -------------------------
Network queries (existing)
------------------------- */
export const fetchInterfaces = async (): Promise<InterfaceInfo[]> => {
const res = await api.get<InterfaceInfo[]>('/network/interfaces');
return res.data;
};
export const fetchLinks = async (): Promise<InterfaceInfo[]> => {
const res = await api.get<InterfaceInfo[]>('/network/links');
return res.data;
};
export const fetchRoutes = async (): Promise<RouteInfo[]> => {
const res = await api.get<RouteInfo[]>('/network/routes');
return res.data;
};
export const fetchBridges = async (): Promise<BridgeInfo[]> => {
const res = await api.get<BridgeInfo[]>('/network/bridges');
return res.data;
};
export const fetchFullState = async (): Promise<FullState> => {
const res = await api.get<FullState>('/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<SnifferStartResponse> => {
const res = await api.post<SnifferStartResponse>('/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<SnifferStopResponse> => {
const res = await api.post<SnifferStopResponse>('/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<SnifferStopResponse> => {
const res = await api.post<SnifferStopResponse>(`/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<SnifferStopResponse> => {
const res = await api.post<SnifferStopResponse>(`/sniffer/stop?bridge=${encodeURIComponent(bridge)}`, {});
return res.data;
};
/**
* Fetch the sniffer status (per-interface).
*/
export const fetchSnifferStatus = async (): Promise<SnifferStatusResponse> => {
const res = await api.get<SnifferStatusResponse>('/sniffer/status');
return res.data;
};
/* -------------------------
Packets
------------------------- */
export const fetchPackets = async (limit = 100): Promise<any> => {
// limit default mirrors OpenAPI default
const res = await api.get('/packets/packets', { params: { limit } });
return res.data;
};
export const clearPackets = async (): Promise<any> => {
// 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<void> => {
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<ExecResult> => {
const res = await api.post<ExecResult>('/firewall/rules', req);
return res.data;
};
export const execFirewallRaw = async (cmd: string): Promise<ExecResult> => {
const res = await api.post<ExecResult>('/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<ScriptWithStatus[]> => {
const res = await api.get<ScriptWithStatus[]>('/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<ScriptInfo[]> => {
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<StatusForNameResponse> => {
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<ScriptUploadResponse> => {
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<ScriptUploadResponse>('/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<Blob> => {
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<Blob> => {
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<RequirementsUploadResult> => {
const fd = new FormData();
fd.append('requirements', requirements);
const res = await api.put<RequirementsUploadResult>(`/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<RequirementsDeleteResult> => {
const res = await api.delete<RequirementsDeleteResult>(`/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<DeleteResult> => {
const params: Record<string, any> = {};
if (typeof qnum !== 'undefined' && qnum !== null) params.qnum = qnum;
const res = await api.delete<DeleteResult>(`/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<OperationResult> => {
const res = await api.post<OperationResult>(`/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<OperationResult> => {
const res = await api.post<OperationResult>(`/scripts/scripts/${encodeURIComponent(name)}/disable`, null, {
params: { qnum },
});
return res.data;
};
export default api;