/*
 * - Virtual Filesystem Root
 *
 * Copyright (c) 2009-2018
 *  AVM GmbH, Berlin, Germany
 *
 * License: Free, use with no restriction.
 */


#ifdef HAVE_CONFIG_H
# include <autoconfig.h>
#endif

#if !defined (__GNUC__) && defined (_AIX)
#pragma alloca
#endif
#ifndef alloca /* Make alloca work the best possible way.  */
# ifdef __GNUC__
#  define alloca __builtin_alloca
# else /* not __GNUC__ */
#  if HAVE_ALLOCA_H
#   include <alloca.h>
#  else /* not __GNUC__ or HAVE_ALLOCA_H */
#    ifndef _AIX /* Already did AIX, up at the top.  */
char *alloca ();
#    endif /* not _AIX */
#  endif /* not HAVE_ALLOCA_H */
# endif /* not __GNUC__ */
#endif /* not alloca */

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <utime.h>
#ifdef HAVE_SYS_WAIT_H
#  include <sys/wait.h>
#endif


#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <ftw.h>
/*
 * musl did not define values for FTW_ACTIONRETVAL
 */
#ifndef FTW_CONTINUE
#define FTW_CONTINUE 0
#define FTW_STOP 1
#endif

#include <getopt.h>
#include <limits.h>
#include <netdb.h>
#include <setjmp.h>
#include <signal.h>
#include <grp.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#ifdef TIME_WITH_SYS_TIME
#  include <sys/time.h>
#  include <time.h>
#else
#  ifdef HAVE_SYS_TIME_H
#    include <sys/time.h>
#  else
#    include <time.h>
#  endif
#endif
#include <unistd.h>
#ifdef HAVE_MMAP
#include <sys/mman.h>
#endif

#include <stdarg.h>


#include "virtual.h"
#include "avm_acl.h"

// #include "extern.h"
#if 0
#ifdef __GNUC__
static void _fLog(const char *fmt, ...) __attribute__ ((__format__(__printf__, 1, 2)));
#endif
static void _fLog(const char *fmt, ...)
{
	FILE *fp = fopen("/dev/console", "w");
//#define ACL_DEBUG_2STDOUT
#ifdef ACL_DEBUG_2STDOUT
	if (!fp) fp = stdout;
#else
	if (!fp) fp = fopen("/var/tmp/acl.debug", "a"); // NOTE that you have to "chmod 777 /var/tmp" before
#endif
	if (fp) {
		va_list ap;
		fprintf(fp, "virtual(%d): ", getpid());
		va_start(ap, fmt);
		vfprintf(fp, fmt, ap);
		va_end(ap);
		fprintf(fp, "\n");
		fclose(fp);
	}
}
#define Log(x) _fLog x
#else
#define Log(x)
#endif

static char *g_real_root = 0; // absolute linux path that is the virtual "/"
static int g_real_root_len;

#define NTMPSTRING 16
static char *tmps[NTMPSTRING] = { NULL, };

#if defined(COMPILE_HOSTTOOLS)
void virtual_freetmpstring(void)
{
	for (int i = 0; i < NTMPSTRING; i++) {
		free(tmps[i]);
		tmps[i] = NULL;
	}
}
#endif

static char *tmpstring(size_t siz)
{
	static int pos = 0;
	char *p;

	free(tmps[pos]);

	p = tmps[pos] = malloc(siz); /* TODO well known memory leak, until program exit(3) */

	pos++; if (pos == NTMPSTRING) pos = 0;

	return p;
}


#if 0 /* not used */
static int virt_dir_exists(const char *abs_virt_path, const char *dirname)
{
	if (!g_real_root) return 0;
	size_t len = g_real_root_len + strlen(abs_virt_path) + 1 + strlen(dirname) + 1;
	char *path = tmpstring(len);

	if (!path) return 0;

	snprintf(path, len, "%s%s/%s", g_real_root, abs_virt_path, dirname);

	struct stat sb;
	if (stat(path, &sb)) return 0;
	return sb.st_mode & S_IFDIR;
}
#endif /* 0 */

static char *virtual_to_real_path(const char *path)
{
	char *real;
	size_t len;

	if (!g_real_root) return NULL;

	if (*path == '/' && strcmp(g_real_root, "/")) {
		// absolute client path -> change virt. to real root

		len = strlen(path) + g_real_root_len + 1;
		real = tmpstring(len);
		if (!real) return NULL;
		if (strcmp(path, "/")) {
			snprintf(real, len, "%s%s", g_real_root, path);
		} else {
			snprintf(real, len, "%s", g_real_root);
		}
	} else {
		// relative virtual paths are autom.
		// relative real paths
		len = strlen(path) + 1;
		real = tmpstring(len);
		if (!real) return NULL;
		snprintf(real, len, "%s", path);
	}
	return real;
}

// change the real path to a virtual client path
void virtual_make_from_real_path(char **ppath)
{
	if (!g_real_root) return;
	if (!strcmp(g_real_root, "/")) return;

	if (*ppath == strstr(*ppath, g_real_root)) {
		char *p;
		size_t diff = strlen(*ppath) - g_real_root_len;
		size_t len;
		if (diff) {
			len = diff + 1;
			p = tmpstring(len);
			if (!p) return;
			snprintf(p, len, "%s", (*ppath) + g_real_root_len);
		} else {
			len = 2;
			p = tmpstring(len);
			if (!p) return;
			snprintf(p, len, "/");
		}
		*ppath = p;
	}
}

/* ------------------------------------------------------------------------ */
/* ------------------------------------------------------------------------ */

static int IsAllowed(const char *path, char **preal_path, int *pwrite_access)
{
	*preal_path = virtual_to_real_path(path);
	if (*preal_path) {
		if (acl_is_allowed_path(*preal_path, pwrite_access)) {
			Log(("%s %s real_path=%s is allwed write_access=%d", __FILE__, __FUNCTION__, *preal_path, *pwrite_access));
			return 1;
		} else {
			Log(("%s %s not allowed to access real_path=%s", __FILE__, __FUNCTION__, *preal_path));
		}
	} else {
		Log(("%s %s no real_path for %s", __FILE__, __FUNCTION__, path));
	}

	errno = EACCES;
	return 0;
}

int virtual_is_write_access(const char *path, char **preal_path)
{
	int write_access;
	if (!IsAllowed(path, preal_path, &write_access)) {
		Log(("virtual_is_write_access failed path=%s", path));
		return 0;
	}
	Log(("virtual_is_write_access path=%s real=%s write_access=%d", path, *preal_path, write_access));
	if (!write_access) errno = EACCES;
	return write_access;
}

int virtual_is_read_access(const char *path, char **preal_path)
{
	int write_access;
	return IsAllowed(path, preal_path, &write_access);
}

/* ------------------------------------------------------------------------ */

void virtual_set_root(const char *real_root)
{
	free(g_real_root);
	if (real_root) g_real_root = strdup(real_root);
	else g_real_root = 0;
	if (g_real_root) g_real_root_len = strlen(g_real_root);
}

FILE *virtual_fopen(const char *fn, const char *mode)
{
	int is_write = 1;
	char *real;
	int write_access;

	int allowed = IsAllowed(fn, &real, &write_access);
	if (mode[0] == 'r' && mode[1] != '+') is_write = 0;

	if (!is_write) {
		FILE *fp = 0;
		if (real) {
			fp = fopen(real, mode);
			if (!fp) return 0; // use original errno
		}
		if (!allowed || !real) {
			if (fp) fclose(fp);
			errno = EACCES;
			return 0;
		}
		return fp;
	} else {
		if (!allowed || !write_access) {
			errno = EACCES;
			return 0;
		}
		return fopen(real, mode);
	}
}

int virtual_mkdir(const char *dir, mode_t mode)
{
	char *real;
	if (!virtual_is_write_access(dir, &real)) {
		return -1;
	}
	return mkdir(real, mode);
}

int virtual_rename(const char *from, const char *to)
{
	char *real_from;
	char *real_to;

	if (!virtual_is_write_access(from, &real_from)) {
		return -1;
	}
	if (!virtual_is_write_access(to, &real_to)) {
		return -1;
	}
	return rename(real_from, real_to);
}

int virtual_stat(const char *path, struct stat *buf)
{
	char *real_path;
	struct stat sb = {0};
	int write_access;

	int allowed = IsAllowed(path, &real_path, &write_access);
	if (real_path) {
		int ret = stat(real_path, &sb);
		if (ret) return ret; // use original errno
	}
	if (!allowed || !real_path) {
		errno = EACCES;
		return -1;
	}
	if (!write_access) sb.st_mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
	memcpy(buf, &sb, sizeof(struct stat));
	return 0;
}


int virtual_rmdir(const char *pathname)
{
	char *real_path;
	if (!virtual_is_write_access(pathname, &real_path)) {
		return -1;
	}
	return rmdir(real_path);
}

/**
 * @brief      Try to delete everything that comes by, throws an error if something goes wrong
 */
static int nftw_delete_everything_cb (const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf)
{
	int write_access = 0;
	int ret = 0;

	// check if specific path is accessable
	ret = acl_is_allowed_path(fpath, &write_access);
	if (ret != 1 || write_access != 1) {
		return FTW_STOP;
	}

	// try to delete file or dir
	switch (typeflag) {
		case FTW_F: // file
			if (unlink(fpath)) {
				return FTW_STOP;
			}
			break;
		case FTW_D: // dir (not called for nftw with FTW_DEPTH)
			if (rmdir(fpath)) {
				return FTW_STOP;
			}
			break;
		case FTW_DNR: // dir, not readable
			return FTW_STOP;
		case FTW_DP: // dir
			if (rmdir(fpath)) {
				return FTW_STOP;
			}
			break;
		case FTW_NS: // can be seen, but stat is not possible
			return FTW_STOP;
		case FTW_SL: // symbolic link
			if (unlink(fpath)) {
				return FTW_STOP;
			}
			break;
		case FTW_SLN: // symbolic link to nonexisting thing
			if (unlink(fpath)) {
				return FTW_STOP;
			}
			break;
		default:
			return FTW_STOP;
	}
	return FTW_CONTINUE;
}

int virtual_rmdir_recursive(const char *pathname)
{
	int ret;
	char *real_path;

	if (!virtual_is_write_access(pathname, &real_path)) {
		errno = EACCES;
		return -1;
	}

	// check if directory
	struct stat stat_buf = {0};
	ret = stat(real_path, &stat_buf);
	if (ret || !S_ISDIR(stat_buf.st_mode)) {
		if (ret) {
			errno = EACCES;
		} else {
			errno = ENOTDIR;
		}
		return -1;
	}

	ret = nftw(real_path, nftw_delete_everything_cb, 10, FTW_DEPTH | FTW_PHYS);
	if (ret != 0) {
		errno = EACCES;
		return -2;
	}

	errno = 0;
	return ret;
}

int virtual_unlink(const char *pathname)
{
	char *real_path;
	if (!virtual_is_write_access(pathname, &real_path)) {
		return -1;
	}
	return unlink(real_path);
}

int virtual_chdir(const char *path)
{
	char *real_path;
	if (!virtual_is_read_access(path, &real_path)) return -1;
	return chdir(real_path);
}

DIR *virtual_opendir(const char *name)
{
	char *real_path;
	int allowed = virtual_is_read_access(name, &real_path);
	DIR *d = 0;
	if (real_path) {
		d = opendir(real_path);
		if (!d) return 0; // use original errno
	}
	if (!allowed || !real_path) {
		if (d) closedir(d);
		errno = EACCES;
		return 0;
	}
	return d;
}

int virtual_chmod(const char *path, mode_t mode)
{
	char *real_path;

	if (!virtual_is_write_access(path, &real_path)) {
		return -1;
	}
#if 0
	// never allow chmod of a mountpoint
	if (acl_is_mountpoint(real_path)) {
		errno = EACCES;
		return -1;
	}
#endif
	uid_t uid = geteuid();
	seteuid(0);
	int ret = chmod(real_path, mode);
	seteuid(uid);
	return ret;
}

int virtual_utime(const char *path, time_t actime, time_t modtime)
{
	char *real_path;

	if (!virtual_is_write_access(path, &real_path)) {
		return -1;
	}
	uid_t uid = geteuid();
	seteuid(0);
	struct utimbuf times;
	times.actime = actime;
	times.modtime = modtime;
	int ret = utime(real_path, &times);
	seteuid(uid);
	return ret;
}