/*
 *	fopen_as_user() - open a file using uid:gid privileges only.
 *
 * This is the magic, forking, file-descriptor passing, version!
 * A user-land work-around for systems missing a true fopen_as_user(3).
 *
 * This function can be safely used by privileged processes to access a file
 * using only the privileges of a specified uid:gid, including when the
 * privileged process may not have access due to restrictions that might be
 * imposed on a network client's superuser by a remote file server.
 *
 * It works on *BSD socket systems that can pass file descriptors with
 * sendmsg() & recvmsg().
 *
 * It currently also has hooks into *BSD stdio internals, specifically the
 * __sflags() function which parses an fopen()/fdopen()/freopen() mode string
 * and turns it into flags for open(2).
 *
 * It also does not yet use initgroups() under the assumption that it's not
 * going to be the group ownership of a file which is critical to gaining its
 * access.
 */

#ident     "@(#):fopen_as_user.c   1.3  03/06/14 23:37:08 ()"

/*
 * This compilation Copyright (c) by Greg A. Woods <woods@planix.com>
 * 
 * Years of publication:  2003
 *
 * Redistribution of this software in both source and binary forms, with
 * or without modification, is permitted provided that all of the
 * following conditions are met:
 * 
 * 1. Redistributions of source code, either alone or as part of a
 *    collective work, must retain this entire copyright notice, and the
 *    following disclaimer, without alteration, in each file that
 *    contains part of this software.
 * 
 * 2. Redistributions of this software in binary form, either alone or
 *    as part of a collective work, must reproduce this entire copyright
 *    notice, and the following disclaimer, without alteration, in
 *    either the documentation (as text files in electronic media, or in
 *    printed matter), and/or any original header files from this
 *    software as per the previous term, and/or other materials provided
 *    as part of the distribution.
 * 
 * 3. Collective works including this software must also include the
 *    following acknowledgement, either alone or as part of this entire
 *    copyright license, in any printed documentation accompanying a
 *    physical distribution (if there is printed documentation), and in
 *    a plain text file separate from the archive files (but perhaps
 *    along with other similar acknowledgments) on any electronic
 *    medium:
 * 
 * 	This product includes software developed by Greg A. Woods.
 * 
 * 4. The name of the author may NOT be used to endorse or promote
 *    products derived from this software without specific prior written
 *    permission.  The use of the author's name strictly to meet the
 *    requirements of the previous terms is not to be considered
 *    promotion or endorsement under this term.
 * 
 * 5. Altered versions (derivative works) must be plainly marked as
 *    such, and must not be misrepresented as being the original
 *    software.  This copyright notice, and the following disclaimer,
 *    must not be removed from any derivative work and must not be
 *    changed in any way.
 * 
 * All other rights are reserved.
 * 
 * DISCLAIMER:
 * 
 * THIS SOFTWARE IS PROVIDED BY GREG A. WOODS ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


#include <sys/cdefs.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/uio.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

FILE *fopen_as_user __P((char *, char *, uid_t, gid_t));

#ifndef HAVE_FOPEN_AS_USER

static ssize_t read_fd __P((int, void *, size_t, int *));

/*
 * file descriptor passing helper function
 *
 * More or less copied from W. Richard Stevens' "UNIX Network Programming",
 * Vol. 1 (2nd ed), p. 387 (obsolete code for old recvmsg() deleted, and
 * converted to use malloc() because CMSG_SPACE() is not a compile-time
 * constant on NetBSD)
 */
static ssize_t
read_fd(fd, ptr, nbytes, precvfd)
	int fd;				/* AF_LOCAL socket */
	void *ptr;			/* for ancilliary data */
	size_t nbytes;			/* size of ptr storage */
	int *precvfd;			/* pointer for storing descriptor */
{
	struct msghdr	msg;
	struct iovec	iov;
	ssize_t		n;
	char           *control;
	struct cmsghdr *cmptr;
	int             oerrno;

	*precvfd = -1;			/* assume the worst.... */

	if (!(control = malloc(CMSG_SPACE(sizeof(*precvfd)))))
		return -1;

	memset(&msg, 0, sizeof(msg));
	msg.msg_control = control;
	msg.msg_controllen = (socklen_t) CMSG_SPACE(sizeof(*precvfd)); /* CMSG_LEN()??? */
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_flags = 0;

	iov.iov_base = ptr;
	iov.iov_len = nbytes;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	n = recvmsg(fd, &msg, 0);
	oerrno = errno;
	if (n <= 0) {
		free(control);
		errno = oerrno;
		return -1;
	}

	cmptr = CMSG_FIRSTHDR(&msg);
	if (!cmptr || cmptr->cmsg_len != CMSG_LEN(sizeof(*precvfd))) {
		free(control);
		errno = EBADF;		/* descriptor was not passed */
		return -1;
	}
	if (cmptr->cmsg_level != SOL_SOCKET) {
		free(control);
		errno = EBADF;
		return -1;
	}
	if (cmptr->cmsg_type != SCM_RIGHTS) {
		free(control);
		errno = EBADF;
		return -1;
	}
	*precvfd = *((int *) ((void *) CMSG_DATA(cmptr)));

	free(control);
	errno = oerrno;

	return (n);
}

static ssize_t write_fd __P((int, void *, size_t, int));

/*
 * this declaration borrowed from:
 *
 *	 src/lib/libc/stdio/local.h
 */
