/*
 * ProFTPD: mod_auth_pam -- Support for PAM-style authentication.
 * Copyright (c) 1998, 1999, 2000 Habeeb J. Dihu aka
 *   MacGyver <macgyver@tos.net>, All Rights Reserved.
 * Copyright 2000-2006 The ProFTPD Project
 *
 * 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.
 */

/*
 * PAM module from ProFTPD
 *
 * This module should work equally well under all Linux distributions (which
 * have PAM support), as well as Solaris 2.5 and above.
 *
 * If you have any problems, questions, comments, or suggestions regarding
 * this module, please feel free to contact Habeeb J. Dihu aka MacGyver
 * <macgyver@tos.net>.
 *
 * -- DO NOT MODIFY THE TWO LINES BELOW --
 * $Libraries: -lpam$
 * $Id: mod_auth_pam.c,v 1.16 2006/06/22 16:08:57 castaglia Exp $
 */

#include "conf.h"
#include "privs.h"

#ifdef HAVE_PAM

#define MOD_AUTH_PAM_VERSION		"mod_auth_pam/1.0.1"

#ifdef HAVE_SECURITY_PAM_APPL_H
# ifdef HPUX11
#  ifndef COMSEC
#    define COMSEC 1
#  endif
# endif /* HPUX11 */
# include <security/pam_appl.h>
#endif /* HAVE_SECURITY_PAM_APPL_H */

#ifdef HAVE_SECURITY_PAM_MODULES_H
# include <security/pam_modules.h>
#endif /* HAVE_SECURITY_PAM_MODULES_H */

/* Needed for the MAXLOGNAME restriction. */
#ifdef HAVE_SYS_PARAM_H
# include <sys/param.h>
#endif

#ifdef HAVE_PAM_PAM_APPL_H
#include <pam/pam_appl.h>
#endif /* HAVE_PAM_PAM_APPL_H */

module auth_pam_module;
static authtable auth_pam_authtab[2];

static pam_handle_t *	pamh			= NULL;
static char *		pamconfig		= "ftp";
static char *		pam_user 		= NULL;
static char *		pam_pass 		= NULL;
static size_t		pam_user_len		= 0;
static size_t		pam_pass_len		= 0;
static int		pam_conv_error		= 0;

static int pam_exchange(num_msg, msg, resp, appdata_ptr)
     int num_msg;
     struct pam_message **msg;
     struct pam_response **resp;
     void *appdata_ptr;
{
  register unsigned int i;
  struct pam_response *response = NULL;

  if (num_msg <= 0)
    return PAM_CONV_ERR;

  response = calloc(num_msg, sizeof(struct pam_response));

  if (response == NULL)
    return PAM_CONV_ERR;

  for (i = 0; i < num_msg; i++) {
    response[i].resp_retcode = 0; /* PAM_SUCCESS; */

    switch (msg[i]->msg_style) {
    case PAM_PROMPT_ECHO_ON:
      /* PAM frees response and resp.  If you don't believe this, please read
       * the actual PAM RFCs as well as have a good look at libpam.
       */
      response[i].resp = pam_user ? strdup(pam_user) : NULL;
      break;

    case PAM_PROMPT_ECHO_OFF:
      /* PAM frees response and resp.  If you don't believe this, please read
       * the actual PAM RFCs as well as have a good look at libpam.
       */
      response[i].resp = pam_pass ? strdup(pam_pass) : NULL;
      break;

    case PAM_TEXT_INFO:
    case PAM_ERROR_MSG:
      /* Ignore it, but pam still wants a NULL response... */
      response[i].resp = NULL;
      break;

    default:
      /* Must be an error of some sort... */
      free(response[i].resp);
      free(response);

      pam_conv_error = 1;
      return PAM_CONV_ERR;
    }
  }

  *resp = response;
  return PAM_SUCCESS;
}

static struct pam_conv pam_conv = {
  &pam_exchange,
  NULL
};

