/* Osmo-CC: Socket handling
 *
 * (C) 2016 by Andreas Eversberg <jolly@eversberg.eu>
 * All Rights Reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netdb.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/select.h>
#include <osmocom/core/timer.h>
#include <osmocom/cc/message.h>
#include <osmocom/cc/cause.h>
#include <osmocom/cc/socket.h>

extern int DLCC;

static const char version_string[] = OSMO_CC_VERSION;

static int _getaddrinfo(const char *host, uint16_t port, struct addrinfo **result)
{
	char portstr[8];
	struct addrinfo hints;
	int rc;

	sprintf(portstr, "%d", port);

	/* bind socket */
	memset(&hints, 0, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_PASSIVE;
	hints.ai_protocol = 0;
	hints.ai_canonname = NULL;
	hints.ai_addr = NULL;
	hints.ai_next = NULL;

	rc = getaddrinfo(host, portstr, &hints, result);
	if (rc < 0) {
		LOGP(DLCC, LOGL_ERROR, "Failed to create socket for host '%s', port '%d': %s.\n", host, port,
		     gai_strerror(rc));
		return rc;
	}
	return rc;
}

/* send a reject message toward CC process.
 * the CC process will change the reject message to a release message when not in INIT_IN state
 */
static void rej_msg(osmo_cc_socket_t *os, uint32_t callref, uint8_t socket_cause, uint8_t isdn_cause,
		    uint16_t sip_cause)
{
	osmo_cc_msg_t *msg;

	/* create message */
	msg = osmo_cc_new_msg(OSMO_CC_MSG_REJ_REQ);
	if (!msg)
		abort();

	/* add cause */
	osmo_cc_add_ie_cause(msg, os->location, isdn_cause, sip_cause, socket_cause);
	osmo_cc_convert_cause_msg(msg);

	/* message down */
	os->recv_msg_cb(os->priv, callref, msg);
}

static void tx_keepalive_timeout(void *data)
{
	osmo_cc_conn_t *conn = data;
	osmo_cc_msg_t *msg;

	/* send keepalive message */ 
	msg = osmo_cc_new_msg(OSMO_CC_MSG_DUMMY_REQ);
	osmo_cc_msg_list_enqueue(&conn->os->write_list, msg, conn->callref);
	osmo_timer_schedule(&conn->tx_keepalive_timer, OSMO_CC_SOCKET_TX_KEEPALIVE);
}

static void close_conn(osmo_cc_conn_t *conn, uint8_t socket_cause);

static void rx_keepalive_timeout(void *data)
{
	osmo_cc_conn_t *conn = data;

	LOGP(DLCC, LOGL_ERROR, "OsmoCC-Socket failed due to timeout.\n");
	close_conn(conn, OSMO_CC_SOCKET_CAUSE_TIMEOUT);
}

static int socket_listen_cb(struct osmo_fd *ofd, unsigned int when);

/* create socket process and bind socket */
int osmo_cc_open_socket(osmo_cc_socket_t *os, const char *host, uint16_t port, void *priv,
			void (*recv_msg_cb)(void *priv, uint32_t callref, osmo_cc_msg_t *msg), uint8_t location)
{
	int try = 0, auto_port = 0;
	struct addrinfo *result, *rp;
	int rc, sock;

	memset(os, 0, sizeof(*os));

try_again:
	/* check for given port, if NULL, autoselect port */
	if (!port || auto_port) {
		port = OSMO_CC_DEFAULT_PORT + try;
		try++;
		auto_port = 1;
	}

	LOGP(DLCC, LOGL_DEBUG, "Create socket for host %s port %d.\n", host, port);

	rc = _getaddrinfo(host, port, &result);
	if (rc < 0)
		return rc;
	for (rp = result; rp; rp = rp->ai_next) {
		int on = 1;
		sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
		if (sock < 0)
			continue;
		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (unsigned char *)&on, sizeof(on));
		rc = bind(sock, rp->ai_addr, rp->ai_addrlen);
		if (rc == 0)
			break;
		close(sock);
	}
	freeaddrinfo(result);
	if (rp == NULL) {
		if (auto_port && port < OSMO_CC_DEFAULT_PORT_MAX) {
			LOGP(DLCC, LOGL_DEBUG, "Failed to bind host %s port %d, trying again.\n", host, port);
			goto try_again;
		}
		LOGP(DLCC, LOGL_ERROR, "Failed to bind given host %s port %d.\n", host, port);
		return -EIO;
	}

	/* listen to socket */
	rc = listen(sock, 10);
	if (rc < 0) {
		LOGP(DLCC, LOGL_ERROR, "Failed to listen on socket.\n");
		close(sock);
		return rc;
	}

	/* register */
	os->ofd.fd = sock;
	os->ofd.cb = socket_listen_cb;
	os->ofd.data = os;
	os->ofd.when = OSMO_FD_READ;
	osmo_fd_register(&os->ofd);
	os->recv_msg_cb = recv_msg_cb;
	os->priv = priv;
	os->location = location;

	return port;
}

