/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 2001, 2002, 2003 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 *
 * As a special exemption, The ProFTPD Project 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.
 */

/*
 * ProFTPD scoreboard support.
 *
 * $Id: scoreboard.c,v 1.31 2004/11/02 18:18:59 castaglia Exp $
 */

#include "conf.h"

#include <signal.h>

/* From src/dirtree.c */
extern char ServerType;

static pid_t scoreboard_opener = 0;
static int scoreboard_fd = -1;
static char scoreboard_file[PR_TUNABLE_PATH_MAX] = PR_RUN_DIR "/proftpd.scoreboard";

static off_t current_pos = 0;
static pr_scoreboard_header_t header;
static pr_scoreboard_entry_t entry;
static struct flock entry_lock;

static unsigned char scoreboard_read_locked = FALSE;
static unsigned char scoreboard_write_locked = FALSE;

/* Internal routines */

static char *handle_score_str(const char *fmt, va_list cmdap) {
  static char buf[PR_TUNABLE_SCOREBOARD_BUFFER_SIZE] = {'\0'};
  memset(buf, '\0', sizeof(buf));
  vsnprintf(buf, sizeof(buf), fmt, cmdap);
  buf[sizeof(buf)-1] = '\0';
  return buf;
}

static int read_scoreboard_header(pr_scoreboard_header_t *sch) {
  int res = 0;

  /* No interruptions, please. */
  pr_signals_block();
 
  /* NOTE: reading a struct from a file using read(2) -- bad (in general). */
  while ((res = read(scoreboard_fd, sch, sizeof(pr_scoreboard_header_t))) !=
      sizeof(pr_scoreboard_header_t)) {
    int rd_errno = errno;

    if (res == 0) {
      pr_signals_unblock();
      errno = EIO;
      return -1;
    }

    if (errno == EINTR) {
      pr_signals_handle();
      continue;
    }

    pr_signals_unblock();
    errno = rd_errno;
    return -1;
  }

  pr_signals_unblock();

  /* Note: these errors will most likely occur only for inetd-run daemons.
   * Standalone daemons erase the scoreboard on startup.
   */
 
  if (sch->sch_magic != PR_SCOREBOARD_MAGIC) {
    pr_close_scoreboard();
    return PR_SCORE_ERR_BAD_MAGIC;
  }

  if (sch->sch_version < PR_SCOREBOARD_VERSION) {
    pr_close_scoreboard();
    return PR_SCORE_ERR_OLDER_VERSION;
  }

  if (sch->sch_version > PR_SCOREBOARD_VERSION) {
    pr_close_scoreboard();
    return PR_SCORE_ERR_NEWER_VERSION;
  }

  return 0;
}

static int rlock_scoreboard(void) {
  struct flock lock;

  lock.l_type = F_RDLCK;
  lock.l_whence = SEEK_SET;
  lock.l_start = 0;
  lock.l_len = 0;

  while (fcntl(scoreboard_fd, F_SETLKW, &lock) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      return -1;
  }

  scoreboard_read_locked = TRUE;
  return 0;
}

static int unlock_entry(void) {

  entry_lock.l_type = F_UNLCK;
  entry_lock.l_whence = SEEK_CUR;
  entry_lock.l_len = sizeof(pr_scoreboard_entry_t);

  while (fcntl(scoreboard_fd, F_SETLKW, &entry_lock) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      return -1;
  }

  return 0;
}

static int unlock_scoreboard(void) {
  struct flock lock;

  lock.l_type = F_UNLCK;
  lock.l_whence = SEEK_SET;
  lock.l_start = 0;
  lock.l_len = 0;

  scoreboard_read_locked = scoreboard_write_locked = FALSE;
  return fcntl(scoreboard_fd, F_SETLK, &lock);
}

static int wlock_entry(void) {
  entry_lock.l_type = F_WRLCK;
  entry_lock.l_whence = SEEK_CUR;
  entry_lock.l_len = sizeof(pr_scoreboard_entry_t);

  while (fcntl(scoreboard_fd, F_SETLKW, &entry_lock) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      return -1;
  }

  return 0;
}

static int wlock_scoreboard(void) {
  struct flock lock;

  lock.l_type = F_WRLCK;
  lock.l_whence = 0;
  lock.l_start = 0;
  lock.l_len = 0;

  while (fcntl(scoreboard_fd, F_SETLKW, &lock) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      return -1;
  }

  scoreboard_write_locked = TRUE;
  return 0;
}


