/*
 * Copyright 2001, David Leonard. All rights reserved.
 * Redistribution and use in source and binary forms with or without
 * modification are permitted provided that this notice is preserved.
 * This software is provided ``as is'' without express or implied warranty.
 *
 * $OpenBSD: list.c,v 1.5 2007/09/04 22:39:31 hshoexer Exp $
 * $DragonFly: src/games/hunt/hunt/list.c,v 1.2 2008/09/04 16:12:51 swildner Exp $
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sockio.h>
#include <sys/ioctl.h>

#include <netinet/in.h>
#include <net/if.h>

#include <arpa/inet.h>

#include "hunt.h"
#include "list.h"

/* Wait at most 5 seconds for a reply */
#define LIST_DELAY	5

struct driver *drivers = NULL;
int numdrivers = 0;
int maxdrivers = 0;

u_int16_t Server_port;

static int numprobes = 0;
static int probe_sock[64];
static struct timeval probe_timeout;

struct driver *
next_driver(void)
{

	return next_driver_fd(-1);
}

struct driver *
next_driver_fd(int fd)
{
	fd_set	r;
	int	maxfd = -1;
	int	i, s, ret, len;
	struct driver *driver;
	u_int16_t resp;

	if (fd == -1 && numprobes == 0)
		return NULL;

    again:
	FD_ZERO(&r);
	if (fd != -1) {
		FD_SET(fd, &r);
		maxfd = fd;
	}
	for (i = 0; i < numprobes; i++) {
		FD_SET(probe_sock[i], &r);
		if (probe_sock[i] > maxfd)
			maxfd = probe_sock[i];
	}

	probe_timeout.tv_sec = LIST_DELAY;
	probe_timeout.tv_usec = 0;
	ret = select(maxfd + 1, &r, NULL, NULL, &probe_timeout);

	if (ret == -1) {
		if (errno == EINTR)
			goto again;
		err(1, "select");
	}

	if (ret == 0) {
		/* Timeout - close all sockets */
		for (i = 0; i < numprobes; i++)
			close(probe_sock[i]);
		numprobes = 0;
		return NULL;
	}

	if (fd != -1 && FD_ISSET(fd, &r)) 
		/* Keypress. Return magic number */
		return (struct driver *)-1;

	for (i = 0; i < numprobes; i++)
		/* Find the first ready socket */
		if (FD_ISSET(probe_sock[i], &r))
			break;

	s = probe_sock[i];

	if (numdrivers >= maxdrivers) {
		if (maxdrivers) {
			maxdrivers *= 2;
			drivers = realloc(drivers, sizeof *driver * maxdrivers);
		} else {
			maxdrivers = 16;
			drivers = calloc(sizeof *driver, maxdrivers);
		}
		if (drivers == NULL)
			err(1, "malloc");
	}
	driver = &drivers[numdrivers];
	len = sizeof driver->addr;
	ret = recvfrom(s, &resp, sizeof resp, 0, &driver->addr, &len);
	if (ret == -1)
		goto again;
	driver->response = ntohs(resp);

	switch (driver->addr.sa_family) {
	case AF_INET:
	case AF_INET6:
		((struct sockaddr_in *)&driver->addr)->sin_port =
		    htons(driver->response);
		break;
	}
	numdrivers++;
	return driver;
}

/* Return the hostname for a driver. */
const char *
driver_name(struct driver *driver)
{
	const char *name;
	static char buf[80];
	struct hostent *hp;
	struct sockaddr_in *sin;

	name = NULL;

	if (driver->addr.sa_family == AF_INET) {
		sin = (struct sockaddr_in *)&driver->addr;
		hp = gethostbyaddr(&sin->sin_addr, sizeof sin->sin_addr,
		    AF_INET);
		if (hp != NULL)
			name = hp->h_name;
		else {
			name = inet_ntop(AF_INET, &sin->sin_addr, 
			    buf, sizeof buf);
		}
	}

	return name;
}

