#include "config.h"

#include "fastcgi.h"

#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define MAX_BUFFER_SIZE (64*1024)

#define CONST_STR_LEN(x) (x), sizeof(x) - 1
#define GSTR_LEN(x) (x) ? (x)->str : "", (x) ? (x)->len : 0
#define UNUSED(x) ((void)(x))

/* #define ERROR(...) g_printerr(G_STRLOC " (" G_STRFUNC "): " __VA_ARGS__) */
#define __STR(x) #x
#define ERROR(...) g_printerr("fcgi-cgi.c:" G_STRINGIFY(__LINE__) ": " __VA_ARGS__)

#define PACKAGE_DESC (PACKAGE_NAME " v" PACKAGE_VERSION " - FastCGI application to run normal cgi applications")

struct fcgi_cgi_server;
typedef struct fcgi_cgi_server fcgi_cgi_server;

struct fcgi_cgi_child;
typedef struct fcgi_cgi_child fcgi_cgi_child;

struct fcgi_cgi_server {
	struct ev_loop *loop;

	fastcgi_server *fsrv;
	GPtrArray *aborted_pending_childs;

	ev_signal
		sig_w_INT,
		sig_w_TERM,
		sig_w_HUP;
};

struct fcgi_cgi_child {
	fcgi_cgi_server *srv;
	fastcgi_connection *fcon;
	gint aborted_id;

	pid_t pid;
	gint child_status;
	ev_child child_watcher;

	gint pipe_in, pipe_out, pipe_err;
	ev_io pipe_in_watcher, pipe_out_watcher, pipe_err_watcher;

	/* write queue */
	fastcgi_queue write_queue;
};

static fcgi_cgi_child* fcgi_cgi_child_create(fcgi_cgi_server *srv, fastcgi_connection *fcon);
static void fcgi_cgi_child_check_done(fcgi_cgi_child *cld);
static void fcgi_cgi_child_close_write(fcgi_cgi_child *cld);
static void fcgi_cgi_child_close_read(fcgi_cgi_child *cld);
static void fcgi_cgi_child_close_error(fcgi_cgi_child *cld);
static void fcgi_cgi_child_free(fcgi_cgi_child *cld);
static void fcgi_cgi_child_error(fcgi_cgi_child *cld);
static void fcgi_cgi_child_start(fcgi_cgi_child *cld, const gchar *path);
static void fcgi_cgi_wrote_data(fastcgi_connection *fcon);

/* move a fd to another and close the old one */
static void move2fd(int srcfd, int dstfd) {
	if (srcfd != dstfd) {
		close(dstfd);
		dup2(srcfd, dstfd);
		close(srcfd);
	}
}

static void fd_init(int fd) {
#ifdef _WIN32
	int i = 1;
#endif
#ifdef FD_CLOEXEC
	/* close fd on exec (cgi) */
	fcntl(fd, F_SETFD, FD_CLOEXEC);
#endif
#ifdef O_NONBLOCK
	fcntl(fd, F_SETFL, O_NONBLOCK | O_RDWR);
#elif defined _WIN32
	ioctlsocket(fd, FIONBIO, &i);
#endif
}

static void fcgi_cgi_child_child_cb(struct ev_loop *loop, ev_child *w, int revents) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) w->data;
	UNUSED(revents);

	ev_child_stop(loop, w);

	fcgi_cgi_child_close_write(cld);

	cld->pid = -1;
	cld->child_status = w->rstatus;
	fcgi_cgi_child_check_done(cld);
}

