netty http/https 透明代理

用 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,635评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,543评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,083评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,640评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,640评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,262评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,833评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,736评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,280评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,369评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,503评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,870评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,340评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,460评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,909评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,512评论 2 359

推荐阅读更多精彩内容

  • 前言 通常使用的网络(包括互联网)是在TCP/IP协议族的基础上运作的。而HTTP属于它内部的一个子集。在整个网络...
    枫叶情结阅读 1,386评论 0 2
  • CDN访问异常篇之502/503/504错误 一. 源站不通或源站域名无法解析 CDN 都是公网上的节点,CDN配...
    TimLi_51bb阅读 8,012评论 0 1
  • Web 页面的实现 Web 基于 HTTP 协议通信 客户端(Client)的 Web 浏览器从 Web 服务器端...
    毛圈阅读 1,092评论 0 2
  • http 应该是从哪个几个方面掌握?0》http背景web数据交互的时候需要数据的简单传输而产生的 1》http是...
    helinyu阅读 1,895评论 2 4
  • 1. 网络基础TCP/IP HTTP基于TCP/IP协议族,HTTP属于它内部的一个子集。 把互联网相关联的协议集...
    yozosann阅读 3,445评论 0 20