8

Whistle 实现原理 —— 从 0 开始实现一个抓包工具

 2 years ago
source link: https://zhuanlan.zhihu.com/p/441818328
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Whistle 实现原理 —— 从 0 开始实现一个抓包工具

作者:腾讯 IMWeb 前端团队

导语 通过这篇文章可以大致了解 Whistle 的实现原理,并学习如何实现一个简单的抓包调试工具。 项目 Github 地址:https://github.com/avwo/whistle

Whistle 是基于 Node.js 实现的跨平台 Web 抓包调试(HTTP)代理,主要功能:

  1. 实时抓包:支持 HTTP、HTTPS、HTTP2、WebSocket、TCP 等常见 Web 请求的抓包;
  2. 修改请求响应:与一般抓包调试工具采用断点的方式不同,Whistle 采用类似系统 host 的配置规则方式;
  3. 扩展功能:支持通过 Node 编写插件,或作为独立 NPM 包引入项目两种扩展方式。
v2-8b7f9a994aa5308461075e798a1e853b_720w.jpg

本文将从最基本的概念开始逐步讲解 Whistle 功能,包含以下内容:

  1. 什么是 HTTP 代理
  2. 实现简单 HTTP 代理
  3. 完整 HTTP 代理架构(Whistle)
  4. 具体实现原理

1. 什么是 HTTP 代理

v2-30df6a47d46d26258c96312bcb46bc64_720w.jpg

代理是客户端到服务端的中转服务,其中:

  1. 不经过代理的请求:客户端和服务端直接建立连接后,即可开始交换数据。
  2. 经过代理的请求:客户端不与服务端直接建立连接,而是先跟代理建立连接后,将目标服务器的地址发送给代理,通过代理再跟服务端建立连接,这里如果代理服务为 HTTP Server,则称为 HTTP 代理。

接下来看下客户端如何将目标服务器地址传给 HTTP 代理,以及 HTTP 代理如何跟目标服务器建立连接。

2. 实现简单 HTTP 代理

先看一个用 Node.js 实现的最简单 HTTP 代理:

const http = require('http');
const { connect } = require('net');

/****************** 工具方法 ******************/
const getHostPort = (host, defaultPort) => {
  let port = defaultPort || 80;
  const index = host.indexOf(':');
  if (index !== -1) {
    port = host.substring(index + 1);
    host = host.substring(0, index);
  }
  return {host, port};
};

const getOptions = (req, defaultPort) => {
  // 这里假定 host 一定存在,完整实现参见 Whistle
  const { host, port } = getHostPort(req.headers.host, defaultPort);
  return {
    hostname: host, // 指定请求域名,用于通过 DNS 获取服务器 IP 及设置请求头 host 字段
    port, // 指定服务器端口
    path: req.url || '/',
    method: req.method,
    headers: req.headers,
    rejectUnauthorized: false, // 给 HTTPS 请求用的,HTTP 请求会自动忽略
  };
};

// 简单处理,出错直接断开,完整实现逻辑参考 Whistle
const handleClose = (req, res) => {
  const destroy = (err) => { // 及时关闭无用的连接,防止内存泄露
    req.destroy();
    res && res.destroy();
  };
  res && res.on('error', destroy);
  req.on('error', destroy);
  req.once('close', destroy);
};


/****************** 服务代码 ******************/
const server = http.createServer();
// 处理 HTTP 请求
server.on('request', (req, res) => {
  // 与服务端建立连接,透传客户端请求及服务端响应内容
  const client = http.request(getOptions(req), (svrRes) => {
    res.writeHead(svrRes.statusCode, svrRes.headers);
    svrRes.pipe(res);
  });
  req.pipe(client);
  handleClose(res, client);
});

// 隧道代理:处理 HTTPS、HTTP2、WebSocket、TCP 等请求
server.on('connect', (req, socket) => {
  // 与服务端建立连接,透传客户端请求及服务端响应内容
  const client = connect(getHostPort(req.url), () => {
    socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    socket.pipe(client).pipe(socket);
  });
  handleClose(socket, client);
});