static int socket_conn_cb(struct osmo_fd *ofd, unsigned int when);

/* create a connection */
static osmo_cc_conn_t *open_conn(osmo_cc_socket_t *os, int sock, uint32_t callref, int read_setup)
{
	osmo_cc_conn_t *conn, **connp;

	/* create connection */
	conn = calloc(1, sizeof(*conn));
	if (!conn) {
		LOGP(DLCC, LOGL_ERROR, "No mem!\n");
		abort();
	}
	conn->os = os;
	conn->ofd.fd = sock;
	conn->ofd.cb = socket_conn_cb;
	conn->ofd.data = conn;
	conn->ofd.when = OSMO_FD_READ;
	osmo_fd_register(&conn->ofd);
	conn->read_version = 1;
	conn->write_version = 1;
	conn->read_setup = read_setup;
	if (callref)
		conn->callref = callref;
	else
		conn->callref = osmo_cc_new_callref();

	osmo_timer_setup(&conn->tx_keepalive_timer, tx_keepalive_timeout, conn);
	osmo_timer_setup(&conn->rx_keepalive_timer, rx_keepalive_timeout, conn);
	osmo_timer_schedule(&conn->tx_keepalive_timer, OSMO_CC_SOCKET_TX_KEEPALIVE);
	osmo_timer_schedule(&conn->rx_keepalive_timer, OSMO_CC_SOCKET_RX_KEEPALIVE);

	LOGP(DLCC, LOGL_DEBUG, "New socket connection (callref %d).\n", conn->callref);

	/* attach to list */
	connp = &os->conn_list;
	while (*connp)
		connp = &((*connp)->next);
	*connp = conn;

	return conn;
}

/* remove a connection */
static void close_conn(osmo_cc_conn_t *conn, uint8_t socket_cause)
{
	osmo_cc_conn_t **connp;
	osmo_cc_msg_list_t *ml;

	/* detach connection first, to prevent a destruction during message handling (double free) */
	connp = &conn->os->conn_list;
	while (*connp != conn)
		connp = &((*connp)->next);
	*connp = conn->next;
	/* send reject message, if socket_cause is set */
	if (socket_cause && !conn->read_setup) {
		/* receive a release or reject (depending on state), but only if we sent a setup */
		rej_msg(conn->os, conn->callref, socket_cause, 0, 0);
	}

	LOGP(DLCC, LOGL_DEBUG, "Destroy socket connection (callref %d).\n", conn->callref);

	/* close socket */
	if (conn->ofd.fd) {
		osmo_fd_unregister(&conn->ofd);
		close(conn->ofd.fd);
	}
	/* free partly received message */
	if (conn->read_msg)
		osmo_cc_free_msg(conn->read_msg);
	/* free send queue */
	while ((ml = conn->write_list)) {
		osmo_cc_free_msg(ml->msg);
		conn->write_list = ml->next;
		free(ml);
	}
	/* free timers */
	osmo_timer_del(&conn->tx_keepalive_timer);
	osmo_timer_del(&conn->rx_keepalive_timer);
	/* free connection (already detached above) */
	free(conn);
}

