#if defined(linux) || defined(__linux__)

/* General purpose Linux console screen save/restore server
   Copyright (C) 1994 Janne Kukonlehto <jtklehto@stekt.oulu.fi>
   Original idea from Unix Interactive Tools version 3.2b (tty.c)
   This code requires root privileges.
   You may want to make the cons.saver setuid root.
   The code should be safe even if it is setuid but who knows?
   
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.
   
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

/* This code does _not_ need to be setuid root. However, it needs
   read/write access to /dev/vcsa* (which is priviledged
   operation). You should create user vcsa, make cons.saver setuid
   user vcsa, and make all vcsa's owned by user vcsa.

   Seeing other peoples consoles is bad thing, but believe me, full
   root is even worse. */

#include <config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#    include <unistd.h>
#endif
#include <termios.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>	/* For isdigit() */
#include <string.h>

#define LINUX_CONS_SAVER_C
#include "cons.saver.h"

#define cmd_input 0
#define cmd_output 1

/* Meaning of console_flag:
   -1 == to be detected,
   0  == not a console
   1  == is a console, Linux < 1.1.67 (black & white)
   2  == is a console, Linux >= 1.1.67 (color)
   3  == is a console, Linux >= 1.1.92 (color, use /dev/vcsa$num
   */
static signed char console_flag = -1;
/*
   Meaning of console_fd:
   -1  == not opened,
   >=0 == opened
   */
static int console_fd = -1;
static char *tty_name;
static int console_minor = 0;
static char *buffer = NULL;
static int buffer_size = 0;
static int columns, rows;
static int vcs_fd;

static void dwrite (int fd, char *buffer)
{
    write (fd, buffer, strlen (buffer));
}

static void tty_getsize (void)
{
    struct winsize winsz;

    winsz.ws_col = winsz.ws_row = 0;
    ioctl (console_fd, TIOCGWINSZ, &winsz);
    if (winsz.ws_col && winsz.ws_row){
	columns = winsz.ws_col;
	rows    = winsz.ws_row;
    } else {
	/* Never happens (I think) */
	dwrite (2, "TIOCGWINSZ failed\n");
	columns = 80;
	rows = 25;
	console_flag = 0;
    }
}

static inline void tty_cursormove(int y, int x)
{
    char buffer [128];

    /* Standard ANSI escape sequence for cursor positioning */
    snprintf (buffer, sizeof (buffer), "\33[%d;%dH", y + 1, x + 1);
    dwrite (console_fd, buffer);
}

static int check_file (char *filename, int check_console)
{
    int fd;
    struct stat stat_buf;

    /* Avoiding race conditions: use of fstat makes sure that
       both 'open' and 'stat' operate on the same file */

    fd = open (filename, O_RDWR);
    if (fd == -1)
	return -1;
    
    do {
	if (fstat (fd, &stat_buf) == -1)
	    break;

	/* Must be character device */
	if (!S_ISCHR (stat_buf.st_mode)){
	    break;
	}

#ifdef DEBUG
	fprintf (stderr,
		 "Device %s: major %d, minor %d\r\n",
		 filename,
		 ((int) stat_buf.st_rdev & 0xff00) >> 8,
		 ((int) stat_buf.st_rdev & 0xff));
#endif

	if (check_console){
	    /* Major number must be 4 */
	    if ((stat_buf.st_rdev & 0xff00) != 0x0400){
		break;
	    }

	    /* Minor number must be between 1 and 63 */
	    console_minor = (int) (stat_buf.st_rdev & 0x00ff);
	    if (console_minor < 1 || console_minor > 63){
		break;
	    }

	    /* Must be owned by the user */
	    if (stat_buf.st_uid != getuid ()){
		break;
	    }
	}
	/* Everything seems to be okay */
	return fd;
    } while (0);

    close (fd);
    return -1;
}

/* Detect console. Return 0 if successful, -1 otherwise.  */
/* Because the name of the tty is supplied by the user and this
   can be a setuid program a lot of checks has to done to avoid
   creating a security hole */
