一個有趣的nginx HTTP 400響應問題分析
背景
之前在一次不規範HTTP請求引發的nginx響應400問題分析與解決 中寫過客戶端query參數未urlencode導致的400問題,當時的結論是:
對於query參數帶空格的請求,由於其不符合HTTP規範,golang的net/http庫無法識別會直接報錯400,而nginx和使用uwsgi與nginx交互的api主服務卻可以兼容,可以正常處理。
最終的臨時解決方案是:在nginx層根據query 參數是否包含空格決定是轉發到golang的log server或api主服務。
本來以為這事就這麼結束了,結果最近查詢nginx的錯誤log,居然又發現少部分400錯誤,最終定位也是因為query 參數包含空格,而且這次報錯是直接在nginx層返回400,後面的轉發判定邏輯都不會被觸發,於是很神奇的發現了兩類空格導致的400問題:
- 第一類是之前解決了的nginx可以兼容識別,但golang 網路庫無法識別會報400的含空格請求,舉例入下:
curl '//test.myexample123.com/test?appname=demoapp&phonetype=android&device_type=android&osn=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&osv=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&channel=Google Play&model=HUAWEIHLK-AL00&build=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)'
{"status": 1, "data": {"test": "ok"}}
- 第二類是這次新發現的nginx層直接返回400的含空格請求,並且還發發現該類報錯很多都是來源於華為手機,如下可看出其400響應為nginx直接返回:
curl '//test.myexample123.com/test?appname=demoapp&phonetype=android&device_type=android&osn=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&osv=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&channel=Google Play&model=HUAWEI HLK-AL00&build=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)'
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.16.1</center>
</body>
</html>
乍看之下其請求參數完全沒看出區別,無論哪類問題只要去掉了空格就不會有問題了,難不成nginx對華為手機還有歧視不成(>_<)。
問題定位
兩類問題都是由於query參數帶空格引起的,最終通過二分法試錯確認了其關鍵區別:如果query參數中包含” H”–即空格+H的組合,nginx層即會直接報錯返回400,而如果不包含” H”這一組合,nginx層將能兼容處理–這解釋了為何大部分400請求來自華為手機,因為華為手機model參數很多都是”HUAWEI HRY-AL00″這類取值,即包含了” H”這一子串,看起來” H”這個組合在nginx內部有特殊含義,華為手機給撞槍口上了。
那” H”在nginx中到底有什麼特殊含義呢?又到了探究源碼的時候了,通過拜讀源碼最終在ngx_http_parse.c 中負責解析http 請求行的ngx_http_parse_request_line 函數中找到了原因,如下
103 ngx_int_t
104 ngx_http_parse_request_line(ngx_http_request_t *r, ngx_buf_t *b)
105 {
106 u_char c, ch, *p, *m;
107 enum {
108 sw_start = 0,
109 sw_method,
110 sw_spaces_before_uri,
111 sw_schema,
112 sw_schema_slash,
113 sw_schema_slash_slash,
114 sw_host_start,
115 sw_host,
116 sw_host_end,
117 sw_host_ip_literal,
118 sw_port,
119 sw_host_http_09,
120 sw_after_slash_in_uri,
121 sw_check_uri,
122 sw_check_uri_http_09,
123 sw_uri,
124 sw_http_09,
125 sw_http_H,
126 sw_http_HT,
127 sw_http_HTT,
128 sw_http_HTTP,
129 sw_first_major_digit,
130 sw_major_digit,
131 sw_first_minor_digit,
132 sw_minor_digit,
133 sw_spaces_after_digit,
134 sw_almost_done
135 } state;
136
137 state = r->state;
138
139 for (p = b->pos; p < b->last; p++) {
140 ch = *p;
141
142 switch (state) {
143
144 /* HTTP methods: GET, HEAD, POST */
145 case sw_start:
146 r->request_start = p;
147
148 if (ch == CR || ch == LF) {
149 break;
150 }
...
486 /* check "/.", "//", "%", and "\" (Win32) in URI */
487 case sw_after_slash_in_uri:
488
489 if (usual[ch >> 5] & (1U << (ch & 0x1f))) {
490 state = sw_check_uri;
491 break;
492 }
493
494 switch (ch) {
495 case ' ':
496 r->uri_end = p;
497 state = sw_check_uri_http_09;
498 break;
499 case CR:
500 r->uri_end = p;
501 r->http_minor = 9;
502 state = sw_almost_done;
503 break;
...
606 /* space+ after URI */
607 case sw_check_uri_http_09:
608 switch (ch) {
...
618 case 'H':
619 r->http_protocol.data = p;
620 state = sw_http_H;
621 break;
622 default:
623 r->space_in_uri = 1;
624 state = sw_check_uri;
625 p--;
626 break;
627 }
628 break;
...
684 case sw_http_H:
685 switch (ch) {
686 case 'T':
687 state = sw_http_HT;
688 break;
689 default:
690 return NGX_HTTP_PARSE_INVALID_REQUEST;
691 }
692 break;
693
694 case sw_http_HT:
695 switch (ch) {
696 case 'T':
697 state = sw_http_HTT;
698 break;
699 default:
700 return NGX_HTTP_PARSE_INVALID_REQUEST;
701 }
702 break;
703
704 case sw_http_HTT:
705 switch (ch) {
706 case 'P':
707 state = sw_http_HTTP;
708 break;
709 default:
710 return NGX_HTTP_PARSE_INVALID_REQUEST;
711 }
712 break;
...
如上ngx_http_parse_request_line函數解析請求行原理為通過for循環逐個遍歷字元,內部使用大量switch語句實現了一個狀態機進行解析。
當解析到sw_after_slash_in_uri分支的case ‘ ‘(495行)時,會設置狀態state=sw_check_uri_http_09,而後在sw_check_uri_http_09分支的case ‘H'(618行)設置state=sw_http_H,而sw_http_H其實是HTTP protocol的解析分支,其負責解析出類似HTTP/1.1 這樣的內容,所以在分支sw_http_H(684行)其期待的正確字元應該是HTTP/1.1的 第二個字元T,而後進入case sw_http_HT期待解析HTTP/1.1的第三個字元T,以此類推最終逐個解析完成整個protocol字元串,但是在sw_http_H分支中若沒有解析到期望的字元T,其默認行為就是直接返回NGX_HTTP_PARSE_INVALID_REQUEST,也就是400常量了。
簡單來說,nginx在解析請求行時,若在query參數中遇到了” H”的組合會導致狀態機認為已經進入protocol欄位的解析分支,當碰到不識別的字元串則認為格式錯誤,會直接返回400,而如果query參數中雖然包含未轉義空格但卻沒有” H”組合,nginx的這個請求行解析狀態機倒還能夠一定程度兼容此類錯誤,將請求正常轉發給upstream server處理。
當然,無論nginx能不能兼容query參數未轉義空格,最正確的做法還是客戶端應該一開始就保證所有query參數都經過必要urlencode再進行使用,這樣壓根就不會有這麼一堆幺蛾子。
轉載請註明出處://www.cnblogs.com/AcAc-t/p/nginx_http_400_for_space_H.html