/**
 * @file websock.c  Implementation of The WebSocket Protocol
 *
 * Copyright (C) 2010 Creytiv.com
 */

#include <re_types.h>
#include <re_fmt.h>
#include <re_mem.h>
#include <re_mbuf.h>
#include <re_sa.h>
#include <re_list.h>
#include <re_tmr.h>
#include <re_srtp.h>
#include <re_tcp.h>
#include <re_tls.h>
#include <re_msg.h>
#include <re_http.h>
#include <re_base64.h>
#include <re_sha.h>
#include <re_sys.h>
#include <re_websock.h>


enum {
	TIMEOUT_CLOSE = 10000,
	BUFSIZE_MAX   = 131072,
};

enum websock_state {
	ACCEPTING = 0,
	CONNECTING,
	OPEN,
	CLOSING,
	CLOSED,
};

struct websock {
	websock_shutdown_h *shuth;
	void *arg;
	bool shutdown;
};

struct websock_conn {
	struct tmr tmr;
	struct sa peer;
	char nonce[24];
	struct websock *sock;
	struct tcp_conn *tc;
	struct tls_conn *sc;
	struct mbuf *mb;
	struct http_req *req;
	websock_estab_h *estabh;
	websock_recv_h *recvh;
	websock_close_h *closeh;
	void *arg;
	enum websock_state state;
	unsigned kaint;
	bool active;
};


static const char magic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";


static void timeout_handler(void *arg);


static void dummy_recv_handler(const struct websock_hdr *hdr, struct mbuf *mb,
			       void *arg)
{
	(void)hdr;
	(void)mb;
	(void)arg;
}


static void internal_close_handler(int err, void *arg)
{
	struct websock_conn *conn = arg;
	(void)err;

	mem_deref(conn);
}


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

	if (sock->shutdown) {
		sock->shutdown = false;
		mem_ref(sock);
		if (sock->shuth)
			sock->shuth(sock->arg);
		return;
	}
}


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

	if (conn->state == OPEN)
		(void)websock_close(conn, WEBSOCK_GOING_AWAY, "Going Away");

	if (conn->state == CLOSING) {

		conn->recvh  = dummy_recv_handler;
		conn->closeh = internal_close_handler;
		conn->arg    = conn;

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

		/* important: the hack below depends on this */
		mem_ref(conn);
		return;
	}

	tmr_cancel(&conn->tmr);
	mem_deref(conn->sc);
	mem_deref(conn->tc);
	mem_deref(conn->mb);
	mem_deref(conn->req);
	mem_deref(conn->sock);
}


static void conn_close(struct websock_conn *conn, int err)
{
	tmr_cancel(&conn->tmr);
	conn->sc = mem_deref(conn->sc);
	conn->tc = mem_deref(conn->tc);
	conn->state = CLOSED;

	conn->closeh(err, conn->arg);
}


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

	conn_close(conn, ETIMEDOUT);
}


static void keepalive_handler(void *arg)
{
	struct websock_conn *conn = arg;

	tmr_start(&conn->tmr, conn->kaint, keepalive_handler, conn);

	(void)websock_send(conn, WEBSOCK_PING, NULL);
}


static enum websock_scode websock_err2scode(int err)
{
	switch (err) {

	case EOVERFLOW: return WEBSOCK_MESSAGE_TOO_BIG;
	case EPROTO:    return WEBSOCK_PROTOCOL_ERROR;
	case EBADMSG:   return WEBSOCK_PROTOCOL_ERROR;
	default:        return WEBSOCK_INTERNAL_ERROR;
	}
}


