/*
 * Copyright (C) 2001 USAGI/WIDE Project.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. Neither the name of the project 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 PROJECT 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 PROJECT 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.
 */
/*
 * Copyright (c) 1983 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:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by the University of
 *	California, Berkeley and its contributors.
 * 4. Neither the name of the University 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 REGENTS 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 REGENTS 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.
 */

char copyright[] =
  "@(#) Copyright (c) 1983 Regents of the University of California.\n"
  "All rights reserved.\n";

/*
 * From: @(#)talkd.c	5.8 (Berkeley) 2/26/91
 */
char talkd_rcsid[] = 
  "$Id: talkd.c,v 1.12 1999/09/28 22:04:15 netbug Exp $";

#ifdef _USAGI
#include "version.h"
#else
#include "../version.h"
#endif

/*
 * talkd - internet talk daemon
 * loops waiting for and processing requests until idle for a while,
 * then exits.
 */

#include <features.h>
#define __USE_GNU
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <syslog.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
/*#include <stdio.h>*/
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <paths.h>
#ifdef INET6
#include <ifaddrs.h>
#endif
#include "prot_talkd.h"
#include "proto.h"
#include "ctlmsg.h"

#define TIMEOUT 30
#define MAXIDLE 120

#if !defined(MAXHOSTNAMELEN)
#define	MAXHOSTNAMELEN	64
#endif
char ourhostname[MAXHOSTNAMELEN];

static time_t lastmsgtime;

static void
timeout(int ignore)
{
	(void)ignore;

	if (time(NULL) - lastmsgtime >= MAXIDLE)
		_exit(0);
	signal(SIGALRM, timeout);
	alarm(TIMEOUT);
}

static int
is_sockaddr_equal(struct sockaddr *sa1, socklen_t sa1len,
		  struct sockaddr *sa2, socklen_t sa2len)
{
	char abuf1[NI_MAXHOST], abuf2[NI_MAXHOST];
	char *abuf1p = abuf1, *abuf2p = abuf2;
	if (!sa1 || !sa2 || !sa1len || !sa2len)
		return 0;
	if (getnameinfo(sa1, sa1len, abuf1, sizeof(abuf1), NULL, 0, NI_NUMERICHOST))
		return 0;
	if (getnameinfo(sa2, sa2len, abuf2, sizeof(abuf2), NULL, 0, NI_NUMERICHOST))
		return 0;

	/* ipv4-mapped ipv6 address hack */
	if (sa1->sa_family == AF_INET6 &&
	    IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6 *)sa1)->sin6_addr))
		abuf1p += sizeof("::ffff:") - 1;
	if (sa2->sa_family == AF_INET6 &&
	    IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6 *)sa2)->sin6_addr))
		abuf2p += sizeof("::ffff:") - 1;

	if (strcmp(abuf1p, abuf2p))
		return 0;
	return 1;
}

/*
 * Returns true if the address belongs to the local host. If it's
 * not a loopback address, try binding to it.
 */
static int
is_local_address(struct sockaddr *sa)
{
#ifdef INET6
	struct ifaddrs *ifa0, *ifa;
#else
	struct sockaddr_in sn;
	socklen_t snlen;
	int sock;
#endif
	int ret;

#ifdef INET6
	if (getifaddrs(&ifa0))
		return 0;	/* XXX: error */

	ret = -1;
	for (ifa=ifa0; ifa; ifa=ifa->ifa_next) {
		if (!ifa->ifa_addr)
			continue;
		if (is_sockaddr_equal(sa, SA_LEN(sa),
				      ifa->ifa_addr, SA_LEN(ifa->ifa_addr))) {
			ret = 0;
			break;
		}
	}
	freeifaddrs(ifa0);
#else
	if (sa->sa_family == AF_INET &&
	    ((struct sockaddr_in*)sa)->sin_addr.s_addr == htonl(INADDR_LOOPBACK))
		return 1;

	switch(sa->sa_family) {
	case AF_INET:
		sock = socket(AF_INET, SOCK_DGRAM, 0);
		if (sock<0) {
			syslog(LOG_WARNING, "socket: %s", strerror(errno));
			return 0;
		}
		memset(&sn, 0, sizeof(sn));
		sn.sin_family = ((struct sockaddr_in*)sa)->sin_family;
		sn.sin_addr = ((struct sockaddr_in*)sa)->sin_addr;
		sn.sin_port = htons(0);
		snlen = sizeof(sn);
		break;
	default:
		syslog(LOG_WARNING, "unknown family: %d", sa->sa_family);
		return 0;	/* XXX: error */
	}
	ret = bind(sock, (struct sockaddr *)&sn, snlen);
	close(sock);
#endif
	return ret==0;
}

