#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "session.h"

#include "xwrap.h"
#include "socktool.h"
#include "connection.h"
#include "server.h"
#include "shell.h"
#include "basic-array.h"

#include <fcntl.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifdef OS_IS_BSD /* need ioctl(2) */
# include <sys/ioctl.h>
#endif

int ras_session_init (RasSession* session, RasServer* server, int fd, int id) {
	/* chain up to parent constructor */
	int rval = ras_conn_init (
			RAS_CONN (session),
			RAS_CONN (server)->perm,
			RAS_CONN (server)->domain,
			"ras-session",
			RAS_CONN (server)->log_file,
			id);

	if (rval < 0) {
		return rval;
	}

	RAS_CONN (session)->fd = fd;
	RAS_CONN (session)->fd_is_set = true;

	ras_socktool_buffer_clear (&session->buffer, true);
	session->shell_pid = 0;
	session->shell_stdio = -1;
	session->shell_use_pty = true;

	ras_session_log (session, "session started");

	return 0;
}

void ras_session_destroy (RasSession* session) {
	ras_session_log (session, "session closed");
	ras_socktool_buffer_clear (&session->buffer, false);

	/* chain up to parent destructor */
	ras_conn_destroy (RAS_CONN (session));
}

char* ras_session_getline (RasSession* session, int* len) {
	char* newline;
	session->buffer.buf_error = false;
	session->buffer.buf_eof = false;
	while ((newline = ras_socktool_getline (
			RAS_CONN (session)->fd, &session->buffer, '\n', len)) == NULL &&
			!(session->buffer.buf_error) && !(session->buffer.buf_eof));
	return newline;
}

