/*
 * $Id$
 *
    Copyright (c) 2006-2019 Chung, Hyung-Hwan. All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:
    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.
    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "httpd.h"
#include "../cmn/mem-prv.h"
#include <qse/cmn/str.h>
#include <qse/si/pio.h>
#include <qse/cmn/fmt.h>
#include <qse/cmn/path.h>

#if defined(_WIN32)
	/* nothing */
#elif defined(__OS2__)
	/* nothing */
#elif defined(__DOS__)
	/* nothing */
#else
#	include "../cmn/syscall.h"
#endif

typedef struct task_cgi_arg_t task_cgi_arg_t;
struct task_cgi_arg_t 
{
	qse_mcstr_t path;
	qse_mcstr_t script;
	qse_mcstr_t suffix;
	qse_mcstr_t root;
	qse_mcstr_t shebang;
	int nph;
	qse_httpd_fnc_t fnc;
	qse_htre_t* req;
};

typedef struct task_cgi_t task_cgi_t;
struct task_cgi_t
{
	int init_failed;
	qse_httpd_t* httpd;

	qse_mchar_t* path;
	qse_mchar_t* script;
	qse_mchar_t* suffix;
	qse_mchar_t* root;
	qse_mchar_t* shebang;

	int method;
	qse_http_version_t version;
	int keepalive; /* taken from the request */
	int nph;
	qse_pio_fnc_t fnc;

	qse_htrd_t* script_htrd;
	qse_env_t* env;
	qse_pio_t pio;
	int pio_inited;

#define CGI_REQ_GOTALL     (1 << 0)
#define CGI_REQ_FWDALL     (1 << 1)
#define CGI_REQ_FWDERR     (1 << 2)
#define CGI_REQ_FWDCHUNKED (1 << 3)
	int          reqflags;
	qse_htre_t*  req; /* original request associated with this */
	qse_mbs_t*   reqfwdbuf; /* content from the request */

#define CGI_RES_CLIENT_DISCON        (1 << 0)
#define CGI_RES_CLIENT_CHUNK         (1 << 1)
#define CGI_RES_SCRIPT_LENGTH (1 << 2)
	int          resflags;
	qse_mbs_t*   res;
	qse_mchar_t* res_ptr;
	qse_size_t   res_left;

	/* content-length that CGI returned */
	qse_size_t script_output_length; /* TODO: a script maybe be able to output more than the maximum value of qse_size_t */
	/* the number of octets received from the script */
	qse_size_t script_output_received; 

	qse_mchar_t buf[MAX_SEND_SIZE];
	qse_size_t  buflen;
};

typedef struct cgi_script_htrd_xtn_t cgi_script_htrd_xtn_t;
struct cgi_script_htrd_xtn_t
{
	task_cgi_t* cgi;
	qse_httpd_client_t* client;
	qse_httpd_task_t* task;
};

typedef struct cgi_client_req_hdr_ctx_t cgi_client_req_hdr_ctx_t;
struct cgi_client_req_hdr_ctx_t
{
	qse_httpd_t* httpd;
	qse_env_t* env;
};

