/*
 * mod_userdir - user-specific document roots
 *
 * Todo:
 *     - userdir.exclude / userdir.include options/setups to allow certain users to be excluded or included
 *
 * Author:
 *     Copyright (c) 2010 Thomas Porzelt
 * License:
 *     MIT, see COPYING file in the lighttpd 2 tree
 */

#include <lighttpd/base.h>
#include <pwd.h>

LI_API gboolean mod_userdir_init(liModules *mods, liModule *mod);
LI_API gboolean mod_userdir_free(liModules *mods, liModule *mod);

struct userdir_part {
	enum {
		USERDIR_PART_STRING,
		USERDIR_PART_USERNAME,
		USERDIR_PART_LETTER
	} type;
	union {
		GString *str;
		guint8 ndx;
	} data;
};
typedef struct userdir_part userdir_part;

static liHandlerResult userdir(liVRequest *vr, gpointer param, gpointer *context) {
	userdir_part *part;
	gchar *c;
	guint i;
	GArray *parts = param;
	gchar *username;
	guint username_len = 0;
	gboolean has_username;

	UNUSED(context);

	if (vr->request.uri.path->str[0] != '/' || vr->request.uri.path->str[1] != '~') {
		return LI_HANDLER_GO_ON;
	}

	/* only allow [a-zA-Z0-9_-] in usernames */
	for (c = vr->request.uri.path->str+2; *c != '\0'; c++) {
		if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || (*c >= '0' && *c <= '9') || *c == '_' || *c == '-')
			continue;
		if (*c == '/')
			break;

		return LI_HANDLER_GO_ON;
	}

	username = vr->request.uri.path->str + 2;
	username_len = c - username;
	if (username_len == 0)
		return LI_HANDLER_GO_ON;

	g_string_truncate(vr->physical.doc_root, 0);

	part = &g_array_index(parts, userdir_part, 0);

	if (part->type != USERDIR_PART_STRING || part->data.str->str[0] != '/') {
		/* pattern not starting with slash, need to lookup user's homedir */
		struct passwd pwd;
		struct passwd *result;
		gchar c_orig = *(username+username_len);

		/* do not allow root user */
		if (username_len == 4 && username[0] == 'r' && username[1] == 'o' && username[2] == 'o' && username[3] == 't') {
			if (!li_vrequest_handle_direct(vr))
				return LI_HANDLER_ERROR;

			vr->response.http_status = 403;
			return LI_HANDLER_GO_ON;
		}

		*(username+username_len) = '\0';
		while (EINTR == getpwnam_r(username, &pwd, vr->wrk->tmp_str->str, vr->wrk->tmp_str->allocated_len, &result)) {
		}
		*(username+username_len) = c_orig;

		if (!result) {
			if (!li_vrequest_handle_direct(vr))
				return LI_HANDLER_ERROR;

			vr->response.http_status = 404;
			return LI_HANDLER_GO_ON;
		}

		/* user found */
		g_string_append(vr->physical.doc_root, pwd.pw_dir);
		g_string_append_c(vr->physical.doc_root, G_DIR_SEPARATOR);
		has_username = TRUE;
	} else {
		has_username = FALSE;
	}

	for (i = 0; i < parts->len; i++) {
		part = &g_array_index(parts, userdir_part, i);
		switch (part->type) {
		case USERDIR_PART_STRING:
			li_g_string_append_len(vr->physical.doc_root, GSTR_LEN(part->data.str));
			break;
		case USERDIR_PART_USERNAME:
			li_g_string_append_len(vr->physical.doc_root, username, username_len);
			has_username = TRUE;
			break;
		case USERDIR_PART_LETTER:
			if (part->data.ndx <= username_len)
				g_string_append_c(vr->physical.doc_root, username[part->data.ndx-1]);
			break;
		}
	}

	if (!has_username) {
		/* pattern without username, append it. /usr/web/ => /usr/web/user/ */
		if (vr->physical.doc_root->str[vr->physical.doc_root->len-1] != G_DIR_SEPARATOR)
			g_string_append_c(vr->physical.doc_root, G_DIR_SEPARATOR);
		li_g_string_append_len(vr->physical.doc_root, username, username_len);
	}

	/* ensure that docroot is ending with a slash */
	if (vr->physical.doc_root->str[vr->physical.doc_root->len-1] != G_DIR_SEPARATOR)
		g_string_append_c(vr->physical.doc_root, G_DIR_SEPARATOR);

	/* build physical path: docroot + uri.path */
	g_string_truncate(vr->physical.path, 0);
	li_g_string_append_len(vr->physical.path, GSTR_LEN(vr->physical.doc_root));
	li_g_string_append_len(vr->physical.path, username + username_len, vr->request.uri.path->str - username - username_len);

	/* rewrite request path to skip username */
	g_string_truncate(vr->wrk->tmp_str, 0);
	li_g_string_append_len(vr->wrk->tmp_str, username + username_len, vr->request.uri.path->str - username - username_len);
	g_string_truncate(vr->request.uri.path, 0);
	if (vr->wrk->tmp_str->len)
		li_g_string_append_len(vr->request.uri.path, GSTR_LEN(vr->wrk->tmp_str));
	else
		g_string_append_c(vr->request.uri.path, G_DIR_SEPARATOR);

	return LI_HANDLER_GO_ON;
}

