/*
 * Copyright (c) 2006,2007 Steven Johnson <sjohnson@sakuraindustries.com>
 * Copyright (c) 2007 Sergey Lyubka <valenok@gmail.com>
 * All rights reserved
 *
 * "THE BEER-WARE LICENSE" (Revision 42):
 * Sergey Lyubka wrote this file.  As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return.
 */

#include "defs.h"

#if !defined(NO_SSI)

#define	CMDBUFSIZ	512		/* SSI command buffer size	*/
#define	NEST_MAX	6		/* Maximum nesting level	*/

struct ssi_func {
	struct llhead	link;
	void		*user_data;
	char		*name;
	shttpd_callback_t func;
};

struct ssi_inc {
	int		state;		/* Buffering state		*/
	int		cond;		/* Conditional state		*/
	FILE		*fp;		/* Icluded file stream		*/
	char		buf[CMDBUFSIZ];	/* SSI command buffer		*/
	size_t		nbuf;		/* Bytes in a command buffer	*/
	FILE		*pipe;		/* #exec stream			*/
	struct ssi_func	func;		/* #call function		*/
};

struct ssi {
	struct conn	*conn;		/* Connection we belong to	*/
	int		nest;		/* Current nesting level	*/
	struct ssi_inc	incs[NEST_MAX];	/* Nested includes		*/
};

enum { SSI_PASS, SSI_BUF, SSI_EXEC, SSI_CALL };
enum { SSI_GO, SSI_STOP };		/* Conditional states		*/

static const struct vec	st = {"<!--#", 5};

void
shttpd_register_ssi_func(struct shttpd_ctx *ctx, const char *name,
		shttpd_callback_t func, void *user_data)
{
	struct ssi_func	*e;

	if ((e = malloc(sizeof(*e))) != NULL) {
		e->name		= _shttpd_strdup(name);
		e->func		= func;
		e->user_data	= user_data;
		LL_TAIL(&ctx->ssi_funcs, &e->link);
	}
}

void
_shttpd_ssi_func_destructor(struct llhead *lp)
{
	struct ssi_func	*e = LL_ENTRY(lp, struct ssi_func, link);

	free(e->name);
	free(e);
}

static const struct ssi_func *
find_ssi_func(struct ssi *ssi, const char *name)
{
	struct ssi_func	*e;
	struct llhead	*lp;

	LL_FOREACH(&ssi->conn->ctx->ssi_funcs, lp) {
		e = LL_ENTRY(lp, struct ssi_func, link);
		if (!strcmp(name, e->name))
			return (e);
	}

	return (NULL);
}

static void
call(struct ssi *ssi, const char *name,
		struct shttpd_arg *arg, char *buf, int len)
{
	const struct ssi_func	*ssi_func;

	(void) memset(arg, 0, sizeof(*arg));

	/*
	 * SSI function may be called with parameters. These parameters
	 * are passed as arg->in.buf, arg->in.len vector.
	 */
	arg->in.buf = strchr(name, ' ');
	if (arg->in.buf != NULL) {
		*arg->in.buf++ = '\0';
		arg->in.len = strlen(arg->in.buf);
	}

	if ((ssi_func = find_ssi_func(ssi, name)) != NULL) {
		arg->priv = ssi->conn;
		arg->user_data = ssi_func->user_data;
		arg->out.buf = buf;
		arg->out.len = len;
		ssi_func->func(arg);
	}
}

static int
evaluate(struct ssi *ssi, const char *name)
{
	struct shttpd_arg	arg;

	call(ssi, name, &arg, NULL, 0);

	return (arg.flags & SHTTPD_SSI_EVAL_TRUE);
}

static void
pass(struct ssi_inc *inc, void *buf, int *n)
{
	if (inc->cond == SSI_GO) {
		(void) memcpy(buf, inc->buf, inc->nbuf);
		(*n) += inc->nbuf;
	}
	inc->nbuf = 0;
	inc->state = SSI_PASS;
}

static int
get_path(struct conn *conn, const char *src,
		int src_len, char *dst, int dst_len)
{
	static struct vec	accepted[] = {
		{"\"",		1},	/* Relative to webserver CWD	*/
		{"file=\"", 	6},	/* Relative to current URI	*/
		{"virtual=\"", 	9},	/* Relative to document root	*/
		{NULL,		0},
	};
	struct vec	*vec;
	const char	*p, *root = conn->ctx->options[OPT_ROOT];
	int		len;

	for (vec = accepted; vec->len > 0; vec++)
		if (src_len > vec->len && !memcmp(src, vec->ptr, vec->len)) {
			src += vec->len;
			src_len -= vec->len;
			if ((p = memchr(src, '"', src_len)) == NULL)
				break;
			if (vec->len == 6) {
				len = _shttpd_snprintf(dst, dst_len, "%s%c%s",
				    root, DIRSEP, conn->uri);
				while (len > 0 && dst[len] != '/')
					len--;
				dst += len;
				dst_len -= len;
			} else if (vec->len == 9) {
				len = _shttpd_snprintf(dst, dst_len, "%s%c",
				    root, DIRSEP);
				dst += len;
				dst_len -= len;
			}
			_shttpd_url_decode(src, p - src, dst, dst_len);
			return (1);
		}

	return (0);
}

