/* Key watching facility.
 *
 * Copyright (C) 2019 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public Licence
 * as published by the Free Software Foundation; either version
 * 2 of the Licence, or (at your option) any later version.
 */

#define _GNU_SOURCE
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <poll.h>
#include <getopt.h>
#include <sys/wait.h>
#include "keyutils.h"
#include <limits.h>
#include "keyctl.h"
#include "watch_queue.h"

#define MAX_MESSAGE_COUNT 256

static int consumer_stop;
static pid_t pid_con = -1, pid_cmd = -1;
static key_serial_t session;
static int watch_fd;
static int debug;

static struct watch_notification_filter filter = {
	.nr_filters	= 0,
	.filters = {
		/* Reserve a slot */
		[0]	= {
			.type			= WATCH_TYPE_KEY_NOTIFY,
		},
	},
};

static inline bool after_eq(unsigned int a, unsigned int b)
{
        return (signed int)(a - b) >= 0;
}

static void consumer_term(int sig)
{
	consumer_stop = 1;
}

static void saw_key_change(FILE *log, struct watch_notification *n,
			   unsigned int len)
{
	struct key_notification *k = (struct key_notification *)n;

	if (len != sizeof(struct key_notification))
		return;

	switch (n->subtype) {
	case NOTIFY_KEY_INSTANTIATED:
		fprintf(log, "%u inst\n", k->key_id);
		break;
	case NOTIFY_KEY_UPDATED:
		fprintf(log, "%u upd\n", k->key_id);
		break;
	case NOTIFY_KEY_LINKED:
		fprintf(log, "%u link %u\n", k->key_id, k->aux);
		break;
	case NOTIFY_KEY_UNLINKED:
		fprintf(log, "%u unlk %u\n", k->key_id, k->aux);
		break;
	case NOTIFY_KEY_CLEARED:
		fprintf(log, "%u clr\n", k->key_id);
		break;
	case NOTIFY_KEY_REVOKED:
		fprintf(log, "%u rev\n", k->key_id);
		break;
	case NOTIFY_KEY_INVALIDATED:
		fprintf(log, "%u inv\n", k->key_id);
		break;
	case NOTIFY_KEY_SETATTR:
		fprintf(log, "%u attr\n", k->key_id);
		break;
	}
}

/*
 * Handle removal notification.
 */
static void saw_removal_notification(FILE *gc, struct watch_notification *n,
				     unsigned int len)
{
	key_serial_t key = 0;
	unsigned int wp;

	wp = (n->info & WATCH_INFO_ID) >> WATCH_INFO_ID__SHIFT;

	if (len >= sizeof(struct watch_notification_removal)) {
		struct watch_notification_removal *r = (void *)n;
		key = r->id;
	}

	fprintf(gc, "%u gc\n", key);
	if (wp == 1)
		exit(0);
}

/*
 * Consume and display events.
 */
static __attribute__((noreturn))
int consumer(FILE *log, FILE *gc, int fd)
{
	unsigned char buffer[433], *p, *end;
	union {
		struct watch_notification n;
		unsigned char buf1[128];
	} n;
	ssize_t buf_len;

	setlinebuf(log);
	setlinebuf(gc);
	signal(SIGTERM, consumer_term);

	do {
		if (!consumer_stop) {
			struct pollfd pf[1];
			pf[0].fd = fd;
			pf[0].events = POLLIN;
			pf[0].revents = 0;

			if (poll(pf, 1, -1) == -1) {
				if (errno == EINTR)
					continue;
				error("poll");
			}
		}

		buf_len = read(fd, buffer, sizeof(buffer));
		if (buf_len == -1) {
			perror("read");
			exit(1);
		}

		if (buf_len == 0) {
			printf("-- END --\n");
			exit(0);
		}

		if (buf_len > sizeof(buffer)) {
			fprintf(stderr, "Read buffer overrun: %zd\n", buf_len);
			exit(4);
		}

		if (debug)
			fprintf(stderr, "read() = %zd\n", buf_len);

		p = buffer;
		end = buffer + buf_len;
		while (p < end) {
			size_t largest, len;

			largest = end - p;
			if (largest > 128)
				largest = 128;
			if (largest < sizeof(struct watch_notification)) {
				fprintf(stderr, "Short message header: %zu\n", largest);
				exit(4);
			}
			memcpy(&n, p, largest);

			if (debug)
				fprintf(stderr, "NOTIFY[%03zx]: ty=%06x sy=%02x i=%08x\n",
					p - buffer, n.n.type, n.n.subtype, n.n.info);

			len = n.n.info & WATCH_INFO_LENGTH;
			if (len < sizeof(n.n) || len > largest) {
				fprintf(stderr, "Bad message length: %zu/%zu\n", len, largest);
				exit(1);
			}

			switch (n.n.type) {
			case WATCH_TYPE_META:
				switch (n.n.subtype) {
				case WATCH_META_REMOVAL_NOTIFICATION:
					saw_removal_notification(gc, &n.n, len);
					break;
				case WATCH_META_LOSS_NOTIFICATION:
					fprintf(log, "-- LOSS --\n");
					break;
				default:
					if (debug)
						fprintf(stderr, "other meta record\n");
					break;
				}
				break;
			case WATCH_TYPE_KEY_NOTIFY:
				saw_key_change(log, &n.n, len);
				break;
			default:
				if (debug)
					fprintf(stderr, "other type\n");
				break;
			}

			p += len;
		}
	} while (!consumer_stop);

	fprintf(log, "Monitoring terminated\n");
	if (gc != log)
		fprintf(gc, "Monitoring terminated\n");
	exit(0);
}

