/*
 * Copyright 2001-2004 Brandon Long
 * All Rights Reserved.
 *
 * ClearSilver Templating System
 *
 * This code is made available under the terms of the ClearSilver License.
 * http://www.clearsilver.net/license.hdf
 *
 */

#include "cs_config.h"

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#if defined(HTML_COMPRESSION)
#include <zlib.h>
#endif

#include "util/neo_misc.h"
#include "util/neo_err.h"
#include "util/neo_hdf.h"
#include "util/neo_str.h"
#include "cgi.h"
#include "cgiwrap.h"
#include "html.h"
#include "cs/cs.h"


struct _cgi_vars
{
  char *env_name;
  char *hdf_name;
} CGIVars[] = {
  {"AUTH_TYPE", "AuthType"},
  {"CONTENT_TYPE", "ContentType"},
  {"CONTENT_LENGTH", "ContentLength"},
  {"DOCUMENT_ROOT", "DocumentRoot"},
  {"GATEWAY_INTERFACE", "GatewayInterface"},
  {"PATH_INFO", "PathInfo"},
  {"PATH_TRANSLATED", "PathTranslated"},
  {"QUERY_STRING", "QueryString"},
  {"REDIRECT_REQUEST", "RedirectRequest"},
  {"REDIRECT_QUERY_STRING", "RedirectQueryString"},
  {"REDIRECT_STATUS", "RedirectStatus"},
  {"REDIRECT_URL", "RedirectURL",},
  {"REMOTE_ADDR", "RemoteAddress"},
  {"REMOTE_HOST", "RemoteHost"},
  {"REMOTE_IDENT", "RemoteIdent"},
  {"REMOTE_PORT", "RemotePort"},
  {"REMOTE_USER", "RemoteUser"},
  {"REMOTE_GROUP", "RemoteGroup"},
  {"REQUEST_METHOD", "RequestMethod"},
  {"REQUEST_URI", "RequestURI"},
  {"SCRIPT_FILENAME", "ScriptFilename"},
  {"SCRIPT_NAME", "ScriptName"},
  {"SERVER_ADDR", "ServerAddress"},
  {"SERVER_ADMIN", "ServerAdmin"},
  {"SERVER_NAME", "ServerName"},
  {"SERVER_PORT", "ServerPort"},
  {"SERVER_ROOT", "ServerRoot"},
  {"SERVER_PROTOCOL", "ServerProtocol"},
  {"SERVER_SOFTWARE", "ServerSoftware"},
  /* SSL Vars from mod_ssl */
  {"HTTPS", "HTTPS"},
  {"SSL_PROTOCOL", "SSL.Protocol"},
  {"SSL_SESSION_ID", "SSL.SessionID"},
  {"SSL_CIPHER", "SSL.Cipher"},
  {"SSL_CIPHER_EXPORT", "SSL.Cipher.Export"},
  {"SSL_CIPHER_USEKEYSIZE", "SSL.Cipher.UseKeySize"},
  {"SSL_CIPHER_ALGKEYSIZE", "SSL.Cipher.AlgKeySize"},
  {"SSL_VERSION_INTERFACE", "SSL.Version.Interface"},
  {"SSL_VERSION_LIBRARY", "SSL.Version.Library"},
  {"SSL_CLIENT_M_VERSION", "SSL.Client.M.Version"},
  {"SSL_CLIENT_M_SERIAL", "SSL.Client.M.Serial"},
  {"SSL_CLIENT_S_DN", "SSL.Client.S.DN"},
  {"SSL_CLIENT_S_DN_x509", "SSL.Client.S.DN.x509"},
  {"SSL_CLIENT_I_DN", "SSL.Client.I.DN"},
  {"SSL_CLIENT_I_DN_x509", "SSL.Client.I.DN.x509"},
  {"SSL_CLIENT_V_START", "SSL.Client.V.Start"},
  {"SSL_CLIENT_V_END", "SSL.Client.V.End"},
  {"SSL_CLIENT_A_SIG", "SSL.Client.A.SIG"},
  {"SSL_CLIENT_A_KEY", "SSL.Client.A.KEY"},
  {"SSL_CLIENT_CERT", "SSL.Client.CERT"},
  {"SSL_CLIENT_CERT_CHAINn", "SSL.Client.CERT.CHAINn"},
  {"SSL_CLIENT_VERIFY", "SSL.Client.Verify"},
  {"SSL_SERVER_M_VERSION", "SSL.Server.M.Version"},
  {"SSL_SERVER_M_SERIAL", "SSL.Server.M.Serial"},
  {"SSL_SERVER_S_DN", "SSL.Server.S.DN"},
  {"SSL_SERVER_S_DN_x509", "SSL.Server.S.DN.x509"},
  {"SSL_SERVER_S_DN_CN", "SSL.Server.S.DN.CN"},
  {"SSL_SERVER_S_DN_EMAIL", "SSL.Server.S.DN.Email"},
  {"SSL_SERVER_S_DN_O", "SSL.Server.S.DN.O"},
  {"SSL_SERVER_S_DN_OU", "SSL.Server.S.DN.OU"},
  {"SSL_SERVER_S_DN_C", "SSL.Server.S.DN.C"},
  {"SSL_SERVER_S_DN_SP", "SSL.Server.S.DN.SP"},
  {"SSL_SERVER_S_DN_L", "SSL.Server.S.DN.L"},
  {"SSL_SERVER_I_DN", "SSL.Server.I.DN"},
  {"SSL_SERVER_I_DN_x509", "SSL.Server.I.DN.x509"},
  {"SSL_SERVER_I_DN_CN", "SSL.Server.I.DN.CN"},
  {"SSL_SERVER_I_DN_EMAIL", "SSL.Server.I.DN.Email"},
  {"SSL_SERVER_I_DN_O", "SSL.Server.I.DN.O"},
  {"SSL_SERVER_I_DN_OU", "SSL.Server.I.DN.OU"},
  {"SSL_SERVER_I_DN_C", "SSL.Server.I.DN.C"},
  {"SSL_SERVER_I_DN_SP", "SSL.Server.I.DN.SP"},
  {"SSL_SERVER_I_DN_L", "SSL.Server.I.DN.L"},
  {"SSL_SERVER_V_START", "SSL.Server.V.Start"},
  {"SSL_SERVER_V_END", "SSL.Server.V.End"},
  {"SSL_SERVER_A_SIG", "SSL.Server.A.SIG"},
  {"SSL_SERVER_A_KEY", "SSL.Server.A.KEY"},
  {"SSL_SERVER_CERT", "SSL.Server.CERT"},
  /* SSL Vars mapped from others */
  /* Hmm, if we're running under mod_ssl w/ +CompatEnvVars, we set these
   * twice... */
  {"SSL_PROTOCOL_VERSION", "SSL.Protocol"},
  {"SSLEAY_VERSION", "SSL.Version.Library"},
  {"HTTPS_CIPHER", "SSL.Cipher"},
  {"HTTPS_EXPORT", "SSL.Cipher.Export"},
  {"HTTPS_SECRETKEYSIZE", "SSL.Cipher.UseKeySize"},
  {"HTTPS_KEYSIZE", "SSL.Cipher.AlgKeySize"},
  {"SSL_SERVER_KEY_SIZE", "SSL.Cipher.AlgKeySize"},
  {"SSL_SERVER_CERTIFICATE", "SSL.Server.CERT"},
  {"SSL_SERVER_CERT_START", "SSL.Server.V.Start"},
  {"SSL_SERVER_CERT_END", "SSL.Server.V.End"},
  {"SSL_SERVER_CERT_SERIAL", "SSL.Server.M.Serial"},
  {"SSL_SERVER_SIGNATURE_ALGORITHM", "SSL.Server.A.SIG"},
  {"SSL_SERVER_DN", "SSL.Server.S.DN"},
  {"SSL_SERVER_CN", "SSL.Server.S.DN.CN"},
  {"SSL_SERVER_EMAIL", "SSL.Server.S.DN.Email"},
  {"SSL_SERVER_O", "SSL.Server.S.DN.O"},
  {"SSL_SERVER_OU", "SSL.Server.S.DN.OU"},
  {"SSL_SERVER_C", "SSL.Server.S.DN.C"},
  {"SSL_SERVER_SP", "SSL.Server.S.DN.SP"},
  {"SSL_SERVER_L", "SSL.Server.S.DN.L"},
  {"SSL_SERVER_IDN", "SSL.Server.I.DN"},
  {"SSL_SERVER_ICN", "SSL.Server.I.DN.CN"},
  {"SSL_SERVER_IEMAIL", "SSL.Server.I.DN.Email"},
  {"SSL_SERVER_IO", "SSL.Server.I.DN.O"},
  {"SSL_SERVER_IOU", "SSL.Server.I.DN.OU"},
  {"SSL_SERVER_IC", "SSL.Server.I.DN.C"},
  {"SSL_SERVER_ISP", "SSL.Server.I.DN.SP"},
  {"SSL_SERVER_IL", "SSL.Server.I.DN.L"},
  {"SSL_CLIENT_CERTIFICATE", "SSL.Client.CERT"},
  {"SSL_CLIENT_CERT_START", "SSL.Client.V.Start"},
  {"SSL_CLIENT_CERT_END", "SSL.Client.V.End"},
  {"SSL_CLIENT_CERT_SERIAL", "SSL.Client.M.Serial"},
  {"SSL_CLIENT_SIGNATURE_ALGORITHM", "SSL.Client.A.SIG"},
  {"SSL_CLIENT_DN", "SSL.Client.S.DN"},
  {"SSL_CLIENT_CN", "SSL.Client.S.DN.CN"},
  {"SSL_CLIENT_EMAIL", "SSL.Client.S.DN.Email"},
  {"SSL_CLIENT_O", "SSL.Client.S.DN.O"},
  {"SSL_CLIENT_OU", "SSL.Client.S.DN.OU"},
  {"SSL_CLIENT_C", "SSL.Client.S.DN.C"},
  {"SSL_CLIENT_SP", "SSL.Client.S.DN.SP"},
  {"SSL_CLIENT_L", "SSL.Client.S.DN.L"},
  {"SSL_CLIENT_IDN", "SSL.Client.I.DN"},
  {"SSL_CLIENT_ICN", "SSL.Client.I.DN.CN"},
  {"SSL_CLIENT_IEMAIL", "SSL.Client.I.DN.Email"},
  {"SSL_CLIENT_IO", "SSL.Client.I.DN.O"},
  {"SSL_CLIENT_IOU", "SSL.Client.I.DN.OU"},
  {"SSL_CLIENT_IC", "SSL.Client.I.DN.C"},
  {"SSL_CLIENT_ISP", "SSL.Client.I.DN.SP"},
  {"SSL_CLIENT_IL", "SSL.Client.I.DN.L"},
  {"SSL_EXPORT", "SSL.Cipher.Export"},
  {"SSL_KEYSIZE", "SSL.Cipher.AlgKeySize"},
  {"SSL_SECKEYSIZE", "SSL.Cipher.UseKeySize"},
  {"SSL_SSLEAY_VERSION", "SSL.Version.Library"},
/* Old vars not in mod_ssl */
  {"SSL_STRONG_CRYPTO", "SSL.Strong.Crypto"},
  {"SSL_SERVER_KEY_EXP", "SSL.Server.Key.Exp"},
  {"SSL_SERVER_KEY_ALGORITHM", "SSL.Server.Key.Algorithm"},
  {"SSL_SERVER_KEY_SIZE", "SSL.Server.Key.Size"},
  {"SSL_SERVER_SESSIONDIR", "SSL.Server.SessionDir"},
  {"SSL_SERVER_CERTIFICATELOGDIR", "SSL.Server.CertificateLogDir"},
  {"SSL_SERVER_CERTFILE", "SSL.Server.CertFile"},
  {"SSL_SERVER_KEYFILE", "SSL.Server.KeyFile"},
  {"SSL_SERVER_KEYFILETYPE", "SSL.Server.KeyFileType"},
  {"SSL_CLIENT_KEY_EXP", "SSL.Client.Key.Exp"},
  {"SSL_CLIENT_KEY_ALGORITHM", "SSL.Client.Key.Algorithm"},
  {"SSL_CLIENT_KEY_SIZE", "SSL.Client.Key.Size"},
  {NULL, NULL}
};

struct _http_vars
{
  char *env_name;
  char *hdf_name;
} HTTPVars[] = {
  {"HTTP_ACCEPT", "Accept"},
  {"HTTP_ACCEPT_CHARSET", "AcceptCharset"},
  {"HTTP_ACCEPT_ENCODING", "AcceptEncoding"},
  {"HTTP_ACCEPT_LANGUAGE", "AcceptLanguage"},
  {"HTTP_COOKIE", "Cookie"},
  {"HTTP_HOST", "Host"},
  {"HTTP_USER_AGENT", "UserAgent"},
  {"HTTP_IF_MODIFIED_SINCE", "IfModifiedSince"},
  {"HTTP_REFERER", "Referer"},
  {"HTTP_VIA", "Via"},
  /* SOAP */
  {"HTTP_SOAPACTION", "Soap.Action"},
  {NULL, NULL}
};

static char *Argv0 = "";

int IgnoreEmptyFormVars = 0;

static int ExceptionsInit = 0;
NERR_TYPE CGIFinished = -1;
NERR_TYPE CGIUploadCancelled = -1;
NERR_TYPE CGIParseNotHandled = -1;

static NEOERR *_add_cgi_env_var (CGI *cgi, char *env, char *name)
{
  NEOERR *err;
  char *s;

  err = cgiwrap_getenv (env, &s);
  if (err != STATUS_OK) return nerr_pass (err);
  if (s != NULL)
  {
    err = hdf_set_buf (cgi->hdf, name, s);
    if (err != STATUS_OK)
    {
      free(s);
      return nerr_pass (err);
    }
  }
  return STATUS_OK;
}

char *cgi_url_unescape (char *value)
{
  int i = 0, o = 0;
  unsigned char *s = (unsigned char *)value;

  if (s == NULL) return value;
  while (s[i])
  {
    if (s[i] == '+')
    {
      s[o++] = ' ';
      i++;
    }
    else if (s[i] == '%' && isxdigit(s[i+1]) && isxdigit(s[i+2]))
    {
      char num;
      num = (s[i+1] >= 'A') ? ((s[i+1] & 0xdf) - 'A') + 10 : (s[i+1] - '0');
      num *= 16;
      num += (s[i+2] >= 'A') ? ((s[i+2] & 0xdf) - 'A') + 10 : (s[i+2] - '0');
      s[o++] = num;
      i+=3;
    }
    else {
      s[o++] = s[i++];
    }
  }
  if (i && o) s[o] = '\0';
  return (char *)s;
}

NEOERR *cgi_js_escape (const char *in, char **esc)
{
  return nerr_pass(neos_js_escape(in, esc));
}

NEOERR *cgi_url_escape (const char *buf, char **esc)
{
  return nerr_pass(neos_url_escape(buf, esc, NULL));
}

NEOERR *cgi_url_escape_more (const char *in, char **esc,
                                 const char *other)
{
  return nerr_pass(neos_url_escape(in, esc, other));
}

static NEOERR *_parse_query (CGI *cgi, char *query)
{
  NEOERR *err = STATUS_OK;
  char *t, *k, *v, *l;
  char buf[256];
  char unnamed[10];
  int unnamed_count = 0;
  HDF *obj, *child;

  if (query && *query)
  {
    k = strtok_r(query, "&", &l);
    while (k && *k)
    {
      v = strchr(k, '=');
      if (v == NULL)
      {
	v = "";
      }
      else
      {
	*v = '\0';
	v++;
      }


      /* Check for some invalid query strings */
      if (*k == 0) {
        /*  '?=foo' gets mapped in as Query._1=foo */
        snprintf(unnamed,sizeof(unnamed), "_%d", unnamed_count++);
        k = unnamed;
      } else if (*k == '.') {
        /* an hdf element can't start with a period */
        *k = '_';
      }
      snprintf(buf, sizeof(buf), "Query.%s", cgi_url_unescape(k));

      if (!(cgi->ignore_empty_form_vars && (*v == '\0')))
      {


	cgi_url_unescape(v);
	obj = hdf_get_obj (cgi->hdf, buf);
	if (obj != NULL)
	{
	  int i = 0;
	  char buf2[10];
	  child = hdf_obj_child (obj);
	  if (child == NULL)
	  {
	    t = hdf_obj_value (obj);
	    err = hdf_set_value (obj, "0", t);
	    if (err != STATUS_OK) break;
	    i = 1;
	  }
	  else
	  {
	    while (child != NULL)
	    {
	      i++;
	      child = hdf_obj_next (child);
	      if (err != STATUS_OK) break;
	    }
	    if (err != STATUS_OK) break;
	  }
	  snprintf (buf2, sizeof(buf2), "%d", i);
	  err = hdf_set_value (obj, buf2, v);
	  if (err != STATUS_OK) break;
	}
	err = hdf_set_value (cgi->hdf, buf, v);
	if (nerr_match(err, NERR_ASSERT)) {
	  STRING str;

	  string_init(&str);
	  nerr_error_string(err, &str);
	  ne_warn("Unable to set Query value: %s = %s: %s", buf, v, str.buf);
	  string_clear(&str);
	  nerr_ignore(&err);
	}
	if (err != STATUS_OK) break;
      }
      k = strtok_r(NULL, "&", &l);
    }
  }
  return nerr_pass(err);
}

/* Is it an error if its a short read? */
static NEOERR *_parse_post_form (CGI *cgi)
{
  NEOERR *err = STATUS_OK;
  char *l, *query;
  int len, r, o;

  l = hdf_get_value (cgi->hdf, "CGI.ContentLength", NULL);
  if (l == NULL) return STATUS_OK;
  len = atoi (l);
  if (len <= 0) return STATUS_OK;

  cgi->data_expected = len;

  query = (char *) malloc (sizeof(char) * (len + 1));
  if (query == NULL)
    return nerr_raise (NERR_NOMEM,
	"Unable to allocate memory to read POST input of length %d", l);


  o = 0;
  while (o < len)
  {
    cgiwrap_read (query + o, len - o, &r);
    if (r <= 0) break;
    o = o + r;
  }
  if (r < 0)
  {
    free(query);
    return nerr_raise_errno (NERR_IO, "Short read on CGI POST input (%d < %d)",
	o, len);
  }
  if (o != len)
  {
    free(query);
    return nerr_raise (NERR_IO, "Short read on CGI POST input (%d < %d)",
	o, len);
  }
  query[len] = '\0';
  err = _parse_query (cgi, query);
  free(query);
  return nerr_pass(err);
}

static NEOERR *_parse_cookie (CGI *cgi)
{
  NEOERR *err;
  char *cookie;
  char *k, *v, *l;
  HDF *obj;

  err = hdf_get_copy (cgi->hdf, "HTTP.Cookie", &cookie, NULL);
  if (err != STATUS_OK) return nerr_pass(err);
  if (cookie == NULL) return STATUS_OK;

  err = hdf_set_value (cgi->hdf, "Cookie", cookie);
  if (err != STATUS_OK)
  {
    free(cookie);
    return nerr_pass(err);
  }
  obj = hdf_get_obj (cgi->hdf, "Cookie");

  k = l = cookie;
  while (*l && *l != '=' && *l != ';') l++;
  while (*k)
  {
    if (*l == '=')
    {
      if (*l) *l++ = '\0';
      v = l;
      while (*l && *l != ';') l++;
      if (*l) *l++ = '\0';
    }
    else
    {
      v = "";
      if (*l) *l++ = '\0';
    }
    k = neos_strip (k);
    v = neos_strip (v);
    if (k[0] && v[0])
    {
      err = hdf_set_value (obj, k, v);
      if (nerr_match(err, NERR_ASSERT)) {
	STRING str;

	string_init(&str);
	nerr_error_string(err, &str);
	ne_warn("Unable to set Cookie value: %s = %s: %s", k, v, str.buf);
	string_clear(&str);
	nerr_ignore(&err);
      }
      if (err) break;
    }
    k = l;
    while (*l && *l != '=' && *l != ';') l++;
  }

  free (cookie);

  return nerr_pass(err);
}

#ifdef ENABLE_REMOTE_DEBUG

static void _launch_debugger (CGI *cgi, char *display)
{
  pid_t myPid, pid;
  char buffer[127];
  char *debugger;
  HDF *obj;
  char *allowed;

  /* Only allow remote debugging from allowed hosts */
  for (obj = hdf_get_child (cgi->hdf, "Config.Displays");
      obj; obj = hdf_obj_next (obj))
  {
    allowed = hdf_obj_value (obj);
    if (allowed && !strcmp (display, allowed)) break;
  }
  if (obj == NULL) return;

  myPid = getpid();

  if ((pid = fork()) < 0)
    return;

  if ((debugger = hdf_get_value (cgi->hdf, "Config.Debugger", NULL)) == NULL)
  {
    debugger = "/usr/local/bin/sudo /usr/local/bin/ddd -display %s %s %d";
  }

  if (!pid)
  {
    sprintf(buffer, debugger, display, Argv0, myPid);
    execl("/bin/sh", "sh", "-c", buffer, NULL);
  }
  else
  {
    sleep(60);
  }
}

#endif

static NEOERR *cgi_pre_parse (CGI *cgi)
{
  NEOERR *err;
  int x = 0;
  char buf[256];
  char *query;

  while (CGIVars[x].env_name)
  {
    snprintf (buf, sizeof(buf), "CGI.%s", CGIVars[x].hdf_name);
    err = _add_cgi_env_var(cgi, CGIVars[x].env_name, buf);
    if (err != STATUS_OK) return nerr_pass (err);
    x++;
  }
  x = 0;
  while (HTTPVars[x].env_name)
  {
    snprintf (buf, sizeof(buf), "HTTP.%s", HTTPVars[x].hdf_name);
    err = _add_cgi_env_var(cgi, HTTPVars[x].env_name, buf);
    if (err != STATUS_OK) return nerr_pass (err);
    x++;
  }
  err = _parse_cookie(cgi);
  if (err != STATUS_OK) return nerr_pass (err);

  err = hdf_get_copy (cgi->hdf, "CGI.QueryString", &query, NULL);
  if (err != STATUS_OK) return nerr_pass (err);
  if (query != NULL)
  {
    err = _parse_query(cgi, query);
    free(query);
    if (err != STATUS_OK) return nerr_pass (err);
  }

  {
    char *d = hdf_get_value(cgi->hdf, "Query.debug_pause", NULL);
    char *d_p = hdf_get_value(cgi->hdf, "Config.DebugPassword", NULL);

    if (hdf_get_int_value(cgi->hdf, "Config.DebugEnabled", 0) &&
        d && d_p && !strcmp(d, d_p)) {
      sleep(20);
    }
  }

#ifdef ENABLE_REMOTE_DEBUG
  {
    char *display;

    display = hdf_get_value (cgi->hdf, "Query.xdisplay", NULL);
    if (display)
    {
      fprintf(stderr, "** Got display %s\n", display);
      _launch_debugger(cgi, display);
    }
  }
#endif

  return STATUS_OK;
}

NEOERR *cgi_register_parse_cb(CGI *cgi, const char *method, const char *ctype,
                              void *rock, CGI_PARSE_CB parse_cb)
{
  struct _cgi_parse_cb *my_pcb;

  if (method == NULL || ctype == NULL)
    return nerr_raise(NERR_ASSERT, "method and type must not be NULL to register cb");

  my_pcb = (struct _cgi_parse_cb *) calloc(1, sizeof(struct _cgi_parse_cb));
  if (my_pcb == NULL)
    return nerr_raise(NERR_NOMEM, "Unable to allocate memory to register parse cb");

  my_pcb->method = strdup(method);
  my_pcb->ctype = strdup(ctype);
  if (my_pcb->method == NULL || my_pcb->ctype == NULL)
  {
    if (my_pcb->method != NULL)
      free(my_pcb->method);
    if (my_pcb->ctype != NULL)
      free(my_pcb->ctype);
    free(my_pcb);
    return nerr_raise(NERR_NOMEM, "Unable to allocate memory to register parse cb");
  }
  if (!strcmp(my_pcb->method, "*"))
    my_pcb->any_method = 1;
  if (!strcmp(my_pcb->ctype, "*"))
    my_pcb->any_ctype = 1;
  my_pcb->rock = rock;
  my_pcb->parse_cb = parse_cb;
  my_pcb->next = cgi->parse_callbacks;
  cgi->parse_callbacks = my_pcb;
  return STATUS_OK;
}

NEOERR *cgi_parse (CGI *cgi)
{
  NEOERR *err;
  char *method, *type;
  struct _cgi_parse_cb *pcb;


  method = hdf_get_value (cgi->hdf, "CGI.RequestMethod", "GET");
  type = hdf_get_value (cgi->hdf, "CGI.ContentType", NULL);
  /* Walk the registered parse callbacks for a matching one */
  pcb = cgi->parse_callbacks;
  while (pcb != NULL)
  {
    if ( (pcb->any_method || !strcasecmp(pcb->method, method)) &&
	 (pcb->any_ctype || (type && !strcasecmp(pcb->ctype, type))) )
    {
      err = pcb->parse_cb(cgi, method, type, pcb->rock);
      if (err && !nerr_handle(&err, CGIParseNotHandled))
	return nerr_pass(err);
    }
    pcb = pcb->next;
  }

  /* Fallback to internal methods */

  if (!strcmp(method, "POST"))
  {
    if (type && !strcmp(type, "application/x-www-form-urlencoded"))
    {
      err = _parse_post_form(cgi);
      if (err != STATUS_OK) return nerr_pass (err);
    }
    else if (type && !strncmp (type, "multipart/form-data", 19))
    {
      err = parse_rfc2388 (cgi);
      if (err != STATUS_OK) return nerr_pass (err);
    }
#if 0
    else
    {
      int len, x, r;
      char *l;
      char buf[4096];
      FILE *fp;

      fp = fopen("/tmp/upload", "w");

      l = hdf_get_value (cgi->hdf, "CGI.ContentLength", NULL);
      if (l == NULL) return STATUS_OK;
      len = atoi (l);

      x = 0;
      while (x < len)
      {
	if (len-x > sizeof(buf))
	  cgiwrap_read (buf, sizeof(buf), &r);
	else
	  cgiwrap_read (buf, len - x, &r);
	fwrite (buf, 1, r, fp);
	x += r;
      }
      fclose (fp);
      if (err) return nerr_pass(err);
    }
#endif
  }
  else if (!strcmp(method, "PUT"))
  {
    FILE *fp;
    int len, x, r, w;
    char *l;
    char buf[4096];
    int unlink_files = hdf_get_int_value(cgi->hdf, "Config.Upload.Unlink", 1);

    err = open_upload(cgi, unlink_files, &fp);
    if (err) return nerr_pass(err);

    l = hdf_get_value (cgi->hdf, "CGI.ContentLength", NULL);
    if (l == NULL) return STATUS_OK;
    len = atoi (l);
    if (len <= 0) return STATUS_OK;

    x = 0;
    while (x < len)
    {
      if (len-x > sizeof(buf))
	cgiwrap_read (buf, sizeof(buf), &r);
      else
	cgiwrap_read (buf, len - x, &r);
      w = fwrite (buf, sizeof(char), r, fp);
      if (w != r)
      {
	err = nerr_raise_errno(NERR_IO, "Short write on PUT: %d < %d", w, r);
	break;
      }
      x += r;
    }
    if (err) return nerr_pass(err);
    fseek(fp, 0, SEEK_SET);
    l = hdf_get_value(cgi->hdf, "CGI.PathInfo", NULL);
    if (l) err = hdf_set_value (cgi->hdf, "PUT", l);
    if (err) return nerr_pass(err);
    if (type) err = hdf_set_value (cgi->hdf, "PUT.Type", type);
    if (err) return nerr_pass(err);
    err = hdf_set_int_value (cgi->hdf, "PUT.FileHandle", uListLength(cgi->files));
    if (err) return nerr_pass(err);
    if (!unlink_files)
    {
      char *name;
      err = uListGet(cgi->filenames, uListLength(cgi->filenames)-1,
	  (void *)&name);
      if (err) return nerr_pass(err);
      err = hdf_set_value (cgi->hdf, "PUT.FileName", name);
      if (err) return nerr_pass(err);
    }
  }
  return STATUS_OK;
}

NEOERR *cgi_init (CGI **cgi, HDF *hdf)
{
  NEOERR *err = STATUS_OK;
  CGI *mycgi;

  if (ExceptionsInit == 0)
  {
    err = nerr_init();
    if (err) return nerr_pass(err);
    err = nerr_register(&CGIFinished, "CGIFinished");
    if (err) return nerr_pass(err);
    err = nerr_register(&CGIUploadCancelled, "CGIUploadCancelled");
    if (err) return nerr_pass(err);
    err = nerr_register(&CGIUploadCancelled, "CGIParseNotHandled");
    if (err) return nerr_pass(err);
    ExceptionsInit = 1;
  }

  *cgi = NULL;
  mycgi = (CGI *) calloc (1, sizeof(CGI));
  if (mycgi == NULL)
    return nerr_raise(NERR_NOMEM, "Unable to allocate space for CGI");

  mycgi->time_start = ne_timef();

  mycgi->ignore_empty_form_vars = IgnoreEmptyFormVars;

  do
  {
    if (hdf == NULL)
    {
      err = hdf_init (&(mycgi->hdf));
      if (err != STATUS_OK) break;
    }
    else
    {
      mycgi->hdf = hdf;
    }
    err = cgi_pre_parse (mycgi);
    if (err != STATUS_OK) break;

  } while (0);

  if (err == STATUS_OK)
    *cgi = mycgi;
  else
  {
    cgi_destroy(&mycgi);
  }
  return nerr_pass(err);
}

static void _destroy_tmp_file(char *filename)
{
  unlink(filename);
  free(filename);
}

void cgi_destroy (CGI **cgi)
{
  CGI *my_cgi;

  if (!cgi || !*cgi)
    return;
  my_cgi = *cgi;
  if (my_cgi->hdf)
    hdf_destroy (&(my_cgi->hdf));
  if (my_cgi->buf)
    free(my_cgi->buf);
  if (my_cgi->files)
    uListDestroyFunc(&(my_cgi->files), (void (*)(void *))fclose);
  if (my_cgi->filenames)
    uListDestroyFunc(&(my_cgi->filenames), (void (*)(void *))_destroy_tmp_file);
  free (*cgi);
  *cgi = NULL;
}

static NEOERR *render_cb (void *ctx, char *buf)
{
  STRING *str = (STRING *)ctx;
  NEOERR *err;

  err = nerr_pass(string_append(str, buf));
  return err;
}

static NEOERR *cgi_headers (CGI *cgi)
{
  NEOERR *err = STATUS_OK;
  HDF *obj, *child;
  char *s, *charset = NULL;

  if (hdf_get_int_value (cgi->hdf, "Config.NoCache", 0))
  {
    /* Ok, we try really hard to defeat caches here */
    /* this isn't in any HTTP rfc's, it just seems to be a convention */
    err = cgiwrap_writef ("Pragma: no-cache\r\n");
    if (err != STATUS_OK) return nerr_pass (err);
    err = cgiwrap_writef ("Expires: Fri, 01 Jan 1990 00:00:00 GMT\r\n");
    if (err != STATUS_OK) return nerr_pass (err);
    err = cgiwrap_writef ("Cache-control: no-cache, must-revalidate, no-cache=\"Set-Cookie\", private\r\n");
    if (err != STATUS_OK) return nerr_pass (err);
  }
  obj = hdf_get_obj (cgi->hdf, "cgiout");
  if (obj)
  {
    s = hdf_get_value (obj, "Status", NULL);
    if (s)
      err = cgiwrap_writef ("Status: %s\r\n", s);
    if (err != STATUS_OK) return nerr_pass (err);
    s = hdf_get_value (obj, "Location", NULL);
    if (s)
      err = cgiwrap_writef ("Location: %s\r\n", s);
    if (err != STATUS_OK) return nerr_pass (err);
    child = hdf_get_obj (cgi->hdf, "cgiout.other");
    if (child)
    {
      child = hdf_obj_child (child);
      while (child != NULL)
      {
	s = hdf_obj_value (child);
	err = cgiwrap_writef ("%s\r\n", s);
	if (err != STATUS_OK) return nerr_pass (err);
	child = hdf_obj_next(child);
      }
    }
    charset = hdf_get_value (obj, "charset", NULL);
    s = hdf_get_value (obj, "ContentType", "text/html");
    if (charset)
      err = cgiwrap_writef ("Content-Type: %s; charset=%s\r\n\r\n", s, charset);
    else
      err = cgiwrap_writef ("Content-Type: %s\r\n\r\n", s);
    if (err != STATUS_OK) return nerr_pass (err);
  }
  else
  {
    /* Default */
    err = cgiwrap_writef ("Content-Type: text/html\r\n\r\n");
    if (err != STATUS_OK) return nerr_pass (err);
  }
  return STATUS_OK;
}

#if defined(HTML_COMPRESSION)
/* Copy these here from zutil.h, which we aren't supposed to include */
#define DEF_MEM_LEVEL 8
#define OS_CODE 0x03

static NEOERR *cgi_compress (STRING *str, char *obuf, int *olen)
{
  z_stream stream;
  int err;

  stream.next_in = (Bytef*)str->buf;
  stream.avail_in = (uInt)str->len;
  stream.next_out = (Bytef*)obuf;
  stream.avail_out = (uInt)*olen;
  if ((uLong)stream.avail_out != *olen)
    return nerr_raise(NERR_NOMEM, "Destination too big: %ld", *olen);

  stream.zalloc = (alloc_func)0;
  stream.zfree = (free_func)0;
  stream.opaque = (voidpf)0;

  /* err = deflateInit(&stream, Z_DEFAULT_COMPRESSION); */
  err = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);
  if (err != Z_OK)
    return nerr_raise(NERR_SYSTEM, "deflateInit2 returned %d", err);

  err = deflate(&stream, Z_FINISH);
  if (err != Z_STREAM_END) {
    deflateEnd(&stream);
    return nerr_raise(NERR_SYSTEM, "deflate returned %d", err);
  }
  *olen = stream.total_out;

  err = deflateEnd(&stream);
  return STATUS_OK;
}
#endif

/* This ws strip function is Dave's version, designed to make debug
 * easier, and the output a bit smaller... but not as small as it could
 * be: essentially, it strips all empty lines, all extra space at the
 * end of the line, except in pre/textarea tags.
 *
 * Ok, expanding to 3 levels:
 * 0 - No stripping
 * 1 - Dave's debug stripper (as above)
 * 2 - strip all extra white space
 *
 * We don't currently strip white space in a tag
 *
 * */

#if 0
static void debug_output(char *header, char *s, int n)
{
  int x;
  fprintf (stderr, "%s ", header);
  for (x = 0; x < n; x++)
  {
    fprintf (stderr, "%c", s[x]);
  }
  fprintf(stderr, "\n");
}
#endif

void cgi_html_ws_strip(STRING *str, int level)
{
  int ws = 0;
  int seen_nonws = level > 1;
  int i, o, l;
  char *ch;

  i = o = 0;
  if (str->len) {
    ws = isspace(str->buf[0]);
  }
  while (i < str->len)
  {
    if (str->buf[i] == '<')
    {
      str->buf[o++] = str->buf[i++];
      if (!strncasecmp(str->buf + i, "textarea", 8))
      {
	ch = str->buf + i;
	do
	{
	  ch = strchr(ch, '<');
	  if (ch == NULL)
	  {
	    memmove(str->buf + o, str->buf + i, str->len - i);
	    str->len = o + str->len - i;
	    str->buf[str->len] = '\0';
	    return;
	  }
	  ch++;
	} while (strncasecmp(ch, "/textarea>", 10));
	ch += 10;
	l = ch - str->buf - i;
	memmove(str->buf + o, str->buf + i, l);
	o += l;
	i += l;
      }
      else if (!strncasecmp(str->buf + i, "pre", 3))
      {
	ch = str->buf + i;
	do
	{
	  ch = strchr(ch, '<');
	  if (ch == NULL)
	  {
	    memmove(str->buf + o, str->buf + i, str->len - i);
	    str->len = o + str->len - i;
	    str->buf[str->len] = '\0';
	    return;
	  }
	  ch++;
	} while (strncasecmp(ch, "/pre>", 5));
	ch += 5;
	l = ch - str->buf - i;
	memmove(str->buf + o, str->buf + i, l);
	o += l;
	i += l;
      }
      else
      {
	ch = strchr(str->buf + i, '>');
	if (ch == NULL)
	{
	  memmove(str->buf + o, str->buf + i, str->len - i);
	  str->len = o + str->len - i;
	  str->buf[str->len] = '\0';
	  return;
	}
	ch++;
	/* debug_output("copying tag", str->buf + i, ch - str->buf - i);
	 * */
	l = ch - str->buf - i;
	memmove(str->buf + o, str->buf + i, l);
	o += l;
	i += l;
      }
      /* debug_output("result", str->buf, o); */
      seen_nonws = 1;
      ws = 0;
    }
    else if (str->buf[i] == '\n')
    {
      /* This has the effect of erasing all whitespace at the eol plus
       * erasing all blank lines */
      while (o && isspace(str->buf[o-1])) o--;
      str->buf[o++] = str->buf[i++];
      ws = level > 1;
      seen_nonws = level > 1;
    }
    else if (seen_nonws && isspace(str->buf[i]))
    {
      if (ws)
      {
	i++;
      }
      else
      {
	str->buf[o++] = str->buf[i++];
	ws = 1;
      }
    }
    else
    {
      seen_nonws = 1;
      ws = 0;
      str->buf[o++] = str->buf[i++];
    }
  }

  str->len = o;
  str->buf[str->len] = '\0';
}

NEOERR *cgi_output (CGI *cgi, STRING *str)
{
  NEOERR *err = STATUS_OK;
  double dis;
  int is_html = 0;
  int use_deflate = 0;
  int use_gzip = 0;
  int do_debug = 0;
  int do_timefooter = 0;
  int ws_strip_level = 0;
  char *s, *e;

  s = hdf_get_value (cgi->hdf, "Query.debug", NULL);
  e = hdf_get_value (cgi->hdf, "Config.DebugPassword", NULL);
  if (hdf_get_int_value(cgi->hdf, "Config.DebugEnabled", 0) && 
      s && e && !strcmp(s, e)) do_debug = 1;
  do_timefooter = hdf_get_int_value (cgi->hdf, "Config.TimeFooter", 1);
  ws_strip_level = hdf_get_int_value (cgi->hdf, "Config.WhiteSpaceStrip", 1);

  dis = ne_timef();
  s = hdf_get_value (cgi->hdf, "cgiout.ContentType", "text/html");
  if (!strcasecmp(s, "text/html"))
    is_html = 1;

#if defined(HTML_COMPRESSION)
  /* Determine whether or not we can compress the output */
  if (is_html && hdf_get_int_value (cgi->hdf, "Config.CompressionEnabled", 0))
  {
    err = hdf_get_copy (cgi->hdf, "HTTP.AcceptEncoding", &s, NULL);
    if (err != STATUS_OK) return nerr_pass (err);
    if (s)
    {
      char *next;

      e = strtok_r (s, ",", &next);
      while (e && !use_deflate)
      {
	if (strstr(e, "deflate") != NULL)
	{
	  use_deflate = 1;
	  use_gzip = 0;
	}
	else if (strstr(e, "gzip") != NULL)
	  use_gzip = 1;
	e = strtok_r (NULL, ",", &next);
      }
      free (s);
    }
    s = hdf_get_value (cgi->hdf, "HTTP.UserAgent", NULL);
    if (s)
    {
      if (strstr(s, "MSIE 4") || strstr(s, "MSIE 5") || strstr(s, "MSIE 6"))
      {
	e = hdf_get_value (cgi->hdf, "HTTP.Accept", NULL);
	if (e && !strcmp(e, "*/*"))
	{
	  use_deflate = 0;
	  use_gzip = 0;
	}
      }
      else
      {
	if (strncasecmp(s, "mozilla/5.", 10))
	{
	  use_deflate = 0;
	  use_gzip = 0;
	}
      }
    }
    else
    {
      use_deflate = 0;
      use_gzip = 0;
    }
    if (use_deflate)
    {
      err = hdf_set_value (cgi->hdf, "cgiout.other.encoding",
	  "Content-Encoding: deflate");
    }
    else if (use_gzip)
    {
      err = hdf_set_value (cgi->hdf, "cgiout.other.encoding",
	  "Content-Encoding: gzip");
    }
    if (err != STATUS_OK) return nerr_pass(err);
  }
#endif

  err = cgi_headers(cgi);
  if (err != STATUS_OK) return nerr_pass(err);

  if (is_html)
  {
    char buf[50];
    int x;

    if (do_timefooter)
    {
      snprintf (buf, sizeof(buf), "\n<!-- %5.3f:%d -->\n",
	  dis - cgi->time_start, use_deflate || use_gzip);
      err = string_append (str, buf);
      if (err != STATUS_OK) return nerr_pass(err);
    }

    if (ws_strip_level)
    {
      cgi_html_ws_strip(str, ws_strip_level);
    }

    if (do_debug)
    {
      err = string_append (str, "<hr>");
      if (err != STATUS_OK) return nerr_pass(err);
      x = 0;
      while (1)
      {
	char *k, *v;
	err = cgiwrap_iterenv (x, &k, &v);
	if (err != STATUS_OK) return nerr_pass(err);
	if (k == NULL) break;
	err =string_appendf (str, "%s = %s<br>", k, v);
	if (err != STATUS_OK) return nerr_pass(err);
	free(k);
	free(v);
	x++;
      }
      err = string_append (str, "<pre>");
      if (err != STATUS_OK) return nerr_pass(err);
      err = hdf_dump_str (cgi->hdf, NULL, 0, str);
      if (err != STATUS_OK) return nerr_pass(err);
    }
  }

#if defined(HTML_COMPRESSION)
    if (is_html && (use_deflate || use_gzip))
    {
      char *dest;
      static int gz_magic[2] = {0x1f, 0x8b}; /* gzip magic header */
      char gz_buf[20]; /* gzip header/footer buffer, len of header is 10 bytes */
      unsigned int crc = 0;
      int len2;

      if (use_gzip)
      {
	crc = crc32(0L, Z_NULL, 0);
	crc = crc32(crc, (const Bytef *)(str->buf), str->len);
      }
      len2 = str->len * 2;
      dest = (char *) malloc (sizeof(char) * len2);
      if (dest != NULL)
      {
	do {
	  err = cgi_compress (str, dest, &len2);
	  if (err == STATUS_OK)
	  {
	    if (use_gzip)
	    {
	      /* I'm using sprintf instead of cgiwrap_writef since
	       * the wrapper writef might not handle values with
	       * embedded NULLs... though I should fix the python one
	       * now as well */
	      sprintf(gz_buf, "%c%c%c%c%c%c%c%c%c%c", gz_magic[0], gz_magic[1],
		  Z_DEFLATED, 0 /*flags*/, 0,0,0,0 /*time*/, 0 /*xflags*/,
		  OS_CODE);
	      err = cgiwrap_write(gz_buf, 10);
	    }
	    if (err != STATUS_OK) break;
	    err = cgiwrap_write(dest, len2);
	    if (err != STATUS_OK) break;

	    if (use_gzip)
	    {
	      /* write crc and len in network order */
	      sprintf(gz_buf, "%c%c%c%c%c%c%c%c",
		  (0xff & (crc >> 0)),
		  (0xff & (crc >> 8)),
		  (0xff & (crc >> 16)),
		  (0xff & (crc >> 24)),
		  (0xff & (str->len >> 0)),
		  (0xff & (str->len >> 8)),
		  (0xff & (str->len >> 16)),
		  (0xff & (str->len >> 24)));
	      err = cgiwrap_write(gz_buf, 8);
	      if (err != STATUS_OK) break;
	    }
	  }
	  else
	  {
	    nerr_log_error (err);
	    err = cgiwrap_write(str->buf, str->len);
	  }
	} while (0);
	free (dest);
      }
      else
      {
	err = cgiwrap_write(str->buf, str->len);
      }
    }
    else
#endif
    {
      err = cgiwrap_write(str->buf, str->len);
    }

  return nerr_pass(err);
}

NEOERR *cgi_html_escape_strfunc(const char *str, char **ret)
{
  return nerr_pass(html_escape_alloc(str, strlen(str), ret));
}

NEOERR *cgi_html_strip_strfunc(const char *str, char **ret)
{
  return nerr_pass(html_strip_alloc(str, strlen(str), ret));
}

NEOERR *cgi_text_html_strfunc(const char *str, char **ret)
{
  return nerr_pass(convert_text_html_alloc(str, strlen(str), ret));
}

NEOERR *cgi_register_strfuncs(CSPARSE *cs)
{
  NEOERR *err;

  err = cs_register_esc_strfunc(cs, "url_escape", cgi_url_escape);
  if (err != STATUS_OK) return nerr_pass(err);
  err = cs_register_esc_strfunc(cs, "html_escape", cgi_html_escape_strfunc);
  if (err != STATUS_OK) return nerr_pass(err);
  err = cs_register_strfunc(cs, "text_html", cgi_text_html_strfunc);
  if (err != STATUS_OK) return nerr_pass(err);
  err = cs_register_esc_strfunc(cs, "js_escape", cgi_js_escape);
  if (err != STATUS_OK) return nerr_pass(err);
  err = cs_register_strfunc(cs, "html_strip", cgi_html_strip_strfunc);
  if (err != STATUS_OK) return nerr_pass(err);
  return STATUS_OK;
}

NEOERR *cgi_cs_init(CGI *cgi, CSPARSE **cs)
{
  NEOERR *err;

  *cs = NULL;

  do
  {
    err = cs_init (cs, cgi->hdf);
    if (err != STATUS_OK) break;
    err = cgi_register_strfuncs(*cs);
    if (err != STATUS_OK) break;
  } while (0);

  if (err && *cs) cs_destroy(cs);
  return nerr_pass(err);
}

NEOERR *cgi_display (CGI *cgi, const char *cs_file)
{
  NEOERR *err = STATUS_OK;
  char *debug;
  CSPARSE *cs = NULL;
  STRING str;
  int do_dump = 0;
  char *t;

  string_init(&str);

  debug = hdf_get_value (cgi->hdf, "Query.debug", NULL);
  t = hdf_get_value (cgi->hdf, "Config.DumpPassword", NULL);
  if (hdf_get_int_value(cgi->hdf, "Config.DebugEnabled", 0) &&
      debug && t && !strcmp (debug, t)) do_dump = 1;

  do
  {
    err = cs_init (&cs, cgi->hdf);
    if (err != STATUS_OK) break;
    err = cgi_register_strfuncs(cs);
    if (err != STATUS_OK) break;
    err = cs_parse_file (cs, cs_file);
    if (err != STATUS_OK) break;
    if (do_dump)
    {
      cgiwrap_writef("Content-Type: text/plain\n\n");
      hdf_dump_str(cgi->hdf, "", 0, &str);
      cs_dump(cs, &str, render_cb);
      cgiwrap_writef("%s", str.buf);
      break;
    }
    else
    {
      err = cs_render (cs, &str, render_cb);
      if (err != STATUS_OK) break;
    }
    err = cgi_output(cgi, &str);
    if (err != STATUS_OK) break;
  } while (0);

  cs_destroy(&cs);
  string_clear (&str);
  return nerr_pass(err);
}

void cgi_neo_error (CGI *cgi, NEOERR *err)
{
  STRING str;

  string_init(&str);
  cgiwrap_writef("Status: 500\n");
  cgiwrap_writef("Content-Type: text/html\n\n");

  cgiwrap_writef("<html><body>\nAn error occured:<pre>");
  nerr_error_traceback(err, &str);
  cgiwrap_write(str.buf, str.len);
  cgiwrap_writef("</pre></body></html>\n");
}

void cgi_error (CGI *cgi, const char *fmt, ...)
{
  va_list ap;

  cgiwrap_writef("Status: 500\n");
  cgiwrap_writef("Content-Type: text/html\n\n");
  cgiwrap_writef("<html><body>\nAn error occured:<pre>");
  va_start (ap, fmt);
  cgiwrap_writevf (fmt, ap);
  va_end (ap);
  cgiwrap_writef("</pre></body></html>\n");
}

void cgi_debug_init (int argc, char **argv)
{
  FILE *fp;
  char line[256];
  char *v, *k;

  Argv0 = argv[0];

  if (argc)
  {
    fp = fopen(argv[1], "r");
    if (fp == NULL)
      return;

    while (fgets(line, sizeof(line), fp) != NULL)
    {
      v = strchr(line, '=');
      if (v != NULL)
      {
	*v = '\0';
	v = neos_strip(v+1);
	k = neos_strip(line);
	cgiwrap_putenv (line, v);
      }
    }
    fclose(fp);
  }
}

void cgi_vredirect (CGI *cgi, int uri, const char *fmt, va_list ap)
{
  cgiwrap_writef ("Status: 302\r\n");
  cgiwrap_writef ("Content-Type: text/html\r\n");
  cgiwrap_writef ("Pragma: no-cache\r\n");
  cgiwrap_writef ("Expires: Fri, 01 Jan 1999 00:00:00 GMT\r\n");
  cgiwrap_writef ("Cache-control: no-cache, no-cache=\"Set-Cookie\", private\r\n");

  if (uri)
  {
    cgiwrap_writef ("Location: ");
  }
  else
  {
    char *host;
    int https = 0;

    if (!strcmp(hdf_get_value(cgi->hdf, "CGI.HTTPS", "off"), "on"))
    {
      https = 1;
    }

    host = hdf_get_value (cgi->hdf, "HTTP.Host", NULL);
    if (host == NULL)
      host = hdf_get_value (cgi->hdf, "CGI.ServerName", "localhost");

    cgiwrap_writef ("Location: %s://%s", https ? "https" : "http", host);

    if ((strchr(host, ':') == NULL)) {
      int port = hdf_get_int_value(cgi->hdf, "CGI.ServerPort", 80);

      if (!((https && port == 443) || (!https && port == 80)))
      {
	cgiwrap_writef(":%d", port);
      }
    }
  }
  cgiwrap_writevf (fmt, ap);
  cgiwrap_writef ("\r\n\r\n");
  cgiwrap_writef ("Redirect page<br><br>\n");
#if 0
  /* Apparently this crashes on some computers... I don't know if its
   * legal to reuse the va_list */
  cgiwrap_writef ("  Destination: <A HREF=\"");
  cgiwrap_writevf (fmt, ap);
  cgiwrap_writef ("\">");
  cgiwrap_writevf (fmt, ap);
  cgiwrap_writef ("</A><BR>\n<BR>\n");
#endif
  cgiwrap_writef ("There is nothing to see here, please move along...");

}

void cgi_redirect (CGI *cgi, const char *fmt, ...)
{
  va_list ap;

  va_start(ap, fmt);
  cgi_vredirect (cgi, 0, fmt, ap);
  va_end(ap);
  return;
}

void cgi_redirect_uri (CGI *cgi, const char *fmt, ...)
{
  va_list ap;

  va_start(ap, fmt);
  cgi_vredirect (cgi, 1, fmt, ap);
  va_end(ap);
  return;
}

char *cgi_cookie_authority (CGI *cgi, const char *host)
{
  HDF *obj;
  char *domain;
  int hlen = 0, dlen = 0;

  if (host == NULL)
  {
    host = hdf_get_value (cgi->hdf, "HTTP.Host", NULL);
  }
  if (host == NULL) return NULL;

  while (host[hlen] && host[hlen] != ':') hlen++;

  obj = hdf_get_obj (cgi->hdf, "CookieAuthority");
  if (obj == NULL) return NULL;
  for (obj = hdf_obj_child (obj);
       obj;
       obj = hdf_obj_next (obj))
  {
    domain = hdf_obj_value (obj);
    dlen = strlen(domain);
    if (hlen >= dlen)
    {
      if (!strncasecmp (host + hlen - dlen, domain, dlen))
	return domain;
    }
  }

  return NULL;
}

/* For more information about Cookies, see:
 * The original Netscape Cookie Spec:
 * http://wp.netscape.com/newsref/std/cookie_spec.html
 *
 * HTTP State Management Mechanism
 * http://www.ietf.org/rfc/rfc2109.txt
 */

NEOERR *cgi_cookie_set (CGI *cgi, const char *name, const char *value,
                        const char *path, const char *domain,
                        const char *time_str, int persistent, int secure)
{
  NEOERR *err;
  STRING str;
  char my_time[256];

  if (path == NULL) path = "/";

  string_init(&str);
  do {
    err = string_appendf(&str, "Set-Cookie: %s=%s; path=%s", name, value, path);
    if (err) break;

    if (persistent)
    {
      if (time_str == NULL)
      {
	/* Expires in one year */
	time_t exp_date = time(NULL) + 31536000;

	strftime (my_time, 48, "%A, %d-%b-%Y 23:59:59 GMT",
	    gmtime (&exp_date));
	time_str = my_time;
      }
      err = string_appendf(&str, "; expires=%s", time_str);
      if (err) break;
    }
    if (domain)
    {
      err = string_appendf(&str, "; domain=%s", domain);
      if (err) break;
    }
    if (secure)
    {
      err = string_append(&str, "; secure");
      if (err) break;
    }
    err = string_append(&str, "\r\n");
  } while (0);
  if (err)
  {
    string_clear(&str);
    return nerr_pass(err);
  }
  cgiwrap_write(str.buf, str.len);
  string_clear(&str);
  return STATUS_OK;
}

/* This will actually issue up to three set cookies, attempting to clear
 * the damn thing. */
NEOERR *cgi_cookie_clear (CGI *cgi, const char *name, const char *domain,
                          const char *path)
{
  if (path == NULL) path = "/";
  if (domain)
  {
    if (domain[0] == '.')
    {
      cgiwrap_writef ("Set-Cookie: %s=; path=%s; domain=%s;"
	  "expires=Thursday, 01-Jan-1970 00:00:00 GMT\r\n", name, path,
	  domain + 1);
    }
    cgiwrap_writef("Set-Cookie: %s=; path=%s; domain=%s;"
	"expires=Thursday, 01-Jan-1970 00:00:00 GMT\r\n", name, path,
	domain);
  }
  cgiwrap_writef("Set-Cookie: %s=; path=%s; "
      "expires=Thursday, 01-Jan-1970 00:00:00 GMT\r\n", name, path);

  return STATUS_OK;
}
