/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* run-passwd.c: this file is part of users-admin, a gnome-system-tools frontend
 * for user administration.
 *
 * Copyright (C) 2002 Diego Gonzalez
 * Copyright (C) 2006 Johannes H. Jensen
 * Copyright (C) 2010 Milan Bouchet-Valat
 *
 * Written by: Diego Gonzalez <diego@pemas.net>
 * Modified by: Johannes H. Jensen <joh@deworks.net>,
 *              Milan Bouchet-Valat <nalimilan@club.fr>.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Most of this code originally comes from gnome-about-me-password.c,
 * from gnome-control-center.
 */

#include <config.h>
#include <glib/gi18n.h>
#include <oobs/oobs.h>

#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>

#if __sun
#include <sys/types.h>
#include <signal.h>
#endif

#include "gst.h"
#include "run-passwd.h"

/* Passwd states */
typedef enum {
	PASSWD_STATE_NONE,		/* Passwd is not asking for anything */
	PASSWD_STATE_AUTH,		/* Passwd is asking for our current password */
	PASSWD_STATE_NEW,		/* Passwd is asking for our new password */
	PASSWD_STATE_RETYPE,		/* Passwd is asking for our retyped new password */
	PASSWD_STATE_ERR		/* Passwd reported an error but has not yet exited */
} PasswdState;

struct PasswdHandler {
	GtkBuilder  *ui;

	const char *current_password;
	const char *new_password;
	const char *retyped_password;

	/* Communication with the passwd program */
	GPid backend_pid;

	GIOChannel *backend_stdin;
	GIOChannel *backend_stdout;

	GQueue *backend_stdin_queue;		/* Write queue to backend_stdin */

	/* GMainLoop IDs */
	guint backend_child_watch_id;		/* g_child_watch_add (PID) */
	guint backend_stdout_watch_id;		/* g_io_add_watch (stdout) */

	/* State of the passwd program */
	PasswdState backend_state;
	gboolean    changing_password;

	PasswdCallback auth_cb;
	gpointer       auth_cb_data;

	PasswdCallback chpasswd_cb;
	gpointer       chpasswd_cb_data;
};

/* Buffer size for backend output */
#define BUFSIZE 64


static GQuark
passwd_error_quark (void)
{
	static GQuark q = 0;

	if (q == 0) {
		q = g_quark_from_static_string("passwd_error");
	}

	return q;
}

/* Error handling */
#define PASSWD_ERROR (passwd_error_quark ())


extern GstTool *tool;

static void
stop_passwd (PasswdHandler *passwd_handler);

static void
free_passwd_resources (PasswdHandler *passwd_handler);

static gboolean
io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler);


/*
 * Spawning and closing of backend {{
 */

/* Child watcher */
static void
child_watch_cb (GPid pid, gint status, PasswdHandler *passwd_handler)
{
	if (WIFEXITED (status)) {
		if (WEXITSTATUS (status) >= 255) {
			g_warning ("Child exited unexpectedly");
		}
	}

	free_passwd_resources (passwd_handler);
}

/* Spawn passwd backend
 * Returns: TRUE on success, FALSE otherwise and sets error appropriately */
