/*
 *	Copyright (c) 1999-2003 Rinet Corp., Novosibirsk, Russia
 *
 * Redistribution and use in source forms, with and without modification,
 * are permitted provided that this entire comment appears intact.
 *
 * THIS SOURCE CODE IS PROVIDED ``AS IS'' WITHOUT ANY WARRANTIES OF ANY KIND.
 */

#ifdef	HAVE_CONFIG_H
#include <config.h>
#endif

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#include "session.h"
#include "events.h"	/* just for tv_sub() */

#define	dprintf(x)	/* nope */

#ifndef	BUF_SIZE
#define	BUF_SIZE	8192
#endif
#ifndef	MAX_STR_LEN
#define	MAX_STR_LEN	1500	/* must be vastly smaller then BUF_SIZE */
#endif

#ifdef	O_NONBLOCK
#define	ASYNC_MODE	O_NONBLOCK
#elif	O_NDELAY
#define	ASYNC_MODE	O_NDELAY
#elif	FNDELAY
#define	ASYNC_MODE	FNDELAY
#elif	O_ASYNC
#define	ASYNC_MODE	O_ASYNC
#elif
#error the fcntl argument to turn ON/OFF non-blocking I/O is unknown
#endif

static int session_read(SESSION *sd);

static SESSION *first_session = 0;	/* first network session in table */

typedef	struct session_binder_ent {
	void (*notify)(void *arg);	/* call it before free */
	void *arg;
	struct session_binder_ent *next;
} SESSION_BINDER;


SESSION *
session_open(sock, peer, type)
	int sock;
	const struct sockaddr *peer;
	SessionType type;
{
	SESSION *sd, *prev = 0, *next = 0;
	static u_long sid = 0;

	/*
	 * Search for first empty or last session slot.
	 */
	for (sd = first_session; sd; sd = sd->next) {
		if (!sd->sid) {
			next = sd->next;
			break;
		}
		prev = sd;
	}
	if (!sd && (sd = (SESSION *)malloc(sizeof(SESSION))) == 0)
		return 0;
	memset(sd, 0, sizeof(SESSION));

	if (++sid == 0) sid++; /* prevent 0 sid */
	sd->sid = sid;
	sd->sock = sock;
	if (peer)
		memcpy(&sd->peer, peer, sizeof(struct sockaddr));
	else	memset(&sd->peer, 0, sizeof(sd->peer));
	memset(&sd->from, 0, sizeof(sd->from));

	sd->type = type;

	/* make chain */
	if (next) sd->next = next;
	else if (prev) prev->next = sd;
	if (!first_session) first_session = sd;

	if (session_start(sd) < 0) {
		sd->sid = 0; /* this slot may be recycled later */
		sd = 0;
	}
	return sd;
}

int
session_start(sd)
	SESSION *sd;
{
	int af;

	/* sanity check */
	if (!sd) {
		errno = EINVAL;
		return -1;
	}
	errno = EBADF;
	if (sd->sock != -1 &&
	    (sd->type == PlainFile || socket_peer((struct sockaddr *)&sd->peer, sd->sock) != -1)) {
		/* already connected for example by accept() */

		socket_nonblock(sd->sock, 0);
		if (sd->type == TextStream)
			socket_keepalive(sd->sock, 1);
		return 0;
	}

	af = sd->peer.ss_family;
	if (!af) af = AF_INET; /* by default */

	if (errno == EBADF || errno == ENOTSOCK) {
		switch (sd->type) {
		case PlainFile:
			sd->sock = -1;
			errno = EINVAL;
			break;
		case TextStream:
			sd->sock = socket(af, SOCK_STREAM, 0);
			break;
		case DataSequence:
			sd->sock = socket(af, SOCK_DGRAM, 0);
			break;
		/* XXX other session types would be added here */
		}
		if (sd->sock == -1)
			return -1;

		errno = ENOTCONN;
	}
	if (errno == ENOTCONN) {
		/*
		 * Make socket `connected' for any type, so error on this
		 * socket will be returned asynchronously without timing out.
		 */
		socket_nonblock(sd->sock, 1);

		if (!sd->peer.ss_family) {
			errno = 0;
			return 0;
		}

		if (connect(sd->sock, (struct sockaddr *)&sd->peer, sizeof(struct sockaddr)) != -1 ||
		    errno == EINPROGRESS)
			return 0;
	}
	/* prevent lost of unused socket */
	session_stop(sd);

	return -1;
}

