/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 2004-2024 The ProFTPD Project team
 *
 * 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., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
 *
 * As a special exemption, The ProFTPD Project team and other respective
 * copyright holders give permission to link this program with OpenSSL, and
 * distribute the resulting executable, without including the source code for
 * OpenSSL in the source distribution.
 */

/* Display of files */

#include "conf.h"

#define DISPLAY_BUFFER_SIZE	32

static int first_msg_sent = FALSE;
static const char *first_msg = NULL;
static const char *prev_msg = NULL;

static const char *trace_channel = "display";

/* Note: The size provided by pr_fs_getsize2() is in KB, not bytes. */
static void format_size_str(char *buf, size_t buflen, off_t size) {
  char *units[] = {"K", "M", "G", "T", "P", "E", "Z", "Y"};
  unsigned int nunits = 8;
  register unsigned int i = 0;
  int res;

  /* Determine the appropriate units label to use. Do not exceed the max
   * possible unit support (yottabytes), by ensuring that i maxes out at
   * index 7 (of 8 possible units).
   */
  while (size > 1024 &&
         i < (nunits - 1)) {
    pr_signals_handle();

    size /= 1024;
    i++;
  }

  /* Now, prepare the buffer. */
  res = pr_snprintf(buf, buflen, "%.3" PR_LU "%sB", (pr_off_t) size, units[i]);
  if (res > 2) {
    /* Check for leading zeroes; it's an aethetic choice. */
    if (buf[0] == '0' && buf[1] != '.') {
      memmove(&buf[0], &buf[1], res-1);
      buf[res-1] = '\0';
    }
  }
}

static int display_add_line(pool *p, const char *resp_code,
    const char *resp_msg) {

  /* Handle the case where the data to Display might contain only one line. */

  if (first_msg_sent == FALSE &&
      first_msg == NULL) {
      first_msg = pstrdup(p, resp_msg);
    return 0;
  }

  if (first_msg != NULL) {
    pr_response_send_raw("%s-%s", resp_code, first_msg);
    first_msg = NULL;
    first_msg_sent = TRUE;

    prev_msg = pstrdup(p, resp_msg);
    return 0;
  }

  if (prev_msg != NULL) {
    pr_response_send_raw(" %s", prev_msg);
  }

  prev_msg = pstrdup(p, resp_msg);
  return 0;
}

static int display_flush_lines(pool *p, const char *resp_code, int flags) {
  if (first_msg != NULL) {
    if (session.auth_mech != NULL) {
      if (flags & PR_DISPLAY_FL_NO_EOM) {
        pr_response_send_raw("%s-%s", resp_code, first_msg);

      } else {
        pr_response_send_raw("%s %s", resp_code, first_msg);
      }

    } else {
      /* There is a special case if the client has not yet authenticated; it
       * means we are handling a DisplayConnect file.  The server will send
       * a banner as well, so we need to treat this is the start of a multiline
       * response.
       */
      pr_response_send_raw("%s-%s", resp_code, first_msg);
    }

  } else {
    if (prev_msg) {
      if (flags & PR_DISPLAY_FL_NO_EOM) {
        pr_response_send_raw(" %s", prev_msg);

      } else {
        pr_response_send_raw("%s %s", resp_code, prev_msg);
      }
    }
  }

  /* Reset state for the next set of lines. */
  first_msg_sent = FALSE;
  first_msg = NULL;
  prev_msg = NULL;

  return 0;
}

