/* $Cambridge: hermes/src/prayer/accountd/authenticate.c,v 1.2 2008/05/19 15:55:54 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

/* Copyright (c) University of Cambridge 2000 - 2002 */
/* See the file NOTICE for conditions of use and distribution. */

#include "accountd.h"

#include <pwd.h>
#include <shadow.h>

/* Code for authenicating user across iostream connection to client.
 * User interaction involving a root process: important that this code is
 * protected and properly verified! */

/* Definies a whole series of different authenication methods, including
 * PAM if PAM support configured in ../Config */

/* No prototype for crypt, at least on Linux */
extern char *crypt(char *password, char *salt);

/* ====================================================================== */

#ifdef ACCOUNTD_PAM_ENABLE
#include <libgen.h>
#include <security/pam_appl.h>

/* checkpassword() code checks password using PAM library. Probably need
 * simple crypt and compare version as well */

static int
chkpwconv(int num_msg,
          struct pam_message **msg,
          struct pam_response **resp, void *appdata_ptr)
{
    char *password = (char *) appdata_ptr;
    const struct pam_message *m;
    struct pam_response *r;

    if (num_msg <= 0)
        return PAM_CONV_ERR;

    if (!(*resp = calloc(num_msg, sizeof(struct pam_response))))
        return PAM_BUF_ERR;

    m = *msg;
    r = *resp;
    if (m->msg_style == PAM_PROMPT_ECHO_OFF) {
        if (!(r->resp = strdup(password))) {
            free(*resp);
            *resp = 0;
            return PAM_BUF_ERR;
        }
    }
    return PAM_SUCCESS;
}

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

    conv.conv = (int (*)()) chkpwconv;  /* Prevents hdr mismatch */
    conv.appdata_ptr = password;

    status = pam_start("accountd", username, &conv, &pamh);
    if (status == PAM_SUCCESS)
        status = pam_authenticate(pamh, 0);
    pam_end(pamh, status);

    return ((status == PAM_SUCCESS) ? T : NIL);
}
#else
static int checkpassword_pam(char *username, char *password)
{
    log_panic("PAM authenication selected but support not compiled in");
    return (NIL);
}
#endif

/* ====================================================================== */

/* Naive implementation: no support for expired passwords yet
 * (we don't use them)
 */

static int checkpassword_spnam(char *username, char *password, int md5)
{
    struct spwd *spwd;
    char *crypted;
    int rc;

    if ((spwd = getspnam(username)) == NIL)
        return (NIL);

    if (md5) {
        char tmp[64];

        sprintf(tmp, "%s", spwd->sp_pwdp);
        tmp[11] = '\0';

        if ((crypted = crypt(password, tmp)) != NIL)
            rc = !strcmp(spwd->sp_pwdp, crypted) ? T : NIL;
        else
            rc = NIL;
    } else {
        if ((crypted = crypt(password, spwd->sp_pwdp)) != NIL)
            rc = !strcmp(spwd->sp_pwdp, crypted);
        else
            rc = NIL;
    }

    endspent();
    return (rc);
}

static int checkpassword_pwnam(char *username, char *password, int md5)
{
    struct passwd *pwd;
    char *crypted;
    int rc;

    if ((pwd = getpwnam(username)) == NIL)
        return (NIL);

    if (md5) {
        char tmp[64];

        sprintf(tmp, "$1$%s", pwd->pw_passwd);
        tmp[8] = '\0';

        if ((crypted = crypt(password, tmp)) != NIL)
            rc = !strcmp(pwd->pw_passwd, crypted) ? T : NIL;
        else
            rc = NIL;
    } else {
        if ((crypted = crypt(password, pwd->pw_passwd)) != NIL)
            rc = !strcmp(pwd->pw_passwd, crypted);
        else
            rc = NIL;
    }

    endspent();
    return (rc);
}

/* ====================================================================== */

static void ucase(char *s)
{
    while (*s) {
        *s = Utoupper(*s);
        s++;
    }
}

/* authenicate() *********************************************************
 *
 * Authenicate login session and drop priveledges.
 *     config:  Global configuration
 *     stream:  iostream connection to client
 *   usernamep: Used to return username of logged in user
 *
 * Returns: T => successful login. NIL otherwise
 ************************************************************************/

