/*****************************************************************************\
 *  net.c - basic network communications for user application I/O
 *****************************************************************************
 *  Copyright (C) 2002 The Regents of the University of California.
 *  Produced at Lawrence Livermore National Laboratory (cf, DISCLAIMER).
 *  Written by Mark Grondona <grondona1@llnl.gov>, Kevin Tew <tew1@llnl.gov>,
 *  et. al.
 *  CODE-OCEC-09-009. All rights reserved.
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm 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 2 of the License, or (at your option)
 *  any later version.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>

#include "src/common/read_config.h"
#include "src/common/slurm_protocol_defs.h"
#include "src/common/xrandom.h"

#if defined(__FreeBSD__) || defined(__NetBSD__)
#define	SOL_TCP		IPPROTO_TCP
#endif

#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__)
#include <sys/param.h>
#include <sys/ucred.h>
#endif

#ifndef NI_MAXHOST
#define NI_MAXHOST 1025
#endif /* NI_MAXHOST */
#ifndef NI_MAXSERV
#define NI_MAXSERV 32
#endif /* NI_MAXSERV */

#define CON_NAME_PLACE_HOLDER_LEN 25

#include "src/common/log.h"
#include "src/common/macros.h"
#include "src/common/net.h"
#include "src/common/slurm_protocol_api.h"
#include "src/common/util-net.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"

/*
 * Define slurm-specific aliases for use by plugins, see slurm_xlator.h
 * for details.
 */
strong_alias(net_stream_listen,		slurm_net_stream_listen);

/* open a stream socket on an ephemeral port and put it into
 * the listen state. fd and port are filled in with the new
 * socket's file descriptor and port #.
 *
 * OUT fd - listening socket file descriptor number
 * OUT port - TCP port number in host byte order
 */
int net_stream_listen(int *fd, uint16_t *port)
{
	slurm_addr_t sin;
	socklen_t len = sizeof(sin);
	int val = 1;

	/* bind ephemeral port */
	slurm_setup_addr(&sin, 0);

	if ((*fd = socket(sin.ss_family, SOCK_STREAM, IPPROTO_TCP)) < 0)
		return -1;

	if (setsockopt(*fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0)
		goto cleanup;
	if (bind(*fd, (struct sockaddr *) &sin, len) < 0)
		goto cleanup;
	if (getsockname(*fd, (struct sockaddr *) &sin, &len) < 0)
		goto cleanup;

	*port = slurm_get_port(&sin);
	if (listen(*fd, SLURM_DEFAULT_LISTEN_BACKLOG) < 0)
		goto cleanup;

	return 1;

cleanup:
	close(*fd);
	return -1;
}

/* set keepalive time on socket */
extern void net_set_keep_alive(int sock)
{
	int opt_int;
	socklen_t opt_len;
	struct linger opt_linger;

	if (slurm_conf.keepalive_time == NO_VAL)
		return;

	opt_len = sizeof(struct linger);
	opt_linger.l_onoff = 1;
	opt_linger.l_linger = slurm_conf.keepalive_time;
	if (setsockopt(sock, SOL_SOCKET, SO_LINGER, &opt_linger, opt_len) < 0)
		error("Unable to set linger socket option: %m");

	opt_len = sizeof(opt_int);
	opt_int = slurm_conf.keepalive_time;
	if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt_int, opt_len) < 0) {
		error("Unable to set keepalive socket option: %m");
		return;
	}

/*
 * TCP_KEEPIDLE used to be defined in FreeBSD, then went away, then came
 * back in 9.0.
 *
 * Removing this call might decrease the robustness of communications,
 * but will probably have no noticeable effect.
 */
#if !defined (__APPLE__) && (! defined(__FreeBSD__) || (__FreeBSD_version > 900000))
	if (slurm_conf.keepalive_interval != NO_VAL) {
		opt_int = slurm_conf.keepalive_interval;
		if (setsockopt(sock, SOL_TCP, TCP_KEEPINTVL,
			       &opt_int, opt_len) < 0) {
			error("Unable to set keepalive interval: %m");
			return;
		}
	}
	if (slurm_conf.keepalive_probes != NO_VAL) {
		opt_int = (int) slurm_conf.keepalive_probes;
		if (setsockopt(sock, SOL_TCP, TCP_KEEPCNT,
			       &opt_int, opt_len) < 0) {
			error("Unable to set keepalive probes: %m");
			return;
		}
	}
	opt_int = slurm_conf.keepalive_time;
	if (setsockopt(sock, SOL_TCP, TCP_KEEPIDLE, &opt_int, opt_len) < 0) {
		error("Unable to set keepalive socket time: %m");
		return;
	}
