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進程限制