/*
 * Copyright 2009 Oracle.  All rights reserved.
 *
 * This file is part of nfs-utils.
 *
 * nfs-utils 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.
 *
 * nfs-utils 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 nfs-utils.  If not, see <http://www.gnu.org/licenses/>.
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <netdb.h>
#include "nfslib.h"

#include <netinet/in.h>

#include <sys/socket.h>
#include <sys/resource.h>

#include <rpc/rpc.h>
#include <rpc/svc.h>

#ifdef HAVE_TCP_WRAPPER
#include "tcpwrapper.h"
#endif

#include "sockaddr.h"
#include "rpcmisc.h"
#include "xlog.h"

#ifdef HAVE_LIBTIRPC

#include <rpc/rpc_com.h>

#define SVC_CREATE_XPRT_CACHE_SIZE	(8)
static SVCXPRT *svc_create_xprt_cache[SVC_CREATE_XPRT_CACHE_SIZE] = { NULL, };

/*
 * Cache an SVC xprt, in case there are more programs or versions to
 * register against it.
 */
static void
svc_create_cache_xprt(SVCXPRT *xprt)
{
	unsigned int i;

	/* Check if we've already got this one... */
	for (i = 0; i < SVC_CREATE_XPRT_CACHE_SIZE; i++)
		if (svc_create_xprt_cache[i] == xprt)
			return;

	/* No, we don't.  Cache it. */
	for (i = 0; i < SVC_CREATE_XPRT_CACHE_SIZE; i++)
		if (svc_create_xprt_cache[i] == NULL) {
			svc_create_xprt_cache[i] = xprt;
			return;
		}

	xlog(L_ERROR, "%s: Failed to cache an xprt", __func__);
}

/*
 * Find a previously cached SVC xprt structure with the given bind address
 * and transport semantics.
 *
 * Returns pointer to a cached SVC xprt.
 *
 * If no matching SVC XPRT can be found, NULL is returned.
 */
static SVCXPRT *
svc_create_find_xprt(const struct sockaddr *bindaddr, const struct netconfig *nconf)
{
	unsigned int i;

	for (i = 0; i < SVC_CREATE_XPRT_CACHE_SIZE; i++) {
		SVCXPRT *xprt = svc_create_xprt_cache[i];
		struct sockaddr *sap;

		if (xprt == NULL)
			continue;
		if (strcmp(nconf->nc_netid, xprt->xp_netid) != 0)
			continue;
		sap = (struct sockaddr *)xprt->xp_ltaddr.buf;
		if (!nfs_compare_sockaddr(bindaddr, sap))
			continue;
		return xprt;
	}
	return NULL;
}

/*
 * Set up an appropriate bind address, given @port and @nconf.
 *
 * Returns getaddrinfo(3) results if successful.  Caller must
 * invoke freeaddrinfo(3) on these results.
 *
 * Otherwise NULL is returned if an error occurs.
 */
__attribute__((__malloc__))
static struct addrinfo *
svc_create_bindaddr(struct netconfig *nconf, const uint16_t port)
{
	struct addrinfo *ai = NULL;
	struct addrinfo hint = {
		.ai_flags	= AI_PASSIVE | AI_NUMERICSERV,
	};
	char buf[8];
	int error;

	if (strcmp(nconf->nc_protofmly, NC_INET) == 0)
		hint.ai_family = AF_INET;
#ifdef IPV6_SUPPORTED
	else if (strcmp(nconf->nc_protofmly, NC_INET6) == 0)
		hint.ai_family = AF_INET6;
#endif	/* IPV6_SUPPORTED */
	else {
		xlog(L_ERROR, "Unrecognized bind address family: %s",
			nconf->nc_protofmly);
		return NULL;
	}

	if (strcmp(nconf->nc_proto, NC_UDP) == 0)
		hint.ai_protocol = (int)IPPROTO_UDP;
	else if (strcmp(nconf->nc_proto, NC_TCP) == 0)
		hint.ai_protocol = (int)IPPROTO_TCP;
	else {
		xlog(L_ERROR, "Unrecognized bind address protocol: %s",
			nconf->nc_proto);
		return NULL;
	}

	(void)snprintf(buf, sizeof(buf), "%u", port);
	error = getaddrinfo(NULL, buf, &hint, &ai);
	if (error != 0) {
		xlog(L_ERROR, "Failed to construct bind address: %s",
			gai_strerror(error));
		return NULL;
	}

	return ai;
}