/* close socket and remove */
void osmo_cc_close_socket(osmo_cc_socket_t *os)
{
	osmo_cc_msg_list_t *ml;

	LOGP(DLCC, LOGL_DEBUG, "Destroy socket.\n");

	/* free all connections */
	while (os->conn_list)
		close_conn(os->conn_list, 0);
	/* close socket */
	if (os->ofd.fd > 0) {
		osmo_fd_unregister(&os->ofd);
		close(os->ofd.fd);
		os->ofd.fd = 0;
	}
	/* free send queue */
	while ((ml = os->write_list)) {
		osmo_cc_free_msg(ml->msg);
		os->write_list = ml->next;
		free(ml);
	}
}

/* send message to send_queue of sock instance */
int osmo_cc_sock_send_msg(osmo_cc_socket_t *os, uint32_t callref, osmo_cc_msg_t *msg, const char *host, uint16_t port)
{
	osmo_cc_msg_list_t *ml;

	/* turn _IND into _REQ and _CNF into _RSP */
	msg->type &= ~1;

	/* create list entry */
	ml = osmo_cc_msg_list_enqueue(&os->write_list, msg, callref);
	if (host)
		strncpy(ml->host, host, sizeof(ml->host) - 1);
	ml->port = port;

	return 0;
}

/* receive message
 * return 1 if work was done.
 */
static int receive_conn(osmo_cc_conn_t *conn)
{
	uint8_t socket_cause = OSMO_CC_SOCKET_CAUSE_BROKEN_PIPE;
	int rc;
	osmo_cc_msg_t *msg;
	uint8_t msg_type;
	int len;
	int work = 0;

	/* get version from remote */
	if (conn->read_version) {
		rc = recv(conn->ofd.fd, conn->read_version_string + conn->read_version_pos,
			  strlen(version_string) - conn->read_version_pos, 0);
		if (rc < 0 && errno == EAGAIN)
			return work;
		work = 1;
		if (rc <= 0) {
			goto close;
		}
		conn->read_version_pos += rc;
		if (conn->read_version_pos == strlen(version_string)) {
			conn->read_version = 0;
			if (!!memcmp(conn->read_version_string, version_string, strlen(version_string) - 1)) {
				LOGP(DLCC, LOGL_NOTICE, "Remote does not seem to be an Osmo-CC socket, rejecting!\n");
				socket_cause = OSMO_CC_SOCKET_CAUSE_FAILED;
				goto close;
			}
			if (conn->read_version_string[strlen(version_string) - 1] !=
			    version_string[strlen(version_string) - 1]) {
				LOGP(DLCC, LOGL_NOTICE,
				     "Remote Osmo-CC socket has wrong version (local=%s, remote=%s), rejecting!\n",
				     version_string, conn->read_version_string);
				socket_cause = OSMO_CC_SOCKET_CAUSE_VERSION_MISMATCH;
				goto close;
			}
		} else
			return work;
	}

try_next_message:
	/* read message header from remote */
	if (!conn->read_msg) {
		rc = recv(conn->ofd.fd, ((uint8_t *)&conn->read_hdr) + conn->read_pos,
			  sizeof(conn->read_hdr) - conn->read_pos, 0);
		if (rc < 0 && errno == EAGAIN)
			return work;
		work = 1;
		if (rc <= 0) {
			goto close;
		}
		conn->read_pos += rc;
		if (conn->read_pos == sizeof(conn->read_hdr)) {
			conn->read_msg = osmo_cc_new_msg(conn->read_hdr.type);
			if (!conn->read_msg)
				abort();
			conn->read_msg->length_networkorder = conn->read_hdr.length_networkorder;
			/* prepare for reading message */
			conn->read_pos = 0;
		} else
			return work;
	}

	/* read message data from remote */
	msg = conn->read_msg;
	len = ntohs(msg->length_networkorder);
	if (len == 0)
		goto empty_message;
	rc = recv(conn->ofd.fd, msg->data + conn->read_pos, len - conn->read_pos, 0);
	if (rc < 0 && errno == EAGAIN)
		return work;
	work = 1;
	if (rc <= 0) {
		goto close;
	}
	conn->read_pos += rc;
	if (conn->read_pos == len) {
empty_message:
		/* start RX keepalive timeer, if not already */
		osmo_timer_schedule(&conn->rx_keepalive_timer, OSMO_CC_SOCKET_RX_KEEPALIVE);
		/* we got our setup message, so we clear the flag */
		conn->read_setup = 0;
		/* prepare for reading header */
		conn->read_pos = 0;
		/* detach message first, because the connection might be destroyed during message handling */
		msg_type = conn->read_msg->type;
		conn->read_msg = NULL;
		/* drop dummy or forward message */
		if (msg_type == OSMO_CC_MSG_DUMMY_REQ)
			osmo_cc_free_msg(msg);
		else
			conn->os->recv_msg_cb(conn->os->priv, conn->callref, msg);
		if (msg_type == OSMO_CC_MSG_REL_REQ || msg_type == OSMO_CC_MSG_REJ_REQ) {
			LOGP(DLCC, LOGL_DEBUG, "closing socket because we received a release or reject message.\n");
			close_conn(conn, 0);
			return 1; /* conn removed */
		}
		goto try_next_message;
	}
	return work;

close:
	LOGP(DLCC, LOGL_ERROR, "OsmoCC-Socket failed, socket cause %d.\n", socket_cause);
	close_conn(conn, socket_cause);
	return work; /* conn removed */
}