static void
send_packet(struct CtlResponse *response, struct sockaddr *sa, int quirk)
{
	char buf[2*CTL_RESPONSE_SIZE];
	socklen_t sz = CTL_RESPONSE_SIZE;
	int cc, err=0;

	memcpy(buf, response, sz);
	if (quirk) {
		sz = irrationalize_reply(buf, sizeof(buf), quirk);
	} else {
		sz = cr_export(response, buf, sizeof(buf));
	}
	while (sz > 0) {
		cc = sendto(1, buf, sz, 0, (struct sockaddr *)sa, SA_LEN((struct sockaddr *)sa));
		if (cc<0) {
			syslog(LOG_WARNING, "sendto: %s", strerror(errno));
			if (err) return;
			err = 1;
		}
		else sz -= cc;
	}
}

/*
 * Issue an error packet. Should not assume anything other than the
 * header part (the u_int8_t's) of mp is valid, and assume as little
 * as possible about that, since it might have been a packet we 
 * couldn't dequirk.
 */
static void 
send_reject_packet(struct CtlMessage *mp, struct sockaddr *sa, int code, int quirk)
{
	struct CtlResponse rsp;
	memset(&rsp, 0, sizeof(rsp));
	rsp.vers = TALK_VERSION;
	rsp.type = mp->type;
	rsp.answer = code;
	send_packet(&rsp, sa, quirk);
}

static void
do_one_packet(void)
{
	char inbuf[2*CTL_MSG_SIZE];
	int quirk = 0;
	struct CtlResponse response;
	struct CtlMessage cm, *mp;
#ifdef INET6
	char theirhost[NI_MAXHOST], theirip[NI_MAXHOST];
#else
	char theirhost[MAXHOSTNAMELEN];
	const char *theirip;
#endif

#ifdef INET6
	struct addrinfo hints, *res0, *res;
	struct sockaddr_storage sa;
	int gai;
#else
	struct hostent *hp;
	struct sockaddr sa;
	int i;
#endif
	int cc, ok;
	socklen_t addrlen;

	addrlen = sizeof(sa);
	cc = recvfrom(0, inbuf, sizeof(inbuf), 0,
		      (struct sockaddr *)&sa, &addrlen);
	if (cc<0) {
		if (errno==EINTR || errno==EAGAIN) {
			return;
		}
		syslog(LOG_WARNING, "recvfrom: %s", strerror(errno));
		return;
	}

	/* 
	 * This should be set on any input, even trash, because even
	 * trash input will cause us to be restarted if we exit.
	 */
	lastmsgtime = time(NULL);

	switch(((struct sockaddr *)&sa)->sa_family){
	case AF_INET:
		if (addrlen != sizeof(struct sockaddr_in)) {
			syslog(LOG_WARNING, "recvfrom: bogus ipv4 address length");
			return;
		}
		break;
#ifdef INET6
	case AF_INET6:
#ifdef __USAGI__
		if (addrlen < sizeof(struct __sockaddr_in6_rfc2133)) {
			syslog(LOG_WARNING, "recvfrom: bogus ipv6 address length");
			return;
		}
#else
		if (addrlen < sizeof(struct sockaddr_in6)) {
			syslog(LOG_WARNING, "recvfrom: bogus ipv6 address length");
			return;
		}
#endif
		break;
#endif
	default:
		syslog(LOG_WARNING, "recvfrom: bogus address family: %d",
			((struct sockaddr *)&sa)->sa_family);
		return;
	}

	/* 
	 * If we get here we have an address we can reply to, although
	 * it may not be good for much. If possible, reply to it, because
	 * if we just drop the packet the remote talk client will keep
	 * throwing junk at us.
	 */
#ifdef INET6
	if (getnameinfo((struct sockaddr *)&sa, addrlen,
			theirip, sizeof(theirip), NULL, 0,
			NI_NUMERICHOST)) {
		syslog(LOG_WARNING, "recvfrom: cannot do getnameinfo() for family=%d, length=%d",
			((struct sockaddr *)&sa)->sa_family, addrlen);
		return;
	}
#else
	theirip = inet_ntoa(((struct sockaddr_in*)&sa)->sin_addr);
#endif

	if (cm_import(inbuf, cc, &cm) < 0) {
		mp = NULL;
	} else {
		mp = &cm;
	}

	/*
	 * Check they're not being weenies.
	 * We should look into using libwrap here so hosts.deny works.
	 * Wrapping talkd with tcpd isn't very useful.
	 */
#ifdef INET6
	if (getnameinfo((struct sockaddr *)&sa, addrlen,
			theirhost, sizeof(theirhost), NULL, 0,
			NI_NAMEREQD)) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		if (mp)
			send_reject_packet(mp, (struct sockaddr*)&sa, MACHINE_UNKNOWN, 0);
		return;
	}