/*
 * Create a listener socket on a specific bindaddr, and set
 * special socket options to allow it to share the same port
 * as other listeners.
 *
 * Returns an open, bound, and possibly listening network
 * socket on success.
 *
 * Otherwise returns -1 if some error occurs.
 */
static int
svc_create_sock(const struct sockaddr *sap, socklen_t salen,
		struct netconfig *nconf)
{
	int fd, type, protocol;
	int one = 1;

	switch(nconf->nc_semantics) {
	case NC_TPI_CLTS:
		type = SOCK_DGRAM;
		break;
	case NC_TPI_COTS_ORD:
		type = SOCK_STREAM;
		break;
	default:
		xlog(D_GENERAL, "%s: Unrecognized bind address semantics: %lu",
			__func__, nconf->nc_semantics);
		return -1;
	}

	if (strcmp(nconf->nc_proto, NC_UDP) == 0)
		protocol = (int)IPPROTO_UDP;
	else if (strcmp(nconf->nc_proto, NC_TCP) == 0)
		protocol = (int)IPPROTO_TCP;
	else {
		xlog(D_GENERAL, "%s: Unrecognized bind address protocol: %s",
			__func__, nconf->nc_proto);
		return -1;
	}

	fd = socket((int)sap->sa_family, type, protocol);
	if (fd == -1) {
		xlog(L_ERROR, "Could not make a socket: (%d) %m",
			errno);
		return -1;
	}

#ifdef IPV6_SUPPORTED
	if (sap->sa_family == AF_INET6) {
		if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY,
				&one, sizeof(one)) == -1) {
			xlog(L_ERROR, "Failed to set IPV6_V6ONLY: (%d) %m",
				errno);
			(void)close(fd);
			return -1;
		}
	}
#endif	/* IPV6_SUPPORTED */

	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
		       &one, sizeof(one)) == -1) {
		xlog(L_ERROR, "Failed to set SO_REUSEADDR: (%d) %m",
			errno);
		(void)close(fd);
		return -1;
	}

	if (bind(fd, sap, salen) == -1) {
		xlog(L_ERROR, "Could not bind socket: (%d) %m",
			errno);
		(void)close(fd);
		return -1;
	}

	if (nconf->nc_semantics == NC_TPI_COTS_ORD)
		if (listen(fd, SOMAXCONN) == -1) {
			xlog(L_ERROR, "Could not listen on socket: (%d) %m",
				errno);
			(void)close(fd);
			return -1;
		}

	return fd;
}

/*
 * The simple case is allowing the TI-RPC library to create a
 * transport itself, given just the bind address and transport
 * semantics.
 *
 * Our local xprt cache is ignored in this path, since the
 * caller is not interested in sharing listeners or ports, and
 * the library automatically avoids ports already in use.
 *
 * Returns the count of started listeners (one or zero).
 */
static unsigned int
svc_create_nconf_rand_port(const char *name, const rpcprog_t program,
		const rpcvers_t version,
		void (*dispatch)(struct svc_req *, SVCXPRT *),
		struct netconfig *nconf)
{
	struct t_bind bindaddr;
	struct addrinfo *ai;
	SVCXPRT	*xprt;

	ai = svc_create_bindaddr(nconf, 0);
	if (ai == NULL)
		return 0;

	bindaddr.addr.buf = ai->ai_addr;
	bindaddr.qlen = SOMAXCONN;

	xprt = svc_tli_create(RPC_ANYFD, nconf, &bindaddr, 0, 0);
	nfs_freeaddrinfo(ai);
	if (xprt == NULL) {
		xlog(L_ERROR, "Failed to create listener xprt "
			"(%s, %u, %s)", name, version, nconf->nc_netid);
		return 0;
	}
	if (svcsock_nonblock(xprt->xp_fd) < 0) {
		/* close() already done by svcsock_nonblock() */
		xprt->xp_fd = RPC_ANYFD;
		SVC_DESTROY(xprt);
		return 0;
	}