static gboolean
spawn_passwd (PasswdHandler *passwd_handler, GError **error)
{
	gchar	*argv[2];
	gchar	*envp[1];
	gint	my_stdin, my_stdout, my_stderr;

	argv[0] = "/usr/bin/passwd";	/* Is it safe to rely on a hard-coded path? */
	argv[1] = NULL;

	envp[0] = NULL;			/* If we pass an empty array as the environment,
					 * will the childs environment be empty, and the
					 * locales set to the C default? From the manual:
					 * "If envp is NULL, the child inherits its
					 * parent'senvironment."
					 * If I'm wrong here, we somehow have to set
					 * the locales here.
					 */

	if (!g_spawn_async_with_pipes (NULL,				/* Working directory */
	                               argv,				/* Argument vector */
	                               envp,				/* Environment */
	                               G_SPAWN_DO_NOT_REAP_CHILD,	/* Flags */
	                               NULL,				/* Child setup */
	                               NULL,				/* Data to child setup */
	                               &passwd_handler->backend_pid,	/* PID */
	                               &my_stdin,			/* Stdin */
	                               &my_stdout,			/* Stdout */
	                               &my_stderr,			/* Stderr */
	                               error)) {			/* GError */

		/* An error occured */
		free_passwd_resources (passwd_handler);

		return FALSE;
	}

	/* 2>&1 */
	if (dup2 (my_stderr, my_stdout) == -1) {
		/* Failed! */
		g_set_error_literal (error,
		                     PASSWD_ERROR,
		                     PASSWD_ERROR_BACKEND,
		                     strerror (errno));

		/* Clean up */
		stop_passwd (passwd_handler);

		return FALSE;
	}

	/* Open IO Channels */
	passwd_handler->backend_stdin = g_io_channel_unix_new (my_stdin);
	passwd_handler->backend_stdout = g_io_channel_unix_new (my_stdout);

	/* Set raw encoding */
	/* Set nonblocking mode */
	if (g_io_channel_set_encoding (passwd_handler->backend_stdin, NULL, error) != G_IO_STATUS_NORMAL ||
		g_io_channel_set_encoding (passwd_handler->backend_stdout, NULL, error) != G_IO_STATUS_NORMAL ||
		g_io_channel_set_flags (passwd_handler->backend_stdin, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL ||
		g_io_channel_set_flags (passwd_handler->backend_stdout, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL ) {

		/* Clean up */
		stop_passwd (passwd_handler);
		return FALSE;
	}

	/* Turn off buffering */
	g_io_channel_set_buffered (passwd_handler->backend_stdin, FALSE);
	g_io_channel_set_buffered (passwd_handler->backend_stdout, FALSE);

	/* Add IO Channel watcher */
	passwd_handler->backend_stdout_watch_id = g_io_add_watch (passwd_handler->backend_stdout,
	                                                          G_IO_IN | G_IO_PRI,
	                                                          (GIOFunc) io_watch_stdout, passwd_handler);

	/* Add child watcher */
	passwd_handler->backend_child_watch_id = g_child_watch_add (passwd_handler->backend_pid, (GChildWatchFunc) child_watch_cb, passwd_handler);

	/* Success! */

	return TRUE;
}

/* Stop passwd backend */
static void
stop_passwd (PasswdHandler *passwd_handler)
{
	/* This is the standard way of returning from the dialog with passwd.
	 * If we return this way we can safely kill passwd as it has completed
	 * its task.
	 */

	if (passwd_handler->backend_pid != -1) {
		kill (passwd_handler->backend_pid, 9);
	}

	/* We must run free_passwd_resources here and not let our child
	 * watcher do it, since it will access invalid memory after the
	 * dialog has been closed and cleaned up.
	 *
	 * If we had more than a single thread we'd need to remove
	 * the child watch before trying to kill the child.
	 */
	free_passwd_resources (passwd_handler);
}

/* Clean up passwd resources */
static void
free_passwd_resources (PasswdHandler *passwd_handler)
{
	GError	*error = NULL;

	/* Remove the child watcher */
	if (passwd_handler->backend_child_watch_id != 0) {

		g_source_remove (passwd_handler->backend_child_watch_id);

		passwd_handler->backend_child_watch_id = 0;
	}


	/* Close IO channels (internal file descriptors are automatically closed) */
	if (passwd_handler->backend_stdin != NULL) {

		if (g_io_channel_shutdown (passwd_handler->backend_stdin, TRUE, &error) != G_IO_STATUS_NORMAL) {
			g_warning ("Could not shutdown backend_stdin IO channel: %s", error->message);
			g_error_free (error);
			error = NULL;
		}

		g_io_channel_unref (passwd_handler->backend_stdin);
		passwd_handler->backend_stdin = NULL;
	}

	if (passwd_handler->backend_stdout != NULL) {

		if (g_io_channel_shutdown (passwd_handler->backend_stdout, TRUE, &error) != G_IO_STATUS_NORMAL) {
			g_warning ("Could not shutdown backend_stdout IO channel: %s", error->message);
			g_error_free (error);
			error = NULL;
		}

		g_io_channel_unref (passwd_handler->backend_stdout);

		passwd_handler->backend_stdout = NULL;
	}

	/* Remove IO watcher */
	if (passwd_handler->backend_stdout_watch_id != 0) {

		g_source_remove (passwd_handler->backend_stdout_watch_id);

		passwd_handler->backend_stdout_watch_id = 0;
	}

	/* Close PID */
	if (passwd_handler->backend_pid != -1) {

		g_spawn_close_pid (passwd_handler->backend_pid);

		passwd_handler->backend_pid = -1;
	}

	/* Clear backend state */
	passwd_handler->backend_state = PASSWD_STATE_NONE;
}

/*
 * }} Spawning and closing of backend
 */

/*
 * Backend communication code {{
 */

/* Write the first element of queue through channel */
static void
io_queue_pop (GQueue *queue, GIOChannel *channel)
{
	gchar	*buf;
	gsize	bytes_written;
	GError	*error = NULL;

	buf = g_queue_pop_head (queue);

	if (buf != NULL) {

		if (g_io_channel_write_chars (channel, buf, -1, &bytes_written, &error) != G_IO_STATUS_NORMAL) {
			g_warning ("Could not write queue element \"%s\" to channel: %s", buf, error->message);
			g_error_free (error);
		}

		/* Ensure passwords are cleared from memory */
		memset (buf, 0, strlen (buf));
		g_free (buf);
	}
}

/* Goes through the argument list, checking if one of them occurs in str
 * Returns: TRUE as soon as an element is found to match, FALSE otherwise */
static gboolean
is_string_complete (gchar *str, ...)
{
	va_list	ap;
	gchar	*arg;

	if (strlen (str) == 0) {
		return FALSE;
	}

	va_start (ap, str);

	while ((arg = va_arg (ap, char *)) != NULL) {
		if (g_strrstr (str, arg) != NULL) {
			va_end (ap);
			return TRUE;
		}
	}

	va_end (ap);

	return FALSE;
}

/*
 * IO watcher for stdout, called whenever there is data to read from the backend.
 * This is where most of the actual IO handling happens.
 */
static gboolean
io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler)
{
	static GString *str = NULL;	/* Persistent buffer */

	gchar		buf[BUFSIZE];		/* Temporary buffer */
	gsize		bytes_read;
	GError		*gio_error = NULL;	/* Error returned by functions */
	GError		*error = NULL;		/* Error sent to callbacks */

	GtkBuilder	*dialog;

	gboolean	reinit = FALSE;

	/* Initialize buffer */
	if (str == NULL) {
		str = g_string_new ("");
	}

	dialog = passwd_handler->ui;

	if (g_io_channel_read_chars (source, buf, BUFSIZE, &bytes_read, &gio_error)
	    != G_IO_STATUS_NORMAL) {
		g_warning ("IO Channel read error: %s", gio_error->message);
		g_error_free (gio_error);

		return TRUE;
	}

	str = g_string_append_len (str, buf, bytes_read);

	/* In which state is the backend? */
	switch (passwd_handler->backend_state) {
		case PASSWD_STATE_AUTH:
			/* Passwd is asking for our current password */

			if (is_string_complete (str->str, "assword: ", "failure", "wrong", "error", NULL)) {

				if (g_strrstr (str->str, "assword: ") != NULL) {
					/* Authentication successful */

					passwd_handler->backend_state = PASSWD_STATE_NEW;

					/* Trigger callback to update authentication status */
					if (passwd_handler->auth_cb)
						passwd_handler->auth_cb (passwd_handler,
						                         NULL,
						                         passwd_handler->auth_cb_data);

				} else {
					/* Authentication failed */

					error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED,
					                             _("Authentication failed"));

					passwd_handler->changing_password = FALSE;

					/* This error can happen both while authenticating or while changing password:
					 * if chpasswd_cb is set, this means we're already changing password */
					if (passwd_handler->chpasswd_cb)
						passwd_handler->chpasswd_cb (passwd_handler,
						                             error,
						                             passwd_handler->auth_cb_data);
					else if (passwd_handler->auth_cb)
						passwd_handler->auth_cb (passwd_handler,
						                         error,
						                         passwd_handler->auth_cb_data);

					g_error_free (error);
				}

				reinit = TRUE;
			}
			break;
		case PASSWD_STATE_NEW:
			/* Passwd is asking for our new password */

			if (is_string_complete (str->str, "assword: ", NULL)) {
				/* Advance to next state */
				passwd_handler->backend_state = PASSWD_STATE_RETYPE;

				/* Pop retyped password from queue and into IO channel */
				io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);

				reinit = TRUE;
			}
			break;
		case PASSWD_STATE_RETYPE:
			/* Passwd is asking for our retyped new password */

			if (is_string_complete (str->str,
			                        "successfully",
			                        "short",
			                        "longer",
			                        "palindrome",
			                        "dictionary",
			                        "simple",
			                        "simplistic",
			                        "similar",
			                        "different",
			                        "case",
			                        "wrapped",
			                        "recovered",
			                        "recent",
			                        "unchanged",
			                        "match",
			                        "1 numeric or special",
			                        "failure",
			                        NULL)) {

				if (g_strrstr (str->str, "successfully") != NULL) {
					/* Hooray! */

					/* Trigger callback to update status */
					if (passwd_handler->chpasswd_cb)
						passwd_handler->chpasswd_cb (passwd_handler,
						                             NULL,
						                             passwd_handler->chpasswd_cb_data);
				}
				else {
					/* Ohnoes! */

					if (g_strrstr (str->str, "recovered") != NULL) {
						/* What does this indicate?
						 * "Authentication information cannot be recovered?" from libpam? */
						error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN,
						                             str->str);
					} else if (g_strrstr (str->str, "short") != NULL ||
						   g_strrstr (str->str, "longer") != NULL) {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
						                     _("The new password is too short"));
					} else if (g_strrstr (str->str, "palindrome") != NULL ||
						   g_strrstr (str->str, "simple") != NULL ||
						   g_strrstr (str->str, "simplistic") != NULL ||
						   g_strrstr (str->str, "dictionary") != NULL) {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
						                     _("The new password is too simple"));
					} else if (g_strrstr (str->str, "similar") != NULL ||
					           g_strrstr (str->str, "different") != NULL ||
					           g_strrstr (str->str, "case") != NULL ||
						   g_strrstr (str->str, "wrapped") != NULL) {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
						                     _("The old and new passwords are too similar"));
					} else if (g_strrstr (str->str, "1 numeric or special") != NULL) {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
						                     _("The new password must contain numeric or special characters"));
					} else if (g_strrstr (str->str, "unchanged") != NULL ||
						   g_strrstr (str->str, "match") != NULL) {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
						                     _("The old and new passwords are the same"));
					} else if (g_strrstr (str->str, "recent") != NULL) {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
						                     _("The new password has already been used recently."));
					} else if (g_strrstr (str->str, "failure") != NULL) {
						/* Authentication failure */
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED,
						                     _("Your password has been changed since you initially authenticated!"));
					}
					else {
						error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN,
						                     _("Unknown error"));
					}

					/* At this point, passwd might have exited, in which case
					 * child_watch_cb should clean up for us and remove this watcher.
					 * On some error conditions though, passwd just re-prompts us
					 * for our new password. */
					passwd_handler->backend_state = PASSWD_STATE_ERR;

					passwd_handler->changing_password = FALSE;

					/* Trigger callback to update status */
					if (passwd_handler->chpasswd_cb)
						passwd_handler->chpasswd_cb (passwd_handler,
						                             error,
						                             passwd_handler->chpasswd_cb_data);

					g_error_free (error);

				}

				reinit = TRUE;

				/* child_watch_cb should clean up for us now */
			}
			break;
		case PASSWD_STATE_NONE:
			/* Passwd is not asking for anything yet */
			if (is_string_complete (str->str, "assword: ", NULL)) {

				/* If the user does not have a password set,
				 * passwd will immediately ask for the new password,
				 * so skip the AUTH phase */
				if (is_string_complete (str->str, "new", "New", NULL)) {
					gchar *pw;

					passwd_handler->backend_state = PASSWD_STATE_NEW;

					/* since passwd didn't ask for our old password
					 * in this case, simply remove it from the queue */
					pw = g_queue_pop_head (passwd_handler->backend_stdin_queue);
					g_free (pw);

					/* Pop the IO queue, i.e. send new password */
					io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);
				} else {

					passwd_handler->backend_state = PASSWD_STATE_AUTH;

					/* Pop the IO queue, i.e. send current password */
					io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);
				}

				reinit = TRUE;
			}
			break;
		default:
			/* Passwd has returned an error */
			reinit = TRUE;
			break;
	}

	if (reinit) {
		g_string_free (str, TRUE);
		str = NULL;
	}

	/* Continue calling us */
	return TRUE;
}

