LittleProxy是一个用Java编写的高性能HTTP代理,它基于Netty事件的网络库之上。它很是稳定,性能良好,而且易于集成到的项目中。html
项目页面:https://github.com/adamfisk/LittleProxyjava
这里介绍几个简单的应用,其它复杂的应用都是能够基于这几个应用进行改造。node
由于代理库是基于网状事件驱动,因此须要对网状原理的了解有所
由于的英文对HTTP协议进行处理,因此了解须要io.netty.handler.codec.http
包下的类。
由于效率,数据大部分的英文由ByteBuf
进行管理的,须要因此了解ByteBuf
相关操做。linux
io.netty.handler.codec.http
包的相关介绍git
主要接口图:github
主要类:
类主要是对上面接口的实现算法
更多能够参考API文档https://netty.io/4.1/api/index.html
辅助类io.netty.handler.codec.http.HttpHeaders.Names
bootstrap
io.netty.buffer.ByteBuf
相关的使用
主要使用的英文Unpooled
状语从句:ByteBufUtil
api
Unpooled.wrappedBuffe
toString(Charset.forName("UTF-8")
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
分别是:
这个流程符合普通代理的流程。
请求数据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,:)),
|
能够看出:
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;
}
返回网址;
}
|
按上面基础代码重写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;
}
|
修改内容会涉及几个很麻烦的事
Transfer-Encoding: chunked
)压缩对于
简单的作法就是修改请求做者:文,让请求头不支持压缩算法,服务器就不会对内容进行压缩。
复杂的办法就是记录响应头,老实进行解压。
解码以后再修改内容,内容修改好以后,再进行压缩。
对于分块
没有什么好的办法,在响应中去掉标识,而后按次拼接,服务器来的块,拼接好,修改好后,一次返回给客户端。
。很代码长就不贴出来了
但写proxyToClientResponse
函数中拼做者:文时,有几个注意事项:
return new DefaultHttpContent(Unpooled.EMPTY_BUFFER);
一个空的响应。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,在应用在浏览器上作屏蔽时,出如今代理代码中已经把改链接断开,但浏览器还在等待的问题