static void auth_pam_exit_ev(const void *event_data, void *user_data) {
  int pam_error = 0;

  /* Sanity check.
   */
  if (pamh == NULL)
    return;

  /* We need privileges to be able to write to things like lastlog and
   * friends.
   */
  pr_signals_block();
  PRIVS_ROOT

  /* Give up our credentials, close our session, and finally close out this
   * instance of PAM authentication.
   */
#ifdef PAM_CRED_DELETE
  pam_error = pam_setcred(pamh, PAM_CRED_DELETE);
#else
  pam_error = pam_setcred(pamh, PAM_DELETE_CRED);
#endif /* !PAM_CRED_DELETE */
  if (pam_error != PAM_SUCCESS)
    pr_log_pri(PR_LOG_NOTICE, "PAM(setcred): %s",
      pam_strerror(pamh, pam_error));

  pam_error = pam_close_session(pamh, PAM_SILENT);
  if (pam_error != PAM_SUCCESS)
    pr_log_pri(PR_LOG_NOTICE, "PAM(close_session): %s",
      pam_strerror(pamh, pam_error));

#ifndef SOLARIS2
  pam_end(pamh, 0);
  pamh = NULL;
#endif

  if (pam_user != NULL) {
    memset(pam_user, '\0', pam_user_len);
    free(pam_user);
    pam_user = NULL;
    pam_user_len = 0;
  }

  PRIVS_RELINQUISH
  pr_signals_unblock();
}

