/* Copyright 1990, Daniel J. Bernstein. All rights reserved. */

/*
pty.c: run a program under a pty session
*/

#include <stdio.h>
extern unsigned short getuid(); /* grrrr */
extern unsigned short geteuid(); /* grrrr */
#include "config.h"
#include "getopt.h"
#include "err.h"
#include "pty.h"
#include "tty.h"
#include "texts.h"
#include "sig.h"
#include "sigler.h"
#include "master.h"
#include "slave.h"
#include "file.h"
#include "logs.h"
#include "misc.h"

int flagpcbreak = 0; /* -pc, character-at-a-time */
int flagpnew = 1; /* -pd, new line discipline---traditionally off to start */
int flagpecho = 1; /* -pe, echo characters */
int flagpcrmod = 1; /* -pn, munge carriage returns */
int flagpraw = 0; /* -pr, raw mode */
int flagpcrt = 1; /* -ps, screen */

getfreepty(fnmty,fnsty,pty1,pty2)
register char fnmty[sizeof(DEVMTY)];
register char fnsty[sizeof(DEVSTY)];
register char pty1[sizeof(PTY1)];
register char pty2[sizeof(PTY2)];
{
 register char *c1;
 register char *c2;
 register char *c1start; /* for ``random'' pty searching */
 register char *c2start;
 int e;

 if (flagxrandom)
  {
   c1start = pty1 + (pid % (sizeof(PTY1) - 1));
   c2start = pty2 + ((pid + date) % (sizeof(PTY2) - 1));
  }
 else
  {
   c1start = pty1;
   c2start = pty2;
  }

 c1 = c1start;
 do
  {
   fnmty[sizeof(DEVMTY) - 3] = *c1;
   fnmty[sizeof(DEVMTY) - 2] = pty2[0];
   if (!access(fnmty,F_OK))
    {
     c2 = c2start;
     fnsty[sizeof(DEVSTY) - 3] = *c1;
     fnmty[sizeof(DEVMTY) - 2] = fnsty[sizeof(DEVSTY) - 2] = *c2;
     do
      {
#ifdef DESPERATE_ALARMS
       sig_startring();
#endif

/* Some other process could come along and mess up our test by opening */
/* the master side before we do. But in that case they'll get the pty */
/* anyway, and we'll move on to another possibility without comment. */
       if (flagxchkopen)
	{
#ifdef DONT_NDELAY
         fdsty = open(fnsty,O_RDWR);
#else
         fdsty = open(fnsty,O_RDWR | O_NDELAY);
#endif
         e = errno;
         fdmty = open(fnmty,O_RDWR);
	}
       else
	{
         fdmty = open(fnmty,O_RDWR);
         fdsty = open(fnsty,O_RDWR);
	 e = errno;
	}

#ifdef DESPERATE_ALARMS
       sig_stopring();
#endif

       if (fdmty != -1)
	{
	 if (flagxskipopen && (fdsty != -1))
	   warnerr2("pty: warning: slave %s still in use\n",fnsty);
	 else
	  {
	   if ((fdsty == -1) && (e != EINTR) && (e != EWOULDBLOCK))
	     fatalerr2p(6,"pty: fatal: slave %s unopenable",fnsty,e);
	   if (flagxchkopen)
	     if (fdsty == -1)
	      {
	       fdsty = open(fnsty,O_RDWR);
	       e = errno;
	      }
	     else
	       warnerr2("pty: warning: slave %s still in use\n",fnsty);
	   if (fdsty == -1)
	     fatalerr2p(6,"pty: fatal: slave %s unopenable",fnsty,e);
	   else
	    {
	     if (flagxchkopen)
	       if (fcntl(fdsty,F_SETFL,0) == -1)
		 fatalerrp(6,"pty: fatal: can't fcntl pty",e);
	     return 0;
	    }
	  }
	}

       if (fdmty != -1) (void) close(fdmty);
       if (fdsty != -1) (void) close(fdsty);
       if (!(*(++c2)))
	 c2 = pty2;
       fnmty[sizeof(DEVMTY) - 2] = fnsty[sizeof(DEVSTY) - 2] = *c2;
      }
     while (c2 != c2start);
    }
   if (!(*(++c1)))
     c1 = pty1;
  }
 while (c1 != c1start);
 return -1;
}

char fnmty[sizeof(DEVMTY)] = DEVMTY;
char fnsty[sizeof(DEVSTY)] = DEVSTY;
char pty1[sizeof(PTY1)] = PTY1;
char pty2[sizeof(PTY2)] = PTY2;

