/* vyatta-restricted.c -- Vyatta restricted mode functionality */

/* This file is part of GNU Bash, the Bourne Again SHell.

   Bash 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, or (at your option) any later
   version.

   Bash 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 Bash; see the file COPYING.  If not, write to the Free Software
   Foundation, 59 Temple Place, Suite 330, Boston, MA 02111 USA.

   This code was originally developed by Vyatta, Inc.
   Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc. */

#include <unistd.h>

#include "shell.h"
#include "bashhist.h"
#include "vyatta-restricted.h"

#define FILENAME_MODE "restricted-mode"
#define FILENAME_OP "allowed-op"
#define FILENAME_CFG "allowed-cfg"
#define FILENAME_PIPE "allowed-pipe"

static char *prev_cmdline = NULL;

/* allowed commands lists */
static char **allowed_op_cmds = NULL;
static char **allowed_cfg_cmds = NULL;
static char **allowed_pipe_cmds = NULL;
static int *pipe_cmd_args = NULL;

static char *vyatta_user_level_dir = NULL;

/* default restricted mode */
static int vyatta_default_output_restricted = 0;
static int vyatta_default_full_restricted = 0;

static char *expand_disable_cmds[] = { "_vyatta_op_run",
                                       "/opt/vyatta/sbin/my_set",
                                       "/opt/vyatta/sbin/my_delete",
                                       "/opt/vyatta/sbin/my_commit",
                                       NULL };

static int
is_expansion_disabled()
{
  char *exp = getenv("VYATTA_ENABLE_SHELL_EXPANSION");
  if (!exp) {
    return 1;
  }
  return 0;
}

void
vyatta_reset_hist_expansion()
{
#if defined (BANG_HISTORY)
  if (is_expansion_disabled()) {
    history_expansion_inhibited = 1;
  } else {
    history_expansion_inhibited = 0;
  }
#endif
}

static int
is_in_command_list(const char *cmd, char *cmds[])
{
  int idx = 0;
  for (idx = 0; cmds[idx]; idx++) {
    if (strcmp(cmd, cmds[idx]) == 0) {
      return 1;
    }
  }
  return 0;
}

static int
is_vyatta_restricted_pipe_command(WORD_LIST *words)
{
  WORD_LIST *w = words;
  int count = 1;
  while (w = w->next) {
    count++;
  }
  if (words) {
    int i = 0;
    if (!allowed_pipe_cmds) {
      /* no restriction */
      return 1;
    }
    for (i = 0; allowed_pipe_cmds[i]; i++) {
      if (strcmp(words->word->word, allowed_pipe_cmds[i]) == 0
          && count == pipe_cmd_args[i]) {
        return 1;
      }
    }
  }
  /* not allowed */
  return 0;
}

static void
make_restricted_word(WORD_DESC *word)
{
  char *c, *ns, *n;
  int sq_count = 0;
  char *uqs = string_quote_removal(word->word, 0);

  for (c = uqs; *c; c++) {
    if (*c == '\'') {
      sq_count++;
    }
  }

  /* strlen + start/end quotes + \0 + extra "'\''" */
  ns = (char *) xmalloc(strlen(uqs) + 2 + 1 + (3 * sq_count));
  n = ns;
  *n = '\'';
  n++;
  for (c = uqs; *c; c++) {
    if (*c == '\'') {
      *n = '\'';
      *(n + 1) = '\\';
      *(n + 2) = '\'';
      *(n + 3) = '\'';
      n += 4;
    } else {
      *n = *c;
      n++;
    }
  }
  *n = '\'';
  *(n + 1) = '\0';

  free(word->word);
  free(uqs);
  word->word = ns;
  word->flags = W_QUOTED;
}

static void
make_restricted_wordlist(WORD_LIST *words)
{
  WORD_LIST *l = words->next; /* skip the first word */
  for (; l; l = l->next) {
    make_restricted_word(l->word);
  }
}

/* this basically disables shell expansions for "simple" commands. */
/* full: do a "full" check (disallow env override && also check pipe). */
void
vyatta_check_expansion(COMMAND *cmd, int full)
{
  struct simple_com *cS;
  struct connection *cC;

  if (!cmd) {
    return;
  }
  if (!full && !is_expansion_disabled()) {
    /* enabled */
    return;
  }
 
  switch (cmd->type) {
  case cm_simple:
    cS = cmd->value.Simple;
    if (cS && !(cS->redirects)) {
      /* simple command, no redirects */
      if (cS->words && cS->words->word && 
	  is_in_command_list(cS->words->word->word, expand_disable_cmds)) {
        /* user command => quote all words */
        make_restricted_wordlist(cS->words);
      }
    }
    break;
  case cm_connection:
    cC = cmd->value.Connection;
    if ((cC->connector == '|') && (cC->first->type == cm_simple)) {
      struct simple_com *cS1 = cC->first->value.Simple;
      if (!(cS1->redirects)) {
        /* simple, no redirects */
        if (is_in_command_list(cS1->words->word->word, expand_disable_cmds)) {
          /* user command => quote all words */
          make_restricted_wordlist(cS1->words);
        }
      }
      if (full && (cC->second->type == cm_simple)) {
        struct simple_com *cS2 = cC->second->value.Simple;
        if (!(cS2->redirects)) {
          /* simple, no redirects */
          /* quote all words (not checking user command after pipe) */
          make_restricted_wordlist(cS2->words);
        }
      }
    }
    break;
  default:
    break;
  }
}