MODRET pam_auth(cmd_rec *cmd) {
  int pam_error = 0, retval = PR_AUTH_ERROR, success = 0;
  config_rec *c = NULL;
  unsigned char *auth_pam = NULL, pam_authoritative = FALSE;

#ifdef SOLARIS2
  char ttyentry[32];
#endif /* SOLARIS2 */

  /* If we have been explicitly disabled, return now.  Otherwise,
   * the module is considered enabled.
   */
  auth_pam = get_param_ptr(main_server->conf, "AuthPAM", FALSE);
  if (auth_pam != NULL &&
      *auth_pam == FALSE) {
    return PR_DECLINED(cmd);
  }

  /* Figure out our default return style: whether or not PAM should allow
   * other auth modules a shot at this user or not is controlled by adding
   * '*' to a module name in the AuthOrder directive.  By default, auth
   * modules are not authoritative, and allow other auth modules a chance at
   * authenticating the user.  This is not the most secure configuration, but
   * it allows things like AuthUserFile to work "out of the box".
   */
  if (auth_pam_authtab[0].auth_flags & PR_AUTH_FL_REQUIRED) {
    pam_authoritative = TRUE;
  }

  /* Just in case...
   */
  if (cmd->argc != 2)
    return pam_authoritative ? PR_ERROR(cmd) : PR_DECLINED(cmd);

  /* Allocate our entries...we free these up at the end of the authentication.
   */
  if ((pam_user_len = strlen(cmd->argv[0]) + 1) > (PAM_MAX_MSG_SIZE + 1))
    pam_user_len = PAM_MAX_MSG_SIZE + 1;

#ifdef MAXLOGNAME
  /* Some platforms' PAM libraries do not handle login strings that
   * exceed this length.
   */
  if (pam_user_len > MAXLOGNAME) {
    pr_log_pri(PR_LOG_NOTICE,
      "PAM(%s): Name exceeds maximum login length (%u)", cmd->argv[0],
      MAXLOGNAME);
    return PR_DECLINED(cmd);
  }
#endif
  pam_user = malloc(pam_user_len);
  if (pam_user == NULL)
    return pam_authoritative ? PR_ERROR(cmd) : PR_DECLINED(cmd);

  sstrncpy(pam_user, cmd->argv[0], pam_user_len);

  if ((pam_pass_len = strlen(cmd->argv[1]) + 1) > (PAM_MAX_MSG_SIZE + 1))
    pam_pass_len = PAM_MAX_MSG_SIZE + 1;
 
  pam_pass = malloc(pam_pass_len);
  if (pam_pass == NULL) {
    memset(pam_user, '\0', pam_user_len);
    free(pam_user);
    pam_user = NULL;
    pam_user_len = 0;
    pam_pass_len = 0;
    return pam_authoritative ? PR_ERROR(cmd) : PR_DECLINED(cmd);
  }

  sstrncpy(pam_pass, cmd->argv[1], pam_pass_len);

  /* Check for which PAM config file to use.  Since we have many different
   * potential servers, they may each require a separate type of PAM
   * authentication.
   */
  if ((c = find_config(main_server->conf, CONF_PARAM, "AuthPAMConfig",
      FALSE)) != NULL)
    pamconfig = c->argv[0];

  /* Due to the different types of authentication used, such as shadow
   * passwords, etc. we need root privs for this operation.
   */
  pr_signals_block();
  PRIVS_ROOT

  /* The order of calls into PAM should be as follows, according to Sun's
   * documentation at http://www.sun.com/software/solaris/pam/:
   *
   * pam_start()
   * pam_authenticate()
   * pam_acct_mgmt()
   * pam_open_session()
   * pam_setcred()
   */
  pam_error = pam_start(pamconfig, pam_user, &pam_conv, &pamh);
  if (pam_error != PAM_SUCCESS)
    goto done;

  pam_set_item(pamh, PAM_RUSER, pam_user);

  /* Set our host environment for PAM modules that check host information.
   */
  if (session.c != NULL)
    pam_set_item(pamh, PAM_RHOST, session.c->remote_name);
  else
    pam_set_item(pamh, PAM_RHOST, "IHaveNoIdeaHowIGotHere");

#ifdef SOLARIS2
  /* Set our TTY environment.  This is apparently required for Solaris
   * environments, since unless PAM_RHOST and PAM_TTY are defined, and
   * the string given to PAM_TTY must be of the form (or at least greater
   * than the length of) "/dev/", pam_open_session() will crash and burn
   * a horrible death that took many hours to debug...YUCK.
   *
   * This bug is Sun bugid 4250887, and should be fixed in an update for
   * Solaris.  -- MacGyver
   */
  snprintf(ttyentry, sizeof(ttyentry), "/dev/ftp%02lu",
    (unsigned long) getpid());
  pam_set_item(pamh, PAM_TTY, ttyentry);
#endif /* SOLARIS2 */

  /* Authenticate, and get any credentials as needed.
   */
  pam_error = pam_authenticate(pamh, PAM_SILENT);

  if (pam_error != PAM_SUCCESS) {
    switch (pam_error) {
      case PAM_USER_UNKNOWN:
        retval = PR_AUTH_NOPWD;
        break;

      default:
        retval = PR_AUTH_BADPWD;
        break;
    }

    pr_log_pri(PR_LOG_NOTICE, "PAM(%s): %s.", cmd->argv[0],
      pam_strerror(pamh, pam_error));
    goto done;
  }

  if (pam_conv_error != 0) {
    retval = PR_AUTH_BADPWD;
    goto done;
  }

  pam_error = pam_acct_mgmt(pamh, PAM_SILENT);

  if (pam_error != PAM_SUCCESS) {
    switch (pam_error) {
#ifdef PAM_AUTHTOKEN_REQD
      case PAM_AUTHTOKEN_REQD:
        retval = PR_AUTH_AGEPWD;
        break;
#endif /* PAM_AUTHTOKEN_REQD */

      case PAM_ACCT_EXPIRED:
#ifdef PAM_ACCT_DISABLED
      case PAM_ACCT_DISABLED:
#endif /* PAM_ACCT_DISABLED */
        retval = PR_AUTH_DISABLEDPWD;
        break;

      case PAM_USER_UNKNOWN:
        retval = PR_AUTH_NOPWD;
        break;

      default:
        retval = PR_AUTH_BADPWD;
        break;
    }

    pr_log_pri(PR_LOG_NOTICE, "PAM(%s): %s.", cmd->argv[0],
      pam_strerror(pamh, pam_error));
    goto done;
  }

  /* Open the session. */
  pam_error = pam_open_session(pamh, PAM_SILENT);

  if (pam_error != PAM_SUCCESS) {
    switch (pam_error) {
      case PAM_SESSION_ERR:
      default:
        retval = PR_AUTH_DISABLEDPWD;
        break;
    }

    pr_log_pri(PR_LOG_NOTICE, "PAM(%s): %s.", cmd->argv[0],
      pam_strerror(pamh, pam_error));
    goto done;
  }

  /* Finally, establish credentials. */
#ifdef PAM_CRED_ESTABLISH
  pam_error = pam_setcred(pamh, PAM_CRED_ESTABLISH);
#else
  pam_error = pam_setcred(pamh, PAM_ESTABLISH_CRED);
#endif /* !PAM_CRED_ESTABLISH */

  if (pam_error != PAM_SUCCESS) {
    switch (pam_error) {
      case PAM_CRED_EXPIRED:
        retval = PR_AUTH_AGEPWD;
        break;

      case PAM_USER_UNKNOWN:
        retval = PR_AUTH_NOPWD;
        break;

      default:
        retval = PR_AUTH_BADPWD;
        break;
    }

    pr_log_pri(PR_LOG_NOTICE, "PAM(%s): %s.", cmd->argv[0],
      pam_strerror(pamh, pam_error));
    goto done;
  }

  success++;

 done:

  /* And we're done.  Clean up and relinquish our root privs.
   */

#if defined(SOLARIS2) || defined(HPUX10) || defined(HPUX11)
  if (success)
    pam_error = pam_close_session(pamh, 0);

  if (pamh)
    pam_end(pamh, pam_error);
  pamh = NULL;
#endif

  if (pam_pass != NULL) {
    pr_memscrub(pam_pass, pam_pass_len);
    free(pam_pass);
    pam_pass = NULL;
    pam_pass_len = 0;
  }

  PRIVS_RELINQUISH
  pr_signals_unblock();

  if (!success) {
    if (pam_user != NULL) {
      memset(pam_user, '\0', pam_user_len);
      free(pam_user);
      pam_user = NULL;
      pam_user_len = 0;
    }

    return pam_authoritative ? PR_ERROR_INT(cmd, retval) : PR_DECLINED(cmd);

  } else {
    session.auth_mech = "mod_auth_pam.c";
    pr_event_register(&auth_pam_module, "core.exit", auth_pam_exit_ev, NULL);
    return PR_HANDLED(cmd);
  }
}