static int detect_console (void)
{
    char console_name [16];
    static char vcs_name [16];

    /* Must be console */
    console_fd = check_file (tty_name, 1);
    if (console_fd == -1)
	return -1;

    /*
     * Only allow /dev/ttyMINOR and /dev/vc/MINOR where MINOR is the minor
     * device number of the console, set in check_file()
     */
    switch (tty_name[5])
    {
	case 'v':
	    snprintf (console_name, sizeof (console_name), "/dev/vc/%d",
		      console_minor);
	    if (strncmp (console_name, tty_name, sizeof (console_name)) != 0)
		return -1;
	    break;
	case 't':
	    snprintf (console_name, sizeof (console_name), "/dev/tty%d",
		      console_minor);
	    if (strncmp (console_name, tty_name, sizeof (console_name)) != 0)
		return -1;
	    break;
	default:
	    return -1;
    }

    snprintf (vcs_name, sizeof (vcs_name), "/dev/vcsa%d", console_minor);
    vcs_fd = check_file (vcs_name, 0);

    /* Try devfs name */
    if (vcs_fd == -1) {
	snprintf (vcs_name, sizeof (vcs_name), "/dev/vcc/a%d", console_minor);
	vcs_fd = check_file (vcs_name, 0);
    }

#ifdef DEBUG
    fprintf (stderr, "vcs_fd = %d console_fd = %d\r\n", vcs_fd, console_fd);
#endif
    
    if (vcs_fd != -1){
	console_flag = 3;
    }

    return 0;
}

static void save_console (void)
{
    int i;

    if (!console_flag)
	return;
    buffer [1] = console_minor;
    if (console_flag >= 2){
	/* Linux >= 1.1.67 */
	/* Get screen contents and cursor position */
	buffer [0] = 8;
	if (console_flag == 2){
	    if ((i = ioctl (console_fd, TIOCLINUX, buffer)) == -1){
		/* Oops, this is not Linux 1.1.67 */
		console_flag = 1;
	    }
	} else {
	    lseek (vcs_fd, 0, 0);
	    read (vcs_fd, buffer, buffer_size);
	}
    }
    if (console_flag == 1){
	int index, x, y;

	/* Linux < 1.1.67 */
	/* Get screen contents */
	buffer [0] = 0;
	if (ioctl(console_fd, TIOCLINUX, buffer) == -1){
	    buffer[0] = buffer[1] = 0;

	    /* Linux bug: bad ioctl on console 8 */
	    if (ioctl(console_fd, TIOCLINUX, buffer) == -1){
		/* Oops, this is not a console after all */
		console_flag = 0;
		return;
	    }
	}
	/* Select the beginning of the bottommost empty line
	   to be the cursor position */
	index = 2 + rows * columns;
	for (y = rows - 1; y >= 0; y--)
	    for (x = columns - 1; x >= 0; x--)
		if (buffer[--index] != ' ')
		    goto non_space_found;
    non_space_found:
	buffer[0] = y + 1;
	buffer[1] = 0;
	/*tty_cursormove(y + 1, 0);*/
    }
}

static void restore_console (void)
{
    if (!console_flag)
	return;
    if (console_flag == 2){
	/* Linux >= 1.1.67 */
	/* Restore screen contents and cursor position */
	buffer [0] = 9;
	buffer [1] = console_minor;
	ioctl (console_fd, TIOCLINUX, buffer);
    }
    if (console_flag == 3){
	lseek (vcs_fd, 0, 0);
	write (vcs_fd, buffer, buffer_size);
    }
    if (console_flag == 1){
	/* Clear screen */
	write(console_fd, "\033[H\033[2J", 7);
	/* Output saved screen contents */
	write(console_fd, buffer + 2, rows * columns);
	/* Move the cursor to the previously selected position */
	tty_cursormove(buffer[0], buffer[1]);
    }
}

