/*
 * iucvtty - Application that provides a full-screen terminal for iucvconn
 *
 * Core application
 *
 * Copyright IBM Corp. 2008, 2017
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */
#include <errno.h>
#include <fcntl.h>
#include <pty.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <syslog.h>
#include <unistd.h>
#include <utmp.h>

#include "lib/util_base.h"

#include "af_iucv.h"
#include "iucvterm/config.h"
#include "iucvterm/functions.h"
#include "iucvterm/gettext.h"

#define SYSLOG_IDENT		"iucvtty"
#define PRG_COMPONENT           SYSLOG_IDENT
#define TERM_DEFAULT		"linux"


static volatile sig_atomic_t sig_shutdown;


/**
 * sig_handler() - Signal handler
 * @sig:	Signal number.
 */
static void sig_handler(int sig)
{
	sig_shutdown = sig;
}

/**
 * exec_login_prog() - execute a login program
 * @cmd:	Path to the (login) program executable
 */
static int exec_login_prog(char *cmd[])
{
	int rc;

	if (cmd != NULL)
		rc = execv(cmd[0], cmd);
	else
		rc = execl("/bin/login", "/bin/login", (char *) NULL);
	return rc;
}

/**
 * receive_term_env() - receive terminal environment with timeout
 * @term:     Terminal environment variable
 * @len:      Maximum characters for the terminal environment variable
 */
static void receive_term_env(int client, char *term, size_t len)
{
	struct timeval tv = { .tv_sec = 0, .tv_usec = 0};

	/* Set a temporary timeout of 10s to time out a stalling
	 * client connection. Use SO_RCVTIMEO to set the timeout.
	 * Using those hard coded values directly is safe as
	 * SO_RCVTIMEO is not being used in other parts of iucvtty.
	 */
	tv.tv_sec = 10;
	setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

	memset(term, 0, len);
	if (iucvtty_rx_termenv(client, term, len))
		snprintf(term, len, "%s", TERM_DEFAULT);

	tv.tv_sec = 0;
	setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

	if (!is_term_valid(term, len)) {
		print_error("Ignoring received TERM env due to invalid character(s)");
		snprintf(term, len, "%s", TERM_DEFAULT);
	}
}

/**
 * iucvtty_worker() - Handle an incoming client connection
 * @client:	Client file descriptor
 * @master:	PTY master file descriptor
 * @slave:	PTY slave file descriptor
 * @cfg:	IUCV TTY configuration structure.
 *
 * Unlike the HVC IUCV terminal device driver, iucvtty expects to
 * receive the TERM environment first.  This is necessary to correctly
 * set up the login program.
 */
static int iucvtty_worker(int client, int master, int slave,
			  const struct iucvterm_cfg *cfg)
{
	int rc;
	struct iucvtty_msg *msg;
	pid_t child;
	fd_set set;
	size_t chunk;
	char term_env[MAX_TERM_SIZE];


	/* flush pending terminal data */
	tcflush(master, TCIOFLUSH);

	/* read and validate terminal parameters from client */
	receive_term_env(client, term_env, sizeof(term_env));

	/* start login program */
	child = fork();
	if (child == -1) {
		print_error("Creating a new process to run the "
			    "login program failed");
		iucvtty_tx_error(client, ERR_FORK);
		return 1;	/* return from worker */
	}
	if (child == 0) {	/* child process */
		closelog();	/* close syslog */

		/* setup terminal */
		if (login_tty(slave)) {
			print_error("Setting up a terminal for user login failed");
			iucvtty_tx_error(client, ERR_SETUP_LOGIN_TTY);
			exit(2);
		}
		setenv("TERM", term_env, 1);
		if (exec_login_prog(cfg->cmd_parms)) {
			print_error("Running the login program failed");
			iucvtty_tx_error(client, ERR_CANNOT_EXEC_LOGIN);
		}
		exit(3);	/* we only reach here if exec has failed */
	}

	/* Close slave fd to properly handle SIGHUP for the child process */
	close(slave);

	/* setup buffers */
	msg = malloc(MSG_BUFFER_SIZE);
	if (msg == NULL) {
		print_error("Allocating memory for the data buffer failed");
		rc = 2;
		goto out_kill_login;
	}

	/* multiplex i/o between login program and socket. */
	rc = 0;
	chunk = 0;
	while (!sig_shutdown) {
		FD_ZERO(&set);
		FD_SET(client, &set);
		FD_SET(master, &set);

		if (select(MAX(master, client) + 1, &set,
			   NULL, NULL, NULL) == -1) {
			if (errno == EINTR)
				continue;
			break;
		}

		if (FD_ISSET(client, &set)) {
			if (iucvtty_read_msg(client, msg,
					     MSG_BUFFER_SIZE, &chunk))
				break;
			switch (msg->type) {
			case MSG_TYPE_DATA:
				iucvtty_copy_data(master, msg);
				break;
			case MSG_TYPE_WINSIZE:
				if (msg->datalen != sizeof(struct winsize))
					break;
				if (ioctl(master, TIOCSWINSZ,
					  (struct winsize *) msg->data))
					print_error("Resizing the terminal "
						    "window failed");
				break;
			case MSG_TYPE_TERMIOS:	/* ignored */
				break;
			case MSG_TYPE_ERROR:
				iucvtty_error(msg);
				break;
			}
		}

		if (FD_ISSET(master, &set))
			if (iucvtty_tx_data(client, master,
					    msg, MSG_BUFFER_SIZE))
				break;
	}
	free(msg);

out_kill_login:
	/* ensure the chld is terminated before calling waitpid:
	 * - in case a sigterm has been received,
	 * - or a sigchld from other than the chld
	 */
	kill(child, SIGKILL);	/* cause a sigchld */
	waitpid(child, NULL, 0);

	return rc;
}