static int write_entry(void) {
  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return -1;
  }

  lseek(scoreboard_fd, entry_lock.l_start, SEEK_SET);

  while (write(scoreboard_fd, &entry, sizeof(entry)) != sizeof(entry)) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      return -1;
  }

  /* Rewind. */
  lseek(scoreboard_fd, entry_lock.l_start, SEEK_SET);

  return 0;
}

/* Public routines
 */

int pr_close_scoreboard(void) {
  if (scoreboard_fd == -1)
    return 0;

  if (scoreboard_read_locked || scoreboard_write_locked)
    unlock_scoreboard();

  while (close(scoreboard_fd) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      break;
  }

  scoreboard_fd = -1;
  scoreboard_opener = 0;

  return 0;
}

void pr_delete_scoreboard(void) {
  if (scoreboard_fd > -1) {
    while (close(scoreboard_fd) < 0) {
      if (errno == EINTR) {
        pr_signals_handle();
        continue;

      } else
        break;
    }
  }

  scoreboard_fd = -1;
  scoreboard_opener = 0;

  if (*scoreboard_file) {
    struct stat st;

    if (stat(scoreboard_file, &st) == 0)
      pr_log_debug(DEBUG3, "deleting existing scoreboard '%s'",
        scoreboard_file);

    unlink(scoreboard_file);
  }
}

const char *pr_get_scoreboard(void) {
  return scoreboard_file;
}

int pr_open_scoreboard(int flags) {
  int res;
  struct stat st;

  /* Try to prevent a file descriptor leak by only opening the scoreboard
   * file if the scoreboard file descriptor is not already positive, i.e.
   * if the scoreboard has not already been opened.
   */
  if (scoreboard_fd >= 0 && scoreboard_opener == getpid()) {
    pr_log_debug(DEBUG7, "scoreboard already opened");
    return 0;
  }

  pr_log_debug(DEBUG7, "opening scoreboard '%s'", scoreboard_file);

  /* Prevent writing to a symlink while avoiding a race condition: open
   * the file name O_RDWR|O_CREAT first, then check to see if it's a symlink.
   * If so, close the file and error out.  If not, truncate as necessary,
   * and continue.
   */
  
  while ((scoreboard_fd = open(scoreboard_file, flags|O_CREAT,
      PR_SCOREBOARD_MODE)) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      return -1;
  }

  /* Make certain that the scoreboard mode will be read-only for everyone
   * except the user owner (this allows for non-root-running daemons to
   * still modify the scoreboard).
   */
  while (fchmod(scoreboard_fd, 0644) < 0) {
    if (errno == EINTR) {
      pr_signals_handle();
      continue;

    } else
      break;
  }

  while (fstat(scoreboard_fd, &st) < 0) {
    int st_errno = errno;

    if (errno == EINTR) {
      pr_signals_handle();
      continue;
    }

    while (close(scoreboard_fd) < 0) {
      if (errno == EINTR) {
        pr_signals_handle();
        continue;

      } else
        break;
    }

    /* Either way, the scoreboard fd should be marked as closed. */
    scoreboard_fd = -1;

    errno = st_errno;
    return -1;
  }

  if (S_ISLNK(st.st_mode)) {
    while (close(scoreboard_fd) < 0) {
      if (errno == EINTR) {
        pr_signals_handle();
        continue;

      } else
        break;
    }

    errno = EPERM;
    scoreboard_fd = -1;
    return -1;
  }

  scoreboard_opener = getpid();

  /* Check the header of this scoreboard file. */
  if ((res = read_scoreboard_header(&header)) == -1) {

    /* If this file is newly created, it needs to have the header
     * written.
     */
    header.sch_magic = PR_SCOREBOARD_MAGIC;
    header.sch_version = PR_SCOREBOARD_VERSION;

    if (ServerType == SERVER_STANDALONE) {
      header.sch_pid = getpid();
      header.sch_uptime = time(NULL);

    } else {
      header.sch_pid = 0;
      header.sch_uptime = 0;
    }

    /* Write-lock the scoreboard file. */
    if (wlock_scoreboard() < 0)
      return -1;

    while (write(scoreboard_fd, &header, sizeof(header)) != sizeof(header)) {
      int wr_errno = errno;

      if (errno == EINTR) {
        pr_signals_handle();
        continue;
      }

      unlock_scoreboard();

      errno = wr_errno;
      return -1;
    }

    unlock_scoreboard();
    return 0;

  } else
    return res;

  return 0;
}

int pr_restore_scoreboard(void) {

  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return -1;
  }

  /* Position the file position pointer of the scoreboard back to
   * where it was, prior to the last pr_rewind_scoreboard() call.
   */
  lseek(scoreboard_fd, current_pos, SEEK_SET);
  return 0;
}

