/*
 * Copyright (c) 2014, 2015, 2018, 2022, 2023 Jonas 'Sortie' Termansen.
 * Copyright (c) 2023 dzwdz.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * login.c
 * Authenticates users.
 */

#include <sys/termmode.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <ioleast.h>
#include <limits.h>
#include <locale.h>
#include <pwd.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

// TODO: The Sortix <limits.h> doesn't expose this at the moment.
#if !defined(HOST_NAME_MAX) && defined(__sortix__)
#include <sortix/limits.h>
#endif

#include "login.h"

static void on_interrupt_signal(int signum)
{
	if ( signum == SIGINT )
		dprintf(1, "^C");
	if ( signum == SIGQUIT )
		dprintf(1, "^\\");
}

bool check_real(const char* username, const char* password)
{
	char fakehashbuf[128];
	char goodhashbuf[128];
	size_t fakematch = 0;
	size_t goodmatch = 0;
	const char* fakehash = NULL;
	const char* goodhash = NULL;
	setpwent();
	struct passwd* pwd;
	while ( (errno = 0, pwd = getpwent()) )
	{
		if ( !strcmp(username, pwd->pw_name) )
		{
			strlcpy(goodhashbuf, pwd->pw_passwd, sizeof(goodhashbuf));
			goodhash = goodhashbuf;
			goodmatch++;
		}
		else
		{
			strlcpy(fakehashbuf, pwd->pw_passwd, sizeof(fakehashbuf));
			fakehash = fakehashbuf;
			fakematch++;
		}
	}
	int errnum = errno;
	endpwent();
	if ( errnum != 0 )
		return errno = errnum, false;
	if ( 1 < goodmatch )
		return errno = EACCES, false;
	errno = 0;
	(void) fakehash;
	return crypt_checkpass(password, goodhash) == 0;
}

bool check_begin(struct check* chk,
                 const char* username,
                 const char* password,
                 bool restrict_termmode)
{
	memset(chk, 0, sizeof(*chk));
	if ( tcgetattr(0, &chk->tio) )
		return false;
	int pipe_fds[2];
	if ( pipe2(pipe_fds, O_CLOEXEC) < 0 )
		return false;
	sigset_t sigttou;
	sigemptyset(&sigttou);
	sigaddset(&sigttou, SIGTTOU);
	sigprocmask(SIG_BLOCK, &sigttou, &chk->oldset);
	if ( (chk->pid = fork()) < 0 )
		return close(pipe_fds[0]), close(pipe_fds[1]), false;
	int success = -2;
	if ( chk->pid == 0 )
	{
		sigdelset(&chk->oldset, SIGINT);
		sigdelset(&chk->oldset, SIGQUIT);
		signal(SIGINT, SIG_DFL);
		signal(SIGQUIT, SIG_DFL);
		unsigned int termmode = TERMMODE_UNICODE | TERMMODE_SIGNAL |
		                        TERMMODE_UTF8 | TERMMODE_LINEBUFFER |
		                        TERMMODE_ECHO;
		if ( restrict_termmode )
			termmode = TERMMODE_SIGNAL;
		if ( setpgid(0, 0) < 0 ||
		     close(pipe_fds[0]) < 0 ||
		     tcsetpgrp(0, getpgid(0)) ||
		     sigprocmask(SIG_SETMASK, &chk->oldset, NULL) < 0 ||
		     settermmode(0, termmode) < 0 ||
		     !check_real(username, password) ||
		     write(pipe_fds[1], &success, sizeof(success)) < 0 )
		{
			assert(1 <= errno);
			write(pipe_fds[1], &errno, sizeof(errno));
		}
		_exit(0);
	}
	close(pipe_fds[1]);
	chk->pipe = pipe_fds[0];
	return true;
}

