24netty(二十)http代理服务器【重点】

netty(五)http服务的基础上,结合java http client https做一个http反向代理服务器

 

1 项目背景:用iframe嵌入某网站页面,难点有二:

1.1 对方response header启用X-Frame-Options:SAMEORIGIN,导致不能被iframe嵌入

1.2 对方网站有session鉴权,甚至static资源文件css js等都需要登录态,session过期一般一小时

2021.2.3 

1.3 母请求/arc/reports/report/25612,内含1)js css,以arc/static/形式get引入;2)ajax,/arc/sqlrun/jsonselect_parallel,内含csrf防御

 

设计一个轻量级反向代理服务器:

 

 

2 coding过程(已有一个可用cookie)

2.0 请求的网页结构:

html

  js css ttf 

  2 * ajax

 

2.1 host请求不到,网络被运维特殊处理了,只有浏览器有正向代理及dns解析,非浏览器google不行,内网host行

2.2 对方是https服务,java 2个httpclient客户端处理 https ,Netty 作为 http client 请求https 的 get与post搞定

2.3 session认证,伪造cookie header解决

2.4 统一url拦截,netty搞定

2.5 破解其中一个数据接口csrf防御

2.5.1 cookie(sessionid & csrftoken) not ok   

2.5.2 header(sessionid & !csrftoken) not ok    (2021.2.3 估计当时写错了,应该是cookie(sessionid & csrftoken)+header(sessionid & !csrftoken))

2.5.3 cookie + header + REFERER & ORIGIN & !HOST   ok  

2.5.4 cookie + header  没机会试了,不过REFERER、ORIGIN本身在header里面,出于不信任浏览器原则,有大概率服务器有了token-token之后是不会多此一举去验证,HOST就更无所谓了  

根据 csrf与防护,get与post ,origin与referer区别,基本判断该目标服务为cookie + header_token形式防csrf

(2021.2.3  从现有的代码回忆,好像也做过实践,httpPost.setHeader("X-CSRFToken", XCSRFToken); 这个代码位于一个single的httpclient对

/arc/sqlrun/jsonselect_parallel

的调用,判断下来,这个XCSRGToken应该是cookie种csrftoken的某种变种,而不是完全一样的,而且是前一次请求

/arc/reports/report/25612

所获得的数据,前一次请求生成csrftoken set-cookie(若cookie无)直接使用之生成X-CSRFToken(若有),又不知道在哪里返回了一个X-CSRFToken)

2.6 session保护 5-10分钟随机访问一次

 

 

3 写完之后发现现象:

3.1 长连接,且未在BodyToResponseEncoder中设置content-length时,浏览器阻塞

可以看到,有6个active,证明浏览器开了6个http链接( netstat -an|grep也可以看到6条连接),但是确请求了7个url,根据 tcp(netty)的调用同步化(异步阻塞)及与http协议、浏览器关系,每个http 链接,req res生命期不重叠,而我们的程序不设置content-length,会导致浏览器接收response阻塞,那为什么会有7个url呢?势必有一个url成功返回了。(这些资源文件以get的形式请求)

这个被返回的url就是第一个,这是一个在channel1 的 html网页请求,且被代理目标服务器已返回chunked标志,然后channel1马不停蹄的进行第二个请求,这次是一个资源文件(css),阻塞,然后其余5个http连接全部阻塞,这也就解释了为什么会有7个url

 

3.2 在ProxySender 中writeAndFlush的回调中增加ctx.close (相当于启用短链接)时,正常,可以看到有很多active channel

            ResHttp resHttp = new ResHttp(ret, map, byteArrayOutputStream.toByteArray());
            ChannelFuture channelFuture = originHttp.getContext().writeAndFlush(resHttp);
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {

                    /**
                     * 短链接设置,当使用短链接时,不需要设置length或chunked
                     */
                //    future.channel().close();

                    if(!future.isSuccess()) {
                        future.cause().printStackTrace();
                        future.channel().close();
                    }
                }
            });

 

