/*
 * iucvtty / iucvconn - IUCV Terminal Applications
 *
 * Common functions
 *
 * 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 <assert.h>
#include <ctype.h>
#include <errno.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

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


/* Global program component for iucv terminal tools */
#define PRG_COMPONENT		"iucvterm"


/**
 * __write() - Write data
 * @fd:		File descriptor
 * @buf:	Pointer to data buffer
 * @len:	Buffer length
 *
 * Write @len number of bytes from the buffer @buf to the file
 * descriptor @fd. The routine handles EINTR and partial writes.
 * Returns the error code from the underlying write(2) syscall.
 */
ssize_t __write(int fd, const void *buf, size_t len)
{
	ssize_t rc;
	size_t  written = 0;

	while (written < len) {
		rc = write(fd, buf + written, len - written);
		if (rc == -1 && errno == EINTR)
			continue;
		if (rc <= 0)
			return rc;
		written += rc;
	}
	return written;
}

/**
 * __read() - Read data
 * @fd:		File descriptor
 * @buf:	Pointer to data buffer
 * @len:	Buffer size
 *
 * Read up to @len number of bytes from file descriptor @fd and stores them
 * in the buffer to which @buf points.
 * The routine handles EINTR and partial reads and returns the error code
 * from the underlying read(2) syscall.
 */
ssize_t __read(int fd, void *buf, size_t len)
{
	ssize_t rc;
	size_t count = 0;

	while (count < len) {
		rc = read(fd, buf + count, len - count);
		if (rc == -1 && errno == EINTR)
			continue;
		if (rc <= 0)
			return rc;
		count += rc;
	}
	return count;
}

#ifdef __DEBUG__
static void __dump_msg(int fd, const struct iucvtty_msg *m, char dir)
{
	write_session_info("%c: (fd=%d) MSG: ver=%02x type=%02x datalen=%u\n",
			   dir, fd, m->version, m->type, (uint16_t) m->datalen);
}
#endif


/**
 * iucvtty_socket() - Creates and return an IUCV socket
 * @sai:	AF_IUCV socket address structure
 * @host:	z/VM guest name
 * @service:	Terminal name passed as additional data to @host after connect
 *
 * This function sets up the struct sockaddr_iucv with the specified
 * VM guest virtual machine and terminal information.
 * Finally, it returns an AF_IUCV socket.
 */
int iucvtty_socket(struct sockaddr_iucv *sai,
		   const char *host, const char *service)
{
	char temp[9];

	memset(sai, 0, sizeof(struct sockaddr_iucv));
	sai->siucv_family = AF_IUCV;

	if (host != NULL) {
		snprintf(temp, 9, "%-8s", host);
		memcpy(sai->siucv_user_id, temp, 8);
	} else
		memset(sai->siucv_user_id, ' ', 8);

	if (service != NULL) {
		snprintf(temp, 9, "%-8s", service);
		memcpy(sai->siucv_name, temp, 8);
	} else
		memset(sai->siucv_name, ' ', 8);

	return socket(PF_IUCV, SOCK_STREAM, 0);
}

/**
 * iucvtty_tx_termenv() - Send terminal environment variable
 * @dest:	File descriptor to output data
 * @dflt:	TERM environment string ('\0' terminated)
 *
 * Copy terminal environment variable to destination @dest.
 */
int iucvtty_tx_termenv(int dest, char *dflt)
{
	struct iucvtty_msg *msg;
	char *term = getenv("TERM");
	size_t len;
	int rc;

	if (term == NULL && dflt != NULL)
		term = dflt;

	len = 0;
	if (term != NULL)
		len = MIN(1 + strlen(term), (size_t)MAX_TERM_SIZE);

	/* Note: The server console tool waits for terminal environment
	 *       information: the message is sent even if it is empty */
	msg = msg_alloc(MSG_TYPE_TERMENV, len);
	if (msg == NULL)
		return -1;
	msg->datalen = len;
	if (msg->datalen)
		snprintf((char *)msg->data, msg->datalen, "%s", term);
	rc = iucvtty_write_msg(dest, msg);
	msg_free(msg);

	return rc;
}

/**
 * iucvtty_rx_termenv() - Receive terminal environment variable
 * @fd:		File descriptor to read data from
 * @buf:	Buffer to store the terminal environment variable
 * @len:	Size of buffer @buf
 */
int iucvtty_rx_termenv(int fd, void *buf, size_t len)
{
	int rc;
	size_t skip;
	struct iucvtty_msg *msg = msg_alloc(MSG_TYPE_TERMENV, len);

	if (msg == NULL)
		return -1;
	skip = 0;
	rc = iucvtty_read_msg(fd, msg, msg_size(msg), &skip);
	iucvtty_skip_msg_chunk(fd, &skip);
	if (!rc) {
		if (msg->datalen == 0) {
			memset(buf, 0, MIN(1u, len));
		} else {
			msg_cpy_to(msg, buf, len);
			memset(buf + MIN(msg->datalen, len - 1), 0, 1);
		}
	}
	msg_free(msg);
	return rc;
}

