大厂技术 坚持周更 精选好文
在《Whistle 实现原理 —— 从 0 开始实现一个抓包工具》一文中给大家详细介绍了如何实现一个抓包调试工具 Whsitle。事实上 Whistle 不仅可以供开发人员本地开发调试,也可以部署到公共服务器给产品运营、测试等非开发人员用来访问测试环境、远程抓包调试。但 Whistle 是单进程服务,无法满足多人多项目同时使用,所以基于 Whistle 开发了多进程多用户的远程代理服务 -- Nohost,其主要功能:
Github 仓库:https://github.com/Tencent/nohost
团队每个成员或业务模块分配一个 Whistle 实例。
每个 Whistle 实例可以设置多个独立的环境。
每个环境可以配置任意 Whistle 规则。
用户可以通过页面选择需要访问的环境。
Nohost 基本架构
有点类似 Node 的 Cluster 模块,其核心思想是:将多进程多问题转成单进程 Whistle 的问题,即给每个团队成员或业务模块分配一个 Whistle 进程,并通过一个 Master 进程来管理各个 Whistle 进程以及分发请求。
交互流程图及实现原理
访问接入 Nohost 的页面,Nohost 会在页面左下角注入一个小圆点(环境切换按钮)(具体原理后面讲)。
如何接入 Nohost 参见:https://nohost.pro/docs/quickstart
点击小圆点弹出环境选择框,点击选择环境后,页面会先将选择的
账号/环境
发送到 Nohost 的 Master 进程。Master 进程会先获取
clientId
(如何获取参见后面的 Master 进程实现原理),将获取的clientId
与选择的账号/环境
存到 Master 进程的内存 LRU Cache 里面。页面等待设置环境的接口响应或超时后自动刷新页面,这时页面所有请求也会经过 Master 进程,Master 进程通过 3 的方式获取每个请求的
clientId
,并根据clientId
获取之前选择的账号/环境
,再将请求转到账号对应的 Whistle 进程及执行对应环境的规则(具体实现参见后面的 Master 进程实现原理)。
这样每个人或业务只需关注自己的 Whistle 进程,功能上也就跟本地的 Whistle 差不多,也可以直接使用所有 Whistle 插件。
Master 进程实现原理
最后看下 Master 进程的实现,Master 进程可以主要有三个功能:
注入小圆点(环境切换按钮)。
记录用户环境选择状态。
Whistle 进程管理与请求分发。
启动(关闭)指定 Whistle 进程并将请求转发到该进程及执行对应环境规则
注入小圆点(环境切换按钮)
小圆点的注入不是简单的往页面追加脚本,还涉及到:
解析 HTTPS 请求(不然 HTTPS 请求无法注入任何内容)。
只对 HTML 页面注入小圆点(还需要排除一些返回 json 数据,但类型写成 HTML 的接口)。Nohost 采用在 Master 里面内置一个 Whistle 进程处理上述问题:
const startWhistle = require('whistle')
其中:
接收请求、解析 HTTPS 请求原理参见:Whistle 实现原理
注入小圆点,主要是由内置插件
whistle.nohost
实现的,分三部分内容:
注入小圆点白名单:在 Nohost 的管理后台上传证书域名,及入口规则配置的域名、路径、正则、通配符等。
插件的
rules.txt
动态获取白名单,并配置规则将上述白名单请求转发到插件。插件的
_rules.txt
配置的私有规则,对请求到插件的 html 页面注入小圆点。
这里引入了一个插件 whistle.nohost
,该插件除了上述注入小圆点功能以外,还有如下的功能。
记录用户环境选择状态
点击选择环境,页面会发送请求 /.whistle-path.5b6af7b9884e1165/whistle.nohost/cgi-bin/select?name=imweb&envId=test&time=1637026271911
,其中:
/.whistle-path.5b6af7b9884e1165
是一个特殊路径,Whistle 自动拦截该路径并作为内部请求。name
:账号(分组)名称。envId
:环境名称的编码,encodeURIComponent(envName)
。
该请求会转到 Nohost 的内置的 whistle.nohost
插件进程的 uiServer
,该插件会通过以下方式获取 clientId
:
先尝试从请求头
x-whistle-client-id
获取clientId
。如果没有请求头
x-whistle-client-id
,则分别通过请求头x-forwarded-for
或req.socket.remoteAddress
获取clientIp
作为clientId
。
将获取的 clientId
和 name/env
存到插件进程的 LRU Cache:lru.set(clientId, name/env)
。
Whistle 进程管理与请求分发
对 Worker 里面的 Whistle 进程的管理也是通过 whistle.nohost
插件实现的:
插件通过实现
rulesServer
和tunnelRulesServer
设置转发规则。插件用上面的方式获取
clientId
并根据clientId
获取用户选择的账号和环境。如果没有选择任何环境,则返回空字符串,请求自动转到现网。
如果有选择环境,则通过 pfork 启动一个 Whistle 进程,并自动分配一个随机端口。
启动 Whistle 成功后获取端口,设置规则
* internal-proxy://127.0.0.1:xxxxx
让 Master 的 Whistle 进程将请求转到该端口。
internal-proxy 是 Whistle 代理直接转发协议,它可以通过用 http 的方式转发 https 请求
Nohost 插件会自动根据请求需求拉起对应进程,且如果该进程有超过 6 分钟没有请求,则会自动关闭。可以通过 Whistle 提供的方法检测进程是否有请求详见:https://github.com/Tencent/nohost/blob/master/lib/plugins/whistle.nohost/lib/whistle.js#L126。
插件的完整实现代码参见:https://github.com/Tencent/nohost/blob/master/lib/plugins/whistle.nohost/lib/rulesServer.js
参考资料
Github 仓库:https://github.com/Tencent/nohost
详细文档:https://nohost.pro/
更多文章
浏览器缓存库设计总结(localStorage/indexedDB)