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,在應用在瀏覽器上作屏蔽時,出如今代理代碼中已經把改鏈接斷開,但瀏覽器還在等待的問題