mod_cgi.cのソースを読んでみる

Pocket

C言語で CGI を作成すると、POSTのデータをfread()などを使って、stdin から読む必要があります。

また、エラーが発生した場合は、何もしないとErrorLogに設定されたログファイルに出力されます。

前々からこの動作について疑問に思っていたので、追求してみることにしました。

環境

追求する環境は下記の通りです。こちらを見ていきます。

Momonga Linux 4

Apache httpd-2.2.6 のソース

今まで思っていた疑問

今までC言語でCGIを書くときに疑問に思っていたのがいくつかあります。ここに挙げておきます。

1. なぜPOSTのデータは fread()などで読む必要があるのか?
2. なぜヘッダ(Content-Lengthなど)がgetenv()で取得できるのか?
3. エラーなどのデバッグ情報は、stderrに書くとErrorLogに設定したファイルに出力されるが、どうしてか?
4. HTTPで出力されるヘッダーのうち、Content-Type だけ出力すれば、上手く動作するのは?

ということで、cgiを扱っているモジュールmod_cgi.cを読んで行きます。

ソースを読む

読んだファイルは下記の通りです

* modules/generators/mod_cgi.c
* os/unix/unixd.c
* srclib/apr/threadproc/unix/proc.c
* server/util_script.c

流れ

ソースが表示されていますが、脇にあるのは行番号です。
cgi_handlerの実行[modules/generators/mod_cgi.c]

httpd.conf などに .cgi などの拡張子の場合、cgi-handler を呼び出す設定が記述されていると思います

cgi-handler が呼び出されると、mod_cgi の中の cgi_handler() 関数が呼び出されます。
ヘッダーの生成[modules/generators/mod_cgi.c]

ヘッダーの生成と少し言い過ぎだと思いましたが、わかりやすいように生成と書きます。

CGIやSSI を使った場合、SCRIPT_NAMEやPATH_INFOなど、普通のHTMLでは設定されないヘッダーがあります。

それがcgi_handler()関数の部分で設定されます。

cgi_handler()の中でCGIを実行したといきに付加されるヘッダー(SCRIPT_NAMEやPATH_INFOなど)がセットされます。

ま、CGI で上書きはできますけど・・・。

ap_add_common_vars(r);
ap_add_cgi_vars(r);

CGI の実行準備[modules/generators/mod_cgi.c]

run_cgi_child()がCGIをexecve()している部分になります。

ここで失敗したらISEが返るようになってますね。

run_cgi_child()の引数のscript_out、script_in、script_errがあります。

mod_cgi が CGI に出力するデータを渡すのが script_out になります。 CGIで STDIN から POST データを読みますが、そのデータが script_out に書き込まれます。mod_cgi から見ると out でいいんでしょうけど、わかりにくいですね。

同様に、script_inが出力用です。普通は HTMLやヘッダー などのデータが返ってきます。

わかりにくい!!

/* run the script in its own process */
if ((rv = run_cgi_child(&script_out, &script_in, &script_err, command, argv, r, p, &e_info)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "couldn't spawn child process: %s", r->filename);
return HTTP_INTERNAL_SERVER_ERROR;
}

CGIで取得できるヘッダー[modules/generators/mod_cgi.c]

下記の部分で、subprocess_envに入っているデータ(CONTENT_LENGTHやQUERY_SCRINGSなど)をenvに入れています。

これが、後ほどCGIをexecve()する時に、envが引数として渡されてます。

つまり、CGI で getenv() して取得できる変数は、r->subprocess_env に入っている必要があります。

#ifdef DEBUG_CGI
fprintf(dbg, "Attempting to exec %s as CGI child (argv0 = %s)\n",
r->filename, argv[0]);
#endif

env = (const char * const *)ap_create_environment(p, r->subprocess_env);

#ifdef DEBUG_CGI
fprintf(dbg, "Environment: \n");
for (i = 0; env[i]; ++i)

pipe の作成[modules/generators/mod_cgi.c]