static void send_contents (void)
{
    unsigned char begin_line=0, end_line=0;
    int index, x, y;
    int lastline;
    unsigned char message;
    unsigned short bytes;
    int bytes_per_char;
    
    bytes_per_char = console_flag == 1 ? 1 : 2;
    
    /* Calculate the number of used lines */
    if (console_flag == 2 || console_flag == 1 || console_flag == 3){
	index = (2 + rows * columns) * bytes_per_char;
	for (y = rows - 1; y >= 0; y--)
	    for (x = columns - 1; x >= 0; x--){
		index -= bytes_per_char;
		if (buffer[index] != ' ')
		    goto non_space_found;
	    }
    non_space_found:
	lastline = y + 1;
    } else
	return;

    /* Inform the invoker that we can handle this command */
    message = CONSOLE_CONTENTS;
    write (cmd_output, &message, 1);

    /* Read the range of lines wanted */
    read (cmd_input, &begin_line, 1);
    read (cmd_input, &end_line, 1);
    if (begin_line > lastline)
	begin_line = lastline;
    if (end_line > lastline)
	end_line = lastline;

    /* Tell the invoker how many bytes it will be */
    bytes = (end_line - begin_line) * columns;
    write (cmd_output, &bytes, 2);

    /* Send the contents */
    for (index = (2 + begin_line * columns) * bytes_per_char;
	 index < (2 + end_line * columns) * bytes_per_char;
	 index += bytes_per_char)
	write (cmd_output, buffer + index, 1);

    /* All done */
}

int main (int argc, char **argv)
{
    unsigned char action = 0;
    int stderr_fd;
    
    /*
     * Make sure Stderr points to a valid place
     */
    close (2);
    stderr_fd = open ("/dev/tty", O_RDWR);
    if (stderr_fd == -1)	/* This may well happen if program is running non-root */
	stderr_fd = open ("/dev/null", O_RDWR);

    if (stderr_fd == -1)
	exit (1);
    
    if (stderr_fd != 2)
	while (dup2 (stderr_fd, 2) == -1 && errno == EINTR)
	    ;
    
    if (argc != 2){
	/* Wrong number of arguments */

	dwrite (2, "Usage: cons.saver <ttyname>\n");
	console_flag = 0;
	write (cmd_output, &console_flag, 1);
	return 3;
    }

    /* Lose the control terminal */
    setsid ();
    
    /* Check that the argument is a legal console */
    tty_name = argv [1];

    if (detect_console () == -1){
	/* Not a console -> no need for privileges */
	setuid (getuid ());
/*	dwrite (2, error); */
	console_flag = 0;
	if (console_fd >= 0)
	    close (console_fd);
    } else {
	/* Console was detected */
	if (console_flag != 3)
	    console_flag = 2; /* Default to Linux >= 1.1.67 */
	/* Allocate buffer for screen image */
	tty_getsize ();
	buffer_size = 4 + 2 * columns * rows;
	buffer = (char*) malloc (buffer_size);
    }

    /* If using /dev/vcs*, we don't need anymore the console fd */
    if (console_flag == 3)
	close (console_fd);
    
    /* Inform the invoker about the result of the tests */
    write (cmd_output, &console_flag, 1);

    /* Read commands from the invoker */
    while (console_flag && read (cmd_input, &action, 1)){
	/* Handle command */
	switch (action){
	case CONSOLE_DONE:
	    console_flag = 0;
	    continue; /* Break while loop instead of switch clause */
	case CONSOLE_SAVE:
	    save_console ();
	    break;
	case CONSOLE_RESTORE:
	    restore_console ();
	    break;
	case CONSOLE_CONTENTS:
	    send_contents ();
	    break;
	} /* switch (action) */
		
	/* Inform the invoker that command is handled */
	write (cmd_output, &console_flag, 1);
    } /* while (read ...) */

    if (buffer)
	free (buffer);
    return 0;   
}

#else

 #error "The Linux console screen saver works only on Linux"

#endif /* #ifdef linux */