static int websock_decode(struct websock_hdr *hdr, struct mbuf *mb)
{
	uint8_t v, *p;
	size_t i;

	if (mbuf_get_left(mb) < 2)
		return ENODATA;

	v = mbuf_read_u8(mb);
	hdr->fin    = v>>7 & 0x1;
	hdr->rsv1   = v>>6 & 0x1;
	hdr->rsv2   = v>>5 & 0x1;
	hdr->rsv3   = v>>4 & 0x1;
	hdr->opcode = v    & 0x0f;

	v = mbuf_read_u8(mb);
	hdr->mask = v>>7 & 0x1;
	hdr->len  = v    & 0x7f;

	if (hdr->len == 126) {

		if (mbuf_get_left(mb) < 2)
			return ENODATA;

		hdr->len = ntohs(mbuf_read_u16(mb));
	}
	else if (hdr->len == 127) {

		if (mbuf_get_left(mb) < 8)
			return ENODATA;

		hdr->len = sys_ntohll(mbuf_read_u64(mb));
	}

	if (hdr->mask) {

		if (mbuf_get_left(mb) < (4 + hdr->len))
			return ENODATA;

		hdr->mkey[0] = mbuf_read_u8(mb);
		hdr->mkey[1] = mbuf_read_u8(mb);
		hdr->mkey[2] = mbuf_read_u8(mb);
		hdr->mkey[3] = mbuf_read_u8(mb);

		for (i=0, p=mbuf_buf(mb); i<hdr->len; i++)
			p[i] = p[i] ^ hdr->mkey[i%4];
	}
	else {
		if (mbuf_get_left(mb) < hdr->len)
			return ENODATA;
	}

	return 0;
}


static void recv_handler(struct mbuf *mb, void *arg)
{
	struct websock_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) {

		struct websock_hdr hdr;
		size_t pos, end;

		pos = conn->mb->pos;

		err = websock_decode(&hdr, conn->mb);
		if (err) {
			if (err == ENODATA) {
				conn->mb->pos = pos;
				err = 0;
				break;
			}

			goto out;
		}

		if (conn->active == hdr.mask) {
			err = EPROTO;
			goto out;
		}

		if (hdr.rsv1 || hdr.rsv2 || hdr.rsv3) {
			err = EPROTO;
			goto out;
		}

		mb = conn->mb;

		end     = mb->end;
		mb->end = mb->pos + (size_t)hdr.len;

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

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

			conn->mb = mbn;
		}
		else {
			conn->mb = NULL;
		}

		switch (hdr.opcode) {

		case WEBSOCK_CONT:
		case WEBSOCK_TEXT:
		case WEBSOCK_BIN:
			mem_ref(conn);
			conn->recvh(&hdr, mb, conn->arg);

			if (mem_nrefs(conn) == 1) {

				if (conn->state == OPEN)
					(void)websock_close(conn,
						            WEBSOCK_GOING_AWAY,
							    "Going Away");

				/*
				 * This is a hack. We enforce CLOSING
				 * state so we know the connection will
				 * continue to live.
				 */
				conn->state = CLOSING;
			}
			mem_deref(conn);
			break;

		case WEBSOCK_CLOSE:
			if (conn->state == OPEN)
				(void)websock_send(conn, WEBSOCK_CLOSE, "%b",
					     mbuf_buf(mb), mbuf_get_left(mb));
			conn_close(conn, 0);
			mem_deref(mb);
			return;

		case WEBSOCK_PING:
			(void)websock_send(conn, WEBSOCK_PONG, "%b",
					   mbuf_buf(mb), mbuf_get_left(mb));
			break;

		case WEBSOCK_PONG:
			break;

		default:
			mem_deref(mb);
			err = EPROTO;
			goto out;
		}

		mem_deref(mb);
	}

 out:
	if (err) {
		(void)websock_close(conn, websock_err2scode(err), NULL);
		conn_close(conn, err);
	}
}


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

	conn_close(conn, err);
}


static int accept_print(struct re_printf *pf, const struct pl *key)
{
	uint8_t digest[SHA_DIGEST_LENGTH];
	SHA_CTX ctx;

	SHA1_Init(&ctx);
	SHA1_Update(&ctx, key->p, key->l);
	SHA1_Update(&ctx, magic, sizeof(magic)-1);
	SHA1_Final(digest, &ctx);

	return base64_print(pf, digest, sizeof(digest));
}


