mitmproxy抓包配置与自动化下载
前端开发时经常需要抓包分析静态资源,mitmproxy是个强大的代理工具,配合自定义Python脚本可以自动化下载。这篇文章分享我的实战配置,包括HTTPS证书设置和自动抓包脚本。
前言
最近在做前端性能优化,需要分析一些网站的静态资源加载情况。
试过几个抓包工具,Burp Suite 太重,Wireshark 对 HTTP 分析不够直观。后来发现 mitmproxy 这个神器,Python 写的,可编程性特别强,配合自己写的下载脚本,基本能覆盖所有抓包需求。
今天把配置和使用经验整理一下,重点是 HTTPS 证书配置和自动化脚本编写。
mitmproxy 简介
什么是 mitmproxy
mitmproxy 是一个 Python 编写的交互式 HTTPS 代理工具,支持 HTTP/HTTPS/HTTP2 协议。
核心特性:
- 交互式界面:命令行界面直观易用
- Python 脚本:支持自定义脚本进行流量处理
- 证书管理:自动生成和管理 HTTPS 证书
- 流量录制:可以保存和回放 HTTP 流量
- API 支持:提供 Web 界面和 REST API
主要组件
- mitmproxy:交互式命令行界面
- mitmdump:命令行版本,适合脚本自动化
- mitmweb:Web 界面版本
安装配置
安装 mitmproxy
Python pip 安装
# 推荐使用pip安装
pip install mitmproxy
# 或者使用pip3
pip3 install mitmproxy
# 验证安装
mitmproxy --version
其他安装方式
# macOS使用Homebrew
brew install mitmproxy
# Ubuntu/Debian
sudo apt-get install mitmproxy
# 从源码安装
git clone https://github.com/mitmproxy/mitmproxy.git
cd mitmproxy
pip install -e .
证书配置
生成证书
# 启动mitmproxy,自动生成证书
mitmproxy
# 证书文件位置(根据系统不同)
# Windows: %USERPROFILE%\.mitmproxy
# Linux/Mac: ~/.mitmproxy
证书文件说明:
mitmproxy-ca-cert.pem:根证书(需要安装到系统)mitmproxy-ca-cert.p12:PKCS12 格式证书mitmproxy-ca.pem:私钥文件
系统证书安装
Windows 系统
- 下载证书:
# 启动mitmproxy后,浏览器访问
http://mitm.it
# 下载对应系统的证书
- 安装证书:
- 双击下载的证书文件
- 选择"本地计算机"→"将所有证书放入下列存储"→"受信任的根证书颁发机构"
- 完成安装
macOS 系统
# 添加到钥匙串
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem
Linux 系统
# Ubuntu/Debian
sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt
sudo update-ca-certificates
# CentOS/RHEL
sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
基本使用
启动代理
交互式界面
# 默认启动(端口8080)
mitmproxy
# 指定端口
mitmproxy -p 8888
# 绑定特定接口
mitmproxy --listen-host 0.0.0.0 -p 8080
Web 界面
# 启动Web界面
mitmweb
# 指定端口
mitmweb -p 8080 --web-port 8081
# 浏览器访问
http://localhost:8081
命令行模式
# 静默运行,适合脚本
mitmdump -p 8080
# 保存流量到文件
mitmdump -w capture.flow
# 从文件读取流量
mitmdump -r capture.flow
客户端配置
浏览器代理设置
Chrome 代理启动:
# Windows
chrome.exe --proxy-server=http://127.0.0.1:8080
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --proxy-server=http://127.0.0.1:8080
# Linux
google-chrome --proxy-server=http://127.0.0.1:8080
Firefox 代理设置:
- 设置 → 网络设置 → 手动代理配置
- HTTP 代理:127.0.0.1,端口:8080
- 勾选"对所有协议使用相同代理"
系统代理设置
Windows:
- 设置 → 网络和 Internet→ 代理
- 手动设置代理:127.0.0.1:8080
macOS:
- 系统偏好设置 → 网络 → 高级 → 代理
- 勾选"网页代理(HTTP)"和"安全网页代理(HTTPS)"
- 服务器:127.0.0.1,端口:8080
自动化脚本编写
Python 脚本基础
基本脚本结构
# download_script.py
from mitmproxy import http
import os
import requests
from urllib.parse import urlparse
class ResourceDownloader:
def __init__(self):
self.download_dir = "./downloads"
if not os.path.exists(self.download_dir):
os.makedirs(self.download_dir)
def request(self, flow: http.HTTPFlow) -> None:
"""处理请求"""
pass
def response(self, flow: http.HTTPFlow) -> None:
"""处理响应"""
url = flow.request.pretty_url
# 过滤静态资源
if self.is_static_resource(url):
self.download_resource(flow)
def is_static_resource(self, url):
"""判断是否为静态资源"""
static_extensions = [
'.css', '.js', '.png', '.jpg', '.jpeg',
'.gif', '.svg', '.ico', '.woff', '.woff2',
'.ttf', '.eot', '.mp4', '.webm', '.mp3'
]
parsed_url = urlparse(url)
path = parsed_url.path.lower()
return any(path.endswith(ext) for ext in static_extensions)
def download_resource(self, flow):
"""下载资源文件"""
try:
url = flow.request.pretty_url
parsed_url = urlparse(url)
# 生成本地文件名
filename = os.path.basename(parsed_url.path)
if not filename:
filename = "index.html"
# 处理文件名冲突
local_path = os.path.join(self.download_dir, filename)
counter = 1
while os.path.exists(local_path):
name, ext = os.path.splitext(filename)
local_path = os.path.join(self.download_dir, f"{name}_{counter}{ext}")
counter += 1
# 保存文件
with open(local_path, 'wb') as f:
f.write(flow.response.content)
print(f"Downloaded: {url} -> {local_path}")
except Exception as e:
print(f"Download failed: {url}, Error: {e}")
# 创建插件实例
addons = [ResourceDownloader()]
运行脚本
# 使用脚本启动mitmproxy
mitmdump -s download_script.py -p 8080
# 或者使用交互式界面
mitmproxy -s download_script.py
高级功能脚本
域名过滤脚本
# domain_filter.py
from mitmproxy import http
import re
class DomainFilter:
def __init__(self):
# 定义目标域名列表
self.target_domains = [
'cdn.example.com',
'static.example.com',
'assets.example.com'
]
# 定义文件类型过滤
self.file_patterns = [
r'\.(css|js|png|jpg|jpeg|gif|svg|ico)$',
r'/assets/',
r'/static/',
r'/css/',
r'/js/'
]
def request(self, flow: http.HTTPFlow) -> None:
url = flow.request.pretty_url
# 检查域名
if not self.is_target_domain(url):
return
# 检查文件类型
if self.is_target_file(url):
print(f"Target request: {url}")
def response(self, flow: http.HTTPFlow) -> None:
url = flow.request.pretty_url
if self.is_target_domain(url) and self.is_target_file(url):
self.save_resource(flow)
def is_target_domain(self, url):
"""检查是否为目标域名"""
for domain in self.target_domains:
if domain in url:
return True
return False
def is_target_file(self, url):
"""检查是否为目标文件类型"""
for pattern in self.file_patterns:
if re.search(pattern, url, re.IGNORECASE):
return True
return False
def save_resource(self, flow):
"""保存资源"""
# 保存逻辑...
pass
addons = [DomainFilter()]
请求修改脚本
# request_modifier.py
from mitmproxy import http
class RequestModifier:
def request(self, flow: http.HTTPFlow) -> None:
# 修改User-Agent
flow.request.headers["User-Agent"] = "Custom-Bot/1.0"
# 添加自定义Header
flow.request.headers["X-Custom-Header"] = "mitmproxy"
# 修改请求参数
if "api/" in flow.request.pretty_url:
# 可以修改查询参数
# flow.request.query["debug"] = "true"
pass
def response(self, flow: http.HTTPFlow) -> None:
# 修改响应头
flow.response.headers["X-Proxied-By"] = "mitmproxy"
# 记录API响应
if "api/" in flow.request.pretty_url:
print(f"API Response: {flow.request.pretty_url}")
print(f"Status: {flow.response.status_code}")
print(f"Content-Length: {len(flow.response.content)}")
addons = [RequestModifier()]
实战案例
网站静态资源分析
目标网站分析脚本
# site_analyzer.py
from mitmproxy import http
import json
import os
from collections import defaultdict
from urllib.parse import urlparse
class SiteAnalyzer:
def __init__(self):
self.resources = defaultdict(list)
self.stats = {
'total_requests': 0,
'static_resources': 0,
'api_calls': 0,
'domains': set(),
'file_types': defaultdict(int)
}
def request(self, flow: http.HTTPFlow) -> None:
self.stats['total_requests'] += 1
url = flow.request.pretty_url
parsed_url = urlparse(url)
# 记录域名
self.stats['domains'].add(parsed_url.netloc)
# 分析文件类型
path = parsed_url.path.lower()
if '.' in path:
ext = path.split('.')[-1]
self.stats['file_types'][ext] += 1
# 分类请求类型
if self.is_static_resource(url):
self.stats['static_resources'] += 1
elif 'api/' in url or path.endswith('.json'):
self.stats['api_calls'] += 1
def response(self, flow: http.HTTPFlow) -> None:
url = flow.request.pretty_url
# 记录资源信息
resource_info = {
'url': url,
'method': flow.request.method,
'status_code': flow.response.status_code,
'content_type': flow.response.headers.get('content-type', ''),
'content_length': len(flow.response.content),
'response_time': flow.response.timestamp_end - flow.request.timestamp_start
}
parsed_url = urlparse(url)
domain = parsed_url.netloc
self.resources[domain].append(resource_info)
def is_static_resource(self, url):
static_extensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico']
return any(url.lower().endswith(ext) for ext in static_extensions)
def save_report(self):
"""保存分析报告"""
report = {
'statistics': {
'total_requests': self.stats['total_requests'],
'static_resources': self.stats['static_resources'],
'api_calls': self.stats['api_calls'],
'unique_domains': len(self.stats['domains']),
'domains': list(self.stats['domains']),
'file_types': dict(self.stats['file_types'])
},
'resources': dict(self.resources)
}
with open('site_analysis.json', 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"Analysis saved to site_analysis.json")
print(f"Total requests: {self.stats['total_requests']}")
print(f"Static resources: {self.stats['static_resources']}")
print(f"API calls: {self.stats['api_calls']}")
print(f"Unique domains: {len(self.stats['domains'])}")
analyzer = SiteAnalyzer()
# 在脚本结束时保存报告
def done():
analyzer.save_report()
addons = [analyzer]
批量下载脚本
# batch_downloader.py
from mitmproxy import http
import os
import hashlib
from urllib.parse import urlparse, unquote
class BatchDownloader:
def __init__(self):
self.base_dir = "./site_resources"
self.downloaded_hashes = set()
# 创建目录结构
self.ensure_dir(self.base_dir)
def ensure_dir(self, path):
"""确保目录存在"""
if not os.path.exists(path):
os.makedirs(path)
def response(self, flow: http.HTTPFlow) -> None:
url = flow.request.pretty_url
# 只处理成功的响应
if flow.response.status_code != 200:
return
# 过滤资源类型
if not self.should_download(url, flow.response):
return
# 避免重复下载
content_hash = hashlib.md5(flow.response.content).hexdigest()
if content_hash in self.downloaded_hashes:
return
self.downloaded_hashes.add(content_hash)
self.save_file(flow, content_hash)
def should_download(self, url, response):
"""判断是否应该下载"""
content_type = response.headers.get('content-type', '').lower()
# 根据Content-Type过滤
download_types = [
'text/css',
'application/javascript',
'text/javascript',
'image/',
'font/',
'application/font'
]
for dtype in download_types:
if dtype in content_type:
return True
# 根据URL扩展名过滤
static_extensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf']
return any(url.lower().endswith(ext) for ext in static_extensions)
def save_file(self, flow, content_hash):
"""保存文件"""
try:
url = flow.request.pretty_url
parsed_url = urlparse(url)
# 创建域名目录
domain_dir = os.path.join(self.base_dir, parsed_url.netloc)
self.ensure_dir(domain_dir)
# 生成文件路径
path_parts = [p for p in parsed_url.path.split('/') if p]
if path_parts:
# 创建目录结构
if len(path_parts) > 1:
sub_dir = os.path.join(domain_dir, *path_parts[:-1])
self.ensure_dir(sub_dir)
file_path = os.path.join(sub_dir, path_parts[-1])
else:
file_path = os.path.join(domain_dir, path_parts[0])
else:
file_path = os.path.join(domain_dir, 'index.html')
# URL解码文件名
file_path = unquote(file_path)
# 处理文件名冲突
if os.path.exists(file_path):
name, ext = os.path.splitext(file_path)
file_path = f"{name}_{content_hash[:8]}{ext}"
# 保存文件
with open(file_path, 'wb') as f:
f.write(flow.response.content)
print(f"Saved: {url} -> {file_path}")
except Exception as e:
print(f"Save failed: {url}, Error: {e}")
addons = [BatchDownloader()]
高级技巧
透明代理模式
# 透明代理模式(需要root权限)
sudo mitmproxy --mode transparent --listen-port 8080
# 配置iptables重定向(Linux)
sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 8080
上游代理配置
# 通过上游代理
mitmproxy --upstream-proxy http://proxy.company.com:8080
# SOCKS代理
mitmproxy --upstream-proxy socks5://127.0.0.1:1080
性能优化
# 增加并发连接数
mitmproxy --set connection_strategy=lazy
# 调整缓冲区大小
mitmproxy --set body_size_limit=50m
# 禁用某些功能提高性能
mitmproxy --set stream_large_bodies=1m
故障排除
常见问题
证书问题
# 重新生成证书
rm -rf ~/.mitmproxy
mitmproxy # 重新生成
# 检查证书是否正确安装
openssl x509 -in ~/.mitmproxy/mitmproxy-ca-cert.pem -text -noout
权限问题
# 确保端口可用
sudo lsof -i :8080
# 使用高端口避免权限问题
mitmproxy -p 18080
脚本调试
# 在脚本中添加调试输出
def request(flow):
print(f"Request: {flow.request.method} {flow.request.pretty_url}")
print(f"Headers: {flow.request.headers}")
def response(flow):
print(f"Response: {flow.response.status_code} {len(flow.response.content)} bytes")
小结
mitmproxy 是一个功能强大的 HTTP 代理工具,特别适合 Web 开发和测试场景。
核心优势:
- Python 脚本支持:高度可定制化
- 证书管理:自动处理 HTTPS
- 多种界面:命令行、Web、交互式
- 流量分析:详细的请求响应信息
实用技巧:
- 合理配置过滤条件,避免下载无用文件
- 使用域名和文件类型过滤提高效率
- 自动化脚本可以大大提高工作效率
- 注意处理文件名冲突和特殊字符
配合自定义 Python 脚本,mitmproxy 几乎可以满足所有 HTTP 代理和分析需求。无论是性能测试、安全分析还是资源下载,都是很好的选择。