#endif

#if 0
	/* Used to validate above operations for testing purposes */
	opt_linger.l_onoff = 0;
	opt_linger.l_linger = 0;
	opt_len = sizeof(struct linger);
	getsockopt(sock, SOL_SOCKET, SO_LINGER, &opt_linger, &opt_len);
	info("got linger time of %d:%d on fd %d", opt_linger.l_onoff,
	     opt_linger.l_linger, sock);

	opt_len = sizeof(opt_len);
	getsockopt(sock, SOL_TCP, TCP_KEEPINTVL, &opt_int, &opt_len);
	info("got keepalive_interval is %d on fd %d", opt_int, sock);
	getsockopt(sock, SOL_TCP, TCP_KEEPCNT, &opt_int, &opt_len);
	info("got keepalive_probes is %d on fd %d", opt_int, sock);
	getsockopt(sock, SOL_TCP, TCP_KEEPIDLE, &opt_int, &opt_len);
	info("got keepalive_time is %d on fd %d", opt_int, sock);
#endif
}

extern int net_set_nodelay(int sock, bool set, const char *con_name)
{
	int opt_int;

	if (sock < 0)
		return EBADF;

	if (set)
		opt_int = 1;
	else
		opt_int = 0;

	if (setsockopt(sock, SOL_TCP, TCP_NODELAY, &opt_int, sizeof(int))) {
		int rc = errno;
		char lcon_name[CON_NAME_PLACE_HOLDER_LEN] = {0};

		if (!con_name) {
			snprintf(lcon_name, sizeof(lcon_name), "fd:%d", sock);
			con_name = lcon_name;
		}

		error("[%s] Unable to set TCP_NODELAY: %s",
		      con_name, slurm_strerror(rc));

		return rc;
	}

	return SLURM_SUCCESS;
}

/*
 * Check if we can bind() the socket s to port port.
 *
 * IN: s - socket
 * IN: port - port number to attempt to bind
 * IN: local - only bind to localhost if true
 * OUT: true/false if port was bound successfully
 */
static bool _is_port_ok(int s, uint16_t port, bool local)
{
	slurm_addr_t addr;
	slurm_setup_addr(&addr, port);

	if (!local) {
		debug3("%s: requesting non-local port", __func__);
	} else if (addr.ss_family == AF_INET) {
		struct sockaddr_in *sin = (struct sockaddr_in *) &addr;
		sin->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	} else if (addr.ss_family == AF_INET6) {
		struct sockaddr_in6 *sin = (struct sockaddr_in6 *) &addr;
		sin->sin6_addr = in6addr_loopback;
	} else {
		error("%s: protocol family %u unsupported",
		      __func__, addr.ss_family);
		return false;
	}

	if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
		log_flag(NET, "%s: bind() failed on port:%d fd:%d: %m",
			 __func__, port, s);
		return false;
	}

	return true;
}

/* net_stream_listen_ports()
 */
int net_stream_listen_ports(int *fd, uint16_t *port, uint16_t *ports, bool local)
{
	slurm_addr_t sin;
	uint32_t min = ports[0], max = ports[1];
	uint32_t num = max - min + 1;

	xassert(num > 0);

	*port = min + (xrandom() % num);

	slurm_setup_addr(&sin, 0); /* Decide on IPv4 or IPv6 */

	*fd = -1;

	for (int i = 0; i < num; i++) {
		if (*fd < 0) {
			const int one = 1;

			if ((*fd = socket(sin.ss_family, SOCK_STREAM,
					  IPPROTO_TCP)) < 0) {
				log_flag(NET, "%s: socket() failed: %m",
					 __func__);
				return -1;
			}

			if (setsockopt(*fd, SOL_SOCKET, SO_REUSEADDR, &one,
				       sizeof(int)) < 0) {
				log_flag(NET, "%s: setsockopt() failed: %m",
					 __func__);
				close(*fd);
				return -1;
			}
		}

		if (_is_port_ok(*fd, *port, local)) {
			if (!listen(*fd, SLURM_DEFAULT_LISTEN_BACKLOG))
				return *fd;

			log_flag(NET, "%s: listen() failed: %m",
				 __func__);

			/*
			 * If bind() succeeds but listen() fails we need to
			 * close and reestablish the socket before trying
			 * again on another port number.
			 */
			if (close(*fd)) {
				log_flag(NET, "%s: close(%d) failed: %m",
					 __func__, *fd);
			}
			*fd = -1;
		}

		if (*port == max)
			*port = min;
		else
			++(*port);
	}

	if (*fd >= 0)
		close(*fd);

	error("%s: all ports in range (%u, %u) exhausted, cannot establish listening port",
	      __func__, min, max);

	return -1;
}

