/*
 * Copyright (c) 1996-2007, OpenFWTK Development Group
 * All rights reserved. See LICENSE.
 */

/* cmd-gw.c */

/* Copyright 1997, 1998 by Eberhard Mattes <em-gw@windhager.de>
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/file.h>
#include <sys/wait.h>
#include "firewall.h"
#include "auth.h"
#include "libemfw.h"
#include "libemtn.h"
#include "firewall2.h"

/* firewall.h lacks some declarations. */

int str_to_port (char *);
int do_daemon (int);
int peername (int, char *, char *, int);
int enargv (char *, char **, int, char *, int);

static int authenticated = 0;
static int authneeded = 0;
static int extendperm = 0;

#if defined(linux) || defined (SOLARIS)
extern void initsetproctitle(int,char**);
#endif

/* Prototypes. */

static int cmd_auth ();
static int cmd_help ();
static int cmd_once ();
static int cmd_quit ();

/* Important: The compiler should put this array into read-only memory
   (.text).  Therefore we make it `const'. */

static Cmd const cmds[] =
{
  {"auth",	 0, "   auth <user>",			cmd_auth},
  {"dig",	 1, "   dig <host>",			0},
  {"exit",	 0, "   exit",				cmd_quit},
  {"finger",	 2, "   finger [<user>@]<host>",	0},
  {"help",	 0, "   help/?",			cmd_help},
  {"login",	 0, "   login <user>",			cmd_auth},
  {"once",	 0, "   once <command> [<arg>...]",	cmd_once},
  {"ping",	 3, "   ping <host>",			0},
  {"quit",	 0, "   quit",				cmd_quit},
  {"traceroute", 4, "   traceroute <host>",		0},
  {"?",		 0, 0,					cmd_help},
  {0,		 0, 0,					0}
};

static char** cmdlist = (char**) 0;
static char** acmdlist = (char**) 0;

static fwparm   options[] = 
{   
  {FWPARM_BOOL,    "-extnd",       (char*) &extendperm },
  {FWPARM_BOOL,    "-authall",     (char*) &authneeded },
  {FWPARM_STRING,  "-authuser",    proxy_stats.authuser},
  {FWPARM_LIST ,   "-commands",    (char*) &cmdlist    },
  {FWPARM_LIST ,   "-auth",        (char*) &acmdlist   },
  {0,              0,              0                   } 
};


/* Important: The compiler should put this array into read-only memory
   (.text).  Therefore we make it `const'. */

static const char * const pgms[] =
{
#ifdef OpenBSD
  "/usr/sbin/dig",		/* 1 */
#else
  "/usr/bin/dig",		/* 1 */
#endif
  "/usr/bin/finger",		/* 2 */
#ifdef SOLARIS
  "/usr/sbin/ping",		/* 3 */
#else
  "/sbin/ping",			/* 3 */
#endif
  "/usr/sbin/traceroute"	/* 4 */
};

#define PGMS	(sizeof (pgms) / sizeof (pgms[0]))

static int child_pid;
static char pgmena[PGMS];
static char apgmena[PGMS];
static char *tokv[MAX_ARG];
static char input[MAX_STR];
static char tokbuf[MAX_STR];
static tnconn *t;

static void cleanup (void);
static void exec_command (char **v, int n);
static void run_prog (int argc, char *argv[], const char *pgm);

