Shambhala API 笔记

这些是关于 Shambhala API 和您需要处理的数据结构等的笔记。它们尚未完成,但希望它们能帮助您入门。

这里有一些关于一般教学风格的说明。为了简洁起见,这里的所有结构声明都是不完整的——真正的声明有更多槽位,但我没有告诉你。在大多数情况下,这些槽位被保留给服务器核心中的一个组件或另一个组件,并且应该谨慎地由模块修改。但是,在某些情况下,它们确实是我还没有完成的事情。欢迎来到前沿。

最后,这里有一个提纲,让您对即将出现的内容以及顺序有一个基本的了解。

基本概念。

我们从概述 Shambhala API 背后的基本概念以及它们在代码中的体现开始。

处理程序、模块和请求

Shambhala 将请求处理分解为一系列步骤,与 Netscape Server API 的方式大致相同(尽管 Shambhala 比 NetSite 多几个阶段,作为我认为将来可能有用的一些东西的钩子)。这些是这些阶段通过查看一系列模块中的每一个来处理,看看它们中的每一个是否具有该阶段的处理程序,并在有处理程序的情况下尝试调用它。处理程序通常可以执行以下三种操作之一大多数阶段由第一个处理它们的模块终止;但是,对于日志记录、“修复”和非访问身份验证检查,所有处理程序始终运行(除非出现错误)。此外,响应阶段是独一无二的,因为模块可以通过一个以请求对象的 MIME 类型为键的调度表来声明多个处理程序。模块可以声明一个可以处理任何请求的响应阶段处理程序,方法是赋予它键*/*(即,通配符 MIME 类型规范)。但是,只有在服务器已经尝试过并且无法为请求对象的 MIME 类型找到更具体的响应处理程序(要么不存在,要么它们都拒绝)时,才会调用通配符处理程序。

处理程序本身是单个参数(一个request_rec 结构,见下文)的函数,它返回一个整数,如上所述。

模块简要介绍

此时,我们需要解释模块的结构。我们的候选者将是其中一个比较混乱的模块,CGI 模块——它处理 CGI 脚本和ScriptAlias 配置文件命令。它实际上比大多数模块复杂得多,但如果我们只有一个例子,它最好是那个到处插手的例子。

让我们从处理程序开始。为了处理 CGI 脚本,该模块为它们声明了一个响应处理程序。由于ScriptAlias,它还具有名称转换阶段的处理程序(以识别ScriptAliased URI),类型检查阶段(任何ScriptAliased 请求都被类型化为 CGI 脚本)。

该模块需要维护一些每个(虚拟)服务器信息,即生效的ScriptAlias;因此,模块结构包含指向构建这些结构的函数的指针,以及指向组合其中两个结构的另一个函数的指针(如果主服务器和虚拟服务器都声明了ScriptAlias)。

最后,该模块包含处理ScriptAlias 命令本身的代码。这个特定的模块只声明了一个命令,但可能还有更多,因此模块具有命令表,它们声明它们的命令,并描述它们在何处被允许以及如何调用它们。

关于这些命令中一些参数的声明类型的最后说明:一个pool 是指向资源池结构的指针;这些由服务器用来跟踪已分配的内存、打开的文件等,要么用于服务特定请求,要么用于处理配置自身的过程。这样,当请求结束时(或者,对于配置池,当服务器重新启动时),内存可以被释放,文件可以被关闭,一次性完成,而无需任何人编写显式代码来跟踪所有这些文件并处置它们。此外,一个cmd_parms 结构包含有关正在读取的配置文件的各种信息以及其他状态信息,这些信息有时对处理配置文件命令(例如ScriptAlias)的函数有用。无需赘述,模块本身

/* Declarations of handlers. */

int translate_scriptalias (request_rec *);
int type_scriptalias (request_rec *);
int cgi_handler (request_rec *);

/* Subsdiary dispatch table for response-phase handlers, by MIME type */

handler_rec cgi_handlers[] = {
{ "application/x-httpd-cgi", cgi_handler },
{ NULL }
};

/* Declarations of routines to manipulate the module's configuration
 * info.  Note that these are returned, and passed in, as void *'s;
 * the server core keeps track of them, but it doesn't, and can't,
 * know their internal structure.
 */

void *make_cgi_server_config (pool *);
void *merge_cgi_server_config (pool *, void *, void *);

/* Declarations of routines to handle config-file commands */

