/*	$NetBSD: server.c,v 1.9 2021/10/29 11:40:23 nia Exp $	*/
/*
 * Copyright (c) 1983-2003, Regents of the University of California.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * + Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * + 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.
 * + Neither the name of the University of California, San Francisco nor
 *   the names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior written
 *   permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
 * OWNER 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>
__RCSID("$NetBSD: server.c,v 1.9 2021/10/29 11:40:23 nia Exp $");

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/poll.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <ifaddrs.h>

#include "hunt_common.h"
#include "pathnames.h"
#include "hunt_private.h"

#ifdef INTERNET

/*
 * Code for finding and talking to hunt daemons.
 */

static struct sockaddr_in *daemons;
static unsigned int numdaemons, maxdaemons;

static struct sockaddr_in *broadcastaddrs;
static int numbroadcastaddrs;

static bool initial = true;
static struct in_addr local_address;
static const char *explicit_host;
static uint16_t port;

void
serverlist_setup(const char *explicit_host_arg, uint16_t port_arg)
{
	char local_name[MAXHOSTNAMELEN + 1];
	struct hostent *hp;

	if (gethostname(local_name, sizeof(local_name)) < 0) {
		leavex(1, "Sorry, I have no hostname.");
	}
	local_name[sizeof(local_name) - 1] = '\0';
	if ((hp = gethostbyname(local_name)) == NULL) {
		leavex(1, "Can't find myself.");
	}
	memcpy(&local_address, hp->h_addr, sizeof(local_address));

	numdaemons = 0;
	maxdaemons = 20;
	daemons = NULL;
	if (reallocarr(&daemons, maxdaemons, sizeof(daemons[0])) != 0)
		leavex(1, "Out of memory.");

	if (explicit_host_arg) {
		explicit_host = explicit_host_arg;
	}
	port = port_arg;
}

static void
add_daemon_addr(const struct sockaddr_storage *addr, socklen_t addrlen,
		uint16_t port_num)
{
	const struct sockaddr_in *sin;

	if (addr->ss_family != AF_INET) {
		return;
	}
	assert(addrlen == sizeof(struct sockaddr_in));
	sin = (const struct sockaddr_in *)addr;

	assert(numdaemons <= maxdaemons);
	if (numdaemons == maxdaemons) {
		maxdaemons += 20;
		if (reallocarr(&daemons, maxdaemons, sizeof(daemons[0])) != 0)
			leave(1, "realloc");
	}

	/*
	 * Note that we do *not* convert from network to host
	 * order since the port number we were sent *should*
	 * already be in network order.
	 *
	 * The result may be either the port number for the hunt
	 * socket or the port number for the stats socket... or the
	 * number of players connected and not a port number at all,
	 * depending on the packet type.
	 *
	 * For now at least it is ok to stuff it in here, because the
	 * usage of the various different packet types and the
	 * persistence of the results vs. the program exiting does not
	 * cause us to get confused. If the game is made more
	 * self-contained in the future we'll need to be more careful
	 * about this, especially if we make the caching of results
	 * less scattershot.
	 */
	daemons[numdaemons] = *sin;
	daemons[numdaemons].sin_port = port_num;
	numdaemons++;
}

static bool
have_daemon_addr(const struct sockaddr_storage *addr, socklen_t addrlen)
{
	unsigned j;
	const struct sockaddr_in *sin;

	if (addr->ss_family != AF_INET) {
		return false;
	}
	assert(addrlen == sizeof(struct sockaddr_in));
	sin = (const struct sockaddr_in *)addr;

	for (j = 0; j < numdaemons; j++) {
		if (sin->sin_addr.s_addr == daemons[j].sin_addr.s_addr) {
			return true;
		}
	}
	return false;
}

static void
getbroadcastaddrs(void)
{
	unsigned num, i;
	struct ifaddrs *ifp, *ip;

	broadcastaddrs = NULL;
	numbroadcastaddrs = 0;

	if (getifaddrs(&ifp) < 0) {
		return;
	}

	num = 0;
	for (ip = ifp; ip; ip = ip->ifa_next) {
		if ((ip->ifa_addr->sa_family == AF_INET) &&
		    (ip->ifa_flags & IFF_BROADCAST)) {
			num++;
		}
	}

	broadcastaddrs = NULL;
	if (reallocarr(&broadcastaddrs, num, sizeof(broadcastaddrs[0])) != 0)
		leavex(1, "Out of memory");

	i = 0;
	for (ip = ifp; ip; ip = ip->ifa_next) {
		if ((ip->ifa_addr->sa_family == AF_INET) &&
		    (ip->ifa_flags & IFF_BROADCAST)) {
			memcpy(&broadcastaddrs[i], ip->ifa_broadaddr,
			       sizeof(broadcastaddrs[i]));
			i++;
		}
	}
	assert(i == num);
	numbroadcastaddrs = num;

	freeifaddrs(ifp);
}

