netty http/https 透明代理
2021-12-04 本文已影响0人
lesliefang
用 netty 200 行代码实现 http/https 透明代理
透明代理就是不对请求做解析和更改,直接进行流的转发。
Https 请求由于加密也无法解析,只能通过 CONNECT协议走隧道(Tunnel)代理。
普通 http 透明代理和 CONNECT 隧道代理的唯一区别就是隧道代理第一个请求是明文的CONNECT 请求, 从请求行中解析出远程主机的 host 和 port 然后建立和远程主机的 TCP 连接,连接建立后代理给客户端返回 200 Connection Established 表明到远程主机的连接已经建立。 客户端就开始发送实际的请求,代理就盲目转发 TCP 流就可以了。
CONNECT 请求不能转发给远程主机,只有代理能识别 CONNECT 请求。
所以透明代理实现很简单,我只需要解析第一个 HTTP 请求(其实不用解析一个完整的HTTP请求,只解析请求行和部分header就够了,由于TCP分包粘包的问题你要把读到的第一个包保存下来,如果不是CONNECT请求还要原样发送到远端服务器的。但用 NETTY 解析一个完整的FullHttpRequest 处理比较简单),判断是不是 CONNECT 请求就行了,之后的处理就都一样了,盲目的转发 TCP 流就行了。
public class HttpProxyServer {
private final int port;
public HttpProxyServer(int port) {
this.port = port;
}
public static void main(String[] args) {
new HttpProxyServer(3000).run();
}
public void run() {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new LoggingHandler(LogLevel.DEBUG),
new HttpRequestDecoder(),
new HttpResponseEncoder(),
new HttpObjectAggregator(1024 * 1024),
new HttpProxyClientHandler());
}
})
.bind(port).sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class HttpProxyClientHandler extends ChannelInboundHandlerAdapter {
private String host;
private int port;
private boolean isConnectMethod = false;
// 客户端到代理的 channel
private Channel clientChannel;
// 代理到远端服务器的 channel
private Channel remoteChannel;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
clientChannel = ctx.channel();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest httpRequest = (FullHttpRequest) msg;
System.out.println(httpRequest);
isConnectMethod = HttpMethod.CONNECT.equals(httpRequest.method());
// 解析目标主机host和端口号
parseHostAndPort(httpRequest);
System.out.println("remote server is " + host + ":" + port);
// disable AutoRead until remote connection is ready
clientChannel.config().setAutoRead(false);
/**
* 建立代理服务器到目标主机的连接
*/
Bootstrap b = new Bootstrap();
b.group(clientChannel.eventLoop()) // 和 clientChannel 使用同一个 EventLoop
.channel(clientChannel.getClass())
.handler(new HttpRequestEncoder());
ChannelFuture f = b.connect(host, port);
remoteChannel = f.channel();
f.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
// connection is ready, enable AutoRead
clientChannel.config().setAutoRead(true);
if (isConnectMethod) {
// CONNECT 请求回复连接建立成功
HttpResponse connectedResponse = new DefaultHttpResponse(httpRequest.protocolVersion(), new HttpResponseStatus(200, "Connection Established"));
clientChannel.writeAndFlush(connectedResponse);
} else {
// 普通http请求解析了第一个完整请求,第一个请求也要原样发送到远端服务器
remoteChannel.writeAndFlush(httpRequest);
}
/**
* 第一个完整Http请求处理完毕后,不需要解析任何 Http 数据了,直接盲目转发 TCP 流就行了
* 所以无论是连接客户端的 clientChannel 还是连接远端主机的 remoteChannel 都只需要一个 RelayHandler 就行了。
* 代理服务器在中间做转发。
*
* 客户端 ---> clientChannel ---> 代理 ---> remoteChannel ---> 远端主机
* 远端主机 ---> remoteChannel ---> 代理 ---> clientChannel ---> 客户端
*/
clientChannel.pipeline().remove(HttpRequestDecoder.class);
clientChannel.pipeline().remove(HttpResponseEncoder.class);
clientChannel.pipeline().remove(HttpObjectAggregator.class);
clientChannel.pipeline().remove(HttpProxyClientHandler.this);
clientChannel.pipeline().addLast(new RelayHandler(remoteChannel));
remoteChannel.pipeline().remove(HttpRequestEncoder.class);
remoteChannel.pipeline().addLast(new RelayHandler(clientChannel));
} else {
clientChannel.close();
}
});
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
flushAndClose(remoteChannel);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
flushAndClose(ctx.channel());
}
private void flushAndClose(Channel ch) {
if (ch != null && ch.isActive()) {
ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 解析header信息,建立连接
* HTTP 请求头如下
* GET http://www.baidu.com/ HTTP/1.1
* Host: www.baidu.com
* User-Agent: curl/7.69.1
* Proxy-Connection:Keep-Alive
* ---------------------------
* HTTPS请求头如下
* CONNECT www.baidu.com:443 HTTP/1.1
* Host: www.baidu.com:443
* User-Agent: curl/7.69.1
* Proxy-Connection: Keep-Alive
*/
private void parseHostAndPort(HttpRequest httpRequest) {
String hostAndPortStr;
if (isConnectMethod) {
// CONNECT 请求以请求行为准
hostAndPortStr = httpRequest.uri();
} else {
hostAndPortStr = httpRequest.headers().get("Host");
}
String[] hostPortArray = hostAndPortStr.split(":");
host = hostPortArray[0];
if (hostPortArray.length == 2) {
port = Integer.parseInt(hostPortArray[1]);
} else if (isConnectMethod) {
// 没有端口号,CONNECT 请求默认443端口
port = 443;
} else {
// 没有端口号,普通HTTP请求默认80端口
port = 80;
}
}
}
public class RelayHandler extends ChannelInboundHandlerAdapter {
private Channel remoteChannel;
public RelayHandler(Channel remoteChannel) {
this.remoteChannel = remoteChannel;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
remoteChannel.writeAndFlush(msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
flushAndClose(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
/**
* 连接断开时关闭另一端连接。
* 如果代理到远端服务器连接断了也同时关闭代理到客户的连接。
* 如果代理到客户端的连接断了也同时关闭代理到远端服务器的连接。
*/
flushAndClose(remoteChannel);
}
private void flushAndClose(Channel ch) {
if (ch != null && ch.isActive()) {
ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
}
}
参考 https://zhuanlan.zhihu.com/p/356167533
github https://github.com/lesliebeijing/HttpTransparentProxy