/*
mod_ecs - Embedded ClearSilver CGI Apache Module

mod_ecs is a heavily modified version of mod_ecgi from:
http://www.webthing.com/software/mod_ecgi.html

This version is designed to run with the ClearSilver CGIKit, specifically 
with the cgi_wrap calls from that kit.  Those calls wrap the standard CGI 
access methods, namely environment variables and stdin/stdout, allowing 
those calls to be replaced easily.  mod_ecs provides replacement calls which
interface directly with the Apache internals.

Additionally, mod_ecs is designed to dlopen() the shared library CGI once, 
and keep it in memory, making the CGI almost identical in performance to a 
regular Apache module.  The fact that your CGI will be called multiple times
is the biggest difference you can expect from a standard ClearSilver based CGI.
This means your code must be clean!

ECS - Embedded ClearSilver

Platform: UNIX only.  Anyone who wants to is welcome to port it elsewhere.

=======================================================
To COMPILE Apache with embedded CGI support, use
	-ldl in EXTRA_LIBS
	possibly -rdynamic in EXTRA_LFLAGS
 I took this out of the config because its not there on freebsd4 
 = ConfigStart
	LIBS="$LIBS -ldl"
 = ConfigEnd
(or as required by your platform)

OK, here's for APACI:
 * MODULE-DEFINITION-START
 * Name: ecs_module
 * MODULE-DEFINITION-END

=======================================================

=======================================================
BUGS
Lots - here are some obvious ones
	- won't work with NPH
	- No mechanism is provided for running from an SSI
	- Can't take part in content-negotiation
	- No graceful cleanup if a CGI program crashes (though it's OK
	  if the CGI fails but returns).
	- Suspected memory leak inherited from Apache (which ignores it
	  because it happens just before exit there).

*/

#include <dlfcn.h>
#include "mod_ecs.h"

#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_core.h"
#include "http_protocol.h"
#include "http_main.h"
#include "http_log.h"
#include "util_script.h"
#include "http_conf_globals.h"

module ecs_module;

/* Configuration stuff */

#define log_reason(reason,name,r) ap_log_error(APLOG_MARK,APLOG_ERR,(r)->server,(reason),(name))
#define log_scripterror(r,conf,ret,error) (log_reason((error),(r)->filename,(r)),ret)

char** ecs_create_argv(pool*,char*,char*,char*,char*,const char*);

/****************************************************************
 *
 * Actual CGI handling...
 */
const int ERROR = 500;
const int INTERNAL_REDIRECT = 3020;

#undef ECS_DEBUG

/******************************************************************
 * cgiwrap routines
 *   We've replaced all the normal CGI api calls with calls to the 
 *   appropriate cgiwrap routines instead.  Then, we provide versions of
 *   the cgiwrap callback here that interface directly with apache.  We
 *   need to mimic a bunch of the stuff that apache does in mod_cgi in
 *   order to implement the output portion of the CGI spec.
 */
typedef struct header_buf {
  char *buf;
  int len;
  int max;
  int loc;
  int nonl;
} HEADER_BUF;

typedef struct wrap_data {
  HEADER_BUF hbuf;
  int end_of_header;
  int returns;
  request_rec *r;
} WRAPPER_DATA;

static int buf_getline (const char *idata, int ilen, char *odata, int olen, int *nonl)
{
  char *eol;
  int len;

  *nonl = 1;
  eol = strchr (idata, '\n');
  if (eol == NULL)
  {
    len = ilen;
  }
  else
  {
    *nonl = 0;
    len = eol - idata + 1;
  }
  if (len > olen) len = olen;
  memcpy (odata, idata, len);
  odata[len] = '\0';
  return len;
}

static int h_getline (char *buf, int len, void *h)
{
  HEADER_BUF *hbuf = (HEADER_BUF *)h;
  int ret;

  buf[0] = '\0';
  if (hbuf->loc > hbuf->len)
    return 0;

  ret = buf_getline (hbuf->buf + hbuf->loc, hbuf->len - hbuf->loc, buf, len, &(hbuf->nonl));
  hbuf->loc += ret;
#if ECS_DEBUG>1
  fprintf (stderr, "h_getline: [%d] %s\n", ret, buf);
#endif
  return ret;
}