/* transmit message
 * return 1 if work was done.
 */
static int transmit_conn(osmo_cc_conn_t *conn)
{
	uint8_t socket_cause = OSMO_CC_SOCKET_CAUSE_BROKEN_PIPE;
	int rc;
	osmo_cc_msg_t *msg;
	int len;
	osmo_cc_msg_list_t *ml;
	int work = 0;

	/* send socket version to remote */
	if (conn->write_version) {
		rc = write(conn->ofd.fd, version_string, strlen(version_string));
		if (rc < 0 && errno == EAGAIN)
			return work;
		work = 1;
		if (rc <= 0) {
			goto close;
		}
		if (rc != strlen(version_string)) {
			LOGP(DLCC, LOGL_ERROR, "short write, please fix handling!\n");
			abort();
		}
		conn->write_version = 0;
	}

	/* send message to remote */
	while (conn->write_list) {
		osmo_timer_del(&conn->tx_keepalive_timer);
		msg = conn->write_list->msg;
		len = sizeof(*msg) + ntohs(msg->length_networkorder);
		rc = write(conn->ofd.fd, msg, len);
		if (rc < 0 && errno == EAGAIN)
			return work;
		work = 1;
		if (rc <= 0) {
			goto close;
		}
		if (rc != len) {
			LOGP(DLCC, LOGL_ERROR, "short write, please fix handling!\n");
			abort();
		}
		/* close socket after sending release/reject message */
		if (msg->type == OSMO_CC_MSG_REL_REQ || msg->type == OSMO_CC_MSG_REJ_REQ) {
			LOGP(DLCC, LOGL_DEBUG, "closing socket because we sent a release or reject message.\n");
			close_conn(conn, 0);
			return work; /* conn removed */
		}
		/* free message after sending */
		ml = conn->write_list;
		conn->write_list = ml->next;
		osmo_cc_free_msg(msg);
		free(ml);
	}

	/* start TX keepalive timer, if not already
	 * because we stop at every message above, we actually restart the timer here.
	 * only if there is no message for the amount of time, the timer fires.
	 */
	if (!osmo_timer_pending(&conn->tx_keepalive_timer))
		osmo_timer_schedule(&conn->tx_keepalive_timer, OSMO_CC_SOCKET_TX_KEEPALIVE);

	return work;

close:
	LOGP(DLCC, LOGL_NOTICE, "OsmoCC-Socket failed.\n");
	close_conn(conn, socket_cause);
	return work; /* conn removed */
}

/* handle all sockets of a socket interface
 * return 1 if work was done.
 */
