Skip to content
Snippets Groups Projects
thttpd_cgi.c 27.4 KiB
Newer Older
/****************************************************************************
 * netutils/thttpd/thttpd_cgi.c
 * CGI support
 *
 *   Copyright (C) 2009 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <spudmonkey@racsa.co.cr>
 *
 * Derived from the file libhttpd.c in the original THTTPD package:
 *
 *   Copyright  1995,1998,1999,2000,2001 by Jef Poskanzer <jef@mail.acme.com>.
 *   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 AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/symtab.h>
#include <nuttx/binfmt.h>
#include <net/uip/thttpd.h>

#include "config.h"
#include "libhttpd.h"
#include "thttpd_alloc.h"
#include "thttpd_strings.h"
#include "timers.h"
#include "fdwatch.h"

#if defined(CONFIG_THTTPD) && defined(CONFIG_THTTPD_CGI_PATTERN)

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/****************************************************************************
 * Private Types
 ****************************************************************************/

enum cgi_outbuffer_e
{
  CGI_OUTBUFFER_READHEADER = 0,  /* Reading header from HTTP client */
  CGI_OUTBUFFER_HEADERREAD,      /* Header has been read */
  CGI_OUTBUFFER_HEADERSENT,      /* Header has been sent to the CGI program */
  CGI_OUTBUFFER_READDATA,        /* Transferring data from CGI to client */
  CGI_OUTBUFFER_DONE,            /* Finished */
};