static void http_resp_handler(int err, const struct http_msg *msg, void *arg)
{
	struct websock_conn *conn = arg;
	const struct http_hdr *hdr;
	struct pl key;
	char buf[32];

	if (err || msg->scode != 101)
		goto fail;

	if (!http_msg_hdr_has_value(msg, HTTP_HDR_UPGRADE, "websocket"))
		goto fail;

	if (!http_msg_hdr_has_value(msg, HTTP_HDR_CONNECTION, "Upgrade"))
		goto fail;

	hdr = http_msg_hdr(msg, HTTP_HDR_SEC_WEBSOCKET_ACCEPT);
	if (!hdr)
		goto fail;

	key.p = conn->nonce;
	key.l = sizeof(conn->nonce);

	if (re_snprintf(buf, sizeof(buf), "%H", accept_print, &key) < 0)
		goto fail;

	if (pl_strcmp(&hdr->val, buf))
		goto fail;

	/* here we are ok */

	conn->state = OPEN;
	(void)tcp_conn_peer_get(conn->tc, &conn->peer);

	if (conn->kaint)
		tmr_start(&conn->tmr, conn->kaint, keepalive_handler, conn);

	conn->estabh(conn->arg);
	return;

 fail:
	conn_close(conn, err ? err : EPROTO);
}


static void http_conn_handler(struct tcp_conn *tc, struct tls_conn *sc,
			      void *arg)
{
	struct websock_conn *conn = arg;

	conn->tc = mem_ref(tc);
	conn->sc = mem_ref(sc);

	tcp_set_handlers(conn->tc, NULL, recv_handler, close_handler, conn);
}


int websock_connect(struct websock_conn **connp, struct websock *sock,
		    struct http_cli *cli, const char *uri, unsigned kaint,
		    websock_estab_h *estabh, websock_recv_h *recvh,
		    websock_close_h *closeh, void *arg,
		    const char *fmt, ...)
{
	struct websock_conn *conn;
	uint8_t nonce[16];
	va_list ap;
	size_t len;
	int err;

	if (!connp || !sock || !cli || !uri || !estabh || !recvh || !closeh)
		return EINVAL;

	conn = mem_zalloc(sizeof(*conn), conn_destructor);
	if (!conn)
		return ENOMEM;

	/* The nonce MUST be selected randomly for each connection */
	rand_bytes(nonce, sizeof(nonce));

	len = sizeof(conn->nonce);

	err = base64_encode(nonce, sizeof(nonce), conn->nonce, &len);
	if (err)
		goto out;

	conn->sock   = mem_ref(sock);
	conn->kaint  = kaint;
	conn->estabh = estabh;
	conn->recvh  = recvh;
	conn->closeh = closeh;
	conn->arg    = arg;
	conn->state  = CONNECTING;
	conn->active = true;

	/* Protocol Handshake */
	va_start(ap, fmt);
	err = http_request(&conn->req, cli, "GET", uri,
			   http_resp_handler, NULL, conn,
			   "Upgrade: websocket\r\n"
			   "Connection: upgrade\r\n"
			   "Sec-WebSocket-Key: %b\r\n"
			   "Sec-WebSocket-Version: 13\r\n"
			   "%v"
			   "\r\n",
			   conn->nonce, sizeof(conn->nonce),
			   fmt, &ap);
	va_end(ap);
	if (err)
		goto out;

	http_req_set_conn_handler(conn->req, http_conn_handler);

 out:
	if (err)
		mem_deref(conn);
	else
		*connp = conn;

	return err;
}


int websock_accept(struct websock_conn **connp, struct websock *sock,
		   struct http_conn *htconn, const struct http_msg *msg,
		   unsigned kaint, websock_recv_h *recvh,
		   websock_close_h *closeh, void *arg)
{
	const struct http_hdr *key;
	struct websock_conn *conn;
	int err;

	if (!connp || !sock || !htconn || !msg || !recvh || !closeh)
		return EINVAL;

	if (!http_msg_hdr_has_value(msg, HTTP_HDR_UPGRADE, "websocket"))
		return EBADMSG;

	if (!http_msg_hdr_has_value(msg, HTTP_HDR_CONNECTION, "Upgrade"))
		return EBADMSG;

	if (!http_msg_hdr_has_value(msg, HTTP_HDR_SEC_WEBSOCKET_VERSION, "13"))
		return EBADMSG;

	key = http_msg_hdr(msg, HTTP_HDR_SEC_WEBSOCKET_KEY);
	if (!key)
		return EBADMSG;

	conn = mem_zalloc(sizeof(*conn), conn_destructor);
	if (!conn)
		return ENOMEM;

	err = http_reply(htconn, 101, "Switching Protocols",
			 "Upgrade: websocket\r\n"
			 "Connection: Upgrade\r\n"
			 "Sec-WebSocket-Accept: %H\r\n"
			 "\r\n",
			 accept_print, &key->val);
	if (err)
		goto out;

	sa_cpy(&conn->peer, http_conn_peer(htconn));
	conn->sock   = mem_ref(sock);
	conn->tc     = mem_ref(http_conn_tcp(htconn));
	conn->sc     = mem_ref(http_conn_tls(htconn));
	conn->kaint  = kaint;
	conn->recvh  = recvh;
	conn->closeh = closeh;
	conn->arg    = arg;
	conn->state  = OPEN;
	conn->active = false;

	tcp_set_handlers(conn->tc, NULL, recv_handler, close_handler, conn);
	http_conn_close(htconn);

	if (conn->kaint)
		tmr_start(&conn->tmr, conn->kaint, keepalive_handler, conn);

 out:
	if (err)
		mem_deref(conn);
	else
		*connp = conn;

	return err;
}


