/*	$OpenBSD$ */

/*
 * Copyright (c) 2009 Michele Marchetto <michele@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <zebra.h>

#include "ldpd.h"
#include "ldp.h"
#include "log.h"
#include "ldpe.h"
#include "ldp_debug.h"

static int	 gen_returned_tlvs(struct ibuf *, uint16_t, uint16_t, char *);
static void	 log_msg_notification(int, struct nbr *, struct notify_msg *);

void
send_notification_full(struct tcp_conn *tcp, struct notify_msg *nm)
{
	struct ibuf	*buf;
	uint16_t	 size;
	int		 err = 0;

	/* calculate size */
	size = LDP_HDR_SIZE + LDP_MSG_SIZE + STATUS_SIZE;
	if (nm->flags & F_NOTIF_PW_STATUS)
		size += PW_STATUS_TLV_SIZE;
	if (nm->flags & F_NOTIF_FEC)
		size += len_fec_tlv(&nm->fec);
	if (nm->flags & F_NOTIF_RETURNED_TLVS)
		size += TLV_HDR_SIZE * 2 + nm->rtlvs.length;

	if ((buf = ibuf_open(size)) == NULL)
		fatal(__func__);

	err |= gen_ldp_hdr(buf, size);
	size -= LDP_HDR_SIZE;
	err |= gen_msg_hdr(buf, MSG_TYPE_NOTIFICATION, size);
	err |= gen_status_tlv(buf, nm->status_code, nm->msg_id, nm->msg_type);
	/* optional tlvs */
	if (nm->flags & F_NOTIF_PW_STATUS)
		err |= gen_pw_status_tlv(buf, nm->pw_status);
	if (nm->flags & F_NOTIF_FEC)
		err |= gen_fec_tlv(buf, &nm->fec);
	if (nm->flags & F_NOTIF_RETURNED_TLVS)
		err |= gen_returned_tlvs(buf, nm->rtlvs.type, nm->rtlvs.length,
		    nm->rtlvs.data);
	if (err) {
		ibuf_free(buf);
		return;
	}

	if (tcp->nbr) {
		log_msg_notification(1, tcp->nbr, nm);
		nbr_fsm(tcp->nbr, NBR_EVT_PDU_SENT);
		tcp->nbr->stats.notif_sent++;
	}

	/* update SNMP session counters */
	switch (nm->status_code) {
	case S_NO_HELLO:
		leconf->stats.session_rejects_hello++;
		break;
	case S_BAD_LDP_ID:
		leconf->stats.bad_ldp_id++;
		break;
	case S_BAD_PDU_LEN:
		leconf->stats.bad_pdu_len++;
		break;
	case S_BAD_MSG_LEN:
		leconf->stats.bad_msg_len++;
		break;
	case S_BAD_TLV_LEN:
		leconf->stats.bad_tlv_len++;
		break;
	case S_BAD_TLV_VAL:
		leconf->stats.malformed_tlv++;
		break;
	case S_KEEPALIVE_TMR:
		leconf->stats.keepalive_timer_exp++;
		break;
	case S_SHUTDOWN:
		leconf->stats.shutdown_send_notify++;
		break;
	default:
		break;
	}

	evbuf_enqueue(&tcp->wbuf, buf);
}

/* send a notification without optional tlvs */
void
send_notification(struct tcp_conn *tcp, uint32_t status_code, uint32_t msg_id,
    uint16_t msg_type)
{
	struct notify_msg	 nm;

	memset(&nm, 0, sizeof(nm));
	nm.status_code = status_code;
	nm.msg_id = msg_id;
	nm.msg_type = msg_type;

	send_notification_full(tcp, &nm);
}

void
send_notification_rtlvs(struct nbr *nbr, uint32_t status_code, uint32_t msg_id,
    uint16_t msg_type, uint16_t tlv_type, uint16_t tlv_len, char *tlv_data)
{
	struct notify_msg	 nm;

	memset(&nm, 0, sizeof(nm));
	nm.status_code = status_code;
	nm.msg_id = msg_id;
	nm.msg_type = msg_type;
	/* do not append the given TLV if it's too big (shouldn't happen) */
	if (tlv_len < 1024) {
		nm.rtlvs.type = tlv_type;
		nm.rtlvs.length = tlv_len;
		nm.rtlvs.data = tlv_data;
		nm.flags |= F_NOTIF_RETURNED_TLVS;
	}

	send_notification_full(nbr->tcp, &nm);
}

