/* Copyright (c) 2000 Ingo Luetkebohle, 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.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR OTHER CODE CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * v 1.1 from 07. July 2002
 *
 * mod_auth_pam:
 *  basic authentication against pluggable authentication module lib
 *
 * The authoritative homepage for this module is at
 *	http://pam.sourceforge.net/
 *
 * For assistance and support, contact the pam-list
 *	https://listman.redhat.com/mailman/listinfo/pam-list
 * or the Help forum on SourceForge.
 *
 * Written by Ingo Luetkebohle, based upon mod_auth.c, with contributions
 * from Fredrik Ohrn (group performance), Dirk-Willem van Gulik (licensing
 * consultation) and Michael Johnson (example group implementation).
 *
 * Thanks to Andrew Morgan and the rest of the Linux-PAM developers who
 * provided invaluable development help and ideas.
 *
 * Changes:
 *   07-Jul-02: License changed to allow redistribution
 *		Performance improvement for group lookup
		(many thanks to Fredrik Ohrn for the patch)
 *   06-Dec-99: Special casing for Solaris 2.6 added
 *              Added versioning message to headers
 *   14-Feb-99: Cleaned up the configuration directives and named them
 *		in a more straightforward way
 *
 *		incorporated getugroups patch from Klaus Wissmann <kw@aw.net>
 *		to look up supplementary groups from /etc/group
 *
 *   22-Jan-99: incorporated changes from Pavel Kankovsky <kan@dcit.cz>
 *		Enabler configuration directive now called AuthPAM_Enabled
 *		Updated for Apache 1.3.x
 *   25-Sep-98: replaced pwdb groups routine with standard C
 *   19-Oct-97: made module fall through (if configured to do so),
 * 		even when a user of the given name is found in the PAM
 *		databases
 *   17-Apr-97: fixed segfault that occured when Apache couldn't look
 *              up the remote host name
 *              removed annoying compiler warnings
 *
 *   05-Apr-97: made fall-through configurable with AuthPAM_Authorative
 *
 *   25-Mar-97: added support for transparent fall-through to other auth
 *              modules with configurable fail delays
 *              added acct_mgmt hook
 *              added group support (through libpwdb)
 *
 * usage information:
 *
 * new-style (DSO)
 *
 *	compile with
 *		apxs -c -lpam mod_auth_pam.c
 *
 * 	install with
 *		apxs -i -a mod_auth_pam.so
 *
 *	configure PAM by adding
 *		/etc/pam.d/httpd
 *	with the appropriate pam modules (for starters, just copy over ftp)
 *
 * old-style (from Apache 1.2.x)
 *
 * 	1. Configuration:
 * 	   Module pam_auth_module mod_auth_pam.o
 *  	   EXTRA_LIBS+= -lpam -ldl
 *
 * 	2. Add an auth and an account entry for service type "httpd"
 *   	   to your PAM configuration
 *
 *
 * configuration directives:
 * AuthFailDelay <msecs>
 *                              number of mili-seconds to wait after a
 *                              failed authentication attempt. this is
 *                              a minimum value and may have been
 *                              increased by other pam apps.
 *                              defaults to 0
 *				REQUIRES lib_pam SUPPORT
 *
 * AuthPAM_Enabled on|off
 *                              If on, mod_auth_pam will try to authenticate
 *				the user.
 *				If off, mod_auth_pam will DECLINE immediately
 *				instead of trying to authenticate the user.
 *				This will make Apache try other modules.
 *				Defaults to on
 *
 * AuthPAM_FallThrough
 *				If on, makes mod_auth_pam DECLINE if it can't
 *				the username, giving other modules a chance.
 *				Please note that, if it DOES find the username,
 *				and the password doesn't match, it will NOT
 *				fall through but return "access denied" instead
 *				Defaults to off
 *
 * AuthPAM_Authorative on|off   DEPRECATED
 */

#include <unistd.h>

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"

#include <security/pam_appl.h>

/* change this to 0 on RedHat 4.x */
#define PAM_STRE_NEEDS_PAMH 1
#define VERSION "1.0a"

module pam_auth_module;

static const char
    *pam_servicename = "httpd", *valid_user = "valid-user";

typedef struct {
    char *name, *pw;
}      auth_pam_userinfo;

/*
 * Solaris 2.6.x has a broken conversation function and needs this
 * as a global variable
 * I refused to pollute code for other platforms with this,
 * so all Solaris 2.6 specific stuff is if'd like the following
 */
#if SOLARIS2 == 260
auth_pam_userinfo *global_userinfo;
#endif

/*
 * the pam_strerror function has different parameters in early PAM
 * versions
 */
#ifndef PAM_STRE_NEEDS_PAMH
#define compat_pam_strerror(pamh, res) pam_strerror(res)
#else
#define compat_pam_strerror(pamh, res) pam_strerror(pamh, res)
#endif

/*
 * configuration directive handling
 */