int pr_rewind_scoreboard(void) {

  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return -1;
  }

  current_pos = lseek(scoreboard_fd, 0, SEEK_CUR);

  /* Position the file position pointer of the scoreboard at the
   * start of the scoreboard (past the header).
   */
  lseek(scoreboard_fd, sizeof(pr_scoreboard_header_t), SEEK_SET);
  return 0;
}

int pr_set_scoreboard(const char *path) {
  char dir[PR_TUNABLE_PATH_MAX] = {'\0'};
  struct stat st;
  char *tmp = NULL;

  sstrncpy(dir, path, sizeof(dir));

  if ((tmp = strrchr(dir, '/')) == NULL) {
    errno = EINVAL;
    return -1;
  }
  *tmp = '\0';

  /* Parent directory must not be world-writeable */

  if (stat(dir, &st) < 0)
    return -1;

  if (!S_ISDIR(st.st_mode)) {
    errno = ENOTDIR;
    return -1;
  }

  if (st.st_mode & S_IWOTH) {
    errno = EPERM;
    return -1;
  }

  sstrncpy(scoreboard_file, path, sizeof(scoreboard_file));
  return 0;
}

int pr_scoreboard_add_entry(void) {
  unsigned char found_slot = FALSE;

  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return -1;
  }

  /* Write-lock the scoreboard file. */
  if (wlock_scoreboard() < 0)
    return -1;

  /* No interruptions, please. */
  pr_signals_block();

  /* If the scoreboard is open, the file position is already past the
   * header.
   */
  while (TRUE) {
    int res = 0;
    while ((res = read(scoreboard_fd, &entry, sizeof(entry))) ==
        sizeof(entry)) {

      /* If this entry's PID is marked as zero, it means this slot can be
       * reused.
       */
      if (!entry.sce_pid) {
        entry_lock.l_start = lseek(scoreboard_fd, 0, SEEK_CUR) - sizeof(entry);
        found_slot = TRUE;
        break;
      }
    }

    if (res == 0) {
      entry_lock.l_start = lseek(scoreboard_fd, 0, SEEK_CUR);
      found_slot = TRUE;
    }

    if (found_slot)
      break;
  }

  memset(&entry, '\0', sizeof(entry));

  entry.sce_pid = getpid();
  entry.sce_uid = geteuid();
  entry.sce_gid = getegid();

  if (write_entry() < 0)
    pr_log_pri(PR_LOG_NOTICE, "error writing scoreboard entry: %s",
      strerror(errno));

  pr_signals_unblock();

  /* We can unlock the scoreboard now. */
  unlock_scoreboard();

  return 0;
}

int pr_scoreboard_del_entry(unsigned char verbose) {

  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return -1;
  }

  memset(&entry, '\0', sizeof(entry));

  /* Write-lock this entry */
  wlock_entry();
  if (write_entry() < 0 && verbose)
    pr_log_pri(PR_LOG_NOTICE, "error deleting scoreboard entry: %s",
      strerror(errno));
  unlock_entry();

  return 0;
}

pid_t pr_scoreboard_get_daemon_pid(void) {
  return header.sch_pid;
}

time_t pr_scoreboard_get_daemon_uptime(void) {
  return header.sch_uptime;
}

pr_scoreboard_entry_t *pr_scoreboard_read_entry(void) {
  static pr_scoreboard_entry_t scan_entry;
  int res = 0;

  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return NULL;
  }

  /* Make sure the scoreboard file is read-locked. */
  if (!scoreboard_read_locked) {

    /* Do not proceed if we cannot lock the scoreboard. */
    if (rlock_scoreboard() < 0)
      return NULL; 
  }

  memset(&scan_entry, '\0', sizeof(scan_entry));

  /* NOTE: use readv(2)? */
  while (TRUE) {
    while ((res = read(scoreboard_fd, &scan_entry, sizeof(scan_entry))) <= 0) {
      if (res < 0 && errno == EINTR) {
        pr_signals_handle();
        continue;

      } else {
        unlock_scoreboard();
        return NULL;
      }
    }

    if (scan_entry.sce_pid) {
      unlock_scoreboard();
      return &scan_entry;

    } else
      continue;
  }

  unlock_scoreboard();
  return NULL;
}

/* We get clever with this function, so that it can be used to update
 * various entry attributes.
 */
