Skip to content

Instantly share code, notes, and snippets.

@s1037989
Created November 5, 2025 22:00
Show Gist options
  • Select an option

  • Save s1037989/59f56e0370db19665232f9824e515fbe to your computer and use it in GitHub Desktop.

Select an option

Save s1037989/59f56e0370db19665232f9824e515fbe to your computer and use it in GitHub Desktop.
mitmdump -s save.py
# 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