介绍
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)
- httpContent(HTTP协议体的抽象,比如POST数据的体,和响应数据的体)
主要类:
类主要是对上面接口的实现
- DefaultHttpObject
- DefautlHttpContent
- DefaultLastHttpContent
- DefaultHttpMessage
- DefaultHttpRequest
- DefaultFullHttpRequest
- DefaultHttpResponse
- DefaultFullHttpResponse
- DefaultHttpRequest
- DefautlHttpContent
更多可以参考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);
基本流程代码
示例代码
1234五67891011121314151617181920212223242526272829三十3132333435 | 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
分别是:
- clientToProxyRequest(默认返回空值,表示不拦截,若返回数据,则不再经过P2S和S2P。这里可以修改数据)
- proxyToServerRequest(这里的原理与上面一条一样,基本原封不动)
- serverToProxyResponse(这里默认返回传入参数,可以做一定的修改)
- proxyToClientResponse(与上面一条类似)
这个流程符合普通代理的流程。
请求数据C – > P – > S,
响应数据S – > P – > C
代码预期会输出的英文1,2,3,4
按顺序执行
但实际运行结果(省略若干非关键性信息):
1234五6789101112 | 1-DefaultHttpRequest(decodeResult:success,version:HTTP / 1.1)2-DefaultHttpRequest(decodeResult:success,version:HTTP / 1.1)1-EmptyLastHttpContent2- EmptyLastHttpContent3-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的内容,同样的原理,不过是要修改请求的内容体。
1234五678910111213141516171819 | @覆盖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函数就是把搜索的关键字提取出来,并添加前缀,然后拼接成新的网址。
1234五67891011121314151617181920 | 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
就报道查看一个空的响应。这个请求就不会继续请求服务端。
如果是多个域名,使用集来存储。如果是需要按后缀,可以用后缀树。
1234五678910111213141516171819 | @覆盖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实现了一个版本。
启动器
1234五67891011121314151617181920212223242526272829三十3132333435363738394041 | 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实现类
1234五67891011121314151617181920212223242526272829三十313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 | 公共类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
类。
示例代码:
1234五6789101112131415 | 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,在应用在浏览器上做屏蔽时,出现在代理代码中已经把改连接断开,但浏览器还在等待的问题