Download | Plain Text | Line Numbers


/*
   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.
 
   Derived from checkpassword-pam by Alexey Mahotkin <alexm@hsys.msk.ru> 2002-2004
   Modified and enhanced by Manuel Mausz 2017
*/
 
#define _DEFAULT_SOURCE 1
 
#include <errno.h>
#include <getopt.h>
#include <grp.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
 
#include "logging.h"
#include "dovecot-auth.h"
 
#define PACKAGE "checkpassword-dovecot"
#define VERSION "0.3"
 
/* command line options */
static int opt_dont_set_env = 0;
static int opt_dont_chdir = 0;
int opt_debug = 0;
int opt_use_syslog = 0;
 
#define AUTH_LOGIN_URI "unix:///run/dovecot/auth-client"
static const char *opt_auth_login_uri = AUTH_LOGIN_URI;
 
static const char* short_options = "a:dehHs:t:V";
 
enum { OPT_SYSLOG = 1 };
static struct option long_options[] = {
    { "auth",     required_argument, NULL, 'a' },
    { "debug",    no_argument,       NULL, 'd' },
    { "noenv",    no_argument,       NULL, 'e' },
    { "help",     no_argument,       NULL, 'h' },
    { "no-chdir", no_argument,       NULL, 'H' },
    { "service",  required_argument, NULL, 's' },
    { "syslog",   no_argument,       NULL, OPT_SYSLOG },
    { "timeout",  required_argument, NULL, 't' },
    { "version",  no_argument,       NULL, 'V' },
    { NULL, 0, NULL, 0 }
};
 
static const char *usage =
"Usage: " PACKAGE " [OPTION]... -- prog...\n"
"\n"
"Authenticate using Dovecot and the checkpassword protocol:\n"
"  https://wiki2.dovecot.org/Design/AuthProtocol\n"
"  http://cr.yp.to/checkpwd/interface.html\n"
"and optional run the program specified as 'prog'\n"
"\n"
"Options are:\n"
"  -a, --auth=URI\tURI to dovecot auth login socket\n"
"\t\t\te.g. unix:///path/to/socket or tcp://ip:port\n"
"\t\t\tdefault is " AUTH_LOGIN_URI "\n"
"  -d, --debug\t\tturn on debugging output\n"
"  -e, --noenv\t\tdo not set uid, gid, environment variables,\n"
"\t\t\tand home directory\n"
"  -H, --no-chdir\tdo not change to home directory\n"
"  -h, --help\t\tdisplay this help and exit\n"
"  -s, --service=SERVICE\tspecify service name to use\n"
"      --syslog\t\tlog to syslog instead of stderr\n"
"  -t, --timeout=SECS\tconnection timeout in seconds\n"
"  -V, --version\t\tdisplay version information and exit\n";
 
/* checkpassword exit code */
#define PROTOCOL_EXIT_SUCCESS  0
#define PROTOCOL_EXIT_INVALID  1   /* invalid credentials */
#define PROTOCOL_EXIT_FAILURE  2   /* unexpected failure */
#define PROTOCOL_EXIT_DISABLED 110 /* login disabled */
#define PROTOCOL_EXIT_TEMPFAIL 111
 
/* checkpassword protocol support */
#define PROTOCOL_FD 3
#define PROTOCOL_BACK_FD 4
#define PROTOCOL_LEN 512
static char upbuf[PROTOCOL_LEN];
 
/* pointers into upbuf[] */
static char *username = NULL;
static char *password = NULL;
 
#define V4MAPPREFIX "::ffff:"
static char *tcpserver_ipaddress(const char *name)
{
    char *remote_ip = getenv(name);
    if (!remote_ip)
        return NULL;
 
    /* check for ipv4 mapped ipv6 address */
    const char *tmp = getenv("PROTO");
    if (tmp && strcmp(tmp, "TCP6") == 0) {
        if (!strncmp(remote_ip, V4MAPPREFIX, strlen(V4MAPPREFIX)))
            remote_ip += strlen(V4MAPPREFIX);
    }
    return remote_ip;
}
 
