#include "plugin.h"

#if WITH_SELINUX

#include <errno.h>
#include <selinux/selinux.h>
#include <semanage/semanage.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <rpm/rpmpol.h>
#include <rpm/rpmfileutil.h>
#include <rpm/rpmmacro.h>
#include <rpm/rpmbase64.h>

#include "lib/rpmte_internal.h"
#include "lib/rpmts_internal.h"	/* rpmtsSELabelFoo() */

rpmPluginHook PLUGIN_HOOKS = \
	PLUGINHOOK_INIT | \
	PLUGINHOOK_CLEANUP | \
	PLUGINHOOK_OPENTE | \
	PLUGINHOOK_COLL_POST_ADD | \
	PLUGINHOOK_COLL_PRE_REMOVE;

typedef enum sepolAction {
    SEPOL_ACTION_IGNORE,
    SEPOL_ACTION_INSTALL,
    SEPOL_ACTION_REMOVE
} sepolAction;

typedef struct sepol {
    char *data;			/*!< policy data */
    char *name;			/*!< policy names */
    ARGV_t types;		/*!< policy types */
    uint32_t flags;		/*!< policy flags */
    sepolAction action;		/*!< install/remove/ignore */
    struct sepol *next;		/*!< next in linked list */
} sepol;

typedef struct sepoltrans {
    int execsemodule;		/*!< 0 = use libsemanage to install policy; non-zero = use semodule */
    semanage_handle_t *sh;	/*!< handle to libsemanage, only used when execsemodule is zero */
    char *semodulepath;		/*!< path to semodule binary */
    ARGV_t semodargs;		/*!< argument list to pass to semodule, only used when execsemodule is non-zero */
    ARGV_t filelist;		/*!< list of temporary files that have been written to disk during the transaction */
    int changes;		/*!< number of changes made during the transaction */
} sepoltrans;


static char * name;
static rpmts ts;

static sepol * policiesHead;
static sepol * policiesTail;


static sepol *sepolNew(rpmte te);
static sepol *sepolFree(sepol * pol);
static int sepolHasType(const sepol * pol, const char *type);

static rpmRC sepolPreparePolicies(sepol * pols, const char *policytype);
static rpmRC sepolWritePolicy(const sepol * pol, char **path);
static rpmRC sepolLoadPolicies(const sepol * pols);

static sepoltrans *sepoltransNew(void);
static sepoltrans *sepoltransFree(sepoltrans * pt);

static rpmRC sepoltransInstall(sepoltrans * pt, const sepol * pol);
static rpmRC sepoltransRemove(sepoltrans * pt, const sepol * pol);
static rpmRC sepoltransCommit(sepoltrans * pt);