#else
	hp = gethostbyaddr((char *)&((struct sockaddr_in*)&sa)->sin_addr, 
			   sizeof(struct in_addr), 
			   AF_INET);
	if (hp == NULL) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		if (mp)
			send_reject_packet(mp, (struct sockaddr*)&sa, MACHINE_UNKNOWN, 0);
		return;
	}
	strncpy(theirhost, hp->h_name, sizeof(theirhost));
	theirhost[sizeof(theirhost)-1] = 0;
#endif

#ifdef INET6
	memset(&hints, 0, sizeof(hints));
#ifdef __USAGI__
	hints.ai_family = ((struct sockaddr *)&sa)->sa_family;
	hints.ai_flags = AI_ALL|AI_V4MAPPED;
#else
	switch(((struct sockaddr *)&sa)->sa_family){
	case AF_INET:
		hints.ai_family = PF_INET;
		break;
	case AF_INET6:
		hints.ai_family = IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6*)&sa)->sin6_addr) ? 
				  PF_INET : PF_INET6;
		break;
	default:
		/*XXX*/
		hints.ai_family = PF_UNSPEC;
	}
#endif
	hints.ai_socktype = SOCK_DGRAM;
	gai = getaddrinfo(theirhost, NULL, &hints, &res0);
	if (gai) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		if (mp)
			send_reject_packet(mp, (struct sockaddr*)&sa, MACHINE_UNKNOWN, 0);
		return;
	}
#else
	hp = gethostbyname(theirhost);
	if (hp == NULL) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		if (mp)
			send_reject_packet(mp, (struct sockaddr*)&sa, MACHINE_UNKNOWN, 0);
		return;
	}
#endif

	ok = 0;
#ifdef INET6
	for (res=res0; res; res=res->ai_next) {
		if (is_sockaddr_equal(res->ai_addr, res->ai_addrlen,
				      (struct sockaddr *)&sa, addrlen)) {
			ok = 1;
			break;
		}
	}
	freeaddrinfo(res0);
#else
	for (i=0; hp->h_addr_list[i] && !ok; i++) {
		if (!memcmp(hp->h_addr_list[i], &((struct sockaddr_in *)&sa)->sin_addr, 
			    sizeof(struct in_addr))) ok = 1;
	}
