摘要:開始重現(xiàn)客戶端指令其實(shí)這次請求的一些貓膩很容易就能發(fā)現(xiàn)在中有空格。而在函數(shù)中,做的主要事情就是來解析數(shù)據(jù)包,在解析完成后執(zhí)行一下回調(diào)函數(shù)。具體的一些回調(diào)函數(shù)就不細(xì)講了,有興趣的童鞋可自行翻閱。如代碼片段所示,前文中所對應(yīng)的函數(shù)就是了。
本文首發(fā)于知乎專欄螞蟻金服體驗(yàn)科技。
首先聲明,我在“Bug”字眼上加了引號,自然是為了說明它并非一個(gè)真 Bug。
問題拋出昨天有個(gè)童鞋在看后臺(tái)監(jiān)控的時(shí)候,突然發(fā)現(xiàn)了一個(gè)錯(cuò)誤:
[error] 000001#0: ... upstream prematurely closed connection while reading response header from upstream. client: 10.10.10.10 server: foo.com request: "GET /foo/bar?rmicmd,begin run clean docker images job HTTP/1.1" upstream: "http://..."
大概意思就是說:一臺(tái)服務(wù)器通過 HTTP 協(xié)議去請求另一臺(tái)服務(wù)器的時(shí)候,單方面被對方服務(wù)器斷開了連接——并且并沒有任何返回。
開始重現(xiàn) 客戶端 CURL 指令其實(shí)這次請求的一些貓膩很容易就能發(fā)現(xiàn)——在 URL 中有空格。所以我們能簡化出一條最簡單的 CURL 指令:
$ curl "http://foo/bar baz" -v
最小 Node.js 源碼注意:不帶任何轉(zhuǎn)義。
好的,那么接下去開始寫相應(yīng)的最簡單的 Node.js HTTP 服務(wù)端源碼。
"use strict"; const http = require("http"); const server = http.createServer(function(req, resp) { console.log("?"); resp.end("hello world"); }); server.listen(5555);
大功告成,啟動(dòng)這段 Node.js 代碼,開始試試看上面的指令吧。
如果你也正在跟著嘗試這件事情的話,你就會(huì)發(fā)現(xiàn) Node.js 的命令行沒有輸出任何信息,尤其是嘲諷的 "?",而在 CURL 的結(jié)果中,你將會(huì)看見:
$ curl "http://127.0.0.1:5555/d d" -v * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0) > GET /d d HTTP/1.1 > Host: 127.0.0.1:5555 > User-Agent: curl/7.54.0 > Accept: */* > * Empty reply from server * Connection #0 to host 127.0.0.1 left intact curl: (52) Empty reply from server
瞧,Empty reply from server。
Nginx發(fā)現(xiàn)了問題之后,就有另一個(gè)問題值得思考了:就 Node.js 會(huì)出現(xiàn)這種情況呢,還是其它一些 HTTP 服務(wù)器也會(huì)有這種情況呢。
于是拿小白鼠 Nginx 做了個(gè)實(shí)驗(yàn)。我寫了這么一個(gè)配置:
server { listen 5555; location / { return 200 $uri; } }
接著也執(zhí)行一遍 CURL,得到了如下的結(jié)果:
$ curl "http://127.0.0.1:5555/d d" -v * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0) > GET /d d HTTP/1.1 > Host: 127.0.0.1:5555 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Server: openresty/1.11.2.1 < Date: Tue, 12 Dec 2017 09:07:56 GMT < Content-Type: application/octet-stream < Content-Length: 4 < Connection: keep-alive < * Connection #0 to host xcoder.in left intact /d d
于是乎,理所當(dāng)然,我暫時(shí)將這個(gè)事件定性為 Node.js 的一個(gè) Bug。
Node.js 源碼排查認(rèn)定了它是個(gè) Bug 之后,我就開始了一貫的看源碼環(huán)節(jié)——由于這個(gè) Bug 的復(fù)現(xiàn)條件比較明顯,我暫時(shí)將其定性為“Node.js HTTP 服務(wù)端模塊在接到請求后解析 HTTP 數(shù)據(jù)包的時(shí)候解析 URI 時(shí)出了問題”。
http.js -> _http_server.js -> _http_common.js源碼以 Node.js 8.9.2 為準(zhǔn)。
這里先預(yù)留一下我們能馬上想到的 node_http_parser.cc,而先講這幾個(gè)文件,是有原因的——這涉及到最后的一個(gè)應(yīng)對方式。
首先看看 lib/http.js 的相應(yīng)源碼:
... const server = require("_http_server"); const { Server } = server; function createServer(requestListener) { return new Server(requestListener); }
那么,馬上進(jìn)入 lib/_http_server.js 看吧。
首先是創(chuàng)建一個(gè) HttpParser 并綁上監(jiān)聽獲取到 HTTP 數(shù)據(jù)包后解析結(jié)果的回調(diào)函數(shù)的代碼:
const { parsers, ... } = require("_http_common"); function connectionListener(socket) { ... var parser = parsers.alloc(); parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; socket.parser = parser; parser.incoming = null; ... state.onData = socketOnData.bind(undefined, this, socket, parser, state); ... socket.on("data", state.onData); ... } function socketOnData(server, socket, parser, state, d) { assert(!socket._paused); debug("SERVER socketOnData %d", d.length); var ret = parser.execute(d); onParserExecuteCommon(server, socket, parser, state, ret, d); }
從源碼中文我們能看到,當(dāng)一個(gè) HTTP 請求過來的時(shí)候,監(jiān)聽函數(shù) connectionListener() 會(huì)拿著 Socket 對象加上一個(gè) data 事件監(jiān)聽——一旦有請求連接過來,就去執(zhí)行 socketOnData() 函數(shù)。
而在 socketOnData() 函數(shù)中,做的主要事情就是 parser.execute(d) 來解析 HTTP 數(shù)據(jù)包,在解析完成后執(zhí)行一下回調(diào)函數(shù) onParserExecuteCommon()。
至于這個(gè) parser,我們能看到它是從 lib/_http_common.js 中來的。
var parsers = new FreeList("parsers", 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); ... parser[kOnHeaders] = parserOnHeaders; parser[kOnHeadersComplete] = parserOnHeadersComplete; parser[kOnBody] = parserOnBody; parser[kOnMessageComplete] = parserOnMessageComplete; parser[kOnExecute] = null; return parser; });
能看出來 parsers 是 HTTPParser 的一條 Free List(效果類似于最簡易的動(dòng)態(tài)內(nèi)存池),每個(gè) Parser 在初始化的時(shí)候綁定上了各種回調(diào)函數(shù)。具體的一些回調(diào)函數(shù)就不細(xì)講了,有興趣的童鞋可自行翻閱。
這么一來,鏈路就比較明晰了:
請求進(jìn)來的時(shí)候,Server 對象會(huì)為該次請求的 Socket 分配一個(gè) HttpParser 對象,并調(diào)用其 execute() 函數(shù)進(jìn)行解析,在解析完成后調(diào)用 onParserExecuteCommon() 函數(shù)。
node_http_parser.cc我們在 lib/_http_common.js 中能發(fā)現(xiàn),HTTPParser 的實(shí)現(xiàn)存在于 src/node_http_parser.cc 中:
const binding = process.binding("http_parser"); const { methods, HTTPParser } = binding;
至于為什么 const binding = process.binding("http_parser") 就是對應(yīng)到 src/node_http_parser.cc 文件,以及這一小節(jié)中下面的一些 C++ 源碼相關(guān)分析,不明白且有興趣的童鞋可自行去閱讀更深一層的源碼,或者網(wǎng)上搜索答案,或者我提前無恥硬廣一下我快要上市的書《Node.js:來一打 C++ 擴(kuò)展》——里面也有說明,以及我的有一場知乎 Live《深入理解 Node.js 包與模塊機(jī)制》。
總而言之,我們接下去要看的就是 src/node_http_parser.cc 了。
env->SetProtoMethod(t, "close", Parser::Close); env->SetProtoMethod(t, "execute", Parser::Execute); env->SetProtoMethod(t, "finish", Parser::Finish); env->SetProtoMethod(t, "reinitialize", Parser::Reinitialize); env->SetProtoMethod(t, "pause", Parser::Pause); env->SetProtoMethod(t, "resume", Parser::Pause ); env->SetProtoMethod(t, "consume", Parser::Consume); env->SetProtoMethod(t, "unconsume", Parser::Unconsume); env->SetProtoMethod(t, "getCurrentBuffer", Parser::GetCurrentBuffer);
如代碼片段所示,前文中 parser.execute() 所對應(yīng)的函數(shù)就是 Parser::Execute() 了。
class Parser : public AsyncWrap { ... static void Execute(const FunctionCallbackInfo& args) { Parser* parser; ... Local
首先進(jìn)入 Parser 的靜態(tài) Execute() 函數(shù),我們看到它把傳進(jìn)來的 Buffer 轉(zhuǎn)化為 C++ 下的 char* 指針,并記錄其數(shù)據(jù)長度,同時(shí)去執(zhí)行當(dāng)前調(diào)用的 parser 對象所對應(yīng)的 Execute() 函數(shù)。
在這個(gè) Execute() 函數(shù)中,有個(gè)最重要的代碼,就是:
size_t nparsed = http_parser_execute(&parser_, &settings, data, len);
這段代碼是調(diào)用真正解析 HTTP 數(shù)據(jù)包的函數(shù),它是 Node.js 這個(gè)項(xiàng)目的一個(gè)自研依賴,叫 http-parser。它獨(dú)立的項(xiàng)目地址在 https://github.com/nodejs/http-parser,我們本文中用的是 Node.js v8.9.2 中所依賴的源碼,應(yīng)該會(huì)有偏差。
http-parser HTTP Request 數(shù)據(jù)包體如果你已經(jīng)對 HTTP 包體了解了,可以略過這一節(jié)。
HTTP 的 Request 數(shù)據(jù)包其實(shí)是文本格式的,在 Raw 的狀態(tài)下,大概是以這樣的形式存在:
方法 URI HTTP/版本 頭1: 我是頭1 頭2: 我是頭2
簡單起見,這里就寫出最基礎(chǔ)的一些內(nèi)容,至于 Body 什么的大家自己找資料看吧。
上面的是什么意思呢?我們看看 CURL 的結(jié)果就知道了,實(shí)際上對應(yīng) curl ... -v 的中間輸出:
GET /test HTTP/1.1 Host: 127.0.0.1:5555 User-Agent: curl/7.54.0 Accept: */*
所以實(shí)際上大家平時(shí)在文章中、瀏覽器調(diào)試工具中看到的什么請求頭啊什么的,都是以文本形式存在的,以換行符分割。
而——重點(diǎn)來了,導(dǎo)致我們本文所述“Bug”出現(xiàn)的請求,它的請求包如下:
GET /foo bar HTTP/1.1 Host: 127.0.0.1:5555 User-Agent: curl/7.54.0 Accept: */*
重點(diǎn)在第一行:
GET /foo bar HTTP/1.1源碼解析
話不多少,我們之間前往 http-parser 的 http_parser.c 看 http_parser_execute () 函數(shù)中的狀態(tài)機(jī)變化。
從源碼中文我們能看到,http-parser 的流程是從頭到尾以 O(n) 的時(shí)間復(fù)雜度對字符串逐字掃描,并且不后退也不往前跳。
那么掃描到每個(gè)字符的時(shí)候,都有屬于當(dāng)前的一個(gè)狀態(tài),如“正在掃描處理 uri”、“正在掃描處理 HTTP 協(xié)議并且處理到了 H”、“正在掃描處理 HTTP 協(xié)議并且處理到了 HT”、“正在掃描處理 HTTP 協(xié)議并且處理到了 HTT”、“正在掃描處理 HTTP 協(xié)議并且處理到了 HTTP”、……
憋笑,這是真的,我們看看代碼就知道了:
case s_req_server: case s_req_server_with_at: case s_req_path: case s_req_query_string_start: case s_req_query_string: case s_req_fragment_start: case s_req_fragment: { switch (ch) { case " ": UPDATE_STATE(s_req_http_start); CALLBACK_DATA(url); break; case CR: case LF: parser->http_major = 0; parser->http_minor = 9; UPDATE_STATE((ch == CR) ? s_req_line_almost_done : s_header_field_start); CALLBACK_DATA(url); break; default: UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch)); if (UNLIKELY(CURRENT_STATE() == s_dead)) { SET_ERRNO(HPE_INVALID_URL); goto error; } } break; }
在掃描的時(shí)候,如果當(dāng)前狀態(tài)是 URI 相關(guān)的(如 s_req_path、s_req_query_string 等),則執(zhí)行一個(gè)子 switch,里面的處理如下:
若當(dāng)前字符是空格,則將狀態(tài)改變?yōu)?s_req_http_start 并認(rèn)為 URI 已經(jīng)解析好了,通過宏 CALLBACK_DATA() 觸發(fā) URI 解析好的事件;
若當(dāng)前字符是換行符,則說明還在解析 URI 的時(shí)候就被換行了,后面就不可能跟著 HTTP 協(xié)議版本的申明了,所以設(shè)置默認(rèn)的 HTTP 版本為 0.9,并修改當(dāng)前狀態(tài),最后認(rèn)為 URI 已經(jīng)解析好了,通過宏 CALLBACK_DATA() 觸發(fā) URI 解析好的事件;
其余情況(所有其它字符)下,通過調(diào)用 parse_url_char() 函數(shù)來解析一些東西并更新當(dāng)前狀態(tài)。(因?yàn)槟呐率窃诮馕?URI 狀態(tài)中,也還有各種不同的細(xì)分,如 s_req_path、s_req_query_string )
這里的重點(diǎn)還是當(dāng)狀態(tài)為解析 URI 的時(shí)候遇到了空格的處理,上面也解釋過了,一旦遇到這種情況,則會(huì)認(rèn)為 URI 已經(jīng)解析好了,并且將狀態(tài)修改為 s_req_http_start。也就是說,有“Bug”的那個(gè)數(shù)據(jù)包
GET /foo bar HTTP/1.1 在解析到 foo 后面的空格的時(shí)候它就將狀態(tài)改為 s_req_http_start 并且認(rèn)為 URI 已經(jīng)解析結(jié)束了。
好的,接下來我們看看 s_req_http_start 怎么處理:
case s_req_http_start: switch (ch) { case "H": UPDATE_STATE(s_req_http_H); break; case " ": break; default: SET_ERRNO(HPE_INVALID_CONSTANT); goto error; } break; case s_req_http_H: STRICT_CHECK(ch != "T"); UPDATE_STATE(s_req_http_HT); break; case s_req_http_HT: ... case s_req_http_HTT: ... case s_req_http_HTTP: ... case s_req_first_http_major: ...
如代碼所見,若當(dāng)前狀態(tài)為 s_req_http_start,則先判斷當(dāng)前字符是不是合標(biāo)。因?yàn)榫?HTTP 請求包體的格式來看,如果 URI 解析結(jié)束的話,理應(yīng)出現(xiàn)類似 HTTP/1.1 的這么一個(gè)版本申明。所以這個(gè)時(shí)候 http-parser 會(huì)直接判斷當(dāng)前字符是否為 H。
若是 H,則將狀態(tài)改為 s_req_http_H 并繼續(xù)掃描循環(huán)的下一位,同理在 s_req_http_H 下若合法狀態(tài)就會(huì)變成 s_req_http_HT,以此類推;
+若是空格,則認(rèn)為是多余的空格,那么當(dāng)前狀態(tài)不做任何改變,并繼續(xù)下一個(gè)掃描;
但如果當(dāng)前字符既不是空格也不是 H,那么好了,http-parser 直接認(rèn)為你的請求包不合法,將你本次的解析設(shè)置錯(cuò)誤 HPE_INVALID_CONSTANT 并 goto 到 error 代碼塊。
至此,我們基本上已經(jīng)明白了原因了:
http-parser 認(rèn)為在 HTTP 請求包體中,第一行的 URI 解析階段一旦出現(xiàn)了空格,就會(huì)認(rèn)為 URI 解析完成,繼而解析 HTTP 協(xié)議版本。但若此時(shí)緊跟著的不是 HTTP 協(xié)議版本的標(biāo)準(zhǔn)格式,http-parser 就會(huì)認(rèn)為你這是一個(gè) HPE_INVALID_CONSTANT 的數(shù)據(jù)包。
不過,我們還是繼續(xù)看看它的 error 代碼塊吧:
error: if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { SET_ERRNO(HPE_UNKNOWN); } RETURN(p - data);
這段代碼中首先判斷一下當(dāng)跳到這段代碼的時(shí)候有沒有設(shè)置錯(cuò)誤,若沒有設(shè)置錯(cuò)誤則將錯(cuò)誤設(shè)置為未知錯(cuò)誤(HPE_UNKNOWN),然后返回已解析的數(shù)據(jù)包長度。
回到 node_http_parser.ccp 是當(dāng)前解析字符指針,data 是這個(gè)數(shù)據(jù)包的起始指針,所以 p - data 就是已解析的數(shù)據(jù)長度。如果成功解析完,這個(gè)數(shù)據(jù)包理論上是等于這個(gè)數(shù)據(jù)包的完整長度,若不等則理論上說明肯定是中途出錯(cuò)提前返回。
看完了 http-parser 的原理后,很多地方茅塞頓開?,F(xiàn)在我們回到它的調(diào)用地 node_http_parser.cc 繼續(xù)閱讀吧。
LocalExecute(char* data, size_t len) { ... size_t nparsed = http_parser_execute(&parser_, &settings, data, len); Local nparsed_obj = Integer::New(env()->isolate(), nparsed); if (!parser_.upgrade && nparsed != len) { enum http_errno err = HTTP_PARSER_ERRNO(&parser_); Local e = Exception::Error(env()->parse_error_string()); Local obj = e->ToObject(env()->isolate()); obj->Set(env()->bytes_parsed_string(), nparsed_obj); obj->Set(env()->code_string(), OneByteString(env()->isolate(), http_errno_name(err))); return scope.Escape(e); } return scope.Escape(nparsed_obj); }
從調(diào)用處我們能看見,在執(zhí)行完 http_parser_execute() 后有一個(gè)判斷,若當(dāng)前請求不是 upgrade 請求(即請求頭中有說明 Upgrade,通常用于 WebSocket),并且解析長度不等于原數(shù)據(jù)包長度(前文說了這種情況屬于出錯(cuò)了)的話,那么進(jìn)入中間的錯(cuò)誤代碼塊。
在錯(cuò)誤代碼塊中,先 HTTP_PARSER_ERRNO(&parser_) 拿到錯(cuò)誤碼,然后通過 Exception::Error() 生成錯(cuò)誤對象,將錯(cuò)誤信息塞進(jìn)錯(cuò)誤對象中,最后返回錯(cuò)誤對象。
如果沒錯(cuò),則返回解析長度(nparsed_obj 即 nparsed)。
回到 _http_server.js在這個(gè)文件中,眼尖的童鞋可能發(fā)現(xiàn)了,執(zhí)行 Execute() 有好多處,這是因?yàn)閷?shí)際上一個(gè) HTTP 請求可能是流式的,所以有時(shí)候可能會(huì)只拿到部分?jǐn)?shù)據(jù)包。所以最后有一個(gè)結(jié)束符需要被確認(rèn)。這也是為什么 http-parser 在解析的時(shí)候只能逐字解析而不能跳躍或者后退了。
我們把 Parser::Execute() 也就是 JavaScript 代碼中的 parser.execute() 給搞清楚后,我們就能回到 _http_server.js 看代碼了。
前文說了,socketOnData 在解析完數(shù)據(jù)包后會(huì)執(zhí)行 onParserExecuteCommon 函數(shù),現(xiàn)在就來看看這個(gè) onParserExecuteCommon() 函數(shù)。
function onParserExecuteCommon(server, socket, parser, state, ret, d) { resetSocketTimeout(server, socket, state); if (ret instanceof Error) { debug("parse error", ret); socketOnError.call(socket, ret); } else if (parser.incoming && parser.incoming.upgrade) { ... } }
長長的一個(gè)函數(shù)被我精簡成這么幾句話,重點(diǎn)很明顯。ret 就是從 socketOnData 傳進(jìn)來已解析的數(shù)據(jù)長度,但是在 C++ 代碼中我們也看到了它還有可能是一個(gè)錯(cuò)誤對象。所以在這個(gè)函數(shù)中一開始就做了一個(gè)判斷,判斷解析的結(jié)果是不是一個(gè)錯(cuò)誤對象,如果是錯(cuò)誤對象則調(diào)用 socketOnError()。
function socketOnError(e) { // Ignore further errors this.removeListener("error", socketOnError); this.on("error", () => {}); if (!this.server.emit("clientError", e, this)) this.destroy(e); }
我們看到,如果真的不小心走到這一步的話,HTTP Server 對象會(huì)觸發(fā)一個(gè) clientError 事件。
整個(gè)事情串聯(lián)起來了:
收到請求后會(huì)通過 http-parser 解析數(shù)據(jù)包;
GET /foo bar HTTP/1.1 會(huì)被解析出錯(cuò)并返回一個(gè)錯(cuò)誤對象;
錯(cuò)誤對象會(huì)進(jìn)入 if (ret instanceof Error) 條件分支并調(diào)用 socketOnError() 函數(shù);
socketOnError() 函數(shù)中會(huì)對服務(wù)器觸發(fā)一個(gè) clientError 事件;(this.server.emit("clientError", e, this))
至此,HTTP Server 并不會(huì)走到你的那個(gè) function(req, resp) 中去,所以不會(huì)有任何的數(shù)據(jù)被返回就結(jié)束了,也就解答了一開始的問題——收不到任何數(shù)據(jù)就請求結(jié)束。
這就是我要逐級進(jìn)來看代碼,而不是直達(dá) http-parser 的原因了——clientError 是一個(gè)關(guān)鍵。
處理辦法要解決這個(gè)“Bug”其實(shí)不難,直接監(jiān)聽 clientError 事件并做一些處理即可。
"use strict"; const http = require("http"); const server = http.createServer(function(req, resp) { console.log("?"); resp.end("hello world"); }).on("clientError", function(err, sock) { console.log("?"); sock.end("HTTP/1.1 400 Bad Request "); }); server.listen(5555);
注意:由于運(yùn)行到 clientError 事件時(shí),并沒有任何 Request 和 Response 的封裝,你能拿到的是一個(gè) Node.js 中原始的 Socket 對象,所以當(dāng)你要返回?cái)?shù)據(jù)的時(shí)候需要自己按照 HTTP 返回?cái)?shù)據(jù)包的格式來輸出。
這個(gè)時(shí)候再揮起你的小手試一下 CURL 吧:
$ curl "http://127.0.0.1:5555/d d" -v * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0) > GET /d d HTTP/1.1 > Host: 127.0.0.1:5555 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 400 Bad Request * no chunk, no close, no size. Assume close to signal end < * Closing connection 0
如愿以償?shù)剌敵隽?400 狀態(tài)碼。
引申接下來我們要引申討論的一個(gè)點(diǎn)是,為什么這貨不是一個(gè)真正意義上的 Bug。
首先我們看看 Nginx 這么實(shí)現(xiàn)這個(gè)黑科技的吧。
Nginx 實(shí)現(xiàn)打開 Nginx 源碼的相應(yīng)位置。
我們能看到它的狀態(tài)機(jī)對于 URI 和 HTTP 協(xié)議聲明中間多了一個(gè)中間狀態(tài),叫 sw_check_uri_http_09,專門處理 URI 后面的空格。
在各種 URI 解析狀態(tài)中,基本上都能找到這么一句話,表示若當(dāng)前狀態(tài)正則解析 URI 的各種狀態(tài)并且遇到空格的話,則將狀態(tài)改為 sw_check_uri_http_09。
case sw_check_uri: switch (ch) { case " ": r->uri_end = p; state = sw_check_uri_http_09; break; ... } ...
然后在 sw_check_uri_http_09 狀態(tài)時(shí)會(huì)做一些檢查:
case sw_check_uri_http_09: switch (ch) { case " ": break; case CR: r->http_minor = 9; state = sw_almost_done; break; case LF: r->http_minor = 9; goto done; case "H": r->http_protocol.data = p; state = sw_http_H; break; default: r->space_in_uri = 1; state = sw_check_uri; p--; break; } break;
例如:
遇到空格則繼續(xù)保持當(dāng)前狀態(tài)開始掃描下一位;
如果是換行符則設(shè)置默認(rèn) HTTP 版本并繼續(xù)掃描;
如果遇到的是 H 才修改狀態(tài)為 sw_http_H 認(rèn)為接下去開始 HTTP 版本掃描;
如果是其它字符,則標(biāo)明一下 URI 中有空格,然后將狀態(tài)改回 sw_check_uri,然后倒退回一格以 sw_check_uri 繼續(xù)掃描當(dāng)前的空格。
在理解了這個(gè)“黑科技”后,我們很快能找到一個(gè)很好玩的點(diǎn),開啟你的 Nginx 并用 CURL 請求以下面的例子一下它看看吧:
$ curl "http://xcoder.in:5555/d H" -v * Trying 103.238.225.181... * TCP_NODELAY set * Connected to xcoder.in (103.238.225.181) port 5555 (#0) > GET /d H HTTP/1.1 > Host: xcoder.in:5555 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 400 Bad Request < Server: openresty/1.11.2.1 < Date: Tue, 12 Dec 2017 11:18:13 GMT < Content-Type: text/html < Content-Length: 179 < Connection: close <400 Bad Request 400 Bad Request
openresty/1.11.2.1 * Closing connection 0
怎么樣?是不是發(fā)現(xiàn)結(jié)果跟之前的不一樣了——它居然也返回了 400 Bad Request。
原因?yàn)楹尉徒唤o童鞋們自己考慮吧。
RFC 2616 與 RFC 2396那么,為什么即使在 Nginx 支持空格 URI 的情況下,我還說 Node.js 這個(gè)不算 Bug,并且指明 Nginx 是“黑科技”呢?
后來我去看了 HTTP 協(xié)議 RFC。
原因在于 Network Working Group 的 RFC 2616,關(guān)于 HTTP 協(xié)議的規(guī)范。
在 RFC 2616 的 3.2.1 節(jié)中做了一些說明,它說了在 HTTP 協(xié)議中關(guān)于 URI 的文法和語義參照了 RFC 2396。
URIs in HTTP can be represented in absolute form or relative to some known base URI, depending upon the context of their use. The two forms are differentiated by the fact that absolute URIs always begin with a scheme name followed by a colon. For definitive information on URL syntax and semantics, see "Uniform Resource Identifiers (URI): Generic Syntax and Semantics," RFC 2396 (which replaces RFCs 1738 and RFC 1808). This specification adopts the definitions of "URI-reference", "absoluteURI", "relativeURI", "port", "host","abs_path", "rel_path", and "authority" from that specification.
而在 RFC 2396 中,我們同樣找到了它的 2.4.3 節(jié)。里面對于 Disallow 的 US-ASCII 字符做了解釋,其中有:
控制符,指 ASCII 碼在 0x00-0x1F 范圍內(nèi)以及 0x7F;
控制符通常不可見;
空格,指 0x20;
空格不可控,如經(jīng)由一些排版軟件轉(zhuǎn)錄后可能會(huì)有變化,而到了 HTTP 協(xié)議這層時(shí),反正空格不推薦使用了,所以就索性用空格作為首行分隔符了;
分隔符,"<"、">"、"#"、"%"、"""。
如 # 將用于瀏覽器地址欄的 Hash;而 % 則會(huì)與 URI 轉(zhuǎn)義一同使用,所以不應(yīng)多帶帶出現(xiàn)在 URI 中。
于是乎,HTTP 請求中,包體的 URI 似乎本就不應(yīng)該出現(xiàn)空格,而 Nginx 是一個(gè)黑魔法的姿勢。
小結(jié)嚯,寫得累死了。本次的一個(gè)探索基于了一個(gè)有空格非正常的 URI 通過 CURL 或者其它一些客戶端請求時(shí),Node.js 出現(xiàn)的 Bug 狀態(tài)。
實(shí)際上發(fā)現(xiàn)這個(gè) Bug 的時(shí)候,客戶端請求似乎是因?yàn)槟沁叺拈_發(fā)者手抖,不小心將不應(yīng)該拼接進(jìn)來的內(nèi)容給拼接到了 URL 中,類似于 $ rm -rf /。
一開始我以為這是 Node.js 的 Bug,在探尋之后發(fā)現(xiàn)是因?yàn)槲覀冏约簺]用 Node.js HTTP Server 提供的 clientError 事件做正確的處理。而 Nginx 的正常請求則是它的黑科技。這些答案都能從 RFC 中尋找——再次體現(xiàn)了遇到問題看源碼看規(guī)范的重要性。
另,我本打算給 http-parser 也加上黑魔法,后來我快寫好的時(shí)候發(fā)現(xiàn)它是流式的,很多狀態(tài)沒法在現(xiàn)有的體系中保留下來,最后放棄了,反正這也不算 Bug。不過在以后有時(shí)間的時(shí)候,感覺還是可以好好整理一下代碼,好好修改一下給提個(gè) PR 上去,以此自勉。
最后,求 fafa。
交流如果你有更多的想法,或者想了解螞蟻金服的 Node.js、前端以及設(shè)計(jì)小伙伴們的更多姿勢,可以報(bào)名首屆螞蟻體驗(yàn)科技大會(huì) SEE Conf,比如有死馬大大的《Developer Experience First —— Techless Web Application 的理念與實(shí)踐》,還有青梔大大的《螞蟻開發(fā)者工具,服務(wù)螞蟻生態(tài)的移動(dòng)研發(fā) IDE》等等。
報(bào)名官網(wǎng):https://seeconf.alipay.com/
期待您的光臨。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/39721.html
摘要:程序人生從黑客到創(chuàng)業(yè),他說技術(shù)創(chuàng)業(yè)該這么做知道創(chuàng)宇,安全焦點(diǎn)民間白帽黑客組織核心成員,分享他創(chuàng)業(yè)感悟和踩過的那些坑。技術(shù)周刊由小組出品,匯聚一周好文章,周刊原文。 業(yè)界動(dòng)態(tài) 他們寫的代碼能上天!NASA的10條安全編碼準(zhǔn)則大公開 NASA的10條代碼編寫規(guī)范準(zhǔn)則 本期推薦 Node.js 中遇到含空格 URL 的神奇Bug——小范圍深入 HTTP 協(xié)議 本文闡述了博主遇到含空格 URL...
摘要:為了實(shí)現(xiàn)文字環(huán)繞效果,規(guī)范規(guī)定的措施是使父容器塌陷,元素脫離文檔流浮動(dòng),元素周圍的內(nèi)容轉(zhuǎn)換為圍繞元素排列。從浮動(dòng)的起因和浮動(dòng)的實(shí)現(xiàn)后果來看,浮動(dòng)不適合用于大范圍的布局,更適合利用其特性實(shí)現(xiàn)一些小范圍的流體布局效果。 張鑫旭的CSS深入理解之float浮動(dòng)學(xué)習(xí)筆記 float的歷史 float為產(chǎn)生文字環(huán)繞效果而生 float的特性 — 包裹和破壞 包裹:即產(chǎn)生一個(gè)BFC破壞:使父容器...
摘要:畫字首先我在畫布上畫了個(gè)點(diǎn),用這些點(diǎn)來組成我們要顯示的字,用不到的字就隱藏起來。星星閃爍效果這個(gè)效果實(shí)現(xiàn)很簡單,就是讓星星不停的震動(dòng),具體就是讓點(diǎn)的目的地坐標(biāo)不停的進(jìn)行小范圍的偏移。 哈哈哈哈?。?!當(dāng)我說在寫這邊文章的時(shí)候,妹子已經(jīng)追到了,哈哈哈哈哈?。。?其實(shí)東西是一年前寫的,妹子早就追到手了,當(dāng)時(shí)就是用這個(gè)東西來表白的咯,二話不說,先看效果(點(diǎn)擊屏幕可顯示下一句) showImg(...
摘要:另載于現(xiàn)代對象設(shè)計(jì)主張組合優(yōu)于繼承。對象的有效范圍,是指對象從創(chuàng)建到丟棄不再引用的這段時(shí)間,不包括等待被銷毀的時(shí)間。對于繼承也是同理,父類和子類應(yīng)當(dāng)有相同的有效范圍。同理,級的對象不要持有級的對象。 另載于 http://www.qingjingjie.com/blogs/9 現(xiàn)代對象設(shè)計(jì)主張組合優(yōu)于繼承??傊疅o論組合還是繼承,對象都成了涉及多個(gè)類的復(fù)合結(jié)構(gòu)。 對象的有效范圍,是指對象...
閱讀 2344·2023-04-25 14:17
閱讀 1536·2021-11-23 10:02
閱讀 2186·2021-11-23 09:51
閱讀 894·2021-10-14 09:49
閱讀 3400·2021-10-11 10:57
閱讀 2932·2021-09-24 09:47
閱讀 3063·2021-08-24 10:00
閱讀 2311·2019-08-29 18:46