int
recv_notification(struct nbr *nbr, char *buf, uint16_t len)
{
	struct ldp_msg		msg;
	struct status_tlv	st;
	struct notify_msg	nm;
	int			tlen;

	memcpy(&msg, buf, sizeof(msg));
	buf += LDP_MSG_SIZE;
	len -= LDP_MSG_SIZE;

	if (len < STATUS_SIZE) {
		session_shutdown(nbr, S_BAD_MSG_LEN, msg.id, msg.type);
		leconf->stats.bad_msg_len++;
		return (-1);
	}
	memcpy(&st, buf, sizeof(st));

	if (ntohs(st.length) > STATUS_SIZE - TLV_HDR_SIZE ||
	    ntohs(st.length) > len - TLV_HDR_SIZE) {
		session_shutdown(nbr, S_BAD_TLV_LEN, msg.id, msg.type);
		leconf->stats.bad_tlv_len++;
		return (-1);
	}
	buf += STATUS_SIZE;
	len -= STATUS_SIZE;

	memset(&nm, 0, sizeof(nm));
	nm.status_code = ntohl(st.status_code);

	/* Optional Parameters */
	while (len > 0) {
		struct tlv 	tlv;
		uint16_t	tlv_type;
		uint16_t	tlv_len;

		if (len < sizeof(tlv)) {
			session_shutdown(nbr, S_BAD_TLV_LEN, msg.id, msg.type);
			leconf->stats.bad_tlv_len++;
			return (-1);
		}

		memcpy(&tlv, buf, TLV_HDR_SIZE);
		tlv_type = ntohs(tlv.type);
		tlv_len = ntohs(tlv.length);
		if (tlv_len + TLV_HDR_SIZE > len) {
			session_shutdown(nbr, S_BAD_TLV_LEN, msg.id, msg.type);
			leconf->stats.bad_tlv_len++;
			return (-1);
		}
		buf += TLV_HDR_SIZE;
		len -= TLV_HDR_SIZE;

		switch (tlv_type) {
		case TLV_TYPE_EXTSTATUS:
		case TLV_TYPE_RETURNEDPDU:
		case TLV_TYPE_RETURNEDMSG:
			/* TODO is there any use for this? */
			break;
		case TLV_TYPE_PW_STATUS:
			if (tlv_len != 4) {
				session_shutdown(nbr, S_BAD_TLV_LEN,
				    msg.id, msg.type);
				return (-1);
			}

			nm.pw_status = ntohl(*(uint32_t *)buf);
			nm.flags |= F_NOTIF_PW_STATUS;
			break;
		case TLV_TYPE_FEC:
			if ((tlen = tlv_decode_fec_elm(nbr, &msg, buf,
			    tlv_len, &nm.fec)) == -1)
				return (-1);
			/* allow only one fec element */
			if (tlen != tlv_len) {
				session_shutdown(nbr, S_BAD_TLV_VAL,
				    msg.id, msg.type);
				leconf->stats.bad_tlv_len++;
				return (-1);
			}
			nm.flags |= F_NOTIF_FEC;
			break;
		default:
			if (!(ntohs(tlv.type) & UNKNOWN_FLAG)) {
				nbr->stats.unknown_tlv++;
				send_notification_rtlvs(nbr, S_UNKNOWN_TLV,
				    msg.id, msg.type, tlv_type, tlv_len, buf);
			}
			/* ignore unknown tlv */
			break;
		}
		buf += tlv_len;
		len -= tlv_len;
	}

	/* sanity checks */
	switch (nm.status_code) {
	case S_PW_STATUS:
		if (!(nm.flags & (F_NOTIF_PW_STATUS|F_NOTIF_FEC))) {
			send_notification(nbr->tcp, S_MISS_MSG,
			    msg.id, msg.type);
			return (-1);
		}

		switch (nm.fec.type) {
		case MAP_TYPE_PWID:
			break;
		default:
			send_notification(nbr->tcp, S_BAD_TLV_VAL,
			    msg.id, msg.type);
			return (-1);
		}
		break;
	case S_ENDOFLIB:
		if (!(nm.flags & F_NOTIF_FEC)) {
			send_notification(nbr->tcp, S_MISS_MSG,
			    msg.id, msg.type);
			return (-1);
		}
		if (nm.fec.type != MAP_TYPE_TYPED_WCARD) {
			send_notification(nbr->tcp, S_BAD_TLV_VAL,
			    msg.id, msg.type);
			return (-1);
		}
		break;
	default:
		break;
	}

	log_msg_notification(0, nbr, &nm);

	if (st.status_code & htonl(STATUS_FATAL)) {
		if (nbr->state == NBR_STA_OPENSENT)
			nbr_start_idtimer(nbr);

		/*
	 	 * RFC 5036 - Section 3.5.1.1:
		 * "When an LSR receives a Shutdown message during session
		 * initialization, it SHOULD transmit a Shutdown message and
		 * then close the transport connection".
		 */
		if (nbr->state != NBR_STA_OPER &&
		    nm.status_code == S_SHUTDOWN) {
			leconf->stats.session_attempts++;
			send_notification(nbr->tcp, S_SHUTDOWN,
			    msg.id, msg.type);
		}

		leconf->stats.shutdown_rcv_notify++;
		nbr_fsm(nbr, NBR_EVT_CLOSE_SESSION);
		return (-1);
	}

	/* lde needs to know about a few notification messages
	 * and update SNMP session counters
	 */
	switch (nm.status_code) {
	case S_PW_STATUS:
	case S_ENDOFLIB:
		ldpe_imsg_compose_lde(IMSG_NOTIFICATION, nbr->peerid, 0,
		    &nm, sizeof(nm));
		break;
	case S_NO_HELLO:
		leconf->stats.session_rejects_hello++;
		break;
	case S_PARM_ADV_MODE:
		leconf->stats.session_rejects_ad++;
		break;
	case S_MAX_PDU_LEN:
		leconf->stats.session_rejects_max_pdu++;
		break;
	case S_PARM_L_RANGE:
		leconf->stats.session_rejects_lr++;
		break;
	case S_BAD_LDP_ID:
		leconf->stats.bad_ldp_id++;
		break;
	case S_BAD_PDU_LEN:
		leconf->stats.bad_pdu_len++;
		break;
	case S_BAD_MSG_LEN:
		leconf->stats.bad_msg_len++;
		break;
	case S_BAD_TLV_LEN:
		leconf->stats.bad_tlv_len++;
		break;
	case S_BAD_TLV_VAL:
		leconf->stats.malformed_tlv++;
		break;
	case S_SHUTDOWN:
		leconf->stats.shutdown_rcv_notify++;
		break;
	default:
		break;
	}

	return (0);
}

