/*
 * Copyright (c) 2007-2013 Grzegorz Nosek
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#define NO_FCGI_DEFINES

#include "config.h"
#include <stdarg.h>
#include <fcgi_stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#include <stdbool.h>
#include <sys/wait.h>
#include <ctype.h>

#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>

#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
#endif

/* glibc doesn't seem to export it */
#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 108
#endif

extern char **environ;
static char * const * inherited_environ;

static const char * blacklisted_env_vars[] = {
	"AUTH_TYPE",
	"CONTENT_LENGTH",
	"CONTENT_TYPE",
	"GATEWAY_INTERFACE",
	"PATH_INFO",
	"PATH_TRANSLATED",
	"QUERY_STRING",
	"REMOTE_ADDR",
	"REMOTE_HOST",
	"REMOTE_IDENT",
	"REMOTE_USER",
	"REQUEST_METHOD",
	"SCRIPT_NAME",
	"SERVER_NAME",
	"SERVER_PORT",
	"SERVER_PROTOCOL",
	"SERVER_SOFTWARE",
	NULL,
};

static int stderr_to_fastcgi = 0;


#define FCGI_BUF_SIZE 4096

static int write_all(int fd, char *buf, size_t size)
{
	size_t nleft = size;
	while (nleft > 0) {
		ssize_t nwritten = write(fd, buf, nleft);
		if (nwritten < 0)
			return nleft - size; /* zero or negative to indicate error */

		buf += nwritten;
		nleft -= nwritten;
	}

	return size;
}

#define MAX_VA_SENTINEL INT_MIN

static int max_va(int p1, ...)
{
	va_list va;
	int max = p1;
	int p;

	va_start(va, p1);
	do {
		p = va_arg(va, int);
		if (p > max)
			max = p;
	} while (p != MAX_VA_SENTINEL);
	va_end(va);

	return max;
}

enum reply_state_t {
	REPLY_STATE_INIT,
	REPLY_STATE_HEADER,
	REPLY_STATE_CR,
	REPLY_STATE_LF,
	REPLY_STATE_2CR,
	REPLY_STATE_2LF,
	REPLY_STATE_BODY,
	REPLY_STATE_MAX
};

enum char_class_t {
	CC_NORMAL,
	CC_CR,
	CC_LF,
	CC_MAX
};

#define ACTION_MASK	(15 << 4)
#define ACTION_EMIT	0
#define ACTION_ERROR	(1 << 4)
#define ACTION_END	(2 << 4)
#define ACTION_SKIP	(3 << 4)
#define ACTION_EXTRA_CR	(4 << 4)
#define ACTION_EXTRA_LF	(5 << 4)

static const unsigned char header_state_machine[REPLY_STATE_MAX][CC_MAX] = {
	[REPLY_STATE_INIT] = {
		[CC_NORMAL] = REPLY_STATE_HEADER,
		[CC_CR] = ACTION_ERROR,
		[CC_LF] = ACTION_ERROR,
	},
	[REPLY_STATE_HEADER] = {
		[CC_NORMAL] = REPLY_STATE_HEADER,
		[CC_CR] = REPLY_STATE_CR,
		[CC_LF] = REPLY_STATE_LF | ACTION_EXTRA_CR,
	},
	[REPLY_STATE_CR] = {
		[CC_NORMAL] = REPLY_STATE_HEADER | ACTION_EXTRA_LF,
		[CC_CR] = REPLY_STATE_CR | ACTION_SKIP,
		[CC_LF] = REPLY_STATE_LF,
	},
	[REPLY_STATE_LF] = {
		[CC_NORMAL] = REPLY_STATE_HEADER,
		[CC_CR] = REPLY_STATE_2CR,
		[CC_LF] = REPLY_STATE_2LF | ACTION_EXTRA_CR,
	},
	[REPLY_STATE_2CR] = {
		[CC_NORMAL] = REPLY_STATE_BODY | ACTION_EXTRA_LF,
		[CC_CR] = REPLY_STATE_CR | ACTION_SKIP,
		[CC_LF] = REPLY_STATE_2LF,
	},
	[REPLY_STATE_2LF] = {
		[CC_NORMAL] = REPLY_STATE_BODY | ACTION_END,
		[CC_CR] = REPLY_STATE_BODY | ACTION_END,
		[CC_LF] = REPLY_STATE_BODY | ACTION_END,
	},
	[REPLY_STATE_BODY] = {
		[CC_NORMAL] = REPLY_STATE_BODY | ACTION_END,
		[CC_CR] = REPLY_STATE_BODY | ACTION_END,
		[CC_LF] = REPLY_STATE_BODY | ACTION_END,
	},
};

