介绍

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. 从OEL5中启动简单的dns服务

    在OEL5中,有一个 dnsmasq,可以适合小型的实验性的dns设置. 如果可以看到类似如下的内容,说明dnsmasq已经设置好了. [root@dnssvr ~]# rpm -qa dnsmasq ...

  2. C#阻止系统休眠

    阻止系统休眠 using System.Runtime.InteropServices; static class WinSleepCtr { //定义API函数 [DllImport("k ...

  3. rabbitmq的安装和使用

    一.RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件).RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的.所有 ...

  4. 洛谷 P1337 [JSOI2004]平衡点 / 吊打XXX

    洛谷 P1337 [JSOI2004]平衡点 / 吊打XXX 点击进入FakeHu的模拟退火博客 神仙模拟退火...去看fakehu的博客吧...懒得写了... 因为精度问题要在求得的最优解附近(大约 ...

  5. 给浏览器和各种软件配置 http https socks5 代理 proxy

    只需要像在 idea 里一样,配置好sr的本地代理ip:127.0.0.1   本地代理端口:1080 浏览器和软件的流量即可走 sr ,就能被 sr 代理了 用上代理之后,确实快了好多,这里是在打开 ...

  6. 2018年美国大学生数学建模竞赛(MCM/ICM) E题解题思路

    任务一就是让大家去做个基本的评价,是典型的评价类问题,所以应该按照 指标+方法的步骤去做,首先就是寻找国家脆弱性的相关概念,然后选择影响国 家脆弱性的指标,如气候变化,经济发展,政治状况等等,再就是构 ...

  7. python request 以json形式发送post请求的正确的姿势

    一个http请求包括三个部分,为别为请求行,请求报头,消息主体,类似以下这样: 请求行,请求报头,消息主题. 以json串提交数据,编码格式: application/json, 必须加上 impor ...

  8. fiddler对安卓APP进行抓包

    操作流程: 1.fiddler导出ca证书 操作路径: Tools -> Fiddler Options -> HTTPS -> Export Fiddler Root Certif ...

  9. JDK 和 JRE 有什么区别

    JDK是Java开发工具包(Java Development Kit),JRE是Java运行环境(Java Runtime Environment),JDK包含了JRE,搭建Java环境的时候,安装J ...

  10. 011 --Mysql中特定查询

    1.优化COUNT()查询 COUNT()可能是被大家误解最多的函数了,它有两种不同的作用,其一是统计某个列值的数量,其二是统计行数.统计列值时,要求列值是非空的,它不会统计NULL.如果确认括号中的 ...