背景

之前在一次不规范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问题:

  1. 第一类是之前解决了的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"}}
  1. 第二类是这次新发现的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

一个有趣的nginx HTTP 400响应问题分析的更多相关文章

  1. 一个有趣的nginx问题引发的小问题

    最近处理一个nginx问题,故障现象是:所有的work进程,都在等锁.调用的是sem_wait 根据对应的堆栈,查看一下大家等的锁都一样,看看这把锁被谁拿了: 锁的结构是: typedef struc ...

  2. Nginx配置各种响应头防止XSS,点击劫持,frame恶意攻击

    为什么要配置HTTP响应头? 不知道各位有没有被各类XSS攻击.点击劫持 (ClickJacking. frame 恶意引用等等方式骚扰过,百度联盟被封就有这些攻击的功劳在里面.为此一直都在搜寻相关防 ...

  3. 【小贴士】关于transitionEnd/animate的一个有趣故事

    前言 在很久之前,我们项目有一个动画功能,功能本身很简单,便是典型的右进左出,并且带动画功能 以当时来说,虽然很简单,但是受限于框架本身的难度,就直接使用了CSS3的方式完成了功能 当时主要使用tra ...

  4. nginx 解决400 bad request 的方法(转载)

    nginx的400错误比较难查找原因,因为此错误并不是每次都会出现的,另外,出现错误的时候,通常在浏览器和日志里看不到任何有关提示. 经长时间观察和大量试验查明,此乃request header过大所 ...

  5. nginx 解决400 bad request 的方法

    nginx的400错误比较难查找原因,因为此错误并不是每次都会出现的,另外,出现错误的时候,通常在浏览器和日志里看不到任何有关提示. 经长时间观察和大量试验查明,此乃request header过大所 ...

  6. nginx与apache 对比 apache是同步多进程模型,一个连接对应一个进程;nginx是异步的,多个连接(万级别)可以对应一个进程

    nginx与apache详细性能对比 http://m.blog.csdn.net/lengzijian/article/details/7699444 http://www.cnblogs.com/ ...

  7. 一个有趣的SQL Server 层级汇总数据问题

        看SQL Server大V宋大侠的博客文章,发现了一个有趣的sql server层级汇总数据问题.          具体的问题如下:     parent_id emp_id emp_nam ...

  8. 一个有趣的模拟光照的shader

    一个有趣的模拟光照的shader(类似法线贴图) http://www.cnblogs.com/flytrace/p/3395911.html -----  可否用于需UI中需要加灯的模型.

  9. 一个有趣的 SQL 查询(查询7天连续登陆)

    一个有趣的 SQL 查询 一个朋友有这样一个SQL查询需求: 有一个登录表(tmp_test),包含用户ID(uid)和登录时间(login_time).表结构如下: . row ********** ...

  10. 另一个有趣的Captcha 网站

    今天在一个网站注册时又发现了一个有趣的Captcha形式.给你一个翻转的图片,然后让你拽下面的slide bar让它回到正常的位置,很有趣.下面是提供这个Captcha的网站. minteye – s ...

随机推荐

  1. 阿里云OSS存储前端API上传(签名上传)

    一.创建用户 在阿里云创建用户https://ram.console.aliyun.com/users,并勾选Open API 保存好信息,很重要,返回后就再也找不到了 新增授权(这里视个人情况,需要 ...

  2. Kubernetes实践技巧:集群升级k8s版本

    更新证书 使用 kubeadm 安装 kubernetes 集群非常方便,但是也有一个比较烦人的问题就是默认的证书有效期只有一年时间,所以需要考虑证书升级的问题,本文的演示集群版本为 v1.16.2 ...

  3. K8s 上的分布式存储集群搭建(Rook/ceph)

    转载自:https://mp.weixin.qq.com/s/CdLioTzU4oWI688lqYKXUQ 1 环境准备 1.1 基础环境 3台配置一致的虚拟机 虚拟机配置:4c 8g 虚拟机操作系统 ...

  4. centos系统安装MySQL8

    使用yum仓库安装MySQL8 1.查看centos系统版本 # cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) 2.下载并安 ...

  5. 2.Ceph 基础篇 - 集群部署及故障排查

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247485243&idx=1&sn=e425c31a ...

  6. Beats:使用Elastic Stack对Nginx Web服务器监控

  7. spring boot http status 400

    SpringBootAdmin不是Spring官方提供的模块,它包含了Client和Server两部分.server部分提供了用户管理界面,client即为被监控的服务.client需要注册到serv ...

  8. 9. Fluentd部署:日志

    Fluentd是用来处理其他系统产生的日志的,它本身也会产生一些运行时日志.Fluentd包含两个日志层:全局日志和插件级日志.每个层次的日志都可以进行单独配置. 日志级别 Fluentd的日志包含6 ...

  9. css语言

    css:样式表.级联样式表.层叠样式表 css写在style标签里面,放在head标签中:大括号中写键值对语法 color:文字颜色 Font-family:字体 Font-size:字号 text- ...

  10. 玖章算术受邀参加红杉Talk「创新的复利」科技专场,共同探讨云计算的前世今生

    9月2日,本周五14:00 「创新的复利」 Sequoia Talk系列论坛,首期直播盛大启动.在第一期科技专场,4位红杉中国资深投资人.8位创新创业者将带我们深入工业软件.机器人.云计算等领域,围绕 ...