/*  Copyright (C) 2002 Fernando Lopez-Lezcano

    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 of the License, 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., 675 Mass Ave, Cambridge, MA 02139, USA.

    Jackstart is based on code and concepts found in sucap.c, written by
    Finn Arne Gangstad <finnag@guardian.no> and givertcap.c, written by
    Tommi Ilmonen, Tommi.Ilmonen@hut.fi

 */

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>
#include <grp.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

#include <config.h>

#undef _POSIX_SOURCE
#include <sys/capability.h>

#include "jack/start.h"
#include "md5.h"
#include "jack_md5.h"

#define READ_BLOCKSIZE 4096

/* JACK_LOCATION must be passed on the gcc command line */
static char *jackd_bin_path = JACK_LOCATION "/jackd";

static char *jackd_md5_sum = JACKD_MD5_SUM;


static int check_capabilities (void)
{
	cap_t caps = cap_init ();
	cap_flag_value_t cap;
	pid_t pid;
	int have_all_caps = 1;

	if (caps == NULL) {
		fprintf (stderr, "jackstart: could not allocate capability working storage\n");
		return 0;
	}
	pid = getpid ();
	cap_clear (caps);
	if (capgetp (pid, caps)) {
		fprintf (stderr, "jackstart: could not get capabilities for process %d\n", pid);
		return 0;
	}
	/* check that we are able to give capabilites to other processes */
	cap_get_flag (caps, CAP_SETPCAP, CAP_EFFECTIVE, &cap);
	if (cap == CAP_CLEAR) {
		have_all_caps = 0;
		goto done;
	}
	/* check that we have the capabilities we want to transfer */
	cap_get_flag (caps, CAP_SYS_NICE, CAP_EFFECTIVE, &cap);
	if (cap == CAP_CLEAR) {
		have_all_caps = 0;
		goto done;
	}
	cap_get_flag (caps, CAP_SYS_RESOURCE, CAP_EFFECTIVE, &cap);
	if (cap == CAP_CLEAR) {
		have_all_caps = 0;
		goto done;
	}
	cap_get_flag (caps, CAP_IPC_LOCK, CAP_EFFECTIVE, &cap);
	if (cap == CAP_CLEAR) {
		have_all_caps = 0;
		goto done;
	}
done:
	cap_free (caps);
	return have_all_caps;
}


static int give_capabilities (pid_t pid)
{
	cap_t caps = cap_init ();
	const unsigned caps_size = 4;
	cap_value_t cap_list[] =
	{ CAP_SETPCAP, CAP_SYS_NICE, CAP_SYS_RESOURCE, CAP_IPC_LOCK };

	if (caps == NULL) {
		fprintf (stderr, "jackstart: could not allocate capability working storage\n");
		return -1;
	}
	cap_clear (caps);
	if (capgetp (pid, caps)) {
		fprintf (stderr, "jackstart: could not get capabilities for process %d\n", pid);
		cap_clear (caps);
	}
	cap_set_flag (caps, CAP_EFFECTIVE, caps_size, cap_list, CAP_SET);
	cap_set_flag (caps, CAP_INHERITABLE, caps_size, cap_list, CAP_SET);
	cap_set_flag (caps, CAP_PERMITTED, caps_size, cap_list, CAP_SET);
	if (capsetp (pid, caps)) {
		fprintf (stderr, "jackstart: could not give capabilities: %s\n", strerror (errno));
		cap_free (caps);
		return -1;
	}
	cap_free (caps);
	return 0;
}

static int check_binary (const char *binpath)
{
	struct stat status;
	FILE *binstream;

	if (lstat (jackd_bin_path, &status)) {
		fprintf (stderr, "jackstart: could not stat %s: %s\n",
			 binpath, strerror (errno));
		return -1;
	}
	if (!(S_ISREG (status.st_mode))) {
		fprintf (stderr, "jackstart: %s is not a regular file\n",
			 binpath);
		return -1;
	}
	if (status.st_uid != 0) {
		fprintf (stderr, "jackstart: %s is not owned by root\n",
			 binpath);
		return -1;
	}
	if ((status.st_mode & 022) != 0) {
		fprintf (stderr,
			 "jackstart: %s mode %o writeable by non-root users\n",
			 binpath, status.st_mode & 07777);
		return -1;
	}
	if ((binstream = fopen (binpath, "r")) == NULL) {
		fprintf (stderr, "jackstart: can't open %s for reading: %s\n",
			 binpath, strerror (errno));
		return -1;
	} else {
		/* md5sum the executable file, check man evp for more details */
		size_t sum;
		md5_t ctx;
		char buffer[READ_BLOCKSIZE + 72];
		unsigned char md_value[MD5_SIZE];
		char md_string[3];
		int i, j;

		md5_init (&ctx);
		while (1) {
			size_t n;
			sum = 0;
			do {
				n = fread (buffer + sum, 1, READ_BLOCKSIZE - sum, binstream);
				sum += n;
			} while (sum < READ_BLOCKSIZE && n != 0);
			if (n == 0 && ferror (binstream)) {
				fprintf (stderr, "jackstart: error while reading %s: %s\n", binpath, strerror (errno));
				return -1;
			}
			if (n == 0) {
				break;
			}
			md5_process (&ctx, buffer, READ_BLOCKSIZE);
		}
		if (sum > 0) {
			md5_process (&ctx, buffer, sum);
		}
		if (fclose (binstream)) {
			fprintf (stderr, "jackstart: could not close %s after reading: %s\n", binpath, strerror (errno));
		}
		md5_finish (&ctx, md_value);
		for (i = 0, j = 0; i < sizeof(md_value); i++, j += 2) {
			sprintf (md_string, "%02x", md_value[i]);
			if (md_string[0] != jackd_md5_sum[j] ||
			    md_string[1] != jackd_md5_sum[j + 1]) {
				fprintf (stderr, "jackstart: md5 checksum for %s does not match\n", binpath);
				return -1;
			}
		}
	}
	return 0;
}

