/*
 * - User Access Control
 *
 * Copyright 2009-2017 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify it under the terms 
 * of the GNU General Public License ("License") as published by the Free Software Foundation 
 * (version 2 of the License). This software 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 copy of the License you received along with this software for more details.
 *
 */

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

/* memory debugging
 * #include "config.h"
 */
#include <stdio.h>
#include <stdbool.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <ctype.h> /* isspace () */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <dirent.h>
#include <assert.h>
#include <stdarg.h>

#include "avm_acl.h"

#define ACL_USER_ID_OFFSET                            (1000)

#define USERS_ACL_FILE "/var/tmp/users.acl"

#if defined(COMPILE_HOSTTOOLS)
#undef  AVM_ACL_COMMON_REAL_PREFIX
#define AVM_ACL_COMMON_REAL_PREFIX "/var/tmp"
#endif

#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, "acl(%d): ", getpid());
		va_start(ap, fmt);
		vfprintf(fp, fmt, ap);
		va_end(ap);
		fprintf(fp, "\n");
		if (fp != stderr && fp != stdout) {
			fclose(fp);
		}
	}
}
#define Log(x) _fLog x

#else
#define Log(x)
#endif

struct avm_acl_context {
	char *username;
	unsigned uid;
	struct acl_directory *acl;
	char *real_root;
	size_t real_root_len;
	unsigned int access_from_internet:1;
	unsigned int logged_in:1;
	unsigned int reconfig:1;
	unsigned int is_samba:1;
};

static struct avm_acl_context glob = {
	NULL,
	0,
	NULL,
	NULL,
	0,
	0,
	0,
	0,
	0,
};

struct acl_directory
{
	struct acl_directory *next;

	char *real_path;    /* the user always has read access in this path and all files/folders below */
	int real_path_len;  /* calc and store the len once to be faster */
	short write_access;

	int common_path_len; // number of chars in real_path for which another acl_directory exists
};

static struct avm_acl_context* avm_acl_create_context(enum avm_acl_mode mode);

static struct avm_acl_context* avm_acl_login(struct avm_acl_context *ctx, const char *username, int access_from_internet);

struct avm_acl_context* avm_acl_login_uid(struct avm_acl_context *ctx, unsigned uid, int access_from_internet);

static int avm_acl_set_mode(struct avm_acl_context *ctx, enum avm_acl_mode mode);

static void acl_unset_user_context(struct avm_acl_context *ctx);

static void avm_acl_free_context(struct avm_acl_context **ppctx);

static int FixUserContext(struct avm_acl_context *ctx)
{
	if (!ctx->reconfig) return 0;
	ctx->reconfig = 0;

	if (!ctx->acl) return -1;
	// reconfig with updated users.acl file
	if (ctx->username) {
		acl_set_user_context(ctx->username, ctx->access_from_internet);
	} else {
		acl_set_user_context_uid(ctx->uid, ctx->access_from_internet);
	}

	return 0;
}


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

// Nutzung macht nur Sinn, wenn der Interne Speicher fuer Netzwerkzugriff nicht aktiviert ist.
static int is_mountpoint(const char *path)
{
	const char *p;
	int len = strlen(path);
	
	if (len < strlen(AVM_ACL_COMMON_REAL_PREFIX)) {
		return 0;
	}

	if (path != strstr(path, AVM_ACL_COMMON_REAL_PREFIX)) return 0;

	p = path + strlen(AVM_ACL_COMMON_REAL_PREFIX);
	if (*p != '/') return 0;
	p++;
	if (*p == '\0') return 0;
	// kein / mehr oder nur am ende
	while(*p != '\0') {
		if (*p == '/') {
			if (*(p+1) != '\0') return 0;
		}
		p++;
	}
	return 1;
}

/**
 * Checks if internal storage (write access) or RAM (no write access) is used for NAS access.
 * The first time the test is called, the result is stored internally and returned on subsequent calls.
 *
 * @param     reset  if it is set, the check result is reset and the next time the function is called,
 *                   the check is run again, 0 will returned for this reset call
 *
 * @retval    0      no internal storage, no write access in the root
 * @retval    1      we have internal storage, write access ist possible
  */
static int InternalMemoryActiveForNetwork(int reset)
{
	static int known = 0;
	static int active;

	if (reset) {
		// just trigger reload
		known = 0;
		return 0;
	}

	if (!known) {
		char buf[PATH_MAX];
		known = 1;
		if (0 == realpath("/var/InternerSpeicher", buf)) {
			active = 0;
		} else {
			// the "/var/mountpoint_excluded_for_network" file is created by ftpd_control script when we don't have internal storage
			if (0 == access("/var/mountpoint_excluded_for_network", R_OK)) {
				active = 0;
			} else {
				active = 1;
			}
		}
	}

	return active;
}

/**
 * Block access to listed files.
 * Only needed for files in one of the directories:
 * /var/InternerSpeicher
 * /var/media/ftp (AVM_ACL_COMMON_REAL_PREFIX)
 *
 * Other locations of bpjm.data do not need to be blocked here, because the directories are out of access scope.
 */