static int cgi_capture_client_header (
	qse_htre_t* req, const qse_mchar_t* key, const qse_htre_hdrval_t* val, void* ctx)
{
	cgi_client_req_hdr_ctx_t* hdrctx;

	hdrctx = (cgi_client_req_hdr_ctx_t*)ctx;

	if (qse_mbscasecmp (key, QSE_MT("Connection")) != 0 &&
	    qse_mbscasecmp (key, QSE_MT("Transfer-Encoding")) != 0 &&
	    qse_mbscasecmp (key, QSE_MT("Content-Length")) != 0)
	{
		qse_mchar_t* http_key, * ptr;
		int ret;

		http_key = qse_mbsdup2 (QSE_MT("HTTP_"), key, req->mmgr);
		if (http_key == QSE_NULL)
		{
			hdrctx->httpd->errnum = QSE_HTTPD_ENOMEM;
			return -1;
		}

		for (ptr = http_key; *ptr; ptr++)
		{
			*ptr = QSE_TOMUPPER((unsigned)*ptr);
			if (*ptr == QSE_MT('-')) *ptr = '_';
		}

		ret = qse_env_insertmbs (hdrctx->env, http_key, val->ptr);
		if (ret <= -1) 
		{
			QSE_MMGR_FREE (req->mmgr, http_key);
			hdrctx->httpd->errnum = QSE_HTTPD_ENOMEM;
			return -1;
		}
		QSE_MMGR_FREE (req->mmgr, http_key);

		/* append values with the same key */
		while ((val = val->next))
		{
			if (qse_env_appendmbs (hdrctx->env, QSE_MT(",")) <= -1 ||
			    qse_env_appendmbs (hdrctx->env, val->ptr) <= -1)
			{
				hdrctx->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}
	}

	return 0;
}

static int cgi_add_header_to_buffer (
	task_cgi_t* cgi, qse_mbs_t* buf, const qse_mchar_t* key, const qse_htre_hdrval_t* val)
{
	QSE_ASSERT (val != QSE_NULL);

	do
	{
		if (qse_mbs_cat (buf, key) == (qse_size_t)-1 ||
		    qse_mbs_cat (buf, QSE_MT(": ")) == (qse_size_t)-1 ||
		    qse_mbs_cat (buf, val->ptr) == (qse_size_t)-1 ||
		    qse_mbs_cat (buf, QSE_MT("\r\n")) == (qse_size_t)-1) 
		{
			/* multiple items with the same keys are also 
			 * copied back to the response buffer */
			cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
			return -1;
		}

		val = val->next;
	}
	while (val);

	return 0;
}

static int cgi_capture_client_trailer (qse_htre_t* req, const qse_mchar_t* key, const qse_htre_hdrval_t* val, void* ctx)
{
	task_cgi_t* cgi = (task_cgi_t*)ctx;

	if (qse_mbscasecmp (key, QSE_MT("Content-Length")) != 0 && 
	    qse_mbscasecmp (key, QSE_MT("Connection")) != 0 &&
	    qse_mbscasecmp (key, QSE_MT("Transfer-Encoding")) != 0)
	{
		return cgi_add_header_to_buffer (cgi, cgi->reqfwdbuf, key, val);
	}

	return 0;
}

static int cgi_capture_script_header (qse_htre_t* req, const qse_mchar_t* key, const qse_htre_hdrval_t* val, void* ctx)
{
	task_cgi_t* cgi = (task_cgi_t*)ctx;

	/* capture a header except Status, Connection, Transfer-Encoding, and Server */
	if (qse_mbscasecmp (key, QSE_MT("Status")) != 0 && 
	    qse_mbscasecmp (key, QSE_MT("Connection")) != 0 &&
	    qse_mbscasecmp (key, QSE_MT("Transfer-Encoding")) != 0 &&
	    qse_mbscasecmp (key, QSE_MT("Server")) != 0) 
	{
		return cgi_add_header_to_buffer (cgi, cgi->res, key, val);
	}

	return 0;
}

static void log_cgi_script_error (task_cgi_t* cgi, const qse_mchar_t* shortmsg)
{
	qse_httpd_act_t msg;
	qse_size_t pos = 0;

	msg.code = QSE_HTTPD_CATCH_MERRMSG;
	pos += qse_mbsxcpy (&msg.u.merrmsg[pos], QSE_COUNTOF(msg.u.merrmsg) - pos, shortmsg);
	pos += qse_mbsxcpy (&msg.u.merrmsg[pos], QSE_COUNTOF(msg.u.merrmsg) - pos, cgi->script);
	cgi->httpd->opt.rcb.logact (cgi->httpd, &msg);
}

static int cgi_htrd_peek_script_output (qse_htrd_t* htrd, qse_htre_t* req)
{
	cgi_script_htrd_xtn_t* xtn;
	task_cgi_t* cgi;
	int keepalive;

	xtn = (cgi_script_htrd_xtn_t*) qse_htrd_getxtn (htrd);
	cgi = xtn->cgi;

	QSE_ASSERT (!cgi->nph);

	if (req->attr.status)
	{
		int nstatus;
		const qse_mchar_t* endptr;

/* TODO: check the syntax of status value??? if not numeric??? */
		/*QSE_MBSTONUM (nstatus, req->attr.status, &endptr, 10);*/
		nstatus = qse_mbstoi (req->attr.status, 10, &endptr);

		/* 
		Would it need this kind of extra work?
		while (QSE_ISMSPACE(*endptr)) endptr++;
		if (*endptr == QSE_MT('\0')) ....
		*/

		if (qse_mbs_fcat (cgi->res, QSE_MT("HTTP/%d.%d %d "), cgi->version.major, cgi->version.minor, nstatus) == (qse_size_t)-1 ||
		    qse_mbs_cat (cgi->res, endptr) == (qse_size_t)-1 ||
		    qse_mbs_cat (cgi->res, QSE_MT("\r\n")) == (qse_size_t)-1) 
		{
			cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
			return -1;
		}
	}
	else 
	{
		const qse_htre_hdrval_t* location;

		location = qse_htre_getheaderval (req, QSE_MT("Location"));
		if (location)
		{
			if (qse_mbs_fcat (cgi->res, QSE_MT("HTTP/%d.%d 301 Moved Permanently\r\n"),
			                  cgi->version.major, cgi->version.minor) == (qse_size_t)-1) 
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}

			/* the actual Location header is added by 
			 * qse_htre_walkheaders() below */
		}
		else
		{
			if (qse_mbs_fcat (cgi->res, QSE_MT("HTTP/%d.%d 200 OK\r\n"),
			                  cgi->version.major, cgi->version.minor) == (qse_size_t)-1) 
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}
	}

	/* Add the server header. the server header in the cgi output will
	 * be ignored by cgi_capture_script_header() */
	if (qse_mbs_cat (cgi->res, QSE_MT("Server: ")) == (qse_size_t)-1 ||
	    qse_mbs_cat (cgi->res, qse_httpd_getname (cgi->httpd)) == (qse_size_t)-1 ||
	    qse_mbs_cat (cgi->res, QSE_MT("\r\n")) == (qse_size_t)-1)
	{
		cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
		return -1;
	}

	if (qse_htre_getheaderval (req, QSE_MT("Date")) == QSE_NULL)
	{
		/* generate the Date header if it's not included in the script output */
		if (qse_mbs_cat (cgi->res, QSE_MT("Date: ")) == (qse_size_t)-1 ||
		    qse_mbs_cat (cgi->res, qse_httpd_fmtgmtimetobb (cgi->httpd, QSE_NULL, 0)) == (qse_size_t)-1 ||
		    qse_mbs_cat (cgi->res, QSE_MT("\r\n")) == (qse_size_t)-1)
		{
			cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
			return -1;
		}
	}

	keepalive = cgi->keepalive;
	if (req->flags & QSE_HTRE_ATTR_LENGTH)
	{
		cgi->resflags |= CGI_RES_SCRIPT_LENGTH;
		cgi->script_output_length = req->attr.content_length;
	}
	else
	{
		/* no Content-Length returned by CGI. */
		if (qse_comparehttpversions (&cgi->version, &qse_http_v11) >= 0) 
		{
			/* the client side supports chunking */
			cgi->resflags |= CGI_RES_CLIENT_CHUNK;
			if (qse_mbs_cat (cgi->res, QSE_MT("Transfer-Encoding: chunked\r\n")) == (qse_size_t)-1) 
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}
		else 
		{
			/* If the CGI script doesn't specify Content-Length, 
			 * i can't honor cgi->keepalive in HTTP/1.0
			 * or earlier. Closing the connection is the
			 * only way to specify the content length */
			cgi->resflags |= CGI_RES_CLIENT_DISCON;
			keepalive = 0; 

			if (qse_httpd_entaskdisconnect (
				cgi->httpd, xtn->client, xtn->task) == QSE_NULL) 
			{
				return -1;
			}
		}
	}

	/* NOTE: 
	 *   an explicit 'disconnect' task is added only if
	 *   the orignal keep-alive request can't be honored.
	 *   so if a client specifies keep-alive but doesn't
	 *   close connection, the connection will stay alive
	 *   until it's cleaned up for an error or idle timeout.
	 */

	if (qse_mbs_cat (cgi->res, (keepalive? QSE_MT("Connection: keep-alive\r\n"): QSE_MT("Connection: close\r\n"))) == (qse_size_t)-1) 
	{
		cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
		return -1;
	}

	/* add all other header fields excluding 
	 * Status, Connection, Transfer-Encoding */
	if (qse_htre_walkheaders (req, cgi_capture_script_header, cgi) <= -1) return -1;
	/* end of header */
	if (qse_mbs_cat (cgi->res, QSE_MT("\r\n")) == (qse_size_t)-1) return -1; 

	/* content body begins here */
	cgi->script_output_received = qse_htre_getcontentlen(req);
	if ((cgi->resflags & CGI_RES_SCRIPT_LENGTH) &&
	    cgi->script_output_received > cgi->script_output_length)
	{
		/* cgi returning too much data... something is wrong in CGI */
		if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
			log_cgi_script_error (cgi, QSE_MT("cgi redundant output - "));
		cgi->httpd->errnum = QSE_HTTPD_EINVAL; /* TODO: change it to a better error code */
		return -1;
	}

	if (cgi->script_output_received > 0)
	{
		/* the initial part of the content body has been received 
		 * along with the header. it need to be added to the result 
		 * buffer. */
		if (cgi->resflags & CGI_RES_CLIENT_CHUNK)
		{
			qse_mchar_t buf[64];

			qse_fmtuintmaxtombs (
				buf, QSE_COUNTOF(buf), 
				cgi->script_output_received, 
				16 | QSE_FMTUINTMAXTOMBS_UPPERCASE, 
				-1, QSE_MT('\0'), QSE_NULL);
			if (qse_mbs_cat (cgi->res, buf) == (qse_size_t)-1 ||
			    qse_mbs_cat (cgi->res, QSE_MT("\r\n")) == (qse_size_t)-1)
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}

		if (qse_mbs_ncat (cgi->res, qse_htre_getcontentptr(req), qse_htre_getcontentlen(req)) == (qse_size_t)-1) 
		{
			cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
			return -1;
		}

		if (cgi->resflags & CGI_RES_CLIENT_CHUNK)
		{
			if (qse_mbs_ncat (cgi->res, QSE_MT("\r\n"), 2) == (qse_size_t)-1) 
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}
	}

	return 0; 
}

static qse_htrd_recbs_t cgi_script_htrd_cbs =
{
	cgi_htrd_peek_script_output,
	QSE_NULL /* not needed for CGI */
};

