Littleproxy的使用
介绍
LittleProxy是一个用Java编写的高性能HTTP代理,它基于Netty事件的网络库之上。 它非常稳定,性能良好,并且易于集成到的项目中。
项目页面: https://github.com/adamfisk/LittleProxy
这里介绍几个简单的应用,其它复杂的应用都是可以基于这几个应用进行改造。
- 按域名或者url进行拦截和过滤
- 修改http头,修改请求参数
- 修改返回Response数据
- 中间人代理,截取https的数据
前置知识
因为代理库是基于netty事件驱动,所以需要对netty的原理有所了解
因为是对http协议进行处理,所以需要了解io.netty.handler.codec.http
包下的类。
因为效率,大部分数据是由ByteBuf
进行管理的,所以需要了解ByteBuf
相关操作。
io.netty.handler.codec.http
包的相关介绍
主要接口图:
- HttpObject
- httpContent(http协议体的抽象,比如POST数据的体,和响应数据的体)
- LastHttpContent
- HttpMessage(http协议头的抽象,包含请求头和响应头)
- FullHttpMessage(也继承于LastHttpContent)
- HttpRequest
- FullHttpRequest(也继承于FullHttpMessage)
- HttpResponse
- FullHttpResponse(也继承于FullHttpMessage)
- httpContent(http协议体的抽象,比如POST数据的体,和响应数据的体)
主要类:
类主要是对上面接口的实现
- DefaultHttpObject
- DefautlHttpContent
- DefaultLastHttpContent
- DefaultHttpMessage
- DefaultHttpRequest
- DefaultFullHttpRequest
- DefaultHttpResponse
- DefaultFullHttpResponse
- DefaultHttpRequest
- DefautlHttpContent
更多可以参考API文档 https://netty.io/4.1/api/index.html
辅助类io.netty.handler.codec.http.HttpHeaders.Names
io.netty.buffer.ByteBuf
的相关使用
主要使用是Unpooled
和ByteBufUtil
- 把string转化为ByteBuf,使用
Unpooled.wrappedBuffe
- 把ByteBuf转化为String ,使用
toString(Charset.forName("UTF-8")
- 格式输出ByteBuf,使用
ByteBufUtil.prettyHexDump(buf);
基本流程代码
示例代码
1 |
|
代码分析:
- 启动代理类
- 实现
HttpFiltersSourceAdapter
的filterRequest
函数 - 实现
HttpFiltersAdapter
的4个关键性函数,并打印日志
HttpFiltersAdapter
分别是:
- clientToProxyRequest(默认返回null,表示不拦截,若返回数据,则不再经过P2S和S2P。这里可以修改数据)
- proxyToServerRequest(这里的原理与上面一条一样,基本原封不动)
- serverToProxyResponse(这里默认返回传入参数,可以做一定的修改)
- proxyToClientResponse(与上面一条类似)
这个流程符合普通代理的流程。
请求数据 C -> P -> S,
响应数据 S -> P -> C
预期代码输出会是 1,2,3,4
按顺序执行
但实际运行结果(省略若干非关键性信息):
1 |
|
可以看出:
- 请求和响应都是分次传输(因为默认Buf容量1024),中间代理并没有收集所有数据之后,再发往C或者S
- 请求和响应分次的结束都是以
Last-xx
这样结束的。 - 如果需要修改请求数据的话,可能需要自己编码,把数据保存下来,再进行发送
修改请求参数
比如这里实现了把每次百度搜索的关键字加一个前缀的功能。
主要原理是修改DefaultHttpRequest
的url中所带的参数(只能修改GET方式的参数)
如果需要修改POST的内容,同样的原理,不过是要修改Request的内容体。
1 |
|
replaceParam函数就是把搜索的关键字提取出来,并添加前缀,然后拼接成新url。
1 |
|
拦截指定域名或者URL
按上面基础代码重写clientToProxyRequest或者proxyToServerRequest。
如果是指定域名,如hm.baidu.com
就返回一个空的response。这个请求就不会继续请求服务端。
如果是多个域名,使用set来存储。如果是需要按后缀,可以用后缀树。
1 |
|
修改返回内容
修改内容会涉及几个很麻烦的事
- 压缩
- chunked(
Transfer-Encoding: chunked
)
对于压缩
简单的做法就是修改请求报文,让请求头不支持压缩算法,服务器就不会对内容进行压缩。
复杂的办法就是记录响应头,老实进行解压。
解码之后再修改内容,内容修改好之后,再进行压缩。
对于chunked
没有什么好的办法,在Response中去掉标识,然后按次拼接,服务器来的块,拼接好,修改好后,一次返回给客户端。
代码很长就不贴出来了。
但写proxyToClientResponse
函数中拼报文时,有几个注意事项:
- 不能直接返回null(客户端会报错),要返回
return new DefaultHttpContent(Unpooled.EMPTY_BUFFER);
一个空的response。 - httpObject的类型,在非chunked是几个
DefaultHttpContent
,最后一个DefaultLastHttpContent
,判断语句Lastxx要写在前面,因为后面是前面的子类(先判断范围小的,再判断范围大的)。 - chunked的方式下是几个
DefaultHttpContent
,最后一个LastHttpContent
,写法同上。 - 一个请求会对应
HttpFiltersAdapter
一个实例,状代码可以写成类成员变量。
中间人代理
中间人代理可以在授信设备安装证书后,截取https流量。
littleproxy实现中间人的方式很简单,实现MitmManager
接口,在启动类中调用withManInTheMiddle
方法。
MitmManager
接口要求返回SSLEngine
对象,实现SslEngineSource
接口。
SSLEngine
对象是要通过SSLContext
调用createSSLEngine
而SSLContext
的初始化,需要证书文件,又涉及CA认证签名体系。
然后https流量会先进行解包,和普通http一样,可以通过上面的手段进行捕获,然后再用自己的证书进行签名
目前使用openssl实现了一个版本。
启动器
1 |
|
SslEngineSource实现类
1 |
|
代理链
代理链的主要作用提供地址的路由。
比如指定x地址,走A代理,指定B地址走Y代理。
主要用到ChainedProxyManager
及ChainedProxyAdapter
类。
示例代码:
1 |
|
可以实现lookupChainedProxies
方法,按httpReqeust的条件,添加不同的代理链,走不同的路径。
测试方法
curl 按 -x 可以指定代理
1 |
|
验证第一段代码效果。
总结
关于http协议的解析,的确可以好好的看看netty上的代码怎么写的,代码比较简洁,主要是关注的包的解析。
当然,在little提供的hook方法中,是需要自己控制http的相关状态,比如报文长度,拼接,及压缩。
还存在的问题
1,代码在windows上执行没有问题,中间人代理部分的代码但在linux上会有问题,在执行nativeCall时,存在第一个文件没有生成就执行第二条命令,这里还需要参考下面的代码不使用命令行的方式,直接用java代码生成jks证书。
2,在应用在浏览器上做屏蔽时,出现在代理代码中已经把改连接断开,但浏览器还在等待的问题