/*-
 * Copyright (c) 1983, 1988, 1989 The 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.
 */

#include <sys/param.h>       /* for MAXHOSTNAMELEN */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>           /* for snprintf(), BUFSIZ */
#include <syslog.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include "rlogind.h"

static int confirmed=0;
static int netf;

static const char *
topdomain(const char *h)
{
	const char *p;
	const char *maybe = NULL;
	int dots = 0;

	for (p = h + strlen(h); p >= h; p--) {
		if (*p == '.') {
			if (++dots == 2)
				return (p);
			maybe = p;
		}
	}
	return (maybe);
}

/*
 * Check whether host h is in our local domain,
 * defined as sharing the last two components of the domain part,
 * or the entire domain part if the local domain has only one component.
 * If either name is unqualified (contains no '.'),
 * assume that the host is local, as it will be
 * interpreted as such.
 */
static int
local_domain(const char *h)
{
	char localhost[MAXHOSTNAMELEN];
	const char *p1, *p2;

	localhost[0] = 0;
	(void) gethostname(localhost, sizeof(localhost));
	p1 = topdomain(localhost);
	p2 = topdomain(h);
	if (p1 == NULL || p2 == NULL || !strcasecmp(p1, p2))
		return(1);
	return(0);
}

static int
soaddr_eq_ip(const struct sockaddr *s1, const struct sockaddr *s2)
{
	const struct sockaddr_in6 * ipv6p = NULL;
	const struct sockaddr_in  * ipv4p = NULL;

	if (s1->sa_family != s2->sa_family) {
		/* handle IPv4-in-IPv6 */
		if (s1->sa_family == AF_INET6) {
			ipv6p = ((const struct sockaddr_in6 *)s1);
		} else if (s1->sa_family == AF_INET) {
			ipv4p = ((const struct sockaddr_in *)s1);
		} else {
			/* syslog(LOG_INFO, "soaddr_eq_ip: s1 is neither IPv4 nor IPv6"); */
			return 0;
		}
		if (s2->sa_family == AF_INET6) {
			ipv6p = ((const struct sockaddr_in6 *)s2);
		} else if (s2->sa_family == AF_INET) {
			ipv4p = ((const struct sockaddr_in *)s2);
		} else {
			/* syslog(LOG_INFO, "soaddr_eq_ip: s2 is neither IPv4 nor IPv6"); */
			return 0;
		}
		if (IN6_IS_ADDR_V4MAPPED(&(ipv6p->sin6_addr))) {
			char ipv6buf[INET6_ADDRSTRLEN + 1];
			char ipv4buf[INET_ADDRSTRLEN + 1];
			memset (ipv6buf, 0x00, INET6_ADDRSTRLEN + 1);
			memset (ipv4buf, 0x00, INET_ADDRSTRLEN + 1);
			if (inet_ntop(AF_INET6, (const void *)&(ipv6p->sin6_addr),ipv6buf, INET6_ADDRSTRLEN+1) != NULL &&
			    inet_ntop(AF_INET, (const void *)&(ipv4p->sin_addr), ipv4buf, INET_ADDRSTRLEN+1) != NULL&&
			    ipv4p->sin_addr.s_addr != INADDR_ANY &&
			    strncmp (ipv6buf, "::ffff:", 7) == 0) { /* probably overkill */
	  			/* syslog(LOG_INFO, "soaddr_eq_ip: (%s,%s)",ipv6buf,ipv4buf); */
				return (strncmp(ipv6buf+7, ipv4buf, sizeof(ipv4buf)) == 0);
			}
			/* syslog(LOG_INFO, "soaddr_eq_ip: failed to compare IPv4-mapped address"); */
			return 0;
		}
	  	/* syslog(LOG_INFO, "soaddr_eq_ip: (%d,%d) IPv6 value is not IPv4-mapped addr"); */
		return 0;
	}
	if (s2->sa_family == AF_INET6) {
	  	/* syslog(LOG_INFO, "soaddr_eq_ip: comparing two AF_INET6 addrs"); */
		return (memcmp(
			(const void*)(
				&((const struct sockaddr_in6 *)s1)->sin6_addr
				),
			(const void*)(
				&((const struct sockaddr_in6 *)s2)->sin6_addr
				),
			sizeof(struct in6_addr))
				== 0);
	}
	else {
	  	/* syslog(LOG_INFO, "soaddr_eq_ip: comparing two AF_INET addrs"); */
		return (memcmp(
			(const void*)(
				&((const struct sockaddr_in *)s1)->sin_addr
				),
			(const void*)(
				&((const struct sockaddr_in *)s2)->sin_addr
				),
			sizeof(struct in_addr))
				== 0);
	}
}