/*
 * }} Backend communication code
 */

/* Adds the current password to the IO queue */
static void
authenticate (PasswdHandler *passwd_handler)
{
	gchar	*s;

	s = g_strdup_printf ("%s\n", passwd_handler->current_password);

	g_queue_push_tail (passwd_handler->backend_stdin_queue, s);
}

/* Adds the new password twice to the IO queue */
static void
update_password (PasswdHandler *passwd_handler)
{
	gchar	*s;

	s = g_strdup_printf ("%s\n", passwd_handler->new_password);

	g_queue_push_tail (passwd_handler->backend_stdin_queue, s);
	/* We need to allocate new space because io_queue_pop() g_free()s
	 * every element of the queue after it's done */
	g_queue_push_tail (passwd_handler->backend_stdin_queue, g_strdup (s));
}


PasswdHandler *
passwd_init ()
{
	PasswdHandler *passwd_handler;

	passwd_handler = g_new0 (PasswdHandler, 1);

	/* Initialize backend_pid. -1 means the backend is not running */
	passwd_handler->backend_pid = -1;

	/* Initialize IO Channels */
	passwd_handler->backend_stdin = NULL;
	passwd_handler->backend_stdout = NULL;

	/* Initialize write queue */
	passwd_handler->backend_stdin_queue = g_queue_new ();

	/* Initialize watchers */
	passwd_handler->backend_child_watch_id = 0;
	passwd_handler->backend_stdout_watch_id = 0;

	/* Initialize backend state */
	passwd_handler->backend_state = PASSWD_STATE_NONE;
	passwd_handler->changing_password = FALSE;

	return passwd_handler;
}