static const char *blockedpaths[] = {
	"/var/InternerSpeicher/.start_indexation.mp3",
	AVM_ACL_COMMON_REAL_PREFIX"/.start_indexation.mp3",
	"/var/InternerSpeicher/FRITZ/bpjm.data",
	AVM_ACL_COMMON_REAL_PREFIX"/FRITZ/bpjm.data",
	"/var/InternerSpeicher/FRITZ/firmware_update.image",
	AVM_ACL_COMMON_REAL_PREFIX"/FRITZ/firmware_update.image",
	"/var/InternerSpeicher/FRITZ/firmware_update.image.active",
	AVM_ACL_COMMON_REAL_PREFIX"/FRITZ/firmware_update.image.active",
	"/var/InternerSpeicher/FRITZ/firmware_update.version",
	AVM_ACL_COMMON_REAL_PREFIX"/FRITZ/firmware_update.version",
	"/var/InternerSpeicher/FRITZ/bpjm.update",
	AVM_ACL_COMMON_REAL_PREFIX"/FRITZ/bpjm.update"
};

#define DIM(array) (sizeof(array)/sizeof((array)[0]))

static bool IsBlockedPath(const char *path)
{
	unsigned int u;

	for (u = 0; u < DIM(blockedpaths); u++) {
		if (!strcmp(path, blockedpaths[u])) {
			Log(("strcmp(%s, %s) is a blockedpath", path, blockedpaths[u]));
			return true;
		}
	}
	return false;
}

static int IsHiddenLostAndFound(const char *path)
{
	static char *internal_lost_and_found = (char *)-1;
	static int internal_lost_and_found_len;
	if ((char *)-1 == internal_lost_and_found) {
		char buf[PATH_MAX];
		if (0 == realpath("/var/InternerSpeicher", buf)) {
			internal_lost_and_found = 0;
			return 0;
		} else {
			internal_lost_and_found_len = strlen(buf) + strlen("/lost+found");
			internal_lost_and_found = (char*)malloc(internal_lost_and_found_len + 1);
			if (!internal_lost_and_found) return 0;
			snprintf(internal_lost_and_found, internal_lost_and_found_len + 1, "%s/lost+found", buf);
		}
	}
	if (0 == internal_lost_and_found) return 0;

	if (path != strstr(path, internal_lost_and_found)) return 0;

	if (path[internal_lost_and_found_len] != '/' && path[internal_lost_and_found_len] != '\0') return 0;
	return 1;
}

/**
 * Check if the path is a symbolic link and check if it is blocked.
 *
 * @param path absolute or relative path MUST not be NULL
 *
 * @retval 1 if @p path is a blocked symbolic link
 * @retval 0 else
 */
static int IsBlockedLink(const char *path)
{
	bool is_symlink = false;
	struct stat sb;

	if (lstat(path, &sb) == 0) {
		if (S_ISLNK(sb.st_mode)) {
			is_symlink = true;
		}
	}

	Log(("%s() %s is %ssymlink", __func__, path, is_symlink ? "" : "no "));

	if (is_symlink) {
		char buf_link[PATH_MAX] = {0};
		if (*path != '/') {
			/* path is a link as relative path */
			char *cwd = get_current_dir_name();
			if (cwd) {
				Log(("cwd:%s.", cwd));
				Log(("path:%s.", path));
				if (!strncmp("../", path, 3)) {
					/* TODO how to handle a non canonical path? */
				} else if (!strncmp("./", path, 2)) {
					snprintf(buf_link, sizeof(buf_link), "%s%s", cwd, path + 1);
				} else {
					snprintf(buf_link, sizeof(buf_link), "%s/%s", cwd, path);
				}
				free(cwd);
			}
		} else {
			/* path is a link as absolute path */
			snprintf(buf_link, sizeof(buf_link), "%s", path);
		}
		if (*buf_link != '\0') {
			Log(("buf_link:%s.", buf_link));
			/* check if the absolute path exists */
			if (access(buf_link, R_OK) == 0) {
				if (IsBlockedPath(buf_link)) {
					return 1;
				}
			}
		}
	}

	return 0;
}

