这里有一些关于一般教学风格的说明。为了简洁起见,这里的所有结构声明都是不完整的——真正的声明有更多槽位,但我没有告诉你。在大多数情况下,这些槽位被保留给服务器核心中的一个组件或另一个组件,并且应该谨慎地由模块修改。但是,在某些情况下,它们确实是我还没有完成的事情。欢迎来到前沿。
最后,这里有一个提纲,让您对即将出现的内容以及顺序有一个基本的了解。
SetEnv
,它们并不适合其他地方。OK
来表明它已完成。DECLINED
。在这种情况下,服务器的行为在所有方面都与处理程序根本不存在一样。*/*
(即,通配符 MIME 类型规范)。但是,只有在服务器已经尝试过并且无法为请求对象的 MIME 类型找到更具体的响应处理程序(要么不存在,要么它们都拒绝)时,才会调用通配符处理程序。处理程序本身是单个参数(一个request_rec
结构,见下文)的函数,它返回一个整数,如上所述。
ScriptAlias
配置文件命令。它实际上比大多数模块复杂得多,但如果我们只有一个例子,它最好是那个到处插手的例子。让我们从处理程序开始。为了处理 CGI 脚本,该模块为它们声明了一个响应处理程序。由于ScriptAlias
,它还具有名称转换阶段的处理程序(以识别ScriptAlias
ed URI),类型检查阶段(任何ScriptAlias
ed 请求都被类型化为 CGI 脚本)。
该模块需要维护一些每个(虚拟)服务器信息,即生效的ScriptAlias
;因此,模块结构包含指向构建这些结构的函数的指针,以及指向组合其中两个结构的另一个函数的指针(如果主服务器和虚拟服务器都声明了ScriptAlias
)。
最后,该模块包含处理ScriptAlias
命令本身的代码。这个特定的模块只声明了一个命令,但可能还有更多,因此模块具有命令表,它们声明它们的命令,并描述它们在何处被允许以及如何调用它们。
关于这些命令中一些参数的声明类型的最后说明:一个pool
是指向资源池结构的指针;这些由服务器用来跟踪已分配的内存、打开的文件等,要么用于服务特定请求,要么用于处理配置自身的过程。这样,当请求结束时(或者,对于配置池,当服务器重新启动时),内存可以被释放,文件可以被关闭,一次性完成,而无需任何人编写显式代码来跟踪所有这些文件并处置它们。此外,一个cmd_parms
结构包含有关正在读取的配置文件的各种信息以及其他状态信息,这些信息有时对处理配置文件命令(例如ScriptAlias
)的函数有用。无需赘述,模块本身
/* Declarations of handlers. */ int translate_scriptalias (request_rec *); int type_scriptalias (request_rec *); int cgi_handler (request_rec *); /* Subsdiary dispatch table for response-phase handlers, by MIME type */ handler_rec cgi_handlers[] = { { "application/x-httpd-cgi", cgi_handler }, { NULL } }; /* Declarations of routines to manipulate the module's configuration * info. Note that these are returned, and passed in, as void *'s; * the server core keeps track of them, but it doesn't, and can't, * know their internal structure. */ void *make_cgi_server_config (pool *); void *merge_cgi_server_config (pool *, void *, void *); /* Declarations of routines to handle config-file commands */ char *script_alias (cmd_parms *, void *per_dir_config, char *fake, char *real); command_rec cgi_cmds[] = { { "ScriptAlias", script_alias, NULL, RSRC_CONF, TAKE2, "a fakename and a realname"}, { NULL } }; module cgi_module = { STANDARD_MODULE_STUFF, NULL, /* initializer */ NULL, /* dir config creater */ NULL, /* dir merger --- default is to override */ make_cgi_server_config, /* server config */ merge_cgi_server_config, /* merge server config */ cgi_cmds, /* command table */ cgi_handlers, /* handlers */ translate_scriptalias, /* filename translation */ NULL, /* check_user_id */ NULL, /* check auth */ NULL, /* check access */ type_scriptalias, /* type_checker */ NULL, /* fixups */ NULL /* logger */ };
request_rec
结构。该结构描述了代表客户端向服务器发出的特定请求。在大多数情况下,每个与客户端的连接只生成一个request_rec
结构。
request_rec
简要介绍request_rec
包含指向资源池的指针,该资源池将在服务器完成处理请求时被清除;指向包含每个服务器和每个连接信息的结构的指针,最重要的是,关于请求本身的信息。最重要的此类信息是一组小的字符字符串,描述了正在请求的对象的属性,包括它的 URI、文件名、内容类型和内容编码(这些分别由处理请求的转换和类型检查处理程序填充)。
其他常用的数据项是表格,这些表格给出客户端原始请求的 MIME 标头、将与 pppp 响应一起发送回的 MIME 标头(模块可以随意添加),以及在服务请求过程中产生的任何子进程的环境变量。这些表格使用table_get
和table_set
例程进行操作。
最后,还有指向两个数据结构的指针,这些数据结构反过来又指向每个模块的配置结构。具体来说,这些结构保存指向模块构建的数据结构的指针,这些数据结构描述了它在给定目录中(通过.htaccess
文件或<Directory>
部分)配置运行的方式,以及它在服务请求过程中构建的私有数据(因此模块的处理程序在一个阶段可以将“笔记”传递给它们在其他阶段的处理程序)。在request_rec
指向的server_rec
数据结构中还有另一个这样的配置向量,它包含每个(虚拟)服务器的配置数据。
这是一个简化的声明,给出了最常用的字段
struct request_rec { pool *pool; conn_rec *connection; server_rec *server; /* What object is being requested */ char *uri; char *filename; char *path_info; char *args; /* QUERY_ARGS, if any */ struct stat finfo; /* Set by server core; * st_mode set to zero if no such file */ char *content_type; char *content_encoding; /* MIME header environments, in and out. Also, an array containing * environment variables to be passed to subprocesses, so people can * write modules to add to that environment. * * The difference between headers_out and err_headers_out is that the * latter are printed even on error, and persist across internal redirects * (so the headers printed for ErrorDocument handlers will have them). */ table *headers_in; table *headers_out; table *err_headers_out; table *subprocess_env; /* Info about the request itself... */ int header_only; /* HEAD request, as opposed to GET */ char *protocol; /* Protocol, as given to us, or HTTP/0.9 */ char *method; /* GET, HEAD, POST, etc. */ int method_number; /* M_GET, M_POST, etc. */ /* Info for logging */ char *the_request; int bytes_sent; /* A flag which modules can set, to indicate that the data being * returned is volatile, and clients should be told not to cache it. */ int no_cache; /* Various other config info which may change with .htaccess files * These are config vectors, with one void* pointer for each module * (the thing pointed to being the module's business). */ void *per_dir_config; /* Options set in config files, etc. */ void *request_config; /* Notes on *this* request */ };
request_rec
结构的来源request_rec
结构都是通过从客户端读取 HTTP 请求并填充字段来构建的。但是,有一些例外*.var
文件)或返回本地“Location:”的 CGI 脚本,那么用户请求的资源最终将通过与客户端最初提供的 URI 不同的 URI 来定位。在这种情况下,服务器执行内部重定向,为新的 URI 构建一个新的request_rec
,并几乎完全像客户端直接请求新的 URI 一样处理它。
ErrorDocument
在范围内,则相同的内部重定向机制就会发挥作用。
此类处理程序可以使用sub_req_lookup_file
和sub_req_lookup_uri
函数构建子请求;这会构建一个新的request_rec
结构并按预期处理它,直到但不包括实际发送响应的点。(如果子请求是针对与原始请求相同的目录中的文件,则这些函数会跳过访问检查)。
(服务器端包含通过构建子请求,然后通过run_sub_request
函数实际调用它们的响应处理程序来工作)。
request_rec
时,都必须返回一个int
来指示发生了什么。这可以是REDIRECT
,那么模块应该在请求的headers_out
中放置一个Location
,以指示客户端应该重定向到哪里。
request_rec
结构中的几个字段来完成工作(或者,对于访问检查器,只是通过返回正确的错误代码)。但是,响应处理程序必须实际将请求发送回客户端。它们应该首先使用send_http_header
函数发送 HTTP 响应标头。(您不必执行任何特殊操作来跳过为 HTTP/0.9 请求发送标头;该函数会自行确定它不应该执行任何操作)。如果请求被标记为header_only
,那么它们应该做的就是这些;它们应该在之后返回,而不尝试任何进一步的输出。
否则,它们应该生成一个请求主体,该主体以适当的方式响应客户端。用于此的原语是rputc
和rprintf
,用于内部生成的输出,以及send_fd
,用于将某个FILE *
的内容直接复制到客户端。
最后要考虑的一点是:在对客户端执行 I/O 时,可能会出现无限延迟。因此,在启动对客户端的 I/O 之前,务必设置超时。
此时,您应该或多或少地理解以下代码片段,它是处理没有更具体处理程序的GET
请求的处理程序;它还展示了如何处理条件GET
,如果在特定响应处理程序中这样做是可取的。(pfopen
和pfclose
函数将返回的FILE *
绑定到资源池机制,因此即使请求被中止,它也会被关闭)。
int default_handler (request_rec *r) { int errstatus; FILE *f; if (r->method_number != M_GET) return DECLINED; if (r->finfo.st_mode == 0) return NOT_FOUND; if ((errstatus = set_content_length (r, r->finfo.st_size)) || (errstatus = set_last_modified (r, r->finfo.st_mtime))) return errstatus; f = pfopen (r->pool, r->filename, "r"); if (f == NULL) { log_reason("file permissions deny server access", r->filename, r); return FORBIDDEN; } register_timeout ("send", r); send_http_header (r); if (!r->header_only) { send_fd (f, r); } kill_timeout(r); pfclose (r->pool, f); return OK; }最后,如果所有这些都太具有挑战性,那么有几种方法可以解决。首先,如上所示,尚未生成任何输出的响应处理程序可以简单地返回一个错误代码,在这种情况下,服务器将自动生成错误响应。其次,它可以通过调用
internal_redirect
来转交给其他处理程序,这就是上面讨论的内部重定向机制的调用方式。内部重定向的响应处理程序应该始终返回OK
。(从不是响应处理程序的处理程序中调用internal_redirect
会导致严重的混淆)。
auth_type
、auth_name
和requires
。get_basic_auth_pw
,它会自动设置connection->user
结构字段,以及note_basic_auth_failure
,它会安排发送回适当的WWW-Authenticate:
标头)。request_rec
结构列表中来处理这个问题,这些结构通过r->prev
和r->next
指针串联在一起。在这种情况下传递给日志处理程序的request_rec
是最初为来自客户端的初始请求构建的;请注意,bytes_sent
字段仅在链中的最后一个请求(实际发送响应的请求)中才是正确的。palloc
及其朋友destroy_sub_request
但是,仅仅给模块命令表不足以将它们完全与服务器核心分离。服务器必须记住这些命令才能在以后对它们进行操作。这涉及维护对模块私有的数据,这些数据可以是每个服务器的,也可以是每个目录的。大多数内容都是每个目录的,特别是包括访问控制和授权信息,但也包括有关如何从后缀确定文件类型的信息,这些信息可以通过AddType
和DefaultType
指令进行修改,等等。一般来说,管理哲学是,任何可以通过目录进行配置的内容都应该进行配置;每个服务器的信息通常用于标准模块集中的信息,例如Alias
es 和Redirect
s,这些信息在请求绑定到底层文件系统中的特定位置之前就会生效。
模拟 NCSA 服务器的另一个要求是能够处理每个目录的配置文件,通常称为.htaccess
文件,尽管即使在 NCSA 服务器中,它们也可以包含与访问控制完全无关的指令。因此,在 URI -> 文件名转换之后,但在执行任何其他阶段之前,服务器会沿着底层文件系统的目录层次结构向下遍历,遵循转换后的路径名,以读取可能存在的任何.htaccess
文件。然后必须将读取的信息合并到来自服务器自身配置文件的适用信息中(来自access.conf
中的<Directory>
部分,或来自srm.conf
中的默认值,实际上,对于大多数目的,它几乎与<Directory />
完全相同)。
最后,在提供了一个涉及读取.htaccess
文件的请求后,我们需要丢弃为处理它们而分配的存储空间。这与在其他类似问题出现的地方解决问题的方式相同,通过将这些结构绑定到每个事务的资源池中。
mod_mime.c
中发挥作用,它定义了文件类型处理程序,该处理程序模拟 NCSA 服务器从后缀确定文件类型的方式。在这里,我们将看到实现AddType
和AddEncoding
命令的代码。这些命令可以出现在.htaccess
文件中,因此必须在模块的私有每个目录数据中进行处理,实际上,该数据由两个单独的table
组成,用于 MIME 类型和编码信息,并声明如下typedef struct { table *forced_types; /* Additional AddTyped stuff */ table *encoding_types; /* Added with AddEncoding... */ } mime_dir_config;当服务器读取配置文件或
<Directory>
部分(包括 MIME 模块的命令之一)时,它需要创建一个mime_dir_config
结构,以便这些命令有东西可以操作。它通过调用在模块的“创建每个目录配置槽”中找到的函数来实现这一点,该函数有两个参数:此配置信息适用的目录的名称(或srm.conf
的NULL
),以及一个指向应该发生分配的资源池的指针。(如果我们正在读取.htaccess
文件,则该资源池是请求的每个请求资源池;否则,它是一个用于配置数据的资源池,并在重新启动时清除。无论哪种方式,创建的结构在池被清除时消失都很重要,如果需要,通过在池上注册清理操作)。
对于 MIME 模块,每个目录配置创建函数只是palloc
上面的结构,并创建几个table
来填充它。看起来像这样
void *create_mime_dir_config (pool *p, char *dummy) { mime_dir_config *new = (mime_dir_config *) palloc (p, sizeof(mime_dir_config)); new->forced_types = make_table (p, 4); new->encoding_types = make_table (p, 4); return new; }现在,假设我们刚刚读入了一个
.htaccess
文件。我们已经有了层次结构中下一个目录的每个目录配置结构。如果我们刚刚读入的.htaccess
文件中没有AddType
或AddEncoding
命令,则其每个目录配置结构对于 MIME 模块仍然有效,我们可以直接使用它。否则,我们需要以某种方式合并这两个结构。为此,服务器会调用模块的每个目录配置合并函数(如果存在)。该函数接受三个参数:要合并的两个结构,以及一个用于分配结果的资源池。对于 MIME 模块,需要做的就是将来自新每个目录配置结构的表与来自父级的表覆盖
void *merge_mime_dir_configs (pool *p, void *parent_dirv, void *subdirv) { mime_dir_config *parent_dir = (mime_dir_config *)parent_dirv; mime_dir_config *subdir = (mime_dir_config *)subdirv; mime_dir_config *new = (mime_dir_config *)palloc (p, sizeof(mime_dir_config)); new->forced_types = overlay_tables (p, subdir->forced_types, parent_dir->forced_types); new->encoding_types = overlay_tables (p, subdir->encoding_types, parent_dir->encoding_types); return new; }注意 --- 如果不存在每个目录合并函数,服务器将只使用子目录的配置信息,而忽略父级的配置信息。对于某些模块,这完全可以(例如,对于包含模块,其每个目录配置信息仅包含
XBITHACK
的状态),对于这些模块,您可以不声明一个,并将模块本身中的相应结构槽留空NULL
。
AddType
和AddEncoding
命令。为了找到命令,服务器会在模块的command table
中查找。该表包含有关命令接受多少个参数以及以何种格式、在何处允许等信息。这些信息足以让服务器使用预解析的参数调用大多数命令处理函数。不用多说,让我们看看AddType
命令处理程序,它看起来像这样(AddEncoding
命令看起来基本相同,这里不会显示)char *add_type(cmd_parms *cmd, mime_dir_config *m, char *ct, char *ext) { if (*ext == '.') ++ext; table_set (m->forced_types, ext, ct); return NULL; }此命令处理程序非常简单。如您所见,它接受四个参数,其中两个是预解析的参数,第三个是所讨论模块的每个目录配置结构,第四个是指向
cmd_parms
结构的指针。该结构包含许多参数,这些参数经常对某些命令有用,但并非对所有命令都有用,包括一个资源池(从中可以分配内存,并将清理操作绑定到该资源池),以及正在配置的(虚拟)服务器,从中可以获取模块的每个服务器配置数据如果需要。此特定命令处理程序非常简单的另一个原因是它不会遇到任何错误条件。如果有,它可以返回错误消息而不是NULL
;这会导致在服务器的stderr
上打印出错误消息,然后快速退出,如果它在主配置文件中;对于.htaccess
文件,语法错误将记录在服务器错误日志中(以及它来自何处的指示),并且请求将被弹回,并显示服务器错误响应(HTTP 错误状态,代码 500)。
MIME 模块的命令表包含这些命令的条目,看起来像这样
command_rec mime_cmds[] = { { "AddType", add_type, NULL, OR_FILEINFO, TAKE2, "a mime type followed by a file extension" }, { "AddEncoding", add_encoding, NULL, OR_FILEINFO, TAKE2, "an encoding (e.g., gzip), followed by a file extension" }, { NULL } };这些表中的条目是
(void *)
指针,它在cmd_parms
结构中传递给命令处理程序 --- 这在许多类似命令由同一个函数处理的情况下很有用。AllowOverride
选项都有相应的掩码位,以及一个额外的掩码位RSRC_CONF
,指示命令可能出现在服务器自己的配置文件中,但不能出现在任何.htaccess
文件中。TAKE2
表示两个预解析的参数。其他选项是TAKE1
,表示一个预解析的参数,FLAG
,表示参数应该是On
或Off
,并作为布尔标志传递,RAW_ARGS
,表示服务器将原始的、未解析的参数(除了命令名称本身之外的所有内容)传递给命令。还有ITERATE
,表示处理程序看起来与TAKE1
相同,但如果存在多个参数,则应多次调用它,最后是ITERATE2
,表示命令处理程序看起来像TAKE2
,但如果存在更多参数,则应多次调用它,保持第一个参数不变。NULL
)。get_module_config
函数从request_rec
的每个目录配置向量中提取的。int find_ct(request_rec *r) { int i; char *fn = pstrdup (r->pool, r->filename); mime_dir_config *conf = (mime_dir_config *)get_module_config(r->per_dir_config, &mime_module); char *type; if (S_ISDIR(r->finfo.st_mode)) { r->content_type = DIR_MAGIC_TYPE; return OK; } if((i=rind(fn,'.')) < 0) return DECLINED; ++i; if ((type = table_get (conf->encoding_types, &fn[i]))) { r->content_encoding = type; /* go back to previous extension to try to use it as a type */ fn[i-1] = '\0'; if((i=rind(fn,'.')) < 0) return OK; ++i; } if ((type = table_get (conf->forced_types, &fn[i]))) { r->content_type = type; } return OK; }
唯一的实质性区别是,当命令需要配置每个服务器的私有模块数据时,它需要转到cmd_parms
数据才能访问它。以下是一个来自别名模块的示例,它也说明了如何返回语法错误(请注意,命令处理程序的每个目录配置参数被声明为一个虚拟参数,因为该模块实际上没有每个目录的配置数据)
char *add_redirect(cmd_parms *cmd, void *dummy, char *f, char *url) { server_rec *s = cmd->server; alias_server_conf *conf = (alias_server_conf *)get_module_config(s->module_config,&alias_module); alias_entry *new = push_array (conf->redirects); if (!is_url (url)) return "Redirect to non-URL"; new->fake = f; new->real = url; return NULL; }