/* $Cambridge: hermes/src/prayer/accountd/authenticate.c,v 1.6 2009/09/28 10:17:27 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"

#ifdef BSD4_4
#define HAVE_SHADOW 0
#else
#define HAVE_SHADOW 1
#endif

#include <pwd.h>
#if HAVE_SHADOW
#include <shadow.h>
#endif

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

#ifdef RAVEN_ENABLE
static int
check_domain(char *a, char *b)
{
    int len = strlen(b);

    if (strncmp(a, b, len) != 0)
        return(0);

    /* Three possible terminators to host part of URL */
    if (a[len] == '/' || a[len] == ':' || a[len] == '\0')
        return(1);

    return(0);
}

static int
check_url(struct raven_wls_response *wls)
{
    char *s = wls->url;
    char *t;

    if (!strncmp(s, "https://", strlen("https://")))
        s += strlen("https://");
    else if (!strncmp(s, "http://", strlen("http://")))
        s += strlen("http://");
    else
        return(0);

    if ((t = strchr(s, '.')) && check_domain(t+1, "hermes.cam.ac.uk"))
        return(1);

    if (check_domain(s, "magenta.csi.cam.ac.uk") ||
        check_domain(s, "cyan.csi.cam.ac.uk"))
        return(1);

    return(0);
}

static int checkpassword_raven(struct config *config,
                               char *username, char *password)
{
    BOOL response = NIL;
    struct raven_wls_response *wls = raven_wls_response_parse
        ((char *)password, config->raven_key_path, NULL);

    if (!wls)
        return(NIL);

    if (!strcmp(wls->status, "200") &&
        !strcmp(wls->principal, username) &&
        check_url(wls) &&
        raven_wls_response_check_timestamp(wls,
                                           RAVEN_WLS_TICKET_MAXSKEW,
                                           RAVEN_WLS_TICKET_LIFETIME))
                                               
        response = T;

    raven_wls_response_free(wls);
    return(response);
}
#else
static int checkpassword_raven(struct config *config,
                               char *username, char *password)
{
    return(0);
}
#endif
/* ====================================================================== */

/* Code for authenticating 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 authentication methods, including
 * PAM if PAM support configured in ../Config */

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

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

#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 authentication selected but support not compiled in");
    return (NIL);
}
#endif

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

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

#if HAVE_SHADOW
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);
}
#endif

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;
    }

    endpwent();
    return (rc);
}

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

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

/* authenticate() *********************************************************
 *
 * Authenticate 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
authenticate(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("authenticate(): 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 authenticate if root */
        if (config->raven_enable && (strlen(password) > 256)) {
            rc = checkpassword_raven(config, username, password);
        } else 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;
#if HAVE_SHADOW
        case AUTHTYPE_SHADOW:
            rc = checkpassword_spnam(username, password, 0);
            break;
        case AUTHTYPE_SHADOW_MD5:
            rc = checkpassword_spnam(username, password, 1);
            break;
#endif
        default:
            log_fatal("Unsupported authentication type");
        }

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

        /* getpwnam() shouldn't fail if PAM authentication working properly */
        if ((pwd = getpwnam(username)) == NIL) {
            ioputs(stream, "NO Incorrect authentication" CRLF);
            ioflush(stream);
            log_misc("Incorrect authentication 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 authenticated" CRLF);
    ioflush(stream);

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

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

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

/* authenticate_preauth() ************************************************
 *
 * Authenticate 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
authenticate_preauth(struct config * config,
                     struct iostream * stream, char **usernamep)
{
    struct passwd *pwd;

    if (getuid() == 0)
        log_fatal("authenticate_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);
}
