/*
 * ProFTPD - FTP server daemon
 * Copyright (c) 1997, 1998 Public Flood Software
 * Copyright (c) 1999, 2000 MacGyver aka Habeeb J. Dihu <macgyver@tos.net>
 * Copyright (c) 2001-2005 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, Public Flood Software/MacGyver aka Habeeb J. Dihu
 * 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.
 */

/* Authentication front-end for ProFTPD
 * $Id: auth.c,v 1.44 2005/06/14 18:11:12 castaglia Exp $
 */

#include "conf.h"

/* The difference between this function, and pr_cmd_alloc(), is that this
 * allocates the cmd_rec directly from the given pool, whereas pr_cmd_alloc()
 * will allocate a subpool from the given pool, and allocate its cmd_rec
 * from the subpool.  This means that pr_cmd_alloc()'s cmd_rec's can be
 * subsequently destroyed easily; this function's cmd_rec's will be destroyed
 * when the given pool is destroyed.
 */
static cmd_rec *make_cmd(pool *cp, int argc, ...) {
  va_list args;
  cmd_rec *c;
  int     i;

  c = pcalloc(cp, sizeof(cmd_rec));

  c->argc = argc;
  c->stash_index = -1;

  if (argc) {
    c->argv = pcalloc(cp, sizeof(void *) * (argc + 1));

    va_start(args, argc);

    for (i = 0; i < argc; i++)
      c->argv[i] = (void *) va_arg(args, char *);

    va_end(args);

    c->argv[argc] = NULL;
  }

  return c;
}

static modret_t *dispatch_auth(cmd_rec *cmd, char *match) {
  authtable *authtab = NULL;
  modret_t *mr = NULL;

  authtab = pr_stash_get_symbol(PR_SYM_AUTH, match, NULL,
    &cmd->stash_index);

  while (authtab) {
    pr_log_debug(DEBUG6, "dispatching auth request \"%s\" to module mod_%s",
      match, authtab->m->name);

    mr = call_module(authtab->m, authtab->handler, cmd);

    if (authtab->auth_flags & PR_AUTH_FL_REQUIRED)
      break;

    if (MODRET_ISHANDLED(mr) ||
        MODRET_ISERROR(mr))
      break;

    authtab = pr_stash_get_symbol(PR_SYM_AUTH, match, authtab,
      &cmd->stash_index);
  }

  return mr;
}

void pr_auth_setpwent(pool *p) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;

  cmd = make_cmd(p, 0);
  mr = dispatch_auth(cmd, "setpwent");

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return;
}

void pr_auth_endpwent(pool *p) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;

  cmd = make_cmd(p, 0);
  mr = dispatch_auth(cmd, "endpwent");

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return;
}

void pr_auth_setgrent(pool *p) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;

  cmd = make_cmd(p, 0);
  mr = dispatch_auth(cmd, "setgrent");

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return;
}

void pr_auth_endgrent(pool *p) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;

  cmd = make_cmd(p, 0);
  mr = dispatch_auth(cmd, "endgrent");

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return;
}

struct passwd *pr_auth_getpwent(pool *p) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  struct passwd *res = NULL;

  cmd = make_cmd(p, 0);
  mr = dispatch_auth(cmd, "getpwent");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr))
    res = mr->data;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  /* Sanity check */
  if (res == NULL)
    return NULL;

  /* Make sure the UID and GID are not -1 */
  if (res->pw_uid == (uid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: UID of -1 not allowed");
    return NULL;
  }

  if (res->pw_gid == (gid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: GID of -1 not allowed");
    return NULL;
  }

  return res;
}

struct group *pr_auth_getgrent(pool *p) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  struct group *res = NULL;

  cmd = make_cmd(p, 0);
  mr = dispatch_auth(cmd, "getgrent");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr))
    res = mr->data;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  /* Sanity check */
  if (res == NULL)
    return NULL;

  /* Make sure the GID is not -1 */
  if (res->gr_gid == (gid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: GID of -1 not allowed");
    return NULL;
  }

  return res;
}