int main(int argc, char *argv[])
{
    char *service_name = NULL;
    int exit_status = PROTOCOL_EXIT_FAILURE, auth_timeout = 10;
    struct auth_reply reply = {0};
 
    log_init(argv[0]);
 
    /* process command line options */
    opterr = 0;
    while (1) {
        int option_index = 0;
        int c = getopt_long(argc, argv, short_options, long_options,
            &option_index);
 
        if (c == -1)
            break;
 
        switch (c) {
            case OPT_SYSLOG:
                opt_use_syslog = 1;
                break;
 
            case 'a':
                opt_auth_login_uri = strdup(optarg);
                break;
 
            case 'd':
                opt_debug = 1;
                break;
 
            case 'e':
                opt_dont_set_env = 1;
                break;
 
            case 'h':
                puts(usage);
                exit(EXIT_SUCCESS);
 
            case 'H':
                opt_dont_chdir = 1;
                break;
 
            case 's':
                service_name = strdup(optarg);
                break;
 
            case 't':
                auth_timeout = atoi(optarg);
                break;
 
            case 'V':
                puts(PACKAGE " " VERSION);
                exit(EXIT_SUCCESS);
 
            case '?':
                log_error("Invalid command line, see --help");
                exit(PROTOCOL_EXIT_FAILURE);
        }
    }
 
    if (service_name == NULL) {
        log_error("Missing service name. Use --service=SERVICE");
        goto out;
    }
 
    log_close();
    log_init(service_name);
 
    /* read the username/password */
    FILE *protocol = fdopen(PROTOCOL_FD, "r");
    if (protocol == NULL) {
        log_error("Error opening fd %d: %s", PROTOCOL_FD, strerror(errno));
        goto out;
    }
    log_debug("Reading username and password");
    size_t uplen = fread(upbuf, 1, PROTOCOL_LEN, protocol);
    (void)fclose(protocol);
    if (uplen == 0) {
        log_error("Checkpassword protocol failure: zero bytes read");
        goto out;
    }
 
    /* extract username */
    size_t i = 0;
    username = upbuf + i;
    while (upbuf[i++]) {
        if (i >= uplen) {
            log_error("Checkpassword protocol failure: username not provided");
            goto out;
        }
    }
    log_debug("Username '%s'", username);
 
    /* extract password */
    password = upbuf + i;
    while (upbuf[i++]) {
        if (i >= uplen) {
            log_error("Checkpassword protocol failure: password not provided");
            goto out;
        }
    }
    log_debug("Password read successfully");
 
    FILE *auth = auth_connect(opt_auth_login_uri, auth_timeout);
    if (auth == NULL) {
        goto out;
    }
 
    char *tmp, *buf = auth_begin();
    buf = auth_add_parameter(buf, "service", service_name);
    if ((tmp = tcpserver_ipaddress("TCPLOCALIP")) != NULL) {
        buf = auth_add_parameter(buf, "lip", tmp);
        if ((tmp = tcpserver_ipaddress("TCPREMOTEIP")) != NULL)
            buf = auth_add_parameter(buf, "rip", tmp);
        if ((tmp = getenv("TCPLOCALPORT")) != NULL)
            buf = auth_add_parameter(buf, "lport", tmp);
        if ((tmp = getenv("TCPREMOTEPORT")) != NULL)
            buf = auth_add_parameter(buf, "rport", tmp);
        if (getenv("SMTPSECURED"))
            buf = auth_add_parameter(buf, "secured", NULL);
    }
 
    int res = auth_login(auth, buf, username, password, &reply);
    (void)fclose(auth);
    if (res == AUTH_FAIL) {
        log_debug("Login failed: Invalid credentials");
        exit_status = PROTOCOL_EXIT_INVALID;
    }
    else if (res == AUTH_TEMP) {
        log_debug("Login failed: Temporary failure");
        exit_status = PROTOCOL_EXIT_TEMPFAIL;
    }
    else if (res == AUTH_NOLOGIN) {
        log_debug("Login failed: Login has been disabled");
        exit_status = PROTOCOL_EXIT_DISABLED;
    }
    else if (res == AUTH_OK) {
        log_debug("Login successful");
        exit_status = PROTOCOL_EXIT_SUCCESS;
    }
 
    bool username_changed = false;
    if (reply.username && strlen(reply.username) > 0 && strcmp(username, reply.username) != 0) {
        log_debug("Username got changed to '%s'", reply.username);
        username_changed = true;
        username = reply.username;
    }
 
    if (reply.reason)
        log_debug("Got optional reason: %s", reply.reason);
 
    if (fcntl(PROTOCOL_BACK_FD, F_GETFD) != -1 || errno != EBADF) {
        log_debug("Sending back login parameters");
        if (username_changed) {
            (void)write(PROTOCOL_BACK_FD, "USER=", 5);
            (void)write(PROTOCOL_BACK_FD, username, strlen(username));
            (void)write(PROTOCOL_BACK_FD, "\0", 1);
        }
        if (reply.reason) {
            (void)write(PROTOCOL_BACK_FD, "REASON=", 7);
            (void)write(PROTOCOL_BACK_FD, reply.reason, strlen(reply.reason));
            (void)write(PROTOCOL_BACK_FD, "\0", 1);
        }
        if (close(PROTOCOL_BACK_FD) == -1) {
            log_error("close() error: %s", strerror(errno));
            exit_status = PROTOCOL_EXIT_FAILURE;
        }
    }
 
    if (exit_status != PROTOCOL_EXIT_SUCCESS)
        goto out;
 
    if (opt_dont_set_env)
        goto execute_program; /* skip setting up process environment */
 
    /* switch to proper uid/gid/groups */
    struct passwd *pw = getpwnam(username);
    if (!pw) {
        if (opt_debug)
            log_error("Error getting information about %s from /etc/passwd: %s", username, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    /* set supplementary groups */
    if (initgroups(username, pw->pw_gid) == -1) {
        log_error("Error setting supplementary groups for user %s: %s", username, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    /* set gid */
    if (setgid(pw->pw_gid) == -1) {
        log_error("setgid(%d) error: %s", pw->pw_gid, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    /* set uid */
    if (setuid(pw->pw_uid) == -1) {
        log_error("setuid(%d) error: %s", pw->pw_uid, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    if (!opt_dont_chdir) {
        /* switch to user home directory */
        if (chdir(pw->pw_dir) == -1) {
            log_error("Error changing directory %s: %s", pw->pw_dir, strerror(errno));
            exit_status = PROTOCOL_EXIT_FAILURE;
            goto out;
        }
    }
 
    /* set $USER */
    if (setenv("USER", username, 1) == -1) {
        log_error("Error setting $USER to %s: %s", username, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    /* set $HOME */
    if (setenv("HOME", pw->pw_dir, 1) == -1) {
        log_error("Error setting $HOME to %s: %s", pw->pw_dir, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    /* set $SHELL */
    if (setenv("SHELL", pw->pw_shell, 1) == -1) {
        log_error("Error setting $SHELL to %s: %s", pw->pw_shell, strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
 execute_program:
    /* execute the program, if any */
    if (optind < argc) {
        auth_reply_free(&reply);
        log_debug("Executing %s", argv[optind]);
        log_close();
 
        execvp(argv[optind], argv + optind);
 
        log_init(service_name);
        log_error("Cannot exec(%s): %s\n", argv[optind], strerror(errno));
        exit_status = PROTOCOL_EXIT_FAILURE;
        goto out;
    }
 
    /* if no program was provided in command line, simply exit */
 out:
    auth_reply_free(&reply);
    log_debug("Exiting with status %d", exit_status);
    log_close();
    exit(exit_status);
}