/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/* gkr-pam-client.h - Simple code for communicating with daemon

   Copyright (C) 2007 Stef Walter

   The Gnome Keyring Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   The Gnome Keyring Library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with the Gnome Library; see the file COPYING.LIB.  If not,
   <http://www.gnu.org/licenses/>.

   Author: Stef Walter <stef@memberwebs.com>
*/

#include "config.h"

#include "gkr-pam.h"

#include "egg/egg-buffer.h"
#include "egg/egg-unix-credentials.h"

#include "daemon/control/gkd-control-codes.h"

#include <sys/types.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <sys/wait.h>

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#if defined(HAVE_GETPEERUCRED)
#include <ucred.h>
#endif

#if defined(LOCAL_PEERCRED)
#include <sys/param.h>
#include <sys/ucred.h>
#endif

static int
check_peer_same_uid (struct passwd *pwd,
                     int sock)
{
	uid_t uid = -1;
	
	/* 
	 * Certain OS require a message to be sent over the unix socket for the 
	 * otherside to get the process credentials. Most uncool.
	 * 
	 * The normal gnome-keyring protocol accomodates this and the client
	 * sends a message/byte before sending anything else. This only works
	 * for the daemon verifying the client. 
	 * 
	 * This code here is used by a client to verify the daemon is running
	 * as the right user. Since we cannot modify the protocol, this only
	 * works on OSs that can do this credentials lookup transparently.
	 */

/* Linux */
#if defined(SO_PEERCRED)
	struct ucred cr;   
	socklen_t cr_len = sizeof (cr);
		
	if (getsockopt (sock, SOL_SOCKET, SO_PEERCRED, &cr, &cr_len) == 0 &&
	    cr_len == sizeof (cr)) {
	    	uid = cr.uid;
	} else {
		syslog (GKR_LOG_ERR, "could not get gnome-keyring-daemon socket credentials, "
		        "(returned len %d/%d)\n", cr_len, (int) sizeof (cr));
		return -1;
	}


/* The BSDs */
#elif defined(LOCAL_PEERCRED)
	uid_t gid;
        struct xucred xuc;
        socklen_t xuc_len = sizeof (xuc);

	if (getsockopt (sock, SOL_SOCKET, LOCAL_PEERCRED, &xuc, &xuc_len) == 0 && 
	    xuc_len == sizeof (xuc)) {
	    	uid = xuc.cr_uid;
	} else {
		syslog (GKR_LOG_ERR, "could not get gnome-keyring-daemon socket credentials, "
		        "(returned len %d/%d)\n", xuc_len, (int)sizeof (xuc));
		return -1;   
	}
	
	
/* NOTE: Add more here */
#else
	syslog (GKR_LOG_WARN, "Cannot verify that the process to which we are passing the login"
	        " password is genuinely running as the same user login: not supported on this OS.");
	uid = pwd->pw_uid;
	
	
#endif

	if (uid != pwd->pw_uid) {
		syslog (GKR_LOG_ERR, "The gnome keyring socket is not running with the same "
		        "credentials as the user login. Disconnecting.");
		return 0;
	}
	
	return 1;
}

static int
write_credentials_byte (int sock)
{
	for (;;) {
		if (egg_unix_credentials_write (sock) < 0) {
			if (errno == EINTR || errno == EAGAIN)
				continue;
			syslog (GKR_LOG_ERR, "couldn't send credentials to daemon: %s", 
			        strerror (errno));
			return -1;
		}
		
		break;
	}
	
	return 0;
}

static int
lookup_daemon (struct passwd *pwd,
               const char *control,
               struct sockaddr_un *addr)
{
	struct stat st;

	if (strlen (control) + 1 > sizeof (addr->sun_path)) {
		syslog (GKR_LOG_ERR, "gkr-pam: address is too long for unix socket path: %s",
		        control);
		return GKD_CONTROL_RESULT_FAILED;
	}

	memset (addr, 0, sizeof (*addr));
	addr->sun_family = AF_UNIX;
	strcpy (addr->sun_path, control);

	/* A bunch of checks to make sure nothing funny is going on */
	if (lstat (addr->sun_path, &st) < 0) {
		if (errno == ENOENT)
			return GKD_CONTROL_RESULT_NO_DAEMON;

		syslog (GKR_LOG_ERR, "Couldn't access gnome keyring socket: %s: %s",
		        addr->sun_path, strerror (errno));
		return GKD_CONTROL_RESULT_FAILED;
	}
	