static char *
find_hostname(struct sockaddr *fromp, socklen_t fromlen,
	char *portname, int *hostokp)
{
	int error;
	char *hname;
	char hname_buf[NI_MAXHOST];
	int hostok = 0;

	error = getnameinfo(fromp, fromlen,
		hname_buf, sizeof(hname_buf), portname, NI_MAXSERV,
		NI_NUMERICSERV);
	if ((error == EAI_AGAIN) && !check_all)
		error = getnameinfo(fromp, fromlen,
				    hname_buf, sizeof(hname_buf), portname, NI_MAXSERV,
				    NI_NUMERICSERV|NI_NUMERICHOST);
	assert(error == 0);

	if (check_all || local_domain(hname_buf)) {
		/*
		 * If name returned is in our domain,
		 * attempt to verify that we haven't been fooled by someone
		 * in a remote net; look up the name and check that this
		 * address corresponds to the name.
		 */
		struct addrinfo hints;
		struct addrinfo *res0, *res;

		memset(&hints, 0, sizeof(hints));
		hints.ai_family = PF_UNSPEC;
		error = getaddrinfo(hname_buf, NULL, &hints, &res);
		assert(error == 0);

		res0 = res;
		while (res) {
			if (soaddr_eq_ip(fromp, res->ai_addr)) {
				hostok = 1;
				break;
			}
			res = res->ai_next;
		}
		freeaddrinfo(res0);

	        /* Special case for INADDR_LOOPBACK, since getnameinfo
	         * returns the canonical machine name when it ought to
	         * return "localhost". Fix it.
	         */
	        {
	            int is_localhost = 0;
	            switch (fromp->sa_family)
	            {
	            case AF_INET:
	              {
	                if (IN_LOOPBACK(ntohl(((struct sockaddr_in *)fromp)->sin_addr.s_addr)))
	                    is_localhost = 1;
	                break;
	              }

	            case AF_INET6:
	              {
	                struct sockaddr_in6 * fromp6 = (struct sockaddr_in6 *)fromp;
	                if (IN6_IS_ADDR_LOOPBACK(&fromp6->sin6_addr)) {
	                    is_localhost = 1;
	                } else {
			    if (IN6_IS_ADDR_V4MAPPED(&fromp6->sin6_addr)) {
				if (IN_LOOPBACK(ntohl(*(u_int32_t *)&fromp6->sin6_addr.s6_addr[12]))) {
				    is_localhost = 1;
				}
			    }
			}
	                break;
	              }

	            default:
	                break;
	            }

	            if (is_localhost) {
			hostok = 1;
	                strncpy (hname_buf, "localhost", sizeof(hname_buf));
	            }
	        }

	}
	else {
		hostok = 1;
	}

	hname = strdup(hname_buf);

	/* 
	 * Actually it might be null if we're out of memory, but
	 * where do we go then? We'd have to bail anyhow.
	 */
	assert(hname != NULL);

	*hostokp = hostok;

	return hname;
}



char * 
network_init(int f, int *hostokp)
{
	struct sockaddr_storage from, *fromp;
	socklen_t fromlen;
	int on = 1;
	char c;
	char *hname;
	char portname[NI_MAXSERV];
	int port;

	fromlen = sizeof (from);
	if (getpeername(f, (struct sockaddr *)&from, &fromlen) < 0) {
		syslog(LOG_ERR,"Can't get peer name of remote host: %m");
		fatal(STDERR_FILENO, "Can't get peer name of remote host", 1);
	}
	if (keepalive &&
	    setsockopt(f, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)) < 0)
		syslog(LOG_WARNING, "setsockopt (SO_KEEPALIVE): %m");
