view src/auth/db-checkpassword.c @ 15187:02451e967a06

Renamed network.[ch] to net.[ch]. The function prefixes already started with net_ instead of network_. And icecap wants to use network.h for other purpose. :)
author Timo Sirainen <tss@iki.fi>
date Wed, 03 Oct 2012 18:17:26 +0300
parents 96fd2c3bf932
children cc4472f02f70
line wrap: on
line source

/* Copyright (c) 2004-2012 Dovecot authors, see the included COPYING file */

#include "auth-common.h"

#if defined(PASSDB_CHECKPASSWORD) || defined(USERDB_CHECKPASSWORD)

#include "lib-signals.h"
#include "buffer.h"
#include "str.h"
#include "ioloop.h"
#include "hash.h"
#include "execv-const.h"
#include "env-util.h"
#include "safe-memset.h"
#include "strescape.h"
#include "child-wait.h"
#include "var-expand.h"
#include "db-checkpassword.h"

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

#define CHECKPASSWORD_MAX_REQUEST_LEN 512

struct chkpw_auth_request {
	struct db_checkpassword *db;
	struct auth_request *request;
	char *auth_password;

	db_checkpassword_callback_t *callback;
	void (*request_callback)();

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

	string_t *input_buf;
	unsigned int output_pos, output_len;

	int exit_status;
	unsigned int exited:1;
};

struct db_checkpassword {
	char *checkpassword_path, *checkpassword_reply_path;

	HASH_TABLE(void *, struct chkpw_auth_request *) clients;
	struct child_wait *child_wait;
};

static void env_put_extra_fields(const char *extra_fields)
{
	const char *const *tmp;
	const char *key, *p;

	for (tmp = t_strsplit_tab(extra_fields); *tmp != NULL; tmp++) {
		key = t_str_ucase(t_strcut(*tmp, '='));
		p = strchr(*tmp, '=');
		if (p == NULL)
			env_put(t_strconcat(key, "=1", NULL));
		else
			env_put(t_strconcat(key, p, NULL));
	}
}

static void checkpassword_request_close(struct chkpw_auth_request *request)
{
	if (request->io_in != NULL)
		io_remove(&request->io_in);
	if (request->io_out != NULL)
		io_remove(&request->io_out);

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

static void checkpassword_request_free(struct chkpw_auth_request **_request)
{
	struct chkpw_auth_request *request = *_request;

	*_request = NULL;

	if (!request->exited) {
		hash_table_remove(request->db->clients,
				  POINTER_CAST(request->pid));
		child_wait_remove_pid(request->db->child_wait, request->pid);
	}
	checkpassword_request_close(request);

	if (request->auth_password != NULL) {
		safe_memset(request->auth_password, 0,
			    strlen(request->auth_password));
		i_free(request->auth_password);
	}
	auth_request_unref(&request->request);
	str_free(&request->input_buf);
	i_free(request);
}

static void checkpassword_finish(struct chkpw_auth_request **_request,
				 enum db_checkpassword_status status)
{
	struct chkpw_auth_request *request = *_request;
	const char *const *extra_fields;

	*_request = NULL;

	extra_fields = t_strsplit_tabescaped(str_c(request->input_buf));
	request->callback(request->request, status, extra_fields,
			  request->request_callback);
	checkpassword_request_free(&request);
}

static void checkpassword_internal_failure(struct chkpw_auth_request **request)
{
	checkpassword_finish(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE);
}

static void
checkpassword_request_finish_auth(struct chkpw_auth_request *request)
{
	switch (request->exit_status) {
	/* vpopmail exit codes: */
	case 3:		/* password fail / vpopmail user not found */
	case 12: 	/* null user name given */
	case 13:	/* null password given */
	case 15:	/* user has no password */
	case 20:	/* invalid user/domain characters */
	case 21:	/* system user not found */
	case 22:	/* system user shadow entry not found */
	case 23:	/* system password fail */

	/* standard checkpassword exit codes: */
	case 1:
		/* (1 is additionally defined in vpopmail for
		   "pop/smtp/webmal/ imap/access denied") */
		auth_request_log_info(request->request, "checkpassword",
				      "Login failed (status=%d)",
				      request->exit_status);
		checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE);
		break;
	case 0:
		if (request->input_buf->used == 0) {
			auth_request_log_error(request->request, "checkpassword",
					       "Received no input");
			checkpassword_internal_failure(&request);
			break;
		}
		checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK);
		break;
	case 2:
		/* checkpassword is called with wrong parameters? unlikely */
		auth_request_log_error(request->request, "checkpassword",
			"Child %s exited with status 2 (tried to use "
			"userdb-only checkpassword program for passdb?)",
			dec2str(request->pid));
		checkpassword_internal_failure(&request);
		break;
	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_internal_failure(&request);
		break;
	}
}