int ras_session_read_header (RasSession* session) {
	int onelinelen;
	char* oneline = ras_session_getline (session, &onelinelen);
	char* space;
	char* next;

	ras_socktool_buffer_free_line (&session->buffer);
	if (session->buffer.buf_error) {
		free (oneline);
		ras_session_log (session, "read error: %s", strerror (errno));
		return -1;
	}
	if (session->buffer.buf_eof) {
		free (oneline);
		ras_session_log (session, "connection closed by peer");
		return -1;
	}

	const char usage_msg[] =
		"Usage: LOGIN    [USERNAME] (login without allocating a pseudo-tty)\n"
		"  or:  LOGINTTY [USERNAME] (login and allocate a pseudo-tty)\n"
		"If USERNAME is not specified, system default will be used.\n";
	const int usage_len = STATIC_STRLEN (usage_msg);

	space = strchr (oneline, ' ');
	if (space == NULL) {
		next = oneline + onelinelen;
	} else {
		*space = '\0';
		for (space++; *space == '\0' && *space == ' '; space++);
		next = space;
	}

	if (strcmp (oneline, "LOGIN") == 0) {
		session->shell_use_pty = false;
	} else if (strcmp (oneline, "LOGINTTY") == 0) {
		session->shell_use_pty = true;
	} else {
		const char badcmd_msg[] = "Error: Unknown login command.\n";
		const int badcmd_len = STATIC_STRLEN (badcmd_msg);
		ras_socktool_write_string (RAS_CONN (session)->fd, badcmd_msg, badcmd_len);
		ras_socktool_write_string (RAS_CONN (session)->fd, usage_msg, usage_len);
		ras_session_log (session, "command %s is not understood", oneline);
		free (oneline);
		return -1;
	}

	struct passwd* login_user = NULL;
	if (*next != '\0') {
		login_user = getpwnam (next);
		if (login_user == NULL) {
			const char baduser_msg[] = "Error: User not found.\n";
			const int baduser_len = STATIC_STRLEN (baduser_msg);
			ras_socktool_write_string (RAS_CONN (session)->fd, baduser_msg, baduser_len);
			ras_session_log (session, "user %s cannot be found", next);
			free (oneline);
			return -1;
		}
	}

	free (oneline);

	if (RAS_CONN (session)->perm == RAS_CONN_PERM_RESTRICTED) {
		uid_t real_uid = getuid ();
		if (login_user != NULL && login_user->pw_uid != real_uid) {
			const char rest_msg[] = "Error: You are not permitted to login from here.\n";
			const int rest_len = STATIC_STRLEN (rest_msg);
			ras_socktool_write_string (RAS_CONN (session)->fd, rest_msg, rest_len);
			ras_session_log (session, "user %s is not permitted", login_user->pw_name);
			return -1;
		}
		setuid (real_uid);
	} else if (login_user != NULL) {
		const char sufail_msg[] = "Error: Unable to change the user credential.\n";
		const int sufail_len = STATIC_STRLEN (sufail_msg);
		int rval;

#if defined(HAVE_INITGROUPS)
		rval = initgroups (login_user->pw_name, login_user->pw_gid);
#elif defined(HAVE_SETGROUPS)
		struct group* groupentry;
		Array* grouplist = array_create (sizeof (gid_t), 1);
		int errno_saved;

		array_pushback (grouplist, &login_user->pw_gid);
		setgrent ();
		while ((groupentry = getgrent ()) != NULL) {
			for (int i = 0; groupentry->gr_mem[i] != NULL; i++) {
				if (strcmp (groupentry->gr_mem[i], login_user->pw_name) == 0) {
					array_pushback (grouplist, &groupentry->gr_gid);
				}
			}
		}
		endgrent ();

		rval = setgroups (array_getlen (grouplist), array_data (grouplist));

		errno_saved = errno;
		array_free (grouplist);
		errno = errno_saved;
#endif

		if (rval < 0) {
			write (RAS_CONN (session)->fd, sufail_msg, sufail_len);
			ras_session_log (session,
					"cannot set supplementary groups: %s", strerror (errno));
			return -1;
		}

		rval = setgid (login_user->pw_gid);
		if (rval < 0) {
			write (RAS_CONN (session)->fd, sufail_msg, sufail_len);
			ras_session_log (session,
					"cannot set primary group to %d: %s",
					login_user->pw_gid, strerror (errno));
			return -1;
		}

		rval = setuid (login_user->pw_uid);
		if (rval < 0) {
			write (RAS_CONN (session)->fd, sufail_msg, sufail_len);
			ras_session_log (session,
					"cannot set user to %s (uid = %d): %s",
					login_user->pw_name, login_user->pw_uid, strerror (errno));
			return -1;
		}
	} else {
		setuid (getuid ());
	}

	return 0;
}

static volatile sig_atomic_t child_terminated;
static pid_t child_pid;
static void session_child_terminated_setter (int signo) {
	int status;
	if (waitpid (child_pid, &status, WUNTRACED | WNOHANG) < 0) {
		return;
	}
	if (WIFEXITED (status) || WIFSIGNALED (status)) {
		child_terminated = true;
	} else if (WIFSTOPPED (status)) {
		kill (child_pid, SIGCONT);
	}
}

static int session_exchange_data_prog_cb (int fd[2], RasBuffer buf[2]) {
	return !child_terminated;
}