char *script_alias (cmd_parms *, void *per_dir_config, char *fake, char *real);

command_rec cgi_cmds[] = {
{ "ScriptAlias", script_alias, NULL, RSRC_CONF, TAKE2,
    "a fakename and a realname"},
{ NULL }
};

module cgi_module = {
   STANDARD_MODULE_STUFF,
   NULL,			/* initializer */
   NULL,			/* dir config creater */
   NULL,			/* dir merger --- default is to override */
   make_cgi_server_config,      /* server config */
   merge_cgi_server_config,	/* merge server config */
   cgi_cmds,			/* command table */
   cgi_handlers,		/* handlers */
   translate_scriptalias,	/* filename translation */
   NULL,			/* check_user_id */
   NULL,			/* check auth */
   NULL,			/* check access */
   type_scriptalias,		/* type_checker */
   NULL,			/* fixups */
   NULL				/* logger */
};

处理程序的工作原理

处理程序的唯一参数是request_rec 结构。该结构描述了代表客户端向服务器发出的特定请求。在大多数情况下,每个与客户端的连接只生成一个request_rec 结构。

request_rec 简要介绍

request_rec 包含指向资源池的指针,该资源池将在服务器完成处理请求时被清除;指向包含每个服务器和每个连接信息的结构的指针,最重要的是,关于请求本身的信息。

最重要的此类信息是一组小的字符字符串,描述了正在请求的对象的属性,包括它的 URI、文件名、内容类型和内容编码(这些分别由处理请求的转换和类型检查处理程序填充)。

其他常用的数据项是表格,这些表格给出客户端原始请求的 MIME 标头、将与 pppp 响应一起发送回的 MIME 标头(模块可以随意添加),以及在服务请求过程中产生的任何子进程的环境变量。这些表格使用table_gettable_set 例程进行操作。

最后,还有指向两个数据结构的指针,这些数据结构反过来又指向每个模块的配置结构。具体来说,这些结构保存指向模块构建的数据结构的指针,这些数据结构描述了它在给定目录中(通过.htaccess 文件或<Directory> 部分)配置运行的方式,以及它在服务请求过程中构建的私有数据(因此模块的处理程序在一个阶段可以将“笔记”传递给它们在其他阶段的处理程序)。在request_rec 指向的server_rec 数据结构中还有另一个这样的配置向量,它包含每个(虚拟)服务器的配置数据。

这是一个简化的声明,给出了最常用的字段

struct request_rec {

  pool *pool;
  conn_rec *connection;
  server_rec *server;

  /* What object is being requested */
  
  char *uri;
  char *filename;
  char *path_info;
  char *args;			/* QUERY_ARGS, if any */
  struct stat finfo;		/* Set by server core;
                                 * st_mode set to zero if no such file */
  
  char *content_type;
  char *content_encoding;
  
  /* MIME header environments, in and out.  Also, an array containing
   * environment variables to be passed to subprocesses, so people can
   * write modules to add to that environment.
   *
   * The difference between headers_out and err_headers_out is that the
   * latter are printed even on error, and persist across internal redirects
   * (so the headers printed for ErrorDocument handlers will have them).
   */
  
  table *headers_in;
  table *headers_out;
  table *err_headers_out;
  table *subprocess_env;

  /* Info about the request itself... */
  
  int header_only;		/* HEAD request, as opposed to GET */
  char *protocol;		/* Protocol, as given to us, or HTTP/0.9 */
  char *method;			/* GET, HEAD, POST, etc. */
  int method_number;		/* M_GET, M_POST, etc. */

  /* Info for logging */

  char *the_request;
  int bytes_sent;

  /* A flag which modules can set, to indicate that the data being
   * returned is volatile, and clients should be told not to cache it.
   */

  int no_cache;

  /* Various other config info which may change with .htaccess files
   * These are config vectors, with one void* pointer for each module
   * (the thing pointed to being the module's business).
   */
  
  void *per_dir_config;		/* Options set in config files, etc. */
  void *request_config;		/* Notes on *this* request */
  
};

request_rec 结构的来源

大多数request_rec 结构都是通过从客户端读取 HTTP 请求并填充字段来构建的。但是,有一些例外

处理请求、拒绝和返回错误代码

