修改nginx源码以删除或自定义 "Server" Header
分类: web 标签: nginx, web 发布于: 2023-03-14

边缘计算等对内存极其敏感的场景下,在进行HTTP通信时往往需要移除一些不必要内容以节约内存。使用nginx正向代理时,它会添加默认的Server Header,即使在nginx.conf中指定了server_tokens off;,也仅仅是隐藏版本号,并未去除这个Header。

隐藏Header

这部分的逻辑在 src/http/ngx_http_header_filter_module.c 中定义:

...
static u_char ngx_http_server_string[] = "Server: nginx" CRLF;
static u_char ngx_http_server_full_string[] = "Server: " NGINX_VER CRLF;
static u_char ngx_http_server_build_string[] = "Server: " NGINX_VER_BUILD CRLF;
...

static ngx_int_t
ngx_http_header_filter(ngx_http_request_t *r)
{
   ...
    if (r->headers_out.server == NULL) {
        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
            len += sizeof(ngx_http_server_full_string) - 1;

        } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) {
            len += sizeof(ngx_http_server_build_string) - 1;

        } else {
            len += sizeof(ngx_http_server_string) - 1;
        }
    }
    ...
    b = ngx_create_temp_buf(r->pool, len);
    ...
    if (r->headers_out.server == NULL) {
        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
            p = ngx_http_server_full_string;
            len = sizeof(ngx_http_server_full_string) - 1;

        } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) {
            p = ngx_http_server_build_string;
            len = sizeof(ngx_http_server_build_string) - 1;

        } else {
            p = ngx_http_server_string;
            len = sizeof(ngx_http_server_string) - 1;
        }

        b->last = ngx_cpymem(b->last, p, len);
    }

可以得知,在处理header时,nginx先计算header部分的内存大小,再申请一块buffer,然后进行填充,所以对headers_out.server进行了两次判断。可以对上述两个if判断块进行注释,这样在请求时就不会有server字段:

~/nginx # curl -I localhost/

HTTP/1.1 200 OK
Date: Wed, 15 Mar 2023 03:10:25 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 15 Mar 2023 02:55:32 GMT
Connection: keep-alive
ETag: "641133a4-267"
Accept-Ranges: bytes

使其可配置

ngx_http_header_filter可以看到clcf结构体,类型为ngx_http_core_loc_conf_t,其为conf文件中http部分配置的解析逻辑:

struct ngx_http_core_loc_conf_s {
    ngx_str_t     name;          /* location name */
    ngx_str_t     escaped_name;

#if (NGX_PCRE)
    ngx_http_regex_t  *regex;
#endif
    ...
    ngx_http_location_tree_node_t   *static_locations;
#if (NGX_PCRE)
    ngx_http_core_loc_conf_t       **regex_locations;
#endif
    ...

实现自定义server name,可以把配置添加到这个块当中:

    // add
    ngx_str_t     customized_server_name;

同时要处理解析配置文件的地方,nginx从这里加载配置,并等待event,在event handler当中根据addr:port等参数寻找server的配置,并组装成request对象。conf文件解析的主要逻辑在ngx_conf_handler等函数当中,这里遍历每个ngx_command数组,每个元素对应一条解析项,配置的解析根据其属性进行对应的处理:

struct ngx_command_s {
    ngx_str_t             name;
    ngx_uint_t            type;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    void                 *post;
};

其中,其中,name是配置项名称,type决定这个配置项可以在哪些块,nginx使用位标记来标识,set则为设置配置的回调方法。conf用于指示配置项所处内存的相对偏移位置,offset表示当前配置项在整个存储配置项的结构体中的偏移位置。由此,可以在src/http/ngx_http_core_module.c中定义自定义的command:

static ngx_command_t  ngx_http_core_commands[] = {
    ...
    
    // add
    { ngx_string("customized_server_name"),
      NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_core_loc_conf_t, customized_server_name),
      NULL },
    ...
    ngx_null_command
}

其中ngx_conf_set_str_slot是nginx预置的回调函数。若在多个域同时出现配置项时,nginx需要根据某些优先规则确定使用那个配置,nginx实现了一个merge机制来处理这个逻辑,对于没有merge函数的配置,最终不会生效。http域的merge定义在此文件ngx_http_core_merge_loc_conf函数中,在这里优先取子层存在的配置,若无则取上层的,否则取默认。可以添加上自定义配置项的merge函数:

static char *
ngx_http_core_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_core_loc_conf_t *prev = parent;
    ngx_http_core_loc_conf_t *conf = child;
    ...
    // add
    ngx_conf_merge_str_value(conf->customized_server_name,
                             prev->customized_server_name, "undefined");
    ...
}

这样一来,就可以在ngx_http_header_filter当中获取到此配置项,修改这两处的逻辑:

static ngx_int_t
ngx_http_header_filter(ngx_http_request_t *r)
{
    ...
    // modify
    if (r->headers_out.server == NULL) {
        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
            len += sizeof("Server: ") - 1 + clcf->customized_server_name.len + 2;
        }
    }
    ...
    b = ngx_create_temp_buf(r->pool, len);
    ...
    
    // modify
    if (r->headers_out.server == NULL) {
        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
            p = clcf->customized_server_name.data;
            len = clcf->customized_server_name.len;

            b->last = ngx_cpymem(b->last, "Server: ", sizeof("Server: ") - 1);
            b->last = ngx_cpymem(b->last, p, len);
            *b->last++ = CR; *b->last++ = LF;
        }
    }
    ...

需要注意len += sizeof("Server: ") - 1 + clcf->customized_server_name.len + 2;计算长度时,sizeof会包含字符串结尾的\0,拷贝到buffer的内容不包含这部分,故需减一。每条Header结束的\r\n占用2字节,故在末尾加2。

编译运行,并在nginx.conf中添加:

...
http {
    customized_server_name jjcjj;
    
    include       mime.types;
    default_type  application/octet-stream;
    ...

启动后,可以看到自定义的Server已经生效:

~/nginx # curl -I localhost/

HTTP/1.1 200 OK
Server: jjcjj
Date: Wed, 15 Mar 2023 03:10:25 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 15 Mar 2023 02:55:32 GMT
Connection: keep-alive
ETag: "641133a4-267"
Accept-Ranges: bytes

当未指定customized_server_name时,Server会返回默认值undefined;添加server_tokens off则不会返回Server Header,效果同上文所述。