main(argc,argv)
int argc;
char *argv[];
{
 int opt;
 int f;

 uid = getuid();
 euid = geteuid();
 pid = getpid();
 pgrp = getpgrp(0);
 date = now();
 setusername();

 while ((opt = getopt(argc,argv,"qQvdDe3Ef:FjJsStTp:x:0ACHUVW")) != EOF)
   switch(opt)
    {
     case 'A': fatalinfo(1,ptyauthor);
     case 'C': fatalinfo(1,ptycopyright);
     case 'H': fatalinfo(1,ptyhelp);
     case 'U': fatalinfo(1,ptyusage);
     case 'V': fatalinfo(1,ptyversion);
     case 'W': fatalinfo(1,ptywarranty);
     case '?': fatalinfo(1,ptyusage);
     case 'q': flagquiet = 1; break;
     case 'Q': flagquiet = 0; flagverbose = 0; break;
     case 'v': flagverbose = 1; break;
     case 'd': flagdetached = 1; flagjobctrl = 0; flagttymodes = 0; break;
     case 'D': flagdetached = 0; flagjobctrl = 1; flagttymodes = 1; break;
     case 'e': flagsameerr = 2; break;
     case '3': flagsameerr = 1; break;
     case 'E': flagsameerr = 0; break;
     case 'f': flagfdpass = 1; 
	       if (sscanf(optarg,"%d",&fdpass) < 1) fatalinfo(1,ptyusage);
	       break;
     case 'F': flagfdpass = 0; break;
     case 'j': flagjobctrl = 1; break;
     case 'J': flagjobctrl = 0; break;
     case 's': flagsession = 1; flagxutmp = 1; break;
     case 'S': flagsession = 0; flagxutmp = 0; break;
     case 't': flagttymodes = 1; break;
     case 'T': flagttymodes = 0; break;
     case '0': flagsameerr = 2; flagsession = 0; flagttymodes = 0;
	       flagxutmp = 0; /* XXX: also flagxwtmp = 0? */
	       flagpcbreak = 3; flagpraw = 3; flagpecho = 2; flagpnew = 2;
	       break;
     case 'p': while (opt = *(optarg++))
		 switch(opt)
		  {
		   case 'c': flagpcbreak = 3; break;
		   case 'C': flagpcbreak = 2; break;
		   case 'd': flagpnew = 3; break;
		   case 'D': flagpnew = 2; break;
		   case 'e': flagpecho = 3; break;
		   case 'E': flagpecho = 2; break;
		   case 'n': flagpcrmod = 3; break;
		   case 'N': flagpcrmod = 2; break;
		   case 'r': flagpraw = 3; break;
		   case 'R': flagpraw = 2; break;
		   case 's': flagpcrt = 3; break;
		   case 'S': flagpcrt = 2; break;
		   case '0': flagpcbreak = 3; flagpraw = 3;
			     flagpecho = 2; flagpnew = 2;
			     break;
		   default: fatalinfo(1,ptyusage); break;
		  }
               break;
     case 'x': while (opt = *(optarg++))
		 switch(opt)
		  {
		   case 'c': flagxchown = 1; break;
		   case 'C': flagxchown = 0; break;
		   case 'u': flagxutmp = 1; break;
		   case 'U': flagxutmp = 0; break;
		   case 'w': flagxwtmp = 1; break;
		   case 'W': flagxwtmp = 0; break;
		   case 'x': flagxexcl = 1; break;
		   case 'X': flagxexcl = 0; break;
		   case 'e': flagxerrwo = 1; break;
		   case 'E': flagxerrwo = 0; break;
		   case 'n': flagxchkopen = 1; break;
		   case 'N': flagxchkopen = 0; break;
		   case 'o': flagxskipopen = 1; break;
		   case 'O': flagxskipopen = 0; break;
		   case 'r': flagxrandom = 1; break;
		   case 'R': flagxrandom = 0; break;
		   case 's': flagxsetuid = 1; break;
		   case 'S': flagxsetuid = 0; break;
		   default: fatalinfo(1,ptyusage); break;
		  }
               break;
    }
 argv += optind, argc -= optind;

 if (!*argv)
   fatalinfo(1,ptyusage);

 /* Option forcing. */
#ifdef NO_UTMP
  if (flagxutmp) if (flagverbose) warnerr2("%s","pty: utmp forced off\n");
  flagxutmp = 0;
#endif
#ifdef NO_WTMP
  if (flagxwtmp) if (flagverbose) warnerr2("%s","pty: wtmp forced off\n");
  flagxwtmp = 0;
#endif
#ifdef NO_CHOWN
  if (flagxchown) if (flagverbose) warnerr2("%s","pty: chown forced off\n");
  flagxchown = 0;
#endif
#ifdef NO_SESSION
  if (flagsession) if (flagverbose) warnerr2("%s","pty: session forced off\n");
  flagsession = 0;
#endif
#ifdef MUST_UTMP
  if (flagxutmp) if (flagverbose) warnerr2("%s","pty: utmp forced on\n");
  flagxutmp = 1;
#endif
#ifdef MUST_WTMP
  if (flagxwtmp) if (flagverbose) warnerr2("%s","pty: wtmp forced on\n");
  flagxwtmp = 1;
#endif
#ifdef MUST_CHOWN
  if (flagxchown) if (flagverbose) warnerr2("%s","pty: chown forced on\n");
  flagxchown = 1;
#endif
#ifdef MUST_SESSION
  if (flagsession) if (flagverbose) warnerr2("%s","pty: session forced on\n");
  flagsession = 1;
#endif
#ifdef NO_FDPASSING
  if (flagfdpass) if (flagverbose) warnerr2("%s","pty: fd passing forced off\n");
  flagfdpass = 0;
#endif

 /* Option munging. */
 if (flagsession) flagsameerr = 0;
 if (flagdetached) flagttymodes = 0;
 if (flagxskipopen) flagxchkopen = 1;

 if (!flagxsetuid)
  {
   (void) setreuid(uid,uid);
   euid = uid;
  }


 sig_init();
 sig_sethandler(SIGALRM,nothing); sig_handle(SIGALRM);
 sig_default(SIGTTIN);
 sig_default(SIGTTOU);

 if (fdpass == -1) /* wow, was this a source of bugs. */
  {
   fdin = 0;
   fdout = 1;
  }

 if (flagdetached)
  {
   tty_initmodes(&tmopty,flagpcbreak,flagpnew,flagpecho,
			 flagpcrmod,flagpraw,flagpcrt);
  }
 else
  {
   if ((fdtty = tty_getctrl()) == -1)
     fatalerr(2,"pty: fatal: cannot find control terminal; try -d?\n");
   if (tty_getmodes(fdtty,&tmotty) == -1)
     fatalerr(3,"pty: fatal: cannot get current tty modes\n");
     /* XXX: is there a way to recover more gracefully? */
   tty_copymodes(&tmopty,&tmotty);
   tty_mungemodes(&tmopty,flagpcbreak,flagpnew,flagpecho,
			  flagpcrmod,flagpraw,flagpcrt);
   tty_copymodes(&tmochartty,&tmotty);
   if (flagttymodes)
     tty_charmode(&tmochartty);
  }

 /* XXX: Here would be a good spot to include pty limits, say through */
 /* the file PTYDIR/LIMITS. Lines of the form user group num, saying */
 /* that user in that group is limited to num ptys, with * for all. */
 /* All pty use would have to be logged somewhere. Anyway, with a */
 /* streams-based pty, there wouldn't be much point to limits. */

 if (getfreepty(fnmty,fnsty,pty1,pty2) == -1)
   fatalerr(5,"pty: fatal: no ptys available\n");

 if (flagverbose)
   warnerr2("pty: successfully opened pty %s\n",fnsty);

 if (tty_modifymodes(fdtty,&tmochartty,&tmotty) == -1)
  {
   (void) tty_setmodes(fdtty,&tmotty); /* XXX --- gasp */
   fatalerr(4,"pty: fatal: cannot set modes of original tty\n");
  }

/* In general, BSD systems check MAXUPRC against the effective uid, */
/* rather than the real uid; and they check it during a fork(). */
/* The combination of these annoying behaviors means that we have */
/* to switch uids while forking, hence possibly losing any security */
/* measures we may have set up before the fork(). Grrrr. */

 (void) setreuid(euid,uid);
 if ((f = fork()) == -1)
  {
   (void) tty_modifymodes(fdtty,&tmotty,&tmochartty);
   fatalerr(7,"pty: fatal: cannot fork once\n");
   /* After this, the signaller will handle tty modes. */
  }
 else if (f == 0)
   if ((f = fork()) == -1)
    {
     (void) kill(pid,SIGTERM); /*XXX*/
     fatalerr(7,"pty: fatal: cannot fork twice\n");
    }
   else if (f == 0)
    {
     (void) setreuid(uid,euid);
     slave(fnsty,argv);
    }
   else
    {
     (void) setreuid(uid,euid);
     if (flagsession)
       if (sessdir() == -1)
         fatal(1);
     master(fnsty,f);
    }
 else
  {
   (void) setreuid(uid,euid);
   if (flagsession)
     if (sessdir() == -1)
      {
       fatalerr(8,"pty: fatal: cannot change to session directory\n");
       (void) tty_modifymodes(fdtty,&tmotty,&tmochartty);
      }
   sigler(fnsty,f);
  }

 fatal(9); /* just in case */
 /*NOTREACHED*/
}