int
session_sock(sd)
	SESSION *sd;
{
	return (sd ? sd->sock : -1);
}

unsigned
session_settimeout(sd, timeout)
	SESSION *sd;
	unsigned timeout;
{
	unsigned prev;

	if (!sd || !sd->sid) return 0;

	prev = sd->timeout;
	sd->timeout = timeout;

	if (sd->timeout < 1)
		timerclear(&sd->expire);

	return prev;
}

void
session_setcallback(sd, connected, read_error, read_data)
	SESSION *sd;
	void (*connected)(SESSION *sd);
	void (*read_error)(SESSION *sd, int error);
	void (*read_data)(SESSION *sd, const unsigned char *data, int len);
{
	if (sd && sd->sid) {
		if (connected && sd->type == TextStream) {
			sd->connected = connected;
		}
		if (read_error)
			sd->read_error = read_error;
		if (read_data)
			sd->read_data = read_data;
	}
}

void
session_setcookie(sd, cookie)
	SESSION *sd;
	const void *cookie;
{
	if (sd && sd->sid) sd->cookie = cookie;
}

const void *
session_cookie(sd)
	SESSION *sd;
{
	return ((sd && sd->sid) ? sd->cookie : 0);
}

void
session_stop(sd)
	SESSION *sd;
{
	if (!sd) return;

	if (sd->sock != -1) {
		close(sd->sock);
		sd->sock = -1;
	}
	if (sd->buf) {
		free(sd->buf);
		sd->buf = 0;
	}
	timerclear(&sd->expire);
}

int
session_idle(sd)
	SESSION *sd;
{
	if (!sd || !sd->sid) return -1;
	return (sd->sock != -1 ? 0 : 1);
}

int
session_bind(sd, notify, arg)
	SESSION *sd;
	void (*notify)(void *arg);
	void *arg;
{
	SESSION_BINDER *curr, *last = 0;

	if (!sd || !notify || !arg) {
		errno = EINVAL;
		return -1;
	}
	/* prevent dups and find last */
	for (curr = sd->sb; curr; curr = curr->next) {
		if (curr->notify == notify && curr->arg == arg)
			return 0;
		last = curr;
	}
	if ((curr = (SESSION_BINDER *)malloc(sizeof(SESSION_BINDER))) == 0)
		return -1;
	curr->notify = notify;
	curr->arg = arg;
	curr->next = 0;
	if (last)
		last->next = curr;
	else	sd->sb = curr;
	return 0;
}

void
session_unbind(sd, notify, arg)
	SESSION *sd;
	void (*notify)(void *arg);
	void *arg;
{
	SESSION_BINDER *curr, *prev, *next;

	curr = (sd ? sd->sb : 0);
	prev = 0;
	while (curr) {
		if ((!notify && !arg) ||
		    (curr->notify == notify && curr->arg == arg)) {
			next = curr->next;
			if (prev)
				prev->next = next;
			else	sd->sb = next;
			free(curr);
			curr = next;
		} else {
			prev = curr;
			curr = curr->next;
		}
	}
}

/*
 * This function free all memory only when free_sd = 0 else it just reset
 * session id and does not free memory. The mean of this behaving is to
 * reuse/recycle session slots without new malloc (avoiding it overhead).
 */
void
session_free(free_sd)
	SESSION *free_sd;
{
	SESSION *sd, *prev, *next;
	SESSION_BINDER *sb;

	sd = first_session;
	prev = next = 0;
	while (sd) {
		if (!free_sd || sd == free_sd) {
			if (!free_sd) {
				next = sd->next;
				if (prev)
					prev->next = next;
				else	first_session = next;
			}

			for (sb = sd->sb; sb; sb = sb->next) {
				if (sb->notify && sb->arg)
					(*sb->notify)(sb->arg);
			}
			session_stop(sd);
			session_unbind(sd, 0, 0); /* to free all */

			if (!free_sd) {
				free(sd);
				sd = next;
				continue;
			}
			sd->sid = 0; /* this slot may be recycled later */
		}
		prev = sd;
		sd = sd->next;
	}
}