#endif
	if (!ok) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		if (mp)
			send_reject_packet(mp, (struct sockaddr*)&sa, MACHINE_UNKNOWN, 0);
		return;
	}

	if (!mp) {
		/*
		 * Try to straighten out bad packets.
		 */
		quirk = rationalize_packet(inbuf, cc, sizeof(inbuf), (struct sockaddr*)&sa);
		if (quirk<0) {
			print_broken_packet(inbuf, cc, (struct sockaddr*)&sa);
			syslog(LOG_WARNING, "%s (%s): unintelligible packet", 
			       theirhost, theirip);
			send_reject_packet(mp, (struct sockaddr*)&sa, UNKNOWN_REQUEST, 0);
			return;
		}
		if (cm_import(inbuf, cc, &cm) < 0) {
			syslog(LOG_WARNING, "%s (%d): cannot import control message; ignored");
			return;
		}
		mp = &cm;
	}

	/*
	 * Make sure we know what we're getting into.
	 */
	if (mp->vers!=TALK_VERSION) {
		syslog(LOG_WARNING, "%s (%s): bad protocol version %d", 
		       theirhost, theirip, mp->vers);
		send_reject_packet(mp, (struct sockaddr*)&sa, BADVERSION, 0);
		return;
	}

	/*
	 * LEAVE_INVITE messages should only come from localhost.
	 * Of course, old talk clients send from our hostname's IP
	 * rather than localhost, complicating the issue...
	 */
	if (mp->type==LEAVE_INVITE && !is_local_address((struct sockaddr*)&sa)) {
		syslog(LOG_WARNING, "%s (%s) sent invite packet",
		       theirhost, theirip);
		send_reject_packet(mp, (struct sockaddr*)&sa, MACHINE_UNKNOWN, quirk);
		return;
	}

	/*
	 * Junk the reply address they reported for themselves. Write 
	 * the real one over it because announce.c gets it from there.
	 */
	memcpy(&mp->ctl_addr, &sa, sizeof(mp->ctl_addr));

	/*
	 * Since invite messages only come from localhost, and nothing
	 * but invite messages use the TCP address, force it to be our
	 * address.
	 * 
	 * Actually, if it's a local address, leave it alone. talk has
	 * to play games to figure out the right interface address to
	 * use, and we don't want to get into that - they can work it
	 * out, but we can't since we don't know who they're trying to
	 * talk to.
	 *
	 * If it's not a local address, someone's trying to play games
	 * with us. Rather than trying to pick a local address to use,
	 * reject the packet.
	 */
	if (mp->type==LEAVE_INVITE) {
		if (!is_local_address((struct sockaddr *)&mp->addr)) {
			syslog(LOG_WARNING, 
			       "invite packet had bad return address");
			send_reject_packet(mp, (struct sockaddr*)&sa, BADADDR, quirk);
			return;
		}
	}
	else {
		/* non-invite packets don't use this field */
		memset(&mp->addr, 0, sizeof(mp->addr));
	}

	process_request(mp, &response, theirhost);

	/* can block here, is this what I want? */
	send_packet(&response, (struct sockaddr*)&sa, quirk);
}

int
main(int argc, char *argv[])
{
#ifdef INET6
	struct sockaddr_storage sa;
#else
	struct sockaddr sa;
#endif
	socklen_t sz = sizeof(sa);
	int do_debug=0, do_badpackets=0, ch;

	/* make sure we're a daemon */
	if (getsockname(0, (struct sockaddr *)&sa, &sz)) {
		const char *msg = strerror(errno);
		write(2, msg, strlen(msg));
		exit(1);
	}
	openlog("talkd", LOG_PID, LOG_DAEMON);
	if (gethostname(ourhostname, sizeof(ourhostname) - 1) < 0) {
		syslog(LOG_ERR, "gethostname: %s", strerror(errno));
		exit(1);
	}
	if (chdir(_PATH_DEV) < 0) {
		syslog(LOG_ERR, "chdir: %s: %s", _PATH_DEV, strerror(errno));
		exit(1);
	}
	while ((ch = getopt(argc, argv, "dp"))!=-1) {
		switch (ch) {
		    case 'd': do_debug=1; break;
		    case 'p': do_badpackets=1; break;
		}
	}
	set_debug(do_debug, do_badpackets);

	signal(SIGALRM, timeout);
	alarm(TIMEOUT);
	for (;;) {
		do_one_packet();
	}
/*	return 0;  <--- unreachable because of the above loop */
}