static sepol *sepolNew(rpmte te)
{
    sepol *head = NULL;
    sepol *ret = NULL;
    sepolAction action;
    Header h;
    struct rpmtd_s policies, names, types, typesidx, flags;
    int i, j;
    int count;

    rpmtdReset(&policies);
    rpmtdReset(&names);
    rpmtdReset(&types);
    rpmtdReset(&typesidx);
    rpmtdReset(&flags);

    h = rpmteHeader(te);
    if (!h) {
	goto exit;
    }

    if (!headerIsEntry(h, RPMTAG_POLICIES)) {
	goto exit;
    }

    if (!headerGet(h, RPMTAG_POLICIES, &policies, HEADERGET_MINMEM)) {
	goto exit;
    }

    count = rpmtdCount(&policies);
    if (count <= 0) {
	goto exit;
    }

    if (!headerGet(h, RPMTAG_POLICYNAMES, &names, HEADERGET_MINMEM)
	|| rpmtdCount(&names) != count) {
	goto exit;
    }

    if (!headerGet(h, RPMTAG_POLICYFLAGS, &flags, HEADERGET_MINMEM)
	|| rpmtdCount(&flags) != count) {
	goto exit;
    }

    if (!headerGet(h, RPMTAG_POLICYTYPES, &types, HEADERGET_MINMEM)) {
	goto exit;
    }

    if (!headerGet(h, RPMTAG_POLICYTYPESINDEXES, &typesidx, HEADERGET_MINMEM)
	|| rpmtdCount(&types) != rpmtdCount(&typesidx)) {
	goto exit;
    }

    action = (rpmteType(te) == TR_ADDED) ? SEPOL_ACTION_INSTALL : SEPOL_ACTION_REMOVE;

    for (i = 0; i < count; i++) {
	sepol *pol = xcalloc(1, sizeof(*pol));
	pol->next = head;
	head = pol;

	pol->data = xstrdup(rpmtdNextString(&policies));
	pol->name = xstrdup(rpmtdNextString(&names));
	pol->flags = *rpmtdNextUint32(&flags);
	pol->action = action;

	for (j = 0; j < rpmtdCount(&types); j++) {
	    uint32_t index = ((uint32_t *) typesidx.data)[j];
	    if (index < 0 || index >= count) {
		goto exit;
	    }
	    if (index != i) {
		continue;
	    }
	    argvAdd(&pol->types, rpmtdNextString(&types));
	}
	argvSort(pol->types, NULL);
    }

    ret = head;

  exit:
    headerFree(h);

    rpmtdFreeData(&policies);
    rpmtdFreeData(&names);
    rpmtdFreeData(&types);
    rpmtdFreeData(&typesidx);
    rpmtdFreeData(&flags);

    if (!ret) {
	sepolFree(head);
    }

    return ret;
}

static sepol *sepolFree(sepol * pol)
{
    while (pol) {
	sepol *next = pol->next;

	pol->data = _free(pol->data);
	pol->name = _free(pol->name);
	pol->types = argvFree(pol->types);
	pol->next = NULL;
	_free(pol);

	pol = next;
    }

    return NULL;
}

int sepolHasType(const sepol * pol, const char *type)
{
    if (!pol || !type) {
	return 0;
    }

    return (argvSearch(pol->types, type, NULL) != NULL) ||
	   (argvSearch(pol->types, RPMPOL_TYPE_DEFAULT, NULL) != NULL);
}

static rpmRC sepolPreparePolicies(sepol * pols, const char *policytype)
{
    sepol *pol;
    rpmRC rc = RPMRC_OK;

    for (pol = pols; pol; pol = pol->next) {
	if (!sepolHasType(pol, policytype)) {
	    pol->action = SEPOL_ACTION_IGNORE;
	}
    }

    return rc;
}

static rpmRC sepolWritePolicy(const sepol * pol, char **path)
{
    char *tmppath = NULL;
    FD_t fd = NULL;
    char *policy = NULL;
    size_t policylen;
    rpmRC rc = RPMRC_FAIL;

    if (rpmBase64Decode(pol->data, (void **) &policy, &policylen) != 0) {
	rpmlog(RPMLOG_ERR, _("Failed to decode policy for %s\n"),
	       pol->name);
	goto exit;
    }

    fd = rpmMkTempFile(NULL, &tmppath);
    if (fd == NULL || Ferror(fd)) {
	rpmlog(RPMLOG_ERR, _("Failed to create temporary file for %s: %s\n"),
	       pol->name, strerror(errno));
	goto exit;
    }

    if (!Fwrite(policy, sizeof(*policy), policylen, fd)) {
	rpmlog(RPMLOG_ERR, _("Failed to write %s policy to file %s\n"),
	       pol->name, tmppath);
	goto exit;
    }

    *path = tmppath;
    rc = RPMRC_OK;

  exit:
    if (fd)
	Fclose(fd);
    _free(policy);
    if (rc != RPMRC_OK)
	_free(tmppath);

    return rc;
}

static rpmRC sepolLoadPolicies(const sepol * pols)
{
    const sepol *pol;
    rpmRC rc = RPMRC_FAIL;
    sepoltrans *pt = sepoltransNew();

    if (pt == NULL)
	goto exit;

    for (pol = pols; pol; pol = pol->next) {
	switch (pol->action) {
	case SEPOL_ACTION_REMOVE:
	    rc = sepoltransRemove(pt, pol);
	    break;
	case SEPOL_ACTION_INSTALL:
	    rc = sepoltransInstall(pt, pol);
	    break;
	case SEPOL_ACTION_IGNORE:
	default:
	    rc = RPMRC_OK;
	    break;
	}

	if (rc != RPMRC_OK)
	    goto exit;
    }

    rc = sepoltransCommit(pt);

exit:
    sepoltransFree(pt);

    return rc;
}

