view src/auth/passdb-checkpassword.c @ 4032:7140f09b464f HEAD

Allow checkpassword program to send extra parameters to Dovecot. EXTRA environment contains a list of environment variables that are passed to Dovecot.
author Timo Sirainen <timo.sirainen@movial.fi>
date Mon, 20 Feb 2006 15:39:37 +0200
parents 357175e274b4
children c58659bf7ca5
line wrap: on
line source

/* Copyright (C) 2004-2005 Timo Sirainen */

#include "common.h"

#ifdef PASSDB_CHECKPASSWORD

#include "buffer.h"
#include "str.h"
#include "ioloop.h"
#include "hash.h"
#include "env-util.h"
#include "passdb.h"
#include "safe-memset.h"

#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

struct checkpassword_passdb_module {
	struct passdb_module module;

	const char *checkpassword_path, *checkpassword_reply_path;
	struct hash_table *clients;
	struct timeout *to_wait;
};

struct chkpw_auth_request {
	int fd_out, fd_in;
	struct io *io_out, *io_in;
	pid_t pid;

	string_t *input_buf;
	char *password;
	unsigned int write_pos;

	struct auth_request *request;
	verify_plain_callback_t *callback;

	int exit_status;
	unsigned int exited:1;
};

static void checkpassword_request_close(struct chkpw_auth_request *request)
{
	if (request->fd_in != -1) {
		if (close(request->fd_in) < 0)
			i_error("checkpassword: close() failed: %m");
		request->fd_in = -1;
	}
	if (request->io_in != NULL)
		io_remove(&request->io_in);

	if (request->io_out != NULL)
		io_remove(&request->io_out);
	if (request->fd_out != -1) {
		if (close(request->fd_out) < 0)
			i_error("checkpassword: close() failed: %m");
	}
}

static void checkpassword_request_finish(struct chkpw_auth_request *request,
					 enum passdb_result result)
{
	struct passdb_module *_module = request->request->passdb->passdb;
	struct checkpassword_passdb_module *module =
		(struct checkpassword_passdb_module *)_module;

	hash_remove(module->clients, POINTER_CAST(request->pid));

	if (strchr(str_c(request->input_buf), '\n') != NULL) {
		auth_request_log_error(request->request, "checkpassword",
				       "LF characters in checkpassword reply");
		result = PASSDB_RESULT_INTERNAL_FAILURE;
	}

	if (result == PASSDB_RESULT_OK) {
		request->request->extra_fields =
			auth_stream_reply_init(request->request);
		auth_stream_reply_import(request->request->extra_fields,
					 str_c(request->input_buf));
	}

	request->callback(result, request->request);
	auth_request_unref(&request->request);

        checkpassword_request_close(request);

	if (request->input_buf != NULL)
		str_free(&request->input_buf);

	safe_memset(request->password, 0, strlen(request->password));
	i_free(request->password);
	i_free(request);
}

static void
checkpassword_request_half_finish(struct chkpw_auth_request *request)
{
	if (!request->exited || request->fd_in != -1)
		return;

	switch (request->exit_status) {
	case 0:
		if (request->input_buf != NULL) {
			checkpassword_request_finish(request, PASSDB_RESULT_OK);
			break;
		}
		/* missing input - fall through */
	case 1:
		auth_request_log_info(request->request, "checkpassword",
				      "Unknown user");
		checkpassword_request_finish(request,
					     PASSDB_RESULT_USER_UNKNOWN);
		break;
	case 2:
		/* checkpassword is called with wrong
		   parameters? unlikely */
	case 111:
		/* temporary problem, treat as internal error */
	default:
		/* whatever error.. */
		auth_request_log_error(request->request, "checkpassword",
			"Child %s exited with status %d",
			dec2str(request->pid), request->exit_status);
		checkpassword_request_finish(request,
					     PASSDB_RESULT_INTERNAL_FAILURE);
		break;
	}
}

