/*
 * Copyright (c) 1983 Regents of the University of California, (c) 1998 David Faure.
 * 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.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR
 *    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.
 *
 * (BSD License, from tdelibs/doc/common/bsd-license.html)
 */

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
 */

#include "includ.h"

/*
 * talkd - internet talk daemon
 * loops waiting for and processing requests until idle for a while,
 * then exits.
 */
#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 <sys/utsname.h>

#include "proto.h"
#include "process.h"
#include "readconf.h"
#include "defs.h"
#include "threads.h"
#include "table.h"
#include "machines/answmach.h"
#include "machines/talkconn.h"

#include <kinstance.h>
KTalkdTable * ktable;

#define TIMEOUT 20
 /* TIMEOUT was 30, but has been reduced to remove the
    zombie process (answering machine) as soon as possible */
#define MAXIDLE 120
 /* #define MAXIDLE 30     for debugging purposes */

#define NB_MAX_CHILD 20 /* Max number of answering / forwarding machines at a time */

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

static time_t lastmsgtime;

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

        int ok_exit = ack_process();
        if (ok_exit && (time(0) - lastmsgtime >= MAXIDLE)) {
        	delete ktable;
        	_exit(0);
	}

	signal(SIGALRM, timeout);
	alarm(TIMEOUT);
}

/*
 * 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(uint32_t addr)
{
	struct sockaddr_in sn;
	int sock, ret;
	if (addr == htonl(INADDR_LOOPBACK)) {
		return 1;
	}

	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 = AF_INET;
	sn.sin_port = htons(0);
	sn.sin_addr.s_addr = addr;
	ret = bind(sock, (struct sockaddr *)&sn, sizeof(sn));
	close(sock);
	return ret==0;
}

static void
send_packet(CTL_RESPONSE *response, struct sockaddr_in *sn, int quirk)
{
	char buf[2*sizeof(CTL_RESPONSE)];
	size_t sz = sizeof(CTL_RESPONSE);
	int cc, err=0;

	memcpy(buf, response, sz);
	if (quirk) {
		sz = irrationalize_reply(buf, sizeof(buf), quirk);
	}
	while (sz > 0) {
		cc = sendto(1, buf, sz, 0, (struct sockaddr *)sn, sizeof(*sn));
		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 uint8_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(CTL_MSG *mp, struct sockaddr_in *sn, int code, int quirk)
{
	CTL_RESPONSE rsp;
	memset(&rsp, 0, sizeof(rsp));
	rsp.vers = TALK_VERSION;
	rsp.type = mp->type;
	rsp.answer = code;
	send_packet(&rsp, sn, quirk);
}

static void
do_one_packet(void)
{
	char inbuf[2*sizeof(CTL_MSG)];
	int quirk = 0;
	CTL_RESPONSE response;
	CTL_MSG *mp;
	char theirhost[MAXHOSTNAMELEN];
	const char *theirip;

	struct hostent *hp;
	struct sockaddr_in sn;
	int cc, i, ok;
	socklen_t addrlen;
	int ret_value = PROC_REQ_OK; /* return value from process_request */

	addrlen = sizeof(sn);
	cc = recvfrom(0, inbuf, sizeof(inbuf), 0,
		      (struct sockaddr *)&sn, &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);

	if (addrlen!=sizeof(sn)) {
		syslog(LOG_WARNING, "recvfrom: bogus address length");
		return;
	}
	if (sn.sin_family!=AF_INET) {
		syslog(LOG_WARNING, "recvfrom: bogus address 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.
	 */
	theirip = inet_ntoa(sn.sin_addr);
	mp = (CTL_MSG *)inbuf;

	/*
	 * 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.
	 */
	hp = gethostbyaddr((char *)&sn.sin_addr, sizeof(struct in_addr),
			   AF_INET);
	if (hp == NULL) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		send_reject_packet(mp, &sn, MACHINE_UNKNOWN, 0);
		return;
	}
	strncpy(theirhost, hp->h_name, sizeof(theirhost));
	theirhost[sizeof(theirhost)-1] = 0;

	hp = gethostbyname(theirhost);
	if (hp == NULL) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		send_reject_packet(mp, &sn, MACHINE_UNKNOWN, 0);
		return;
	}

	for (i=ok=0; hp->h_addr_list[i] && !ok; i++) {
		if (!memcmp(hp->h_addr_list[i], &sn.sin_addr,
			    sizeof(sn.sin_addr))) ok = 1;
	}
	if (!ok) {
		syslog(LOG_WARNING, "%s: bad dns", theirip);
		send_reject_packet(mp, &sn, MACHINE_UNKNOWN, 0);
		return;
	}

	/*
	 * Try to straighten out bad packets.
	 */
	quirk = rationalize_packet(inbuf, cc, sizeof(inbuf), &sn);
	if (quirk<0) {
		print_broken_packet(inbuf, cc, &sn);
		syslog(LOG_WARNING, "%s (%s): unintelligible packet",
		       theirhost, theirip);
		send_reject_packet(mp, &sn, UNKNOWN_REQUEST, 0);
		return;
	}

	/*
	 * 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, &sn, 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(sn.sin_addr.s_addr)) {
		syslog(LOG_WARNING, "%s (%s) sent invite packet",
		       theirhost, theirip);
		send_reject_packet(mp, &sn, 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.
	 */
	mp->ctl_addr.ta_family = AF_INET;
	mp->ctl_addr.ta_port = sn.sin_port;
	mp->ctl_addr.ta_addr = sn.sin_addr.s_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) {
		mp->addr.ta_family = AF_INET;
		if (!is_local_address(mp->addr.ta_addr)) {
			syslog(LOG_WARNING,
			       "invite packet had bad return address");
			send_reject_packet(mp, &sn, BADADDR, quirk);
			return;
		}
	}
	else {
		/* non-invite packets don't use this field */
		memset(&mp->addr, 0, sizeof(mp->addr));
	}

        ret_value = process_request(mp, &response, theirhost);

        if (ret_value != PROC_REQ_FORWMACH)
        {
            /* can block here, is this what I want? */
            send_packet(&response, &sn, quirk);
        }

        if (Options.answmach && (ret_value>=PROC_REQ_MIN_A))
        {
            ktalk_debug("Launch answer machine, mode %d.", ret_value);
            AnswMachine::launchAnswMach(*mp, ret_value);
            new_process();
        }
}