static int cgi_add_env (
	qse_httpd_t* httpd, qse_httpd_client_t* client, 
	qse_env_t* env, qse_htre_t* req, 
	const qse_mchar_t* path, 
	const qse_mchar_t* script,
	const qse_mchar_t* suffix,
	const qse_mchar_t* root,
	const qse_mchar_t* content_type,
	qse_size_t content_length, 
	int chunked)
{
/* TODO: error check for various insert... */

	cgi_client_req_hdr_ctx_t ctx;
	qse_mchar_t buf[128];
	const qse_http_version_t* v;
	const qse_mchar_t* qparam;

	v = qse_htre_getversion(req);
	qparam = qse_htre_getqparam(req);

#ifdef _WIN32
	if (qse_env_insert (env, QSE_T("PATH"), QSE_NULL) <= -1) goto oops_nomem;
#else
	if (qse_env_insertmbs (env, QSE_MT("LANG"), QSE_NULL) <= -1 ||
	    qse_env_insertmbs (env, QSE_MT("PATH"), QSE_NULL)) goto oops_nomem;
#endif

	if (qse_env_insertmbs (env, QSE_MT("GATEWAY_INTERFACE"), QSE_MT("CGI/1.1")) <= -1) goto oops_nomem;
	qse_mbsxfmt (buf, QSE_COUNTOF(buf), QSE_MT("HTTP/%d.%d"), (int)v->major, (int)v->minor);
	if (qse_env_insertmbs (env, QSE_MT("SERVER_PROTOCOL"), buf) <= -1) goto oops_nomem;

	if (qse_env_insertmbs (env, QSE_MT("SCRIPT_FILENAME"), path) <= -1 ||
	    qse_env_insertmbs (env, QSE_MT("SCRIPT_NAME"), script) <= -1 ||
	    qse_env_insertmbs (env, QSE_MT("DOCUMENT_ROOT"), root) <= -1) goto oops_nomem;

	if (suffix && suffix[0] != QSE_MT('\0')) 
	{
		const qse_mchar_t* tmp[3];
		qse_mchar_t* tr;

		tmp[0] = root; 
		tmp[1] = suffix; 
		tmp[2] = QSE_NULL;

		tr = qse_mbsadup(tmp, QSE_NULL, qse_httpd_getmmgr(httpd));
		if (tr) 
		{
			qse_canonmbspath (tr, tr, 0);
			if (qse_env_insertmbs (env, QSE_MT("PATH_TRANSLATED"), tr) <= -1) 
			{
				QSE_MMGR_FREE (qse_httpd_getmmgr(httpd), tr);
				goto oops_nomem;
			}
			QSE_MMGR_FREE (qse_httpd_getmmgr(httpd), tr);
		}
		if (qse_env_insertmbs (env, QSE_MT("PATH_INFO"), suffix) <= -1) goto  oops_nomem;
	}

	if (qse_env_insertmbs (env, QSE_MT("REQUEST_METHOD"), qse_htre_getqmethodname(req)) <= -1 ||
	    qse_env_insertmbs (env, QSE_MT("REQUEST_URI"), qse_htre_getqpath(req)) <= -1) goto oops_nomem;

	if (qparam && qse_env_insertmbs (env, QSE_MT("QUERY_STRING"), qparam) <= -1) goto oops_nomem;

	qse_fmtuintmaxtombs (buf, QSE_COUNTOF(buf), content_length, 10, -1, QSE_MT('\0'), QSE_NULL);
	if (qse_env_insertmbs (env, QSE_MT("CONTENT_LENGTH"), buf) <= -1) goto oops_nomem;
	if (content_type && qse_env_insertmbs (env, QSE_MT("CONTENT_TYPE"), content_type) <= -1) goto oops_nomem;

	if (chunked && qse_env_insertmbs (env, QSE_MT("TRANSFER_ENCODING"), QSE_MT("chunked")) <= 1) goto oops_nomem;

	if (qse_env_insertmbs (env, "SERVER_SOFTWARE", qse_httpd_getname(httpd)) <= -1) goto oops_nomem;
	qse_nwadtombs (&client->local_addr, buf, QSE_COUNTOF(buf), QSE_NWADTOMBS_PORT);
	if (qse_env_insertmbs (env, QSE_MT("SERVER_PORT"), buf) <= -1) goto oops_nomem;
	qse_nwadtombs (&client->local_addr, buf, QSE_COUNTOF(buf), QSE_NWADTOMBS_ADDR);
	if (qse_env_insertmbs (env, QSE_MT("SERVER_ADDR"), buf) <= -1) goto oops_nomem;
	if (qse_env_insertmbs (env, QSE_MT("SERVER_NAME"), buf) <= -1) goto oops_nomem; /* TODO:  convert it to a host name */

	qse_nwadtombs (&client->remote_addr, buf, QSE_COUNTOF(buf), QSE_NWADTOMBS_PORT);
	if (qse_env_insertmbs (env, QSE_MT("REMOTE_PORT"), buf) <= -1) goto oops_nomem;
	qse_nwadtombs (&client->remote_addr, buf, QSE_COUNTOF(buf), QSE_NWADTOMBS_ADDR);
	if (qse_env_insertmbs (env, QSE_MT("REMOTE_ADDR"), buf) <= -1) goto oops_nomem;

	ctx.httpd = httpd;
	ctx.env = env;
	if (qse_htre_walkheaders (req, cgi_capture_client_header, &ctx) <= -1 ||
	    qse_htre_walktrailers (req, cgi_capture_client_header, &ctx) <= -1) goto oops;

	return 0;

oops_nomem:
	httpd->errnum = QSE_HTTPD_ENOMEM;
oops:
	return -1;
}

static int cgi_snatch_client_input (
	qse_htre_t* req, const qse_mchar_t* ptr, qse_size_t len, void* ctx)
{
	qse_httpd_task_t* task;
	task_cgi_t* cgi; 

	task = (qse_httpd_task_t*)ctx;
	cgi = (task_cgi_t*)task->ctx;

#if 0
if (ptr) qse_printf (QSE_T("!!!CGI SNATCHING [%.*hs]\n"), len, ptr);
else qse_printf (QSE_T("!!!CGI SNATCHING DONE\n"));
#endif

	QSE_ASSERT (cgi->req);
	QSE_ASSERT (!(cgi->reqflags & CGI_REQ_GOTALL));

	if (ptr == QSE_NULL)
	{
		/*
		 * this callback is called with ptr of QSE_NULL 
		 * when the request is completed or discarded. 
		 * and this indicates that there's nothing more to read
		 * from the client side. this can happen when the end of
		 * a request is seen or when an error occurs 
		 */
		QSE_ASSERT (len == 0);

		if (cgi->reqflags & CGI_REQ_FWDCHUNKED)
		{
			/* add the 0-sized chunk and trailers */
			if (qse_mbs_cat (cgi->reqfwdbuf, QSE_MT("0\r\n")) == (qse_size_t)-1 ||
			    qse_htre_walktrailers (req, cgi_capture_client_trailer, cgi) <= -1 ||
			    qse_mbs_cat (cgi->reqfwdbuf, QSE_MT("\r\n")) == (qse_size_t)-1)
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}

		/* mark the there's nothing to read form the client side */
		cgi->reqflags |= CGI_REQ_GOTALL;

		/* deregister the callback so that data fed to this 
		 * request object before the current task terminats 
		 * doesn't trigger this callback. */
		qse_htre_unsetconcb (cgi->req);
		cgi->req = QSE_NULL;

		/* since there is no more to read from the client side.
		 * the relay trigger is not needed any more. */
		task->trigger.cmask &= ~QSE_HTTPD_TASK_TRIGGER_READ;

		if (QSE_MBS_LEN(cgi->reqfwdbuf) > 0 && cgi->pio_inited &&
		    !(task->trigger.v[1].mask & QSE_HTTPD_TASK_TRIGGER_WRITE))
		{
			/* there's nothing more to read from the client side.
			 * there's something to forward in the forwarding buffer.
			 * but no write trigger is set. add the write trigger 
			 * for task invocation. */
			task->trigger.v[1].mask = QSE_HTTPD_TASK_TRIGGER_WRITE;
		}
	}
	else if (!(cgi->reqflags & CGI_REQ_FWDERR))
	{
		/* we can write to the child process if a forwarding error 
		 * didn't occur previously. we store data from the client side
		 * to the forwaring buffer only if there's no such previous
		 * error. if an error occurred, we simply drop the data. */
		if (cgi->reqflags & CGI_REQ_FWDCHUNKED)
		{
			qse_mchar_t buf[64];
			qse_fmtuintmaxtombs (
				buf, QSE_COUNTOF(buf), len,
				16 | QSE_FMTUINTMAXTOMBS_UPPERCASE,
				-1, QSE_MT('\0'), QSE_NULL);

			if (qse_mbs_cat (cgi->reqfwdbuf, buf) == (qse_size_t)-1 ||
			    qse_mbs_cat (cgi->reqfwdbuf, QSE_MT("\r\n")) == (qse_size_t)-1 ||
			    qse_mbs_ncat (cgi->reqfwdbuf, ptr, len) == (qse_size_t)-1 ||
			    qse_mbs_cat (cgi->reqfwdbuf, QSE_MT("\r\n")) == (qse_size_t)-1)
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}
		else
		{
			if (qse_mbs_ncat (cgi->reqfwdbuf, ptr, len) == (qse_size_t)-1)
			{
				cgi->httpd->errnum = QSE_HTTPD_ENOMEM;
				return -1;
			}
		}

		/* output pipe to child */
		task->trigger.v[1].mask = QSE_HTTPD_TASK_TRIGGER_WRITE;
#if 0
qse_printf (QSE_T("!!!CGI SNATCHED [%.*hs]\n"), len, ptr);
#endif
	}

	return 0;
}

