- version: for lighttpd 1.4.26
需求描述
mod_header_access 模块用来删除 lighttpd 处理过程中获取和产生的 request 和 response 的头信息中指定的内容,例如:
Cache-Control: no-cache
通过修改 headers 信息,从而可以影响后续模块如 mod_cache/mod_proxy 的处理行为。
request 指客户端(浏览器)发送至 lighttpd 的请求,response 指从后端 backend 返回给 lighttpd 的应答——例如 mod_proxy 从后端 apache 返回的响应,不包括对 lighttpd 返回给客户端的响应 headers 的处理。
对 headers 的删除操作应该包括对值的检查,例如,Cache-Control
符合 no-cache
和 max-age=0
时应该删除,而其他的值则不执行。需要能够对这些值进行配置。
要求模块在删除时进行检查,确保对一下关键的 header,如 Host
不会进行删除操作导致不能正常提供服务。
需求挖掘和设计
数据处理流程结构
通过对需求的分析,可以得到如下的关系图:
图中 m1 即是我们需要添加的模块:mod_header_access,其 hook 点设置的目的,就是为了在删除某些头信息之后,影响后面的模块的 hook 点从处理功能。例如,对 Cache-Control 为 no-cache 的情况,m1 为 mod_header_access,m2 为 mod_cache,则 m1 在 hook 1 位置删除该头信息后就会改变 m2 在 hook 1 位置的处理方式。
关于 hook 点位置选择,需要参考 lighttpd 的状态机并考虑对其他模块的影响。请跳到 hook 点位置选择。
配置项
对于值的检查和配置,相应的配置文件 lighttpd.conf 选项如下:
header_access.del-request-header = ("Cache-Control" => ("no-cache", "max-age=0")) header_access.del-response-header = ("Cache-Control" => ("no-cache", "max-age=0")) header_access.debug = "enable"
这需要在模块的初始化配置点 handle_set_defaults()
中进行相应的解析,并在这个 plugin 特定的 plugin_config 配置中组织一个二维数组(array)。
headers 数组操作
关键的数据结构为 con->request.headers
和 con->response.headers
,这是两个 array *
结构的数组。
(完整的数据结构关系图后续补充,目前时间实在是太过于紧张...)
自然,对 con->request.headers 和 con->response.headers 需要进行删除数组元素的操作。但 array.c
中并未提供相关的 array_remove_element()
函数,所以我们需要增加一个。算法设计如下:
首先看看操作前的数组结构示例:
这里 data
在插入时只进行 append 操作,为了保证排序,作者使用了一个 sorted
数组,每个元素的值就是 data 中对应元素的下标,在插入时即进行排序,保证 sorted 的顺序,也就保证了 array 的顺序。
删除为 key 的元素时(图中的 data_unset y),找到该元素在 data 和 sorted 中对应的位置 idx 和 pos(array.c 中已提供 array_get_index
函数),并找到 data 末尾元素(data[i_tail], i_tail = array->used - 1)在 sorted 中的位置 p_tail。然后,分别将 data[idx] 和 data[i_tail] 的内容交换,将 sorted[pos] 和 sorted[p_tail] 的内容交换。交换后的结构如下:
最后,将 sorted pos 后的元素全部执行一次 memmove()
操作,因为 sorted 中只包含指针,所以移动的开销应该是可以接受的。
另外,原来有一个 array_pop()
函数,应该是有 BUG 的,因为其在进行 a->used-- 后并没有相应修改 a->sorted 数组!
hook 点位置选择
在 mod_header_access 中编写的函数需要在 mod_header_access_plugin_init()
阶段挂载到对应 plugin 的 hook 回调函数上,但具体应该使用哪个 hook 点,需要对 lighttpd 的状态机和相关模块如 mod_cache 和 mod_proxy 的关系进行梳理。
lighttpd 状态机的变化如下图所示:
在每个状态,有 0 到多个 hook 点,关系如下表所示(以 mod_cache 为例):
STATE hook_points mod_cache ======================================================================= network_init(): plugins_call_init: mod_cache_init plugins_call_set_defaults: mod_cache_set_defaults fd_event_poll(): plugins_call_handle_trigger: (NULL) plugins_call_connection_reset: mod_cache_cleanup ----------------------------------- state: connect plugins_call_handle_joblist: (NULL) state: req-start [NULL] state: read [NULL] state: req-end [NULL] state: handle-req plugins_call_handle_uri_raw: (NULL) plugins_call_handle_uri_clean: mod_cache_uri_handler plugins_call_handle_docroot: mod_cache_docroot_handler plugins_call_handle_physical: (NULL) plugins_call_handle_subrequest: mod_cache_handle_memory_storage plugins_call_handle_subrequest_start: (NULL) state: resp-start plugins_call_handle_response_start: mod_cache_handle_response_start state: write plugins_call_handle_response_filter: mod_cache_handle_response_filter state: resp-end plugins_call_handle_request_done: (NULL) ----------------------------------- state: req-start plugins_call_connection_reset: mod_cache_cleanup state: read plugins_call_handle_joblist: (NULL) state: error plugins_call_connection_reset: state: close [NULL]
将几个关键的模块合并在一起考虑,可以得到如下关系表:
plugin \ hook | uri_raw | uri_clean | ... | response_start | response_filter |
---|---|---|---|---|---|
mod_setenv | ... | ||||
mod_header_access | ? | DEL_REQ_HEADER | ... | DEL_RESP_HEADER | ? |
mod_cache | ..affected.. | ... | ..affected.. | ||
mod_proxy | ..affected.. | ... | ..affected.. |
因为 delele header 的目的是为了改变 mod_cache/mod_proxy 的处理方式,所以配置的模块的加载顺序也应该如上所示,将 mod_header_access 放在 mod_cache/mod_proxy 之前。否则,mod_header_access 的 hook 就必须提前,例如,放到 handle_uri_raw 中,但这样一来,mod_setenv 在 handle_uri_clean 部分又可能会设置增加一些头信息,导致 delete 失效。
mod_cache/mod_proxy 对 request 的处理都在 handle_uri_clean,所以 mod_header_access 放在 handle_uri_clean 处最为合理,可以防止中间其他模块加入的头信息的影响(如 mod_setenv ——后续可以考虑增加设置选项是否覆盖其他模块)。
对 response 处理,放在 handle_response_start 点。因为根据对 lighttpd 状态机的分析,在这个 hook 点的所有模块的回调函数都结束后,会调用 http_response_write_header()
函数将 con->response.headers
数组的内容写到一个缓冲区 con->output_queue
,此后的 hook 点都不会再修改这个缓冲区,而最终写到网络 socket 中的内容,都从这个缓冲区来;同时,我们也不需要修改发送给客户端浏览器的头信息,所以不需要再在后面的 hook 点做处理。调用关系如下:
state:resp-start connections.c:connection_state_matchine connections.c:connection_handle_write_prepare plugin.c:plugins_call_handle_response_start mod_header_access->handle_response_start (**) mod_cache->handle_response_start mod_proxy->handle_response_start response.c:http_response_write_header /* "con->response.headers" written to "con->output_queue") */ plugin.c:plugins_call_handle_response_filter ... state:write connections.c:connection_handle_write network.c:network_write_chunkqueue(con->output_queue) ......