/* the following copies defaults values to 'Options.*' variables so that
 * configuration file can overwrite them */

int
main(int argc, char *argv[])
{
	struct sockaddr_in sn;
	socklen_t sz = sizeof(sn);
	int do_debug=0, do_badpackets=0, ch;

	new TDEInstance("ktalkd"); // for TDEConfig and friends
	ktable = new KTalkdTable();

	/* make sure we're a daemon */
	if (getsockname(0, (struct sockaddr *)&sn, &sz)) {
		const char *msg = strerror(errno);
		write(2, msg, strlen(msg));
		exit(1);
	}

	openlog(*argv, LOG_PID, LOG_DAEMON);

	/* get local machine name */
	// using 'uname' instead of 'gethostname', as suggested by Stephan Kulow
	struct utsname buf;
	if (uname (&buf) == -1) {
	    syslog (LOG_ERR, "Unable to get name of local host : %s", strerror(errno));
	    exit(1);
	}
	strcpy(Options.hostname, buf.nodename);

	/* if (gethostname(Options.hostname, sizeof (Options.hostname) - 1) < 0) {
		syslog(LOG_ERR, "gethostname: %m");
		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':
		    Options.debug_mode = 1;
		    do_debug=1; break;
		    case 'p': do_badpackets=1; break;
		}
	}
	set_debug(do_debug, do_badpackets);

	TalkConnection::init(); /* global initialization */
	process_config_file(); /* read configuration */

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