static int display_fh(pr_fh_t *fh, const char *fs, const char *resp_code,
    int flags) {
  struct stat st;
  char buf[PR_TUNABLE_BUFFER_SIZE] = {'\0'};
  int len, res;
  const unsigned int *current_clients = NULL;
  const unsigned int *max_clients = NULL;
  off_t fs_size = 0;
  pool *p;
  const void *v;
  xaset_t *s;
  config_rec *c = NULL;
  const char *mg_time, *outs = NULL, *rfc1413_ident = NULL, *user;
  const char *serverfqdn = main_server->ServerFQDN;
  char mg_size[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_size_units[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_max[DISPLAY_BUFFER_SIZE] = "unlimited";
  char total_files_in[DISPLAY_BUFFER_SIZE] = {'\0'};
  char total_files_out[DISPLAY_BUFFER_SIZE] = {'\0'};
  char total_files_xfer[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_class_limit[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_cur[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_xfer_bytes[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_cur_class[DISPLAY_BUFFER_SIZE] = {'\0'};
  char mg_xfer_units[DISPLAY_BUFFER_SIZE] = {'\0'};

  /* Stat the opened file to determine the optimal buffer size for IO. */
  memset(&st, 0, sizeof(st));
  if (pr_fsio_fstat(fh, &st) == 0) {
    fh->fh_iosz = st.st_blksize;
  }

  /* Note: The size provided by pr_fs_getsize() is in KB, not bytes. */
  res = pr_fs_fgetsize(fh->fh_fd, &fs_size);
  if (res < 0 &&
      errno != ENOSYS) {
    (void) pr_log_debug(DEBUG7, "error getting filesystem size for '%s': %s",
      fh->fh_path, strerror(errno));
    fs_size = 0;
  }

  pr_snprintf(mg_size, sizeof(mg_size), "%" PR_LU, (pr_off_t) fs_size);
  format_size_str(mg_size_units, sizeof(mg_size_units), fs_size);

  p = make_sub_pool(session.pool);
  pr_pool_tag(p, "Display Pool");

  s = (session.anon_config ? session.anon_config->subset : main_server->conf);

  mg_time = pr_strtime3(p, time(NULL), FALSE);

  max_clients = get_param_ptr(s, "MaxClients", FALSE);

  v = pr_table_get(session.notes, "client-count", NULL);
  if (v != NULL) {
    current_clients = v;
  }

  pr_snprintf(mg_cur, sizeof(mg_cur), "%u",
    current_clients ? *current_clients : 1);

  if (session.conn_class != NULL &&
      session.conn_class->cls_name != NULL) {
    const unsigned int *class_clients = NULL;
    config_rec *maxc = NULL;
    unsigned int maxclients = 0;

    v = pr_table_get(session.notes, "class-client-count", NULL);
    if (v != NULL) {
      class_clients = v;
    }

    pr_snprintf(mg_cur_class, sizeof(mg_cur_class), "%u",
      class_clients ? *class_clients : 0);

    /* For the %z variable, first we scan through the MaxClientsPerClass,
     * and use the first applicable one.  If none are found, look for
     * any MaxClients set.
     */

    maxc = find_config(main_server->conf, CONF_PARAM, "MaxClientsPerClass",
      FALSE);

    while (maxc != NULL) {
      pr_signals_handle();

      if (strcmp(maxc->argv[0], session.conn_class->cls_name) != 0) {
        maxc = find_config_next(maxc, maxc->next, CONF_PARAM,
          "MaxClientsPerClass", FALSE);
        continue;
      }

      maxclients = *((unsigned int *) maxc->argv[1]);
      break;
    }

    if (maxclients == 0) {
      maxc = find_config(main_server->conf, CONF_PARAM, "MaxClients", FALSE);
      if (maxc != NULL) {
        maxclients = *((unsigned int *) maxc->argv[0]);
      }
    }

    pr_snprintf(mg_class_limit, sizeof(mg_class_limit), "%u", maxclients);

  } else {
    pr_snprintf(mg_class_limit, sizeof(mg_class_limit), "%u",
      max_clients ? *max_clients : 0);
    pr_snprintf(mg_cur_class, sizeof(mg_cur_class), "%u", 0);
  }

  pr_snprintf(mg_xfer_bytes, sizeof(mg_xfer_bytes), "%" PR_LU,
    (pr_off_t) session.total_bytes >> 10);
  pr_snprintf(mg_xfer_units, sizeof(mg_xfer_units), "%" PR_LU "B",
    (pr_off_t) session.total_bytes);

  if (session.total_bytes >= 10240) {
    pr_snprintf(mg_xfer_units, sizeof(mg_xfer_units), "%" PR_LU "kB",
      (pr_off_t) session.total_bytes >> 10);

  } else if ((session.total_bytes >> 10) >= 10240) {
    pr_snprintf(mg_xfer_units, sizeof(mg_xfer_units), "%" PR_LU "MB",
      (pr_off_t) session.total_bytes >> 20);

  } else if ((session.total_bytes >> 20) >= 10240) {
    pr_snprintf(mg_xfer_units, sizeof(mg_xfer_units), "%" PR_LU "GB",
      (pr_off_t) session.total_bytes >> 30);
  }

  pr_snprintf(mg_max, sizeof(mg_max), "%u", max_clients ? *max_clients : 0);

  user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
  if (user == NULL) {
    user = "";
  }

  c = find_config(main_server->conf, CONF_PARAM, "MasqueradeAddress", FALSE);
  if (c != NULL) {
    pr_netaddr_t *masq_addr = NULL;

    if (c->argv[0] != NULL) {
      masq_addr = c->argv[0];
    }

    if (masq_addr != NULL) {
      serverfqdn = pr_netaddr_get_dnsstr(masq_addr);
    }
  }

  /* "Stringify" the file number for this session. */
  pr_snprintf(total_files_in, sizeof(total_files_in), "%u",
    session.total_files_in);
  total_files_in[sizeof(total_files_in)-1] = '\0';

  pr_snprintf(total_files_out, sizeof(total_files_out), "%u",
    session.total_files_out);
  total_files_out[sizeof(total_files_out)-1] = '\0';

  pr_snprintf(total_files_xfer, sizeof(total_files_xfer), "%u",
    session.total_files_xfer);
  total_files_xfer[sizeof(total_files_xfer)-1] = '\0';

  rfc1413_ident = pr_table_get(session.notes, "mod_ident.rfc1413-ident", NULL);
  if (rfc1413_ident == NULL) {
    rfc1413_ident = "UNKNOWN";
  }

  while (pr_fsio_gets(buf, sizeof(buf), fh) != NULL) {
    char *tmp;

    pr_signals_handle();

    buf[sizeof(buf)-1] = '\0';
    len = strlen(buf);

    while (len &&
           (buf[len-1] == '\r' || buf[len-1] == '\n')) {
      buf[len-1] = '\0';
      len--;
    }

    /* Check for any Variable-type strings. */
    tmp = strstr(buf, "%{");
    while (tmp) {
      char *key, *tmp2;
      const char *val;

      pr_signals_handle();

      tmp2 = strchr(tmp, '}');
      if (!tmp2) {
        tmp = strstr(tmp + 1, "%{");
        continue;
      }

      key = pstrndup(p, tmp, tmp2 - tmp + 1);

      /* There are a couple of special-case keys to watch for:
       *
       *   env:$var
       *   time:$fmt
       *
       * The Var API does not easily support returning values for keys
       * where part of the value depends on part of the key.  That's why
       * these keys are handled here, instead of in pr_var_get().
       */

      if (strncmp(key, "%{time:", 7) == 0) {
        char time_str[128], *fmt;
        time_t now;
        struct tm *tm;

        fmt = pstrndup(p, key + 7, strlen(key) - 8);

        time(&now);
        memset(time_str, 0, sizeof(time_str));

        tm = pr_localtime(p, &now);
        if (tm != NULL) {
          strftime(time_str, sizeof(time_str), fmt, tm);
        }

        val = pstrdup(p, time_str);

      } else if (strncmp(key, "%{env:", 6) == 0) {
        char *env_var;

        env_var = pstrndup(p, key + 6, strlen(key) - 7);
        val = pr_env_get(p, env_var);
        if (val == NULL) {
          pr_trace_msg("var", 4,
            "no value set for environment variable '%s', using \"(none)\"",
            env_var);
          val = "(none)";
        }

      } else {
        val = pr_var_get(key);
        if (val == NULL) {
          pr_trace_msg("var", 4,
            "no value set for name '%s' [%s], using \"(none)\"", key,
            strerror(errno));
          val = "(none)";
        }
      }

      outs = sreplace(p, buf, key, val, NULL);
      sstrncpy(buf, outs, sizeof(buf));

      tmp = strstr(outs, "%{");
    }

    outs = sreplace(p, buf,
      "%C", (session.cwd[0] ? session.cwd : "(none)"),
      "%E", main_server->ServerAdmin,
      "%F", mg_size,
      "%f", mg_size_units,
      "%i", total_files_in,
      "%K", mg_xfer_bytes,
      "%k", mg_xfer_units,
      "%L", serverfqdn,
      "%M", mg_max,
      "%N", mg_cur,
      "%o", total_files_out,
      "%R", (session.c && session.c->remote_name ?
        session.c->remote_name : "(unknown)"),
      "%T", mg_time,
      "%t", total_files_xfer,
      "%U", user,
      "%u", rfc1413_ident,
      "%V", main_server->ServerName,
      "%x", session.conn_class ? session.conn_class->cls_name : "(unknown)",
      "%y", mg_cur_class,
      "%z", mg_class_limit,
      NULL);

    sstrncpy(buf, outs, sizeof(buf));

    if (flags & PR_DISPLAY_FL_SEND_NOW) {
      /* Normally we use pr_response_add(), and let the response code
       * automatically handle all of the multiline response formatting.
       * However, some of the Display files are at times waiting for the
       * response chains to be flushed, which won't work (i.e. DisplayConnect
       * and DisplayQuit).
       */
      display_add_line(p, resp_code, outs);

    } else {
      pr_response_add(resp_code, "%s", outs);
    }
  }

  if (flags & PR_DISPLAY_FL_SEND_NOW) {
    display_flush_lines(p, resp_code, flags);
  }

  destroy_pool(p);
  return 0;
}

int pr_display_fh(pr_fh_t *fh, const char *fs, const char *resp_code,
    int flags) {
  if (fh == NULL ||
      resp_code == NULL) {
    errno = EINVAL;
    return -1;
  }

  return display_fh(fh, fs, resp_code, flags);
}

int pr_display_file(const char *path, const char *fs, const char *resp_code,
    int flags) {
  pr_fh_t *fh = NULL;
  int res, xerrno;
  struct stat st;

  if (path == NULL ||
      resp_code == NULL) {
    errno = EINVAL;
    return -1;
  }

  fh = pr_fsio_open_canon(path, O_RDONLY);
  xerrno = errno;

  if (fh == NULL) {
    pr_trace_msg(trace_channel, 4, "unable to open file '%s': %s",
      path, strerror(xerrno));

    errno = xerrno;
    return -1;
  }

  res = pr_fsio_fstat(fh, &st);
  xerrno = errno;

  if (res < 0) {
    pr_trace_msg(trace_channel, 4, "unable to stat file '%s': %s",
      path, strerror(xerrno));

    pr_fsio_close(fh);

    errno = xerrno;
    return -1;
  }

  if (S_ISDIR(st.st_mode)) {
    pr_fsio_close(fh);
    xerrno = EISDIR;

    pr_trace_msg(trace_channel, 4,
      "display file can not be a directory '%s': %s", path, strerror(xerrno));

    errno = xerrno;
    return -1;
  }

  res = display_fh(fh, fs, resp_code, flags);
  xerrno = errno;

  pr_fsio_close(fh);

  errno = xerrno;
  return res;
}
