介绍
LittleProxy是一个用Java编写的高性能HTTP代理,它基于Netty事件的网络库之上。它非常稳定,性能良好,并且易于集成到的项目中。
项目页面:https://github.com/adamfisk/LittleProxy
这里介绍几个简单的应用,其它复杂的应用都是可以基于这几个应用进行改造。
- 按域名或者URL进行拦截和过滤
- 修改HTTP头,修改请求参数
- 修改返回响应数据
- 中间人代理,截取HTTPS的数据
前置知识
因为代理库是基于网状事件驱动,所以需要对网状原理的了解有所
因为的英文对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
- 把字符串转化为ByteBuf,使用
Unpooled.wrappedBuffe
- 把ByteBuf转化为String,使用
toString(Charset.forName("UTF-8")
- 格式输出ByteBuf,使用
ByteBufUtil.prettyHexDump(buf);
基本流程代码
示例代码
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
35
|
public static void main(String [] args){
HttpProxyServer server = DefaultHttpProxyServer.bootstrap()。withPort(8181)
.withFiltersSource(new HttpFiltersSourceAdapter(){
@覆盖
public HttpFilters filterRequest(HttpRequest req,ChannelHandlerContext ct){
返回新的HttpFiltersAdapter(req){
@覆盖
public HttpResponse clientToProxyRequest(HttpObject httpObject){
System.out.println(“1-”+ httpObject);
return super.clientToProxyRequest(httpObject);
}
@覆盖
public HttpResponse proxyToServerRequest(HttpObject httpObject){
System.out.println(“2-”+ httpObject);
return super.proxyToServerRequest(httpObject);
}
@覆盖
public HttpObject serverToProxyResponse(HttpObject httpObject){
System.out.println(“3-”+ httpObject);
return super.serverToProxyResponse(httpObject);
}
@覆盖
public HttpObject proxyToClientResponse(HttpObject httpObject){
System.out.println(“4-”+ httpObject);
return super.proxyToClientResponse(httpObject);
}
};
}
})。开始();
}
|
代码分析:
- 启动代理类
- 实现
HttpFiltersSourceAdapter
的filterRequest
函数 - 实现
HttpFiltersAdapter
的4个关键性函数,并打印日志
HttpFiltersAdapter
分别是:
- clientToProxyRequest(默认返回空值,表示不拦截,若返回数据,则不再经过P2S和S2P。这里可以修改数据)
- proxyToServerRequest(这里的原理与上面一条一样,基本原封不动)
- serverToProxyResponse(这里默认返回传入参数,可以做一定的修改)
- proxyToClientResponse(与上面一条类似)
这个流程符合普通代理的流程。
请求数据C - > P - > S,
响应数据S - > P - > C
代码预期会输出的英文1,2,3,4
按顺序执行
但实际运行结果(省略若干非关键性信息):
1
2
3
4
五
6
7
8
9
10
11
12
|
1-DefaultHttpRequest(decodeResult:success,version:HTTP / 1.1)
2-DefaultHttpRequest(decodeResult:success,version:HTTP / 1.1)
1-EmptyLastHttpContent
2- EmptyLastHttpContent
3-DefaultHttpResponse(decodeResult:success,version:HTTP / 1.1)
4-DefaultHttpResponse(decodeResult:success,version:HTTP / 1.1)
3-DefaultHttpContent(data:SlicedAbstractByteBuf(ridx:0,widx:624,cap:624/624,),)
4-DefaultHttpContent(data:SlicedAbstractByteBuf(ridx:0,widx:624,cap:624/612,:)),
3-DefaultHttpContent(data:SlicedAbstractByteBuf(ridx:0,widx:1024,cap:1024/1024,:,)
4-DefaultHttpContent(data:SlicedAbstractByteBuf(ridx:0,widx:1024,cap:1024/1024,:)),
3-DefaultLastHttpContent(data:SlicedAbstractByteBuf(ridx:0,widx:733,cap:733/733,:)),
4-DefaultLastHttpContent(data:SlicedAbstractByteBuf(ridx:0,widx:733,cap:733/733,:)),
|
可以看出:
- 请求和响应都是分次传输(因为默认BUF容量1024),中间代理并没有收集所有数据之后,再发往Ç或者小号
- 状语从句:请求响应分次的结束都是以
Last-xx
这样结束的。 - 如果需要修改请求数据的话,可能需要自己编码,把数据保存下来,再进行发送
修改请求参数
比如这里实现了把每次百度搜索的关键字加一个前缀的功能。
主要原理的英文修改DefaultHttpRequest
的URL中所带的参数(只能修改GET方式的参数)
如果需要修改POST的内容,同样的原理,不过是要修改请求的内容体。
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@覆盖
public HttpResponse proxyToServerRequest(HttpObject httpObject){
if(httpObject instanceof DefaultHttpRequest)
{
DefaultHttpRequest dhr =(DefaultHttpRequest)httpObject;
String url = dhr.getUri();
String host = dhr.headers()。get(HttpHeaders.Names.HOST);
String method = dhr.getMethod()。toString();
if(method.equals(“GET”)&& host.equals(“www.baidu.com”))
{
尝试{
dhr.setUri(replaceParam(URL));
} catch(例外e){
e.printStackTrace();
}
}
}
return null;
}
|
replaceParam函数就是把搜索的关键字提取出来,并添加前缀,然后拼接成新的网址。
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static public String replaceParam(String url)抛出异常
{
String add_str =“你好”;
String paramKey =“&wd =”;
int wd_start = url.indexOf(paramKey);
int wd_end = -1;
if(wd_start!= -1)
{
wd_end = url.indexOf(“&”,wd_start + paramKey.length());
}
if(wd_end!= - 1)
{
String key = url.substring(wd_start + paramKey.length(),wd_end);
String new_key = URLEncoder.encode(add_str,“UTF-8”)+ key;
String new_url = url.substring(0,wd_start + paramKey.length())
+ new_key + url.substring(wd_end,url.length());
返回new_url;
}
返回网址;
}
|
拦截指定域名或者URL
按上面基础代码重写clientToProxyRequest或者proxyToServerRequest。
如果是指定域名,如hm.baidu.com
就报道查看一个空的响应。这个请求就不会继续请求服务端。
如果是多个域名,使用集来存储。如果是需要按后缀,可以用后缀树。
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@覆盖
public HttpResponse proxyToServerRequest(HttpObject httpObject){
if(httpObject instanceof DefaultHttpRequest)
{
DefaultHttpRequest dhr =(DefaultHttpRequest)httpObject;
String url = dhr.getUri();
String host = dhr.headers()。get(HttpHeaders.Names.HOST);
String method = dhr.getMethod()。toString();
if(“hm.baidu.com”.endsWith(host)&&!method.equals(“CONNECT”))
{
返回new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK);
}
如果(!method.equals( “CONNECT”))
{
System.out.println(方法+“http://”+ host + url);
}
}
return null;
}
|
修改返回内容
修改内容会涉及几个很麻烦的事
- 压缩
- chunked(
Transfer-Encoding: chunked
)
压缩对于
简单的做法就是修改请求作者:文,让请求头不支持压缩算法,服务器就不会对内容进行压缩。
复杂的办法就是记录响应头,老实进行解压。
解码之后再修改内容,内容修改好之后,再进行压缩。
对于分块
没有什么好的办法,在响应中去掉标识,然后按次拼接,服务器来的块,拼接好,修改好后,一次返回给客户端。
。很代码长就不贴出来了
但写proxyToClientResponse
函数中拼作者:文时,有几个注意事项:
- 不能直接返回空(客户端会报错),报道查看要
return new DefaultHttpContent(Unpooled.EMPTY_BUFFER);
一个空的响应。 - httpObject的类型,在非分块是几个
DefaultHttpContent
,最后一个DefaultLastHttpContent
,判断语句Lastxx要写在前面,因为后面是前面的子类(先判断范围小的,再判断范围大的)。 - 分块的方式下是几个
DefaultHttpContent
,最后一个LastHttpContent
,写法同上。 - 请求一个会对应
HttpFiltersAdapter
一个实例,状代码可以写成类成员变量。
中间人代理
中间人代理可以在授信设备安装证书后,截取HTTPS流量。
littleproxy实现中间人的方式很简单,实现MitmManager
接口,启动在类中调用withManInTheMiddle
方法。
MitmManager
要求接口报道查看SSLEngine
对象,实现SslEngineSource
接口。
SSLEngine
的英文对象要通过SSLContext
调用createSSLEngine
而SSLContext
的初始化,需要证书文件,又涉及CA认证签名体系。
然后HTTPS流量会先进行解包,和普通HTTP一样,可以通过上面的手段进行捕获,然后再用自己的证书进行签名
目前使用的OpenSSL实现了一个版本。
启动器
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
35
36
37
38
39
40
41
|
public static void main(String [] args){
HttpProxyServer server = DefaultHttpProxyServer.bootstrap()。withPort(8181).withTransparent(true)
.withManInTheMiddle(new MitmManager(){
private HashMap <String,SslEngineSource> sslEngineSources = new HashMap <String,SslEngineSource>();
@覆盖
public SSLEngine serverSslEngine(String peerHost,int peerPort){
if(!sslEngineSources.containsKey(peerHost)){
sslEngineSources.put(peerHost,new FclSslEngineSource(peerHost,peerPort));
}
return sslEngineSources.get(peerHost).newSslEngine();
}
@覆盖
public SSLEngine serverSslEngine(){
return null;
}
@覆盖
public SSLEngine clientSslEngineFor(HttpRequest httpRequest,SSLSession serverSslSession){
return sslEngineSources.get(serverSslSession.getPeerHost())。newSslEngine();
}
})withFiltersSource(new HttpFiltersSourceAdapter(){
@覆盖
public HttpFilters filterRequest(HttpRequest req,ChannelHandlerContext ct){
返回新的HttpFiltersAdapter(req){
@覆盖
public HttpResponse proxyToServerRequest(HttpObject httpObject){
if(httpObject instanceof DefaultHttpRequest){
DefaultHttpRequest dhr =(DefaultHttpRequest)httpObject;
String url = dhr.getUri();
String method = dhr.getMethod()。toString();
String host = dhr.headers()。get(Names.HOST);
System.out.println(method +“”+(“CONNECT”.equals(method)?“”:host)+ url);
}
return super.proxyToServerRequest(httpObject);
}
};
}
})。开始();
}
|
SslEngineSource实现类
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
公共类FclSslEngineSource实现SslEngineSource {
私有String主机;
私人港口;
private SSLContext sslContext;
private final File keyStoreFile; //当前域名的JKS文件
private String dir =“cert /”; //证书目录文件
private static final String PASSWORD =“123123”;
private static final String PROTOCOL =“TLS”;
public static String CA_KEY =“MITM_CA.key”;
public static String CA_CRT =“MITM_CA.crt”;
public FclSslEngineSource(String peerHost,int peerPort){
this.host = peerHost;
this.port = peerPort;
this.keyStoreFile = new File(dir + host +“。jks”);
initCA();
initializeKeyStore();
initializeSSLContext();
}
@覆盖
public SSLEngine newSslEngine(){
SSLEngine sslengine = sslContext.createSSLEngine(host,port);
返回sslengine;
}
@覆盖
public SSLEngine newSslEngine(String peerHost,int peerPort){
SSLEngine sslengine = sslContext.createSSLEngine(host,port);
返回sslengine;
}
public void initCA(){
if(!new File(CA_CRT).exists()){
//如果不存在,就创建证书
//生成证书
nativeCall(“openssl”,“genrsa”,“ - out”,CA_KEY,“2048”);
//生成CA证书
nativeCall(“openssl”,“req”,“ - x509”,“ - new”,“ - node”,“ - key”,CA_KEY,“ - subj”,“”/ CN = NOT_TRUST_CA “”,
“-days”,“365”,“ - out”,CA_CRT);
}
}
private void initializeKeyStore(){
if(!new File(dir).isDirectory())
{
new File(dir).mkdirs();
}
//存在证书就不用再生成了
if(keyStoreFile.isFile()){
返回;
}
//生成站点键
nativeCall(“openssl”,“genrsa”,“ - out”,dir + host +“。key”,“2048”);
//生成待签名证书
nativeCall(“openssl”,“req”,“ - new”,“ - key”,dir + host +“。key”,“ - subj”,“”/ CN =“+ host +”“”,“退房手续”,
dir + host +“。ccs”);
//用ca进行签名
nativeCall(“openssl”,“x509”,“ - req”,“ - days”,“30”,“ - in”,dir + host +“。csr”,“ - CA”,CA_CRT,“ - CAkey”,
CA_KEY,“ - CAcreateserial”,“ - out”,dir + host +“。crt”);
//把crt导成p12
nativeCall(“openssl”,“pkcs12”,“ - export”,“ - clcerts”,“ - password”,“pass:”+ PASSWORD,“ - in”,
dir + host +“。crt”,“ - inkey”,dir + host +“。key”,“ - out”,dir + host +“。p12”);
//把p12导成jks
nativeCall(“keytool”,“ - importkeystore”,“ - sckeykeystore”,dir + host +“。p12”,“ - srcstoretype”,“pkcs12”,
“-destkeystore”,dir + host +“。jks”,“ - adsstoretype”,“jks”,“ - srcstorepass”,PASSWORD,
“-deststorepass”,PASSWORD);
;
}
private void initializeSSLContext(){
String algorithm = Security.getProperty(“ssl.KeyManagerFactory.algorithm”);
algorithm = algorithm == null?“SunX509”:算法;
尝试{
final KeyStore ks = KeyStore.getInstance(“JKS”);
ks.load(new FileInputStream(keyStoreFile),PASSWORD.toCharArray());
//设置密钥管理器工厂以使用我们的密钥库
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(ks,PASSWORD.toCharArray());
TrustManager [] trustManagers = new TrustManager [] {new X509TrustManager(){
//信任所有服务器的TrustManager
@覆盖
public void checkClientTrusted(X509Certificate [] arg0,String arg1)抛出CertificateException {
}
@覆盖
public void checkServerTrusted(X509Certificate [] arg0,String arg1)抛出CertificateException {
}
@覆盖
public X509Certificate [] getAcceptedIssuers(){
return null;
}
}};
KeyManager [] keyManagers = kmf.getKeyManagers();
//初始化SSLContext以与我们的密钥管理器一起使用。
sslContext = SSLContext.getInstance(PROTOCOL);
sslContext.init(keyManagers,trustManagers,null);
} catch(final Exception e){
抛出新错误(“无法初始化服务器端SSLContext”,e);
}
}
private String nativeCall(final String ... commands){
final ProcessBuilder pb = new ProcessBuilder(命令);
尝试{
final process process = pb.start();
final InputStream is = process.getInputStream();
return IOUtils.toString(is);
} catch(final IOException e){
e.printStackTrace(System.out的);
返回“”;
}
}
}
|
代理链
代理链的主要作用提供地址的路由。
比如指定X地址,走甲代理,指定乙地址走ÿ代理。
用到主要ChainedProxyManager
及ChainedProxyAdapter
类。
示例代码:
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
|
public static void main(String [] args){
DefaultHttpProxyServer.bootstrap()。withTransparent(真).withPort(8181)
.withChainProxyManager(new ChainedProxyManager(){
@覆盖
public void lookupChainedProxies(HttpRequest httpRequest,Queue <ChainedProxy> chainedProxies){
chainedProxies.add(new ChainedProxyAdapter(){
@覆盖
public InetSocketAddress getChainedProxyAddress(){
返回新的InetSocketAddress(“127.0.0.1”,1080);
}
});
}
})。开始();
}
|
实现可以lookupChainedProxies
方法,按httpReqeust的条件,添加不同的代理链,走不同的路径。
总结
关于HTTP协议的解析,的确可以好好的看看网状上的代码怎么写的,代码比较简洁,主要是关注的包的解析。
当然,在小提供的钩子方法中,是需要自己控制HTTP的相关状态,比如报文长度,拼接,及压缩。
还存在的问题
如图1所示,代码在窗口上执行没有问题,中间人代理部分的代码但在linux的上会有问题,在执行nativeCall时,存在第一个文件没有生成就执行第二条命令,这里还需要参考下面的代码不使用命令行的方式,直接用java代码生成jks证书
.2,在应用在浏览器上做屏蔽时,出现在代理代码中已经把改连接断开,但浏览器还在等待的问题