AWS API-Gateway no reply when http delete with chunked
HTTP DELETE with header Transfer-Encoding:chunked
导致AWS API-Gateway empty reply
背景
client:spring-cloud-netflix-zuul里SimpleHostRoutingFilter的ApacheHttpClient
server:AWS API-Gateway REST-Private类型,同时non-proxy integration
现象:client http delete request 到 server后,server只回应了ACK,没有HTTP response,直到client timeout。但是curl delete可以成功得到response
猜测:HttpClient与curl发出的HTTP request有区别,进而抓包准备分析数据
分析步骤
准备分析所需数据,下面是简要步骤
tcpdump
|
|
curl
|
|
java
|
|
从java log里解析client nonce与master secert:
|
|
wireshark
import pcap后,需要配置(Pre)-Master-Secret log filename。Go to Settings / Protocols / SSL / (Pre)-Master-Secret log filename
diff and experiment
对比HTTP request如下:
看出了什么吗?
- HttpClient:包含Transfer-Encoding: chunked头,并以一个empty chunk结束包(0\r\n)
- curl:正常结束包(\r\n)
通过手动设置chunked,让curl复现了timeout:
|
|
抓包结果如上图左边HttpClient一样,以empty chunk结束包,然后成功timeout。
至此,client端已经通过curl复现出了,应该是DELETE with chunked header产生的影响。同时也测试了POST/PUT with chunked header,都可以正确得到response。如下图,是一个POST request with chunked header,并正确收到了response:
猜测
猜测:HTTP DELETE request with chunked header and empty chunked body 会导致AWS API-Gateway no reply
由于API-Gateway是个黑盒,cloudwatch的日志也都是应用层以上的,没法完整的看到所有header(如content-length必须在真正执行发送时才知道)。同时也没法确定,API-Gateway异常时发生在Method Request阶段,还是Integration Request阶段。
通过搜索,发现API-Gateway Important Notes:
The following table lists the headers that may be dropped, remapped, or otherwise modified when sent to your integration endpoint or sent back by your integration endpoint.
Header name | Request (http/http_proxy/lambda) | Response (http/http_proxy/lambda) |
---|---|---|
Content-Length | Passthrough (generated based on body) | Passthrough |
Transfer-Encoding | Dropped/Dropped/Exception | Dropped |
drop Transfer-Encoding这是一个proxy需要做的事,没啥问题。参考补充知识里的 Transfer-Encoding与proxy
继续基于Transfer-Encoding搜索发现一些和我们类似的案例:stream response,times out if response is too large,chunked transfer encoding,
结合我们上面的测试结论,大胆推测以下几点(除去lambda):
- 推测1:关于drop Transfer-Encoding: chunked问题。API-Gateway应该是先buffer request,再发送给integration endpoint。 而后续API-Gateway是否还要 加这个header取决于它的逻辑。至少对于proxy来说,这个header不能原封不动的retransmit。buffer也带来的大文件问题(API-Gateway support default payload = 10MB) 和 stream 问题,看起来目前都没支持,所以就有和lambda结合的骚操作。
- 推测2:关于只有DELETE timeout,REST API-Gateway对待DELETE request with chunked payload时,由于rfc2616没有明确禁止delete带payload,所以不同的HTTP实现处理方式不一样,有的会ignore DELETE payload body,有的会reject请求。新的规范rfc7231里也表示了,sending a payload body on a DELETE request might cause some existing implementations to reject the request。可能API-Gateway就是这个implementation。StackOverflow资料
- 推测3:rfc7230里规定了message body length如何定义,针对有payload语义的HTTP method,Content-Length或Transfer-Encoding必须存在,需要标识何时结束message。并且对invalid request,server有411、400去返回client。对invalid response,proxy会502给client,proxy会向server close conn废弃收到的response。而API-Gateway对于invalid request的empty reply的行为有些奇怪,要么是某种考虑的正常行为,要么是触发异常直接no reply。
下面是通过源码分析chunked header在client和server的处理方式。
Httpclient加入chunked header
本地debug,定位到 apache RequestContent
。代码如下:
|
|
由于SimpleHostRoutingFilter里requestEntity默认chunked=false,而delete的对应的content-length = -1,从而造成了底层库加入了这个header。至于为什么有content-length<0这个逻辑,推测有些content-length>Long.MAX_VALUE,必须通过chunked传输。
而且这段逻辑没有区分HTTP method,从这个角度与抓包信息理解,HTTP method只是语义标识,而HTTP payload如何传输,是chunk coding还是content lenght,则是由HttpEntityEnclosingRequest自己控制。
解决方式很简单,不让它传这个头,参考RibbonApacheHttpRequest里
|
|
针对转发的DELETE请求,如果获取到的content-length<0,手动set成0。
golang与tomcat处理chunked request
golang版本1.14,net/http/server.go#1822里c.readRequest(ctx)是一切的入口,最终到net/http/server.go#966里readRequest(c.bufr, keepHostHeader) 返回校验完的request。其中主要方法是:func readTransfer(msg interface{}, r *bufio.Reader) (err error) {}
|
|
对chunked的请求content-length=-1,最终会走到26行,通过chunkedReader继续去读buffer,参考net/http/internal/chunked.go。所以,go http server不会强制限制DELETE body的问题。
我们在看下Tomcat里Http11Processor处理方式,入口在Http11Processor#343 prepareRequest,处理transfer-encoding位置 Http11Processor#721。如下主要部分:
|
|
在处理transfer-encoding和content-length和golang相似,也都没有 限定到DELETE请求的判断逻辑。
补充知识
Transfer-Encoding与proxy
HTTP headers根据用途分类:
- General Headers:req和rsp都可以有的头。如:Date、Cache-Control、Connection
- Request Headers:如:Accept、User-Agent等。Content-Length只在有body的req中(如POST)
- Response Headers:如:Age、Location、Server等。
- Entity Headers:用来描述message body。通常和req和rsp一起使用,如Content-Length、Content-Encoding、Content-Language。
End-to-end headers:These headers must be transmitted to the final recipient of the message: the server for a request, or the client for a response. Intermediate proxies must retransmit these headers unmodified and caches must store them.
Hop-by-hop headers:These headers are meaningful only for a single transport-level connection, and must not be retransmitted by proxies or cached. Note that only hop-by-hop headers may be set using the Connection general header.
Transfer-Encoding是个Hop-by-hop header,也就说:它只作用于proxy,即client到proxy的第一跳;它不能被proxy重传到下一跳。
而一个proxy该如何处理这些Hop-by-hop header呢,参考解释了proxy如何做
其中第1点就是,Remove Hop-by-hop Headers。因为proxy在向下一跳转发是,必须要把当前请求收完处理好。避免引入安全问题。还有就是7和8点,proxy应该对req/rsp支持chunking;对于chunking合理的buffer也是避免不了的,但是也要通过某种方式,避免下一跳等待req/rsp太久;有些reverse proxy会buffer req/rsp,当然也就限制了大文件和stream。
Response chunked问题
我们知道,Chunked transfer encoding是HTTP/1.1里一种streaming data transfer机制,每一块chunk都是独立发送与接收的,最后会进行TCP segment重组,拼接成完整的payload。也就是下图中的TCP segment of a reassembled PDU,最后一个是empty chunk
从图上看出,这个两个chunk的payload之和都没到1K,chunk没有带来优势,反而增加了一次TCP通信,而且一旦chunk也代表这个TCP conn必须保持以用来接收后续数据。
细想一下response是server主动发起的,我们认为它是安全的。request的情况下,如果每个client都是chunked request给server,server占用的TCP conn就会逐步被蚕食,无法响应其它的请求。
Tomcat bug : parsing request headers fail
root-cause如下:
一个HTTP请求,拆成了2个TCP包,其中第一个包的最后一位只有’\r’ -> 0d ,然后’\n’ -> 0a 在下一个TCP包里 最终导致了,Tomcat会对这个HTTP请求 报400
详情看:Tomcat parsing request headers fail bug
|
|
Nginx + Tomcat issues
nginx的proxy_http_version默认是1.0,这就造成了一些奇怪的问题,下面的示例来源于client->nignx->tomcat处理file upload的Request时候。
现象nginx在proxy一个file upload请求时候,没有携带token,tomcat在请求初期毫秒级内,识别到了auth failed,并以http1.1 response了401,并connection: close。但client仍然hang住,直到60s后,tomcat才回了Nginx一个Fin的no data的tcp packet。
而在将proxy_http_version改成1.1后,上面的问题就消失了,client没有hang住,立马收到了401。抓包发现,tomcat还是response了两个tcp packet,第一个是http1.1, transfer-encoding: chunked, connection: close,第二个packet是chunked结束标准0\r\n。
所以,定义处理如下几个问题:
a. 为什么tomcat隔了60s才回了fin结束,并且针对正常的rest api,tomcat可以正常快速response,两个tcp packet,第二个是fin表明关闭连接。
b. 为什么协议改成1.1,就没有60s了,tomcat处理http response成chunked,并正确结束http请求。
c. nginx作为proxy是否buffer了entire file,这样会不会有DDoS问题
d. tomcat是否buffer完整个file,才会进入auth filter校验token,是否会有DDoS问题
第一个问题,60s像是tomcat或linux网络层,主动断了connection。默认情况下,nginx用http1.0请求,connection: close。后端处理完成后就主动关闭连接,所以 TIME_WAIT 在tomcat。
同时我们要知道判断一个http请求如何结束的标志有两种:content-length/chunked和server主动close connection。所以tomcat在处理http1.0时,采用了close connection。但是,它处理rest api正常,但处理file upload时multipart/form-data确hang住了(参考g-nginx->gateway-1.pcap)。猜测tomcat在处理file upload时,虽然提前返回了401 http rsp,但在后续的close connection时hang了60s。可能这是一个bug。
netstat -n | awk '/^tcp/ {++S$NF} END {for(a in S) print a, Sa}'
ss -ant | awk 'NR>1 {++s$1} END {for(k in s) print k,sk}'
然而,在tomcat和nginx里watch netstat状态发现,它们都会一直处在established 60s,然后tomcat状态转为time_wati,nginx这个连接消失。所以,进一步思考是不是tomcat response的HTTP包有问题。查询g-nginx->gateway-1.pcap发现,tomcat在一个tcp包里,包含了完整的http协议内容,但是没有content-length,也没有transfer-encoding:chunked,所以猜测Nginx作为一个proxy,没法正确确认收到了完整的http请求,也就没法结束这个tcp connection,直到tomcat的connection timeout。
第二个问题,http version 1.1后,就没有这个问题了,因为1.1后,tomcat处理response为chunked,并且能够快速发出HTTP ending包来结束请求。
同时引入新问题,正常的api request而不是file upload,tomcat在处理http1.0 connection close,也是会tcp segment完了后直接time_wait。这样大量的短连接,也会产生DDoS的问题,所以才会有尽量使用1.1的说法,防止tomcat time_wait过多,建立不了新连接。
第三个问题,nginx或者说是 一个透明代理,它是否该buffer请求,默认nginx的proxy_request_buffering是开启的,它同时也有个buffer size去控制。如果是一个大文件,nginx如果buffer它会很消耗资源,所以一般proxy都会有max file size limitation。而且buffer也会提高一些性能(复用connection或合并tcp packet之类)。所以,在file upload的情况下,该不该buffer?安全角度还是性能角度,这需要trade off。
第四个问题,tomcat并不是receive entire file后才会进入auth filter的,这个结论的前提是file upload。所以猜测,tomcat对于http body的接收是个stream,如果你不去get它,它就不会阻塞住,通过仍然能处理header。所以,只要你不去显示的get body,tomcat就不会有大文件消耗资源的问题。
reference
Decoding any Java-originated SSL Connection with Pre-Shared Master Secret
DELETE request with empty body results 411 on zuul proxies #1894