好久没写博客了。在元旦到来之前,我会发一篇文章,谈谈我在实现代理服务器的过程中遇到的一些坑。同时祝各位读者新年快乐。背景长期以来,贴吧开发人员多,业务耦合大,需求变更频繁,容易出现bug。我负责的广告相关业务与UI息息相关。一旦因为某种原因出现bug(甚至代码被别人改了),势必会极大地影响广告收入。解决问题的方法之一是经常测试。由于无法避免代码层面的耦合,所以通过定期检查总是可以避免出现问题。所以我们维护了一套核心案例,狠抓核心功能。选择核心案例实际上是覆盖率和测试成本之间的权衡。但是,多个case的测试步骤不同,测试效率始终难以提升。因此,我们的目标是构建一个代理服务器,可以在运行时将任何包(包括在线包)的数据更改为我想要的。也就是说,这个代理服务器也可以理解为一个私有服务器,可以获取和修改客户端的请求数据,也可以获取和修改服务器的响应数据。代理服务器工作模型在早期的版本中,我们选择了简单的HTTP协议。这种选择具有最高的技术要求。我们自己实现了一个代理服务器,打开socket,监听端口,然后把客户端的请求发送给服务器,再把服务器的返回数据发回给客户端。这种模式也被称为:“中间人模式”(MITM:ManInTheMiddle)。虽然道理很简单,但是在实现的时候还是有一些需要注意的地方。首先,当socket接受数据时,要开启一个新的进程/线程进行处理。既然涉及到一个新的进程/线程,那么我们就要注意它的释放时机,否则会导致内存约束增加。其次,对于socket来说,它没有等待的功能,也就是说我无从知道什么时候有数据可以读取,所以这个难的任务就交给了select。我们传入需要监听的socket对象作为参数,函数会一直阻塞,直到有可读或可写的对象,或者超时时间到了。Keep-Alive字段可以重用TCP连接,这是一种常见的HTTP协议优化方式,在HTTP1.1中已经是默认选项。填写该字段后,服务器返回的数据可能是批量的,可以提高用户体验,但也会增加代理服务器的实现难度。因此,当代理服务器作为客户端向真实服务器请求数据时,应该删除该字段。由于整个流程都是自己实现的,所以挂接、下行数据和修改都比较容易。只注意接收到所有数据后修改,即整个过程可以简单用下图表示:技术选择短连接由于长连接是基于TCP的,不需要每次都新建连接,并且省略了不必要的HTTP头,效率明显优于HTTP。所以各大公司在实际生产环境中基本都选择长连接作为连接方式。但是由于对WebSocket协议不熟悉,而且我们还支持短连接,代理服务器最终选择了HTTP协议。为此,在应用程序启动时,模拟后台向客户端发送一条控制信息,强制客户端选择一个HTTP请求。这样即使是在线的数据包也可以通过代理服务器。HTTPS因为苹果强制使用HTTPS而推迟,但明年也是一个趋势。考虑到后续的使用,我们决定对之前实现的代理服务器进行升级。由于HTTPS涉及解析请求协议,以及加解密、证书管理等,上述自研方案难以hold住。经过一番研究,***选择了一个比较知名的开源库mitmproxy。Mitmproxy选择这个库的主要原因是它直接支持HTTPS,但是没有中文文档,而且国内使用比较少,所以访问起来可能需要一点时间。这是一个python库。首先,安装虚拟环境。如果本地没有安装,输入:sudopipinstallvirtualenv安装完成后,进入mitmproxy/venv3.5/bin文件夹,输入:source./active,开启virtualenv环境。Hook脚本库可以理解为Charles在命令行的交互版本,但我不打算使用它的功能。因为我的需求主要是使用脚本来hook请求,所以选择了mitmdump这个工具。使用时,指定脚本即可:mitmdump-s"xxx.py"脚本也很简单,我们可以重写请求或接收函数:defrequest(flow):flow.response.content="
helloworld
"运行脚本后,将手机的代理设置为本地ip地址,将端口号修改为8080,然后用手机浏览器打开mitm.it/。如果一切配置顺利,你会看到证书安装界面。安装证书后,用手机访问任意网站(包括HTTPS),应该会看到一个小小的helloworld,至此所有配置完成。错误修复这个开源库有一个严重的错误,在解析多部分数据时可能会发生。它使用splitline的方法来分割换行,但是如果数据中有\n,就会丢失。不幸的是,很多protobuf编码的数据都有\n,如果丢失会导致解析失败。如果不幸遇到和我一样的坑,可以将相关代码改成我的版本:foriincontent.split(b"--"+boundary):parts=i.split(b'\r\n\r\n',2)iflen(parts)>1andparts[0][0:2]!=b"--":match=rx.search(parts[0])ifmatch:key=match.group(1)value=parts[1][0:len(parts[1])-2]#Removelast\r\nr.append((key,value))More至此,基本上一个支持HTTPS的代理服务器已经实现成功。接下来要处理的可能是解析protobuf、完善业务代码等小事,只要细心,基本不会出问题。