这个模块根据请求的 'User-Agent' 包头信息给指定变量赋值,具体使用方法参见 github[1]。

其实,官方文档并不太容易看明白,Tengine 官网对此模块的说明文档[2] 对此模块描述的更好一点。

但是看完两个文档,对其中的使用示例仍存疑问:

http {
    user_agent $ngx_browser {
        default                     unknown;

        greedy                      Firefox;

        Chrome      18.0+           chrome18;
        Chrome      17.0~17.9999    chrome17;
        Chrome      5.0-            chrome_low;
    }
}

这个例子使用了此模块 user_agent 指令定义了变量 $ngx_browser,变量的值由 请求的 User-Agent 包头字段和上面的配置规则计算而来。例如,User-Agent 中 含有 Chrome/19.0 字样时,$ngx_browser 会被赋值为 chrome18。但是,greedy 指令指定的 Firefox 有何用处呢?

文档 [2] 对 greedy 指令的说明如下:

greedy keyword

If the keyword is greedy, it will continue to scan the user-agent string until it can find other item which is not greedy. If it can't find any other item, this keyword will be returned as last.

实际上,经过测试和分析模块代码得知,示例中的 greedy 指令没有任何作用!因为 Firefox 作为关健字没有对应任何值,所以,正确的用法是:

http {
    user_agent $ngx_browser {
        default                     unknown;

        greedy                      Firefox;

        Firefox                     firefox; # 也可以指定具体版本号
        Chrome      18.0+           chrome18;
        Chrome      17.0~17.9999    chrome17;
        Chrome      5.0-            chrome_low;
    }
}

另外,还有一个情形文档并没有说明。如果有多个 greedy 指令,比如,我们又添加了 一条 greedy 指令,配置示例如下:

http {
    user_agent $ngx_browser {
        default                     unknown;

        greedy                      Firefox;
        greedy                      Chrome;

        Firefox                     firefox; # 也可以指定具体版本号
        Chrome      18.0+           chrome18;
        Chrome      17.0~17.9999    chrome17;
        Chrome      5.0-            chrome_low;
    }
}

在请求的 "User-Agent" 值为 .... Chrome/18.1 Firefox/39.0 时,最终变量 $ngx_browser 的值是 firefox 还是 chrome18 呢?

实际测试得知,$ngx_browser 最终为 chrome18,这种情况在文档 [2] 对 greedy 指令描述中并无说明:文档里说,设置为 greedy 的 keyword,在没有 non-greedy keyword 出现的情况下,会被返回。但是文档里没有描述,两个 greedy keyword 都出现在 "User-Agent" 字段值中时,返回第一个匹配上的 keyword 还是返回第二个;

User-Agent

"User-Agent" 的格式和发展历史可参见百度百科Wikipedia 上的介绍:

... most Web browsers use a User-Agent value as follows: Mozilla/[version] ([system and browser information]) [platform] ([platform details]) [extensions].

从实例,如 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36 中也可以看到,此模块关注的关键字都出现在 "User-Agent" 的 extensions 部分,即 整个字符串的后部,这也就解释了方档 [1] 对模块的匹配方式的描述:

... It scans the string in a reverse order...

Aho-Corasick

最终对这个模块感兴趣,就是因为它实现了 Nginx 版本的 Trie 树。 翻阅代码后发现,准确的说,ngx_trie.[c|h] 实现的是 Aho-Corasick Algorithm, 这是一个 多模式字符串匹配算法,用到本模块工作的场景里也算是相当合适的。

对 Aho-Corasick Algorithm 算法的解释和 ngx_trie 的实现,这里就不再赘述了。下 面列举几个相关资源:

自定义 "block" 配置

模块的配置方式和 Nginx 标准模块 Geo 很相似:定义一个变量;变量根据 "{}" 中的规则和当前请求信息取值;"{}" 中的指令 可以使用 include 指令从其它文件中引入。

本节对本模块如何实现 "block" 配置和 include 指令的工作原理进行分析。

模块提供 user_agent 指令标识 "block" 配置的开始,该指定定义如下:

{ ngx_string("user_agent"),
  NGX_HTTP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE1,
  ngx_http_user_agent_block,
  NGX_HTTP_MAIN_CONF_OFFSET,
  0,
  NULL },

其中,NGX_HTTP_MAIN_CONF 表示该指令只能出现在的 http {} 上下文中; NGX_CONF_BLOCK 表示该指令定义了一个 {} 上下文即 block 配置区;同时, NGX_CONF_TAKE1 表示该指令接收一个参数即我们要定义的变量;NGX_HTTP_MAIN_CONF_OFFSET 表示此模块的配置结构体位于 http {} 上下文的 main_conf 数组中 (参见 配置文件解析)。

