mitmproxy技术博客




2020-01-13

blog_main_img

抓包工具很多,mitmproxy 的气质比较特别:它既能像传统代理一样看请求响应,又能用 Python 写脚本,把“看一眼”升级成“自动处理一批”。

如果你在调试自己的 Web 服务、移动 App、接口网关、爬虫测试环境,mitmproxy 很适合放在工具箱里。它能帮你看清楚请求到底发了什么、响应到底回了什么,也能在授权环境里做 Mock、过滤、重放、记录。

本文只讨论自有系统、测试环境、授权设备里的调试用法。不要把代理工具用于未授权流量截获、凭据窃取或绕过第三方防护。

mitmproxy 到底站在哪

mitmproxy 的位置很简单:客户端和服务端之间。

Client  ->  mitmproxy  ->  Server
Client  <-  mitmproxy  <-  Server

客户端把 HTTP 或 HTTPS 请求交给 mitmproxy,mitmproxy 再把请求转发给目标服务。响应回来时,也会先经过 mitmproxy,再回到客户端。

这样你就能看到完整的请求头、请求体、响应状态、响应头和响应体。更好玩的是,mitmproxy 不只是“展示流量”,它还支持拦截、修改、保存、重放和脚本化处理。

mitmproxy 代理链路

三个工具入口:别一上来就纠结

mitmproxy 家族里常见三个命令:

mitmproxy
mitmweb
mitmdump

mitmproxy 是终端交互界面,适合在命令行里边看边筛。

mitmweb 带 Web 界面,适合喜欢浏览器操作的人,查看请求响应更直观。

mitmdump 偏自动化,适合跑 Python addon、保存流量、接入脚本任务。

初学建议从 mitmweb 开始,看得清楚;写脚本时切到 mitmdump,更干净。

安装和启动

如果你用 Python 环境,官方支持通过 PyPI 安装:

pip install mitmproxy

也可以用系统包管理器或官方镜像。具体取决于你的开发环境和团队部署方式。

启动一个最普通的代理:

mitmweb --listen-host 127.0.0.1 --listen-port 8080

或者使用终端界面:

mitmproxy --listen-host 127.0.0.1 --listen-port 8080

然后在浏览器、App、脚本或测试客户端里,把 HTTP 代理设置为:

127.0.0.1:8080

这就是 Regular 模式,也就是最直接、最稳的入口。

HTTPS 为什么要装证书

HTTP 是明文,mitmproxy 能直接看。

HTTPS 多了一层 TLS。客户端会验证服务端证书,如果 mitmproxy 想展示 HTTPS 内容,就需要让客户端信任 mitmproxy 生成的本地 CA 证书。

常见流程是:

启动 mitmproxy
配置客户端代理
访问 mitm.it
按客户端系统安装证书
重新发起请求

证书只应该安装在自己的测试设备、开发机或授权环境里。调试结束后,如果不再需要,可以把代理配置和证书信任清理掉。

这里也要补一句:有些 App 会做证书绑定,这属于应用自己的安全设计。本文不讨论绕过证书绑定,只讨论你有权限调试的普通代理链路。

常用模式怎么选

mitmproxy 支持多种代理模式,但日常调试不需要一口气全背下来。

mitmproxy 模式速查

Regular 模式:

mitmproxy --mode regular

客户端显式配置代理,最适合浏览器、脚本、可控 App 调试。

Reverse 模式:

mitmproxy --mode reverse:http://127.0.0.1:9000 --listen-port 8080

它把 mitmproxy 放在服务前面。客户端访问 8080,mitmproxy 再转发到 9000。本地服务联调、接口 Mock、网关行为观察都挺顺手。

Upstream 模式:

mitmproxy --mode upstream:http://proxy.example:8080

它会把流量继续交给上游代理。公司内网、测试网关链路里可能用得上。

透明代理、WireGuard、Local Capture 等模式更适合特殊网络场景。能用 Regular 就先用 Regular,排查成本低很多。

