/*  $Id: ckpasswd.c 7565 2006-08-28 02:42:54Z eagle $
**
**  The default username/password authenticator.
**
**  This program is intended to be run by nnrpd and handle usernames and
**  passwords.  It can authenticate against a regular flat file (the type
**  managed by htpasswd), a DBM file, the system password file or shadow file,
**  or PAM.
*/

/* Used for unused parameters to silence gcc warnings. */
#define UNUSED  __attribute__((__unused__))

/* Make available the bool type. */
#if INN_HAVE_STDBOOL_H
# include <stdbool.h>
#else
# undef true
# undef false
# define true   (1)
# define false  (0)
# ifndef __cplusplus
#  define bool int
# endif
#endif /* INN_HAVE_STDBOOL_H */

#include <stdlib.h>
#include <string.h>
#include <crypt.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>

#define DB_DBM_HSEARCH 1
#include <db.h>
#define OPT_DBM "d:"

#if HAVE_GETSPNAM
# include <shadow.h>
# define OPT_SHADOW "s"
#else
# define OPT_SHADOW ""
#endif

#include "messages.h"
#include "xmalloc.h"

#include <security/pam_appl.h>

/* Holds the authentication information from nnrpd. */
struct auth_info {
    char *username;
    char *password;
};

/*
**  The PAM conversation function.
**
**  Since we already have all the information and can't ask the user
**  questions, we can't quite follow the real PAM protocol.  Instead, we just
**  return the password in response to every question that PAM asks.  There
**  appears to be no generic way to determine whether the message in question
**  is indeed asking for the password....
**
**  This function allocates an array of struct pam_response to return to the
**  PAM libraries that's never freed.  For this program, this isn't much of an
**  issue, since it will likely only be called once and then the program will
**  exit.  This function uses malloc and strdup instead of xmalloc and xstrdup
**  intentionally so that the PAM conversation will be closed cleanly if we
**  run out of memory rather than simply terminated.
**
**  appdata_ptr contains the password we were given.
*/
static int pass_conv(int num_msg, const struct pam_message **msgm UNUSED, struct pam_response **response, void *appdata_ptr)
{
    int i;

    *response = malloc(num_msg * sizeof(struct pam_response));
    if (*response == NULL)
        return PAM_CONV_ERR;
    for (i = 0; i < num_msg; i++) {
        (*response)[i].resp = strdup((char *)appdata_ptr);
        (*response)[i].resp_retcode = 0;
    }
    return PAM_SUCCESS;
}


/*
**  Authenticate a user via PAM.
**
**  Attempts to authenticate a user with PAM, returning true if the user
**  successfully authenticates and false otherwise.  Note that this function
**  doesn't attempt to handle any remapping of the authenticated user by the
**  PAM stack, but just assumes that the authenticated user was the same as
**  the username given.
**
**  Right now, all failures are handled via die.  This may be worth revisiting
**  in case we want to try other authentication methods if this fails for a
**  reason other than the system not having PAM support.
*/

static bool auth_pam(const char *username, char *password)
{
    pam_handle_t *pamh;
    struct pam_conv conv;
    int status;

    conv.conv = pass_conv;
    conv.appdata_ptr = password;
    status = pam_start("nnrpd", username, &conv, &pamh);
    if (status != PAM_SUCCESS)
        die("pam_start failed: %s", pam_strerror(pamh, status));
    status = pam_authenticate(pamh, PAM_SILENT);
    if (status != PAM_SUCCESS)
        die("pam_authenticate failed: %s", pam_strerror(pamh, status));
    status = pam_acct_mgmt(pamh, PAM_SILENT);
    if (status != PAM_SUCCESS)
        die("pam_acct_mgmt failed: %s", pam_strerror(pamh, status));
    status = pam_end(pamh, status);
    if (status != PAM_SUCCESS)
        die("pam_end failed: %s", pam_strerror(pamh, status));

    /* If we get to here, the user successfully authenticated. */
    return true;
}


/*
**  Try to get a password out of a dbm file.  The dbm file should have the
**  username for the key and the crypted password as the value.  The crypted
**  password, if found, is returned as a newly allocated string; otherwise,
**  NULL is returned.
*/
#if !(defined(HAVE_DBM) || defined(HAVE_BDB_DBM))
static char *
password_dbm(char *user UNUSED, const char *file UNUSED)
{
    return NULL;
}
#else
static char *
password_dbm(char *name, const char *file)
{
    datum key, value;
    DBM *database;
    char *password;

    database = dbm_open(file, O_RDONLY, 0600);
    if (database == NULL)
        return NULL;
    key.dptr = name;
    key.dsize = strlen(name);
    value = dbm_fetch(database, key);
    if (value.dptr == NULL) {
        dbm_close(database);
        return NULL;
    }
    password = xmalloc(value.dsize + 1);
    strlcpy(password, value.dptr, value.dsize + 1);
    dbm_close(database);
    return password;
}
#endif /* HAVE_DBM || HAVE_BDB_DBM */


/*
**  Try to get a password out of the system /etc/shadow file.  The crypted
**  password, if found, is returned as a newly allocated string; otherwise,
**  NULL is returned.
*/
#if !HAVE_GETSPNAM
static char *
password_shadow(const char *user UNUSED)
{
    return NULL;
}
#else
static char *
password_shadow(const char *user)
{
    struct spwd *spwd;

    spwd = getspnam(user);
    if (spwd != NULL)
        return xstrdup(spwd->sp_pwdp);
    return NULL;
}
#endif /* HAVE_GETSPNAM */


/*
**  Try to get a password out of the system password file.  The crypted
**  password, if found, is returned as a newly allocated string; otherwise,
**  NULL is returned.
*/
static char *
password_system(const char *username)
{
    struct passwd *pwd;

    pwd = getpwnam(username);
    if (pwd != NULL)
        return xstrdup(pwd->pw_passwd);
    return NULL;
}


/*
**  Try to get the name of a user's primary group out of the system group 
**  file.  The group, if found, is returned as a newly allocated string;
**  otherwise, NULL is returned.  If the username is not found, NULL is
**  returned.
*/
static char *
group_system(const char *username)
{
    struct passwd *pwd;
    struct group *gr;

    pwd = getpwnam(username);
    if (pwd == NULL)
        return NULL;
    gr = getgrgid(pwd->pw_gid);
    if (gr == NULL)
        return NULL;
    return xstrdup(gr->gr_name);
}


/*
**  Output username (and group, if desired) in correct return format.
*/
static void
output_user(const char *username, bool wantgroup)
{
    if (wantgroup) {
        char *group = group_system(username);
        if (group == NULL)
            die("group info for user %s not available", username);
        printf("User:%s@%s\n", username, group);
    }
    else
        printf("User:%s\n", username);
}


/*
**  Main routines.
**
**  We handle the variences between systems with #if blocks above, so that
**  this code can look fairly clean.
*/

int
check_password(const char* username, const char* password)
{
    bool wantgroup = false;
    struct auth_info *authinfo = NULL;

    authinfo = xmalloc(sizeof(struct auth_info));
    authinfo->username = username;
    authinfo->password = password;

    if (auth_pam(authinfo->username, authinfo->password)) {
        output_user(authinfo->username, wantgroup);
        return 0;
    }
    password = password_system(authinfo->username);
    if (password == NULL)
        return 1;
    if (strcmp(password, crypt(authinfo->password, password)) != 0)
        return 1;

    /* The password matched. */
    output_user(authinfo->username, wantgroup);
    return 0;
}