blob: 45588203acfccf578fca884dd9e2ad76619e352e [file] [log] [blame]
/*
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;
}