	rpc_createerr.cf_stat = rpc_createerr.cf_error.re_errno = 0;
	if (!svc_reg(xprt, program, version, dispatch, nconf)) {
		/* svc_reg(3) destroys @xprt in this case */
		xlog(L_ERROR, "Failed to register (%s, %u, %s): %s",
				name, version, nconf->nc_netid, 
				clnt_spcreateerror("svc_reg() err"));
		return 0;
	}

	return 1;
}

/*
 * If a port is specified on the command line, that port value will be
 * the same for all listeners created here.  Create each listener
 * socket in advance and set SO_REUSEADDR, rather than allowing the
 * RPC library to create the listeners for us on a randomly chosen
 * port via svc_tli_create(RPC_ANYFD).
 *
 * Some callers want to listen for more than one RPC version using the
 * same port number.  For example, mountd could want to listen for MNT
 * version 1, 2, and 3 requests.  This means mountd must use the same
 * set of listener sockets for multiple RPC versions, since, on one
 * system, you can't have two listener sockets with the exact same
 * bind address (and port) and transport protocol.
 *
 * To accomplish this, this function caches xprts as they are created.
 * This cache is checked to see if a previously created xprt can be
 * used, before creating a new xprt for this [program, version].  If
 * there is a cached xprt with the same bindaddr and transport
 * semantics, we simply register the new version with that xprt,
 * rather than creating a fresh xprt for it.
 *
 * The xprt cache implemented here is local to a process.  Two
 * separate RPC daemons can not share a set of listeners.
 *
 * Returns the count of started listeners (one or zero).
 */
static unsigned int
svc_create_nconf_fixed_port(const char *name, const rpcprog_t program,
		const rpcvers_t version,
		void (*dispatch)(struct svc_req *, SVCXPRT *),
		const uint16_t port, struct netconfig *nconf)
{
	struct addrinfo *ai;
	SVCXPRT	*xprt;

	ai = svc_create_bindaddr(nconf, port);
	if (ai == NULL)
		return 0;

	xprt = svc_create_find_xprt(ai->ai_addr, nconf);
	if (xprt == NULL) {
		int fd;

		fd = svc_create_sock(ai->ai_addr, ai->ai_addrlen, nconf);
		fd = svcsock_nonblock(fd);
		if (fd == -1)
			goto out_free;

		xprt = svc_tli_create(fd, nconf, NULL, 0, 0);
		if (xprt == NULL) {
			xlog(D_GENERAL, "Failed to create listener xprt "
				"(%s, %u, %s)", name, version, nconf->nc_netid);
			(void)close(fd);
			goto out_free;
		}
	}

	if (!svc_reg(xprt, program, version, dispatch, nconf)) {
		/* svc_reg(3) destroys @xprt in this case */
		xlog(D_GENERAL, "Failed to register (%s, %u, %s)",
				name, version, nconf->nc_netid);
		goto out_free;
	}

	svc_create_cache_xprt(xprt);

	nfs_freeaddrinfo(ai);
	return 1;

out_free:
	nfs_freeaddrinfo(ai);
	return 0;
}

static unsigned int
svc_create_nconf(const char *name, const rpcprog_t program,
		const rpcvers_t version,
		void (*dispatch)(struct svc_req *, SVCXPRT *),
		const uint16_t port, struct netconfig *nconf)
{
	if (port != 0)
		return svc_create_nconf_fixed_port(name, program,
			version, dispatch, port, nconf);

	return svc_create_nconf_rand_port(name, program,
			version, dispatch, nconf);
}

/**
 * nfs_svc_create - start up RPC svc listeners
 * @name: C string containing name of new service
 * @program: RPC program number to register
 * @version: RPC version number to register
 * @dispatch: address of function that handles incoming RPC requests
 * @port: if not zero, transport listens on this port
 *
 * Sets up network transports for receiving RPC requests, and starts
 * the RPC dispatcher.  Returns the number of started network transports.
 */
