Littleproxy的使用

 

介紹

 

LittleProxy是一個用Java編寫的高性能HTTP代理,它基於Netty事件的網絡庫之上。它很是穩定,性能良好,而且易於集成到的項目中。html

 

項目頁面:https//github.com/adamfisk/LittleProxyjava

 

這裏介紹幾個簡單的應用,其它複雜的應用都是能夠基於這幾個應用進行改造。node

 

  • 按域名或者URL進行攔截和過濾
  • 修改HTTP頭,修改請求參數
  • 修改返回響應數據
  • 中間人代理,截取HTTPS的數據

 

前置知識

 

由於代理庫是基於網狀事件驅動,因此須要對網狀原理的瞭解有所
由於的英文對HTTP協議進行處理,因此瞭解須要io.netty.handler.codec.http包下的類。
由於效率,數據大部分的英文由ByteBuf進行管理的,須要因此瞭解ByteBuf相關操做。linux

 

io.netty.handler.codec.http包的相關介紹git

 

主要接口圖:github

 

  • HttpObject
    • httpContent(HTTP協議體的抽象,好比POST數據的體,和響應數據的體)
      • LastHttpContent
    • HttpMessage(HTTP協議頭的抽象,包含請求頭和響應頭)
      • FullHttpMessage(也繼承於LastHttpContent)
      • HttpRequest的
        • FullHttpRequest(也繼承於FullHttpMessage)
      • 的HttpResponse
        • FullHttpResponse(也繼承於FullHttpMessage)

 

主要類:
類主要是對上面接口的實現算法

 

  • DefaultHttpObject
    • DefautlHttpContent
      • DefaultLastHttpContent
    • DefaultHttpMessage
      • DefaultHttpRequest
        • DefaultFullHttpRequest
      • DefaultHttpResponse
        • DefaultFullHttpResponse

 

更多能夠參考API文檔https://netty.io/4.1/api/index.html
輔助類io.netty.handler.codec.http.HttpHeaders.Namesbootstrap

 

io.netty.buffer.ByteBuf相關的使用
主要使用的英文Unpooled狀語從句:ByteBufUtilapi

 

  • 把字符串轉化爲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);
}
};
}
})。開始();
}

 

 

 

代碼分析:

 

  • 啓動代理類
  • 實現HttpFiltersSourceAdapterfilterRequest函數
  • 實現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地址,走甲代理,指定乙地址走ÿ代理。

 

用到主要ChainedProxyManagerChainedProxyAdapter類。
示例代碼:

 

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

相關文章
相關標籤/搜索