static void cgi_forward_client_input_to_script (
	qse_httpd_t* httpd, qse_httpd_task_t* task, int writable)
{
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;

	QSE_ASSERT (cgi->reqfwdbuf != QSE_NULL);

	if (QSE_MBS_LEN(cgi->reqfwdbuf) > 0)
	{
		/* there is something to forward in the forwarding buffer. */

		if (cgi->reqflags & CGI_REQ_FWDERR)
		{
			/* a forwarding error has occurred previously.
			 * clear the forwarding buffer */
			qse_mbs_clear (cgi->reqfwdbuf);
		}
		else
		{
			/* normal forwarding */
			qse_ssize_t n;

			n = qse_pio_write (
				&cgi->pio, QSE_PIO_IN,
				QSE_MBS_PTR(cgi->reqfwdbuf),
				QSE_MBS_LEN(cgi->reqfwdbuf)
			);
			if (n <= -1)
			{
				if (qse_pio_geterrnum(&cgi->pio) != QSE_PIO_EAGAIN)
				{
					if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
						log_cgi_script_error (cgi, QSE_MT("cgi pio write error - "));

					cgi->reqflags |= CGI_REQ_FWDERR;
					qse_mbs_clear (cgi->reqfwdbuf); 

					if (!(cgi->reqflags & CGI_REQ_GOTALL))
					{
						QSE_ASSERT (cgi->req);
						qse_htre_discardcontent (cgi->req);

						/* NOTE: 
						 *  this qse_htre_discardcontent() invokes
						 *  cgi_snatch_client_input() 
						 *  which sets cgi->req to QSE_NULL
						 *  and toggles on CGI_REQ_GOTALL. */
						QSE_ASSERT (!cgi->req);
						QSE_ASSERT (cgi->reqflags & CGI_REQ_GOTALL);
					}

					/* mark the end of input to the child explicitly. */
					qse_pio_end (&cgi->pio, QSE_PIO_IN);
					task->trigger.v[1].mask = 0; /* pipe output to child */
				}
			}
			else if (n > 0) 
			{
/* TODO: improve performance.. instead of copying the remaing part 
to the head all the time..  grow the buffer to a certain limit. */
				qse_mbs_del (cgi->reqfwdbuf, 0, n);
				if (QSE_MBS_LEN(cgi->reqfwdbuf) <= 0)
				{
					if (cgi->reqflags & CGI_REQ_GOTALL) goto done;
					else task->trigger.v[1].mask = 0; /* pipe output to child */
				}
			}
		}
	}
	else if (cgi->reqflags & CGI_REQ_GOTALL)
	{
	done:
		/* there is nothing to read from the client side and
		 * there is nothing more to forward from the client-side to the peer 
		 * in the forwarding buffer. */
		QSE_ASSERT (cgi->req == QSE_NULL);

		/* mark the end of input to the child explicitly. */
		qse_pio_end (&cgi->pio, QSE_PIO_IN);

		/* nothing more to write to the cgi script. so exclude 
		 * the input pipe to the script from the mux */
		task->trigger.v[1].mask &= ~QSE_HTTPD_TASK_TRIGGER_WRITE; 

		/* nothing to read from the client side. */
		task->trigger.cmask &= ~QSE_HTTPD_TASK_TRIGGER_READ;
	}
}