/* ------------------------------------------------------------------------ */
static int avm_acl_is_allowed_path_check(struct avm_acl_context *ctx, const char *path_, int *pwrite_access)
{
	if (pwrite_access) {
		*pwrite_access = 0;
	}

	if (!ctx) {
		return 0;
	}

	if (!path_) {
		return 0;
	}

	struct acl_directory *a = ctx->acl;
	char buf[PATH_MAX];
	char *path;

	path = strdup(path_);
	if (!path) {
		return 0;
	}

	Log(("acl_is_allowed_path_check %s.", path));

	if (ctx->is_samba && (!InternalMemoryActiveForNetwork(0) || !ctx->logged_in) && !strcmp(path, AVM_ACL_COMMON_REAL_PREFIX)) {
		// our path of $IPC service
		if (pwrite_access) {
			*pwrite_access = 0;
		}
		free(path);
		Log(("acl_is_allowed_path - Read-Only for $IPC"));
		return 1;
	}

	if (!ctx->logged_in) {
		free(path);
		Log(("acl_is_allowed_path - not logged in"));
		return 0;
	}

	if (*path == '\0') {
		/* realpath() with empty string would fail */
		if (0 == getcwd(buf, sizeof(buf))) {
			free(path);
			Log(("acl_is_allowed_real_path - getcwd failed"));
			return 0;
		}
		buf[sizeof(buf)-1] = '\0';
	} else {
		if (buf != realpath(path, buf)) {
			int len, buf_remaining;
			// Check if parent exists
			// and construct realpath
			char *p = &path[strlen(path)];
			while (p > path && *p != '/') p--;
			if (*p != '/') {
				Log(("realpath failed and path=%s has no / p=%s.", path, p));
				if (buf != realpath(".", buf)) {
					// parent does not exists
					free(path);
					Log(("realpath of current dir failed"));
					return 0;
				}
				len = strlen(buf);
				if (0 == len) {
					free(path);
					return 0;
				}
				buf_remaining = sizeof(buf) - len - 1;
				if (buf[len-1] != '/') {
					if (buf_remaining < 2) {
						free(path);
						return 0;
					}
					buf[len] = '/';
					buf[len+1] = '\0';
					buf_remaining--;
				}
				len = strlen(path);
				if (buf_remaining < len) {
					free(path);
					return 0;
				}
				strncat(buf, path, len);
			} else {
				if (!strcmp(p+1, "..")) {
					free(path);
					return 0;
				}
				*p = '\0';
				if (buf != realpath(path, buf)) {
					// parent does not exists
					Log(("realpath of parent=%s failed, buf=%s.", path, buf));
					*p = '/';
					free(path);
					return 0;
				}
				len = strlen(buf);
				if (0 == len) {
					free(path);
					return 0;
				}
				buf_remaining = sizeof(buf) - len - 1;
				*p = '/';
				if (buf[len-1] == '/') {
					len = strlen(p+1);
					if (buf_remaining < len) {
						free(path);
						return 0;
					}
					strncat(buf, p+1, len);
				} else {
					len = strlen(p);
					if (buf_remaining < len) {
						free(path);
						return 0;
					}
					strncat(buf, p, len);
				}
			}
			Log(("acl_is_real_path - constructed realpath %s.", buf));
		}
	}

	Log(("checking access to %s.", buf));

	if (IsHiddenLostAndFound(buf)) {
		Log(("acl_is_allowed_path - path %s - not allowed cause its in lost+found in internal flash", buf));
		free(path);
		return 0;
	}

	if (IsBlockedLink(path)) {
		Log(("link %s is blocked", path));
		free(path);
		return 0;
	}

	if (IsBlockedPath(buf)) {
		Log(("path %s is blocked", path));
		free(path);
		return 0;
	}

	if (FixUserContext(ctx)) {
		Log(("acl_is_allowed_path - path %s - not allowed cause FixUserContext failed", buf));
		free(path);
		return 0;
	}

	while(a) {
		if (buf == strstr(buf, a->real_path)) {
			char c = buf[a->real_path_len];
			if (c == '\0' || c == '/') break; // found
		}
		a = a->next;
	}
	if (a) {
		if (pwrite_access) {
			if (InternalMemoryActiveForNetwork(0)) {
				// /var/media/ftp is the internal memory (we can save changes)
				// and all mountpoints are within the internal memory
				*pwrite_access = a->write_access  ? 1 : 0;
			} else {
				// no write access in the internal memory mountpoint
				if (!ctx->is_samba && is_mountpoint(buf)) {
					// no write access in the internal memory
					// return no write access for ftp and NAS-website
					*pwrite_access = 0;
				} else {
					// all directories below the mountpoint
					// special handling for smb, also signal write access in the mountpoint
					// (if not, maybe we have problems with write access direct below the mountpoint)
					*pwrite_access = a->write_access  ? 1 : 0;
				}
			}
		}
	} else {
		// check for path component to a shared "partition home"
		// eg. the following  paths are configured:
		//  /var/media/ftp/STICK/home/sub1/sub2/sub3
		//  /var/media/ftp/STICK/home/sub4/sub5
		// --> now we also have to allow readonly access to exactly 
		//  /var/media/ftp/STICK/home
		//  /var/media/ftp/STICK/home/sub1
		//  /var/media/ftp/STICK/home/sub1/sub2
		//  /var/media/ftp/STICK/home/sub4
		// otherwise the user would not be able to reach 
		// /var/media/ftp/STICK/home/sub1/sub2/sub3 or
		// /var/media/ftp/STICK/home/sub4/sub5 
		// from his "partition root" /var/media/ftp/STICK/home
		int len = strlen(buf);
		for (a = ctx->acl; a; a = a->next) {
			char c;
			// Log(("checking intermediate path %s against allowed %s.", buf, a->real_path));
			if (0 == a->common_path_len) continue;
			if (len < a->common_path_len) continue; // ist unterhalb der "partition root"
			c = buf[a->common_path_len];
			if (c != '\0' && c != '/') continue;
			assert(strlen(buf) >= a->common_path_len);
			if (memcmp(a->real_path, buf, a->common_path_len)) continue;
			if (a->real_path != strstr(a->real_path, buf)) continue;
			c = a->real_path[len];
			if (c == '/') {
				Log(("intermediate path %s IS ALLOWED against allowed %s.", buf, a->real_path));
				break; // found
			}
		}
		/* Bei Freigabe eines Unterverzeichnisses wird dennoch als einziger! Samba-Share
		 * immer /var/media/ftp freigegeben.
		 * Ein User muss daher auf alle Verzeichnisse auf dem Weg zu seinem user-spezifischen Real-Root
		 * mit Read-Only-Rechten zugreifen duerfen.
		 * Beispiel: Freigabe es Verzeichnisses /Musik/Pop/neu fuer den User
		 * --> user-spezifischer Real-Root ist /var/media/ftp/Musik/Pop/neu
		 * --> Leserecht auch auf folgende Verzeichnisse noetig:
		 * /var/media/ftp
		 * /var/media/ftp/Musik
		 * /var/media/ftp/Pop
		 * (aber auf /var/media/ftp/Musik/Pop/neu ggf. auch Schreibrechte!)
		 */
		if (!a && ctx->is_samba && len >= strlen(AVM_ACL_COMMON_REAL_PREFIX)) {
			for (a = ctx->acl; a; a = a->next) {
				int rl;
				char *p;
				// Log(("samba: checking for parent path %s against allowed %s.", buf, a->real_path));
				p = strrchr(a->real_path, '/'); // remove last dir
				if (!p) continue;
				rl = p - a->real_path;
				if (len > rl) continue; // buf ist auf gleichem Level oder subdir von a->real_path
				if (a->real_path[len] != '/') continue;
				if (!memcmp(buf, a->real_path, len)) {
					Log(("samba: parent path %s IS ALLOWED (read only) against allowed %s.", buf, a->real_path));
					break;
				}
			}
		}
		if (a) {
			if (pwrite_access) {
				*pwrite_access = 0;
			}
		}
	}

	if (!a) {
		Log(("acl_is_allowed_path - path %s - not in any allow path for this user ", buf));
		free(path);
		return 0;
	}

	Log(("access to %s allowed write_access=%d ret=1", buf, pwrite_access ? *pwrite_access : 42));
	free(path);

	return 1;
}