bool check_end(struct check* chk, bool* result, bool try)
{
	if ( try && !chk->pipe_nonblock )
	{
		fcntl(chk->pipe, F_SETFL, fcntl(chk->pipe, F_GETFL) | O_NONBLOCK);
		chk->pipe_nonblock = true;
	}
	while ( chk->errnum_done < sizeof(chk->errnum_bytes) )
	{
		ssize_t amount = read(chk->pipe, chk->errnum_bytes + chk->errnum_done,
		                      sizeof(chk->errnum_bytes) - chk->errnum_done);
		if ( amount <= 0 )
		{
			if ( amount == 0 )
				errno = EOF;
			break;
		}
		chk->errnum_done += amount;
	}
	int code;
	pid_t wait_ret = waitpid(chk->pid, &code, try ? WNOHANG : 0);
	if ( try && wait_ret == 0 )
		return false;
	tcsetattr(0, TCSAFLUSH, &chk->tio);
	tcsetpgrp(0, getpgid(0));
	sigprocmask(SIG_SETMASK, &chk->oldset, NULL);
	if ( wait_ret < 0 )
		return *result = false, true;
	if ( chk->errnum_done < sizeof(chk->errnum_bytes) )
		chk->errnum = EEOF;
	if ( WIFSIGNALED(code) )
		chk->errnum = EINTR;
	else if ( !(WIFEXITED(code) && WEXITSTATUS(code) == 0) )
		chk->errnum = EINVAL;
	int success = -2;
	if ( chk->errnum < 1 && chk->errnum != success )
		chk->errnum = EINVAL;
	if ( chk->errnum != success )
		return errno = chk->errnum, *result = false, true;
	return *result = true, true;
}

bool check(const char* username, const char* password)
{
	struct check chk;
	if ( !check_begin(&chk, username, password, false) )
		return false;
	bool result;
	check_end(&chk, &result, false);
	return result;
}

static int setcloexecfrom(int from)
{
	int fd = from - 1;
	while ( (fd = fcntl(fd, F_NEXTFD)) != -1 )
	{
		int flags = fcntl(fd, F_GETFD);
		if ( flags < 0 )
			return -1;
		if ( !(flags & FD_CLOEXEC) && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0 )
			return -1;
	}
	return 0;
}

bool login(const char* username, const char* session)
{
	char login_pid[sizeof(pid_t) * 3];
	snprintf(login_pid, sizeof(login_pid), "%" PRIiPID, getpid());
	struct passwd* pwd = getpwnam(username);
	if ( !pwd )
		return false;
	struct termios tio;
	if ( tcgetattr(0, &tio) )
		return false;
	int pipe_fds[2];
	if ( pipe2(pipe_fds, O_CLOEXEC) < 0 )
		return false;
	char* login_shell;
	if ( asprintf(&login_shell, "-%s", pwd->pw_shell) < 0 )
		return close(pipe_fds[0]), close(pipe_fds[1]), false;
	if ( session && *session == '\0' )
		session = login_shell;
	sigset_t oldset, sigttou;
	sigemptyset(&sigttou);
	sigaddset(&sigttou, SIGTTOU);
	sigprocmask(SIG_BLOCK, &sigttou, &oldset);
	pid_t child_pid = fork();
	if ( child_pid < 0 )
		return free(login_shell), close(pipe_fds[0]), close(pipe_fds[1]), false;
	if ( child_pid == 0 )
	{
		sigdelset(&oldset, SIGINT);
		sigdelset(&oldset, SIGQUIT);
		sigdelset(&oldset, SIGTSTP);
		signal(SIGINT, SIG_DFL);
		signal(SIGQUIT, SIG_DFL);
		(void) (
		setpgid(0, 0) < 0 ||
		close(pipe_fds[0]) < 0 ||
		setgid(pwd->pw_gid) < 0 ||
		setuid(pwd->pw_uid) < 0 ||
		setenv("LOGIN_PID", login_pid, 1) < 0 ||
		setenv("LOGNAME", pwd->pw_name, 1) < 0 ||
		setenv("USER", pwd->pw_name, 1) < 0 ||
		chdir(pwd->pw_dir) < 0 ||
		setenv("HOME", pwd->pw_dir, 1) < 0 ||
		setenv("SHELL", pwd->pw_shell, 1) < 0 ||
		close(0) < 0 ||
		close(1) < 0 ||
		close(2) < 0 ||
		open("/dev/tty", O_RDONLY) != 0 ||
		open("/dev/tty", O_WRONLY) != 1 ||
		open("/dev/tty", O_WRONLY) != 2 ||
		setcloexecfrom(3) < 0 ||
		tcsetpgrp(0, getpgid(0)) ||
		sigprocmask(SIG_SETMASK, &oldset, NULL) < 0 ||
		settermmode(0, TERMMODE_NORMAL) < 0 ||
		(session ?
			execlp(session + (session[0] == '-'), session, (const char*) NULL) < 0
			:
			(execlp("./.session", "./.session", (const char*) NULL) < 0 &&
			 errno != ENOENT && errno != EACCES) ||
			(execlp("/etc/session", "/etc/session", (const char*) NULL) < 0 &&
			 errno != ENOENT && errno != EACCES) ||
			execlp(pwd->pw_shell, login_shell, (const char*) NULL)));
		write(pipe_fds[1], &errno, sizeof(errno));
		_exit(127);
	}
	free(login_shell);
	close(pipe_fds[1]);
	int errnum;
	if ( readall(pipe_fds[0], &errnum, sizeof(errnum)) < (ssize_t) sizeof(errnum) )
		errnum = 0;
	close(pipe_fds[0]);
	int child_status;
	if ( waitpid(child_pid, &child_status, 0) < 0 )
		errnum = errno;
	tcsetattr(0, TCSAFLUSH, &tio);
	tcsetpgrp(0, getpgid(0));
	sigprocmask(SIG_SETMASK, &oldset, NULL);
	dprintf(1, "\e[H\e[2J");
	fsync(1);
	if ( errnum != 0 )
		return errno = errnum, false;
	return true;
}