static int task_init_cgi (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	task_cgi_t* cgi;
	task_cgi_arg_t* arg;
	qse_size_t content_length;
	qse_size_t len;
	const qse_mchar_t* ptr;
	const qse_htre_hdrval_t* tmp;

	cgi = (task_cgi_t*)qse_httpd_gettaskxtn (httpd, task);
	arg = (task_cgi_arg_t*)task->ctx;

/* TODO: can content length be a different type???
 *  maybe qse_uintmax_t.... it thinks the data size can be larger than the max pointer size 
 * qse_htre_t and qse_htrd_t also needs changes to support it
 */

	QSE_MEMSET (cgi, 0, QSE_SIZEOF(*cgi));
	cgi->httpd = httpd;

	cgi->path = (qse_mchar_t*)(cgi + 1);
	cgi->script = cgi->path + arg->path.len + 1;
	cgi->suffix = cgi->script + arg->script.len + 1;
	cgi->root = cgi->suffix + arg->suffix.len + 1;
	cgi->shebang = cgi->root + arg->root.len + 1;
	qse_mbscpy ((qse_mchar_t*)cgi->path, arg->path.ptr);
	qse_mbscpy ((qse_mchar_t*)cgi->script, arg->script.ptr);
	qse_mbscpy ((qse_mchar_t*)cgi->suffix, arg->suffix.ptr);
	qse_mbscpy ((qse_mchar_t*)cgi->root, arg->root.ptr);
	qse_mbscpy ((qse_mchar_t*)cgi->shebang, arg->shebang.ptr);

	cgi->method = qse_htre_getqmethodtype(arg->req);
	cgi->version = *qse_htre_getversion(arg->req);
	cgi->keepalive = (arg->req->flags & QSE_HTRE_ATTR_KEEPALIVE);
	cgi->nph = arg->nph;
	cgi->req = QSE_NULL;
	if (arg->fnc.ptr) 
	{
		/* the function pointer is set */
		cgi->fnc.ptr = arg->fnc.ptr;
		cgi->fnc.ctx = arg->fnc.ctx;
	}

	content_length = 0;
	if (arg->req->state & QSE_HTRE_DISCARDED) goto done;

	len = qse_htre_getcontentlen(arg->req);
	if ((arg->req->state & QSE_HTRE_COMPLETED) && len <= 0)
	{
		/* the content part is completed and no content 
		 * in the content buffer. there is nothing to forward */
		cgi->reqflags |= CGI_REQ_GOTALL;
		goto done;
	}

	if (!(arg->req->state & QSE_HTRE_COMPLETED) &&
	    !(arg->req->flags & QSE_HTRE_ATTR_LENGTH))
	{
		/* if the request is not completed and doesn't have
		 * content-length set, it's not really possible to
		 * pass the content. this function, however, allows
		 * such a request to entask a cgi script dropping the
		 * content */

		if (httpd->opt.trait & QSE_HTTPD_CGINOCHUNKED)
		{
			qse_htre_discardcontent (arg->req);
			cgi->reqflags |= CGI_REQ_GOTALL;
		}
		else 
		{
			/* do chunking to cgi */
			cgi->reqfwdbuf = qse_mbs_open (qse_httpd_getmmgr(httpd), 0, (len < 512? 512: len));
			if (cgi->reqfwdbuf == QSE_NULL) 
			{
				httpd->errnum = QSE_HTTPD_ENOMEM;
				goto oops;
			}

			if (len > 0)
			{
				qse_mchar_t buf[64];

				qse_fmtuintmaxtombs (
					buf, QSE_COUNTOF(buf), len,
					16 | QSE_FMTUINTMAXTOMBS_UPPERCASE,
					-1, QSE_MT('\0'), QSE_NULL);

				ptr = qse_htre_getcontentptr(arg->req);
				if (qse_mbs_cat (cgi->reqfwdbuf, buf) == (qse_size_t)-1 ||
				    qse_mbs_cat (cgi->reqfwdbuf, QSE_MT("\r\n")) == (qse_size_t)-1 ||
				    qse_mbs_ncat (cgi->reqfwdbuf, ptr, len) == (qse_size_t)-1 ||
				    qse_mbs_cat (cgi->reqfwdbuf, QSE_MT("\r\n")) == (qse_size_t)-1) 
				{
					httpd->errnum = QSE_HTTPD_ENOMEM;
					goto oops;
				}

			}

			cgi->reqflags |= CGI_REQ_FWDCHUNKED;
			cgi->req = arg->req; 
			qse_htre_setconcb (cgi->req, cgi_snatch_client_input, task);
		}
	}
	else
	{	
		/* create a buffer to hold request content from the client
		 * and copy content received already */
		cgi->reqfwdbuf = qse_mbs_open (qse_httpd_getmmgr(httpd), 0, (len < 512? 512: len));
		if (cgi->reqfwdbuf == QSE_NULL) 
		{
			httpd->errnum = QSE_HTTPD_ENOMEM;
			goto oops;
		}

		ptr = qse_htre_getcontentptr(arg->req);
		if (qse_mbs_ncpy (cgi->reqfwdbuf, ptr, len) == (qse_size_t)-1) 
		{
			httpd->errnum = QSE_HTTPD_ENOMEM;
			goto oops;
		}

		if (arg->req->state & QSE_HTRE_COMPLETED)
		{
			/* no furthur forwarding is needed. 
			 * even a chunked request entaksed when completed 
			 * should reach here. if content-length is set
			 * the length should match len. */
			QSE_ASSERT (len > 0);
			QSE_ASSERT (!(arg->req->flags & QSE_HTRE_ATTR_LENGTH) ||
			            ((arg->req->flags & QSE_HTRE_ATTR_LENGTH) && 
			             arg->req->attr.content_length == len));
			cgi->reqflags |= CGI_REQ_GOTALL;
			content_length = len;
		}
		else
		{
			/* CGI entasking is invoked probably from the peek handler
			 * that was triggered after the request header is received.
			 * you can know this because the request is not completed.
			 * In this case, arrange to forward content
			 * bypassing the buffer in the request object itself. */

/* TODO: callback chain instead of a single pointer??? 
       if the request is already set up with a callback, something will go wrong.
*/
			/* set up a callback to be called when the request content
			 * is fed to the htrd reader. qse_htre_addcontent() that 
			 * htrd calls invokes this callback. */

			/* remember this request pointer if and only if the content 
			 * is not fully read. so reading into this request object
			 * is guaranteed not to break integrity of this object */
			cgi->req = arg->req; 
			qse_htre_setconcb (cgi->req, cgi_snatch_client_input, task);

			QSE_ASSERT (arg->req->flags & QSE_HTRE_ATTR_LENGTH);
			content_length = arg->req->attr.content_length;
		}
	}

done:
	cgi->env = qse_env_open (qse_httpd_getmmgr(httpd), 0, 0);
	if (cgi->env == QSE_NULL) 
	{
		httpd->errnum = QSE_HTTPD_ENOMEM;
		goto oops;
	}

	/* get the content type header value */
	tmp = qse_htre_getheaderval(arg->req, QSE_MT("Content-Type"));
	if (tmp) while (tmp->next) tmp = tmp->next; /* get the last value */

	if (cgi_add_env (
		httpd, client, cgi->env, arg->req, 
		cgi->path, cgi->script, cgi->suffix, cgi->root, 
		(tmp? tmp->ptr: QSE_NULL), content_length, 
		(cgi->reqflags & CGI_REQ_FWDCHUNKED)) <= -1)
	{
		goto oops;
	}

	/* no triggers yet since the main loop doesn't allow me to set 
	 * triggers in the task initializer. however the main task handler
	 * will be invoked so long as the client handle is writable by
	 * the main loop. */

	task->ctx = cgi;
	return 0;

oops:
	/* since a new task can't be added in the initializer,
	 * i mark that initialization failed and let task_main_cgi()
	 * add an error task */
	cgi->init_failed = 1;
	task->ctx = cgi;

	if (cgi->env)
	{
		qse_env_close (cgi->env);
		cgi->env = QSE_NULL;
	}
	if (cgi->reqfwdbuf)
	{
		qse_mbs_close (cgi->reqfwdbuf);
		cgi->reqfwdbuf = QSE_NULL;
	}

	return 0;
}

static void task_fini_cgi (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;
	if (cgi->env) qse_env_close (cgi->env);
	if (cgi->pio_inited) 
	{
		/* kill cgi in case it is still alive.
		 * qse_pio_wait() in qse_pio_fini() can block. */
		qse_pio_kill (&cgi->pio); 
		qse_pio_fini (&cgi->pio);
	}
	if (cgi->res) qse_mbs_close (cgi->res);
	if (cgi->script_htrd) qse_htrd_close (cgi->script_htrd);
	if (cgi->reqfwdbuf) qse_mbs_close (cgi->reqfwdbuf);
	if (cgi->req) 
	{
		/* this task is destroyed but the request
		 * associated is still alive. so clear the 
		 * callback to prevent the callback call. */
		QSE_ASSERT (!(cgi->reqflags & CGI_REQ_GOTALL));
		qse_htre_unsetconcb (cgi->req);
	}
}

static QSE_INLINE qse_ssize_t cgi_read_script_output_to_buffer (
	qse_httpd_t* httpd, qse_httpd_client_t* client, task_cgi_t* cgi)
{
	qse_ssize_t n;

	n = qse_pio_read (
		&cgi->pio, QSE_PIO_OUT,
		&cgi->buf[cgi->buflen], 
		QSE_SIZEOF(cgi->buf) - cgi->buflen
	);
	if (n <= -1)
	{
		if (qse_pio_geterrnum(&cgi->pio) != QSE_PIO_EAGAIN)
		{
			if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
				log_cgi_script_error (cgi, QSE_MT("cgi pio read error - "));
			return -1;
		}

		n = -999;
	}
	else if (n > 0) cgi->buflen += n;

	return n;
}

static QSE_INLINE qse_ssize_t cgi_write_script_output_to_client (
	qse_httpd_t* httpd, qse_httpd_client_t* client, task_cgi_t* cgi)
{
	qse_ssize_t n;

	QSE_ASSERT (cgi->buflen > 0);

	httpd->errnum = QSE_HTTPD_ENOERR;
	n = httpd->opt.scb.client.send (httpd, client, cgi->buf, cgi->buflen);
	if (n <= -1)
	{
		if (httpd->errnum != QSE_HTTPD_EAGAIN)
		{
			if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
				log_cgi_script_error (cgi, QSE_MT("cgi write error to client - "));
		}
		else n = 0;
	}
	else if (n > 0)
	{
		QSE_MEMCPY (&cgi->buf[0], &cgi->buf[n], cgi->buflen - n);
		cgi->buflen -= n;
	}

	return n;
}