typedef struct {
    int
        fail_delay,		/* fail delay in ms -- needs library support */
        fall_through,		/* 1 to DECLINE instead of AUTH_REQUIRED if
				 * we can't find the username (defaults to 0) */
        enabled;		/* 1 to use mod_auth_pam, 0 otherwise
				 * (defaults to 1) */
}      auth_pam_dir_config;

void auth_pam_init(server_rec * s, pool * p)
{
    ap_add_version_component("mod_auth_pam/" VERSION);
}

static
void *create_auth_pam_dir_config(pool * p, char *dummy)
{
    auth_pam_dir_config *new =
    (auth_pam_dir_config *) ap_palloc(p, sizeof(auth_pam_dir_config));

    new->fail_delay = 0;	/* 0 ms */
    new->fall_through = 0;	/* off */
    new->enabled = 1;		/* on */
    return new;
}

static char *auth_fail_delay(cmd_parms * cmd, auth_pam_dir_config * config, char *secs)
{
    if (secs)
	config->fail_delay = atoi(secs);
    return NULL;
}

static char *auth_fall_through(cmd_parms * cmd, auth_pam_dir_config * config, int arg)
{
    config->fall_through = arg;
    return NULL;
}

static char *auth_enable(cmd_parms * cmd, auth_pam_dir_config * config, int arg)
{
    config->enabled = arg;
    return NULL;
}

static command_rec auth_pam_cmds[] = {
    {"AuthFailDelay", (const char *(*) ()) auth_fail_delay, 0, OR_AUTHCFG, TAKE1,
	"number of micro seconds to wait after failed authentication attempt. "
        "default is 0 "},
    {"AuthPAM_Authorative", (const char *(*) ()) auth_fall_through, NULL, OR_AUTHCFG, FLAG,
    "no longer in use -- see AuthPAM_FallThrough instead"},
    {"AuthPAM_FallThrough", (const char *(*) ()) auth_fall_through, NULL, OR_AUTHCFG, FLAG,
	"on|off - determines if other authentication methods are attempted if this "
        "one fails; default is off "},
    {"AuthPAM_Enabled", (const char *(*) ()) auth_enable, NULL, OR_AUTHCFG, FLAG,
	"on|off - determines if PAM authentication is enabled; default is on"
    },
    {NULL}
};

/*
 * auth_pam_talker: supply authentication information to PAM when asked
 *
 * Assumptions:
 *   A password is asked for by requesting input without echoing
 *   A username is asked for by requesting input _with_ echoing
 *
 */
static
int auth_pam_talker(int num_msg,
		        const struct pam_message ** msg,
		        struct pam_response ** resp,
		        void *appdata_ptr)
{
    unsigned short i = 0;
    auth_pam_userinfo *userinfo = (auth_pam_userinfo *) appdata_ptr;
    struct pam_response *response = 0;

#if SOLARIS2 == 260
    if (!userinfo)
	userinfo = global_userinfo;
    /* fprintf(stderr,"%s : %s", userinfo->name, userinfo->pw); */
#endif

    /* parameter sanity checking */
    if (!resp || !msg || !userinfo)
	return PAM_CONV_ERR;

    /* allocate memory to store response */
    response = malloc(num_msg * sizeof(struct pam_response));
    if (!response)
	return PAM_CONV_ERR;

    /* copy values */
    for (i = 0; i < num_msg; i++) {
	/* initialize to safe values */
	response[i].resp_retcode = 0;
	response[i].resp = 0;

	/* select response based on requested output style */
	switch (msg[i]->msg_style) {
	case PAM_PROMPT_ECHO_ON:
	    /* on memory allocation failure, auth fails */
	    response[i].resp = strdup(userinfo->name);
	    break;
	case PAM_PROMPT_ECHO_OFF:
	    response[i].resp = strdup(userinfo->pw);
	    break;
	default:
	    if (response)
		free(response);
	    return PAM_CONV_ERR;
	}
    }
    /* everything okay, set PAM response values */
    *resp = response;
    return PAM_SUCCESS;
}

/*
 * These functions return 0 if client is OK, and proper error status
 * if not... either AUTH_REQUIRED, if we made a check, and it failed, or
 * SERVER_ERROR, if things are so totally confused that we couldn't
 * figure out how to tell if the client is authorized or not.
 *
 * If they return DECLINED, and all other modules also decline, that's
 * treated by the server core as a configuration error, logged and
 * reported as such.
 */

/*
 * Determine user ID, and check if it really is that user
 */