static bool read_terminal_line(char* buffer, size_t size)
{
	assert(size);
	size--;
	sigset_t intset;
	sigemptyset(&intset);
	sigaddset(&intset, SIGINT);
	sigaddset(&intset, SIGQUIT);
	bool newline = false;
	size_t sofar = 0;
	while ( !newline && sofar < size )
	{
		sigset_t oldset;
		sigprocmask(SIG_UNBLOCK, &intset, &oldset);
		ssize_t amount = read(0, buffer + sofar, size - sofar);
		sigprocmask(SIG_SETMASK, &oldset, NULL);
		if ( amount <= 0 )
			return false;
		for ( ssize_t i = 0; i < amount; i++ )
		{
			if ( buffer[sofar + i] != '\n' )
				continue;
			newline = true;
			amount = i;
			break;
		}
		sofar += amount;
	}
	while ( !newline )
	{
		char c;
		if ( read(0, &c, 1) <= 0 )
			return false;
		newline = c == '\n';
	}
	buffer[sofar] = '\0';
	return true;
}

bool parse_username(const char* input,
                    char** username,
                    char** session,
                    enum special_action* action)
{
	*username = NULL;
	*session = NULL;
	*action = SPECIAL_ACTION_NONE;
	if ( !strcmp(input, "exit") )
		return *action = SPECIAL_ACTION_POWEROFF, true;
	else if ( !strcmp(input, "poweroff") )
		return *action = SPECIAL_ACTION_POWEROFF, true;
	else if ( !strcmp(input, "reboot") )
		return *action = SPECIAL_ACTION_REBOOT, true;
	else if ( !strcmp(input, "halt") )
		return *action = SPECIAL_ACTION_HALT, true;
	else if ( !strcmp(input, "reinit") )
		return *action = SPECIAL_ACTION_REINIT, true;

	// Skip leading spaces to allow logging in as special accounts.
	while ( isspace(*input) )
		input++;
	char* colon = strchr(input, ':');
	if ( !colon )
	{
		*username = strdup(input);
		return *username != NULL;
	}
	if ( strchr(colon + 1, ':') )
	{
		// Input contains more than one colon.
		return false;
	}
	*username = strndup(input, colon - input);
	if ( !*username )
		return false;
	*session = strdup(colon + 1);
	if ( !*session )
	{
		free(*username);
		*username = NULL;
		return false;
	}
	return true;
}

