首页 技术 正文
技术 2022年11月14日
0 收藏 562 点赞 4,419 浏览 11372 个字

介绍

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);

基本流程代码

示例代码

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); } }; } })。开始();}

代码分析:

  • 启动代理类
  • 实现HttpFiltersSourceAdapterfilterRequest函数
  • 实现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地址,走甲代理,指定乙地址走ÿ代理。

用到主要ChainedProxyManagerChainedProxyAdapter类。
示例代码:

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

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,103
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,579
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,427
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,199
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,834
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,917