static sepoltrans *sepoltransNew(void)
{
    sepoltrans *pt = xcalloc(1, sizeof(*pt));
    pt->semodulepath = rpmExpand("%{__semodule}", NULL);
    pt->execsemodule = (!rpmChrootDone() && access(pt->semodulepath, X_OK) == 0);
    pt->changes = 0;

    if (pt->execsemodule) {
	argvAdd(&pt->semodargs, "semodule");
    } else {
	pt->sh = semanage_handle_create();
	if (!pt->sh) {
	    rpmlog(RPMLOG_ERR, _("Failed to create semanage handle\n"));
	    goto err;
	}
	semanage_set_create_store(pt->sh, 1);
	semanage_set_check_contexts(pt->sh, 0);
	if (semanage_connect(pt->sh) < 0) {
	    rpmlog(RPMLOG_ERR, _("Failed to connect to policy handler\n"));
	    goto err;
	}
	if (semanage_begin_transaction(pt->sh) < 0) {
	    rpmlog(RPMLOG_ERR, _("Failed to begin policy transaction: %s\n"),
		   errno ? strerror(errno) : "");
	    goto err;
	}
	semanage_set_reload(pt->sh, !rpmChrootDone());
    }

    return pt;

  err:
    if (pt->sh) {
	if (semanage_is_connected(pt->sh)) {
	    semanage_disconnect(pt->sh);
	}
	semanage_handle_destroy(pt->sh);
    }
    free(pt);

    return NULL;
}

static sepoltrans *sepoltransFree(sepoltrans * pt)
{
    ARGV_t file;

    if (!pt) {
	return NULL;
    }

    for (file = pt->filelist; file && *file; file++) {
	if (unlink(*file) < 0) {
	    rpmlog(RPMLOG_WARNING, _("Failed to remove temporary policy file %s: %s\n"),
		   *file, strerror(errno));
	}
    }
    argvFree(pt->filelist);

    if (pt->execsemodule) {
	argvFree(pt->semodargs);
    } else {
	semanage_disconnect(pt->sh);
	semanage_handle_destroy(pt->sh);
    }

    free(pt->semodulepath);
    memset(pt, 0, sizeof(*pt)); /* trash and burn */

    free(pt);
    return NULL;
}

static rpmRC sepoltransInstall(sepoltrans * pt, const sepol * pol)
{
    rpmRC rc = RPMRC_OK;
    char *path = NULL;

    rc = sepolWritePolicy(pol, &path);
    if (rc != RPMRC_OK) {
	return rc;
    }
    argvAdd(&pt->filelist, path);

    if (pt->execsemodule) {
	const char *flag = (pol->flags & RPMPOL_FLAG_BASE) ? "-b" : "-i";
	if (argvAdd(&pt->semodargs, flag) < 0 || argvAdd(&pt->semodargs, path) < 0) {
	    rc = RPMRC_FAIL;
	}
    } else {
	if (pol->flags & RPMPOL_FLAG_BASE) {
	    if (semanage_module_install_base_file(pt->sh, path) < 0) {
		rc = RPMRC_FAIL;
	    }
	} else {
	    if (semanage_module_install_file(pt->sh, path) < 0) {
		rc = RPMRC_FAIL;
	    }
	}
    }

    if (rc != RPMRC_OK) {
	rpmlog(RPMLOG_ERR, _("Failed to install policy module: %s (%s)\n"),
	       pol->name, path);
    } else {
	pt->changes++;
    }

    _free(path);

    return rc;
}

