Nginx request line 换行导致的 400 异常分析

NGINX 处理转发请求时报错

最近在做一个需求时,NGINX 出现了 400 异常。具体细节如下:有两台独立的 NGINX,每一台 NGINX 都能正常的处理请求。但是当我用 lua 脚本,将一台 NGINX 接收到的请求筛选后,转发给另外一台 NGINX 处理时,收到如下 400 响应:

HTTP/1.1 400 Bad Request
Server: openresty/1.11.2.2
Date: Mon, 06 Aug 2018 13:36:20 GMT
Content-Type: text/html
Content-Length: 179
Connection: close
 
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>openresty/1.11.2.2</center>
</body>
</html>

而另外一台 NGINX 的 access 日志中,只显示请求响应 400,error 日志中无其他报错。当我用 postman 或者 curl 命令直接请求这台 NGINX 时,响应一切正常。 curl 命令内容如下:

curl --request POST \
  --url http://127.0.0.1:80/root/do/uri \
  --header 'beta: 1' \
  --data 'param1=aWBIWi7VwmZE0kQqHcyUIZ1wy4I9b99p8wNLiL0IOzYNv5Dz61Pv620shbsolqBCw%252B8S6rNzLywb%250AJ5dBwjPtc6h%252BxJoTuP4h%252Bi6kd5CFx%252BBWQgvjIFDIdbwfb0WpnkBc%252FE3aDQJJbmajLnJM3NHjaw%253D%253D%250A&param1 =4220'

在将 NGINX 的 error 日志调成 debug 级别后,重新复现问题时,发现 NGINX 在响应400 时,会有一行 info 级别的报错:

2018/08/06 16:15:01 [info] 280#0: *251 client sent invalid request while reading client request line, client: 215.131.75.20, server: ng_instance, request: "POST /uri-path"

根据报错信息显示 “NGINX 在读取请求行时,发现客户端发起的是一个无效请求”,而又结合直接请求这台 NGINX 时响应正常的现象分析,第一台 NGINX 在转发请求时,发起的请求可能有问题。在第二台 NGINX 上使用 tcpdump 命令抓包分析正常请求和异常请求的内容时,发现被转发的异常请求的 request line 中有换行,除此外无其他任何区别:

  • 正常请求
POST /uri-path HTTP/1.1
User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.3.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Host: 215.131.72.4
Accept: */*
Content-Type: application/x-www-form-urlencoded
beta: 1
X-Forwarded-Host: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Forwarded-Server: 127.0.0.1
X-Forwarded-For: 127.0.0.1
Content-Length: 212
  • 被转发的异常请求
POST /uri-path
HTTP/1.1
Host: 127.0.0.1
X-Forwarded-Host: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Forwarded-Server: 127.0.0.1
X-Forwarded-For: 127.0.0.1
Content-Length: 212
User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.3.0 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
Accept: */*
Content-Type: application/x-www-form-urlencoded
beta: 1

既然只发现这一处差异,那是不是 request line 中有换行导致 NGINX 在处理请求时出现 400 异常呢?

什么是正确的 request line ?

首先看一下 RFC 2616 定义的标准 HTTP 请求的格式:

Request       = Request-Line                    
                        *(( general-header      
                         | request-header       
                         | entity-header ) CRLF)
                        CRLF
                        [ message-body ]        

第一行就是 Request-Line,Request-Line 显示了请求的方式、请求资源的位置及使用的 HTTP 协议版本。根据 RFC 2616 文档定义,Request-Line 是由三部分组成:请求方法+Request-URI+HTTP-Version,而且这三部分之间,只能用空格(Space)分开,Request-Line 的最后部分,必须是一个换行符(CRLF)。根据这个定义,异常请求的 request line 中有个换行一定是不合法的。

5.1 Request-Line

    The Request-Line begins with a method token, followed by the
    Request-URI and the protocol version, and ending with CRLF. The
    elements are separated by SP characters. No CR or LF is allowed
    except in the final CRLF sequence.
    
        Request-Line   = Method SP Request-URI SP HTTP-Version CRLF

NGINX 读取 request line 与 400 异常