int avm_acl_is_allowed_path(enum avm_acl_mode mode, const char *username, const char *path_, int access_from_internet, int *pwrite_access)
{
	struct avm_acl_context *ctx = NULL;
	int ret = 0;

	ctx = avm_acl_create_context(mode);
	if (!ctx) {
		ret = 0;
		goto end;
	}

	ctx = avm_acl_login(ctx, username, access_from_internet);
	if (!ctx) {
		ret = 0;
		goto end;
	}

	ret = avm_acl_is_allowed_path_check(ctx, path_, pwrite_access);

end:
	avm_acl_free_context(&ctx);

	return ret;
}

int avm_acl_is_allowed_path_uid(enum avm_acl_mode mode, unsigned uid, const char *path_, int access_from_internet, int *pwrite_access)
{
	struct avm_acl_context *ctx = NULL;
	int ret = 0;

	ctx = avm_acl_create_context(mode);
	if (!ctx) {
		ret = 0;
		goto end;
	}

	ctx = avm_acl_login_uid(ctx, uid, access_from_internet);
	if (!ctx) {
		ret = 0;
		goto end;
	}

	ret = avm_acl_is_allowed_path_check(ctx, path_, pwrite_access);

end:
	avm_acl_free_context(&ctx);

	return ret;
}

int acl_is_allowed_path(const char *path_, int *pwrite_access)
{
	return avm_acl_is_allowed_path_check(&glob, path_, pwrite_access);
}

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


static void add_virt_path(struct avm_acl_context *ctx, const char *path, int write_access)
{
	struct acl_directory **pp;
	struct acl_directory *a;
	struct acl_directory *a2;
	Log(("add_virt_path %s write_access:%d", path, write_access));

	if (path[0] != '/') return;
	
	if (path[1] == '\0') path = "";

	a = calloc(1, sizeof(struct acl_directory));
	if (!a) return;
#ifdef HAVE_ASPRINTF
	asprintf(&a->real_path, "%s%s", AVM_ACL_COMMON_REAL_PREFIX, path);
	if (!a->real_path) {
		free(a);
		return;
	}
#else
	{
	size_t len = strlen(AVM_ACL_COMMON_REAL_PREFIX) + strlen(path) + 1;
	a->real_path = malloc(len);
	if (!a->real_path) {
		free(a);
		return;
	}
	snprintf(a->real_path, len, "%s%s", AVM_ACL_COMMON_REAL_PREFIX, path);
	}
#endif

	a->real_path_len = strlen(a->real_path);
	a->write_access = write_access;

	// calc common_path_len -> bis zum letzten gemeinsamen Verzeichnis vom Pfad-Anfang
	for (a2 = ctx->acl; a2; a2 = a2->next) {
		int len;
		char *p1 = a->real_path;
		char *p2 = a2->real_path;
		while(*p1 == *p2 && *p1 != '\0') { p1++; p2++; }
		if ((*p1 == '\0' || *p1 == '/') && (*p2 == '\0' || *p2 == '/')) {
		} else {
			p1--;
			while(*p1 != '/') p1--;
		}
		// match bis einschliesslich p1-1
		len = p1 - a->real_path;
		if (len > a->common_path_len) {
Log(("common_path_len of new %s set to %u", a->real_path, len));
			a->common_path_len = len;
		}
		if (len > a2->common_path_len) {
Log(("common_path_len of existing %s set to %u", a2->real_path, len));
			a2->common_path_len = len;
		}
	}

	pp = &ctx->acl;
	while(*pp) {
		pp = &((*pp)->next);
	}
	*pp = a;
}


/* ----------------------------------------------------------------------- */
enum eToken { TOKEN_EOF, 
			TOKEN_STRING,
			TOKEN_USER,
			TOKEN_ID,
			TOKEN_DIR,
			TOKEN_ACCESS,
			TOKEN_LOCAL_READ,
			TOKEN_LOCAL_WRITE,
			TOKEN_INTERNET_READ,
			TOKEN_INTERNET_WRITE,
			TOKEN_INTERNET_SECURED,
			TOKEN_APP,
			TOKEN_APP_USERID,
			TOKEN_APP_NAS_RIGHTS,
			TOKEN_APP_INTERNET_RIGHTS
};

