13.10 创建自己的处理器
在了解了处理器的细节之后我们就可以创建自己的处理器了。本章中我们将创建一个简单的处理器example_handler,该处理器的作用很简单,只是返回固定的报文信息。
13.10.1 定义处理器
在创建自己的过滤器之前,你必须首先要考虑清楚你的处理器的名称以及它在什么条件下会被调用。对于我们的示例处理器而言,处理器的名称为”example_handler”,它的触发条件则如下所示:
<Location /example_status>
SetHandler example_handler
</Location>
example_handler在URI为http://xxx.xxx.xxx/example_status的时候被触发,为此我们必须在配置文件httpd.conf中增加上面的配置信息。
13.10.2 声明处理器
一旦确定处理的名称,那么我们就可以定义该处理器。事实上处理器是一种名为handler的特殊的挂钩,因此声明处理器就是声明handler挂钩的一个实例:
ap_hook_handler(example_handler, NULL, NULL, APR_HOOK_MIDDLE);
该声明将在指定模块的register_hooks中被注册。
13.10.3 实现处理器
在注册处理器的时候我们可以看到当处理器被调用的时候,example_handler函数将被调用。所有的处理器函数基本上都具有相同的样式:
static int x_handler(request_rec *r)
{
…
Return OK;
}
函数具有唯一的参数request_rec,同时函数会返回OK或者DECLINE。如果处理器成功的处理结束就返回OK。如果处理器发现不是它能处理的请求则返回DECLINE,这跟普通的挂钩完全相同。
对于example_handler,它的主要任务就是返回给客户端固定的HTML报文。因此整个过程相对简单:
static int x_handler(request_rec *r)
{
if (strcmp(r->handler, "example-handler")) {
return DECLINED;
}
对于大部分处理器来说,尤其是类似于status_handler,info_handler等都需要首先判断请求中传递的处理器是否是指定的处理器。
/*
* We're about to start sending content, so we need to force the HTTP
* headers to be sent at this point. Otherwise, no headers will be sent
* at all. We can set any we like first, of course. **NOTE** Here's
* where you set the "Content-type" header, and you do so by putting it in
* r->content_type, *not* r->headers_out("Content-type"). If you don't
* set it, it will be filled in with the server's default type (typically
* "text/plain"). You *must* also ensure that r->content_type is lower
* case.
*
* We also need to start a timer so the server can know if the connexion
* is broken.
*/
ap_set_content_type(r, "text/html");
if (r->header_only) {
return OK;
}
如果客户端仅仅需要我们返回头信息,那么我们就没有必要继续发送后面的报文体。
ap_rputs(DOCTYPE_HTML_3_2, r);
ap_rputs("<HTML>/n", r);
ap_rputs(" <HEAD>/n", r);
ap_rputs(" <TITLE>mod_example Module Content-Handler Output/n", r);
ap_rputs(" </TITLE>/n", r);
ap_rputs(" </HEAD>/n", r);
ap_rputs(" <BODY>/n", r);
ap_rputs(" <H1><SAMP>mod_example</SAMP> Module Content-Handler Output/n", r);
ap_rputs(" </H1>/n", r);
ap_rputs(" <P>/n", r);
ap_rprintf(r, " Apache HTTP Server version: /"%s/"/n",
ap_get_server_version());
ap_rputs(" <BR>/n", r);
ap_rprintf(r, " Server built: /"%s/"/n", ap_get_server_built());
ap_rputs(" </P>/n", r);;
ap_rputs(" <P>/n", r);
ap_rputs(" The format for the callback trace is:/n", r);
ap_rputs(" </P>/n", r);
ap_rputs(" <DL>/n", r);
ap_rputs(" <DT><EM>n</EM>.<SAMP><routine-name>", r);
ap_rputs("(<routine-data>)</SAMP>/n", r);
ap_rputs(" </DT>/n", r);
ap_rputs(" <DD><SAMP>[<applies-to>]</SAMP>/n", r);
ap_rputs(" </DD>/n", r);
ap_rputs(" </DL>/n", r);
ap_rputs(" <P>/n", r);
ap_rputs(" The <SAMP><routine-data></SAMP> is supplied by/n", r);
ap_rputs(" the routine when it requests the trace,/n", r);
ap_rputs(" and the <SAMP><applies-to></SAMP> is extracted/n", r);
ap_rputs(" from the configuration record at the time of the trace./n", r);
ap_rputs(" <STRONG>SVR()</STRONG> indicates a server environment/n", r);
ap_rputs(" (blank means the main or default server, otherwise it's/n", r);
ap_rputs(" the name of the VirtualHost); <STRONG>DIR()</STRONG>/n", r);
ap_rputs(" indicates a location in the URL or filesystem/n", r);
ap_rputs(" namespace./n", r);
ap_rputs(" </P>/n", r);
ap_rprintf(r, " <H2>Static callbacks so far:</H2>/n <OL>/n%s </OL>/n",
“Test”);
ap_rputs(" <H2>Request-specific callbacks so far:</H2>/n", r);
ap_rputs(" <H2>Environment for <EM>this</EM> call:</H2>/n", r);
ap_rputs(" </BODY>/n", r);
ap_rputs("</HTML>/n", r);
return OK;
}
我们在前面设置的发送的内容格式是”text/html”,因此真正发送的时候我们就必须构造HTML报文。通过ap_rputs以及ap_rprintf函数可以将指定的字符串写入到当前的请求中。
13.11 CGI脚本
尽管在前面我们已经谈过多种内容处理器,包括default_handler,mod_alias等等。但是相对于CGI内容处理器而言,它们都只能算是非常简单的处理器。
在本章中我们首先描述CGI的一些基本概念,在后面的部分,我们将深入的讨论CGI处理器的实现细节。
13.11.1 CGI工作原理
CGI的工作原理非常的简单。当浏览器请求的是一个CGI脚本的时候,服务器将首先找到该文件,然后启动一个新的子进程,并在该子进程中运行该脚本文件。如果客户端需要提交额外的信息给服务器,比如POST,那么此时服务器就会通过标准输入文件描述符将请求的信息发送给脚本。反之,如果脚本执行完毕后需要返回数据给客户端浏览器,那么它也会将输出内容写入到标准输出文件描述符,然后服务器读取该内容并发送给客户。客户端,Apache服务器以及CGI进程之间的关系可以用下图进行描述:
图13-
CGI脚本可以采用各种语言编写,但是大多数情况下会采用一些脚本语言,比如Perl,Python,甚至可以用Unix Shell进行编写。
13.11.2 CGI的相关配置
由于CGI属于处理器的一种,因此在使用之前必须能够对它进行配置处理,通知服务器哪些文件必须被CGI脚本处理。Apache中可以通过两种途径来建立这种对应的处理关系:
(1)ScriptAlias指令
ScriptAlias URL-path file-path|directory-path
这个指令在mod_alias模块中实现,用于映射一个URL到文件系统。该指令具有两个参数:将要用于访问这个目录的URI以及目录本身。以URL-path开头的(%已解码的)的URL会被映射到由第二个参数指定的具有完整路径名的本地文件系统中的脚本。ScriptAlias的标准配置如下:
ScriptAlias /cgi-bin/ /usr/local/apache/cgi-bin
这意味着任何针对/cgi-bin/ URI位置中的请求都会被视为CGI脚本处理。不过这也可能会带来一个潜在的问题。比如如果一个图片文件正好位于对应的目录中,那么该图片也会被视为CGI脚本来执行。显然这是错误的做法。
(2)目录内配置CGI脚本
如果想把ScriptAlias指令的目录之外的其余目录中文件视为CGI脚本,那么可以将CGI相关的配置放到该目录对应.htacess中或者httpd.conf文件的对应的<Directory>配置段中。相关的配置命令包括:
Options命令
通过Options命令的ExecCGI参数通知服务器该目录下的文件可以视为CGI脚本
AddHandler
Options只是通知服务器该目录下的文件可以被视为CGI脚本执行,但是具体哪个文件会被视为CGI脚本,服务器还不清楚。此时还需要通过AddHandler指令在文件类型和文件处理器之间建立对应的关系,比如:AddHandler cgi-script cgi。
该指令指令所有的xxx.cgi的文件都由处理器cgi-script进行处理。大部分情况下,用户都会使用通用的.cgi扩展名称来命名他们的cgi脚本,这就允许使用简单的命令海为整个服务器配置所有的cgi脚本。
ScriptInterpreterSource
对于Unix操作系统而言,上面的两个指令足够。但是对于window还需要一个额外的ScriptInterpreterSource指令。在CGI脚本中,通常在第一行会使用#!指令执行该脚本的解释器,比如#!/usr/local/perl则指令使用perl解释器。不过Window并不支持#!行,为此,Apache引入了ScriptInterpreterSource指令用以指令解释器,比如:ScriptInterpreterSource registry。
该指令使得服务器将在window的注册表中去查找与指令的文件关联的处理器程序。
13.11.3 CGI脚本示例
通过上面的配置,我们现在可以运行一些CGI脚本。在运行脚本之前,我们必须能够编写脚本。我们将使用两种语言来编写脚本:C语言和Perl脚本。
13.11.3.1 Helloworld
第一个CGI脚本非常简单,就是在浏览器上输出字符串“HelloWorld”。
Perl脚本:
#!/usr/bin/perl
print “Content-Type: text/plain/n/n”
print “Hello World/n”
C语言版本:
#include < stdio.h >
#include < stdib.h >
void main()
{
printf(″Contenttype:text/plain/n/n″);
printf("HelloWorld");
fflush(stdout);
}
对于脚本语言而言,需要注意的第一件事情就是指定需要调用的脚本解释器。对于Perl语言而言,大部分的Perl脚本都会使用PATH环境变量来找到Perl解释器,所以#!/usr/bin/perl可以简化为#!perl。这种简化方式有的时候会存在一定的问题。CGI脚本通常不会使用与常规用户相同的环境来运行。从安全的角度而言,将CGI脚本环境从Web服务器中剥离出来是个明智的选择。因此最好的办法就是指定解释器的完整的路径名称。
第二个问题就是CGI会在输出实际内容之前首先输出一些头信息。这些头信息很重要:
prinft (″Contenttype:text/plain/n/n″);
此行通过标准输出将字符串″Contenttype:text/plainnn″传送给Web服务器。它是一个MIME头信息,它告诉Web服务器随后的输出是以纯ASCII文本的形式。如果没有这个头信息,请求就会失败。当然CGI还会输出更多的信息,重要的是,脚本必须以空行终止头信息,这就是为什么用户在头信息之后会使用两个”/n”的原因。第一个”/n”用于终止内容类型(Content-Type)。HTTP协议规定头信息必须以”/n”结束。第二个”/n”用于终止头信息块。第二个”/n”之后的内容都将被认为是主体数据。
对于C语言编写的CGI,在编译通过后,将它们的后缀名称修改为.cgi,然后保存到/cgi-bin/目录中,这样第一个CGI程序就结束了。
13.11.3.2 输出当前的所有环境变量
下面的例子用于在浏览器中输出服务器中所有的环境变量的信息:
Perl脚本版本:
#!/usr/bin/perl
print “Content-Type: text/plain/n/n”;
foreach $var(sort(keys(%ENV))){
$val = $ENV($var};
$val =~ s|/n|//n|g;
$val =~ s|"|//"|g;
print "$s{var} = /"${var}/"/n";
}
C语言版本:
#include<stdio.h>
extern char **environ;
int main(int argc, int **argv)
{
int i;
if( environ != NULL)
for (i = 0; environ[i] != NULL; i++)
printf("%s/n", environ[i]);
return 0;
}
13.11.4 配置CGI处理器
13.11.5 mod_cgi模块分析
本章的最后部分我们将分析cgi处理器的实现细节。cgi处理器在模块mod_cgi中实现。
13.11.5.1 模块结构
module AP_MODULE_DECLARE_DATA cgi_module =
{
STANDARD20_MODULE_STUFF,
NULL, /* dir config creater */
NULL, /* dir merger --- default is to override */
create_cgi_config, /* server config */
merge_cgi_config, /* merge server config */
cgi_cmds, /* command apr_table_t */
register_hooks /* register hooks */
};
mod_cgi是一个常规的Apache2.0模块。这里没有目录配置指令,所有两个目录配置挂钩函数都被设置为NULL。不过这边有服务器相关的指令,而且继承规则也不简单,所以需要一个创建函数和一个合并函数。
cgi_cmds中定义了该模块能够处理的所有的命令:
static const command_rec cgi_cmds[] =
{
AP_INIT_TAKE1("ScriptLog", set_scriptlog, NULL, RSRC_CONF,
"the name of a log for script debugging info"),
AP_INIT_TAKE1("ScriptLogLength", set_scriptlog_length, NULL, RSRC_CONF,
"the maximum length (in bytes) of the script debug log"),
AP_INIT_TAKE1("ScriptLogBuffer", set_scriptlog_buffer, NULL, RSRC_CONF,
"the maximum size (in bytes) to record of a POST request"),
{NULL}
};
mod_cgi模块本身需要处理的三个指令都是跟CGI日志相关的。对于web服务器而言最难诊断的内容之一就是CGI脚本。为了帮助诊断CGI脚本,Apache中提供了上面的三个指令。ScriptLog定义CGI日志文件;ScriptLogLength则定义了CGI日志文件的最大,当日志长度达到这个最大长度的时候,服务器就会停止写入CGI脚本的日志信息。ScriptBuffer则定义了能够记录到文件的PUT或者POST的请求主体的长度。
mod_cgi中关心的挂钩都在register_hooks中注册:
static void register_hooks(apr_pool_t *p)
{
static const char * const aszPre[] = { "mod_include.c", NULL };
ap_hook_handler(cgi_handler, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_post_config(cgi_post_config, aszPre, NULL, APR_HOOK_REALLY_FIRST);
}
针对这个模块的挂钩函数非常简单,一个就是内容处理器,负责生成内容;另外一个就是 post_config挂钩,用于在处理完配置文件之后进行调用。Post_config会使用mod_include模块注册一个函数,因此它必须在mod_include之后才能被调用。
13.11.5.2 处理器函数
处理器函数在每次CGI请求的时候会被调用,这个函数相对非常的复杂。我们需要逐步分析它。
static int cgi_handler(request_rec *r)
{
int nph;
apr_size_t dbpos = 0;
const char *argv0;
const char *command;
const char **argv;
char *dbuf = NULL;
apr_file_t *script_out = NULL, *script_in = NULL, *script_err = NULL;
apr_bucket_brigade *bb;
apr_bucket *b;
int is_included;
int seen_eos, child_stopped_reading;
apr_pool_t *p;
cgi_server_conf *conf;
apr_status_t rv;
cgi_exec_info_t e_info;
conn_rec *c = r->connection;
在深入进一步分析之前,我们需要对一些数据结构进行深入的了解:
第一个需要分析的是cgi_exec_info_t结构:
typedef struct {
apr_int32_t in_pipe;
apr_int32_t out_pipe;
apr_int32_t err_pipe;
int process_cgi;
apr_cmdtype_e cmd_type;
apr_int32_t detached;
prog_types prog_type;
apr_bucket_brigade **bb;
include_ctx_t *ctx;
ap_filter_t *next;
apr_int32_t addrspace;
} cgi_exec_info_t;
CGI处理器中重要的一个工作就是建立一个子进程单独处理CGI脚本的内容。上面的结构用于帮助建立子进程的结构。这个结构中的大部分字段都可以告诉服务器如何建立子进程。前三个字段xxx_pipe会通知服务器要将哪些标准文件描述符与管道相连接。当启动子进程的时候,服务器就会将管道与读取内容的标准输出文件描述符,以及向脚本传递信息的标准输入文件描述符进行关联。实际的问题是,管道是否应该与错误文件描述符进行关联。
cmd_type告诉速服务器这是脚本还是已经编译好的程序。它的取值类型为apr_cmdtype_e:
typedef enum {
APR_SHELLCMD, /**< use the shell to invoke the program */
APR_PROGRAM, /**< invoke the program directly, no copied env */
APR_PROGRAM_ENV, /**< invoke the program, replicating our environment */
APR_PROGRAM_PATH, /**< find program on PATH, use our environment */
APR_SHELLCMD_ENV /**< use the shell to invoke the program,
* replicating our environment
*/
} apr_cmdtype_e;
对于大部分操作系统而言,这没有什么用。但是在某些操作系统上,比如Window,如果不具有#!的逻辑,而且还是脚本的话,那么Apache就会实现这个逻辑。
prog_type用以描述为什么要将这个模块作为CGI脚本直接调用,活着调用它进行SSL处理。prog_type也是被定义为枚举类型:
typedef enum {RUN_AS_SSI, RUN_AS_CGI} prog_types;
ctx和next两个成员用于mod_include模块,目前我们可以忽略它们。
if(strcmp(r->handler, CGI_MAGIC_TYPE) && strcmp(r->handler, "cgi-script"))
return DECLINED;
所有的处理器都必须完成的第一件事情就是确保这个请求是针对该处理器的。通过检查请求结构中的处理器字段来完成。在Apache1.3版本中,这个检查过程是没有必要的,因为核心只会调用与这个请求相匹配的处理器。如果要编写处理器,则通常必须包含类似的该行,否则模块就会尝试为所有接收到的请求提供服务。
is_included = !strcmp(r->protocol, "INCLUDED");
p = r->main ? r->main->pool : r->pool;
argv0 = apr_filepath_name_get(r->filename);
nph = !(strncmp(argv0, "nph-", 4));
conf = ap_get_module_config(r->server->module_config, &cgi_module);
if (!(ap_allow_options(r) & OPT_EXECCGI) && !is_scriptaliased(r))
return log_scripterror(r, conf, HTTP_FORBIDDEN, 0,
"Options ExecCGI is off in this directory");
上面的代码检查指定的目录是否已经允许运行CGI脚本,正如前面所言,如果某个目录允许执行CGI脚本,则它的Options指令中必须设置ExecCGI选项。如果在ScriptAlias命令中使用了这个目录,那么就可以认为CGI脚本已经在该目录中受到执行许可。
if (nph && is_included)
return log_scripterror(r, conf, HTTP_FORBIDDEN, 0,
"attempt to include NPH CGI script");
在理解上面的两行代码之前,我们有必要稍微了解一下NPH CGI脚本的概念。
此处需要补充CGI的相关内容
if (r->finfo.filetype == 0)
return log_scripterror(r, conf, HTTP_NOT_FOUND, 0,
"script not found or unable to stat");
if (r->finfo.filetype == APR_DIR)
return log_scripterror(r, conf, HTTP_FORBIDDEN, 0,
"attempt to invoke directory as script");
if ((r->used_path_info == AP_REQ_REJECT_PATH_INFO) &&
r->path_info && *r->path_info)
{
/* default to accept */
return log_scripterror(r, conf, HTTP_NOT_FOUND, 0,
"AcceptPathInfo off disallows user's path");
}
上面的代码用于对请求的脚本的路径信息进行检查。需要请求的CGI脚本的信息保存在请求r->finfo中。如果r->finfo.filetype为零,则意味着没有指定CGI脚本的路径。如果指定的CGI脚本路径是目录名称,则这也是不允许的。
大部分情况下,请求所映射的脚本的路径名称都是在磁盘上存在实际文件的。不过也有例外,那就是AcceptInfoPath指令所处理的内容。此指令决定了是否接受包含在某确定文件(或是某现有目录的一个不存在的文件)后附加的路径信息。此路径信息将在脚本里以PATH_INFO环境变量的形式出现。
比如说,假设/test/所指向的目录下只包括一个文件:here.html。那么对/test/here.html/more和/test/nothere.html/more的请求都会得到/more这样的PATH_INFO变量。
AcceptPathInfo指令的三个参数为:
1. off
仅当一个请求映射到一个真实存在的路径时,它才会被接受。这样,如上述/test/here.html/more这样的在真实文件名后跟随一个路径名的请求将会返回一个404 NOT FOUND错误。Apache中使用宏AP_REQ_REJECT_PATH_INFO来对应该值。
2. on
如果前面的路径映射到一个真实存在的文件,此请求将被接受。如果/test/here.html映射着一个有效的文件,上例中/test/here.html/more这个请求就会被接受。 Apache中使用AP_REQ_ACCEPT_PATH_INFO宏来定义该值。
3. default
对于附加路径名的请求的处理方式由其对应的处理器来决定。对应普通文本的核心处理器默认会拒绝PATH_INFO。而用于伺服脚本的处理器,比如cgi-script和isapi-isa,默认会接受PATH_INFO。Apache中使用AP_REQ_DEFAULT_PATH_INFO定义该值。
AcceptPathInfo指令存在的首要目的就是允许您覆盖处理器关于是否接受PATH_INFO的默认设置。这种覆盖是很必要的。比如说,当您使用了类似INCLUDES这样的过滤器来根据PATH_INFO产生内容时。核心处理器通常会拒绝这样的请求,而您就可以用下述的配置使这样的脚本成为可能:
<Files "mypaths.shtml">
Options +Includes
SetOutputFilter INCLUDES
AcceptPathInfo on
</Files>
AcceptPathInfo指令的参数最终会保存到r->used_path_info成员中,而PATH_INFO环境变量的值则会用r->path_info进行记录。如果当前服务器不支持附加路径信息,即r->used_path_info == AP_REQ_REJECT_PATH_INFO,但是r->path_info不为空,那么则意味着URI中添加了附加路径信息。这种情况是不允许的。
ap_add_common_vars(r);
ap_add_cgi_vars(r);
为了执行CGI脚本进行,我们还必须进行必要的环境变量设置。这下变量包括公共的CGI脚本环境变量和HTTP1.1所需要的CGI环境变量。公共的CGI环境变量包括SystemRoot,COMSPEC等等。而HTTP1.1相关的CGI环境变量则包括SCRIPT_NAME,PATH_INFO等等。具体的设置细节我们稍后讨论。
e_info.process_cgi = 1;
e_info.cmd_type = APR_PROGRAM;
e_info.detached = 0;
e_info.in_pipe = APR_CHILD_BLOCK;
e_info.out_pipe = APR_CHILD_BLOCK;
e_info.err_pipe = APR_CHILD_BLOCK;
e_info.prog_type = RUN_AS_CGI;
e_info.bb = NULL;
e_info.ctx = NULL;
e_info.next = NULL;
e_info.addrspace = 0;
/* build the command line */
if ((rv = cgi_build_command(&command, &argv, r, p, &e_info)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"don't know how to spawn child process: %s",
r->filename);
return HTTP_INTERNAL_SERVER_ERROR;
}
在建立CGI进程执行脚本之前,必须建立能够启动CGI脚本的命令行。这要使用cgi_build_command函数完成。
/* run the script in its own process */
if ((rv = run_cgi_child(&script_out, &script_in, &script_err,
command, argv, r, p, &e_info)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"couldn't spawn child process: %s", r->filename);
return HTTP_INTERNAL_SERVER_ERROR;
}
最后run_cgi_child函数将真正启动一个新的CGI进行处理CGI脚本。在稍后的部分 ,我们会分析启动进程的细节。
/* Transfer any put/post args, CERN style...
* Note that we already ignore SIGPIPE in the core server.
*/
bb = apr_brigade_create(r->pool, c->bucket_alloc);
seen_eos = 0;
child_stopped_reading = 0;
if (conf->logname) {
dbuf = apr_palloc(r->pool, conf->bufbytes + 1);
dbpos = 0;
}
do {
apr_bucket *bucket;
rv = ap_get_brigade(r->input_filters, bb, AP_MODE_READBYTES,
APR_BLOCK_READ, HUGE_STRING_LEN);
if (rv != APR_SUCCESS) {
return rv;
}
for (bucket = APR_BRIGADE_FIRST(bb);
bucket != APR_BRIGADE_SENTINEL(bb);
bucket = APR_BUCKET_NEXT(bucket))
{
const char *data;
apr_size_t len;
if (APR_BUCKET_IS_EOS(bucket)) {
seen_eos = 1;
break;
}
/* We can't do much with this. */
if (APR_BUCKET_IS_FLUSH(bucket)) {
continue;
}
/* If the child stopped, we still must read to EOS. */
if (child_stopped_reading) {
continue;
}
/* read */
apr_bucket_read(bucket, &data, &len, APR_BLOCK_READ);
if (conf->logname && dbpos < conf->bufbytes) {
int cursize;
if ((dbpos + len) > conf->bufbytes) {
cursize = conf->bufbytes - dbpos;
}
else {
cursize = len;
}
memcpy(dbuf + dbpos, data, cursize);
dbpos += cursize;
}
/* Keep writing data to the child until done or too much time
* elapses with no progress or an error occurs.
*/
rv = apr_file_write_full(script_out, data, len, NULL);
if (rv != APR_SUCCESS) {
/* silly script stopped reading, soak up remaining message */
child_stopped_reading = 1;
}
}
apr_brigade_cleanup(bb);
}
while (!seen_eos);
上面的代码将从客户端读取信息,并将其发到给CGI脚本,以便对其进行处理。读取按照正常的存储段组的读取操作进行。不过有一种情况我们必须考虑到,那就是万一CGI进程停止了读取过程。在上面的代码中,child_stopped_reading用来标记CGI进程是否停止报文读取。任何时候如果调用apr_file_write_full向CGI进程写入数据导致失败的时候,child_stopped_reading都会被设置。
如果CGI脚本停止从服务器读取信息,那么此时读取进程并不能停止从客户端读取信息。这样做就会违反HTTP规范。为了处理这种情况,需要设置一个循环,该循环从客户端读取数据。如果CGI进程停止读取,那么这些数据将被简单的抛弃,直到所有的客户端数据读取完毕。这就是处代码的作用。
if (conf->logname) {
dbuf[dbpos] = '/0';
}
/* Is this flush really needed? */
apr_file_flush(script_out);
apr_file_close(script_out);
AP_DEBUG_ASSERT(script_in != NULL);
apr_brigade_cleanup(bb);
#if APR_FILES_AS_SOCKETS
apr_file_pipe_timeout_set(script_in, 0);
apr_file_pipe_timeout_set(script_err, 0);
b = cgi_bucket_create(r, script_in, script_err, c->bucket_alloc);
#else
b = apr_bucket_pipe_create(script_in, c->bucket_alloc);
#endif
APR_BRIGADE_INSERT_TAIL(bb, b);
b = apr_bucket_eos_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b);
将数据写入给CGI进程之后,CGI进程将开始执行。执行后的数据通过sript_in管道再返回给Apache服务器。在接受服务器的返回之前,服务器必须分配足够的空间一边保存这些数据。由于所有的数据都是通过script_xxx管道进行的,为此服务器将创建一个存储段组,其中包含一个管道存储段和一个EOS存储段。
/* Handle script return... */
if (!nph) {
const char *location;
char sbuf[MAX_STRING_LEN];
int ret;
if ((ret = ap_scan_script_header_err_brigade(r, bb, sbuf))) {
ret = log_script(r, conf, ret, dbuf, sbuf, bb, script_err);
/* Set our status. */
r->status = ret;
/* Pass EOS bucket down the filter chain. */
apr_brigade_cleanup(bb);
b = apr_bucket_eos_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b);
ap_pass_brigade(r->output_filters, bb);
return ret;
}
上面的代码用于处理CGI脚本的输出。不过对于nph脚本和非nph脚本的返回处理不同。对于非nph脚本而言,当CGI脚本开始返回消息的时候,必须确保该信息有效。返回的数据中必须包含相关的头信息,如果没有,那么就必须记录错误的信息,并将错误信息发送给服务器。
location = apr_table_get(r->headers_out, "Location");
if (location && r->status == 200) {
/* For a redirect whether internal or not, discard any
* remaining stdout from the script, and log any remaining
* stderr output, as normal. */
discard_script_output(bb);
apr_brigade_destroy(bb);
apr_file_pipe_timeout_set(script_err, r->server->timeout);
log_script_err(r, script_err);
}
if (location && location[0] == '/' && r->status == 200) {
/* This redirect needs to be a GET no matter what the original
* method was.
*/
r->method = apr_pstrdup(r->pool, "GET");
r->method_number = M_GET;
/* We already read the message body (if any), so don't allow
* the redirected request to think it has one. We can ignore
* Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR.
*/
apr_table_unset(r->headers_in, "Content-Length");
ap_internal_redirect_handler(location, r);
return OK;
}
第二个可能从CGI脚本中返回的值是位置头(Location header),它告诉服务器,CGI脚本正在发出到另外的URI的重定向。为了处理这种情况,服务器需要从CGI脚本中读取所有的剩余的数据,并进行内部重定向。内部重定向将意味着客户将不能够看到响应。核心服务器会完成所有的工作,并且向客户端返回正确的页面。
else if (location && r->status == 200) {
/* XX Note that if a script wants to produce its own Redirect
* body, it now has to explicitly *say* "Status: 302"
*/
return HTTP_MOVED_TEMPORARILY;
}
rv = ap_pass_brigade(r->output_filters, bb);
}
if (nph) {
struct ap_filter_t *cur;
/* get rid of all filters up through protocol... since we
* haven't parsed off the headers, there is no way they can
* work
*/
cur = r->proto_output_filters;
while (cur && cur->frec->ftype < AP_FTYPE_CONNECTION) {
cur = cur->next;
}
r->output_filters = r->proto_output_filters = cur;
bb = apr_brigade_create(r->pool, c->bucket_alloc);
b = apr_bucket_pipe_create(tempsock, c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b);
b = apr_bucket_eos_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b);
ap_pass_brigade(r->output_filters, bb);
}
return OK; /* NOT r->status, even if it has changed. */
13.11.5.3 CGI命令行构建
在了解了处理器函数的执行细节之后,我们对整个流程有一个完整的了解,不过对于一些细节,我们还有待挖掘。首先我们来看CGI进程启动的命令行的构建过程。
通常情况下CGI进程通常是一个相对独立的可执行进程,为了启动该进程,服务器必须对启动的命令行进行构建,构建使用函数cgi_build_command完成:
cgi_build_comand被声明为可选挂钩函数类型:
static APR_OPTIONAL_FN_TYPE(ap_cgi_build_command) *cgi_build_command;
它对应的可选挂钩函数为ap_cgi_build_command,该函数在mod_cgi.h中被声明:
APR_DECLARE_OPTIONAL_FN(apr_status_t, ap_cgi_build_command,
(const char **cmd, const char ***argv,
request_rec *r, apr_pool_t *p,
cgi_exec_info_t *e_info));
该可选挂钩返回apr_status_t类型,而且需要五个参数。任何模块如果需要使用该函数,它都可以注册该函数,比如
APR_REGISTER_OPTIONAL_FN(ap_cgi_build_command);
同样,服务器如果需要使用该函数,他也必须首先获取该函数的指针,比如:
cgi_build_command = APR_RETRIEVE_OPTIONAL_FN(ap_cgi_build_command);
在大部分系统平台上,ap_cgi_build_command都不需要额外注册实现,它们都使用默认的命令行构建函数default_build_command,而对于window平台和netware平台而言,则由于存在一些差异性,必须使用它们自己的命令行构建函数。
13.11.5.4 生成CGI进程
在创建生成完需要执行的命令行之后,我们就可以启动该CGI进程了。Apache核心使用函数run_cgi_child函数创建一个新的CGI进程,该函数的原型如下:
static apr_status_t run_cgi_child(apr_file_t **script_out,
apr_file_t **script_in,
apr_file_t **script_err,
const char *command,
const char * const argv[],
request_rec *r,
apr_pool_t *p,
cgi_exec_info_t *e_info)
函数具有多达八个参数。srcipt_xxx是三个管道,用于服务器和CGI进程之间的通信。script_out用于从服务器输出数据至CGI进程,script_in则用于CGI进程返回数据至服务器进程。script_err则用于CGI进程返回错误信息至服务器。
command则是用于启动进程的命令行,argv则是对应的命令行参数。
const char * const *env;
apr_procattr_t *procattr;
apr_proc_t *procnew;
apr_status_t rc = APR_SUCCESS;
#if defined(RLIMIT_CPU) || defined(RLIMIT_NPROC) || /
defined(RLIMIT_DATA) || defined(RLIMIT_VMEM) || defined (RLIMIT_AS)
core_dir_config *conf = ap_get_module_config(r->per_dir_config,
&core_module);
#endif
在有些操作系统平台上,为了防止一些恶意的进程,系统允许对进程的执行做一些限制。通过getrlimit和setrlimit可以获取和设置这些限制。
RLIMIT_CPU:程序执行的最大CPU时间,单位是秒。超过之后程序中止,中止的信号是SIGXCPU
RLIMIT_DATA:进程中所能允许分配的堆栈的最大时间。
RLIMIT_NPROC:程序允许产生的最大子进程数量
RLIMIT_VMEM和RLIMIT_AS的含义类似,定义限制说明一个进程的映射地址空间可以占据的字节数。如果超出限制,分配动态内存和到mmap的调用会失败。
只有当OS中定义了上面的RLIMIT_XXX指令之后,才会执行上面的代码。Apache中通过配置文件对进程进行各种限制,这些指令包括RLimitCPU,RLimitMEM,RLimitNPROC等等。如果需要进行这种处理,那么服务器就会从核心中读取配置信息
if (((rc = apr_procattr_create(&procattr, p)) != APR_SUCCESS) ||
((rc = apr_procattr_io_set(procattr,
e_info->in_pipe,
e_info->out_pipe,
e_info->err_pipe)) != APR_SUCCESS) ||
((rc = apr_procattr_dir_set(procattr,
ap_make_dirstr_parent(r->pool,
r->filename))) != APR_SUCCESS) ||
#ifdef RLIMIT_CPU
((rc = apr_procattr_limit_set(procattr, APR_LIMIT_CPU,
conf->limit_cpu)) != APR_SUCCESS) ||
#endif
#if defined(RLIMIT_DATA) || defined(RLIMIT_VMEM) || defined(RLIMIT_AS)
((rc = apr_procattr_limit_set(procattr, APR_LIMIT_MEM,
conf->limit_mem)) != APR_SUCCESS) ||
#endif
#ifdef RLIMIT_NPROC
((rc = apr_procattr_limit_set(procattr, APR_LIMIT_NPROC,
conf->limit_nproc)) != APR_SUCCESS) ||
#endif
((rc = apr_procattr_cmdtype_set(procattr,
e_info->cmd_type)) != APR_SUCCESS) ||
((rc = apr_procattr_detach_set(procattr,
e_info->detached)) != APR_SUCCESS) ||
((rc = apr_procattr_addrspace_set(procattr,
e_info->addrspace)) != APR_SUCCESS) ||
((rc = apr_procattr_child_errfn_set(procattr, cgi_child_errfn)) != APR_SUCCESS)) {
/* Something bad happened, tell the world. */
ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r,
"couldn't set child process attributes: %s", r->filename);
}
上面的代码首先会调用apr_procattr_create函数创建一个属性描述数据结构procattr,并对CGI脚本的进行属性进行设置。这些进程信息包括进程在运行时候所处的目录,与管道相关联的文件描述符,以及系统中对进程运行的各种限制。