/*
 * pam_imap
 *
 * PAM module to authenticate against an IMAP server
 * 
 * Copyright (C) Malcolm Beattie 1998, 1999
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * 20 Jan 1998  Initial version
 * 18 Aug 1999  Added support for %u in the hostname (Ray Miller,
 *              ray.miller@oucs.ox.ac.uk)
 * 
 * Required configuration option:
 *     host (%u in the value is replaced by the username, %% to escape %)
 * Optional configuration options:
 *     port, try_first_pass, use_first_pass, debug
 */ 

#define _POSIX_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#ifndef LINUX 
#include <security/pam_appl.h>
#endif /* LINUX */

#define PAM_SM_AUTH
#include <security/pam_modules.h>

#define IMAP_LOGIN_PROMPT "login: "
#define IMAP_PASSWORD_PROMPT "IMAP Password: "

#define IMAP_SERVICE "imap"
#define IMAP_FALLBACK_PORT 143

#define MAX_USERNAME 64
#define MAX_PASSWORD 128
/*
 * The following lists the characters that can't appear in an IMAP
 * quoted string, as per RFC 1203.
 */
#define BAD_QUOTED_STRING_CHARS "\"{\r\n%\\"

#define OR_RETURN_ERR ; if (err) return err

typedef enum {
    NO_FIRST_PASS, TRY_FIRST_PASS, USE_FIRST_PASS
} first_pass_t;

static void
pam_log(int err, char *format, ...)
{
    va_list args;

    va_start(args, format);
    openlog("PAM-IMAP", LOG_CONS|LOG_PID, LOG_AUTHPRIV);
    vsyslog(err, format, args);
    va_end(args);
}

static char *
safestrdup(const char *str)
{
    char *newstr;

    if (!str)
	return 0;
    newstr = malloc(strlen(str) + 1);
    if (newstr)
	strcpy(newstr, str);
    else
	pam_log(LOG_CRIT, "malloc failed");
    return newstr;
}

static int
get_address(char *hostname, int port, struct sockaddr_in *sinp)
{
    struct in_addr addr;

    if (!hostname) {
	pam_log(LOG_ERR, "No host argument supplied");
	return PAM_AUTH_ERR;
    }
    if (!inet_aton(hostname, &addr)) {
	struct hostent *he = gethostbyname(hostname);
	if (!he) {
	    pam_log(LOG_ERR, "hostname %s not found", hostname);
	    return PAM_AUTHINFO_UNAVAIL;
	}
	memcpy(&addr, he->h_addr_list[0], sizeof(addr));
    }
    if (port == -1) {
	struct servent *se = getservbyname(IMAP_SERVICE, "tcp");
	port = se ? ntohs(se->s_port) : IMAP_FALLBACK_PORT;
    }
    memset(sinp, 0, sizeof(*sinp));
    sinp->sin_family = AF_INET;
    sinp->sin_port = htons(port);
    sinp->sin_addr = addr;
    return PAM_SUCCESS;
}

static FILE *
server_connect(int sd, struct sockaddr_in *sinp)
{
    FILE *fp;

    if (sd == -1) {
	pam_log(LOG_ERR, "socket: %s", strerror(errno));
	return 0;
    }

    if (connect(sd, (struct sockaddr*)sinp, sizeof(*sinp)) == -1) {
	pam_log(LOG_ERR, "connect: %s", strerror(errno));
	return 0;
    }

    fp = fdopen(sd, "r+");
    if (!fp)
	pam_log(LOG_ERR, "fdopen: %s", strerror(errno));

    return fp;
}

static int
get_password(pam_handle_t *pamh, first_pass_t first_pass, char **password)
{
    int err;
    *password = 0;

    if (first_pass == TRY_FIRST_PASS || first_pass == USE_FIRST_PASS) {
	err = pam_get_item(pamh, PAM_AUTHTOK, (const void**)password);
	if (err != PAM_SUCCESS && first_pass == USE_FIRST_PASS)
	    return PAM_AUTH_ERR;
    }
    if (!*password) {
	struct pam_conv *conv;
	struct pam_message msg[1];
	const struct pam_message *msgp[1];
	struct pam_response *resp;

	err = pam_get_item(pamh, PAM_CONV, (const void**)&conv) OR_RETURN_ERR;

	msg[0].msg_style = PAM_PROMPT_ECHO_OFF;
	msg[0].msg = IMAP_PASSWORD_PROMPT;
	msgp[0] = msg;
	resp = 0;
	err = conv->conv(1, msgp, &resp, conv->appdata_ptr) OR_RETURN_ERR;

	err = pam_set_item(pamh, PAM_AUTHTOK, resp->resp);
	free(resp);
	if (err != PAM_SUCCESS)
	    return PAM_AUTHINFO_UNAVAIL;

	err = pam_get_item(pamh, PAM_AUTHTOK, (const void**)password);
	if (err != PAM_SUCCESS)
	    return PAM_AUTHINFO_UNAVAIL;
    }
    return PAM_SUCCESS;
}