/*
 * Open the watch device and allocate a buffer.
 */
static int open_watch(void)
{
	int pipefd[2], fd;

	if (pipe2(pipefd, O_NOTIFICATION_PIPE | O_NONBLOCK) == -1)
		error("pipe2");

	fd = pipefd[0];

	if (ioctl(fd, IOC_WATCH_QUEUE_SET_SIZE, MAX_MESSAGE_COUNT) == -1)
		error("/dev/watch_queue(size)");

	if (filter.nr_filters &&
	    ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter) == -1)
		error("/dev/watch_queue(filter)");

	return fd;
}

/*
 * Parse a filter character representation into a subtype number.
 */
static bool parse_subtype(struct watch_notification_type_filter *t, char filter)
{
	static const char filter_mapping[] =
		"i" /* 0 NOTIFY_KEY_INSTANTIATED */
		"p" /* 1 NOTIFY_KEY_UPDATED */
		"l" /* 2 NOTIFY_KEY_LINKED */
		"n" /* 3 NOTIFY_KEY_UNLINKED */
		"c" /* 4 NOTIFY_KEY_CLEARED */
		"r" /* 5 NOTIFY_KEY_REVOKED */
		"v" /* 6 NOTIFY_KEY_INVALIDATED */
		"s" /* 7 NOTIFY_KEY_SETATTR */
		;
	const char *p;
	unsigned int st_bits;
	unsigned int st_index;
	unsigned int st_bit;
	int subtype;

	p = strchr(filter_mapping, filter);
	if (!p)
		return false;

	subtype = p - filter_mapping;
	st_bits = sizeof(t->subtype_filter[0]) * 8;
	st_index = subtype / st_bits;
	st_bit = 1U << (subtype % st_bits);
	t->subtype_filter[st_index] |= st_bit;
	return true;
}

/*
 * Parse filters.
 */
static void parse_watch_filter(char *str)
{
	struct watch_notification_filter *f = &filter;
	struct watch_notification_type_filter *t0 = &f->filters[0];

	f->nr_filters	= 1;
	t0->type	= WATCH_TYPE_KEY_NOTIFY;

	for (; *str; str++) {
		if (parse_subtype(t0, *str))
			continue;
		fprintf(stderr, "Unknown filter character '%c'\n", *str);
		exit(2);
	}
}

/*
 * Watch a key or keyring for changes.
 */
void act_keyctl_watch(int argc, char *argv[])
{
	key_serial_t key;
	int wfd, opt;

	while (opt = getopt(argc, argv, "f:"),
	       opt != -1) {
		switch (opt) {
		case 'f':
			parse_watch_filter(optarg);
			break;
		default:
			fprintf(stderr, "Unknown option\n");
			exit(2);
		}
	}

	argv += optind;
	argc -= optind;
	if (argc != 1)
		format();

	key = get_key_id(argv[0]);
	wfd = open_watch();

	if (keyctl_watch_key(key, wfd, 0x01) == -1)
		error("keyctl_watch_key");

	consumer(stdout, stdout, wfd);
}

/*
 * Add a watch on a key to the monitor created by watch_session.
 */
void act_keyctl_watch_add(int argc, char *argv[])
{
	key_serial_t key;
	int fd;

	if (argc != 3)
		format();

	fd = atoi(argv[1]);
	key = get_key_id(argv[2]);

	if (keyctl_watch_key(key, fd, 0x02) == -1)
		error("keyctl_watch_key");
	exit(0);
}

/*
 * Remove a watch on a key from the monitor created by watch_session.
 */
void act_keyctl_watch_rm(int argc, char *argv[])
{
	key_serial_t key;
	int fd;

	if (argc != 3)
		format();

	fd = atoi(argv[1]);
	key = get_key_id(argv[2]);

	if (keyctl_watch_key(key, fd, -1) == -1)
		error("keyctl_watch_key");
	exit(0);
}

static void exit_cleanup(void)
{
	pid_t me = getpid();
	int w;

	if (me != pid_cmd && me != pid_con) {
		keyctl_watch_key(session, watch_fd, -1);
		if (pid_cmd != -1) {
			kill(pid_cmd, SIGTERM);
			waitpid(pid_cmd, &w, 0);
		}
		if (pid_con != -1) {
			kill(pid_con, SIGTERM);
			waitpid(pid_con, &w, 0);
		}
	}
}

