/*
 * dotlockfile.c	Command line version of liblockfile.
 *			Runs setgid mail so is able to lock mailboxes
 *			as well. Liblockfile can call this command.
 *
 *		Copyright (C) Miquel van Smoorenburg and contributors 1999-2021
 *
 *		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.
 */

#include "autoconf.h"

#include <sys/types.h>
#if HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
#include <pwd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
#include "maillock.h"
#include "lockfile.h"

#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif

#ifndef HAVE_GETOPT_H
extern int getopt();
extern char *optarg;
extern int optind;
#endif

static volatile char *tmplock;
static int quiet;

/*
 *	If we got SIGINT, SIGQUIT, SIGHUP, remove the
 *	tempfile and re-raise the signal.
 */
static void got_signal(int sig)
{
	if (tmplock && tmplock[0])
		unlink((char *)tmplock);
	signal(sig, SIG_DFL);
	raise(sig);
}

static void ignore_signal(int sig)
{
	(void)sig;
}

/*
 *	Install signal handler only if the signal was
 *	not ignored already.
 */
static int set_signal(int sig, void (*handler)(int))
{
	struct sigaction sa;

	if (sigaction(sig, NULL, &sa) < 0)
		return -1;
	if (sa.sa_handler == SIG_IGN)
		return 0;
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = handler;
	return sigaction(sig, &sa, NULL);
}

/*
 *	Sleep for an amount of time while regularly checking if
 *	our parent is still alive.
 */
int check_sleep(int sleeptime, int flags)
{
	int		i;
	int		interval = 5;
	static int	ppid = 0;

	if (ppid == 0) ppid = getppid();

	if (flags & L_INTERVAL_D_)
		interval = 1;

	for (i = 0; i < sleeptime; i += interval) {
		sleep(interval);
		if (kill(ppid, 0) < 0 && errno == ESRCH)
			return L_ERROR;
	}
	return 0;
}

/*
 *	Split a filename up in  file and directory.
 */
#ifdef MAILGROUP
static int fn_split(char *fn, char **fn_p, char **dir_p)
{
	static char	*buf = NULL;
	char		*p;

	if (buf)
		free (buf);
	buf = (char *) malloc (strlen (fn) + 1);
	if (! buf)
		return L_ERROR;
	strcpy(buf, fn);
	if ((p = strrchr(buf, '/')) != NULL) {
		*p++   = 0;
		*fn_p  = p;
		*dir_p = buf;
	} else {
		*fn_p  = fn;
		*dir_p = ".";
	}
	return L_SUCCESS;
}
#endif

/*
 *	Return name of lockfile for mail.
 */
static char *mlockname(char *user)
{
	static char	*buf = NULL;
	char		*e;

	if (buf)
		free(buf);

	e = getenv("MAIL");
	if (e) {
		buf = (char *)malloc(strlen(e)+6);
		if (!buf)
			return NULL;
		sprintf(buf, "%s.lock", e);
	} else {
		buf = (char *)malloc(strlen(MAILDIR)+strlen(user)+6);
		if (!buf)
			return NULL;
		sprintf(buf, "%s%s.lock", MAILDIR, user);
	}
	return buf;
}

static void perror_exit(const char *why)
{
	if (!quiet) {
		fprintf(stderr, "dotlockfile: ");
		perror(why);
	}
	exit(L_ERROR);
}

/*
 *	Print usage message and exit.
 */
static void usage(void)
{
	fprintf(stderr, "Usage:  dotlockfile -l [-r retries] [-i interval] [-p] [-q] <-m|lockfile>\n");
	fprintf(stderr, "        dotlockfile -l [-r retries] [-i interval] [-p] [-q] <-m|lockfile> [-P] command args...\n");
	fprintf(stderr, "        dotlockfile -u|-t\n");
	exit(1);
}