static struct defined_token
{
	char *string;
	enum eToken token;
} tokens[] = {
	{ "user", TOKEN_USER },
	{ "id", TOKEN_ID },
	{ "dir", TOKEN_DIR },
	{ "access", TOKEN_ACCESS },
	{ "read", TOKEN_LOCAL_READ },
	{ "write", TOKEN_LOCAL_WRITE },
	{ "internet-read", TOKEN_INTERNET_READ },
	{ "internet-write", TOKEN_INTERNET_WRITE },
	{ "internet-secured", TOKEN_INTERNET_SECURED },
	{ "app", TOKEN_APP },
	{ "userid", TOKEN_APP_USERID },
	{ "nas-rights", TOKEN_APP_NAS_RIGHTS },
	{ "internet-rights", TOKEN_APP_INTERNET_RIGHTS },

	{ 0, TOKEN_EOF }
};

struct UserACLReader {
	FILE *fp;

	char *string; // current token
	size_t string_len;
	size_t string_buf_size;
};

static int avmacl_atoi(const char *s)
{
	int v = 0;
	char *endptr = NULL;

	errno = 0;
	v = strtol(s, &endptr, 10);
	if (errno != 0) {
		v = 0;
	}

	return v;
}

static void add_char(struct UserACLReader *r, int c)
{
	if (!r->string) return;
	if (r->string_len >= r->string_buf_size-2) return;
	r->string[r->string_len] = c;
	r->string_len++;
}

static char *GetString(struct UserACLReader *r)
{
	return r->string;
}

static enum eToken UserACLFile_GetToken(struct UserACLReader *r)
{
	int c;
	short bQuoted;
	short bEscaped = 0;
	if (!r->fp) return TOKEN_EOF;
	do {
		c = fgetc(r->fp);	
		if (c == EOF) {
			fclose(r->fp);
			r->fp = 0;
			return TOKEN_EOF;
		}
	} while(isspace(c));

	if (!r->string) {
		r->string_buf_size = 256;
		r->string = (char *)malloc(r->string_buf_size);
		if (!r->string) return TOKEN_EOF;
	}
	r->string_len = 0;
	if (c == '\"') {
		bQuoted = 1;
	} else { 
		bQuoted = 0;
		add_char(r, c);
	}

	do {
		c = fgetc(r->fp);
		if (c == EOF) break;

		if (bQuoted) {
			// end at "
			if (bEscaped) {
				bEscaped = 0;
				add_char(r, c);
			} else { 
				if (c == '\\') bEscaped = 1;
				else if (c == '\"') break; // string end
				else add_char(r, c);
			}
		} else {
			// end at white space
			if (isspace(c)) break; // stringend
			add_char(r, c);
		}
	} while(1);

	r->string[r->string_len++] = '\0';

	if (!bQuoted) {
		struct defined_token *dt;
		for (dt = &tokens[0]; dt->string; dt++) {
			if (!strcmp(r->string, dt->string)) return dt->token;
		}
	}
	return TOKEN_STRING;
}

static struct UserACLReader *UserACLFile_ReadOpen(void)
{
	struct UserACLReader *r = (struct UserACLReader *)calloc(1, sizeof(struct UserACLReader));
	if (!r) return 0;
	r->fp = fopen(USERS_ACL_FILE, "rt");
	if (!r->fp) {
		free(r);
		return 0;
	}
	return r;
}

static void UserACLFile_ReadClose(struct UserACLReader *r)
{
	if (!r) return;
	free(r->string);
	if (r->fp) fclose(r->fp);

	free(r);
}


/* 
user "Udo G" id 70
dir "/x"
access write
*/

static int ReadUser(struct avm_acl_context *ctx, struct UserACLReader *r, int access_from_internet, int allow_write_access)
{
	enum eToken t;
	short ungot_token = 0;
	do {
		short read_access = 0;    // effective read right to this path
		short write_access = 0;   // effective write right to this path
		char *path;
		if (!ungot_token) t = UserACLFile_GetToken(r);
		ungot_token = 0;
		if (t != TOKEN_DIR) break;
		t = UserACLFile_GetToken(r);
		if (t != TOKEN_STRING) break;

		path = strdup(GetString(r));
		t = UserACLFile_GetToken(r);
		// if (!access_from_internet) read_access = 1;
		if (t == TOKEN_ACCESS) {
			short end = 0;
			do {
				t = UserACLFile_GetToken(r);
				switch(t) {
				case TOKEN_LOCAL_READ:
					if (!access_from_internet) read_access = 1;
					break;
				case TOKEN_LOCAL_WRITE:
					if (!access_from_internet) { write_access = 1; read_access = 1; }
					break;
				case TOKEN_INTERNET_READ:
					if (access_from_internet) read_access = 1;
					break;
				case TOKEN_INTERNET_WRITE:
					if (access_from_internet) { write_access = 1; read_access = 1; }
					break;
				default:
					ungot_token = 1;
					end = 1;
				}
			} while(!end);
		} else {
			ungot_token = 1;
		}
		if (read_access && path && path[0] == '/') {
			add_virt_path(ctx, path, (allow_write_access && write_access) ? 1 : 0);
		}
		free(path);
	} while(1);

	UserACLFile_ReadClose(r);

	return 0;
}