static void write_queue(fcgi_cgi_child *cld) {
	if (-1 == cld->pipe_out) return;

	if (fastcgi_queue_write(cld->pipe_out, &cld->write_queue, 256*1024) < 0) {
		fcgi_cgi_child_close_write(cld);
		return;
	}

	if (-1 != cld->pipe_out) {
		if (cld->write_queue.length > 0) {
			ev_io_start(cld->srv->loop, &cld->pipe_out_watcher);
			if (cld->write_queue.length > MAX_BUFFER_SIZE) {
				fastcgi_suspend_read(cld->fcon);
			} else {
				fastcgi_resume_read(cld->fcon);
			}
		} else {
			if (cld->write_queue.closed) {
				fcgi_cgi_child_close_write(cld);
			} else {
				ev_io_stop(cld->srv->loop, &cld->pipe_out_watcher);
				fastcgi_resume_read(cld->fcon);
			}
		}
	}
}

static GByteArray* read_chunk(gint fd, guint maxlen) {
	gssize res;
	GByteArray *buf;
	int tmp_errno;

	buf = g_byte_array_sized_new(maxlen);
	g_byte_array_set_size(buf, maxlen);
	if (0 == maxlen) return buf;

	res = read(fd, buf->data, maxlen);
	if (res == -1) {
		tmp_errno = errno;
		g_byte_array_free(buf, TRUE);
		errno = tmp_errno;
		return NULL;
	} else if (res == 0) {
		g_byte_array_free(buf, TRUE);
		errno = ECONNRESET;
		return NULL;
	} else {
		g_byte_array_set_size(buf, res);
		return buf;
	}
}

static void fcgi_cgi_child_pipe_in_cb(struct ev_loop *loop, ev_io *w, int revents) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) w->data;
	GByteArray *buf;
	UNUSED(loop); UNUSED(revents);

	if (NULL == (buf = read_chunk(cld->pipe_in, 64*1024))) {
		switch (errno) {
		case EINTR:
		case EAGAIN:
#if EWOULDBLOCK != EAGAIN
		case EWOULDBLOCK:
#endif
			return; /* try again later */
		case ECONNRESET:
			fcgi_cgi_child_close_read(cld);
			break;
		default:
			ERROR("read from fd=%d failed, %s\n", cld->pipe_in, g_strerror(errno));
			fcgi_cgi_child_close_read(cld);
			break;
		}
	} else if (cld->fcon) {
		fastcgi_send_out_bytearray(cld->fcon, buf);
		fcgi_cgi_wrote_data(cld->fcon);
	} else {
		g_byte_array_free(buf, TRUE);
	}
}

static void fcgi_cgi_child_pipe_out_cb(struct ev_loop *loop, ev_io *w, int revents) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) w->data;
	UNUSED(loop); UNUSED(revents);

	write_queue(cld);
}

static void fcgi_cgi_child_pipe_err_cb(struct ev_loop *loop, ev_io *w, int revents) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) w->data;
	GByteArray *buf;
	UNUSED(loop); UNUSED(revents);

	if (NULL == (buf = read_chunk(cld->pipe_err, 64*1024))) {
		switch (errno) {
		case EINTR:
		case EAGAIN:
#if EWOULDBLOCK != EAGAIN
		case EWOULDBLOCK:
#endif
			return; /* try again later */
		case ECONNRESET:
			fcgi_cgi_child_close_error(cld);
			break;
		default:
			ERROR("read from fd=%d failed, %s\n", cld->pipe_err, g_strerror(errno));
			fcgi_cgi_child_close_error(cld);
			break;
		}
	} else if (cld->fcon) {
		fastcgi_send_err_bytearray(cld->fcon, buf);
		fcgi_cgi_wrote_data(cld->fcon);
	} else {
		g_byte_array_free(buf, TRUE);
	}
}

static fcgi_cgi_child* fcgi_cgi_child_create(fcgi_cgi_server *srv, fastcgi_connection *fcon) {
	fcgi_cgi_child *cld = g_slice_new0(fcgi_cgi_child);

	cld->srv = srv;
	cld->fcon = fcon;
	cld->aborted_id = -1;

	cld->pid = -1;
	ev_child_init(&cld->child_watcher, fcgi_cgi_child_child_cb, -1, 0);
	cld->child_watcher.data = cld;

	cld->pipe_in = cld->pipe_out = -1;
	ev_io_init(&cld->pipe_in_watcher, fcgi_cgi_child_pipe_in_cb, -1, 0);
	cld->pipe_in_watcher.data = cld;
	ev_io_init(&cld->pipe_out_watcher, fcgi_cgi_child_pipe_out_cb, -1, 0);
	cld->pipe_out_watcher.data = cld;
	ev_io_init(&cld->pipe_err_watcher, fcgi_cgi_child_pipe_err_cb, -1, 0);
	cld->pipe_err_watcher.data = cld;

	return cld;
}