static int header_write (HEADER_BUF *hbuf, const char *data, int dlen)
{
  char buf[1024];
  int done, len;
  int nonl = hbuf->nonl;

  done = 0;
  while (done < dlen)
  {
    nonl = hbuf->nonl;
    len = buf_getline (data + done, dlen - done, buf, sizeof(buf), &(hbuf->nonl));
    if (len == 0)
      break;
    done += len;
    if (hbuf->len + len > hbuf->max)
    {
      hbuf->max *= 2;
      if (hbuf->len + len > hbuf->max)
      {
	hbuf->max += len + 1;
      }
      hbuf->buf = (char *) realloc ((void *)(hbuf->buf), hbuf->max);
    }
    memcpy (hbuf->buf + hbuf->len, buf, len);
    hbuf->len += len;
    if (!nonl && (buf[0] == '\n' || buf[0] == '\r'))
    {
      /* end of headers */
      return done;
    }
  }

  return 0;
}

/* The normal CGI module passes the returned data through
 * ap_scan_script_header().  We can't do that directly, since we don't
 * have a constant stream of data, so we buffer the header into our own
 * structure, and call ap_scan_script_header_err_core() with our own
 * getline() function to walk the header buffer we have.  We could
 * probably get some speed improvement by keeping the header buffer
 * between runs, instead of growing it every time... for later.  Also,
 * we currently don't use the pool allocation routines here, so we have
 * to be very careful not to leak.  We could probably at least use the
 * ap_register_cleanup() function to make sure we clean up our mess...
 */
static int wrap_write (void *data, const char *buf, size_t len)
{
  WRAPPER_DATA *wrap = (WRAPPER_DATA *)data;
  int wl;
  int ret;

#if ECS_DEBUG>1
  fprintf (stderr, "wrap_write (%s, %d)\n", buf, len);
#endif
  if (!wrap->end_of_header)
  {
    wl = header_write (&(wrap->hbuf), buf, len);
    if (wl == 0)
    {
      return len;
    }
    wrap->end_of_header = 1;
    wrap->hbuf.loc = 0;
#if ECS_DEBUG>1
    fprintf (stderr, "ap_scan_script_header_err_core\n%s\n", wrap->hbuf.buf);
#endif
    wrap->returns = ap_scan_script_header_err_core(wrap->r, NULL, h_getline, 
	(void *)&(wrap->hbuf));
#if ECS_DEBUG>1
    fprintf (stderr, "ap_scan_script_header_err_core.. done\n");
#endif
    if (len >= wl)
    {
      len = len - wl;
      buf = buf + wl;
    }

    if (wrap->returns == OK)
    {
      const char* location = ap_table_get (wrap->r->headers_out, "Location");

      if (location && location[0] == '/' && wrap->r->status == 200) 
      {
	wrap->returns = INTERNAL_REDIRECT;
      } 
      else if (location && wrap->r->status == 200) 
      {
	/* XX Note that if a script wants to produce its own Redirect
	 * body, it now has to explicitly *say* "Status: 302"
	 */
	wrap->returns = REDIRECT;
      } 
      else 
      {
#ifdef ECS_DEBUG
	fprintf (stderr, "ap_send_http_header\n");
#endif
	ap_send_http_header(wrap->r);
#ifdef ECS_DEBUG
	fprintf (stderr, "ap_send_http_header.. done\n");
#endif
      }
    }
  }
  /* if header didn't return OK, ignore the rest */
  if ((wrap->returns != OK) || wrap->r->header_only)
  {
    return len;
  }
#if ECS_DEBUG>1
  fprintf (stderr, "ap_rwrite(%s,%d)\n", buf, len);
#endif
  ret = ap_rwrite (buf, len, wrap->r);
#if ECS_DEBUG>1
  fprintf (stderr, "ap_rwrite.. done\n");
#endif
  return ret;
}

int wrap_vprintf (void *data, const char *fmt, va_list ap)
{
  char buf[4096];
  int len;

  len = ap_vsnprintf (buf, sizeof(buf), fmt, ap);
  return wrap_write (data, buf, len);
}

static int wrap_read (void *data, char *buf, size_t len)
{
  WRAPPER_DATA *wrap = (WRAPPER_DATA *)data;
  int ret;
  int x = 0;

#if ECS_DEBUG>1
  fprintf (stderr, "wrap_read (%s, %d)\n", buf, len);
#endif
  do
  {
    ret = ap_get_client_block(wrap->r, buf + x, len - x);
    if (ret <= 0) break;
    x += ret;
  } while (x < len);
#if ECS_DEBUG>1
  fprintf (stderr, "done ap_get_client_block\n");
#endif
  if (ret < 0) return ret;
  return x;
}

