/*
 * Copyright (c) 1995, 1996, 1997, 1998 Kungliga Tekniska Högskolan
 * (Royal Institute of Technology, Stockholm, Sweden).
 * 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 Kungliga Tekniska
 *      Högskolan and its contributors.
 * 
 * 4. Neither the name of the Institute 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 INSTITUTE 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 INSTITUTE 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 "kx.h"

RCSID("$Id: kxd.c,v 1.43 1998/04/19 07:45:06 assar Exp $");

/*
 * Signal handler that justs waits for the children when they die.
 */

static RETSIGTYPE
childhandler (int sig)
{
     pid_t pid;
     int status;

     do { 
       pid = waitpid (-1, &status, WNOHANG|WUNTRACED);
     } while(pid > 0);
     signal (SIGCHLD, childhandler);
     SIGRETURN(0);
}

static void
fatal(int, des_cblock *, des_key_schedule,
      struct sockaddr_in *, struct sockaddr_in *,
      char *format, ...)
#ifdef __GNUC__
__attribute__ ((format (printf, 6, 7)))
#endif
;

static void
fatal (int fd, des_cblock *key, des_key_schedule schedule,
       struct sockaddr_in *thisaddr,
       struct sockaddr_in *thataddr,
       char *format, ...)
{
    u_char msg[1024];
    u_char *p;
    va_list args;
    int len;

    va_start(args, format);
    p = msg;
    *p++ = ERROR;
    vsnprintf (p + 4, sizeof(msg) - 5, format, args);
    syslog (LOG_ERR, p + 4);
    len = strlen (p + 4);
    p += krb_put_int (len, p, 4);
    p += len;
    write_encrypted (fd, msg, p - msg, schedule, key, thisaddr, thataddr);
    va_end(args);
    exit (1);
}

static void
cleanup(int nsockets, struct x_socket *sockets)
{
    int i;

    if(xauthfile[0])
	unlink(xauthfile);
    for (i = 0; i < nsockets; ++i) {
	if (sockets[i].pathname != NULL) {
	    unlink (sockets[i].pathname);
	    free (sockets[i].pathname);
	}
    }
}

static int
recv_conn (int sock, des_cblock *key, des_key_schedule schedule,
	   struct sockaddr_in *thisaddr,
	   struct sockaddr_in *thataddr)
{
     int status;
     KTEXT_ST ticket;
     AUTH_DAT auth;
     char user[ANAME_SZ + 1];
     char instance[INST_SZ + 1];
     int addrlen;
     char version[KRB_SENDAUTH_VLEN + 1];
     struct passwd *passwd;
     char remotehost[MaxHostNameLen];
     void *ret;
     int len;
     u_char msg[1024], *p;
     u_int32_t tmp;
     int flags;

     addrlen = sizeof(*thisaddr);
     if (getsockname (sock, (struct sockaddr *)thisaddr, &addrlen) < 0 ||
	 addrlen != sizeof(*thisaddr)) {
	 syslog (LOG_ERR, "getsockname: %m");
	 exit (1);
     }
     addrlen = sizeof(*thataddr);
     if (getpeername (sock, (struct sockaddr *)thataddr, &addrlen) < 0 ||
	 addrlen != sizeof(*thataddr)) {
	 syslog (LOG_ERR, "getpeername: %m");
	 exit (1);
     }

     inaddr2str (thataddr->sin_addr, remotehost, sizeof(remotehost));