POSTデータをCGIに渡すために、pipeを作成して渡しています。

もちろん、エラーとCGIの出力もpipeを作成してます。

下記のapr_procattr_io_set()を辿っていくと、pipeを作成している部分までいけます。

/* Transmute ourselves into the script.
* NB only ISINDEX scripts get decoded arguments.
*/
if (((rc = apr_procattr_create(&procattr, p)) != APR_SUCCESS) ||
((rc = apr_procattr_io_set(procattr,
e_info->in_pipe,
e_info->out_pipe,
e_info->err_pipe)) != APR_SUCCESS) ||

CGIとexecve()[modules/generators/mod_cgi.c]

ap_os_create_privileged_process() の中で CGI をfork()して、execve()しています。

pipe が開いているので、ここでは実行完了してません。

else {
procnew = apr_pcalloc(p, sizeof(*procnew));
rc = ap_os_create_privileged_process(r, procnew, command, argv, env,
procattr, p);

if (rc != APR_SUCCESS) {
/* Bad things happened. Everyone should have cleaned up. */
ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_TOCLIENT, rc, r,
"couldn't create child process: %d: %s", rc,
apr_filepath_name_get(r->filename));
}
else {
apr_pool_note_subprocess(p, procnew, APR_KILL_AFTER_TIMEOUT);

*script_in = procnew->out;
if (!*script_in)
return APR_EBADF;
apr_file_pipe_timeout_set(*script_in, r->server->timeout);

if (e_info->prog_type == RUN_AS_CGI) {
*script_out = procnew->in;
if (!*script_out)
return APR_EBADF;
apr_file_pipe_timeout_set(*script_out, r->server->timeout);

*script_err = procnew->err;
if (!*script_err)
return APR_EBADF;
apr_file_pipe_timeout_set(*script_err, r->server->timeout);
}
}
}

POSTデータの読み出し[modules/generators/mod_cgi.c]

CGIを実行する準備をしましたので、pipe に送るPOSTデータを読み出します。

これは、cgi_handler()の部分で行っています。

brigade を apr_brigade_create() で作ってます。

apr_bucket_read()で読んで、apr_file_write_full() でデータをpipe に書き込んでます。

/* Transfer any put/post args, CERN style...
* Note that we already ignore SIGPIPE in the core server.
*/
bb = apr_brigade_create(r->pool, c->bucket_alloc);
seen_eos = 0;
child_stopped_reading = 0;
if (conf->logname) {
dbuf = apr_palloc(r->pool, conf->bufbytes + 1);
dbpos = 0;
}
do {
apr_bucket *bucket;

rv = ap_get_brigade(r->input_filters, bb, AP_MODE_READBYTES,
APR_BLOCK_READ, HUGE_STRING_LEN);

if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"Error reading request entity data");
return HTTP_INTERNAL_SERVER_ERROR;
}

for (bucket = APR_BRIGADE_FIRST(bb);
bucket != APR_BRIGADE_SENTINEL(bb);
bucket = APR_BUCKET_NEXT(bucket))
{
const char *data;
apr_size_t len;

if (APR_BUCKET_IS_EOS(bucket)) {
seen_eos = 1;
break;
}

/* We can't do much with this. */
if (APR_BUCKET_IS_FLUSH(bucket)) {
continue;
}

/* If the child stopped, we still must read to EOS. */
if (child_stopped_reading) {
continue;
}

/* read */
apr_bucket_read(bucket, &data, &len, APR_BLOCK_READ);

if (conf->logname && dbpos < conf->bufbytes) {
int cursize;

if ((dbpos + len) > conf->bufbytes) {
cursize = conf->bufbytes - dbpos;
}
else {
cursize = len;
}
memcpy(dbuf + dbpos, data, cursize);
dbpos += cursize;
}

/* Keep writing data to the child until done or too much time
* elapses with no progress or an error occurs.
*/
rv = apr_file_write_full(script_out, data, len, NULL);

if (rv != APR_SUCCESS) {
/* silly script stopped reading, soak up remaining message */
child_stopped_reading = 1;
}
}
apr_brigade_cleanup(bb);
}
while (!seen_eos);

