/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */

#include "common.h"
#include "array.h"
#include "ioloop.h"
#include "net.h"
#ifdef HAVE_LIBSYSTEMD
#  include <systemd/sd-daemon.h>
#endif
#include "service.h"
#include "service-listen.h"

#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/socket.h>

#define MIN_BACKLOG 4

static unsigned int service_get_backlog(struct service *service)
{
	unsigned int backlog;

	i_assert(service->process_limit > 0);
	i_assert(service->client_limit > 0);

	/* as unlikely as it is, avoid overflows */
	if (service->client_limit > INT_MAX / service->process_limit)
		backlog = INT_MAX;
	else
		backlog = service->process_limit * service->client_limit;
	return I_MAX(backlog, MIN_BACKLOG);
}

static int
service_file_chown(const struct service_listener *l, const char *path,
		   const char **error_r)
{
	uid_t uid = l->set.fileset.uid;
	uid_t gid = l->set.fileset.gid;

	if ((uid == (uid_t)-1 || uid == master_uid) &&
	    (gid == (gid_t)-1 || gid == master_gid))
		return 0;

	if (chown(path, uid, gid) < 0) {
		*error_r = t_strdup_printf("chown(%s, %lld, %lld) failed: %m",
					   path, (long long)uid, (long long)gid);
		return -1;
	}
	return 0;
}

int service_unix_listener_listen(struct service_listener *l, const char *path,
				 bool verify_addrinuse, const char **error_r)
{
        struct service *service = l->service;
	const struct file_listener_settings *set = l->set.fileset.set;
	mode_t old_umask;
	int fd, i;

	old_umask = umask((set->mode ^ 0777) & 0777);
	for (i = 0;; i++) {
		fd = net_listen_unix(path, service_get_backlog(service));
		if (fd != -1)
			break;

		if (errno != EADDRINUSE) {
			*error_r = t_strdup_printf(
				"net_listen_unix(%s) failed: %m", path);
			return -1;
		}

		/* already in use - see if it really exists.
		   after 3 times just fail here. */
		if (verify_addrinuse)
			fd = net_connect_unix(path);
		else {
			fd = -1;
			errno = ECONNREFUSED;
		}
		if (fd != -1 || errno != ECONNREFUSED || i >= 3) {
			i_close_fd(&fd);
			*error_r = t_strdup_printf("Socket already exists: %s",
						   path);
			return 0;
		}

		/* delete and try again */
		if (unlink(path) < 0 && errno != ENOENT) {
			*error_r = t_strdup_printf("unlink(%s) failed: %m",
						   path);
			return -1;
		}
	}
	umask(old_umask);

	i_assert(fd != -1);

	if (service_file_chown(l, path, error_r) < 0) {
		i_close_fd(&fd);
		return -1;
	}
	net_set_nonblock(fd, TRUE);
	fd_close_on_exec(fd, TRUE);

	l->fd = fd;
	return 1;
}

static int
service_fifo_listener_listen(struct service_listener *l, const char **error_r)
{
	const struct file_listener_settings *set = l->set.fileset.set;
	unsigned int i;
	mode_t old_umask;
	int fd, ret;

	for (i = 0;; i++) {
		old_umask = umask((set->mode ^ 0777) & 0777);
		ret = mkfifo(set->path, set->mode);
		umask(old_umask);

		if (ret == 0)
			break;
		if (ret < 0 && (errno != EEXIST || i == 1)) {
			*error_r = t_strdup_printf("mkfifo(%s) failed: %m",
						   set->path);
			return -1;
		}
		if (unlink(set->path) < 0) {
			*error_r = t_strdup_printf("unlink(%s) failed: %m",
						   set->path);
			return -1;
		}
	}
	if (service_file_chown(l, set->path, error_r) < 0)
		return -1;

	/* open as RDWR, so that even if the last writer closes,
	   we won't get EOF errors */
	fd = open(set->path, O_RDWR | O_NONBLOCK);
	if (fd == -1) {
		*error_r = t_strdup_printf("open(%s) failed: %m", set->path);
		return -1;
	}

	fd_close_on_exec(fd, TRUE);

	l->fd = fd;
	return 1;
}

#ifdef HAVE_LIBSYSTEMD
static int
systemd_listen_fd(const struct ip_addr *ip, in_port_t port, int *fd_r,
		  const char **error_r)
{
	static int sd_fds = -1;
	int fd, fd_max;

	if (sd_fds < 0) {
		sd_fds = sd_listen_fds(0);
		if (sd_fds < 0) {
			*error_r = t_strdup_printf("sd_listen_fds() failed: %s",
						   strerror(-sd_fds));
			return -1;
		}
	}

	fd_max = SD_LISTEN_FDS_START + sd_fds - 1;
	for (fd = SD_LISTEN_FDS_START; fd <= fd_max; fd++) {
		if (sd_is_socket_inet(fd, ip->family, SOCK_STREAM, 1, port) > 0) {
			*fd_r = fd;
			return 0;
		}
	}
	/* when systemd didn't provide a usable socket,
	   fall back to the regular socket creation code */
	*fd_r = -1;
	return 0;
}
#endif