struct passwd *pr_auth_getpwnam(pool *p, const char *name) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  struct passwd *res = NULL;

  cmd = make_cmd(p, 1, name);
  mr = dispatch_auth(cmd, "getpwnam");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr))
    res = mr->data;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  /* Sanity check */
  if (res == NULL) {
    pr_log_pri(PR_LOG_NOTICE, "no such user '%s'", name);
    return NULL;
  }

  /* Make sure the UID and GID are not -1 */
  if (res->pw_uid == (uid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: UID of -1 not allowed");
    return NULL;
  }

  if (res->pw_gid == (gid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: GID of -1 not allowed");
    return NULL;
  }

  pr_log_debug(DEBUG10, "retrieved UID %lu for user '%s'",
    (unsigned long) res->pw_uid, name);
  return res;
}

struct passwd *pr_auth_getpwuid(pool *p, uid_t uid) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  struct passwd *res = NULL;

  cmd = make_cmd(p, 1, (void *) &uid);
  mr = dispatch_auth(cmd, "getpwuid");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr))
    res = mr->data;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  /* Sanity check */
  if (res == NULL) {
    pr_log_pri(PR_LOG_NOTICE, "no such UID %lu", (unsigned long) uid);
    return NULL;
  }

  /* Make sure the UID and GID are not -1 */
  if (res->pw_uid == (uid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: UID of -1 not allowed");
    return NULL;
  }

  if (res->pw_gid == (gid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: GID of -1 not allowed");
    return NULL;
  }

  pr_log_debug(DEBUG10, "retrieved user '%s' for UID %lu",
    res->pw_name, (unsigned long) uid);
  return res;
}

struct group *pr_auth_getgrnam(pool *p, const char *name) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  struct group *res = NULL;

  cmd = make_cmd(p, 1, name);
  mr = dispatch_auth(cmd, "getgrnam");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr))
    res = mr->data;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  /* Sanity check */
  if (res == NULL) {
    pr_log_pri(PR_LOG_NOTICE, "no such group '%s'", name);
    return NULL;
  }

  /* Make sure the GID is not -1 */
  if (res->gr_gid == (gid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: GID of -1 not allowed");
    return NULL;
  }

  pr_log_debug(DEBUG10, "retrieved GID %lu for group '%s'",
    (unsigned long) res->gr_gid, name);
  return res;
}

struct group *pr_auth_getgrgid(pool *p, gid_t gid) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  struct group *res = NULL;

  cmd = make_cmd(p, 1, (void *) &gid);
  mr = dispatch_auth(cmd, "getgrgid");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr))
    res = mr->data;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  /* Sanity check */
  if (res == NULL) {
    pr_log_pri(PR_LOG_NOTICE, "no such GID %lu", (unsigned long) gid);
    return NULL;
  }

  /* Make sure the GID is not -1 */
  if (res->gr_gid == (gid_t) -1) {
    pr_log_pri(PR_LOG_ERR, "error: GID of -1 not allowed");
    return NULL;
  }

  pr_log_debug(DEBUG10, "retrieved group '%s' for GID %lu",
    res->gr_name, (unsigned long) gid);
  return res;
}

int pr_auth_authenticate(pool *p, const char *name, const char *pw) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  int res = PR_AUTH_NOPWD;

  cmd = make_cmd(p, 2, name, pw);
  mr = dispatch_auth(cmd, "auth");

  if (MODRET_ISHANDLED(mr))
    res = MODRET_HASDATA(mr) ? PR_AUTH_RFC2228_OK : PR_AUTH_OK;

  else if (MODRET_ISERROR(mr))
    res = MODRET_ERROR(mr);

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

int pr_auth_check(pool *p, const char *cpw, const char *name, const char *pw) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  int res = PR_AUTH_BADPWD;

  cmd = make_cmd(p, 3, cpw, name, pw);
  mr = dispatch_auth(cmd, "check");

  if (MODRET_ISHANDLED(mr))
    res = MODRET_HASDATA(mr) ? PR_AUTH_RFC2228_OK : PR_AUTH_OK;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