static int task_main_cgi_5 (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;

	QSE_ASSERT (cgi->pio_inited);

	if (cgi->reqfwdbuf) 
	{
		if (task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_READABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 0);
		}
		else if (task->trigger.v[1].mask & QSE_HTTPD_TASK_TRIGGER_WRITABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 1);
		}
	}

	if (/*(task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_WRITABLE) && */ cgi->buflen > 0)
	{
		if (cgi_write_script_output_to_client (httpd, client, cgi) <= -1)
		{
			/* can't return internal server error any more... */
			return -1;
		}
	}

	/* if forwarding didn't finish, something is not really right... 
	 * so long as the output from CGI is finished, no more forwarding
	 * is performed */
	return (cgi->buflen > 0 || !(cgi->reqflags & CGI_REQ_GOTALL) ||
	        (cgi->reqfwdbuf && QSE_MBS_LEN(cgi->reqfwdbuf) > 0))? 1: 0;
}

static int task_main_cgi_4_nph (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;
	qse_ssize_t n;

	QSE_ASSERT (cgi->nph);

	if (cgi->reqfwdbuf) 
	{
		if (task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_READABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 0);
		}
		else if (task->trigger.v[1].mask & QSE_HTTPD_TASK_TRIGGER_WRITABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 1);
		}
	}

	if (task->trigger.v[0].mask & QSE_HTTPD_TASK_TRIGGER_READABLE)
	{
		if (cgi->buflen < QSE_SIZEOF(cgi->buf))
		{
			n = qse_pio_read (
				&cgi->pio, QSE_PIO_OUT,
				&cgi->buf[cgi->buflen], 
				QSE_SIZEOF(cgi->buf) - cgi->buflen
			);
			if (n <= -1)
			{
				if (qse_pio_geterrnum(&cgi->pio) != QSE_PIO_EAGAIN)
				{
					if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
						log_cgi_script_error (cgi, QSE_MT("cgi pio read error - "));
					return -1;
				}
			}
			else if (n == 0)
			{
				/* switch to the next phase */
				task->main = task_main_cgi_5;
				task->trigger.v[0].mask = 0;
				task->trigger.cmask |= QSE_HTTPD_TASK_TRIGGER_WRITE;
				return 1;
			}
			else cgi->buflen += n;
		}

		if (cgi->buflen > 0 && cgi_write_script_output_to_client (httpd, client, cgi) <= -1) return -1;
	}

	return 1;
}

static int task_main_cgi_4 (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;
	qse_ssize_t n;
	
	QSE_ASSERT (!cgi->nph);
	QSE_ASSERT (cgi->pio_inited);

#if 0
printf ("task_main_cgi_4 trigger[0].mask=%d trigger[1].mask=%d cmask=%d\n", 
	task->trigger.v[0].mask, task->trigger.v[1].mask, task->trigger.cmask);
#endif

	if (cgi->reqfwdbuf) 
	{
		if (task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_READABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 0);
		}
		else if (task->trigger.v[1].mask & QSE_HTTPD_TASK_TRIGGER_WRITABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 1);
		}
	}

	if (task->trigger.v[0].mask & QSE_HTTPD_TASK_TRIGGER_READABLE)
	{
		if (cgi->resflags & CGI_RES_CLIENT_CHUNK)
		{
			qse_size_t count, extra;

			/* this function assumes that the chunk length does not 
			 * exceed 4 hexadecimal digits. */
			QSE_ASSERT (QSE_SIZEOF(cgi->buf) <= 0xFFFF);
	
#define CHLEN_RESERVE 6 

			extra = CHLEN_RESERVE + 2;
			count = QSE_SIZEOF(cgi->buf) - cgi->buflen;
			if (count > extra)
			{
				n = qse_pio_read (
					&cgi->pio, QSE_PIO_OUT,
					&cgi->buf[cgi->buflen + CHLEN_RESERVE], 
					count - extra
				);
				if (n <= -1)
				{
					if (qse_pio_geterrnum(&cgi->pio) != QSE_PIO_EAGAIN)
					{
						if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
							log_cgi_script_error (cgi, QSE_MT("cgi pio read error - "));
						return -1;
					}
				}
				else if (n == 0) 
				{
					/* the cgi script closed the output */
					cgi->buf[cgi->buflen++] = QSE_MT('0');
					cgi->buf[cgi->buflen++] = QSE_MT('\r');
					cgi->buf[cgi->buflen++] = QSE_MT('\n');
					cgi->buf[cgi->buflen++] = QSE_MT('\r');
					cgi->buf[cgi->buflen++] = QSE_MT('\n');

					task->main = task_main_cgi_5;
					task->trigger.v[0].mask = 0;
					task->trigger.cmask |= QSE_HTTPD_TASK_TRIGGER_WRITE;
					return 1;
				}
				else
				{
					/* set the chunk length. if the length string is less 
					 * than 4 digits, the right side of the string is filled
					 * with space letters. for example, the chunk length line
					 * for the length 10 will be "A   \r\n". */
					cgi->buflen += qse_fmtuintmaxtombs (
						&cgi->buf[cgi->buflen], CHLEN_RESERVE - 2 + 1,
						n, 16 | QSE_FMTUINTMAXTOMBS_UPPERCASE | QSE_FMTUINTMAXTOMBS_FILLRIGHT, 
						-1, QSE_MT(' '), QSE_NULL
					); 
					cgi->buf[cgi->buflen++] = QSE_MT('\r');
					cgi->buf[cgi->buflen++] = QSE_MT('\n');

					cgi->buflen += n; /* +n for the data read above */

					/* set the trailing CR & LF for a chunk */
					cgi->buf[cgi->buflen++] = QSE_MT('\r');
					cgi->buf[cgi->buflen++] = QSE_MT('\n');
		
					cgi->script_output_received += n;

					if ((cgi->resflags & CGI_RES_SCRIPT_LENGTH) &&
					    cgi->script_output_received > cgi->script_output_length)
					{
						/* cgi returning too much data... something is wrong in CGI */
						if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
							log_cgi_script_error (cgi, QSE_MT("cgi redundant output - "));
						return -1;
					}
				}
			}
		}
		else
		{
			if (cgi->buflen < QSE_SIZEOF(cgi->buf))
			{
				n = qse_pio_read (
					&cgi->pio, QSE_PIO_OUT,
					&cgi->buf[cgi->buflen], 
					QSE_SIZEOF(cgi->buf) - cgi->buflen
				);
				if (n <= -1)
				{
					if (qse_pio_geterrnum(&cgi->pio) != QSE_PIO_EAGAIN)
					{
						if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
							log_cgi_script_error (cgi, QSE_MT("cgi pio read error - "));
						return -1;
					}
				}
				else if (n == 0) 
				{
					/* switch to the next phase */
					task->main = task_main_cgi_5;
					task->trigger.v[0].mask = 0;
					task->trigger.cmask |= QSE_HTTPD_TASK_TRIGGER_WRITE;
					return 1;
				}
				else
				{
					cgi->script_output_received += n;
					if ((cgi->resflags & CGI_RES_SCRIPT_LENGTH) &&
						cgi->script_output_received > cgi->script_output_length)
					{
						/* cgi returning too much data... something is wrong in CGI */
						if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
							log_cgi_script_error (cgi, QSE_MT("cgi redundant output - "));
						return -1;
					}
				}
			}
		}
	
		/* the main loop invokes the task function only if the client 
		 * side is writable. it should be safe to write whenever
		 * this task function is called. */
		if (cgi->buflen > 0 && cgi_write_script_output_to_client (httpd, client, cgi) <= -1) return -1;
	}

	return 1;
}

