插件开发

插件脚手架及辅助工具:https://github.com/avwo/lack

插件开发示例参见:https://github.com/whistle-plugins/examples

为了满足一些特定业务场景的需要,whistle提供了插件扩展能力,通过插件可以新增whistle的协议实现更复杂的操作、也可以用来存储或监控指定请求、集成业务本地开发调试环境等等,基本上可以做任何你想做的事情,且开发、发布及安装whistle插件也都很简单。

先了解下如何安装使用插件:

安装插件

Whistle 插件就是一个名为 whistle.your-plugin-name@org/whistle.your-plugin-name 的 npm 包,其中 your-plugin-name 为插件名称,只能包含 小写字母(a-z)数字(0-9)_- 四种字符(不建议使用下划线 _),并通过 w2 i 命令行命令安装(客户端版本可以直接通过界面安装详见客户端文档):

w2 i whistle.your-plugin-name
# 或
w2 i @org/whistle.your-plugin-name

支持设置 registry:w2 i whistle.your-plugin-name --registry=https://registry.npmmirror.com

安装成功后可以在whistle的插件列表看到新安装的插件: 插件列表

使用插件

插件安装后,whistle会新增两个协议,分别为 whistle.your-plugin-nameyour-pluign-name,用户通过配置:

pattern whistle.your-plugin-name://xxx
# 或
pattern your-plugin-name://xxx

如果插件名称包含 _ ,则无法配置 your_plugin_name://

匹配到上述规则的请求会自动请求插件相应的server,上述两种配置有些差异,后面再详细说下,下面我们先了解下插件基本原理,并快速开发一个whistle插件的项目。

实现原理

whistle的插件是一个独立运行的进程,这样是为了确保插件不会影响到whistle主进程的稳定性,并通过暴露一些http server的方式实现与whistle的交互,whistle会在特定阶段请求特定的server,具体看下面的原理图:

http请求 http请求插件过程

图一:表示http(s)、WebSocket请求涉及到的插件内置server

tunnel请求 tunnel请求插件过程

图二:表示tunnel请求涉及到的插件内置server,tunnel请求如上图指没有开启https捕获,或代理到whistle的Socket请求

从上面几个图可以知,whistle插件会设计以下7种server:

  1. statsServer:统计请求信息的服务
  2. resStatsServer:统计响应信息的服务
  3. rulesServer:设置请求规则的服务(支持http/https/websocket请求)
  4. resRulesServer:设置响应规则的服务(支持http/https/websocket请求)
  5. tunnelRulesServer:设置tunnel请求规则的服务
  6. server:whistle会把指定请求转发到该server
  7. uiServer: whistle插件的界面,可以通过特定的url访问

插件就是通过上述中的0-N个server实现相应的功能,并通过配置规则将请求转发到插件的各个server:

pattern whistle.your-plugin-name://xxx
# 或
pattern your-plugin-name://xxx

只要匹配pattern的请求才会转到插件的各个server,上述两种配置的区别是:

  1. pattern whistle.your-plugin-name://xxx:请求默认不会走到插件里面的 server 服务 whistle.xxx规则

    可以通过在rulesServer里面重新设置 pattern your-plugin-name://xxx 把请求转发到 server 里面

  2. pattern your-plugin-name://xxx:默认会经历所有server

除了用户手动配置,插件也可以自动规则,只需在插件模块的根目录新建一个 rules.txt 文件,里面可以设置所有whistle规则,包括当前插件的添加的新协议,这个文件的规则是公共的,相当于在:

# rules.txt
www.test.com/cgi-bin whistle.your-plugin-name://xxx

如果规则是跟具体请求无关,也可以在 rules.txt 里面设置定时从插件、或远程、或本地更新规则:

@whistle.your-plugin-name/cgi-bin/xxx

这样whistle会定时请求插件 whistle.your-plugin-name uiServer里面的 /cgi-bin/xxx 的接口。

上述配置表示所有 www.test.com/cgi-bin 路径下的请求都会转到插件来。