/**
 * main() - IUCV TTY program startup
 */
int main(int argc, char *argv[])
{
	struct iucvterm_cfg	conf;		/* program configuration */
	struct sockaddr_iucv	saddr, caddr;	/* IUCV socket address info */
	char 			client_host[9];	/* client guest name */
	int 			server, client;	/* socket file descriptors */
	int 			master, slave;	/* pre-allocated PTY fds */
	struct sigaction	sigact;		/* signal handler */
	int 			rc;
	socklen_t		len;


	/* gettext initialization */
	gettext_init();

	/* parse command line arguments */
	parse_options(PROG_IUCV_TTY, &conf, argc, argv);

	/* create server socket... */
	server = iucvtty_socket(&saddr, NULL, conf.service);
	if (server == -1) {
		print_error((errno == EAFNOSUPPORT)
			    ? N_("The AF_IUCV address family is not available")
			    : N_("Creating the AF_IUCV socket failed"));
		return 1;
	}
	if (bind(server, (struct sockaddr *) &saddr, sizeof(saddr)) == -1) {
		print_error("Binding the AF_IUCV socket failed");
		close(server);
		return 1;
	}
	if (listen(server, 1) == -1) {
		print_error("Listening for incoming connections failed");
		close(server);
		return 1;
	}

	/* pre-allocate PTY master/slave file descriptors */
	if (openpty(&master, &slave, NULL, NULL, NULL)) {
		print_error("Opening a new PTY master/slave device pair failed");
		close(server);
		return 1;
	}

	/* set close-on-exec for file descriptors */
	if (fcntl(master, F_SETFD, FD_CLOEXEC) ||
	    fcntl(server, F_SETFD, FD_CLOEXEC)) {
		print_error("Setting file controls failed");
		close(server);
		rc = 1;
		goto exit_on_error;
	}

	/* syslog */
	openlog(SYSLOG_IDENT, LOG_PID, LOG_AUTHPRIV);
	syslog(LOG_INFO, "Listening on terminal ID: %s, using pts device: %s",
		conf.service, ttyname(slave));

	rc = 0;
	len = sizeof(struct sockaddr_iucv);
	/* accept a new client connection */
	client = accept(server, (struct sockaddr *) &caddr, &len);
	if (client == -1) {
		print_error("An incoming connection could not be accepted");
		close(server);
		rc = 2;
		goto exit_on_error;
	}

	/* check if client is allowed to connect */
	userid_cpy(client_host, caddr.siucv_user_id);
	if (is_client_allowed(client_host, &conf)) {
		iucvtty_tx_error(client, ERR_NOT_AUTHORIZED);
		syslog(LOG_WARNING, "Rejected client connection from %s; "
				    "Client is not allowed to connect.",
				    client_host);
		rc = 3;

	} else { /* client is allowed to connect */
		syslog(LOG_INFO, "Accepted client connection from %s",
			client_host);
		/* close server socket */
		close(server);
		/* set close-on-exec for client socket */
		if (fcntl(client, F_SETFD, FD_CLOEXEC)) {
			print_error("Setting file controls failed");
			rc = 4;
			goto exit_on_error;
		}

		/* setup signal handler to notify shutdown signal */
		sigemptyset(&sigact.sa_mask);
		sigact.sa_flags = SA_RESTART;
		sigact.sa_handler = sig_handler;
		if (sigaction(SIGCHLD, &sigact, NULL)
			|| sigaction(SIGTERM, &sigact, NULL)
			|| sigaction(SIGINT,  &sigact, NULL)
			|| sigaction(SIGPIPE, &sigact, NULL)) {
			print_error("Registering a signal handler failed");
			rc = 4;
			goto exit_on_error;
		}

		/* handle client terminal connection */
		rc = iucvtty_worker(client, master, slave, &conf);
	}

	close(client);

exit_on_error:
	close(slave);
	close(master);
	closelog();

	return rc;
}