static void config ()
{
  Cfg *cf;
  int j;

  memset (pgmena, 0, PGMS * sizeof(pgmena[0]));
  memset (pgmena, 0, PGMS * sizeof(pgmena[0]));

  if ((cf=proxy_conf_hosts(proxy_confp,proxy_stats.rladdr,             
                                       proxy_stats.riaddr))) 
    proxy_parse_options(cf,options);
  else 
    proxy_exit();

  if(cmdlist != (char **)0) {
    char    **xp;

    for(xp = cmdlist; *xp != (char *)0; xp++) {
      if(!strcmp(*xp,"*")) {
	memset (pgmena, 1, PGMS * sizeof(pgmena[0]));
        continue;
      }
      for(j=0; cmds[j].cnam != NULL; j++) {
        if (cmds[j].flg > PGMS)
          abort ();
        if(!strcmp(*xp,cmds[j].cnam)) {
	  pgmena[cmds[j].flg - 1] = 1;
          break;
        }
      }
      if(cmds[j].cnam == NULL) {
        syslog (LLEV, "fwtkcfgerr: unknown command, line %d", cf->ln);
        exit (1);
      }
    }
  }

  memcpy(apgmena, pgmena, PGMS * sizeof(pgmena[0]));

  if(acmdlist != (char **)0) {
    char    **xp;

    for(xp = acmdlist; *xp != (char *)0; xp++) {
      if(!strcmp(*xp,"*")) {
	memset (apgmena, 1, PGMS * sizeof(pgmena[0]));
        continue;
      }
      for(j=0; cmds[j].cnam != NULL; j++) {
        if (cmds[j].flg > PGMS)
          abort ();
        if(!strcmp(*xp,cmds[j].cnam)) {
	  apgmena[cmds[j].flg - 1] = 1;
          break;
        }
      }
      if(cmds[j].cnam == NULL) {
        syslog (LLEV, "fwtkcfgerr: unknown command, line %d", cf->ln);
        exit (1);
      }
    }
  }
}

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

  if (argc >= 2 && strcmp (argv[1], "-check") == 0)
    {
      check_snprintf (0);
      exit (0);
    }

  proxy_init(argc,argv);
  proxy_chroot_setugid();
  config();
  atexit (cleanup);

  t = tn_init (0, TN_INIT_TELNET);
  if (authneeded && cmd_auth(0, NULL, NULL))
    proxy_exit();

  tn_puts (t, "Welcome to cmd-gw.\r\n");

  for (;;)
    {
      tn_puts (t, "Command: ");
      *proxy_stats.dst = '\0';
      proxy_update_operation("IDLE");
      tn_gets (t, input, sizeof (input), PROXY_TIMEOUT, TN_GETS_ECHO);
      syslog (LLEV, "command: %.512s", input);
      proxy_stats.outbytes += strlen(input);
      proxy_update_operation(input);
      tokc = enargv (input, tokv, sizeof (tokv) / sizeof (tokv[0]),
		     tokbuf, sizeof (tokbuf));
      if (tokc <= 0)
	continue;
      exec_command (tokv, tokc);
    }
}

static int cmd_help (int argc, char *argv[], char *raw)
{
  const Cmd *c;

  tn_puts (t, "Command list:\r\n");
  for (c = cmds; c->cnam != NULL; ++c)
    if (c->help != NULL)
      {
	if (c->flg < 0 || c->flg > PGMS)
	  abort ();
	if (c->flg == 0 || pgmena[c->flg-1])
	  tn_printf (t, "%.79s\r\n", c->help);
      }
  return 0;
}

static int cmd_once (int argc, char *argv[], char *raw)
{
  if (argc < 2)
    tn_puts (t, "Missing argument.\r\n");
  else
    {
      exec_command (argv + 1, argc - 1);
      proxy_exit ();
    }
  return 0;
}

static int cmd_quit (int argc, char *argv[], char *raw)
{
  tn_puts (t, "Good bye!\r\n");
  proxy_exit ();
  return 0;
}