SESSION *
session_find(peer, type)
	const struct sockaddr *peer;
	SessionType type;
{
	SESSION *sd;

	/* sanity check */
	if (!peer) return 0;

	for (sd = first_session; sd; sd = sd->next) {
		if (!sd->sid || sd->type != type ||
		   sd->peer.ss_family != peer->sa_family)
			continue;

		if (peer->sa_family == AF_INET) {
			struct sockaddr_in *sin = (struct sockaddr_in *)&sd->peer;
			if (sin->sin_port == ((struct sockaddr_in *)peer)->sin_port &&
			    !memcmp(&sin->sin_addr,
				    &((struct sockaddr_in *)peer)->sin_addr,
				    sizeof(sin->sin_addr)))
				return sd;
		}
#ifdef	INET6
		else if (peer->sa_family == AF_INET6) {
			struct sockaddr_in6 *sin = (struct sockaddr_in6 *)&sd->peer;
			if (sin->sin6_port == ((struct sockaddr_in6 *)peer)->sin6_port &&
			    !memcmp(&sin->sin6_addr,
				    &((struct sockaddr_in6 *)peer)->sin6_addr,
				    sizeof(sin->sin6_addr)))
				return sd;
		}
#endif
	}
	return 0;
}

int
session_send(sd, data, len)
	SESSION *sd;
	const unsigned char *data;
	int len;
{
	int wlen = 0;

	if (!sd || len < 0) {
		errno = EINVAL;
		return -1;
	}
	if (!sd->sid || sd->sock == -1) {
		errno = ENOTCONN;
		return -1;
	}
	if (data) {
		if (sd->type == PlainFile) {
			if (len) wlen = write(sd->sock, data, len);

		} else if (sd->type == TextStream) {
			char buf[BUF_SIZE];

			if (!sd->peer.ss_family) {
				errno = ENOTCONN;
				return -1;
			}
			if (len > sizeof(buf)-2) len = sizeof(buf)-2;
			if (len) memcpy(buf, data, len);
			if (!len || buf[len-1] != '\n') {
				buf[len++] = '\r';
				buf[len++] = '\n';
			}
			wlen = write(sd->sock, buf, len);

		} else if (sd->type == DataSequence) {

			if (!sd->peer.ss_family) {
				errno = ENOTCONN;
				return -1;
			}
			if (len) wlen = send(sd->sock, data, len, 0);

		} else { /* XXX other session types must be added here */
			wlen = -1;
			errno = ESOCKTNOSUPPORT;
		}
	}
	if (wlen == -1) {
		if (errno == EAGAIN || errno == EINPROGRESS) {
			errno = 0;
			wlen = 0;
		} else if (sd->read_error) {
			(*sd->read_error)(sd, errno);
			return wlen;
		}
	}
	if (sd->timeout > 0) {
		gettimeofday(&sd->expire, 0);
		sd->expire.tv_sec += sd->timeout;
	}
	return wlen;
}

