LittleProxy是一个用Java编写的高性能HTTP代理,它基于Netty事件的网络库之上。 它非常稳定,性能良好,并且易于集成到的项目中。
项目页面: https://github.com/adamfisk/LittleProxy
这里介绍几个简单的应用,其它复杂的应用都是可以基于这几个应用进行改造。
因为代理库是基于netty事件驱动,所以需要对netty的原理有所了解
因为是对http协议进行处理,所以需要了解 io.netty.handler.codec.http
包下的类。
因为效率,大部分数据是由 ByteBuf
进行管理的,所以需要了解 ByteBuf
相关操作。
io.netty.handler.codec.http
包的相关介绍
主要接口图:
主要类:
类主要是对上面接口的实现
更多可以参考API文档 https://netty.io/4.1/api/index.html
辅助类 io.netty.handler.codec.http.HttpHeaders.Names
io.netty.buffer.ByteBuf
的相关使用
主要使用是 Unpooled
和 ByteBufUtil
Unpooled.wrappedBuffe toString(Charset.forName("UTF-8") ByteBufUtil.prettyHexDump(buf);
示例代码
public static void main(String[] args) { HttpProxyServer server = DefaultHttpProxyServer.bootstrap().withPort(8181) .withFiltersSource(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest req, ChannelHandlerContext ct) { return new HttpFiltersAdapter(req) { @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { System.out.println("1-" + httpObject); return super.clientToProxyRequest(httpObject); } @Override public HttpResponse proxyToServerRequest(HttpObject httpObject) { System.out.println("2-" + httpObject); return super.proxyToServerRequest(httpObject); } @Override public HttpObject serverToProxyResponse(HttpObject httpObject) { System.out.println("3-" + httpObject); return super.serverToProxyResponse(httpObject); } @Override public HttpObject proxyToClientResponse(HttpObject httpObject) { System.out.println("4-" + httpObject); return super.proxyToClientResponse(httpObject); } }; } }).start(); }
代码分析:
HttpFiltersSourceAdapter
的 filterRequest
函数 HttpFiltersAdapter
的4个关键性函数,并打印日志
HttpFiltersAdapter
分别是:
这个流程符合普通代理的流程。
请求数据 C -> P -> S,
响应数据 S -> P -> C
预期代码输出会是 1,2,3,4
按顺序执行
但实际运行结果(省略若干非关键性信息):
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/624, : ), ) 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, : ), )
可以看出:
Last-xx
比如这里实现了把每次百度搜索的关键字加一个前缀的功能。
主要原理是修改 DefaultHttpRequest
的url中所带的参数(只能修改GET方式的参数)
如果需要修改POST的内容,同样的原理,不过是要修改Request的内容体。
@Override 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")) { try { dhr.setUri(replaceParam(url)); } catch (Exception e) { e.printStackTrace(); } } } return null; }
replaceParam函数就是把搜索的关键字提取出来,并添加前缀,然后拼接成新url。
static public String replaceParam(String url) throws Exception { 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()); return new_url; } return url; }
按上面基础代码重写clientToProxyRequest或者proxyToServerRequest。
如果是指定域名,如 hm.baidu.com
就返回一个空的response。这个请求就不会继续请求服务端。
如果是多个域名,使用set来存储。如果是需要按后缀,可以用后缀树。
@Override 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")) { return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); } if(!method.equals("CONNECT")) { System.out.println(method+ " http://"+host+url); } } return null; }
修改内容会涉及几个很麻烦的事
Transfer-Encoding: chunked
对于压缩
简单的做法就是修改请求报文,让请求头不支持压缩算法,服务器就不会对内容进行压缩。
复杂的办法就是记录响应头,老实进行解压。
解码之后再修改内容,内容修改好之后,再进行压缩。
对于chunked
没有什么好的办法,在Response中去掉标识,然后按次拼接,服务器来的块,拼接好,修改好后,一次返回给客户端。
代码很长就不贴出来了。
但写 proxyToClientResponse
函数中拼报文时,有几个注意事项:
return new DefaultHttpContent(Unpooled.EMPTY_BUFFER);
一个空的response。 DefaultHttpContent
,最后一个 DefaultLastHttpContent
,判断语句Lastxx要写在前面,因为后面是前面的子类(先判断范围小的,再判断范围大的)。 DefaultHttpContent
,最后一个 LastHttpContent
,写法同上。 HttpFiltersAdapter
一个实例,状代码可以写成类成员变量。 中间人代理可以在授信设备安装证书后,截取https流量。
littleproxy实现中间人的方式很简单,实现 MitmManager
接口,在启动类中调用 withManInTheMiddle
方法。
MitmManager
接口要求返回 SSLEngine
对象,实现 SslEngineSource
接口。
SSLEngine
对象是要通过 SSLContext
调用 createSSLEngine
而 SSLContext
的初始化,需要证书文件,又涉及CA认证签名体系。
然后https流量会先进行解包,和普通http一样,可以通过上面的手段进行捕获,然后再用自己的证书进行签名
目前使用openssl实现了一个版本。
启动器
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>(); @Override public SSLEngine serverSslEngine(String peerHost, int peerPort) { if (!sslEngineSources.containsKey(peerHost)) { sslEngineSources.put(peerHost, new FclSslEngineSource(peerHost, peerPort)); } return sslEngineSources.get(peerHost).newSslEngine(); } @Override public SSLEngine serverSslEngine() { return null; } @Override public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) { return sslEngineSources.get(serverSslSession.getPeerHost()).newSslEngine(); } }).withFiltersSource(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest req, ChannelHandlerContext ct) { return new HttpFiltersAdapter(req) { @Override 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 + " " + host + url); } return super.proxyToServerRequest(httpObject); } }; } }).start(); }
SslEngineSource实现类
public class FclSslEngineSource implements SslEngineSource { private String host; private int port; 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(); } @Override public SSLEngine newSslEngine() { SSLEngine sslengine = sslContext.createSSLEngine(host, port); return sslengine; } @Override public SSLEngine newSslEngine(String peerHost, int peerPort) { SSLEngine sslengine = sslContext.createSSLEngine(host, port); return sslengine; } public void initCA() { if (!new File(CA_CRT).exists()) { // 如果不存在,就创建证书 // 生成证书 nativeCall("openssl", "genrsa", "-out", CA_KEY, "2048"); // 生成CA证书 nativeCall("openssl", "req", "-x509", "-new", "-nodes", "-key", CA_KEY, "-subj", "/"/CN=NOT_TRUST_CA/"", "-days", "365", "-out", CA_CRT); } } private void initializeKeyStore() { // 存在证书就不用再生成了 if (keyStoreFile.isFile()) { return; } // 生成站点key nativeCall("openssl", "genrsa", "-out", dir + host + ".key", "2048"); // 生成待签名证书 nativeCall("openssl", "req", "-new", "-key", dir + host + ".key", "-subj", "/"/CN=" + host + "/"", "-out", dir + host + ".csr"); // 用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", "-srckeystore", dir + host + ".p12", "-srcstoretype", "pkcs12", "-destkeystore", dir + host + ".jks", "-deststoretype", "jks", "-srcstorepass", PASSWORD, "-deststorepass", PASSWORD); ; } private void initializeSSLContext() { String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); algorithm = algorithm == null ? "SunX509" : algorithm; try { final KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(keyStoreFile), PASSWORD.toCharArray()); // Set up key manager factory to use our key store final KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); kmf.init(ks, PASSWORD.toCharArray()); TrustManager[] trustManagers = new TrustManager[] { new X509TrustManager() { // TrustManager that trusts all servers @Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } } }; KeyManager[] keyManagers = kmf.getKeyManagers(); // Initialize the SSLContext to work with our key managers. sslContext = SSLContext.getInstance(PROTOCOL); sslContext.init(keyManagers, trustManagers, null); } catch (final Exception e) { throw new Error("Failed to initialize the server-side SSLContext", e); } } private String nativeCall(final String... commands) { final ProcessBuilder pb = new ProcessBuilder(commands); try { final Process process = pb.start(); final InputStream is = process.getInputStream(); return IOUtils.toString(is); } catch (final IOException e) { e.printStackTrace(System.out); return ""; } } }
关于http协议的解析,的确可以好好的看看netty上的代码怎么写的,代码比较简洁,主要是关注的包的解析。
当然,在little提供的hook方法中,是需要自己控制http的相关状态,比如报文长度,拼接,及压缩。