原文:《The Apache Modules Book-Application Development with Apache》
原则上,可以使用通用网关接口(CGI)进行任何操作。但CGI提供了一个很好的解决方案的问题的范围要小得多!Apache中的内容生成器也是如此。 它是处理请求和构建Web应用程序的核心。 实际上,它可以扩展到基础系统允许网络服务器做任何事情。 内容生成器是Apache中最基本的模块。 所有主要的传统应用程序通常用作内容生成器。 例如,由Apache代理的CGI,PHP和应用程序服务器是内容生成器。
5.1 HelloWorld模块
在本章中,我们将开发一个简单的内容生成器。 习惯的HelloWorld示例演示了模块编程的基本概念,包括完整的模块结构以及处理程序回调和request_rec的使用。
在本章结尾,我们将扩展我们的HelloWorld模块,以报告请求和响应标头,环境变量和任何发布到服务器的数据的完整信息,我们将配置好写入内容生成器模块,在我们可能会使用CGI脚本或可比较的延期的情况下。
5.1.1 The Module Skeleton
每个Apache模块通过导出模块数据结构来工作。 一般来说,Apache 2.x模块采用以下形式:
module AP_MODULE_DECLARE_DATA some_module = {
STANDARD20_MODULE_STUFF,
some_dir_cfg, /* create per-directory config struct */创建每个目录
some_dir_merge, /* merge per-directory config struct */合并每个目录
some_svr_cfg, /* create per-host config struct */创建每个主机config struct
some_svr_merge, /* merge per-host config struct */ 合并每个主机config struct
some_cmds, /* configuration directives for this module */此模块的配置指令
some_hooks /* register module's hooks/etc. with the core */注册模块的挂钩等。 与核心
};
STANDARD20_MODULE_STUFF 宏扩展以提供版本信息,确保编译后的模块只有在完全二进制兼容时加载到服务器构建中,以及文件名和保留字段。 大部分剩余字段涉及模块配置; 他们将在第9章中详细讨论。为了我们的HelloWorld模块的目的,我们只需要hook:
module AP_MODULE_DECLARE_DATA helloworld_module = {
STANDARD20_MODULE_STUFF,
NULL,
NULL,
NULL,
NULL,
NULL,
helloworld_hooks
};
已经声明了模块结构,现在我们需要实例化钩子函数。 Apache 将在服务器启动时运行该功能。 其目的是将模块的处理功能注册到服务器核心,以便随后在适当时调用我们的模块的功能。 在 HelloWorld 的情况下,我们只需要注册一个简单的内容生成器或处理程序,这是我们可以在这里插入的许多功能之一。
static void helloworld_hooks(apr_pool_t *pool)
{
ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE);
}
最后,我们需要实现helloworld_handler。 这是一个回调函数,Apache将在处理HTTP请求时在适当的时候调用该函数。 它可以选择处理或忽略请求。 如果处理请求,则该函数负责向客户端发送有效的HTTP响应,并确保读取(或丢弃)来自客户机的任何数据。 这与CGI脚本的责任非常相似,或者实际上与整个Web服务器的责任相似。
这里是我们最简单的处理程序:
static int helloworld_handler(request_rec *r)
{
if (!r->handler || (strcmp(r->handler, "helloworld") != 0)) {
return DECLINED;
}
if (r->method_number != M_GET) {
return HTTP_METHOD_NOT_ALLOWED;
}
ap_set_content_type(r, "text/html;charset=ascii");
ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n",
r);
ap_rputs("<html><head><title>Apache HelloWorld "
"Module</title></head>", r);
ap_rputs("<body><h1>Hello World!</h1>", r);
ap_rputs("<p>This is the Apache HelloWorld module!</p>", r);
ap_rputs("</body></html>", r);
return OK;
}
这个回调函数从几个基本的理智检查开始。 首先,我们检查r->handler程序来确定请求是否适用于我们。 如果请求不适合我们,我们通过返回DECLINED来忽略它。 然后Apache将控制权传给下一个处理程序。
其次,我们只想支持HTTP GET和HEAD方法。 我们检查这些情况,如果合适,返回一个表示不允许该方法的HTTP错误代码。 在此返回错误代码将导致Apache将错误页面返回给客户端。 请注意,HTTP标准(见附录C)将HEAD定义为与GET相同,除了在HEAD中省略的响应体。 这两种方法都包含在Apache的M_GET中,内容生成器函数应该将它们视为相同。
执行这些检查的顺序很重要。 如果我们扭转它们,我们的模块可能会导致Apache在诸如接受它们的CGI脚本之类的另一个处理程序的POST请求的情况下返回错误页面。
一旦我们确信该请求是可接受的,并且适用于此处理程序,我们会生成实际的响应 - 在这种情况下,这是一个微不足道的HTML页面。 完成后,我们返回OK,告诉Apache我们已经处理了这个请求,并且它不应该调用任何其他处理程序。
5.1.2 Return Values
即使这个琐碎的处理程序也有三个可能的返回值。 通常,模块提供的处理程序可以返回
- OK,表明处理程序已完全成功处理该请求。 不需要进一步处理。
- DECLINED,表示处理程序对请求不感兴趣,并拒绝处理该请求。 然后Apache会尝试下一个处理程序。 默认的处理程序,它简单地从本地磁盘返回文件(或错误页面,如果失败),永远不会返回DECLINED,所以请求总是由一些功能处理。
- HTTP状态代码,用于指示错误。 处理程序对请求负责,但无法或不愿意完成。
HTTP状态代码会转移Apache内的整个处理链。 正常处理请求被中止,Apache设置内部重定向到错误文档,该文档可能是Apache的预定义默认值之一,也可能是由服务器配置中的ErrorDocument指令指定的文档或处理程序。请注意,该转移工作 只有当Apache还没有开始将响应发送到客户端时,这可能是处理错误的重要设计考虑因素。 为了确保正确的行为,在编写任何数据(我们的第一个ap_rputs语句)之前,必须进行任何这样的转移。在可能的情况下,最好在请求处理周期中处理错误。 这个考虑在第6章进一步讨论。
5.1.3 The Handler Field处理程序字段
检查r->handler程序可能看起来违反直觉,但是这一步通常在所有内容生成器中都是必需的。 Apache将调用任何模块注册的所有内容生成器,直到其中一个返回OK或HTTP状态代码。 因此,每个模块都需要检查r->handler程序,它告诉模块是否应该处理请求。
这个方案是通过实现Apache的hook(钩子)而实现的,它们旨在使任何数量的函数(或没有)在hook上运行。 内容生成器在Apache的hook中是独一无二的,因为只有一个内容生成器函数必须对每个请求负责。 共享实现的其他hook具有不同的语义,我们将在第6章和第10章中看到。
5.1.4 The Complete Module
把它们放在一起并添加所需的headers,我们有一个完整的mod_helloworld.c源文件
/* The simplest HelloWorld module */
#include <httpd.h>
#include <http_protocol.h>
#include <http_config.h>
static int helloworld_handler(request_rec *r)
{
if (!r->handler || strcmp(r->handler, "helloworld")) {
return DECLINED;
}
if (r->method_number != M_GET) {
return HTTP_METHOD_NOT_ALLOWED;
}
ap_set_content_type(r, "text/html;charset=ascii");
ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n",
r);
ap_rputs("<html><head><title>Apache HelloWorld "
"Module</title></head>", r);
ap_rputs("<body><h1>Hello World!</h1>", r);
ap_rputs("<p>This is the Apache HelloWorld module!</p>", r);
ap_rputs("</body></html>", r);
return OK;
}
static void helloworld_hooks(apr_pool_t *pool)
{
ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE);
}
module AP_MODULE_DECLARE_DATA helloworld_module = {
STANDARD20_MODULE_STUFF,
NULL,
NULL,
NULL,
NULL,
NULL,
helloworld_hooks
} ;
这就是我们所需要的! 现在我们可以构建模块并将其插入到Apache中。 我们使用与Apache捆绑在一起的apxs实用程序,用于确保编译标志和路径正确:
编译这个模块
$ apxs -c mod_helloworld.c
(以root身份登录)安装
# apxs -i mod_helloworld.la
然后将其配置为httpd.conf文件中的一个handler程序:
LoadModule helloworld_module modules/mod_helloworld.so
<Location /helloworld>
SetHandler helloworld
</Location>
此代码导致任何在我们服务器上的/helloworld调用这个模块作为其handler程序。
请注意,helloworld_hooks和helloworld_handler函数都声明为静态。 在Apache模块中,这种做法是典型的 - 尽管不是很普遍。 通常,仅导出模块符号,并且其他所有内容都保持为模块本身的私有。 因此,将所有函数声明为静态是一个很好的做法。 当模块导出其他模块的服务或API时,可能会出现例外情况,如第10章所述。当模块在多个源文件中实现并且需要某些符号对这些文件是共同的时,就会出现另一种情况。 在这种情况下应采用命名惯例,以避免符号空间污染。
5.1.5 Using the request_rec Object
正如我们刚刚看到的,我们处理函数的单个参数是request_rec对象。 所有涉及到请求处理的hooks都使用相同的参数。
request_rec对象是表示HTTP请求的大型数据结构,并提供对处理请求所涉及的所有数据的访问。 这也是许多较低级API调用的参数。 例如,在helloworld_handler中,它作为ap_set_content_type的参数和作为ap_rputs的I / O描述符的参数。
我们来看另一个例子。 假设我们要从本地文件系统提供文件,而不是固定的HTML页面。 为此,我们将使用r-> filename参数来标识文件。 但是我们也可以使用文件统计信息来优化发送文件的过程。 我们可以发送文件本身,而不是使用ap_rwrite读取文件并发送其内容,从而允许APR利用可用的系统优化:
static int helloworld_handler(request_rec *r)
{
apr_file_t *fd;
apr_size_t sz;
apr_status_t rv;
/* "Is it for us?" checks omitted for brevity */
/* It's an error if r->filename and finfo haven't been set for us.
* We could omit this check if we make certain assumptions concerning
* use of our module, but if 'normal' processing is prevented by
* some other module, then r->filename might be null, and we don't
* want to risk a segfault!
*/
if (r->filename == NULL) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,"Incomplete request_rec!") ;
return HTTP_INTERNAL_SERVER_ERROR ;
}
ap_set_content_type(r, "text/html;charset=ascii");
/* Now we can usefully set some additional headers from file info
* (1) Content-Length
* (2) Last-Modified
*/
ap_set_content_length(r, r->finfo.size);
if (r->finfo.mtime) {
char *datestring = apr_palloc(r->pool, APR_RFC822_DATE_LEN);
apr_rfc822_date(datestring, r->finfo.mtime);
apr_table_setn(r->headers_out, "Last-Modified", datestring);
}
rv = apr_file_open(&fd, r->filename,
APR_READ|APR_SHARELOCK|APR_SENDFILE_ENABLED,APR_OS_DEFAULT, r->pool);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't open %s", r->filename);
return HTTP_NOT_FOUND ;
}
ap_send_fd(fd, r, 0, r->finfo.size, &sz);
/* file_close here is purely optional. If we omit it, APR will close
* the file for us when r is destroyed, because apr_file_open
* registered a close on r->pool.
*/
apr_file_close(fd);
return OK;
}
5.2 The Request, the Response, and the Environment
将这个小的转移放在文件系统中,HelloWorld模块还有什么有用的功能?
那么,模块可以按照与Apache捆绑在一起的printenv CGI脚本程序的方式来报告一般信息。 Apache模块中最常用(有用的)三个信息组中有三个本别是是请求头,响应头和内部环境变量。 让我们更新原来的HelloWorld模块,在响应页面打印。
这些信息集合中的每一个都保存在作为request_rec对象的一部分的APR表中。 我们可以遍历表,使用apr_table_do和回调来打印完整的内容。 我们将使用HTML表来表示这些Apache表。
首先,这是一个回调,将表项打印为HTML行。 当然,我们需要转义HTML的数据:
static int printitem(void *rec, const char *key, const char *value)
{
/* rec is a user data pointer. We'll pass the request_rec in it. */
request_rec *r = rec;
ap_rprintf(r, "<tr><th scope=\"row\">%s</th><td>%s</td></tr>\n",
ap_escape_html(r->pool, key),
ap_escape_html(r->pool, value));
/* Zero would stop iterating; any other return value continues */
return 1;
}
其次,我们提供一个使用回调打印整个表的功能:
static void printtable(request_rec *r, apr_table_t *t,
const char *caption, const char *keyhead,
const char *valhead)
{
/* Print a table header */
ap_rprintf(r, "<table<caption>%s</caption><thead>"
"<tr><th scope=\"col\">%s</th><th scope=\"col\">%s"
"</th></tr></thead><tbody>", caption, keyhead, valhead);
/* Print the data: apr_table_do iterates over entries with
* our callback
*/
apr_table_do(printitem, r, t, NULL);
/* Finish the table */
ap_rputs("</tbody></table>\n", r);
}
现在我们可以在HelloWorld处理程序中包装这个功能:
static int helloworld_handler(request_rec *r)
{
if (!r->handler || (strcmp(r->handler, "helloworld") != 0)) {
return DECLINED ;
}
if (r->method_number != M_GET) {
return HTTP_METHOD_NOT_ALLOWED;
}
ap_set_content_type(r, "text/html;charset=ascii");
ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
"<html><head><title>Apache HelloWorld Module</title></head>"
"<body><h1>Hello World!</h1>"
"<p>This is the Apache HelloWorld module!</p>", r);
/* Print the tables */
printtable(r, r->headers_in, "Request Headers", "Header", "Value");
printtable(r, r->headers_out, "Response Headers", "Header", "Value");
printtable(r, r->subprocess_env, "Environment", "Variable", "Value");
ap_rputs("</body></html>", r);
return OK;
}
5.2.1 Module I/O
我们的HelloWorld模块使用类似stdio的函数系列生成输出:ap_rputc,ap_rputs,ap_rwrite,ap_rvputs,ap_vrprintf,ap_rprintf和ap_rflush。 我们也看到了“发送文件”调用ap_send_file。 这个简单的高级API最初是从较早的Apache版本继承而来,它仍然适用于许多内容生成器。 它在http_protocol.h中定义。
由于引入了过滤器链,产生输出的基本机制是基于buckets和brigades,如第3章和第8章所述。过滤器模块采用不同的机制来产生输出,这些机制也可用于或者说有时适用于内容处理程序。
在过滤器中处理或生成输出有两种根本不同的方法:
- 直接操纵bucket(铲斗)和brigades(旅)
- 使用另一个类似stdio的API(这是比ap_r * API更好的选择,因为向后兼容性不是问题)
我们将在第8章中详细描述这些机制。现在我们来看一下在内容生成器中使用面向过滤器的I / O的基本机制。
使用过滤器I / O进行输出有三个步骤:
- 创建一个斗旅。
- 使用我们正在写的数据填写旅。
- 将旅转移到堆栈上的第一个输出过滤器(r-> output_filters)。
通过创建一个新的brigade或者重新使用一个brigade,这些步骤可以根据需要重复多次。 如果响应大和/或生成速度较慢,我们可能希望将其沿较小的块中的过滤器链传递。 然后,响应可以通过过滤器传递给客户端,从而为我们提供了一个有效的管道,并避免了缓冲整个响应的开销。 过滤器模块是非常有用的目标。
对于我们的HelloWorld模块,我们所需要做的就是创建brigade,然后使用util_filter.h中定义的替代stdio类API替换ap_r *系列调用:ap_fflush,ap_fwrite,ap_fputs,ap_fputc,ap_fputstrs和ap_fprintf。 这些调用有一个稍微不同的原型:而不是将request_rec作为文件描述符传递,我们必须通过我们正在写入的目标过滤器和bucket bridage。 我们将在第8章中看到这个方案的例子。
5.2.1.1 Output
这是我们第一个使用面向过滤器输出的简单的HelloWorld处理程序。 这个较低级别的API比简单的类似stdio的缓冲I / O复杂一点,有时可以实现模块的优化(尽管在这种情况下,任何差异都可以忽略不计)。 我们还可以通过明确处理输出错误来利用稍微更精细的控制。
static int helloworld_handler(request_rec *r)
{
static const char *const helloworld =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
"<html><head><title>Apache HelloWorld Module</title></head>"
"<body><h1>Hello World!</h1>"
"<p>This is the Apache HelloWorld module!</p>"
"</body></html>";
apr_status_t rv;
apr_bucket_brigade *bb;
apr_bucket *b;
if (!r->handler || strcmp(r->handler,"helloworld")) {
return DECLINED;
}
if (r->method_number != M_GET) {
return HTTP_METHOD_NOT_ALLOWED;
}
bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
ap_set_content_type(r, "text/html;charset=ascii");
/* We could instead use the stdio-like filter API calls like
* ap_fputs(r->filters_out, bb, helloworld);
* which is basically the same as using ap_rputs and family.
*
* Alternatively, we can wrap our output in a bucket, append an
* EOS, and pass it down the filter chain.
*/
b = apr_bucket_immortal_create(helloworld, strlen(helloworld),
bb->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b);
APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_eos_create(bb->bucket_alloc));
rv = ap_pass_brigade(r->filters_out, bb);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "Output Error");
return HTTP_INTERNAL_SERVER_ERROR;
}
return OK;
}
5.2.1.2 Input
模块输入略有不同。 再次,我们拥有一种从Apache 1.x继承的遗留方法,但现在大多数开发人员都将其视为已弃用(尽管该方法仍然支持)。 在大多数情况下,我们更愿意直接在新的代码中使用输入过滤器链:
- 创建一个bucket brigade。
- 将数据从第一个输入滤波器(r-> input_filters)拉到brigade中。
- 读取我们的数据buckests中的数据,并使用它。
这两种输入法通常在现有模块中找到,包括Apache 2.x的模块。 我们将依次介绍我们的HelloWorld模块。 我们将更新模块以支持POST并计算POSTed的字节数(请注意,此操作通常但不总是在Content-Length请求标头中可用)。 我们不会解码或显示实际数据; 虽然我们可以这样做,但是这个任务通常最好由一个输入过滤器(或者一个库,比如libapreq)来处理。 我们在这里使用的功能记录在http_protocol.h中:
#define BUFLEN 8192
static int check_postdata_old_method(request_rec *r)
{
char buf[BUFLEN];
size_t bytes, count = 0;
/* Decide how to treat input */
if (ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK) != OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad request body!");
ap_rputs("<p>Bad request body.</p>\n", r);
return HTTP_BAD_REQUEST;
}
if (ap_should_client_block(r)) {
for (bytes = ap_get_client_block(r, buf, BUFLEN); bytes > 0;
bytes = ap_get_client_block(r, buf, BUFLEN)) {
count += bytes;
}
ap_rprintf(r, "<p>Got %d bytes of request body data.</p>\n", count);
} else {
ap_rputs("<p>No request body.</p>\n", r);
}
return OK;
}
static int helloworld_handler(request_rec *r)
{
if (!r->handler || strcmp(r->handler, "helloworld")) {
return DECLINED;
}
/* We could be just slightly sloppy and drop this altogether,
* but it's good practice to reject anything that's not explicitly
* allowed. It cuts off *potential* exploits for someone trying
* to compromise the server.
*/
if ((r->method_number != M_GET) && (r->method_number != M_POST)) {
return HTTP_METHOD_NOT_ALLOWED;
}
ap_set_content_type(r, "text/html;charset=ascii");
ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
"<html><head><title>Apache HelloWorld Module</title></head>"
"<body><h1>Hello World!</h1>"
"<p>This is the Apache HelloWorld module!</p>", r);
/* Print the tables */
printtable(r, r->headers_in, "Request Headers", "Header", "Value");
printtable(r, r->headers_out, "Response Headers", "Header", "Value");
printtable(r, r->subprocess_env, "Environment", "Variable", "Value");
/* Ignore the return value -– it's too late to bail out now
* even if there's an error
*/
check_postdata_old_method(r);
ap_rputs("</body></html>", r);
return OK ;
}
最后,最后,使用使用util_filter.h中记录的函数的直接访问输入过滤器的首选方法是check_postdata。
我们创建一个brigade,然后循环直到EOS,从输入过滤器填充brigade。我们将在第8章再次看到这种技术。
static int check_postdata_new_method(request_rec *r)
{
apr_status_t status;
int end = 0;
apr_size_t bytes, count = 0;
const char *buf;
apr_bucket *b;
apr_bucket_brigade *bb;
/* Check whether there's any input to read. A client can tell
* us that fact by using Content-Length or Transfer-Encoding.
*/
int has_input = 0;
const char *hdr = apr_table_get(r->headers_in, "Content-Length");
if (hdr) {
has_input = 1;
}
hdr = apr_table_get(r->headers_in, "Transfer-Encoding");
if (hdr) {
if (strcasecmp(hdr, "chunked") == 0) {
has_input = 1;
}else {
ap_rprintf(r, "<p>Unsupported Transfer Encoding: %s</p>",
ap_escape_html(r->pool, hdr));
return OK; /* we allow this, but just refuse to handle it */
}
}
if (!has_input) {
ap_rputs("<p>No request body.</p>\n", r);
return OK;
}
/* OK, we have some input data. Now read and count it. */
/* Create a brigade to put the data into. */
bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
/* Loop until we get an EOS on the input */
do {
/* Read a chunk of input into bb */
status = ap_get_brigade(r->input_filters, bb, AP_MODE_READBYTES,APR_BLOCK_READ, BUFLEN);
if ( status == APR_SUCCESS ) {
/* Loop over the contents of bb */
for (b = APR_BRIGADE_FIRST(bb);
b != APR_BRIGADE_SENTINEL(bb);
b = APR_BUCKET_NEXT(b) ) {
/* Check for EOS */
if (APR_BUCKET_IS_EOS(b)) {
end = 1;
break;
}
/* Ignore other metadata */
else if (APR_BUCKET_IS_METADATA(b)) {
continue;
}
/* To get the actual length, we need to read the data */
bytes = BUFLEN;
status = apr_bucket_read(b, &buf, &bytes,
APR_BLOCK_READ);
count += bytes;
}
}
/* Discard data we're finished with */
apr_brigade_cleanup(bb);
} while (!end && (status == APR_SUCCESS));
if (status == APR_SUCCESS) {
ap_rprintf(r, "<p>Got %d bytes of request body data.</p>\n",
count);
return OK;
}
else {
ap_rputs("<p>Error reading request body.</p>", r);
return OK; /* Just send the above message and ignore the data */
}
}
5.2.1.3 I/O Errors
当我们得到I / O错误时会发生什么?
过滤器(第8章所述)通过返回APR错误代码表示错误给我们; 他们也可以设置r->状态。 我们的处理程序可以通过检查ap_pass_brigade和ap_get_brigade的返回值来检测这样的事件,如前面的例子。 正常的行为是停止处理并返回适当的HTTP错误代码。 此行为会导致Apache向客户端发送错误文档(在第6章中讨论)。 我们还应该记录错误消息,从而帮助系统管理员诊断问题。
但是如果错误是客户端连接被终止了怎么办? 这是浪费时间,试图将错误文档发送给已经消失的客户端。 我们可以通过检查r-> connection->aborted检测到这种断开连接,如本章末尾的默认处理程序所示。
5.2.2 Reading Form Data
我们现在有读取输入数据的基础。 但是,只有当我们知道如何处理这些数据时,数据才有用。 我们需要在网上处理的最常见的数据形式是通过提交HTML表单的Web浏览器发送给我们的数据。 此类数据遵循通用浏览器支持的两种标准格式之一,并由enctype属性控制为HTML中的<form>元素:
- application / x-www-form-urlencoded(通过POST或GET提交的普通Web表单)
- 多部分/表单数据(Netscape的文件上传表单的多部分格式)
历史上,以这些格式中的任何一种解码形式的数据是应用程序的责任。 例如,任何CGI库或脚本模块都包含处理此任务的代码。 Apache本身不包括此功能作为标准,但由第三方模块(如mod_form和mod_upload)提供。
分析表单数据
标准表单数据(application/x-www-form-urlencoded)的格式是一系列的键/值对,由&号(“&”)分隔。 任何字符都可以使用%nn序列进行转义,其中nn是字节的十六进制表示形式,某些字符必须被转义。 键数据并不总是唯一的,解析数据是复杂的 例如,HTML <select multiple>元素可以提交密钥的几个值。
表示这些数据的天然结构是一张行李表。 这个结构可以在Apache中表示为apr_array_header_t *(数组)值的apr_hash_t *(哈希表)。 我们可以将输入数据解析成该表示,如下所示:
/* Parse form data from a string. The input string is NOT preserved. */
static apr_hash_t *parse_form_from_string(request_rec *r, char *args)
{
apr_hash_t *form;
apr_array_header_t *values;
char *pair;
char *eq;
const char *delim = "&";
char *last;
char **ptr;
if (args == NULL) {
return NULL;
}
form = apr_hash_make(r->pool);
/* Split the input on '&' */
for (pair = apr_strtok(args, delim, &last); pair != NULL;pair = apr_strtok(NULL, delim, &last)) {
for (eq = pair; *eq; ++eq) {
if (*eq == '+') {
*eq = ' ';
}
}
/* split into Key / Value and unescape it */
eq = strchr(pair, '=');
if (eq) {
*eq++ = '\0';
ap_unescape_url(pair);
ap_unescape_url(eq);
}
else {
eq = "";
ap_unescape_url(pair);
}
/* Store key/value pair in our form hash. Given that there
* may be many values for the same key, we store values
* in an array (which we'll have to create the first
* time we encounter the key in question).
*/
values = apr_hash_get(form, pair, APR_HASH_KEY_STRING);
if (values == NULL) {
values = apr_array_make(r->pool, 1, sizeof(const char*));
apr_hash_set(form, pair, APR_HASH_KEY_STRING, values);
}
ptr = apr_array_push(values);
*ptr = apr_pstrdup(r->pool, eq);
}
return form;
}
该方案基于从单个输入缓冲区解析整个输入数据。 在表单提交的总大小相当小的情况下,正常网络表单的情况通常是正常的。 我们应该通过限制接受的输入的大小来限制拒绝服务(DoS)攻击(由服务器管理员指定的数据的最大数量)。 涉及流解析的替代方法可能适用于较大的形式,特别是那些涉及文件上传的方法,可能涉及到兆字节或甚至千兆字节的数据。 mod_upload3模块提供了更适合大型上传的解析器。
我们可以使用我们刚刚定义的函数来解析GET提交的数据:
static apr_hash_t* parse_form_from_GET(request_rec *r)
{
return parse_form_from_string(r, r->args);
}
解析由POST提交的数据更多的工作,因为我们必须读取数据:
/* Get POSTed data. Assume we have already checked that the
* content type is application/x-www-form-urlencoded.
* Assume *form is null on entry.
*/
static int parse_form_from_POST(request_rec *r, apr_hash_t **form)
{
int bytes, eos;
apr_size_t count;
apr_status_t rv;
apr_bucket_brigade *bb;
apr_bucket_brigade *bbin;
char *buf;
apr_bucket *b;
const char *clen = apr_table_get(r->headers_in, "Content-Length");
if (clen != NULL) {
bytes = strtol(clen, NULL, 0);
if (bytes >= MAX_SIZE) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Request too big (%d bytes; limit %d)",
bytes, MAX_SIZE);
return HTTP_REQUEST_ENTITY_TOO_LARGE;
}
}
else {
bytes = MAX_SIZE;
}
bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
bbin = apr_brigade_create(r->pool, r->connection->bucket_alloc);
count = 0;
do {
rv = ap_get_brigade(r->input_filters, bbin, AP_MODE_READBYTES,
APR_BLOCK_READ, bytes);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"failed to read form input");
return HTTP_INTERNAL_SERVER_ERROR;
}
for (b = APR_BRIGADE_FIRST(bbin);
b != APR_BRIGADE_SENTINEL(bbin);
b = APR_BUCKET_NEXT(b) ) {
if (APR_BUCKET_IS_EOS(b)) {
eos = 1;
}
if (!APR_BUCKET_IS_METADATA(b)) {
if (b->length != (apr_size_t)(-1)) {
count += b->length;
if (count > MAX_SIZE) {
/* This is more data than we accept, so we're
* going to kill the request. But we have to
* mop it up first.
*/
apr_bucket_delete(b);
}
}
}
if (count <= MAX_SIZE) {
APR_BUCKET_REMOVE(b);
APR_BRIGADE_INSERT_TAIL(bb, b);
}
}
} while (!eos);
/* OK, done with the data. Kill the request if we got too much data. */
if (count > MAX_SIZE) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"Request too big (%d bytes; limit %s)",
bytes, MAX_SIZE);
return HTTP_REQUEST_ENTITY_TOO_LARGE;
}
/* We've got all the data. Now put it in a buffer and parse it. */
buf = apr_palloc(r->pool, count+1);
rv = apr_brigade_flatten(bb, buf, &count);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"Error (flatten) reading form data");
return HTTP_INTERNAL_SERVER_ERROR;
}
buf[count] = '\0';
*form = parse_form_from_string(r, buf);
return OK;
}
在这一点上,我们为确保容易访问表单数据奠定了基础,我们可以提供一些访问器功能。 mod_form执行类似的功能,但使用我们尚未遇到的技术来提供更清洁的API,其中处理程序模块不需要关注哈希。
以下示例显示了一个函数,它以逗号分隔的字符串形式返回键的所有值,该表达式对Perl(使用CGI.pm)或PHP等脚本环境的用户来说很熟悉。 其他高级别的访问者现在也很容易写。
char *form_value(apr_pool_t *pool, apr_hash_t *form, const char *key)
{
apr_array_header_t *v_arr = apr_hash_get(form, key,
APR_HASH_KEY_STRING);
/* Caveat: this is ambiguous because values may contain commas */
return apr_array_pstrcat(pool, v_arr, ',');
}
结合这些功能,我们可以更新我们的HelloWorld处理程序来显示表单数据。 我们假设表单数据由ASCII输入和任何非ASCII字符的替代问号组成:
static int helloworld_handler(request_rec *r)
{
apr_hash_t *formdata = NULL;
int rv = OK;
if (!r->handler || (strcmp(r->handler, "helloworld") != 0)) {
return DECLINED;
}
/* We could be just slightly sloppy and drop this altogether,
* but it's good practice to reject anything that's not explicitly
* allowed. It cuts off *potential* exploits for someone trying
* to compromise the server.
*/
if ((r->method_number != M_GET) && (r->method_number != M_POST)) {
return HTTP_METHOD_NOT_ALLOWED;
}
ap_set_content_type(r, "text/html;charset=ascii");
ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
"<html><head><title>Apache HelloWorld Module</title></head>"
"<body><h1>Hello World!</h1>"
"<p>This is the Apache HelloWorld module!</p>", r);
/* Print the tables */
printtable(r, r->headers_in, "Request Headers", "Header", "Value");
printtable(r, r->headers_out, "Response Headers", "Header", "Value");
printtable(r, r->subprocess_env, "Environment", "Variable", "Value");
/* Display the form data */
if (r->method_number == M_GET) {
formdata = parse_form_from_GET(r);
}
else if (r->method_number == M_POST) {
const char* ctype = apr_table_get(r->headers_in, "Content-Type");
if (ctype && (strcasecmp(ctype,
"application/x-www-form-urlencoded")
== 0)) {
rv = parse_form_from_POST(r, &formdata);
}
}
if (rv != OK) {
ap_rputs("<p>Error reading form data!</p>", r);
}
else if (formdata == NULL) {
ap_rputs("<p>No form data found.</p>", r);
}
else {
/* Parsed the form successfully, so we have data to display */
apr_array_header_t *arr;
char *key;
apr_ssize_t klen;
apr_hash_index_t *index;
char *val;
char *p;
ap_rprintf(r, "<h2>Form data supplied by method %s</h2>\n<dl>",
r->method) ;
for (index = apr_hash_first(r->pool, formdata); index != NULL;
index = apr_hash_next(index)) {
apr_hash_this(index, (void**)&key, &klen, (void**)&arr);
ap_rprintf(r, "<dt>%s</dt>\n",ap_escape_html(r->pool, key));
for (val = apr_array_pop(arr); val != NULL;
val = apr_array_pop(arr)) {
for (p = val; *p != '\0'; ++p) {
if (!isascii(*p)) {
*p = '?';
}
}
ap_rprintf(r, "<dd>%s</dd>\n",
ap_escape_html(r->pool, val));
}
}
ap_rputs("</dl>", r) ;
}
ap_rputs("</body></html>", r) ;
return OK ;
}
5.3 The Default Handler
到目前为止,我们在简单的处理程序中介绍了简单的变体,并强调了开发与正常CGI或PHP脚本相同的内容处理程序所需的工具。 总结本章,我们将介绍Apache的默认处理程序。 虽然它服务于服务器文件系统中的文件,但这个处理程序与我们早期的功能不同,因为它执行了更多的内务管理,从而说明了更多的核心API。 Apache的默认处理程序比前面示例中显示的处理程序更为先进,您可能希望在第一次阅读时略过。
static int default_handler(request_rec *r)
{
conn_rec *c = r->connection;
apr_bucket_brigade *bb;
apr_bucket *e;
core_dir_config *d;
int errstatus;
apr_file_t *fd = NULL;
apr_status_t status;
int bld_content_md5;
ap_get_module_config检索模块的配置(第9章):
d = (core_dir_config *)ap_get_module_config(r->per_dir_config,
&core_module);
如果我们的系统被配置为这样做,我们可以计算一个MD5哈希值,但只有当没有一个过滤器会转换内容并使我们的哈希无效时。
bld_content_md5 = (d->content_md5 & 1)
&& r->output_filters->frec->ftype != AP_FTYPE_RESOURCE;
因为这是最后的处理程序,如果我们不要求请求,我们不能返回DECLINED。
ap_allow_standard_methods(r, MERGE_ALLOW,
M_GET, M_OPTIONS, M_POST, -1);
下一个检查执行内务处理任务。 这不是真的必要,因为如果在销毁请求时未使用的输入仍然存在,Apache将为我们执行这些任务。
/* If filters intend to consume the request body, they must
* register an InputFilter to slurp the contents of the POST
* data from the POST input stream. It no longer exists when
* the output filters are invoked by the default handler.
*/
if ((errstatus = ap_discard_request_body(r)) != OK) {
return errstatus;
}
if (r->method_number == M_GET || r->method_number == M_POST) {
if (r->finfo.filetype == 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"File does not exist: %s", r->filename);
return HTTP_NOT_FOUND;
}
此处理程序仅提供普通文件; Apache以不同的方式处理目录。 如果一个目录的请求到达这个处理程序,那是一个配置错误。
/* Don't try to serve a directory. Some OSs do weird things
* with raw I/O on a directory.
*/
if (r->finfo.filetype == APR_DIR) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Attempt to serve directory: %s", r->filename);
return HTTP_NOT_FOUND;
}
处理请求URI结尾处的任何额外的垃圾。
if ((r->used_path_info != AP_REQ_ACCEPT_PATH_INFO) &&
r->path_info && *r->path_info)
{
/* default to reject */
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"File does not exist: %s",
apr_pstrcat(r->pool, r->filename,
r->path_info, NULL));
return HTTP_NOT_FOUND;
}
/* We understood the (non-GET) method, but it might not be
legal for this particular resource. Check whether the
'deliver_script' flag is set. If so, then go ahead
and deliver the file because
it isn't really content (only GET normally returns content).
Note: The only possible non-GET method
at this point is POST. In the future, we should enable
script delivery for all methods. */
if (r->method_number != M_GET) {
core_request_config *req_cfg;
req_cfg = ap_get_module_config(r->request_config,
&core_module);
if (!req_cfg->deliver_script) {
/* The flag hasn't been set for this request. Punt. */
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"This resource does not accept the %s method.",
r->method);
return HTTP_METHOD_NOT_ALLOWED;
}
}
if ((status = apr_file_open(&fd, r->filename,APR_READ|APR_BINARY
#if APR_HAS_SENDFILE
| ((d->enable_sendfile == ENABLE_SENDFILE_OFF)
? 0 : APR_SENDFILE_ENABLED)
#endif
, 0, r->pool)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
"file permissions deny server access: %s", r->filename);
return HTTP_FORBIDDEN;
}
现在我们再设置一些标准头文件:
ap_update_mtime(r, r->finfo.mtime);
ap_set_last_modified(r);
ap_set_etag(r);
apr_table_setn(r->headers_out, "Accept-Ranges", "bytes");
ap_set_content_length(r, r->finfo.size);
bb = apr_brigade_create(r->pool, c->bucket_alloc);
ap_meets_conditions执行一些有用的检查,将文件信息交叉引用到请求标头,以确定我们是否真的需要发送文件或仅确认客户端缓存副本的有效性。 在特殊情况下,可能会确定我们的文件对客户端是无用的,应该被丢弃。
if ((errstatus = ap_meets_conditions(r)) != OK) {
apr_file_close(fd);
r->status = errstatus;
}
else {
if (bld_content_md5) {
apr_table_setn(r->headers_out, "Content-MD5",
ap_md5digest(r->pool, fd));
}
/* For platforms where the size of the file may be larger
* than can be stored in a single bucket (where the
* length field is an apr_size_t), split it into several
* buckets */
if (sizeof(apr_off_t) > sizeof(apr_size_t)
&& r->finfo.size > AP_MAX_SENDFILE) {
apr_off_t fsize = r->finfo.size;
e = apr_bucket_file_create(fd, 0, AP_MAX_SENDFILE,
r->pool, c->bucket_alloc);
while (fsize > AP_MAX_SENDFILE) {
apr_bucket *ce;
apr_bucket_copy(e, &ce);
APR_BRIGADE_INSERT_TAIL(bb, ce);
e->start += AP_MAX_SENDFILE;
fsize -= AP_MAX_SENDFILE;
}
e->length = (apr_size_t)fsize;
/* Resize just the last bucket */
}
else {
e = apr_bucket_file_create(fd, 0,
(apr_size_t)r->finfo.size,
r->pool, c->bucket_alloc);
}
#if APR_HAS_MMAP
if (d->enable_mmap == ENABLE_MMAP_OFF) {
(void)apr_bucket_file_enable_mmap(e, 0);
}
#endif
APR_BRIGADE_INSERT_TAIL(bb, e);
}
e = apr_bucket_eos_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, e);
status = ap_pass_brigade(r->output_filters, bb);
if (status == APR_SUCCESS
|| r->status != HTTP_OK
|| c->aborted) {
return OK;
}
else {
/* No way to know what type of error occurred */
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r,
"default_handler: ap_pass_brigade returned %i",
status);
return HTTP_INTERNAL_SERVER_ERROR;
}
}
else { /* unusual method (not GET or POST) */
if (r->method_number == M_INVALID) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Invalid method in request %s", r->the_request);
return HTTP_NOT_IMPLEMENTED;
}
另一个API调用支持OPTIONS方法:
if (r->method_number == M_OPTIONS) {
return ap_send_http_options(r);
}
return HTTP_METHOD_NOT_ALLOWED;
}
}
5.4 Summary
本章讨论内容生成器和相关主题:
- 介绍了Apache模块结构。
- 它显示了一个模块如何使用内核注册一个处理函数。
- 描述了基本的处理程序API。
- 它描述了内容生成器模块的作用,并开发了一个简单的模块。
- 它显示内容生成器如何与request_rec对象一起使用,以获取诸如标题和环境变量的信息,以执行I / O以及访问表单数据。
- 它演示了基本的错误处理。
- 描述了模块中常见的基本管理。
- 它引入了Apache的默认处理程序,展示了稍微更先进的技术来高效地提供静态文件,并适当关注HTTP协议。
此时,您应该可以将应用程序编写为模块或将CGI脚本重写为模块。 虽然我们介绍了一个模块的整体结构框架,但是我们的覆盖面已经有几个空白。 模块结构的其余部分涉及配置; 他们将在第9章中讨论。钩子的含义及其注册在第10章中讨论。接下来,第6章,第7章和第8章通过引入请求处理周期,访问和身份验证以及 过滤链。