static void wait_timeout(void *context)
{
	struct checkpassword_passdb_module *module = context;
	struct chkpw_auth_request *request;
	int status;
	pid_t pid;

	/* FIXME: if we ever do some other kind of forking, this needs fixing */
	while ((pid = waitpid(-1, &status, WNOHANG)) != 0) {
		if (pid == -1) {
			if (errno == ECHILD)
				timeout_remove(&module->to_wait);
			else if (errno != EINTR)
				i_error("waitpid() failed: %m");
			return;
		}

		request = hash_lookup(module->clients, POINTER_CAST(pid));
		if (request == NULL) {
			/* unknown child finished */
			if (WIFSIGNALED(status)) {
				i_error("checkpassword: Unknown child %s died "
					"with signal %d", dec2str(pid),
					WTERMSIG(status));
			}
			continue;
		}

		if (WIFSIGNALED(status)) {
			i_error("checkpassword: Child %s died with signal %d",
				dec2str(pid), WTERMSIG(status));
		} else if (WIFEXITED(status)) {
			auth_request_log_debug(request->request,
				"checkpassword", "exit_status=%d",
				request->exit_status);

			request->exited = TRUE;
			request->exit_status = WEXITSTATUS(status);
			checkpassword_request_half_finish(request);
			request = NULL;
		} else {
			/* shouldn't happen */
			auth_request_log_debug(request->request,
				"checkpassword", "Child exited with status=%d",
				status);
		}

		if (request != NULL) {
			checkpassword_request_finish(request,
				PASSDB_RESULT_INTERNAL_FAILURE);
		}
	}
}

static void
checkpassword_verify_plain_child(struct auth_request *request,
				 struct checkpassword_passdb_module *module,
				 int fd_in, int fd_out)
{
	const char *cmd, *const *args;

	if (dup2(fd_out, 3) < 0 || dup2(fd_in, 4) < 0) {
		auth_request_log_error(request, "checkpassword",
				       "dup2() failed: %m");
	} else {
		/* very simple argument splitting. */
		cmd = t_strconcat(module->checkpassword_path, " ",
				  module->checkpassword_reply_path, NULL);
		args = t_strsplit(cmd, " ");

		/* Besides passing the standard username and password in a
		   pipe, also pass some other possibly interesting information
		   via environment. */
		env_put(t_strconcat("SERVICE=", request->service, NULL));
		if (request->local_ip.family != 0) {
			env_put(t_strconcat("LOCAL_IP=",
					    net_ip2addr(&request->local_ip),
					    NULL));
		}
		if (request->remote_ip.family != 0) {
			env_put(t_strconcat("REMOTE_IP=",
					    net_ip2addr(&request->remote_ip),
					    NULL));
		}
		if (request->master_user != NULL) {
			env_put(t_strconcat("MASTER_USER=",
					    request->master_user, NULL));
		}

		auth_request_log_debug(request, "checkpassword",
				       "Executed: %s", cmd);

		execv(args[0], (char **)args);
		auth_request_log_error(request, "checkpassword",
				       "execv(%s) failed: %m", args[0]);
	}
	exit(2);
}

static void checkpassword_child_input(void *context)
{
	struct chkpw_auth_request *request = context;
	unsigned char buf[1024];
	ssize_t ret;

	ret = read(request->fd_in, buf, sizeof(buf));
	if (ret <= 0) {
		if (ret < 0) {
			auth_request_log_error(request->request,
				"checkpassword", "read() failed: %m");
		}

		auth_request_log_debug(request->request, "checkpassword",
				       "Received no input");
		checkpassword_request_close(request);
		checkpassword_request_half_finish(request);
	} else {
		if (request->input_buf == NULL)
			request->input_buf = str_new(default_pool, 512);
		str_append_n(request->input_buf, buf, ret);

		auth_request_log_debug(request->request, "checkpassword",
			"Received input: %s", str_c(request->input_buf));
	}
}

static void checkpassword_child_output(void *context)
{
	/* Send: username \0 password \0 timestamp \0.
	   Must be 512 bytes or less. The "timestamp" parameter is actually
	   useful only for APOP authentication. We don't support it, so
	   keep it empty */
	struct chkpw_auth_request *request = context;
	struct auth_request *auth_request = request->request;
	buffer_t *buf;
	const unsigned char *data;
	size_t size;
	ssize_t ret;

	buf = buffer_create_dynamic(pool_datastack_create(), 512+1);
	buffer_append(buf, auth_request->user, strlen(auth_request->user)+1);
	buffer_append(buf, request->password, strlen(request->password)+1);
	buffer_append_c(buf, '\0');
	data = buffer_get_data(buf, &size);

	if (size > 512) {
		auth_request_log_error(request->request, "checkpassword",
			"output larger than 512 bytes: %"PRIuSIZE_T, size);
		checkpassword_request_finish(request,
					     PASSDB_RESULT_INTERNAL_FAILURE);
		return;
	}

	ret = write(request->fd_out, data + request->write_pos,
		    size - request->write_pos);
	if (ret <= 0) {
		if (ret < 0) {
			auth_request_log_error(request->request,
				"checkpassword", "write() failed: %m");
		}
		checkpassword_request_finish(request,
					     PASSDB_RESULT_INTERNAL_FAILURE);
		return;
	}

	request->write_pos += ret;
	if (request->write_pos < size)
		return;

	io_remove(&request->io_out);

	if (close(request->fd_out) < 0)
		i_error("checkpassword: close() failed: %m");
        request->fd_out = -1;
}