/* Configuration handlers
 */

MODRET set_authpam(cmd_rec *cmd) {
  int bool = -1;
  config_rec *c = NULL;

  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  bool = get_boolean(cmd, 1);
  if (bool == -1)
    CONF_ERROR(cmd, "expected Boolean parameter");

  c = add_config_param(cmd->argv[0], 1, NULL);
  c->argv[0] = pcalloc(c->pool, sizeof(unsigned char));
  *((unsigned char *) c->argv[0]) = bool;

  return PR_HANDLED(cmd);
}

MODRET set_authpamconfig(cmd_rec *cmd) {
  CHECK_ARGS(cmd, 1);
  CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);

  add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);

  return PR_HANDLED(cmd);
}

static authtable auth_pam_authtab[] = {
  { 0, "auth", pam_auth },
  { 0, NULL, NULL }
};

static conftable auth_pam_conftab[] = {
  { "AuthPAM",			set_authpam,			NULL },
  { "AuthPAMConfig",		set_authpamconfig,		NULL },
  { NULL, NULL, NULL }
};

module auth_pam_module = {
  NULL, NULL,

  /* Module API version */
  0x20,

  /* Module name */
  "auth_pam",

  /* Module configuration handler table */
  auth_pam_conftab,

  /* Module command handler table */
  NULL,

  /* Module authentication handler table */
  auth_pam_authtab,

  /* Module initialization */
  NULL,

  /* Session initialization */
  NULL,

  /* Module version */
  MOD_AUTH_PAM_VERSION
};

#endif /* HAVE_PAM */
