/*
 * Copyright (c) 1995, 1996, 1997 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. 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 "bsd_locl.h"

#ifndef HAVE_FORKPTY

RCSID("$Id: forkpty.c,v 1.57 1999/12/02 16:58:28 joda Exp $");

/* Only CRAY is known to have problems with forkpty(). */
#if defined(CRAY)
static int forkpty_ok = 0;
#else
static int forkpty_ok = 1;
#endif

#ifndef HAVE_PTSNAME
static char *ptsname(int fd)
{
#ifdef HAVE_TTYNAME
  return ttyname(fd);
#else
  return NULL;
#endif
}
#endif

#ifndef HAVE_GRANTPT
#define grantpt(fdm) (0)
#endif

#ifndef HAVE_UNLOCKPT
#define unlockpt(fdm) (0)
#endif

#ifndef HAVE_VHANGUP
#define vhangup() (0)
#endif

#ifndef HAVE_REVOKE
static
void
revoke(char *line)
{
    int slave;
    RETSIGTYPE (*ofun)();

    if ( (slave = open(line, O_RDWR)) < 0)
	return;
    
    ofun = signal(SIGHUP, SIG_IGN);
    vhangup();
    signal(SIGHUP, ofun);
    /*
     * Some systems (atleast SunOS4) want to have the slave end open
     * at all times to prevent a race in the child. Login will close
     * it so it should really not be a problem. However for the
     * paranoid we use the close on exec flag so it will only be open
     * in the parent. Additionally since this will be the controlling
     * tty of rlogind the final vhangup() in rlogind should hangup all
     * processes. A working revoke would of course have been prefered
     * though (sigh).
     */
    fcntl(slave, F_SETFD, 1);
    /* close(slave); */
}
#endif


static int pty_major, pty_minor;

static void
pty_scan_start(void)
{
    pty_major = -1;
    pty_minor = 0;
}

static char *bsd_1 = "0123456789abcdefghijklmnopqrstuv";
/* there are many more */
static char *bsd_2 = "pqrstuvwxyzabcdefghijklmnoABCDEFGHIJKLMNOPQRSTUVWXYZ";

static int
pty_scan_next(char *buf, size_t sz)
{
#ifdef CRAY
    if(++pty_major >= sysconf(_SC_CRAY_NPTY))
	return -1;
    snprintf(buf, sz, "/dev/pty/%03d", pty_major);
#else
    if(++pty_major == strlen(bsd_1)){
	pty_major = 0;
	if(++pty_minor == strlen(bsd_2))
	    return -1;
    }
#ifdef __hpux
    snprintf(buf, sz, "/dev/ptym/pty%c%c", bsd_2[pty_major], bsd_1[pty_minor]);
#else
    snprintf(buf, sz, "/dev/pty%c%c", bsd_2[pty_major], bsd_1[pty_minor]);
#endif /* __hpux */
#endif /* CRAY */
    return 0;
}

static void
pty_scan_tty(char *buf, size_t sz)
{
#ifdef CRAY
    snprintf(buf, sz, "/dev/ttyp%03d", pty_major);
#elif defined(__hpux)
    snprintf(buf, sz, "/dev/pty/tty%c%c", bsd_2[pty_major], bsd_1[pty_minor]);
#else
    snprintf(buf, sz, "/dev/tty%c%c", bsd_2[pty_major], bsd_1[pty_minor]);
#endif
}

static int
ptym_open_streams_flavor(char *pts_name,
			 size_t pts_name_sz,
			 int *streams_pty)
{
    /* Try clone device master ptys */
    const char *const clone[] = { "/dev/ptc", "/dev/ptmx",
				  "/dev/ptm", "/dev/ptym/clone", 0 };
    int	fdm;
    const char *const *q;
    
    for (q = clone; *q; q++) {
	fdm = open(*q, O_RDWR);
	if (fdm >= 0)
	    break;
    }
    if (fdm >= 0) {
	char *ptr1;
	if ((ptr1 = ptsname(fdm)) != NULL) /* Get slave's name */
	    /* Return name of slave */  
	    strlcpy(pts_name, ptr1, pts_name_sz);
	else {
	    close(fdm);
	    return(-4);
	}
	if (grantpt(fdm) < 0) {	/* Grant access to slave */
	    close(fdm);
	    return(-2);
	}
	if (unlockpt(fdm) < 0) {	/* Clear slave's lock flag */
	    close(fdm);
	    return(-3);
	}
	return(fdm);			/* return fd of master */
    }
    return -1;
}