static void
checkpassword_request_finish_lookup(struct chkpw_auth_request *request)
{
	switch (request->exit_status) {
	case 3:
		/* User does not exist. */
		auth_request_log_info(request->request, "userdb-checkpassword",
				      "User unknown");
		checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_FAILURE);
		break;
	case 2:
		/* This is intentionally not 0. checkpassword-reply exits with
		   2 on success when AUTHORIZED is set. */
		if (request->input_buf->used == 0) {
			auth_request_log_error(request->request, "checkpassword",
					       "Received no input");
			checkpassword_internal_failure(&request);
			break;
		}
		checkpassword_finish(&request, DB_CHECKPASSWORD_STATUS_OK);
		break;
	default:
		/* whatever error... */
		auth_request_log_error(request->request, "userdb-checkpassword",
			"Child %s exited with status %d",
			dec2str(request->pid), request->exit_status);
		checkpassword_internal_failure(&request);
		break;
	}
}

static void
checkpassword_request_half_finish(struct chkpw_auth_request *request)
{
	/* the process must have exited, and the input fd must have closed */
	if (!request->exited || request->fd_in != -1)
		return;

	if (request->auth_password != NULL)
		checkpassword_request_finish_auth(request);
	else
		checkpassword_request_finish_lookup(request);
}

static void env_put_auth_vars(struct auth_request *request)
{
	const struct var_expand_table *tab;
	unsigned int i;

	tab = auth_request_get_var_expand_table(request, NULL);
	for (i = 0; tab[i].key != '\0' || tab[i].long_key != NULL; i++) {
		if (tab[i].long_key != NULL && tab[i].value != NULL) {
			env_put(t_strdup_printf("AUTH_%s=%s",
						t_str_ucase(tab[i].long_key),
						tab[i].value));
		}
	}
}

static void checkpassword_setup_env(struct auth_request *request)
{
	/* Besides passing the standard username and password in a
	   pipe, also pass some other possibly interesting information
	   via environment. Use UCSPI names for local/remote IPs. */
	env_put("PROTO=TCP"); /* UCSPI */
	env_put(t_strconcat("SERVICE=", request->service, NULL));
	if (request->local_ip.family != 0) {
		env_put(t_strconcat("TCPLOCALIP=",
				    net_ip2addr(&request->local_ip), NULL));
		/* FIXME: for backwards compatibility only,
		   remove some day */
		env_put(t_strconcat("LOCAL_IP=",
				    net_ip2addr(&request->local_ip), NULL));
	}
	if (request->remote_ip.family != 0) {
		env_put(t_strconcat("TCPREMOTEIP=",
				    net_ip2addr(&request->remote_ip), NULL));
		/* FIXME: for backwards compatibility only,
		   remove some day */
		env_put(t_strconcat("REMOTE_IP=",
				    net_ip2addr(&request->remote_ip), NULL));
	}
	if (request->local_port != 0) {
		env_put(t_strdup_printf("TCPLOCALPORT=%u",
					request->local_port));
	}
	if (request->remote_port != 0) {
		env_put(t_strdup_printf("TCPREMOTEPORT=%u",
					request->remote_port));
	}
	if (request->master_user != NULL) {
		env_put(t_strconcat("MASTER_USER=",
				    request->master_user, NULL));
	}
	if (request->extra_fields != NULL) {
		const char *fields =
			auth_stream_reply_export(request->extra_fields);

		/* extra fields could come from master db */
		env_put_extra_fields(fields);
	}
	env_put_auth_vars(request);
}