struct cgi_outbuffer_s
{
  enum cgi_outbuffer_e state;    /* State of the transfer */
  char  *buffer;                 /* Allocated I/O buffer */
  size_t size;                   /* Size of the allocation */
  size_t len;                    /* Amount of valid data in the allocated buffer */
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static void create_environment(httpd_conn *hc);
static char **make_argp(httpd_conn *hc);
static inline int cgi_interpose_input(httpd_conn *hc, int wfd, char *buffer);
static inline int cgi_interpose_output(httpd_conn *hc, int rfd, char *inbuffer,
patacongo's avatar
patacongo committed
                                       struct cgi_outbuffer_s *hdr);
static int  cgi_child(int argc, char **argv);

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* Used to hold off the main task until the CGI tasks have been configured */

static sem_t g_cgisem;

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/* semaphore helpers */

static inline void cgi_semtake(void)
{
  while (sem_wait(&g_cgisem) != 0)
    {
      /* The only case that an error should occr here is if the wait was\
       * awakened by a signal.
       */

      ASSERT(*get_errno_ptr() == EINTR);
    }
}

static inline void cgi_semgive(void)
{
  sem_post(&g_cgisem);
}

/* Set up environment variables. Be real careful here to avoid
 * letting malicious clients overrun a buffer.  We don't have
 * to worry about freeing stuff since we're a sub-task.
 */

static void create_environment(httpd_conn *hc)
{
  char *cp;
  char buf[256];

  setenv("PATH", CONFIG_THTTPD_CGI_PATH, TRUE);
#ifdef CGI_LD_LIBRARY_PATH
  setenv("LD_LIBRARY_PATH", CGI_LD_LIBRARY_PATH, TRUE);
#endif /* CGI_LD_LIBRARY_PATH */

  setenv("SERVER_SOFTWARE",  CONFIG_THTTPD_SERVER_SOFTWARE, TRUE);

  /* If vhosting, use that server-name here. */
#ifdef CONFIG_THTTPD_VHOST
  if (hc->vhostname)
    {
      cp = hc->vhostname;
    }
  else
#endif
    {
      cp = hc->hs->hostname;
    }

  if (cp)
    {
      setenv("SERVER_NAME", cp, TRUE);
    }

  setenv("GATEWAY_INTERFACE", "CGI/1.1", TRUE);
  setenv("SERVER_PROTOCOL", hc->protocol, TRUE);

  (void)snprintf(buf, sizeof(buf), "%d", (int)CONFIG_THTTPD_PORT);
  setenv("SERVER_PORT", buf, TRUE);

  setenv("REQUEST_METHOD", httpd_method_str(hc->method), TRUE);

  if (hc->pathinfo[0] != '\0')
    {
      char *cp2;
      size_t l;

      (void)snprintf(buf, sizeof(buf), "/%s", hc->pathinfo);
      setenv("PATH_INFO", buf, TRUE);

      l = strlen(httpd_root) + strlen(hc->pathinfo) + 1;
      cp2 = NEW(char, l);
      if (cp2)
        {
          (void)snprintf(cp2, l, "%s%s", httpd_root, hc->pathinfo);
          setenv("PATH_TRANSLATED", cp2, TRUE);
        }
    }

  (void)snprintf(buf, sizeof(buf), "/%s",strcmp(hc->origfilename, ".") == 0 ? "" : hc->origfilename);
  setenv("SCRIPT_NAME", buf, TRUE);

  if (hc->query[0] != '\0')
    {
      setenv("QUERY_STRING", hc->query, TRUE);
    }

  setenv("REMOTE_ADDR", httpd_ntoa(&hc->client_addr), TRUE);
  if (hc->referer[0] != '\0')
    {
      setenv("HTTP_REFERER", hc->referer, TRUE);
    }

  if (hc->useragent[0] != '\0')
    {
      setenv("HTTP_USER_AGENT", hc->useragent, TRUE);
    }

  if (hc->accept[0] != '\0')
    {
      setenv("HTTP_ACCEPT", hc->accept, TRUE);
    }

  if (hc->accepte[0] != '\0')
    {
      setenv("HTTP_ACCEPT_ENCODING", hc->accepte, TRUE);
    }

  if (hc->acceptl[0] != '\0')
    {
      setenv("HTTP_ACCEPT_LANGUAGE", hc->acceptl, TRUE);
    }

  if (hc->cookie[0] != '\0')
    {
      setenv("HTTP_COOKIE", hc->cookie, TRUE);
    }

  if (hc->contenttype[0] != '\0')
    {
      setenv("CONTENT_TYPE", hc->contenttype, TRUE);
    }

  if (hc->hdrhost[0] != '\0')
    {
      setenv("HTTP_HOST", hc->hdrhost, TRUE);
    }

  if (hc->contentlength != -1)
    {
      (void)snprintf(buf, sizeof(buf), "%lu", (unsigned long)hc->contentlength);
      setenv("CONTENT_LENGTH", buf, TRUE);
    }

  if (hc->remoteuser[0] != '\0')
    {
      setenv("REMOTE_USER", hc->remoteuser, TRUE);
    }

  if (hc->authorization[0] != '\0')
    {
      setenv("AUTH_TYPE", "Basic", TRUE);
    }

  /* We only support Basic auth at the moment. */

  if (getenv("TZ") != NULL)
    {
      setenv("TZ", getenv("TZ"), TRUE);
    }

  setenv("CGI_PATTERN", CONFIG_THTTPD_CGI_PATTERN, TRUE);
}

/* Set up argument vector */

static FAR char **make_argp(httpd_conn *hc)
{
  FAR char **argp;
  int argn;
  char *cp1;
  char *cp2;

  /* By allocating an arg slot for every character in the query, plus one
   * for the filename and one for the NULL, we are guaranteed to have
   * enough.  We could actually use strlen/2.
   */

  argp = NEW(char *, strlen(hc->query) + 2);
  if (!argp)
    {
      return NULL;
    }

  argp[0] = strrchr(hc->expnfilename, '/');
  if (argp[0])
    {
      ++argp[0];
    }
  else
    {
      argp[0] = hc->expnfilename;
    }
  argn = 1;

  /* According to the CGI spec at http://hoohoo.ncsa.uiuc.edu/cgi/cl.html,
   * "The server should search the query information for a non-encoded =
   * character to determine if the command line is to be used, if it finds
   * one, the command line is not to be used."
   */

  if (strchr(hc->query, '=') == NULL)
    {
      for (cp1 = cp2 = hc->query; *cp2 != '\0'; ++cp2)
        {
          if (*cp2 == '+')
            {
              *cp2 = '\0';
              httpd_strdecode(cp1, cp1);
              argp[argn++] = cp1;
              cp1 = cp2 + 1;
            }
        }

      if (cp2 != cp1)
        {
          httpd_strdecode(cp1, cp1);
          argp[argn++] = cp1;
        }
    }

  argp[argn] = NULL;
  return argp;
}

/* Data is available from the client socket. This routine is used only for POST
 * requests.  It reads the data from the client and sends it to the child thread.
 */

static inline int cgi_interpose_input(httpd_conn *hc, int wfd, char *buffer)
{
  size_t nbytes;
  ssize_t nbytes_read;
  ssize_t nbytes_written;

  nbytes = hc->read_idx - hc->checked_idx;
patacongo's avatar
patacongo committed
  llvdbg("nbytes: %d contentlength: %d\n", nbytes, hc->contentlength);
  if (nbytes > 0)
    {
      if (httpd_write(wfd, &(hc->read_buf[hc->checked_idx]), nbytes) != nbytes)
        {
patacongo's avatar
patacongo committed
          lldbg("httpd_write failed\n");
          return 1;
        }
    }

  if (nbytes < hc->contentlength)
    {
      do
        {
          nbytes_read = read(hc->conn_fd, buffer, MIN(sizeof(buffer), hc->contentlength - nbytes));
patacongo's avatar
patacongo committed
          llvdbg("nbytes_read: %d\n", nbytes_read);
          if (nbytes_read < 0)
            {
              if (errno != EINTR)
                {
patacongo's avatar
patacongo committed
                   lldbg("read failed: %d\n", errno);
                  return 1;
                }
            }
        }
      while (nbytes_read < 0);

      if (nbytes_read > 0)
        {
          nbytes_written = httpd_write(wfd, buffer, nbytes_read);
patacongo's avatar
patacongo committed
          llvdbg("nbytes_written: %d\n", nbytes_written);
          if (nbytes_written != nbytes_read)
            {
patacongo's avatar
patacongo committed
              lldbg("httpd_write failed\n");
              return 1;
            }
        }
    }

  if (nbytes >= hc->contentlength)
    {
      /* Special hack to deal with broken browsers that send a LF or CRLF
       * after POST data, causing TCP resets - we just read and discard up
       * to 2 bytes.  Unfortunately this doesn't fix the problem for CGIs
       * which avoid the interposer task due to their POST data being
       * short.  Creating an interposer task for all POST CGIs is
       * unacceptably expensive.  The eventual fix will come when interposing
       * gets integrated into the main loop as a tasklet instead of a task.
       */

      /* Turn on no-delay mode in case we previously cleared it. */

      httpd_set_ndelay(hc->conn_fd);

      /* And read up to 2 bytes. */

      (void)read(hc->conn_fd, buffer, sizeof(buffer));
      return 1;
    }
  return 0;
}

/* This routine is used for parsed-header CGIs.  The idea here is that the
 * CGI can return special headers such as "Status:" and "Location:" which
 * change the return status of the response.  Since the return status has to
 * be the very first line written out, we have to accumulate all the headers
 * and check for the special ones before writing the status.  Then we write
 * out the saved headers and proceed to echo the rest of the response.
 */

static inline int cgi_interpose_output(httpd_conn *hc, int rfd, char *inbuffer,
patacongo's avatar
patacongo committed
                                       struct cgi_outbuffer_s *hdr)
{
  ssize_t nbytes_read;
  char *br = NULL;
  int status;
  const char *title;
  char *cp;