如上所述,每个处理程序在被调用来处理特定request_rec 时,都必须返回一个int 来指示发生了什么。这可以是请注意,如果返回的错误代码是REDIRECT,那么模块应该在请求的headers_out 中放置一个Location,以指示客户端应该重定向哪里。

响应处理程序的特殊注意事项

大多数阶段的处理程序通过简单地设置request_rec 结构中的几个字段来完成工作(或者,对于访问检查器,只是通过返回正确的错误代码)。但是,响应处理程序必须实际将请求发送回客户端。

它们应该首先使用send_http_header 函数发送 HTTP 响应标头。(您不必执行任何特殊操作来跳过为 HTTP/0.9 请求发送标头;该函数会自行确定它不应该执行任何操作)。如果请求被标记为header_only,那么它们应该做的就是这些;它们应该在之后返回,而不尝试任何进一步的输出。

否则,它们应该生成一个请求主体,该主体以适当的方式响应客户端。用于此的原语是rputcrprintf,用于内部生成的输出,以及send_fd,用于将某个FILE * 的内容直接复制到客户端。

最后要考虑的一点是:在对客户端执行 I/O 时,可能会出现无限延迟。因此,在启动对客户端的 I/O 之前,务必设置超时。

此时,您应该或多或少地理解以下代码片段,它是处理没有更具体处理程序的GET 请求的处理程序;它还展示了如何处理条件GET,如果在特定响应处理程序中这样做是可取的。(pfopenpfclose 函数将返回的FILE * 绑定到资源池机制,因此即使请求被中止,它也会被关闭)。

int default_handler (request_rec *r)
{
    int errstatus;
    FILE *f;
    
    if (r->method_number != M_GET) return DECLINED;
    if (r->finfo.st_mode == 0) return NOT_FOUND;
	
    if ((errstatus = set_content_length (r, r->finfo.st_size))
	|| (errstatus = set_last_modified (r, r->finfo.st_mtime)))
        return errstatus;
    
    f = pfopen (r->pool, r->filename, "r");

    if (f == NULL) {
        log_reason("file permissions deny server access", r->filename, r);
        return FORBIDDEN;
    }
      
    register_timeout ("send", r);
    send_http_header (r);

    if (!r->header_only) {
	send_fd (f, r);
    }
    
    kill_timeout(r);
    pfclose (r->pool, f);
    return OK;
}
最后,如果所有这些都太具有挑战性,那么有几种方法可以解决。首先,如上所示,尚未生成任何输出的响应处理程序可以简单地返回一个错误代码,在这种情况下,服务器将自动生成错误响应。其次,它可以通过调用internal_redirect 来转交给其他处理程序,这就是上面讨论的内部重定向机制的调用方式。内部重定向的响应处理程序应该始终返回OK

(从不是响应处理程序的处理程序中调用internal_redirect会导致严重的混淆)。

身份验证处理程序的特殊注意事项

这里应该详细讨论的内容

日志记录处理程序的特殊注意事项

当请求内部重定向时,会存在一个问题,即记录什么。Shambhala 通过将整个重定向链捆绑到一个request_rec结构列表中来处理这个问题,这些结构通过r->prevr->next指针串联在一起。在这种情况下传递给日志处理程序的request_rec是最初为来自客户端的初始请求构建的;请注意,bytes_sent字段仅在链中的最后一个请求(实际发送响应的请求)中才是正确的。

资源分配和资源池

编写和设计服务器池服务器的问题之一是防止泄漏,即分配资源(内存、打开的文件等),而没有随后释放它们。资源池机制旨在防止这种情况。这里应该详细讨论的内容

配置、命令等

此服务器的设计目标之一是保持与 NCSA 1.3 服务器的外部兼容性 --- 也就是说,读取相同的配置文件,正确处理其中的所有指令,并且通常作为 NCSA 的直接替换。另一方面,另一个设计目标是将服务器的大部分功能移到模块中,这些模块与单片服务器核心尽可能少地关联。协调这些目标的唯一方法是将大多数命令的处理从中央服务器移到模块中。

但是,仅仅给模块命令表不足以将它们完全与服务器核心分离。服务器必须记住这些命令才能在以后对它们进行操作。这涉及维护对模块私有的数据,这些数据可以是每个服务器的,也可以是每个目录的。大多数内容都是每个目录的,特别是包括访问控制和授权信息,但也包括有关如何从后缀确定文件类型的信息,这些信息可以通过AddTypeDefaultType指令进行修改,等等。一般来说,管理哲学是,任何可以通过目录进行配置的内容都应该进行配置;每个服务器的信息通常用于标准模块集中的信息,例如Aliases 和Redirects,这些信息在请求绑定到底层文件系统中的特定位置之前就会生效。

