From 71df79215e28a772c12eba02b573476771c27e27 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 20:32:28 +0000 Subject: [PATCH 1/2] Add http-mitm-proxy for HTTPS interception - Install http-mitm-proxy package - Add test script demonstrating header injection - Auto-generates certs per domain on first connect - Works with curl, Python, Node when CA is trusted To use system-wide: cp .http-mitm-proxy/certs/ca.pem /usr/local/share/ca-certificates/mitm.crt update-ca-certificates export https_proxy=http://localhost:8080 --- .gitignore | 1 + bun.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + test-mitm-proxy.ts | 46 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 test-mitm-proxy.ts diff --git a/.gitignore b/.gitignore index d21dcbe..898f438 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Agent runtime data (contexts, logs) .mini-agent/ +.http-mitm-proxy/ diff --git a/bun.lock b/bun.lock index df96ba0..5246264 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "@opentui/core": "^0.1.55", "@opentui/react": "^0.1.55", "effect": "^3.19.8", + "http-mitm-proxy": "^1.1.0", "react": "19", "react-dom": "19", "yaml": "^2.7.0", @@ -520,6 +521,8 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], @@ -570,6 +573,8 @@ "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -606,6 +611,8 @@ "effect": ["effect@3.19.8", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-OmLw8EfH02vdmyU2fO4uY9He/wepwKI5E/JNpE2pseaWWUbaYOK9UlxIiKP20ZEqQr+S/jSqRDGmpiqD/2DeCQ=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -624,6 +631,8 @@ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], @@ -702,6 +711,8 @@ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -740,6 +751,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "http-mitm-proxy": ["http-mitm-proxy@1.1.0", "", { "dependencies": { "async": "^3.2.5", "debug": "^4.3.4", "mkdirp": "^1.0.4", "node-forge": "^1.3.1", "semaphore": "^1.1.0", "uuid": "^9.0.1", "ws": "^8.14.2", "yargs": "^17.7.2" }, "bin": { "http-mitm-proxy": "dist/bin/mitm-proxy.js" } }, "sha512-GyjWXiukopLzp6Yg4Dk1M+6Yfp5c/rPtqXDGaRopeJH1bd9F6k2cyJrB98kKmJaMU2P7kA5LLHbtuFc8HcNvrA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -776,6 +789,8 @@ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -852,6 +867,8 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], @@ -872,6 +889,8 @@ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], "node-pty": ["node-pty@1.0.0", "", { "dependencies": { "nan": "^2.17.0" } }, "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA=="], @@ -960,6 +979,8 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -980,6 +1001,8 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "semaphore": ["semaphore@1.1.0", "", {}, "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -1016,6 +1039,8 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], @@ -1092,7 +1117,7 @@ "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="], @@ -1116,6 +1141,8 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], @@ -1124,8 +1151,14 @@ "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], @@ -1134,8 +1167,12 @@ "@anthropic-ai/tokenizer/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@effect/experimental/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@effect/rpc-http/@effect/rpc": ["@effect/rpc@0.54.4", "", { "peerDependencies": { "@effect/platform": "^0.79.4", "effect": "^3.13.12" } }, "sha512-iu3TGWCt4OMH8iKL1ATeROhAxrMF+HdF3NbR5lWls9yWJwBgVU+cps3ZzRbNQhFPWXDGqVuYgmYNY1GKbZgMaw=="], + "@effect/sql/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], @@ -1156,6 +1193,8 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -1174,6 +1213,10 @@ "react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@anthropic-ai/tokenizer/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@opentelemetry/sdk-logs/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], @@ -1185,5 +1228,11 @@ "@opentelemetry/sdk-metrics/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index 1664ef1..0b1ad35 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@opentui/core": "^0.1.55", "@opentui/react": "^0.1.55", "effect": "^3.19.8", + "http-mitm-proxy": "^1.1.0", "react": "19", "react-dom": "19", "yaml": "^2.7.0" diff --git a/test-mitm-proxy.ts b/test-mitm-proxy.ts new file mode 100644 index 0000000..4981e98 --- /dev/null +++ b/test-mitm-proxy.ts @@ -0,0 +1,46 @@ +/** + * Test script for http-mitm-proxy + * + * This sets up a MITM proxy that: + * 1. Intercepts HTTPS requests + * 2. Adds a custom header + * 3. Logs request details + */ + +import { Proxy as MitmProxy } from "http-mitm-proxy" + +const proxy = new MitmProxy() + +proxy.onError((ctx, err) => { + console.error("Proxy error:", err) +}) + +proxy.onRequest((ctx, callback) => { + const host = ctx.clientToProxyRequest.headers.host || "unknown" + const url = ctx.clientToProxyRequest.url || "/" + console.log(`[PROXY] ${ctx.clientToProxyRequest.method} ${host}${url}`) + + // Inject custom header + if (ctx.proxyToServerRequestOptions?.headers) { + ctx.proxyToServerRequestOptions.headers["X-Injected-By-Proxy"] = "mitm-test" + } + + callback() +}) + +proxy.onResponse((ctx, callback) => { + console.log(`[PROXY] Response: ${ctx.serverToProxyResponse?.statusCode}`) + callback() +}) + +const PORT = 8080 + +proxy.listen({ port: PORT }, () => { + console.log(`MITM proxy listening on port ${PORT}`) + console.log(`CA cert will be at: ${process.cwd()}/.http-mitm-proxy/certs/ca.pem`) + console.log("") + console.log("To test:") + console.log(` export NODE_EXTRA_CA_CERTS=${process.cwd()}/.http-mitm-proxy/certs/ca.pem`) + console.log(` export https_proxy=http://localhost:${PORT}`) + console.log(` curl -v https://httpbin.org/headers`) +}) From e09e0d3ce31536218c9b0e7dbb6c3d269b236528 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 21:01:13 +0000 Subject: [PATCH 2/2] Add comprehensive MITM proxy testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test/mitm-proxy.ts: Main proxy with header injection and response modification - test/mitm-proxy-mkcert.ts: Variant using mkcert for Go-compatible certs - test/mitm-proxy.test.ts: Vitest tests covering curl, wget, Python, Node - test/mitm-scripts/: Individual test scripts for each client - docs/MITM-PROXY.md: Setup documentation Key features: - Uses Proxy.gunzip for automatic gzip/deflate decompression - Injects X-Proxy-Injected header on outgoing requests - Appends marker to text responses (html/json/plain) - Auto-generates certs per domain Client compatibility tested: - curl, wget, Python urllib, Node undici: ✅ - npm: ✅ (needs explicit config) - Go/gh: ⚠️ (cert serial number issues with node-forge) --- .gitignore | 2 + bun.lock | 3 + docs/MITM-PROXY.md | 183 +++++++++++++++++++++++++++++++ package.json | 1 + test-mitm-proxy.ts | 46 -------- test/mitm-proxy-mkcert.ts | 175 +++++++++++++++++++++++++++++ test/mitm-proxy.test.ts | 140 +++++++++++++++++++++++ test/mitm-proxy.ts | 108 ++++++++++++++++++ test/mitm-scripts/test-curl.sh | 33 ++++++ test/mitm-scripts/test-node.ts | 48 ++++++++ test/mitm-scripts/test-python.py | 48 ++++++++ 11 files changed, 741 insertions(+), 46 deletions(-) create mode 100644 docs/MITM-PROXY.md delete mode 100644 test-mitm-proxy.ts create mode 100644 test/mitm-proxy-mkcert.ts create mode 100644 test/mitm-proxy.test.ts create mode 100644 test/mitm-proxy.ts create mode 100755 test/mitm-scripts/test-curl.sh create mode 100644 test/mitm-scripts/test-node.ts create mode 100755 test/mitm-scripts/test-python.py diff --git a/.gitignore b/.gitignore index 898f438..a44bd09 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Agent runtime data (contexts, logs) .mini-agent/ .http-mitm-proxy/ +.mkcert-ca/ +.mitm-mkcert-certs/ diff --git a/bun.lock b/bun.lock index 5246264..9274f07 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,7 @@ "eslint-plugin-sort-destructure-keys": "^2.0.0", "semver": "^7.7.3", "tuistory": "^0.0.2", + "undici": "^7.18.2", "vitest": "^3.2.0", }, "peerDependencies": { @@ -1109,6 +1110,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], diff --git a/docs/MITM-PROXY.md b/docs/MITM-PROXY.md new file mode 100644 index 0000000..478ff19 --- /dev/null +++ b/docs/MITM-PROXY.md @@ -0,0 +1,183 @@ +# MITM Proxy Setup + +This document describes how to set up an HTTPS-intercepting MITM proxy for testing and development. + +## Overview + +The proxy intercepts HTTPS traffic, allowing you to: +- Inject headers on outgoing requests +- Modify response bodies (for text content) +- Log all HTTPS traffic + +## Quick Start + +```bash +# Start the proxy +bun run test/mitm-proxy.ts + +# In another terminal, test it: +export https_proxy=http://localhost:8080 +curl --cacert .http-mitm-proxy/certs/ca.pem https://httpbin.org/headers +``` + +## Client Compatibility + +| Client | Works | Notes | +|--------|-------|-------| +| **curl** | ✅ | `--cacert path/to/ca.pem` | +| **wget** | ✅ | `--ca-certificate=path/to/ca.pem` | +| **Python urllib** | ✅ | `ssl.load_verify_locations(ca_path)` | +| **Node.js/Bun** | ✅ | `NODE_EXTRA_CA_CERTS=path/to/ca.pem` | +| **npm** | ✅ | Requires explicit config (see below) | +| **Go http.Client** | ⚠️ | See "Go Compatibility" below | +| **gh CLI** | ⚠️ | Uses Go internally | + +## Configuration by Client + +### curl + +```bash +export https_proxy=http://localhost:8080 +curl --cacert .http-mitm-proxy/certs/ca.pem https://example.com +``` + +### wget + +```bash +export HTTPS_PROXY=http://localhost:8080 +wget --ca-certificate=.http-mitm-proxy/certs/ca.pem https://example.com +``` + +### Python + +```python +import urllib.request +import ssl + +proxy = urllib.request.ProxyHandler({'https': 'http://localhost:8080'}) +ctx = ssl.create_default_context() +ctx.load_verify_locations('.http-mitm-proxy/certs/ca.pem') +https = urllib.request.HTTPSHandler(context=ctx) +opener = urllib.request.build_opener(proxy, https) + +response = opener.open('https://example.com') +``` + +### Node.js / Bun + +```bash +export NODE_EXTRA_CA_CERTS=.http-mitm-proxy/certs/ca.pem +export https_proxy=http://localhost:8080 +node my-script.js +``` + +Or programmatically with undici: + +```typescript +import { ProxyAgent } from 'undici' +import { readFileSync } from 'node:fs' + +const dispatcher = new ProxyAgent({ + uri: 'http://localhost:8080', + requestTls: { ca: readFileSync('.http-mitm-proxy/certs/ca.pem') } +}) + +const response = await fetch('https://example.com', { dispatcher }) +``` + +### npm + +npm ignores `https_proxy` env var. Configure explicitly: + +```bash +npm config set proxy http://localhost:8080 +npm config set https-proxy http://localhost:8080 +npm config set cafile .http-mitm-proxy/certs/ca.pem +npm config set strict-ssl false + +npm install some-package +``` + +To clean up after testing: + +```bash +npm config delete proxy https-proxy cafile strict-ssl +``` + +## Go Compatibility + +**Issue:** http-mitm-proxy uses node-forge for certificate generation, which can produce certificates with negative serial numbers. Go's `crypto/x509` package rejects these as invalid. + +**Workaround:** Use mkcert for certificate generation: + +```bash +# Install mkcert +apt-get install mkcert + +# Create CA +CAROOT=.mkcert-ca mkcert -install + +# Use mkcert proxy variant +bun run test/mitm-proxy-mkcert.ts + +# Set SSL_CERT_FILE for Go clients +export SSL_CERT_FILE=.mkcert-ca/rootCA.pem +export HTTPS_PROXY=http://localhost:8080 +``` + +**Note:** Even with mkcert certs, Go clients may encounter "too many transfer encodings" errors due to HTTP/1.x transport strictness. This is a known limitation. + +## System-Wide Trust + +To avoid passing CA cert path to every command: + +### Linux (Debian/Ubuntu) + +```bash +cp .http-mitm-proxy/certs/ca.pem /usr/local/share/ca-certificates/mitm-proxy.crt +update-ca-certificates +``` + +### macOS + +```bash +sudo security add-trusted-cert -d -r trustRoot \ + -k /Library/Keychains/System.keychain \ + .http-mitm-proxy/certs/ca.pem +``` + +## Response Modification + +The proxy uses `Proxy.gunzip` to automatically decompress gzip/deflate responses before modification. Modifies these content types: +- `text/html` +- `text/plain` +- `application/json` + +Binary content (images, tarballs) passes through unmodified. + +## Files + +| File | Purpose | +|------|---------| +| `test/mitm-proxy.ts` | Basic proxy (node-forge certs) | +| `test/mitm-proxy-mkcert.ts` | Proxy with mkcert certs (Go-compatible) | +| `.http-mitm-proxy/certs/` | Auto-generated certificates | +| `.mkcert-ca/` | mkcert CA (if using mkcert variant) | + +## Troubleshooting + +### "certificate verify failed" +- Ensure CA cert is trusted (system-wide or per-command) +- Check the correct CA path is being used + +### npm install fails with Z_DATA_ERROR +- Earlier versions corrupted compressed responses +- Fixed by using `Proxy.gunzip` for automatic decompression + +### Go "negative serial number" +- Use mkcert proxy variant instead of default +- Or upgrade to http-mitm-proxy version that fixes node-forge cert generation + +### Go "too many transfer encodings" +- Known issue with Go's strict HTTP/1.x parsing +- Workaround: use HTTP/2 where possible diff --git a/package.json b/package.json index 0b1ad35..ca6cd6e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint-plugin-sort-destructure-keys": "^2.0.0", "semver": "^7.7.3", "tuistory": "^0.0.2", + "undici": "^7.18.2", "vitest": "^3.2.0" }, "peerDependencies": { diff --git a/test-mitm-proxy.ts b/test-mitm-proxy.ts deleted file mode 100644 index 4981e98..0000000 --- a/test-mitm-proxy.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Test script for http-mitm-proxy - * - * This sets up a MITM proxy that: - * 1. Intercepts HTTPS requests - * 2. Adds a custom header - * 3. Logs request details - */ - -import { Proxy as MitmProxy } from "http-mitm-proxy" - -const proxy = new MitmProxy() - -proxy.onError((ctx, err) => { - console.error("Proxy error:", err) -}) - -proxy.onRequest((ctx, callback) => { - const host = ctx.clientToProxyRequest.headers.host || "unknown" - const url = ctx.clientToProxyRequest.url || "/" - console.log(`[PROXY] ${ctx.clientToProxyRequest.method} ${host}${url}`) - - // Inject custom header - if (ctx.proxyToServerRequestOptions?.headers) { - ctx.proxyToServerRequestOptions.headers["X-Injected-By-Proxy"] = "mitm-test" - } - - callback() -}) - -proxy.onResponse((ctx, callback) => { - console.log(`[PROXY] Response: ${ctx.serverToProxyResponse?.statusCode}`) - callback() -}) - -const PORT = 8080 - -proxy.listen({ port: PORT }, () => { - console.log(`MITM proxy listening on port ${PORT}`) - console.log(`CA cert will be at: ${process.cwd()}/.http-mitm-proxy/certs/ca.pem`) - console.log("") - console.log("To test:") - console.log(` export NODE_EXTRA_CA_CERTS=${process.cwd()}/.http-mitm-proxy/certs/ca.pem`) - console.log(` export https_proxy=http://localhost:${PORT}`) - console.log(` curl -v https://httpbin.org/headers`) -}) diff --git a/test/mitm-proxy-mkcert.ts b/test/mitm-proxy-mkcert.ts new file mode 100644 index 0000000..933981f --- /dev/null +++ b/test/mitm-proxy-mkcert.ts @@ -0,0 +1,175 @@ +/** + * MITM proxy using mkcert for certificate generation. + * + * This version uses mkcert instead of node-forge for cert generation, + * which produces certs that Go's crypto/x509 accepts (proper serial numbers). + */ + +import { Proxy as MitmProxy } from "http-mitm-proxy" +import { execSync, spawnSync } from "node:child_process" +import { existsSync, mkdirSync, readFileSync } from "node:fs" + +export const INJECTED_HEADER = "X-Proxy-Injected" +export const INJECTED_HEADER_VALUE = "mitm-active" +export const RESPONSE_MARKER = "\n" + +const MKCERT_CAROOT = process.env.MKCERT_CAROOT || "/home/user/mini-agent/.mkcert-ca" +const CERTS_DIR = "/home/user/mini-agent/.mitm-mkcert-certs" + +// Ensure CA exists +function ensureMkcertCa() { + if (!existsSync(`${MKCERT_CAROOT}/rootCA.pem`)) { + console.log("Creating mkcert CA...") + execSync(`CAROOT=${MKCERT_CAROOT} mkcert -install`, { stdio: "inherit" }) + } +} + +// Generate cert for hostname using mkcert +function generateCert(hostname: string): { keyFileData: string; certFileData: string } { + mkdirSync(CERTS_DIR, { recursive: true }) + + const keyFile = `${CERTS_DIR}/${hostname}-key.pem` + const certFile = `${CERTS_DIR}/${hostname}.pem` + + // Check if cert already exists + if (existsSync(keyFile) && existsSync(certFile)) { + return { + keyFileData: readFileSync(keyFile, "utf-8"), + certFileData: readFileSync(certFile, "utf-8") + } + } + + // Generate new cert with mkcert + const result = spawnSync( + "mkcert", + ["-key-file", keyFile, "-cert-file", certFile, hostname], + { + env: { ...process.env, CAROOT: MKCERT_CAROOT }, + encoding: "utf-8" + } + ) + + if (result.status !== 0) { + throw new Error(`mkcert failed for ${hostname}: ${result.stderr}`) + } + + return { + keyFileData: readFileSync(keyFile, "utf-8"), + certFileData: readFileSync(certFile, "utf-8") + } +} + +export function createMitmProxyWithMkcert(port: number) { + ensureMkcertCa() + + const proxy = new MitmProxy() + + proxy.onError((_ctx, err) => { + console.error("[PROXY ERROR]", err?.message || err) + }) + + // Use mkcert for cert generation instead of node-forge + proxy.onCertificateMissing = (ctx, _files, callback) => { + console.log(`[PROXY] Generating cert for ${ctx.hostname}`) + try { + const { certFileData, keyFileData } = generateCert(ctx.hostname) + callback(null, { keyFileData, certFileData }) + } catch (err) { + console.error(`[PROXY] Cert generation failed:`, err) + callback(err as Error) + } + } + + proxy.onRequest((ctx, callback) => { + const host = ctx.clientToProxyRequest.headers.host || "unknown" + const url = ctx.clientToProxyRequest.url || "/" + console.log(`[PROXY] → ${ctx.clientToProxyRequest.method} ${host}${url}`) + + // Inject header on outgoing request + if (ctx.proxyToServerRequestOptions?.headers) { + ctx.proxyToServerRequestOptions.headers[INJECTED_HEADER] = INJECTED_HEADER_VALUE + } + + // Track chunks for this specific request + let pendingChunk: Buffer | null = null + let shouldModifyResponse = false + + // Check content type to decide if we should modify response + ctx.onResponse((ctx, callback) => { + const contentType = ctx.serverToProxyResponse?.headers["content-type"] || "" + const contentEncoding = ctx.serverToProxyResponse?.headers["content-encoding"] + + // Only modify uncompressed text responses + shouldModifyResponse = !contentEncoding && + (contentType.includes("text/html") || + contentType.includes("text/plain") || + contentType.includes("application/json")) + + callback() + }) + + // Buffer chunks to append marker to the final one (only for text) + ctx.onResponseData((_ctx, chunk, callback) => { + if (!shouldModifyResponse) { + callback(null, chunk) + return + } + const toSend = pendingChunk + pendingChunk = chunk + callback(null, toSend ?? Buffer.alloc(0)) + }) + + ctx.onResponseEnd((ctx, callback) => { + if (shouldModifyResponse && pendingChunk) { + const withMarker = Buffer.concat([pendingChunk, Buffer.from(RESPONSE_MARKER)]) + ctx.proxyToClientResponse.write(withMarker) + } + callback() + }) + + callback() + }) + + proxy.onResponse((ctx, callback) => { + const status = ctx.serverToProxyResponse?.statusCode + console.log(`[PROXY] ← ${status}`) + callback() + }) + + return { + start: () => + new Promise((resolve, reject) => { + proxy.listen({ port }, (err: Error | null | undefined) => { + if (err) reject(err) + else resolve() + }) + }), + stop: () => + new Promise((resolve) => { + proxy.close() + resolve() + }), + port, + caPath: `${MKCERT_CAROOT}/rootCA.pem` + } +} + +// Run standalone if executed directly +if (import.meta.main) { + const PORT = Number(process.env.PROXY_PORT) || 8080 + const proxy = createMitmProxyWithMkcert(PORT) + + await proxy.start() + console.log(`MITM proxy (mkcert) listening on port ${PORT}`) + console.log(`CA cert: ${proxy.caPath}`) + console.log("") + console.log("Usage:") + console.log(` export https_proxy=http://localhost:${PORT}`) + console.log(` export SSL_CERT_FILE=${proxy.caPath} # for Go`) + console.log(` curl https://httpbin.org/headers`) + + process.on("SIGINT", async () => { + await proxy.stop() + process.exit(0) + }) +} diff --git a/test/mitm-proxy.test.ts b/test/mitm-proxy.test.ts new file mode 100644 index 0000000..d37880b --- /dev/null +++ b/test/mitm-proxy.test.ts @@ -0,0 +1,140 @@ +/** + * Integration tests for MITM proxy. + * + * Tests header injection and response modification across multiple HTTP clients. + */ + +import { execSync } from "node:child_process" +import { existsSync, readFileSync } from "node:fs" +import { ProxyAgent } from "undici" +import { afterAll, beforeAll, describe, expect, it } from "vitest" +import { createMitmProxy, INJECTED_HEADER, INJECTED_HEADER_VALUE, RESPONSE_MARKER } from "./mitm-proxy.ts" + +const PORT = 18080 +let proxy: ReturnType | null = null +let caCertPath: string + +describe("MITM Proxy", () => { + beforeAll(async () => { + proxy = createMitmProxy(PORT) + await proxy.start() + caCertPath = proxy.caPath + + // Wait for CA cert to be generated + let attempts = 0 + while (!existsSync(caCertPath) && attempts < 10) { + await new Promise((r) => setTimeout(r, 500)) + attempts++ + } + + if (!existsSync(caCertPath)) { + throw new Error(`CA cert not generated at ${caCertPath}`) + } + }, 30000) + + afterAll(async () => { + if (proxy) { + await proxy.stop() + } + }) + + describe("curl", () => { + it("injects header on outgoing request", () => { + const result = execSync( + `https_proxy=http://localhost:${PORT} curl -s --cacert "${caCertPath}" https://httpbin.org/headers`, + { encoding: "utf-8", timeout: 30000 } + ) + // Response includes our marker after JSON + const parts = result.split(RESPONSE_MARKER.trim()) + const json = parts[0] ?? "" + const data = JSON.parse(json) + expect(data.headers[INJECTED_HEADER]).toBe(INJECTED_HEADER_VALUE) + }) + + it("appends marker to text/html response", () => { + const result = execSync( + `https_proxy=http://localhost:${PORT} curl -s --cacert "${caCertPath}" https://httpbin.org/html`, + { encoding: "utf-8", timeout: 30000 } + ) + expect(result).toContain(RESPONSE_MARKER.trim()) + }) + }) + + describe("wget", () => { + it("injects header on outgoing request", () => { + const result = execSync( + `HTTPS_PROXY=http://localhost:${PORT} wget -q --ca-certificate="${caCertPath}" -O - https://httpbin.org/headers`, + { encoding: "utf-8", timeout: 30000 } + ) + const parts = result.split(RESPONSE_MARKER.trim()) + const json = parts[0] ?? "" + const data = JSON.parse(json) + expect(data.headers[INJECTED_HEADER]).toBe(INJECTED_HEADER_VALUE) + }) + }) + + describe("python urllib", () => { + it("injects header and modifies response", () => { + const result = execSync(`python3 test/mitm-scripts/test-python.py ${PORT} "${caCertPath}"`, { + encoding: "utf-8", + timeout: 30000 + }) + expect(result).toContain("Header injection working") + expect(result).toContain("Response modification working") + }) + }) + + describe("node/bun undici", () => { + it("injects header via ProxyAgent", async () => { + const ca = readFileSync(caCertPath) + const dispatcher = new ProxyAgent({ + uri: `http://localhost:${PORT}`, + requestTls: { ca } + }) + + const res = await fetch("https://httpbin.org/headers", { + dispatcher + } as unknown as RequestInit) + const text = await res.text() + const parts = text.split(RESPONSE_MARKER.trim()) + const json = parts[0] ?? "" + const data = JSON.parse(json) as { headers: Record } + expect(data.headers[INJECTED_HEADER]).toBe(INJECTED_HEADER_VALUE) + }) + + it("appends marker to response", async () => { + const ca = readFileSync(caCertPath) + const dispatcher = new ProxyAgent({ + uri: `http://localhost:${PORT}`, + requestTls: { ca } + }) + + const res = await fetch("https://httpbin.org/html", { + dispatcher + } as unknown as RequestInit) + const text = await res.text() + expect(text).toContain(RESPONSE_MARKER.trim()) + }) + }) + + describe("gzip responses", () => { + it("decompresses and modifies gzip responses", async () => { + const ca = readFileSync(caCertPath) + const dispatcher = new ProxyAgent({ + uri: `http://localhost:${PORT}`, + requestTls: { ca } + }) + + const res = await fetch("https://httpbin.org/gzip", { + dispatcher, + headers: { "Accept-Encoding": "gzip" } + } as unknown as RequestInit) + const text = await res.text() + + // Response was originally gzipped (check the gzipped field) + expect(text).toContain("\"gzipped\": true") + // Marker was appended after decompression + expect(text).toContain(RESPONSE_MARKER.trim()) + }) + }) +}) diff --git a/test/mitm-proxy.ts b/test/mitm-proxy.ts new file mode 100644 index 0000000..53711d2 --- /dev/null +++ b/test/mitm-proxy.ts @@ -0,0 +1,108 @@ +/** + * MITM proxy for testing HTTPS interception. + * + * Uses Proxy.gunzip to automatically handle compressed responses. + */ + +import { Proxy as MitmProxy } from "http-mitm-proxy" + +export const INJECTED_HEADER = "X-Proxy-Injected" +export const INJECTED_HEADER_VALUE = "mitm-active" +export const RESPONSE_MARKER = "\n" + +export function createMitmProxy(port: number) { + const proxy = new MitmProxy() + + proxy.onError((_ctx, err) => { + console.error("[PROXY ERROR]", err?.message || err) + }) + + proxy.onRequest((ctx, callback) => { + const host = ctx.clientToProxyRequest.headers.host || "unknown" + const url = ctx.clientToProxyRequest.url || "/" + console.log(`[PROXY] → ${ctx.clientToProxyRequest.method} ${host}${url}`) + + // Inject header on outgoing request + if (ctx.proxyToServerRequestOptions?.headers) { + ctx.proxyToServerRequestOptions.headers[INJECTED_HEADER] = INJECTED_HEADER_VALUE + } + + // Auto-decompress gzip/deflate responses, modify, then re-compress + ctx.use(MitmProxy.gunzip) + + // Check if we should modify this response + let shouldModify = false + ctx.onResponse((_ctx, callback) => { + const contentType = ctx.serverToProxyResponse?.headers["content-type"] || "" + shouldModify = contentType.includes("text/html") || + contentType.includes("text/plain") || + contentType.includes("application/json") + callback() + }) + + // Buffer to append marker at end + let pendingChunk: Buffer | null = null + + ctx.onResponseData((_ctx, chunk, callback) => { + if (!shouldModify) { + callback(null, chunk) + return + } + const toSend = pendingChunk + pendingChunk = chunk + callback(null, toSend ?? Buffer.alloc(0)) + }) + + ctx.onResponseEnd((ctx, callback) => { + if (shouldModify && pendingChunk) { + const withMarker = Buffer.concat([pendingChunk, Buffer.from(RESPONSE_MARKER)]) + ctx.proxyToClientResponse.write(withMarker) + } + callback() + }) + + callback() + }) + + proxy.onResponse((ctx, callback) => { + const status = ctx.serverToProxyResponse?.statusCode + console.log(`[PROXY] ← ${status}`) + callback() + }) + + return { + start: () => + new Promise((resolve, reject) => { + proxy.listen({ port }, (err: Error | null | undefined) => { + if (err) reject(err) + else resolve() + }) + }), + stop: () => + new Promise((resolve) => { + proxy.close() + resolve() + }), + port, + caPath: `${process.cwd()}/.http-mitm-proxy/certs/ca.pem` + } +} + +// Run standalone if executed directly +if (import.meta.main) { + const PORT = Number(process.env.PROXY_PORT) || 8080 + const proxy = createMitmProxy(PORT) + + await proxy.start() + console.log(`MITM proxy listening on port ${PORT}`) + console.log(`CA cert: ${proxy.caPath}`) + console.log("") + console.log("Usage:") + console.log(` export https_proxy=http://localhost:${PORT}`) + console.log(` curl https://httpbin.org/headers`) + + process.on("SIGINT", async () => { + await proxy.stop() + process.exit(0) + }) +} diff --git a/test/mitm-scripts/test-curl.sh b/test/mitm-scripts/test-curl.sh new file mode 100755 index 0000000..588caaf --- /dev/null +++ b/test/mitm-scripts/test-curl.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Test MITM proxy with curl +# Args: $1 = proxy port, $2 = CA cert path + +set -e + +PROXY_PORT="${1:-8080}" +CA_CERT="${2:-.http-mitm-proxy/certs/ca.pem}" + +# Test 1: Check injected header via httpbin +RESPONSE=$(https_proxy="http://localhost:$PROXY_PORT" \ + curl -s --cacert "$CA_CERT" https://httpbin.org/headers) + +if echo "$RESPONSE" | grep -q "X-Proxy-Injected"; then + echo "✓ Header injection working" +else + echo "✗ Header injection failed" + echo "$RESPONSE" + exit 1 +fi + +# Test 2: Check response body modification +RESPONSE=$(https_proxy="http://localhost:$PROXY_PORT" \ + curl -s --cacert "$CA_CERT" https://example.com) + +if echo "$RESPONSE" | grep -q "MITM_PROXY_MARKER"; then + echo "✓ Response modification working" +else + echo "✗ Response modification failed" + exit 1 +fi + +echo "curl tests passed" diff --git a/test/mitm-scripts/test-node.ts b/test/mitm-scripts/test-node.ts new file mode 100644 index 0000000..b74ce74 --- /dev/null +++ b/test/mitm-scripts/test-node.ts @@ -0,0 +1,48 @@ +#!/usr/bin/env bun +/** + * Test MITM proxy with Node/Bun fetch. + * Uses undici ProxyAgent for explicit proxy control. + */ + +import { readFileSync } from "node:fs" +import { ProxyAgent } from "undici" + +const proxyPort = process.argv[2] || "8080" +const caCert = process.argv[3] || ".http-mitm-proxy/certs/ca.pem" + +const ca = readFileSync(caCert) + +// Create proxy agent with custom CA +const dispatcher = new ProxyAgent({ + uri: `http://localhost:${proxyPort}`, + requestTls: { ca } +}) + +// Test 1: Header injection +const headersRes = await fetch("https://httpbin.org/headers", { + dispatcher +} as unknown as RequestInit) +const headersData = (await headersRes.json()) as { headers: Record } + +if (headersData.headers["X-Proxy-Injected"]) { + console.log("✓ Header injection working") +} else { + console.log("✗ Header injection failed") + console.log(headersData) + process.exit(1) +} + +// Test 2: Response modification +const exampleRes = await fetch("https://example.com", { + dispatcher +} as unknown as RequestInit) +const body = await exampleRes.text() + +if (body.includes("MITM_PROXY_MARKER")) { + console.log("✓ Response modification working") +} else { + console.log("✗ Response modification failed") + process.exit(1) +} + +console.log("Node/Bun tests passed") diff --git a/test/mitm-scripts/test-python.py b/test/mitm-scripts/test-python.py new file mode 100755 index 0000000..32cf2fe --- /dev/null +++ b/test/mitm-scripts/test-python.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Test MITM proxy with Python urllib.""" + +import json +import ssl +import sys +import urllib.request + + +def main(): + proxy_port = sys.argv[1] if len(sys.argv) > 1 else "8080" + ca_cert = sys.argv[2] if len(sys.argv) > 2 else ".http-mitm-proxy/certs/ca.pem" + + # Set up proxy and SSL context + proxy_handler = urllib.request.ProxyHandler( + {"https": f"http://localhost:{proxy_port}"} + ) + ctx = ssl.create_default_context() + ctx.load_verify_locations(ca_cert) + https_handler = urllib.request.HTTPSHandler(context=ctx) + opener = urllib.request.build_opener(proxy_handler, https_handler) + + # Test 1: Header injection + response = opener.open("https://httpbin.org/headers") + data = json.loads(response.read().decode()) + + if "X-Proxy-Injected" in data.get("headers", {}): + print("✓ Header injection working") + else: + print("✗ Header injection failed") + print(data) + sys.exit(1) + + # Test 2: Response modification + response = opener.open("https://example.com") + body = response.read().decode() + + if "MITM_PROXY_MARKER" in body: + print("✓ Response modification working") + else: + print("✗ Response modification failed") + sys.exit(1) + + print("Python tests passed") + + +if __name__ == "__main__": + main()