mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
feat(inspector): add harExport (F4)
This commit is contained in:
188
src/lib/inspector/harExport.ts
Normal file
188
src/lib/inspector/harExport.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* HAR (HTTP Archive) v1.2 export for the Traffic Inspector.
|
||||
*
|
||||
* HAR is the standard format consumed by Chrome DevTools, Charles, Fiddler,
|
||||
* Postman, and most observability tools. Exporting lets users carry the
|
||||
* trace out of OmniRoute into their existing workflow.
|
||||
*
|
||||
* Secrets are *always* masked on export, regardless of the UI state — see
|
||||
* Hard Rule #1 (no credentials in artefacts). The OmniRoute capture source
|
||||
* (agent-bridge / custom-host / http-proxy / system-proxy) is preserved as
|
||||
* `_source`, a custom field allowed by the HAR spec's underscore convention.
|
||||
*/
|
||||
|
||||
import { maskSecret } from "@/mitm/maskSecrets";
|
||||
import type { InterceptedRequest } from "@/mitm/inspector/types";
|
||||
|
||||
const HAR_VERSION = "1.2";
|
||||
const CREATOR_NAME = "OmniRoute Traffic Inspector";
|
||||
const CREATOR_VERSION = "3.8.6";
|
||||
|
||||
interface HarNameValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface HarPostData {
|
||||
mimeType: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface HarRequest {
|
||||
method: string;
|
||||
url: string;
|
||||
httpVersion: string;
|
||||
headers: HarNameValue[];
|
||||
queryString: HarNameValue[];
|
||||
cookies: HarNameValue[];
|
||||
headersSize: number;
|
||||
bodySize: number;
|
||||
postData?: HarPostData;
|
||||
}
|
||||
|
||||
interface HarContent {
|
||||
size: number;
|
||||
mimeType: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface HarResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
httpVersion: string;
|
||||
headers: HarNameValue[];
|
||||
cookies: HarNameValue[];
|
||||
content: HarContent;
|
||||
redirectURL: string;
|
||||
headersSize: number;
|
||||
bodySize: number;
|
||||
}
|
||||
|
||||
interface HarTimings {
|
||||
send: number;
|
||||
wait: number;
|
||||
receive: number;
|
||||
}
|
||||
|
||||
interface HarEntry {
|
||||
startedDateTime: string;
|
||||
time: number;
|
||||
request: HarRequest;
|
||||
response: HarResponse;
|
||||
cache: Record<string, never>;
|
||||
timings: HarTimings;
|
||||
serverIPAddress?: string;
|
||||
_source?: string;
|
||||
_agent?: string;
|
||||
_detectedKind?: string;
|
||||
_contextKey?: string;
|
||||
_sessionId?: string;
|
||||
_annotation?: string;
|
||||
_note?: string;
|
||||
_omniRouteId?: string;
|
||||
}
|
||||
|
||||
export interface HarFile {
|
||||
log: {
|
||||
version: string;
|
||||
creator: { name: string; version: string };
|
||||
entries: HarEntry[];
|
||||
};
|
||||
}
|
||||
|
||||
function headersToList(headers: Record<string, string>): HarNameValue[] {
|
||||
return Object.entries(headers).map(([name, value]) => ({
|
||||
name,
|
||||
value: maskSecret(value),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildUrl(host: string, path: string): string {
|
||||
// CONNECT entries carry path ":443" — treat them as opaque pseudo-URL.
|
||||
if (path.startsWith(":")) return `https://${host}${path}`;
|
||||
if (!host) return path;
|
||||
return `https://${host}${path}`;
|
||||
}
|
||||
|
||||
function buildPostData(req: InterceptedRequest): HarPostData | undefined {
|
||||
if (!req.requestBody) return undefined;
|
||||
const ct =
|
||||
req.requestHeaders["content-type"] ??
|
||||
req.requestHeaders["Content-Type"] ??
|
||||
"application/octet-stream";
|
||||
return { mimeType: ct, text: maskSecret(req.requestBody) };
|
||||
}
|
||||
|
||||
function buildResponseContent(req: InterceptedRequest): HarContent {
|
||||
const ct =
|
||||
req.responseHeaders["content-type"] ??
|
||||
req.responseHeaders["Content-Type"] ??
|
||||
"application/octet-stream";
|
||||
if (req.responseBody == null) {
|
||||
return { size: req.responseSize, mimeType: ct };
|
||||
}
|
||||
return { size: req.responseSize, mimeType: ct, text: maskSecret(req.responseBody) };
|
||||
}
|
||||
|
||||
function buildEntry(req: InterceptedRequest): HarEntry {
|
||||
const numericStatus = typeof req.status === "number" ? req.status : 0;
|
||||
const statusText = typeof req.status === "string" ? req.status : "";
|
||||
|
||||
const entry: HarEntry = {
|
||||
startedDateTime: req.timestamp,
|
||||
time: req.totalLatencyMs ?? 0,
|
||||
request: {
|
||||
method: req.method,
|
||||
url: buildUrl(req.host, req.path),
|
||||
httpVersion: "HTTP/1.1",
|
||||
headers: headersToList(req.requestHeaders),
|
||||
queryString: [],
|
||||
cookies: [],
|
||||
headersSize: -1,
|
||||
bodySize: req.requestSize,
|
||||
postData: buildPostData(req),
|
||||
},
|
||||
response: {
|
||||
status: numericStatus,
|
||||
statusText,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headers: headersToList(req.responseHeaders),
|
||||
cookies: [],
|
||||
content: buildResponseContent(req),
|
||||
redirectURL: "",
|
||||
headersSize: -1,
|
||||
bodySize: req.responseSize,
|
||||
},
|
||||
cache: {},
|
||||
timings: {
|
||||
send: 0,
|
||||
wait: req.upstreamLatencyMs ?? 0,
|
||||
receive: (req.totalLatencyMs ?? 0) - (req.upstreamLatencyMs ?? 0),
|
||||
},
|
||||
_source: req.source,
|
||||
_omniRouteId: req.id,
|
||||
};
|
||||
|
||||
if (req.agent) entry._agent = req.agent;
|
||||
if (req.detectedKind) entry._detectedKind = req.detectedKind;
|
||||
if (req.contextKey) entry._contextKey = req.contextKey;
|
||||
if (req.sessionId) entry._sessionId = req.sessionId;
|
||||
if (req.annotation) entry._annotation = req.annotation;
|
||||
if (req.note) entry._note = req.note;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert intercepted requests into a HAR v1.2 file. Always masks secrets in
|
||||
* headers and bodies — callers do not need to pre-mask.
|
||||
*/
|
||||
export function toHar(requests: InterceptedRequest[]): HarFile {
|
||||
return {
|
||||
log: {
|
||||
version: HAR_VERSION,
|
||||
creator: { name: CREATOR_NAME, version: CREATOR_VERSION },
|
||||
entries: requests.map(buildEntry),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user