lighttpd - Plugin: CGI
本文是阅读lighttpd源代码时做的笔记,mod_cgi部分。这次(文字部分)直接从笔记软件复制过来(代码还是一段段嵌入),速度是快不少。缺点是字体难看了一些。
由于是第一遍读lighttpd,加上水平有限,难免会有些错误的地方,不吝赐教。
由于是第一遍读lighttpd,加上水平有限,难免会有些错误的地方,不吝赐教。
CGI和FastCGI的支持,是每个Web Server相当重要的功能。本章分析了Lighttpd CGI的实现,即mod_cgi。
1. Config CGI for lighttpd
CGI原理和如何写CGI不是本文的重点,不过配置CGI有所了解有助于理解mod_cgi的代码。要配置lighttpd来运行CGI进程,先要加载mod_cgi模块,在server.modules里面加入"mod_cgi";还要告诉WebServer,何时将数据的处理交给CGI进程,这个可以通过cgi.assign选项指定。
server.modules = (... "mod_cgi", ...) cgi.assign = ( ".pl" => "/usr/bin/perl", ".perl" => "/usr/bin/perl", 这个是我杜撰的后缀,没人用的吧 ".cgi" => "", 可以直接执行,无需解释器 ".py" => "/usr/bin/python" "/testfile" => "/usr/bin/mycgi" ) 无后缀
对于“.pl”,".cgi",".py"结尾的文档,都会视为CGI程序。后面紧跟而这些程序的解释器。
- 可以为不同的后缀文件指定相同的解释器,
- 可以不指定解释器(例如,C语言写的cgi),后者解释器部分为空字符串""。
- 如果URL的path部分没有指定扩展名而是路径+可执行文件,那么需要指定可执行文件的绝对路径。
将CGI归拢到一个目录“/cgi-bin”,并且支持此目录下的index文件是比较通用的做法,不过这里不打算涉及[如果好奇心太重,或者老板真的需要,一定要知道怎么弄,参考doc/outdated/cgi.txt,或者google吧])。
1.1 Example
着手弄个可以运行的例子很简单。在docroot下建两个文件,hello和hello.pl,并且有可执行权限。hello用C语言写(其实只要是可执行文件就行了,是脚本也可以),
#include <stdio.h> int main(int argc, char *argv[]) { printf("Content-Type: text/plain\r\n\r\n"); printf("hello, lighttpd (hello)"); return 0; }
hello.pl用Perl写,
#!/usr/bin/perl -w print "Content-Type: text/plain\r\n\r\n"; print "hello, lighttpd (hello.pl)";
配置文件如下,
cgi.assign = ( ".pl" => "/usr/bin/perl", ".cgi" => "", "/hello" => "/tmp/lighttpd/www/hello" )
2. mod_cgi
lighttpd的CGI支持是通过plugin实现的(大多数主流Web Server都是这样,mini_httpd算是另类,因为它太小了,更本没有plugin机制)。
2.1 Data Structure
这里先简单罗列出一些数据结构,并简单介绍,它们的具体意义则在稍后的章节介绍。到时候可以回来查看。首先是两个数组,
43 typedef struct { 44 char **ptr; 45 46 size_t size; 47 size_t used; 48 } char_array; 50 typedef struct { 51 pid_t *ptr; 52 size_t used; 53 size_t size; 54 } buffer_pid_t;
ascii字符串的数组,和PID数组。然后是两个CGI模块重定义的结构体plugin_data, plugin_config。
56 typedef struct { 57 array *cgi; 58 unsigned short execute_x_only; 59 } plugin_config; 61 typedef struct { 62 PLUGIN_DATA; 63 buffer_pid_t cgi_pid; 64 65 buffer *tmp_buf; 66 buffer *parse_response; 67 68 plugin_config **config_storage; 69 70 plugin_config conf; 71 } plugin_data;
由名字可以判断出,cgi_pid是用来保存和mod_cgi相关的CGI进程的PID数组。config_storage保存server的所有选项,不光光是mod_cgi的选项。
87 typedef enum { T_CONFIG_UNSET, 88 T_CONFIG_STRING, 89 T_CONFIG_SHORT, 90 T_CONFIG_INT, 91 T_CONFIG_BOOLEAN, 92 T_CONFIG_ARRAY, 93 T_CONFIG_LOCAL, 94 T_CONFIG_DEPRECATED, 95 T_CONFIG_UNSUPPORTED 96 } config_values_type_t; 97 98 typedef enum { T_CONFIG_SCOPE_UNSET, 99 T_CONFIG_SCOPE_SERVER, 100 T_CONFIG_SCOPE_CONNECTION 101 } config_scope_type_t; 102 103 typedef struct { 104 const char *key; 105 void *destination; 106 107 config_values_type_t type; 108 config_scope_type_t scope; 109 } config_values_t;
对于每个plugin_config的cv(config value)。key是配置名,例如"cgi.assign",destination根据不同的选项指向不同的内容,该内容用于保存具体的选项值。然后是选项值类型,和作用范围。
73 typedef struct { 74 pid_t pid; 75 int fd; 76 int fde_ndx; /* index into the fd-event buffer */ 77 78 connection *remote_conn; /* dumb pointer */ 79 plugin_data *plugin_data; /* dumb pointer */ 80 81 buffer *response; 82 buffer *response_header; 83 } handler_ctx;
此结构是递交个fdevent的私有结构,无需把整个plugin_data交个它,指需要必要的部分。pid是CGI进程的PID,fd是Web Server和CGI进程之间,用于读取CGI数据的fd。fde_ndx如注释所说是fd-event的下标。response和response_header是两个buffer,fdevent的Handle会填写这两个buffer。
2.2 Initilization
初始化的代码显示对应的lighttpd版本、模块名,以及各个hook都在何时被调用,Server初始化的时候加载各个module时,plugins_load,时调用mod_cgi_plugin_init,
1435 int mod_cgi_plugin_init(plugin *p) { 1436 p->version = LIGHTTPD_VERSION_ID; 1437 p->name = buffer_init_string("cgi"); 1438 1439 p->connection_reset = cgi_connection_close_callback; 1440 p->handle_subrequest_start = cgi_is_handled; 1441 p->handle_subrequest = mod_cgi_handle_subrequest; ... 1445 p->handle_trigger = cgi_trigger; 1446 p->init = mod_cgi_init; 1447 p->cleanup = mod_cgi_free; 1448 p->set_defaults = mod_fastcgi_set_defaults; ... 1453 }
各个加载plugin加载完毕,后Server通过plugins_call_init调用各个模块的初始化函数,此处即mod_cgi_init。它用来初始化并分配此模块的plugin_data结构。mod_cgi_free则释放相应的资源。这两个hook具体就不看了。
2.3 Configration
虚函数set_defaults用来处理配置选项。它会将配置选项解析成内部形式,并保存到模块自定义的plugin_config中。mod_cgi的实现是mod_fastcgi_set_defaults。(为什么叫fastcgi?mod_fastcgi.c也有个同名函数)。
148 SETDEFAULTS_FUNC(mod_fastcgi_set_defaults) { ... 152 config_values_t cv[] = { 153 { "cgi.assign", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 0 */ 154 { "cgi.execute-x-only", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 1 */ 155 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET} 156 }; 157
声明选项的名字、类型,作用范围。但它们的具体值留空。第一个是array类型,array类型可以存放[key, value]的数组。
160 p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *));
到每个模块的的set_defaults前srv->config_context已经被设置成模块相关的配置,如果配置文件中只有
cgi.assign = ("" => "",
"" => "")
那么srv->config_context->used就是1,不会有别的配置漏进来!具体原因见configuration部分。
162 for (i = 0; i < srv->config_context->used; i++) { 163 plugin_config *s; 164 165 s = calloc(1, sizeof(plugin_config)); 166 assert(s); 167 168 s->cgi = array_init(); 169 s->execute_x_only = 0; 170 171 cv[0].destination = s->cgi; 172 cv[1].destination = &(s->execute_x_only); 173 174 p->config_storage[i] = s; 175 176 if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) { 177 return HANDLER_ERROR; 178 } 179 } 180 181 return HANDLER_GO_ON; 182 }
cv[i]的->destination的赋值部分每次都是不同的值,即新分配的plugin_data的cgi和execute_x_only。然后调用config_insert_values_global,后者根据cv[i]>key,为每个cv找到Server对应的选项,然后调用此config_insert_values_intenal。后者根据cv[i].type,将具体的值放到cv[i].destination中。
要知道,cv[i].destination指向了plugin_config对应的字段cgi,execute_x_only,所以config_insert_values_global相当于,从server的config把值取到了mod_cgi的plugin_config对应的位置。
config_insert_values_global(config_insert_values_intenal)在key不匹配的情况下也是返回0,所以,虽然p->config_storage里面包含了srv->config_context->used个plugin_data,但只有NELEM(cv) - 1个会被真正赋值。另外,plugin_config->cgi是不带类型的数组array,如果它对应的cv类型是T_CONFIG_ARRAY的话,那么cgi的元素就是data_string{}。
2.4 Timer (Tigger)
cgi_trigger每秒钟被调用一次。它的作用是为每个Plugin下已经结束的CGI(子进程)处理后事(如果CGI已经结束会留下僵尸)。使用的是waitpid的WNOHANG模式,WNOHANG除了不会在子进程没结束的时候挂起调用者,它的返回值,
>0: 状态改变的子进程的PID0: 进程状态还没改变(还没结束)-1: 如果未退出,或者正常退出,都返回0。如果出错,返回-1。
退出的时候会把pid从plugin中删除,如果是异常退出,通常会记录log。
2.5 Patch Connection
我们在很多Plugin中会看到patch connection函数,它到底是做什么的呢?在[2.3]我们看到了如何解析保存配置到plugin_data->config_storage。但config_storage并非真正起作用的config。plugin_data除了有一个config_storage,还有一个conf字段。
61 typedef struct { ... 68 plugin_config **config_storage; 69 70 plugin_config conf; 71 } plugin_data;
这是因为同样的配置可能会出现多次,
cgi.assign = (...) 全局配置
$SERVER("host") = "" {
cgi.assign = (...) 非全局配置,或者"条件”配置
}
}
所谓“全局配置”,即config_storage[0]保存的配置,它通常是config文件中未包含在某个配置块中的配置。关于“条件配置”的具体定义参考doc/outdated/configuration.txt。
接下来我们看mod_cgi_patch_connection的实现,
1206 #define PATCH(x) \ 1207 p->conf.x = s->x; 1208 static int mod_cgi_patch_connection(server *srv, connection *con, plugin_data *p) { 1209 size_t i, j; 1210 plugin_config *s = p->config_storage[0]; 1211 1212 PATCH(cgi); 1213 PATCH(execute_x_only); 1214
先复制全局配置,如果没有全局配置,那么先复制第一个。
1215 /* skip the first, the global context */ 1216 for (i = 1; i < srv->config_context->used; i++) { 1217 data_config *dc = (data_config *)srv->config_context->data[i]; 1218 s = p->config_storage[i]; 1219 1220 /* condition didn't match */ 1221 if (!config_check_cond(srv, con, dc)) continue; 1222
这里的“条件”检查,是决定用哪个配置的关键。如何符合条件就要merge。什么叫符合条件? 也就是connection是否符合<field> <operator> <value>,例如,是不是符合某个cookie,目的是否是某个host,IP:port是否是某个socket等等。config_check_cond具体的实现就不看了。
1223 /* merge config */ 1224 for (j = 0; j < dc->value->used; j++) { 1225 data_unset *du = dc->value->data[j]; 1226 1227 if (buffer_is_equal_string(du->key, CONST_STR_LEN("cgi.assign"))) { 1228 PATCH(cgi); 1229 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("cgi.execute-x-only"))) { 1230 PATCH(execute_x_only); 1231 } 1232 } 1233 } 1234 1235 return 0; 1236 } 1237 #undef PATCH
2.7 Handle subrequest_start
1239 URIHANDLER_FUNC(cgi_is_handled) { 1240 size_t k, s_len; 1241 plugin_data *p = p_d; 1242 buffer *fn = con->physical.path; 1243 stat_cache_entry *sce = NULL; 1244 1245 if (con->mode != DIRECT) return HANDLER_GO_ON; 1246 1247 if (fn->used == 0) return HANDLER_GO_ON; 1248 1249 mod_cgi_patch_connection(srv, con, p); 1250
在一些sanity检查后选择适当的配置。
1251 if (HANDLER_ERROR == stat_cache_get_entry(srv, con, con->physical.path, &sce)) return HANDLER_GO_ON; 1252 if (!S_ISREG(sce->st.st_mode)) return HANDLER_GO_ON; 1253 if (p->conf.execute_x_only == 1 && (sce->st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0) return HANDLER_GO_ON; 1254
接下来调用stat_cache_get_entry读取物理路径文件到stat_cache_entry结构。之后做类型,和执行权限相关检查。我们可以看到execute_x_only 选项的使用。
1255 s_len = fn->used - 1; 1256 1257 for (k = 0; k < p->conf.cgi->used; k++) { 1258 data_string *ds = (data_string *)p->conf.cgi->data[k]; 1259 size_t ct_len = ds->key->used - 1; 1260 1261 if (ds->key->used == 0) continue; 1262 if (s_len < ct_len) continue; 1263 1264 if (0 == strncmp(fn->ptr + s_len - ct_len, ds->key->ptr, ct_len)) { 1265 if (cgi_create_env(srv, con, p, ds->value)) { 1266 con->mode = DIRECT; 1267 con->http_status = 500; 1268 1269 buffer_reset(con->physical.path); 1270 return HANDLER_FINISHED; 1271 } 1272 /* one handler is enough for the request */ 1273 break; 1274 } 1275 } 1276 1277 return HANDLER_GO_ON; 1278 }
对于cgi.assign中的每个“key/value”条目,一律视为“后缀”,和文件名进行比较,如果匹配,调用cgi_create_env建立CGI进程。如果key不是后缀,而是整个URL的path部分,例如
cgi.assign = ("/testfile" => "")
也能够匹配(必须和整个“物理”path匹配)。
2.7.1 cgi_create_env
cgi_create_env是整个mod_cgi的核心函数,用于为CGI设置环境、fd,创建CGI进程。
static int cgi_create_env(server *srv, connection *con, plugin_data *p, buffer *cgi_handler);
它的前3个参数简单明了,最后一个参数cgi_handler是配置信息"=>"后面的部分,实际上也就是要执行的CGI程序,例如解释器,或者直接是可执行文件。结合此函数的调用的地方(cgi_is_handled),理一下三个变量的关系,
con->physical.path (fn->ptr) 用户访问的“物理”文档名,及URL的Path部分经过处理后对应的Server上的文件 p->conf.cgi->data[k]->key (ds->key) file-extensions,要匹配的文件后缀或者路径 p->conf.cgi->data[k]->value (ds->value) CGI Handler (解释器,或者可执行文件)
函数很长,我们分段解释,
738 static int cgi_create_env(server *srv, connection *con, plugin_data *p, buffer *cgi_handler) { 739 pid_t pid; 740 741 #ifdef HAVE_IPV6 742 char b2[INET6_ADDRSTRLEN + 1]; 743 #endif 744 745 int to_cgi_fds[2]; 746 int from_cgi_fds[2]; 747 struct stat st; 748
函数的内部参数pid保存CGI进程的PID,to_cgi_fds和from_cgi_fds分别是管道的fd,稍后解释。st是cgi_handler的struct stat结构,只是用来保存一下,没有实际意义。b2是用来保存IPv6地址的buffer,也没有实际意义。
751 if (cgi_handler->used > 1) { 752 /* stat the exec file */ 753 if (-1 == (stat(cgi_handler->ptr, &st))) { 754 log_error_write(srv, __FILE__, __LINE__, "sbss", 755 "stat for cgi-handler", cgi_handler, 756 "failed:", strerror(errno)); 757 return -1; 758 } 759 }
检查Handler文件是否存在。
Pipes: STDIN/STDOUT
Web Server(这里指lihttpd的mod_cgi模块)和CGI进程之间使用STDOUT/STDIN和环境变量进行通信(环境变量是单向的)[参考CGI标准]。将两个进程的stdxxx相连的是管道。fork之后,子进程(CGI进程)把from_cgi_fds[0](读端)关闭,把STDOUT设置为from_cgi_fd[1](写端);同时把STDIN设置为to_cgi_fds[0](读端),吧to_cgi_fds[1](写端)关闭。父进程(Web Server)则做类似的动作,但没有设置STDIN/STDOUT。
761 if (pipe(to_cgi_fds)) { ... 764 } 765 766 if (pipe(from_cgi_fds)) { ... 771 } 772 773 /* fork, execve */ 774 switch (pid = fork()) { 775 case 0: { ... 787 /* move stdout to from_cgi_fd[1] */ 788 close(STDOUT_FILENO); 789 dup2(from_cgi_fds[1], STDOUT_FILENO); 790 close(from_cgi_fds[1]); 791 /* not needed */ 792 close(from_cgi_fds[0]); 793 794 /* move the stdin to to_cgi_fd[0] */ 795 close(STDIN_FILENO); 796 dup2(to_cgi_fds[0], STDIN_FILENO); 797 close(to_cgi_fds[0]); 798 /* not needed */ 799 close(to_cgi_fds[1]); ... 1058 break; 1059 } .... 1069 default: { 1070 handler_ctx *hctx; 1071 /* father */ 1072 1073 close(from_cgi_fds[1]); 1074 close(to_cgi_fds[0]); .... 1196 break; 1197 } 1198 }
下图是经过这些步骤之后的情况,env/exec部分稍后就会提到,
Child's Role
Child除了设置STDIN/STDOUT之外,还需要为Hander的执行准备环境变量。环境变量是根据配置,以及Server,Conn的相关信息重头准备的。下面的代码假设支持IPv6。
801 /* create environment */ 802 env.ptr = NULL; 803 env.size = 0; 804 env.used = 0; 805 806 if (buffer_is_empty(con->conf.server_tag)) { 807 cgi_env_add(&env, CONST_STR_LEN("SERVER_SOFTWARE"), CONST_STR_LEN(PACKAGE_DESC)); 808 } else { 809 cgi_env_add(&env, CONST_STR_LEN("SERVER_SOFTWARE"), CONST_BUF_LEN(con->conf.server_tag)); 810 } ...
准备ENV的代码就不列了,没多大意义。不过可以看看都设置了、或者说可以设置(根据配置)哪些Env(从CGI标准中也可以找到类似的列表),
除了以上的变量,还需要冲request.header中把HTTP的各个Header传递到env里面。这些Header的名字转换为环境变量名的时候,会加上HTTP_的前缀(除了CONTENT_TYPE),并全部改为大写,SERVER_SOFTWARE 软件名,可配置SERVER_NAME Server名或者IP地址GATEWAY_INTERFACE CGI版本,“CGI/1.1”SERVER_PROTOCOL HTTP版本SERVER_PORTSERVER_ADDRREQUEST_METHODPATHINFOREDRECT_STATUSQUERY_STRINGREQUEST_URIREMOTE_ADDRREMOTE_PORTHTTPS 是不是“on”CONTENT_LENGTHSCRIPT_FILENAME con->physical.pathSCRIPT_NAME con->uri.pathDUCUMENT_ROOT con->physical.basedirLD_PRELOAD 从Server继承给CGILD_LIBRARY_PATH 从Server继承给CGISYSTEMROOT 从Server继承给CGI
954 for (n = 0; n < con->request.headers->used; n++) { 955 data_string *ds; 956 957 ds = (data_string *)con->request.headers->data[n]; 958 959 if (ds->value->used && ds->key->used) { 960 size_t j; 961 962 buffer_reset(p->tmp_buf); 963 964 if (0 != strcasecmp(ds->key->ptr, "CONTENT-TYPE")) { 965 buffer_copy_string_len(p->tmp_buf, CONST_STR_LEN("HTTP_")); 966 p->tmp_buf->used--; /* strip \0 after HTTP_ */ 967 } 968 969 buffer_prepare_append(p->tmp_buf, ds->key->used + 2); 970 971 for (j = 0; j < ds->key->used - 1; j++) { 972 char cr = '_'; 973 if (light_isalpha(ds->key->ptr[j])) { 974 /* upper-case */ 975 cr = ds->key->ptr[j] & ~32; 976 } else if (light_isdigit(ds->key->ptr[j])) { 977 /* copy */ 978 cr = ds->key->ptr[j]; 979 } 980 p->tmp_buf->ptr[p->tmp_buf->used++] = cr; 981 } 982 p->tmp_buf->ptr[p->tmp_buf->used++] = '\0'; 983 984 cgi_env_add(&env, CONST_BUF_LEN(p->tmp_buf), CONST_BUF_LEN(ds->value)); 985 } 986 }
con->enviornment中的环境变量是用户配置的,要传递给CGI的变量,
988 for (n = 0; n < con->environment->used; n++) { 989 data_string *ds; 990 991 ds = (data_string *)con->environment->data[n]; 992 993 if (ds->value->used && ds->key->used) { 994 size_t j; 995 996 buffer_reset(p->tmp_buf); 997 998 buffer_prepare_append(p->tmp_buf, ds->key->used + 2); 999 1000 for (j = 0; j < ds->key->used - 1; j++) { 1001 char cr = '_'; 1002 if (light_isalpha(ds->key->ptr[j])) { 1003 /* upper-case */ 1004 cr = ds->key->ptr[j] & ~32; 1005 } else if (light_isdigit(ds->key->ptr[j])) { 1006 /* copy */ 1007 cr = ds->key->ptr[j]; 1008 } 1009 p->tmp_buf->ptr[p->tmp_buf->used++] = cr; 1010 } 1011 p->tmp_buf->ptr[p->tmp_buf->used++] = '\0'; 1012 1013 cgi_env_add(&env, CONST_BUF_LEN(p->tmp_buf), CONST_BUF_LEN(ds->value)); 1014 } 1015 }
接下来为exec调用准备参数,并把fd>=3的FD全部关闭,
1017 if (env.size == env.used) { 1018 env.size += 16; 1019 env.ptr = realloc(env.ptr, env.size * sizeof(*env.ptr)); 1020 } 1021 1022 env.ptr[env.used] = NULL; 1023 1024 /* set up args */ 1025 argc = 3; 1026 args = malloc(sizeof(*args) * argc); 1027 i = 0; 1028 1029 if (cgi_handler->used > 1) { 1030 args[i++] = cgi_handler->ptr; 1031 } 1032 args[i++] = con->physical.path->ptr; 1033 args[i ] = NULL; 1034 1035 /* search for the last / */ 1036 if (NULL != (c = strrchr(con->physical.path->ptr, '/'))) { 1037 *c = '\0'; 1038 1039 /* change to the physical directory */ 1040 if (-1 == chdir(con->physical.path->ptr)) { 1041 log_error_write(srv, __FILE__, __LINE__, "ssb", "chdir failed:", strerror(errno), con->physical.path); 1042 } 1043 *c = '/'; 1044 } 1045 1046 /* we don't need the client socket */ 1047 for (i = 3; i < 256; i++) { 1048 if (i != srv->errorlog_fd) close(i); 1049 } 1050
最后,执行cgi,
1051 /* exec the cgi */ 1052 execve(args[0], args, env.ptr); 1053 1054 /* log_error_write(srv, __FILE__, __LINE__, "sss", "CGI failed:", strerror(errno), args[0]); */ 1055 1056 /* */ 1057 SEGFAULT(); 1058 break;
Father's Role
看看Web Server这边都需要做些什么。HTTP Request的部分已经通过环境变量传递给CGI了,如果Content-Length不为0,则还需要将之前从client收到的body部分传递给CGI,
1076 if (con->request.content_length) { 1077 chunkqueue *cq = con->request_content_queue; 1078 chunk *c; 1079 1080 assert(chunkqueue_length(cq) == (off_t)con->request.content_length); 1081 1082 /* there is content to send */ 1083 for (c = cq->first; c; c = cq->first) { .... 1131 if ((r = write(to_cgi_fds[1], c->mem->ptr + c->offset, c->mem->used - c->offset - 1)) < 0) { .... 1148 if (r > 0) { 1149 c->offset += r; 1150 cq->bytes_out += r; 1151 } else { .... 1155 } 1156 chunkqueue_remove_finished_chunks(cq); 1157 }
其间涉及的FILE_CHUNK, MEM_CHUNK暂不讨论。发送完body后,就没有什么在需要传递给CGI的了,所以先把写方向的管道关闭。接下来,需要从CGI读取返回的数据,作为HTTP Response递交给HTTP Client,但lighttpd是非阻塞IO加事件驱动的“单进程(Worker)” WebServer,我们不能阻塞在这里等待,于是为CGI注册一个fdevent的Handler来处理来自CGI(from_cgi_fd[0])的数据。
1160 close(to_cgi_fds[1]); 1161 1162 /* register PID and wait for them asyncronously */ 1163 con->mode = p->id; 1164 buffer_reset(con->physical.path); 1165 1166 hctx = cgi_handler_ctx_init(); 1167 1168 hctx->remote_conn = con; 1169 hctx->plugin_data = p; 1170 hctx->pid = pid; 1171 hctx->fd = from_cgi_fds[0]; 1172 hctx->fde_ndx = -1; 1173 1174 con->plugin_ctx[p->id] = hctx; 1175 1176 fdevent_register(srv->ev, hctx->fd, cgi_handle_fdevent, hctx); 1177 fdevent_event_set(srv->ev, &(hctx->fde_ndx), hctx->fd, FDEVENT_IN); 只需要读取 1178 1179 if (-1 == fdevent_fcntl_set(srv->ev, hctx->fd)) { ... 资源清理,记录日志,返回错误 1194 } 1195 1196 break;
hctx是要交给fdevent的私有数据,类型是handler_ctx,各自的的意义见[2.1]的描述。我们要看看cgi_handler_fdevent是如何实现的。
Receive From CGI: cgi_handler_fdevent
623 static handler_t cgi_handle_fdevent(server *srv, void *ctx, int revents) { 624 handler_ctx *hctx = ctx; 625 connection *con = hctx->remote_conn; 626 627 joblist_append(srv, con); ... 635 if (revents & FDEVENT_IN) { 636 switch (cgi_demux_response(srv, hctx)) { 637 case FDEVENT_HANDLED_NOT_FINISHED: 638 break; 639 case FDEVENT_HANDLED_FINISHED: 645 cgi_connection_close(srv, hctx); 648 return HANDLER_FINISHED; 649 case FDEVENT_HANDLED_ERROR: ... 660 break; 661 } 662 } 663 664 if (revents & FDEVENT_OUT) { 665 /* nothing to do */ 666 } 667 668 /* perhaps this issue is already handled */ 669 if (revents & FDEVENT_HUP) { ... 699 } else if (revents & FDEVENT_ERR) { ... 707 return HANDLER_ERROR; 708 } 709 710 return HANDLER_FINISHED; 711 }
cgi_handler_fdevent主要作用是接收CGI程序的返回数据,它是在cgi_demux_response中处理的。余下的部分负责处理信号和错误情况。
338 static int cgi_demux_response(server *srv, handler_ctx *hctx) { 339 plugin_data *p = hctx->plugin_data; 340 connection *con = hctx->remote_conn; 341 342 while(1) { ... 349 if (ioctl(con->fd, FIONREAD, &toread) || toread == 0 || toread <= 4*1024) { 350 buffer_prepare_copy(hctx->response, 4 * 1024); 351 } else { 352 if (toread > MAX_READ_LIMIT) toread = MAX_READ_LIMIT; 353 buffer_prepare_copy(hctx->response, toread + 1); 354 }
看看一次能读多少数据,如果少于4K,以4K记。大于4K电话,不能大于上限。这里的read是非阻塞的,所有如果暂时没有数据可读(EAGAIN),
则函数立即返回FDEVENT_HANDLED_NOT_FINISHED。把它们到hctx->response中。while(1)说明,如果有东西可以读,就持续的读出这是为了提高效率(由事件触发,一旦“可读”事件到来,就尽力的读取。这里没有像Kernel收网络数据那样做一个读取次数的限制,也没有NAPI那样的bottom half)。
357 if (-1 == (n = read(hctx->fd, hctx->response->ptr, hctx->response->size - 1))) { 358 if (errno == EAGAIN || errno == EINTR) { 359 /* would block, wait for signal */ 360 return FDEVENT_HANDLED_NOT_FINISHED; 361 } 362 /* error */ 363 log_error_write(srv, __FILE__, __LINE__, "sdd", strerror(errno), con->fd, hctx->fd); 364 return FDEVENT_HANDLED_ERROR; 365 } 366 367 if (n == 0) { 368 /* read finished */ 369 370 con->file_finished = 1; 371 372 /* send final chunk */ 373 http_chunk_append_mem(srv, con, NULL, 0); 374 joblist_append(srv, con); 375 376 return FDEVENT_HANDLED_FINISHED; 377 } 378 379 hctx->response->ptr[n] = '\0'; 380 hctx->response->used = n+1;
能走到下面,代表读到了一些东西。虽然内容未必是ASCII字符,但最后留出一个'\0',对于调试很方便的。con->file_started标记表示,body,及document部分有没有开始。这个以连续的\r\n\r\n判定(当然如果少掉\r也是可以容忍的)。
header的处理阶段(file_started == 0)和body的处理阶段。header的处理本身没什么特别的,无非是看看有没有header,header什么时候结束。header结束有两种可能
- 1. 有header字段,并且出现"\n(\r?)\n"字符串
- 2. 尚未见到任何header字段的情况下,之间出现"(\r)\n"
这意味着CGI程序必须通过空行告知header域的结束,无论是否有header。
“body”过程的部分,这部分只需要调用http_chunk_append_mem将读到的任何数据写入con->write_queue,后者会作为response发送给Client。
384 if (con->file_started == 0) { ... 390 buffer_append_string_buffer(hctx->response_header, hctx->response); ... 453 if (is_header_end) { 454 if (!is_header) { 455 /* no header, but a body */ 456 457 if (con->request.http_version == HTTP_VERSION_1_1) { 458 con->response.transfer_encoding = HTTP_TRANSFER_ENCODING_CHUNKED; 459 } 460 461 http_chunk_append_mem(srv, con, hctx->response_header->ptr, hctx->response_header->used); 462 joblist_append(srv, con); 463 } else { ... 486 /* parse the response header */ 487 cgi_response_parse(srv, con, p, hctx->response_header); ... 499 } 500 501 con->file_started = 1; 502 } 503 } else { 504 http_chunk_append_mem(srv, con, hctx->response->ptr, hctx->response->used); 505 joblist_append(srv, con); 506 }
各个buffer的关系,以及不同阶段总结如下图,
之前有一个函数我们还没看,即如果有header并且header已经读完后,会调用它:cgi_response_parse,这个函数会根据CGI的返回的header部分,设置一些重要的字段,例如con->http_status等。
231 static int cgi_response_parse(server *srv, connection *con, plugin_data *p, buffer *in) { ... 238 buffer_copy_string_buffer(p->parse_response, in);
plugin_data->parse_response用来解析CGI的response。然后一次一行,
240 for (s = p->parse_response->ptr; 241 NULL != (ns = strchr(s, '\n')); 242 s = ns + 1, line++) { ... 252 if (line == 0 && 253 0 == strncmp(s, "HTTP/1.", 7)) { ... 262 status = strtol(s+9, NULL, 10); 263 264 if (status >= 100 && 265 status < 1000) { 266 /* we expected 3 digits and didn't got them */ 267 con->parsed_response |= HTTP_STATUS; 268 con->http_status = status; 269 } ... 271 } else {
提取并设置http status,位图con->parsed_response包含了以下“各位:)”(base.h)。
/* fcgi_response_header contains ... */#define HTTP_STATUS BV(0)#define HTTP_CONNECTION BV(1)#define HTTP_CONTENT_LENGTH BV(2)#define HTTP_DATE BV(3)#define HTTP_LOCATION BV(4)
后面解析各个Header,插入con->response.headers,并设置位图内相应的位。
... 285 if (NULL == (ds = (data_string *)array_get_unused_element( con->response.headers, TYPE_STRING))) { 286 ds = data_response_init(); 287 } 288 buffer_copy_string_len(ds->key, key, key_len); 289 buffer_copy_string(ds->value, value); 290 291 array_insert_unique(con->response.headers, (data_unset *)ds); 292 293 switch(key_len) { 294 case 4: 295 if (0 == strncasecmp(key, "Date", key_len)) { 296 con->parsed_response |= HTTP_DATE; 297 } 298 break; ... ... 322 default: 323 break; 324 } 325 } 326 } 327 328 /* CGI/1.1 rev 03 - 7.2.1.2 */ 329 if ((con->parsed_response & HTTP_LOCATION) && 330 !(con->parsed_response & HTTP_STATUS)) { 331 con->http_status = 302; 332 } 333 334 return 0; 335 }
如果未能判定出 HTTP_Status,视为302。
2.8 Handle Subrequest
2.9 Connection Close (Reset)