/**
 * @file http/server.c HTTP Server
 *
 * Copyright (C) 2011 Creytiv.com
 */

#include <re_types.h>
#include <re_mem.h>
#include <re_mbuf.h>
#include <re_sa.h>
#include <re_list.h>
#include <re_fmt.h>
#include <re_tmr.h>
#include <re_tcp.h>
#include <re_tls.h>
#include <re_http.h>


enum {
	TIMEOUT_IDLE = 600000,
	TIMEOUT_INIT = 10000,
	BUFSIZE_MAX  = 524288,
};

struct http_sock {
	struct list connl;
	struct tcp_sock *ts;
	struct tls *tls;
	http_req_h *reqh;
	void *arg;
};

struct http_conn {
	struct le le;
	struct tmr tmr;
	struct sa peer;
	struct http_sock *sock;
	struct tcp_conn *tc;
	struct tls_conn *sc;
	struct mbuf *mb;
};


static void conn_close(struct http_conn *conn);


static void sock_destructor(void *arg)
{
	struct http_sock *sock = arg;
	struct le *le;

	for (le=sock->connl.head; le;) {

		struct http_conn *conn = le->data;

		le = le->next;

		conn_close(conn);
		mem_deref(conn);
	}

	mem_deref(sock->tls);
	mem_deref(sock->ts);
}


static void conn_destructor(void *arg)
{
	struct http_conn *conn = arg;

	list_unlink(&conn->le);
	tmr_cancel(&conn->tmr);
	mem_deref(conn->sc);
	mem_deref(conn->tc);
	mem_deref(conn->mb);
}


static void conn_close(struct http_conn *conn)
{
	list_unlink(&conn->le);
	tmr_cancel(&conn->tmr);
	conn->sc = mem_deref(conn->sc);
	conn->tc = mem_deref(conn->tc);
	conn->sock = NULL;
}


static void timeout_handler(void *arg)
{
	struct http_conn *conn = arg;

	conn_close(conn);
	mem_deref(conn);
}


static void recv_handler(struct mbuf *mb, void *arg)
{
	struct http_conn *conn = arg;
	int err = 0;

	if (conn->mb) {

		const size_t len = mbuf_get_left(mb), pos = conn->mb->pos;

		if ((mbuf_get_left(conn->mb) + len) > BUFSIZE_MAX) {
			err = EOVERFLOW;
			goto out;
		}

		conn->mb->pos = conn->mb->end;

		err = mbuf_write_mem(conn->mb, mbuf_buf(mb), len);
		if (err)
			goto out;

		conn->mb->pos = pos;
	}
	else {
		conn->mb = mem_ref(mb);
	}

	while (conn->mb) {
		size_t end, pos = conn->mb->pos;
		struct http_msg *msg;

		err = http_msg_decode(&msg, conn->mb, true);
		if (err) {
			if (err == ENODATA) {
				conn->mb->pos = pos;
				err = 0;
				break;
			}

			goto out;
		}

		if (mbuf_get_left(conn->mb) < msg->clen) {
			conn->mb->pos = pos;
			mem_deref(msg);
			break;
		}

		mb = conn->mb;

		end     = mb->end;
		mb->end = mb->pos + msg->clen;

		if (end > mb->end) {
			struct mbuf *mbn = mbuf_alloc(end - mb->end);
			if (!mbn) {
				mem_deref(msg);
				err = ENOMEM;
				goto out;
			}

			(void)mbuf_write_mem(mbn, mb->buf + mb->end,
					     end - mb->end);
			mbn->pos = 0;

			mem_deref(conn->mb);
			conn->mb = mbn;
		}
		else {
			conn->mb = mem_deref(conn->mb);
		}

		if (conn->sock)
			conn->sock->reqh(conn, msg, conn->sock->arg);

		mem_deref(msg);

		if (!conn->tc) {
			err = ENOTCONN;
			goto out;
		}

		tmr_start(&conn->tmr, TIMEOUT_IDLE, timeout_handler, conn);
	}

 out:
	if (err) {
		conn_close(conn);
		mem_deref(conn);
	}
}


static void close_handler(int err, void *arg)
{
	struct http_conn *conn = arg;
	(void)err;

	conn_close(conn);
	mem_deref(conn);
}


static void connect_handler(const struct sa *peer, void *arg)
{
	struct http_sock *sock = arg;
	struct http_conn *conn;
	int err;

	conn = mem_zalloc(sizeof(*conn), conn_destructor);
	if (!conn) {
		err = ENOMEM;
		goto out;
	}

	list_append(&sock->connl, &conn->le, conn);
	conn->peer = *peer;
	conn->sock = sock;

	err = tcp_accept(&conn->tc, sock->ts, NULL, recv_handler,
			 close_handler, conn);
	if (err)
		goto out;

#ifdef USE_TLS
	if (sock->tls) {
		err = tls_start_tcp(&conn->sc, sock->tls, conn->tc, 0);
		if (err)
			goto out;
	}
#endif

	tmr_start(&conn->tmr, TIMEOUT_INIT, timeout_handler, conn);

 out:
	if (err) {
		mem_deref(conn);
		tcp_reject(sock->ts);
	}
}


/**
 * Create an HTTP socket
 *
 * @param sockp Pointer to returned HTTP Socket
 * @param laddr Network address to listen on
 * @param reqh  Request handler
 * @param arg   Handler argument
 *
 * @return 0 if success, otherwise errorcode
 */