unsigned int
nfs_svc_create(char *name, const rpcprog_t program, const rpcvers_t version,
		void (*dispatch)(struct svc_req *, SVCXPRT *),
		const uint16_t port)
{
	const struct sigaction create_sigaction = {
		.sa_handler	= SIG_IGN,
	};
	int maxrec = RPC_MAXDATASIZE;
	unsigned int visible, up, servport;
	struct netconfig *nconf;
	void *handlep;

	/*
	 * Ignore SIGPIPE to avoid exiting sideways when peers
	 * close their TCP connection while we're trying to reply
	 * to them.
	 */
	(void)sigaction(SIGPIPE, &create_sigaction, NULL);

	/*
	 * Setting MAXREC also enables non-blocking mode for tcp connections.
	 * This avoids DOS attacks by a client sending many requests but never
	 * reading the reply:
	 * - if a second request already is present for reading in the socket,
	 *   after the first request just was read, libtirpc will break the
	 *   connection. Thus an attacker can't simply send requests as fast as
	 *   he can without waiting for the response.
	 * - if the write buffer of the socket is full, the next write() will
	 *   fail with EAGAIN. libtirpc will retry the write in a loop for max.
	 *   2 seconds. If write still fails, the connection will be closed.
	 */   
	rpc_control(RPC_SVC_CONNMAXREC_SET, &maxrec);

	handlep = setnetconfig();
	if (handlep == NULL) {
		xlog(L_ERROR, "Failed to access local netconfig database: %s",
			nc_sperror());
		return 0;
	}

	visible = 0;
	up = 0;
	while ((nconf = getnetconfig(handlep)) != NULL) {
		if (!(nconf->nc_flag & NC_VISIBLE))
			continue;
		visible++;

		if (!strcmp(nconf->nc_proto, NC_UDP) && !NFSCTL_UDPISSET(_rpcprotobits))
			continue;

		if (!strcmp(nconf->nc_proto, NC_TCP) && !NFSCTL_TCPISSET(_rpcprotobits))
			continue;

		if (port == 0)
			servport = getservport(program, nconf->nc_proto);
		else
			servport = port;

		up += svc_create_nconf(name, program, version, dispatch,
						servport, nconf);
	}

	if (visible == 0)
		xlog(L_ERROR, "Failed to find any visible netconfig entries");

	if (endnetconfig(handlep) == -1)
		xlog(L_ERROR, "Failed to close local netconfig database: %s",
			nc_sperror());

	return up;
}

/**
 * nfs_svc_unregister - remove service registrations from local rpcbind database
 * @program: RPC program number to unregister
 * @version: RPC version number to unregister
 *
 * Removes all registrations for [ @program, @version ] .
 */
void
nfs_svc_unregister(const rpcprog_t program, const rpcvers_t version)
{
	if (rpcb_unset(program, version, NULL) == FALSE)
		xlog(D_GENERAL, "Failed to unregister program %lu, version %lu",
			(unsigned long)program, (unsigned long)version);
}

#else	/* !HAVE_LIBTIRPC */

/**
 * nfs_svc_create - start up RPC svc listeners
 * @name: C string containing name of new service
 * @program: RPC program number to register
 * @version: RPC version number to register
 * @dispatch: address of function that handles incoming RPC requests
 * @port: if not zero, transport listens on this port
 *
 * Sets up network transports for receiving RPC requests, and starts
 * the RPC dispatcher.  Returns the number of started network transports.
 */
unsigned int
nfs_svc_create(char *name, const rpcprog_t program, const rpcvers_t version,
		void (*dispatch)(struct svc_req *, SVCXPRT *),
		const uint16_t port)
{
	rpc_init(name, (int)program, (int)version, dispatch, (int)port);
	return 1;
}

/**
 * nfs_svc_unregister - remove service registrations from local rpcbind database
 * @program: RPC program number to unregister
 * @version: RPC version number to unregister
 *
 * Removes all registrations for [ @program, @version ] .
 */
void
nfs_svc_unregister(const rpcprog_t program, const rpcvers_t version)
{
	if (pmap_unset((unsigned long)program, (unsigned long)version) == FALSE)
		xlog(D_GENERAL, "Failed to unregister program %lu, version %lu",
			(unsigned long)program, (unsigned long)version);
}

#endif	/* !HAVE_LIBTIRPC */