  /* Make sure the connection is in blocking mode.  It should already be
   * blocking, but we might as well be sure.
   */

  httpd_clear_ndelay(hc->conn_fd);

  /* Loop while there are things we can do without waiting for more input */

patacongo's avatar
patacongo committed
  llvdbg("state: %d\n", hdr->state);
  switch (hdr->state)
    {
      case CGI_OUTBUFFER_READHEADER:
        {
          /* Slurp in all headers as they become available from the client. */

          do
            {
              /* Read until we successfully read data or until an error occurs.
               * EAGAIN is not an error, but it is still cause to return.
               */

              nbytes_read = read(hc->conn_fd, inbuffer, sizeof(inbuffer));
patacongo's avatar
patacongo committed
              nllvdbg("Read %d bytes from fd %d\n", nbytes_read, hc->conn_fd);

              if (nbytes_read < 0)
                {
                  if (errno != EINTR)
                    {
                      if (errno != EAGAIN)
                        {
patacongo's avatar
patacongo committed
                          nlldbg("read: %d\n", errno);
                        }
                      return 1;
                    }
                }
            }
          while (nbytes_read < 0);

          /* Check for end-of-file */

          if (nbytes_read <= 0)
            {
patacongo's avatar
patacongo committed
              nllvdbg("End-of-file\n");
              br         = &(hdr->buffer[hdr->len]);
              hdr->state = CGI_OUTBUFFER_HEADERREAD;
            }
          else
            {
              /* Accumulate more header data */

              httpd_realloc_str(&hdr->buffer, &hdr->size, hdr->len + nbytes_read);
              (void)memcpy(&(hdr->buffer[hdr->len]), inbuffer, nbytes_read);
              hdr->len += nbytes_read;
              hdr->buffer[hdr->len] = '\0';
patacongo's avatar
patacongo committed
              nllvdbg("Header bytes accumulated: %d\n", hdr->len);

              /* Check for end of header */

              if ((br = strstr(hdr->buffer, "\r\n\r\n")) != NULL ||
                  (br = strstr(hdr->buffer, "\012\012")) != NULL)
                {
patacongo's avatar
patacongo committed
                  nllvdbg("End-of-header\n");
                  hdr->state = CGI_OUTBUFFER_HEADERREAD;
                }
              else
                {
                  /* Return.  We will be called again when more data is available
                   * on the socket.
                   */

                  return 0;
                }
            }
        }
        break;

      case CGI_OUTBUFFER_HEADERREAD:
        {
          /* If there were no headers, bail. */

          if (hdr->buffer[0] == '\0')
            {
              hdr->state = CGI_OUTBUFFER_DONE;
              return 1;
            }

          /* Figure out the status.  Look for a Status: or Location: header; else if 
           * there's an HTTP header line, get it from there; else default to 200.
           */

          status = 200;
          if (strncmp(hdr->buffer, "HTTP/", 5) == 0)
            {
              cp = hdr->buffer;
              cp += strcspn(cp, " \t");
              status = atoi(cp);
            }

          if ((cp = strstr(hdr->buffer, "Status:")) != NULL &&
              cp < br && (cp == hdr->buffer || *(cp - 1) == '\012'))
            {
              cp += 7;
              cp += strspn(cp, " \t");
              status = atoi(cp);
            }

          if ((cp = strstr(hdr->buffer, "Location:")) != NULL &&
              cp < br && (cp == hdr->buffer || *(cp - 1) == '\012'))
            {
              status = 302;
            }

          /* Write the status line. */

patacongo's avatar
patacongo committed
          nllvdbg("Status: %d\n", status);
          switch (status)
            {
            case 200:
              title = ok200title;
              break;

            case 302:
              title = err302title;
              break;

            case 304:
              title = err304title;
              break;

            case 400:
              BADREQUEST("status");
              title = httpd_err400title;
              break;

#ifdef CONFIG_THTTPD_AUTH_FILE
            case 401:
              title = err401title;
              break;
#endif

            case 403:
              title = err403title;
              break;

            case 404:
              title = err404title;
             break;

           case 408:
              title = httpd_err408title;
              break;

            case 500:
              INTERNALERROR("status");
              title = err500title;
              break;

            case 501:
              NOTIMPLEMENTED("status");
              title = err501title;
              break;

            case 503:
              title = httpd_err503title;
              break;

            default:
              title = "Something";
              break;
            }

          (void)snprintf(inbuffer, sizeof(inbuffer), "HTTP/1.0 %d %s\r\n", status, title);
          (void)httpd_write(hc->conn_fd, inbuffer, strlen(inbuffer));

          /* Write the saved hdr->buffer to the client. */

          (void)httpd_write(hc->conn_fd, hdr->buffer, hdr->len);
        }

        /* Then set up to read the data following the header from the CGI program and
         * pass it back to the client. We return now; we will be called again when
         * data is available on the pipe.
         */

        hdr->state = CGI_OUTBUFFER_READDATA;
        return 0;

      case CGI_OUTBUFFER_READDATA:
        {
          /* Read data from the pipe. */

          do
            {
              /* Read until we successfully read data or until an error occurs.
               * EAGAIN is not an error, but it is still cause to return.
               */

              nbytes_read = read(rfd, inbuffer, sizeof(inbuffer));
patacongo's avatar
patacongo committed
              nllvdbg("Read %d bytes from fd %d\n", nbytes_read, rfd);

              if (nbytes_read < 0)
                {
                  if (errno != EINTR)
                    {
                      if (errno != EAGAIN)
                        {
patacongo's avatar
patacongo committed
                          nlldbg("read: %d\n", errno);
                        }
                      return 1;
                    }
                }
            }
          while (nbytes_read < 0);

          /* Check for end of file */

          if (nbytes_read == 0)
            {
patacongo's avatar
patacongo committed
              nllvdbg("End-of-file\n");
              close(hc->conn_fd);
              close(rfd);
              hdr->state = CGI_OUTBUFFER_DONE;
              return 1;
            }
          else
            {
               /* Forward the data from the CGI program to the client */

             (void)httpd_write(hc->conn_fd, inbuffer, strlen(inbuffer));
            }
        }
        break;

      case CGI_OUTBUFFER_DONE:
      default:
        return 1;
    }
  return 0;
}

/* CGI child task. */

static int cgi_child(int argc, char **argv)
{
  FAR httpd_conn *hc = (FAR httpd_conn*)strtoul(argv[1], NULL, 16);
#if CONFIG_THTTPD_CGI_TIMELIMIT > 0
  ClientData client_data;
#endif
  FAR char **argp;
  struct cgi_outbuffer_s hdr;
  struct fdwatch_s *fw;
  char   *buffer;
  char   *directory;
  char   *dupname;
  boolean indone;
  boolean outdone;
  int     child;
  int     pipefd[2];
  int     wfd = -1;
  int     rfd = -1;
  int     fd;
  int     ret;
  int     err = 1;

  /* Use low-level debug out (because the low-level output may survive closing
   * all file descriptors
   */

  nllvdbg("Started: %s\n", argv[1]);

  /* Update all of the environment variable settings, these will be inherited
   * by the CGI task.
   */

  create_environment(hc);

  /* Make the argument vector. */

  argp = make_argp(hc);

  /* Close all file descriptors EXCEPT for stdin, stdout, stderr and
   * hc->conn_fd.  We'll keep stderr open for error reporting; stdin and
   * stdout will be closed later by dup2().  Keeping stdin and stdout open
   * now prevents re-use of fd=0 and 1 by pipe().
  nllvdbg("Closing descriptors\n");
  for (fd = 3; fd < (CONFIG_NFILE_DESCRIPTORS + CONFIG_NSOCKET_DESCRIPTORS); fd++)
       /* Keep hc->conn_fd open for obvious reasons */
         {
           close(fd);
         }
    }