/**
 * iucvtty_tx_data() - Send terminal data
 * @from:	File descriptor to read data from
 * @msg:	Pointer to iucv tty message buffer
 * @len:	Size of message buffer
 *
 * This routine reads data from file descriptor @from and stores them in
 * a data array of the specified iucv tty message @msg. It reads up to
 * @len - MSG_DATA_OFFSET bytes from fd @from.
 */
int iucvtty_read_data(int from, struct iucvtty_msg *msg, size_t len)
{
	ssize_t r;

	r = read(from, msg->data, len - MSG_DATA_OFFSET);
	if (r == -1 && errno == EINTR)	/* REVIEW: loop if EINTR ? */
		r = read(from, msg->data, len - MSG_DATA_OFFSET);
	if (r <= 0)
		return -1;
	msg->version = MSG_VERSION;
	msg->type    = MSG_TYPE_DATA;
	msg->datalen = (uint16_t) r;

	return 0;
}

/**
 * iucvtty_tx_data() - Send terminal data
 * @dest:	File descriptor to send data to
 * @from:	File descriptor to read data from
 * @msg:	Pointer to iucv tty message buffer
 * @len:	Size of message buffer
 *
 * This routine reads data from file descriptor @from and stores them in
 * a data array of the specified iucv tty message @msg. It reads up to
 * @len - MSG_DATA_OFFSET bytes from fd @from.
 * Finally, the iucv tty message written to file descriptor @dest.
 */
int iucvtty_tx_data(int dest, int from, struct iucvtty_msg *msg, size_t len)
{
	if (iucvtty_read_data(from, msg, len))
		return -1;
	if (iucvtty_write_msg(dest, msg))
		return -1;
	return 0;
}

/**
 * iucvtty_tx_winsize() - Send terminal window size information.
 * @dest:	Destination
 * @from:	Terminal file descriptor to request winsize
 *
 * Sends the terminal window size from terminal file descriptor
 * @from to the destination @dest.
 * If the window size is not retrieved, the routine will not fail.
 * The routine fails if there is a problem sending the window size
 * to @dest. The return codes are specified by iucvtty_write_msg().
 */
int iucvtty_tx_winsize(int dest, int from)
{
	int rc;
	struct iucvtty_msg *msg = msg_alloc(MSG_TYPE_WINSIZE,
					      sizeof(struct winsize));
	if (msg == NULL)
		return -1;
	rc = 0;
	if (ioctl(from, TIOCGWINSZ, msg->data) > -1)
		rc = iucvtty_write_msg(dest, msg);
	msg_free(msg);

	return rc;
}

/**
 * iucvtty_tx_error() - Send an error code
 * @dest:	Destination
 * @errCode:	Error code
 */
int iucvtty_tx_error(int dest, uint32_t errCode)
{
	struct iucvtty_msg *msg;
	int rc;

	msg = msg_alloc(MSG_TYPE_ERROR, sizeof(errCode));
	if (msg == NULL)
		return -1;
	msg_cpy_from(msg, &errCode, sizeof(errCode));
	rc = iucvtty_write_msg(dest, msg);
	msg_free(msg);
	return rc;
}

/**
 * iucvtty_copy_data() - Copy IUCV message data
 * @dest:	Destination to copy data to
 * @msg:	IUCV message
 */
int iucvtty_copy_data(int dest, struct iucvtty_msg *msg)
{
	if (__write(dest, msg->data, msg->datalen) <= 0)
		return -1;
	return 0;
}

/**
 * iucvtty_skip_msg_chunk() - Skip (receive & forget) count number of bytes
 * @fd:		File descriptor
 * @chunk:	Remaining chunk from a previous iucvtty_read_msg() call
 *
 * See iucvtty_read_msg() for an explanation when to use this routine.
 * Note: The @chunk parameter must not be NULL.
 */
void iucvtty_skip_msg_chunk(int fd, size_t *chunk)
{
	int r;
	char buf[256];

	if (*chunk <= 0)
		return;

	while (*chunk) {
		r = __read(fd, buf, MIN(sizeof(buf), *chunk));
		if (r <= 0)
			break;
		*chunk -= r;
	}

	*chunk = 0;
}

/**
 * iucvtty_read_msg_chunk() - Read IUCV message chunk
 * @fd:	      File descriptor to read from
 * @msg:      Pointer to IUCV message buffer
 * @msglen:     IUCV message buffer size (including message header)
 * @chunk:    Size of chunk data to read
 *
 * Stores new message data and calculates next chunk size if not all data
 * could be read.  Returns zero on success, non-zero otherwise.
 */