void
passwd_destroy (PasswdHandler *passwd_handler)
{
	g_queue_free (passwd_handler->backend_stdin_queue);
	stop_passwd (passwd_handler);
	g_free (passwd_handler);
}

void
passwd_authenticate (PasswdHandler *passwd_handler,
                     const char    *current_password,
                     PasswdCallback cb,
                     const gpointer user_data)
{
	GError *error = NULL;

	/* Don't stop if we've already started chaging password */
	if (passwd_handler->changing_password)
		return;

	/* Clear data from possible previous attempts to change password */
	passwd_handler->new_password = NULL;
	passwd_handler->chpasswd_cb = NULL;
	passwd_handler->chpasswd_cb_data = NULL;
	g_queue_foreach (passwd_handler->backend_stdin_queue, (GFunc) g_free, NULL);
	g_queue_clear (passwd_handler->backend_stdin_queue);

	passwd_handler->current_password = current_password;
	passwd_handler->auth_cb = cb;
	passwd_handler->auth_cb_data = user_data;

	/* Spawn backend */
	stop_passwd (passwd_handler);

	if (!spawn_passwd (passwd_handler, &error)) {
		g_warning ("%s", error->message);
		g_error_free (error);

		return;
	}

	authenticate (passwd_handler);

	/* Our IO watcher should now handle the rest */
}