int pr_auth_requires_pass(pool *p, const char *name) {
  cmd_rec *cmd;
  modret_t *mr;
  int res = TRUE;

  cmd = make_cmd(p, 1, name);
  mr = dispatch_auth(cmd, "requires_pass");

  if (MODRET_ISHANDLED(mr))
    res = FALSE;

  else if (MODRET_ISERROR(mr))
    res = MODRET_ERROR(mr);

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

const char *pr_auth_uid2name(pool *p, uid_t uid) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  static char namebuf[64];
  char *res = "(?)";

  memset(namebuf, '\0', sizeof(namebuf));

  cmd = make_cmd(p, 1, (void *) &uid);
  mr = dispatch_auth(cmd, "uid2name");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr)) {
    res = mr->data;
    sstrncpy(namebuf, res, sizeof(namebuf));
    res = namebuf;
  }

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

const char *pr_auth_gid2name(pool *p, gid_t gid) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  static char namebuf[64];
  char *res = "(?)";

  memset(namebuf, '\0', sizeof(namebuf));

  cmd = make_cmd(p, 1, (void *) &gid);
  mr = dispatch_auth(cmd, "gid2name");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr)) {
    res = mr->data;
    sstrncpy(namebuf, res, sizeof(namebuf));
    res = namebuf;
  }

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

uid_t pr_auth_name2uid(pool *p, const char *name) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  uid_t res = (uid_t) -1;

  cmd = make_cmd(p, 1, name);
  mr = dispatch_auth(cmd, "name2uid");

  if (MODRET_ISHANDLED(mr))
    res = *((uid_t *) mr->data);
  else
    errno = EINVAL;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

gid_t pr_auth_name2gid(pool *p, const char *name) {
  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  gid_t res = (gid_t) -1;

  cmd = make_cmd(p, 1, name);
  mr = dispatch_auth(cmd, "name2gid");

  if (MODRET_ISHANDLED(mr))
    res = *((gid_t *) mr->data);
  else
    errno = EINVAL;

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

int pr_auth_getgroups(pool *p, const char *name, array_header **group_ids,
    array_header **group_names) {

  cmd_rec *cmd = NULL;
  modret_t *mr = NULL;
  int res = -1;

  /* Allocate memory for the array_headers of GIDs and group names. */
  if (group_ids)
    *group_ids = make_array(permanent_pool, 2, sizeof(gid_t));

  if (group_names)
    *group_names = make_array(permanent_pool, 2, sizeof(char *));

  cmd = make_cmd(p, 3, name, group_ids ? *group_ids : NULL,
    group_names ? *group_names : NULL);

  mr = dispatch_auth(cmd, "getgroups");

  if (MODRET_ISHANDLED(mr) && MODRET_HASDATA(mr)) {
    res = *((int *) mr->data);

    /* Note: the number of groups returned should, barring error,
     * always be at least 1, as per getgroups(2) behavior.  This one
     * ID is present because it is the primary group membership set in
     * struct passwd, from /etc/passwd.  This will need to be documented
     * for the benefit of auth_getgroup() implementors.
     */

    if (group_ids) {
      register unsigned int i;
      char *strgids = "";
      gid_t *gids = (*group_ids)->elts;

      for (i = 0; i < (*group_ids)->nelts; i++) {
        char buf[64];
        snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) gids[i]);
        buf[sizeof(buf)-1] = '\0';

        strgids = pstrcat(p, strgids, i != 0 ? ", " : "", buf, NULL);
      }

      pr_log_debug(DEBUG10, "retrieved group %s: %s",
        (*group_ids)->nelts == 1 ? "ID" : "IDs", strgids);
    }

    if (group_names) {
      register unsigned int i;
      char *strgroups = ""; 
      char **groups = (*group_names)->elts;

      for (i = 0; i < (*group_names)->nelts; i++)
        strgroups = pstrcat(p, strgroups, i != 0 ? ", " : "", groups[i], NULL);
    
      pr_log_debug(DEBUG10, "retrieved group %s: %s",
        (*group_names)->nelts == 1 ? "name" : "names", strgroups);
    }
  }

  if (cmd->tmp_pool) {
    destroy_pool(cmd->tmp_pool);
    cmd->tmp_pool = NULL;
  }

  return res;
}