static int
service_inet_listener_listen(struct service_listener *l, const char **error_r)
{
        struct service *service = l->service;
	enum net_listen_flags flags = 0;
	const struct inet_listener_settings *set = l->set.inetset.set;
	in_port_t port = set->port;
	int fd;

#ifdef HAVE_LIBSYSTEMD
	if (systemd_listen_fd(&l->set.inetset.ip, port, &fd, error_r) < 0)
		return -1;

	if (fd == -1)
#endif
	{
		if (service->set->reuse_port)
			flags |= NET_LISTEN_FLAG_REUSEPORT;
		fd = net_listen_full(&l->set.inetset.ip, &port, &flags,
				     service_get_backlog(service));
		if (fd < 0) {
			*error_r = t_strdup_printf("listen(%s, %u) failed: %m",
						   l->inet_address, set->port);
			return errno == EADDRINUSE ? 0 : -1;
		}
	}
	net_set_nonblock(fd, TRUE);
	fd_close_on_exec(fd, TRUE);

	l->fd = fd;
	return 1;
}

int service_listener_listen(struct service_listener *l)
{
	const char *error;
	int ret;

	switch (l->type) {
	case SERVICE_LISTENER_UNIX:
		ret = service_unix_listener_listen(l, l->set.fileset.set->path,
						   TRUE, &error);
		break;
	case SERVICE_LISTENER_FIFO:
		ret = service_fifo_listener_listen(l, &error);
		break;
	case SERVICE_LISTENER_INET:
		ret = service_inet_listener_listen(l, &error);
		break;
	default:
		i_unreached();
	}
	if (ret <= 0)
		e_error(l->service->event, "%s", error);
	return ret;
}

static int service_listen(struct service *service)
{
	struct service_listener *l;
	int ret = 1, ret2 = 0;

	array_foreach_elem(&service->listeners, l) {
		if (l->fd != -1)
			continue;

		ret2 = service_listener_listen(l);
		if (ret2 < ret)
			ret = ret2;
	}
	return ret;
}

#ifdef HAVE_LIBSYSTEMD
static int get_socket_info(int fd, sa_family_t *family_r, in_port_t *port_r)
{
	union sockaddr_union {
		struct sockaddr sa;
		struct sockaddr_in in4;
		struct sockaddr_in6 in6;
	} sockaddr;
	socklen_t l;

	if (port_r != NULL)
		*port_r = 0;
	if (family_r != NULL)
		*family_r = AF_UNSPEC;

	i_zero(&sockaddr);
	l = sizeof(sockaddr);

	if (getsockname(fd, &sockaddr.sa, &l) < 0)
	      return -errno;

	if (family_r != NULL)
		*family_r = sockaddr.sa.sa_family;
	if (port_r != NULL) {
		if (sockaddr.sa.sa_family == AF_INET) {
			if (l < sizeof(struct sockaddr_in))
				return -EINVAL;

			*port_r = ntohs(sockaddr.in4.sin_port);
		} else {
			if (l < sizeof(struct sockaddr_in6))
				return -EINVAL;

			*port_r = ntohs(sockaddr.in6.sin6_port);
		}
	}
	return 0;
}

static int services_verify_systemd(struct service_list *service_list)
{
	struct service *service;
	static int sd_fds = -1;
	int fd, fd_max;

	if (sd_fds < 0) {
		sd_fds = sd_listen_fds(0);
		if (sd_fds == -1) {
			e_error(service_list->event,
				"sd_listen_fds() failed: %m");
			return -1;
		}
	}

	fd_max = SD_LISTEN_FDS_START + sd_fds - 1;
	for (fd = SD_LISTEN_FDS_START; fd <= fd_max; fd++) {
		if (sd_is_socket_inet(fd, 0, SOCK_STREAM, 1, 0) > 0) {
			bool found = FALSE;
			in_port_t port;
			sa_family_t family;
			get_socket_info(fd, &family, &port);

			array_foreach_elem(&service_list->services, service) {
				struct service_listener *l;

				array_foreach_elem(&service->listeners, l) {
					if (l->type != SERVICE_LISTENER_INET)
						continue;
					if (l->set.inetset.set->port == port &&
					    l->set.inetset.ip.family == family) {
						found = TRUE;
						break;
					}
				}
				if (found)
					break;
			}
			if (!found) {
				e_error(service_list->event,
					"systemd listens on port %d, "
					"but it's not configured in Dovecot. "
					"Closing.", port);
				if (shutdown(fd, SHUT_RDWR) < 0 &&
				    errno != ENOTCONN)
					e_error(service_list->event,
						"shutdown(%d) failed: %m", fd);
				if (dup2(dev_null_fd, fd) < 0)
					e_error(service_list->event,
						"dup2(%d, %d) failed: %m",
						dev_null_fd, fd);
			}
		}
	}
	return 0;
}
#endif

