Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何实现一个HTTP/HTTPS代理客户端 #38

Open
fwon opened this issue Apr 7, 2017 · 1 comment
Open

如何实现一个HTTP/HTTPS代理客户端 #38

fwon opened this issue Apr 7, 2017 · 1 comment

Comments

@fwon
Copy link
Owner

fwon commented Apr 7, 2017

原理

作为一个合格的前端工程师,你一定用过Fiddler或Charles之类的抓包工具。但是在Mac上做开发时,相关的抓包工具很多是收费的。当你费劲心思下载到了破解版,却还是难以忍受其丑陋的win风格界面和令人悲伤的闪退问题。有没有想过自己来实现一个代理客户端呢?其实这个真的可以有。

中间人

一个http代理服务器的原理很简单。有了Nodejs作为武器,创建一个代理服务器就是分分钟的事。具体可参见jerryQu写的两篇文章 《HTTP 代理原理及实现(一)》 《HTTP 代理原理及实现(二)》 文章对HTTP代理的原理和实践讲得比较清楚。

简单来讲就是要实现一个中间人,用户通过设置代理,网络请求就会通过中间人代理,再发往正式服务器。

这种中间人的实现方式有两种。
一种为普通的HTTP代理,通过Node.js开启一个HTTP服务,并将我们的浏览器或手机设置到该服务所在的ip和端口,那么HTTP流量就会经过该代理,从而实现数据的拦截。
pic

对于非HTTP请求,比如HTTPS, 或其他应用层请求。可以通过在Node.js 中开启一个TCP服务,监听CONNECT请求,因为应用层也是基于传输层的,所以数据在到达应用层之前会首先经过传输层,从而我们能实现传输层数据监听。
pic

但是对于CONNECT捕抓到的请求,无法获取到HTTP相关的信息,包括头信息等,这对一般的前端分析作用不大,那么想要真正监听HTTPS,还需要支持证书相关的验证。

证书

假设我们是通过浏览器设置代理进行抓包实验(或全局代理),在这个过程中我们主要关注的是浏览器和代理服务器之间的交互,这个过程大概如下:

  1. 浏览器客户端发出了一个请求,该请求会首先经过代理服务器。
  2. 代理服务器获取到客户端请求,知道了真实服务器的地址,它可能会做一些手脚,比如对请求数据进行修改,再发往真实服务器,获取到数据再返回给浏览器(利用这一点能实现跨域支持等)。或者代理服务器压根就不会请求真实服务器,而是直接伪造一份假数据给浏览器(利用这一点能实现接口mock)。
  3. 浏览器接收到数据,并返回给用户,显示在页面。

上面这三步在HTTP中会无比流畅,然而如果请求是HTTPS,浏览器会验证代理服务器的安全性。这里会涉及到TLS握手的过程,其中也包括了证书的验证。

代理服务器返回HTTPS请求时,需要将对应请求域名的证书发给浏览器,浏览器再向本地的CA根证书验证域名证书的安全性。如果验证通过,则继续后续请求,验证失败浏览器会返回安全警告。
pic

这里提到了两个证书,一个是域名证书,一个是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根证书已经被我们本地信任了,所以浏览器也会信任该域名证书,成功返回代理服务器的数据。

pic

具体的操作流程是这样的,首先利用Node.js 生成AnyProxy CA证书,并手动信任。浏览器往cn.vuejs.org发出请求,代理服务器拦截到请求,知道请求是发往cn.vuejs.org,在返回数据之前,利用AnyProxy证书动态签发cn.vuejs.org域名证书,放于本地(用于下次请求,这里省去了中间证书的步骤),同时将该域名证书返回给浏览器。那么浏览器在接受到cn.vuejs.org域名证书后,往证书链上寻找到根证书AnyProxy,并通过AnyProxy证书验证域名证书是否受信任,同时还要检查域名证书的有效性,包括过期时间等。由于域名证书是我们通过AnyProxy动态创建的,所以保证了其受信任和有效性。最后浏览器返回代理服务器结果。从而实现了HTTPS请求的抓取。

具体的证书签发实现可参考 forge 库,现在广泛使用的证书是X.509格式。

Electron

解决了证书问题,可以说已经完成了一大半的工作,那如何快速实现一个代理客户端呢?对于一个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为例:
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组件中就可以直接通过this.$remoteApi调用基于Nodejs或Electron的接口了。这样就有效地分离了前端界面和客户端的代码,只要剥离了$remoteApi, 前端界面也可作为一个独立的项目进行开发。

方案优缺点

Electron的这种实现方式也不是什么新鲜套路了,对于NW.js 有的大多数缺点Electron也有。

其中一个通病就是性能问题,主要是渲染性能方面。基于webkit引擎来渲染UI界面,跟原生的系统UI还是有一定的差距。毕竟是基于DOM节点的渲染,每次节点的重排都是一次大的开销。这点只能通过在前端框架中来优化,比如利用Virtual DOM等相关技术。而视觉上的缺点则可以通过CSS做到竟可能接近原生控件。

而对于JS的执行性能,v8表示hold得住。

优点当然也比较明显,对比于Cocoa,Qt等传统桌面客户端技术,基于前端技术的实现成本较低(C++牛请忽略)跨平台支持更好(框架都帮你做好了),且天然支持热更新。

更重要的是,有这么多优秀软件帮你背书啊.....以下都是基于Electron开发。
pic

当然,我并不是在安利Electron。毕竟别人能开发得这么原生态,你不一定行...

关键还是看技术,Electron是完全能够开发出中大型产品级的软件的。

说了这么多,代码呢?

能读到这里,感谢你的坚持!

下面基于以上理论实现的代理客户端。目前支持以下功能:

  1. 支持HTTP/HTTPS请求抓取。
  2. 支持网速模拟。
  3. 支持请求拦截修改,实现跨域等功能。
  4. 实现接口Mock,用于本地开发调试。

源码地址 欢迎试玩。

@light0x00
Copy link

正是我想要的 感谢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants