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やヘッダー などのデータが返ってきます。

わかりにくい!!

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

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

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

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

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

<br />
#ifdef DEBUG_CGI<br />
fprintf(dbg, "Attempting to exec %s as CGI child (argv0 = %s)\n",<br />
r-&gt;filename, argv[0]);<br />
#endif</p>
<p>env = (const char * const *)ap_create_environment(p, r-&gt;subprocess_env);</p>
<p>#ifdef DEBUG_CGI<br />
fprintf(dbg, "Environment: \n");<br />
for (i = 0; env[i]; ++i)<br />

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

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

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

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

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

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

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

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

<br />
else {<br />
procnew = apr_pcalloc(p, sizeof(*procnew));<br />
rc = ap_os_create_privileged_process(r, procnew, command, argv, env,<br />
procattr, p);</p>
<p>if (rc != APR_SUCCESS) {<br />
/* Bad things happened. Everyone should have cleaned up. */<br />
ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_TOCLIENT, rc, r,<br />
"couldn't create child process: %d: %s", rc,<br />
apr_filepath_name_get(r-&gt;filename));<br />
}<br />
else {<br />
apr_pool_note_subprocess(p, procnew, APR_KILL_AFTER_TIMEOUT);</p>
<p>*script_in = procnew-&gt;out;<br />
if (!*script_in)<br />
return APR_EBADF;<br />
apr_file_pipe_timeout_set(*script_in, r-&gt;server-&gt;timeout);</p>
<p>if (e_info-&gt;prog_type == RUN_AS_CGI) {<br />
*script_out = procnew-&gt;in;<br />
if (!*script_out)<br />
return APR_EBADF;<br />
apr_file_pipe_timeout_set(*script_out, r-&gt;server-&gt;timeout);</p>
<p>*script_err = procnew-&gt;err;<br />
if (!*script_err)<br />
return APR_EBADF;<br />
apr_file_pipe_timeout_set(*script_err, r-&gt;server-&gt;timeout);<br />
}<br />
}<br />
}<br />

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

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

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

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

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

<br />
/* Transfer any put/post args, CERN style...<br />
* Note that we already ignore SIGPIPE in the core server.<br />
*/<br />
bb = apr_brigade_create(r-&gt;pool, c-&gt;bucket_alloc);<br />
seen_eos = 0;<br />
child_stopped_reading = 0;<br />
if (conf-&gt;logname) {<br />
dbuf = apr_palloc(r-&gt;pool, conf-&gt;bufbytes + 1);<br />
dbpos = 0;<br />
}<br />
do {<br />
apr_bucket *bucket;</p>
<p>rv = ap_get_brigade(r-&gt;input_filters, bb, AP_MODE_READBYTES,<br />
APR_BLOCK_READ, HUGE_STRING_LEN);</p>
<p>if (rv != APR_SUCCESS) {<br />
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,<br />
"Error reading request entity data");<br />
return HTTP_INTERNAL_SERVER_ERROR;<br />
}</p>
<p>for (bucket = APR_BRIGADE_FIRST(bb);<br />
bucket != APR_BRIGADE_SENTINEL(bb);<br />
bucket = APR_BUCKET_NEXT(bucket))<br />
{<br />
const char *data;<br />
apr_size_t len;</p>
<p>if (APR_BUCKET_IS_EOS(bucket)) {<br />
seen_eos = 1;<br />
break;<br />
}</p>
<p>/* We can't do much with this. */<br />
if (APR_BUCKET_IS_FLUSH(bucket)) {<br />
continue;<br />
}</p>
<p>/* If the child stopped, we still must read to EOS. */<br />
if (child_stopped_reading) {<br />
continue;<br />
}</p>
<p>/* read */<br />
apr_bucket_read(bucket, &amp;data, &amp;len, APR_BLOCK_READ);</p>
<p>if (conf-&gt;logname &amp;&amp; dbpos &lt; conf-&gt;bufbytes) {<br />
int cursize;</p>
<p>if ((dbpos + len) &gt; conf-&gt;bufbytes) {<br />
cursize = conf-&gt;bufbytes - dbpos;<br />
}<br />
else {<br />
cursize = len;<br />
}<br />
memcpy(dbuf + dbpos, data, cursize);<br />
dbpos += cursize;<br />
}</p>
<p>/* Keep writing data to the child until done or too much time<br />
* elapses with no progress or an error occurs.<br />
*/<br />
rv = apr_file_write_full(script_out, data, len, NULL);</p>
<p>if (rv != APR_SUCCESS) {<br />
/* silly script stopped reading, soak up remaining message */<br />
child_stopped_reading = 1;<br />
}<br />
}<br />
apr_brigade_cleanup(bb);<br />
}<br />
while (!seen_eos);<br />

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

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

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

<br />
/* Is this flush really needed? */<br />
apr_file_flush(script_out);<br />
apr_file_close(script_out);</p>
<p>AP_DEBUG_ASSERT(script_in != NULL);</p>
<p>apr_brigade_cleanup(bb);<br />

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

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

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

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

<br />
#if APR_FILES_AS_SOCKETS<br />
apr_file_pipe_timeout_set(script_in, 0);<br />
apr_file_pipe_timeout_set(script_err, 0);</p>
<p>b = cgi_bucket_create(r, script_in, script_err, c-&gt;bucket_alloc);<br />
#else<br />
b = apr_bucket_pipe_create(script_in, c-&gt;bucket_alloc);<br />
#endif<br />
APR_BRIGADE_INSERT_TAIL(bb, b);<br />
b = apr_bucket_eos_create(c-&gt;bucket_alloc);<br />
APR_BRIGADE_INSERT_TAIL(bb, b);<br />

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

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

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

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

<br />
/* Handle script return... */<br />
if (!nph) {<br />
const char *location;<br />
char sbuf[MAX_STRING_LEN];<br />
int ret;</p>
<p>if ((ret = ap_scan_script_header_err_brigade(r, bb, sbuf))) {<br />
ret = log_script(r, conf, ret, dbuf, sbuf, bb, script_err);<br />

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

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

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

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

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 にマージしたりやらで。

<br />
AP_DECLARE(int) ap_scan_script_header_err_core(request_rec *r, char *buffer,<br />
int (*getsfunc) (char *, int, void *),<br />
void *getsfunc_data)<br />
{<br />
char x[MAX_STRING_LEN];<br />
char *w, *l;<br />
int p;<br />
int cgi_status = HTTP_UNSET;<br />
apr_table_t *merge;<br />
apr_table_t *cookie_table;</p>
<p>if (buffer) {<br />
*buffer = '\0';<br />
}<br />
w = buffer ? buffer : x;</p>
<p>/* temporary place to hold headers to merge in later */<br />
merge = apr_table_make(r-&gt;pool, 10);</p>
<p>/* The HTTP specification says that it is legal to merge duplicate<br />
* headers into one. Some browsers that support Cookies don't like<br />
* merged headers and prefer that each Set-Cookie header is sent<br />
* separately. Lets humour those browsers by not merging.<br />
* Oh what a pain it is.<br />
*/<br />
cookie_table = apr_table_make(r-&gt;pool, 2);<br />
apr_table_do(set_cookie_doo_doo, cookie_table, r-&gt;err_headers_out, "Set-Cookie", NULL);</p>
<p>while (1) {</p>
<p>if ((*getsfunc) (w, MAX_STRING_LEN - 1, getsfunc_data) == 0) {<br />
ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_TOCLIENT, 0, r,<br />
"Premature end of script headers: %s",<br />
apr_filepath_name_get(r-&gt;filename));<br />
return HTTP_INTERNAL_SERVER_ERROR;<br />
}</p>
<p>/* Delete terminal (CR?)LF */</p>
<p>p = strlen(w);<br />
/* Indeed, the host's '\n':<br />
'\012' for UNIX; '\015' for MacOS; '\025' for OS/390<br />
-- whatever the script generates.<br />
*/<br />
if (p &gt; 0 &amp;&amp; w[p - 1] == '\n') {<br />
if (p &gt; 1 &amp;&amp; w[p - 2] == CR) {<br />
w[p - 2] = '\0';<br />
}<br />
else {<br />
w[p - 1] = '\0';<br />
}<br />
}</p>
<p>/*<br />
* If we've finished reading the headers, check to make sure any<br />
* HTTP/1.1 conditions are met. If so, we're done; normal processing<br />
* will handle the script's output. If not, just return the error.<br />
* The appropriate thing to do would be to send the script process a<br />
* SIGPIPE to let it know we're ignoring it, close the channel to the<br />
* script process, and *then* return the failed-to-meet-condition<br />
* error. Otherwise we'd be waiting for the script to finish<br />
* blithering before telling the client the output was no good.<br />
* However, we don't have the information to do that, so we have to<br />
* leave it to an upper layer.<br />
*/<br />
if (w[0] == '\0') {<br />
int cond_status = OK;</p>
<p>/* PR#38070: This fails because it gets confused when a<br />
* CGI Status header overrides ap_meets_conditions.<br />
*<br />
* We can fix that by dropping ap_meets_conditions when<br />
* Status has been set. Since this is the only place<br />
* cgi_status gets used, let's test it explicitly.<br />
*<br />
* The alternative would be to ignore CGI Status when<br />
* ap_meets_conditions returns anything interesting.<br />
* That would be safer wrt HTTP, but would break CGI.<br />
*/<br />
if ((cgi_status == HTTP_UNSET) &amp;&amp; (r-&gt;method_number == M_GET)) {<br />
cond_status = ap_meets_conditions(r);<br />
}<br />
apr_table_overlap(r-&gt;err_headers_out, merge,<br />
APR_OVERLAP_TABLES_MERGE);<br />
if (!apr_is_empty_table(cookie_table)) {<br />
/* the cookies have already been copied to the cookie_table */<br />
apr_table_unset(r-&gt;err_headers_out, "Set-Cookie");<br />
r-&gt;err_headers_out = apr_table_overlay(r-&gt;pool,<br />
r-&gt;err_headers_out, cookie_table);<br />
}<br />
return cond_status;<br />
}</p>
<p>/* if we see a bogus header don't ignore it. Shout and scream */</p>
<p>#if APR_CHARSET_EBCDIC<br />
/* Chances are that we received an ASCII header text instead of<br />
* the expected EBCDIC header lines. Try to auto-detect:<br />
*/<br />
if (!(l = strchr(w, ':'))) {<br />
int maybeASCII = 0, maybeEBCDIC = 0;<br />
unsigned char *cp, native;<br />
apr_size_t inbytes_left, outbytes_left;</p>
<p>for (cp = w; *cp != '\0'; ++cp) {<br />
native = apr_xlate_conv_byte(ap_hdrs_from_ascii, *cp);<br />
if (apr_isprint(*cp) &amp;&amp; !apr_isprint(native))<br />
++maybeEBCDIC;<br />
if (!apr_isprint(*cp) &amp;&amp; apr_isprint(native))<br />
++maybeASCII;<br />
}<br />
if (maybeASCII &gt; maybeEBCDIC) {<br />
ap_log_error(APLOG_MARK, APLOG_ERR, 0, r-&gt;server,<br />
"CGI Interface Error: Script headers apparently ASCII: (CGI = %s)",<br />
r-&gt;filename);<br />
inbytes_left = outbytes_left = cp - w;<br />
apr_xlate_conv_buffer(ap_hdrs_from_ascii,<br />
w, &amp;inbytes_left, w, &amp;outbytes_left);<br />
}<br />
}<br />
#endif /*APR_CHARSET_EBCDIC*/<br />
if (!(l = strchr(w, ':'))) {<br />
char malformed[(sizeof MALFORMED_MESSAGE) + 1<br />
+ MALFORMED_HEADER_LENGTH_TO_SHOW];</p>
<p>strcpy(malformed, MALFORMED_MESSAGE);<br />
strncat(malformed, w, MALFORMED_HEADER_LENGTH_TO_SHOW);</p>
<p>if (!buffer) {<br />
/* Soak up all the script output - may save an outright kill */<br />
while ((*getsfunc) (w, MAX_STRING_LEN - 1, getsfunc_data)) {<br />
continue;<br />
}<br />
}</p>
<p>ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_TOCLIENT, 0, r,<br />
"%s: %s", malformed,<br />
apr_filepath_name_get(r-&gt;filename));<br />
return HTTP_INTERNAL_SERVER_ERROR;<br />
}</p>
<p>*l++ = '\0';<br />
while (*l &amp;&amp; apr_isspace(*l)) {<br />
++l;<br />
}</p>
<p>if (!strcasecmp(w, "Content-type")) {<br />
char *tmp;</p>
<p>/* Nuke trailing whitespace */</p>
<p>char *endp = l + strlen(l) - 1;<br />
while (endp &gt; l &amp;&amp; apr_isspace(*endp)) {<br />
*endp-- = '\0';<br />
}</p>
<p>tmp = apr_pstrdup(r-&gt;pool, l);<br />
ap_content_type_tolower(tmp);<br />
ap_set_content_type(r, tmp);<br />
}<br />
/*<br />
* If the script returned a specific status, that's what<br />
* we'll use - otherwise we assume 200 OK.<br />
*/<br />
else if (!strcasecmp(w, "Status")) {<br />
r-&gt;status = cgi_status = atoi(l);<br />
r-&gt;status_line = apr_pstrdup(r-&gt;pool, l);<br />
}<br />
else if (!strcasecmp(w, "Location")) {<br />
apr_table_set(r-&gt;headers_out, w, l);<br />
}<br />
else if (!strcasecmp(w, "Content-Length")) {<br />
apr_table_set(r-&gt;headers_out, w, l);<br />
}<br />
else if (!strcasecmp(w, "Content-Range")) {<br />
apr_table_set(r-&gt;headers_out, w, l);<br />
}<br />
else if (!strcasecmp(w, "Transfer-Encoding")) {<br />
apr_table_set(r-&gt;headers_out, w, l);<br />
}<br />
/*<br />
* If the script gave us a Last-Modified header, we can't just<br />
* pass it on blindly because of restrictions on future values.<br />
*/<br />
else if (!strcasecmp(w, "Last-Modified")) {<br />
ap_update_mtime(r, apr_date_parse_http(l));<br />
ap_set_last_modified(r);<br />
}<br />
else if (!strcasecmp(w, "Set-Cookie")) {<br />
apr_table_add(cookie_table, w, l);<br />
}<br />
else {<br />
apr_table_add(merge, w, l);<br />
}<br />
}</p>
<p>return OK;<br />
}<br />

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

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

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

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

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

