Writing a Content Generator(translate)

原文:《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("\n",
r);
    ap_rputs("Apache HelloWorld "
"Module", r);
    ap_rputs("

Hello World!

", r); ap_rputs("

This is the Apache HelloWorld module!

", r); ap_rputs("", 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 
#include 
#include 
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("\n",
r);
    ap_rputs("Apache HelloWorld "
"Module", r);
    ap_rputs("

Hello World!

", r); ap_rputs("

This is the Apache HelloWorld module!

", r); ap_rputs("", 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

SetHandler helloworld

此代码导致任何在我们服务器上的/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, "%s%s\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, "%s"
                "%s%s"
                "", 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("\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("\n"
            "Apache HelloWorld Module"
            "

Hello World!

" "

This is the Apache HelloWorld module!

", 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("", 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进行输出有三个步骤:

  1. 创建一个斗旅。
  2. 使用我们正在写的数据填写旅。
  3. 将旅转移到堆栈上的第一个输出过滤器(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 =
        "\n"
        "Apache HelloWorld Module"
        "

Hello World!

" "

This is the Apache HelloWorld module!

" ""; 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继承的遗留方法,但现在大多数开发人员都将其视为已弃用(尽管该方法仍然支持)。 在大多数情况下,我们更愿意直接在新的代码中使用输入过滤器链:

  1. 创建一个bucket brigade。
  2. 将数据从第一个输入滤波器(r-> input_filters)拉到brigade中。
  3. 读取我们的数据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("

Bad request body.

\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, "

Got %d bytes of request body data.

\n", count); } else { ap_rputs("

No request body.

\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("\n" "Apache HelloWorld Module" "

Hello World!

" "

This is the Apache HelloWorld module!

", 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("", 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, "

Unsupported Transfer Encoding: %s

", ap_escape_html(r->pool, hdr)); return OK; /* we allow this, but just refuse to handle it */ } } if (!has_input) { ap_rputs("

No request body.

\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, "

Got %d bytes of request body data.

\n", count); return OK; } else { ap_rputs("

Error reading request body.

", 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中的

元素:

  • application / x-www-form-urlencoded(通过POST或GET提交的普通Web表单)
  • 多部分/表单数据(Netscape的文件上传表单的多部分格式)

历史上,以这些格式中的任何一种解码形式的数据是应用程序的责任。 例如,任何CGI库或脚本模块都包含处理此任务的代码。 Apache本身不包括此功能作为标准,但由第三方模块(如mod_form和mod_upload)提供。
分析表单数据
标准表单数据(application/x-www-form-urlencoded)的格式是一系列的键/值对,由&号(“&”)分隔。 任何字符都可以使用%nn序列进行转义,其中nn是字节的十六进制表示形式,某些字符必须被转义。 键数据并不总是唯一的,解析数据是复杂的 例如,HTML