static void fcgi_cgi_child_check_done(fcgi_cgi_child *cld) {
	if (!cld->fcon) {
		if (-1 != cld->aborted_id && (cld->pid == -1 || (cld->pipe_out == -1 && cld->pipe_in == -1))) {
			fcgi_cgi_child *t_cld;
			GPtrArray *a = cld->srv->aborted_pending_childs;
			guint i = a->len - 1;
			t_cld = g_ptr_array_index(a, cld->aborted_id) = g_ptr_array_index(a, i);
			g_ptr_array_set_size(a, i);
			t_cld->aborted_id = cld->aborted_id;
			cld->aborted_id = -1;
			fcgi_cgi_child_free(cld);
		}
	} else {
		if (cld->pid == -1 && cld->pipe_out == -1 && cld->pipe_in == -1 && cld->pipe_err == -1) {
			fastcgi_end_request(cld->fcon, cld->child_status, FCGI_REQUEST_COMPLETE);
			fcgi_cgi_child_free(cld);
		}
	}
}

static void fcgi_cgi_child_close_write(fcgi_cgi_child *cld) {
	if (cld->pipe_out != -1) {
		ev_io_stop(cld->srv->loop, &cld->pipe_out_watcher);
		close(cld->pipe_out);
		cld->pipe_out = -1;
		fastcgi_queue_clear(&cld->write_queue);
		cld->write_queue.closed = TRUE;
		fcgi_cgi_child_check_done(cld);
	}
}

static void fcgi_cgi_child_close_read(fcgi_cgi_child *cld) {
	if (cld->pipe_in != -1) {
		ev_io_stop(cld->srv->loop, &cld->pipe_in_watcher);
		close(cld->pipe_in);
		cld->pipe_in = -1;
		if (cld->fcon) fastcgi_send_out(cld->fcon, NULL);
		fcgi_cgi_child_check_done(cld);
	}
}

static void fcgi_cgi_child_close_error(fcgi_cgi_child *cld) {
	if (cld->pipe_err != -1) {
		ev_io_stop(cld->srv->loop, &cld->pipe_err_watcher);
		close(cld->pipe_err);
		cld->pipe_err = -1;
		if (cld->fcon) fastcgi_send_err(cld->fcon, NULL);
		fcgi_cgi_child_check_done(cld);
	}
}

static void fcgi_cgi_child_free(fcgi_cgi_child *cld) {
	if (cld->fcon) cld->fcon->data = NULL;
	cld->fcon = NULL;
	fcgi_cgi_child_close_write(cld);
	fcgi_cgi_child_close_read(cld);
	fcgi_cgi_child_close_error(cld);
	ev_child_stop(cld->srv->loop, &cld->child_watcher);
	g_slice_free(fcgi_cgi_child, cld);
}

static void fcgi_cgi_child_error(fcgi_cgi_child *cld) {
	if (cld->fcon) {
		fastcgi_connection_close(cld->fcon);
	}
}

static void copy_env_var(GPtrArray *env, const char *name) {
	guint i, namelen = strlen(name);
	const char *value;

	for (i = 0; i < env->len; ) {
		const char *entry = g_ptr_array_index(env, i);
		if (0 == strncmp(entry, name, namelen) && entry[namelen] == '=') {
			g_ptr_array_remove_index_fast(env, i);
			g_free((char*) entry);
		} else {
			++i;
		}
	}

	value = getenv(name);
	if (NULL != value) {
		guint valuelen = strlen(value);
		char *entry = g_malloc(namelen + valuelen + 2);
		memcpy(entry, name, namelen);
		entry[namelen] = '=';
		memcpy(entry + namelen + 1, value, valuelen);
		entry[namelen+valuelen+1] = 0;
		g_ptr_array_add(env, entry);
	}
}