  /* Create pipes that will be interposed between the CGI task's stdin or
   * stdout and the socket.
   *
   * Setup up the STDIN pipe - a pipe to transfer data received on the
   * socket to the CGI program.
   */

  nllvdbg("Create STDIN pipe\n");
  ret = pipe(pipefd);
  if (ret < 0)
    {
      nlldbg("STDIN pipe: %d\n", errno);
      goto errout_with_descriptors;
    }
  else
    {
      /* Then map the receiving end the pipe to stdin, save the sending end, and
       * closing the original receiving end
       */

      ret = dup2(pipefd[0], 0);
      if (ret < 0)
        {
          nlldbg("STDIN dup2: %d\n", errno);
          close(pipefd[1]);
          goto errout_with_descriptors;
        }

      wfd = pipefd[1];
      close(pipefd[0]);
    }

  /* Set up the STDOUT pipe - a pipe to transfer data received from the CGI program
   * to the client.
   */

  if (ret == 0)
    {
      nllvdbg("Create STDOUT pipe\n");
      ret = pipe(pipefd);
      if (ret < 0)
        {
          nlldbg("STDOUT pipe: %d\n", errno);
          goto errout_with_descriptors;
        }
      else
        {
          /* Then map the sending end the pipe to stdout, save the receiving end, and
           * closing the original sending end
           */

          ret = dup2(pipefd[1], 1);
          if (ret < 0)
            {
              nlldbg("STDOUT dup2: %d\n", errno);
              close(pipefd[1]);
              goto errout_with_descriptors;
            }

          rfd = pipefd[0];
          close(pipefd[1]);
        }
    }