struct fcgi_context {
	int fd_stdin;
	int fd_stdout;
	int fd_stderr;
	unsigned int reply_state;
	pid_t cgi_pid;
};

static void fcgi_finish(struct fcgi_context *fc, const char* msg)
{
	if (fc->reply_state == REPLY_STATE_INIT) {
		FCGI_puts("Status: 502 Bad Gateway\nContent-type: text/plain\n");
		FCGI_printf("An error occurred while %s\n", msg);
	}

	if (fc->fd_stdin >= 0) close(fc->fd_stdin);
	if (fc->fd_stdout >= 0) close(fc->fd_stdout);
	if (fc->fd_stderr >= 0) close(fc->fd_stderr);

	if (fc->cgi_pid)
		kill(SIGTERM, fc->cgi_pid);
}

static const char * fcgi_pass_fd(struct fcgi_context *fc, int *fdp, FCGI_FILE *ffp, char *buf, size_t bufsize)
{
	ssize_t nread;
	char *p = buf;
	unsigned char cclass, next_state;

	nread = read(*fdp, buf, bufsize);
	if (nread > 0) {
		while (p < buf + nread) {
			if (*p == '\r') {
				cclass = CC_CR;
			} else if (*p == '\n') {
				cclass = CC_LF;
			} else {
				cclass = CC_NORMAL;
			}
			next_state = header_state_machine[fc->reply_state][cclass];
			fc->reply_state = next_state & ~ACTION_MASK;
			switch(next_state & ACTION_MASK) {
				case ACTION_ERROR:
					return "parsing CGI reply";

				case ACTION_END:
					goto out_of_loop;

				case ACTION_SKIP:
					goto next_char;

				case ACTION_EXTRA_CR:
					if (FCGI_fputc('\r', ffp) == EOF) return "writing CGI reply";
					break;

				case ACTION_EXTRA_LF:
					if (FCGI_fputc('\n', ffp) == EOF) return "writing CGI reply";
					break;
			}
			if (FCGI_fputc(*p, ffp) == EOF) {
				return "writing CGI reply";
			}
next_char:
			p++;
		}
out_of_loop:
		if (p < buf + nread) {
			if (FCGI_fwrite(p, 1, buf + nread - p, ffp) != (size_t)(buf + nread - p)) {
				return "writing CGI reply";
			}
		}
	} else {
		if (nread < 0) {
			return "reading CGI reply";
		}
		close(*fdp);
		*fdp = -1;
	}

	return NULL;
}

static const char * fcgi_pass_raw_fd(int *fdp, int fd_out, char *buf, size_t bufsize)
{
	ssize_t nread;

	nread = read(*fdp, buf, bufsize);
	if (nread > 0) {
		if (write_all(fd_out, buf, nread) != nread) {
			return "writing CGI reply";
		}
	} else {
		if (nread < 0) {
			return "reading CGI reply";
		}
		close(*fdp);
		*fdp = -1;
	}
	return NULL;
}

static bool fcgi_pass_request(struct fcgi_context *fc)
{
	char buf[FCGI_BUF_SIZE];
	ssize_t nread;

	/* eat the whole request and pass it to CGI */
	while ((nread = FCGI_fread(buf, 1, sizeof(buf), FCGI_stdin)) > 0) {
		if (write_all(fc->fd_stdin, buf, nread) <= 0) {
			fcgi_finish(fc, "reading the request");
			return false;
		}
	}
	close(fc->fd_stdin);
	fc->fd_stdin = -1;

	return true;
}

static void fcgi_pass(struct fcgi_context *fc)
{
	char buf[FCGI_BUF_SIZE];
	fd_set rset;
	int maxfd = 1 + max_va(fc->fd_stdout, fc->fd_stderr, MAX_VA_SENTINEL);
	int nready;
	const char *err;

	if (!fcgi_pass_request(fc))
		return;

	/* now pass CGI reply back */
	while (fc->fd_stdout >= 0 || fc->fd_stderr >= 0) {
		FD_ZERO(&rset);
		if (fc->fd_stdout >= 0) FD_SET(fc->fd_stdout, &rset);
		if (fc->fd_stderr >= 0) FD_SET(fc->fd_stderr, &rset);
		nready = select(maxfd, &rset, NULL, NULL, NULL);
		if (nready < 0) {
			if (errno == EAGAIN) continue;
			fcgi_finish(fc, "waiting for CGI reply");
			return;
		}
		if (fc->fd_stdout >= 0 && FD_ISSET(fc->fd_stdout, &rset)) {
			err = fcgi_pass_fd(fc, &fc->fd_stdout, FCGI_stdout, buf, sizeof(buf));
			if (err) {
				fcgi_finish(fc, err);
				return;
			}
		}
		if (fc->fd_stderr >= 0 && FD_ISSET(fc->fd_stderr, &rset)) {
			if (stderr_to_fastcgi)
				err = fcgi_pass_fd(fc, &fc->fd_stderr, FCGI_stderr, buf, sizeof(buf));
			else
				err = fcgi_pass_raw_fd(&fc->fd_stderr, 2, buf, sizeof(buf));
			if (err) {
				fcgi_finish(fc, err);
				return;
			}
		}
	}

	fc->cgi_pid = 0;

	fcgi_finish(fc, "reading CGI reply (no response received)");
}