static int iucvtty_read_msg_chunk(int fd, struct iucvtty_msg *msg,
				  size_t msglen, size_t *chunk)
{
	ssize_t r;
	size_t datalen;

	/* Calculate message data length to read */
	datalen = MIN(msglen - MSG_DATA_OFFSET, *chunk);

	for (;;) {
		r = read(fd, msg->data, datalen);
		if (r == -1 && errno == EINTR)
			continue;
		if (r <= 0)
			return -1;
		break;
	}

	/* Update message and re-calculate next chunk */
	msg->datalen = r;
	*chunk -= r;

	return 0;
}

/**
 * validate_msg() - Perform sanity checks on a received message
 * @msg:	IUCV message buffer
 *
 * Returns zero if the message is valid; otherwise non-zero
 */
static int validate_msg(struct iucvtty_msg *msg)
{
	switch (msg->type) {
	case MSG_TYPE_DATA:
		/* The datalen ranges from 0 to its maximum of 0xffff
		 * which is the maximum of the type definition of uint16_t.
		 *
		 * Consider the datalen value as valid.
		 */
		break;
	case MSG_TYPE_ERROR:
		if (msg->datalen != sizeof(uint32_t))
			return 1;
		break;
	case MSG_TYPE_TERMENV:
		if (msg->datalen > MAX_TERM_SIZE)
			return 1;
		break;
	case MSG_TYPE_TERMIOS:	/* ignored */
		break;
	case MSG_TYPE_WINSIZE:
		if (msg->datalen != sizeof(struct winsize))
			return 1;
		break;
	default:
		/* Invalid message type */
		return 1;
	}

	return 0;
}

/**
 * iucvtty_read_msg() - Read/Receive an IUCV message
 * @fd:		File descriptor to read from
 * @msg:	Pointer to IUCV message buffer
 * @msglen:	IUCV message buffer size (MUST BE > MSG_DATA_OFFSET)
 * @chunk:	Size of remaining data; must be passed on next call
 *
 * The function reads up to @len bytes from file descriptor @fd.  If the
 * received message is larger than @len bytes, the @chunk value is set to
 * the number of remaining bytes.  The function shall then be re-called to
 * create a new message and receive the outstanding data.  Alternatively,
 * call iucvtty_skip_msg_chunk() to discard remaining data.
 *
 * NOTE: The @len parameter must be greater than MSG_DATA_OFFSET!
 *       The @chunk parameter must not be NULL.
 */
int iucvtty_read_msg(int fd, struct iucvtty_msg *msg,
		     size_t msglen, size_t *chunk)
{
	int rc;
	ssize_t r;		/* number of bytes read from fd */

	/* Ensure message buffer size can hold data and can be processed */
	assert(msglen > MSG_DATA_OFFSET);
	assert(chunk != NULL);

	/* Read pending message data */
	if (*chunk)
		return iucvtty_read_msg_chunk(fd, msg, msglen, chunk);

	/* Read message header of new message */
	r = __read(fd, msg, MSG_DATA_OFFSET);
	if (r <= 0) {
		rc = -1;
		goto out_read_error;
	}

#ifdef __DEBUG__
	__dump_msg(fd, msg, 'R');
#endif

	/* Check message header */
	if (msg->version != MSG_VERSION) {
		fprintf(stderr, _("%s: %s\n"),
			PRG_COMPONENT, _("The version of the received data "
					 "message is not supported"));
		fprintf(stderr, "MSG: msg->version=%u msg->type=%u msg->datalen=%u\n",
			msg->version, msg->type, msg->datalen);
		rc = -2;
		goto out_read_error;
	}

	/* Check for an empty message */
	if (!msg->datalen)
		return 0;

	/* Check message type and data length */
	if (validate_msg(msg)) {
		fprintf(stderr, _("%s: %s\n"),
			PRG_COMPONENT, _("The received message is invalid"));
		fprintf(stderr, "MSG: msg->version=%u msg->type=%u msg->datalen=%u\n",
			msg->version, msg->type, msg->datalen);
		rc = -3;
		goto out_read_error;
	}

	/* Process the new message as a one chunk */
	*chunk = msg->datalen;
	msg->datalen = 0;
	rc = iucvtty_read_msg_chunk(fd, msg, msglen, chunk);

out_read_error:
	return rc;
}

/**
 * iucvtty_write_msg() - Write/Send IUCV message
 * @fd:		File descriptor
 * @msg:	Pointer to IUCV message
 */