static void fcgi_cgi_child_start(fcgi_cgi_child *cld, const gchar *path) {
	int pipes_to[2] = {-1, -1}, pipes_from[2] = {-1, -1}, pipes_err[2] = {-1, -1};
	pid_t pid;

	if (-1 == pipe(pipes_to)) {
		ERROR("couldn't create pipe: %s\n", g_strerror(errno));
		goto error;
	}

	if (-1 == pipe(pipes_from)) {
		ERROR("couldn't create pipe: %s\n", g_strerror(errno));
		goto error;
	}

	if (-1 == pipe(pipes_err)) {
		ERROR("couldn't create pipe: %s\n", g_strerror(errno));
		goto error;
	}

	pid = fork();
	switch (pid) {
	case 0: {
		GPtrArray *enva = cld->fcon->environ;
		char **newenv;
		char *const args[] = { (char *) path, NULL };

		close(pipes_to[1]); close(pipes_from[0]); close(pipes_err[0]);
		move2fd(pipes_to[0], 0);
		move2fd(pipes_from[1], 1);
		move2fd(pipes_err[1], 2);

		/* try changing the directory. don't care about memleaks, execve() coming soon :) */
		{
			char *dir = strdup(path), *sep;
			if (NULL == (sep = strrchr(dir, '/'))) {
				chdir("/");
			} else {
				*sep = '\0';
				chdir(dir);
			}
		}

		copy_env_var(enva, "PATH");
		g_ptr_array_add(enva, NULL);
		newenv = (char**) g_ptr_array_free(enva, FALSE);
		execve(path, args, newenv);

		ERROR("couldn't execve '%s': %s\n", path, g_strerror(errno));
		exit(-1);

		}
		break;
	case -1:
		ERROR("couldn't fork: %s\n", g_strerror(errno));
		goto error;
	default:
		cld->pid = pid;
		ev_child_set(&cld->child_watcher, cld->pid, 0);
		ev_child_start(cld->srv->loop, &cld->child_watcher);
		close(pipes_to[0]); close(pipes_from[1]); close(pipes_err[1]);
		fd_init(pipes_to[1]); fd_init(pipes_from[0]); fd_init(pipes_err[0]);
		cld->pipe_out = pipes_to[1];
		cld->pipe_in = pipes_from[0];
		cld->pipe_err = pipes_err[0];
		ev_io_set(&cld->pipe_out_watcher, cld->pipe_out, EV_WRITE);
		ev_io_set(&cld->pipe_in_watcher, cld->pipe_in, EV_READ);
		ev_io_set(&cld->pipe_err_watcher, cld->pipe_err, EV_READ);
		if (cld->write_queue.length > 0) ev_io_start(cld->srv->loop, &cld->pipe_out_watcher);
		ev_io_start(cld->srv->loop, &cld->pipe_in_watcher);
		ev_io_start(cld->srv->loop, &cld->pipe_err_watcher);
		break;
	}

	return;
error:
	close(pipes_to[0]); close(pipes_to[1]);
	close(pipes_from[0]); close(pipes_from[1]);
	close(pipes_err[0]); close(pipes_err[1]);
	fcgi_cgi_child_error(cld);
}

static void fcgi_cgi_direct_result(fastcgi_connection *fcon, int status) {
	GString *s = g_string_new(0);
	g_string_append_printf(s, "Status: %i\r\n\r\n", status);
	fastcgi_send_out(fcon, s);
	fastcgi_send_out(fcon, NULL);
	fastcgi_send_err(fcon, NULL);
	fastcgi_end_request(fcon, 0, FCGI_REQUEST_COMPLETE);
}