static rpmRC sepoltransRemove(sepoltrans * pt, const sepol * pol)
{
    rpmRC rc = RPMRC_OK;

    if (pol->flags & RPMPOL_FLAG_BASE) {
	return RPMRC_FAIL;
    }

    if (pt->execsemodule) {
	if (argvAdd(&pt->semodargs, "-r") < 0 || argvAdd(&pt->semodargs, pol->name) < 0) {
	    rc = RPMRC_FAIL;
	}
    } else {
	if (semanage_module_remove(pt->sh, (char *) pol->name) < 0) {
	    rc = RPMRC_FAIL;
	}
    }

    if (rc != RPMRC_OK) {
	rpmlog(RPMLOG_ERR, _("Failed to remove policy module: %s\n"),
	       pol->name);
    } else {
	pt->changes++;
    }

    return rc;
}

static rpmRC sepoltransCommit(sepoltrans * pt)
{
    rpmRC rc = RPMRC_OK;

    if (pt->changes == 0) {
	return rc;
    }

    if (pt->execsemodule) {
	int status;
	pid_t pid = fork();
	int fd;

	switch (pid) {
	case -1:
	    rpmlog(RPMLOG_ERR, _("Failed to fork process: %s\n"),
		   strerror(errno));
	    rc = RPMRC_FAIL;
	    break;
	case 0:
	    fd = open("/dev/null", O_RDWR);
	    dup2(fd, STDIN_FILENO);
	    dup2(fd, STDOUT_FILENO);
	    dup2(fd, STDERR_FILENO);
	    execv(pt->semodulepath, pt->semodargs);
	    rpmlog(RPMLOG_ERR, _("Failed to execute %s: %s\n"),
		   pt->semodulepath, strerror(errno));
	    exit(1);
	default:
	    waitpid(pid, &status, 0);
	    if (!WIFEXITED(status)) {
		rpmlog(RPMLOG_ERR, _("%s terminated abnormally\n"),
		       pt->semodulepath);
		rc = RPMRC_FAIL;
	    } else if (WEXITSTATUS(status)) {
		rpmlog(RPMLOG_ERR, _("%s failed with exit code %i\n"),
		       pt->semodulepath, WEXITSTATUS(status));
		rc = RPMRC_FAIL;
	    }
	}
    } else {
	if (semanage_commit(pt->sh) < 0) {
	    rpmlog(RPMLOG_ERR, _("Failed to commit policy changes\n"));
	    rc = RPMRC_FAIL;
	}
    }

    return rc;
}

static rpmRC sepolRelabelFiles(void)
{
    rpmRC rc = RPMRC_OK;
    pid_t pid;
    int fd;
    int status;
    char *restoreconPath = rpmExpand("%{__restorecon}", NULL);

    if (!restoreconPath) {
	rpmlog(RPMLOG_ERR, _("Failed to expand restorecon path"));
	return RPMRC_FAIL;
    }

    /* execute restorecon -R / */
    pid = fork();
    switch (pid) {
    case -1:
	rpmlog(RPMLOG_ERR, _("Failed to fork process: %s\n"),
	       strerror(errno));
	rc = RPMRC_FAIL;
	break;
    case 0:
	fd = open("/dev/null", O_RDWR);
	dup2(fd, STDIN_FILENO);
	dup2(fd, STDOUT_FILENO);
	dup2(fd, STDERR_FILENO);
	execl(restoreconPath, "restorecon", "-R", "/", NULL);
	rpmlog(RPMLOG_ERR, _("Failed to execute %s: %s\n"), restoreconPath,
	       strerror(errno));
	exit(1);
    default:
	waitpid(pid, &status, 0);
	if (!WIFEXITED(status)) {
	    rpmlog(RPMLOG_ERR, _("%s terminated abnormally\n"),
		   restoreconPath);
	    rc = RPMRC_FAIL;
	} else if (WEXITSTATUS(status)) {
	    rpmlog(RPMLOG_ERR, _("%s failed with exit code %i\n"),
		   restoreconPath, WEXITSTATUS(status));
	    rc = RPMRC_FAIL;
	}
    }

    _free(restoreconPath);

    return rc;
}