static
int pam_auth_basic_user(request_rec * r)
{
    int res = 0;
    /* mod_auth_pam specific */
    auth_pam_userinfo userinfo = {NULL, NULL};
    auth_pam_dir_config *conf = (auth_pam_dir_config *)
    ap_get_module_config(r->per_dir_config, &pam_auth_module);
    /* PAM specific  */
    struct pam_conv conv_info = {&auth_pam_talker, (void *) &userinfo};
    pam_handle_t *pamh = NULL;

#if SOLARIS2 == 260
    global_userinfo = &userinfo;
#endif

    /* enabled? */
    if (!conf->enabled)
	return DECLINED;

    /* read sent pw */
    if ((res = ap_get_basic_auth_pw(r, (const char **) &(userinfo.pw))))
	return res;

    /* this is only set after get_basic_auth_pw was called */
    userinfo.name = r->connection->user;

    /* initialize pam */
    if ((res = pam_start(pam_servicename,
			 userinfo.name,
			 &conv_info,
			 &pamh)) != PAM_SUCCESS) {

	ap_log_reason((char *) compat_pam_strerror(pamh, res), r->uri, r);
	return DECLINED;
    }

    /* set fail delay */
#ifdef PAM_FAIL_DELAY
    pam_fail_delay(pamh, conf->fail_delay);
#endif

    /* set remote user information */
    /*
     * this seems to cause segfaults in lots of cases -- disabled for
     * now pam_set_item(pamh, PAM_USER, userinfo.name);
     * pam_set_item(pamh, PAM_RHOST, get_remote_host(r->connection,
     * conf, REMOTE_NAME));
     */

    /* try to authenticate user, log error on failure */
    if ((res = pam_authenticate(pamh, PAM_DISALLOW_NULL_AUTHTOK)) !=
	PAM_SUCCESS) {
	ap_log_reason((char *) compat_pam_strerror(pamh, res), r->uri, r);
	if (conf->fall_through && (res == PAM_USER_UNKNOWN)) {
	    /*
	     * we don't know about the user, but other auth modules
	     * might do
	     */
	    pam_end(pamh, PAM_SUCCESS);
	    return DECLINED;
	}
	else {
	    pam_end(pamh, PAM_SUCCESS);
	    ap_note_basic_auth_failure(r);
	    return AUTH_REQUIRED;
	}			/* endif fall_through */
    }				/* endif authenticate */

    /* check that the account is healthy */
    if ((res = pam_acct_mgmt(pamh, PAM_DISALLOW_NULL_AUTHTOK)) != PAM_SUCCESS) {
	ap_log_reason((char *) compat_pam_strerror(pamh, res), r->uri, r);
	pam_end(pamh, PAM_SUCCESS);
	return AUTH_REQUIRED;
    }

    pam_end(pamh, PAM_SUCCESS);
    return OK;
}

/*
 * Look if that user (as authenticated above) is allowed here
 */

static
int pam_check_auth(request_rec * r)
{
    register int i = 0;
    char method_restricted = 0, *line = 0, *word = 0;
    auth_pam_dir_config *conf = (auth_pam_dir_config *)
    ap_get_module_config(r->per_dir_config, &pam_auth_module);

    char *p_group = NULL;

    /* check for allowed users/group */
    const array_header *reqs_arr = ap_requires(r);
    require_line *reqs = 0;

    /* enabled? */
    if (!conf->enabled)
	return DECLINED;

    /* if any valid user suffices return success */
    if (!reqs_arr)
	return (OK);

    /* otherwise */
    reqs = (require_line *) reqs_arr->elts;

    /* loop over requirement lines */
    for (i = 0; i < reqs_arr->nelts; i++) {
	/* if method of this requirement matches current method */
	if (reqs[i].method_mask & (1 << r->method_number)) {
	    method_restricted = 1;

	    line = reqs[i].requirement;
	    word = ap_getword(r->pool, (const char **) &line, ' ');

	    /* if any user is ok */
	    if (!strcmp(word, valid_user))
		return OK;

	    /* loop over line */
	    if (!strcmp(word, "user")) {
		while (*line) {
		    word = ap_getword_conf(r->pool, (const char **) &line);
		    /* if username matches remote username */
		    if (!strcmp(r->connection->user, word))
			/* return success */
			return OK;
		}
	    }			/* end if user */
	    else if (!strcmp(word, "group")) {
		struct group *grent;
		char **members;

		if (!p_group) {
		    struct passwd *pwent;

		    if ((pwent = getpwnam(r->connection->user)) && (grent = getgrgid(pwent->pw_gid)))
			p_group = grent->gr_name;
		}

		while (*line) {
		    word = ap_getword_conf(r->pool, (const char **) &line);

		    if (p_group && !strcmp(p_group, word))
			return OK;

		    if ((grent = getgrnam(word)) && grent->gr_mem) {
			members = grent->gr_mem;

			while (*members) {
			    if (!strcmp(*members, word))
				return OK;

			    members++;
			}
		    }
		}
	    }			/* end if group */
	}
    }

    if (!method_restricted)
	return OK;

    ap_note_basic_auth_failure(r);
    return AUTH_REQUIRED;
}

module pam_auth_module = {
    STANDARD_MODULE_STUFF,
    auth_pam_init,		/* initializer */
    create_auth_pam_dir_config,	/* dir config creater */
    NULL,			/* dir merger --- default is to override */
    NULL,			/* server config */
    NULL,			/* merge server config */
    auth_pam_cmds,		/* command table */
    NULL,			/* handlers */
    NULL,			/* filename translation */
    pam_auth_basic_user,	/* check_user_id */
    pam_check_auth,		/* check auth */
    NULL,			/* check access */
    NULL,			/* type_checker */
    NULL,			/* fixups */
    NULL			/* logger */
};