	if (st.st_uid != pwd->pw_uid) {
		syslog (GKR_LOG_ERR, "The gnome keyring socket is not owned with the same "
		        "credentials as the user login: %s", addr->sun_path);
		return GKD_CONTROL_RESULT_FAILED;
	}
	
	if (S_ISLNK(st.st_mode) || !S_ISSOCK(st.st_mode)) {
		syslog (GKR_LOG_ERR, "The gnome keyring socket is not a valid simple "
		        "non-linked socket");
		return GKD_CONTROL_RESULT_FAILED;
	}

	return GKD_CONTROL_RESULT_OK;
}

static int
connect_daemon (struct passwd *pwd,
                struct sockaddr_un *addr,
                int *out_sock)
{
	int sock;

	/* Now we connect */
	sock = socket (AF_UNIX, SOCK_STREAM, 0);
	if (sock < 0) {
		syslog (GKR_LOG_ERR, "couldn't create control socket: %s", strerror (errno));
		return GKD_CONTROL_RESULT_FAILED;
	}

	/* close on exec */
	fcntl (sock, F_SETFD, 1);

	if (connect (sock, (struct sockaddr *)addr, sizeof (*addr)) < 0) {
		if (errno == ECONNREFUSED) {
			close (sock);
			return GKD_CONTROL_RESULT_NO_DAEMON;
		}
		syslog (GKR_LOG_ERR, "couldn't connect to gnome-keyring-daemon socket at: %s: %s",
		        addr->sun_path, strerror (errno));
		close (sock);
		return GKD_CONTROL_RESULT_FAILED;
	}

	/* Verify the server is running as the right user */
	
	if (check_peer_same_uid (pwd, sock) <= 0) {
		close (sock);
		return GKD_CONTROL_RESULT_FAILED;
	}
	
	/* This lets the server verify us */
	
	if (write_credentials_byte (sock) < 0) {
		close (sock);
		return GKD_CONTROL_RESULT_FAILED;
	}
	
	*out_sock = sock;
	return GKD_CONTROL_RESULT_OK;
}

static void
write_part (int fd, const unsigned char *data, int len, int *res)
{
	assert (res);
	
	/* Already an error present */
	if (*res != GKD_CONTROL_RESULT_OK)
		return;
	
	assert (data);
	
	while (len > 0) {
		int r = write (fd, data, len);
		if (r < 0) {
			if (errno == EAGAIN) 
				continue;
			syslog (GKR_LOG_ERR, "couldn't send data to gnome-keyring-daemon: %s", 
			        strerror (errno));
			*res = GKD_CONTROL_RESULT_FAILED;
			return;
		}
		data += r;
		len -= r;
	}
}

static int 
read_part (int fd,
           unsigned char *data,
           int len,
           int disconnect_ok)
{
	int r, all;
	
	all = len;
	while (len > 0) {
		r = read (fd, data, len);
		if (r < 0) {
			if (errno == EAGAIN)
				continue;
			if (errno == ECONNRESET && disconnect_ok)
				return 0;
			syslog (GKR_LOG_ERR, "couldn't read data from gnome-keyring-daemon: %s",
			        strerror (errno));
			return -1;
		} 
		if (r == 0) {
			if (disconnect_ok)
				return 0;
			syslog (GKR_LOG_ERR, "couldn't read data from gnome-keyring-daemon: %s",
			        "unexpected end of data");
			return -1;
		}
		
		data += r;
		len -= r;
	}

	return all;
}

