/* vim: set sw=4 sts=4 : */

/* Copyright (c) 2007-2008, David B. Cortarello
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *   * Neither the name of Kwort nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
*/

/* Thanks to euklid for the #ifdef __linux__, so now it works in bsd too ;) */

#ifdef __linux__
#define _XOPEN_SOURCE
#include <shadow.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include "errors.h"

struct group *fgetgrent(FILE * stream);
struct passwd *fgetpwent(FILE * stream);
int setgroups(size_t size, const gid_t * list);

char *get_real_name(const char *command)
{
	char *real_PATH, *path, *subtoken, *PATH;
	int fd, i = 0, path_len;

	if ((fd = open(command, O_RDONLY)) != -1) {
		close(fd);
		if (*command == '/')
			return (char *)command;
		else {
			path = calloc(2 + strlen(command), sizeof(char));
			sprintf(path, "./%s", command);
			return path;
		}
	}
	real_PATH = getenv("PATH");
	PATH = calloc(sizeof(char), strlen(real_PATH) + 33);
	sprintf(PATH, "%s:%s", real_PATH, "/usr/sbin:/usr/local/sbin:/sbin");
	if (PATH == NULL)
		return NULL;
	else {
		path_len = strlen(PATH);
		while ((subtoken = strtok(PATH + i, ":")) != NULL && i < path_len) {
			path = calloc(strlen(command) + 2 + strlen(subtoken), sizeof(char));
			sprintf(path, "%s/%s", subtoken, command);
			if ((fd = open(path, O_RDONLY)) != -1) {
				close(fd);
				return path;
			}
			else {
				free(path);
				i += strlen(subtoken) + 1;
			}
		}
	}
	free(PATH);
	return NULL;
}

inline int *fixed_realloc(int *ptr, int actual, int size)
{
	int *new, i;

	if (!size && ptr != NULL)
		free(ptr);
	if (!actual)
		return calloc(sizeof(int), size);
	new = calloc(sizeof(int), size);
	for (i = 0; i < (actual < size ? actual : size); i++)
		new[i] = ptr[i];
	free(ptr);
	return new;
}

int set_additional_g(const char *username)
{
	int i, x = 0;
	int *groups = NULL;
	struct group *ag;
	FILE *file;

	if ((file = fopen("/etc/group", "r")) == NULL)
		return ERR_OPEN_FILE_GROUP;
	while ((ag = fgetgrent(file)) != NULL) {
		for (i = 0; ag->gr_mem[i]; i++) {
			if (!strcmp(ag->gr_mem[i], username)) {
				x += 1;
				groups = fixed_realloc(groups, x - 1, x);
				groups[x - 1] = ag->gr_gid;
				break;
			}
		}
	}
	fclose(file);
	if (x) {
		if (setgroups(x, groups))
			return ERR_FAILED_ADDITIONAL_GIDS;
		free(groups);
	}
	return ERR_SUCCESS;
}

int make_me_new(const char *username, const char *password, int chk_passwd)
{
	struct passwd *pw;
	struct spwd *sp;
	const char *correct;
	FILE *file;
	char *encrypted;
	int error;

	if ((file = fopen("/etc/passwd", "r")) == NULL)
		return ERR_OPEN_FILE_PASSWD;
	while ((pw = fgetpwent(file)) != NULL)
		if (!strcmp(pw->pw_name, username))
			break;
	fclose(file);
	if (pw == NULL)
		return ERR_INVALID_USERNAME;
	if (chk_passwd) {
#ifdef __linux__
		sp = getspnam(pw->pw_name);
		if (sp)
			correct = sp->sp_pwdp;
		else
#endif
			correct = pw->pw_passwd;
		if ((encrypted = crypt(password, correct)) == NULL)
			return ERR_FAILED_CRYPT;
		if (strcmp(encrypted, correct))
			return ERR_WRONG_PASSWD;
	}
	if ((error = set_additional_g(username)) != ERR_SUCCESS)
		return error;
	if (setgid(pw->pw_gid))
		return ERR_FAILED_SET_GID;
	if (setuid(pw->pw_uid))
		return ERR_FAILED_SET_UID;
	return ERR_SUCCESS;
}

int try(const char *username, const char *password, int argc, char **argv)
{
	char *real_name;
	int error;

	if ((error = make_me_new(username, password, 1)) != ERR_SUCCESS)
		return error;
	if ((real_name = get_real_name(argv[0])) == NULL)
		return ERR_COMMAND_NOT_FOUND;
	argv[0] = real_name;
	if (execvp(real_name, argv) == -1) ;
	return ERR_PERMISSION_DENIED;
	return ERR_SUCCESS;
}

int check_user(const char *username)
{
	struct passwd *pw;
	FILE *file;

	if ((file = fopen("/etc/passwd", "r")) == NULL)
		return ERR_OPEN_FILE_PASSWD;
	while ((pw = fgetpwent(file)) != NULL)
		if (!strcmp(pw->pw_name, username))
			break;
	fclose(file);
	if (pw == NULL)
		return ERR_INVALID_USERNAME;
	return ERR_SUCCESS;
}
