/*-
 * Copyright (c) 2009-2010 The FreeBSD Foundation
 * Copyright (c) 2011 Pawel Jakub Dawidek <pawel@dawidek.net>
 * All rights reserved.
 *
 * This software was developed by Pawel Jakub Dawidek under sponsorship from
 * the FreeBSD Foundation.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD: release/9.1.0/sbin/hastd/proto_tcp.c 231017 2012-02-05 15:51:19Z trociny $");

#include <sys/param.h>	/* MAXHOSTNAMELEN */
#include <sys/socket.h>

#include <arpa/inet.h>

#include <netinet/in.h>
#include <netinet/tcp.h>

#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "pjdlog.h"
#include "proto_impl.h"
#include "subr.h"

#define	TCP_CTX_MAGIC	0x7c41c
struct tcp_ctx {
	int			tc_magic;
	struct sockaddr_storage	tc_sa;
	int			tc_fd;
	int			tc_side;
#define	TCP_SIDE_CLIENT		0
#define	TCP_SIDE_SERVER_LISTEN	1
#define	TCP_SIDE_SERVER_WORK	2
};

static int tcp_connect_wait(void *ctx, int timeout);
static void tcp_close(void *ctx);

/*
 * Function converts the given string to unsigned number.
 */
static int
numfromstr(const char *str, intmax_t minnum, intmax_t maxnum, intmax_t *nump)
{
	intmax_t digit, num;

	if (str[0] == '\0')
		goto invalid;	/* Empty string. */
	num = 0;
	for (; *str != '\0'; str++) {
		if (*str < '0' || *str > '9')
			goto invalid;	/* Non-digit character. */
		digit = *str - '0';
		if (num > num * 10 + digit)
			goto invalid;	/* Overflow. */
		num = num * 10 + digit;
		if (num > maxnum)
			goto invalid;	/* Too big. */
	}
	if (num < minnum)
		goto invalid;	/* Too small. */
	*nump = num;
	return (0);
invalid:
	errno = EINVAL;
	return (-1);
}

static int
tcp_addr(const char *addr, int defport, struct sockaddr_storage *sap)
{
	char iporhost[MAXHOSTNAMELEN], portstr[6];
	struct addrinfo hints;
	struct addrinfo *res;
	const char *pp;
	intmax_t port;
	size_t size;
	int error;

	if (addr == NULL)
		return (-1);

	bzero(&hints, sizeof(hints));
	hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	if (strncasecmp(addr, "tcp4://", 7) == 0) {
		addr += 7;
		hints.ai_family = PF_INET;
	} else if (strncasecmp(addr, "tcp6://", 7) == 0) {
		addr += 7;
		hints.ai_family = PF_INET6;
	} else if (strncasecmp(addr, "tcp://", 6) == 0) {
		addr += 6;
	} else {
		/*
		 * Because TCP is the default assume IP or host is given without
		 * prefix.
		 */
	}

	/*
	 * Extract optional port.
	 * There are three cases to consider.
	 * 1. hostname with port, eg. freefall.freebsd.org:8457
	 * 2. IPv4 address with port, eg. 192.168.0.101:8457
	 * 3. IPv6 address with port, eg. [fe80::1]:8457
	 * We discover IPv6 address by checking for two colons and if port is
	 * given, the address has to start with [.
	 */
	pp = NULL;
	if (strchr(addr, ':') != strrchr(addr, ':')) {
		if (addr[0] == '[')
			pp = strrchr(addr, ':');
	} else {
		pp = strrchr(addr, ':');
	}
	if (pp == NULL) {
		/* Port not given, use the default. */
		port = defport;
	} else {
		if (numfromstr(pp + 1, 1, 65535, &port) == -1)
			return (errno);
	}
	(void)snprintf(portstr, sizeof(portstr), "%jd", (intmax_t)port);
	/* Extract host name or IP address. */
	if (pp == NULL) {
		size = sizeof(iporhost);
		if (strlcpy(iporhost, addr, size) >= size)
			return (ENAMETOOLONG);
	} else if (addr[0] == '[' && pp[-1] == ']') {
		size = (size_t)(pp - addr - 2 + 1);
		if (size > sizeof(iporhost))
			return (ENAMETOOLONG);
		(void)strlcpy(iporhost, addr + 1, size);
	} else {
		size = (size_t)(pp - addr + 1);
		if (size > sizeof(iporhost))
			return (ENAMETOOLONG);
		(void)strlcpy(iporhost, addr, size);
	}

	error = getaddrinfo(iporhost, portstr, &hints, &res);
	if (error != 0) {
		pjdlog_debug(1, "getaddrinfo(%s, %s) failed: %s.", iporhost,
		    portstr, gai_strerror(error));
		return (EINVAL);
	}
	if (res == NULL)
		return (ENOENT);

	memcpy(sap, res->ai_addr, res->ai_addrlen);

	freeaddrinfo(res);

	return (0);
}

