All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 1m4s
337 lines
11 KiB
TypeScript
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;
|