static int ReadUsersACLFileByID(struct avm_acl_context *ctx, unsigned id, int access_from_internet, int allow_write_access)
{
	enum eToken t;
	struct UserACLReader *r = UserACLFile_ReadOpen();
	if (!r) return -1;

	t = UserACLFile_GetToken(r);

	while(t != TOKEN_EOF) {
		if (t == TOKEN_USER) {
			do {
				t = UserACLFile_GetToken(r);
			} while(t == TOKEN_STRING);
				if (t == TOKEN_ID) {
					t = UserACLFile_GetToken(r);
					if (t == TOKEN_STRING) {
						if (avmacl_atoi(GetString(r)) == id) break;
					}
				}
		} else if (t == TOKEN_APP) {
			/* app by id not supported here */
		}
		t = UserACLFile_GetToken(r);
	}

	if (t == TOKEN_EOF) goto fail;

	return ReadUser(ctx, r, access_from_internet, allow_write_access);

fail:
	UserACLFile_ReadClose(r);
	return -1;
}

static int ReadUsersACLFileByFriendlyName(struct avm_acl_context *ctx, const char *username, int access_from_internet)
{
	enum eToken t;
	int appid = 0;
	int app_userid = 0;
	int app_internet_rights = 0;
	int app_write_access = 0;
	struct UserACLReader *r = UserACLFile_ReadOpen();
	if (!r) return -1;

	t = UserACLFile_GetToken(r);

	while(t != TOKEN_EOF) {
		if (t == TOKEN_USER) {
			short bFound = 0;
			do {
				t = UserACLFile_GetToken(r);
				if (!bFound && t == TOKEN_STRING && !strcasecmp(GetString(r), username)) bFound = 1;
			} while (t == TOKEN_STRING);
			if (bFound) {
				if (t != TOKEN_ID) goto fail;
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_STRING) goto fail;
				break;
			}
		} else if (t == TOKEN_APP) {
			short bFound = 0;
			do {
				t = UserACLFile_GetToken(r);
				if (!bFound && t == TOKEN_STRING && !strcasecmp(GetString(r), username)) bFound = 1;
			} while (t == TOKEN_STRING);
			if (bFound) {
				if (t != TOKEN_ID) goto fail;
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_STRING) goto fail;
				appid = avmacl_atoi(GetString(r));
				if (appid <= 0) goto fail;
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_APP_USERID) goto fail;
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_STRING) goto fail;
				app_userid = avmacl_atoi(GetString(r));
				if (app_userid <= 0) goto fail;
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_APP_NAS_RIGHTS) goto fail;
				t = UserACLFile_GetToken(r);
				switch(t) {
				case TOKEN_LOCAL_READ:  app_write_access = 0; break;  // read-only
				case TOKEN_LOCAL_WRITE: app_write_access = 1; break;  // read-write  
				default: goto fail;
				}
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_APP_INTERNET_RIGHTS) goto fail;
				t = UserACLFile_GetToken(r);
				if (t != TOKEN_STRING) goto fail;
				app_internet_rights = avmacl_atoi(GetString(r));
				if (app_internet_rights < 0) goto fail;
				break;
			}
		}
		t = UserACLFile_GetToken(r);
	}

	if (t == TOKEN_EOF) goto fail;

	if (0 != appid) {
		int ret;
		UserACLFile_ReadClose(r);
		if (access_from_internet && 0 == app_internet_rights) return 0;
		if (app_internet_rights) {
			access_from_internet = 0; // this way internet rights are added to an app, even if the user has no internet rights
		}
		ret = ReadUsersACLFileByID(ctx, app_userid, access_from_internet, app_write_access);
		return ret;
	} else {
		return ReadUser(ctx, r, access_from_internet, 1/*allow write access*/);
	}

fail:
	UserACLFile_ReadClose(r);
	return -1;
}

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

static void acl_unset_user_context(struct avm_acl_context *ctx)
{
	if (!ctx) {
		/* TODO */
		return;
	}

	while (ctx->acl) {
		struct acl_directory *a = ctx->acl;
		ctx->acl = a->next;

		free(a->real_path);
		free(a);
	}

	free(ctx->real_root);
	ctx->real_root = NULL;
	ctx->real_root_len = 0;
	ctx->logged_in = 0;
	/* keep ctx->is_samba */
}


static int calc_real_root(struct avm_acl_context *ctx)
{
	int root_path_len;
	struct acl_directory *root = ctx->acl;
Log(("calc_real_root"));
	// calc real root (what is virtual /) for this user
	// the real root is the longest common parent path of all real directories the user can access (at least read)
// ACHTUNG: TODO Das Home-Dir in /etc/passwd muss fuer Local Access als auch fuer Access aus dem Internet gelten!!!!!!!
//           bzw der ftp muss das homedir hieraus berechnen und nicht aus /etc/passwd lesen!

	if (!root) return -1;
	while(root->next) root = root->next; // note that longer path with common parent are first in list, so we go to the last

	root_path_len = root->real_path_len;

	do { 
		// check obs gemeinsamer parent ist
		struct acl_directory *a = ctx->acl;
		char *p;

		if (root_path_len == 1) break; // "/" passt immer
		while(a != root) {
			char c;
			if (a->real_path_len < root_path_len || memcmp(a->real_path, root->real_path, root_path_len)) break; // passt nicht
			c = a->real_path[root_path_len];
			if (c != '/' && c != '\0') break; // passt nicht
			a = a->next;
		}
		if (a == root) break; // passt!

		// shorten root - go up one dir
		if (root_path_len == strlen(AVM_ACL_COMMON_REAL_PREFIX)) break; // cant shorten more!

		p = root->real_path + root_path_len - 1;
		while(*p != '/' && p > root->real_path) p--;
		if (p == root->real_path) {
			root_path_len = 1; // '/' - should not happen
			break;
		}
		root_path_len = p - root->real_path;
	} while(1);

	ctx->real_root_len = root_path_len;
	ctx->real_root = malloc(root_path_len+1);
	if (!ctx->real_root) return -1;
	memcpy(ctx->real_root, root->real_path, root_path_len);
	ctx->real_root[root_path_len] = '\0';

	Log(("calc_real_root: real_root is %s.", ctx->real_root));

	return 0;
}