gboolean
passwd_change_password (PasswdHandler *passwd_handler,
                        const char    *new_password,
                        PasswdCallback cb,
                        const gpointer user_data)
{
	GError *error = NULL;

	passwd_handler->changing_password = TRUE;

	passwd_handler->new_password = new_password;
	passwd_handler->chpasswd_cb = cb;
	passwd_handler->chpasswd_cb_data = user_data;

	/* Stop passwd if an error occured and it is still running */
	if (passwd_handler->backend_state == PASSWD_STATE_ERR) {

		/* Stop passwd, free resources */
		stop_passwd (passwd_handler);
	}

	/* Check that the backend is still running, or that an error
	 * has occured but it has not yet exited */
	if (passwd_handler->backend_pid == -1) {
		/* If it is not, re-run authentication */

		/* Spawn backend */
		stop_passwd (passwd_handler);

		if (!spawn_passwd (passwd_handler, &error)) {
			g_warning ("%s", error->message);
			g_error_free (error);

			return FALSE;
		}

		/* Add current and new passwords to queue */
		authenticate (passwd_handler);
		update_password (passwd_handler);
	} else {
		/* Only add new passwords to queue */
		update_password (passwd_handler);
	}

	/* Pop new password through the backend. If user has no password, popping the queue
	   would output current password, while 'passwd' is waiting for the new one. So wait
	   for io_watch_stdout() to remove current password from the queue, and output
	   the new one for us.*/
	if (passwd_handler->current_password)
		io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);

	/* Our IO watcher should now handle the rest */

	return TRUE;
}
