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 @@ 0 dipilih - + @@ -75,6 +75,6 @@
Idle
- + \ No newline at end of file