static const char *_ip_reserved_to_str(const slurm_addr_t *addr)
{
	if (addr->ss_family == AF_INET) {
		const struct sockaddr_in *addr4 = (struct sockaddr_in *) addr;
		const in_addr_t ipv4 = addr4->sin_addr.s_addr;

		if (ipv4 == INADDR_LOOPBACK)
			return "127.0.0.1";
		else if (ipv4 == INADDR_ANY)
			return "0.0.0.0";
		else if (ipv4 == INADDR_BROADCAST)
			return "255.255.255.255";
#ifdef INADDR_DUMMY
		else if (ipv4 == INADDR_DUMMY)
			return "192.0.0.8";
#endif /* INADDR_DUMMY */
	} else if (addr->ss_family == AF_INET6) {
		const struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *) addr;
		const struct in6_addr ipv6 = addr6->sin6_addr;

		/*
		 * RFC5156 Special-Use IPv6 Addresses
		 * defined by RFC4291 as "::" or "::1" formatted by RFC6874
		 * referencing RFC3986:
		 *	IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"
		 *
		 * RFC6874 Appendix A gives representing numeric IPv6address
		 * with brackets and without as both valid options. Returning
		 * addresses with brackets to avoid a constructed string with
		 * [::]:PORT over :::PORT and [::1]:PORT over ::1:PORT while
		 * both are valid.
		 */
		if (IN6_IS_ADDR_UNSPECIFIED(&ipv6)) {
			return "[::]";
		} else if (IN6_IS_ADDR_LOOPBACK(&ipv6))
			return "[::1]";
	}

	return NULL;
}

static char *_fmt_ip_host_port_str(const slurm_addr_t *addr, const char *host)
{
	/* Include 2 extra bytes for [] for IPv6 */
	static const size_t max_host_bytes =
		MAX(INET_ADDRSTRLEN, (INET6_ADDRSTRLEN + 2));
	char *resp = NULL;
	char nhost[max_host_bytes];
	uint16_t port = 0;

	if (addr->ss_family == AF_INET) {
		const struct sockaddr_in *in = (struct sockaddr_in *) addr;

		port = ntohs(in->sin_port);

		if (!host) {
			if (inet_ntop(AF_INET, &in->sin_addr, nhost,
				      sizeof(nhost))) {
				host = nhost;
			} else {
				/* this should never happen */
				log_flag_hex(NET, addr, sizeof(*addr),
					     "%s: inet_ntop(AF_INET) failed: %s",
					     slurm_strerror(errno));
				return NULL;
			}
		}
	} else if (addr->ss_family == AF_INET6) {
		const struct sockaddr_in6 *in6 = (struct sockaddr_in6 *) addr;

		port = ntohs(in6->sin6_port);

		if (!host) {
			if (inet_ntop(AF_INET6, &in6->sin6_addr, (nhost + 1),
				      (sizeof(nhost) - 2))) {
				const size_t len = strlen(nhost + 1);
				/*
				 * Construct RFC3986 host port pair:
				 * IP-literal = "[" ( IPv6address / IPvFuture  ) "]"
				 */
				nhost[0] = '[';
				nhost[len + 1] = ']';
				nhost[len + 2] = '\0';
				host = nhost;
			} else {
				/* this should never happen */
				log_flag_hex(NET, addr, sizeof(*addr),
					     "%s: inet_ntop(AF_INET6) failed: %s",
					     slurm_strerror(errno));
				return NULL;
			}
		}
	}

	/*
	 * RFC3986 definitions:
	 *	host        = IP-literal / IPv4address / reg-name
	 *	port        = *DIGIT
	 *	authority   = [ userinfo "@" ] host [ ":" port ]
	 *
	 * Where authority obsoletes the prior hostport from RFC2396:
	 *	hostport    = host [ ":" port ]
	 */
	if (host && port)
		xstrfmtcat(resp, "%s:%hu", host, port);
	else if (port)
		xstrfmtcat(resp, ":%hu", port);
	else if (host)
		xstrfmtcat(resp, "%s", host);

	return resp;
}

