作为一个合格的前端工程师,你一定用过 Fiddler 或 Charles 之类的抓包工具。但是在 Mac 上做开发时,相关的抓包工具很多是收费的。当你费劲心思下载到了破解版,却还是难以忍受其丑陋的 win 风格界面和令人悲伤的闪退问题。有没有想过自己来实现一个代理客户端呢?其实这个真的可以有。
一个 http 代理服务器的原理很简单。有了 Nodejs 作为武器,创建一个代理服务器就是分分钟的事。具体可参见 jerryQu 写的两篇文章 《HTTP 代理原理及实现(一)》 《HTTP 代理原理及实现(二)》 文章对 HTTP 代理的原理和实践讲得比较清楚。
简单来讲就是要实现一个中间人,用户通过设置代理,网络请求就会通过中间人代理,再发往正式服务器。
这种中间人的实现方式有两种。
一种为普通的 HTTP 代理,通过 Node.js 开启一个 HTTP 服务,并将我们的浏览器或手机设置到该服务所在的 ip 和端口,那么 HTTP 流量就会经过该代理,从而实现数据的拦截。
对于非 HTTP 请求,比如 HTTPS, 或其他应用层请求。可以通过在 Node.js 中开启一个 TCP 服务,监听 CONNECT 请求,因为应用层也是基于传输层的,所以数据在到达应用层之前会首先经过传输层,从而我们能实现传输层数据监听。
但是对于 CONNECT 捕抓到的请求,无法获取到 HTTP 相关的信息,包括头信息等,这对一般的前端分析作用不大,那么想要真正监听 HTTPS,还需要支持证书相关的验证。
假设我们是通过浏览器设置代理进行抓包实验(或全局代理),在这个过程中我们主要关注的是浏览器和代理服务器之间的交互,这个过程大概如下:
上面这三步在 HTTP 中会无比流畅,然而如果请求是 HTTPS,浏览器会验证代理服务器的安全性。这里会涉及到 TLS 握手的过程,其中也包括了证书的验证。
代理服务器返回 HTTPS 请求时,需要将对应请求域名的证书发给浏览器,浏览器再向本地的 CA 根证书验证域名证书的安全性。如果验证通过,则继续后续请求,验证失败浏览器会返回安全警告。
这里提到了两个证书,一个是域名证书,一个是 CA 根证书。
域名证书是每个支持 HTTPS 网站都需要有的一份证书,用于客户端验证该网站的安全性,而该证书通常是通过安全机构申请的,这个机构就是 CA(Certificate Authority,证书颁发机构) 。在每台用户计算机的操作系统或浏览器中,都会保存一份 CA 列表,也就是有多个根证书,不同 CA 分别包含了不同的域名证书,浏览器在获取到域名证书之后,会向 CA 根证书进行验证,如果验证通过则能正常收发请求。
对于代理服务器来说,我们并没有合法的域名证书(证书只存在真实目标服务器,无法获取到),怎么让浏览器相信我们是个安全的代理(服务器)呢?答案是————伪造!
没错,我们既要伪造域名证书,也要伪造根证书。其实根证书是可以自己签发的。下面两条命令首先生成了一个私钥,然后利用私钥生成 crt 证书,我们只要双击 crt 文件进行安装,并设置为信任,就成功建立了一个本地根证书。
- openssl genrsa -out private.pem 2048
- openssl req -new -x509 -key private.pem -out public.crt -days 99999
利用根证书,我们能够签发更多的域名证书。证书是链式验证的,验证域名证书的时候,会往上验证 CA 根证书,由于 CA 根证书已经被我们本地信任了,所以浏览器也会信任该域名证书,成功返回代理服务器的数据。
具体的操作流程是这样的,首先利用 Node.js 生成 AnyProxy CA 证书,并手动信任。浏览器往 cn.vuejs.org 发出请求,代理服务器拦截到请求,知道请求是发往 cn.vuejs.org,在返回数据之前,利用 AnyProxy 证书动态签发 cn.vuejs.org 域名证书,放于本地(用于下次请求,这里省去了中间证书的步骤),同时将该域名证书返回给浏览器。那么浏览器在接受到 cn.vuejs.org 域名证书后,往证书链上寻找到根证书 AnyProxy,并通过 AnyProxy 证书验证域名证书是否受信任,同时还要检查域名证书的有效性,包括过期时间等。由于域名证书是我们通过 AnyProxy 动态创建的,所以保证了其受信任和有效性。最后浏览器返回代理服务器结果。从而实现了 HTTPS 请求的抓取。
具体的证书签发实现可参考 forge 库,现在广泛使用的证书是 X.509 格式。
解决了证书问题,可以说已经完成了一大半的工作,那如何快速实现一个代理客户端呢?对于一个 JSer 来说,能利用 Node.js 来写是最好不过了。
Electron 大家应该不陌生了,它提供了一种解决方案,让我们能够利用 Node.js 和 前端三宝 html + JS + CSS 来实现客户端软件。咋一听感觉像 NW.js。经过一番了解,才知道其实 NW.js 可以算是 Electron 的前身了,都是出自同个作者之手,只不过该作者现在维护 Electron 去了,这其中涉及到一些产权的问题,感兴趣的可以围观一下知乎上 原作者的回答 。关于 Electron 和 NW.js 的区别 官网上是这么说的 。简单讲就是 Electron 优化了 NW.js 中的一些不足。 秉着与时俱进的态度,我们当然要使用 Electron。
有了 Electron 作为容器,我们小前端就可以用 HTML+JS+CSS 来开发客户端了。就像开发前端页面一样柔顺。Electron 的使用比较简单,提供的 API 也比较清晰。核心概念就是 Main Process 和 Render Process。
顾名思义 Main Process 是主进程,用于运行 Electron 的基本操作,如创建窗口,创建菜单等。Render Process 是渲染进程,我们需要在渲染进程中创建软件界面,每个渲染进程对应的是一个窗口,主进程开启了多个窗口就会有多个渲染进程。
Electron 提供了 IPC 用于进程间通信。分别是 ipcMain 和 ipcRender。该通信机制允许 ipcRender 向 ipcMain 发送信号请求,并通过 ipcMain 返回数据。反回来 ipcMain 无法向特定的 ipcRender 发起请求。而且通信间传递的消息会被格式化为 JSON 字符串,所以并不支持在两个进程间传递句柄方法等,也就是不支持上下文传递。
Arguments will be serialized in JSON internally and hence no functions or prototype chain will be included.
假如要实现在渲染进程中点击一个按钮,则关闭客户端窗口,可以通过 ipcRender 发送一个信号给 ipcMain, ipcMain 接收到该信号后调用 Electron 的 API 关闭窗口。对于类似这种比较简单的指令操作,运用 IPC 实现就可以了,但是如果操作比较复杂,并且需要传递复杂数据类型,则用 IPC 就行不通了。
Electron 提供了另一个 API remote ,用于在 Render Process 中直接操作主进程的方法。这样就不需要移交 Main Process 处理,直接在前端页面中调用 Electron 的 API。
由于 Electron 本身包含了 chromium 和 Node.js 的代码, 所以不考虑项目本身体积,打包后的软件最小仍然有 100M+, 这也是 Electron 最为显著的缺点之一。所以基本体积是无法避免的,我们只能尽量减小其他开发文件的大小,避免将一些无关包文件也打包进去。
为什么要强调这点呢?因为基于 Node.js 开发的项目往往会有一个庞大的 node_modules 文件夹,里面包含了一些开发和生产所用的包,也即对应 package.json 中的 dependencies 和 devDependencies。而 devDependencies 中的包是不需要打包到软件的。这里推荐使用 electron-packager , 能自动排除 dev 依赖包,并支持自定义排除包文件夹。也可以打包出支持不同系统格式的软件。
界面开发采用传统前端页面开发方式,意味着你可以使用任何前端框架,利用 Angular,Vue,React 等框架来提升开发效率。
这些框架都支持模块化,利用 webpack 等打包工具,webpack 本身会提供 require 等模块加载的方法,在前端开发的时候能实现类似后端的模块动态加载。
但是,当我们在 Render Process 中使用 webpack 进行开发,用 require 引入模块的时候就会出现冲突。因为 require 此时是 webpack 提供的一个引用本地文件的 API,而不是 Node.js 的 require, 导致我们无法通过 require 来引用 Node.js 的 API,或者 Electron 的 API。这有什么解决方案吗?
这里提供一个简单的方法,我们将需要用到 Node.js API 和 Electron API 的方法抽象到 renderer.js, 从 HTML 中单独引入,也就避免了 webpack 对 renderer.js 进行处理。然后通过插件的方式引入到前端框架中,以 Vue 为例:
index.html
- <script src="renderer.js">
- </script>
- <!--提供Render Process 方法 -->
- <script src="./dist/build.js">
- </script>
- <!--webpack 打包文件-->
renderer.js
- const electron = require('electron');
- const remote = electron.remote;
- const remoteApi = remote.require('./api.js');
- global.remoteApi = remoteApi;
Vue 入口文件 main.js
- Vue.use({
- install (Vue, options) {
- //添加实例方法
- Vue.prototype.$remoteApi = global.remoteApi;
- }
- });
在 Vue 组件中就可以直接通过
调用基于 Nodejs 或 Electron 的接口了。这样就有效地分离了前端界面和客户端的代码,只要剥离了 $remoteApi, 前端界面也可作为一个独立的项目进行开发。
- this.$remoteApi
Electron 的这种实现方式也不是什么新鲜套路了,对于 NW.js 有的大多数缺点 Electron 也有。
其中一个通病就是性能问题,主要是渲染性能方面。基于 webkit 引擎来渲染 UI 界面,跟原生的系统 UI 还是有一定的差距。毕竟是基于 DOM 节点的渲染,每次节点的重排都是一次大的开销。这点只能通过在前端框架中来优化,比如利用 Virtual DOM 等相关技术。而视觉上的缺点则可以通过 CSS 做到竟可能接近原生控件。
而对于 JS 的执行性能,v8 表示 hold 得住。
优点当然也比较明显,对比于 Cocoa,Qt 等传统桌面客户端技术,基于前端技术的实现成本较低(C++ 牛请忽略)跨平台支持更好(框架都帮你做好了),且天然支持热更新。
更重要的是,有这么多优秀软件帮你背书啊..... 以下都是基于 Electron 开发。
当然,我并不是在安利 Electron。毕竟别人能开发得这么原生态,你不一定行...
关键还是看技术,Electron 是完全能够开发出中大型产品级的软件的。
说了这么多,代码呢?
能读到这里,感谢你的坚持!
下面基于以上理论实现的代理客户端。目前支持以下功能:
源码地址 欢迎试玩。
来源: http://www.tuicool.com/articles/za6ruaM