int pr_scoreboard_update_entry(pid_t pid, ...) {
  va_list ap;
  char *tmp = NULL;
  int entry_tag = 0;

  if (scoreboard_fd < 0) {
    errno = EINVAL;
    return -1;
  }

  /* If updating some fields, clear the begin_idle field.
   */

  va_start(ap, pid);

  while ((entry_tag = va_arg(ap, int)) != 0) {
    switch (entry_tag) {
      case PR_SCORE_USER:
        tmp = va_arg(ap, char *);
        memset(entry.sce_user, '\0', sizeof(entry.sce_user));
        sstrncpy(entry.sce_user, tmp, sizeof(entry.sce_user));
        break;

      case PR_SCORE_CLIENT_ADDR: {
          pr_netaddr_t *remote_addr = va_arg(ap, pr_netaddr_t *);

          snprintf(entry.sce_client_addr, sizeof(entry.sce_client_addr),
            "%s", remote_addr ? pr_netaddr_get_ipstr(remote_addr) :
            "(unknown)");
          entry.sce_client_addr[sizeof(entry.sce_client_addr) - 1] = '\0';
        }
        break;

      case PR_SCORE_CLIENT_NAME: {
          char *remote_name = va_arg(ap, char *);
          
          snprintf(entry.sce_client_name, sizeof(entry.sce_client_name),
            "%s", remote_name ? remote_name : "(unknown)");
          entry.sce_client_name[sizeof(entry.sce_client_name) - 1] = '\0';

        }
        break;

      case PR_SCORE_CLASS:
        tmp = va_arg(ap, char *);
        memset(entry.sce_class, '\0', sizeof(entry.sce_class));
        sstrncpy(entry.sce_class, tmp, sizeof(entry.sce_class));
        break;

      case PR_SCORE_CWD:
        tmp = va_arg(ap, char *);
        memset(entry.sce_cwd, '\0', sizeof(entry.sce_cwd));
        sstrncpy(entry.sce_cwd, tmp, sizeof(entry.sce_cwd));
        break;

      case PR_SCORE_CMD: {
          char *cmdstr = NULL;
          tmp = va_arg(ap, char *);
          cmdstr = handle_score_str(tmp, ap);

          memset(entry.sce_cmd, '\0', sizeof(entry.sce_cmd));
          sstrncpy(entry.sce_cmd, cmdstr, sizeof(entry.sce_cmd));
          tmp = va_arg(ap, void *);
        }
        break;

      case PR_SCORE_CMD_ARG: {
          char *argstr = NULL;
          tmp = va_arg(ap, char *);
          argstr = handle_score_str(tmp, ap);

          memset(entry.sce_cmd_arg, '\0', sizeof(entry.sce_cmd_arg));
          sstrncpy(entry.sce_cmd_arg, argstr, sizeof(entry.sce_cmd_arg));
          tmp = va_arg(ap, void *);
        }
        break;

      case PR_SCORE_SERVER_PORT:
        entry.sce_server_port = va_arg(ap, int);
        break;

      case PR_SCORE_SERVER_ADDR: {
          pr_netaddr_t *server_addr = va_arg(ap, pr_netaddr_t *);
          int server_port = va_arg(ap, int);

          snprintf(entry.sce_server_addr, sizeof(entry.sce_server_addr),
            "%s:%d", server_addr ? pr_netaddr_get_ipstr(server_addr) :
            "(unknown)", server_port);
          entry.sce_server_addr[sizeof(entry.sce_server_addr)-1] = '\0';
        }
        break;

      case PR_SCORE_SERVER_LABEL:
        tmp = va_arg(ap, char *);
        memset(entry.sce_server_label, '\0', sizeof(entry.sce_server_label));
        sstrncpy(entry.sce_server_label, tmp, sizeof(entry.sce_server_label));
        break;

      case PR_SCORE_BEGIN_IDLE:
        /* Ignore this */
        (void) va_arg(ap, time_t);

        time(&entry.sce_begin_idle);
        break;

      case PR_SCORE_BEGIN_SESSION:
        /* Ignore this */
        (void) va_arg(ap, time_t);

        time(&entry.sce_begin_session);
        break;

      case PR_SCORE_XFER_DONE:
        entry.sce_xfer_done = va_arg(ap, off_t);
        break;

      case PR_SCORE_XFER_SIZE:
        entry.sce_xfer_size = va_arg(ap, off_t);
        break;

      case PR_SCORE_XFER_LEN:
        entry.sce_xfer_len = va_arg(ap, off_t);
        break;

      case PR_SCORE_XFER_ELAPSED:
        entry.sce_xfer_elapsed = va_arg(ap, unsigned long);
        break;

      default:
        errno = EINVAL;
        return -1;
    }
  }

  /* Write-lock this entry */
  wlock_entry();
  if (write_entry() < 0)
    pr_log_pri(PR_LOG_NOTICE, "error writing scoreboard entry: %s",
      strerror(errno));
  unlock_entry();

  return 0;
}