static const char *
checkpassword_get_cmd(struct auth_request *request, const char *args,
		      const char *checkpassword_reply_path)
{
	string_t *str;

	str = t_str_new(256);
	var_expand(str, args,
		   auth_request_get_var_expand_table(request, NULL));
	return t_strconcat(str_c(str), " ", checkpassword_reply_path, NULL);
}

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

	ret = read(request->fd_in, buf, sizeof(buf));
	if (ret > 0) {
		str_append_n(request->input_buf, buf, ret);
		return;
	}

	if (ret < 0) {
		auth_request_log_error(request->request,
			"checkpassword", "read() failed: %m");
		checkpassword_internal_failure(&request);
	} else if (strchr(str_c(request->input_buf), '\n') != NULL) {
		auth_request_log_error(request->request, "checkpassword",
				       "LF characters in checkpassword reply");
		checkpassword_internal_failure(&request);
	} else {
		auth_request_log_debug(request->request, "checkpassword",
			"Received input: %s", str_c(request->input_buf));
		checkpassword_request_close(request);
		checkpassword_request_half_finish(request);
	}
}

static void checkpassword_child_output(struct chkpw_auth_request *request)
{
	/* 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 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(),
				    CHECKPASSWORD_MAX_REQUEST_LEN);
	buffer_append(buf, auth_request->user, strlen(auth_request->user)+1);
	if (request->auth_password != NULL) {
		buffer_append(buf, request->auth_password,
			      strlen(request->auth_password)+1);
	} else {
		buffer_append_c(buf, '\0');
	}
	buffer_append_c(buf, '\0');
	data = buffer_get_data(buf, &size);

	i_assert(size == request->output_len);
	/* already checked this */
	i_assert(size <= CHECKPASSWORD_MAX_REQUEST_LEN);

	ret = write(request->fd_out, data + request->output_pos,
		    size - request->output_pos);
	if (ret <= 0) {
		if (ret < 0) {
			auth_request_log_error(request->request,
				"checkpassword", "write() failed: %m");
		} else {
			auth_request_log_error(request->request,
				"checkpassword", "write() returned 0");
		}
		checkpassword_internal_failure(&request);
		return;
	}

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

	/* finished sending the data */
	io_remove(&request->io_out);

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

static void ATTR_NORETURN
checkpassword_exec(struct db_checkpassword *db, struct auth_request *request,
		   int fd_in, int fd_out, bool authenticate)
{
	const char *cmd, *const *args;

	/* fd 3 is used to send the username+password for the script
	   fd 4 is used to communicate with checkpassword-reply */
	if (dup2(fd_out, 3) < 0 || dup2(fd_in, 4) < 0) {
		auth_request_log_error(request, "checkpassword",
				       "dup2() failed: %m");
		exit(111);
	}

	if (!authenticate) {
		/* We want to retrieve passdb/userdb data and don't do
		   authorization, so we need to signalize the
		   checkpassword program that the password shall be
		   ignored by setting AUTHORIZED.  This needs a
		   special checkpassword program which knows how to
		   handle this. */
		env_put("AUTHORIZED=1");
		if (request->credentials_scheme != NULL) {
			/* passdb credentials lookup */
			env_put("CREDENTIALS_LOOKUP=1");
			env_put(t_strdup_printf("SCHEME=%s",
						request->credentials_scheme));
		}
	}
	checkpassword_setup_env(request);
	cmd = checkpassword_get_cmd(request, db->checkpassword_path,
				    db->checkpassword_reply_path);
	auth_request_log_debug(request, "checkpassword", "execute: %s", cmd);

	/* very simple argument splitting. */
	args = t_strsplit(cmd, " ");
	execv_const(args[0], args);
}

static void sigchld_handler(const struct child_wait_status *status,
			    struct db_checkpassword *db)
{
	struct chkpw_auth_request *request = 
		hash_table_lookup(db->clients, POINTER_CAST(status->pid));

	i_assert(request != NULL);

	hash_table_remove(db->clients, POINTER_CAST(status->pid));
	request->exited = TRUE;

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

		auth_request_log_debug(request->request,
				       "checkpassword", "exit_status=%d",
				       request->exit_status);
		checkpassword_request_half_finish(request);
	} else {
		/* shouldn't happen */
		auth_request_log_debug(request->request, "checkpassword",
				       "Child %s exited with status=%d",
				       dec2str(status->pid), status->status);
		checkpassword_internal_failure(&request);
	}
}