static int cmd_auth (int argc, char *argv[], char *raw)
{
  char lbuf[MAX_STR];
  char rbuf[MAX_STR];
  char username[AUTH_USIZ+1];
  static int authopened = 0;

  authenticated = 0;

  if (argc == 0 || argc == 1) {
    tn_puts(t,"Username: ");
    tn_gets (t, username, sizeof (username), PROXY_TIMEOUT, TN_GETS_ECHO);
  } else if (argc == 2)
    strlcpy(username,argv[1],sizeof(username));
  else {
    tn_puts (t, "Syntax error\r\n");
    return 1;
  }

  if(strlen(username) > AUTH_USIZ - 1) {
    tn_puts(t,"User name too long\r\n");
    return 1;
  }
  if(!authopened) {
    if(auth_open(proxy_confp)) {
      tn_puts(t,"Cannot connect to authentication server\r\n");
      return 1;
    }
    if(auth_recv(rbuf,sizeof(rbuf)))
      goto lostconn;
    if(strncmp(rbuf,"Authsrv ready",13)) {
      tn_puts(t, rbuf);
      auth_close();
      return 1;
    }
    authopened++;
  }
  /* send username */
  if(strlen(proxy_stats.rladdr) + strlen(proxy_stats.riaddr) + strlen(username) + 100 > MAX_STR) {
    tn_puts(t,"Buffer overflow on authentication stage");
    return 1;
  }

  snprintf(rbuf, sizeof(rbuf), "authorize %s 'cmd-gw %s/%s'",username,proxy_stats.rladdr,proxy_stats.riaddr);

  if(auth_send(rbuf))
    goto lostconn;
  while (1) {
    if(auth_recv(rbuf,sizeof(rbuf)))
      goto lostconn;
    if(!strncmp(rbuf,"ok",2)) 
      break;
    if(!strncmp(rbuf,"display ",8)) {
      tn_puts(t,&rbuf[8]);
      if (auth_send("response dummy"))
        goto lostconn;
	continue;
    }
    if(!strncmp(rbuf,"challenge ",10)) {
      tn_puts(t,&rbuf[10]);
      tn_gets(t, rbuf, sizeof (rbuf), PROXY_TIMEOUT, TN_GETS_ECHO);
      snprintf(lbuf, sizeof (lbuf), "response '%s'", rbuf);
      if (auth_send(lbuf))
        goto lostconn;
      continue;
    }
    if(!strncmp(rbuf,"chalnecho ",10)) {
      tn_puts(t,&rbuf[10]);
      tn_gets(t, rbuf, sizeof (rbuf), PROXY_TIMEOUT, 0);
      snprintf(lbuf, sizeof (lbuf), "response '%s'", rbuf);
      if (auth_send(lbuf))
        goto lostconn;
      continue;
    }
    if(!strncmp(rbuf,"password",8)) {
      tn_puts(t,"Password:");
      tn_gets(t, rbuf, sizeof (rbuf), PROXY_TIMEOUT, 0);
      snprintf(lbuf, sizeof (lbuf), "response '%s'", rbuf);
      if (auth_send(lbuf))
        goto lostconn;
      continue;
    }
    tn_puts(t,rbuf);
    tn_puts(t,"\r\n");
    return 1;
  }

  tn_puts(t,"Authenticated (");
  tn_puts(t,rbuf);
  tn_puts(t,")\r\n");
  memcpy(pgmena, apgmena, PGMS * sizeof(pgmena[0]));
  return 0;

lostconn:
  auth_close();
  authopened = 0;
  tn_puts(t, "Cannot connect to authentication server");
  return 1;
}


static void exec_command (char **v, int n)
{
  Cmd *cp;
  int rc;

  cp = find_command ((Cmd *)cmds, v[0]);
  if (cp == (Cmd *)0)
    tn_puts (t, "Ambiguous command.\r\n");
  else if (cp->cnam == (char *)0)
    tn_puts (t, "Unrecognized command.\r\n");
  else if (cp->flg == 0)
    cp->cfun (n, v, input);
  else if (cp->flg < 1 || cp->flg > PGMS)
    abort ();
  else if (pgmena[cp->flg-1] == 0)
    tn_puts (t, "Unrecognized command.\r\n"); /* Same message as above! */
  else if((extendperm) && 
          (((rc = auth_perm(proxy_confp,proxy_stats.authuser,"cmd-gw",
                            proxy_stats.riaddr,v)) == 1) || (rc == -1))) {
    syslog(LLEV,"deny host=%.512s/%.20s operation %.100s - extended permissions",proxy_stats.rladdr,proxy_stats.riaddr,input);
    tn_puts (t, "Operation denied\r\n");
    return;
  } else
    run_prog (n, v, pgms[cp->flg-1]);
}

/* This function is called by exit(). */

static void cleanup (void)
{
  if (child_pid > 0)
    {
      kill (child_pid, SIGINT);
      child_pid = -1;
    }
}

static sig_atomic_t child_died;

static void sigchld (int signo)
{
  int status;

  /* As we have only one child process, handling SIGCHLD is simple. */

  wait (&status);
  child_died = 1; child_pid = -1;
}

