⚉ Can you hear me?
Vanessa 是一款简单但强大的 Web 代理抓包调试工具,由 rikumi 独立开发,同时也是我的本科毕业设计作品。
Vanessa 的命名来自于 Cytus II 中的角色。在人工智慧被人类打败后的世界,智能机体 Vanessa 作为人类的俘虏,沉睡在荒草丛生的 Library 里,驱动着人类世界科技的正常运转,直到被醒来的同伴 Ivy 唤醒,开始了与人类和自我对抗的旅程。同时这个命名也意味着 Bug 很多。
Vanessa 的灵感来自 Whistle,部分实现代码参照了 http-mitm-proxy,将其拆分为支持作为代理的 Koa 服务器以及用于代理的各种内置中间件两个部分进行实现。Vanessa 的功能与 Whistle 相比,可以参考下表。
特性 | Whistle | Vanessa |
---|---|---|
平台支持 | 全平台 | 理论全平台,仅测 macOS |
HTTP 抓包 | 支持 | 支持 |
HTTPS MITM 抓包 | 可选 | 强制开启 |
WebSocket 抓包 | 支持 | 仅代理,暂不支持抓包 |
证书安装 | 自行安装 | HTTP 页面引导安装 |
配置文件 | 支持,特殊语法 | 支持,Node Koa 中间件语法 |
开机自启 | 不支持 | 自动配置 |
上游代理 | 手动设置 | 自动检测(系统代理,环境变量代理) |
多用户 | 不支持 | IP 多用户(共享配置,单独开关) |
插件 | 支持 | 暂不支持(后续将支持 npm 安装第三方 koa 中间件) |
npm i -g vanessa
或yarn global add vanessa
;- 安装完成后,输入监听端口号,即可在后台启动,同时设置开机自启动;
- 在需要代理的设备(如本机)设置系统 HTTP 代理(Firefox 浏览器设置独立代理)到本机设定好的端口上;
- 在需要代理的设备上,访问 http://vanes.sa/,根据提示安装并信任证书后,点击 Take me to Vanessa 进入管理页面。
在管理界面左侧 New rule,输入新规则的名称,可以创建新的中间件规则。Vanessa 规则是标准的 Koa 中间件格式,可以访问 https://koajs.com/ 学习 Koa 中间件的用法。
另外,Vanessa 作为代理服务器,在 Koa 中间件的语法上增加了一些新的特性:
中间件会按照数字优先排序进行排序,这种排序方式类似于在常见的文件管理器(如 Finder)中看到的顺序,例如:
'1-test' <
'2-test' <
'10-foo' <
'10-test' <
'20-test1' <
'20-test2' <
'20-test222';
排在最前面的中间件将会最先执行,每一个中间件的 next()
方法将指向下一个中间件,以此类推。
由于一些实现上的缺陷,暂时不支持在管理界面对中间件进行重命名,需要删除后重新添加。
当多个中间件对上下文的同一属性进行操作,或者某一个中间件中选择不调用 next()
(例如改为发送某个本地文件,或通过 Koa ctx.throw()
返回一个错误)时,需要结合中间件的顺序进行考虑。
在中间件配置中,使用 console.log()
console.error()
可将参数以类似于 Node.js 的格式输出到管理界面的控制台上。在规则的编辑状态,可以查看规则在每次请求中的控制台输出;在请求记录的查看状态,可以查看该请求经过的所有中间件时的控制台输出。
在中间件配置中,next()
方法实质上会递归调用剩余的中间件,最终对真实的远程服务器发起请求并得到响应。以 next()
为界,中间件分为请求和响应两个执行阶段。像 Koa 中间件的常见书写方式一样,使用 await next()
可以对两个阶段的处理逻辑进行区分;另外,在书写异步回调等特殊情况下,不清楚当前的阶段,可以通过 ctx.phase
获得,其值可以为 'request'
或 'response'
。
注意:若在某个中间件中选择不调用 next()
(例如改为发送某个本地文件,或通过 Koa ctx.throw()
返回一个错误),那么 ctx.phase
将不会被改为 'response'
,请留意。
在管理界面中,选择左侧需要查看的请求,会在右侧编辑器中显示有关请求信息的 JSON 文本,其原理是 Vanessa 会对最近 1000 次请求的上下文对象短时间持有引用,并在管理界面中显示。
你可以在中间件配置中对 ctx
添加新的属性,使其出现在请求详情的 JSON 文本中。
Koa 中,服务器本身作为请求的终点,请求的各项属性均为只读的,不可以更改(更改了也没有意义);Vanessa 作为代理服务器,ctx.request
中的属性(url
、host
、method
字符串,query
对象等)均可以修改,修改后将会影响代理服务器请求远程服务器时的行为。
另外,由于 Koa 请求对象可能并不完整包含请求所需的所有参数,Vanessa 提供了附加配置对象 ctx.requestOptions
,可在其中添加 http.request()
(https.request()
)支持的其它配置属性。
抓包修改请求过程中,常常有两种需要修改域名(Host)的情况:一种是让请求走到不同的远程主机,但请求的虚拟域名(即 Headers 中的 Host
)不变,可以帮助我们将现有的请求路由到不同于 DNS 实际解析的服务器(如测试服务器)的同时,保持原有的 Host
头;另一种需求是让请求的目标主机和虚拟域名同时改变,以便将这一次请求替换为完全不同的远程文件。
修改 ctx.host
(ctx.request.host
)将保持虚拟域名不变,只改变请求发送到的虚拟主机(可以为域名或 IP 地址);修改 ctx.request.headers.host
则会保持远程主机不变,只改变发送到该主机上的虚拟域名。
注意:
- 管理界面中显示的地址是
ctx.request.url
,其中的域名部分将会显示为虚拟域名的值,而非实际主机的域名。 - 由于实现上的限制,目前发出 HTTPS 请求所带的 SNI 头部将会保持与远程主机一致,而不是与虚拟域名一致,这并不符合 SNI 的设计本意。这一实现缺陷在后续版本可能会修正,请留意。
在 HTTPS 中间人代理的语境下,客户端发来的请求是 CONNECT 请求,它要求我们连接到远程服务器,并原样转发连接中的所有(密文的)数据;中间人代理随之建立对应的虚拟远程服务器,让客户端与虚拟远程服务器之间进行加密通讯,而虚拟远程服务器受到根证书的信任,可以解密客户端发来的 HTTPS 请求中的 HTTP 报文。
这也就意味着,客户端真实发来的请求与能解密得到 HTTP 报文的请求分属于不同的连接。因为这样的特性,在中间件中获取原始请求中的客户端 IP 地址等属性会出现麻烦。
因此,在 Vanessa 中:
- 通过
ctx.request.protocol
可以获得客户端发来的原始请求的协议(http
或https
),并可以进行修改,改变最终发给真实远程服务器的请求协议; - 通过
ctx.request.ip
可以取得客户端的真实 IP; - 通过
ctx.rawRequest
可以取得原始的 CONNECT 请求,而非虚拟远程服务器收到的具体 HTTP 请求;
通过 ctx.proxy
(ctx.request.proxy
)字符串,可以设置本次请求所使用的上游代理(链式代理)。例:
const cp = require('child_process');
const detect = (cmd, keyword) => cp.execSync(cmd).includes(keyword);
const detectOfficeWifi = detect.bind(
null,
'/Sy*/L*/Priv*/Apple8*/V*/C*/R*/a* -I',
'OfficeWiFi'
);
const detectVPN = detect.bind(null, 'ps -A', 'ScmClient');
const NGN = 'http://localhost:12759';
const V2Ray = 'http://localhost:8001';
module.exports = async (ctx, next) => {
const startTime = Date.now();
const isOfficeWifi = detectOfficeWifi();
const isIntranet = /\.(oa|wsd|server)\.com$|^1\d+\./.test(ctx.hostname);
const isVPN = detectVPN();
if (!isVPN && isOfficeWifi != isIntranet) {
ctx.proxy = NGN;
} else {
ctx.proxy = !isIntranet && V2Ray;
}
console.log(`Set proxy to ${ctx.proxy}, took ${Date.now() - startTime}ms`);
await next();
};
通过 ctx.test()
方法可以快速对 URL 进行匹配,匹配语法类似于 koa-router
,如果模式串中出现协议,协议为精确匹配,其余部分使用 path-to-regexp
进行匹配。具体可参见 path-to-regexp 的文档。例:
module.exports = async (ctx, next) => {
console.log(ctx.url); // https://support.qq.com/products/28096
ctx.test('https://support.qq.com/:route+'); // { route: 'products/28096' }
ctx.test('http://support.qq.com/:route+'); // null
ctx.test('//support.qq.com/:route+'); // { route: 'products/28096' }
ctx.test('support.qq.com/:route+'); // { route: 'products/28096' }
ctx.test('support.qq.com/:route'); // null
ctx.test(':sub.qq.com/:route+'); // { sub: 'support', route: 'products/28096' }
ctx.test('support.qq.com/products/:id'); // { id: '28096' }
await next();
};
通过 ctx.send()
方法可以发送本地文件,但一定要 await
。若选择了发送本地文件,就不需要再调用 next()
。例如:
module.exports = async (ctx, next) => {
let res;
if (ctx.test('//docs.qq.com/sheet/(.+)')) {
await ctx.send('~/Documents/sheet/dev/html/pc.html');
} else if ((res = ctx.test('//docs.qq.com/static/img/:route+'))) {
await ctx.send('~/Documents/sheet/dev/img/' + res.route);
} else if ((res = ctx.test('//docs.qq.com/static/dev/:route+'))) {
await ctx.send('~/Documents/sheet/dev/' + res.route);
} else {
await next();
}
};
ctx.session
为当前会话的持久化对象,它是每个用户(按 IP 区分)的唯一存储对象,可以进行读写,Vanessa 会自动负责持久化。
Vanessa 借用了 Koa 的 ctx.request.body
和 ctx.response.body
两个对象,用于保存当前的请求流和响应流。若要对流进行所需的变换(Stream.Transform),只需要将该流 pipe
过所需的变换,并将得到的可读流重新赋给 ctx.request.body
或 ctx.response.body
即可。例如:
module.exports = async (ctx, next) => {
ctx.request.body = ctx.request.body.pipe(myDuplex);
await next();
ctx.response.body = ctx.response.body.pipe(myDuplex);
};
特别说明,ctx.req
和 ctx.res
这两个属性在 Vanessa 中有特殊的用途,尽量不要对它们进行修改,否则可能导致未定义行为。
另外,Vanessa 提供了一些常用的流读取和流变换方法,位于 ctx.request
以及 ctx.response
中:
以下流读取方法可以使用在任何不早于流产生的阶段,即在请求阶段可读取请求流,在响应阶段可读取请求流和响应流。
async all()
:Hold 住整个流,等所有数据传输完毕,并使用 raw-body 的默认配置,解析和返回 UTF-8 字符串数据,限 1M 以内。async json()
:同all()
,但解析为 JSON 格式,可能会抛出异常。async cheerio()
:同all()
,但解析为 Cheerio 格式,方便操作 HTML 文档。
以下流变换方法只能使用在流产生的阶段,即在请求阶段只能变换请求流,在响应阶段只能变换响应流;这些方法将会自动用变换后的流替换原有的流,并支持链式调用。
transform(duplex)
:将流通过一个流变换。replace(find, replace)
:利用 replacestream 在流内进行实时正则查找替换,在不破坏流的实时性的同时,保证跨数据包边界的查找串也能正常替换。prepend(data)
:在流的起始端增加一段字符串或 Buffer。append(data)
:在流的末尾端增加一段字符串或 Buffer。delay(ms)
:使流中的所有数据包延后指定的毫秒数。throttle(bytesPerSecond)
:限制流中数据的传输速度不超过一个固定的数值(字节/秒)。
首先需要明白的是,在代理服务器中,提到 HTTPS/HTTPS 有两种情况,一种是请求本身所使用的协议,另一种代理服务器所监听的协议,经过排列组合事实上一共有四种情况:
- 通过 HTTP 代理发出 HTTP 请求
- 通过 HTTP 代理发出 HTTPS 请求
- 通过 HTTPS 代理发出 HTTP 请求
- 通过 HTTPS 代理发出 HTTPS 请求
Vanessa 是 HTTP 代理,暂时不支持通过 HTTPS 连接,但通过 Vanessa 可以正常收发 HTTP/HTTPS 协议的请求。
Vanessa 支持在请求时自动探测 Windows 和 macOS 的系统代理及环境变量代理,但开机自启动的 Vanessa 不在 Shell 中运行,不具备环境变量,只能探测系统代理。
因此,日常推荐的做法是,让系统代理保持第三方代理程序的设置,让 Vanessa 可以自动探测到它们,然后在 Chrome 浏览器中通过 SwitchyOmega 等浏览器插件连接 Vanessa 进行使用;
如果你禁用了 Tencent iOA/V2Ray/ShadowSocks 等代理服务的自动设置系统代理功能(例如 Tencent iOA 进入调试模式,V2RayNG 设置为手动模式等),并将系统代理设置为 Vanessa,此时 Vanessa 将无法再自动检测第三方代理,会自动改为直连,需要手动书写中间件进行设置,或在 ~/Library/LaunchAgents/vanessa.plist
中添加环境变量。