void handle_special(enum special_action action)
{
	switch ( action )
	{
	case SPECIAL_ACTION_NONE: return;
	case SPECIAL_ACTION_POWEROFF: exit(0);
	case SPECIAL_ACTION_REBOOT: exit(1);
	case SPECIAL_ACTION_HALT: exit(2);
	case SPECIAL_ACTION_REINIT: exit(3);
	}
}

int textual(void)
{
	unsigned int termmode = TERMMODE_UNICODE | TERMMODE_SIGNAL | TERMMODE_UTF8 |
	                        TERMMODE_LINEBUFFER | TERMMODE_ECHO;
	if ( settermmode(0, termmode) < 0 )
		err(2, "settermmode");
	unsigned int pw_termmode = termmode & ~(TERMMODE_ECHO);

	char* username = NULL;
	char* session = NULL;

	while ( true )
	{
		char hostname[HOST_NAME_MAX + 1];
		hostname[0] = '\0';
		gethostname(hostname, sizeof(hostname));
		printf("%s login: ", hostname);
		fflush(stdout);
		char input[256];
		errno = 0;
		if ( !read_terminal_line(input, sizeof(input)) )
		{
			printf("\n");
			if ( errno && errno != EINTR )
			{
				warn("fgets");
				sleep(1);
			}
			printf("\n");
			continue;
		}

		enum special_action action;
		free(username);
		free(session);
		username = NULL;
		session = NULL;
		if ( !parse_username(input, &username, &session, &action) )
		{
			printf("Invalid username\n\n");
			continue;
		}
		handle_special(action);

		if ( settermmode(0, pw_termmode) < 0 )
			err(2, "settermmode");
		printf("Password (will not echo): ");
		fflush(stdout);
		char password[256];
		errno = 0;
		bool password_success = read_terminal_line(password, sizeof(password));
		printf("\n");
		if ( settermmode(0, termmode) < 0 )
			err(2, "settermmode");
		if ( !password_success )
		{
			if ( errno && errno != EINTR )
			{
				warn("fgets");
				sleep(1);
			}
			printf("\n");
			continue;
		}

		bool result = check(username, password);
		explicit_bzero(password, sizeof(password));
		if ( !result )
		{
			const char* msg = "Invalid username/password";
			if ( errno != EACCES )
				msg = strerror(errno);
			printf("%s\n", msg);
			printf("\n");
			continue;
		}

		if ( !login(username, session) )
		{
			warn("logging in as %s", username);
			printf("\n");
		}
	}

	return 0;
}

int main(void)
{
	setlocale(LC_ALL, "");
	if ( getuid() != 0 )
		errx(2, "must be user root");
	if ( getgid() != 0 )
		errx(2, "must be group root");
	if ( !isatty(0) )
	{
		close(0);
		if ( open("/dev/tty", O_RDONLY) != 0 )
			err(2, "/dev/tty");
	}
	if ( !isatty(1) )
	{
		close(1);
		if ( open("/dev/tty", O_WRONLY) != 1 )
			err(2, "/dev/tty");
	}
	if ( !isatty(2) )
	{
		if ( dup2(1, 2) < 0 )
			err(2, "dup2");
	}
	if ( tcgetpgrp(0) != getpgid(0) )
		errx(2, "must be in foreground process group");
	if ( getpgid(0) != getpid() )
		errx(2, "must be progress group leader");
	struct sigaction sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = on_interrupt_signal;
	sigaddset(&sa.sa_mask, SIGINT);
	sigaddset(&sa.sa_mask, SIGQUIT);
	sigaction(SIGINT, &sa, NULL);
	sigaction(SIGQUIT, &sa, NULL);
	sigaddset(&sa.sa_mask, SIGTSTP);
	sigprocmask(SIG_BLOCK, &sa.sa_mask, NULL);
	int result = -1;
	if ( result == -1 )
		result = graphical();
	if ( result == -1 )
		result = textual();
	return result;
}