server.listen(8080);

上述代码实现了一个具有转发请求功能的 HTTP 代理,从代码可知 HTTP 代理就是一个普通的 HTTP Server,并监听 requestconnect 这两个事件,客户端会通过这两个事件将目标服务器地址传过来,其中:

  1. request:一般普通 HTTP 会通过该事件将目标服务器地址传过来。
  2. connect:一般非 HTTP 请求,如 HTTPS、HTTP/2、WebSocket、TCP 等会通过该事件将目标服务器地址传过来,触发该事件的代理请求也叫隧道代理

可以在事件里面的 req.urlreq.headers.host 获取目标服务器的地址(host:port),再跟该服务器地址建立连接并将结果通过 HTTP 响应的方式返回给客户端,这里只是实现代理的最基本功能,完整的 HTTP 除了请求转发,至少应该还有:

  1. 查看实时抓包;
  2. 解析 HTTPS 请求;
  3. 修改请求响应内容;
  4. 扩展功能。

下面以 Whistle 为例看下如何用 Node.js 实现一个完整的 HTTP 代理。

3. 完整 HTTP 代理架构(Whistle)

主要分五个模块:

  1. 请求接入模块
  2. 隧道代理模块
  3. 处理 HTTP 请求模块
  4. 规则管理模块
  5. 插件管理模块

4. 具体实现原理

下面分别看下这五个模块具体是怎么实现的。

4.1 请求接入模块

所有请求先会经过请求接入模块,Whistle 支持四种请求接入方式:

  1. HTTP & HTTPS 直接请求:相当于配 hosts 或 DNS 的方式,将请求转发到 Whistle;
  2. HTTP 代理:Whistle 默认接入方式,即配系统代理或通过浏览器插件配 HTTP 代理的方式;
  3. HTTPS 代理:在 HTTP 代理之上对代理请求进行了加密,即 HTTPS Server,可以通过指定证书转成 HTTP 代理请求;
  4. Socks5 代理:利用 npm 包 socksv5 转成普通的 TCP 请求,并将 TCP 请求转成隧道代理请求。

基实现原理是:将所有请求都转成 HTTP 代理的 隧道代理请求HTTP 请求,再解析 隧道代理请求 转成 HTTP 请求。

如何将普通 tcp 请求转成隧道代理请求参见: lack-proxy

下面看下如何从 隧道代理请求 解析出 HTTP 请求。

4.2 隧道代理模块