static char *wrap_getenv (void *data, const char *s)
{
  WRAPPER_DATA *wrap = (WRAPPER_DATA *)data;
  char *v;

  v = (char *) ap_table_get (wrap->r->subprocess_env, s);
  if (v) return strdup(v);
  return NULL;
}

static int wrap_putenv (void *data, const char *k, const char *v)
{
  WRAPPER_DATA *wrap = (WRAPPER_DATA *)data;

  ap_table_set (wrap->r->subprocess_env, k, v);

  return 0;
}	

static char *wrap_iterenv (void *data, int x, char **k, char **v)
{
  WRAPPER_DATA *wrap = (WRAPPER_DATA *)data;
  array_header *env = ap_table_elts(wrap->r->subprocess_env);
  table_entry *entry = (table_entry*)env->elts;

  if (x >= env->nelts) return 0;

  if (entry[x].key == NULL || entry[x].val == NULL)
    return 0;

  *k = strdup(entry[x].key);
  *v = strdup(entry[x].val);

  return 0;
}	

/*************************************************************************
 * Actual mod_ecs data structures for configuration
 */

typedef void (*InitFunc)();
typedef void (*CleanupFunc)();
typedef int (*CGIMainFunc)(int,char**,char**);
typedef int (*WrapInitFunc)(void *,void *,void*,void*,void*,void*,void*);

typedef struct {
  const char *libpath;
  ap_os_dso_handle_t dlib;
} ecs_deplibs;

typedef struct {
  const char *libpath;
  ap_os_dso_handle_t dlib;
  WrapInitFunc wrap_init;
  CGIMainFunc start;
  time_t mtime;
  int loaded;
} ecs_manager;

typedef struct {
  array_header *deplibs;
  array_header *handlers;
  int fork_enabled;
  int reload_enabled;
} ecs_server_conf;

const char *ECSInit = "ECSInit";
const char *ECSCleanUp = "ECSCleanup";
const char *WrapInit = "cgiwrap_init_emu";
const char *CGIMain = "main";

static void dummy (ap_os_dso_handle_t dlhandle)
{
}

static void slib_cleanup (ap_os_dso_handle_t dlhandle)
{
  CleanupFunc cleanupFunc;
  if ((cleanupFunc = (CleanupFunc)ap_os_dso_sym(dlhandle, ECSCleanUp))) {
    (*cleanupFunc)();
  }
  ap_os_dso_unload(dlhandle);
#ifdef ECS_DEBUG
  fprintf(stderr, "Unloading handle %d", dlhandle);
#endif
}

void *create_ecs_config (pool *p, server_rec *dummy)
{
  ecs_server_conf *new = ap_palloc (p, sizeof(ecs_server_conf));
  new->deplibs = ap_make_array(p,1,sizeof(ecs_deplibs));
  new->handlers = ap_make_array(p,1,sizeof(ecs_manager));
  new->fork_enabled = 0;
  new->reload_enabled = 0;
  return (void *) new;
}

char** e_setup_cgi_env (request_rec* r)
{
  char** env;

  ap_add_common_vars(r);
  ap_add_cgi_vars(r);
  env = ap_create_environment(r->pool,r->subprocess_env);

  return env;
}

const char *set_dep_lib (cmd_parms *parms, void *dummy, char *arg)
{
  ecs_server_conf *cls = ap_get_module_config (parms->server->module_config,
      &ecs_module);
  ecs_deplibs *entry;
  ap_os_dso_handle_t dlhandle;
  InitFunc init_func;

  if ((dlhandle = ap_os_dso_load(arg)) == NULL) {
    return ap_os_dso_error();
  }

  if ((init_func = (InitFunc)ap_os_dso_sym(dlhandle, ECSInit))) {
    (*init_func)();
  }

  ap_register_cleanup (cls->deplibs->pool, dlhandle, slib_cleanup, slib_cleanup);

  entry = (ecs_deplibs*)ap_push_array(cls->deplibs);
  entry->libpath = ap_pstrdup(cls->deplibs->pool, arg);
  entry->dlib = dlhandle;

  return NULL;
}

/* Load an ecs shared library */
static const char *load_library (ap_pool *p, ecs_manager *entry, int do_stat, char *prefix)
{
  ap_os_dso_handle_t dlhandle;
  InitFunc init_func;
  CGIMainFunc cgi_main;
  WrapInitFunc wrap_init;
  char *err;
  struct stat s;

  if (do_stat)
  {
    if (stat(entry->libpath, &s) == -1)
    {
      err = ap_psprintf (p, "Failed to stat library file %s: %d", entry->libpath, errno);
      return err;
    }
    entry->mtime = s.st_mtime;
  }

  if (entry->loaded == 1)
  {
    fprintf (stderr, "Warning: attempting to reload %s but it's already loaded\n", entry->libpath);
  }

  /* This does a RTLD_NOW, if we want lazy, we're going to have to do it
   * ourselves */
  if ((dlhandle = ap_os_dso_load(entry->libpath)) == NULL) {
    return ap_os_dso_error();
  }

  if (entry->dlib == dlhandle)
  {
    fprintf (stderr, "Warning: Reload of %s returned same handle\n", entry->libpath);
  }

  if ((init_func = (InitFunc)ap_os_dso_sym(dlhandle, ECSInit))) {
    (*init_func)();
  }
  if (!(wrap_init = (WrapInitFunc)ap_os_dso_sym(dlhandle, WrapInit))) {
    err = ap_psprintf (p, "Failed to find wrap init function %s in shared object: %s", WrapInit, dlerror());
    ap_os_dso_unload(dlhandle);
    return err;
  }
  if (!(cgi_main = (CGIMainFunc)ap_os_dso_sym(dlhandle, CGIMain))) {
    err = ap_psprintf (p, "Failed to find entry function %s in shared object: %s", CGIMain, dlerror());
    ap_os_dso_unload(dlhandle);
    return err;
  }

  /* Um, this may be a problem... */
  ap_register_cleanup (p, dlhandle, slib_cleanup, dummy);

  entry->dlib = dlhandle;
  entry->wrap_init = wrap_init;
  entry->start = cgi_main;
  entry->loaded = 1;

  fprintf (stderr, "%sLoaded library %s [%d]\n", prefix, entry->libpath, dlhandle);

  return NULL;
}

const char *set_pre_lib (cmd_parms *parms, void *dummy, char *arg)
{
  ecs_server_conf *cls = ap_get_module_config (parms->server->module_config,
      &ecs_module);
  ecs_manager *entry;

  entry = (ecs_manager*)ap_push_array(cls->handlers);
  entry->libpath = ap_pstrdup(cls->handlers->pool, arg);

  return load_library (cls->handlers->pool, entry, 1, "Pre");
}

const char *set_fork (cmd_parms *parms, void *dummy, int flag)
{
  ecs_server_conf *cls = ap_get_module_config (parms->server->module_config,
      &ecs_module);

  cls->fork_enabled = (flag ? 1 : 0);

  return NULL;
}

const char *set_reload (cmd_parms *parms, void *dummy, int flag)
{
  ecs_server_conf *cls = ap_get_module_config (parms->server->module_config,
      &ecs_module);

  cls->reload_enabled = (flag ? 1 : 0);

  return NULL;
}

static ecs_manager *findHandler(array_header *a, char *file)
{
  ecs_manager *list = (ecs_manager*)(a->elts);
  int i;

  for (i = 0; i < a->nelts; i++)
  {
    if (!strcmp(list[i].libpath, file))
      return &(list[i]);
  }
  return NULL;
}

static int run_dl_cgi (ecs_server_conf *sconf, request_rec* r, char* argv0)
{
  int ret = 0;
  void* handle;
  int cgi_status;
  int argc;
  char** argv;
  WRAPPER_DATA *wdata;
  ecs_manager *handler;
  const char *err;

  char** envp = e_setup_cgi_env(r);

  /* Find/open library */
  handler = findHandler (sconf->handlers, r->filename);
  if (handler == NULL)
  {
    ecs_manager my_handler;
    my_handler.libpath = ap_pstrdup(sconf->handlers->pool, r->filename);
    err = load_library(sconf->handlers->pool, &my_handler, 1, "");
    if (err != NULL)
    {
      log_reason("Error opening library:", err, r);
      ret = ERROR;
    }
    else
    {
      handler = (ecs_manager*)ap_push_array(sconf->handlers);
      handler->dlib = my_handler.dlib;
      handler->wrap_init = my_handler.wrap_init;
      handler->start = my_handler.start;
      handler->mtime = my_handler.mtime;
      handler->loaded = my_handler.loaded;
      handler->libpath = my_handler.libpath;
    }
  }
  else if (sconf->reload_enabled)
  {
    struct stat s;
    if (stat(handler->libpath, &s) == -1)
    {
      log_reason("Unable to stat file: ", handler->libpath, r);
      ret = ERROR;
    }
    else if (!handler->loaded || (s.st_mtime > handler->mtime))
    {
      if (handler->loaded)
      {
	int x;
	fprintf (stderr, "Unloading %s\n", handler->libpath);
	slib_cleanup(handler->dlib);
	/* Really unload this thing */
	while ((x < 100) && (dlclose(handler->dlib) != -1)) x++;
	if (x == 100) 
	  fprintf (stderr, "dlclose() never returned -1");
	handler->loaded = 0;
      }
      err = load_library(sconf->handlers->pool, handler, 0, "Re");
      if (err != NULL)
      {
	log_reason("Error opening library:", err, r);
	ret = ERROR;
      }
      handler->mtime = s.st_mtime;
    }
  }

  if (!ret) {
    if ((!r->args) || (!r->args[0]) || (ap_ind(r->args,'=') >= 0) ) 
    {
      argc = 1;
      argv = &argv0;
    } else {
      argv = ecs_create_argv(r->pool, NULL,NULL,NULL,argv0,r->args);
      for (argc = 0 ; argv[argc] ; ++argc);
    }
  }

  /*  Yow ... at last we can go ...

      Now, what to do if CGI crashes (aaargh)
      Methinks an atexit ... cleanup perhaps; have to figgerout
      what the atexit needs to invoke ... yuk!

      Or maybe better to catch SIGSEGV and SIGBUS ?
      - we don't want coredumps from someone else's bugs, do we?
      still doesn't guarantee anything very good :-(

      Ugh .. nothing better???
   */
  if (!ret)
  {
    wdata = (WRAPPER_DATA *) ap_pcalloc (r->pool, sizeof (WRAPPER_DATA));
    /* We use malloc here because there is no pool alloc command for
     * realloc... */
    wdata->hbuf.buf = (char *) malloc (sizeof(char) * 1024);
    wdata->hbuf.max = 1024;
    wdata->r = r;

#ifdef ECS_DEBUG
    fprintf (stderr, "wrap_init()\n");
#endif
    handler->wrap_init(wdata, wrap_read, wrap_vprintf, wrap_write, wrap_getenv, wrap_putenv, wrap_iterenv);

#ifdef ECS_DEBUG
    fprintf (stderr, "cgi_main()\n");
#endif
    cgi_status = handler->start(argc,argv,envp);
    if (cgi_status != 0)
    {
      /*log_reason("CGI returned error status", cgi_status, r) ;*/
      ret = ERROR;
    }

    if (wdata->returns != OK)
      ret = wdata->returns;

    free (wdata->hbuf.buf);
  }

  return ret;
}

int run_xcgi (ecs_server_conf *conf, request_rec* r, char* argv0)
{
  int len_read;
  char argsbuffer[HUGE_STRING_LEN];
  int ret = 0;

  ret = run_dl_cgi (conf, r, argv0);

  if (ret == INTERNAL_REDIRECT)
  {
    const char* location = ap_table_get (r->headers_out, "Location");

    /* This redirect needs to be a GET no matter what the original
     * method was.
     */
    r->method = ap_pstrdup(r->pool, "GET");
    r->method_number = M_GET;

    /* We already read the message body (if any), so don't allow
     * the redirected request to think it has one.  We can ignore 
     * Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR.
     */
    ap_table_unset(r->headers_in, "Content-Length");

    ap_internal_redirect_handler (location, r);
    return OK;
  } 

  return ret;
}

int ecs_handler (request_rec* r)
{
  int retval;
  char *argv0;
  int is_included = !strcmp (r->protocol, "INCLUDED");
  void *sconf = r->server->module_config;
  ecs_server_conf *conf =
    (ecs_server_conf *)ap_get_module_config(sconf, &ecs_module);

  ap_error_log2stderr(r->server);
#ifdef ECS_DEBUG
  fprintf(stderr, "running ecs_handler %s\n", r->filename);
#endif

  if((argv0 = strrchr(r->filename,'/')) != NULL)
    argv0++;
  else argv0 = r->filename;

  if (!(ap_allow_options (r) & OPT_EXECCGI) )
    return log_scripterror(r, conf, FORBIDDEN,
	"Options ExecCGI is off in this directory");

  if (S_ISDIR(r->finfo.st_mode))
    return log_scripterror(r, conf, FORBIDDEN,
	"attempt to invoke directory as script");
  if (r->finfo.st_mode == 0)
    return log_scripterror(r, conf, NOT_FOUND,
	"file not found or unable to stat");

#ifdef ECS_DEBUG
  fprintf (stderr, "ap_setup_client_block\n");
#endif
  if ((retval = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR)))
    return retval;