static void run_prog (int argc, char *argv[], const char *pgm)
{
  int fd, pgm_output[2];

  /* Feed the program's output through a pipe to replace \n with
     \r\n. */

  if (pipe (pgm_output) != 0)
    {
      tn_printf (t, "pipe(): %s\r\n", strerror (errno));
      return;
    }

  if (pgm_output[0] >= FD_SETSIZE)
    {
      tn_printf (t, "file descriptor out of range for select()\r\n");
      close (pgm_output[0]); close (pgm_output[1]);
      return;
    }

  /* Ensure that pgm_output[1] is > 2.  This simplifies the code in
     the child process. */

  if (pgm_output[1] <= 2)
    {
      errno = 0;
      fd = fcntl (pgm_output[1], F_DUPFD, 3);
      if (fd < 3)
	{
	  tn_printf (t, "fcntl(F_DUPFD): %s\r\n", strerror (errno));
	  return;
	}
      close (pgm_output[1]);
      pgm_output[1] = fd;
    }

  /* Set up SIGCHLD. */

  child_died = 0;
  signal (SIGCHLD, sigchld);

  /* Create a child process. */

  child_pid = fork ();
  if (child_pid == -1)
    {
      tn_printf (t, "fork(): %s\r\n", strerror (errno));
      close (pgm_output[0]); close (pgm_output[1]);
    }
  else if (child_pid == 0)
    {
      /* Child process. */

      int e;
      char *s;

      signal (SIGCHLD, SIG_DFL);
      close (0); close (1); close (2);
      if (pgm_output[0] > 2)
	close (pgm_output[0]);
      errno = 0;
      if (open ("/dev/null", O_RDWR) != 0) /* This may fail in jail */
	syslog (LLEV, "fwtksyserr: /dev/null: %s", strerror(errno));
      errno = 0;
      if (dup2 (pgm_output[1], 1) != 1)
	{
	  syslog (LLEV, "fwtksyserr: dup2(): %s", strerror(errno));
	  exit (2);
	}
      errno = 0;

      if (dup2 (pgm_output[1], 2) != 2)
	{
	  syslog (LLEV, "fwtksyserr: dup2(): %s", strerror(errno));
	  exit (2);
	}
      close (pgm_output[1]);
      /* TODO: Close all other file descriptors (authdb!) */
      argv[0] = (char *)pgm;
      argv[argc] = 0;		/* Don't trust enargv() */
      execv (pgm, argv);
      e = errno;
      write (1, pgm, strlen (pgm)); write (1, ": ", 2);
      s = strerror (e);
      write (1, s, strlen (s)); write (1, "\n", 1);
      exit (2);
    }
  else
    {
      /* Parent process. */

      fd_set rfds;
      struct timeval timeout;
      int n;

      tn_reset_dfa (t);
      close (pgm_output[1]);
      for (;;)
	{
	  FD_ZERO (&rfds);
	  FD_SET (pgm_output[0], &rfds);
	  FD_SET (0, &rfds);
	  timeout.tv_sec = child_died ? 0 : PROXY_TIMEOUT;
	  timeout.tv_usec = 0;
	  n = select (FD_SETSIZE, &rfds, (fd_set *)0, (fd_set *)0, &timeout);
	  if (n == -1)
	    {
	      if (errno == EINTR)
		continue;
	      syslog (LLEV, "fwtksyserr: select(): %s", strerror(errno));
	      exit (1);
	    }
	  if (n == 0)
	    {
	      syslog (LLEV, "Timeout");
	      tn_puts (t, "Timeout\r\n");
	      proxy_exit ();
	    }
	  if (FD_ISSET (0, &rfds))
	    {
	      int c = tn_getc (t);
	      tndfa dfa = tn_dfa (t, c);
	      if (dfa == TN_IP || (dfa == TN_CHAR && c == 0x03))
		kill (child_pid, SIGINT);
	      proxy_stats.outbytes++; 
	    }
	  if (FD_ISSET (pgm_output[0], &rfds))
	    {
	      char c;		/* TODO: use bigger buffer, non-blocking */
	      n = read (pgm_output[0], &c, 1);
              if (n == -1 && errno == EINTR)
                continue;
              if (n <= 0)
                break;
	      proxy_stats.inbytes++;
	      if (c == '\n')
		tn_putnl (t);
	      else
		tn_putc (t, c);
	    }
	}
      close (pgm_output[0]);
      signal (SIGCHLD, SIG_DFL);
      child_pid = -1;
    }
}
