/*
 * Copyright (c) 1997,2007-8,2020,21 Andrew G. Morgan <morgan@kernel.org>
 *
 * This sets/verifies the capabilities of a given file.
 */

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/capability.h>
#include <unistd.h>

static void usage(int status)
{
    fprintf(stderr,
	    "usage: setcap [--license] [-f] [-h] [-n <rootid>] [-q] [-v]"
	    " (-r|-|<caps>) <filename> [ ... (-r|-|<capsN>) <filenameN> ]\n"
	    "\n"
	    " Note <filename> must be a regular (non-symlink) file.\n"
	    " -r          remove capability from file\n"
	    " -           read capability text from stdin\n"
	    " <capsN>     cap_from_text(3) formatted file capability\n"
	    " [ Note: capsh --suggest=\"something...\" might help you pick. ]"
	    "\n"
	    " --license   display the license info\n"
	    " -f          force setting even when the capability is invalid\n"
	    " -h          this message and exit status 0\n"
	    " -n <rootid> write a user namespace (!= 0) limited capability\n"
	    " -q          quietly\n"
	    " -v          validate supplied capability matches file\n"
	);
    exit(status);
}

/* parse a positive integer with some error handling */
static unsigned long pos_uint(const char *text, const char *prefix, int *ok)
{
    char *remains;
    unsigned long value;
    ssize_t len = strlen(text);

    if (len == 0 || *text == '-') {
	goto fail;
    }
    value = strtoul(text, &remains, 0);
    if (*remains || value == 0) {
	goto fail;
    }
    if (ok != NULL) {
	*ok = 1;
    }
    return value;

fail:
    if (ok == NULL) {
	fprintf(stderr, "%s: want positive integer, got \"%s\"\n",
		prefix, text);
	exit(1);
    }
    *ok = 0;
    return 0;
}

#define MAXCAP 2048

static int read_caps(int quiet, const char *filename, char *buffer)
{
    int i = MAXCAP;

    if (!quiet) {
	fprintf(stderr,	"Please enter caps for file [empty line to end]:\n");
    }
    while (i > 0) {
	int j = read(STDIN_FILENO, buffer, i);

	if (j < 0) {
	    fprintf(stderr, "\n[Error - aborting]\n");
	    exit(1);
	}

	if (j==0 || buffer[0] == '\n') {
	    /* we're done */
	    break;
	}

	/* move on... */

	i -= j;
	buffer += j;
    }

    /* <NUL> terminate */
    buffer[0] = '\0';

    return (i < MAXCAP ? 0:-1);
}

