介绍

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)

主要类:
类主要是对上面接口的实现

  • DefaultHttpObject

    • DefautlHttpContent

      • DefaultLastHttpContent
    • DefaultHttpMessage
      • DefaultHttpRequest

        • DefaultFullHttpRequest
      • DefaultHttpResponse
        • DefaultFullHttpResponse

更多可以参考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);
}
};
}
})。开始();
}

代码分析:

  • 启动代理类
  • 实现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,在应用在浏览器上做屏蔽时,出现在代理代码中已经把改连接断开,但浏览器还在等待的问题

Littleproxy的使用的更多相关文章

  1. 高性能的HTTP代理 LittleProxy

    引用: https://github.com/adamfisk/LittleProxy 拦截和操纵HTTPS流量,LittleProxy使用中间人(MITM)管理器. LittleProxy的默认实现 ...

  2. 使用Python + Selenium打造浏览器爬虫

    Selenium 是一款强大的基于浏览器的开源自动化测试工具,最初由 Jason Huggins 于 2004 年在 ThoughtWorks 发起,它提供了一套简单易用的 API,模拟浏览器的各种操 ...

  3. 2019年十大开源WEB应用防火墙点评

    2019年十大开源WEB应用防火墙点评 随着WEB应用的爆炸式成长和HTTPS加密的普及,针对网络应用层的攻击,像SQL注入.跨站脚本攻击.参数篡改.应用平台漏洞攻击.拒绝服务攻击等越来越多,传统的防 ...

  4. 【音乐爬虫】Python爬虫-selenium+browsermob-proxy 解决动态网页 js渲染问题

    1.一般的python爬虫很简单,直接请求对应网址,解析返回的数据即可,但是有很多网站的数据的js动态渲染的,你直接请求是得不到对应的数据的 这时就需要其它手段来处理了. 2.以一个例子来说明,整个过 ...

随机推荐

  1. Python 装饰器笔记

    一.装饰器无参数 1.原函数无参数 def wrap_in_tag_b(fn): # wrap_in_tag_b 是真正的装饰器 def wrapped(): return "<b&g ...

  2. LBP人脸识别的python实现

    这几天看了看LBP及其人脸识别的流程,并在网络上搜相应的python代码,有,但代码质量不好,于是自己就重新写了下,对于att_faces数据集的识别率能达到95.0%~99.0%(40种类型,每种随 ...

  3. P4284 [SHOI2014]概率充电器

    P4284 [SHOI2014]概率充电器 今天上课讲到的题orz,第一次做这种上下搞两次dp的题. g[i]表示i的子树(包括i)不给i充电的概率. f[i]表示i的父亲不给i充电的概率. g[]可 ...

  4. pycharm如何回到过去某个时间

    在编写代码是,我们可能会写错代码,或者是误删某个文件,那么问题来了,如何回到过去的某个时间段,来弥补我们犯下的错呢? 1.如果是恢复删除的文件则右击之前文件所在的文件夹 2.右击文件夹的显示效果如图 ...

  5. 菜鸟vimer成长记——第0章、我眼中的vim学习

    这是一系统总结vim的学习文章,记录我自己学习vim的过程和感悟.与此同时也想分享出来给大家,欢迎大家互相讨论和学习. 在这里假设你们对vim已经有了一些基本的认识或者说已经使用过几个月的vim.该系 ...

  6. yum 执行不了, 解决方法

    在执行yum命令时忽然发现出现以下报错: 1 2 3 4 5 # yum list File "/usr/bin/yum", line 30 except KeyboardInte ...

  7. ASP.NET MVC - PageData的应用

    一.要实现一个功能,在不同的页面放置一段如下的内容,用于采集用户行为信息: <input type='hidden' id='page_id' value='xxxx' /> <sc ...

  8. 分析(function(window, undefined) {})(window)

    有的时候,我们会在JS框架中看到这行 (function(window, undefined) {})(window) ,它是做什么用的,我们来分析下它 首先这就是一个匿名函数,立即执行它 (func ...

  9. 基于tensorflow实现mnist手写识别 (多层神经网络)

    标题党其实也不多,一个输入层,三个隐藏层,一个输出层 老样子先上代码 导入mnist的路径很长,现在还记不住 import tensorflow as tf import tensorflow.exa ...

  10. 转载:GBDT算法梳理

    学习内容: 前向分布算法 负梯度拟合 损失函数 回归 二分类,多分类 正则化 优缺点 sklearn参数 应用场景 转自:https://zhuanlan.zhihu.com/p/58105824 G ...