extern char *sockaddr_to_string(const slurm_addr_t *addr, socklen_t addrlen,
				bool resolve)
{
	int prev_errno = errno;
	char *resp = NULL;
	const char *rsv_host = NULL;

	if (addr->ss_family == AF_UNSPEC) {
		log_flag(NET, "%s: Cannot resolve socket's unspecified address family.",
			 __func__);
		return NULL;
	}

	if (addr->ss_family == AF_UNIX) {
		const struct sockaddr_un *addr_un =
			(const struct sockaddr_un *) addr;

		xassert(addr_un->sun_path[sizeof(addr_un->sun_path) - 1] ==
			'\0');

		/* path may not be set */
		if (addr_un->sun_path[0])
			return xstrdup_printf("unix:%s", addr_un->sun_path);
		else if (addr_un->sun_path[1]) /* abstract socket */
			return xstrdup_printf("unix:@%s",
					      &addr_un->sun_path[1]);
		else /* path not defined */
			return xstrdup_printf("unix:");
	}

	/* Check for reserved addresses that getnameinfo() won't resolve */
	if ((rsv_host = _ip_reserved_to_str(addr))) {
		resp = _fmt_ip_host_port_str(addr, rsv_host);
	} else if (resolve) {
		/* Attempt to resolve hostname */
		char *host = xgetnameinfo(addr);
		resp = _fmt_ip_host_port_str(addr, host);
		xfree(host);
	} else {
		/*
		 * Caller does not want a (potentially blocking) reverse DNS
		 * lookup; format the IP literal via inet_ntop() instead.
		 */
		resp = _fmt_ip_host_port_str(addr, NULL);
	}

	/*
	 * Avoid clobbering errno as this function is likely to be used for
	 * error logging, and stepping on errno prevents %m from working.
	 */
	errno = prev_errno;
	return resp;
}

extern char *addrinfo_to_string(const struct addrinfo *addr, bool resolve)
{
	return sockaddr_to_string((const slurm_addr_t *) addr->ai_addr,
				  addr->ai_addrlen, resolve);
}

extern slurm_addr_t sockaddr_from_unix_path(const char *path)
{
	slurm_addr_t addr = {
		.ss_family = AF_UNSPEC,
	};
	struct sockaddr_un *un = (struct sockaddr_un *) &addr;

	if (!path)
		return addr;

	if (strlcpy(un->sun_path, path, sizeof(un->sun_path)) != strlen(path))
		return addr;

	/* Did not overflow - set family to indicate success */
	addr.ss_family = AF_UNIX;
	return addr;
}

extern int net_get_peer(int fd, uid_t *cred_uid, gid_t *cred_gid,
			pid_t *cred_pid)
{
	/* set all pointers to invalid defaults */
	*cred_uid = SLURM_AUTH_NOBODY;
	*cred_gid = SLURM_AUTH_NOBODY;
	*cred_pid = 0;

#if !defined(__APPLE__) && !defined(__FreeBSD__) && !defined(__NetBSD__)
	struct ucred cred = {
		.uid = SLURM_AUTH_NOBODY,
		.gid = SLURM_AUTH_NOBODY,
		.pid = 0,
	};
	socklen_t len = sizeof(cred);

	if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len)) {
		log_flag(NET, "%s: [fd:%d] getsockopt(SO_PEERCRED) failed: %m",
			 __func__, fd);
		return ESLURM_AUTH_SOCKET_INVALID_PEER;
	}

	*cred_uid = cred.uid;
	*cred_gid = cred.gid;
	*cred_pid = cred.pid;
#else
	struct xucred cred = {
		.cr_uid = SLURM_AUTH_NOBODY,
		.cr_groups = { SLURM_AUTH_NOBODY, },
		.cr_pid = 0,
	};
	socklen_t len = sizeof(cred);

	if (getsockopt(fd, 0, LOCAL_PEERCRED, &cred, &len)) {
		log_flag(NET, "%s: [fd:%d] getsockopt(SO_PEERCRED) failed: %m",
			 __func__, fd);
		return ESLURM_AUTH_SOCKET_INVALID_PEER;
	}

	*cred_uid = cred.cr_uid;
	*cred_gid = cred.cr_groups[0];
	*cred_pid = cred.cr_pid;
#endif

	/* Sanity check returned creds */

	/* special user/group nobody is never allowed to authenticate */
	if (*cred_uid == SLURM_AUTH_NOBODY)
		return ESLURM_AUTH_NOBODY;
	if (*cred_gid == SLURM_AUTH_NOBODY)
		return ESLURM_AUTH_NOBODY;

	return SLURM_SUCCESS;
}