int main(int argc, char **argv)
{
    int tried_to_cap_setfcap = 0;
    char buffer[MAXCAP+1];
    int retval, quiet = 0, verify = 0, forced = 0;
    cap_t mycaps;
    cap_value_t capflag;
    uid_t rootid = 0, f_rootid;

    if (argc < 2) {
	usage(1);
    }

    mycaps = cap_get_proc();
    if (mycaps == NULL) {
	fprintf(stderr, "warning - unable to get process capabilities"
		" (old libcap?)\n");
    }

    cap_t cap_d = NULL;
    char **arg = argv+1;
    for (; --argc > 0; arg++) {
	const char *text;

	cap_free(cap_d);
	cap_d = NULL;

	if (!strcmp("--license", *arg)) {
	    printf(
		"%s see LICENSE file for details.\n"
		"Copyright (c) 1997,2007-8,2020-21 Andrew G. Morgan"
		" <morgan@kernel.org>\n", argv[0]);
	    exit(0);
	}
	if (!strcmp(*arg, "-f")) {
	    forced = 1;
	    continue;
	}
	if (!strcmp(*arg, "-h")) {
	    usage(0);
	}
	if (!strcmp(*arg, "-n")) {
	    if (argc < 2) {
		fprintf(stderr,
			"usage: .. -n <rootid> .. - rootid!=0 file caps");
		exit(1);
	    }
	    --argc;
	    rootid = (uid_t) pos_uint(*++arg, "bad ns rootid", NULL);
	    continue;
	}
	if (!strcmp(*arg, "-q")) {
	    quiet = 1;
	    continue;
	}
	if (!strcmp(*arg, "-v")) {
	    verify = 1;
	    continue;
	}

	if (!strcmp(*arg, "-r")) {
	    cap_free(cap_d);
	    cap_d = NULL;
	} else {
	    if (!strcmp(*arg,"-")) {
		retval = read_caps(quiet, *arg, buffer);
		if (retval)
		    usage(1);
		text = buffer;
	    } else {
		text = *arg;
	    }

	    int non_space = 0, j;
	    for (j = 0; text[j]; j++) {
		if (!isspace(text[j])) {
		    non_space = 1;
		    break;
		}
	    }
	    if (!non_space) {
		fprintf(stderr, "empty space is an invalid capability, did you mean -r?\n");
		exit(1);
	    }
	    cap_d = cap_from_text(text);
	    if (cap_d == NULL) {
		printf("argument: %s\n", text);
		perror("fatal error");
		usage(1);
	    }
	    if (cap_set_nsowner(cap_d, rootid)) {
		perror("unable to set nsowner");
		exit(1);
	    }
#ifdef DEBUG
	    {
		char *result = cap_to_text(cap_d, NULL);
		fprintf(stderr, "caps set to: [%s]\n", result);
		cap_free(result);
	    }
#endif
	}

	if (--argc <= 0) {
	    usage(1);
	}

	/*
	 * Set the filesystem capability for this file.
	 */
	if (verify) {
	    cap_t cap_on_file;
	    int cmp;

	    if (cap_d == NULL) {
		cap_d = cap_init();
		if (cap_d == NULL) {
		    perror("unable to obtain empty capability");
		    exit(1);
		}
	    }

	    cap_on_file = cap_get_file(*++arg);
	    if (cap_on_file == NULL) {
		cap_on_file = cap_init();
		if (cap_on_file == NULL) {
		    perror("unable to use missing capability");
		    exit(1);
		}
	    }

	    cmp = cap_compare(cap_on_file, cap_d);
	    f_rootid = cap_get_nsowner(cap_on_file);
	    cap_free(cap_on_file);

	    if (cmp != 0 || rootid != f_rootid) {
		if (!quiet) {
		    if (rootid != f_rootid) {
			printf("nsowner[got=%d, want=%d],", f_rootid, rootid);
		    }
		    printf("%s differs in [%s%s%s]\n", *arg,
			   CAP_DIFFERS(cmp, CAP_PERMITTED) ? "p" : "",
			   CAP_DIFFERS(cmp, CAP_INHERITABLE) ? "i" : "",
			   CAP_DIFFERS(cmp, CAP_EFFECTIVE) ? "e" : "");
		}
		exit(1);
	    }
	    if (!quiet) {
		printf("%s: OK\n", *arg);
	    }
	} else {
	    if (!tried_to_cap_setfcap) {
		capflag = CAP_SETFCAP;

		/*
		 * Raise the effective CAP_SETFCAP.
		 */
		if (cap_set_flag(mycaps, CAP_EFFECTIVE, 1, &capflag, CAP_SET)
		    != 0) {
		    perror("unable to manipulate CAP_SETFCAP - "
			   "try a newer libcap?");
		    exit(1);
		}
		if (cap_set_proc(mycaps) != 0) {
		    perror("unable to set CAP_SETFCAP effective capability");
		    exit(1);
		}
		tried_to_cap_setfcap = 1;
	    }
#ifdef linux
	    {
		/* Linux's file capabilities have a compressed representation. */
		int explained = 0;
		int somebits = 0;
		cap_value_t cap;
		cap_flag_value_t per_state;

		for (cap = 0;
		     cap_get_flag(cap_d, cap, CAP_PERMITTED, &per_state) != -1;
		     cap++) {
		    cap_flag_value_t inh_state, eff_state, combined;

		    cap_get_flag(cap_d, cap, CAP_INHERITABLE, &inh_state);
		    cap_get_flag(cap_d, cap, CAP_EFFECTIVE, &eff_state);
		    combined = (inh_state | per_state);
		    somebits |= !!eff_state;
		    if (combined != eff_state) {
			explained = 1;
			break;
		    }
		}
		if (somebits && explained) {
		    fprintf(stderr, "Error: under Linux, effective file capabilities must either be empty, or\n"
			    "       exactly match the union of selected permitted and inheritable bits.\n");
		    if (!forced) {
			exit(1);
		    }
		}
	    }
#endif /* def linux */
	    errno = 0;
	    retval = cap_set_file(*++arg, cap_d);
	    if (retval != 0) {
		switch (errno) {
		case EINVAL:
		    fprintf(stderr,
			    "Invalid file '%s' for capability operation\n",
			    *arg);
		    exit(1);
		case ENODATA:
		    if (cap_d == NULL) {
			fprintf(stderr,
				"File '%s' has no capability to remove\n",
				*arg);
			if (forced) {
			    break;
			}
			exit(1);
		    }
		    /* FALLTHROUGH */
		default:
		    fprintf(stderr,
			    "Failed to set capabilities on file '%s': %s\n",
			    *arg, strerror(errno));
		    exit(1);
		}
	    }
	}
    }
    if (cap_d) {
	cap_free(cap_d);
    }

    exit(0);
}