static int
tcp_setup_new(const char *addr, int side, void **ctxp)
{
	struct tcp_ctx *tctx;
	int ret, nodelay;

	PJDLOG_ASSERT(addr != NULL);
	PJDLOG_ASSERT(side == TCP_SIDE_CLIENT ||
	    side == TCP_SIDE_SERVER_LISTEN);
	PJDLOG_ASSERT(ctxp != NULL);

	tctx = malloc(sizeof(*tctx));
	if (tctx == NULL)
		return (errno);

	/* Parse given address. */
	if ((ret = tcp_addr(addr, PROTO_TCP_DEFAULT_PORT, &tctx->tc_sa)) != 0) {
		free(tctx);
		return (ret);
	}

	PJDLOG_ASSERT(tctx->tc_sa.ss_family != AF_UNSPEC);

	tctx->tc_fd = socket(tctx->tc_sa.ss_family, SOCK_STREAM, 0);
	if (tctx->tc_fd == -1) {
		ret = errno;
		free(tctx);
		return (ret);
	}

	PJDLOG_ASSERT(tctx->tc_sa.ss_family != AF_UNSPEC);

	/* Socket settings. */
	nodelay = 1;
	if (setsockopt(tctx->tc_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay,
	    sizeof(nodelay)) == -1) {
		pjdlog_errno(LOG_WARNING, "Unable to set TCP_NOELAY");
	}

	tctx->tc_side = side;
	tctx->tc_magic = TCP_CTX_MAGIC;
	*ctxp = tctx;

	return (0);
}

static int
tcp_setup_wrap(int fd, int side, void **ctxp)
{
	struct tcp_ctx *tctx;

	PJDLOG_ASSERT(fd >= 0);
	PJDLOG_ASSERT(side == TCP_SIDE_CLIENT ||
	    side == TCP_SIDE_SERVER_WORK);
	PJDLOG_ASSERT(ctxp != NULL);

	tctx = malloc(sizeof(*tctx));
	if (tctx == NULL)
		return (errno);

	tctx->tc_fd = fd;
	tctx->tc_sa.ss_family = AF_UNSPEC;
	tctx->tc_side = side;
	tctx->tc_magic = TCP_CTX_MAGIC;
	*ctxp = tctx;

	return (0);
}

static int
tcp_client(const char *srcaddr, const char *dstaddr, void **ctxp)
{
	struct tcp_ctx *tctx;
	struct sockaddr_storage sa;
	int ret;

	ret = tcp_setup_new(dstaddr, TCP_SIDE_CLIENT, ctxp);
	if (ret != 0)
		return (ret);
	tctx = *ctxp;
	if (srcaddr == NULL)
		return (0);
	ret = tcp_addr(srcaddr, 0, &sa);
	if (ret != 0) {
		tcp_close(tctx);
		return (ret);
	}
	if (bind(tctx->tc_fd, (struct sockaddr *)&sa, sa.ss_len) == -1) {
		ret = errno;
		tcp_close(tctx);
		return (ret);
	}
	return (0);
}

static int
tcp_connect(void *ctx, int timeout)
{
	struct tcp_ctx *tctx = ctx;
	int error, flags;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);
	PJDLOG_ASSERT(tctx->tc_side == TCP_SIDE_CLIENT);
	PJDLOG_ASSERT(tctx->tc_fd >= 0);
	PJDLOG_ASSERT(tctx->tc_sa.ss_family != AF_UNSPEC);
	PJDLOG_ASSERT(timeout >= -1);

	flags = fcntl(tctx->tc_fd, F_GETFL);
	if (flags == -1) {
		pjdlog_common(LOG_DEBUG, 1, errno, "fcntl(F_GETFL) failed");
		return (errno);
	}
	/*
	 * We make socket non-blocking so we can handle connection timeout
	 * manually.
	 */
	flags |= O_NONBLOCK;
	if (fcntl(tctx->tc_fd, F_SETFL, flags) == -1) {
		pjdlog_common(LOG_DEBUG, 1, errno,
		    "fcntl(F_SETFL, O_NONBLOCK) failed");
		return (errno);
	}

	if (connect(tctx->tc_fd, (struct sockaddr *)&tctx->tc_sa,
	    tctx->tc_sa.ss_len) == 0) {
		if (timeout == -1)
			return (0);
		error = 0;
		goto done;
	}
	if (errno != EINPROGRESS) {
		error = errno;
		pjdlog_common(LOG_DEBUG, 1, errno, "connect() failed");
		goto done;
	}
	if (timeout == -1)
		return (0);
	return (tcp_connect_wait(ctx, timeout));
