gnome-control-center/panels/user-accounts/run-passwd.c
William Jon McCann bf5cb44016 Add user-accounts dialog
Copied from the accounts-dialog git module.
2010-10-30 18:04:15 -04:00

772 lines
31 KiB
C

/* -*- 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 <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#if __sun
#include <sys/types.h>
#include <signal.h>
#endif
#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_DONE, /* Passwd succeeded but has not yet exited */
PASSWD_STATE_ERR /* Passwd reported an error but has not yet exited */
} PasswdState;
struct PasswdHandler {
const char *current_password;
const char *new_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 ())
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");
}
if (WEXITSTATUS (status) == 0) {
if (passwd_handler->backend_state == PASSWD_STATE_RETYPE) {
passwd_handler->backend_state = PASSWD_STATE_DONE;
if (passwd_handler->chpasswd_cb)
passwd_handler->chpasswd_cb (passwd_handler,
NULL,
passwd_handler->auth_cb_data);
}
}
}
free_passwd_resources (passwd_handler);
}
static void
ignore_sigpipe (gpointer data)
{
signal (SIGPIPE, SIG_IGN);
}
/* 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 */
ignore_sigpipe, /* 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 (strstr (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 */
gboolean reinit = FALSE;
/* Initialize buffer */
if (str == NULL) {
str = g_string_new ("");
}
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 (strstr (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",
"case",
"different",
"wrapped",
"recovered",
"recent",
"unchanged",
"match",
"1 numeric or special",
"failure",
"DIFFERENT",
"BAD PASSWORD",
NULL)) {
if (strstr (str->str, "successfully") != NULL) {
/* Hooray! */
passwd_handler->backend_state = PASSWD_STATE_DONE;
/* 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 (strstr (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 (strstr (str->str, "short") != NULL ||
strstr (str->str, "longer") != NULL) {
error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
_("The new password is too short"));
} else if (strstr (str->str, "palindrome") != NULL ||
strstr (str->str, "simple") != NULL ||
strstr (str->str, "simplistic") != NULL ||
strstr (str->str, "dictionary") != NULL) {
error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
_("The new password is too simple"));
} else if (strstr (str->str, "similar") != NULL ||
strstr (str->str, "different") != NULL ||
strstr (str->str, "case") != NULL ||
strstr (str->str, "wrapped") != NULL) {
error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
_("The old and new passwords are too similar"));
} else if (strstr (str->str, "recent") != NULL) {
error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
_("The new password has already been used recently."));
} else if (strstr (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 (strstr (str->str, "unchanged") != NULL ||
strstr (str->str, "match") != NULL) {
error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
_("The old and new passwords are the same"));
} else if (strstr (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 if (strstr (str->str, "DIFFERENT")) {
error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
_("The new password does not contain enough different characters"));
}
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 (void)
{
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;
}