static int services_listen_master(struct service_list *service_list)
{
	const char *path;
	mode_t old_umask;

	if (service_list->master_fd != -1)
		return 1;

	path = t_strdup_printf("%s/master", service_list->set->base_dir);
	old_umask = umask(0600 ^ 0777);
	service_list->master_fd = net_listen_unix(path, 16);
	if (service_list->master_fd == -1 && errno == EADDRINUSE) {
		/* already in use. all the other sockets were fine, so just
		   delete this and retry. */
		i_unlink_if_exists(path);
		service_list->master_fd = net_listen_unix(path, 16);
	}
	umask(old_umask);

	if (service_list->master_fd == -1) {
		e_error(service_list->event,
			"net_listen_unix(%s) failed: %m", path);
		return 0;
	}
	fd_close_on_exec(service_list->master_fd, TRUE);
	return 1;
}

int services_listen(struct service_list *service_list)
{
	struct service *service;
	int ret = 1, ret2;

	array_foreach_elem(&service_list->services, service) {
		ret2 = service_listen(service);
		if (ret2 < ret)
			ret = ret2;
	}
	/* reloading config wants to continue even when we're returning 0. */
	if (ret >= 0) {
		ret2 = services_listen_master(service_list);
		if (ret2 < ret)
			ret = ret2;
	}

#ifdef HAVE_LIBSYSTEMD
	if (ret > 0)
		services_verify_systemd(service_list);
#endif
	return ret;
}

static bool listener_equals(const struct service_listener *l1,
			    const struct service_listener *l2)
{
	if (l1->type != l2->type)
		return FALSE;

	switch (l1->type) {
	case SERVICE_LISTENER_UNIX:
	case SERVICE_LISTENER_FIFO:
		/* We could just keep using the same listener, but it's more
		   likely to cause problems if old process accepts a connection
		   before it knows that it should die. So just always unlink
		   and recreate unix/fifo listeners. */
		return FALSE;
	case SERVICE_LISTENER_INET:
		if (memcmp(&l1->set.inetset.ip, &l2->set.inetset.ip,
			   sizeof(l1->set.inetset.ip)) != 0)
			return FALSE;
		if (l1->set.inetset.set->port != l2->set.inetset.set->port)
			return FALSE;
		return l1->reuse_port_process_index == l2->reuse_port_process_index;
	}
	return FALSE;
}

int services_listen_using(struct service_list *new_service_list,
			  struct service_list *old_service_list)
{
	struct service *const *services, *old_service, *new_service;
	ARRAY(struct service_listener *) new_listeners_arr;
	ARRAY(struct service_listener *) old_listeners_arr;
	struct service_listener *const *new_listeners, *const *old_listeners;
	unsigned int i, j, count, new_count, old_count;

	/* copy master listener */
	new_service_list->master_fd = old_service_list->master_fd;
	old_service_list->master_fd = -1;

	/* rescue anvil's UNIX socket listener */
	new_service = service_lookup_type(new_service_list, SERVICE_TYPE_ANVIL);
	old_service = service_lookup_type(old_service_list, SERVICE_TYPE_ANVIL);
	if (old_service != NULL && new_service != NULL) {
		new_listeners = array_get(&new_service->listeners, &new_count);
		old_listeners = array_get(&old_service->listeners, &old_count);
		for (i = 0; i < old_count && i < new_count; i++) {
			if (new_listeners[i]->type != old_listeners[i]->type)
				break;
		}
		if (i != new_count && i != old_count) {
			e_error(old_service_list->event,
				"Can't change anvil's listeners on the fly");
			return -1;
		}
		for (i = 0; i < new_count; i++) {
			new_listeners[i]->fd = old_listeners[i]->fd;
			old_listeners[i]->fd = -1;
		}
	}

	/* first create an arrays of all listeners to make things easier */
	t_array_init(&new_listeners_arr, 64);
	services = array_get(&new_service_list->services, &count);
	for (i = 0; i < count; i++)
		array_append_array(&new_listeners_arr, &services[i]->listeners);

	t_array_init(&old_listeners_arr, 64);
	services = array_get(&old_service_list->services, &count);
	for (i = 0; i < count; i++)
		array_append_array(&old_listeners_arr, &services[i]->listeners);

	/* then start moving fds */
	new_listeners = array_get(&new_listeners_arr, &new_count);
	old_listeners = array_get(&old_listeners_arr, &old_count);

	for (i = 0; i < new_count; i++) {
		for (j = 0; j < old_count; j++) {
			if (old_listeners[j]->fd != -1 &&
			    listener_equals(new_listeners[i],
					    old_listeners[j])) {
				new_listeners[i]->fd = old_listeners[j]->fd;
                                old_listeners[j]->fd = -1;
				break;
			}
		}
	}

	/* close what's left */
	for (j = 0; j < old_count; j++) {
		if (old_listeners[j]->fd == -1)
			continue;

		i_close_fd(&old_listeners[j]->fd);
		switch (old_listeners[j]->type) {
		case SERVICE_LISTENER_UNIX:
		case SERVICE_LISTENER_FIFO: {
			i_unlink(old_listeners[j]->set.fileset.set->path);
			break;
		}
		case SERVICE_LISTENER_INET:
			break;
		}
	}

	/* and let services_listen() deal with the remaining fds */
	return services_listen(new_service_list);
}