<br />
location = apr_table_get(r-&gt;headers_out, "Location");</p>
<p>if (location &amp;&amp; r-&gt;status == 200) {<br />
/* For a redirect whether internal or not, discard any<br />
* remaining stdout from the script, and log any remaining<br />
* stderr output, as normal. */<br />
discard_script_output(bb);<br />
apr_brigade_destroy(bb);<br />
apr_file_pipe_timeout_set(script_err, r-&gt;server-&gt;timeout);<br />
log_script_err(r, script_err);<br />
}</p>
<p>if (location &amp;&amp; location[0] == '/' &amp;&amp; r-&gt;status == 200) {<br />
/* This redirect needs to be a GET no matter what the original<br />
* method was.<br />
*/<br />
r-&gt;method = apr_pstrdup(r-&gt;pool, "GET");<br />
r-&gt;method_number = M_GET;</p>
<p>/* We already read the message body (if any), so don't allow<br />
* the redirected request to think it has one. We can ignore<br />
* Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR.<br />
*/<br />
apr_table_unset(r-&gt;headers_in, "Content-Length");</p>
<p>ap_internal_redirect_handler(location, r);<br />
return OK;<br />
}<br />
else if (location &amp;&amp; r-&gt;status == 200) {<br />
/* XX Note that if a script wants to produce its own Redirect<br />
* body, it now has to explicitly *say* "Status: 302"<br />
*/<br />
return HTTP_MOVED_TEMPORARILY;<br />
}<br />

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

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

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

<br />
rv = ap_pass_brigade(r-&gt;output_filters, bb);<br />

コメントを残す