done:
	flags &= ~O_NONBLOCK;
	if (fcntl(tctx->tc_fd, F_SETFL, flags) == -1) {
		if (error == 0)
			error = errno;
		pjdlog_common(LOG_DEBUG, 1, errno,
		    "fcntl(F_SETFL, ~O_NONBLOCK) failed");
	}
	return (error);
}

static int
tcp_connect_wait(void *ctx, int timeout)
{
	struct tcp_ctx *tctx = ctx;
	struct timeval tv;
	fd_set fdset;
	socklen_t esize;
	int error, flags, ret;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);
	PJDLOG_ASSERT(tctx->tc_side == TCP_SIDE_CLIENT);
	PJDLOG_ASSERT(tctx->tc_fd >= 0);
	PJDLOG_ASSERT(timeout >= 0);

	tv.tv_sec = timeout;
	tv.tv_usec = 0;
again:
	FD_ZERO(&fdset);
	FD_SET(tctx->tc_fd, &fdset);
	ret = select(tctx->tc_fd + 1, NULL, &fdset, NULL, &tv);
	if (ret == 0) {
		error = ETIMEDOUT;
		goto done;
	} else if (ret == -1) {
		if (errno == EINTR)
			goto again;
		error = errno;
		pjdlog_common(LOG_DEBUG, 1, errno, "select() failed");
		goto done;
	}
	PJDLOG_ASSERT(ret > 0);
	PJDLOG_ASSERT(FD_ISSET(tctx->tc_fd, &fdset));
	esize = sizeof(error);
	if (getsockopt(tctx->tc_fd, SOL_SOCKET, SO_ERROR, &error,
	    &esize) == -1) {
		error = errno;
		pjdlog_common(LOG_DEBUG, 1, errno,
		    "getsockopt(SO_ERROR) failed");
		goto done;
	}
	if (error != 0) {
		pjdlog_common(LOG_DEBUG, 1, error,
		    "getsockopt(SO_ERROR) returned error");
		goto done;
	}
	error = 0;
done:
	flags = fcntl(tctx->tc_fd, F_GETFL);
	if (flags == -1) {
		if (error == 0)
			error = errno;
		pjdlog_common(LOG_DEBUG, 1, errno, "fcntl(F_GETFL) failed");
		return (error);
	}
	flags &= ~O_NONBLOCK;
	if (fcntl(tctx->tc_fd, F_SETFL, flags) == -1) {
		if (error == 0)
			error = errno;
		pjdlog_common(LOG_DEBUG, 1, errno,
		    "fcntl(F_SETFL, ~O_NONBLOCK) failed");
	}
	return (error);
}

static int
tcp_server(const char *addr, void **ctxp)
{
	struct tcp_ctx *tctx;
	int ret, val;

	ret = tcp_setup_new(addr, TCP_SIDE_SERVER_LISTEN, ctxp);
	if (ret != 0)
		return (ret);

	tctx = *ctxp;

	val = 1;
	/* Ignore failure. */
	(void)setsockopt(tctx->tc_fd, SOL_SOCKET, SO_REUSEADDR, &val,
	   sizeof(val));

	PJDLOG_ASSERT(tctx->tc_sa.ss_family != AF_UNSPEC);

	if (bind(tctx->tc_fd, (struct sockaddr *)&tctx->tc_sa,
	    tctx->tc_sa.ss_len) == -1) {
		ret = errno;
		tcp_close(tctx);
		return (ret);
	}
	if (listen(tctx->tc_fd, 8) == -1) {
		ret = errno;
		tcp_close(tctx);
		return (ret);
	}

	return (0);
}

static int
tcp_accept(void *ctx, void **newctxp)
{
	struct tcp_ctx *tctx = ctx;
	struct tcp_ctx *newtctx;
	socklen_t fromlen;
	int ret;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);
	PJDLOG_ASSERT(tctx->tc_side == TCP_SIDE_SERVER_LISTEN);
	PJDLOG_ASSERT(tctx->tc_fd >= 0);
	PJDLOG_ASSERT(tctx->tc_sa.ss_family != AF_UNSPEC);

	newtctx = malloc(sizeof(*newtctx));
	if (newtctx == NULL)
		return (errno);

	fromlen = tctx->tc_sa.ss_len;
	newtctx->tc_fd = accept(tctx->tc_fd, (struct sockaddr *)&tctx->tc_sa,
	    &fromlen);
	if (newtctx->tc_fd == -1) {
		ret = errno;
		free(newtctx);
		return (ret);
	}

	newtctx->tc_side = TCP_SIDE_SERVER_WORK;
	newtctx->tc_magic = TCP_CTX_MAGIC;
	*newctxp = newtctx;

	return (0);
}

