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

上一篇下一篇

猜你喜欢

热点阅读