CGI へのPOSTデータを送るpipeを閉じる[modules/generators/mod_cgi.c]

CGI にデータを渡す pipe を閉じちゃいます。そうすれば、CGIが実行されます。

そして、先ほど使用した bb を綺麗にします。これは、CGIから受け取るデータに再利用するためです。

/* Is this flush really needed? */
apr_file_flush(script_out);
apr_file_close(script_out);

AP_DEBUG_ASSERT(script_in != NULL);

apr_brigade_cleanup(bb);

CGI からデータを受け取る[modules/generators/mod_cgi.c]

cgi_bucket_create() もしくは apr_bucket_pipe_create() で、CGIからのデータを受け取ります。

受け取ったデータを bb に追加してます。

そのままだと eos がないので、 apr_bucket_eos_create() で eos bucket を作成し、bbの最後に追加します。

#if APR_FILES_AS_SOCKETS
apr_file_pipe_timeout_set(script_in, 0);
apr_file_pipe_timeout_set(script_err, 0);

b = cgi_bucket_create(r, script_in, script_err, c->bucket_alloc);
#else
b = apr_bucket_pipe_create(script_in, c->bucket_alloc);
#endif
APR_BRIGADE_INSERT_TAIL(bb, b);
b = apr_bucket_eos_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, b);

CGI からのデータをヘッダーとBody部に分ける[modules/generators/mod_cgi.c]

ここから nph という変数での分岐になりますが、面倒なので !nph の場合だけ説明します。

CGI から受け取ったデータは、そのままだと ヘッダー と Body のデータが bb に入っています。

ap_scan_script_header_err_brigade() で、出力するヘッダーを r->headers_out に入れています。