int osmo_cc_handle_socket(osmo_cc_socket_t *os)
{
	int sock;
	osmo_cc_conn_t *conn;
	osmo_cc_msg_list_t *ml, **mlp;
	int flags;
	struct addrinfo *result, *rp;
	int rc;
	int work = 0;

	/* handle messages in send queue */
	while ((ml = os->write_list)) {
		work = 1;
		/* detach list entry */
		os->write_list = ml->next;
		ml->next = NULL;
		/* search for socket connection */
		for (conn = os->conn_list; conn; conn=conn->next) {
			if (conn->callref == ml->callref)
				break;
		}
		if (conn) {
			/* attach to list */
			mlp = &conn->write_list;
			while (*mlp)
				mlp = &((*mlp)->next);
			*mlp = ml;
			conn->ofd.when |= OSMO_FD_WRITE;
			/* done with message */
			continue;
		}

		/* reject and release are ignored */
		if (ml->msg->type == OSMO_CC_MSG_REJ_REQ
		 || ml->msg->type == OSMO_CC_MSG_REL_REQ) {
			/* drop message */
			osmo_cc_free_msg(ml->msg);
			free(ml);
			/* done with message */
			continue;
		}

		/* reject, if this is not a setup message */
		if (ml->msg->type != OSMO_CC_MSG_SETUP_REQ
		 && ml->msg->type != OSMO_CC_MSG_ATTACH_REQ) {
			LOGP(DLCC, LOGL_ERROR, "Message with unknown callref.\n");
			rej_msg(os, ml->callref, 0, OSMO_CC_ISDN_CAUSE_INVAL_CALLREF, 0);
			/* drop message */
			osmo_cc_free_msg(ml->msg);
			free(ml);
			/* done with message */
			continue;
		}
		/* connect to remote */
		rc = _getaddrinfo(ml->host, ml->port, &result);
		if (rc < 0) {
			rej_msg(os, ml->callref, OSMO_CC_SOCKET_CAUSE_FAILED, 0, 0);
			/* drop message */
			osmo_cc_free_msg(ml->msg);
			free(ml);
			/* done with message */
			continue;
		}
		for (rp = result; rp; rp = rp->ai_next) {
			sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
			if (sock < 0)
				continue;
			/* set nonblocking io, to prevent connect() and subsequent reads from blocking */
			flags = fcntl(sock, F_GETFL);
			flags |= O_NONBLOCK;
			fcntl(sock, F_SETFL, flags);
			/* connect */
			rc = connect(sock, rp->ai_addr, rp->ai_addrlen);
			if (rc == 0 || errno == EINPROGRESS)
				break;
			close(sock);
		}
		freeaddrinfo(result);
		if (rp == NULL) {
			LOGP(DLCC, LOGL_ERROR, "Failed to connect to given host %s port %d.\n", ml->host, ml->port);
			rej_msg(os, ml->callref, OSMO_CC_SOCKET_CAUSE_FAILED, 0, 0);
			/* drop message */
			osmo_cc_free_msg(ml->msg);
			free(ml);
			/* done with message */
			continue;
		}
		/* create connection */
		conn = open_conn(os, sock, ml->callref, 0);
		/* attach to list */
		conn->write_list = ml;
		conn->ofd.when |= OSMO_FD_WRITE;
		/* done with (setup) message */
	}

	return work;
}

static int socket_listen_cb(struct osmo_fd *ofd, unsigned int when)
{
	osmo_cc_socket_t *os = ofd->data;
	struct sockaddr_storage sa;
	socklen_t slen = sizeof(sa);
	int sock;
	int flags;

	if (when & OSMO_FD_READ) {
		/* handle new socket connection */
		if ((sock = accept(os->ofd.fd, (struct sockaddr *)&sa, &slen)) > 0) {
			/* set nonblocking io, to prevent subsequent reads from blocking */
			flags = fcntl(sock, F_GETFL);
			flags |= O_NONBLOCK;
			fcntl(sock, F_SETFL, flags);
			/* create connection */
			open_conn(os, sock, 0, 1);
		}
	}

	return 0;
}

static int socket_conn_cb(struct osmo_fd *ofd, unsigned int when)
{
	osmo_cc_conn_t *conn = ofd->data;
	int work;

	if (when & OSMO_FD_READ) {
		/* check for rx */
		work = receive_conn(conn);
		/* if "change" is set, connection list might have changed, so we restart processing the list */
		if (work)
			return 0;
	}
	if (when & OSMO_FD_WRITE) {
		/* check for tx */
		work = transmit_conn(conn);
		/* if "change" is set, connection list might have changed, so we restart processing the list */
		if (work)
			return 0;
		else
			conn->ofd.when &= ~OSMO_FD_WRITE;
	}

	return 0;
}