static int
start_probe(struct sockaddr *addr, u_int16_t req)
{
	u_int16_t msg;
	int s;
	int enable;

	if (numprobes >= (int)(sizeof probe_sock / sizeof probe_sock[0])) {
		/* Just ridiculous */
		return -1;
	}

	s = socket(addr->sa_family, SOCK_DGRAM, 0);
	if (s < 0) {
		warn("socket");
		return -1;
	}

	enable = 1;
	setsockopt(s, SOL_SOCKET, SO_BROADCAST, &enable, sizeof enable);

	switch (addr->sa_family) {
	case AF_INET:
	case AF_INET6:
		((struct sockaddr_in *)addr)->sin_port = 
		    htons(Server_port);
		break;
	}

	msg = htons(req);
	if (sendto(s, &msg, sizeof msg, 0, addr, addr->sa_len) == -1)
		warn("sendto");
	probe_sock[numprobes++] = s;

	return 0;
}

void
probe_cleanup(void)
{
	int i;

	for (i = 0; i < numprobes; i++)
		close(probe_sock[i]);
	numprobes = 0;
}

/*
 * If we have no preferred host then send a broadcast message to everyone.
 * Otherwise, send the request message only to the preferred host.
 */
void
probe_drivers(u_int16_t req, char *preferred)
{
	struct sockaddr_in *target;
	struct sockaddr_in localhost;
	struct hostent *he;
        char *inbuf = NULL, *ninbuf;
        struct ifconf ifc;
        struct ifreq *ifr;
        int fd, inlen = 8192;
        int i, len;

	numdrivers = 0;

	probe_cleanup();

	/* Send exclusively to a preferred host. */
	if (preferred) {
		struct sockaddr_in sin;

		target = NULL;

		if (!target) {
			sin.sin_family = AF_INET;
			sin.sin_len = sizeof sin;
			if (inet_pton(AF_INET, preferred, &sin.sin_addr) == 1)
				target = &sin;
		}

		if (!target && (he = gethostbyname(preferred)) != NULL) {
			sin.sin_family = he->h_addrtype;
			sin.sin_len = sizeof sin;
			memcpy(&sin.sin_addr, he->h_addr, he->h_length);
			target = &sin;
		}

		if (!target)
			errx(1, "Bad hostname: %s", preferred);

		start_probe((struct sockaddr *)target, req);
		return;
	}

	/* Send a query to the local machine: */
	localhost.sin_family = AF_INET;
	localhost.sin_len = sizeof localhost;
	localhost.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	start_probe((struct sockaddr *)&localhost, req);

        if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
                err(1, "socket");

	/* Find all attached networks: */
        while (1) {
                ifc.ifc_len = inlen;
                if ((ninbuf = realloc(inbuf, inlen)) == NULL)
			err(1, "malloc");
                ifc.ifc_buf = inbuf = ninbuf;
                if (ioctl(fd, SIOCGIFCONF, (char *)&ifc) < 0) 
                        err(1, "SIOCGIFCONF");
                if (ifc.ifc_len + (int)sizeof(*ifr) < inlen)
                        break;
                inlen *= 2;
        }

	/* Send a request to every attached broadcast address: */
        ifr = ifc.ifc_req;
        for (i = 0; i < ifc.ifc_len;
             i += len, ifr = (struct ifreq *)((caddr_t)ifr + len)) {
                len = sizeof(ifr->ifr_name) +
                      (ifr->ifr_addr.sa_len > sizeof(struct sockaddr) ?
                       ifr->ifr_addr.sa_len : sizeof(struct sockaddr));

		if (ifr->ifr_addr.sa_family != AF_INET)
			continue;

                if (ioctl(fd, SIOCGIFFLAGS, (caddr_t)ifr) < 0) {
                        warn("%s: SIOCGIFFLAGS", ifr->ifr_name);
			continue;
		}
                if ((ifr->ifr_flags & IFF_UP) == 0)
			continue;
		if ((ifr->ifr_flags & IFF_BROADCAST) != 0) {
			if (ioctl(fd, SIOCGIFBRDADDR, (caddr_t)ifr) < 0)  {
				warn("%s: SIOCGIFBRDADDR", ifr->ifr_name);
				continue;
			}
			target = (struct sockaddr_in *)&ifr->ifr_dstaddr;
		} else if ((ifr->ifr_flags & IFF_POINTOPOINT) != 0) {
			if (ioctl(fd, SIOCGIFDSTADDR, (caddr_t)ifr) < 0)  {
				warn("%s: SIOCGIFDSTADDR", ifr->ifr_name);
				continue;
			}
			target = (struct sockaddr_in *)&ifr->ifr_broadaddr;
		} else
			continue;

		start_probe((struct sockaddr *)target, req);
        }
        free(inbuf);
        (void) close(fd);
}
