背景

之前在一次不规范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. 利用高级组策略管理AGPM复制组策略GPO

    有时候管理多个林,在一个林中配置了GPO之后,想复制出来用到其它林里.默认系统的组策略管理里没有这个功能.但是微软在微软企业桌面优化套件Microsoft Desktop Optimization P ...

  2. Elastic:应用程序性能监控/管理(APM)实践

    在今天的文章里,我们将介绍Elastic的一个重要的应用:应用程序性能管理(Application Performance Monitoring/Management),简称APM.那么到底什么是AP ...

  3. overflow 隐藏滚动条样式

    在使用overflow,属性值为auto或者scroll时,很多时候会有多余的滚动条在旁边,这时就非常影响观瞻,所以我们有时需要将滚动条隐藏掉. 今天就说两种我用到的隐藏滚动条的方法,如果有其他解决方 ...

  4. 关于csh-C-shell的记录

    csh,由柏克莱大学的 Bill Joy 设计的,语法有点类似C语言,所以才得名为 C shell ,简称为 csh Bill Joy 是一个风云人物,他创立了 BSD 操作系统,开发了 vi 编辑器 ...

  5. @Transactional注解真的有必要声明rollbackFor属性吗?

    @Transactional注解真的有必要声明rollbackFor属性吗? ​ 今天在看spring的事务底层源码时,想到一个问题,@Transactional注解真的有必要声明rollbackFo ...

  6. 微软出品自动化神器【Playwright+Java】系列(五) 之 常见点击事件操作

    写在前面 明天就是周五了,这周有那么一两天心情特别不好,真的是做什么都没兴致,所以导致整个人都很丧,什么都不想做. 本打算周一就更新这篇文章的,但由于公司一直加班,每天到家很晚,都是挤时间去学,理解后 ...

  7. Selenium+Python系列(二) - 元素定位那些事

    一.写在前面 今天一实习生小孩问我,说哥你自动化学了多久才会的,咋学的? 自学三个月吧,真的是硬磕呀,当时没人给讲! 其实,学什么都一样,真的就是你想改变的决心有多强罢了. 二.元素定位 这部分内容可 ...

  8. Silky微服务框架之服务引擎

    构建服务引擎 在注册Silky微服务应用一节中,我们了解到在ConfigureServices阶段,通过IServiceCollection的扩展方法AddSilkyServices<T> ...

  9. Charles基本功能

    Windows: 运行安装应用程序以在程序菜单中安装 Charles. Mac OS X: 通过双击解压缩下载文件,然后将 Charles 应用程序复制到 Applications 目录中. Linu ...

  10. 6.-Django设计模式及模版层

    一.MVC (java等其他语言) MVC代表Model-view-Contorller(模型-视图-控制器)模式 M模型层主要用于对数据库层的封装 V视图层用于向用户展示结果 C控制器用于处理请求. ...