Created
November 5, 2025 22:00
-
-
Save s1037989/59f56e0370db19665232f9824e515fbe to your computer and use it in GitHub Desktop.
mitmdump -s save.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # save.py | |
| # mitmdump -s save.py | |
| """ | |
| mitmproxy addon: save each response body to a separate file and print only | |
| metadata + response headers (no bodies). | |
| Filename format: | |
| <ct_main_sanitized>__<url_escaped>__<timestamp>.response | |
| Example printed output block: | |
| ================================================================================ | |
| # captured: 2025-11-05T12:34:56-0600 | |
| # request: GET https://example.com/path?q=1 | |
| # filename: application_json__https%3A%2F%2Fexample.com%2Fpath%3Fq%3D1__170- | |
| HTTP/1.1 200 OK | |
| Content-Type: application/json; charset=utf-8 | |
| Content-Length: 123 | |
| ... | |
| ================================================================================ | |
| """ | |
| from mitmproxy import http | |
| import os | |
| import time | |
| import sys | |
| from urllib.parse import quote_plus | |
| OUTDIR = "responses" | |
| os.makedirs(OUTDIR, exist_ok=True) | |
| def _ct_main_sanitized(ct_header: str) -> str: | |
| """ | |
| Return a filesystem-safe main content-type string derived from Content-Type header. | |
| Examples: | |
| "application/json; charset=utf-8" -> "application_json" | |
| "text/html" -> "text_html" | |
| "" or None -> "unknown" | |
| """ | |
| if not ct_header: | |
| return "unknown" | |
| main = ct_header.split(";", 1)[0].strip().lower() | |
| if not main: | |
| return "unknown" | |
| # replace problematic characters with underscores | |
| sanitized = main.replace("/", "_").replace("+", "_").replace(" ", "_") | |
| # also collapse any remaining characters that might be unsafe | |
| return "".join(ch for ch in sanitized if ch.isalnum() or ch in ("_", "-")) | |
| def _make_filename(content_type_header: str, url: str, ts: int) -> str: | |
| ct_s = _ct_main_sanitized(content_type_header) | |
| # url-escape the absolute request url | |
| url_esc = quote_plus(url, safe="") | |
| # optional: limit very long escaped urls to keep filenames reasonable | |
| # (trim the middle if too long) | |
| MAX_URL_ESC_LEN = 512 | |
| if len(url_esc) > MAX_URL_ESC_LEN: | |
| # keep head and tail | |
| head = url_esc[:200] | |
| tail = url_esc[-300:] | |
| url_esc = head + "__TRUNC__" + tail | |
| fname = f"{ct_s}__{url_esc}__{ts}.response" | |
| # make sure filename isn't absurdly long for filesystems | |
| if len(fname) > 255: | |
| fname = fname[:240] + "__cut__.response" | |
| return fname | |
| def _print_headers_only(flow: http.HTTPFlow, filename: str) -> None: | |
| r = flow.response | |
| req = flow.request | |
| ts = time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime()) | |
| sep = "=" * 80 | |
| # status line (include http_version when available) | |
| http_version = getattr(r, "http_version", "1.1") | |
| status_line = f"HTTP/{http_version} {r.status_code} {r.reason or ''}".strip() | |
| out_lines = [ | |
| sep, | |
| f"# captured: {ts}", | |
| f"# request: {req.method} {req.url}", | |
| f"# filename: {filename}", | |
| status_line | |
| ] | |
| # response headers | |
| for k, v in r.headers.items(): | |
| out_lines.append(f"{k}: {v}") | |
| out_lines.append(sep + "\n") | |
| sys.stdout.write("\n".join(out_lines)) | |
| sys.stdout.flush() | |
| def response(flow: http.HTTPFlow) -> None: | |
| """ | |
| mitmproxy response hook: | |
| - save response.content (decompressed bytes) to file | |
| - print metadata and response headers only | |
| """ | |
| try: | |
| req = flow.request | |
| r = flow.response | |
| # timestamp in seconds | |
| ts = int(time.time()) | |
| # compute filename | |
| ct_header = r.headers.get("content-type", "") | |
| fname = _make_filename(ct_header, req.url, ts) | |
| path = os.path.join(OUTDIR, fname) | |
| # write raw uncompressed bytes (mitmproxy exposes decompressed bytes via .content) | |
| body_bytes = r.content if r.content is not None else b"" | |
| try: | |
| with open(path, "wb") as fh: | |
| fh.write(body_bytes) | |
| except Exception as e: | |
| # still print headers but mark file write error | |
| _print_headers_only(flow, f"[error writing file: {e}]") | |
| return | |
| # print metadata + headers (no body) | |
| _print_headers_only(flow, fname) | |
| except Exception as exc: | |
| # never crash mitmdump; write error to stderr and continue | |
| sys.stderr.write(f"[save_headers_and_bodies.py error] {exc}\n") | |
| sys.stderr.flush() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment