/* This file is part of Mailfromd.
   Copyright (C) 2007-2020 Sergey Poznyakoff

   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 3, 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, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <unistd.h>
#include <mailutils/assoc.h>
#include <mailutils/errno.h>
#include <mailutils/error.h>
#include <mailutils/errno.h>
#include <mailutils/nls.h>
#include <mailutils/list.h>
#include <mailutils/iterator.h>
#include <mailutils/alloc.h>
#include <sysexits.h>
#include "libmf.h"

#define NGIDSINBLOCK 16

struct mf_gid_block {
	struct mf_gid_block *next;
	size_t count;
	size_t size;
	gid_t gid[1];
};
	
struct mf_gid_list {
	struct mf_gid_block *head, *tail;
	int sorted;
};

struct mf_gid_list *
mf_gid_list_alloc() 
{
	struct mf_gid_list *p = mu_alloc(sizeof(*p));
	p->head = p->tail = NULL;
	p->sorted = 0;
	return p;
}

void
mf_gid_list_free(struct mf_gid_list *gl)
{
	struct mf_gid_block *p;
	if (!gl)
		return;
	for (p = gl->head; p; ) {
		struct mf_gid_block *next = p->next;
		free(p);
		p = next;
	}
}

static void
mf_gid_list_add_block(struct mf_gid_list *gl)
{
	struct mf_gid_block *p = mu_alloc(sizeof(*p) + NGIDSINBLOCK);
	p->next = NULL;
	p->size = NGIDSINBLOCK;
	p->count = 0;
	if (gl->tail)
		gl->tail->next = p;
	else
		gl->head = p;
	gl->tail = p;
}
	
void
mf_gid_list_add(struct mf_gid_list *gl, gid_t gid)
{
	if (!gl->tail || gl->tail->count == gl->tail->size)
		mf_gid_list_add_block(gl);
	gl->tail->gid[gl->tail->count++] = gid;
	gl->sorted = 0;
}

struct mf_gid_list *
mf_gid_list_dup(struct mf_gid_list *src)
{
	struct mf_gid_list *dst = mf_gid_list_alloc();
	struct mf_gid_block *p;

	if (src) { 
		for (p = src->head; p; p = p->next) {
			size_t i;
			for (i = 0; i < p->count; i++)
				mf_gid_list_add(dst, p->gid[i]);
		}
	}
	
	return dst;
}

static int
gid_cmp(const void *a, const void *b)
{
	gid_t const *ga = a;
	gid_t const *gb = b;

	if (*ga < *gb)
		return -1;
	if (*ga > *gb)
		return 1;
	return 0;
}

void
mf_gid_list_array(struct mf_gid_list *gl, size_t *gc, gid_t **gv)
{
	struct mf_gid_block *p;
	size_t i, j;
	
	if (!gl || gl->head == NULL) {
		*gc = 0;
		*gv = NULL;
		return;
	}

	p = gl->head;
	if (p->next) {
		size_t n;
		struct mf_gid_block *q;
	
		for (n = 0, p = gl->head; p; p = p->next)
			n += p->count;
		p = mu_realloc(gl->head, sizeof(*p) + n);
		gl->head = p;
		n = p->count;
		for (q = p->next; q; ) {
			struct mf_gid_block *next = q->next;
			size_t i;
			for (i = 0; i < q->count; i++)
				p->gid[n++] = q->gid[i];
			free(q);
			q = next;
		}
		p->count = n;
		p->next = NULL;
	}

	if (!gl->sorted) {
		qsort(p->gid, p->count, sizeof(p->gid[0]), gid_cmp);

		for (i = j = 1; i < p->count; j++)
			if (p->gid[j-1] != p->gid[j])
				p->gid[i++] = p->gid[j];
		p->count = i;
		gl->sorted = 1;
	}
	
	*gc = p->count;
	*gv = p->gid;
}
	
void
get_user_groups(struct mf_gid_list *gl, const char *user)
{
	struct group *gr;
	
	setgrent();
	while ((gr = getgrent())) {
		char **p;
		for (p = gr->gr_mem; *p; p++)
			if (strcmp(*p, user) == 0) {
				mf_gid_list_add(gl, gr->gr_gid);
			}
	}
	endgrent();
}

/* Switch to the given UID/GID */
int
switch_to_privs(uid_t uid, gid_t gid, struct mf_gid_list *retain_groups)
{
	int rc = 0;
	struct mf_gid_list *gl;
	size_t gc;
	gid_t *gv;

	if (uid == 0) {
		mu_error(_("refusing to run as root"));
		return 1;
	}

	/* Create a list of supplementary groups */
	gl = mf_gid_list_dup(retain_groups);
	mf_gid_list_add(gl, gid ? gid : getegid());
	mf_gid_list_array(gl, &gc, &gv);

	/* Reset group permissions */
	if (geteuid() == 0 && setgroups(gc, gv)) {
		mu_error(_("setgroups failed: %s"),
			 mu_strerror(errno));
		rc = 1;
	}
	mf_gid_list_free(gl);
	
	/* Switch to the user's gid. On some OSes the effective gid must
	   be reset first */

#if defined(HAVE_SETEGID)
	if ((rc = setegid(gid)) < 0)
		mu_error(_("setegid(%lu) failed: %s"),
			 (unsigned long) gid, mu_strerror(errno));
#elif defined(HAVE_SETREGID)
	if ((rc = setregid(gid, gid)) < 0)
		mu_error(_("setregid(%lu,%lu) failed: %s"),
		         (unsigned long) gid, (unsigned long) gid,
			 mu_strerror(errno));
#elif defined(HAVE_SETRESGID)
	if ((rc = setresgid(gid, gid, gid)) < 0)
		mu_error(_("setresgid(%lu,%lu,%lu) failed: %s"),
		         (unsigned long) gid,
		         (unsigned long) gid,
		         (unsigned long) gid,
			 mu_strerror(errno));
#endif

	if (rc == 0 && gid != 0) {
		if ((rc = setgid(gid)) < 0 && getegid() != gid) 
			mu_error(_("setgid(%lu) failed: %s"),
				 (unsigned long) gid, mu_strerror(errno));
		if (rc == 0 && getegid() != gid) {
			mu_error(_("cannot set effective gid to %lu"),
			         (unsigned long) gid);
			rc = 1;
		}
	}

	/* Now reset uid */
	if (rc == 0 && uid != 0) {
		uid_t euid;

		if (setuid(uid)
		    || geteuid() != uid
		    || (getuid() != uid
			&& (geteuid() == 0 || getuid() == 0))) {
			
#if defined(HAVE_SETREUID)
			if (geteuid() != uid) {
				if (setreuid(uid, -1) < 0) { 
					mu_error(_("setreuid(%lu,-1) failed: %s"),
					         (unsigned long) uid,
						 mu_strerror(errno));
					rc = 1;
				}
				if (setuid(uid) < 0) {
					mu_error(_("second setuid(%lu) failed: %s"),
					         (unsigned long) uid,
						 mu_strerror(errno));
					rc = 1;
				}
			} else
#endif
				{
					mu_error(_("setuid(%lu) failed: %s"),
					         (unsigned long) uid,
						 mu_strerror(errno));
					rc = 1;
				}
		}
	
		euid = geteuid();
		if (uid != 0 && setuid(0) == 0) {
			mu_error(_("seteuid(0) succeeded when it should not"));
			rc = 1;
		} else if (uid != euid && setuid(euid) == 0) {
			mu_error(_("cannot drop non-root setuid privileges"));
			rc = 1;
		}

	}

	return rc;
}


static int
translate_item (void *item, void *data)
{
	struct mf_gid_list *dst = data;
	struct group *group = getgrnam(item);
	if (!group) {
		mu_error(_("unknown group: %s"), (char*) item);
		return 1;
	}
	mf_gid_list_add(dst, group->gr_gid);
	return 0;
}

static struct mf_gid_list *
grouplist_translate(mu_list_t src)
{
	struct mf_gid_list *dst = mf_gid_list_alloc();
	mu_list_foreach(src, translate_item, dst);
	return dst;
}
	
void
mf_priv_setup(struct mf_privs *privs)
{
	struct passwd *pw;
	struct mf_gid_list *grp;

	if (!privs || !privs->user)
		return;

	pw = getpwnam(privs->user);
	if (!pw) {
		mu_error(_("no such user: %s"), privs->user);
		exit(EX_CONFIG);
	}

	grp = grouplist_translate(privs->groups);
	if (privs->allgroups)
		get_user_groups(grp, privs->user);
	if (switch_to_privs(pw->pw_uid, pw->pw_gid, grp))
		exit(EX_SOFTWARE);
	mf_gid_list_free(grp);
}


void
mf_epriv_setup(struct mf_privs *privs)
{
	uid_t uid;
	gid_t gid;
	
	if (privs) {
		struct passwd *pw;
		if (!privs->user)
			return;

		pw = getpwnam(privs->user);
		if (!pw) {
			mu_error(_("No such user: %s"), privs->user);
			exit (EX_CONFIG);
		}
		uid = pw->pw_uid;
		gid = pw->pw_gid;
	} else {
		uid = 0;
		gid = 0;
	}
	
	if (setegid(gid)) {
		mu_error(_("cannot switch to EGID %lu: %s"),
			 (unsigned long) gid, mu_strerror(errno));
		exit(EX_USAGE);
	}
	if (seteuid(uid)) {
		mu_error(_("cannot switch to EUID %lu: %s"),
			 (unsigned long) uid, mu_strerror(errno));
		exit(EX_USAGE);
	}
}