const fs = require("fs"); const path = require("path"); const zlib = require("zlib"); const { DATA_DIR } = require("./paths"); const { LOG_BLACKLIST_URL_PARTS } = require("./config"); function time() { return new Date().toLocaleTimeString("en-US", { hour12: false }); } const log = (msg) => console.log(`[${time()}] [MITM] ${msg}`); const err = (msg) => console.error(`[${time()}] ❌ [MITM] ${msg}`); const DUMP_DIR = path.join(DATA_DIR, "logs", "mitm"); if (!fs.existsSync(DUMP_DIR)) fs.mkdirSync(DUMP_DIR, { recursive: true }); const EMPTY_BODY_RE = /^\s*(\{\s*\}|\[\s*\]|null)?\s*$/; function slugify(s, max = 80) { return String(s).replace(/[^a-zA-Z0-9]/g, "_").substring(0, max); } function isBlacklisted(url) { if (!url) return false; return LOG_BLACKLIST_URL_PARTS.some(part => url.includes(part)); } // Decode body buffer based on content-encoding header function decodeBody(buf, encoding) { if (!buf || buf.length === 0) return buf; try { const enc = (encoding || "").toLowerCase(); if (enc.includes("gzip")) return zlib.gunzipSync(buf); if (enc.includes("br")) return zlib.brotliDecompressSync(buf); if (enc.includes("deflate")) return zlib.inflateSync(buf); } catch { /* return raw on failure */ } return buf; } // Save raw request: method + url + headers + body function dumpRequest(req, bodyBuffer, tag = "raw") { if (isBlacklisted(req.url)) return null; try { const ts = new Date().toISOString().replace(/[:.]/g, "-"); const slug = slugify((req.headers.host || "") + req.url); const file = path.join(DUMP_DIR, `${ts}_${tag}_${slug}.req.json`); let parsed = null; try { parsed = JSON.parse(bodyBuffer.toString()); } catch { /* not JSON */ } fs.writeFileSync(file, JSON.stringify({ method: req.method, url: req.url, host: req.headers.host, headers: req.headers, body: parsed ?? bodyBuffer.toString("utf8") }, null, 2)); return file; } catch { return null; } } // Buffer-based response dumper — collects chunks then decodes + writes once on end() // Trade-off: holds response in RAM, but enables gzip/br decoding for readable output. function createResponseDumper(req, tag = "raw") { if (isBlacklisted(req.url)) return null; const ts = new Date().toISOString().replace(/[:.]/g, "-"); const slug = slugify((req.headers.host || "") + req.url); const file = path.join(DUMP_DIR, `${ts}_${tag}_${slug}.res.txt`); let status = 0; let headers = {}; const chunks = []; return { writeHeader: (s, h) => { status = s; headers = h || {}; }, writeChunk: (chunk) => { if (chunk == null) return; chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }, end: () => { try { const raw = Buffer.concat(chunks); const enc = headers["content-encoding"] || headers["Content-Encoding"]; const decoded = decodeBody(raw, enc); const text = decoded.toString("utf8"); // Skip empty / trivially-empty bodies if (EMPTY_BODY_RE.test(text)) return; // Strip content-encoding since body is now decoded const cleanHeaders = { ...headers }; delete cleanHeaders["content-encoding"]; delete cleanHeaders["Content-Encoding"]; const out = `STATUS: ${status}\nHEADERS: ${JSON.stringify(cleanHeaders, null, 2)}\n---BODY---\n${text}`; fs.writeFileSync(file, out); } catch { /* ignore */ } }, file }; } module.exports = { log, err, dumpRequest, createResponseDumper };