/*
 * session.c - PPP session control.
 *
 * Copyright (c) 2007 Diego Rivera. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. The name(s) of the authors of this software must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission.
 *
 * 3. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Paul Mackerras
 *     <paulus@ozlabs.org>".
 *
 * THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
 * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Derived from auth.c, which is:
 *
 * Copyright (c) 1984-2000 Carnegie Mellon University. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Office of Technology Transfer
 *      Carnegie Mellon University
 *      5000 Forbes Avenue
 *      Pittsburgh, PA  15213-3890
 *      (412) 268-4387, fax: (412) 268-7395
 *      tech-transfer@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pwd.h>

#ifdef HAVE_CRYPT_H
#include <crypt.h>
#endif

#ifdef HAVE_SHADOW_H
#include <shadow.h>
#endif

#include <time.h>
#include <utmp.h>
#include <fcntl.h>
#include <unistd.h>
#include "pppd-private.h"
#include "session.h"

#ifdef PPP_WITH_PAM
#include <security/pam_appl.h>
#endif /* #ifdef PPP_WITH_PAM */

#define SET_MSG(var, msg) if (var != NULL) { var[0] = msg; }
#define COPY_STRING(s) ((s) ? strdup(s) : NULL)

#define SUCCESS_MSG "Session started successfully"
#define ABORT_MSG "Session can't be started without a username"
#define SERVICE_NAME "ppp"

#define SESSION_FAILED  0
#define SESSION_OK      1

/* We have successfully started a session */
static bool logged_in = 0;

#ifdef PPP_WITH_PAM
/*
 * Static variables used to communicate between the conversation function
 * and the server_login function
 */
static const char *PAM_username;
static const char *PAM_password;
static int   PAM_session = 0;
static pam_handle_t *pamh = NULL;

/* PAM conversation function
 * Here we assume (for now, at least) that echo on means login name, and
 * echo off means password.
 */

static int conversation (int num_msg,
    const struct pam_message **msg,
    struct pam_response **resp, void *appdata_ptr)
{
    int replies = 0;
    struct pam_response *reply = NULL;

    reply = malloc(sizeof(struct pam_response) * num_msg);
    if (!reply) return PAM_CONV_ERR;

    for (replies = 0; replies < num_msg; replies++) {
        switch (msg[replies]->msg_style) {
            case PAM_PROMPT_ECHO_ON:
                reply[replies].resp_retcode = PAM_SUCCESS;
                reply[replies].resp = COPY_STRING(PAM_username);
                /* PAM frees resp */
                break;
            case PAM_PROMPT_ECHO_OFF:
                reply[replies].resp_retcode = PAM_SUCCESS;
                reply[replies].resp = COPY_STRING(PAM_password);
                /* PAM frees resp */
                break;
            case PAM_TEXT_INFO:
                /* fall through */
            case PAM_ERROR_MSG:
                /* ignore it, but pam still wants a NULL response... */
                reply[replies].resp_retcode = PAM_SUCCESS;
                reply[replies].resp = NULL;
                break;
            default:
                /* Must be an error of some sort... */
                free (reply);
                return PAM_CONV_ERR;
        }
    }
    *resp = reply;
    return PAM_SUCCESS;
}

static struct pam_conv pam_conv_data = {
    &conversation,
    NULL
};
#endif /* #ifdef PPP_WITH_PAM */

