Node.js 中遇到含空格 URL 的神奇“Bug”——小范围深入 HTTP 协议
首先声明,我在“Bug”字眼上加了引号,自然是为了说明它并非一个真 Bug。
             
              
             
            
1. 问题抛出
昨天有个童鞋在看后台监控的时候,突然发现了一个错误:
[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 协议去请求另一台服务器的时候,单方面被对方服务器断开了连接——并且并没有任何返回。
             
              
             
            
2. 开始重现
2.1. 客户端 CURL 指令
其实这次请求的一些猫腻很容易就能发现——在 URL 中有空格。所以我们能简化出一条最简单的 CURL 指令:
$ curl "http://foo/bar baz" -v注意: 不带任何转义。
             
              
             
            
2.2. 最小 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 。
             
              
             
            
2.3. 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 127.0.0.1 left intact
/d d 
              于是乎,理所当然,我 暂时 将这个事件定性为 Node.js 的一个 Bug。
             
              
             
            
3. Node.js 源码排查
认定了它是个 Bug 之后,我就开始了一贯的看源码环节——由于这个 Bug 的复现条件比较明显,我暂时将其定性为“Node.js HTTP 服务端模块在接到请求后解析 HTTP 数据包的时候解析 URI 时出了问题”。
             
              
             
            
3.1. http.js -> _http_server.js -> _http_common.js
源码以 Node.js 8.9.2 为准。
这里先预留一下我们能马上想到的 http:// 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;
             
              能看出来
              
               parsers
              
              是
              
               HTTPParser
              
              的一条 Free List(效果类似于最简易的动态内存池),每个 Parser 在初始化的时候绑定上了各种回调函数。具体的一些回调函数就不细讲了,有兴趣的童鞋可自行翻阅。
             
            
这么一来,链路就比较明晰了:
             
              
               请求进来的时候,Server 对象会为该次请求的 Socket 分配一个
              
              
               
                HttpParser
               
              
              
               对象,并调用其
              
              
               
                execute()
               
              
              
               函数进行解析,在解析完成后调用
              
              
               
                onParserExecuteCommon()
               
              
              
               函数。
              
             
            
             
              
             
            
3.2. http:// node_http_parser.cc
             
              我们在
              
               lib/_http_common.js
              
              中能发现,
              
               HTTPParser
              
              的实现存在于
              
               src/
               
                
                 http://
                
                
                 node_http_parser.cc
                
                
                
               
              
              中:
             
            
const binding = process.binding('http_parser');
const { methods, HTTPParser } = binding;
             
              至于为什么
              
               const binding = process.binding('http_parser')
              
              就是对应到
              
               src/
               
                
                 http://
                
                
                 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<true>);
env->SetProtoMethod(t, "resume", Parser::Pause<false>);
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<Value>& args) {
    Parser* parser;
    Local<Object> buffer_obj = args[0].As<Object>();
    char* buffer_data = Buffer::Data(buffer_obj);
    size_t buffer_len = Buffer::Length(buffer_obj);
    Local<Value> ret = parser->Execute(buffer_data, buffer_len);
    if (!ret.IsEmpty())
      args.GetReturnValue().Set(ret);
  Local<Value> 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<Value>());
    Local<Integer> 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<Value> e = Exception::Error(env()->parse_error_string());
      Local<Object> 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 中所依赖的源码,应该会有偏差。
             
              
             
            
3.3. http-parser
3.3.1. 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.13.3.2. 源码解析
             
              话不多少,我们之间前往 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_CONSTANT并goto到error代码块。
至此,我们基本上已经明白了原因了:
             
              
               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就是已解析的数据长度。如果成功解析完,这个数据包理论上是等于这个数据包的完整长度,若不等则理论上说明肯定是中途出错提前返回。
3.4. 回到 http:// node_http_parser.cc
看完了 http-parser 的原理后,很多地方茅塞顿开。现在我们回到它的调用地 node_http_parser.cc 继续阅读吧。
Local<Value> Execute(char* data, size_t len) {
  size_t nparsed =
    http_parser_execute(&parser_, &settings, data, len);
  Local<Integer> nparsed_obj = Integer::New(env()->isolate(), nparsed);
  if (!parser_.upgrade && nparsed != len) {
    enum http_errno err = HTTP_PARSER_ERRNO(&parser_);
    Local<Value> e = Exception::Error(env()->parse_error_string());
    Local<Object> 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_obj
              
              即
              
               nparsed
              
              )。
             
            
             
              在这个文件中,眼尖的童鞋可能发现了,执行
              
               Execute()
              
              有好多处,这是因为实际上一个 HTTP 请求可能是流式的,所以有时候可能会只拿到部分数据包。所以最后有一个结束符需要被确认。
              
               这也是为什么 http-parser 在解析的时候只能逐字解析而不能跳跃或者后退了。
              
             
            
            3.5. 回到 _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
              
              是一个关键。
             
            
4. 处理办法
             
              要解决这个“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\r\n\r\n');
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 状态码。
             
              
             
            
5. 引申
接下来我们要引申讨论的一个点是,为什么这货不是一个真正意义上的 Bug。
首先我们看看 Nginx 这么实现这个黑科技的吧。
             
              
             
            
5.1. 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
<head><title>400 Bad Request</title></head>


 
                         
              