static int check_file_perms(const char *path)
{
	struct stat ls;
	struct stat fs;

	if (lstat(path, &ls) < 0) {
		return -ENOENT;
	} else if (S_ISREG(ls.st_mode)) {
		if (ls.st_mode & S_IXUSR) {
			return 0;
		} else {
			return -EACCES;
		}
	} else if (!S_ISLNK(ls.st_mode)) {
		return -EACCES;
	}

	if (stat(path, &fs) < 0) {
		return -ENOENT;
	} else if (S_ISREG(fs.st_mode)) {
		if (fs.st_mode & S_IXUSR) {
			return 0;
		} else {
			return -EACCES;
		}
	} else {
		return -EACCES;
	}
}

static char *get_cgi_filename(void) /* and fixup environment */
{
	int buflen = 1, docrootlen;
	char *buf = NULL;
	char *docroot, *scriptname, *p;

	int rf_len;
	char *pathinfo = NULL;

	if ((p = getenv("SCRIPT_FILENAME"))) {
		if (check_file_perms(p) != 0)
			goto err;
		return strdup(p);
	}

	if ((p = getenv("DOCUMENT_ROOT"))) {
		docroot = p;
		docrootlen = strlen(p);
		buflen += docrootlen;
	} else {
		goto err;
	}

	if ((p = getenv("SCRIPT_NAME"))) {
		buflen += strlen(p);
		scriptname = p;
	} else {
		goto err;
	}

	buf = malloc(buflen);
	if (!buf) goto err;

	strcpy(buf, docroot);
	strcpy(buf + docrootlen, scriptname);
	pathinfo = strdup(buf);
	if (!pathinfo) {
		goto err;
	}

	while(1) {
		switch(check_file_perms(buf)) {
			case -EACCES:
				goto err;
			case 0:
				rf_len = strlen(buf);
				if (rf_len < buflen - 1) {
					setenv("PATH_INFO", pathinfo + rf_len, 1);
					setenv("SCRIPT_NAME", buf + docrootlen, 1);
				} else {
					unsetenv("PATH_INFO");
				}
				free(pathinfo);
				return buf;
			default:
				p = strrchr(buf, '/');
				if (!p) goto err;
				*p = 0;
		}
	}

err:
	free(pathinfo);
	free(buf);
	return NULL;
}

static int blacklisted_env(const char *var_name, const char *var_name_end)
{
	const char **p;

	if (var_name_end - var_name > 4 && !strncmp(var_name, "HTTP", 4)) {
		/* HTTP_*, HTTPS */
		return 1;
	}

	for (p = blacklisted_env_vars; *p; p++) {
		if (!strcmp(var_name, *p)) {
			return 1;
		}
	}

	return 0;
}

static void inherit_environment(void)
{
	char * const * p;
	char *q;

	for (p = inherited_environ; *p; p++) {
		q = strchr(*p, '=');
		if (!q) {
			fprintf(stderr, "Suspect value in environment: %s\n", *p);
			continue;
		}
		*q = 0;

		if (!getenv(*p) && !blacklisted_env(*p, q)) {
			*q = '=';
			putenv(*p);
		}

		*q = '=';
	}
}

static void cgi_error(const char *message, const char *reason, const char *filename)
{
	printf("Status: %s\r\nContent-Type: text/plain\r\n\r\n%s\r\n",
		message, message);
	fflush(stdout);
	if (filename) {
		fprintf(stderr, "%s (%s)\n", reason, filename);
	} else {
		fputs(reason, stderr);
		fputc('\n', stderr);
	}
	_exit(99);
}