static void fcgi_cgi_new_request(fastcgi_connection *fcon) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) fcon->data;
	const gchar *binpath;
	struct stat st;
	if (cld) return;

	binpath = fastcgi_connection_environ_lookup(fcon, CONST_STR_LEN("INTERPRETER"));
	if (!binpath) binpath = fastcgi_connection_environ_lookup(fcon, CONST_STR_LEN("SCRIPT_FILENAME"));

	if (!binpath) {
		fcgi_cgi_direct_result(fcon, 500);
		return;
	}

	if (-1 == stat(binpath, &st)) {
		switch (errno) {
		case EACCES:
		case ENOTDIR:
			fcgi_cgi_direct_result(fcon, 403);
			break;
		case ENOENT:
			fcgi_cgi_direct_result(fcon, 404);
			break;
		default:
			fcgi_cgi_direct_result(fcon, 500);
			break;
		}
		return;
	}

	if (!S_ISREG(st.st_mode) || !((S_IXUSR | S_IXGRP | S_IXOTH) && st.st_mode)) {
		fcgi_cgi_direct_result(fcon, 403);
		return;
	}

	cld = fcgi_cgi_child_create(fcon->fsrv->data, fcon);
	fcon->data = cld;

	fcgi_cgi_child_start(cld, binpath);
}

static void fcgi_cgi_wrote_data(fastcgi_connection *fcon) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) fcon->data;
	if (!cld) return;
	if (cld->fcon->write_queue.length < MAX_BUFFER_SIZE) {
		if (-1 != cld->pipe_in) ev_io_start(cld->srv->loop, &cld->pipe_in_watcher);
		if (-1 != cld->pipe_err) ev_io_start(cld->srv->loop, &cld->pipe_err_watcher);
	} else {
		if (-1 != cld->pipe_in) ev_io_stop(cld->srv->loop, &cld->pipe_in_watcher);
		if (-1 != cld->pipe_err) ev_io_stop(cld->srv->loop, &cld->pipe_err_watcher);
	}
}

static void fcgi_cgi_received_stdin(fastcgi_connection *fcon, GByteArray *data) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) fcon->data;
	/* if proc is running but pipe closed -> drop data */
	if (!cld || cld->write_queue.closed) {
		if (data) g_byte_array_free(data, TRUE);
		return;
	}
	if (NULL != data) {
		fastcgi_queue_append_bytearray(&cld->write_queue, data);
	} else {
		cld->write_queue.closed = TRUE;
	}
	write_queue(cld); /* if we don't call this we have to check the write-queue length */
}

static void fcgi_cgi_request_aborted(fastcgi_connection *fcon) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) fcon->data;
	if (!cld) return;
	fcgi_cgi_child_close_write(cld);
}

static void fcgi_cgi_reset_connection(fastcgi_connection *fcon) {
	fcgi_cgi_child *cld = (fcgi_cgi_child*) fcon->data;
	if (!cld) return;
	fcon->data = NULL;
	cld->fcon = NULL;
	if (cld->pid == -1 || (cld->pipe_out == -1 && cld->pipe_in == -1)) {
		fcgi_cgi_child_free(cld);
	} else {
		fcgi_cgi_child_close_write(cld);
		if (-1 != cld->pipe_in) ev_io_start(cld->srv->loop, &cld->pipe_in_watcher);
		if (-1 != cld->pipe_err) ev_io_start(cld->srv->loop, &cld->pipe_err_watcher);
		cld->aborted_id = cld->srv->aborted_pending_childs->len;
		g_ptr_array_add(cld->srv->aborted_pending_childs, cld);
	}
}

static const fastcgi_callbacks cgi_callbacks = {
	/* cb_new_connection: */ NULL,
	/* cb_new_request: */ fcgi_cgi_new_request,
	/* cb_wrote_data: */ fcgi_cgi_wrote_data,
	/* cb_received_stdin: */ fcgi_cgi_received_stdin,
	/* cb_received_data: */ NULL,
	/* cb_request_aborted: */ fcgi_cgi_request_aborted,
	/* cb_reset_connection: */ fcgi_cgi_reset_connection
};