关键点(HTTP 请求也可以走隧道代理):

  1. 通过匹配的全局规则判断是否要解析隧道代理请求,如果不解析,则当成普通 TCP 请求处理;
  2. 如果需要,则通过 socket.once('data', handler) 读取请求点第一帧数据;
  3. 将第一帧数据转成字符串,通过正则 /^(\w+)\s+(\S+)\s+HTTP\/1.\d$/mi 是否是 HTTP 请求?如果是 HTTP 请求,再判断下是否是 CONNECT 请求,即隧道代理请求(隧道代理请求也可以代理隧道代理请求),如果是,则转回隧道代理方法处理,如果不是,则转到 HTTP 请求模块处理;
  4. 如果不是 HTTP 请求,则当成 HTTPS 请求处理,这里需要用到中间人的方式将 HTTPS 请求转成 HTTP 请求;
  5. Whistle 会先按以下顺序获取请求证书:
  • 通过匹配的插件获取(可以通过规则 sniCallback://plugin 指定加载证书的插件);
  • 通过启动参数 -z certDir 指定目录或 ~/.WhistleAppData/custom_certs 加载的自定义证书;
  • 如果没有上述两种自动证书,Whistle 会自动生成一个默认的证书。
  1. 获取到证书后,再利用该证书启动一个 HTTPS Server,将 HTTPS 请求转成 HTTP 请求交给 HTTP 请求模块处理。

4.3 HTTP 请求处理模块

HTTP 请求处理可以分两个阶段:

  1. 请求阶段:
  • 匹配全局规则;
  • 如果规则里类似 http://whistle.xxx 的规则,执行对应插件钩子,获取插件规则并跟匹配的全局规则合并;
  • 执行规则、记录状态并请求到指定服务。

2. 响应阶段:

  • 执行匹配插件的钩子,获取插件规则并跟匹配的全局规则合并;
  • 执行规则、记录状态并请求返回客户端。

4.4 规则管理

与传统抓包调试代理 采用断点修改请求响应数据不同,Whistle 采用配置规则的方式修改请求响应,采用配置方式的好处是操作简单,且可以将操作持久化存储及共享给他人,先看几个例子:

Whistle 的规则管理主要两个功能:

解析规则

Whistle 有两类规则:

  1. 全局规则(公共规则),所有请求都会尝试匹配的规则,由以下规则组成:

2.插件规则(私有规则),即进入插件的请求(匹配的全局规则里有 http://whistle.xxx 协议)才会匹配到的规则,由以下规则组成:
文档:https://wproxy.org/whistle/plugins.html

    • 插件 reqRulesServer 等 hooks 动态返回;
    • 插件根目录 _rules.txt 等文件配置的静态规则;

匹配规则

Whistle 规则的完整结构为:

文档:https://wproxy.org/whistle/mode.html

4.5 插件管理

Whistle 插件的功能很多,不仅具备 Node 的所有能力,且可以操作 Whistle 的所有规则(理论上可以基于插件实现一个 Whistle),主要用来做以下事情:

  1. 鉴权功能
  2. 提供 UI 交互界面
  3. 作为请求 Server(直接响应或转发并修改请求响应)
  4. 统计请求信息(查看上报 / 打点数据等)
  5. 设置规则(动态,静态,全局及私有规则)
  6. 获取抓包数据
  7. 编解码请求响应数据流(pipe stream 功能)
  8. 扩展界面右键菜单(如:分享抓包数据)
  9. 保存并同步 Rules & Values 数据
  10. 自定义 HTTPS 请求的证书
  1. whistle.script:实现通过自定义脚本动态设置规则
  2. whistle.vase:提供灵活强大的 mock 能力
  3. whistle.inspect:方便快速注入 vConsole、eruda 等页面调试工具
  4. whistle.sni-callback:自定义证书插件

其它插件例子参见:https://github.com/whistle-plugins

Whistle 是如何实现插件功能?主要遵循以下三个设计原则:

  1. 完备性:
    确保所有功能点都可扩展,如:请求鉴权、生成证书、获取抓包、设置规则、请求处理等。
  2. 稳定性:
    插件内部异常不影响其它功能,Whistle 的每个插件独立进程,插件与 Whistle 之间通过 HTTP 协议交互。
    Whistle 是使用 npm 包 pfork 来启动插件进程,进程间的交换是直接通过 Node 的 http 模块实现的),方便开发者利用 http 的生态开发插件。
  3. 易用性:
    方便用户开发及使用。
  4. 开发: 结构简单 (npm 包) + 脚手架 lack
    使用: 安装 npm 包即可,用法跟内置协议一样,且可内置交互界面。

有关插件的更多细节参见:https://wproxy.org/whistle/plugins.html

事实上,Whistle 除了支持插件扩展,还可以同时作为独立模块引入项目使用;除了本地开发使用,也可以基于 Whistle 开发出支持多人使用的开发联调协作工具,比如后面会给大家介绍其实现原理的:

  1. 基于 Whistle 实现的多人多环境远程抓包调试工具。
    Nohost:https://github.com/Tencent/nohost
  2. 基于 Whistle 和 Nohost 实现的分布式远程抓包调试工具 TDE 等等。
    TDE 目前只在腾讯内部使用,后续后逐步对外开源。

5. 参考资料


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK