Apache HTTP Server 版本 2.4

本文档将讨论如何为 Apache HTTP Server 2.4 创建模块,通过探索一个名为 mod_example 的示例模块。在本文档的第一部分,该模块的目的是计算并打印出 Web 服务器上现有文件的各种摘要值,每当我们访问 URL http://hostname/filename.sum 时。例如,如果我们想知道位于 http://www.example.com/index.html 的文件的 MD5 摘要值,我们将访问 http://www.example.com/index.html.sum。
在本文档的第二部分,我们将讨论配置指令和上下文感知,我们将研究一个简单地将自己的配置写入客户端的模块。
首先,您应该具备 C 编程语言的基本知识。在大多数情况下,我们将尽量做到尽可能地具有教学意义,并链接到描述示例中使用的函数的文档,但也有很多情况下,需要假设“它有效”或自己深入研究各种函数调用的来龙去脉。
最后,您需要基本了解如何在 Apache HTTP Server 中加载和配置模块,以及如何获取 Apache 的头文件(如果您还没有),因为这些头文件是编译新模块所必需的。
为了编译我们在这篇文档中构建的源代码,我们将使用 APXS。假设您的源文件名为 mod_example.c,编译、安装和激活模块就像下面这样简单
apxs -i -a -c mod_example.c

每个模块都以相同的声明开始,或者说是名称标签,它将模块定义为Apache 中的一个独立实体
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
create_dir_conf, /* Per-directory configuration handler */
merge_dir_conf, /* Merge handler for per-directory configurations */
create_svr_conf, /* Per-server configuration handler */
merge_svr_conf, /* Merge handler for per-server configurations */
directives, /* Any directives we may have for httpd */
register_hooks /* Our hook registering function */
};
这段代码让服务器知道我们现在已经在系统中注册了一个新模块,并且它的名字是 example_module。模块的名称主要用于两件事
目前,我们只关心模块名称的第一个用途,它在我们需要加载模块时会发挥作用
LoadModule example_module modules/mod_example.so
本质上,这告诉服务器打开 mod_example.so 并查找名为 example_module 的模块。
在我们这个名称标签中,还包含一些关于我们希望如何处理事情的引用:我们对配置文件或 .htaccess 中的哪些指令做出响应,我们如何在特定上下文中操作,以及我们对哪些处理程序感兴趣,并将其注册到 Apache HTTP 服务中。我们将在本文档的后面部分回到所有这些元素。
在 Apache HTTP Server 2.4 中处理请求时,您需要做的第一件事是创建一个挂钩到请求处理过程的挂钩。挂钩本质上是一条消息,告诉服务器您愿意为客户端提供的某些请求提供服务,或者至少看一下。所有处理程序,无论是 mod_rewrite、mod_authn_*、mod_proxy 等等,都挂钩到请求过程的特定部分。正如您可能意识到的,模块服务于不同的目的;有些是身份验证/授权处理程序,有些是文件或脚本处理程序,而另一些第三模块则重写 URI 或代理内容。此外,最终,服务器的用户决定每个模块如何以及何时发挥作用。因此,服务器本身并不假定知道哪个模块负责处理特定请求,并将询问每个模块他们是否对给定请求感兴趣。然后,每个模块决定是礼貌地拒绝服务请求、接受服务请求,还是完全拒绝服务请求,就像身份验证/授权模块所做的那样。

为了让像我们的 mod_example 这样的处理程序更容易知道客户端是否请求我们应该处理的内容,服务器有指令来提示模块他们的帮助是否需要。其中两个是 AddHandler 和 SetHandler。让我们看一个使用 AddHandler 的示例。在我们的示例情况下,我们希望所有以 .sum 结尾的请求都由 mod_example 服务,因此我们将添加一个配置指令告诉服务器这样做
AddHandler example-handler .sum
这告诉服务器以下内容:每当我们收到对以 .sum 结尾的 URI 的请求时,我们都要让所有模块知道我们正在寻找名为“example-handler” 的模块。因此,当一个以 .sum 结尾的请求被服务时,服务器将让所有模块知道,这个请求应该由“example-handler” 服务。正如您将在后面看到的,当我们开始构建 mod_example 时,我们将检查由 AddHandler 传递的这个处理程序标签,并根据这个标签的值回复服务器。
首先,我们只想创建一个简单的处理程序,当请求特定 URL 时,它会回复客户端浏览器,因此我们暂时不会费心设置配置处理程序和指令。我们的初始模块定义将如下所示
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
NULL,
NULL,
NULL,
NULL,
NULL,
register_hooks /* Our hook registering function */
};
这让服务器知道我们对任何花哨的东西都不感兴趣,我们只想挂钩到请求,并可能处理其中一些请求。
我们示例声明中的引用 register_hooks 是我们将创建的用于管理我们如何挂钩到请求过程的函数的名称。在这个示例模块中,该函数只有一个目的;创建一个简单的挂钩,它在所有重写、访问控制等处理完成后被调用。因此,我们将让服务器知道我们希望将其作为最后一个模块之一挂钩到其进程中
static void register_hooks(apr_pool_t *pool)
{
/* Create a hook in the request handler, so we get called when a request arrives */
ap_hook_handler(example_handler, NULL, NULL, APR_HOOK_LAST);
}
example_handler 引用是将处理请求的函数。我们将在下一章讨论如何创建处理程序。
挂钩到请求处理阶段只是您可以创建的众多挂钩之一。其他一些挂钩方法是
ap_hook_child_init:放置一个在子进程生成时执行的挂钩(通常用于在服务器分叉后初始化模块)ap_hook_pre_config:放置一个在读取任何配置数据之前执行的挂钩(非常早期的挂钩)ap_hook_post_config:放置一个在解析配置后但服务器分叉之前执行的挂钩ap_hook_pre_translate_name:放置一个在解码之前执行的挂钩,当 URI 需要被转换为服务器上的文件名时ap_hook_translate_name:放置一个在 URI 需要被转换为服务器上的文件名时执行的挂钩(想想 mod_rewrite)ap_hook_quick_handler:类似于 ap_hook_handler,但它在任何其他请求挂钩(翻译、身份验证、修复等)之前运行ap_hook_log_transaction:放置一个在服务器即将添加当前请求的日志条目时执行的挂钩处理程序本质上是一个函数,当向服务器发出请求时,它会收到一个回调。它传递一个当前请求的记录(它是如何发出的,传递了哪些头文件和请求,谁发出了请求等等),并负责告诉服务器它对请求不感兴趣,或者使用提供的工具处理请求。
让我们从创建一个非常简单的请求处理程序开始,它执行以下操作
text/html在 C 代码中,我们的示例处理程序现在将如下所示
static int example_handler(request_rec *r)
{
/* First off, we need to check if this is a call for the "example-handler" handler.
* If it is, we accept it and do our things, if not, we simply return DECLINED,
* and the server will try somewhere else.
*/
if (!r->handler || strcmp(r->handler, "example-handler")) return (DECLINED);
/* Now that we are handling this request, we'll write out "Hello, world!" to the client.
* To do so, we must first set the appropriate content type, followed by our output.
*/
ap_set_content_type(r, "text/html");
ap_rprintf(r, "Hello, world!");
/* Lastly, we must tell the server that we took care of this request and everything went fine.
* We do so by simply returning the value OK to the server.
*/
return OK;
}
现在,我们将我们学到的所有东西放在一起,最终得到一个类似于 mod_example_1.c 的程序。本示例中使用的函数将在后面的 "您应该了解的一些有用函数" 部分进行解释。
任何请求中最重要的部分是请求记录。在对处理程序函数的调用中,这由与每次调用一起传递的 request_rec* 结构表示。这个结构,通常在模块中简称为 r,包含您的模块完全处理任何 HTTP 请求并相应地做出响应所需的所有信息。
request_rec 结构的一些关键元素是
r->handler (char*): 包含服务器当前要求处理此请求的处理程序的名称r->method (char*): 包含正在使用的 HTTP 方法,例如 GET 或 POSTr->filename (char*): 包含客户端请求的已转换文件名r->args (char*): 包含请求的查询字符串(如果有)r->headers_in (apr_table_t*): 包含客户端发送的所有头文件r->connection (conn_rec*): 包含有关当前连接的信息的记录r->user (char*): 如果 URI 需要身份验证,则将其设置为提供的用户名r->useragent_ip (char*): 连接到我们的客户端的 IP 地址r->pool (apr_pool_t*):此请求的内存池。我们将在“内存管理”一章中讨论这一点。可以在 httpd.h 头文件中或 http://ci.apache.org/projects/httpd/trunk/doxygen/structrequest__rec.html 上找到 request_rec 结构中包含的所有值的完整列表。
让我们在另一个示例处理程序中尝试使用其中一些变量
static int example_handler(request_rec *r)
{
/* Set the appropriate content type */
ap_set_content_type(r, "text/html");
/* Print out the IP address of the client connecting to us: */
ap_rprintf(r, "<h2>Hello, %s!</h2>", r->useragent_ip);
/* If we were reached through a GET or a POST request, be happy, else sad. */
if ( !strcmp(r->method, "POST") || !strcmp(r->method, "GET") ) {
ap_rputs("You used a GET or a POST method, that makes us happy!<br/>", r);
}
else {
ap_rputs("You did not use POST or GET, that makes us sad :(<br/>", r);
}
/* Lastly, if there was a query string, let's print that too! */
if (r->args) {
ap_rprintf(r, "Your query string was: %s", r->args);
}
return OK;
}
Apache 依赖于处理程序的返回值来表示请求是否已处理,以及如果已处理,请求是否成功。如果模块对处理特定请求不感兴趣,它应该始终返回 DECLINED 值。如果它正在处理请求,它应该返回通用值 OK,或者返回特定的 HTTP 状态代码,例如
static int example_handler(request_rec *r)
{
/* Return 404: Not found */
return HTTP_NOT_FOUND;
}
返回 OK 或 HTTP 状态码并不一定意味着请求将结束。服务器可能仍然有其他处理程序对该请求感兴趣,例如日志模块,在成功请求后,它将记录请求内容和执行过程的摘要。要完全停止并阻止模块完成后的任何进一步处理,可以返回 DONE 值,让服务器知道它应该停止对该请求的所有活动并继续下一个请求,而无需通知其他处理程序。
通用响应代码
DECLINED: 我们不处理此请求OK: 我们处理了此请求,并且执行顺利DONE: 我们处理了此请求,服务器应该直接关闭此线程,无需进一步处理HTTP 特定返回代码(摘录)
HTTP_OK (200): 请求正常HTTP_MOVED_PERMANENTLY (301): 资源已移至新 URLHTTP_UNAUTHORIZED (401): 客户端无权访问此页面HTTP_FORBIDDEN (403): 权限被拒绝HTTP_NOT_FOUND (404): 文件未找到HTTP_INTERNAL_SERVER_ERROR (500): 内部服务器错误(不言自明)ap_rputs(const char *string, request_rec *r): ap_rputs("Hello, world!", r);
ap_rprintf: printf 类似,只是它将结果发送到客户端。ap_rprintf(r, "Hello, %s!", r->useragent_ip);
ap_set_content_type(request_rec *r, const char *type): ap_set_content_type(r, "text/plain"); /* force a raw text output */
由于内存池系统,在 Apache HTTP Server 2.4 中管理您的资源非常容易。本质上,每个服务器、连接和请求都有自己的内存池,当其作用域结束时(例如,当请求完成或服务器进程关闭时)会清理。您的模块只需要连接到此内存池,您就不必担心清理工作 - 很棒,对吧?
在我们的模块中,我们将主要为每个请求分配内存,因此在创建新对象时使用 r->pool 引用是合适的。在池中分配内存的一些函数是
void* apr_palloc( apr_pool_t *p, apr_size_t size): 在池中为您分配 size 个字节的内存void* apr_pcalloc( apr_pool_t *p, apr_size_t size): 在池中为您分配 size 个字节的内存,并将所有字节设置为 0char* apr_pstrdup( apr_pool_t *p, const char *s): 创建字符串 s 的副本。这对于复制常量值很有用,以便您可以编辑它们char* apr_psprintf( apr_pool_t *p, const char *fmt, ...): 与 sprintf 类似,只是服务器为您提供了一个适当分配的目标变量让我们将这些函数放入一个示例处理程序中
static int example_handler(request_rec *r)
{
const char *original = "You can't edit this!";
char *copy;
int *integers;
/* Allocate space for 10 integer values and set them all to zero. */
integers = apr_pcalloc(r->pool, sizeof(int)*10);
/* Create a copy of the 'original' variable that we can edit. */
copy = apr_pstrdup(r->pool, original);
return OK;
}
这对我们的模块来说很好,它不需要任何预初始化的变量或结构。但是,如果我们想在请求开始之前尽早初始化一些内容,我们可以简单地在我们的 register_hooks 函数中添加对函数的调用来解决它
static void register_hooks(apr_pool_t *pool)
{
/* Call a function that initializes some stuff */
example_init_function(pool);
/* Create a hook in the request handler, so we get called when a request arrives */
ap_hook_handler(example_handler, NULL, NULL, APR_HOOK_LAST);
}
在这个请求前初始化函数中,我们不会使用与为基于请求的函数分配资源时相同的池。相反,我们将使用服务器为我们提供的池,以便在每个进程级别分配内存。
在我们的示例模块中,我们想添加一个功能,它检查客户端想要查看哪种类型的摘要,MD5 或 SHA1。这可以通过向请求添加查询字符串来解决。查询字符串通常由几个键值对组成,这些键值对组合成一个字符串,例如 valueA=yes&valueB=no&valueC=maybe。模块本身负责解析这些键值对并获取所需的数据。在我们的示例中,我们将查找名为 digest 的键,如果设置为 md5,我们将生成 MD5 摘要,否则我们将生成 SHA1 摘要。
自从 Apache HTTP Server 2.4 发布以来,解析来自 GET 和 POST 请求的请求数据从未如此简单。解析 GET 和 POST 数据所需的一切只有四行简单代码
apr_table_t *GET; apr_array_header_t*POST; ap_args_to_table(r, &GET); ap_parse_form_data(r, NULL, &POST, -1, 8192);
在我们的特定示例模块中,我们正在查找查询字符串中的 digest 值,该值现在位于名为 GET 的表中。要提取此值,我们只需要执行一个简单的操作
/* Get the "digest" key from the query string, if any. */ const char *digestType = apr_table_get(GET, "digest"); /* If no key was returned, we will set a default value instead. */ if (!digestType) digestType = "sha1";
用于 POST 和 GET 数据的结构并不完全相同,因此如果我们要从 POST 数据而不是查询字符串中获取值,我们将不得不使用更多行代码,如本文档最后一章中的 此示例 中所述。
现在我们已经了解了如何解析表单数据和管理我们的资源,我们可以继续创建我们模块的高级版本,该版本可以输出文件的 MD5 或 SHA1 摘要
static int example_handler(request_rec *r)
{
int rc, exists;
apr_finfo_t finfo;
apr_file_t *file;
char *filename;
char buffer[256];
apr_size_t readBytes;
int n;
apr_table_t *GET;
apr_array_header_t *POST;
const char *digestType;
/* Check that the "example-handler" handler is being called. */
if (!r->handler || strcmp(r->handler, "example-handler")) return (DECLINED);
/* Figure out which file is being requested by removing the .sum from it */
filename = apr_pstrdup(r->pool, r->filename);
filename[strlen(filename)-4] = 0; /* Cut off the last 4 characters. */
/* Figure out if the file we request a sum on exists and isn't a directory */
rc = apr_stat(&finfo, filename, APR_FINFO_MIN, r->pool);
if (rc == APR_SUCCESS) {
exists =
(
(finfo.filetype != APR_NOFILE)
&& !(finfo.filetype & APR_DIR)
);
if (!exists) return HTTP_NOT_FOUND; /* Return a 404 if not found. */
}
/* If apr_stat failed, we're probably not allowed to check this file. */
else return HTTP_FORBIDDEN;
/* Parse the GET and, optionally, the POST data sent to us */
ap_args_to_table(r, &GET);
ap_parse_form_data(r, NULL, &POST, -1, 8192);
/* Set the appropriate content type */
ap_set_content_type(r, "text/html");
/* Print a title and some general information */
ap_rprintf(r, "<h2>Information on %s:</h2>", filename);
ap_rprintf(r, "<b>Size:</b> %u bytes<br/>", finfo.size);
/* Get the digest type the client wants to see */
digestType = apr_table_get(GET, "digest");
if (!digestType) digestType = "MD5";
rc = apr_file_open(&file, filename, APR_READ, APR_OS_DEFAULT, r->pool);
if (rc == APR_SUCCESS) {
/* Are we trying to calculate the MD5 or the SHA1 digest? */
if (!strcasecmp(digestType, "md5")) {
/* Calculate the MD5 sum of the file */
union {
char chr[16];
uint32_t num[4];
} digest;
apr_md5_ctx_t md5;
apr_md5_init(&md5);
readBytes = 256;
while ( apr_file_read(file, buffer, &readBytes) == APR_SUCCESS ) {
apr_md5_update(&md5, buffer, readBytes);
}
apr_md5_final(digest.chr, &md5);
/* Print out the MD5 digest */
ap_rputs("<b>MD5: </b><code>", r);
for (n = 0; n < APR_MD5_DIGESTSIZE/4; n++) {
ap_rprintf(r, "%08x", digest.num[n]);
}
ap_rputs("</code>", r);
/* Print a link to the SHA1 version */
ap_rputs("<br/><a href='?digest=sha1'>View the SHA1 hash instead</a>", r);
}
else {
/* Calculate the SHA1 sum of the file */
union {
char chr[20];
uint32_t num[5];
} digest;
apr_sha1_ctx_t sha1;
apr_sha1_init(&sha1);
readBytes = 256;
while ( apr_file_read(file, buffer, &readBytes) == APR_SUCCESS ) {
apr_sha1_update(&sha1, buffer, readBytes);
}
apr_sha1_final(digest.chr, &sha1);
/* Print out the SHA1 digest */
ap_rputs("<b>SHA1: </b><code>", r);
for (n = 0; n < APR_SHA1_DIGESTSIZE/4; n++) {
ap_rprintf(r, "%08x", digest.num[n]);
}
ap_rputs("</code>", r);
/* Print a link to the MD5 version */
ap_rputs("<br/><a href='?digest=md5'>View the MD5 hash instead</a>", r);
}
apr_file_close(file);
}
/* Let the server know that we responded to this request. */
return OK;
}
此版本的完整内容可以在此处找到:mod_example_2.c.
在本节文档中,我们将把目光从摘要模块转移到创建一个新的示例模块,它的唯一功能是写出它自己的配置。这样做是为了检查服务器如何处理配置,以及当您开始为模块编写高级配置时会发生什么。
如果您正在阅读本文,那么您可能已经知道配置指令是什么。简而言之,指令是一种告诉单个模块(或一组模块)如何执行的方式,例如这些指令控制 mod_rewrite 的工作方式
RewriteEngine On
RewriteCond "%{REQUEST_URI}" "^/foo/bar"
RewriteRule "^/foo/bar/(.*)$" "/foobar?page=$1"
每个配置指令都由一个单独的函数处理,该函数解析给定的参数并相应地设置配置。
首先,我们将在 C 空间中创建一个基本配置
typedef struct {
int enabled; /* Enable or disable our module */
const char *path; /* Some path to...something */
int typeOfAction; /* 1 means action A, 2 means action B and so on */
} example_config;
现在,让我们通过创建一个非常小的模块来将它放到实际应用中,该模块只打印出一个硬编码的配置。您会注意到我们使用 register_hooks 函数将配置值初始化为默认值
typedef struct {
int enabled; /* Enable or disable our module */
const char *path; /* Some path to...something */
int typeOfAction; /* 1 means action A, 2 means action B and so on */
} example_config;
static example_config config;
static int example_handler(request_rec *r)
{
if (!r->handler || strcmp(r->handler, "example-handler")) return(DECLINED);
ap_set_content_type(r, "text/plain");
ap_rprintf(r, "Enabled: %u\n", config.enabled);
ap_rprintf(r, "Path: %s\n", config.path);
ap_rprintf(r, "TypeOfAction: %x\n", config.typeOfAction);
return OK;
}
static void register_hooks(apr_pool_t *pool)
{
config.enabled = 1;
config.path = "/foo/bar";
config.typeOfAction = 0x00;
ap_hook_handler(example_handler, NULL, NULL, APR_HOOK_LAST);
}
/* Define our module as an entity and assign a function for registering hooks */
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
NULL, /* Per-directory configuration handler */
NULL, /* Merge handler for per-directory configurations */
NULL, /* Per-server configuration handler */
NULL, /* Merge handler for per-server configurations */
NULL, /* Any directives we may have for httpd */
register_hooks /* Our hook registering function */
};
到目前为止一切顺利。要访问我们的新处理程序,我们可以在我们的配置中添加以下内容
<Location "/example">
SetHandler example-handler
</Location>
当我们访问时,我们将看到我们的模块输出当前配置。
如果我们想更改我们的配置,不是通过在模块中硬编码新值,而是通过使用 httpd.conf 文件或可能是 .htaccess 文件,该怎么办?现在是让服务器知道我们希望这成为可能的时候了。为此,我们必须首先更改我们的 *名称标签* 以包含对我们想要向服务器注册的配置指令的引用
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
NULL, /* Per-directory configuration handler */
NULL, /* Merge handler for per-directory configurations */
NULL, /* Per-server configuration handler */
NULL, /* Merge handler for per-server configurations */
example_directives, /* Any directives we may have for httpd */
register_hooks /* Our hook registering function */
};
这将告诉服务器我们现在接受来自配置文件的指令,并且名为 example_directives 的结构包含有关我们的指令是什么以及它们如何工作的的信息。由于我们的模块配置中有三个不同的变量,我们将添加一个包含三个指令和一个 NULL 的结构
static const command_rec example_directives[] =
{
AP_INIT_TAKE1("exampleEnabled", example_set_enabled, NULL, RSRC_CONF, "Enable or disable mod_example"),
AP_INIT_TAKE1("examplePath", example_set_path, NULL, RSRC_CONF, "The path to whatever"),
AP_INIT_TAKE2("exampleAction", example_set_action, NULL, RSRC_CONF, "Special action value!"),
{ NULL }
};

如您所见,每个指令至少需要设置 5 个参数
AP_INIT_TAKE1: 这是一个宏,告诉服务器该指令只接受一个参数。如果我们需要两个参数,我们可以使用宏 AP_INIT_TAKE2 等等(有关更多宏,请参阅 httpd_conf.h)。exampleEnabled: 这是我们指令的名称。更准确地说,这是用户必须在他们的配置中输入的内容,以便在我们的模块中调用配置更改。example_set_enabled: 这是一个指向 C 函数的引用,该函数解析指令并相应地设置配置。我们将在下一段中讨论如何创建它。RSRC_CONF: 这告诉服务器指令在何处被允许。我们将在后面的章节中详细介绍此值,但现在,RSRC_CONF 意味着服务器只会在服务器上下文中接受这些指令。"Enable or disable....": 这只是一个简短的描述,说明指令的作用。(我们定义中“缺失”的参数通常设置为 NULL,是一个可选函数,可以在解析参数的初始函数运行后运行。这通常被省略,因为验证参数的函数也可以用来设置它们。)
现在我们已经告诉服务器期望我们模块的一些指令,是时候为这些指令创建一些函数了。服务器在配置文件中读取的是文本,因此自然地,它传递给我们的指令处理程序的是一个或多个字符串,我们需要自己识别并对其进行操作。您会注意到,由于我们将 exampleAction 指令设置为接受两个参数,因此它的 C 函数也定义了一个额外的参数
/* Handler for the "exampleEnabled" directive */
const char *example_set_enabled(cmd_parms *cmd, void *cfg, const char *arg)
{
if(!strcasecmp(arg, "on")) config.enabled = 1;
else config.enabled = 0;
return NULL;
}
/* Handler for the "examplePath" directive */
const char *example_set_path(cmd_parms *cmd, void *cfg, const char *arg)
{
config.path = arg;
return NULL;
}
/* Handler for the "exampleAction" directive */
/* Let's pretend this one takes one argument (file or db), and a second (deny or allow), */
/* and we store it in a bit-wise manner. */
const char *example_set_action(cmd_parms *cmd, void *cfg, const char *arg1, const char *arg2)
{
if(!strcasecmp(arg1, "file")) config.typeOfAction = 0x01;
else config.typeOfAction = 0x02;
if(!strcasecmp(arg2, "deny")) config.typeOfAction += 0x10;
else config.typeOfAction += 0x20;
return NULL;
}
现在我们已经设置了指令,并为它们配置了处理程序,我们可以将我们的模块组装成一个大文件
/* mod_example_config_simple.c: */
#include <stdio.h>
#include "apr_hash.h"
#include "ap_config.h"
#include "ap_provider.h"
#include "httpd.h"
#include "http_core.h"
#include "http_config.h"
#include "http_log.h"
#include "http_protocol.h"
#include "http_request.h"
/*
==============================================================================
Our configuration prototype and declaration:
==============================================================================
*/
typedef struct {
int enabled; /* Enable or disable our module */
const char *path; /* Some path to...something */
int typeOfAction; /* 1 means action A, 2 means action B and so on */
} example_config;
static example_config config;
/*
==============================================================================
Our directive handlers:
==============================================================================
*/
/* Handler for the "exampleEnabled" directive */
const char *example_set_enabled(cmd_parms *cmd, void *cfg, const char *arg)
{
if(!strcasecmp(arg, "on")) config.enabled = 1;
else config.enabled = 0;
return NULL;
}
/* Handler for the "examplePath" directive */
const char *example_set_path(cmd_parms *cmd, void *cfg, const char *arg)
{
config.path = arg;
return NULL;
}
/* Handler for the "exampleAction" directive */
/* Let's pretend this one takes one argument (file or db), and a second (deny or allow), */
/* and we store it in a bit-wise manner. */
const char *example_set_action(cmd_parms *cmd, void *cfg, const char *arg1, const char *arg2)
{
if(!strcasecmp(arg1, "file")) config.typeOfAction = 0x01;
else config.typeOfAction = 0x02;
if(!strcasecmp(arg2, "deny")) config.typeOfAction += 0x10;
else config.typeOfAction += 0x20;
return NULL;
}
/*
==============================================================================
The directive structure for our name tag:
==============================================================================
*/
static const command_rec example_directives[] =
{
AP_INIT_TAKE1("exampleEnabled", example_set_enabled, NULL, RSRC_CONF, "Enable or disable mod_example"),
AP_INIT_TAKE1("examplePath", example_set_path, NULL, RSRC_CONF, "The path to whatever"),
AP_INIT_TAKE2("exampleAction", example_set_action, NULL, RSRC_CONF, "Special action value!"),
{ NULL }
};
/*
==============================================================================
Our module handler:
==============================================================================
*/
static int example_handler(request_rec *r)
{
if(!r->handler || strcmp(r->handler, "example-handler")) return(DECLINED);
ap_set_content_type(r, "text/plain");
ap_rprintf(r, "Enabled: %u\n", config.enabled);
ap_rprintf(r, "Path: %s\n", config.path);
ap_rprintf(r, "TypeOfAction: %x\n", config.typeOfAction);
return OK;
}
/*
==============================================================================
The hook registration function (also initializes the default config values):
==============================================================================
*/
static void register_hooks(apr_pool_t *pool)
{
config.enabled = 1;
config.path = "/foo/bar";
config.typeOfAction = 3;
ap_hook_handler(example_handler, NULL, NULL, APR_HOOK_LAST);
}
/*
==============================================================================
Our module name tag:
==============================================================================
*/
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
NULL, /* Per-directory configuration handler */
NULL, /* Merge handler for per-directory configurations */
NULL, /* Per-server configuration handler */
NULL, /* Merge handler for per-server configurations */
example_directives, /* Any directives we may have for httpd */
register_hooks /* Our hook registering function */
};
在我们的 httpd.conf 文件中,我们现在可以通过添加几行来更改硬编码的配置
ExampleEnabled On ExamplePath "/usr/bin/foo" ExampleAction file allow
因此,我们应用了配置,访问了我们网站上的 /example,我们看到配置已经适应了我们在配置文件中写入的内容。
在 Apache HTTP Server 2.4 中,不同的 URL、虚拟主机、目录等对服务器用户可能具有非常不同的含义,因此模块必须在不同的上下文中运行。例如,假设您为 mod_rewrite 设置了以下配置
<Directory "/var/www">
RewriteCond "%{HTTP_HOST}" "^example.com$"
RewriteRule "(.*)" "http://www.example.com/$1"
</Directory>
<Directory "/var/www/sub">
RewriteRule "^foobar$" "index.php?foobar=true"
</Directory>
在此示例中,您将为 mod_rewrite 设置了两个不同的上下文
/var/www 中,所有针对 http://example.com 的请求都必须转到 http://www.example.com/var/www/sub 中,所有针对 foobar 的请求都必须转到 index.php?foobar=true如果 mod_rewrite(或者整个服务器)没有上下文感知能力,那么这些重写规则将应用于所有请求,无论它们是在哪里以及如何发起的。但由于该模块可以从服务器直接获取特定于上下文的配置,因此它不需要自己知道哪些指令在当前上下文中有效,因为服务器会处理这个问题。
那么,模块如何获取服务器、目录或位置的特定配置呢?它通过一个简单的调用来实现。
example_config *config = (example_config*) ap_get_module_config(r->per_dir_config, &example_module);
就是这样!当然,在幕后还有很多事情要做,我们将在本章中讨论这些事情,从服务器如何了解我们的配置以及如何在特定上下文中进行设置开始。
在本章中,我们将使用我们之前上下文结构的略微修改版本。我们将设置一个 context 变量,我们可以使用它来跟踪服务器在不同位置使用哪个上下文配置。
typedef struct {
char context[256];
char path[256];
int typeOfAction;
int enabled;
} example_config;
我们处理请求的处理程序也将被修改,但仍然非常简单。
static int example_handler(request_rec *r)
{
if(!r->handler || strcmp(r->handler, "example-handler")) return(DECLINED);
example_config *config = (example_config*) ap_get_module_config(r->per_dir_config, &example_module);
ap_set_content_type(r, "text/plain");
ap_rprintf("Enabled: %u\n", config->enabled);
ap_rprintf("Path: %s\n", config->path);
ap_rprintf("TypeOfAction: %x\n", config->typeOfAction);
ap_rprintf("Context: %s\n", config->context);
return OK;
}
在我们开始使模块具有上下文感知能力之前,我们必须首先定义我们将接受哪些上下文。正如我们在上一章中看到的,定义指令需要设置五个元素。
AP_INIT_TAKE1("exampleEnabled", example_set_enabled, NULL, RSRC_CONF, "Enable or disable mod_example"),
RSRC_CONF 定义告诉服务器我们只允许在全局服务器上下文中使用此指令,但由于我们现在正在尝试使用具有上下文感知能力的模块版本,因此我们应该将其设置为更宽松的值,即 ACCESS_CONF,它允许我们在 <Directory> 和 <Location> 块内使用该指令。为了更好地控制指令的放置,您可以将以下限制组合在一起以形成一个特定的规则。
RSRC_CONF:允许在 .conf 文件(而不是 .htaccess)中使用,位于 <Directory> 或 <Location> 之外。ACCESS_CONF:允许在 .conf 文件(而不是 .htaccess)中使用,位于 <Directory> 或 <Location> 之内。OR_OPTIONS:当设置 AllowOverride Options 时,允许在 .conf 文件和 .htaccess 中使用。OR_FILEINFO:当设置 AllowOverride FileInfo 时,允许在 .conf 文件和 .htaccess 中使用。OR_AUTHCFG:当设置 AllowOverride AuthConfig 时,允许在 .conf 文件和 .htaccess 中使用。OR_INDEXES:当设置 AllowOverride Indexes 时,允许在 .conf 文件和 .htaccess 中使用。OR_ALL:允许在 .conf 文件和 .htaccess 中的任何位置使用。管理配置的一个更智能的方法是让服务器帮助您创建它们。为此,我们必须首先更改我们的 名称标签,让服务器知道它应该帮助我们创建和管理我们的配置。由于我们为模块配置选择了每个目录(或每个位置)的上下文,因此我们将在标签中添加一个每个目录的创建者和合并函数引用。
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
create_dir_conf, /* Per-directory configuration handler */
merge_dir_conf, /* Merge handler for per-directory configurations */
NULL, /* Per-server configuration handler */
NULL, /* Merge handler for per-server configurations */
directives, /* Any directives we may have for httpd */
register_hooks /* Our hook registering function */
};
现在我们已经告诉服务器帮助我们创建和管理配置,我们的第一步是创建一个用于创建新的空白配置的函数。我们通过创建我们在名称标签中引用的函数作为每个目录的配置处理程序来实现。
void *create_dir_conf(apr_pool_t *pool, char *context) {
context = context ? context : "(undefined context)";
example_config *cfg = apr_pcalloc(pool, sizeof(example_config));
if(cfg) {
/* Set some default values */
strcpy(cfg->context, context);
cfg->enabled = 0;
cfg->path = "/foo/bar";
cfg->typeOfAction = 0x11;
}
return cfg;
}
创建具有上下文感知能力的配置的下一步是合并配置。此过程的这一部分特别适用于您拥有父配置和子配置的情况,例如以下情况。
<Directory "/var/www">
ExampleEnabled On
ExamplePath "/foo/bar"
ExampleAction file allow
</Directory>
<Directory "/var/www/subdir">
ExampleAction file deny
</Directory>
在这个例子中,自然地假设目录 /var/www/subdir 应该继承为 /var/www 目录设置的值,因为我们没有为该目录指定 ExampleEnabled 或 ExamplePath。服务器不会假定知道这是否属实,但巧妙地执行以下操作。
/var/www 创建一个新的配置。/var/www 给出的指令设置配置值。/var/www/subdir 创建一个新的配置。/var/www/subdir 给出的指令设置配置值。/var/www/subdir 的一个新配置中。此建议由我们在名称标签中引用的 merge_dir_conf 函数处理。此函数的目的是评估这两个配置并决定如何合并它们。
void *merge_dir_conf(apr_pool_t *pool, void *BASE, void *ADD) {
example_config *base = (example_config *) BASE ; /* This is what was set in the parent context */
example_config *add = (example_config *) ADD ; /* This is what is set in the new context */
example_config *conf = (example_config *) create_dir_conf(pool, "Merged configuration"); /* This will be the merged configuration */
/* Merge configurations */
conf->enabled = ( add->enabled == 0 ) ? base->enabled : add->enabled ;
conf->typeOfAction = add->typeOfAction ? add->typeOfAction : base->typeOfAction;
strcpy(conf->path, strlen(add->path) ? add->path : base->path);
return conf ;
}
现在,让我们尝试将所有内容整合在一起,创建一个新的具有上下文感知能力的模块。首先,我们将创建一个配置,让我们测试模块的工作原理。
<Location "/a">
SetHandler example-handler
ExampleEnabled on
ExamplePath "/foo/bar"
ExampleAction file allow
</Location>
<Location "/a/b">
ExampleAction file deny
ExampleEnabled off
</Location>
<Location "/a/b/c">
ExampleAction db deny
ExamplePath "/foo/bar/baz"
ExampleEnabled on
</Location>
然后我们将组装我们的模块代码。请注意,由于我们现在在处理程序中使用名称标签作为引用来获取配置,因此我添加了一些原型以使编译器满意。
/*$6
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* mod_example_config.c
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
#include <stdio.h>
#include "apr_hash.h"
#include "ap_config.h"
#include "ap_provider.h"
#include "httpd.h"
#include "http_core.h"
#include "http_config.h"
#include "http_log.h"
#include "http_protocol.h"
#include "http_request.h"
/*$1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Configuration structure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
typedef struct
{
char context[256];
char path[256];
int typeOfAction;
int enabled;
} example_config;
/*$1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Prototypes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
static int example_handler(request_rec *r);
const char *example_set_enabled(cmd_parms *cmd, void *cfg, const char *arg);
const char *example_set_path(cmd_parms *cmd, void *cfg, const char *arg);
const char *example_set_action(cmd_parms *cmd, void *cfg, const char *arg1, const char *arg2);
void *create_dir_conf(apr_pool_t *pool, char *context);
void *merge_dir_conf(apr_pool_t *pool, void *BASE, void *ADD);
static void register_hooks(apr_pool_t *pool);
/*$1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Configuration directives
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
static const command_rec directives[] =
{
AP_INIT_TAKE1("exampleEnabled", example_set_enabled, NULL, ACCESS_CONF, "Enable or disable mod_example"),
AP_INIT_TAKE1("examplePath", example_set_path, NULL, ACCESS_CONF, "The path to whatever"),
AP_INIT_TAKE2("exampleAction", example_set_action, NULL, ACCESS_CONF, "Special action value!"),
{ NULL }
};
/*$1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Our name tag
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
module AP_MODULE_DECLARE_DATA example_module =
{
STANDARD20_MODULE_STUFF,
create_dir_conf, /* Per-directory configuration handler */
merge_dir_conf, /* Merge handler for per-directory configurations */
NULL, /* Per-server configuration handler */
NULL, /* Merge handler for per-server configurations */
directives, /* Any directives we may have for httpd */
register_hooks /* Our hook registering function */
};
/*
=======================================================================================================================
Hook registration function
=======================================================================================================================
*/
static void register_hooks(apr_pool_t *pool)
{
ap_hook_handler(example_handler, NULL, NULL, APR_HOOK_LAST);
}
/*
=======================================================================================================================
Our example web service handler
=======================================================================================================================
*/
static int example_handler(request_rec *r)
{
if(!r->handler || strcmp(r->handler, "example-handler")) return(DECLINED);
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
example_config *config = (example_config *) ap_get_module_config(r->per_dir_config, &example_module);
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
ap_set_content_type(r, "text/plain");
ap_rprintf(r, "Enabled: %u\n", config->enabled);
ap_rprintf(r, "Path: %s\n", config->path);
ap_rprintf(r, "TypeOfAction: %x\n", config->typeOfAction);
ap_rprintf(r, "Context: %s\n", config->context);
return OK;
}
/*
=======================================================================================================================
Handler for the "exampleEnabled" directive
=======================================================================================================================
*/
const char *example_set_enabled(cmd_parms *cmd, void *cfg, const char *arg)
{
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
example_config *conf = (example_config *) cfg;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
if(conf)
{
if(!strcasecmp(arg, "on"))
conf->enabled = 1;
else
conf->enabled = 0;
}
return NULL;
}
/*
=======================================================================================================================
Handler for the "examplePath" directive
=======================================================================================================================
*/
const char *example_set_path(cmd_parms *cmd, void *cfg, const char *arg)
{
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
example_config *conf = (example_config *) cfg;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
if(conf)
{
strcpy(conf->path, arg);
}
return NULL;
}
/*
=======================================================================================================================
Handler for the "exampleAction" directive ;
Let's pretend this one takes one argument (file or db), and a second (deny or allow), ;
and we store it in a bit-wise manner.
=======================================================================================================================
*/
const char *example_set_action(cmd_parms *cmd, void *cfg, const char *arg1, const char *arg2)
{
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
example_config *conf = (example_config *) cfg;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
if(conf)
{
{
if(!strcasecmp(arg1, "file"))
conf->typeOfAction = 0x01;
else
conf->typeOfAction = 0x02;
if(!strcasecmp(arg2, "deny"))
conf->typeOfAction += 0x10;
else
conf->typeOfAction += 0x20;
}
}
return NULL;
}
/*
=======================================================================================================================
Function for creating new configurations for per-directory contexts
=======================================================================================================================
*/
void *create_dir_conf(apr_pool_t *pool, char *context)
{
context = context ? context : "Newly created configuration";
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
example_config *cfg = apr_pcalloc(pool, sizeof(example_config));
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
if(cfg)
{
{
/* Set some default values */
strcpy(cfg->context, context);
cfg->enabled = 0;
memset(cfg->path, 0, 256);
cfg->typeOfAction = 0x00;
}
}
return cfg;
}
/*
=======================================================================================================================
Merging function for configurations
=======================================================================================================================
*/
void *merge_dir_conf(apr_pool_t *pool, void *BASE, void *ADD)
{
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
example_config *base = (example_config *) BASE;
example_config *add = (example_config *) ADD;
example_config *conf = (example_config *) create_dir_conf(pool, "Merged configuration");
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
conf->enabled = (add->enabled == 0) ? base->enabled : add->enabled;
conf->typeOfAction = add->typeOfAction ? add->typeOfAction : base->typeOfAction;
strcpy(conf->path, strlen(add->path) ? add->path : base->path);
return conf;
}
我们现在已经了解了如何为 Apache HTTP Server 2.4 创建简单的模块并对其进行配置。您接下来做什么完全取决于您,但我希望您从阅读本文档中获得了一些有价值的东西。如果您对如何进一步开发模块有任何疑问,欢迎加入我们的 邮件列表 或查看我们其他文档以获取更多提示。
typedef struct {
const char *key;
const char *value;
} keyValuePair;
keyValuePair *readPost(request_rec *r) {
apr_array_header_t *pairs = NULL;
apr_off_t len;
apr_size_t size;
int res;
int i = 0;
char *buffer;
keyValuePair *kvp;
res = ap_parse_form_data(r, NULL, &pairs, -1, HUGE_STRING_LEN);
if (res != OK || !pairs) return NULL; /* Return NULL if we failed or if there are is no POST data */
kvp = apr_pcalloc(r->pool, sizeof(keyValuePair) * (pairs->nelts + 1));
while (pairs && !apr_is_empty_array(pairs)) {
ap_form_pair_t *pair = (ap_form_pair_t *) apr_array_pop(pairs);
apr_brigade_length(pair->value, 1, &len);
size = (apr_size_t) len;
buffer = apr_palloc(r->pool, size + 1);
apr_brigade_flatten(pair->value, buffer, &size);
buffer[len] = 0;
kvp[i].key = apr_pstrdup(r->pool, pair->name);
kvp[i].value = buffer;
i++;
}
return kvp;
}
static int example_handler(request_rec *r)
{
/*~~~~~~~~~~~~~~~~~~~~~~*/
keyValuePair *formData;
/*~~~~~~~~~~~~~~~~~~~~~~*/
formData = readPost(r);
if (formData) {
int i;
for (i = 0; &formData[i]; i++) {
if (formData[i].key && formData[i].value) {
ap_rprintf(r, "%s = %s\n", formData[i].key, formData[i].value);
} else if (formData[i].key) {
ap_rprintf(r, "%s\n", formData[i].key);
} else if (formData[i].value) {
ap_rprintf(r, "= %s\n", formData[i].value);
} else {
break;
}
}
}
return OK;
}
static int example_handler(request_rec *r)
{
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
const apr_array_header_t *fields;
int i;
apr_table_entry_t *e = 0;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
fields = apr_table_elts(r->headers_in);
e = (apr_table_entry_t *) fields->elts;
for(i = 0; i < fields->nelts; i++) {
ap_rprintf(r, "%s: %s\n", e[i].key, e[i].val);
}
return OK;
}
static int util_read(request_rec *r, const char **rbuf, apr_off_t *size)
{
/*~~~~~~~~*/
int rc = OK;
/*~~~~~~~~*/
if((rc = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR))) {
return(rc);
}
if(ap_should_client_block(r)) {
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
char argsbuffer[HUGE_STRING_LEN];
apr_off_t rsize, len_read, rpos = 0;
apr_off_t length = r->remaining;
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
*rbuf = (const char *) apr_pcalloc(r->pool, (apr_size_t) (length + 1));
*size = length;
while((len_read = ap_get_client_block(r, argsbuffer, sizeof(argsbuffer))) > 0) {
if((rpos + len_read) > length) {
rsize = length - rpos;
}
else {
rsize = len_read;
}
memcpy((char *) *rbuf + rpos, argsbuffer, (size_t) rsize);
rpos += rsize;
}
}
return(rc);
}
static int example_handler(request_rec *r)
{
/*~~~~~~~~~~~~~~~~*/
apr_off_t size;
const char *buffer;
/*~~~~~~~~~~~~~~~~*/
if(util_read(r, &buffer, &size) == OK) {
ap_rprintf(r, "We read a request body that was %" APR_OFF_T_FMT " bytes long", size);
}
return OK;
}
可用语言: en