static int
is_vyatta_restricted_command(COMMAND *cmd)
{
  struct simple_com *cS;
  struct connection *cC;

  if (!cmd) {
    return 1;
  }

  switch (cmd->type) {
  case cm_simple:
    cS = cmd->value.Simple;
    if (!(cS->redirects)) {
      /* simple command, no redirects */
      return 1;
    }
    break;
  case cm_connection:
    cC = cmd->value.Connection;
    if (cC->connector == '|') {
      if ((cC->first->type == cm_simple) && (cC->second->type == cm_simple)) {
        struct simple_com *cS1 = cC->first->value.Simple;
        struct simple_com *cS2 = cC->second->value.Simple;
        if (!(cS1->redirects) && !(cS2->redirects)) {
          /* both are simple and no redirects */
          if (is_vyatta_restricted_pipe_command(cS2->words)) {
            /* pipe command is allowed => allowed */
            return 1;
          }
        }
      }
    }
    break;
  default:
    break;
  }
  /* not allowed */
  return 0;
}

static int
is_vyatta_cfg_command(const char *cmd)
{
  if (!allowed_cfg_cmds) {
    /* no restriction */
    return 1;
  }
  return is_in_command_list(cmd, allowed_cfg_cmds);
}

static int
is_vyatta_op_command(const char *cmd)
{
  if (!allowed_op_cmds) {
    /* no restriction */
    return 1;
  }
  return is_in_command_list(cmd, allowed_op_cmds);
}

int
is_vyatta_command(char *cmdline, COMMAND *cmd)
{
  char *cfg = getenv("_OFR_CONFIGURE");
  int in_cfg = (cfg) ? (strcmp(cfg, "ok") == 0) : 0;
  char *start = cmdline;
  char *end = NULL;
  char save = 0;
  int ret = 0;

  /* check expansions (full) */
  vyatta_check_expansion(cmd, 1);

  if (!prev_cmdline) {
    prev_cmdline = strdup("");
  }
  if (strcmp(cmdline, prev_cmdline) == 0) {
    /* still at the same line. not checking. */
    return 1;
  }
  if (!is_vyatta_restricted_command(cmd)) {
    return 0;
  }

  while (*start && (whitespace(*start) || *start == '\n')) {
    start++;
  }
  if (*start == 0) {
    /* empty command line is valid */
    free(prev_cmdline);
    prev_cmdline = strdup(cmdline);
    return 1;
  }
  end = start;
  while (*end && (!whitespace(*end) && *end != '\n')) {
    end++;
  }
  save = *end;
  *end = 0;

  if (in_cfg) {
    ret = is_vyatta_cfg_command(start);
  } else {
    ret = is_vyatta_op_command(start);
  }
  *end = save;

  if (ret) {
    /* valid command */
    free(prev_cmdline);
    prev_cmdline = strdup(cmdline);
  }
  return ret;
}

static FILE *
fopen_level_file(char *file_name)
{
  FILE *f = NULL;
#define BUF_SIZE 1024
  char *buf = (char *) xmalloc(BUF_SIZE);
  if (!buf) {
    return NULL;
  }

  do {
    int r = snprintf(buf, BUF_SIZE, "%s/%s", vyatta_user_level_dir, file_name);
    if (r >= BUF_SIZE) {
      break;
    }

    f = fopen(buf, "r");
  } while (0);

  free(buf);
  return f;
}

static char *
fgets_level_file(char *buf, int size, FILE *lfile)
{
  if (fgets(buf, size, lfile)) {
    int end = 0;
    while (buf[end] && isprint(buf[end]) && !isspace(buf[end])) {
      end++;
    }
    buf[end] = 0;
    return buf;
  } else {
    return NULL;
  }
}

static void
set_default_mode()
{
  FILE *lfile = NULL;
  char *line = NULL;
  char buf[256];

  /* default to full restricted */
  vyatta_default_output_restricted = 0;
  vyatta_default_full_restricted = 1;

  if (!(lfile = fopen_level_file(FILENAME_MODE))) {
    return;
  }
  while (line = fgets_level_file(buf, 256, lfile)) {
    if (strcmp(line, "output") == 0) {
      vyatta_default_output_restricted = 1;
      vyatta_default_full_restricted = 0;
      break;
    }
    if (strcmp(line, "full") == 0) {
      vyatta_default_output_restricted = 0;
      vyatta_default_full_restricted = 1;
      break;
    }
  }
  fclose(lfile);
  return;
}