static int
ptym_open_bsd_flavor(char *pts_name, size_t pts_name_sz, int *streams_pty)
{
    int fdm;
    char ptm[MaxPathLen];

    pty_scan_start();

    while (pty_scan_next(ptm, sizeof(ptm)) != -1) {
	fdm = open(ptm, O_RDWR);
	if (fdm < 0)
	    continue;
#if SunOS == 40
	/* Avoid a bug in SunOS4 ttydriver */
	if (fdm > 0) {
	    int pgrp;
	    if ((ioctl(fdm, TIOCGPGRP, &pgrp) == -1)
		&& (errno == EIO))
		/* All fine */;
	    else {
		close(fdm);
		continue;
	    }
	}
#endif
	pty_scan_tty(pts_name, sizeof(ptm));
#if CRAY
	/* this is some magic from the telnet code */
	{
	    struct stat sb;
	    if(stat(pts_name, &sb) < 0) {
		close(fdm);
		continue;
	    }
	    if(sb.st_uid || sb.st_gid || sb.st_mode != 0600) {
		chown(pts_name, 0, 0);
		chmod(pts_name, 0600);
		close(fdm);
		fdm = open(ptm, 2);
		if (fdm < 0)
		    continue;
	    }
	}
	/*
	 * Now it should be safe...check for accessability.
	 */
	if (access(pts_name, 6) != 0){
	    /* no tty side to pty so skip it */
	    close(fdm);
	    continue;
	}
#endif
	return fdm;	/* All done! */
    }
    
    /* We failed to find BSD style pty */
    errno = ENOENT;
    return -1;
}

/*
 *
 * Open a master pty either using the STREAM flavor or the BSD flavor.
 * Depending on if there are any free ptys in the different classes we
 * need to try both. Normally try STREAMS first and then BSD.
 *
 * Kludge alert: Under HP-UX 10 and perhaps other systems STREAM ptys
 * doesn't get initialized properly so we try them in different order
 * until the problem has been resolved.
 *
 */
static int
ptym_open(char *pts_name, size_t pts_name_sz, int *streams_pty)
{
    int	fdm;

#ifdef HAVE__GETPTY
    {
	char *p = _getpty(&fdm, O_RDWR, 0600, 1);
	if (p) {
	    *streams_pty = 1;
	    strlcpy (pts_name, p, pts_name_sz);
	    return fdm;
	}
    }
#endif

#ifdef STREAMSPTY
    fdm = ptym_open_streams_flavor(pts_name, pts_name_sz, streams_pty);
    if (fdm >= 0)
      {
	*streams_pty = 1;
	return fdm;
      }
#endif
    
    fdm = ptym_open_bsd_flavor(pts_name, pts_name_sz, streams_pty);
    if (fdm >= 0)
      {
	*streams_pty = 0;
	return fdm;
      }

#ifndef STREAMSPTY
    fdm = ptym_open_streams_flavor(pts_name, pts_name_sz, streams_pty);
    if (fdm >= 0)
      {
	*streams_pty = 1;
	return fdm;
      }
#endif
    
    return -1;
}

static int
maybe_push_modules(int fd, char **modules)
{
#ifdef I_PUSH
  char **p;
  int err;

  for(p=modules; *p; p++){
    err=ioctl(fd, I_FIND, *p);
    if(err == 1)
      break;
    if(err < 0 && errno != EINVAL)
      return -17;
    /* module not pushed or does not exist */
  }
  /* p points to null or to an already pushed module, now push all
     modules before this one */

  for(p--; p >= modules; p--){
    err = ioctl(fd, I_PUSH, *p);
    if(err < 0 && errno != EINVAL)
      return -17;
  }
#endif
  return 0;
}