static rpmRC sepolGo(void)
{
    semanage_handle_t *sh;
    int existingPolicy;
    char *policytype = NULL;
    rpmRC rc = RPMRC_FAIL;

    static int performed = 0;
    if (performed) {
	return RPMRC_OK;
    }
    performed = 1;

    if (rpmChrootIn()) {
	goto exit;
    }

    if (selinux_getpolicytype(&policytype) < 0) {
	goto exit;
    }

    sepolPreparePolicies(policiesHead, policytype);

    /* determine if this is the first time installing policy */
    sh = semanage_handle_create();
    existingPolicy = (semanage_is_managed(sh) == 1);
    semanage_handle_destroy(sh);

    /* now load the policies */
    rc = sepolLoadPolicies(policiesHead);

    /* re-init selinux and re-read the files contexts, since things may have changed */
    selinux_reset_config();
    if (!(rpmtsFlags(ts) & RPMTRANS_FLAG_NOCONTEXTS)) {
	if (rpmtsSELabelInit(ts, 0) == RPMRC_OK) {
	    /* if this was the first time installing policy, every package before
	     * policy was installed will be mislabeled (e.g. semodule). So, relabel
	     * the entire filesystem if this is the case */
	    if (!existingPolicy) {
		if (sepolRelabelFiles() != RPMRC_OK) {
		    rpmlog(RPMLOG_WARNING, _("Failed to relabel filesystem. Files may be mislabeled\n"));
		}
	    }
	} else {
	    rpmlog(RPMLOG_WARNING, _("Failed to reload file contexts. Files may be mislabeled\n"));
	}
    }

  exit:
    if (rpmChrootOut()) {
	rc = RPMRC_FAIL;
    }

    _free(policytype);

    return rc;
}

static rpmRC sepolAddTE(rpmte te)
{
    sepol *pol;
    sepol *polTail;

    if (!rpmteHasCollection(te, name)) {
	return RPMRC_OK;
    }

    pol = sepolNew(te);
    if (!pol) {
	/* something's wrong with the policy information, either missing or
	 * corrupt. abort */
	rpmlog(RPMLOG_ERR, _("Failed to extract policy from %s\n"),
	       rpmteNEVRA(te));
	return RPMRC_FAIL;
    }

    /* find the tail of pol */
    polTail = pol;
    while (polTail->next) {
	polTail = polTail->next;
    }

    /* add the new policy to the list */
    if (!policiesHead) {
	policiesHead = pol;
	policiesTail = polTail;
    } else {
	if (rpmteType(te) == TR_ADDED) {
	    /* add to the end of the list */
	    policiesTail->next = pol;
	    policiesTail = polTail;
	} else {
	    /* add to the beginning of the list */
	    polTail->next = policiesHead;
	    policiesHead = pol;
	}
    }

    return RPMRC_OK;
}

#endif	/* WITH_SELINUX */


rpmRC PLUGINHOOK_INIT_FUNC(rpmts _ts, const char *_name, const char *_opts)
{
#if WITH_SELINUX
    ts = _ts;
    name = strdup(_name);
    policiesHead = policiesTail = NULL;
#endif
    return RPMRC_OK;
}

rpmRC PLUGINHOOK_CLEANUP_FUNC(void)
{
#if WITH_SELINUX
    _free(name);
    ts = NULL;
    policiesHead = policiesTail = sepolFree(policiesHead);
#endif
    return RPMRC_OK;
}

rpmRC PLUGINHOOK_OPENTE_FUNC(rpmte te)
{
    rpmRC rc = RPMRC_OK;
#if WITH_SELINUX
    rc = sepolAddTE(te);
#endif
    return rc;
}

rpmRC PLUGINHOOK_COLL_POST_ADD_FUNC(void)
{
    rpmRC rc = RPMRC_OK;
#if WITH_SELINUX
    rc = sepolGo();
#endif
    return rc;
}

rpmRC PLUGINHOOK_COLL_PRE_REMOVE_FUNC(void)
{
    rpmRC rc = RPMRC_OK;
#if WITH_SELINUX
    rc = sepolGo();
#endif
    return rc;
}