/* Handle script return... */
if (!nph) {
const char *location;
char sbuf[MAX_STRING_LEN];
int ret;

if ((ret = ap_scan_script_header_err_brigade(r, bb, sbuf))) {
ret = log_script(r, conf, ret, dbuf, sbuf, bb, script_err);

ap_scan_script_header_err_brigade()でヘッダーを取り出す[server/util_script.c]

ap_scan_script_header_err_brigade()の中身を見ていきます。

とは言っても、中身は下のように、 ap_scan_script_header_err_core()が呼ばれています。

AP_DECLARE(int) ap_scan_script_header_err_brigade(request_rec *r,
apr_bucket_brigade *bb,
char *buffer)
{
return ap_scan_script_header_err_core(r, buffer, getsfunc_BRIGADE, bb);
}

ap_scan_script_header_err_core()を見てみる[server/util_script.c]

*getfunc()のソース(getfunc_BRIGADE()) を見てみたところ、brigade の bucket を改行ごとに分割して、捨てています。

bb から改行部分以前の bucket は捨てていますが、文字列は *w にあります。ここにヘッダー(Content-Length: 19とか)が入ってきます。

*w をヘッダーの r->headers_out に入れてあげます。通常のヘッダー以外のやつは merge に入れます。 cookie については cookie という table に入れているようです。

ヘッダー部とボディー部は、2つの改行が連続でありますので、分割される bucket は468行目の w[0] == ‘\0’ になります。それでこの関数を抜けます。

あとは、r->headers_out にマージしたりやらで。

AP_DECLARE(int) ap_scan_script_header_err_core(request_rec *r, char *buffer,
int (*getsfunc) (char *, int, void *),
void *getsfunc_data)
{
char x[MAX_STRING_LEN];
char *w, *l;
int p;
int cgi_status = HTTP_UNSET;
apr_table_t *merge;
apr_table_t *cookie_table;

if (buffer) {
*buffer = '\0';
}
w = buffer ? buffer : x;

/* temporary place to hold headers to merge in later */
merge = apr_table_make(r->pool, 10);

/* The HTTP specification says that it is legal to merge duplicate
* headers into one. Some browsers that support Cookies don't like
* merged headers and prefer that each Set-Cookie header is sent
* separately. Lets humour those browsers by not merging.
* Oh what a pain it is.
*/
cookie_table = apr_table_make(r->pool, 2);
apr_table_do(set_cookie_doo_doo, cookie_table, r->err_headers_out, "Set-Cookie", NULL);

while (1) {

if ((*getsfunc) (w, MAX_STRING_LEN - 1, getsfunc_data) == 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_TOCLIENT, 0, r,
"Premature end of script headers: %s",
apr_filepath_name_get(r->filename));
return HTTP_INTERNAL_SERVER_ERROR;
}

/* Delete terminal (CR?)LF */

p = strlen(w);
/* Indeed, the host's '\n':
'\012' for UNIX; '\015' for MacOS; '\025' for OS/390
-- whatever the script generates.
*/
if (p > 0 && w[p - 1] == '\n') {
if (p > 1 && w[p - 2] == CR) {
w[p - 2] = '\0';
}
else {
w[p - 1] = '\0';
}
}

/*
* If we've finished reading the headers, check to make sure any
* HTTP/1.1 conditions are met. If so, we're done; normal processing
* will handle the script's output. If not, just return the error.
* The appropriate thing to do would be to send the script process a
* SIGPIPE to let it know we're ignoring it, close the channel to the
* script process, and *then* return the failed-to-meet-condition
* error. Otherwise we'd be waiting for the script to finish
* blithering before telling the client the output was no good.
* However, we don't have the information to do that, so we have to
* leave it to an upper layer.
*/
if (w[0] == '\0') {
int cond_status = OK;

/* PR#38070: This fails because it gets confused when a
* CGI Status header overrides ap_meets_conditions.
*
* We can fix that by dropping ap_meets_conditions when
* Status has been set. Since this is the only place
* cgi_status gets used, let's test it explicitly.
*
* The alternative would be to ignore CGI Status when
* ap_meets_conditions returns anything interesting.
* That would be safer wrt HTTP, but would break CGI.
*/
if ((cgi_status == HTTP_UNSET) && (r->method_number == M_GET)) {
cond_status = ap_meets_conditions(r);
}
apr_table_overlap(r->err_headers_out, merge,
APR_OVERLAP_TABLES_MERGE);
if (!apr_is_empty_table(cookie_table)) {
/* the cookies have already been copied to the cookie_table */
apr_table_unset(r->err_headers_out, "Set-Cookie");
r->err_headers_out = apr_table_overlay(r->pool,
r->err_headers_out, cookie_table);
}
return cond_status;
}

/* if we see a bogus header don't ignore it. Shout and scream */

#if APR_CHARSET_EBCDIC
/* Chances are that we received an ASCII header text instead of
* the expected EBCDIC header lines. Try to auto-detect:
*/
if (!(l = strchr(w, ':'))) {
int maybeASCII = 0, maybeEBCDIC = 0;
unsigned char *cp, native;
apr_size_t inbytes_left, outbytes_left;

for (cp = w; *cp != '\0'; ++cp) {
native = apr_xlate_conv_byte(ap_hdrs_from_ascii, *cp);
if (apr_isprint(*cp) && !apr_isprint(native))
++maybeEBCDIC;
if (!apr_isprint(*cp) && apr_isprint(native))
++maybeASCII;
}
if (maybeASCII > maybeEBCDIC) {
ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
"CGI Interface Error: Script headers apparently ASCII: (CGI = %s)",
r->filename);
inbytes_left = outbytes_left = cp - w;
apr_xlate_conv_buffer(ap_hdrs_from_ascii,
w, &inbytes_left, w, &outbytes_left);
}
}
#endif /*APR_CHARSET_EBCDIC*/
if (!(l = strchr(w, ':'))) {
char malformed[(sizeof MALFORMED_MESSAGE) + 1
+ MALFORMED_HEADER_LENGTH_TO_SHOW];

strcpy(malformed, MALFORMED_MESSAGE);
strncat(malformed, w, MALFORMED_HEADER_LENGTH_TO_SHOW);

if (!buffer) {
/* Soak up all the script output - may save an outright kill */
while ((*getsfunc) (w, MAX_STRING_LEN - 1, getsfunc_data)) {
continue;
}
}

ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_TOCLIENT, 0, r,
"%s: %s", malformed,
apr_filepath_name_get(r->filename));
return HTTP_INTERNAL_SERVER_ERROR;
}

*l++ = '\0';
while (*l && apr_isspace(*l)) {
++l;
}

if (!strcasecmp(w, "Content-type")) {
char *tmp;

/* Nuke trailing whitespace */

char *endp = l + strlen(l) - 1;
while (endp > l && apr_isspace(*endp)) {
*endp-- = '\0';
}

tmp = apr_pstrdup(r->pool, l);
ap_content_type_tolower(tmp);
ap_set_content_type(r, tmp);
}
/*
* If the script returned a specific status, that's what
* we'll use - otherwise we assume 200 OK.
*/
else if (!strcasecmp(w, "Status")) {
r->status = cgi_status = atoi(l);
r->status_line = apr_pstrdup(r->pool, l);
}
else if (!strcasecmp(w, "Location")) {
apr_table_set(r->headers_out, w, l);
}
else if (!strcasecmp(w, "Content-Length")) {
apr_table_set(r->headers_out, w, l);
}
else if (!strcasecmp(w, "Content-Range")) {
apr_table_set(r->headers_out, w, l);
}
else if (!strcasecmp(w, "Transfer-Encoding")) {
apr_table_set(r->headers_out, w, l);
}
/*
* If the script gave us a Last-Modified header, we can't just
* pass it on blindly because of restrictions on future values.
*/
else if (!strcasecmp(w, "Last-Modified")) {
ap_update_mtime(r, apr_date_parse_http(l));
ap_set_last_modified(r);
}
else if (!strcasecmp(w, "Set-Cookie")) {
apr_table_add(cookie_table, w, l);
}
else {
apr_table_add(merge, w, l);
}
}