static void userdir_free(liServer *srv, gpointer param) {
	GArray *parts = param;
	guint i;

	UNUSED(srv);

	for (i = 0; i < parts->len; i++) {
		userdir_part *part = &g_array_index(parts, userdir_part, i);
		if (part->type == USERDIR_PART_STRING) {
			g_string_free(part->data.str, TRUE);
		}
	}

	g_array_free(parts, TRUE);
}

static liAction* userdir_create(liServer *srv, liWorker *wrk, liPlugin* p, liValue *val, gpointer userdata) {
	GString *str;
	gchar *c, *c_last;
	GArray *parts;
	userdir_part part;
	UNUSED(wrk); UNUSED(p); UNUSED(userdata);

	val = li_value_get_single_argument(val);

	if (LI_VALUE_STRING != li_value_type(val)) {
		ERROR(srv, "%s", "userdir expects a string as parameter");
		return NULL;
	}

	str = val->data.string;

	if (0 == str->len) {
		ERROR(srv, "%s", "userdir parameter must not be an empty string");
		return NULL;
	}

	parts = g_array_new(FALSE, FALSE, sizeof(userdir_part));

	/* parse pattern */
	for (c_last = c = str->str; c != str->str + str->len; c++) {
		if (*c == '*') {
			/* username */
			if (c - c_last > 0) {
				/* push last string */
				part.type = USERDIR_PART_STRING;
				part.data.str = g_string_new_len(c_last, c - c_last);
				g_array_append_val(parts, part);
			}

			c_last = c+1;
			part.type = USERDIR_PART_USERNAME;
			g_array_append_val(parts, part);
		} else if (*c == '$' && *(c+1) >= '1' && *(c+2) <= '9') {
			/* letter */
			if (c - c_last > 0) {
				/* push last string */
				part.type = USERDIR_PART_STRING;
				part.data.str = g_string_new_len(c_last, c - c_last);
				g_array_append_val(parts, part);
			}

			c_last = c+2;
			part.type = USERDIR_PART_LETTER;
			part.data.ndx = *(c+1) - '0';
			g_array_append_val(parts, part);
		}
	}

	if (c - c_last > 0) {
		/* push last string */
		part.type = USERDIR_PART_STRING;
		part.data.str = g_string_new_len(c_last, c - c_last);
		g_array_append_val(parts, part);
	}

	return li_action_new_function(userdir, NULL, userdir_free, parts);
}

static const liPluginAction actions[] = {
	{ "userdir", userdir_create, NULL },

	{ NULL, NULL, NULL }
};

static void plugin_userdir_init(liServer *srv, liPlugin *p, gpointer userdata) {
	UNUSED(srv); UNUSED(userdata);

	p->actions = actions;

}


gboolean mod_userdir_init(liModules *mods, liModule *mod) {
	UNUSED(mod);

	MODULE_VERSION_CHECK(mods);

	mod->config = li_plugin_register(mods->main, "mod_userdir", plugin_userdir_init, NULL);

	return mod->config != NULL;
}

gboolean mod_userdir_free(liModules *mods, liModule *mod) {
	if (mod->config)
		li_plugin_free(mods->main, mod->config);

	return TRUE;
}