/* Helper function for pr_auth_get_anon_config(), for handling the
 * (horrible) AnonymousGroup directive.
 */
static config_rec *auth_anonymous_group(pool *p, char *user) {
  config_rec *c;
  int ret = 0;

  /* Retrieve the session group membership information, so that this check
   * may work properly.
   */
  if (!session.gids && !session.groups &&
      (ret = pr_auth_getgroups(p, user, &session.gids, &session.groups)) < 1)
    pr_log_debug(DEBUG2, "no supplemental groups found for user '%s'", user);

  c = find_config(main_server->conf, CONF_PARAM, "AnonymousGroup", FALSE);

  if (c)
    do {
      ret = pr_expr_eval_group_and((char **) c->argv);

    } while (!ret &&
      (c = find_config_next(c, c->next, CONF_PARAM, "AnonymousGroup",
        FALSE)) != NULL);

  return ret ? c : NULL;
}

/* This is one messy function.  Yuck.  Yay legacy code. */
config_rec *pr_auth_get_anon_config(pool *p, char **login_name,
    char **user_name, char **anon_name) {
  config_rec *c = NULL, *topc = NULL;
  char *config_user_name, *config_anon_name = NULL;
  unsigned char is_alias = FALSE, force_anon = FALSE, *auth_alias_only = NULL;

  /* Precendence rules:
   *   1. Search for UserAlias directive.
   *   2. Search for Anonymous directive.
   *   3. Normal user login
   */

  config_user_name = get_param_ptr(main_server->conf, "UserName", FALSE);
  if (config_user_name && user_name)
    *user_name = config_user_name;

  c = find_config(main_server->conf, CONF_PARAM, "UserAlias", TRUE);
  if (c) {
    do {
      if (strcmp(c->argv[0], "*") == 0 ||
          strcmp(c->argv[0], *login_name) == 0) {
        is_alias = TRUE;
        break;
      }

    } while ((c = find_config_next(c, c->next, CONF_PARAM, "UserAlias",
      TRUE)) != NULL);
  }

  /* This is where things get messy, rapidly. */
  topc = c;

  while (c && c->parent &&
    (auth_alias_only = get_param_ptr(c->parent->set, "AuthAliasOnly", FALSE))) {

    /* while() loops should always handle signals. */
    pr_signals_handle();

    /* If AuthAliasOnly is on, ignore this one and continue. */
    if (auth_alias_only &&
        *auth_alias_only == TRUE) {
      c = find_config_next(c, c->next, CONF_PARAM, "UserAlias", TRUE);
      continue;
    }

    is_alias = FALSE;

    find_config_set_top(topc);
    c = find_config_next(c, c->next, CONF_PARAM, "UserAlias", TRUE);

    if (c &&
        (strcmp(c->argv[0], "*") == 0 ||
         strcmp(c->argv[0], *login_name) == 0))
      is_alias = TRUE;
  }

  if (c) {
    *login_name = c->argv[1];

    /* If the alias is applied inside an <Anonymous> context, we have found
     * our anon block.
     */
    if (c->parent &&
        c->parent->config_type == CONF_ANON)
      c = c->parent;
    else
      c = NULL;
  }

  /* Next, search for an anonymous entry. */

  if (!c)
    c = find_config(main_server->conf, CONF_ANON, NULL, FALSE);
  else
    find_config_set_top(c);

  if (c) {
    do {
      config_anon_name = get_param_ptr(c->subset, "UserName", FALSE);

      if (!config_anon_name)
        config_anon_name = config_user_name;

      if (config_anon_name &&
          strcmp(config_anon_name, *login_name) == 0) {
         if (anon_name)
           *anon_name = config_anon_name;
         break;
      }
 
    } while ((c = find_config_next(c, c->next, CONF_ANON, NULL,
      FALSE)) != NULL);
  }

  if (!c) {
    c = auth_anonymous_group(p, *login_name);

    if (c)
      force_anon = TRUE;
  }

  if (!is_alias && !force_anon) {
    auth_alias_only = get_param_ptr(c ? c->subset : main_server->conf,
      "AuthAliasOnly", FALSE);

    if (auth_alias_only &&
        *auth_alias_only == TRUE) {
      if (c && c->config_type == CONF_ANON)
        c = NULL;
      else
        *login_name = NULL;

      auth_alias_only = get_param_ptr(main_server->conf, "AuthAliasOnly",
        FALSE);
      if (*login_name &&
          auth_alias_only &&
          *auth_alias_only == TRUE)
        *login_name = NULL;

      if ((!login_name || !c) &&
          anon_name)
        *anon_name = NULL;
    }
  }

  return c;
}

