引子
我们知道 "If is evil" (原文),将它用于
location {}
时可能会造成出乎意料的问题。"If is evil" 一文中列举出了几个这
样的例子,同时也总结了 if
的正确和安全的用法。
这几个例子展示的问题有些是 Nginx 的固有逻辑,有些作为 BUG 己经被修正了。本文先 对 rewrite 模块的实现进行分析,然后再从 Nginx 源代码的角度对这些例子为什么 有 ”不合常理“ 的结果进行解释。
几点事实
-
从 Nginx Configuration Inheritance Model 这篇文章中我们知道, rewrite 模块提供配置指令被称为 action directives,下一层级的 location 中不会从上一级 location 继承这些配置指令。从 Nginx 源代码里的
ngx_http_rewrite_merge_loc_conf
函数中我们能找到证据。 -
server 作用域中的 rewrite 模块指令也不会向下传递到 location 作 用域,但是这些指令会在 SERVER_REWRITE 阶段 (先于 REWRITE 阶段) 被执行。 也就是说,server 作用域的 rewrite 模块指令和 location 作用域的 rewrite 模块指令都会被 Nginx 执行。
-
rewrite 模块提供的指令的执行顺序和其在配置文件中的定义顺序一致。
-
if
指令在 Nginx 内部创建了一个无名 location,if
条件为真时,Nginx 使用 这个无名 location 作用域的配置处理当前请求。 -
POST REWRITE 是 Nginx 内部定义的阶段,通过检查请求 uri 是否被 rewrite 模块修改 (
r->uri_changed
),判断是否需要使用修改后的 uri 重新开始 FIND CONFIG 以重新匹配合适的 location。比如在 location 中有配置如rewrite ... last;
且rewrite
成功和请求 uri 匹配成功时。
rewrite 模块
rewrite 模块是一个 phase handler,
其初始化函数 ngx_http_rewrite_init
在 SERVER REWRITE 和 REWRITE 阶段
注册了相同的处理函数 ngx_http_rewrite_handler
。其中,SERVER REWRITE 阶段
的处理函数用于执行 server 作用域中的 rewrite 模块指令,而 REWRITE 阶
段的处理函数用于执行 location 和 if
作用域的 rewrite 模块指令。
rewrite 模块文档 中提到:
The
ngx_http_rewrite_module
module directives are compiled at the configuration stage into internal instructions that are interpreted during request processing. An interpreter is a simple virtual stack machine.
这个 ”虚拟栈机“ (Virtual stack machine) 就是我们在配置变量
一文中分析的 Nginx 的命令解释器 ngx_http_script_engine_t
。Nginx 将包含变量的
配置指令 (directive) 编译为可以在运行对变量进行求值的命令 (instruction,实际上
是一系统预先定义的函数),然后在 Nginx 处理请求的过程中执行这些命令对变量求值。
rewrite 模块提供的配置指令基本都和变量有关系,if
除了使用变量之外,还需
要根据变量值执行不同的逻辑 (使用不同的 location 继续处理请求),所以这个模块
也是 Nginx 提供的命令解释器的大用户。rewrite 模块被编译后的命令保存在
ngx_http_rewrite_loc_conf_t::codes
字段中。
typedef struct {
ngx_array_t *codes;
ngx_uint_t stack_size;
...
} ngx_http_http_rewrite_loc_conf_t;
接下来,我们选取两个配置指令 break
和 if
,从配置解析和请求处理两个阶段对相
关代码进行分析。
break
Nginx 大部分模块提供的配置指令的生效顺序和其在配置文件中的顺序并没有什么关系,
比如 proxy 模块。但是有些模块提供的配置指令像编程语言的语句一样,生效顺序
又和配置顺序一致,比如 http core 模块提供的使用了正则表达式的 location
和本文的主角 rewrite 模块提供的 if
, break
, return
和 rewrite
指令。
break
指令用于在运行时中断 rewrite 模块指令编译后的命令执行流程,也就是
说,break
之前的命令执行完毕后,就会结束当前的 SERVER REWRITE
或者 REWRITE
阶段,随后 Nginx 进入下一个阶段 (PHASE)。
break
配置指令的解析函数是 ngx_http_rewrite_break
:
static char *
ngx_http_rewrite_break(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_rewrite_loc_conf_t *lcf = conf;
ngx_http_script_code_pt *code;
code = ngx_http_script_start_code(cf->pool, &lcf->codes, sizeof(uintptr_t));
....
*code = ngx_http_script_break_code;
return NGX_CONF_OK;
}
其中,ngx_http_script_start_code
函数用于在 ngx_http_rewrite_loc_conf_t
的
codes
字段中开辟 sizeof(uintptr_t)
大小的字节,用于存储编译后的命令。随后,
break
指令被 Nginx 编译成了命令 ngx_http_script_break_code
,这个命令在运行
时由命令解释器调用:
/* ngx_http_rewrite_handler */
ngx_http_script_code_pt code; /* 命令解释器识别的命令 */
ngx_http_script_engine_t *e;
...
e = ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t));
...
e->ip = rlcf->codes->elts;
e->request = r;
e->quote = 1;
e->log = rlcf->log;
e->status = NGX_DECLINED;
while (*(uintptr_t *) e->ip) {
code = *(ngx_http_script_code_pt *) e->ip;
code(e);
}
if (e->status < NGX_HTTP_BAD_REQUEST) {
return e->status;
}
假如我们在当前请求使用的 location 作用域中只定义的 break
是唯一个
rewrite 模块指令,那么命令指针 e->ip
的初始值就是 ngx_http_script_break_code
,
接下来进入解释循环后,ngx_http_rewrite_handler
马上开始执行这个命令:
/* ngx_http_script_break_code */
e->request->uri_changed = 0;
e->ip = ngx_http_script_exit;
随后,命令指针 e->ip
指向 ngx_http_script_exit
命令,进入下一次循环后
ngx_http_rewrite_handler
调用这个命令将命令指针 e->ip
设置为 NULL
,结束
命令解释的 while
循环:
#define ngx_http_script_exit (u_char *) &ngx_http_script_exit_code
static uintptr_t ngx_http_script_exit_code = (uintptr_t) NULL;
这次的命令解释器执行过程中,Nginx 并没有修改 e->status
的值,所以最终
ngx_http_rewrite_handler
函数的返回值是 NGX_DECLINED
,于是 phase checker
就跳过了当前 phase handler,即 ngx_http_rewrite_handler
,开始调用下一个
phase handler。
在实际生成环境中,单独使用 break
并没有太大的实际意义。但是如果和 if
指令
结合使用,那作用就大了。
if
Nginx 在运行过程中对 if
指令定义的表达式求值:如果表达式值为 true
,Nginx
使用 if
创建的无名 location {}
的配置处理该请求;如果表达式值为 false
,
Nginx 跳过 if {}
,继续执行后面的 rewrite 模块指令。
先来大概了解下 if
的用法:
if
指令语法是:if (condition) { ... }
;if
指令只能用于server {}
和location {}
中;if
指令的condition
可以是:- 单个变量 - 变量值为空字符串或者 "0" 时表达式为
false
,其余情形为true
; - 一元运算符和一个变量 - 支持的一元运算符有
-f
,!-f
,-d
,!-d
,-e
,!-e
,-x
,!-x
。这些运算符的具体作用参见官方文档; - 二元运算符和两个变量 (或一个变量和一个常量,变量必须放在运算符前面) -
=
,!=
,~
,~*
,!~
,!~*
。这些运算符的具体作用参见官方文档;
- 单个变量 - 变量值为空字符串或者 "0" 时表达式为
if
指令的配置解析和向运行时命令的编译过程,及运行时命令的解释执行,比上文分
析的 break
要复杂很多,和上文对 break
的分析类似,我们依然按静态配置解析和
运行时命令执行两个阶段,对 if
指令的代码实现进行分析。
在罗列代码之前,我们先来给出几个结论:
(1) if {}
作用域里的 rewrite 模块指令和它所在的 server 或者 location
作用域里的 rewrite 模块指令共用编译后的命令存储空间,即
ngx_http_rewrite_loc_conf_t::codes
。这个设定最早可追溯到 Nginx 早期的
0.17 RELEASE 版本:
*) Changes: the
ngx_http_rewrite_module
was rewritten from the scratch. Now it is possible to redirect, to return the error codes, to check the variables and referers. The directives can be used inside locations. The redirect directive was canceled.
我猜测,Nginx 之所以这样实现是为了简化逻辑。如果让 if {}
创建的无名 location {}
拥有自己独立的命令数组,那么进入了这个无名 location{}
后,如果需要执行其中的
rewrite 模块指令对应的命令,势必需要再次调用 ngx_http_rewrite_handler
或
者初始化一个新的解释器。而如果在 if {}
配置的还有其它 rewrite 模块指令,
那么在执行完无名 location 的命令后,还需要再次恢复原先的解释器环境。这个过程
增加了代码实现的复杂度。
上面的描述引出了另外一个结论: (2) if
条件为 true
的情况下,Nginx 虽然把请
求和无名 location {}
关联了起来,但是由于当前依然处于 REWRITE 阶段,
if {}
后面的 rewrite 模块指令依然需要继续执行。"What happens in Vegas
stays in Vegas"。在 REWRITE 阶段定义的指令,在此阶段执行完毕后,Nginx 才
开始执行下一个阶段的指令。
配置解析
ngx_http_rewrite_if
是 if
指令的配置解析函数。它会:
-
创建一个无名
location {}
(由ngx_http_conf_ctx_t::loc_conf
表示),并和 它的父location {}
建立关系,以方便后续进行配置继承,/* ngx_http_rewrite_if */ ngx_http_conf_ctx_t *ctx, *pctx; ngx_http_core_loc_conf_t *clcf, *pclcf; ... ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t)); ... pctx = ctx->ctx; ctx->main_conf = pctx->main_conf; ctx->srv_conf = pctx->srv_conf; ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void*) * ngx_http_max_module); ... for (i = 0; ngx_moudles[i]; i++) { ... if (module->create_loc_conf) { mconf = module->create_loc_conf(cf); ctx->loc_conf[ngx_modules[i]->ctx_index = mconf; } } pclcf = pctx->loc_conf[ngx_http_core_module.ctx_index]; clcf = ctx->loc_conf[ngx_http_core_module.ctx_index]; clcf->loc_conf = ctx->loc_conf; clcf->name = pclcf->name; clcf->noname = 1; ngx_http_add_location(cf, &pclcf->locations, clcf);
-
将 condition 和跳转逻辑编译为解释器可识别的命令,
/* ngx_http_rewrite_if */ ngx_http_rewrite_if_condition(cf, lcf); if_code = ngx_array_push_n(lcf->codes, sizeof(ngx_http_script_if_code_t)); if_code->code = ngx_http_script_if_code; /* the inner directives must be compiled to the same code array */ nlcf = ctx->loc_conf[ngx_http_rewrite_module.ctx_index]; nlcf->codes = lcf->codes; ... if_code->next = (u_char *) lcf->codes->elts + lcf->ocdes->nelts - (u_char *) if_code; /* the code array belong to parent block */ nlcf->codes = NULL;
-
对无名
location {}
内部的配置指令进行解析 (配置指令的解析、组织和存储,可 以参考配置文件解析 一文)。/* ngx_http_rewrite_if */ save = *cf; cf->ctx = ctx; ... rv = ngx_conf_parse(cf, NULL); *cf = save;
而配置继承的相关逻辑在配置文件解析 一文也有分析。对 rewrite 模块而言,配置文件解析完毕后,Nginx 会在
ngx_http_merge_servers
函数中将会调用 ngx_http_rewrite_merge_loc_conf
函数,完成自己的配置继承操作。
if
指令 condition 的解析和编译都在函数 ngx_http_rewrite_if_condition
中
进行,这个函数完整的执行流程我们就暂且不详细描述了。为了方便下文__命令执行__部
分的叙述,我们先构造一个配置示例,然后直接给出编译后的命令数组。
对于下面配置片断:
location / {
root html;
if ($host == ”example.com") {
root html/$host;
}
}
经 ngx_http_rewrite_if_condition
函数解析编译后的命令数组如下图所示:
lcf->codes +---------------------------------+
| ngx_http_script_var_code_t | .code = ngx_http_script_var_code
| | .index = index of $host
+---------------------------------+
| | .code = ngx_http_script_value_code
| ngx_http_script_value_code_t | .value = 0
| | .text_len = length of "example.com"
| | .text_data = "example.com"
+---------------------------------+
| ngx_http_script_equal_code |
if_code +---------------------------------+
| ngx_http_script_if_code_t | .code = ngx_http_script_if_code
| | .next = lcf->codes->elts + lcf->codes->nelts
+---------------------------------+ - (u_char *) if_code;
| other codes compiled from |
| directives in `if {}` |
if_code->next +---------------------------------+
| directives following `if {}` |
| if any |
+---------------------------------+
命令执行
下面的代码分析根据上一节构造的示例进行。
现在,假设 Nginx 开始运行后,收到了 HTTP 请求 http://example.com/
。经过
FIND CONFIG 阶段,请求和 location / {}
关联了起来,接下来进入了
REWRITE 阶段,Nginx 调用 phase handler ngx_http_rewrite_handler
函数:
/* ngx_http_rewrite_handler */
ngx_http_script_code_pt code;
ngx_http_script_engine_t *e;
ngx_http_rewrite_loc_conf_t *rlcf;
...
rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rewrite_module);
e = ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t));
e->sp = ngx_pcalloc(r->pool, rlcf->stack_size * sizeof(ngx_http_variable_value_t));
e->ip = rlcf->codes->elts;
e->request = r;
e->quote = 1;
e->status = NGX_DECLINED;
while (*(uintptr_t *) e->ip) {
code = *(ngx_http_script_code_pt *) e->ip;
code(e);
}
对上述代码的补充说明:
-
命令解释器 (
ngx_http_script_engine_t
) 有 instructive pointere->ip
,也 有 stack 和 栈顶指针e->sp
,简直就是一个软 CPU; -
stack 用来暂存命令的执行结果。比如命令
ngx_http_script_var_code_t
对变量 取值成功后,会将变量值入栈; -
e->ip
始终指向将要执行的命令,如果e->ip == NULL
则表示命令数组执行完毕, 这时解释器循环退出。rewrite 模块的命令数组的结束标记由ngx_http_rewrite_merge_loc_conf
函数设定;/* ngx_http_rewrite_merge_loc_conf */ code = ngx_array_push_n(conf->codes, sizeof(uintptr_t)); *code = (uintptr_t) NULL;
-
由于每个命令在命令数组中占用的空间不同,
e->ip
下一条指令的位置由当前命令占 用的空间决定; -
Nginx 使用结构体表示一条命令,其第一个成员必须为
ngx_http_script_code_pt
类型的函数指针,剩余成员为函数参数 (或命令的上下文),这样通过e->ip
经过不同 的类型转换,就能得到命令函数和它的参数;
现在命令解释器开始执行第一条命令 ngx_http_script_var_code_t
了,它对应的命令
函数是 ngx_http_script_var_code
:
/* ngx_http_script_var_code */
ngx_http_variable_value_t *value;
ngx_http_script_var_code_t *code;
code = (ngx_http_script_var_code_t *) e->ip;
e->ip += sizeof(ngx_http_script_var_code_t);
value = ngx_http_get_flushed_variable(e->request, code->index);
...
*e->sp = *value;
e->sp++;
上面命令执行完毕后,Nginx 得到了 $host
的值,并将它存到了命令解释器的栈顶。
同时,e->ip
也指向了下一条命令 ngx_http_script_value_code_t
。
在下一次命令执行循环中,Nginx 会调用其命令函数 ngx_http_script_value_code
:
/* ngx_http_script_value_code */
ngx_http_script_value_code_t *code;
code = (ngx_http_script_value_code_t *) e->ip;
e->sp->len = code->text_len;
e->sp->data = (u_char *) code->text_data;
e->sp++;
这条命令只简单的将 example.com
字符常量做入栈处理。同时,e->ip
指向了下一
条命令 ngx_http_script_equal_code
:
/* ngx_http_script_equal_code */
ngx_http_variable_value_t *val, *res;
e->sp--;
val = e->sp;
res = e->sp - 1;
e->ip += sizeof(uintptr_t);
if (val->len == res->len
&& ngx_strncmp(val->data, res->data, res->len) == 0)
{
*res = ngx_http_variable_true_value;
return;
}
ngx_http_script_equal_code
命令将栈顶的两个值进行比较,如果两个值相等,将
最终结果 ngx_http_variable_true_value
存入栈顶。此时,e->ip
指向了命令
ngx_http_script_if_code_t
。
同上,命令解释器开始执行命令函数 ngx_http_script_if_code
:
/* ngx_http_script_if_code */
ngx_http_script_if_code_t *code;
code = (ngx_http_script_if_code_t *) e->ip;
e->sp--;
if (e->sp->len && (e->sp->len != 1 || e->sp->data[0] != '0')) {
if (code->loc_conf) {
e->request->loc_conf = code->loc_conf;
ngx_http_update_location_config(e->request);
}
e->ip += sizeof(ngx_http_script_if_code_t);
return;
}
e->ip += code->next;
ngx_http_script_if_code
函数检查栈顶元素的值是否为“真”值。如果是,将当前请求
和 if {}
关联起来;如果不是,继续执行 if {}
后面的 rewrite 模块命令。
对于我们构造的例子,请求此刻和 if {}
关联起来了,并且由于 if {}
后并没有
其它 rewrite 模块指令,e->ip
此时的值变为 NULL
,也就是说,整个命令解释
到此就算完成了,随后,ngx_http_rewrite_handler
返回 NGX_DECLINED
,Nginx 开
始对当前请求进行下一阶段的处理。
小结
rewrite 模块基本分析完了,我个人认为这个模块是 Nginx 模块中较复杂和精妙的 一个,它实现了一种精简的 “语言”。
下一篇在本片得到结论的基础上,对 "If is evil" 中提到的 “令人意外” 的例子一一 解释。
Comments
不要轻轻地离开我,请留下点什么...