Nginx 在解析配置文件过程中,如果碰到 user_agent 字样,它会调用函数 ngx_http_user_agent_block 对后面的参数和 "block" 配置区完成进一步的解析:

/* ngx_http_user_agent_block(cf, cmd, conf) */
ngx_str_t                   *value, name;
ngx_conf_t                  *save;
ngx_http_user_agent_ctx_t   *ctx;

...
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_user_agent_ctx_t));
...
ctx->pool = cf->pool;
ctx->trie = ngx_trie_create(ctx->pool);
ctx->default_value = NULL;
...
save = *cf;
cf->ctx = ctx;
cf->handler = ngx_http_user_agent;
cf->handler_conf = conf;

rv = ngx_conf_parse(cf, NULL);

*cf = save;

对上述代码的补充说明:

  • conf 参数一般指向模块提供的 ngx_http_XXX_create_[main|srv|loc]_conf 等 函数创建的模块配置结构体。由于本模块并未使用这种机制保存配置信息,所以该参数 值为 NULL

  • 在开始解析 "block" 配置区之前,需要保存当前配置解析器的上下文到 save 变量 中,以便解析完毕后恢复上下文;

  • 模块定义的 "block" 配置区无法使用 Nginx 提供的对 http {}, main {} 等内建 "block" 配置区的解析方式完成解析,所以需要模块提供自定义解析函数。在本模块中, 自定义函数是 ngx_http_user_agent,它在 ngx_conf_parse 函数中被调用:

    /* ngx_conf_parse(cf, filename) */
    ...
    for ( ;; ) {
        rc = ngx_conf_read_token(cf);
        ...
        if (cf->handler) {
            /*
             * the custom handler, i.e., that is used in the http's
             * "types { ... }" directive
             */
            ...
            rv = (*cf->handler)(cf, NULL, cf->handler_conf);
            ...
        }
        ...
    }
    

ngx_http_user_agent 函数随后开始对 {} 中的配置解析进行分析 (每次调用处理 一条配置指令):

/* ngx_http_user_agent(cf, cmd, conf) */
ngx_str_t                       *args, *name, file;
...
ngx_trie_t                      *trie;
ngx_http_user_agent_ctx_t       *ctx;
...
ctx = cf->ctx;
trie = ctx->trie;

args = cf->args->elts;
nelts = cf->args->nelts;

...
if (nelts == 2) {
    if (ngx_strcmp(args[0].data, "include") == 0) {

        file = args[1];
        ngx_conf_full_name(cf->cycle, &file, 1);
        ...
        return ngx_conf_parse(cf, &file);
    }
    ...
    if (ngx_strcmp(args[0].data, "greedy") == 0) {
        mode = NGX_TRIE_REVERSE | NGX_TRIE_CONTINUE;
        trie->insert(trie, args + 1, mode);

        return NGX_CONF_OK;
    }
}

if (nelts == 2) {
    /* directives like "Chrome chrome13" */
    ...
}

if (nelts == 3) {
    /* directives like "Chrome 18.0+ chrome18" */
    ...
}

对上述代码的补充说明:

  • cf->ctxngx_http_user_agent_block 函数中被赋值;

  • include 指令引用的配置文件在当前上下文中解析,ngx_conf_parse 函数此时会 先打开该配置文件,然后再次调用 ngx_http_user_agent 函数进行处理:

    /* ngx_conf_parse(cf, filename) */
    if (filename) {
    
        /* open configuration file */
    
        fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
        ...
        prev = cf->conf_file;
    
        cf->conf_file = &conf_file;
        ...
        cf->conf_file->file.fd = fd;
        ...
    }
    
    for ( ;; ) {
        rc = ngx_conf_read_token(cf);
        ...
        if (cf->handler) {
            ...
            rv = (*cf->handler)(cf, NULL, cf->handler_conf);
            ...
        }
        ...
    }
    
    if (filename) {
        ...
        ngx_close_file(fd);
        cf->conf_file = prev;
    }
    

至此,自定义 "block" 配置域的解析工作就完成了。

自定义变量

本模块的主要功能就是提供一个自定义变量,在配置文件的其它地方可以根据该变量的值 完成一些比如速度限定、返回不同的响应数据等功能。

关于 Nginx 变量的使用和实现原理,我们在 配置变量 一文中已经做过了分析,下面来分析一下,本模块作为一个第三方模块,是如何定义变量 和对变量取值的。