int set_groups(pool *p, gid_t primary_gid, array_header *suppl_gids) {
  int res = 0;
  pool *tmp_pool = NULL;

#ifdef HAVE_SETGROUPS
  register unsigned int i = 0;
  gid_t *gids = NULL, *proc_gids = NULL;
  size_t ngids = 0, nproc_gids = 0;
  char *strgids = "";

  /* sanity check */
  if (!p || !suppl_gids)
    return 0;

  tmp_pool = make_sub_pool(p);
  pr_pool_tag(tmp_pool, "set_groups() tmp pool");

  /* Check for a NULL supplemental group ID list. */
  if (suppl_gids) {
    ngids = suppl_gids->nelts;
    gids = suppl_gids->elts;

    if (ngids && gids) {
      proc_gids = pcalloc(tmp_pool, sizeof(gid_t) * (ngids));

      /* Note: the list of supplemental GIDs may contain duplicates.  Sort
       * through the list and keep only the unique IDs - this should help avoid
       * running into the NGROUPS limit when possible.  This algorithm may slow
       * things down some; optimize it if/when possible.
       */
      proc_gids[nproc_gids++] = gids[0];
    }
  }

  for (i = 1; i < ngids; i++) {
    register unsigned int j = 0;
    unsigned char skip_gid = FALSE;

    /* This duplicate ID search only needs to be done after the first GID
     * in the given list is examined, as the first GID cannot be a duplicate.
     */
    for (j = 0; j < nproc_gids; j++) {
      if (proc_gids[j] == gids[i]) {
        skip_gid = TRUE;
        break;
      }
    }

    if (!skip_gid)
      proc_gids[nproc_gids++] = gids[i];
  }

  for (i = 0; i < nproc_gids; i++) {
    char buf[64];
    snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) proc_gids[i]);
    buf[sizeof(buf)-1] = '\0';

    strgids = pstrcat(p, strgids, i != 0 ? ", " : "", buf, NULL);
  }

  pr_log_debug(DEBUG10, "setting group %s: %s", nproc_gids == 1 ? "ID" : "IDs",
    strgids);

  /* Set the supplemental groups. */
  res = setgroups(nproc_gids, proc_gids);
  if (res < 0) {
    destroy_pool(tmp_pool);
    return res;
  }
#endif /* !HAVE_SETGROUPS */

#ifndef PR_DEVEL_COREDUMP
  /* Set the primary GID of the process.
   */
  res = setgid(primary_gid);
  if (res < 0) {
    if (tmp_pool)
      destroy_pool(tmp_pool);
    return res;
  }
#endif /* PR_DEVEL_COREDUMP */

  if (tmp_pool)
    destroy_pool(tmp_pool);

  return res;
}