static void
checkpassword_verify_plain(struct auth_request *request, const char *password,
			   verify_plain_callback_t *callback)
{
	struct passdb_module *_module = request->passdb->passdb;
	struct checkpassword_passdb_module *module =
		(struct checkpassword_passdb_module *)_module;
	struct chkpw_auth_request *chkpw_auth_request;
	int fd_in[2], fd_out[2];
	pid_t pid;

	fd_in[0] = -1;
	if (pipe(fd_in) < 0 || pipe(fd_out) < 0) {
		auth_request_log_error(request, "checkpassword",
				       "pipe() failed: %m");
		callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
		if (fd_in[0] != -1) {
			(void)close(fd_in[0]);
			(void)close(fd_in[1]);
		}
		return;
	}

	pid = fork();
	if (pid == -1) {
		auth_request_log_error(request, "checkpassword",
				       "fork() failed: %m");
		callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
		(void)close(fd_in[0]);
		(void)close(fd_in[1]);
		(void)close(fd_out[0]);
		(void)close(fd_out[1]);
		return;
	}

	if (pid == 0) {
		(void)close(fd_in[0]);
		(void)close(fd_out[1]);
		checkpassword_verify_plain_child(request, module,
						 fd_in[1], fd_out[0]);
		/* not reached */
	}

	if (close(fd_in[1]) < 0) {
		auth_request_log_error(request, "checkpassword",
				       "close(fd_in[1]) failed: %m");
	}
	if (close(fd_out[0]) < 0) {
		auth_request_log_error(request, "checkpassword",
				       "close(fd_out[0]) failed: %m");
	}

	auth_request_ref(request);
	chkpw_auth_request = i_new(struct chkpw_auth_request, 1);
	chkpw_auth_request->fd_in = fd_in[0];
	chkpw_auth_request->fd_out = fd_out[1];
	chkpw_auth_request->pid = pid;
	chkpw_auth_request->password = i_strdup(password);
	chkpw_auth_request->request = request;
	chkpw_auth_request->callback = callback;

	chkpw_auth_request->io_in =
		io_add(fd_in[0], IO_READ, checkpassword_child_input,
		       chkpw_auth_request);
	chkpw_auth_request->io_out =
		io_add(fd_out[1], IO_WRITE, checkpassword_child_output,
		       chkpw_auth_request);

	hash_insert(module->clients, POINTER_CAST(pid), chkpw_auth_request);

	if (module->to_wait == NULL) {
		/* FIXME: we could use SIGCHLD */
		module->to_wait = timeout_add(100, wait_timeout, module);
	}
}

static struct passdb_module *
checkpassword_preinit(struct auth_passdb *auth_passdb, const char *args)
{
	struct checkpassword_passdb_module *module;

	module = p_new(auth_passdb->auth->pool,
		       struct checkpassword_passdb_module, 1);
	module->checkpassword_path = p_strdup(auth_passdb->auth->pool, args);
	module->checkpassword_reply_path =
		PKG_LIBEXECDIR"/checkpassword-reply";

	module->clients =
		hash_create(default_pool, default_pool, 0, NULL, NULL);
	return &module->module;
}

static void checkpassword_deinit(struct passdb_module *_module)
{
	struct checkpassword_passdb_module *module =
		(struct checkpassword_passdb_module *)_module;
	struct hash_iterate_context *iter;
	void *key, *value;

	iter = hash_iterate_init(module->clients);
	while (hash_iterate(iter, &key, &value)) {
		checkpassword_request_finish(value,
					     PASSDB_RESULT_INTERNAL_FAILURE);
	}
	hash_iterate_deinit(iter);
	hash_destroy(module->clients);

	if (module->to_wait != NULL)
		timeout_remove(&module->to_wait);
}

struct passdb_module_interface passdb_checkpassword = {
	"checkpassword",

	checkpassword_preinit,
	NULL,
	checkpassword_deinit,

	checkpassword_verify_plain,
	NULL
};

#endif