static int
tcp_wrap(int fd, bool client, void **ctxp)
{

	return (tcp_setup_wrap(fd,
	    client ? TCP_SIDE_CLIENT : TCP_SIDE_SERVER_WORK, ctxp));
}

static int
tcp_send(void *ctx, const unsigned char *data, size_t size, int fd)
{
	struct tcp_ctx *tctx = ctx;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);
	PJDLOG_ASSERT(tctx->tc_fd >= 0);
	PJDLOG_ASSERT(fd == -1);

	return (proto_common_send(tctx->tc_fd, data, size, -1));
}

static int
tcp_recv(void *ctx, unsigned char *data, size_t size, int *fdp)
{
	struct tcp_ctx *tctx = ctx;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);
	PJDLOG_ASSERT(tctx->tc_fd >= 0);
	PJDLOG_ASSERT(fdp == NULL);

	return (proto_common_recv(tctx->tc_fd, data, size, NULL));
}

static int
tcp_descriptor(const void *ctx)
{
	const struct tcp_ctx *tctx = ctx;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);

	return (tctx->tc_fd);
}

static bool
tcp_address_match(const void *ctx, const char *addr)
{
	const struct tcp_ctx *tctx = ctx;
	struct sockaddr_storage sa1, sa2;
	socklen_t salen;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);

	if (tcp_addr(addr, PROTO_TCP_DEFAULT_PORT, &sa1) != 0)
		return (false);

	salen = sizeof(sa2);
	if (getpeername(tctx->tc_fd, (struct sockaddr *)&sa2, &salen) == -1)
		return (false);

	if (sa1.ss_family != sa2.ss_family || sa1.ss_len != sa2.ss_len)
		return (false);

	switch (sa1.ss_family) {
	case AF_INET:
	    {
		struct sockaddr_in *sin1, *sin2;

		sin1 = (struct sockaddr_in *)&sa1;
		sin2 = (struct sockaddr_in *)&sa2;

		return (memcmp(&sin1->sin_addr, &sin2->sin_addr,
		    sizeof(sin1->sin_addr)) == 0);
	    }
	case AF_INET6:
	    {
		struct sockaddr_in6 *sin1, *sin2;

		sin1 = (struct sockaddr_in6 *)&sa1;
		sin2 = (struct sockaddr_in6 *)&sa2;

		return (memcmp(&sin1->sin6_addr, &sin2->sin6_addr,
		    sizeof(sin1->sin6_addr)) == 0);
	    }
	default:
		return (false);
	}
}

static void
tcp_local_address(const void *ctx, char *addr, size_t size)
{
	const struct tcp_ctx *tctx = ctx;
	struct sockaddr_storage sa;
	socklen_t salen;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);

	salen = sizeof(sa);
	if (getsockname(tctx->tc_fd, (struct sockaddr *)&sa, &salen) == -1) {
		PJDLOG_VERIFY(strlcpy(addr, "N/A", size) < size);
		return;
	}
	PJDLOG_VERIFY(snprintf(addr, size, "tcp://%S", &sa) < (ssize_t)size);
}

static void
tcp_remote_address(const void *ctx, char *addr, size_t size)
{
	const struct tcp_ctx *tctx = ctx;
	struct sockaddr_storage sa;
	socklen_t salen;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);

	salen = sizeof(sa);
	if (getpeername(tctx->tc_fd, (struct sockaddr *)&sa, &salen) == -1) {
		PJDLOG_VERIFY(strlcpy(addr, "N/A", size) < size);
		return;
	}
	PJDLOG_VERIFY(snprintf(addr, size, "tcp://%S", &sa) < (ssize_t)size);
}

static void
tcp_close(void *ctx)
{
	struct tcp_ctx *tctx = ctx;

	PJDLOG_ASSERT(tctx != NULL);
	PJDLOG_ASSERT(tctx->tc_magic == TCP_CTX_MAGIC);

	if (tctx->tc_fd >= 0)
		close(tctx->tc_fd);
	tctx->tc_magic = 0;
	free(tctx);
}

static struct proto tcp_proto = {
	.prt_name = "tcp",
	.prt_client = tcp_client,
	.prt_connect = tcp_connect,
	.prt_connect_wait = tcp_connect_wait,
	.prt_server = tcp_server,
	.prt_accept = tcp_accept,
	.prt_wrap = tcp_wrap,
	.prt_send = tcp_send,
	.prt_recv = tcp_recv,
	.prt_descriptor = tcp_descriptor,
	.prt_address_match = tcp_address_match,
	.prt_local_address = tcp_local_address,
	.prt_remote_address = tcp_remote_address,
	.prt_close = tcp_close
};

static __constructor void
tcp_ctor(void)
{

	proto_register(&tcp_proto, true);
}