除了 rules.txt 外,根目录还可以建插件私有规则:

  1. _rules.txt:whistle规则,相当于 rulesServer 返回的规则
  2. _values.txt:json对象,相当于 rulesServer 返回的Values

也可以在rules里面设置Values:操作值

为了方便各个server直接的通讯及初始化插件配置,可以在插件根目录创建一个 initial.js 的文件,插件首先加载该文件,另外还可以通过插件直接获取抓包数据等,具体插件API及如何开发插件参见:插件开发Demo

调试插件

插件脚手架及辅助工具:https://github.com/avwo/lack

  1. 开启whistle的调试模式:
     w2 stop
     w2 run
    

    这样可以在控制台看到插件console输出的日志及错误

  2. 修改插件代码后,需要触发插件项目的 package.json 修改才会重新加载该插件,比如添加或删除一个空格。

抓包调试protobuf等自定义协议

whistle v1.13.0版本开始支持通过插件自定义长连接数据的解析器,并通过 Network/Frames 进行显示发送接收数据,及监听状态变化、获取构造请求数据等,具体参考:https://github.com/whistle-plugins/whistle.custom-parser

API

插件的上述各个server里面每个请求的 (req, res) 对象内置了一些属性及方法,用来获取请求配置信息,以及抓包数据。

  1. initial.js: 项目根目录文件,可选,插件在加载所有server之前加载
     module.exports = (options) => {
       // options里有storage对象及Storage类,以及一些自定义请求头字段的名称
       options = {
       name: plugin.moduleName,
       script: PLUGIN_MAIN,
       value: plugin.path,
       REQ_FROM_HEADER: REQ_FROM_HEADER,
       RULE_VALUE_HEADER: RULE_VALUE_HEADER,
       MAX_AGE_HEADER: MAX_AGE_HEADER,
       ETAG_HEADER: ETAG_HEADER,
       FULL_URL_HEADER: FULL_URL_HEADER,
       REAL_URL_HEADER: REAL_URL_HEADER,
       REQ_ID_HEADER: REQ_ID_HEADER,
       CUSTOM_PARSER_HEADER: CUSTOM_PARSER_HEADER,
       STATUS_CODE_HEADER: STATUS_CODE_HEADER,
       LOCAL_HOST_HEADER: LOCAL_HOST_HEADER,
       HOST_VALUE_HEADER: LOCAL_HOST_HEADER,
       PROXY_VALUE_HEADER: PROXY_VALUE_HEADER,
       PAC_VALUE_HEADER: PAC_VALUE_HEADER,
       METHOD_HEADER: METHOD_HEADER,
       CLIENT_IP_HEADER: config.CLIENT_IP_HEAD,
       CLIENT_PORT_HEAD: CLIENT_PORT_HEAD,
       GLOBAL_VALUE_HEAD: GLOBAL_VALUE_HEAD,
       HOST_IP_HEADER: HOST_IP_HEADER,
       debugMode: config.debugMode,
       config: conf
     }
     };
    
  2. statsServer:统计请求信息的服务

     exports.statsServer = (server, options) => {
       // options 同上,initial.js的options是同一个对象
       server.on('request', (req, res) => {
         const oReq = req.originalReq;
         const oRes = req.originalRes;
    
         req.clientIp: 请求的客户端IP,注意:挂在req里面
    
         oReq.id: 请求的ID,每个请求对应一个唯一的ID
         oReq.headers: 请求的原始headers,而req.headers包含了一些插件自定义字段
         oReq.ruleValue: 配置的规则值, 如:whistle.xxx://ruleValue
         oReq.url: 请求的完整url
         oReq.realUrl: 请求的真实url,一般为空
         oReq.method: 请求方法
         oReq.clientPort: 请求的客户端端口
         oReq.globalValue: pattern @globalValue
         oReq.proxyValue: 配置的代理规则,一般为空
         oReq.pacValue: 配置的pac规则,一般为空
    
         oRes.serverIp: 服务端IP,只有在server或resServer、resStatsServer才能获取到
         oRes.statusCode: 响应状态码,同 oRes.serverIp
    
         // 获取抓包数据,不需要等待响应完成
         req.getReqSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取完整的抓包数据,要等待响应完成
         req.getSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取WebSocket或Socket请求的帧数据列表
         // 返回 1~16 个帧数据
         req.getFrames((list) => {
           // 如果为空表示该长连接已断开
           if (!list) {
             return;
           }
           // do sth
         }):
       });
     };
    
  3. resStatsServer:统计响应信息的服务

     exports.resStatsServer = (server, options) => {
       // options 同上,initial.js的options是同一个对象
       server.on('request', (req, res) => {
         const oReq = req.originalReq;
         const oRes = req.originalRes;
    
         req.clientIp: 请求的客户端IP,注意:挂在req里面
    
         oReq.id: 请求的ID,每个请求对应一个唯一的ID
         oReq.headers: 请求的原始headers,而req.headers包含了一些插件自定义字段
         oReq.ruleValue: 配置的规则值, 如:whistle.xxx://ruleValue
         oReq.url: 请求的完整url
         oReq.realUrl: 请求的真实url,一般为空
         oReq.method: 请求方法
         oReq.clientPort: 请求的客户端端口
         oReq.globalValue: pattern @globalValue
         oReq.proxyValue: 配置的代理规则,一般为空
         oReq.pacValue: 配置的pac规则,一般为空
    
         oRes.serverIp: 服务端IP,只有在server或resServer、resStatsServer才能获取到
         oRes.statusCode: 响应状态码,同 oRes.serverIp
    
         // 获取抓包数据,不需要等待响应完成
         req.getReqSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取完整的抓包数据,要等待响应完成
         req.getSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取WebSocket或Socket请求的帧数据列表
         // 返回 1~16 个帧数据
         req.getFrames((list) => {
           // 如果为空表示该长连接已断开
           if (!list) {
             return;
           }
           // do sth
         }):
       });
     };
    
  4. rulesServer:设置请求规则的服务(支持http/https/websocket请求)

     exports.rulesServer = (server, options) => {
       // options 同上,initial.js的options是同一个对象
       server.on('request', (req, res) => {
         const oReq = req.originalReq;
         const oRes = req.originalRes;
    
         req.clientIp: 请求的客户端IP,注意:挂在req里面
    
         oReq.id: 请求的ID,每个请求对应一个唯一的ID
         oReq.headers: 请求的原始headers,而req.headers包含了一些插件自定义字段
         oReq.ruleValue: 配置的规则值, 如:whistle.xxx://ruleValue
         oReq.url: 请求的完整url
         oReq.realUrl: 请求的真实url,一般为空
         oReq.method: 请求方法
         oReq.clientPort: 请求的客户端端口
         oReq.globalValue: pattern @globalValue
         oReq.proxyValue: 配置的代理规则,一般为空
         oReq.pacValue: 配置的pac规则,一般为空
    
         oRes.serverIp: 服务端IP,只有在server或resServer、resStatsServer才能获取到
         oRes.statusCode: 响应状态码,同 oRes.serverIp
    
         // 获取抓包数据,不需要等待响应完成
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getReqSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取完整的抓包数据,要等待响应完成
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取WebSocket或Socket请求的帧数据列表
         // 返回 1~16 个帧数据
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getFrames((list) => {
           // 如果为空表示该长连接已断开
           if (!list) {
             return;
           }
           // do sth
         }):
       });
     };
    
  5. resRulesServer:设置响应规则的服务(支持http/https/websocket请求)

     exports.resRulesServer = (server, options) => {
       // options 同上,initial.js的options是同一个对象
       server.on('request', (req, res) => {
         const oReq = req.originalReq;
         const oRes = req.originalRes;
    
         req.clientIp: 请求的客户端IP,注意:挂在req里面
    
         oReq.id: 请求的ID,每个请求对应一个唯一的ID
         oReq.headers: 请求的原始headers,而req.headers包含了一些插件自定义字段
         oReq.ruleValue: 配置的规则值, 如:whistle.xxx://ruleValue
         oReq.url: 请求的完整url
         oReq.realUrl: 请求的真实url,一般为空
         oReq.method: 请求方法
         oReq.clientPort: 请求的客户端端口
         oReq.globalValue: pattern @globalValue
         oReq.proxyValue: 配置的代理规则,一般为空
         oReq.pacValue: 配置的pac规则,一般为空
    
         oRes.serverIp: 服务端IP,只有在server或resServer、resStatsServer才能获取到
         oRes.statusCode: 响应状态码,同 oRes.serverIp
    
         // 获取抓包数据,不需要等待响应完成
         req.getReqSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取完整的抓包数据,要等待响应完成
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取WebSocket或Socket请求的帧数据列表
         // 返回 1~16 个帧数据
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getFrames((list) => {
           // 如果为空表示该长连接已断开
           if (!list) {
             return;
           }
           // do sth
         }):
       });
     };
    
  6. tunnelRulesServer:设置tunnel请求规则的服务

     exports.tunnelRulesServer = (server, options) => {
       // options 同上,initial.js的options是同一个对象
       server.on('request', (req, res) => {
         const oReq = req.originalReq;
         const oRes = req.originalRes;
    
         req.clientIp: 请求的客户端IP,注意:挂在req里面
    
         oReq.id: 请求的ID,每个请求对应一个唯一的ID
         oReq.headers: 请求的原始headers,而req.headers包含了一些插件自定义字段
         oReq.ruleValue: 配置的规则值, 如:whistle.xxx://ruleValue
         oReq.url: 请求的完整url
         oReq.realUrl: 请求的真实url,一般为空
         oReq.method: 请求方法
         oReq.clientPort: 请求的客户端端口
         oReq.globalValue: pattern @globalValue
         oReq.proxyValue: 配置的代理规则,一般为空
         oReq.pacValue: 配置的pac规则,一般为空
    
         oRes.serverIp: 服务端IP,只有在server或resServer、resStatsServer才能获取到
         oRes.statusCode: 响应状态码,同 oRes.serverIp
    
         // 获取抓包数据,不需要等待响应完成
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getReqSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取完整的抓包数据,要等待响应完成
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         }):
         // 获取WebSocket或Socket请求的帧数据列表
         // 返回 1~16 个帧数据
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getFrames((list) => {
           // 如果为空表示该长连接已断开
           if (!list) {
             return;
           }
           // do sth
         }):
       });
     };
    
  7. server:whistle会把指定请求转发到该server

     exports.statsServer = (server, options) => {
       // options 同上,initial.js的options是同一个对象
       server.on('request', (req, res) => {
         const oReq = req.originalReq;
         const oRes = req.originalRes;
    
         req.clientIp: 请求的客户端IP,注意:挂在req里面
    
         oReq.id: 请求的ID,每个请求对应一个唯一的ID
         oReq.headers: 请求的原始headers,而req.headers包含了一些插件自定义字段
         oReq.ruleValue: 配置的规则值, 如:whistle.xxx://ruleValue
         oReq.url: 请求的完整url
         oReq.realUrl: 请求的真实url,一般为空
         oReq.method: 请求方法
         oReq.clientPort: 请求的客户端端口
         oReq.globalValue: pattern @globalValue
         oReq.proxyValue: 配置的代理规则,一般为空
         oReq.pacValue: 配置的pac规则,一般为空
    
         oRes.serverIp: 服务端IP,只有在server或resServer、resStatsServer才能获取到
         oRes.statusCode: 响应状态码,同 oRes.serverIp
    
         // 获取抓包数据,不需要等待响应完成
         req.getReqSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         });
         // 获取完整的抓包数据,要等待响应完成
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getSession((s) => {
           // 如果设置了 enable://hide 会获取到空数据
           if (!s) {
             return;
           }
           // do sth
         });
         // 获取WebSocket或Socket请求的帧数据列表
         // 返回 1~16 个帧数据
         // 这里unsafe主要是提醒不要在回调里面返回规则
         // 如果这样不会触发响应
         req.unsafe_getFrames((list) => {
           // 如果为空表示该长连接已断开
           if (!list) {
             return;
           }
           // do sth
         });
    
         // 请求和响应的拦截器都是可选的
         // 获取请求体和响应体的原始内容(可能是已压缩的内容)
         req.passThrough((rawReqBody, next) => {
           // rawReqBody 为请求体的 Buffer,如果没有请求体,则为 null
           next({
             url: '修改请求地址',
             headers: {},
             body: '新内容 string or buffer',
           });
           // rawReqBody 可能是 gzip 后的内容,可以通过下面方法确保获取未压缩的内容
           req.getBuffer((err, buffer) => {
             if (err) {
               // 如果解压失败,继续请求
               return next();
             }
             // buffer 为未压缩的内容
             next({
               url: '修改请求地址',
               rules: '请求规则',
               body: '新内容 string or buffer',
             });
             // next() 表示保留请求内容继续请求
             // next(null) 表示去除请求内容继续请求
             // 通过修改 req.headers 修改请求头
             // 通过修改 req.method 修改请求方法
           });
    
           req.getText((err, text) => {
             if (err) {
               // 如果解压失败,继续请求
               return next();
             }
             // text 为未压缩的文本内容
             next({
               url: '修改请求地址',
               rules: '请求规则',
               body: '新内容 string or buffer',
             });
             // next() 表示保留请求内容继续请求
             // next(null) 表示去除请求内容继续请求
             // 通过修改 req.headers 修改请求头
             // 通过修改 req.method 修改请求方法
           });
    
           req.getJson((err, json) => {
             if (err) {
               // 如果解压失败,继续请求
               return next();
             }
             // json 为请求表单或 JSON 对象
             next({
               url: '修改请求地址',
               rules: '请求规则',
               body: '新内容 string or buffer',
             });
             // next() 表示保留请求内容继续请求
             // next(null) 表示去除请求内容继续请求
             // 通过修改 req.headers 修改请求头
             // 通过修改 req.method 修改请求方法
           });
    
         }, (rawResBody, next, _res) => {
           // rawResBody 为响应体的 Buffer,如果没有响应体,则为 null
           // rawResBody 可能是 gzip 后的内容,可以通过下面方法确保获取未压缩的内容
           _res.getBuffer((err, buffer) => {
             if (err) {
               // 如果解压失败,继续请求
               return next();
             }
             // buffer 为未压缩的内容
             next({
               rules: '响应规则,如:* replaceStatus://200',
               body: '新内容 string or buffer',
             });
             // next() 表示保留响应内容继续请求
             // next(null) 表示去除响应内容继续请求
             // 通过修改 _res.headers 修改响应头
             // 通过修改 _res.statusCode 修改响应状态码
           });
    
           _res.getText((err, text) => {
             if (err) {
               // 如果解压失败,继续请求
               return next();
             }
             // text 为未压缩的文本内容
             next({
               rules: '响应规则',
               body: '新内容 string or buffer',
             });
             // next() 表示保留响应内容继续请求
             // next(null) 表示去除响应内容继续请求
             // 通过修改 _res.headers 修改响应头
             // 通过修改 _res.statusCode 修改响应状态码
           });
    
           _res.getJson((err, json) => {
             if (err) {
               // 如果解压失败,继续请求
               return next();
             }
             // json 为响应的 JSON 对象
             next({
               rules: '响应规则',
               body: '新内容 string or buffer',
             });
             // next() 表示保留响应内容继续请求
             // next(null) 表示去除响应内容继续请求
             // 通过修改 _res.headers 修改响应头
             // 通过修改 _res.statusCode 修改响应状态码
           });
         });
       });
     };
    

更多例子参考:

  1. https://github.com/whistle-plugins/examples
  2. https://github.com/whistle-plugins

results matching ""

    No results matching ""