static void
do_include(struct ssi *ssi)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	char		buf[FILENAME_MAX];
	FILE		*fp;

	assert(inc->nbuf >= 13);

	if (inc->cond == SSI_STOP) {
		/* Do nothing - conditional FALSE */
	} else if (ssi->nest >= (int) NELEMS(ssi->incs) - 1) {
		_shttpd_elog(E_LOG, ssi->conn,
		    "ssi: #include: maximum nested level reached");
	} else if (!get_path(ssi->conn,
	    inc->buf + 13, inc->nbuf - 13, buf, sizeof(buf))) {
		_shttpd_elog(E_LOG, ssi->conn, "ssi: bad #include: [%.*s]",
		    inc->nbuf, inc->buf);
	} else if ((fp = fopen(buf, "r")) == NULL) {
		_shttpd_elog(E_LOG, ssi->conn,
		    "ssi: fopen(%s): %s", buf, strerror(errno));
	} else {
		ssi->nest++;
		ssi->incs[ssi->nest].fp = fp;
		ssi->incs[ssi->nest].nbuf = 0;
		ssi->incs[ssi->nest].cond = SSI_GO;
	}
}

static char *
trim_spaces(struct ssi_inc *inc)
{
	char	*p = inc->buf + inc->nbuf - 2;

	/* Trim spaces from the right */
	*p-- = '\0';
	while (isspace(* (unsigned char *) p))
		*p-- = '\0';

	/* Shift pointer to the start of attributes */
	for (p = inc->buf; !isspace(* (unsigned char *) p); p++);
	while (*p && isspace(* (unsigned char *) p)) p++;

	return (p);
}

static void
do_if(struct ssi *ssi)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	char		*name = trim_spaces(inc);

	inc->cond = evaluate(ssi, name) ? SSI_GO : SSI_STOP;
}

static void
do_elif(struct ssi *ssi)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	char		*name = trim_spaces(inc);

	if (inc->cond == SSI_STOP && evaluate(ssi, name))
		inc->cond = SSI_GO;
	else
		inc->cond = SSI_STOP;
}
static void
do_endif(struct ssi *ssi)
{
	ssi->incs[ssi->nest].cond = SSI_GO;
}

static void
do_else(struct ssi *ssi)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;

	inc->cond = inc->cond == SSI_GO ? SSI_STOP : SSI_GO;
}

static void
do_call2(struct ssi *ssi, char *buf, int len, int *n)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	struct shttpd_arg	arg;

	call(ssi, inc->buf, &arg, buf, len);
	(*n) += arg.out.num_bytes;
	if (arg.flags & SHTTPD_END_OF_OUTPUT)
		inc->state = SSI_PASS;
}

static void
do_call(struct ssi *ssi, char *buf, int len, int *n)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	char		*name = trim_spaces(inc);

	if (inc->cond == SSI_GO) {
		(void) memmove(inc->buf, name, strlen(name) + 1);
		inc->state = SSI_CALL;
		do_call2(ssi, buf, len, n);
	}
}

static void
do_exec2(struct ssi *ssi, char *buf, int len, int *n)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	int		i, ch;

	for (i = 0; i < len; i++) {
		if ((ch = fgetc(inc->pipe)) == EOF) {
			inc->state = SSI_PASS;
			(void) pclose(inc->pipe);
			inc->pipe = NULL;
			break;
		}
		*buf++ = ch;
		(*n)++;
	}
}

static void
do_exec(struct ssi *ssi, char *buf, int len, int *n)
{
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	char		cmd[sizeof(inc->buf)], *e, *p;

	p = trim_spaces(inc);

	if (inc->cond == SSI_STOP) {
		/* Do nothing - conditional FALSE */
	} else if (*p != '"' || (e = strchr(p + 1, '"')) == NULL) {
		_shttpd_elog(E_LOG, ssi->conn, "ssi: bad exec(%s)", p);
	} else if (!_shttpd_url_decode(p + 1, e - p - 1, cmd, sizeof(cmd))) {
		_shttpd_elog(E_LOG, ssi->conn,
		    "ssi: cannot url_decode: exec(%s)", p);
	} else if ((inc->pipe = popen(cmd, "r")) == NULL) {
		_shttpd_elog(E_LOG, ssi->conn, "ssi: popen(%s)", cmd);
	} else {
		inc->state = SSI_EXEC;
		do_exec2(ssi, buf, len, n);
	}
}

static const struct ssi_cmd {
	struct vec	vec;
	void (*func)();
} known_ssi_commands [] = {
	{{"include ",	8}, do_include	},
	{{"if ",	3}, do_if	},
	{{"elif ",	5}, do_elif	},
	{{"else",	4}, do_else	},
	{{"endif",	5}, do_endif	},
	{{"call ",	5}, do_call	},
	{{"exec ",	5}, do_exec	},
	{{NULL,		0}, NULL	}
};