/**
 * Cleanup context after usage. Needed to free all internal resources.
 *
 * @param ppctx
 */
static void avm_acl_free_context(struct avm_acl_context **ppctx)
{
	if (ppctx && *ppctx) {
		acl_unset_user_context((*ppctx));
		free((*ppctx)->username);
		free(*ppctx);
		*ppctx = NULL;
	}
}

static int avm_acl_reset_context(struct avm_acl_context *ctx, const char *username, int access_from_internet)
{
	char *old = NULL;

	if (!ctx) {
		return -1;
	}

	acl_unset_user_context(ctx);

	if (username) {
		if (ReadUsersACLFileByFriendlyName(ctx, username, access_from_internet)) {
			// no access at all
			goto err;
		}

		// parameter username may be the same pointer as ctx->username
		old = ctx->username;
		ctx->username = strdup(username);
		free(old);
		if (!ctx->username) {
			goto err;
		}

		ctx->uid = 0;
		ctx->logged_in = 1;
	} else {
		ctx->uid = 0;
		ctx->logged_in = 0;
	}

	ctx->access_from_internet = access_from_internet ? 1 : 0;

	return 0;

err:
	return -1;
}

static int avm_acl_reset_context_uid(struct avm_acl_context *ctx, unsigned uid, int access_from_internet)
{
	if (!ctx) {
		return -1;
	}

	if (uid <= ACL_USER_ID_OFFSET) {
		return -1;
	}

	acl_unset_user_context(ctx);

	if (ReadUsersACLFileByID(ctx, uid - ACL_USER_ID_OFFSET, access_from_internet, 1/*allow write access*/)) {
		// no access at all
		goto err;
	}

	free(ctx->username);
	ctx->username = NULL;
	ctx->uid = uid;
	ctx->access_from_internet = access_from_internet ? 1 : 0;
	ctx->logged_in = 1;
	/* keep ctx->is_samba */

	return 0;

err:
	return -1;
}

/**
 * Create a new context by user name.
 *
 * @param mode access mode needed to apply special handling for SMB access
 *
 * @return context or NULL on failure.
 */
static struct avm_acl_context* avm_acl_create_context(enum avm_acl_mode mode)
{
	struct avm_acl_context *ctx = NULL;

	ctx = (struct avm_acl_context*)calloc(1, sizeof(struct avm_acl_context));
	if (!ctx) {
		goto err;
	}

	if (0 != avm_acl_set_mode(ctx, mode)) {
		goto err;
	}

	return ctx;

err:
	avm_acl_free_context(&ctx);

	return NULL;
}

/**
 * Change the status of the context by logging in with an user name.
 * On failure NULL is returned and the context was freed.
 *
 * @param ctx context
 * @param username username
 * @param access_from_internet LAN access if 0 else WAN access
 *
 * @return context or NULL on failure.
 */
static struct avm_acl_context* avm_acl_login(struct avm_acl_context *ctx, const char *username, int access_from_internet)
{
	if (!ctx) {
		return NULL;
	}

	if (avm_acl_reset_context(ctx, username, access_from_internet)) {
		goto err;
	}

	return ctx;

err:
	avm_acl_free_context(&ctx);

	return ctx;
}

int acl_set_user_context(char *username, int access_from_internet)
{
	Log(("acl_set_user_context username %s access_from_internet:%d", username ? username : "<null>", access_from_internet));
Log(("sizeof(off_t)=%zu sizeof(struct stat)=%zu _FILE_OFFSET_BITS=%u", sizeof(off_t), sizeof(struct stat), _FILE_OFFSET_BITS));

	if (avm_acl_reset_context(&glob, username, access_from_internet)) {
		// no access at all
		goto bad;
	}

#if 0 // TEST
	// laengere pfade mit gleichen Parent-Pfaden vorher anfuegen!
	// add_virt_path(&glob, "/JetFlash-TS8GJF160-01/readonly", 0/* READ ONLY */);
	// add_virt_path(&glob, "/JetFlash-TS8GJF160-01", 1/* write access to everything */);
#endif

	return 0;

bad:
	acl_unset_user_context(&glob);
	Log(("acl_set_user_context failed"));
	return -1;
}

/**
 * Change the status of the context by logging in with an user ID.
 * On failure NULL is returned and the context was freed.
 *
 * @param ctx context
 * @param uid user ID
 * @param access_from_internet LAN access if 0 else WAN access
 *
 * @return context or NULL on failure.
 */
struct avm_acl_context* avm_acl_login_uid(struct avm_acl_context *ctx, unsigned uid, int access_from_internet)
{
	if (uid <= ACL_USER_ID_OFFSET) {
		goto err;
	}

	if (avm_acl_reset_context_uid(ctx, uid, access_from_internet)) {
		// no access at all
		goto err;
	}

	return ctx;

err:
	avm_acl_free_context(&ctx);

	return ctx;
}

int acl_set_user_context_uid(unsigned uid, int access_from_internet)
{
	Log(("acl_set_user_context_uid uid:%u access_from_internet:%d", uid, access_from_internet));

	acl_unset_user_context(&glob);

	if (uid <= ACL_USER_ID_OFFSET) return -1;

	if (avm_acl_reset_context_uid(&glob, uid, access_from_internet)) {
		// no access at all
		goto bad;
	}

	return 0;

bad:
	acl_unset_user_context(&glob);
	Log(("acl_set_user_context_uid failed"));
	return -1;
}

int acl_set_full_access_user_context(void)
{
Log(("acl_set_full_user_context()"));
	acl_unset_user_context(&glob);
	add_virt_path(&glob, "/", 1/* write access to everything */);
	glob.logged_in = 1;
	return 0;
}

/**
 * Return real root directory path.
 *
 * @param ctx context
 *
 * @return path or NULL on failure
 */
static char *avm_acl_get_common_real_dir(struct avm_acl_context *ctx)
{
	if (!ctx) {
		return NULL;
	}

	if (!ctx->acl) {
		return NULL;
	}

	if (!ctx->real_root) {
		if (calc_real_root(ctx)) {
			return NULL; // failed
		}
	}

	return ctx->real_root;
}

char *acl_get_user_common_real_dir(void)
{
Log(("acl_get_user_common_real_dir"));
	return avm_acl_get_common_real_dir(&glob);
}

static struct acl_string_list *avm_acl_get_all_real_paths_with_read_access(struct avm_acl_context *ctx)
{
	struct acl_string_list *head = 0;
	struct acl_string_list *tail = 0;

	if (!ctx) {
		return NULL;
	}

	struct acl_directory *a;
	for (a = ctx->acl; a && a->real_path; a = a->next) {
		struct acl_string_list *sl = (struct acl_string_list *)malloc(sizeof(struct acl_string_list));
		if (!sl) break;
		sl->s = strdup(a->real_path);
		if (!sl->s) { free(sl); break; }
		sl->next = 0;
		if (!head) {
			head = sl;
		} else {
			tail->next = sl;
		}
		tail = sl;
	}

	return head;
}


struct acl_string_list *acl_get_user_all_real_paths_with_read_access(void)
{
	struct acl_string_list *head = NULL;

	head = avm_acl_get_all_real_paths_with_read_access(&glob);

	return head;
}

struct acl_string_list *avm_acl_user_get_all_real_paths_with_read_access_uid(enum avm_acl_mode mode, unsigned uid, int access_from_internet)
{
	struct avm_acl_context *ctx = NULL;
	struct acl_string_list *list = NULL;

	ctx = avm_acl_create_context(mode);
	if (!ctx) {
		goto end;
	}

	ctx = avm_acl_login_uid(ctx, uid, access_from_internet);
	if (!ctx) {
		goto end;
	}

	list = avm_acl_get_all_real_paths_with_read_access(ctx);

end:
	avm_acl_free_context(&ctx);

	return list;
}

struct acl_string_list *avm_acl_user_get_all_real_paths_with_read_access(enum avm_acl_mode mode, const char *username, int access_from_internet)
{
	struct avm_acl_context *ctx = NULL;
	struct acl_string_list *list = NULL;

	ctx = avm_acl_create_context(mode);
	if (!ctx) {
		goto end;
	}

	ctx = avm_acl_login(ctx, username, access_from_internet);
	if (!ctx) {
		goto end;
	}

	list = avm_acl_get_all_real_paths_with_read_access(ctx);

end:
	avm_acl_free_context(&ctx);

	return list;
}

void avm_acl_string_list_free(struct acl_string_list **pp)
{
	struct acl_string_list *p;

	while (*pp) {
		p = *pp;
		*pp = p->next;
		free(p->s);
		free(p);
	}
}

/**
 * Switch context mode.
 * Used to enable SMB mode on already created context.
 *
 * @param ctx
 * @param mode
 *
 * @return 0 on success
 */
static int avm_acl_set_mode(struct avm_acl_context *ctx, enum avm_acl_mode mode)
{
	if (!ctx) {
		return -1;
	}

	switch (mode) {
		case avm_acl_mode_unknown:
			goto err;
		case avm_acl_mode_smb:
			ctx->is_samba = 1;
			break;
		case avm_acl_mode_ftp:
		case avm_acl_mode_http:
			ctx->is_samba = 0;
			break;
	}

	return 0;
err:
	return -1;
}

void acl_set_samba_mode(void)
{
Log(("acl_set_samba_mode"));
	avm_acl_set_mode(&glob, avm_acl_mode_smb);
}

void acl_reconfig(void)
{
	InternalMemoryActiveForNetwork(1/*reset*/);
	glob.reconfig = 1;
}

int acl_force_secured_internet_access(void)
{
	enum eToken t;
	int secured = 0;
	struct UserACLReader *r = UserACLFile_ReadOpen();
	if (!r) return 0;

	t = UserACLFile_GetToken(r);

	while(t != TOKEN_EOF && t != TOKEN_USER) {
		if (t == TOKEN_INTERNET_SECURED) {
			secured = 1;
			break;
		}
		t = UserACLFile_GetToken(r);
	}
	UserACLFile_ReadClose(r);
	return secured;
}