int http_listen(struct http_sock **sockp, const struct sa *laddr,
		http_req_h *reqh, void *arg)
{
	struct http_sock *sock;
	int err;

	if (!sockp || !laddr || !reqh)
		return EINVAL;

	sock = mem_zalloc(sizeof(*sock), sock_destructor);
	if (!sock)
		return ENOMEM;

	err = tcp_listen(&sock->ts, laddr, connect_handler, sock);
	if (err)
		goto out;

	sock->reqh = reqh;
	sock->arg  = arg;

 out:
	if (err)
		mem_deref(sock);
	else
		*sockp = sock;

	return err;
}


/**
 * Create an HTTP secure socket
 *
 * @param sockp Pointer to returned HTTP Socket
 * @param laddr Network address to listen on
 * @param cert  File path of TLS certificate
 * @param reqh  Request handler
 * @param arg   Handler argument
 *
 * @return 0 if success, otherwise errorcode
 */
int https_listen(struct http_sock **sockp, const struct sa *laddr,
		 const char *cert, http_req_h *reqh, void *arg)
{
	struct http_sock *sock;
	int err;

	if (!sockp || !laddr || !cert || !reqh)
		return EINVAL;

	err = http_listen(&sock, laddr, reqh, arg);
	if (err)
		return err;

#ifdef USE_TLS
	err = tls_alloc(&sock->tls, TLS_METHOD_SSLV23, cert, NULL);
#else
	err = EPROTONOSUPPORT;
#endif
	if (err)
		goto out;

 out:
	if (err)
		mem_deref(sock);
	else
		*sockp = sock;

	return err;
}


/**
 * Get the peer address of an HTTP connection
 *
 * @param conn HTTP connection
 *
 * @return Peer address
 */
const struct sa *http_conn_peer(const struct http_conn *conn)
{
	return conn ? &conn->peer : NULL;
}


/**
 * Get the TCP connection of an HTTP connection
 *
 * @param conn HTTP connection
 *
 * @return TCP connection
 */
struct tcp_conn *http_conn_tcp(struct http_conn *conn)
{
	return conn ? conn->tc : NULL;
}


/**
 * Get the TLS connection of an HTTP connection
 *
 * @param conn HTTP connection
 *
 * @return TLS connection
 */
struct tls_conn *http_conn_tls(struct http_conn *conn)
{
	return conn ? conn->sc : NULL;
}


/**
 * Close the HTTP connection
 *
 * @param conn HTTP connection
 */
void http_conn_close(struct http_conn *conn)
{
	if (!conn)
		return;

	conn->sc = mem_deref(conn->sc);
	conn->tc = mem_deref(conn->tc);
}


static int http_vreply(struct http_conn *conn, uint16_t scode,
		       const char *reason, const char *fmt, va_list ap)
{
	struct mbuf *mb;
	int err;

	if (!conn || !scode || !reason)
		return EINVAL;

	if (!conn->tc)
		return ENOTCONN;

	mb = mbuf_alloc(8192);
	if (!mb)
		return ENOMEM;

	err = mbuf_printf(mb, "HTTP/1.1 %u %s\r\n", scode, reason);
	if (fmt)
		err |= mbuf_vprintf(mb, fmt, ap);
	else
		err |= mbuf_write_str(mb, "Content-Length: 0\r\n\r\n");
	if (err)
		goto out;

	mb->pos = 0;

	err = tcp_send(conn->tc, mb);
	if (err)
		goto out;

 out:
	mem_deref(mb);

	return err;
}


/**
 * Send an HTTP response
 *
 * @param conn   HTTP connection
 * @param scode  Response status code
 * @param reason Response reason phrase
 * @param fmt    Formatted HTTP message
 *
 * @return 0 if success, otherwise errorcode
 */
int http_reply(struct http_conn *conn, uint16_t scode, const char *reason,
	       const char *fmt, ...)
{
	va_list ap;
	int err;

	va_start(ap, fmt);
	err = http_vreply(conn, scode, reason, fmt, ap);
	va_end(ap);

	return err;
}


/**
 * Send an HTTP response with content formatting
 *
 * @param conn   HTTP connection
 * @param scode  Response status code
 * @param reason Response reason phrase
 * @param ctype  Content type
 * @param fmt    Formatted HTTP content
 *
 * @return 0 if success, otherwise errorcode
 */
int http_creply(struct http_conn *conn, uint16_t scode, const char *reason,
		const char *ctype, const char *fmt, ...)
{
	struct mbuf *mb;
	va_list ap;
	int err;

	if (!ctype || !fmt)
		return EINVAL;

	mb = mbuf_alloc(8192);
	if (!mb)
		return ENOMEM;

	va_start(ap, fmt);
	err = mbuf_vprintf(mb, fmt, ap);
	va_end(ap);
	if (err)
		goto out;

	err = http_reply(conn, scode, reason,
			 "Content-Type: %s\r\n"
			 "Content-Length: %zu\r\n"
			 "\r\n"
			 "%b",
			 ctype,
			 mb->end,
			 mb->buf, mb->end);
	if (err)
		goto out;

 out:
	mem_deref(mb);

	return err;
}


/**
 * Send an HTTP error response
 *
 * @param conn   HTTP connection
 * @param scode  Response status code
 * @param reason Response reason phrase
 *
 * @return 0 if success, otherwise errorcode
 */
int http_ereply(struct http_conn *conn, uint16_t scode, const char *reason)
{
	return http_creply(conn, scode, reason, "text/html",
			   "<!DOCTYPE html>\n"
			   "<html>\n"
			   "<head><title>%u %s</title></head>\n"
			   "<body><h2>%u %s</h2></body>\n"
			   "</html>\n",
			   scode, reason,
			   scode, reason);
}