extern int      __sflags __P((const char *, int *));

/*
 * file descriptor passing helper function
 *
 * More or less copied from W. Richard Stevens' "UNIX Network Programming",
 * Vol. 1 (2nd ed), p. 389 (obsolete code for old recvmsg() deleted, and
 * converted to use malloc() because CMSG_SPACE() is not a compile-time
 * constant on NetBSD)
 */
static ssize_t
write_fd(fd, ptr, nbytes, sendfd)
	int fd;				/* AF_LOCAL socket */
	void *ptr;			/* for ancilliary data */
	size_t nbytes;			/* size of ancilliary data */
	int sendfd;			/* descriptor to send */
{
	struct msghdr	msg;
	struct iovec	iov;
	char           *control;
	struct cmsghdr *cmptr;
	int             oerrno;
	int             rv;

	if (!(control = malloc(CMSG_SPACE(sizeof(sendfd)))))
		return -1;

	memset(&msg, 0, sizeof(msg));
	msg.msg_control = control;
	msg.msg_controllen = (socklen_t) CMSG_LEN(sizeof(sendfd));
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_flags = 0;

	cmptr = CMSG_FIRSTHDR(&msg);
	cmptr->cmsg_len = CMSG_LEN(sizeof(sendfd));
	cmptr->cmsg_level = SOL_SOCKET;
	cmptr->cmsg_type = SCM_RIGHTS;
	*((int *) ((void *) CMSG_DATA(cmptr))) = sendfd;

	iov.iov_base = ptr;
	iov.iov_len = nbytes;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	rv = sendmsg(fd, &msg, 0);
	oerrno = errno;

	free(control);
	errno = oerrno;

	return rv;
}

/*
 *	fopen_as_user() - open a file using uid:gid privileges only.
 *
 * More or less copied from W. Richard Stevens' "UNIX Network Programming",
 * Vol. 1 (2nd ed), p. 385
 *
 * XXX WARNING: This is an incomplete implementation! (e.g. no initgroups())
 */
FILE *
fopen_as_user(path, mode, uid, gid)
	char *path;
	char *mode;
	uid_t uid;
	gid_t gid;
{
	int fd;
	int pid;
	int sockfd[2];
	int status;
	FILE *fp = NULL;

	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd) < 0)
		return NULL;

	switch ((pid = fork())) {
	case -1: {
		int oerrno = errno;

		/* fork() error */
		close(sockfd[0]);
		close(sockfd[1]);
		errno = oerrno;
		return NULL;
		/* NOTREACHED */
	}
	case 0: {
		int cfd;
		int oflags;

# if (ELAST > 254)
#  include "ERORR:  This code returns errno via _exit() and ELAST > 254!"
# endif
		/* in the child process */

		/*
		 * Take care to use _exit() so as to avoid any side-effects of
		 * exit(), such as atexit()s registered by the parent process.
		 *
		 * Note this means we have to be very careful to always close
		 * what we open!
		 */
		close(sockfd[0]);

		if (setgid(gid) != 0) {
			int oerrno = errno;

			close(sockfd[1]);
			_exit(oerrno + 1);
		}
		if (setuid(uid) != 0) {
			int oerrno = errno;

			close(sockfd[1]);
			_exit(oerrno + 1);
		}
		if (__sflags(mode, &oflags) == 0) { /* magic *BSD stdio internals */
			int oerrno = errno;

			close(sockfd[1]);
			_exit(oerrno + 1);
		}
		if ((cfd = open(path, oflags, DEFFILEMODE)) < 0) {
			int oerrno = errno;

			close(sockfd[1]);
			_exit(oerrno + 1);
		}
		/*
		 * When sending a descriptor across a stream pipe we always
		 * send at least one byte of datea, even if the receiver does
		 * nothing with the data, otherwise the receiver cannot tell
		 * whether a return value of 0 from read_fd() means "no data
		 * (but possibly a descriptor)", or just "end of file".
		 */
		if (write_fd(sockfd[1], "", sizeof(""), cfd) < 0) {
			int oerrno = errno;

			close(sockfd[1]);
			close(cfd);
			_exit(oerrno + 1);
		}
		close(cfd);
		close(sockfd[1]);
		_exit(0);
		/* NOTREACHED */
	}
	default:
		/* in the parent process */
		close(sockfd[1]);
		if (waitpid(pid, &status, 0) < 0) { /* WUNTRACED??? */
			int oerrno = errno;

			close(sockfd[0]);
			errno = oerrno;
			return NULL;
		}
		if (WIFEXITED(status)) {
			char ch;

			if (WEXITSTATUS(status)) {
				close(sockfd[0]);
				errno = WEXITSTATUS(status) - 1;
				return NULL;
			}
			if (read_fd(sockfd[0], &ch, 1, &fd) <= 0) {
				int oerrno = errno;

				close(sockfd[0]);
				errno = oerrno;
				return NULL;
			}
			if (fd < 0) {
				int oerrno = errno;

				close(sockfd[0]);
				errno = oerrno;
				return NULL;
			}
			close(sockfd[0]);
		} else {
			close(sockfd[0]);
			errno = EINTR;
			return NULL;
		}
	}
	if (!(fp = fdopen(fd, mode))) {
		int oerrno = errno;

		close(fd);
		errno = oerrno;
		return NULL;
	}

	return fp;
}
#endif /* HAVE_FOPEN_AS_USER */