static void
set_allowed_op_cmds()
{
  FILE *lfile = NULL;
  char *line = NULL;
  char buf[256];
  int count = 1;

  if (allowed_op_cmds) {
    return;
  }

  if (!(lfile = fopen_level_file(FILENAME_OP))) {
    return;
  }
  while (line = fgets_level_file(buf, 256, lfile)) {
    count++;
  }
  fclose(lfile);

  /* count is 1 more than number of lines */
  allowed_op_cmds = (char **) xmalloc(sizeof(char *) * count);
  memset(allowed_op_cmds, 0, sizeof(char *) * count);

  if (!(lfile = fopen_level_file(FILENAME_OP))) {
    return;
  }
  count = 0;
  while (line = fgets_level_file(buf, 256, lfile)) {
    allowed_op_cmds[count] = strdup(line);
    count++;
  }
  fclose(lfile);
  return;
}

static void
set_allowed_cfg_cmds()
{
  FILE *lfile = NULL;
  char *line = NULL;
  char buf[256];
  int count = 1;

  if (allowed_cfg_cmds) {
    return;
  }

  if (!(lfile = fopen_level_file(FILENAME_CFG))) {
    return;
  }
  while (line = fgets_level_file(buf, 256, lfile)) {
    count++;
  }
  fclose(lfile);

  /* count is 1 more than number of lines */
  allowed_cfg_cmds = (char **) xmalloc(sizeof(char *) * count);
  memset(allowed_cfg_cmds, 0, sizeof(char *) * count);

  if (!(lfile = fopen_level_file(FILENAME_CFG))) {
    return;
  }
  count = 0;
  while (line = fgets_level_file(buf, 256, lfile)) {
    allowed_cfg_cmds[count] = strdup(line);
    count++;
  }
  fclose(lfile);
  return;
}

static void
set_allowed_pipe_cmds()
{
  FILE *lfile = NULL;
  char *line = NULL;
  char buf[256];
  int count = 0;

  if (allowed_pipe_cmds) {
    return;
  }

  if (!(lfile = fopen_level_file(FILENAME_PIPE))) {
    return;
  }
  while (line = fgets_level_file(buf, 256, lfile)) {
    count++;
  }
  fclose(lfile);

  count = (count / 2) + 2;
  /* count is 1 more than entries */
  allowed_pipe_cmds = (char **) xmalloc(sizeof(char *) * count);
  memset(allowed_pipe_cmds, 0, sizeof(char *) * count);
  pipe_cmd_args = (int *) xmalloc(sizeof(int *) * count);
  memset(pipe_cmd_args, 0, sizeof(int *) * count);

  if (!(lfile = fopen_level_file(FILENAME_PIPE))) {
    return;
  }
  count = 0;
  while (line = fgets_level_file(buf, 256, lfile)) {
    allowed_pipe_cmds[count] = strdup(line);
    if (!(line = fgets_level_file(buf, 256, lfile))) {
      free(allowed_pipe_cmds[count]);
      allowed_pipe_cmds[count] = NULL;
      break;
    }
    pipe_cmd_args[count] = atoi(line);
    /* limit to between 1 and 256 */
    if (pipe_cmd_args[count] < 1 || pipe_cmd_args[count] > 256) {
      free(allowed_pipe_cmds[count]);
      allowed_pipe_cmds[count] = NULL;
      pipe_cmd_args[count] = 0;
      break;
    }
    count++;
  }
  fclose(lfile);
  return;
}

static int
init_vyatta_restricted_mode()
{
  if (!(vyatta_user_level_dir = getenv("VYATTA_USER_LEVEL_DIR"))) {
    /* level dir not set, return failure */
    return 0;
  }

  /* set the default restricted mode based on level */
  set_default_mode();

  /* set the allowed commands */
  set_allowed_op_cmds();
  set_allowed_cfg_cmds();
  set_allowed_pipe_cmds();

  return 1;
}

int
in_vyatta_restricted_mode(enum vyatta_restricted_type type)
{
  char *rval = getenv("VYATTA_RESTRICTED_MODE");
  int output = 0;
  int full = 0;

  if (!vyatta_user_level_dir && !init_vyatta_restricted_mode()) {
    /* init failed, return false (not restricted) */
    return 0;
  }

  output = vyatta_default_output_restricted;
  full = vyatta_default_full_restricted;
  
  /* environment var overrides default */
  if (rval) {
    output = (strcmp(rval, "output") == 0);
    full = (strcmp(rval, "full") == 0);
  }

  if (type == OUTPUT && (output || full)) {
    return 1;
  }
  if (type == FULL && full) {
    return 1;
  }

  return 0;
}