static void handle_fcgi_request(void)
{
	int pipe_in[2];
	int pipe_out[2];
	int pipe_err[2];
	char *filename;
	char *last_slash;
	pid_t pid;

	struct fcgi_context fc;

	if (pipe(pipe_in) < 0) goto err_pipein;
	if (pipe(pipe_out) < 0) goto err_pipeout;
	if (pipe(pipe_err) < 0) goto err_pipeerr;

	switch((pid = fork())) {
		case -1:
			goto err_fork;

		case 0: /* child */
			close(pipe_in[1]);
			close(pipe_out[0]);
			close(pipe_err[0]);

			dup2(pipe_in[0], 0);
			dup2(pipe_out[1], 1);
			dup2(pipe_err[1], 2);

			close(pipe_in[0]);
			close(pipe_out[1]);
			close(pipe_err[1]);

			close(FCGI_fileno(FCGI_stdout));

			signal(SIGCHLD, SIG_DFL);
			signal(SIGPIPE, SIG_DFL);

			filename = get_cgi_filename();
			inherit_environment();
			if (!filename)
				cgi_error("403 Forbidden", "Cannot get script name, are DOCUMENT_ROOT and SCRIPT_NAME (or SCRIPT_FILENAME) set and is the script executable?", NULL);

			last_slash = strrchr(filename, '/');
			if (!last_slash)
				cgi_error("403 Forbidden", "Script name must be a fully qualified path", filename);

			*last_slash = 0;
			if (chdir(filename) < 0)
				cgi_error("403 Forbidden", "Cannot chdir to script directory", filename);

			*last_slash = '/';

			execl(filename, filename, (void *)NULL);
			cgi_error("502 Bad Gateway", "Cannot execute script", filename);

		default: /* parent */
			close(pipe_in[0]);
			close(pipe_out[1]);
			close(pipe_err[1]);

			fc.fd_stdin = pipe_in[1];
			fc.fd_stdout = pipe_out[0];
			fc.fd_stderr = pipe_err[0];
			fc.reply_state = REPLY_STATE_INIT;
			fc.cgi_pid = pid;

			fcgi_pass(&fc);
	}
	return;

err_fork:
	close(pipe_err[0]);
	close(pipe_err[1]);

err_pipeerr:
	close(pipe_out[0]);
	close(pipe_out[1]);

err_pipeout:
	close(pipe_in[0]);
	close(pipe_in[1]);

err_pipein:

	FCGI_puts("Status: 502 Bad Gateway\nContent-type: text/plain\n");
	FCGI_puts("System error");
}

static void fcgiwrap_main(void)
{
	signal(SIGCHLD, SIG_IGN);
	signal(SIGPIPE, SIG_IGN);

	inherited_environ = environ;

	while (FCGI_Accept() >= 0) {
		handle_fcgi_request();
	}
}

static volatile sig_atomic_t nrunning;

static void sigchld_handler(int dummy)
{
	int status;

	while ((dummy = waitpid(-1, &status, WNOHANG)) != -1) {
		/* sanity check */
		if (nrunning > 0)
			nrunning--;

		/* we _should_ print something about the exit code
		 * but the sighandler context is _very_ bad for this
		 */
	}
}

static void prefork(int nchildren)
{
	int startup = 1;

	if (nchildren == 1) {
		return;
	}

	signal(SIGCHLD, sigchld_handler);

	while (1) {
		while (nrunning < nchildren) {
			pid_t pid = fork();
			if (pid == 0) {
				return;
			} else if (pid != -1) {
				nrunning++;
			} else {
				if (startup) {
					fprintf(stderr, "Failed to prefork: %s\n", strerror(errno));
					exit(1);
				} else {
					fprintf(stderr, "Failed to fork: %s\n", strerror(errno));
					break;
				}
			}
		}
		startup = 0;
		pause();
	}
}

static int listen_on_fd(int fd) {
	int one = 1;

	if (listen(fd, 511) < 0) {
		perror("Failed to listen");
		return -1;
	}
	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one) < 0) {
		perror("Failed to enable SO_REUSEADDR");
		return -1;
	}
	if (dup2(fd, 0) < 0) {
		perror("Failed to move socket to fd 0");
		return -1;
	}
	if (close(fd) < 0) {
		perror("Failed to close original socket");
		return -1;
	}

	return 0;
}