static void
do_command(struct ssi *ssi, char *buf, size_t len, int *n)
{
	struct ssi_inc		*inc = ssi->incs + ssi->nest;
	const struct ssi_cmd	*cmd;

	assert(len > 0);
	assert(inc->nbuf <= len);
	inc->state = SSI_PASS;

	for (cmd = known_ssi_commands; cmd->func != NULL; cmd++)
		if (inc->nbuf > (size_t) st.len + cmd->vec.len &&
		    !memcmp(inc->buf + st.len, cmd->vec.ptr, cmd->vec.len)) {
			cmd->func(ssi, buf, len, n);
			break;
		}

	if (cmd->func == NULL)
		pass(inc, buf, n);

	inc->nbuf = 0;
}

static int
read_ssi(struct stream *stream, void *vbuf, size_t len)
{
	struct ssi	*ssi = stream->conn->ssi;
	struct ssi_inc	*inc = ssi->incs + ssi->nest;
	char		*buf = vbuf;
	int		ch = EOF, n = 0;

again:

	if (inc->state == SSI_CALL)
		do_call2(ssi, buf, len, &n);
	else if (inc->state == SSI_EXEC)
		do_exec2(ssi, buf, len, &n);

	while (n + inc->nbuf < len && (ch = fgetc(inc->fp)) != EOF)

		switch (inc->state) {

		case SSI_PASS:
			if (ch == '<') {
				inc->nbuf = 0;
				inc->buf[inc->nbuf++] = ch;
				inc->state = SSI_BUF;
			} else if (inc->cond == SSI_GO) {
				buf[n++] = ch;
			}
			break;

		/*
		 * We are buffering whole SSI command, until closing "-->".
		 * That means that when do_command() is called, we can rely
		 * on that full command with arguments is buffered in and
		 * there is no need for streaming.
		 * Restrictions:
		 *  1. The command must fit in CMDBUFSIZ
		 *  2. HTML comments inside the command ? Not sure about this.
		 */
		case SSI_BUF:
			if (inc->nbuf >= sizeof(inc->buf) - 1) {
				pass(inc, buf + n, &n);
			} else if (ch == '>' &&
			    !memcmp(inc->buf + inc->nbuf - 2, "--", 2)) {
				do_command(ssi, buf + n, len - n, &n);
				inc = ssi->incs + ssi->nest;
			} else {
				inc->buf[inc->nbuf++] = ch;

				/* If not SSI tag, pass it */
				if (inc->nbuf <= (size_t) st.len &&
				    memcmp(inc->buf, st.ptr, inc->nbuf) != 0)
					pass(inc, buf + n, &n);
			}
			break;

		case SSI_EXEC:
		case SSI_CALL:
			break;

		default:
			/* Never happens */
			abort();
			break;
		}

	if (ssi->nest > 0 && n + inc->nbuf < len && ch == EOF) {
		(void) fclose(inc->fp);
		inc->fp = NULL;
		ssi->nest--;
		inc--;
		goto again;
	}

	return (n);
}

static void
close_ssi(struct stream *stream)
{
	struct ssi	*ssi = stream->conn->ssi;
	size_t		i;

	for (i = 0; i < NELEMS(ssi->incs); i++) {
		if (ssi->incs[i].fp != NULL)
			(void) fclose(ssi->incs[i].fp);
		if (ssi->incs[i].pipe != NULL)
			(void) pclose(ssi->incs[i].pipe);
	}

	free(ssi);
}

void
_shttpd_do_ssi(struct conn *c)
{
	char		date[64];
	struct ssi	*ssi;

	(void) strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S GMT",
	    localtime(&_shttpd_current_time));

	c->loc.io.head = c->loc.headers_len = _shttpd_snprintf(c->loc.io.buf,
	    c->loc.io.size,
	    "HTTP/1.1 200 OK\r\n"
	    "Date: %s\r\n"
	    "Content-Type: text/html\r\n"
	    "Connection: close\r\n\r\n",
	    date);

	c->status = 200;
	c->loc.io_class = &_shttpd_io_ssi;
	c->loc.flags |= FLAG_R | FLAG_ALWAYS_READY;

	if (c->method == METHOD_HEAD) {
		_shttpd_stop_stream(&c->loc);
	} else if ((ssi = calloc(1, sizeof(struct ssi))) == NULL) {
		_shttpd_send_server_error(c, 500,
		    "Cannot allocate SSI descriptor");
	} else {
		ssi->incs[0].fp = fdopen(c->loc.chan.fd, "r");
		ssi->conn = c;
		c->ssi = ssi;
	}
}

const struct io_class	_shttpd_io_ssi =  {
	"ssi",
	read_ssi,
	NULL,
	close_ssi
};

#endif /* !NO_SSI */
