国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Node.js 中遇到含空格 URL 的神奇“Bug”——小范圍深入 HTTP 協議

edgardeng / 2812人閱讀

摘要:開始重現客戶端指令其實這次請求的一些貓膩很容易就能發現在中有空格。而在函數中,做的主要事情就是來解析數據包,在解析完成后執行一下回調函數。具體的一些回調函數就不細講了,有興趣的童鞋可自行翻閱。如代碼片段所示,前文中所對應的函數就是了。

本文首發于知乎專欄螞蟻金服體驗科技。

首先聲明,我在“Bug”字眼上加了引號,自然是為了說明它并非一個真 Bug。

問題拋出

昨天有個童鞋在看后臺監控的時候,突然發現了一個錯誤:

[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://..."

大概意思就是說:一臺服務器通過 HTTP 協議去請求另一臺服務器的時候,單方面被對方服務器斷開了連接——并且并沒有任何返回。

開始重現 客戶端 CURL 指令

其實這次請求的一些貓膩很容易就能發現——在 URL 中有空格。所以我們能簡化出一條最簡單的 CURL 指令:

$ curl "http://foo/bar baz" -v

注意:不帶任何轉義。

最小 Node.js 源碼

好的,那么接下去開始寫相應的最簡單的 Node.js HTTP 服務端源碼。

"use strict";

const http = require("http");

const server = http.createServer(function(req, resp) {
    console.log("?");
    resp.end("hello world");
});

server.listen(5555);

大功告成,啟動這段 Node.js 代碼,開始試試看上面的指令吧。

如果你也正在跟著嘗試這件事情的話,你就會發現 Node.js 的命令行沒有輸出任何信息,尤其是嘲諷的 "?",而在 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: */*
>
* 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

發現了問題之后,就有另一個問題值得思考了:就 Node.js 會出現這種情況呢,還是其它一些 HTTP 服務器也會有這種情況呢。

于是拿小白鼠 Nginx 做了個實驗。我寫了這么一個配置:

server {
    listen 5555;

    location / {
        return 200 $uri;
    }
}

接著也執行一遍 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 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

于是乎,理所當然,我暫時將這個事件定性為 Node.js 的一個 Bug。

Node.js 源碼排查

認定了它是個 Bug 之后,我就開始了一貫的看源碼環節——由于這個 Bug 的復現條件比較明顯,我暫時將其定性為“Node.js HTTP 服務端模塊在接到請求后解析 HTTP 數據包的時候解析 URI 時出了問題”。

http.js -> _http_server.js -> _http_common.js

源碼以 Node.js 8.9.2 為準。

這里先預留一下我們能馬上想到的 node_http_parser.cc,而先講這幾個文件,是有原因的——這涉及到最后的一個應對方式。

首先看看 lib/http.js 的相應源碼:

...
const server = require("_http_server");

const { Server } = server;

function createServer(requestListener) {
  return new Server(requestListener);
}

那么,馬上進入 lib/_http_server.js 看吧。

首先是創建一個 HttpParser 并綁上監聽獲取到 HTTP 數據包后解析結果的回調函數的代碼:

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);
}

從源碼中文我們能看到,當一個 HTTP 請求過來的時候,監聽函數 connectionListener() 會拿著 Socket 對象加上一個 data 事件監聽——一旦有請求連接過來,就去執行 socketOnData() 函數。

而在 socketOnData() 函數中,做的主要事情就是 parser.execute(d) 來解析 HTTP 數據包,在解析完成后執行一下回調函數 onParserExecuteCommon()

至于這個 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;
});

能看出來 parsersHTTPParser 的一條 Free List(效果類似于最簡易的動態內存池),每個 Parser 在初始化的時候綁定上了各種回調函數。具體的一些回調函數就不細講了,有興趣的童鞋可自行翻閱。

這么一來,鏈路就比較明晰了:

請求進來的時候,Server 對象會為該次請求的 Socket 分配一個 HttpParser 對象,并調用其 execute() 函數進行解析,在解析完成后調用 onParserExecuteCommon() 函數。

node_http_parser.cc

我們在 lib/_http_common.js 中能發現,HTTPParser 的實現存在于 src/node_http_parser.cc 中:

const binding = process.binding("http_parser");
const { methods, HTTPParser } = binding;

至于為什么 const binding = process.binding("http_parser") 就是對應到 src/node_http_parser.cc 文件,以及這一小節中下面的一些 C++ 源碼相關分析,不明白且有興趣的童鞋可自行去閱讀更深一層的源碼,或者網上搜索答案,或者我提前無恥硬廣一下我快要上市的書《Node.js:來一打 C++ 擴展》——里面也有說明,以及我的有一場知乎 Live《深入理解 Node.js 包與模塊機制》。

總而言之,我們接下去要看的就是 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() 所對應的函數就是 Parser::Execute() 了。

class Parser : public AsyncWrap {
  ...

  static void Execute(const FunctionCallbackInfo& args) {
    Parser* parser;
    ...

    Local buffer_obj = args[0].As();
    char* buffer_data = Buffer::Data(buffer_obj);
    size_t buffer_len = Buffer::Length(buffer_obj);

    ...

    Local ret = parser->Execute(buffer_data, buffer_len);

    if (!ret.IsEmpty())
      args.GetReturnValue().Set(ret);
  }

  Local Execute(char* data, size_t len) {
    EscapableHandleScope scope(env()->isolate());

    current_buffer_len_ = len;
    current_buffer_data_ = data;
    got_exception_ = false;

    size_t nparsed =
      http_parser_execute(&parser_, &settings, data, len);

    Save();

    // Unassign the "buffer_" variable
    current_buffer_.Clear();
    current_buffer_len_ = 0;
    current_buffer_data_ = nullptr;

    // If there was an exception in one of the callbacks
    if (got_exception_)
      return scope.Escape(Local());

    Local nparsed_obj = Integer::New(env()->isolate(), nparsed);
    // If there was a parse error in one of the callbacks
    // TODO(bnoordhuis) What if there is an error on EOF?
    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);
  }
}

首先進入 Parser 的靜態 Execute() 函數,我們看到它把傳進來的 Buffer 轉化為 C++ 下的 char* 指針,并記錄其數據長度,同時去執行當前調用的 parser 對象所對應的 Execute() 函數。

在這個 Execute() 函數中,有個最重要的代碼,就是:

size_t nparsed =
    http_parser_execute(&parser_, &settings, data, len);

這段代碼是調用真正解析 HTTP 數據包的函數,它是 Node.js 這個項目的一個自研依賴,叫 http-parser。它獨立的項目地址在 https://github.com/nodejs/http-parser,我們本文中用的是 Node.js v8.9.2 中所依賴的源碼,應該會有偏差。

http-parser HTTP Request 數據包體

如果你已經對 HTTP 包體了解了,可以略過這一節。

HTTP 的 Request 數據包其實是文本格式的,在 Raw 的狀態下,大概是以這樣的形式存在:

方法 URI HTTP/版本
頭1: 我是頭1
頭2: 我是頭2

簡單起見,這里就寫出最基礎的一些內容,至于 Body 什么的大家自己找資料看吧。

上面的是什么意思呢?我們看看 CURL 的結果就知道了,實際上對應 curl ... -v 的中間輸出:

GET /test HTTP/1.1
Host: 127.0.0.1:5555
User-Agent: curl/7.54.0
Accept: */*

所以實際上大家平時在文章中、瀏覽器調試工具中看到的什么請求頭啊什么的,都是以文本形式存在的,以換行符分割。

而——重點來了,導致我們本文所述“Bug”出現的請求,它的請求包如下:

GET /foo bar HTTP/1.1
Host: 127.0.0.1:5555
User-Agent: curl/7.54.0
Accept: */*

重點在第一行:

GET /foo bar HTTP/1.1
源碼解析

話不多少,我們之間前往 http-parser 的 http_parser.c 看 http_parser_execute () 函數中的狀態機變化。

從源碼中文我們能看到,http-parser 的流程是從頭到尾以 O(n) 的時間復雜度對字符串逐字掃描,并且不后退也不往前跳。

那么掃描到每個字符的時候,都有屬于當前的一個狀態,如“正在掃描處理 uri”、“正在掃描處理 HTTP 協議并且處理到了 H”、“正在掃描處理 HTTP 協議并且處理到了 HT”、“正在掃描處理 HTTP 協議并且處理到了 HTT”、“正在掃描處理 HTTP 協議并且處理到了 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;
}

在掃描的時候,如果當前狀態是 URI 相關的(如 s_req_path、s_req_query_string 等),則執行一個子 switch,里面的處理如下:

若當前字符是空格,則將狀態改變為 s_req_http_start 并認為 URI 已經解析好了,通過宏 CALLBACK_DATA() 觸發 URI 解析好的事件;

若當前字符是換行符,則說明還在解析 URI 的時候就被換行了,后面就不可能跟著 HTTP 協議版本的申明了,所以設置默認的 HTTP 版本為 0.9,并修改當前狀態,最后認為 URI 已經解析好了,通過宏 CALLBACK_DATA() 觸發 URI 解析好的事件;

其余情況(所有其它字符)下,通過調用 parse_url_char() 函數來解析一些東西并更新當前狀態。(因為哪怕是在解析 URI 狀態中,也還有各種不同的細分,如 s_req_path、s_req_query_string

這里的重點還是當狀態為解析 URI 的時候遇到了空格的處理,上面也解釋過了,一旦遇到這種情況,則會認為 URI 已經解析好了,并且將狀態修改為 s_req_http_start。也就是說,有“Bug”的那個數據包
GET /foo bar HTTP/1.1 在解析到 foo 后面的空格的時候它就將狀態改為 s_req_http_start 并且認為 URI 已經解析結束了。

好的,接下來我們看看 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:
  ...

如代碼所見,若當前狀態為 s_req_http_start,則先判斷當前字符是不是合標。因為就 HTTP 請求包體的格式來看,如果 URI 解析結束的話,理應出現類似 HTTP/1.1 的這么一個版本申明。所以這個時候 http-parser 會直接判斷當前字符是否為 H。

若是 H,則將狀態改為 s_req_http_H 并繼續掃描循環的下一位,同理在 s_req_http_H 下若合法狀態就會變成 s_req_http_HT,以此類推;

+若是空格,則認為是多余的空格,那么當前狀態不做任何改變,并繼續下一個掃描;

但如果當前字符既不是空格也不是 H,那么好了,http-parser 直接認為你的請求包不合法,將你本次的解析設置錯誤 HPE_INVALID_CONSTANTgotoerror 代碼塊。

至此,我們基本上已經明白了原因了:

http-parser 認為在 HTTP 請求包體中,第一行的 URI 解析階段一旦出現了空格,就會認為 URI 解析完成,繼而解析 HTTP 協議版本。但若此時緊跟著的不是 HTTP 協議版本的標準格式,http-parser 就會認為你這是一個 HPE_INVALID_CONSTANT 的數據包。

不過,我們還是繼續看看它的 error 代碼塊吧:

error:
  if (HTTP_PARSER_ERRNO(parser) == HPE_OK) {
    SET_ERRNO(HPE_UNKNOWN);
  }

  RETURN(p - data);

這段代碼中首先判斷一下當跳到這段代碼的時候有沒有設置錯誤,若沒有設置錯誤則將錯誤設置為未知錯誤(HPE_UNKNOWN),然后返回已解析的數據包長度。

p 是當前解析字符指針,data 是這個數據包的起始指針,所以 p - data 就是已解析的數據長度。如果成功解析完,這個數據包理論上是等于這個數據包的完整長度,若不等則理論上說明肯定是中途出錯提前返回。

回到 node_http_parser.cc

看完了 http-parser 的原理后,很多地方茅塞頓開?,F在我們回到它的調用地 node_http_parser.cc 繼續閱讀吧。

Local Execute(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);
}

從調用處我們能看見,在執行完 http_parser_execute() 后有一個判斷,若當前請求不是 upgrade 請求(即請求頭中有說明 Upgrade,通常用于 WebSocket),并且解析長度不等于原數據包長度(前文說了這種情況屬于出錯了)的話,那么進入中間的錯誤代碼塊。

在錯誤代碼塊中,先 HTTP_PARSER_ERRNO(&parser_) 拿到錯誤碼,然后通過 Exception::Error() 生成錯誤對象,將錯誤信息塞進錯誤對象中,最后返回錯誤對象。

如果沒錯,則返回解析長度(nparsed_objnparsed)。

在這個文件中,眼尖的童鞋可能發現了,執行 Execute() 有好多處,這是因為實際上一個 HTTP 請求可能是流式的,所以有時候可能會只拿到部分數據包。所以最后有一個結束符需要被確認。這也是為什么 http-parser 在解析的時候只能逐字解析而不能跳躍或者后退了。

回到 _http_server.js

我們把 Parser::Execute() 也就是 JavaScript 代碼中的 parser.execute() 給搞清楚后,我們就能回到 _http_server.js 看代碼了。

前文說了,socketOnData 在解析完數據包后會執行 onParserExecuteCommon 函數,現在就來看看這個 onParserExecuteCommon() 函數。

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) {
    ...
  }
}

長長的一個函數被我精簡成這么幾句話,重點很明顯。ret 就是從 socketOnData 傳進來已解析的數據長度,但是在 C++ 代碼中我們也看到了它還有可能是一個錯誤對象。所以在這個函數中一開始就做了一個判斷,判斷解析的結果是不是一個錯誤對象,如果是錯誤對象則調用 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 對象會觸發一個 clientError 事件。

整個事情串聯起來了:

收到請求后會通過 http-parser 解析數據包;

GET /foo bar HTTP/1.1 會被解析出錯并返回一個錯誤對象;

錯誤對象會進入 if (ret instanceof Error) 條件分支并調用 socketOnError() 函數;

socketOnError() 函數中會對服務器觸發一個 clientError 事件;(this.server.emit("clientError", e, this)

至此,HTTP Server 并不會走到你的那個 function(req, resp) 中去,所以不會有任何的數據被返回就結束了,也就解答了一開始的問題——收不到任何數據就請求結束。

這就是我要逐級進來看代碼,而不是直達 http-parser 的原因了——clientError 是一個關鍵。

處理辦法

要解決這個“Bug”其實不難,直接監聽 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);

注意:由于運行到 clientError 事件時,并沒有任何 Request 和 Response 的封裝,你能拿到的是一個 Node.js 中原始的 Socket 對象,所以當你要返回數據的時候需要自己按照 HTTP 返回數據包的格式來輸出。

這個時候再揮起你的小手試一下 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

如愿以償地輸出了 400 狀態碼。

引申

接下來我們要引申討論的一個點是,為什么這貨不是一個真正意義上的 Bug。

首先我們看看 Nginx 這么實現這個黑科技的吧。

Nginx 實現

打開 Nginx 源碼的相應位置。

我們能看到它的狀態機對于 URI 和 HTTP 協議聲明中間多了一個中間狀態,叫 sw_check_uri_http_09,專門處理 URI 后面的空格。

在各種 URI 解析狀態中,基本上都能找到這么一句話,表示若當前狀態正則解析 URI 的各種狀態并且遇到空格的話,則將狀態改為 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 狀態時會做一些檢查:

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;

例如:

遇到空格則繼續保持當前狀態開始掃描下一位;

如果是換行符則設置默認 HTTP 版本并繼續掃描;

如果遇到的是 H 才修改狀態為 sw_http_H 認為接下去開始 HTTP 版本掃描;

如果是其它字符,則標明一下 URI 中有空格,然后將狀態改回 sw_check_uri,然后倒退回一格以 sw_check_uri 繼續掃描當前的空格。

在理解了這個“黑科技”后,我們很快能找到一個很好玩的點,開啟你的 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

怎么樣?是不是發現結果跟之前的不一樣了——它居然也返回了 400 Bad Request。

原因為何就交給童鞋們自己考慮吧。

RFC 2616 與 RFC 2396

那么,為什么即使在 Nginx 支持空格 URI 的情況下,我還說 Node.js 這個不算 Bug,并且指明 Nginx 是“黑科技”呢?

后來我去看了 HTTP 協議 RFC。

原因在于 Network Working Group 的 RFC 2616,關于 HTTP 協議的規范。

在 RFC 2616 的 3.2.1 節中做了一些說明,它說了在 HTTP 協議中關于 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 節。里面對于 Disallow 的 US-ASCII 字符做了解釋,其中有:

控制符,指 ASCII 碼在 0x00-0x1F 范圍內以及 0x7F;

控制符通常不可見;

空格,指 0x20;

空格不可控,如經由一些排版軟件轉錄后可能會有變化,而到了 HTTP 協議這層時,反正空格不推薦使用了,所以就索性用空格作為首行分隔符了;

分隔符,"<"">"、"#"、"%"、"""。

# 將用于瀏覽器地址欄的 Hash;而 % 則會與 URI 轉義一同使用,所以不應多帶帶出現在 URI 中。

于是乎,HTTP 請求中,包體的 URI 似乎本就不應該出現空格,而 Nginx 是一個黑魔法的姿勢。

小結

嚯,寫得累死了。本次的一個探索基于了一個有空格非正常的 URI 通過 CURL 或者其它一些客戶端請求時,Node.js 出現的 Bug 狀態。

實際上發現這個 Bug 的時候,客戶端請求似乎是因為那邊的開發者手抖,不小心將不應該拼接進來的內容給拼接到了 URL 中,類似于 $ rm -rf /。

一開始我以為這是 Node.js 的 Bug,在探尋之后發現是因為我們自己沒用 Node.js HTTP Server 提供的 clientError 事件做正確的處理。而 Nginx 的正常請求則是它的黑科技。這些答案都能從 RFC 中尋找——再次體現了遇到問題看源碼看規范的重要性。

另,我本打算給 http-parser 也加上黑魔法,后來我快寫好的時候發現它是流式的,很多狀態沒法在現有的體系中保留下來,最后放棄了,反正這也不算 Bug。不過在以后有時間的時候,感覺還是可以好好整理一下代碼,好好修改一下給提個 PR 上去,以此自勉。

最后,求 fafa。

交流

如果你有更多的想法,或者想了解螞蟻金服的 Node.js、前端以及設計小伙伴們的更多姿勢,可以報名首屆螞蟻體驗科技大會 SEE Conf,比如有死馬大大的《Developer Experience First —— Techless Web Application 的理念與實踐》,還有青梔大大的《螞蟻開發者工具,服務螞蟻生態的移動研發 IDE》等等。

報名官網:https://seeconf.alipay.com/

期待您的光臨。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/39721.html

相關文章

  • ELSE 技術周刊(2017.12.18期)

    摘要:程序人生從黑客到創業,他說技術創業該這么做知道創宇,安全焦點民間白帽黑客組織核心成員,分享他創業感悟和踩過的那些坑。技術周刊由小組出品,匯聚一周好文章,周刊原文。 業界動態 他們寫的代碼能上天!NASA的10條安全編碼準則大公開 NASA的10條代碼編寫規范準則 本期推薦 Node.js 中遇到含空格 URL 的神奇Bug——小范圍深入 HTTP 協議 本文闡述了博主遇到含空格 URL...

    douzifly 評論0 收藏0
  • 【學習筆記】CSS深入理解之float

    摘要:為了實現文字環繞效果,規范規定的措施是使父容器塌陷,元素脫離文檔流浮動,元素周圍的內容轉換為圍繞元素排列。從浮動的起因和浮動的實現后果來看,浮動不適合用于大范圍的布局,更適合利用其特性實現一些小范圍的流體布局效果。 張鑫旭的CSS深入理解之float浮動學習筆記 float的歷史 float為產生文字環繞效果而生 float的特性 — 包裹和破壞 包裹:即產生一個BFC破壞:使父容器...

    denson 評論0 收藏0
  • 手把手教你擼一個泡妞神奇

    摘要:畫字首先我在畫布上畫了個點,用這些點來組成我們要顯示的字,用不到的字就隱藏起來。星星閃爍效果這個效果實現很簡單,就是讓星星不停的震動,具體就是讓點的目的地坐標不停的進行小范圍的偏移。 哈哈哈哈!?。‘斘艺f在寫這邊文章的時候,妹子已經追到了,哈哈哈哈哈!??! 其實東西是一年前寫的,妹子早就追到手了,當時就是用這個東西來表白的咯,二話不說,先看效果(點擊屏幕可顯示下一句) showImg(...

    funnyZhang 評論0 收藏0
  • 對象設計要考慮有效范

    摘要:另載于現代對象設計主張組合優于繼承。對象的有效范圍,是指對象從創建到丟棄不再引用的這段時間,不包括等待被銷毀的時間。對于繼承也是同理,父類和子類應當有相同的有效范圍。同理,級的對象不要持有級的對象。 另載于 http://www.qingjingjie.com/blogs/9 現代對象設計主張組合優于繼承。總之無論組合還是繼承,對象都成了涉及多個類的復合結構。 對象的有效范圍,是指對象...

    hatlonely 評論0 收藏0

發表評論

0條評論

edgardeng

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<