背景
之前在一次不规范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 'http://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 'http://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再进行使用,这样压根就不会有这么一堆幺蛾子。
转载请注明出处:https://www.cnblogs.com/AcAc-t/p/nginx_http_400_for_space_H.html