变量的定义在 ngx_http_user_agent_block 函数中完成:

/* ngx_http_user_agent_block */
...
ngx_http_variable_t         *var;
ngx_http_user_agent_ctx_t   *ctx;
...
name = value[1];
name.data++;
name.len--;

var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE);
...
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_user_agent_ctx_t));

ctx->pool = cf->pool;
ctx->trie = ngx_trie_create(ctx->pool);
ctx->default_value = NULL;

var->get_handler = ngx_http_user_agent_variable;
var->data = (uintptr_t) ctx;

对上述代码的补充说明:

  • value[1] 是含有 $ 字符的变量名称,在将其注册到变量引擎前,需要去掉 $ (个人觉得这里最好对 $ 符号是否存在进行检查 - 参考 rewrite 模块的 set 指 令);

  • NGX_HTTP_VAR_CHANGEABLE 标志位表示这个变量可以被重复定义,比如,可以在配置 文件中再次使用 user_agent 指令,甚至使用 set 指令对此变量重新定义;

  • 对变量 (ngx_http_variable_t) 设置 get_handlerset_handler (NULL) 和 关联数据 data

在运行时,需要对此变量取值时,Nginx 调用 get_handler,即 ngx_http_user_agent_variable 函数计算变量值:

static ngx_int_t
ngx_http_user_agent_variable(ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data)
{
    ...
    ngx_array_t                     *value;
    ngx_http_user_agent_ctx_t       *uacf;
    ...
    uacf = (ngx_http_user_agent_ctx_t *) data;
    trie = uacf->trie;

    if (r->headers_in.user_agent == NULL) {
        goto end;
    }

    user_agent = &(r->headers_in.user_agent->value);

    value = trie->query(trie, user_agent, &pos, NGX_TRIE_REVERSE);
    ...
        *v = *(array[i].var);
        return NGX_OK;
    ...
    *v = *uacf->default_value;
    return NGX_OK;
}

对上述代码的补充说明:

  • get_handler 负责对其对应的变量求值,变量值最终存储于 ngx_http_variable_value_t 类型的变量 v 中;

  • 由于每个 "keyword" 对应的变量值是固定的,这些变量值事先转换成 ngx_http_variable_value_t 类型变量,并存放于 ngx_http_user_agent_interval_t::var 中;

    interval->var = ngx_pcalloc(ctx->pool, sizeof(ngx_http_variable_value_t));
    ...
    interval->var->len = args[1].len;
    interval->var->data = args[1].data;
    
    interval->var->not_found = 0;
    interval->var->n_cacheable = 0;
    interval->var->valid = 1;
    

其它细节

版本号是如何存储的

本模块支持四种形式的版本号设定:version+, version-, versionversion~version。这四种版本号都由 ngx_http_user_agent_get_version 函数识 别并存储至 ngx_http_user_agent_interval_t 结构体中:

typedef struct {
    uint64_t                    left;
    uint64_t                    right;

    ngx_http_variable_value_t   *var;
} ngx_http_user_agent_interval_t;

同时,因为一个 "keyword" 可以对应多个版本号设置,例如上文中的配置示例:

Chrome      18.0+           chrome18;
Chrome      17.0~17.9999    chrome17;
Chrome      5.0-            chrome_low;

ngx_trie 中又使用 "keyword" 作为唯一关键字,所以多个版本号及其对应的配置 存储于数组 (ngx_array_t) 中,并作为这个 "keyword" 在 ngx_trie 中的 "value":

/* ngx_http_user_agent */

ngx_array_t             *value;
ngx_trie_node_t         *node;

...
node = trie->insert(trie, name, mode);
...
value = (ngx_array_t *) node->value;
...
p = (ngx_http_user_agent_interval_t *) ngx_array_push(value);
...
node->value = (void *) value;

当然,同一个 "keyword" 对应的多个版本号是不允许重叠的。在每次解析完新的指令行 后,将新解析的版本号和同一 "keyword" 的其它版本号进行检验 (left, right)。

下面再来看一下,运行时对 user_agent 定义的变量取值时,是如何对版本号进行匹配 的:

/* ngx_http_user_agent_variable */

ngx_array_t                     *value;
ngx_http_user_agent_interva_t   *array;
...

value = trie->query(trie, user_agent, &pos, NGX_TRIE_REVERSE);
...
array = value->elts;
n = value->nelts;

for (i = 0; i < n; i++) {
    if (version >= array[i].left && version <= array[i].right) {
        *v = *v(array[i].var);
        return NGX_OK;
    }
}
...
Comments

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

comments powered by Disqus

Published

Category

Nginx

Tags

Contact