static int setup_socket(char *url) {
	char *p = url;
	char *q;
	int fd;
	int port;
	size_t sockaddr_size;

	union {
		struct sockaddr sa;
		struct sockaddr_un sa_un;
		struct sockaddr_in sa_in;
		struct sockaddr_in6 sa_in6;
	} sa;

	if (!strncmp(p, "unix:", sizeof("unix:") - 1)) {
		p += sizeof("unix:") - 1;

		if (strlen(p) >= UNIX_PATH_MAX) {
			fprintf(stderr, "Socket path too long, exceeds %d characters\n",
			        UNIX_PATH_MAX);
			return -1;
		}

		sockaddr_size = sizeof sa.sa_un;
		sa.sa_un.sun_family = AF_UNIX;
		strcpy(sa.sa_un.sun_path, p);
	} else if (!strncmp(p, "tcp:", sizeof("tcp:") - 1)) {
		p += sizeof("tcp:") - 1;

		q = strchr(p, ':');
		if (!q) {
			goto invalid_url;
		}
		port = atoi(q+1);
		if (port <= 0 || port > 65535) {
			goto invalid_url;
		}
		sockaddr_size = sizeof sa.sa_in;
		sa.sa_in.sin_family = AF_INET;
		sa.sa_in.sin_port = htons(port);
		*q = 0;
		if (inet_pton(AF_INET, p, &sa.sa_in.sin_addr) < 1) {
			goto invalid_url;
		}
	} else if (!strncmp(p, "tcp6:[", sizeof("tcp6:[") - 1)) {
		p += sizeof("tcp6:[") - 1;
		q = strchr(p, ']');
		if (!q || !q[0] || q[1] != ':') {
			goto invalid_url;
		}
		port = atoi(q+2);
		if (port <= 0 || port > 65535) {
			goto invalid_url;
		}
		sockaddr_size = sizeof sa.sa_in6;
		sa.sa_in6.sin6_family = AF_INET6;
		sa.sa_in6.sin6_port = htons(port);
		*q = 0;
		if (inet_pton(AF_INET6, p, &sa.sa_in6.sin6_addr) < 1) {
			goto invalid_url;
		}
	} else {
invalid_url:
		fprintf(stderr, "Valid socket URLs are:\n"
		                "unix:/path/to/socket for Unix sockets\n"
		                "tcp:dot.ted.qu.ad:port for IPv4 sockets\n"
		                "tcp6:[ipv6_addr]:port for IPv6 sockets\n");
		return -1;
	}

	fd = socket(sa.sa.sa_family, SOCK_STREAM, 0);
	if (fd < 0) {
		perror("Failed to create socket");
		return -1;
	}
	if (bind(fd, &sa.sa, sockaddr_size) < 0) {
		perror("Failed to bind");
		return -1;
	}

	return listen_on_fd(fd);
}

int main(int argc, char **argv)
{
	int nchildren = 1;
	char *socket_url = NULL;
	int c;

	while ((c = getopt(argc, argv, "c:hfs:")) != -1) {
		switch (c) {
			case 'f':
				stderr_to_fastcgi++;
				break;
			case 'h':
				printf("Usage: %s [OPTION]\nInvokes CGI scripts as FCGI.\n\n"
					PACKAGE_NAME" version "PACKAGE_VERSION"\n\n"
					"Options are:\n"
					"  -f\t\t\tSend CGI's stderr over FastCGI\n"
					"  -c <number>\t\tNumber of processes to prefork\n"
					"  -s <socket_url>\tSocket to bind to (say -s help for help)\n"
					"  -h\t\t\tShow this help message and exit\n"
					"\nReport bugs to Grzegorz Nosek <"PACKAGE_BUGREPORT">.\n"
					PACKAGE_NAME" home page: <http://nginx.localdomain.pl/wiki/FcgiWrap>\n",
					argv[0]
				);
				return 0;
			case 'c':
				nchildren = atoi(optarg);
				break;
			case 's':
				socket_url = strdup(optarg);
				break;
			case '?':
				if (optopt == 'c' || optopt == 's')
					fprintf(stderr, "Option -%c requires an argument.\n", optopt);
				else if (isprint(optopt))
					fprintf(stderr, "Unknown option `-%c'.\n", optopt);
				else
					fprintf(stderr,
						"Unknown option character `\\x%x'.\n",
						optopt);
				return 1;
			default:
				abort();
		}
	}

#ifdef HAVE_SYSTEMD
	if (sd_listen_fds(true) > 0) {
		/* systemd woke us up. we should never see more than one FD passed to us. */
		if (listen_on_fd(SD_LISTEN_FDS_START) < 0) {
			return 1;
		}
	} else
#endif
	if (socket_url) {
		if (setup_socket(socket_url) < 0) {
			return 1;
		}
		free(socket_url);
	}

	prefork(nchildren);
	fcgiwrap_main();
	return 0;
}