static int task_main_cgi_3 (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	/* send the http initial line and headers built using the headers
	 * returned by CGI. it may include some contents as well */

	task_cgi_t* cgi = (task_cgi_t*)task->ctx;
	qse_ssize_t n;
	qse_size_t count;

	QSE_ASSERT (!cgi->nph);

#if 0
printf ("task_main_cgi_3 trigger[0].mask=%d trigger[1].mask=%d cmask=%d\n", 
	task->trigger.v[0].mask, task->trigger.v[1].mask, task->trigger.cmask);
#endif
	if (cgi->reqfwdbuf)
	{
		if (task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_READABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 0);
		}
		else if (task->trigger.v[1].mask & QSE_HTTPD_TASK_TRIGGER_WRITABLE)
		{
			cgi_forward_client_input_to_script (httpd, task, 1);
		}
	}

	/* send the partial reponse received with the initial line and headers
	 * so long as the client-side handle is writable... */
	if (/*(task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_WRITABLE) && */ cgi->res_left > 0)
	{
		count = cgi->res_left;
		if (count >= MAX_SEND_SIZE) count = MAX_SEND_SIZE;

		httpd->errnum = QSE_HTTPD_ENOERR;
		n = httpd->opt.scb.client.send (httpd, client, cgi->res_ptr, count);
		if (n <= -1) 
		{
			if (httpd->errnum != QSE_HTTPD_EAGAIN)
			{
				if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
					log_cgi_script_error (cgi, QSE_MT("cgi initial write error to client - "));
				return -1;
			}
		}
		else if (n > 0)
		{
			cgi->res_ptr += n;
			cgi->res_left -= n;

			if (cgi->res_left <= 0) 
			{
				qse_mbs_clear (cgi->res);

				if ((cgi->resflags & CGI_RES_SCRIPT_LENGTH) &&
				    cgi->script_output_received >= cgi->script_output_length)
				{
					/* if a cgi script specified the content length
					 * and it has emitted as much as the length,
					 * i don't wait for the script to finish.
					 * one potential side-effect is that the script
					 * can be killed prematurely if it wants to do
					 * something extra after having done so.
					 * however, a CGI script shouln't do that... */ 
					task->main = task_main_cgi_5;
					task->trigger.cmask |= QSE_HTTPD_TASK_TRIGGER_WRITE;
				}
				else
				{
					task->main = task_main_cgi_4;
					task->trigger.cmask &= ~QSE_HTTPD_TASK_TRIGGER_WRITE;
				}
				return 1;
			}
		}
	}

	return 1; /* more work to do */
}

static int task_main_cgi_2 (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	/* several calls to this function will read output from the cgi 
	 * until the end of header is reached. when the end is reached, 
	 * it is possible that some contents are also read in.
	 * The callback function to qse_htrd_feed() handles this also.
	 */
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;
	qse_ssize_t n;
	
	QSE_ASSERT (!cgi->nph);
	QSE_ASSERT (cgi->pio_inited);

#if 0
printf ("task_main_cgi_2 trigger[0].mask=%d trigger[1].mask=%d cmask=%d\n", 
	task->trigger.v[0].mask, task->trigger.v[1].mask, task->trigger.cmask);
#endif

	if (cgi->reqfwdbuf) 
	{
		if (task->trigger.cmask & QSE_HTTPD_TASK_TRIGGER_READABLE)
		{
			/* client side is readable */
			cgi_forward_client_input_to_script (httpd, task, 0);
		}
		else if (task->trigger.v[1].mask & QSE_HTTPD_TASK_TRIGGER_WRITABLE)
		{
			/* can write to the input pipe to the cgi script */
			cgi_forward_client_input_to_script (httpd, task, 1);
		}
	}

	if (task->trigger.v[0].mask & QSE_HTTPD_TASK_TRIGGER_READABLE)
	{
		/* can read from cgi's output pipe */

		n = qse_pio_read (
			&cgi->pio, QSE_PIO_OUT,
			&cgi->buf[cgi->buflen], 
			QSE_SIZEOF(cgi->buf) - cgi->buflen
		);
		if (n <= -1)
		{
			if (qse_pio_geterrnum(&cgi->pio) != QSE_PIO_EAGAIN)
			{
				/* can't return internal server error any more... */
				if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
					log_cgi_script_error (cgi, QSE_MT("cgi pio read error - "));
				goto oops;
			}
		}
		else if (n == 0) 
		{
			/* end of output from cgi before it has seen a header.
			 * the cgi script must be crooked. */
			if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
				log_cgi_script_error (cgi, QSE_MT("cgi premature eof - "));
			goto oops;
		}
		else
		{
			cgi->buflen += n;
		}

		if (cgi->buflen > 0)
		{
			if (qse_htrd_feed (cgi->script_htrd, cgi->buf, cgi->buflen) <= -1)
			{
				if (cgi->httpd->opt.trait & QSE_HTTPD_LOGACT) 
					log_cgi_script_error (cgi, QSE_MT("cgi feed error - "));
				goto oops;
			}

			cgi->buflen = 0;
		}

		if (QSE_MBS_LEN(cgi->res) > 0)
		{
			/* the htrd handler composed some response.
			 * this means that at least it finished processing CGI headers.
			 * some contents could be in cgi->res, though.
			 *
			 * qse_htrd_feed() must have executed the peek handler
			 * (cgi_htrd_peek_script_output()) which handled the 
			 * request header. so i won't call qse_htrd_feed() any more.
			 * intead, i'll simply read directly from the pipe.
			 */
			cgi->res_ptr = QSE_MBS_PTR(cgi->res);
			cgi->res_left = QSE_MBS_LEN(cgi->res);

			task->main = task_main_cgi_3;
			task->trigger.cmask |= QSE_HTTPD_TASK_TRIGGER_WRITE;
			return 1;
		}
	}

	/* complete headers not seen yet. i need to be called again */
	return 1;

oops:
	return (qse_httpd_entaskerrorwithmvk (httpd, client, task, 500, cgi->method, &cgi->version, cgi->keepalive) == QSE_NULL)? -1: 0;
}