模拟 NCSA 服务器的另一个要求是能够处理每个目录的配置文件,通常称为.htaccess文件,尽管即使在 NCSA 服务器中,它们也可以包含与访问控制完全无关的指令。因此,在 URI -> 文件名转换之后,但在执行任何其他阶段之前,服务器会沿着底层文件系统的目录层次结构向下遍历,遵循转换后的路径名,以读取可能存在的任何.htaccess文件。然后必须将读取的信息合并到来自服务器自身配置文件的适用信息中(来自access.conf中的<Directory>部分,或来自srm.conf中的默认值,实际上,对于大多数目的,它几乎与<Directory />完全相同)。

最后,在提供了一个涉及读取.htaccess文件的请求后,我们需要丢弃为处理它们而分配的存储空间。这与在其他类似问题出现的地方解决问题的方式相同,通过将这些结构绑定到每个事务的资源池中。

每个目录的配置结构

让我们看看所有这些如何在mod_mime.c中发挥作用,它定义了文件类型处理程序,该处理程序模拟 NCSA 服务器从后缀确定文件类型的方式。在这里,我们将看到实现AddTypeAddEncoding命令的代码。这些命令可以出现在.htaccess文件中,因此必须在模块的私有每个目录数据中进行处理,实际上,该数据由两个单独的table组成,用于 MIME 类型和编码信息,并声明如下
typedef struct {
    table *forced_types;	/* Additional AddTyped stuff */
    table *encoding_types;	/* Added with AddEncoding... */
} mime_dir_config;
当服务器读取配置文件或<Directory>部分(包括 MIME 模块的命令之一)时,它需要创建一个mime_dir_config结构,以便这些命令有东西可以操作。它通过调用在模块的“创建每个目录配置槽”中找到的函数来实现这一点,该函数有两个参数:此配置信息适用的目录的名称(或srm.confNULL),以及一个指向应该发生分配的资源池的指针。

(如果我们正在读取.htaccess文件,则该资源池是请求的每个请求资源池;否则,它是一个用于配置数据的资源池,并在重新启动时清除。无论哪种方式,创建的结构在池被清除时消失都很重要,如果需要,通过在池上注册清理操作)。

对于 MIME 模块,每个目录配置创建函数只是palloc上面的结构,并创建几个table来填充它。看起来像这样

void *create_mime_dir_config (pool *p, char *dummy)
{
    mime_dir_config *new =
      (mime_dir_config *) palloc (p, sizeof(mime_dir_config));

    new->forced_types = make_table (p, 4);
    new->encoding_types = make_table (p, 4);
    
    return new;
}
现在,假设我们刚刚读入了一个.htaccess文件。我们已经有了层次结构中下一个目录的每个目录配置结构。如果我们刚刚读入的.htaccess文件中没有AddTypeAddEncoding命令,则其每个目录配置结构对于 MIME 模块仍然有效,我们可以直接使用它。否则,我们需要以某种方式合并这两个结构。

为此,服务器会调用模块的每个目录配置合并函数(如果存在)。该函数接受三个参数:要合并的两个结构,以及一个用于分配结果的资源池。对于 MIME 模块,需要做的就是将来自新每个目录配置结构的表与来自父级的表覆盖

void *merge_mime_dir_configs (pool *p, void *parent_dirv, void *subdirv)
{
    mime_dir_config *parent_dir = (mime_dir_config *)parent_dirv;
    mime_dir_config *subdir = (mime_dir_config *)subdirv;
    mime_dir_config *new =
      (mime_dir_config *)palloc (p, sizeof(mime_dir_config));

    new->forced_types = overlay_tables (p, subdir->forced_types,
					parent_dir->forced_types);
    new->encoding_types = overlay_tables (p, subdir->encoding_types,
					  parent_dir->encoding_types);

    return new;
}
注意 --- 如果不存在每个目录合并函数,服务器将只使用子目录的配置信息,而忽略父级的配置信息。对于某些模块,这完全可以(例如,对于包含模块,其每个目录配置信息仅包含XBITHACK的状态),对于这些模块,您可以不声明一个,并将模块本身中的相应结构槽留空NULL

