/* 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; }