static int
keyring_daemon_op (struct passwd *pwd,
                   struct sockaddr_un *addr,
                   int op,
                   int argc,
                   const char *argv[])
{
	int ret = GKD_CONTROL_RESULT_OK;
	unsigned char buf[4];
	int want_disconnect;
	int i, sock = -1;
	uint oplen, l;

	assert (addr);

	/* 
	 * We only support operations with zero or more strings
	 * and an empty (only result code) return. 
	 */
	 
	assert (op == GKD_CONTROL_OP_CHANGE ||
	        op == GKD_CONTROL_OP_UNLOCK ||
	        op == GKD_CONTROL_OP_QUIT);

	ret = connect_daemon (pwd, addr, &sock);
	if (ret != GKD_CONTROL_RESULT_OK)
		goto done;

	/* Calculate the packet length */
	oplen = 8; /* The packet size, and op code */
	for (i = 0; i < argc; ++i)  
		oplen += 4 + strlen (argv[i]);

	/* Write out the length, and op */
	egg_buffer_encode_uint32 (buf, oplen);
	write_part (sock, buf, 4, &ret);
	egg_buffer_encode_uint32 (buf, op);
	write_part (sock, buf, 4, &ret);
	
	/* And now the arguments */
	for (i = 0; i < argc; ++i) {
		if (argv[i] == NULL)
			l = 0x7FFFFFFF;
		else 
			l = strlen (argv[i]);
		egg_buffer_encode_uint32 (buf, l);
		write_part (sock, buf, 4, &ret);
		if (argv[i] != NULL)
			write_part (sock, (unsigned char*)argv[i], l, &ret);
	}
	
	if (ret != GKD_CONTROL_RESULT_OK)
		goto done;
	/*
	 * If we're asking the daemon to quit, then we expect
	 * disconnects after we send the initial request
	 */
	want_disconnect = (op == GKD_CONTROL_OP_QUIT);

	/* Read the response length */
	if (read_part (sock, buf, 4, want_disconnect) != 4) {
		ret = GKD_CONTROL_RESULT_FAILED;
		goto done;
	}

	/* We only support simple responses */	
	l = egg_buffer_decode_uint32 (buf);
	if (l != 8) {
		syslog (GKR_LOG_ERR, "invalid length response from gnome-keyring-daemon: %d", l);
		ret = GKD_CONTROL_RESULT_FAILED;
		goto done;
	}

	if (read_part (sock, buf, 4, want_disconnect) != 4) {
		ret = GKD_CONTROL_RESULT_FAILED;
		goto done;
	}
	ret = egg_buffer_decode_uint32 (buf);

	/*
	 * If we asked the daemon to quit, wait for it to disconnect
	 * by waiting until the socket disconnects from the other end.
	 */
	if (want_disconnect) {
		while (read (sock, buf, 4) > 0);
	}

done:
	if (sock >= 0)
		close (sock);
	
	return ret;
}

int
gkr_pam_client_run_operation (struct passwd *pwd, const char *control,
                              int op, int argc, const char* argv[])
{
	struct sigaction ignpipe, oldpipe, defchld, oldchld;
	struct sockaddr_un addr;
	int res;
	pid_t pid;
	int status;
	
	/* Make dumb signals go away */
	memset (&ignpipe, 0, sizeof (ignpipe));
	memset (&oldpipe, 0, sizeof (oldpipe));
	ignpipe.sa_handler = SIG_IGN;
	sigaction (SIGPIPE, &ignpipe, &oldpipe);
	
	memset (&defchld, 0, sizeof (defchld));
	memset (&oldchld, 0, sizeof (oldchld));
	defchld.sa_handler = SIG_DFL;
	sigaction (SIGCHLD, &defchld, &oldchld);

	res = lookup_daemon (pwd, control, &addr);
	if (res != GKD_CONTROL_RESULT_OK)
		goto out;

	if (pwd->pw_uid == getuid () && pwd->pw_gid == getgid () && 
	    pwd->pw_uid == geteuid () && pwd->pw_gid == getegid ()) {

		/* Already running as the right user, simple */
		res = keyring_daemon_op (pwd, &addr, op, argc, argv);
		
	} else {
		
		/* Otherwise run a child process to do the dirty work */
		switch (pid = fork ()) {
		case -1:
			syslog (GKR_LOG_ERR, "gkr-pam: couldn't fork: %s", 
			        strerror (errno));
			res = GKD_CONTROL_RESULT_FAILED;
			break;
			
		case 0:
			/* Setup process credentials */
			if (setgid (pwd->pw_gid) < 0 || setuid (pwd->pw_uid) < 0 ||
			    setegid (pwd->pw_gid) < 0 || seteuid (pwd->pw_uid) < 0) {
				syslog (GKR_LOG_ERR, "gkr-pam: couldn't switch to user: %s: %s", 
				        pwd->pw_name, strerror (errno));
				exit (GKD_CONTROL_RESULT_FAILED);
			}
	
			res = keyring_daemon_op (pwd, &addr, op, argc, argv);
			exit (res);
			return 0; /* Never reached */
			
		default:
			/* wait for child process */
			if (wait (&status) != pid) {
				syslog (GKR_LOG_ERR, "gkr-pam: couldn't wait on child process: %s", 
				        strerror (errno));
				res = GKD_CONTROL_RESULT_FAILED;
			}
			
			res = WEXITSTATUS (status);
			break;
		};
	}

out:
	sigaction (SIGCHLD, &oldchld, NULL);
	sigaction (SIGPIPE, &oldpipe, NULL);
	
	return res;
}