int iucvtty_write_msg(int fd, struct iucvtty_msg *msg)
{
	msg->version = MSG_VERSION;

	if (__write(fd, msg, msg_size(msg)) <= 0)
		return -1;

#ifdef __DEBUG__
	__dump_msg(fd, msg, 'S');
#endif
	return 0;
}


/**
 * iucv_msg_error() - Reports an IUCV message error
 * @comp:	Program component
 * @errnum:	IUCV message error code
 */
void iucv_msg_error(const char *comp, uint32_t errnum)
{
	const char *translated;

	switch (errnum) {
	case ERR_FORK:
		translated = _("Creating a new process to run the "
				"login program failed");
		break;
	case ERR_CANNOT_EXEC_LOGIN:
		translated = _("Running the login program failed");
		break;
	case ERR_SETUP_LOGIN_TTY:
		translated = _("Setting up a terminal for user login failed");
		break;
	case ERR_NOT_AUTHORIZED:
		translated = _("The z/VM guest virtual machine is not "
				"permitted to connect");
		break;
	default:
		translated = _("The specified error code is not known");
		break;
	}
	fprintf(stderr, "%s: %s (%s=%" PRIu32 ")\r\n",
		comp, translated, _("error code"), errnum);
}

/**
 * program_error() - Report an program/syscall error.
 * @comp:	Program component name
 * @d:		Error message, subject to gettext translation
 */
void program_error(const char *comp, const char *d)
{
	fprintf(stderr, _("%s: %s: %s\n"), comp, _(d), strerror(errno));
}


/**
 * __regerror - Report an error from a previous regex api call
 * @error:	Error code
 * @re:		Reference to the used regular expression
 */
static inline void __regerror(int error, const regex_t *re)
{
	char errbuf[81];

	regerror(error, re, errbuf, 81);
	fprintf(stderr, _("The regular expression has an error: %s\n"), errbuf);
	return;
}

/**
 * is_regex_valid() - Check if the specified regex is syntactically correct.
 * @re:		String representation of the regular expression
 *
 * Returns zero on success, otherwise -1.
 */
int is_regex_valid(const char *re)
{
	regex_t regex;
	int rc;

	if (re == NULL)
		return -1;
	rc = regcomp(&regex, re, REG_EXTENDED | REG_ICASE | REG_NOSUB);
	if (rc) {
		__regerror(rc, &regex);
		rc = -1;
	}
	regfree(&regex);

	return rc;
}

/**
 * strmatch() - Match a string using a regular expression
 * @str:	String to match
 * @re:		Regular expression
 *
 * Returns zero on success, -1 on error or if @str is NULL; and
 * 1 if the regular expression did not match the string.
 */
int strmatch(const char *str, const char *re)
{
	regex_t regex;
	regmatch_t pmatch[1];
	size_t nmatch = 0;
	int rc;

	if (re == NULL)
		return -1;

	rc = regcomp(&regex, re, REG_EXTENDED | REG_ICASE | REG_NOSUB);
	if (rc) {
		__regerror(rc, &regex);
		regfree(&regex);
		return -1;
	}

	rc = regexec(&regex, str, nmatch, pmatch, 0);
	if (rc == REG_NOMATCH)
		rc = 1;

	regfree(&regex);
	return rc;
}

/**
 * is_client_allowed() - Check if the client is allowed to connect.
 * @client:	Client name
 * @cfg:	Pointer to the IUCV terminal configuration structure
 *
 * The return code is identical to strmatch().  If client checking is
 * disabled, the function returns zero.
 */
int is_client_allowed(const char *client, const struct iucvterm_cfg *cfg)
{
	if (!CFG_CHKCLNT(cfg))
		return 0;

	return strmatch(client, cfg->client_re);
}

/**
 * is_term_valid() - Validate TERM environment value
 * @term:	Terminal environment name to validate
 * @len:	Maximum number of characters to validate
 */
int is_term_valid(const char *term, size_t len)
{
	const char *c;

	if (term == NULL || *term == '\0')
		return 0;

	for (c = term; *c != '\0'; c++) {
		if ((size_t)(c - term) >= len)
			return 0;
		if (!isalnum((unsigned char)*c) &&
		    *c != '.' &&
		    *c != '_' &&
		    *c != '-')
			return 0;
	}

	return 1;
}

/**
 * userid_cpy() - Copy z/VM user ID and skip trailing spaces.
 * @dest:	Destination buffer
 * @userid:	z/VM user ID
 */
void userid_cpy(char dest[9], const char userid[8])
{
	ssize_t pos;

	/* find pos of last character (pos 0..7) or -1 if user ID is empty */
	for (pos = 7; pos >= 0; pos--)
		if (userid[pos] != ' ')
			break;

	if (pos >= 0)
		memcpy(dest, userid, pos + 1);
	dest[pos + 1] = '\0';
}
