mitmproxy 自定义命令(Commands)完全指南:编写类型安全的 Addon 命令并绑定到快捷键
【免费下载链接】mitmproxyAn interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers.项目地址: https://gitcode.com/GitHub_Trending/mi/mitmproxy
本篇技术指南基于 mitmproxy 官方文档 Custom Commands,讲解如何通过@command.command装饰器为 mitmproxy 编写带类型标注的自定义命令,涵盖命令声明、基于 flow 选择器的批量操作、路径参数处理、完整的支持类型清单,并结合 mitmproxy/command.py 与 mitmproxy/types.py 源码剖析命令的注册、参数解析与运行时类型校验机制,读完后可独立开发可绑定到 mitmproxy console 快捷键的 addon 命令。
命令是什么:类型化的用户交互机制
Commands 让用户能够主动与 addon 交互——查询 addon 状态、命令其执行动作、让其转换数据。与 Options 类似,命令是**类型化(typed)**的:命令的调用和返回值都会在运行时被检查(runtime type checked)。
命令是 mitmproxy 中非常强大的构造物——以 mitmproxy console 为例,其中所有用户交互本质上都是把命令绑定到按键上实现的。这意味着你写的每一个自定义命令,都可以:
- 在 console 命令提示符(按
:进入)中以:命令名 参数...的形式执行; - 获得完整的 Tab 补全和错误提示能力;
- 被内置帮助系统(按
C查看所有命令)收录; - 直接绑定为键盘快捷键,成为可复用的操作函数。
第一个命令:声明与执行
最简单的例子见官方示例 commands-simple.py:
"""Add a custom command to mitmproxy's command prompt.""" import logging from mitmproxy import command class MyAddon: def __init__(self): self.num = 0 @command.command("myaddon.inc") def inc(self) -> None: self.num += 1 logging.info(f"num = {self.num}") addons = [MyAddon()]启动加载该 addon 的 mitmproxy console:
> mitmproxy -s ./examples/addons/commands-simple.py确保事件日志(Event Log,按E切换)可见,然后在命令提示符(输入:开始)中执行:
:myaddon.inc此时事件日志会打印num = 1,再次执行则打印num = 2。注意 Tab 补全是可用的——自定义命令与内置命令享有完全一致的能力。
关于这个例子有几点需要说明:
- 命令通过
@command.command装饰器声明。每个命令有唯一名称,惯例是「点分隔命名,并以 addon 名作前缀」,如myaddon.inc。 - 类型标注是强制的,包括返回类型(本例为
None)。正是这一点让 mitmproxy 能在整个工具链中支持 addon 命令:运行时调用会做类型检查、命令会被内置帮助收录、console 的命令编辑器可以做高级补全和错误检查。
源码视角:装饰器与注册机制
从 mitmproxy/command.py 的实现看,command装饰器只是给被装饰函数挂上command_name标记:
def command(name: str | None = None): def decorator(function): @functools.wraps(function) def wrapper(*args, **kwargs): verify_arg_signature(function, args, kwargs) return function(*args, **kwargs) wrapper.__dict__["command_name"] = name or function.__name__.replace("_", ".") return wrapper return decorator若不传name,默认取函数名并把下划线替换为点(my_func→my.func)。当 addon 被加载时,CommandManager.collect_commands 会遍历 addon 的所有属性,凡是带有command_name字符串标记的对象都通过self.add(...)注册进全局命令表self.commands;若类型校验失败,仅记录 warning 而不中断整个 addon 加载。
每个命令被包装为Command对象,其构造函数会立即做两件事:
- 用
inspect.signature(func, eval_str=True)解析函数签名; - 逐个检查参数标注与返回类型是否都在
mitmproxy.types.CommandTypes类型注册表中,否则抛出CommandError——这就是文档中"类型标注是强制的"的底层原因。
函数 docstring 会被自动提取并折行后作为命令的help文本,显示在console.view.commands(按C)的命令列表里。
与 flows 协作:把流量选择器直接变成命令参数
由于命令参数是类型化的,mitmproxy 可以为某些重要数据类型提供特殊的便捷机制,其中最有用的是表示 mitmproxy 流量的flow类。
考虑官方示例 commands-flows.py:
"""Handle flows as command arguments.""" import logging from collections.abc import Sequence from mitmproxy import command from mitmproxy import flow from mitmproxy import http from mitmproxy.log import ALERT class MyAddon: @command.command("myaddon.addheader") def addheader(self, flows: Sequence[flow.Flow]) -> None: for f in flows: if isinstance(f, http.HTTPFlow): f.request.headers["myheader"] = "value" logging.log(ALERT, "done") addons = [MyAddon()]myaddon.addheader命令本身很简单:接收一个 flow 序列,给每个请求添加一个 header。真正有意思的地方在于用户如何指定 flows。因为 mitmproxy 能检查函数的类型签名,它可以把一个文本形式的 flow 选择器透明地展开成一个 flow 序列。这等于让用户拥有了完整的 flow 过滤器能力。
实际操作:先加载 addon 并制造一些流量:
> mitmproxy -s ./examples/addons/commands-flows.py然后以各种方式调用命令。只作用于当前焦点 flow:
:myaddon.addheader @focus作用于全部 flow:
:myaddon.addheader @all或者只处理google.com的 flow:
:myaddon.addheader ~d google.com更重要的是,如果计划频繁使用,可以把这些命令直接绑定到 mitmproxy 的键盘快捷键上。flow 选择器加命令的组合极其强大,让我们能够构建并暴露对 flows 进行操作的可复用函数。
选择器解析的底层链路
当命令参数标注为Sequence[flow.Flow]或flow.Flow时,解析由 mitmproxy/types.py 中的_FlowsType/_FlowType完成——它们调用内置命令view.flows.resolve把选择器字符串解析为真实的 flow 对象:
class _FlowsType(_BaseFlowType): typ = Sequence[flow.Flow] display = "flow[]" def parse(self, manager, t, s): try: return manager.call_strings("view.flows.resolve", [s]) ...而view.flows.resolve的实现位于 mitmproxy/addons/view.py:
@command.command("view.flows.resolve") def resolve(self, flow_spec: str) -> Sequence[mitmproxy.flow.Flow]: if flow_spec == "@all": return [i for i in self._store.values()] if flow_spec == "@focus": return [self.focus.flow] if self.focus.flow else [] elif flow_spec == "@shown": return [i for i in self] elif flow_spec == "@hidden": ... elif flow_spec == "@marked": return [i for i in self._store.values() if i.marked] elif flow_spec == "@unmarked": return [i for i in self._store.values() if not i.marked] elif re.match(r"@[0-9a-f\-,]{36,}", flow_spec): ids = flow_spec[1:].split(",") return [i for i in self._store.values() if i.id in ids] else: filt = flowfilter.parse(flow_spec) return [i for i in self._store.values() if filt(i)]从这段实现可以确认,flow 选择器支持以下形式:
- 视图标记:
@all(store 中全部)、@focus(当前焦点)、@shown(当前视图可见)、@hidden(被过滤掉的)、@marked/@unmarked(按标记状态); - flow ID 列表:
@id1,id2,...(UUID 形式,以逗号分隔); - 完整 flow 过滤器表达式:如
~d google.com、~u api/、~c 200、~h content-type等,运算符清单见 mitmproxy/flowfilter.py 模块头部文档(~q请求、~s响应、~h/~hq/~hs头、~b/~bq/~bs正文、~tcontent-type、~d域名、~m方法、~uURL、~c响应码等)。
此外,_FlowType(单数flow.Flow)解析后会校验选择器必须恰好匹配 1 个 flow,否则报错Command requires one flow, specification matched N——所以单 flow 参数与 flow 序列参数的语义差异是明确且被强制执行的。
Paths:路径参数与任意数量参数
命令可以接受任意数量的参数。官方示例 commands-paths.py 在 flow 序列的基础上再加一个路径参数:
"""Handle file paths as command arguments.""" import logging from collections.abc import Sequence from mitmproxy import command from mitmproxy import flow from mitmproxy import http from mitmproxy import types from mitmproxy.log import ALERT class MyAddon: @command.command("myaddon.histogram") def histogram( self, flows: Sequence[flow.Flow], path: types.Path, ) -> None: totals: dict[str, int] = {} for f in flows: if isinstance(f, http.HTTPFlow): totals[f.request.host] = totals.setdefault(f.request.host, 0) + 1 with open(path, "w+") as fp: for cnt, dom in sorted((v, k) for (k, v) in totals.items()): fp.write(f"{cnt}: {dom}\n") logging.log(ALERT, "done") addons = [MyAddon()]该命令计算给定 flow 集合中域名直方图,并写入命令第二个参数指定的路径。调用方式:
:myaddon.histogram @all /tmp/xxx注意 mitmproxy 对 flow 选择器和路径都提供 Tab 补全。路径补全由 mitmproxy/types.py 的_PathType.completion实现:它先用glob展开前缀(支持~展开),目录项在展示时追加/后缀;parse阶段则只做os.path.expanduser,把字符串转换为types.Path(一个str子类)。
支持类型清单(Supported Types)
命令参数与返回值只支持以下类型(若你需要使用清单之外的类型,官方文档建议提交 pull request 扩展):
| 类别 | 类型标注 | 说明 |
|---|---|---|
| 原始类型 | str、int、bool | bool在命令行上写作true/false,见_BoolType.parse |
| 序列 | typing.Sequence[str] | 命令行上以逗号分隔:a, b, c解析为 3 个字符串 |
| Flows 及 flow 序列 | flow.Flow、typing.Sequence[flow.Flow] | 接受@focus、@all及任意 flow 过滤器表达式 |
| 多选字符串 | types.Choice | 选项来自另一个返回Sequence[str]的命令,补全时动态执行该命令获取 |
| 元类型 | types.Cmd、types.Arg | 用于构造"调用其他命令"的命令,最常见于键绑定场景,内置 console 键绑定是丰富示例集 |
| 数据类型 | types.CutSpec、types.Data | cut 机制(目前为 alpha 版),提供从 flow 中剪取数据的便捷方式;Data不能从字符串解析,只能作为命令返回值 |
| 路径 | types.Path | str子类,支持~展开与目录 Tab 补全 |
这些类型在 mitmproxy/types.py 末尾统一注册到CommandTypes的TypeManager中:
CommandTypes = TypeManager( _ArgType, _BoolType, _ChoiceType, _CmdType, _CutSpecType, _DataType, _FlowType, _FlowsType, _IntType, _MarkerType, _PathType, _StrType, _StrSeqType, _BytesType, )每个类型对象实现统一的三接口契约(_BaseType):
parse(manager, typ, s):把命令行字符串解析为对应 Python 值,无效时抛ValueError;is_valid(manager, typ, val):校验一个值是否合法用于该类型(命令返回值也要过这道检查);completion(manager, t, s):返回 Tab 补全候选项。
运行时调用链:从字符串参数到函数调用
完整执行路径在 mitmproxy/command.py 的Command.call中:
def call(self, args: Sequence[str]) -> Any: bound_args = self.prepare_args(args) # 逐参数 parsearg + bind ret = self.func(*bound_args.args, **bound_args.kwargs) if ret is None and self.return_type is None: return typ = mitmproxy.types.CommandTypes.get(self.return_type) if not typ.is_valid(self.manager, typ, ret): raise exceptions.CommandError( f"{self.name} returned unexpected data - expected {typ.display}" ) return retprepare_args先用inspect.signature.bind校验参数个数与位置,再对每个字符串参数调用parsearg(内部即对应类型的parse),最后执行函数并校验返回值类型。参数个数不符会抛出Command argument mismatch并打印期望签名与实际收到的参数——这正是文档所说"调用与返回值都在运行时被检查"的具体含义。
命令 + 快捷键:把 addon 变成 console 操作
文档指出types.Cmd与types.Arg元类型"在键绑定中最常用,参考内置 mitmproxy console 键绑定即可看到丰富的示例集"。mitmproxy/tools/console/defaultkeys.py 就是这种用法的权威样本:
km.add( "b", "console.command cut.save @focus response.content ", ["flowlist", "flowview"], "Save response body to file", ) km.add( "x", """ console.choose.cmd "Export as..." export.formats console.command export.file {choice} @focus """, ["flowlist", "flowview"], "Export this flow to file", ) km.add( "z", 'console.command.confirm "Delete all flows" view.flows.remove @all', ["flowlist"], "Clear flow list", )可以看到三个典型模式:
- 命令 + flow 选择器:
cut.save @focus response.content——按键直接对焦点 flow 触发命令; - 多行命令序列:
console.choose.cmd先弹出选项(选项来自export.formats命令的返回值),再用{choice}占位符把用户选择插入下一条命令export.file {choice} @focus; - 带确认的命令:
console.command.confirm "提示文本" <命令> <参数>在执行破坏性操作(如删除全部 flow)前要求确认。
结合本文前面myaddon.addheader的例子,你可以把自己的命令用同样的方式注册进键映射,从而获得与内置功能完全一致的操作体验。
小结
- 用
@command.command("addon.name")声明命令,所有参数与返回类型都必须标注且必须属于CommandTypes注册表支持的类型; - 参数标注为
Sequence[flow.Flow]时,用户可在命令行传入@focus、@all、@marked、@id,...或任意 flow 过滤器表达式(~d、~u、~c等),由内置view.flows.resolve统一解析(见 mitmproxy/addons/view.py); types.Path参数获得~展开与目录 Tab 补全;Sequence[str]用逗号分隔;types.Choice的候选项动态来自另一个命令;- 解析(
parse)、绑定(signature.bind)、返回值校验(is_valid)都在运行时强制执行,错误以CommandError呈现; - 最终形态是把命令绑定到键盘快捷键,参照 defaultkeys.py 的内置键绑定即可构建高频操作。
以上路径均可在当前仓库中直接查看:文档 docs/src/content/addons/commands.md,示例 examples/addons/commands-simple.py、examples/addons/commands-flows.py、examples/addons/commands-paths.py,核心实现 mitmproxy/command.py 与 mitmproxy/types.py。
【免费下载链接】mitmproxyAn interactive TLS-capable intercepting HTTP proxy for penetration testers and software developers.项目地址: https://gitcode.com/GitHub_Trending/mi/mitmproxy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考