     k_getsockinst (sock, instance, sizeof(instance));
     status = krb_recvauth (KOPT_DO_MUTUAL, sock, &ticket, "rcmd", instance,
			    thataddr, thisaddr, &auth, "", schedule,
			    version);
     if (status != KSUCCESS) {
	 syslog (LOG_ERR, "krb_recvauth: %s",
		 krb_get_err_text(status));
	 exit(1);
     }
     if( strncmp(version, KX_VERSION, KRB_SENDAUTH_VLEN) != 0) {
	 /* Try to be nice to old kx's */
	 if (strncmp (version, KX_OLD_VERSION, KRB_SENDAUTH_VLEN) == 0) {
	     char *old_errmsg = "\001Old version of kx. Please upgrade.";

	     syslog (LOG_ERR, "Old version client (%s)", version);

	     krb_net_read (sock, user, sizeof(user));
	     krb_net_write (sock, old_errmsg, strlen(old_errmsg) + 1);
	     exit (1);
	 }

	 fatal(sock, key, schedule, thisaddr, thataddr,
	       "Bad version %s", version);
     }
     memcpy(key, &auth.session, sizeof(des_cblock));

     len = read_encrypted (sock, msg, sizeof(msg), &ret,
			   schedule, key, thataddr, thisaddr);
     if (len < 0)
	 return 1;
     p = (u_char *)ret;
     if (*p != INIT)
	 fatal(sock, key, schedule, thisaddr, thataddr,
	       "Bad message");
     p++;
     p += krb_get_int (p, &tmp, 4, 0);
     len = min(sizeof(user), tmp);
     strncpy (user, p, len);
     p += tmp;
     user[len] = '\0';

     passwd = k_getpwnam (user);
     if (passwd == NULL)
	  fatal (sock, key, schedule, thisaddr, thataddr,
		 "Cannot find uid");
     if (kuserok(&auth, user) != 0)
	  fatal (sock, key, schedule, thisaddr, thataddr,
		 "%s is not allowed to login as %s",
		 krb_unparse_name_long (auth.pname,
					auth.pinst,
					auth.prealm),
		 user);
     if (setgid (passwd->pw_gid) ||
	 initgroups(passwd->pw_name, passwd->pw_gid) ||
	 setuid(passwd->pw_uid)) {
	 fatal (sock, key, schedule, thisaddr, thataddr,
		"Cannot set uid");
     }
     syslog (LOG_INFO, "from %s(%s): %s -> %s",
	     remotehost,
	     inet_ntoa(thataddr->sin_addr),
	     krb_unparse_name_long (auth.pname, auth.pinst, auth.prealm),
	     user);
     umask(077);
     flags = *p++;
     if (!(flags & PASSIVE)) {
	 p += krb_get_int (p, &tmp, 4, 0);
	 len = min(tmp, display_size);
	 strncpy (display, p, len);
	 display[len] = '\0';
	 p += tmp;
	 p += krb_get_int (p, &tmp, 4, 0);
	 len = min(tmp, xauthfile_size);
	 strncpy (xauthfile, p, len);
	 p += tmp;
     }
#if defined(SO_KEEPALIVE) && defined(HAVE_SETSOCKOPT)
     if (flags & KEEP_ALIVE) {
	 int one = 1;

	 setsockopt (sock, SOL_SOCKET, SO_KEEPALIVE, (void *)&one,
		     sizeof(one));
     }
#endif
     return flags;
}

/*
 *
 */

static int
passive_session (int fd, int sock, des_cblock *key,
		 des_key_schedule schedule)
{
    if (verify_and_remove_cookies (fd, sock))
	return 1;
    else
	return copy_encrypted (fd, sock, key, schedule);
}

static int
active_session (int fd, int sock, des_cblock *key,
		des_key_schedule schedule)
{
    fd = connect_local_xsocket(0);

    if (replace_cookie (fd, sock, xauthfile))
	return 1;
    else
	return copy_encrypted (fd, sock, key, schedule);
}

static int
doit_conn (int fd, int meta_sock, int flags,
	   des_cblock *key, des_key_schedule schedule,
	   struct sockaddr_in *thisaddr,
	   struct sockaddr_in *thataddr)
{
    int sock, sock2;
    struct sockaddr_in addr;
    int addrlen;
    u_char msg[1024], *p;