static int task_main_cgi (
	qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
	task_cgi_t* cgi = (task_cgi_t*)task->ctx;
	int pio_options, x;
	int http_errnum = 500;
	qse_mchar_t* xpath;

	if (cgi->init_failed) goto oops;

	if (cgi->nph)
	{
		/* i cannot know how long the content will be
		 * since i don't parse the header. so i have to close
		 * the connection regardless of content-length or transfer-encoding
		 * in the actual header. */
		if (qse_httpd_entaskdisconnect (httpd, client, task) == QSE_NULL) goto oops;
	}
	else
	{
		cgi_script_htrd_xtn_t* xtn;
		cgi->script_htrd = qse_htrd_open (qse_httpd_getmmgr(httpd), QSE_SIZEOF(cgi_script_htrd_xtn_t));
		if (cgi->script_htrd == QSE_NULL) goto oops;

		xtn = (cgi_script_htrd_xtn_t*) qse_htrd_getxtn (cgi->script_htrd);
		xtn->cgi = cgi;
		xtn->task = task;
		xtn->client = client;
		qse_htrd_setrecbs (cgi->script_htrd, &cgi_script_htrd_cbs);
		qse_htrd_setopt (
			cgi->script_htrd, 
			QSE_HTRD_SKIPINITIALLINE | 
			QSE_HTRD_PEEKONLY | 
			QSE_HTRD_REQUEST
		);

		cgi->res = qse_mbs_open (qse_httpd_getmmgr(httpd), 0, 256);
		if (cgi->res == QSE_NULL) goto oops;
	}

	/* <<WARNING>>
	 *  QSE_PIO_INNOBLOCK and QSE_PIO_OUTNONBLOCK are not supported
	 *   on non-unix/linux platforms. so the CGI task can only be
	 *   used on unix/linux platforms */
	pio_options = QSE_PIO_READOUT | QSE_PIO_WRITEIN | QSE_PIO_MBSCMD | QSE_PIO_INNOBLOCK | QSE_PIO_OUTNOBLOCK;
	if (httpd->opt.trait & QSE_HTTPD_CGIERRTONUL)
		pio_options |= QSE_PIO_ERRTONUL;
	else
		pio_options |= QSE_PIO_ERRTOOUT;
	if (httpd->opt.trait & QSE_HTTPD_CGINOCLOEXEC) 
		pio_options |= QSE_PIO_NOCLOEXEC;

	if (cgi->fnc.ptr)
	{
		xpath = (qse_mchar_t*)&cgi->fnc;
		pio_options |= QSE_PIO_FNCCMD;
	}
	else
	{
		if (cgi->shebang[0] != QSE_MT('\0'))
		{
			const qse_mchar_t* tmp[4];
			tmp[0] = cgi->shebang;
			tmp[1] = QSE_MT(" ");
			tmp[2] = cgi->path;
			tmp[3] = QSE_NULL;
			xpath = qse_mbsadup (tmp, QSE_NULL, qse_httpd_getmmgr(httpd));
			if (xpath == QSE_NULL) goto oops;
		}
		else xpath = cgi->path;
	}

	x = qse_pio_init (
		&cgi->pio, qse_httpd_getmmgr(httpd), (const qse_char_t*)xpath,
		cgi->env, pio_options);
	if (xpath != cgi->path && 
	    xpath != (qse_mchar_t*)&cgi->fnc) QSE_MMGR_FREE (qse_httpd_getmmgr(httpd), xpath);

	if (x <= -1)
	{
		qse_pio_errnum_t errnum;

		errnum = qse_pio_geterrnum (&cgi->pio);
		if (errnum == QSE_PIO_ENOENT) http_errnum = 404;
		else if (errnum == QSE_PIO_EACCES) http_errnum = 403;

		goto oops;
	}

	cgi->pio_inited = 1;

	/* set the trigger that the main loop can use this
	 * handle for multiplexing 
	 *
	 * if the output from the child is available, this task
	 * writes it back to the client. so add a trigger for
	 * checking the data availability from the child process */
	task->trigger.v[0].mask = QSE_HTTPD_TASK_TRIGGER_READ;
	task->trigger.v[0].handle = qse_pio_gethnd (&cgi->pio, QSE_PIO_OUT);
	task->trigger.v[1].handle = qse_pio_gethnd (&cgi->pio, QSE_PIO_IN);
	task->trigger.cmask = 0;

	if (cgi->reqfwdbuf)
	{
		/* the existence of the forwarding buffer leads to a trigger
		 * for checking data availiability from the client side. */

		if (cgi->req)
		{
			/* there are still things to forward from the client-side. 
			 * i can rely on this relay trigger for task invocation. */
			task->trigger.cmask = QSE_HTTPD_TASK_TRIGGER_READ;
		}

		if (QSE_MBS_LEN(cgi->reqfwdbuf) > 0)
		{
			/* there's nothing more to read from the client side but
			 * some contents are already read into the forwarding buffer.
			 * this is possible because the main loop can still read 
			 * between the initializer function (task_init_cgi()) and 
			 * this function. so let's forward it initially. */
			cgi_forward_client_input_to_script (httpd, task, 0);

			/* if the initial forwarding clears the forwarding 
			 * buffer, there is nothing more to forward. 
			 * (nothing more to read from the client side, nothing 
			 * left in the forwarding buffer). if not, this task should
			 * still be invoked for forwarding.
			 */
			if (QSE_MBS_LEN(cgi->reqfwdbuf) > 0)
			{
				/* since the buffer is not cleared, this task needs
				 * a trigger for invocation. ask the main loop to
				 * invoke this task so long as it is able to write
				 * to the child process */
				task->trigger.v[1].mask = QSE_HTTPD_TASK_TRIGGER_WRITE;
			}
		}
	}
	else
	{
		/* no forwarding buffer. the request should not send any contents
		 * to the cgi script. close the input to the script */
		qse_pio_end (&cgi->pio, QSE_PIO_IN);
	}

	task->main = cgi->nph? task_main_cgi_4_nph: task_main_cgi_2;
	return 1;

oops:
	if (cgi->res) 
	{
		qse_mbs_close (cgi->res);
		cgi->res = QSE_NULL;
	}
	if (cgi->script_htrd) 
	{
		qse_htrd_close (cgi->script_htrd);
		cgi->script_htrd = QSE_NULL;
	}

	return (qse_httpd_entaskerrorwithmvk (
		httpd, client, task, http_errnum, 
		cgi->method, &cgi->version, cgi->keepalive) == QSE_NULL)? -1: 0;
}

/* TODO: global option or individual paramter for max cgi lifetime 
*        non-blocking pio read ...
*/

qse_httpd_task_t* qse_httpd_entaskcgi (
	qse_httpd_t* httpd,
	qse_httpd_client_t* client,
	qse_httpd_task_t* pred,
	const qse_httpd_rsrc_cgi_t* cgi,
	qse_htre_t* req)
{
	qse_httpd_task_t task;
	task_cgi_arg_t arg;
	qse_httpd_rsrc_cgi_t rsrc;

	QSE_MEMSET (&arg, 0, QSE_SIZEOF(arg));

	rsrc = *cgi;
	if (rsrc.flags & QSE_HTTPD_RSRC_CGI_FNC)
	{
		/* rsrc.script must carry a pointer to qse_pio_fnc_t */
		if (rsrc.script == QSE_NULL || ((qse_pio_fnc_t*)rsrc.script)->ptr == QSE_NULL)
		{
			httpd->errnum = QSE_HTTPD_EINVAL;
			return QSE_NULL;
		}

		arg.fnc.ptr = (qse_httpd_fncptr_t)rsrc.path;
		arg.fnc.ctx = (void*)rsrc.shebang;

		/* reset the script to an empty string for less interference 
		 * with code handling normal script */
		rsrc.path = QSE_MT("");
		rsrc.shebang = QSE_MT("");
	}
	else
	{
		QSE_ASSERT (rsrc.path != QSE_NULL);
		if (rsrc.shebang == QSE_NULL) rsrc.shebang = QSE_MT("");
	}

	if (rsrc.script == QSE_NULL) rsrc.script = qse_htre_getqpath(req);
	if (rsrc.suffix == QSE_NULL) rsrc.suffix = QSE_MT("");
	if (rsrc.root == QSE_NULL) rsrc.root = QSE_MT("");

	arg.path.ptr = (qse_mchar_t*)rsrc.path;
	arg.path.len = qse_mbslen(rsrc.path);
	arg.script.ptr = (qse_mchar_t*)rsrc.script;
	arg.script.len = qse_mbslen(rsrc.script);
	arg.suffix.ptr = (qse_mchar_t*)rsrc.suffix;
	arg.suffix.len = qse_mbslen(rsrc.suffix);
	arg.root.ptr = (qse_mchar_t*)rsrc.root;
	arg.root.len = qse_mbslen(rsrc.root);
	arg.nph = ((rsrc.flags & QSE_HTTPD_RSRC_CGI_NPH) != 0);
	arg.shebang.ptr = (qse_mchar_t*)rsrc.shebang;
	arg.shebang.len = qse_mbslen(rsrc.shebang);
	arg.req = req;

	QSE_MEMSET (&task, 0, QSE_SIZEOF(task));
	task.init = task_init_cgi;
	task.fini = task_fini_cgi;
	task.main = task_main_cgi;
	task.ctx = &arg;

	return qse_httpd_entask (
		httpd, client, pred, &task, 
		QSE_SIZEOF(task_cgi_t) + 
		((arg.path.len + 1) * QSE_SIZEOF(*arg.path.ptr)) + 
		((arg.script.len + 1) * QSE_SIZEOF(*arg.script.ptr)) + 
		((arg.suffix.len + 1) * QSE_SIZEOF(*arg.suffix.ptr)) +
		((arg.root.len + 1) * QSE_SIZEOF(*arg.root.ptr)) +
		((arg.shebang.len + 1) * QSE_SIZEOF(*arg.shebang.ptr))
	);
}