static int websock_encode(struct mbuf *mb, bool fin,
			  enum websock_opcode opcode, bool mask, size_t len)
{
	int err;

	err = mbuf_write_u8(mb, (fin<<7) | (opcode & 0x0f));

	if (len > 0xffff) {
		err |= mbuf_write_u8(mb, (mask<<7) | 127);
		err |= mbuf_write_u64(mb, sys_htonll(len));
	}
	else if (len > 125) {
		err |= mbuf_write_u8(mb, (mask<<7) | 126);
		err |= mbuf_write_u16(mb, htons(len));
	}
	else {
		err |= mbuf_write_u8(mb, (mask<<7) | len);
	}

	if (mask) {
		uint8_t mkey[4];
		uint8_t *p;
		size_t i;

		rand_bytes(mkey, sizeof(mkey));

		err |= mbuf_write_mem(mb, mkey, sizeof(mkey));

		for (i=0, p=mbuf_buf(mb); i<len; i++)
			p[i] = p[i] ^ mkey[i%4];
	}

	return err;
}


static int websock_vsend(struct websock_conn *conn, enum websock_opcode opcode,
			 enum websock_scode scode, const char *fmt, va_list ap)
{
	const size_t hsz = conn->active ? 14 : 10;
	size_t len, start;
	struct mbuf *mb;
	int err = 0;

	if (conn->state != OPEN)
		return ENOTCONN;

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

	mb->pos = hsz;

	if (scode)
		err |= mbuf_write_u16(mb, htons(scode));
	if (fmt)
		err |= mbuf_vprintf(mb, fmt, ap);
	if (err)
		goto out;

	len = mb->pos - hsz;

	if (len > 0xffff)
		start = mb->pos = 0;
	else if (len > 125)
		start = mb->pos = 6;
	else
		start = mb->pos = 8;

	err = websock_encode(mb, true, opcode, conn->active, len);
	if (err)
		goto out;

	mb->pos = start;

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

 out:
	mem_deref(mb);

	return err;
}


int websock_send(struct websock_conn *conn, enum websock_opcode opcode,
		 const char *fmt, ...)
{
	va_list ap;
	int err;

	if (!conn)
		return EINVAL;

	va_start(ap, fmt);
	err = websock_vsend(conn, opcode, 0, fmt, ap);
	va_end(ap);

	return err;
}


int websock_close(struct websock_conn *conn, enum websock_scode scode,
		  const char *fmt, ...)
{
	va_list ap;
	int err;

	if (!conn)
		return EINVAL;

	if (!scode)
		fmt = NULL;

	va_start(ap, fmt);
	err = websock_vsend(conn, WEBSOCK_CLOSE, scode, fmt, ap);
	va_end(ap);

	if (!err)
		conn->state = CLOSING;

	return err;
}


const struct sa *websock_peer(const struct websock_conn *conn)
{
	return conn ? &conn->peer : NULL;
}


struct tcp_conn *websock_tcp(const struct websock_conn *conn)
{
	return conn ? conn->tc : NULL;
}


int websock_alloc(struct websock **sockp, websock_shutdown_h *shuth, void *arg)
{
	struct websock *sock;

	if (!sockp)
		return EINVAL;

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

	sock->shuth = shuth;
	sock->arg   = arg;

	*sockp = sock;

	return 0;
}


void websock_shutdown(struct websock *sock)
{
	if (!sock || sock->shutdown)
		return;

	sock->shutdown = true;
	mem_deref(sock);
}