int
gen_status_tlv(struct ibuf *buf, uint32_t status_code, uint32_t msg_id,
    uint16_t msg_type)
{
	struct status_tlv	st;

	memset(&st, 0, sizeof(st));
	st.type = htons(TLV_TYPE_STATUS);
	st.length = htons(STATUS_TLV_LEN);
	st.status_code = htonl(status_code);
	/*
	 * For convenience, msg_id and msg_type are already in network
	 * byte order.
	 */
	st.msg_id = msg_id;
	st.msg_type = msg_type;

	return (ibuf_add(buf, &st, STATUS_SIZE));
}

static int
gen_returned_tlvs(struct ibuf *buf, uint16_t type, uint16_t length,
    char *tlv_data)
{
	struct tlv	 rtlvs;
	struct tlv	 tlv;
	int		 err;

	rtlvs.type = htons(TLV_TYPE_RETURNED_TLVS);
	rtlvs.length = htons(length + TLV_HDR_SIZE);
	tlv.type = htons(type);
	tlv.length = htons(length);

	err = ibuf_add(buf, &rtlvs, sizeof(rtlvs));
	err |= ibuf_add(buf, &tlv, sizeof(tlv));
	err |= ibuf_add(buf, tlv_data, length);

	return (err);
}

void
log_msg_notification(int out, struct nbr *nbr, struct notify_msg *nm)
{
	if (nm->status_code & STATUS_FATAL) {
		debug_msg(out, "notification: lsr-id %pI4, status %s (fatal error)", &nbr->id,
		    status_code_name(nm->status_code));
		return;
	}

	debug_msg(out, "notification: lsr-id %pI4, status %s",
	    &nbr->id, status_code_name(nm->status_code));
	if (nm->flags & F_NOTIF_FEC)
		debug_msg(out, "notification:   fec %s", log_map(&nm->fec));
	if (nm->flags & F_NOTIF_PW_STATUS)
		debug_msg(out, "notification:   pw-status %s",
		    (nm->pw_status == PW_FORWARDING) ? "forwarding" : "not forwarding");
}