3.3 长连接,设置content-length时,正常

        response.headers().remove("X-Frame-Options");

        /**
         * 对长连接,chunked与length要有其一但又不能共存,在postman上与chunked冲突
         */
        if(!response.headers().contains("Transfer-Encoding") && !"chunked".equals(response.headers().get("Transfer-Encoding")))
            response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());

 

此外:下面3项非本文重点关注

3.4 http header会有多个存在,保险起见,先remove,再add,或用setHeader

        ////////////////////////////////////////////////////////
        httpUriRequest.setHeader("AUTH", "memories");
        httpUriRequest.setHeader("Cookie", "AUTH=memories");
        /**
         * 必须移除,apache http client(特有,netty无)post时会报错Content-Length header already present
         */
        httpUriRequest.removeHeaders("Content-Length");
        ////////////////////////////////////////////////////////

//        httpUriRequest.setHeader("Origin", dir);
//        httpUriRequest.setHeader("Referer", dir);
//        httpUriRequest.setHeader("Host", host);

 

3.5

AbstractProxySender在向被代理服务发送请求时,有两种配置方式,apache httpclient  和netty

使用netty时,postman采用了短链接,返回报文采用contentlength

使用httpclient时,postman采用了长连接,返回报文采用chunked

postman直接访问springboot时,返回报文与httpclient一致,看来我写的netty client还是没有别人的成熟

testproxy.postman_collection2.1.json.zip

postman直接访问spring boot https时,由于私有证书,要勾选忽略SSL, java 2个httpclient客户端处理 https

 

3.6

加入高低水位,参照 netty高低水位流控 

    public void writeAndFlush(ChannelHandlerContext ctx) {
        int time = 0;
        final int timeLimit = 5;
        while (!ctx.channel().isWritable()) {
            try {
                Thread.sleep(100);
                ++time;
            } catch (Exception e) {
                e.printStackTrace();
                time = timeLimit + 1;
            } finally {
                if(time > timeLimit)
                    break;
            }
        }

        if(time <= timeLimit) {
            ChannelFuture channelFuture = ctx.writeAndFlush(this);
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {

                    /**
                     * 短链接设置,当使用短链接时,不需要设置length或chunked
                     */
                    //    future.channel().close();

                    if(!future.isSuccess()) {
                        future.cause().printStackTrace();
                        future.channel().close();
                    }
                }
            });
        }
    }

 

3.7 代理在请求后端时,原先使用单线程消费者,由于后端的httpclient和netty都是阻塞式客户端,故碰到一个几十个css等资源的html得一个一个请求,很慢,故后来开了10个消费线程,取2(n+1),n为CPU核数,毕竟不像浏览器,同一个host只有6个并发(3.1~3.3)

 

 

结论:

1 证明了浏览器同一时刻同一host 6个连接 tcp(netty)的调用同步化(异步阻塞)及与http协议、浏览器关系;另一种证明方式为,浏览器访问服务器,服务端netstat可以看到6个established的tcp连接

2 证明了浏览器的http连接是同步阻塞的 tcp(netty)的调用同步化(异步阻塞)及与http协议、浏览器关系,一条连接上一个请求没有返回response之前,不会发出下一个请求,各请求-响应生命期不重叠

3 长连接中,chunked和length需要其一,也只需要其一(postman冲突),否则导致浏览器阻塞 21-ahttpclient 与TIME_WAIT 客户端close与服务端close

4 用短链接则不需要设置那两个header 21-ahttpclient 与TIME_WAIT 客户端close与服务端close

5 实践了是存在多个同名http header的,注意先remove再add,或直接用set

6 实践了破解header_token+cookie的csrf防御

 

 

testproxy.postman_collection2.1.json.zip

 

 

 2021.5.28  經過實踐,證明了6個tcp連接限制適用於跨tab,和chrome進程限制

posted on 2019-12-26 22:40  silyvin  阅读(1195)  评论(0编辑  收藏  举报