美文网首页
netty http/https 透明代理

netty http/https 透明代理

作者: lesliefang | 来源:发表于2021-12-04 18:01 被阅读0次

    用 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

    相关文章

      网友评论

          本文标题:netty http/https 透明代理

          本文链接:https://www.haomeiwen.com/subject/ojqbxrtx.html