  /* chdir to the directory containing the binary. This isn't in the CGI 1.1
   * spec, but it's what  other HTTP servers do.
  dupname = httpd_strdup(hc->expnfilename);
  if (dupname)
      directory = dirname(dupname);
      if (directory)
          (void)chdir(directory); /* ignore errors */
      httpd_free(dupname);
    }

  /* Allocate memory for buffering */

  memset(&hdr, 0, sizeof(struct cgi_outbuffer_s));
  httpd_realloc_str(&hdr.buffer, &hdr.size, 500);
  if (!hdr.buffer)
    {
      nlldbg("hdr allocation failed\n");
      goto errout_with_descriptors;
    }

  buffer = (char*)httpd_malloc(CONFIG_THTTPD_IOBUFFERSIZE);
  if (!buffer)
    {
      nlldbg("buffer allocation failed\n");
      goto errout_with_header;
    }

  /* Create fdwatch structures */

  fw = fdwatch_initialize(2);
  if (!fw)
    {
      nlldbg("fdwatch allocation failed\n");
      goto errout_with_buffer;
    }

  /* Run the CGI program. */

  nllvdbg("Starting CGI\n");
  child = exec(hc->expnfilename, (FAR const char **)argp, g_thttpdsymtab, g_thttpdnsymbols);
  if (child < 0)
    {
      /* Something went wrong. */

      nlldbg("execve %s: %d\n", hc->expnfilename, errno);
      goto errout_with_watch;
   }

  /* Schedule a kill for the child task in case it runs too long. */

#if CONFIG_THTTPD_CGI_TIMELIMIT > 0
  client_data.i = child;
  if (tmr_create(NULL, cgi_kill, client_data, CONFIG_THTTPD_CGI_TIMELIMIT * 1000L, 0) == NULL)
    {
      nlldbg("tmr_create(cgi_kill child) failed\n");
      goto errout_with_watch;
    }
#endif

  /* Add the read descriptors to the watch */

  fdwatch_add_fd(fw, hc->conn_fd, NULL);
  fdwatch_add_fd(fw, rfd, NULL);

  /* Then perform the interposition */

  indone  = FALSE;
  outdone = FALSE;

  nllvdbg("Interposing\n");
  do
    {
      (void)fdwatch(fw, 5000);

      if (!indone && fdwatch_check_fd(fw, hc->conn_fd))
        {
          /* Transfer data from the client to the CGI program (POST) */

          nllvdbg("Interpose input\n");
          indone = cgi_interpose_input(hc, wfd, buffer);
          if (indone)
            {
              fdwatch_del_fd(fw, hc->conn_fd);
            }
        }

      if (fdwatch_check_fd(fw, rfd))
        {
          /* Handle receipt of headers and CGI program response (GET) */

          nllvdbg("Interpose output\n");
          outdone = cgi_interpose_output(hc, rfd, buffer, &hdr);
        }
  }
  while (!outdone);
  err = 0;

  /* Get rid of watch structures */

errout_with_watch:
  fdwatch_uninitialize(fw);

  /* Free memory */

errout_with_buffer:
  httpd_free(buffer);
errout_with_header:
  httpd_free(hdr.buffer);

  /* Close all descriptors */

errout_with_descriptors:
  close(wfd);
  close(rfd);
  close(hc->conn_fd);

  INTERNALERROR("errout");
  httpd_send_err(hc, 500, err500title, "", err500form, hc->encodedurl);
  httpd_write_response(hc);
  nllvdbg("Return %d\n", ret);
  return err;
}

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

int cgi(httpd_conn *hc)
{
  char arg[16];
  char *argv[1];
  pid_t child;
  int   retval = ERROR;

  /* Set up a semaphore to hold off the make THTTPD thread until the CGI
   * threads are configured (basically until the file descriptors are all
   * dup'ed and can be closed by the main thread.
   */

  sem_init(&g_cgisem, 0, 0);

  if (hc->method == METHOD_GET || hc->method == METHOD_POST)
    {
#ifdef CONFIG_THTTPD_CGILIMIT
      if (hc->hs->cgi_count >= CONFIG_THTTPD_CGILIMIT)
        {
          httpd_send_err(hc, 503, httpd_err503title, "", httpd_err503form,
                         hc->encodedurl);
        }
#endif
      ++hc->hs->cgi_count;
      httpd_clear_ndelay(hc->conn_fd);

      /* Start the child task.  We use a trampoline task here so that we can
       * safely muck with the file descriptors before actually started the CGI
       * task.
       */

      snprintf(arg, 16, "%p", hc); /* task_create doesn't handle binary arguments. */
      argv[0] = arg;

#ifndef CONFIG_CUSTOM_STACK
      child = task_create("CGI child", CONFIG_THTTPD_CGI_PRIORITY,
                          CONFIG_THTTPD_CGI_STACKSIZE,
                          (main_t)cgi_child, (const char **)argv);
#else
      child = task_create("CGI child", CONFIG_THTTPD_CGI_PRIORITY,
                          (main_t)cgi_child, (const char **)argv);
#endif
      if (child < 0)
        {
          ndbg("task_create: %d\n", errno);
          INTERNALERROR("task_create");
          httpd_send_err(hc, 500, err500title, "", err500form, hc->encodedurl);
        }

      ndbg("Started CGI task %d for file '%s'\n", child, hc->expnfilename);

      /* Wait for the CGI threads to become initialized */

      cgi_semtake();

      hc->bytes_sent    = CONFIG_THTTPD_CGI_BYTECOUNT;
      hc->should_linger = FALSE;
    }
  else
    {
      NOTIMPLEMENTED("CGI");
      httpd_send_err(hc, 501, err501title, "", err501form, httpd_method_str(hc->method));
  /* Successfully started */

  retval = OK;

errout_with_sem:
  sem_destroy(&g_cgisem);
  return retval;
#endif /* CONFIG_THTTPD && CONFIG_THTTPD_CGI_PATTERN */