static void
send_messages(int contactsock, unsigned short msg)
{
	struct sockaddr_in contactaddr;
	struct hostent *hp;
	uint16_t wiremsg;
	int option;
	int i;

	memset(&contactaddr, 0, sizeof(contactaddr));
	contactaddr.sin_family = SOCK_FAMILY;
	contactaddr.sin_port = htons(port);

	if (explicit_host != NULL) {	/* explicit host given */
		hp = gethostbyname(explicit_host);
		if (hp == NULL) {
			leavex(1, "%s: Unknown host", explicit_host);
		}
		memcpy(&contactaddr.sin_addr, hp->h_addr,
		       sizeof(contactaddr.sin_addr));
		wiremsg = htons(msg);
		(void) sendto(contactsock, &wiremsg, sizeof(wiremsg), 0,
			      (struct sockaddr *)&contactaddr,
			      sizeof(contactaddr));
		return;
	}

	if (!initial) {
		/* favor host of previous session by contacting it first */
		contactaddr.sin_addr = Daemon.sin_addr;

		/* Must be playing! */
		assert(msg == C_PLAYER);
		wiremsg = htons(msg);

		(void) sendto(contactsock, &wiremsg, sizeof(wiremsg), 0,
		    (struct sockaddr *)&contactaddr, sizeof(contactaddr));
	}

	if (initial) {
		getbroadcastaddrs();
	}

#ifdef SO_BROADCAST
	/* Sun's will broadcast even though this option can't be set */
	option = 1;
	if (setsockopt(contactsock, SOL_SOCKET, SO_BROADCAST,
	    &option, sizeof option) < 0) {
		leave(1, "setsockopt broadcast");
		/* NOTREACHED */
	}
#endif

	/* send broadcast packets on all interfaces */
	wiremsg = htons(msg);
	for (i = 0; i < numbroadcastaddrs; i++) {
		contactaddr.sin_addr = broadcastaddrs[i].sin_addr;
		if (sendto(contactsock, &wiremsg, sizeof(wiremsg), 0,
		    (struct sockaddr *)&contactaddr,
		    sizeof(contactaddr)) < 0) {
			leave(1, "sendto");
		}
	}
	contactaddr.sin_addr = local_address;
	if (sendto(contactsock, &wiremsg, sizeof(wiremsg), 0,
	    (struct sockaddr *)&contactaddr, sizeof(contactaddr)) < 0) {
		leave(1, "sendto");
	}
}

static void
get_responses(int contactsock)
{
	struct pollfd set[1];
	struct sockaddr_storage addr;
	socklen_t addrlen;
	int r;
	uint16_t port_num;
	ssize_t portlen;

	/* forget all old responses */
	numdaemons = 0;

	errno = 0;
	set[0].fd = contactsock;
	set[0].events = POLLIN;
	for (;;) {
		r = poll(set, 1, 1000);
		if (r < 0) {
			if (errno == EINTR) {
				continue;
			}
			leave(1, "poll");
		}
		if (r == 0) {
			break;
		}

		addrlen = sizeof(addr);
		portlen = recvfrom(contactsock, &port_num, sizeof(port_num), 0,
				   (struct sockaddr *)&addr, &addrlen);
		if (portlen < 0) {
			if (errno == EINTR) {
				continue;
			}
			leave(1, "recvfrom");
		}
		if (portlen == 0) {
			leavex(1, "recvfrom: Unexpected EOF");
		}
		if ((size_t)portlen != sizeof(port_num)) {
			/* trash, ignore it */
			continue;
		}
		if (have_daemon_addr(&addr, addrlen)) {
			/* this shouldn't happen */
			continue;
		}

		add_daemon_addr(&addr, addrlen, port_num);
	}

	initial = false;
}

void
serverlist_query(unsigned short msg)
{
	int contactsock;

	if (!initial && explicit_host != NULL) {
		/* already did the work, no point doing it again */
		return;
	}

	contactsock = socket(SOCK_FAMILY, SOCK_DGRAM, 0);
	if (contactsock < 0) {
		leave(1, "socket system call failed");
	}

	send_messages(contactsock, msg);
	get_responses(contactsock);

	(void) close(contactsock);
}

unsigned
serverlist_num(void)
{
	return numdaemons;
}

const struct sockaddr_storage *
serverlist_gethost(unsigned i, socklen_t *len_ret)
{
	struct sockaddr_in *ret;

	assert(i < numdaemons);
	ret = &daemons[i];
	*len_ret = sizeof(*ret);
	return (struct sockaddr_storage *)ret;
}

unsigned short
serverlist_getresponse(unsigned i)
{
	assert(i < numdaemons);
	return daemons[i].sin_port;
}

#endif /* INTERNET */