static fcgi_cgi_server* fcgi_cgi_server_create(struct ev_loop *loop, int fd, guint maxconns) {
	fcgi_cgi_server* srv = g_slice_new0(fcgi_cgi_server);
	srv->loop = loop;
	srv->aborted_pending_childs = g_ptr_array_new();
	srv->fsrv = fastcgi_server_create(loop, fd, &cgi_callbacks, maxconns);
	srv->fsrv->data = srv;
	return srv;
}

static void fcgi_cgi_server_free(fcgi_cgi_server* srv) {
	guint i;
	for (i = 0; i < srv->aborted_pending_childs->len; i++) {
		fcgi_cgi_child_free(g_ptr_array_index(srv->aborted_pending_childs, i));
	}
	g_ptr_array_free(srv->aborted_pending_childs, TRUE);
	fastcgi_server_free(srv->fsrv);
	g_slice_free(fcgi_cgi_server, srv);
}

#define CATCH_SIGNAL(loop, cb, n) do {\
	ev_init(&srv->sig_w_##n, cb); \
	ev_signal_set(&srv->sig_w_##n, SIG##n); \
	ev_signal_start(loop, &srv->sig_w_##n); \
	srv->sig_w_##n.data = srv; \
	ev_unref(loop); /* Signal watchers shouldn't keep loop alive */ \
} while (0)

#define UNCATCH_SIGNAL(loop, n) do {\
	ev_ref(loop); \
	ev_signal_stop(loop, &srv->sig_w_##n); \
} while (0)

static void sigint_cb(struct ev_loop *loop, struct ev_signal *w, int revents) {
	fcgi_cgi_server *srv = (fcgi_cgi_server*) w->data;
	UNUSED(revents);

	if (!srv->fsrv->do_shutdown) {
		ERROR("Got signal, shutdown\n");
		fastcgi_server_stop(srv->fsrv);
	} else {
		ERROR("Got second signal, force shutdown\n");
		ev_unloop(loop, EVUNLOOP_ALL);
	}
}

typedef struct {
	gint maxconns;

	gboolean show_version;
} options;

static options opts = {
	/* maxconns:*/ 16,
	/* version: */ FALSE,
};

static const GOptionEntry entries[] = {
	{ "max-connections", 'c', 0, G_OPTION_ARG_INT, &opts.maxconns, "Maximum number of connections (default 16)", "number" },
	{ "version", 'v', 0, G_OPTION_ARG_NONE, &opts.show_version, "Show version", NULL },
	{ NULL, 0, 0, 0, NULL, NULL, NULL }
};

int main(int argc, char **argv) {
	GOptionContext *context;
	GError *error = NULL;
	struct ev_loop *loop;
	fcgi_cgi_server* srv;

	context = g_option_context_new("<application> [app arguments]");
	g_option_context_add_main_entries(context, entries, NULL);
	g_option_context_set_summary(context, PACKAGE_DESC);

	if (!g_option_context_parse (context, &argc, &argv, &error)) {
		g_printerr("Option parsing failed: %s\n", error->message);
		return -1;
	}

	if (opts.show_version) {
		g_printerr(PACKAGE_DESC);
		g_printerr("\nBuild-Date: " __DATE__ " " __TIME__ "\n");
		return 0;
	}

	loop = ev_default_loop(0);
	srv = fcgi_cgi_server_create(loop, 0, opts.maxconns);

	signal(SIGPIPE, SIG_IGN);
	CATCH_SIGNAL(loop, sigint_cb, INT);
	CATCH_SIGNAL(loop, sigint_cb, TERM);
	CATCH_SIGNAL(loop, sigint_cb, HUP);

	ev_loop(loop, 0);
	fcgi_cgi_server_free(srv);
	return 0;
}
