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