void db_checkpassword_call(struct db_checkpassword *db,
			   struct auth_request *request,
			   const char *auth_password,
			   db_checkpassword_callback_t *callback,
			   void (*request_callback)())
{
	struct chkpw_auth_request *chkpw_auth_request;
	unsigned int output_len;
	int fd_in[2], fd_out[2];
	pid_t pid;

	/* <username> \0 <password> \0 timestamp \0 */
	output_len = strlen(request->user) + 3;
	if (auth_password != NULL)
		output_len += strlen(auth_password);
	if (output_len > CHECKPASSWORD_MAX_REQUEST_LEN) {
		auth_request_log_info(request, "checkpassword",
			"Username+password combination too long (%u bytes)",
			output_len);
		callback(request, DB_CHECKPASSWORD_STATUS_FAILURE,
			 NULL, request_callback);
		return;
	}

	fd_in[0] = -1;
	if (pipe(fd_in) < 0 || pipe(fd_out) < 0) {
		auth_request_log_error(request, "checkpassword",
				       "pipe() failed: %m");
		if (fd_in[0] != -1) {
			i_close_fd(&fd_in[0]);
			i_close_fd(&fd_in[1]);
		}
		callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE,
			 NULL, request_callback);
		return;
	}

	pid = fork();
	if (pid == -1) {
		auth_request_log_error(request, "checkpassword",
				       "fork() failed: %m");
		i_close_fd(&fd_in[0]);
		i_close_fd(&fd_in[1]);
		i_close_fd(&fd_out[0]);
		i_close_fd(&fd_out[1]);
		callback(request, DB_CHECKPASSWORD_STATUS_INTERNAL_FAILURE,
			 NULL, request_callback);
		return;
	}

	if (pid == 0) {
		/* child */
		i_close_fd(&fd_in[0]);
		i_close_fd(&fd_out[1]);
		checkpassword_exec(db, request, fd_in[1], fd_out[0],
				   auth_password != NULL);
		/* 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->db = db;
	chkpw_auth_request->pid = pid;
	chkpw_auth_request->fd_in = fd_in[0];
	chkpw_auth_request->fd_out = fd_out[1];
	chkpw_auth_request->auth_password = i_strdup(auth_password);
	chkpw_auth_request->request = request;
	chkpw_auth_request->output_len = output_len;
	chkpw_auth_request->input_buf = str_new(default_pool, 256);
	chkpw_auth_request->callback = callback;
	chkpw_auth_request->request_callback = request_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_table_insert(db->clients, POINTER_CAST(pid), chkpw_auth_request);
	child_wait_add_pid(db->child_wait, pid);
}

struct db_checkpassword *
db_checkpassword_init(const char *checkpassword_path,
		      const char *checkpassword_reply_path)
{
	struct db_checkpassword *db;

	db = i_new(struct db_checkpassword, 1);
	db->checkpassword_path = i_strdup(checkpassword_path);
	db->checkpassword_reply_path = i_strdup(checkpassword_reply_path);
	hash_table_create_direct(&db->clients, default_pool, 0);
	db->child_wait =
		child_wait_new_with_pid((pid_t)-1, sigchld_handler, db);
	return db;
}

void db_checkpassword_deinit(struct db_checkpassword **_db)
{
	struct db_checkpassword *db = *_db;
	struct hash_iterate_context *iter;
	void *key;
	struct chkpw_auth_request *request;

	*_db = NULL;

	iter = hash_table_iterate_init(db->clients);
	while (hash_table_iterate(iter, db->clients, &key, &request))
		checkpassword_internal_failure(&request);
	hash_table_iterate_deinit(&iter);

	child_wait_free(&db->child_wait);
	hash_table_destroy(&db->clients);
	i_free(db->checkpassword_reply_path);
	i_free(db->checkpassword_path);
	i_free(db);
}

#endif