命令处理

现在我们有了这些结构,我们需要能够弄清楚如何填充它们。这涉及处理实际的AddTypeAddEncoding命令。为了找到命令,服务器会在模块的command table中查找。该表包含有关命令接受多少个参数以及以何种格式、在何处允许等信息。这些信息足以让服务器使用预解析的参数调用大多数命令处理函数。不用多说,让我们看看AddType命令处理程序,它看起来像这样(AddEncoding命令看起来基本相同,这里不会显示)
char *add_type(cmd_parms *cmd, mime_dir_config *m, char *ct, char *ext)
{
    if (*ext == '.') ++ext;
    table_set (m->forced_types, ext, ct);
    return NULL;
}
此命令处理程序非常简单。如您所见,它接受四个参数,其中两个是预解析的参数,第三个是所讨论模块的每个目录配置结构,第四个是指向cmd_parms结构的指针。该结构包含许多参数,这些参数经常对某些命令有用,但并非对所有命令都有用,包括一个资源池(从中可以分配内存,并将清理操作绑定到该资源池),以及正在配置的(虚拟)服务器,从中可以获取模块的每个服务器配置数据如果需要。

此特定命令处理程序非常简单的另一个原因是它不会遇到任何错误条件。如果有,它可以返回错误消息而不是NULL;这会导致在服务器的stderr上打印出错误消息,然后快速退出,如果它在主配置文件中;对于.htaccess文件,语法错误将记录在服务器错误日志中(以及它来自何处的指示),并且请求将被弹回,并显示服务器错误响应(HTTP 错误状态,代码 500)。

MIME 模块的命令表包含这些命令的条目,看起来像这样

command_rec mime_cmds[] = {
{ "AddType", add_type, NULL, OR_FILEINFO, TAKE2, 
    "a mime type followed by a file extension" },
{ "AddEncoding", add_encoding, NULL, OR_FILEINFO, TAKE2, 
    "an encoding (e.g., gzip), followed by a file extension" },
{ NULL }
};
这些表中的条目是最后,在完成所有这些设置后,我们必须使用它。这最终是在模块的处理程序中完成的,特别是对于其文件类型处理程序,它看起来或多或少像这样;请注意,每个目录配置结构是通过使用get_module_config函数从request_rec的每个目录配置向量中提取的。
int find_ct(request_rec *r)
{
    int i;
    char *fn = pstrdup (r->pool, r->filename);
    mime_dir_config *conf =
      (mime_dir_config *)get_module_config(r->per_dir_config, &mime_module);
    char *type;

    if (S_ISDIR(r->finfo.st_mode)) {
        r->content_type = DIR_MAGIC_TYPE;
	return OK;
    }
    
    if((i=rind(fn,'.')) < 0) return DECLINED;
    ++i;

    if ((type = table_get (conf->encoding_types, &fn[i])))
    {
        r->content_encoding = type;

	/* go back to previous extension to try to use it as a type */
	
        fn[i-1] = '\0';
	if((i=rind(fn,'.')) < 0) return OK;
	++i;
    }

    if ((type = table_get (conf->forced_types, &fn[i])))
    {
        r->content_type = type;
    }
    
    return OK;
}

旁注——每个服务器的配置、虚拟服务器等。

每个服务器模块配置的基本思想与每个目录配置的基本思想相同;存在一个创建函数和一个合并函数,后者在虚拟服务器部分覆盖基本服务器配置时被调用,并且必须计算一个组合结构。(与每个目录配置一样,如果未指定合并函数,并且在某个虚拟服务器中配置了模块,则默认情况下会简单地忽略基本配置)。

唯一的实质性区别是,当命令需要配置每个服务器的私有模块数据时,它需要转到cmd_parms数据才能访问它。以下是一个来自别名模块的示例,它也说明了如何返回语法错误(请注意,命令处理程序的每个目录配置参数被声明为一个虚拟参数,因为该模块实际上没有每个目录的配置数据)

char *add_redirect(cmd_parms *cmd, void *dummy, char *f, char *url)
{
    server_rec *s = cmd->server;
    alias_server_conf *conf =
        (alias_server_conf *)get_module_config(s->module_config,&alias_module);
    alias_entry *new = push_array (conf->redirects);

    if (!is_url (url)) return "Redirect to non-URL";
    
    new->fake = f; new->real = url;
    return NULL;
}