int ras_session_start_shell (RasSession* session) {
	const char noshell_msg[] = "Error: Unable to start shell.\n";
	const int noshell_len = STATIC_STRLEN (noshell_msg);
	const char nopty_msg[] = "Error: Cannot allocate a pseudo-tty.\n";
	const int nopty_len = STATIC_STRLEN (nopty_msg);
	const char nosid_msg[] = "Error: Cannot start a new session.\n";
	const int nosid_len = STATIC_STRLEN (nosid_msg);
	int sockpair[2]; /* parent = 0, child = 1 */
	char* slave_dev = NULL;

	child_terminated = false;
	child_pid = -1;
	struct sigaction session_action = {
		.sa_handler = session_child_terminated_setter,
		.sa_flags = 0
	};
	sigaction (SIGCHLD, &session_action, NULL);

	if (session->shell_use_pty) {
		session->shell_stdio = posix_openpt (O_RDWR | O_NOCTTY);
		if (session->shell_stdio < 0) {
			write (RAS_CONN (session)->fd, nopty_msg, nopty_len);
			ras_session_log (session, "cannot posix_openpt: %s", strerror (errno));
			return -1;
		}
		if (grantpt (session->shell_stdio) < 0) {
			write (RAS_CONN (session)->fd, nopty_msg, nopty_len);
			ras_session_log (session, "cannot grantpt: %s", strerror (errno));
			close (session->shell_stdio);
			return -1;
		}
		if (unlockpt (session->shell_stdio) < 0) {
			write (RAS_CONN (session)->fd, nopty_msg, nopty_len);
			ras_session_log (session, "cannot unlockpt: %s", strerror (errno));
			close (session->shell_stdio);
			return -1;
		}
		if ((slave_dev = ptsname (session->shell_stdio)) == NULL) {
			write (RAS_CONN (session)->fd, nopty_msg, nopty_len);
			ras_session_log (session, "cannot ptsname: %s", strerror (errno));
			close (session->shell_stdio);
			return -1;
		}
		ras_session_log (session, "new pseudo-tty is %s", slave_dev);
	} else {
		if (socketpair (AF_UNIX, SOCK_STREAM, 0, sockpair) < 0) {
			write (RAS_CONN (session)->fd, noshell_msg, noshell_len);
			ras_session_log (session, "cannot socketpair: %s", strerror (errno));
			return -1;
		}
		session->shell_stdio = sockpair[0];
	}

	fflush (stdout);
	fflush (stderr);

	session->shell_pid = fork ();
	if (session->shell_pid < 0) {
		write (RAS_CONN (session)->fd, noshell_msg, noshell_len);
		ras_session_log (session, "cannot fork to run shell: %s", strerror (errno));
		return -1;
	} else if (session->shell_pid > 0) {
		/* parent process */

		child_pid = session->shell_pid;
		ras_session_log (session, "child started with pid %d", session->shell_pid);

		int exfd[2] = { session->shell_stdio, RAS_CONN (session)->fd };
		ras_session_log (session, "shell has fd %d, session has fd %d",
			exfd[0], exfd[1]);
		ras_socktool_exchange_data (exfd, session_exchange_data_prog_cb);
	} else {
		/* child process */
		int child_stdio;

		if (setsid () < 0) {
			write (RAS_CONN (session)->fd, nosid_msg, nosid_len);
			ras_session_log (session, "cannot setsid: %s", strerror (errno));
		}

		if (slave_dev != NULL) {
			child_stdio = open (slave_dev, O_RDWR);
			if (child_stdio < 0) {
				write (RAS_CONN (session)->fd, nopty_msg, nopty_len);
				ras_session_log (session, "cannot open slave pty: %s", strerror (errno));
				exit (1);
			}
#ifdef OS_IS_BSD
			if (ioctl (child_stdio, TIOCSCTTY, NULL) < 0) {
				write (RAS_CONN (session)->fd, nopty_msg, nopty_len);
				ras_session_log (session, "cannot set controlling terminal: %s", strerror (errno));
				exit (1);
			}
#endif
		} else {
			child_stdio = sockpair[1];
		}

		ras_session_log (session, "shell environment is OK, closing log file");

		ras_socktool_reset_signals ();

		ras_socktool_close_except (child_stdio);
		dup2 (child_stdio, STDIN_FILENO);
		dup2 (child_stdio, STDOUT_FILENO);
		dup2 (child_stdio, STDERR_FILENO);
		close (child_stdio);

		if (RAS_CONN (session)->perm == RAS_CONN_PERM_RESTRICTED) {
			char* argv[] = { "-ras-shell", "-r", NULL };
			exit (ras_shell_main (2, argv));
		} else {
			char* argv[] = { "-ras-shell", NULL };
			exit (ras_shell_main (1, argv));
		}
	}

	return 0;
}