#ifdef IP_TOS
	#define IPTOS_LOWDELAY          0x10
	on = IPTOS_LOWDELAY;
	if (setsockopt(f, IPPROTO_IP, IP_TOS, &on, sizeof(on)) < 0)
		syslog(LOG_WARNING, "setsockopt (IP_TOS): %m");
#endif
	fromp = &from;

	if (((struct sockaddr_in *) fromp)->sin_family == AF_INET6 &&
		IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6 *) fromp)->sin6_addr)) {

		struct addrinfo *res, hints = {};
		char client_addr[INET6_ADDRSTRLEN] = {};
		char client_port[6] = {};

		inet_ntop(AF_INET6, &((struct sockaddr_in6 *) fromp)->sin6_addr,
			client_addr, sizeof(client_addr));

		sprintf(client_port, "%d", ntohs(((struct sockaddr_in6 *) fromp)->sin6_port));

		hints.ai_family = AF_INET;
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;

		getaddrinfo(client_addr, client_port, &hints, &res);

		memcpy(fromp, res->ai_addr, sizeof(struct sockaddr_in6));
		freeaddrinfo(res);
	}

	alarm(60);
	read(f, &c, 1);

	if (c != 0)
		exit(1);

	alarm(0);

	hname = find_hostname((struct sockaddr *)fromp, fromlen,
		portname, hostokp);
	assert(hname != NULL);

	port = atoi(portname);
	if (! port) {
	    syslog(LOG_NOTICE, "Unknown port %s", portname);
	    fatal(f, "Permission denied", 0);
	}
	if ((fromp->ss_family != AF_INET && fromp->ss_family != AF_INET6) ||
	    port >= IPPORT_RESERVED || port < IPPORT_RESERVED/2) {
	    syslog(LOG_NOTICE, "Connection from %s on illegal port",
	    	portname);
	    fatal(f, "Permission denied", 0);
	}

#ifdef IP_OPTIONS
	{
	    u_char optbuf[BUFSIZ/3], *cp;
	    char lbuf[BUFSIZ];
	    int lboff;
	    socklen_t optsize = sizeof(optbuf);
	    int ipproto;
	    struct protoent *ip;

	    if ((ip = getprotobyname("ip")) != NULL)
		    ipproto = ip->p_proto;
	    else
		    ipproto = IPPROTO_IP;
	    if (getsockopt(0, ipproto, IP_OPTIONS, (char *)optbuf,
		&optsize) == 0 && optsize != 0) {
		    lboff=0;
		    for (cp = optbuf; optsize > 0; cp++, optsize--, lboff += 3)
			    snprintf(lbuf+lboff, sizeof(lbuf)-lboff, 
				     " %2.2x", *cp);
		    syslog(LOG_NOTICE,
			"Connection received using IP options (ignored):%s",
			lbuf);
		    if (setsockopt(0, ipproto, IP_OPTIONS,
				   NULL, optsize) != 0) {
			    syslog(LOG_ERR, "setsockopt IP_OPTIONS NULL: %m");
			    exit(1);
		    }
	    }
	}
#endif

	return hname;
}

void network_confirm(void) {
    assert(confirmed>=0);

    if (confirmed == 0) {		/* do_rlogin may do this */
	write(netf, "", 1);
	confirmed = 1;		/* we sent the null! */
    }
}

void network_anticonfirm(void) {
    char x='\01';		/* error indicator */

    assert(confirmed>=0);

    if (!confirmed) {
	write(netf, &x, 1);
	/* 
	 * Still not confirmed, but we shouldn't ever get here again
	 * as we should be in the process of crashing.
	 */
	confirmed = -1;
    }
}

void network_close(void) {
    shutdown(netf, 2);
}
