引子

上一篇 分析了 rewrite 模块的实现和工作流程,这一篇我们来解释一下为什么 "if is evil" 中的例子会造成 “不合常理” 的结果。

下面针对每一个例子,我们先把配置片断完整列出来。然后,再给出目前它在 Nginx 中的 状态。最后,如果文字无法很好解释清楚,我们再从代码上对该例子进行分析。

例 1

# only second header will be present in respons
# not really bug, just how it works
location /only-one-if {
    set $true 1;

    if ($true) {
        add_header X-First 1;
    }

    if ($true) {
        add_header X-Second 2;
    }

    return 204;
}
  • 依然和原文描述一致。它属于 Nginx 的固有工作模式。

  • 原因:

    • add_header 指令是 ngx_http_headers_module 模块 (output filter module) 提供的指令,这个指令基本算是工作在 CONTENT 阶段。

    • REWRITE 阶段的指令执行完后,请求才会进入下一个阶段。那两个 if 指令 的 condition 都是 true,那么最后一个 if {} 会成为请求关联的 location 作用域。于是,进入 CONTENT 阶段后,第二个 if {} 中的 add_header 指 令才会生效。

例 2

# request will be sent to backend without uri changed
# to '/' due to if
location /proxy-pass-uri {
    proxy_pass http://127.0.0.1:8080/;

    set $true 1;

    if ($true) {
        # nothing
    }
}
  • Nginx BUG, 因为最新稳定版 1.8.0 已经修正了:ChangeLog

  • 大致原因就是因为往 if {} 创建的无名 location {} 命并配置时,漏了一个变量 plcf->location

  • 另外,值得提起的一个点是:proxy_pass 是一个 action directive, 按照定义和其它嵌套 location 的实现来看,这个指令不应该被子作用域继承,但是 if {} 当作子作用域的话,貌似是个例外,原因如下:

    • proxy_pass, fastcgi_pass 等指令提供了 location handler。一般情况下, Nginx 开始在 FIND_CONFIG 阶段处理请求时,因为要为请求匹配新的 location {}, 在 ngx_http_core_find_config_phase 函数里先将 r->content_handlerNULL,匹配成功后,如果匹配到的 location {} 提供了 location handler, 再次在 ngx_http_update_location_config 函数中将 r->content_handler 赋与 新值:

      /* ngx_http_proxy_pass */
      clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
      clcf->handler = ngx_http_proxy_handler;
      
      /* ngx_http_core_find_config_phase */
      r->content_handler = NULL;
      
      /* ngx_http_update_location_config */
      if (clcf->handler) {
          r->content_handler = clcf->handler;
      }
      
    • 由上一篇的分析我们能看到,如果 if 指令后的 conditon 成立,在命令函数 ngx_http_script_if_code 中只调用了 ngx_http_update_location_config 函数 而并没有让请求重新进入 FIND_CONFIG 阶段。这样一来,r->content_handler 的值就得以保留。

    • 这也算是 if 的实现带来的 “副” 作用吧;

例 3

# try_files won't work due to if
location /if-try-files {
    try_files /file @fallback;

    set $true 1;

    if ($true) {
        # nothing
    }
}
  • Nginx 固有逻辑

  • 原因:首先,try_files 不会被子作用域继承。其次,try_files 工作于 Nginx 内部定义的 TRY_FILES 阶段,并且此阶段优先级低于 REWRITE。所以,当 if 指令的 condition 成立时,请求和 if 创建的无名作用域关联,而此无名作用 域又不会继承 try_files 相关配置。

例 4

# nginx will SIGSEGV
location /crash {
    set $true 1;

    if ($true) {
        fastcgi_pass 127.0.0.1:9000;
    }

    if ($true) {
        # no handler here
    }
}
  • Nginx BUG,目前稳定版本 1.2.0 已修正:Changelog
  • 原因:参见 ChangeLog。

例 5

# alias with captures isn't correctly inherited into implicit nested
# location created by if
location ~* ^/if-and-alias/(?<file>.*) {
    alias /tmp/$file;

    set $true 1;

    if ($true) {
        # nothing
    }
}
  • Nginx BUG, 目前在稳定版 1.8.0 已修正: Changelog
  • 原因:参见 ChangeLog。

小结

"if is evil" 一文中出现的例子要么已经被修正了,要么就是确实不能那么用。但是, 原文不是所有 if 带来的问题大全,兴许某一天,你自己就会碰见。希望能通过这两 篇的讲解,将来某一天碰到莫名的问题时,你能自己分析原因,说不定还能找到 Nginx 的 BUG 呢。

总之,安全驾驶,有备无患:

  • Please make sure you actually do understand how it works.
  • Do proper tesing.
Comments

不要轻轻地离开我,请留下点什么...

comments powered by Disqus

Published

Category

Nginx

Tags

Contact