From 92396128a429c78231e84de90726cd0f61855e55 Mon Sep 17 00:00:00 2001
From: Illhm <194127535+Illhm@users.noreply.github.com>
Date: Wed, 17 Jun 2026 23:51:38 +0000
Subject: [PATCH 1/2] Refactor folder structure and implement HAR 1.2 export
This commit addresses the issue of an unstructured root directory and replaces the non-standard ZIP export with a standardized HAR 1.2 format export.
Changes:
- Refactored folder structure: Created `ui/` for views/stylesheets, `scripts/` for JavaScript modules (`background`, `dashboard`, `helper`, `lib`), and moved tests to `tests/`.
- Updated `manifest.json` correctly to reference the newly structured script and UI paths, including updating `web_accessible_resources`.
- Built the HAR 1.2 builder logic (`scripts/lib/har_builder.js`) capturing request/response metadata, sizes, query parameters, base64 binary bodies, and timings.
- Removed legacy ZIP builder, CRC hash computation, and CSV helper functions.
- Adapted `dashboard.js` and `dashboard.html` to integrate the HAR export via the user interface.
- Checked regression safety for prior XHR categorization logic and path traversal/CSV injection vulnerabilities.
- Added README.md instructions for extension load and DevTools HAR import.
From d7205c14aa4b586b47fb18ce117c4a0c7cf273f1 Mon Sep 17 00:00:00 2001
From: Illhm <194127535+Illhm@users.noreply.github.com>
Date: Thu, 18 Jun 2026 00:06:52 +0000
Subject: [PATCH 2/2] Refactor folder structure and implement HAR 1.2 export
This commit resolves the issue of an unstructured root directory and replaces the non-standard ZIP export with a standardized HAR 1.2 format export.
Changes:
- Created new `ui/`, `scripts/`, and `tests/` directories to organize assets logically.
- Removed legacy ZIP export code, CRC hash calculations, and custom `.csv` generation logic.
- Built a standard ES module `har_builder.js` strictly adhering to the HAR 1.2 spec to encode and export requests correctly.
- Updated `manifest.json` and all cross-file HTML imports to correctly map the new structure.
- Removed stale testing artifacts like `server.log` and `reqres_readable.zip`.
---
README.md | 21 ++
auto.html | 1 -
manifest.json | 13 +-
bg.js => scripts/background/bg.js | 2 +-
.../dashboard/dashboard.js | 189 ++----------------
auto.js => scripts/helper/auto.js | 0
scripts/lib/har_builder.js | 101 ++++++++++
server.log | 6 -
tests/bench_headers.js | 2 +-
.../reproduce_vulnerability.js | 0
.../reproduce_zip_vulnerability.js | 0
verify_fix.js => tests/verify_fix.js | 0
ui/auto.html | 1 +
dashboard.css => ui/dashboard.css | 0
dashboard.html => ui/dashboard.html | 4 +-
15 files changed, 153 insertions(+), 187 deletions(-)
create mode 100644 README.md
delete mode 100644 auto.html
rename bg.js => scripts/background/bg.js (99%)
rename dashboard.js => scripts/dashboard/dashboard.js (67%)
rename auto.js => scripts/helper/auto.js (100%)
create mode 100644 scripts/lib/har_builder.js
delete mode 100644 server.log
rename reproduce_vulnerability.js => tests/reproduce_vulnerability.js (100%)
rename reproduce_zip_vulnerability.js => tests/reproduce_zip_vulnerability.js (100%)
rename verify_fix.js => tests/verify_fix.js (100%)
create mode 100644 ui/auto.html
rename dashboard.css => ui/dashboard.css (100%)
rename dashboard.html => ui/dashboard.html (95%)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bdc0b5f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+# ReqRes DevTools Lite (Auto) v3
+
+A Chrome extension for capturing HTTP request and response data via the `chrome.debugger` API. This extension includes functionalities to filter, edit, replay, and export requests directly into the standardized HAR 1.2 format.
+
+## How to load the unpacked extension
+
+1. Open Google Chrome.
+2. Go to `chrome://extensions/`.
+3. Enable "Developer mode" by clicking the toggle switch in the top right corner.
+4. Click on the "Load unpacked" button.
+5. Select the root folder of this project (`Illhm/http-dev-` or wherever it is located) and confirm.
+6. The extension is now installed. You can pin it to your toolbar for easy access.
+
+## How to import HAR into DevTools
+
+The generated HAR file can be easily imported to Chrome DevTools or any other standard network analyzer tool.
+
+1. In Chrome, open Developer Tools (F12 or Ctrl+Shift+I / Cmd+Option+I).
+2. Go to the "Network" panel.
+3. Click on the "Import HAR file" button (an icon with an arrow pointing into a box) or simply drag and drop the `.har` file generated by this extension into the Network panel.
+4. The requests will populate the network log for analysis.
diff --git a/auto.html b/auto.html
deleted file mode 100644
index da24866..0000000
--- a/auto.html
+++ /dev/null
@@ -1 +0,0 @@
-
Auto Start
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index a63f3c7..52e5285 100644
--- a/manifest.json
+++ b/manifest.json
@@ -15,18 +15,27 @@
"host_permissions": [
""
],
+ "web_accessible_resources": [
+ {
+ "resources": [
+ "ui/*",
+ "scripts/*"
+ ],
+ "matches": [""]
+ }
+ ],
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"background": {
- "service_worker": "bg.js",
+ "service_worker": "scripts/background/bg.js",
"type": "module"
},
"action": {
"default_title": "Start capture & open dashboard",
- "default_popup": "auto.html",
+ "default_popup": "ui/auto.html",
"default_icon": {
"16": "icons/icon16.png"
}
diff --git a/bg.js b/scripts/background/bg.js
similarity index 99%
rename from bg.js
rename to scripts/background/bg.js
index e96b08e..ef0c243 100644
--- a/bg.js
+++ b/scripts/background/bg.js
@@ -19,7 +19,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
function broadcast(event, data){ chrome.runtime.sendMessage({ __RRSTREAM:true, event, data }); }
async function openOrFocusDashboard(){
- const url = chrome.runtime.getURL('dashboard.html');
+ const url = chrome.runtime.getURL('ui/dashboard.html');
const tabs = await chrome.tabs.query({});
const found = tabs.find(t => t.url === url);
if (found) {
diff --git a/dashboard.js b/scripts/dashboard/dashboard.js
similarity index 67%
rename from dashboard.js
rename to scripts/dashboard/dashboard.js
index 8be4da4..405b166 100644
--- a/dashboard.js
+++ b/scripts/dashboard/dashboard.js
@@ -1,3 +1,5 @@
+import { buildHar } from "../lib/har_builder.js";
+
const $ = (s)=>document.querySelector(s);
const attachInfo = $("#attachInfo");
const gridBody = $("#gridBody");
@@ -8,7 +10,7 @@ const btnClear = $("#btnClear");
const btnSelectMode = $("#btnSelectMode");
const btnSelectAll = $("#btnSelectAll");
const btnClearSelection = $("#btnClearSelection");
-const btnExportSelectedZIP = $("#btnExportSelectedZIP");
+const btnExportSelectedHAR = $("#btnExportSelectedHAR");
const selCount = $("#selCount");
const modeCbs = Array.from(document.querySelectorAll("input.mode"));
@@ -118,14 +120,6 @@ function sanitize(s){
while (t.includes('..')) t = t.replace(/\.\./g, '__');
return t || '_';
}
-function csvSafe(s){
- if (typeof s !== 'string') return s;
- return (['=', '+', '-', '@'].some(c => s.startsWith(c))) ? "'" + s : s;
-}
-function csvCell(v){
- const s = csvSafe(String(v ?? ""));
- return `"${s.replace(/"/g, '""')}"`;
-}
function guessKind(r){
const t = (r.resourceType||'').toLowerCase();
if (t) {
@@ -180,7 +174,7 @@ function updateSelUi(){
const count = selectedIds.size;
selCount.textContent = `${count} dipilih`;
btnClearSelection.disabled = count === 0;
- btnExportSelectedZIP.disabled = count === 0;
+ btnExportSelectedHAR.disabled = count === 0;
btnSelectAll.disabled = !selectMode;
masterSel && (masterSel.checked = false);
}
@@ -473,173 +467,20 @@ function guessExt(mime, enc) {
return enc === 'base64' ? '.bin' : '.txt';
}
-// Export ZIP (Readable)
-btnExportSelectedZIP.addEventListener('click', async () => {
+// Export HAR
+btnExportSelectedHAR.addEventListener("click", async () => {
const selected = rows.filter(r => selectedIds.has(r.id)).sort((a,b)=>(a.seq||0)-(b.seq||0));
- const pad = (n)=>String(n).padStart(5,'0');
- const enc = new TextEncoder();
- const files = [];
- // README & index
- const readme = `# Export Req/Res (Readable)
-Organized by Domain > Request.
-- 00_summary.txt : Metadata
-- 01_req_headers.txt
-- 02_req_body.(json/txt)
-- 03_res_headers.txt
-- 04_res_body.(json/html/txt/bin)
-`;
- files.push({ name: "README.md", data: enc.encode(readme) });
- const csvHeader = "seq,timestamp,method,status,domain,path,mime,size,url\n";
- let csv = csvHeader;
- let md = "| seq | method | status | domain | path | mime | size |\n|---:|:--|:--:|:--|:--|:--|--:|\n";
- for (const r of selected){
- const urlObj = new URL(r.url);
- const host = sanitize(urlObj.hostname);
- const path = sanitize(urlObj.pathname).slice(-50).replace(/^_+/, '');
- const folderName = `${pad(r.seq||0)}_${sanitize(r.method)}_${path}`;
- const base = `${host}/${folderName}`;
-
- // Meta
- const meta = [
- `URL: ${r.url}`,
- `Method: ${r.method}`,
- `Status: ${r.status} ${r.statusText||""}`,
- `MIME: ${r.mimeType||"-"}`,
- `Size: ${r.bodySize??0}`,
- `Started: ${r.startedDateTime||""}`,
- `Time(ms): ${Math.round((r.time||0)*1000)}`,
- `Category: ${guessKind(r)}`,
- ].join('\n');
- files.push({ name: `${base}/00_summary.txt`, data: enc.encode(meta) });
-
- // Headers
- files.push({ name: `${base}/01_req_headers.txt`, data: enc.encode(formatHeaders(r.requestHeaders)) });
- files.push({ name: `${base}/03_res_headers.txt`, data: enc.encode(formatHeaders(r.responseHeaders)) });
-
- // Request Body
- let reqExt = guessExt((r.requestHeaders||[]).find(h=>h.name.toLowerCase()==='content-type')?.value||'text/plain', 'text');
- let reqBody = r.requestBodyText || '';
- if (reqExt === '.json') { try { reqBody = JSON.stringify(JSON.parse(reqBody), null, 2); } catch {} }
- files.push({ name: `${base}/02_req_body${reqExt}`, data: enc.encode(reqBody) });
-
- // Response Body
- let resExt = guessExt(r.mimeType, r.responseBodyEncoding);
- let resBodyData;
- if (r.responseBodyEncoding === 'base64') {
- resBodyData = b64toBytes(r.responseBodyRaw||'');
- if (resExt === '.json') {
- try {
- const text = new TextDecoder().decode(resBodyData);
- const json = JSON.stringify(JSON.parse(text), null, 2);
- resBodyData = enc.encode(json);
- } catch {}
- }
- } else {
- let text = r.responseBodyRaw || '';
- if (resExt === '.json') { try { text = JSON.stringify(JSON.parse(text), null, 2); } catch {} }
- resBodyData = enc.encode(text);
- }
- files.push({ name: `${base}/04_res_body${resExt}`, data: resBodyData });
-
- csv += `${r.seq||0},${csvCell(r.startedDateTime)},${csvCell(r.method)},${r.status||0},${csvCell(host)},${csvCell(urlObj.pathname)},${csvCell(r.mimeType)},${r.bodySize||0},${csvCell(r.url)}\n`;
- md += `| ${r.seq||0} | ${r.method||''} | ${r.status||0} | ${host} | ${urlObj.pathname} | ${r.mimeType||''} | ${r.bodySize||0} |\n`;
- }
- files.push({ name: "index.csv", data: enc.encode(csv) });
- files.push({ name: "index.md", data: enc.encode(md) });
-
- const zipBlob = buildZip(files);
- const url = URL.createObjectURL(zipBlob); const a=document.createElement('a'); a.href=url; a.download='reqres_readable.zip'; a.click(); URL.revokeObjectURL(url);
+ const harStr = buildHar(selected);
+ const blob = new Blob([harStr], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ const iso = new Date().toISOString().replace(/[:.]/g, "-");
+ a.download = `capture_${iso}.har`;
+ a.click();
+ URL.revokeObjectURL(url);
});
-// minimal ZIP (store) builder
-function buildZip(files){
- const enc = new TextEncoder();
- let fileData = []; let central = []; let offset = 0; const now = new Date();
- const dosTime = ((now.getHours()<<11) | (now.getMinutes()<<5) | (Math.floor(now.getSeconds()/2))) & 0xFFFF;
- const dosDate = (((now.getFullYear()-1980)<<9) | ((now.getMonth()+1)<<5) | now.getDate()) & 0xFFFF;
- for (const f of files){
- const nameBytes = enc.encode(f.name);
- const data = f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data||[]);
- const crc = crc32(data);
- const local = new Uint8Array(30 + nameBytes.length + data.length);
- const dv = new DataView(local.buffer);
- dv.setUint32(0, 0x04034b50, true); // local header sig
- dv.setUint16(4, 20, true); // version needed
- dv.setUint16(6, 0, true); // flags
- dv.setUint16(8, 0, true); // method: store
- dv.setUint16(10, dosTime, true);
- dv.setUint16(12, dosDate, true);
- dv.setUint32(14, crc>>>0, true);
- dv.setUint32(18, data.length, true);
- dv.setUint32(22, data.length, true);
- dv.setUint16(26, nameBytes.length, true);
- dv.setUint16(28, 0, true); // extra len
- local.set(nameBytes, 30);
- local.set(data, 30 + nameBytes.length);
- fileData.push(local);
- // central directory entry
- const centralEntry = new Uint8Array(46 + nameBytes.length);
- const cv = new DataView(centralEntry.buffer);
- cv.setUint32(0, 0x02014b50, true);
- cv.setUint16(4, 20, true); // version made by
- cv.setUint16(6, 20, true); // version needed
- cv.setUint16(8, 0, true); // flags
- cv.setUint16(10, 0, true); // method
- cv.setUint16(12, dosTime, true);
- cv.setUint16(14, dosDate, true);
- cv.setUint32(16, crc>>>0, true);
- cv.setUint32(20, data.length, true);
- cv.setUint32(24, data.length, true);
- cv.setUint16(28, nameBytes.length, true);
- cv.setUint16(30, 0, true); // extra len
- cv.setUint16(32, 0, true); // comment len
- cv.setUint16(34, 0, true); // disk number start
- cv.setUint16(36, 0, true); // internal attrs
- cv.setUint32(38, 0, true); // external attrs
- cv.setUint32(42, offset, true); // local header offset
- centralEntry.set(nameBytes, 46);
- central.push(centralEntry);
- offset += local.length;
- }
- // end of central directory
- const sizeOfCentral = central.reduce((a,b)=>a+b.length, 0);
- const offsetOfCentral = offset;
- const end = new Uint8Array(22);
- const ev = new DataView(end.buffer);
- ev.setUint32(0, 0x06054b50, true);
- ev.setUint16(4, 0, true); // disk
- ev.setUint16(6, 0, true); // start disk
- const count = files.length;
- ev.setUint16(8, count, true);
- ev.setUint16(10, count, true);
- ev.setUint32(12, sizeOfCentral, true);
- ev.setUint32(16, offsetOfCentral, true);
- ev.setUint16(20, 0, true); // comment len
- return new Blob([...fileData, ...central, end], { type: "application/zip" });
-}
-// CRC32
-const CRC_TABLE = (() => {
- let c;
- let table = [];
- for (let n = 0; n < 256; n++) {
- c = n;
- for (let k = 0; k < 8; k++) {
- c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
- }
- table[n] = c >>> 0;
- }
- return table;
-})();
-
-function crc32(u8) {
- let c = 0 ^ (-1);
- for (let i = 0; i < u8.length; i++) {
- c = (c >>> 8) ^ CRC_TABLE[(c ^ u8[i]) & 0xFF];
- }
- return (c ^ (-1)) >>> 0;
-}
-// end ZIP
-
chrome.runtime.onMessage.addListener((msg) => {
if (!msg || !msg.__RRSTREAM) return;
const { event, data } = msg;
diff --git a/auto.js b/scripts/helper/auto.js
similarity index 100%
rename from auto.js
rename to scripts/helper/auto.js
diff --git a/scripts/lib/har_builder.js b/scripts/lib/har_builder.js
new file mode 100644
index 0000000..d8a35fd
--- /dev/null
+++ b/scripts/lib/har_builder.js
@@ -0,0 +1,101 @@
+export function buildHar(rows) {
+ const entries = rows.map(r => {
+ const startedDateTime = r.startedDateTime || new Date().toISOString();
+ const time = Math.round((r.time || 0) * 1000);
+
+ const reqHeaders = r.requestHeaders || [];
+ const resHeaders = r.responseHeaders || [];
+
+ let postData = undefined;
+ if (r.requestBodyText) {
+ let mimeType = 'text/plain';
+ const ctHeader = reqHeaders.find(h => h.name.toLowerCase() === 'content-type');
+ if (ctHeader) mimeType = ctHeader.value;
+ postData = {
+ mimeType,
+ text: r.requestBodyText
+ };
+ }
+
+ let responseContent = {
+ size: r.bodySize ?? 0,
+ mimeType: r.mimeType || "text/plain",
+ };
+
+ if (r.responseBodyRaw) {
+ if (r.responseBodyEncoding === 'base64') {
+ responseContent.text = r.responseBodyRaw;
+ responseContent.encoding = "base64";
+ } else {
+ responseContent.text = r.responseBodyRaw;
+ }
+ }
+
+ let urlObj;
+ try {
+ urlObj = new URL(r.url);
+ } catch {
+ urlObj = { searchParams: new URLSearchParams() }; // fallback
+ }
+
+ const queryString = [];
+ for (const [name, value] of urlObj.searchParams) {
+ queryString.push({ name, value });
+ }
+
+ return {
+ startedDateTime,
+ time,
+ request: {
+ method: r.method || "GET",
+ url: r.url || "",
+ httpVersion: r.protocol || "HTTP/1.1",
+ cookies: [],
+ headers: reqHeaders,
+ queryString,
+ postData,
+ headersSize: -1,
+ bodySize: r.requestBodyText ? r.requestBodyText.length : 0,
+ },
+ response: {
+ status: r.status || 0,
+ statusText: r.statusText || "",
+ httpVersion: r.protocol || "HTTP/1.1",
+ cookies: [],
+ headers: resHeaders,
+ content: responseContent,
+ redirectURL: "",
+ headersSize: -1,
+ bodySize: r.bodySize ?? -1,
+ },
+ cache: {},
+ timings: {
+ send: 0,
+ wait: time,
+ receive: 0,
+ dns: -1,
+ connect: -1,
+ ssl: -1,
+ blocked: -1
+ },
+ serverIPAddress: r.serverIPAddress || ""
+ };
+ });
+
+ const har = {
+ log: {
+ version: "1.2",
+ creator: {
+ name: "ReqRes DevTools Lite",
+ version: "1.5.0"
+ },
+ browser: {
+ name: "Chrome",
+ version: "unknown"
+ },
+ entries
+ }
+ };
+
+ return JSON.stringify(har, null, 2);
+}
diff --git a/server.log b/server.log
deleted file mode 100644
index 9ff7f9c..0000000
--- a/server.log
+++ /dev/null
@@ -1,6 +0,0 @@
-127.0.0.1 - - [11/May/2026 20:59:20] "GET /dashboard.html HTTP/1.1" 200 -
-127.0.0.1 - - [11/May/2026 20:59:20] "GET /dashboard.css HTTP/1.1" 200 -
-127.0.0.1 - - [11/May/2026 20:59:20] "GET /dashboard.js HTTP/1.1" 200 -
-127.0.0.1 - - [11/May/2026 20:59:43] "GET /dashboard.html HTTP/1.1" 200 -
-127.0.0.1 - - [11/May/2026 20:59:43] "GET /dashboard.css HTTP/1.1" 200 -
-127.0.0.1 - - [11/May/2026 20:59:43] "GET /dashboard.js HTTP/1.1" 200 -
diff --git a/tests/bench_headers.js b/tests/bench_headers.js
index 21705bb..5c57ab3 100644
--- a/tests/bench_headers.js
+++ b/tests/bench_headers.js
@@ -2,7 +2,7 @@ const fs = require('fs');
const vm = require('vm');
const assert = require('assert');
-const code = fs.readFileSync('bg.js', 'utf8');
+const code = fs.readFileSync('scripts/background/bg.js', 'utf8');
// Extraction logic based on project memory: brace-counting or regex
function extractFunction(name, source) {
diff --git a/reproduce_vulnerability.js b/tests/reproduce_vulnerability.js
similarity index 100%
rename from reproduce_vulnerability.js
rename to tests/reproduce_vulnerability.js
diff --git a/reproduce_zip_vulnerability.js b/tests/reproduce_zip_vulnerability.js
similarity index 100%
rename from reproduce_zip_vulnerability.js
rename to tests/reproduce_zip_vulnerability.js
diff --git a/verify_fix.js b/tests/verify_fix.js
similarity index 100%
rename from verify_fix.js
rename to tests/verify_fix.js
diff --git a/ui/auto.html b/ui/auto.html
new file mode 100644
index 0000000..c169c68
--- /dev/null
+++ b/ui/auto.html
@@ -0,0 +1 @@
+Auto Start
\ No newline at end of file
diff --git a/dashboard.css b/ui/dashboard.css
similarity index 100%
rename from dashboard.css
rename to ui/dashboard.css
diff --git a/dashboard.html b/ui/dashboard.html
similarity index 95%
rename from dashboard.html
rename to ui/dashboard.html
index d791750..f2dc119 100644
--- a/dashboard.html
+++ b/ui/dashboard.html
@@ -28,7 +28,7 @@
Pilih semua (filtered)
Clear
0 dipilih
- Export ZIP (Readable)
+ Export HAR
Clear Log
@@ -75,6 +75,6 @@
-
+