一个有趣的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 '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
一个有趣的nginx HTTP 400响应问题分析的更多相关文章
- 一个有趣的nginx问题引发的小问题
最近处理一个nginx问题,故障现象是:所有的work进程,都在等锁.调用的是sem_wait 根据对应的堆栈,查看一下大家等的锁都一样,看看这把锁被谁拿了: 锁的结构是: typedef struc ...
- Nginx配置各种响应头防止XSS,点击劫持,frame恶意攻击
为什么要配置HTTP响应头? 不知道各位有没有被各类XSS攻击.点击劫持 (ClickJacking. frame 恶意引用等等方式骚扰过,百度联盟被封就有这些攻击的功劳在里面.为此一直都在搜寻相关防 ...
- 【小贴士】关于transitionEnd/animate的一个有趣故事
前言 在很久之前,我们项目有一个动画功能,功能本身很简单,便是典型的右进左出,并且带动画功能 以当时来说,虽然很简单,但是受限于框架本身的难度,就直接使用了CSS3的方式完成了功能 当时主要使用tra ...
- nginx 解决400 bad request 的方法(转载)
nginx的400错误比较难查找原因,因为此错误并不是每次都会出现的,另外,出现错误的时候,通常在浏览器和日志里看不到任何有关提示. 经长时间观察和大量试验查明,此乃request header过大所 ...
- nginx 解决400 bad request 的方法
nginx的400错误比较难查找原因,因为此错误并不是每次都会出现的,另外,出现错误的时候,通常在浏览器和日志里看不到任何有关提示. 经长时间观察和大量试验查明,此乃request header过大所 ...
- nginx与apache 对比 apache是同步多进程模型,一个连接对应一个进程;nginx是异步的,多个连接(万级别)可以对应一个进程
nginx与apache详细性能对比 http://m.blog.csdn.net/lengzijian/article/details/7699444 http://www.cnblogs.com/ ...
- 一个有趣的SQL Server 层级汇总数据问题
看SQL Server大V宋大侠的博客文章,发现了一个有趣的sql server层级汇总数据问题. 具体的问题如下: parent_id emp_id emp_nam ...
- 一个有趣的模拟光照的shader
一个有趣的模拟光照的shader(类似法线贴图) http://www.cnblogs.com/flytrace/p/3395911.html ----- 可否用于需UI中需要加灯的模型.
- 一个有趣的 SQL 查询(查询7天连续登陆)
一个有趣的 SQL 查询 一个朋友有这样一个SQL查询需求: 有一个登录表(tmp_test),包含用户ID(uid)和登录时间(login_time).表结构如下: . row ********** ...
- 另一个有趣的Captcha 网站
今天在一个网站注册时又发现了一个有趣的Captcha形式.给你一个翻转的图片,然后让你拽下面的slide bar让它回到正常的位置,很有趣.下面是提供这个Captcha的网站. minteye – s ...
随机推荐
- NODE 基于express 框架和mongoDB的cookie和session认证 和图片的上传和删除
源码地址 https://gitee.com/zyqwasd/mongdbSession 本项目的mongodb是本地的mongodb 开启方法可以百度一下 端口是默认的27017 页面效果 1. 注 ...
- 输入法词库解析(二)搜狗拼音细胞词库.scel(.qcel)
详细代码:https://github.com/cxcn/dtool 前言 .scel 是搜狗拼音输入法所使用的细胞词库格式,可以在 https://pinyin.sogou.com/dict/ 下载 ...
- 使用Gitlab CI/CD功能在本地部署 Spring Boot 项目
前提条件: 1.Docker安装Gitlab,地址:https://www.cnblogs.com/sanduzxcvbnm/p/13814730.html 2.Docker安装Gitlab-runn ...
- proxysql cluster 的搭建
文章转载自:https://blog.51cto.com/lee90/2298804 官方文档: https://proxysql.com/blog/proxysql-cluster 环境架构 在一主 ...
- Elasticsearch:Elasticsearch SQL介绍及实例(二)
转载自:https://blog.csdn.net/UbuntuTouch/article/details/105699014
- 类似-Xms、-Xmn这些参数的含义
答: 堆内存分配: JVM初始分配的内存由-Xms指定,默认是物理内存的1/64 JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的 ...
- 我的 Kafka 旅程 - Consumer
kafka采用Consumer消费者Pull主动拉取数据的方式,当Broker无数据时,消费者空转.Kafka并不删除已消费的消息,各自独立的消费者可消费同一个Broker分区数据. 消费流程 1.消 ...
- mac通过docker一键部署Jenkins
目录 mac通过docker一键部署Jenkins 一.前言 二.系统配置 三.安装步骤 Dockerhub查看镜像地址 1.一键安装 1.1.下载脚本 1.2.安装程序 1.2.1.安装程序详情 1 ...
- 2020-2021 Winter Petrozavodsk Camp, Belarusian SU Contest (XXI Open Cup, Grand Prix of Belarus) 题解
题目列表 C. Brave Seekers of Unicorns D. Bank Security Unification G. Biological Software Utilities I. B ...
- Android类加载流程
背景 由于前前前阵子写了个壳,得去了解类的加载流程,当时记了一些潦草的笔记.这几天把这些东西简单梳理了一下,本文分析的代码基于Android8.1.0源码. 流程分析 从loadClass开始,我们来 ...