int
session_start(const int flags, const char *user, const char *passwd, const char *ttyName, char **msg)
{
#ifdef PPP_WITH_PAM
    bool ok = 1;
    const char *usr;
    int pam_error;
    bool try_session = 0;
#else /* #ifdef PPP_WITH_PAM */
    struct passwd *pw;
    char *cbuf;
#ifdef HAVE_SHADOW_H
    struct spwd *spwd;
    struct spwd *getspnam();
    long now = 0;
#endif /* #ifdef HAVE_SHADOW_H */
#endif /* #ifdef PPP_WITH_PAM */

    SET_MSG(msg, SUCCESS_MSG);

    /* If no verification is requested, then simply return an OK */
    if (!(SESS_ALL & flags)) {
        return SESSION_OK;
    }

    if (user == NULL) {
       SET_MSG(msg, ABORT_MSG);
       return SESSION_FAILED;
    }

#ifdef PPP_WITH_PAM
    /* Find the '\\' in the username */
    /* This needs to be fixed to support different username schemes */
    if ((usr = strchr(user, '\\')) == NULL)
	usr = user;
    else
	usr++;

    PAM_session = 0;
    PAM_username = usr;
    PAM_password = passwd;

    dbglog("Initializing PAM (%d) for user %s", flags, usr);
    pam_error = pam_start (SERVICE_NAME, usr, &pam_conv_data, &pamh);
    dbglog("---> PAM INIT Result = %d", pam_error);
    ok = (pam_error == PAM_SUCCESS);

    if (ok) {
        ok = (pam_set_item(pamh, PAM_TTY, ttyName) == PAM_SUCCESS) &&
	    (pam_set_item(pamh, PAM_RHOST, ifname) == PAM_SUCCESS);
    }

    if (ok && (SESS_AUTH & flags)) {
        dbglog("Attempting PAM authentication");
        pam_error = pam_authenticate (pamh, PAM_SILENT);
        if (pam_error == PAM_SUCCESS) {
            /* PAM auth was OK */
            dbglog("PAM Authentication OK for %s", user);
        } else {
            /* No matter the reason, we fail because we're authenticating */
            ok = 0;
            if (pam_error == PAM_USER_UNKNOWN) {
                dbglog("User unknown, failing PAM authentication");
                SET_MSG(msg, "User unknown - cannot authenticate via PAM");
            } else {
                /* Any other error means authentication was bad */
                dbglog("PAM Authentication failed: %d: %s", pam_error,
		       pam_strerror(pamh, pam_error));
                SET_MSG(msg, (char *) pam_strerror (pamh, pam_error));
            }
        }
    }

    if (ok && (SESS_ACCT & flags)) {
        dbglog("Attempting PAM account checks");
        pam_error = pam_acct_mgmt (pamh, PAM_SILENT);
        if (pam_error == PAM_SUCCESS) {
            /*
	     * PAM account was OK, set the flag which indicates that we should
	     * try to perform the session checks.
	     */
            try_session = 1;
            dbglog("PAM Account OK for %s", user);
        } else {
            /*
	     * If the account checks fail, then we should not try to perform
	     * the session check, because they don't make sense.
	     */
            try_session = 0;
            if (pam_error == PAM_USER_UNKNOWN) {
                /*
		 * We're checking the account, so it's ok to not have one
		 * because the user might come from the secrets files, or some
		 * other plugin.
		 */
                dbglog("User unknown, ignoring PAM restrictions");
                SET_MSG(msg, "User unknown - ignoring PAM restrictions");
            } else {
                /* Any other error means session is rejected */
                ok = 0;
                dbglog("PAM Account checks failed: %d: %s", pam_error,
		       pam_strerror(pamh, pam_error));
                SET_MSG(msg, (char *) pam_strerror (pamh, pam_error));
            }
        }
    }

    if (ok && try_session && (SESS_ACCT & flags)) {
        /* Only open a session if the user's account was found */
        pam_error = pam_open_session (pamh, PAM_SILENT);
        if (pam_error == PAM_SUCCESS) {
            dbglog("PAM Session opened for user %s", user);
            PAM_session = 1;
        } else {
            dbglog("PAM Session denied for user %s", user);
            SET_MSG(msg, (char *) pam_strerror (pamh, pam_error));
            ok = 0;
        }
    }

    /* This is needed because apparently the PAM stuff closes the log */
    reopen_log();

    /* If our PAM checks have already failed, then we must return a failure */
    if (!ok) return SESSION_FAILED;

#else /* #ifdef PPP_WITH_PAM */

/*
 * Use the non-PAM methods directly.  'pw' will remain NULL if the user
 * has not been authenticated using local UNIX system services.
 */

    pw = NULL;
    if ((SESS_AUTH & flags)) {
	pw = getpwnam(user);

	endpwent();
	/*
	 * Here, we bail if we have no user account, because there is nothing
	 * to verify against.
	 */
	if (pw == NULL)
	    return SESSION_FAILED;

#ifdef HAVE_SHADOW_H

	spwd = getspnam(user);
	endspent();

	/*
	 * If there is no shadow entry for the user, then we can't verify the
	 * account.
	 */
	if (spwd == NULL)
	    return SESSION_FAILED;

	/*
	 * We check validity all the time, because if the password has expired,
	 * then clearly we should not authenticate against it (if we're being
	 * called for authentication only).  Thus, in this particular instance,
	 * there is no real difference between using the AUTH, SESS or ACCT
	 * flags, or combinations thereof.
	 */
	now = time(NULL) / 86400L;
	if ((spwd->sp_expire > 0 && now >= spwd->sp_expire)
	    || ((spwd->sp_max >= 0 && spwd->sp_max < 10000)
	    && spwd->sp_lstchg >= 0
	    && now >= spwd->sp_lstchg + spwd->sp_max)) {
	    warn("Password for %s has expired", user);
	    return SESSION_FAILED;
	}

	/* We have a valid shadow entry, keep the password */
	pw->pw_passwd = spwd->sp_pwdp;

#endif /* #ifdef HAVE_SHADOW_H */

	/*
	 * If no passwd, don't let them login if we're authenticating.
	 */
        if (pw->pw_passwd == NULL || strlen(pw->pw_passwd) < 2)
            return SESSION_FAILED;
#ifdef HAVE_CRYPT_H
	cbuf = crypt(passwd, pw->pw_passwd);
	if (!cbuf || strcmp(cbuf, pw->pw_passwd) != 0)
#endif
            return SESSION_FAILED;
    }

#endif /* #ifdef PPP_WITH_PAM */

    /*
     * Write a wtmp entry for this user.
     */

    if (SESS_ACCT & flags) {
	if (strncmp(ttyName, "/dev/", 5) == 0)
	    ttyName += 5;
	logwtmp(ttyName, user, ifname); /* Add wtmp login entry */
	logged_in = 1;

#if defined(_PATH_LASTLOG) && !defined(PPP_WITH_PAM)
	/*
	 * Enter the user in lastlog only if he has been authenticated using
	 * local system services.  If he has not, then we don't know what his
	 * UID might be, and lastlog is indexed by UID.
	 */
	if (pw != NULL) {
            struct lastlog ll;
            int fd;
	    time_t tnow;

            if ((fd = open(_PATH_LASTLOG, O_RDWR, 0)) >= 0) {
                (void)lseek(fd, (off_t)(pw->pw_uid * sizeof(ll)), SEEK_SET);
                memset((void *)&ll, 0, sizeof(ll));
		(void)time(&tnow);
                ll.ll_time = tnow;
                strlcpy(ll.ll_line, ttyName, sizeof(ll.ll_line));
                strlcpy(ll.ll_host, ifname, sizeof(ll.ll_host));
                (void)write(fd, (char *)&ll, sizeof(ll));
                (void)close(fd);
            }
	}
#endif /* _PATH_LASTLOG and not PPP_WITH_PAM */
	info("user %s logged in on tty %s intf %s", user, ttyName, ifname);
    }

    return SESSION_OK;
}

/*
 * session_end - Logout the user.
 */
void
session_end(const char* ttyName)
{
#ifdef PPP_WITH_PAM
    int pam_error = PAM_SUCCESS;

    if (pamh != NULL) {
        if (PAM_session) pam_error = pam_close_session (pamh, PAM_SILENT);
        PAM_session = 0;
        pam_end (pamh, pam_error);
        pamh = NULL;
	/* Apparently the pam stuff does closelog(). */
	reopen_log();
    }
#endif
    if (logged_in) {
	if (strncmp(ttyName, "/dev/", 5) == 0)
	    ttyName += 5;
	logwtmp(ttyName, "", ""); /* Wipe out utmp logout entry */
	logged_in = 0;
    }
}