static int
do_imap_login(FILE *fp, const char *username, char *password, int debug)
{
    char line[MAX_USERNAME + MAX_PASSWORD + 16];

    if (!username || strlen(username) > MAX_USERNAME
	|| !password || strlen(password) == 0 || strlen(password) > MAX_PASSWORD
	|| strstr(username, BAD_QUOTED_STRING_CHARS))
    {
	if (debug)
	    pam_log(LOG_DEBUG, "badly formed username or password");
	return PAM_AUTH_ERR;
    }
    /* get and ignore the banner */
    if (!fgets(line, sizeof(line), fp))
	return PAM_AUTHINFO_UNAVAIL;
    if (debug)
	pam_log(LOG_DEBUG, "got server banner: %s", line);

    /* Do the login command using a synchronising literal for the password */
    if (fprintf(fp, "1 login \"%s\" {%d}\r\n", username, strlen(password)) < 0
	|| fflush(fp))
    {
	pam_log(LOG_ERR, "failed to login command to IMAP server: %s",
		strerror(errno));
	return PAM_AUTHINFO_UNAVAIL;
    }
    if (debug)
	pam_log(LOG_DEBUG, "sent login request");

    /* Wait for response */
    if (!fgets(line, sizeof(line), fp))
	return PAM_AUTHINFO_UNAVAIL;

    /* Should be synchronisation go-ahead (or immediate username failure) */
    if (line[0] == '1') {
	/* It didn't even like our username */
	return PAM_AUTH_ERR;
    }
    else if (line[0] != '+') {
	pam_log(LOG_ERR, "bad server response whilst awaiting '+': %s", line);
	return PAM_AUTHINFO_UNAVAIL;
    }
    if (debug)
	pam_log(LOG_DEBUG, "username accepted, sending password");

    /* Send the password */
    if (fwrite(password, strlen(password), 1, fp) != 1
	|| fputs("\r\n", fp) == EOF	/* necessary to prod some servers */
	|| fflush(fp)) {
	pam_log(LOG_ERR, "failed to write password to IMAP server: %s",
		strerror(errno));
	return PAM_AUTHINFO_UNAVAIL;
    }

    if (debug)
	pam_log(LOG_DEBUG, "sent password, awaiting reply");
    /* Get the response, ignoring unsolicited data */
    do {
	if (!fgets(line, sizeof(line), fp))
	    return PAM_AUTHINFO_UNAVAIL;
	if (debug)
	    pam_log(LOG_DEBUG, "got reply line: %s", line);
    } while (line[0] == '*');

    if (line[0] != '1') {
	pam_log(LOG_ERR, "bad server response whilst awaiting tag: %s", line);
	return PAM_AUTHINFO_UNAVAIL;
    }

    if (!strncmp(line, "1 OK ", 5))
	return PAM_SUCCESS;

    if (!strncmp(line, "1 NO ", 5))
	return PAM_AUTH_ERR;

    pam_log(LOG_ERR, "bad server response for tagged reply : %s", line);
    return PAM_AUTHINFO_UNAVAIL;
}

PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    const char *username;
    char *password, *hostname;
    int sd, err, debug = 0, port = -1;
    first_pass_t first_pass = NO_FIRST_PASS;
    struct sockaddr_in sin;
    FILE *fp;

    for (; argc--; argv++) {
	if (!strncmp(*argv, "host=", 5)) {
	    hostname = safestrdup(*argv + 5);
	    if (!hostname)
		return PAM_AUTHINFO_UNAVAIL;
	}
	else if (!strncmp(*argv, "port=", 5))
	    port = atoi(*argv + 5);
	else if (!strcmp(*argv, "try_first_pass"))
	    first_pass = TRY_FIRST_PASS;
	else if (!strcmp(*argv, "use_first_pass"))
	    first_pass = USE_FIRST_PASS;
	else if (!strcmp(*argv, "debug"))
	    debug++;
	else
	    pam_log(LOG_ERR, "unknown option: %s", *argv);
    }

    if (debug) {
	pam_log(LOG_DEBUG, "host=%s port=%d first_pass=%d",
		hostname ? hostname : "(none)", port, first_pass);
    }
    err = pam_get_user(pamh, &username, IMAP_LOGIN_PROMPT) OR_RETURN_ERR;
    if (debug)
	pam_log(LOG_DEBUG, "username=%s", username);

    err = get_password(pamh, flags, &password) OR_RETURN_ERR;
    if (debug)
	pam_log(LOG_DEBUG, "got password OK");
	
    if ((flags & PAM_DISALLOW_NULL_AUTHTOK) && *password == '\0')
	return PAM_AUTH_ERR;

    err = get_address(hostname, port, &sin) OR_RETURN_ERR;
    if (debug) {
	pam_log(LOG_DEBUG, "host address is %s, port %u",
		inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
    }

    sd = socket(AF_INET, SOCK_STREAM, 0);
    fp = server_connect(sd, &sin);
    if (!fp)
	return PAM_AUTHINFO_UNAVAIL;

    err = do_imap_login(fp, username, password, debug);
    close(sd);		/* abrupt shutdown: maybe we should LOGOUT? */
    return err;
}

PAM_EXTERN int
pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    return PAM_SUCCESS;
}


#ifdef PAM_STATIC
struct pam_module _pam_imap_auth_modstruct = {
    "pam_imap_auth",
    pam_sm_authenticate,
    pam_sm_setcred,
    NULL,
    NULL,
    NULL,
    NULL,
};
#endif