static int
session_read(sd)
	SESSION *sd;
{
	int rlen = 0, rest = 0;
	char *cp, *line, buf[BUF_SIZE];

	if (!sd) {
		errno = EINVAL;
		return -1;
	}
	if (!sd->sid || sd->sock == -1) {
		errno = ENOTCONN;
		return -1;
	}
	buf[0] = '\0';

	if (sd->type == PlainFile) {
		rlen = read(sd->sock, buf, sizeof(buf));

	} else if (sd->type == TextStream) {
		if (sd->buf) { /* previous line was truncated */
			rest = strlen(strcpy(buf, sd->buf));
			free(sd->buf);
			sd->buf = 0;
		}
		rlen = read(sd->sock, &buf[rest], (sizeof(buf)-1) - rest);

	} else if (sd->type == DataSequence) {
		struct sockaddr from;
		socklen_t slen = sizeof(from);

		rlen = recvfrom(sd->sock, buf, sizeof(buf), 0, &from, &slen);
		if (rlen != -1) {
			/* just for sanity */
			if (slen < sizeof(struct sockaddr_in) ||
			    slen > sizeof(struct sockaddr))
				return 0; /* should not happen */

			if (sd->peer.ss_family &&
			    sd->peer.ss_family != from.sa_family)
				return 0; /* bad family */

			/* save packet from */
			memcpy(&sd->from, &from, slen);
		} else	memset(&sd->from, 0, sizeof(sd->from));

	} else { /* XXX other session types must be added here */
		errno = ESOCKTNOSUPPORT;
		return -1;
	}

	if (rlen < 1) {
		if (!rlen || !errno)
			errno = ECONNRESET;
		return -1;
	}

	if (sd->type == PlainFile || sd->type == DataSequence) {
		if (!sd->sid || sd->sock == -1)
			return 0;
		if (sd->read_data)
			(*sd->read_data)(sd, (u_char *)buf, rlen);

	} else { /* TextStream */
		buf[rest + rlen] = '\0';
		for (cp = buf; (line = strchr(cp, '\n')) != 0; cp = line) {
			if (line > cp && line[-1] == '\r') line[-1] = '\0';
			*line++ = '\0';
			if (!sd->sid || sd->sock == -1)
				return 0;
			if (sd->read_data) {
				rest = strlen(cp);
				if (rest > MAX_STR_LEN) {
					errno = EMSGSIZE;
					return -1;
				}
				(*sd->read_data)(sd, (u_char *)cp, rest);
			}
		}
		if (cp && *cp) { /* truncated line, save it for next read */
			if (strlen(cp) > MAX_STR_LEN) {
				errno = EMSGSIZE;
				return -1;
			}
			sd->buf = strdup(cp);
		}
	}
	return rlen;
}

int
session_select(nfds, readfds, writefds, timeout, block)
	int *nfds;
	fd_set *readfds, *writefds;
	struct timeval *timeout;
	int *block;
{
	SESSION *sd;
	struct timeval earliest, now;
	int active = 0, pending = 0;

	timerclear(&earliest);

	/*
	 * For each request outstanding, add it's socket to the readfds,
	 * and if it is the earliest timeout to expire, mark it as lowest.
	 */
	for (sd = first_session; sd; sd = sd->next) {
		if (!sd->sid || sd->sock == -1) {
			if (sd->sock != -1) /* lost session? free socket */
				session_stop(sd);
			continue;
		}

		active++;
		if (sd->sock + 1 > *nfds)
			*nfds = sd->sock + 1;

		if (!sd->connected) {
			FD_SET(sd->sock, readfds);

			dprintf(("session_select: sock %d set for read", sd->sock));
		} else {
			FD_SET(sd->sock, writefds);

			dprintf(("session_select: sock %d set for write", sd->sock));
		}

		if (timerisset(&sd->expire)) {
			pending++;
			if (!timerisset(&earliest) ||
			    timercmp(&sd->expire, &earliest, <))
				earliest = sd->expire;
		}
	}

	/*dprintf(("session_select: active=%d pending=%d", active, pending));*/

	if (!pending)
		return active;

	/*
	 * Transforms earliest from an absolute time into a delta time, the
	 * time left until the select should timeout.
	 */
	gettimeofday(&now, 0);
	tv_sub(&earliest, &now);

	/* if it was blocking before or our delta time is less, reset timeout */
	if (*block || timercmp(&earliest, timeout, <)) {
		*timeout = earliest;
		*block = 0;
	}
	return active;
}

/*
 * Checks to see if any of the fd's set in the readfds belong to a session.
 */