BOOL
authenicate(struct config *config, struct iostream *stream,
            char **usernamep)
{
    char buffer[MAXLENGTH];
    char *username = NIL;
    char *password = NIL;
    struct passwd *pwd;
    char *cmd, *line;
    int rc = NIL;

    *usernamep = NIL;

    if (getuid() != 0)
        log_fatal("authenicate(): called without root priveledges");

    ioputs(stream, "* ");
    if (config->filter_restricted)
        ioputs(stream, "[FILTER_RESTRICTED] ");

    ioprintf(stream, "Prayer ACCOUNTD server %s" CRLF, VERSION);
    ioflush(stream);

    while (1) {
        if (!iostream_getline(stream, buffer, MAXLENGTH))
            return (NIL);

        if (buffer[0] == '\0')  /* Ignore empty lines */
            continue;

        line = buffer;
        if (!(cmd = string_get_token(&line))) {
            ioputs(stream, "BAD Invalid command" CRLF);
            ioflush(stream);
            continue;
        }
        ucase(cmd);

        if (!strcmp(cmd, "LOGOUT")) {
            ioputs(stream, "OK Logout" CRLF);
            ioflush(stream);
            return (NIL);
        }

        if (strcmp(cmd, "LOGIN") != NIL) {
            ioputs(stream, "BAD Invalid command" CRLF);
            ioflush(stream);
            continue;
        }

        if (!((username = string_get_token(&line)) && (username[0]))) {
            ioputs(stream, "BAD No username" CRLF);
            ioflush(stream);
            continue;
        }

        string_canon_decode(username);

        if (!((password = string_get_token(&line)) && (password[0]))) {
            ioputs(stream, "BAD No password" CRLF);
            ioflush(stream);
            continue;
        }

        string_canon_decode(password);

        /* Empty username/password already reported, just need to loop */
        if (!(username && username[0] && password && password[0]))
            continue;

        /* Only need to authenicate if root */
        switch (config->authtype) {
        case AUTHTYPE_PAM:
            rc = checkpassword_pam(username, password);
            break;
        case AUTHTYPE_PWD:
            rc = checkpassword_pwnam(username, password, 0);
            break;
        case AUTHTYPE_PWD_MD5:
            rc = checkpassword_pwnam(username, password, 1);
            break;
        case AUTHTYPE_SHADOW:
            rc = checkpassword_spnam(username, password, 0);
            break;
        case AUTHTYPE_SHADOW_MD5:
            rc = checkpassword_spnam(username, password, 1);
            break;
        default:
            log_fatal("Unsupported authenication type");
        }

        if (!rc) {
            ioputs(stream, "NO Incorrect authenication" CRLF);
            ioflush(stream);
            log_misc("Incorrect authenication for: %s", username);
            continue;
        }

        /* getpwnam() shouldn't fail if PAM authenication working properly */
        if ((pwd = getpwnam(username)) == NIL) {
            ioputs(stream, "NO Incorrect authenication" CRLF);
            ioflush(stream);
            log_misc("Incorrect authenication for: %s", username);
            continue;
        }

        if (pwd->pw_uid == 0) {
            ioputs(stream, "NO Root login disabled" CRLF);
            ioflush(stream);
            continue;
        }
        break;
    }

    setgid(pwd->pw_gid);        /* Change GID permanently for session */
    setuid(pwd->pw_uid);        /* Change UID permanently for session */

    if (chdir(pwd->pw_dir)) {
        ioputs(stream, "NO Can't select user home directory" CRLF);
        ioflush(stream);
        return (NIL);
    }

    ioputs(stream, "OK Session authenicated" CRLF);
    ioflush(stream);

    if (getuid() == 0)
        log_fatal("authenicate(): Ended up as root user!");

    *usernamep = strdup(username);
    return (T);
}

/* ====================================================================== */

/* authenicate_preauth() *************************************************
 *
 * Authenicate login session and drop priveledges.
 *     config:  Global configuration
 *     stream:  iostream connection to client
 *   usernamep: Used to return username of logged in user
 *
 * Returns: T => successful login. NIL otherwise
 ************************************************************************/

BOOL
authenicate_preauth(struct config * config,
                    struct iostream * stream, char **usernamep)
{
    struct passwd *pwd;

    if (getuid() == 0)
        log_fatal("authenicate_preauth(): called as root");

    if ((pwd = getpwuid(getuid())) == NIL)
        log_fatal("session(): getpwuid() failed");

    *usernamep = strdup(pwd->pw_name);

    ioputs(stream, "* ");
    if (config->filter_restricted)
        ioputs(stream, "[FILTER_RESTRICTED] ");

    ioprintf(stream, "PREAUTH Prayer ACCOUNTD server %s [user=%s]" CRLF,
             VERSION, *usernamep);
    ioflush(stream);

    if (chdir(pwd->pw_dir)) {
        ioputs(stream,
               "* Can't switch to PREAUTH user's home directory" CRLF);
        ioflush(stream);
        return (NIL);
    }
    return (T);
}