static int
ptys_open(int fdm, char *pts_name, int streams_pty)
{
    int fds;

    if (streams_pty) {
	/* Streams style slave ptys */
	if ( (fds = open(pts_name, O_RDWR)) < 0) {
	    close(fdm);
	    return(-5);
	}

	{
	  char *ttymodules[] = { "ttcompat", "ldterm", "ptem", NULL };
	  char *ptymodules[] = { "pckt", NULL };
	  
	  if(maybe_push_modules(fds, ttymodules)<0){
	    close(fdm);
	    close(fds);
	    return -6;
	  }
	  if(maybe_push_modules(fdm, ptymodules)<0){
	    close(fdm);
	    close(fds);
	    return -7;
	  }
	}
    } else {
        /* BSD style slave ptys */
	struct group *grptr;
	int gid;
	if ( (grptr = getgrnam("tty")) != NULL)
	    gid = grptr->gr_gid;
	else
	    gid = -1;	/* group tty is not in the group file */

	/* Grant access to slave */
	if (chown(pts_name, getuid(), gid) < 0)
	  fatal(0, "chown slave tty failed", 1);
	if (chmod(pts_name, S_IRUSR | S_IWUSR | S_IWGRP) < 0)
	  fatal(0, "chmod slave tty failed", 1);

	if ( (fds = open(pts_name, O_RDWR)) < 0) {
	    close(fdm);
	    return(-1);
	}
    }
    return(fds);
}

int
forkpty_truncate(int *ptrfdm,
		 char *slave_name,
		 size_t slave_name_sz,
		 struct termios *slave_termios,
		 struct winsize *slave_winsize)
{
    int		fdm, fds, streams_pty;
    pid_t	pid;
    char	pts_name[20];

    if (!forkpty_ok)
        fatal(0, "Protocol not yet supported, use telnet", 0);

    if ( (fdm = ptym_open(pts_name, sizeof(pts_name), &streams_pty)) < 0)
	return -1;

    if (slave_name != NULL)
	/* Return name of slave */
	strlcpy(slave_name, pts_name, slave_name_sz);

    pid = fork();
    if (pid < 0)
	return(-1);
    else if (pid == 0) {		/* Child */
	if (setsid() < 0)
	    fatal(0, "setsid() failure", errno);

        revoke(slave_name);

#if defined(NeXT) || defined(ultrix)
	/* The NeXT is severely broken, this makes things slightly
	 * better but we still doesn't get a working pty. If there
	 * where a TIOCSCTTY we could perhaps fix things but... The
	 * same problem also exists in xterm! */
	if (setpgrp(0, 0) < 0)
	    fatal(0, "NeXT kludge failed setpgrp", errno);
#endif

	/* SVR4 acquires controlling terminal on open() */
	if ( (fds = ptys_open(fdm, pts_name, streams_pty)) < 0)
	    return -1;
	close(fdm);		/* All done with master in child */
	
#if	defined(TIOCSCTTY) && !defined(CIBAUD) && !defined(__hpux)
	/* 44BSD way to acquire controlling terminal */
	/* !CIBAUD to avoid doing this under SunOS */
	if (ioctl(fds, TIOCSCTTY, (char *) 0) < 0)
	    return -1;
#endif
#if defined(NeXT)
	{
	    int t = open("/dev/tty", O_RDWR);
	    if (t < 0)
	        fatal(0, "Failed to open /dev/tty", errno);
	    close(fds);
	    fds = t;
	}
#endif
	/* Set slave's termios and window size */
	if (slave_termios != NULL) {
	    if (tcsetattr(fds, TCSANOW, slave_termios) < 0)
		return -1;
	}
#ifdef TIOCSWINSZ
	if (slave_winsize != NULL) {
	    if (ioctl(fds, TIOCSWINSZ, slave_winsize) < 0)
		return -1;
	}
#endif
	/* slave becomes stdin/stdout/stderr of child */
	if (dup2(fds, STDIN_FILENO) != STDIN_FILENO)
	    return -1;
	if (dup2(fds, STDOUT_FILENO) != STDOUT_FILENO)
	    return -1;
	if (dup2(fds, STDERR_FILENO) != STDERR_FILENO)
	    return -1;
	if (fds > STDERR_FILENO)
	    close(fds);
	return(0);		/* child returns 0 just like fork() */
    }
    else {			/* Parent */
	*ptrfdm = fdm;	/* Return fd of master */
	return(pid);	/* Parent returns pid of child */
    }
}

int
forkpty(int *ptrfdm,
	char *slave_name,
	struct termios *slave_termios,
	struct winsize *slave_winsize)
{
    return forkpty_truncate (ptrfdm,
			     slave_name,
			     MaxPathLen,
			     slave_termios,
			     slave_winsize);
}

#endif /* HAVE_FORKPTY */