int main(int argc, char **argv)
{
	struct passwd	*pwd;
	struct lockargs_s_ args = { 0 };
	gid_t		gid, egid;
	char		*lockfile = NULL;
	char		**cmd = NULL;
	int 		c, r;
	int		retries = 5;
	int		interval = 0;
	int		flags = 0;
	int		lock = 0;
	int		unlock = 0;
	int		check = 0;
	int		touch = 0;
	int		writepid = 0;
	int		passthrough = 0;
	int cwd_fd = -1;
	int need_privs = 0;
	pid_t pid = -1;
	int e, wstatus;

	/*
	 *	Remember real and effective gid, and
	 *	drop privs for now.
	 */
	gid = getgid();
	egid = getegid();
	if (gid != egid) {
		if (setregid(-1, gid) < 0)
			perror_exit("setregid(-1, gid)");
	}

	set_signal(SIGINT, got_signal);
	set_signal(SIGQUIT, got_signal);
	set_signal(SIGHUP, got_signal);
	set_signal(SIGTERM, got_signal);
	set_signal(SIGPIPE, got_signal);

	/*
	 *	Process the options.
	 */
	while ((c = getopt(argc, argv, "+qpNr:mluci:tP")) != EOF) switch(c) {
		case 'q':
			quiet = 1;
			break;
		case 'p':
			writepid = 1;
			break;
		case 'N':
			/* NOP */
			break;
		case 'r':
			retries = atoi(optarg);
			if (retries <= 0 &&
			    retries != -1 && strcmp(optarg, "0") != 0) {
				if (!quiet)
					fprintf(stderr, "dotlockfile: "
						"-r %s: invalid argument\n",
						optarg);
				return L_ERROR;
			}
			if (retries == -1) {
				/* 4000 years */
				retries = 0x7ffffff0;
			}
			break;
		case 'm':
			if ((pwd = getpwuid(geteuid())) == NULL) {
				if (!quiet)
					fprintf(stderr, "dotlockfile: You don't exist. Go away.\n");
				return L_ERROR;
			}
			lockfile = mlockname(pwd->pw_name);
			if (!lockfile) {
				if (!quiet)
					perror("dotlockfile");
				return L_ERROR;
			}
			break;
		case 'l':
			lock = 1;
			break;
		case 'u':
			unlock = 1;
			break;
		case 'c':
			check = 1;
			break;
		case 'i':
			interval = atoi(optarg);
			if (interval <= 0 && strcmp(optarg, "0") != 0) {
				fprintf(stderr, "dotlockfile: -i needs argument >= 0\n");
				return L_ERROR;
			}
			flags |= L_INTERVAL_D_;
			args.interval = interval;
			break;
		case 't':
			touch = 1;
			break;
		case 'P':
			passthrough = 1;
			break;
		default:
			usage();
			break;
	}

	/*
	 * next argument may be lockfile name
	 */
	if (!lockfile) {
		if (optind == argc)
			usage();
		lockfile = argv[optind++];
	}

	/*
	 * next arguments may be command [args...]
	 */
	if (optind < argc)
		cmd = argv + optind;

	/*
	 *	Options sanity check
	 */
	if ((cmd || lock) && (touch || check || unlock))
		usage();

	if (writepid)
		flags |= (cmd ? L_PID : L_PPID);

#ifdef MAXPATHLEN
	if (strlen(lockfile) >= MAXPATHLEN) {
		if (!quiet)
			fprintf(stderr, "dotlockfile: %s: name too long\n", lockfile);
		return L_NAMELEN;
	}
#endif

	/*
	 *	Check if we run setgid.
	 */
#ifdef MAILGROUP
	if (gid != egid) {
		/*
		 *	See if the requested lock is for a mailbox.
		 *	First, remember current working directory.
		 */
#ifdef O_PATH
		cwd_fd = open(".", O_PATH|O_CLOEXEC);
#else
		cwd_fd = open(".", O_RDONLY|O_CLOEXEC);
#endif
		if (cwd_fd < 0) {
			if (!quiet)
				fprintf(stderr, "dotlockfile: opening \".\": %s\n",
					strerror(errno));
			return L_ERROR;
		}
		/*
		 *	Now change directory to the directory the lockfile is in.
		 */
		char *file, *dir;
		r = fn_split(lockfile, &file, &dir);
		if (r != L_SUCCESS) {
			if (!quiet)
				perror("dotlockfile");
			return L_ERROR;
		}
		if (chdir(dir) != 0) {
			if (!quiet)
				fprintf(stderr, "dotlockfile: %s: %s\n", dir, strerror(errno));
			return L_ERROR;
		}

		lockfile = file;
		need_privs = is_maillock(lockfile);
	}
#endif

	/*
	 *	See if we actually need to run setgid.
	 */
	if (need_privs) {
		if (setregid(gid, egid) != 0)
			perror_exit("setregid");
	} else {
		if (gid != egid && setgid(gid) != 0)
			perror_exit("setgid");
	}

	/*
	 *	Simple check for a valid lockfile ?
	 */
	if (check)
		return (lockfile_check(lockfile, flags) < 0) ? 1 : 0;


	/*
	 *	Touch lock ?
	 */
	if (touch)
		return (lockfile_touch(lockfile) < 0) ? 1 : 0;

	/*
	 *	Remove lockfile?
	 */
	if (unlock)
		return (lockfile_remove(lockfile) == 0) ? 0 : 1;


	/*
	 *	No, lock.
	 */
	r = lockfile_create_set_tmplock(lockfile, &tmplock, retries, flags, &args);
	if (r != 0 || !cmd)
		return r;


	/*
	 *	Spawn command.
	 *
	 *	Using an empty signal handler means that we ignore the
	 *	signal, but that it's restored to SIG_DFL at execve().
	 */
	set_signal(SIGINT, ignore_signal);
	set_signal(SIGQUIT, ignore_signal);
	set_signal(SIGHUP, ignore_signal);
	set_signal(SIGALRM, ignore_signal);

	pid = fork();
	if (pid < 0) {
		if (!quiet)
			perror("fork");
		lockfile_remove(lockfile);
		exit(L_ERROR);
	}
	if (pid == 0) {
		/* drop setgid */
		if (gid != egid && setgid(gid) < 0) {
			perror("setgid");
			exit(127);
		}
		/* restore current working directory */
		if (cwd_fd >= 0) {
			if (fchdir(cwd_fd) < 0) {
				perror("dotlockfile: restoring cwd:");
				exit(127);
			}
			close(cwd_fd);
		}
		/* exec */
		execvp(cmd[0], cmd);
		perror(cmd[0]);
		exit(127);
	}

	/* wait for child */
	while (1) {
		if (!writepid)
			alarm(30);
		e = waitpid(pid, &wstatus, 0);
		if (e >= 0 || errno != EINTR)
			break;
		if (!writepid)
			lockfile_touch(lockfile);
	}

	alarm(0);
	lockfile_remove(lockfile);

	if (passthrough) {
		if (WIFEXITED(wstatus))
			return WEXITSTATUS(wstatus);
		if (WIFSIGNALED(wstatus))
			return 128+WTERMSIG(wstatus);
	}
	return 0;
}