/*****************************************************************************
 * Filename:    cryptfs/vt.c                                                 *
 * Description: TTY handling                                                 *
 * Copyright:   2009 by Alexander Motzkau                                    *
 *****************************************************************************/
#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/vt.h>

#include "tty.h"
#include "options.h"
#include "actions.h"
#include "misc.h"

enum ttytype found_tty = (enum ttytype) -1;

/* Infos for virtual terminals. */
int vtnr = -1;
int origvtnr;

pid_t xterm_pid;

void reserve_tty()
{
    struct stat info;
    int fd;
    
    for(fd=0; fd<3; fd++)
	if(fstat(fd,&info)<0)
	{
	    int nfd;
	    nfd = open("/dev/null", O_RDWR);

	    if(nfd<0) /* We're screwed. */
		continue;
		
	    if(nfd==fd)
		continue;
	    
	    dup2(nfd, fd);
	    if(nfd > 2)
		close(nfd);
	}
}


char *devs[] = { "/dev/tty", "/dev/tty0", "/dev/console", NULL};
int modes[] = { O_RDWR, O_RDONLY, O_WRONLY, 0 };

void get_tty(struct configuration* config)
{
    struct tty* tconf;
    char *orig_term;
    
    if(found_tty != -1)
	return;
    
    orig_term = getenv("TERM");
    
    for(tconf=config->ttys; tconf!=NULL; tconf = tconf->next)
    {
	char* orig_disp;
	char *orig_auth;
	
	orig_disp = orig_auth = NULL;
	
	if(tconf->type == xterm)
	{
	    if(tconf->display!=NULL)
	    {
		orig_disp = getenv("DISPLAY");
		setenv("DISPLAY", tconf->display, 1);
	    }
	    if(tconf->authority!=NULL)
	    {
		orig_auth = getenv("XAUTHORITY");
		setenv("XAUTHORITY", tconf->authority, 1);
	    }
	}
	
	if(handle_commands(tconf->preexec, true, config))
	    switch(tconf->type)
	    {
		case current:
		    if(isatty(STDIN_FILENO) &&
			(isatty(STDOUT_FILENO) || isatty(STDERR_FILENO)))
		    {
			if(!isatty(STDOUT_FILENO))
			{
			    if(dup2(STDERR_FILENO, STDOUT_FILENO)<0)
				errnomsg("dup2 to stdout");
			}
		    
			if(!isatty(STDERR_FILENO))
			{
			    if(dup2(STDOUT_FILENO, STDERR_FILENO)<0)
				errnomsg("dup2 to stderr");
			}

		        found_tty = current;
		    }
		    else
		    {
			int fd;
		    
			fd = open("/dev/tty", O_RDWR);
			if(fd < 0)
			    break;

			if(dup2(fd, STDIN_FILENO)<0)
			    errnomsg("dup2 to stdin");
			if(dup2(fd, STDERR_FILENO)<0)
			    errnomsg("dup2 to stderr");
			if(dup2(fd, STDOUT_FILENO)<0)
			    errnomsg("dup2 to stdout");
			close(fd);
		    
			found_tty = current;
		    }
		    break;
		    
		case fixed:
		    {
			int fd;
		
			fd = open(tconf->dev, O_RDWR);
			if(fd<0)
			{
			    errnomsg(tconf->dev);
			    break;
		        }
		    
			if(!isatty(fd))
		        {
			    errmsg("%s is not a TTY.\n", tconf->dev);
			    break;
		        }
		    
			if(dup2(fd, STDIN_FILENO)<0) errnomsg("dup2 to stdin");
		        if(dup2(fd, STDERR_FILENO)<0) errnomsg("dup2 to stderr");
			if(dup2(fd, STDOUT_FILENO)<0) errnomsg("dup2 to stdout");
		        close(fd);

			found_tty = fixed;
		    }
		    break;
		case vt:
		    {
			/* Let's search for a console to control
			   the virtual terminals. */
		        char **tdev;
			int cfd, fd;
		        struct vt_stat vtstat;
			
			cfd = -1;
		        for(tdev = devs; *tdev != NULL; tdev++)
			{
			    int *mode;
			    for(mode = modes; *mode != 0; mode++)
			    {
				cfd = open(*tdev, *mode);
			        if(cfd >= 0 || errno != EACCES)
				    break;
			    }
			
			    if(ioctl(cfd, VT_GETSTATE, &vtstat) < 0)
				close(cfd);
			    else
				break;
		        }
			
			if(*tdev == NULL)
		    	    break; /* Nothing found. I'm sorry. */
			    
			if(ioctl(cfd, VT_OPENQRY, &vtnr) < 0 || vtnr < 0)
		        {
			    close(cfd);
			    break;
		        }
		    
			if(vtnr < tconf->min)
		    	    vtnr = tconf->min;
			
			fd = -1;
			do
			{
			    char vtname[32];
			
			    if(vtnr<16 && (vtstat.v_state & (1<<vtnr)))
			    {
				/* Already in use. */
				vtnr++;
				continue;
			    }
			
			    if(snprintf(vtname, 32, "/dev/tty%d", vtnr) > 31)
			    {
				close(cfd);
			        break;
			    }
			
			    fd = open(vtname, O_RDWR);
			    if(fd >= 0)
				break;
			    
			    vtnr++;
		        }while(vtnr < 16);
		    
			if(fd < 0)
		        {
			    close(cfd);
			    errmsg("Couldn't find a virtual terminal.\n");
			    break;
		        }
		    
			ioctl(cfd, VT_ACTIVATE, vtnr);
		        ioctl(cfd, VT_WAITACTIVE, vtnr);
			close(cfd);
		        origvtnr = vtstat.v_active;
		    
			setenv("TERM", "linux", 1);
		    
		        if(dup2(fd, STDIN_FILENO)<0)
			    errnomsg("dup2 to stdin");
			if(dup2(fd, STDERR_FILENO)<0)
			    errnomsg("dup2 to stderr");
		        if(dup2(fd, STDOUT_FILENO)<0)
			    errnomsg("dup2 to stdout");
			close(fd);
		
			found_tty = vt;
		    }
		    break;
		case xterm:
		    {
			int msfd, slfd;
		        char *slname;
			struct termios modes;
		    
			msfd = getpt();
		        if(msfd < 0)
			{
			    errnomsg("getpt");	
			    break;
		        }
		    
			if(grantpt(msfd)<0)
		        {
			    errnomsg("grantpt");
			    close(msfd);
			    break;
		        }
		    
			if(unlockpt(msfd)<0)
		        {
			    errnomsg("unlockpt");
			    close(msfd);
			    break;
		        }
		    
			slname = ptsname(msfd);
		        if(slname == NULL)
			{
			    errnomsg("ptsname");
			    close(msfd);
			    break;
		        }
		    
			slfd = open(slname, O_RDWR);
		        if(slfd < 0)
			{
			    errnomsg(slname);
			    close(msfd);
			    break;
		        }

		
			if(!tcgetattr(msfd, &modes))
		        {
		    
			    modes.c_iflag = (modes.c_iflag
					  & ~( INLCR|IGNCR))
					  | ICRNL;
			    modes.c_oflag = (modes.c_oflag
					  & ~( OCRNL|ONLRET|NLDLY|CRDLY|TABDLY|BSDLY|VTDLY|FFDLY ))
					  | OPOST|ONLCR;
				      
			    modes.c_cflag = (modes.c_cflag & ~(CLOCAL));
			    modes.c_lflag = modes.c_lflag
				          | ISIG|ICANON|ECHO|ECHOE|ECHOK|ECHOKE|ECHOCTL;
			
			    modes.c_cc[VERASE] = 8; /* Backspace ^H*/
				      
			    if(tcsetattr(msfd, TCSANOW, &modes)) errnomsg("tcsetattr");
			}
		        else
			    errnomsg("tcgetattr");
		    
			xterm_pid = fork();
		    
			if(xterm_pid<0)
			{
			    errnomsg("fork");
			    close(slfd);
			    close(msfd);
			    break;
		        }
		    
			if(!xterm_pid)
		        {
			    /* child */
			
			    char cmdbuf[1024];
			    close(slfd);
			
			    if(snprintf(cmdbuf,1024,"%s -SXX%d",
				(tconf->cmd==NULL)?"xterm":(tconf->cmd),
			        msfd)>=1024)
			    {
				close(msfd);
			        errmsg("Xterm command too long.\n");
				exit(1);
			    }
			
			    execl("/bin/sh", "/bin/sh", "-c", cmdbuf, NULL);
			    errnomsg("execl");
			
			    exit(1);
			}
		        else
			{
			    /* parent */
			    char c;
			
			    close(msfd);

			    /* Get the first line (the window id). */			
			    while(read(slfd, &c, 1)>0 && c!='\n');
			    if(c!='\n') /* Something is wrong. */
			    {
				errmsg("xterm failed.\n");
				close(slfd);
				break;
			    }
			
			    setenv("TERM", "xterm", 1);
			    
			    if(dup2(slfd, STDIN_FILENO)<0) errnomsg("dup2 to stdin");
			    if(dup2(slfd, STDERR_FILENO)<0) errnomsg("dup2 to stderr");
			    if(dup2(slfd, STDOUT_FILENO)<0) errnomsg("dup2 to stdout");
			    close(slfd);
		
		    	    found_tty = xterm;
			}
		    }
		    break;
	    }
	
	if((int)found_tty >= 0)
	{
	    if(handle_commands(tconf->postexec, true, config))
		return;
	}
	else
	    handle_commands(tconf->postexec, false, config);

	if(tconf->type == xterm)
	{
	    if(tconf->display!=NULL)
	    {
		if(orig_disp)
		    setenv("DISPLAY", orig_disp, 1);
		else
		    unsetenv("DISPLAY");
	    }
	    if(tconf->authority!=NULL)
	    {
		if(orig_auth)
		    setenv("XAUTHORITY", orig_auth, 1);
		else
		    unsetenv("XAUTHORITY");
	    }
	}
	
	if(orig_term)
	    setenv("TERM", orig_term, 1);
	else
	    unsetenv("TERM");
    }
    
    /* Nothing found. I'm sorry. */
    errmsg("No TTY found.\n");
    found_tty = (enum ttytype) -2;

    return;    
}

void release_tty()
{
    switch(found_tty)
    {
	case vt:
	{
	    /* Switch back to a virtual terminal, if we switched. */
	    struct vt_stat vtstat;
	    if(vtnr < 0)
		break;
	    	
	    if(ioctl(STDERR_FILENO, VT_GETSTATE, &vtstat) < 0)
		break;
		
	    if(vtstat.v_active != vtnr)
		break;

	    ioctl(STDERR_FILENO, VT_ACTIVATE, origvtnr);
	    ioctl(STDERR_FILENO, VT_WAITACTIVE, origvtnr);
	    break;
	}
	
	case xterm:
	{
	    /* int status; */
	    
	    kill(xterm_pid, SIGTERM);
	    
	    /*
	    if(waitpid(xterm_pid, &status, 0)<0)
	    {
	        errnomsg("waitpid");
	        break;
	    }
	    */
	    
	    break;
	}
	
	default:
	    break;
    }
}