    sock = socket (AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
	syslog (LOG_ERR, "socket: %m");
	return 1;
    }
#if defined(TCP_NODELAY) && defined(HAVE_SETSOCKOPT)
    {
	int one = 1;
	setsockopt (sock, IPPROTO_TCP, TCP_NODELAY, (void *)&one, sizeof(one));
    }
#endif
#if defined(SO_KEEPALIVE) && defined(HAVE_SETSOCKOPT)
     if (flags & KEEP_ALIVE) {
	 int one = 1;

	 setsockopt (sock, SOL_SOCKET, SO_KEEPALIVE, (void *)&one,
		     sizeof(one));
     }
#endif
    memset (&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    if (bind (sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
	syslog (LOG_ERR, "bind: %m");
	return 1;
    }
    addrlen = sizeof(addr);
    if (getsockname (sock, (struct sockaddr *)&addr,
		     &addrlen) < 0) {
	syslog (LOG_ERR, "getsockname: %m");
	return 1;
    }
    if (listen (sock, SOMAXCONN) < 0) {
	syslog (LOG_ERR, "listen: %m");
	return 1;
    }
    p = msg;
    *p++ = NEW_CONN;
    p += krb_put_int (ntohs(addr.sin_port), p, 4);

    if (write_encrypted (meta_sock, msg, p - msg, schedule, key,
			 thisaddr, thataddr) < 0) {
	syslog (LOG_ERR, "write: %m");
	return 1;
    }

    sock2 = accept (sock, (struct sockaddr *)thisaddr, &addrlen);
    if (sock2 < 0) {
	syslog (LOG_ERR, "accept: %m");
	return 1;
    }
    close (sock);
    close (meta_sock);

    if (flags & PASSIVE)
	return passive_session (fd, sock2, key, schedule);
    else
	return active_session (fd, sock2, key, schedule);
}

/*
 *
 */

static void
check_user_console (int fd, des_cblock *key, des_key_schedule schedule,
		    struct sockaddr_in *thisaddr,
		    struct sockaddr_in *thataddr)
{
     struct stat sb;

     if (stat ("/dev/console", &sb) < 0)
	 fatal (fd, key, schedule, thisaddr, thataddr,
		"Cannot stat /dev/console");
     if (getuid() != sb.st_uid)
	 fatal (fd, key, schedule, thisaddr, thataddr,
		"Permission denied");
}

/*
 * Receive a connection on `sock' and process it.
 */

static int
doit(int sock, int tcpp)
{
     des_key_schedule schedule;
     des_cblock key;
     struct sockaddr_in me, him;
     int flags;
     u_char msg[1024], *p;
     struct x_socket *sockets;
     int nsockets;

     flags = recv_conn (sock, &key, schedule, &me, &him);

     if (flags & PASSIVE) {
	  int tmp;
	  int len;

	  tmp = get_xsockets (&nsockets, &sockets, tcpp);
	  if (tmp < 0)
	       return 1;
	  display_num = tmp;
	  if (tcpp)
	       snprintf (display, display_size, "localhost:%u", display_num);
	  else
	       snprintf (display, display_size, ":%u", display_num);
	  if(create_and_write_cookie (xauthfile, xauthfile_size, 
				      cookie, cookie_len)) {
             syslog(LOG_ERR, "create_and_write_cookie: %m");
             fatal (sock, &key, schedule, &me, &him,
                    "Cookie-creation failed with: %s",
		    strerror(errno));
             cleanup(nsockets, sockets);
	     return 1;
	  }

	  p = msg;
	  *p++ = ACK;
	  len = strlen (display);
	  p += krb_put_int (len, p, 4);
	  strncpy (p, display, len);
	  p += len;
	  len = strlen (xauthfile);
	  p += krb_put_int (len, p, 4);
	  strncpy (p, xauthfile, len);
	  p += len;
	  
	  if(write_encrypted (sock, msg, p - msg, schedule, &key,
			      &me, &him) < 0) {
	      syslog (LOG_ERR, "write: %m");
	      cleanup(nsockets, sockets);
	      return 1;
	  }
	  for (;;) {
	       pid_t child;
	       int fd;
	       fd_set fds;
	       int i;
	       int ret;
	       
	       FD_ZERO(&fds);
	       FD_SET(sock, &fds);
	       for (i = 0; i < nsockets; ++i)
		   FD_SET(sockets[i].fd, &fds);
	       ret = select(FD_SETSIZE, &fds, NULL, NULL, NULL);
	       if(ret <= 0)
		   continue;
	       if(FD_ISSET(sock, &fds)){
		   /* there are no processes left on the remote side
		    */
		   cleanup(nsockets, sockets);
		   exit(0);
	       } else if(ret) {
		   for (i = 0; i < nsockets; ++i) {
		       if (FD_ISSET(sockets[i].fd, &fds)) {
			   if (sockets[i].pathname == NULL) {
			       struct sockaddr_in peer;
			       int len = sizeof(peer);

			       fd = accept (sockets[i].fd,
					    (struct sockaddr *)&peer,
					    &len);
			       /* XXX */
			       if (fd >= 0 && suspicious_address (fd, peer)) {
				   close (fd);
				   fd = -1;
				   errno = EINTR;
			       }
			   } else {
			       int zero = 0;

			       fd = accept (sockets[i].fd, NULL, &zero);
			   }
			   break;
		       }
		   }
	       }
	       if (fd < 0)
		    if (errno == EINTR)
			 continue;
		    else {
			syslog (LOG_ERR, "accept: %m");
			return 1;
		    }

	       child = fork ();
	       if (child < 0) {
		   syslog (LOG_ERR, "fork: %m");
		   return 1;
	       } else if (child == 0) {
		    for (i = 0; i < nsockets; ++i)
			close (sockets[i].fd);
		    return doit_conn (fd, sock, flags,
				      &key, schedule, &me, &him);
	       } else {
		    close (fd);
	       }
	  }
     } else {
	  check_user_console (sock, &key, schedule, &me, &him);

	  p = msg;
	  *p++ = ACK;
	  
	  if(write_encrypted (sock, msg, p - msg, schedule, &key,
			      &me, &him) < 0) {
	      syslog (LOG_ERR, "write: %m");
	      return 1;
	  }
	  for (;;) {
	      pid_t child;
	      int len;
	      void *ret;
	      
	      len = read_encrypted (sock, msg, sizeof(msg), &ret,
				    schedule, &key,
				    &him, &me);
	      if (len < 0) {
		  syslog (LOG_ERR, "read: %m");
		  return 1;
	      }
	      p = (u_char *)ret;
	      if (*p != NEW_CONN) {
		  syslog (LOG_ERR, "bad_message: %d", *p);
		  return 1;
	      }

	      child = fork ();
	      if (child < 0) {
		  syslog (LOG_ERR, "fork: %m");
		  return 1;
	      } else if (child == 0) {
		  return doit_conn (sock, sock, flags,
				    &key, schedule, &me, &him);
	      } else {
	      }
	  }
     }
}

static void
usage (void)
{
     fprintf (stderr, "Usage: %s [-i] [-t] [-p port]\n", __progname);
     exit (1);
}

/*
 * kxd - receive a forwarded X conncection
 */

int
main (int argc, char **argv)
{
     int c;
     int no_inetd = 0;
     int tcpp = 0;
     int port = 0;

     set_progname (argv[0]);

     while( (c = getopt (argc, argv, "itp:")) != EOF) {
	  switch (c) {
	  case 'i':
	       no_inetd = 1;
	       break;
	  case 't':
	       tcpp = 1;
	       break;
	  case 'p':
	      port = htons(atoi (optarg));
	      break;
	  case '?':
	  default:
	       usage ();
	  }
     }

     if (no_inetd)
	  mini_inetd (port ? port : k_getportbyname("kx", "tcp",
						    htons(KX_PORT)));
     openlog(__progname, LOG_PID|LOG_CONS, LOG_DAEMON);
     signal (SIGCHLD, childhandler);
     return doit(STDIN_FILENO, tcpp);
}
