/**
 * @file stun/ctrans.c  STUN Client transactions
 *
 * Copyright (C) 2010 Creytiv.com
 */
#include <string.h>
#include <re_types.h>
#include <re_fmt.h>
#include <re_mem.h>
#include <re_mbuf.h>
#include <re_sa.h>
#include <re_udp.h>
#include <re_tcp.h>
#include <re_list.h>
#include <re_tmr.h>
#include <re_md5.h>
#include <re_stun.h>
#include "stun.h"


struct stun_ctrans {
	struct le le;
	struct tmr tmr;
	struct sa dst;
	uint8_t tid[STUN_TID_SIZE];
	struct stun_ctrans **ctp;
	uint8_t *key;
	size_t keylen;
	void *sock;
	struct mbuf *mb;
	size_t pos;
	struct stun *stun;
	stun_resp_h *resph;
	void *arg;
	int proto;
	uint32_t txc;
	uint32_t ival;
	uint16_t met;
};


static void completed(struct stun_ctrans *ct, int err, uint16_t scode,
		      const char *reason, const struct stun_msg *msg)
{
	stun_resp_h *resph = ct->resph;
	void *arg = ct->arg;

	list_unlink(&ct->le);
	tmr_cancel(&ct->tmr);

	if (ct->ctp) {
		*ct->ctp = NULL;
		ct->ctp = NULL;
	}

	ct->resph = NULL;

	/* must be destroyed before calling handler */
	mem_deref(ct);

	if (resph)
		resph(err, scode, reason, msg, arg);
}


static void destructor(void *arg)
{
	struct stun_ctrans *ct = arg;

	list_unlink(&ct->le);
	tmr_cancel(&ct->tmr);
	mem_deref(ct->key);
	mem_deref(ct->sock);
	mem_deref(ct->mb);
}


static void timeout_handler(void *arg)
{
	struct stun_ctrans *ct = arg;
	const struct stun_conf *cfg = stun_conf(ct->stun);
	int err = ETIMEDOUT;

	if (ct->txc++ >= cfg->rc)
		goto error;

	ct->mb->pos = ct->pos;
	err = udp_send(ct->sock, &ct->dst, ct->mb);
	if (err)
		goto error;

	ct->ival = (ct->txc >= cfg->rc) ? cfg->rto * cfg->rm : ct->ival * 2;

	tmr_start(&ct->tmr, ct->ival, timeout_handler, ct);
	return;

 error:
	completed(ct, err, 0, NULL, NULL);
}


static bool match_handler(struct le *le, void *arg)
{
	struct stun_ctrans *ct = le->data;
	struct stun_msg *msg = arg;

	if (ct->met != stun_msg_method(msg))
		return false;

	if (memcmp(ct->tid, stun_msg_tid(msg), STUN_TID_SIZE))
		return false;

	return true;
}


static void udp_recv_handler(const struct sa *src, struct mbuf *mb, void *arg)
{
	struct stun *stun = arg;
	(void)src;

	(void)stun_recv(stun, mb);
}


static void tcp_recv_handler(struct mbuf *mb, void *arg)
{
	struct stun_ctrans *ct = arg;

	(void)stun_recv(ct->stun, mb);
}


static void tcp_estab_handler(void *arg)
{
	struct stun_ctrans *ct = arg;
	int err;

	err = tcp_send(ct->sock, ct->mb);
	if (!err)
		return;

	completed(ct, err, 0, NULL, NULL);
}


static void tcp_close_handler(int err, void *arg)
{
	struct stun_ctrans *ct = arg;

	completed(ct, err, 0, NULL, NULL);
}


int stun_ctrans_recv(struct stun *stun, const struct stun_msg *msg,
		     const struct stun_unknown_attr *ua)
{
	struct stun_errcode ec = {0, "OK"};
	struct stun_attr *errcode;
	struct stun_ctrans *ct;
	int err = 0, herr = 0;

	if (!stun || !msg || !ua)
		return EINVAL;

	switch (stun_msg_class(msg)) {

	case STUN_CLASS_ERROR_RESP:
		errcode = stun_msg_attr(msg, STUN_ATTR_ERR_CODE);
		if (!errcode)
			herr = EPROTO;
		else
			ec = errcode->v.err_code;
		/*@fallthrough@*/

	case STUN_CLASS_SUCCESS_RESP:
		ct = list_ledata(list_apply(&stun->ctl, true,
					    match_handler, (void *)msg));
		if (!ct) {
			err = ENOENT;
			break;
		}

		switch (ec.code) {

		case 401:
		case 438:
			break;

		default:
			if (!ct->key)
				break;

			err = stun_msg_chk_mi(msg, ct->key, ct->keylen);
			break;
		}

		if (err)
			break;

		if (!herr && ua->typec > 0)
			herr = EPROTO;

		completed(ct, herr, ec.code, ec.reason, msg);
		break;

	default:
		break;
	}

	return err;
}