return OK;
}

リダイレクトかどうか判定[modules/generators/mod_cgi.c]

Location ヘッダーがあった場合、リダイレクト用にいくつか行います。

まずは、リダイレクトするということは、保持していた brigade がいらないということです。apr_brigade_destroy() で削除します。

また、内部リダイレクトの場合、method を GET に書き換えます。POST のリダイレクトは、RFC 的に推奨されていなかったと思います。

内部以外については、HTTP_MOVED_TEMPORARILYを返してます。

location = apr_table_get(r->headers_out, "Location");

if (location && r->status == 200) {
/* For a redirect whether internal or not, discard any
* remaining stdout from the script, and log any remaining
* stderr output, as normal. */
discard_script_output(bb);
apr_brigade_destroy(bb);
apr_file_pipe_timeout_set(script_err, r->server->timeout);
log_script_err(r, script_err);
}

if (location && location[0] == '/' && r->status == 200) {
/* This redirect needs to be a GET no matter what the original
* method was.
*/
r->method = apr_pstrdup(r->pool, "GET");
r->method_number = M_GET;

/* We already read the message body (if any), so don't allow
* the redirected request to think it has one. We can ignore
* Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR.
*/
apr_table_unset(r->headers_in, "Content-Length");

ap_internal_redirect_handler(location, r);
return OK;
}
else if (location && r->status == 200) {
/* XX Note that if a script wants to produce its own Redirect
* body, it now has to explicitly *say* "Status: 302"
*/
return HTTP_MOVED_TEMPORARILY;
}

brigade を次のフィルターに渡す[modules/generators/mod_cgi.c]

最後に ap_pass_brigade() で bb を次のフィルターに渡してます。

あとは、rv の値を返して、cgi_handler() は終了です。

rv = ap_pass_brigade(r->output_filters, bb);

コメントを残す