过滤流量:别被请求列表淹没

接口一多,流量列表会很吵。mitmproxy 支持过滤表达式,可以只看你关心的东西。

比如只看某个域名:

~d api.example.com

只看响应状态为成功的请求:

~c 200

只看 URL 中包含某段路径的请求:

~u /api/order

只看 POST 请求:

~m POST

这些过滤表达式在交互界面和脚本里都很实用。调试接口时,先过滤再分析,效率会高很多。

保存和回放流量

如果你想把一次调试过程保存下来,可以用:

mitmdump -w flows.mitm

读取保存的流量:

mitmdump -r flows.mitm

这适合做问题复盘、接口变更对比、回归测试样本沉淀。

也可以在界面里选中某条请求进行重放。比如某个接口偶发失败,你可以改参数、改 Header,再重发看看服务端如何响应。

Python Addon:mitmproxy 的精华区

mitmproxy 的 addon 机制允许你用 Python 写钩子函数。请求经过时触发 request,响应回来时触发 response

mitmproxy Python addon 流程

先写一个最小脚本 logger.py

from mitmproxy import http


def request(flow: http.HTTPFlow) -> None:
    print("REQ", flow.request.method, flow.request.pretty_url)


def response(flow: http.HTTPFlow) -> None:
    print("RESP", flow.response.status_code, flow.request.pretty_url)

运行:

mitmdump -s logger.py

只要流量经过,就会打印请求方法、URL 和响应状态码。

给请求加一个调试 Header

本地联调时,你可能希望所有请求都带上一个标记,方便服务端日志筛选。

from mitmproxy import http


def request(flow: http.HTTPFlow) -> None:
    if flow.request.host == "api.example.com":
        flow.request.headers["X-Debug-Source"] = "mitmproxy"

这类脚本只适合自己的服务或授权测试环境。不要给第三方服务乱加业务 Header,结果不可控,也不礼貌。

修改响应:适合 Mock 和前端联调

前端等后端接口时,mitmproxy 可以临时改响应体,做一个轻量 Mock。

import json
from mitmproxy import http


def response(flow: http.HTTPFlow) -> None:
    if flow.request.path == "/api/user/profile":
        data = {
            "id": 1001,
            "name": "debug-user",
            "role": "tester",
            "vip": True,
        }

        flow.response = http.Response.make(
            200,
            json.dumps(data, ensure_ascii=False),
            {"Content-Type": "application/json; charset=utf-8"},
        )

这个脚本的意思是:只要路径命中 /api/user/profile,就直接返回一段测试 JSON,不再使用真实响应。

这招很适合前端页面联调、异常分支验证、灰度字段验证。

记录 JSON 接口到本地文件

如果你想把某些接口响应沉淀成样本,可以在 addon 里写文件。

import json
from pathlib import Path
from mitmproxy import http


OUTPUT_DIR = Path("captured_json")
OUTPUT_DIR.mkdir(exist_ok=True)