int stun_ctrans_request(struct stun_ctrans **ctp, struct stun *stun, int proto,
			void *sock, const struct sa *dst, struct mbuf *mb,
			const uint8_t tid[], uint16_t met, const uint8_t *key,
			size_t keylen, stun_resp_h *resph, void *arg)
{
	struct stun_ctrans *ct;
	int err = 0;

	if (!stun || !mb)
		return EINVAL;

	ct = mem_zalloc(sizeof(*ct), destructor);
	if (!ct)
		return ENOMEM;

	list_append(&stun->ctl, &ct->le, ct);
	memcpy(ct->tid, tid, STUN_TID_SIZE);
	ct->proto = proto;
	ct->sock  = mem_ref(sock);
	ct->mb    = mem_ref(mb);
	ct->pos   = mb->pos;
	ct->stun  = stun;
	ct->met   = met;

	if (key) {
		ct->key = mem_alloc(keylen, NULL);
		if (!ct->key) {
			err = ENOMEM;
			goto out;
		}

		memcpy(ct->key, key, keylen);
		ct->keylen = keylen;
	}

	switch (proto) {

	case IPPROTO_UDP:
		if (!dst) {
			err = EINVAL;
			break;
		}

		ct->dst = *dst;
		ct->ival = stun_conf(stun)->rto;
		tmr_start(&ct->tmr, ct->ival, timeout_handler, ct);

		if (!sock) {
			err = udp_listen((struct udp_sock **)&ct->sock, NULL,
					 udp_recv_handler, stun);
			if (err)
				break;
		}

		ct->txc = 1;
		err = udp_send(ct->sock, dst, mb);
		break;

	case IPPROTO_TCP:
		ct->txc = stun_conf(stun)->rc;
		tmr_start(&ct->tmr, stun_conf(stun)->ti, timeout_handler, ct);
		if (sock) {
			err = tcp_send(sock, mb);
			break;
		}

		err = tcp_connect((struct tcp_conn **)&ct->sock, dst,
				  tcp_estab_handler, tcp_recv_handler,
				  tcp_close_handler, ct);
		break;

	default:
		err = EPROTONOSUPPORT;
		break;
	}

 out:
	if (!err) {
		if (ctp) {
			ct->ctp = ctp;
			*ctp = ct;
		}

		ct->resph = resph;
		ct->arg   = arg;
	}
	else
		mem_deref(ct);

	return err;
}


static bool close_handler(struct le *le, void *arg)
{
	struct stun_ctrans *ct = le->data;
	(void)arg;

	completed(ct, ECONNABORTED, 0, NULL, NULL);

	return false;
}


void stun_ctrans_close(struct stun *stun)
{
	if (!stun)
		return;

	(void)list_apply(&stun->ctl, true, close_handler, NULL);
}


static bool debug_handler(struct le *le, void *arg)
{
	struct stun_ctrans *ct = le->data;
	struct re_printf *pf = arg;
	int err = 0;

	err |= re_hprintf(pf, " method=%s", stun_method_name(ct->met));
	err |= re_hprintf(pf, " tid=%w", ct->tid, sizeof(ct->tid));
	err |= re_hprintf(pf, " rto=%ums", stun_conf(ct->stun)->rto);
	err |= re_hprintf(pf, " tmr=%llu", tmr_get_expire(&ct->tmr));
	err |= re_hprintf(pf, " n=%u", ct->txc);
	err |= re_hprintf(pf, " interval=%u", ct->ival);
	err |= re_hprintf(pf, "\n");

	return 0 != err;
}


int stun_ctrans_debug(struct re_printf *pf, const struct stun *stun)
{
	int err;

	if (!stun)
		return 0;

	err = re_hprintf(pf, "STUN client transactions: (%u)\n",
			 list_count(&stun->ctl));

	(void)list_apply(&stun->ctl, true, debug_handler, pf);

	return err;
}
