Nginx Rewrite重定向模块原理
今天花了点时间学习nginx rewrite模块的代码,其跟脚本解析是紧密相连的。
老毛病,贴个代码,以后补图,脑袋快撑不住了····
其他注释的代码在这里:https://github.com/kulv2012/ReadNginxSrc
先来看一下重定向模块的基本数据结构,ngx_http_rewrite_loc_conf_t,这个结构比较简单,主要就是codes数组,这个数组保存了一系列语法单元的函数,用来进行脚本解析的,也是脚本运算的结果函数数组。
typedef struct { ngx_array_t *codes;/* uintptr_t *///此结构保存着code_t的实现函数数组,用来做重定向的。 //这里是句柄数组组成的,逻辑上分为一组一组的,每条rewrite语句占用一组,每一组可能包含好几条code()函数指针等数据。 //如果匹配失败就通过next跳过本组。 ngx_uint_t stack_size;//貌似是e->sp[]数组的大小,其用来存储正则简析时,存放类似堆栈的临时值。 ngx_flag_t log; ngx_flag_t uninitialized_variable_warn; } ngx_http_rewrite_loc_conf_t;
rewrite模块支持的指令都包含在ngx_http_rewrite_commands里面,包括rewrite,return,break,if,set等。模块初始化函数比较简单,只是将ngx_http_rewrite_handler函数句柄设置到SERVER_REWRITE_PHASE 和 REWRITE_PHASE过程中去。这样在ngx_http_core_run_phases的时候会调用这个句柄,也就是ngx_http_core_find_config_phase之后,会调用ngx_http_rewrite_handler进行重定向的过程。这里我们再回顾一下ngx_http_core_run_phases所使用的重要数据结构cmcf->phase_engine.handlers,各个阶段默认情况下的句柄分别为:ngx_http_init_phase_handlers里面设置的。在我编译的nginx环境下,其句柄数组结构为:
0 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x45963a <ngx_http_realip_handler>, next = 1}
1 {checker = 0x42f792 <ngx_http_core_rewrite_phase>, handler = 0x45d41e <ngx_http_rewrite_handler>, next = 2}
2 {checker = 0x42fc03 <ngx_http_core_find_config_phase>, handler = 0, next = 0}
3 {checker = 0x42f792 <ngx_http_core_rewrite_phase>, handler = 0x45d41e <ngx_http_rewrite_handler>, next = 4}
4 {checker = 0x42f7cd <ngx_http_core_post_rewrite_phase>, handler = 0, next = 2}
5 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x45963a <ngx_http_realip_handler>, next = 8}
6 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x458b17 <ngx_http_limit_req_handler>, next = 8}
7 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x457c9a <ngx_http_limit_conn_handler>, next = 8}
8 {checker = 0x42f889 <ngx_http_core_access_phase>, handler = 0x45711a <ngx_http_access_handler>, next = 11}//简单的IP禁止
9 {checker = 0x42f889 <ngx_http_core_access_phase>, handler = 0x456b81 <ngx_http_auth_basic_handler>, next = 11}//简单HTTP密码验证
10 {checker = 0x42f9a0 <ngx_http_core_post_access_phase>, handler = 0, next = 11}
11 {checker = 0x4305a8 <ngx_http_core_content_phase>, handler = 0x449afb <ngx_http_index_handler>, next = 14}
12 {checker = 0x4305a8 <ngx_http_core_content_phase>, handler = 0x455a22 <ngx_http_autoindex_handler>, next = 14}
13 {checker = 0x4305a8 <ngx_http_core_content_phase>, handler = 0x449241 <ngx_http_static_handler>, next = 14}
14 {checker = 0, handler = 0x8d200002, next = 0}
因此模块的重点就在ngx_http_rewrite_handler函数了。不过我们先来关注一下指令的初始化解析的过程。
零、ngx_http_rewrite_handler重定向回调:
ngx_http_rewrite_init函数在初始化时将这个函数加入SERVER_REWRITE_PHASE 和 REWRITE_PHASE过程中。这样每次进入ngx_core_run_phrases()后会调用这个地方进行重定向。重定向完毕后,如果不是break,就将进入下一次find config 阶段,后者成功后又将进行重新的rewrite,就像有个新的请求到来一样。
这个函数首先取得ngx_http_rewrite_module的location配置,然后申请一个ngx_http_script_engine_t脚本解析的结构体,这个结构体是提供给脚本解析的各个回调函数的参数。然后为e->sp变量堆栈数组申请空间,用来存放各个code的结果空间大小为stack_size,设置解析的指针表e->ip = rlcf->codes->elts,这个比较重要,codes我们上面介绍过了,是一组组函数指针。我们来看一下ngx_http_script_engine_t这个结构的代码:
typedef struct { u_char *ip; /*关于pos && code: 每次调用code,都会将解析到的新的字符串放入pos指向的字符串处, 然后将pos向后移动,下次进入的时候,会自动将数据追加到后面的。 对于ip也是这个原理,code里面会将e->ip向后移动。移动的大小根据不同的变量类型相关。 ip指向一快内存,其内容为变量相关的一个结构体,比如ngx_http_script_copy_capture_code_t, 结构体之后,又是下一个ip的地址。比如移动时是这样的 : code = (ngx_http_script_copy_capture_code_t *) e->ip; e->ip += sizeof(ngx_http_script_copy_capture_code_t);//移动这么多位移。 */ u_char *pos;//pos之前的数据就是解析成功的,后面的数据将追加到pos后面。 ngx_http_variable_value_t *sp;//这里貌似是用sp来保存中间结果,比如保存当前这一步的进度,到下一步好用e->sp--来找到上一步的结果。 ngx_str_t buf;//存放结果,也就是buffer,pos指向其中。 ngx_str_t line;//记录请求行URI e->line = r->uri; /* the start of the rewritten arguments */ u_char *args; unsigned flushed:1; unsigned skip:1; unsigned quote:1; unsigned is_args:1; unsigned log:1; ngx_int_t status; ngx_http_request_t *request;//所属的请求 } ngx_http_script_engine_t;
关于ip和code上面注释比较详细了,不多说,ngx_http_rewrite_handler初始化ngx_http_script_engine_t后,就开始循环的调用e->ip进行解析,重定向匹配,比如某个location {}里面有多个rewrite指令的 话,那么他们的解析函数组会按顺序安排好的,举个例子,对于下面的指令:
rewrite ^(.*)$ http://$http_host.mp4 break;
其解析后的函数数组为:
ngx_http_rewrite_handler
- ngx_http_script_regex_start_code 解析完了正则表达式。并求出总长度,设置到了e上了
- 1 ngx_http_script_copy_len_code 7
- 2 ngx_http_script_copy_var_len_code 18
- 3 ngx_http_script_copy_len_code 4 === 29
- ngx_http_script_copy_code 拷贝"http://" 到e->buf
- ngx_http_script_copy_var_code 拷贝"115.28.34.175:8881"
- ngx_http_script_copy_code 拷贝".mp4"
- ngx_http_script_regex_end_code
这样就能够首先调用ngx_http_script_regex_start_code函数,这个函数会匹配 ^(.*)$ 看看是否匹配成功,如果成功,那它就会调用后面的1.1到1.3的长度计算函数,来计算后面http://$http_host.mp4的解析后的长度。
然后ngx_http_script_regex_start_code函数返回,下一个函数就是ngx_http_script_copy_code函数,这个函数会拷贝rewrite后面目标的前面静态字符串,也就是http://,下一个是ngx_http_script_copy_var_code,这个是用来拷贝变量的。具体看下面的代码:
/*ngx_http_rewrite_init函数在初始化时将这个函数加入SERVER_REWRITE_PHASE 和 REWRITE_PHASE过程中。 //这样每次进入ngx_core_run_phrases()后会调用这个地方进行重定向。 重定向完毕后,如果不是break,就将进入下一次find config 阶段, 后者成功后又将进行重新的rewrite,就像有个新的请求到来一样。 */ static ngx_int_t ngx_http_rewrite_handler(ngx_http_request_t *r) { 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); if (rlcf->codes == NULL) {//如果没有处理函数,直接返回,因为这个模块肯定没有一条rewrite。也就是不需要 return NGX_DECLINED;//如果返回OK就代表处理完毕,不用处理i后面的其他过程了。 } //新建一个脚本引擎,开始进行codes的解析。 e = ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t)); if (e == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } //下面的stack_size到底在哪里设置的/代码里面都找不到。 //功能是用来存放计算的中间结果。 e->sp = ngx_pcalloc(r->pool, rlcf->stack_size * sizeof(ngx_http_variable_value_t)); if (e->sp == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } e->ip = rlcf->codes->elts; e->request = r; e->quote = 1; e->log = rlcf->log; e->status = NGX_DECLINED; /*对于这样的: rewrite ^(.*)$ http://$http_host.mp4 break; 下面的循环是i这样走的 ngx_http_rewrite_handler 1. ngx_http_script_regex_start_code 解析完了正则表达式。并求出总长度,设置到了e上了 1.1 ngx_http_script_copy_len_code 7 1.2 ngx_http_script_copy_var_len_code 18 1.3 ngx_http_script_copy_len_code 4 === 29 2. ngx_http_script_copy_code 拷贝"http://" 到e->buf 3. ngx_http_script_copy_var_code 拷贝"115.28.34.175:8881" 4. ngx_http_script_copy_code 拷贝".mp4" 5. ngx_http_script_regex_end_code */ while (*(uintptr_t *) e->ip) {//遍历每一个函数指针,分别调用他们。 code = *(ngx_http_script_code_pt *) e->ip; code(e);//执行对应指令的函数,比如if等, } if (e->status == NGX_DECLINED) { return NGX_DECLINED; } if (r->err_status == 0) { return e->status; } return r->err_status; }
ngx_http_rewrite_handler相对比较简单,只是循环调用了一下脚本引擎根据当前location指令内容设置的各个回调函数,那么,这些回调函数是怎么设置进去的呢?答案是:指令解析的时候。下面逐一来看看。
一、rewrite 语句解析
rewrite语句语法为:
Syntax: | rewrite regex replacement [ flag ] |
当ngx_conf_parse 函数解析到rewrite指令时,会调用ngx_http_rewrite函数去解析上面的指令行,ngx_http_rewrite函数完成如下几个功能:
- 解析正则表达式,提取子模式,命名子模式存入variables等;
- 解析第四个参数last,break等。
- 调用ngx_http_script_compile将目标字符串解析为结构化的codes句柄数组,以便解析时进行计算;
- 根据第三步的结果,生成lcf->codes 组,后续rewrite时,一组组的进行匹配即可。失败自动跳过本组,到达下一组rewrite。
我们一步步来看ngx_http_rewrite函数,其开头调用ngx_http_script_start_code申请了一个脚本开始节点,用来做什么的呢,上面我们在ngx_http_rewrite_handler看到,这是用来做脚本解析的,其设置的code为ngx_http_script_regex_start_code函数完成了一个重要的功能:
- 调用正则表达式引擎编译URL参数行,如果匹配失败,则e->ip += code->next;让调用方调到下一个表达式块进行解析。
- 如果成功,调用code->lengths,从而获取正则表达式替换后的字符串长度,以备在此函数返回后的code函数调用中能够存储新字符串长度。
接着调用ngx_http_regex_compile函数,完成正则表达式的编译,也就是下面正则表达式的^(/xyz.*)$部分解析,该函数完成了下面的功能:
- 1. 调用ngx_regex_compile编译正则表达式,并且得到相关的数据,比如子模式数目,命名子模式数目,列表等。
- 2. 将正则表达式信息存储到ngx_http_regex_t里面,包括正则句柄,子模式数目
- 3. 将命名子模式 加入到cmcf->variables_keys和cmcf->variables中。以备后续通过名字查找变量值。
ngx_http_regex_compile函数完成后,正则表达式部分的数据结构以及准备好了,后面简单做了一下是否要重定向,是否要调整站点302,以及last,break,redirect等指令,设置相关的标志,对于break参数,我们会设置regex->break_cycle ,在脚本模块会看到这个参数的用处。这个参数决定了rewrite后,如果URL有变化,nginx还是会设置url_changed=0,来避免进行重定向。下面看一下代码:
/* 1. 解析正则表达式,提取子模式,命名子模式存入variables等; 2. 解析第四个参数last,break等。 3.调用ngx_http_script_compile将目标字符串解析为结构化的codes句柄数组,以便解析时进行计算; 4.根据第三步的结果,生成lcf->codes 组,后续rewrite时,一组组的进行匹配即可。失败自动跳过本组,到达下一组rewrite */ static char * ngx_http_rewrite(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {//碰到"rewrite"指令时调用这里。 //比如rewrite ^(/xyz/aa.*)$ http://$http_host/aa.mp4 break; ngx_http_rewrite_loc_conf_t *lcf = conf; ngx_str_t *value; ngx_uint_t last; ngx_regex_compile_t rc; ngx_http_script_code_pt *code; ngx_http_script_compile_t sc; ngx_http_script_regex_code_t *regex; ngx_http_script_regex_end_code_t *regex_end; u_char errstr[NGX_MAX_CONF_ERRSTR]; //在本模块的codes的尾部,这里应该算一块新的指令组的头部,增加一个开始回调ngx_http_script_regex_start_code //这里申请的是ngx_http_script_regex_code_t,其第一个成员code为经常被e->ip指向的函数指针,被当做code调用的。 regex = ngx_http_script_start_code(cf->pool, &lcf->codes, sizeof(ngx_http_script_regex_code_t)); if (regex == NULL) { return NGX_CONF_ERROR; } ngx_memzero(regex, sizeof(ngx_http_script_regex_code_t)); value = cf->args->elts; ngx_memzero(&rc, sizeof(ngx_regex_compile_t)); rc.pattern = value[1];//记录 ^(/xyz/aa.*)$ rc.err.len = NGX_MAX_CONF_ERRSTR; rc.err.data = errstr; /* TODO: NGX_REGEX_CASELESS */ //解析正则表达式,填写ngx_http_regex_t结构并返回。正则句柄,命名子模式等都在里面了。 regex->regex = ngx_http_regex_compile(cf, &rc); if (regex->regex == NULL) { return NGX_CONF_ERROR; } //ngx_http_script_regex_start_code函数匹配正则表达式,计算目标字符串长度并分配空间。 //将其设置为第一个code函数,求出目标字符串大小。尾部还有ngx_http_script_regex_end_code regex->code = ngx_http_script_regex_start_code; regex->uri = 1; regex->name = value[1];//记录正则表达式 if (value[2].data[value[2].len - 1] == '?') {//如果目标结果串后面用问好结尾,则nginx不会拷贝参数到后面的 /* the last "?" drops the original arguments */ value[2].len--; } else { regex->add_args = 1;//自动追加参数。 } last = 0; if (ngx_strncmp(value[2].data, "http://", sizeof("http://") - 1) == 0 || ngx_strncmp(value[2].data, "https://", sizeof("https://") - 1) == 0 || ngx_strncmp(value[2].data, "$scheme", sizeof("$scheme") - 1) == 0) {//nginx判断,如果是用http://等开头的rewrite,就代表是垮域重定向。会做302处理。 regex->status = NGX_HTTP_MOVED_TEMPORARILY; regex->redirect = 1;//标记要做302重定向。 last = 1; } if (cf->args->nelts == 4) {//处理后面的参数。 if (ngx_strcmp(value[3].data, "last") == 0) { last = 1; } else if (ngx_strcmp(value[3].data, "break") == 0) { regex->break_cycle = 1;//需要break,这里体现了跟last的区别,参考ngx_http_script_regex_start_code。 //这个标志会影响正则解析成功之后的代码,让其设置了一个url_changed=0,也就骗nginx说,URL没有变化, //你不用重新来跑find config phrase了。不然还得像个新连接一样跑一遍。 last = 1; } else if (ngx_strcmp(value[3].data, "redirect") == 0) { regex->status = NGX_HTTP_MOVED_TEMPORARILY; regex->redirect = 1; last = 1; } else if (ngx_strcmp(value[3].data, "permanent") == 0) { regex->status = NGX_HTTP_MOVED_PERMANENTLY; regex->redirect = 1; last = 1; } else { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"", &value[3]); return NGX_CONF_ERROR; } }
ngx_http_rewrite函数下一步就是要进行脚本解析了,上面的部分解析完正则表达式。脚本解析的数据存放在ngx_http_script_compile_t类型的变量sc里面,然后调用ngx_http_script_compile()函数,这个函数我们在Nginx 脚本引擎解析源码注释 里面看到了,它传入一个字符串,进行编译解析,比如http://$host/aaa.php;将计算长度的lcode放入sc->lengths,计算值的放入sc->values。由于前面初始化的时候设置了sc.values = &lcf->codes;因此ngx_http_script_compile函数会将结果存放在模块的当前location配置结构里面,也就是:ngx_http_rewrite_loc_conf_t:codes,这样在ngx_http_rewrite_handler里面就可以这样取出这个codes了:rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rewrite_module);
最后,为该指令增加一个结束的语法函数ngx_http_script_regex_end_code进行扫尾工作,比如如果需要redirect,设置一下头部header的location,该302了。
另外ngx_http_rewrite函数最后一行也很关键,通过它连接了不同的语句之间的跳转关系,将regex->next设置为下一个codes数组项的位置。这个的用处主要体现在正则表达式的匹配上,ngx_http_script_regex_start_code函数如果调用ngx_http_regex_exec进行正则匹配的时候,当前指令不匹配,那么就需要跳转到下一条指令进行匹配,这个是通过e->ip += code->next;进行的,也就是跳过本指令组的函数,因为一条rewrite指令在codes数组里面占用好几项的。
最后看一下ngx_http_rewrite的余下代码:
ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); sc.cf = cf; sc.source = &value[2];//字符串 http://$http_host/aa.mp4 sc.lengths = ®ex->lengths;//输出参数,里面会包含一些如何求目标字符串长度的函数回调。如上会包含三个: 常量 变量 常量 sc.values = &lcf->codes;//将子模式存入这里。 sc.variables = ngx_http_script_variables_count(&value[2]); sc.main = regex;//这是顶层的表达式,里面包含了lengths等。 sc.complete_lengths = 1; sc.compile_args = !regex->redirect; if (ngx_http_script_compile(&sc) != NGX_OK) { return NGX_CONF_ERROR; } regex = sc.main;//这里这么做的原因是可能上面会改变内存地址。 regex->size = sc.size; regex->args = sc.args; if (sc.variables == 0 && !sc.dup_capture) {//如果没有变量,那就将lengths置空,这样就不用做多余的正则解析而直接进入字符串拷贝codes regex->lengths = NULL; } regex_end = ngx_http_script_add_code(lcf->codes, sizeof(ngx_http_script_regex_end_code_t), ®ex); if (regex_end == NULL) { return NGX_CONF_ERROR; } /*经过上面的处理,后面的rewrite会解析出如下的函数结构: rewrite ^(.*)$ http://$http_host.mp4 break; ngx_http_script_regex_start_code 解析完了正则表达式。根据lengths求出总长度,申请空间。 ngx_http_script_copy_len_code 7 ngx_http_script_copy_var_len_code 18 ngx_http_script_copy_len_code 4 === 29 ngx_http_script_copy_code 拷贝"http://" 到e->buf ngx_http_script_copy_var_code 拷贝"115.28.34.175:8881" ngx_http_script_copy_code 拷贝".mp4" ngx_http_script_regex_end_code */ regex_end->code = ngx_http_script_regex_end_code;//结束回调。对应前面的开始。 regex_end->uri = regex->uri; regex_end->args = regex->args; regex_end->add_args = regex->add_args;//是否添加参数。 regex_end->redirect = regex->redirect; if (last) {//参考上面,如果rewrite 末尾有last,break,等,就不会再次解析后面的数据了,那么,就将code设置为空。 code = ngx_http_script_add_code(lcf->codes, sizeof(uintptr_t), ®ex); if (code == NULL) { return NGX_CONF_ERROR; } *code = NULL; } //下一个解析句柄组的地址。 regex->next = (u_char *) lcf->codes->elts + lcf->codes->nelts - (u_char *) regex; return NGX_CONF_OK; }
解析完成之后,在当前的location 上下文中,ngx_http_rewrite_loc_conf_t:codes成员变量是个数组,记录了当前location的所有语法函数列表,这些函数就组成了nginx rewrite,if,break等语句的解析方法:依次调用这些函数就行;
比如rewrite语句:rewrite ^(/xyz.*)$ http://$http_host/kulv.php break; 语法解析后的codes将增加如下几项:
ngx_http_script_regex_start_code | 正则匹配,如果成功,用regex->lengths语法计算目标字符串长度,否则移动e->ip进入下一条指令的的语法函数组:e->ip += code->next; |
ngx_http_script_copy_code | 拷贝简单字符串:http:// |
ngx_http_script_copy_var_code | 拷贝变量:$http_host,变量从ngx_http_script_var_code_t:index中取得在cmcf->variables中的下标。 |
ngx_http_script_copy_code | 拷贝简单字符串:/kulv.php |
ngx_http_script_regex_end_code | 检查是否是重定向,如果是旧302 |
其实后面的目标字符串http://$http_host/kulv.php 在nginx中叫做复杂表达式。ngx_http_complex_value函数可以搞定它,根据val复杂表达式结构,获取其代表的目标值,存入value.
其关系如下图所示。
总之,一句话描述rewrite就是:在配置解析阶段,通过解析rewrite指令的参数建立对应的语法函数列表,设置到location的codes数组上;在处理过程中ngx_http_rewrite_handler函数依次调用这些codes来解析对应的语句,拼接出rewrite后的目标URL,然后进行重定向;
二、if 指令解析
在ngx_conf_parse碰到if指令时,会调用ngx_http_rewrite_if函数进行解析。if指令比rewrite指令稍微复杂一点。为了篇幅我们再下一篇文章介绍吧。
近期评论