tingya | 18 一月, 2005 22:46
该文章主要对Apache中的命令表进行了介绍和分析
/////////////////////////////////////////////////////////////
//Apache源代码分析——命令表解析
//张中庆于西安交通大学软件所
//tingya$stu,xjtu,edu,cn,将$换成@,,换成.防止地址被收集
//转载请保留出处
//最初出自西安交通大学兵马俑Linux版
//////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
命令行参数处理
如果用户是通过命令行进行Apache启动,那么启动语法如下:
Httpd [-d directory][-D parameter] [-f file] [-C directive] [-c directive] [-L] [-l] [-S] [-V] [-X]
其中,-d命令用来设置ServerRoot,即服务器的根目录。-D用来定义<IiDefine>的参数值,即预先定义一些变量。-f用来设置配置文件的路径,正常情况下,我们使用httpd.conf,不过通过-f选项,我们可以进行修改。-L用来列出当前可用的所有的命令,并退出。-l选项用来列出Apache中编译的模块并退出。-S用来显示虚拟主机的相关信息。-v用来显示Apache的版本号以及Apache编译的时间,将其打印出来,同时退出。-V显示编译设置,并退出。-X用来将Apache设置成为单进程模式。这种模式通常用来进行调试使用。
在这些命令中与配置命令相关的选项有三个:-f,-C和-c。由于命令行的命令和配置文件中命令的设置可能会相互覆盖,因此我们有必要考虑到这种覆盖可能。-C和-c就是处理这种情况。-C列出了各种指令,这些指令必须在读入配置文件之前进行处理,而-c列出的命令则必须在读入配置文件之后进行处理。Apache中给出了两个ap_array_header_t类型的数组ap_server_pre_read_config和ap_server_post_read_config分别记录-C和-c之后所需要处理的命令。为了处理命令行参数,Apache中定义了apr_getopt_t结构来保存 ,apr_getopt_t结构如下:
struct apr_getopt_t {
apr_pool_t *cont;
apr_getopt_err_fn_t *errfn;
void *errarg;
int ind;
int opt;
int reset;
int argc;
const char **argv;
char const* place;
int interleave;
int skip_start;
int skip_end;
};
errfn则是函数在发生错误的时候调用来打印错误消息。如果errfn为NULL,则表明不是输出错误信息。errarg则是用户定义的传给错误消息的第一个参数。
Ind标记该选项在父选项中的索引。如果lnd与argc即参数的总个数相同,则表明已经到了处理的末尾。该选项应该是最后一个选项。
argc和argv与通常的含义相同,分别表示参数的个数和参数字符串。
Place通常用来记录与选项关联的一些参数。
Apache对ap_getopt_t结构的初始化是从apr_getopt_init开始的。一旦初始化完毕,Apache将不断调用apr_getopt对命令行进行解析,同时根据命令进行相应的设置。
对命令行参数进行处理的函数大多数都集中在getopt.c文件中。
///////////////////////////////////////////////////////////////////////////////////////
APR_DECLARE(apr_status_t) apr_getopt_init(apr_getopt_t **os, apr_pool_t *cont,
int argc, const char *const *argv)
该函数主要对apr_getopt()中需要使用的ap_getopt_t结构进行初始化。os是apr_getopt函数中需要使用的ap_getopt_t结构。cont则是初始化过程中需要使用的内存池。argc和argv则是参数个数和参数字符串,其通常来源于main(argc,argv)中的相应变量。
///////////////////////////////////////////////////////////////////////////////////////
APR_DECLARE(apr_status_t) apr_getopt(apr_getopt_t *os, const char *opts,
char *optch, const char **optarg)
apr_getopt是命令行参数解析的核心函数。os则是前面使用apr_getopt_init初始化的结构,一旦进行初始化,main函数中的argc和argv都保存到了os中的argc和argv成员中。opts是应用程序能够接受的可选项的字符串。Optch则是下一个需要解析的字符串,
函数会返回四个值,同时退出。返回值及其含义如下所示:
APR_EOF -- 没有更多的选项进行解析
APR_BADCH -- 发现一个错误的选项字符
APR_BADARG -- 该选项参数后面没有参数
APR_SUCCESS -- 下一个选项被发现
函数流程:
///////////////////////////////////////////////////////////////////////////////////////
Apache配置指令
///////////////////////////////////////////////////////////////////////////////////////
Apache中关于配置指令最重要的数据结构就是指令表了,指令表的结构为command_rec,其定义如下:
typedef struct command_struct command_rec;
struct command_struct {
const char *name;
cmd_func func;
void *cmd_data;
int req_override;
enum cmd_how args_how;
const char *errmsg;
};
command_rec结构描述了与处理命令相关的各个信息,与ap_directive_t不同,其通常位于模块内部,由模块使用。name给出了命令的名称,其值与ap_directive_t中的directive相同;func则是每个模块中用来处理该命令的方法,其是cmd_func类型,Apache中定义为typedef const char *(*cmd_func) ();由于cmd_func不带有任何参数,因此如果需要传入适当的参数的话只能通过cmd_data进行。
为了能够阐述req_override和args_how的具体含义,我们还需要阐述一些相关的概念。
1. 指令类型
Apache中提供了12种类型的指令,这些类型是与实际的配置文件中指令处理相一致的。每种指令都大同小异,唯一的区别就在于其处理的参数的数目以及在将指令传递给指令实现函数之前,服务器如何解释这些参数的方式。由于各个指令的参数不相同,为此也导致了指令的处理函数的格式不相同。
apache中对于指令类型的定义是通过枚举类型cmd_how来实现的,cmd_how定义如下:
enum cmd_how {
RAW_ARGS,
TAKE1,
TAKE2,
ITERATE,
ITERATE2,
FLAG,
NO_ARGS,
TAKE12,
TAKE3,
TAKE23,
TAKE123,
TAKE13
};
对于所有的指令处理函数,其都将返回字符串。如果指令处理函数正确的处理了指令,那么函数返回NULL,否则应该返回错误提示信息。对于各种指令,服务器的处理方法如下:
RAW_ARGS
该指令会通知apache不要对传入的参数做任何的处理,只需要原封不动的传递个指令处理函数即可。使用这种指令会存在一定的风险,因为服务器不做任何的语法检查,因此难免会有错误成为“漏网之鱼”。
这种指令的处理函数通常如下所示:
const char * func(cmd_parms* parms , void* mconfig,char* args);
args只是简单的命令行内容,当然也包括指令在内。
TAKE1
顾名思义,这种类型的指令“Take 1 argument”,其只允许传入一个参数。 这种指令的处理函数通常如下所示:
const char * func(cmd_parms* parms , void* mconfig,const char* first);
ITERATE
该类型指令属于迭代类型。这种指令允许传入多个参数,不过一次只能处理一个,服务器必须遍历处理它们。每次遍历处理的过程又与TAKE1类型指令相同。因此这种指令的处理函数与TAKE1指令相同。
TAKE2,TAKE12,ITERATE2
TAKE2类型必须向指令处理函数传入两个参数;而TAKE12可以接受一个或者两个参数。ITERATE2与ITERATE1类似,都属于参数迭代处理类型,不过ITERATE2要求至少传递两个参数给函数。不过,第二个参数能够使用多次,服务器会遍历它们,直到所有的参数都传递给处理函数。如果只向TAKE12传递一个参数,那么服务器将把第二个参数设置为NULL。这三种类型的指令处理函数原型如下:
const char *two_args_func(cmd_parms* parms , void* mconfig,
const char* first,const char* second);
TAKE3,TAKE23,TAKE13,TAKE123
这组指令最多都可以接受3个参数,如果参数超过三个,则处理函数将会报错。TAKE3意味着参数必须是三个;TAKE23则意味着至少两个参数,也可以为三个,不能少于两个或者多于三个。TAKE13则意味这可以接受一个参数或者三个参数,除此之外都是非法。TAKE123意味着可以接受一个,两个或者三个参数。这四种指令的处理函数原型如下:
const char *three_args_func(cmd_parms* parms , void* mconfig,
const char* first,const char* second,const char* third);
NO_ARGS
该类型的指令不接受任何的参数,其最常用的就是作为复杂指令的闭标签存在。比如<Directory>总是必须有</Directory>与之对应。尽管<Directory>通常需要一个参数来指定其所设置的目录,不过对于</Directory>则没有这个参数。因此其就是NO_ARGS指令类型。这种指令的处理函数通常如下所示:
const char *no_args_func(cmd_parms* parms , void* mconfig);
FLAG
这种类型最简单,其只允许用来启动或者关闭的指令。与前面的几种类型中,服务器直接将配置命令后的参数传递给函数不同,这种指令不会直接传递参数,而是首先对参数进行处理,并将处理的结果true或者false作为进一步的参数传递给函数。这种指令的处理函数通常如下所示:
const char *flag_args_func(cmd_parms* parms , void* mconfig,int flag);
不管是什么指令,其对应的处理函数都是以两个参数开始:cmd_parms* parms和void* mconfig。cmd_parms结构用来存储处理配置命令时候所需要的辅助内容。在处理任何配置信息文件的时候,该结构都将被创建。其用于apache核心传递各种参数给指令处理方法。关于cmd_parms的具体解释,我们在后面将给出。
另一个参数void* mconfig表示针对指令位置的配置记录,基于所遇到的指令位置的不同,该配置记录可以是服务器配置记录,也可以是目录配置记录。
2. 指令位置
关于指令的另外一个重要的概念就是指令位置字段的概念(location field)。位置字段主要用于控制各个指令在配置文件中允许出现的位置,包括三种:顶层位置,目录区和虚拟主机区。如果服务器发现一个指令不允许出现在出现的地方,比如LoadModule只允许出现在顶层位置,如果发现其在<Directory>中出现,服务器将报错,同时打印错误信息退出。另外位置字段还将控制指令是否允许在文件.htaccess中使用。
对于指令位置字段,Apache中提供了下面几个控制选项:
#define OR_NONE 0
#define OR_LIMIT 1
#define OR_OPTIONS 2
#define OR_FILEINFO 4
#define OR_AUTHCFG 8
#define OR_INDEXES 16
#define OR_UNSET 32
#define ACCESS_CONF 64
#define RSRC_CONF 128
#define EXEC_ON_READ 256
#define OR_ALL (OR_LIMIT|OR_OPTIONS|OR_FILEINFO|OR_AUTHCFG|OR_INDEXES)
对于上面的选项,Apache又分成两类:配置文件说明选项和.htaccess文件说明选项。
ACCESS_CONF
该选项允许指令出现在Directory或者Location区间以内的顶级配置文件中,因此该选项通常用来对Apache中目录或者文件进行某些控制。
RSRC_CONF
该选项允许指令出现在Directory或者Location区间以外的顶级配置文件中,当然也可以出现在VirtualHost区间中,因此该选项通用用来对Apache服务器或者虚拟主机的进行某些控制。
EXEC_ON_READ
该选项是Apache2.0中新增加的。在Apache1.3中,对指令的处理是边读边执行的,而Apache2.0中并不是这样。Apache2.0首先预处理配置文件,将所有的配置命令读取到一个树型结构中,树的每个结点为ap_directive_t类型。我们称之为配置树。一旦建立配置树,Apache然后才会遍历并处理所有指令。通常情况下,这种处理方式会很好,因为延后处理使的可以控制模块之间的依赖性。比如,线程化的MPM需要在定义MaxClients指令之前就必须定义ThreadsPerChild指令。MPM不会强求管理员处理这种情况,即使在配置文件中ThreadsPerChild定义在MaxClients之后Apache也会在分析之前在配置树中进行次序调整。
不过延后处理也不是完美无缺,有的时候可能导致问题。比如,Include通常用于在配置文件中读取另外一个配置文件。如果不能立即读取到配置文件,那么第二个配置文件中的配置命令将不可能生成到配置树中。解决的方法就是在读取到Include命令的时候取消延后策略,而是立即执行该命令。EXEC_ON_READ选项可以强制服务器在将指令从配置文件中读取之后立即执行。
通过前面的分析,我们可以看出,如果某个指令可能会改变配置树,那么该指令就应该为EXEC_ON_READ,不过所谓的改变配置树不是指改变配置树的顺序,而是改变配置树的信息。如果想要改变配置树的次序,那么应该在预先配置阶段进行或者在处理配置指令时候完成这样的工作。
除了上面的三个用于对配置文件进行控制,Apache中还提供了八个选项用于控制.htaccess文件中指令。
OR_NONE
该选项则不允许在.htaccess文件中使用任何指令,这是默认值。不过大多数指令都会对其进行修改。只有那些仅仅在顶级配置文件中才有效的指令以及那些仅仅有服务器管理员才可以使用的指令才会设置该选项。
OR_LIMIT
只有那些可能涉及虚拟主机访问的指令才会设置该选项。在核心服务器上,Allow,Deny以及Order指令都会设置该选项。使用这个选项的指令允许放置在Directory和Location标签中,以及AllowOverride设置为Limit的.htaccess文件中。
OR_OPTIONS
使用该选项的指令通常用来控制特定的目录设置。在标准的Apache发行版本中,Options指令和XbitHack指令都会使用该选项。使用该选项的指令必须置于Directory和Location以及AllowOverride设置为Options的.htaccess文件中。
OR_FILEINFO
使用该选项的指令通常用于控制文档类型或者文档信息。设置该选项的指令包括SetHandler,ErrorDocument,DefaultType等等。这中类型的指令可以存在于Directory和Location标签以及AllowOverride设置为FileInfo的.htaccess文件中。
OR_AUTHCFG
使用该选项的指令通常用语控制授权或者认证等信息。设置该选项的命令包括AuthUserFile,AuthName以及Require。这种命令可以存在于Directory和Location以及AllowOverride设置为AuthConfig的.htaccess文件中。
OR_INDEXS
使用该选项的命令通常用于控制目录索引的输出。示例指令包括AddDescription,AddIcon,AddIconByEncoding。这种命令可以存在于Directory和Location以及AllowOverride设置为Indexs的.htaccess文件。
OR_ALL
这个选项是前面所有选项的组合。使用该选项的命令可以存在于Directory和Location标签中,以及只要AllowOverride不为None的.htaccess文件中。
OR_UNSET
这个特殊的值指出,这个目录没有设置重写。模块不应该使用值。核心会使用这个值来正确的控制继承。
目前在配置文件中,对于<Directory>标签外部的部分,req_override状态为:
RSRC_CONF|OR_OPTIONS|OR_FILEINFO|OR_INDEXS
而在<Directory>标签内部的部分,状态为:
ACCESS_CONF|OR_LIMIT|OR_OPTIONS|OR_FILEINFO|OR_AUTHCFG|OR_INDEXS
而在.htaccess文件中,状态则由AllowOverride命令决定。
3. 指令处理宏
apache中定义了十二种指令,为此apache中也相应的定义的配置处理命令。Apache中定义的十二个处理宏定义如下:
# define AP_INIT_NO_ARGS(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, RAW_ARGS, help }
# define AP_INIT_RAW_ARGS(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, RAW_ARGS, help }
# define AP_INIT_TAKE1(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE1, help }
# define AP_INIT_ITERATE(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, ITERATE, help }
# define AP_INIT_TAKE2(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE2, help }
# define AP_INIT_TAKE12(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE12, help }
# define AP_INIT_ITERATE2(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, ITERATE2, help }
# define AP_INIT_TAKE13(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE13, help }
# define AP_INIT_TAKE23(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE23, help }
# define AP_INIT_TAKE123(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE123, help }
# define AP_INIT_TAKE3(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, TAKE3, help }
# define AP_INIT_FLAG(directive, func, mconfig, where, help)
{ directive, func, mconfig, where, FLAG, help }
从上面的定义可以看出,十二个宏,每个宏都具有相同的格式,唯一不同的就是其传入的指令类型。正常的情况下在模块的command_rec表格中增加一个新的指令有两种方法:一种是直接填充command_rec结构中的各个成员变量,另一种就是使用宏填充。如果使用宏,那么代码编译时就不会产生警告消息。但是如果直接填充结构,那么在采用维护模式进行编译时,代码就会产生警告。下面我们来看一下AP_INIT_TAKE1的一个具体实现。
AP_INIT_TAKE1(“AuthType”,
ap_set_string_slot,
(void*)APR_XtOffsetOf(core_dir_config,ap_auth_type),
OR_AUTHCFG,
“An Http authorization(e.g,”Basic”)”
);
该宏的第一个字段AuthType是指令的名称。该名称可以是任何合法的内容,但是不应该包含空格字符,Apache在处理命令时候以空格作为结束符,一旦遇到空格即停止。当服务器读取配置文件的时候,它就会读入各行代码,并且搜索所有已经激活模块中的指令。如果找不到指令,服务器就终止。
该宏的第二个字段是该模块中对该命令的处理函数。在上面的示例中,模块中处理AuthType命令的处理函数为ap_set_string_slog。尽管所有的声明的函数的原型都相同,但是这个函数的原型会根据所定义的指令类型而改变。不同的类型的指令的原型在前面我们已经介绍过。
第三个字段是应该向函数传递的附加数据。不同的函数附加数据不同。如果没有附加数据,则此处为NULL通过该字段的设置,Apache 可以用一个函数来处理多个指令。比如在core模块中dirsection函数方法同时可以处理Directory和DirectoryMatch两个命令,那么该字段设置为0(NULL)和1就可以分开。在该示例中,附加数据为(void*)APR_XtOffsetOf(core_dir_config,ap_auth_type)。核心服务器会使用ap_set_string_slot函数来设置结构中的字符串。因为这个函数可以用于设置任何结构中的任何字符串,所以服务器必须要传递结构中正确字段的偏移量,以便芄恢浪谋涞哪谌荨PR_XtOffsetOf宏能够计算这个偏移量,通过将这个宏放入到指令定义的第三个字段中,就可以根据第二个字段中规定的函数得到这个偏移量。
第四个字段将会描述指令可以放置到配置文件的哪些位置。配置文件解析器能够确定指令是在目录中找到还是在虚拟主机区间找到。通过在这里规定这个值,核心服务器就能够进行基本的错误检查以确保没有在不合理的位置中规定指令。
最后一个字段就是在管理员不正确配置服务器时输出的错误字符串。这个错误的消息不应该解释指令所进行的工作。相反,它应该解释怎样使用指令。这样就可以帮助管理员改正可能存在的配置错误。
该宏最终的产生效果类似于下面的语句:
command_rec.name = “AuthType”;
command_rec.func = ap_set_string_slot;
command_rec.cmd_data = (void*)APR_XtOffsetOf(core_dir_config,ap_auth_type);
command_rec.req_override = OR_AUTHCFG;
command_rec.args_how = TAKE1;
command_rec.errmsg = “An Http authorization(e.g,”Basic”);
我们来看实际的例子,模块mod_alias中的命令表格如下:
static const command_rec alias_cmds[] =
{
AP_INIT_TAKE2("Alias", add_alias, NULL, RSRC_CONF,
"a fakename and a realname"),
AP_INIT_TAKE2("ScriptAlias", add_alias, "cgi-script", RSRC_CONF,
"a fakename and a realname"),
AP_INIT_TAKE23("Redirect", add_redirect, (void *) HTTP_MOVED_TEMPORARILY,
OR_FILEINFO,
"an optional status, then document to be redirected and "
"destination URL"),
AP_INIT_TAKE2("AliasMatch", add_alias_regex, NULL, RSRC_CONF,
"a regular expression and a filename"),
AP_INIT_TAKE2("ScriptAliasMatch", add_alias_regex, "cgi-script", RSRC_CONF,
"a regular expression and a filename"),
AP_INIT_TAKE23("RedirectMatch", add_redirect_regex,
(void *) HTTP_MOVED_TEMPORARILY, OR_FILEINFO,
"an optional status, then a regular expression and "
"destination URL"),
AP_INIT_TAKE2("RedirectTemp", add_redirect2,
(void *) HTTP_MOVED_TEMPORARILY, OR_FILEINFO,
"a document to be redirected, then the destination URL"),
AP_INIT_TAKE2("RedirectPermanent", add_redirect2,
(void *) HTTP_MOVED_PERMANENTLY, OR_FILEINFO,
"a document to be redirected, then the destination URL"),
{NULL}
};