/* 
 * mod_rlimit.c -- Bernard Blackham <bernard@ucs.uwa.edu.au>
 *
 * Enforces CPU time limits upon child processes.
 * Ideally it would also cause apache to throw core dumps into a specified
 * directory, but that doesn't work yet.
 *
 * Compile with:
 *   gcc -o /usr/lib/apache/1.3/mod_rlimit.so -shared -Wall -O2 -DEAPI \
 *       -I/usr/include/apache-1.3 mod_rlimit.c
 * 
 * And make sure you have apache-dev installed. To use, in httpd.conf:
 * 
 *   LoadModule rlimit_module /usr/lib/apache/1.3/mod_rlimit.so
 *   
 *   <IfModule mod_rlimit.c>
 *   	ChildCPUTimeLimit 30
 *   	CoreDumpLocation /tmp/apache-cores/ # Not yet working :(
 *   </IfModule>
 *
 */
#include <sys/prctl.h>

#include <httpd.h>
#include <http_log.h>
#include <http_config.h>
#include <http_core.h>
#include <ap_config.h>
#include <ap_alloc.h>
#include <ap.h>

/* Module configuration struct: */
typedef struct {
	unsigned int cputime;
	char* dump_location; /* do we really need this then? */
} rlimit_server_conf;

/* Random globals that I can't seem to do without */
char* dump_location; /* global so we can reach it from our segfault handler */
void (*old_sigxcpu_handler)(int);
void (*old_sigsegv_handler)(int);

module rlimit_module;    /* forward decl */

/* Returns a pointer to the module's config */
static void *rlimit_make_config (pool *p, server_rec *s) {
	struct rlimit lim;

	rlimit_server_conf *c =
		(rlimit_server_conf*) ap_pcalloc(p, sizeof(rlimit_server_conf));

	c->cputime = 0;
	dump_location = c->dump_location = NULL;

	lim.rlim_cur = 0;
	lim.rlim_max = RLIM_INFINITY;
	setrlimit(RLIMIT_CORE, &lim);

	return c;
}

static const char* set_child_cputime_limit(cmd_parms *cmd, void *z, char *arg) {
	server_rec *s = cmd->server;
	rlimit_server_conf *c =
		(rlimit_server_conf*) ap_get_module_config(s->module_config,
												   &rlimit_module);

	c->cputime = atoi(arg);

	/*
	ap_log_error(APLOG_MARK, APLOG_NOTICE, NULL,
			"mod_rlimit: enforcing %d seconds of CPU time per child",
			c->cputime);
	*/

	return NULL;
}

static const char* set_coredump_location(cmd_parms *cmd, void* dummy, char* arg) {
	server_rec *s = cmd->server;
	rlimit_server_conf *c =
		(rlimit_server_conf*) ap_get_module_config(s->module_config,
												   &rlimit_module);

	dump_location = c->dump_location = arg;

	ap_log_error(APLOG_MARK, APLOG_NOTICE, NULL,
			"mod_rlimit: dumping cores into %s", c->dump_location);

	return NULL;
}

static const command_rec rlimit_cmds[] = {
{ "ChildCPUTimeLimit", set_child_cputime_limit, NULL, RSRC_CONF, TAKE1,
	"CPU seconds to allow a child to exist for"},
{ "CoreDumpLocation", set_coredump_location, NULL, RSRC_CONF, TAKE1,
	"Directory to dump cores in"},
{ NULL }
};

/* Handles signals SIGXCPU and SIGSEGV.
 * Dump core if a SIGSEGV. Doesn't currently work. Gah.
 */
static void rlimit_sig_handler(int signum) {
	char dirname[40];

	switch(signum) {
		case SIGSEGV:
			snprintf(dirname, 40, "core.%d.segfault", getpid());
			break;
		case SIGXCPU:
			ap_log_error(APLOG_MARK, APLOG_CRIT, NULL,
					"Child %d exceeded CPU time limit", getpid());
			if (old_sigxcpu_handler != SIG_ERR) old_sigxcpu_handler(SIGXCPU);
			return;
		default:
			snprintf(dirname, 40, "core.%d.sig%d", getpid(), signum);
			break;
	}

	/* hmm. we have no server_rec, and hence little logging we can do */
	if (chdir(dump_location) == 0)
		if (mkdir(dirname, 0700) == 0)
			chdir(dirname);

	if (signum == SIGSEGV && old_sigsegv_handler != SIG_ERR)
		old_sigsegv_handler(SIGSEGV);
}

static void rlimit_child_init(server_rec *s, pool *p) {
	struct rlimit lim;
	rlimit_server_conf *c =
		(rlimit_server_conf*) ap_get_module_config(s->module_config,
												   &rlimit_module);

	if (c->cputime) {
		lim.rlim_cur = c->cputime;
		/* add enough spare time on the hard limit so we can log and exit */
		lim.rlim_max = c->cputime+1; 

		if (setrlimit(RLIMIT_CPU, &lim) == -1) {
			ap_log_error(APLOG_MARK, APLOG_ERR, NULL,
					"mod_rlimit: setrlimit(RLIMIT_CPU) failed: %s",
					strerror(errno));
		}

		if ((old_sigxcpu_handler = signal(SIGXCPU, rlimit_sig_handler))
				== SIG_ERR) {
			ap_log_error(APLOG_MARK, APLOG_ERR, NULL,
					"mod_rlimit: signal(SIGXCPU) failed: %s", strerror(errno));
		}
	}

	if (c->dump_location) {
		lim.rlim_cur = lim.rlim_max = RLIM_INFINITY;

		if (setrlimit(RLIMIT_CORE, &lim) == -1) {
			ap_log_error(APLOG_MARK, APLOG_ERR, NULL,
					"mod_rlimit: setrlimit(RLIMIT_CORE) failed: %s",
					strerror(errno));
			return;
		}

		if ((old_sigsegv_handler = signal(SIGSEGV, rlimit_sig_handler))
				== SIG_ERR) {
			ap_log_error(APLOG_MARK, APLOG_ERR, NULL,
					"mod_rlimit: signal(SIGSEGV) failed: %s", strerror(errno));
		}

		/* Wackiness required for programs to coredump on Linux */
		if (prctl(PR_SET_DUMPABLE, 1) == -1) {
			ap_log_error(APLOG_MARK, APLOG_ERR, NULL,
					"mod_rlimit: prctl(PR_SET_DUMPABLE) failed: %s",
					strerror(errno));
		}
	}
}

module MODULE_VAR_EXPORT rlimit_module = {
   STANDARD_MODULE_STUFF,
   NULL,                     /* initializer */
   NULL,                     /* dir config creator */
   NULL,                     /* dir merger */
   rlimit_make_config,       /* server config */
   NULL,                     /* merge server config */
   rlimit_cmds,              /* command table */
   NULL,                     /* handlers */
   NULL,                     /* filename translation */
   NULL,                     /* check_user_id */
   NULL,                     /* check auth */
   NULL,                     /* check access */
   NULL,                     /* type_checker */
   NULL,                     /* fixups */
   NULL,                     /* logger */
   NULL,                     /* header parser */
   rlimit_child_init,        /* child init */
   NULL,                     /* child exit */
};