void
session_operate(readfds, writefds)
	fd_set *readfds, *writefds;
{
	SESSION *sd;
	int try_conn, error;

	for (sd = first_session; sd; sd = sd->next) {
		if (!sd->sid || sd->sock == -1)
			continue;

		try_conn = (sd->connected != 0);

		if (!try_conn && FD_ISSET(sd->sock, readfds)) {

			dprintf(("session_operate: sock %d ready to read", sd->sock));

			if (sd->type == PlainFile)
				error = 0;
			else	error = socket_error(sd->sock);

			if (!error && session_read(sd) < 0)
				error = errno;
			if (error && sd->sid && sd->read_error)
				(*sd->read_error)(sd, error);
		}

		if (try_conn && FD_ISSET(sd->sock, writefds)) {

			dprintf(("session_operate: sock %d ready to write", sd->sock));

			error = socket_error(sd->sock);
			if (!error) {
				socket_peer((struct sockaddr *)&sd->peer, sd->sock);
				socket_nonblock(sd->sock, 0);
				if (sd->type == TextStream)
					socket_keepalive(sd->sock, 1);
				if (sd->sid && sd->connected)
					(*sd->connected)(sd);
				sd->connected = 0; /* fire a shot only once! */
			} else if (sd->sid && sd->read_error)
				(*sd->read_error)(sd, error);
		}
	}
}

/*
 * Checks to see if any of the sessions have an outstanding request
 * that has timed out.
 */
void
session_timeout()
{
	SESSION *sd;
	struct timeval now;

	gettimeofday(&now, 0);

	for (sd = first_session; sd; sd = sd->next) {
		if (!sd->sid || sd->sock == -1)
			continue;

		if (timerisset(&sd->expire) && timercmp(&sd->expire, &now, <)) {
			if (sd->read_error) (*sd->read_error)(sd, ETIMEDOUT);
		}
	}
}

/*
 * Return session peer pointer.
 */
const struct sockaddr *
session_peer(sd)
	SESSION *sd;
{
	return ((sd && sd->peer.ss_family) ? (struct sockaddr *)&sd->peer : 0);
}

/*
 * Return session from pointer.
 */
const struct sockaddr *
session_from(sd)
	SESSION *sd;
{
	return ((sd && sd->from.ss_family) ? (struct sockaddr *)&sd->from : 0);
}

/*
 * Return connected socket peer ip address and port.
 */
int
socket_peer(peer, sock)
	struct sockaddr *peer;
	int sock;
{
	socklen_t arglen;

	if (!peer) {
		errno = EINVAL;
		return -1;
	}
	arglen = sizeof(struct sockaddr);
	return getpeername(sock, peer, &arglen);
}

/*
 * Return socket name ip address and port.
 */
int
socket_name(name, sock)
	struct sockaddr *name;
	int sock;
{
	socklen_t arglen;

	if (!name) {
		errno = EINVAL;
		return -1;
	}
	arglen = sizeof(*name);
	return getsockname(sock, name, &arglen);
}

/*
 * Return socket error (like errno) in the session or 0 if no errors.
 */
int
socket_error(sock)
	int sock;
{
	int argbuf;
	socklen_t arglen;
	struct sockaddr peer;

	arglen = sizeof(argbuf);
	if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &argbuf, &arglen) < 0)
		return errno;
	if (argbuf)
		return argbuf;

	arglen = sizeof(argbuf);
	if (getsockopt(sock, SOL_SOCKET, SO_TYPE, &argbuf, &arglen) < 0)
		return errno;
	if (argbuf == SOCK_STREAM) {
		arglen = sizeof(peer);
		if (getpeername(sock, &peer, &arglen) < 0)
			return errno;
	}
	return 0;
}

/*
 * Make socket blocked or non-blocked for sync/async I/O.
 */
int
socket_nonblock(sock, on)
	int sock;
	int on; /* boolean */
{
	int mode;
	int prev; /* boolean */

	/* get current value of I/O mode */
	if ((mode = fcntl(sock, F_GETFL, 0)) < 0)
		return -1;

	prev = (mode & ASYNC_MODE) != 0;
	if (on != prev) {
		if (on)	mode |= ASYNC_MODE;
		else	mode &= ~ASYNC_MODE;
		if (fcntl(sock, F_SETFL, mode))
			return -1;
	}
	return prev;
}

int
socket_keepalive(sock, on)
	int sock, on;
{
#ifdef  SO_KEEPALIVE
	int curr = 0;
	socklen_t slen = sizeof(curr);
	if (getsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &curr, &slen) < 0)
		return -1;

	curr = (curr != 0);
	if (on != curr) {
		if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)) < 0)
			return -1;
	}
	return 0;
#else
	errno = ESOCKTNOSUPPORT;
	return -1;
#endif
}

