import { Entry, Har, PostData, Request, Response } from "har-format"; import { render, Component } from "preact"; type MyState = { finished_entries: Entry[]; unfinished_entries: Map; }; class TrafficLog extends Component { connection: WebSocket | undefined; state: MyState = { finished_entries: [], unfinished_entries: new Map() }; constructor() { super(); } componentDidMount() { // This should also be dynamic this.connection = new WebSocket("ws://localhost:10001"); this.connection.onmessage = (msg) => { this.process_msg(msg.data); this.setState({ finished_entries: this.state.finished_entries, unfinished_entries: this.state.unfinished_entries, }); }; this.connection.onclose = this.connection.onerror = () => { window.location.reload(); }; } render() { const download_har = () => { var tempLink = document.createElement("a"); var taBlob = new Blob([JSON.stringify(this.export_har())], { type: "text/plain", }); tempLink.setAttribute("href", URL.createObjectURL(taBlob)); tempLink.setAttribute("download", `rentgendroid-capture.har`); tempLink.click(); URL.revokeObjectURL(tempLink.href); }; const inspect_har = async () => { const req_body = { har: this.export_har(), private_data: [ [ "adid", document.getElementById("adid_priv_info_table")! .textContent, ], [ "latitude", document.getElementById("lat_priv_info_table")! .textContent, ], [ "longitude", document.getElementById("lon_priv_info_table")! .textContent, ], ], }; const resp = await fetch("/inspect_har", { method: "POST", body: JSON.stringify(req_body), }); const resp_text = await resp.text(); const newWindow = window.open(); newWindow?.document.write(resp_text); newWindow?.document.close(); }; const contentWithLineBreaks = this.state.finished_entries.map((req) => { return ( {req.request.url}
); }); return (

stats:

Request + responce pairs:{" "} {this.state.finished_entries.length}

Waiting for the responce:{" "} {this.state.unfinished_entries.size}

{contentWithLineBreaks}
); } process_msg(s: string) { let obj = JSON.parse(s); console.log(obj); if (obj.type !== "data") return; if (obj.payload && obj.payload.data && obj.payload.data.requestReceived) this.process_req(obj.payload.data.requestReceived); if ( obj.payload && obj.payload.data && obj.payload.data.responseCompleted ) this.process_res(obj.payload.data.responseCompleted); } process_res(res: any) { let entry = this.state.unfinished_entries.get(res.id)!; let content_type = "application/text"; let headers = JSON.parse(res.rawHeaders).map( (header: [string, string]) => { if (header[0].toLowerCase() === "content-type") content_type = header[1]; return { name: header[0], value: header[1], comment: "" }; } ); //'{"startTime":1751745139334, // "startTimestamp":347666.762487, // "bodyReceivedTimestamp":347667.529477, // "headersSentTimestamp":347906.038202, // "responseSentTimestamp":347906.616067}' let timing_events = JSON.parse(res.timingEvents); let start_ts = timing_events.startTimestamp; let got_headers_ts = timing_events.headersSentTimestamp; let end_ts = timing_events.responseSentTimestamp; let wait_time = got_headers_ts - start_ts; let recieve_time = end_ts - got_headers_ts; let response: Response = { status: res.statusCode, statusText: res.statusMessage, httpVersion: entry.request.httpVersion, cookies: [], headers, content: { size: 0, mimeType: content_type, text: res.body, encoding: "base64", }, redirectURL: "", headersSize: -1, bodySize: -1, }; entry.response = response; entry.timings.wait = wait_time; entry.timings.receive = recieve_time; this.state.unfinished_entries.delete(res.id); this.state.finished_entries.push(entry); } process_req(req: any) { let content_type = "application/text"; let headers = JSON.parse(req.rawHeaders).map( (header: [string, string]) => { if (header[0].toLowerCase() === "Content-Type") content_type = header[1]; return { name: header[0], value: header[1], comment: "" }; } ); let timing_events = JSON.parse(req.timingEvents); let start_time: number = timing_events.startTime!; let start_datetime = new Date(start_time).toISOString(); let request: Request = { method: req.method, url: req.url, httpVersion: req.httpVersion, cookies: [], headers, queryString: [], postData: req.body ? ({ text: req.body, mimeType: content_type } as PostData) : undefined, headersSize: -1, bodySize: -1, comment: "", }; //'{"startTime":1751745139334,"startTimestamp":347666.762487,"bodyReceivedTimestamp":347667.529477,"headersSentTimestamp":347906.038202,"responseSentTimestamp":347906.616067}' let entry: Entry = { startedDateTime: start_datetime, time: 0, request: request, response: { status: 0, statusText: "", httpVersion: "", cookies: [], headers: [], content: { size: 0, mimeType: "", }, redirectURL: "", headersSize: 0, bodySize: 0, }, cache: {}, timings: { wait: 0, receive: 0, }, }; this.state.unfinished_entries.set(req.id, entry); } export_har(): Har { let ret: Har = { log: { version: "1.2", creator: { name: "Rentgendroid", version: "0.0.1", }, entries: [ ...this.state.finished_entries, ...this.state.unfinished_entries.values(), ], }, }; return ret; } } render(, document.getElementById("traffic-log")!);