int main (int argc, char **argv)
{
	uid_t uid, euid;
	pid_t pid, parent_pid;
	gid_t gid;
	int pipe_fds[2];
	int err;

	parent_pid = getpid ();

	/* get real user and group ids, effective user id */
	uid = getuid ();
	gid = getgid ();
	euid = geteuid ();

	/* are we running suid root? */
	if (uid != 0) {
		if (euid != 0) {
			fprintf (stderr, "jackstart: not running suid root, can't use capabilities\n");
			fprintf (stderr, "    (currently running with uid=%d and euid=%d),\n", uid, euid);
			fprintf (stderr, "    make jackstart suid root or start jackd directly\n\n");
		}
	}
	/* see if we can get the required capabilities */
	if (check_capabilities () == 0) {
		size_t size;
		cap_t cap = cap_init ();
		capgetp (0, cap);
		fprintf (stderr, "jackstart: cannot get realtime capabilities, current capabilities are:\n");
		fprintf (stderr, "           %s\n", cap_to_text (cap, &size));
		fprintf (stderr, "    probably running under a kernel with capabilities disabled,\n");
		fprintf (stderr, "    a suitable kernel would have printed something like \"=eip\"\n\n");
	}

	/* check the executable, owner, permissions, md5 checksum */
	if (check_binary (jackd_bin_path)) {
		exit (1);
	}

	/* set process group to current pid */
	if (setpgid (0, getpid ())) {
		fprintf (stderr, "jackstart: failed to set process group: %s\n",
			 strerror (errno));
		exit (1);
	}

	/* create pipe to synchronize with jackd */
	if (pipe (pipe_fds)) {
		fprintf (stderr, "jackstart: could not create pipe: %s\n",
			 strerror (errno));
		exit (1);
	}

	/* make sure the file descriptors are the right ones,
	   otherwise dup them, this is to make sure that both
	   jackstart and jackd use the same fds
	 */
	if (pipe_fds[0] != PIPE_READ_FD) {
		if (dup2 (pipe_fds[0], PIPE_READ_FD) != PIPE_READ_FD) {
			fprintf (stderr, "jackstart: could not dup pipe read file descriptor: %s\n",
				 strerror (errno));
			exit (1);
		}
	}
	if (pipe_fds[1] != PIPE_WRITE_FD) {
		if (dup2 (pipe_fds[1], PIPE_WRITE_FD) != PIPE_WRITE_FD) {
			fprintf (stderr, "jackstart: could not dup pipe write file descriptor: %s\n",
				 strerror (errno));
			exit (1);
		}
	}
	/* fork off a child to wait for jackd to start */
	fflush (NULL);
	pid = fork ();
	if (pid == -1) {
		fprintf (stderr, "jackstart: fork failed\n");
		exit (1);
	}
	if (pid) {
		/* mother process: drops privileges, execs jackd */
		close (PIPE_READ_FD);

		/* get rid of any supplemental groups */
		if (!getuid () && setgroups (0, 0)) {
			fprintf (stderr, "jackstart: setgroups failed: %s\n", strerror (errno));
			exit (1);
		}

		/* set gid and uid */
		setregid (gid, gid);
		setreuid (uid, uid);
		execvp (jackd_bin_path, argv);

		/* we could not start jackd, clean up and exit */
		fprintf (stderr, "jackstart: unable to execute %s: %s\n", jackd_bin_path, strerror (errno));
		close (PIPE_WRITE_FD);
		wait (&err);
		exit (1);
	} else {
		/* child process: grants privileges to jackd */
		close (PIPE_WRITE_FD);

		/* wait for jackd to start */
		while (1) {
			int ret;
			char c;

			/* picking up pipe closure is a tricky business.
			   this seems to work as well as anything else.
			 */

			ret = read (PIPE_READ_FD, &c, 1);
			fprintf (stderr, "back from read, ret = %d errno == %s\n", ret, strerror (errno));
			if (ret == 1) {
				break;
			} else if (errno != EINTR) {
				break;
			}
		}

		/* set privileges on jackd process */
		give_capabilities (parent_pid);
	}
	exit (0);
}