NGINX 接收到请求后,会对请求进行解析。在 NGINX 实现中,会根据 HTTP 协议规范实现一个有限状态机,NGINX 在解析请求时,会经过这个状态机。在读取 request line 时,NGINX 会记录请求方法、URI 信息及 HTTP 协议信息,以及其他有用的信息放在缓存中,以便后面流程使用。如果请求行处理正常,接着会按照状态机流程处理请求头。但是在处理请求行时,如果客户端提前关闭连接、或者请求行不符合协议规范,读取数据时发生了错误,NGINX 就会立刻终止请求解析,同时给客户端返回一个400错误。当然,如果是客户端提前关闭了连接,400 错误就无法正常到达客户端。因此,被 NGINX 转发的请求,由于 request line 多了一个换行,所以 NGINX 返回了 400 BAD REQUEST 异常。

那么,这个换行是怎么来的呢?

哪里来的换行?

在我们实现的逻辑中,我们会把需要被 NGINX 转发的接口放到一个配置文件中,当需要增加或减少接口时,直接修改配置文件即可:

/uri-path0#/new-uri-path0
/uri-path1#/new-uri-path1
/uri-path2#/new-uri-path2
/uri-path3#/new-uri-path3
/uri-path4#/new-uri-path4
/uri-path5#/new-uri-path5
/uri-path6#/new-uri-path6
/uri-path7#/new-uri-path7
/uri-path8#/new-uri-path8
/uri-path9#/new-uri-path9

配置文件中的每一行都有两部分组成:原 URI 和目的 URI,中间用“#”号隔开,且原 URI 和目的 URI 不一样。我们的需求就是如果 NGINX 接收的请求在这个配置文件中,则将请求的URI 替换成目的 URI,然后转发给另外一台 NGINX 处理。

我们用 lua 脚本一行行读取这个配置文件,然后解析成原 URI 和目的 URI,存放在共享内存中。刚开始的时候,功能一直正常,请求转发和请求处理都可以正常处理。几天之后,NGINX 在处理转发请求时,开始报 400 异常。同时,我们从日志中也观察到,客户端 NGINX 在进行 URI 替换时, 日志中输出的替换后的 URI会多一个^M符号。为什么之前日志中没有这个符号,现在突然多个这个符号呢?这个符号又是什么意思呢?

2018/08/06 22:11:28 [debug] 362#362: *1842 [lua] requestRewrite.lua:60: requestRewrite(): rewrite module target uri is /new-uri-path9^M

其实 Linux/Unix 系统、Mac 系统和 DOS/Windows 系统在处理文本文件时,对换行处理是不一样的。Windows 中的换行符是\r\n,Linux/Unix 下的换行符是\n, Mac 系统是\r。其中\r表示的是回车,\n是换行。因为对换行处理的不一致,导致 Windows 系统的文本文件在 Linux/Unix
打开时,每行的结尾会多一个^M符号;而 Linux/Unix 系统的文本文件在 Windows 中打开时,所有行都会变成一行。

而导致我们这次问题的原因,就是因为最开始大家在编辑这个配置文件的时候,使用的都是 Mac 电脑,即 Unix 系统。后来,其他同事使用 Windows 电脑编辑这个配置文件,并上传到服务器。lua 脚本在读取配置文件的时候,每一行后面都多个了一个\r,即换行符,因此解析得到的目的 URI,也多了一个换行符。所以,客户端 NGINX 在转发请求的时候,request line 中多了一个换行符,导致目的 NGINX 在解析的时候报错。

所以,lua 在读取配置文件时,增加对换行的兼容,问题得到解决。

处理‘^M’ 方式

当在 Linux/Unix 系统打开 Windows 的文本文件出现^M符号时,可以很简单通过下面方式解决:

cat file | sed 's/^M//' > newfile

或者:

 cat file | tr -d '\r' > newfile

参考文档

Nginx giving 400 error
nginx的请求处理阶段 (90%)
nginx中http request处理的流程
How to Remove ^M in Linux & Unix
lua文件读取注意事项
VIM 小技巧 - replace '^M'

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容

  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,670评论 24 1,002
  • 1. Nginx的模块与工作原理 Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单...
    rosekissyou阅读 10,211评论 5 124
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • Nginx 的配置文件使用的就是一门微型的编程语言,许多真实世界里的 Nginx 配置文件其实就是一个一个的小程序...
    SkTj阅读 4,195评论 0 7
  • 门当户对真的很重要吗,结婚是精神重要还是物质重要。 结婚是一生中非常重要的事情,很多人在结婚的时候是反复考虑,门当...
    柯依生阅读 278评论 0 0