static void run_command(int argc, char *argv[], int wfd)
{
	char buf[16];

	pid_cmd = fork();
	if (pid_cmd == -1)
		error("fork");
	if (pid_cmd != 0)
		return;

	pid_cmd = -1;
	pid_con = -1;

	sprintf(buf, "%u", wfd);
	setenv("KEYCTL_WATCH_FD", buf, true);

	/* run the standard shell if no arguments */
	if (argc == 0) {
		const char *q = getenv("SHELL");
		if (!q)
			q = "/bin/sh";
		execl(q, q, NULL);
		error(q);
	}

	/* run the command specified */
	execvp(argv[0], argv);
	error(argv[0]);
}

/*
 * Open a logfiles.
 */
static FILE *open_logfile(const char *logfile)
{
	unsigned int flags;
	FILE *log;
	int lfd;

	log = fopen(logfile, "a");
	if (!log)
		error(logfile);

	lfd = fileno(log);
	flags = fcntl(lfd, F_GETFD);
	if (flags == -1)
		error("F_GETFD");
	if (fcntl(lfd, F_SETFD, flags | FD_CLOEXEC) == -1)
		error("F_SETFD");

	return log;
}

/*
 * Set up a new session keyring with a monitor that is exposed on an explicit
 * file descriptor in the program that it starts.
 */
void act_keyctl_watch_session(int argc, char *argv[])
{
	const char *session_name = NULL;
	const char *logfile, *gcfile, *target_fd;
	unsigned int flags;
	pid_t pid;
	FILE *log, *gc;
	int wfd, tfd, opt, w, e = 0, e2 = 0;

	while (opt = getopt(argc, argv, "+df:n:"),
	       opt != -1) {
		switch (opt) {
		case 'd':
			debug = 1;
			break;
		case 'f':
			parse_watch_filter(optarg);
			break;
		case 'n':
			session_name = optarg;
			break;
		default:
			fprintf(stderr, "Unknown option\n");
			exit(2);
		}
	}

	argv += optind;
	argc -= optind;

	if (argc < 4)
		format();

	logfile = argv[0];
	gcfile = argv[1];
	target_fd = argv[2];
	tfd = atoi(target_fd);
	if (tfd < 3 || tfd > 9) {
		fprintf(stderr, "The target fd must be between 3 and 9\n");
		exit(2);
	}

	wfd = open_watch();
	if (wfd != tfd) {
		if (dup2(wfd, tfd) == -1)
			error("dup2");
		close(wfd);
		wfd = tfd;
	}
	watch_fd = wfd;

	atexit(exit_cleanup);

	/* We want the fd to be inherited across a fork. */
	flags = fcntl(wfd, F_GETFD);
	if (flags == -1)
		error("F_GETFD");
	if (fcntl(wfd, F_SETFD, flags & ~FD_CLOEXEC) == -1)
		error("F_SETFD");

	log = open_logfile(logfile);
	gc = open_logfile(gcfile);

	pid_con = fork();
	if (pid_con == -1)
		error("fork");
	if (pid_con == 0) {
		pid_cmd = -1;
		pid_con = -1;
		consumer(log, gc, wfd);
	}

	/* Create a new session keyring and watch it. */
	session = keyctl_join_session_keyring(session_name);
	if (session == -1)
		error("keyctl_join_session_keyring");

	if (keyctl_watch_key(session, wfd, 0x01) == -1)
		error("keyctl_watch_key/session");

	fprintf(stderr, "Joined session keyring: %d\n", session);

	/* Start the command and then wait for it to finish and the
	 * notification consumer to clean up.
	 */
	run_command(argc - 3, argv + 3, wfd);
	close(wfd);
	wfd = -1;

	while (pid = wait(&w),
	       pid != -1) {
		if (pid == pid_cmd) {
			if (pid_con != -1)
				kill(pid_con, SIGTERM);
			if (WIFEXITED(w)) {
				e2 = WEXITSTATUS(w);
				pid_cmd = -1;
			} else if (WIFSIGNALED(w)) {
				e2 = WTERMSIG(w) + 128;
				pid_cmd = -1;
			} else if (WIFSTOPPED(w)) {
				raise(WSTOPSIG(w));
			}
		} else if (pid == pid_con) {
			if (pid_cmd != -1)
				kill(pid_cmd, SIGTERM);
			if (WIFEXITED(w)) {
				e = WEXITSTATUS(w);
				pid_con = -1;
			} else if (WIFSIGNALED(w)) {
				e = WTERMSIG(w) + 128;
				pid_con = -1;
			}
		}
	}

	if (e == 0)
		e = e2;
	exit(e);
}

/*
 * Wait for monitoring to synchronise.
 */
void act_keyctl_watch_sync(int argc, char *argv[])
{
	long ret;
	int wfd, count;

	if (argc != 2)
		format();

	wfd = atoi(argv[1]);

	ret = ioctl(wfd, PIPE_IOC_SYNC);
	if (ret == 0)
		exit(0);

	if (ret == -1 && errno != ENOTTY)
		error("ioctl(PIPE_IOC_SYNC)");

	for (;;) {
		ret = ioctl(wfd, FIONREAD, &count);
		if (ret == -1)
			error("ioctl(FIONREAD)");
		if (count == 0)
			break;
		usleep(200 * 1000);
	}

	exit(0);
}