#ifdef ECS_DEBUG
  fprintf (stderr, "before run\n");
#endif
  return run_xcgi(conf, r, argv0);
}

handler_rec ecs_handlers[] = {
  { ECS_MAGIC_TYPE, ecs_handler },
  { "ecs-cgi", ecs_handler},
  { NULL }
};

command_rec ecs_cmds[] = {
 { "ECSFork", set_fork, NULL, OR_FILEINFO, FLAG,
   "On or off to enable or disable (default) forking before calling cgi_main" },
 { "ECSReload", set_reload, NULL, OR_FILEINFO, FLAG,
   "On or off to enable or disable (default) checking if the shared library\n" \
   "  has changed and reloading it if it has"},
 { "ECSDepLib", set_dep_lib, NULL, RSRC_CONF, TAKE1,
   "The location of a dependent lib to dlopen during init"},
 { "ECSPreload", set_pre_lib, NULL, RSRC_CONF, TAKE1,
   "The location of a shared lib handler to preload during init"},
 { NULL }
};

module ecs_module = {
   STANDARD_MODULE_STUFF,
   NULL,			/* initializer */
   NULL,			/* dir config creater */
   NULL,			/* dir merger --- default is to override */
   create_ecs_config,		/* server config */
   NULL, /*merge_ecs_config,*/	       	/* merge server config */
   ecs_cmds,			/* command table */
   ecs_handlers,		/* handlers */
   NULL,			/* filename translation */ 
   NULL,			/* check_user_id */
   NULL,			/* check auth */
   NULL,			/* check access */
   NULL,			/* type_checker */
   NULL,			/* fixups */
   NULL,			/* logger */
#if MODULE_MAGIC_NUMBER >= 19970103
   NULL,       			/* [3] header parser */ 
#endif
#if MODULE_MAGIC_NUMBER >= 19970719
   NULL,       			/* process initializer */
#endif
#if MODULE_MAGIC_NUMBER >= 19970728
   NULL,       			/* process exit/cleanup */
#endif
#if MODULE_MAGIC_NUMBER >= 19970902
   NULL,       			/* [1] post read_request handling */
#endif
};


/* Here's some stuff that essentially duplicates util_script.c
   This really should be merged, but if _I_ do that it'll break
   modularity and leave users with a nasty versioning problem.

   If I get a round tuit sometime, I might ask the Apache folks
   about integrating some changes in the main source tree.
*/
/* If a request includes query info in the URL (stuff after "?"), and
 * the query info does not contain "=" (indicative of a FORM submission),
 * then this routine is called to create the argument list to be passed
 * to the CGI script.  When suexec is enabled, the suexec path, user, and
 * group are the first three arguments to be passed; if not, all three
 * must be NULL.  The query info is split into separate arguments, where
 * "+" is the separator between keyword arguments.
 */
char **ecs_create_argv(pool *p, char *path, char *user, char *group,
                          char *av0, const char *args)
{
    int x, numwords;
    char **av;
    char *w;
    int idx = 0;

    /* count the number of keywords */

    for (x = 0, numwords = 1; args[x]; x++)
        if (args[x] == '+') ++numwords;

    if (numwords > APACHE_ARG_MAX - 5) {
        numwords = APACHE_ARG_MAX - 5; /* Truncate args to prevent overrun */
    }
    av = (char **)ap_palloc(p, (numwords + 5) * sizeof(char *));

    if (path)
        av[idx++] = path;
    if (user)
        av[idx++] = user;
    if (group)
        av[idx++] = group;

    av[idx++] = av0;

    for (x = 1; x <= numwords; x++) {
        w = ap_getword_nulls(p, &args, '+');
        ap_unescape_url(w);
        av[idx++] = ap_escape_shell_cmd(p, w);
    }
    av[idx] = NULL;
    return av;
}