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_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);