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 系统

  1. 下载证书:
# 启动mitmproxy后,浏览器访问
http://mitm.it
# 下载对应系统的证书
​
  1. 安装证书:
    • 双击下载的证书文件
    • 选择"本地计算机"→"将所有证书放入下列存储"→"受信任的根证书颁发机构"
    • 完成安装

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 代理和分析需求。无论是性能测试、安全分析还是资源下载,都是很好的选择。

更多推荐

章节目录