def response(flow: http.HTTPFlow) -> None:
    content_type = flow.response.headers.get("content-type", "")

    if "application/json" not in content_type:
        return

    if not flow.request.path.startswith("/api/"):
        return

    body = flow.response.get_text(strict=False)

    try:
        parsed = json.loads(body)
    except json.JSONDecodeError:
        return

    safe_name = flow.request.path.strip("/").replace("/", "_") or "root"
    target = OUTPUT_DIR / f"{safe_name}.json"
    target.write_text(
        json.dumps(parsed, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

这比手动复制接口响应舒服很多。调试一轮下来,样本文件自然就攒好了。

按规则拦截请求

有些请求在调试时不想真的发出去,比如埋点、上传、某些副作用接口。可以直接在请求阶段返回响应。

from mitmproxy import http


def request(flow: http.HTTPFlow) -> None:
    blocked_paths = {
        "/api/event/track",
        "/api/debug/upload",
    }

    if flow.request.path in blocked_paths:
        flow.response = http.Response.make(
            204,
            b"",
            {"X-Blocked-By": "mitmproxy"},
        )

这样客户端会收到一个空响应,真实服务不会接到这条请求。

一个稍微完整点的 Addon 类

函数式脚本很轻,但业务复杂后,用类会更清爽。

from mitmproxy import ctx, http


class ApiInspector:
    def load(self, loader):
        loader.add_option(
            name="target_host",
            typespec=str,
            default="api.example.com",
            help="只处理指定 host 的请求",
        )

    def request(self, flow: http.HTTPFlow) -> None:
        if flow.request.host != ctx.options.target_host:
            return

        flow.request.headers["X-Trace-From"] = "mitmproxy-addon"
        ctx.log.info(f"request {flow.request.method} {flow.request.path}")

    def response(self, flow: http.HTTPFlow) -> None:
        if flow.request.host != ctx.options.target_host:
            return

        flow.response.headers["X-Checked-By"] = "mitmproxy-addon"
        ctx.log.info(f"response {flow.response.status_code} {flow.request.path}")


addons = [ApiInspector()]

运行时传入配置:

mitmdump -s inspector.py --set target_host=api.example.com

这就是 mitmproxy addon 的舒服之处:代理工具变成了一个可编排的 Python 运行环境。

用 Python 请求库配合 mitmproxy

如果你调试的是 Python 脚本,可以直接给 requests 配代理。

import requests


proxies = {
    "http": "http://127.0.0.1:8080",
    "https": "http://127.0.0.1:8080",
}

resp = requests.get(
    "https://example.com/api/ping",
    proxies=proxies,
    timeout=10,
)

print(resp.status_code)
print(resp.text[:200])

如果 HTTPS 证书还没配好,requests 可能会报证书校验错误。开发测试里可以把 mitmproxy 的 CA 证书路径交给 verify

resp = requests.get(
    "https://example.com/api/ping",
    proxies=proxies,
    verify="/path/to/mitmproxy-ca-cert.pem",
)

不建议为了省事长期关闭证书校验。证书校验本来就是 HTTPS 安全链路的一部分,调试完也要把配置收回来。

常见坑:看起来没流量,不一定是 mitmproxy 坏了

客户端没配置代理。

这是最常见的。先确认浏览器、系统代理、脚本代理参数到底有没有指向 mitmproxy。

证书没安装或没信任。

HTTP 能看,HTTPS 报错,多半要检查 CA 证书信任。

代理监听地址不对。

本机调试用 127.0.0.1 没问题;手机连电脑代理时,需要监听局域网地址,比如:

mitmweb --listen-host 0.0.0.0 --listen-port 8080

同时要确认防火墙允许访问。

客户端自己绕开了系统代理。

有些 SDK、App 或运行环境不走系统代理,需要在应用内部配置代理,或者换适合的捕获模式。

响应内容是压缩的。

mitmproxy 通常能处理常见压缩,但脚本里读写响应体时,要注意 Content-Encoding 和内容类型。

适合放进团队流程里的用法

mitmproxy 不只是临时抓包工具,也可以变成团队里的调试基建。

比如:

  • 前端联调时统一 Mock 某些接口
  • 测试同学保存关键接口样本
  • 后端排查 Header、Cookie、参数是否符合预期
  • 网关变更前后对比请求响应
  • 自动化任务里记录异常响应片段

它最大的价值不是“我能看到流量”,而是“我能把流量变成可处理的数据”。

小结

mitmproxy 的核心能力可以概括成三句话:

把客户端流量导进来
把请求响应看清楚
用 Python 把规则跑起来

入门时,用 mitmweb + Regular 模式 看清楚链路;写脚本时,用 mitmdump -s addon.py 自动处理;服务端联调时,再考虑 Reverse 模式。

只要边界守住,它就是一个非常顺手的 HTTP 调试工具:能看、能改、能存、能重放,还能用 Python 接管复杂规则。

参考资料: