/*********************************************************************************************************
* Software License Agreement (BSD License)                                                               *
* Author: Sebastien Decugis <sdecugis@freediameter.net>							 *
*													 *
* Copyright (c) 2015, WIDE Project and NICT								 *
* All rights reserved.											 *
* 													 *
* Redistribution and use of this software in source and binary forms, with or without modification, are  *
* permitted provided that the following conditions are met:						 *
* 													 *
* * Redistributions of source code must retain the above 						 *
*   copyright notice, this list of conditions and the 							 *
*   following disclaimer.										 *
*    													 *
* * Redistributions in binary form must reproduce the above 						 *
*   copyright notice, this list of conditions and the 							 *
*   following disclaimer in the documentation and/or other						 *
*   materials provided with the distribution.								 *
* 													 *
* * Neither the name of the WIDE Project or NICT nor the 						 *
*   names of its contributors may be used to endorse or 						 *
*   promote products derived from this software without 						 *
*   specific prior written permission of WIDE Project and 						 *
*   NICT.												 *
* 													 *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED *
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A *
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR *
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 	 *
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 	 *
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR *
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF   *
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.								 *
*********************************************************************************************************/

#include "rt_redir.h"

/* This structure contains the data to keep when a new peer's connection is attempted (for later) */
struct redir_task {
	struct msg * answer; /* the message that was being processed */
	uint32_t     rhu;    /* Redirect-Host-Usage value */
	uint32_t     rmct;   /* Redirect-Max-Cache-Time value */
	struct fd_list rh;   /* the list of Redirect-Hosts */
};

/* Received answers FWD callback */
int redir_fwd_cb(void * cbdata, struct msg ** msg)
{
	struct msg * m, * q;
	struct rt_data *rtd;
	struct msg_hdr * hdr;
	union avp_value *a_rc = NULL, *a_rhu = NULL, *a_rmct = NULL, *a_oh = NULL;
	int known = 0, actives = 0;
	struct fd_list * li;
	struct avp * avp;
	struct redir_task task = { .answer = NULL, .rhu = 0, .rmct = 0, .rh = FD_LIST_INITIALIZER(task.rh) };
	DiamId_t nh;
	size_t   nhlen;
	int nbrh = 0;
	struct redir_entry * entry;

	TRACE_ENTRY("%p %p", cbdata, msg);

	CHECK_PARAMS(msg && *msg);

	m = *msg;

	/* First get the header */
	CHECK_FCT( fd_msg_hdr(m, &hdr) );

	/* Check if we have an error */
	ASSERT(!(hdr->msg_flags & CMD_FLAG_REQUEST));
	if (!(hdr->msg_flags & CMD_FLAG_ERROR)) {
		/* This answer does not have the E flag, no need to process further */
		return 0;
	}

	/* Now get the AVPs we are interested in */
	CHECK_FCT(  fd_msg_browse(m, MSG_BRW_FIRST_CHILD, &avp, NULL)  );
	while (avp) {
		struct avp_hdr * ahdr;

		CHECK_FCT(  fd_msg_avp_hdr( avp, &ahdr )  );
		if (! (ahdr->avp_flags & AVP_FLAG_VENDOR)) {
			switch (ahdr->avp_code) {
				case AC_ORIGIN_HOST:
					/* Parse this AVP */
					CHECK_FCT( fd_msg_parse_dict ( avp, fd_g_config->cnf_dict, NULL ) );
					ASSERT( ahdr->avp_value );
					a_oh = ahdr->avp_value;
					break;

				case AC_RESULT_CODE:
					/* Parse this AVP */
					CHECK_FCT( fd_msg_parse_dict ( avp, fd_g_config->cnf_dict, NULL ) );
					ASSERT( ahdr->avp_value );
					a_rc = ahdr->avp_value;

					if (a_rc->u32 != ER_DIAMETER_REDIRECT_INDICATION) {
						/* It is not a REDIRECT error, we don't do anything */
						goto out;
					}
					break;

				case AC_REDIRECT_HOST:
					{
						struct redir_host * h = NULL;
						DiamId_t id = NULL;
						size_t	 len = 0;
						int 	 secure = 0;
						uint16_t port = 0;
						int	 l4 = 0;
						char	 proto = 0;

						/* Parse this AVP */
						CHECK_FCT( fd_msg_parse_dict ( avp, fd_g_config->cnf_dict, NULL ) );
						ASSERT( ahdr->avp_value );

						nbrh++;

						CHECK_FCT_DO( fd_os_parse_DiameterURI(ahdr->avp_value->os.data, ahdr->avp_value->os.len,
									&id, &len, &secure, &port, &l4, &proto),
							{
								TRACE_DEBUG(INFO, "Received an invalid Redirect-Host AVP value ('%.*s'), ignored", (int)ahdr->avp_value->os.len, ahdr->avp_value->os.data);
								break;
							} );

						/* Now check if the transport & protocol are supported */
						if (proto && (proto != 'd')) {
							TRACE_DEBUG(FULL, "Ignored unsupported non-Diameter Redirect-Host AVP (%.*s)", (int)ahdr->avp_value->os.len, ahdr->avp_value->os.data);
							free(id);
							break;
						}
						if (l4 && (l4 == IPPROTO_UDP)) {
							TRACE_DEBUG(FULL, "Ignored unsupported UDP Redirect-Host AVP (%.*s)", (int)ahdr->avp_value->os.len, ahdr->avp_value->os.data);
							free(id);
							break;
						}

						/* It looks OK, save this entry. */

						CHECK_MALLOC( h = malloc(sizeof(struct redir_host)) );
						memset(h, 0, sizeof(struct redir_host));
						fd_list_init(&h->chain, h);
						h->id = id;
						h->len = len;
						/* later: secure, port */

						/* The list is kept ordered by id so that it is faster to compare to candidates later */
						for (li = task.rh.next; li != &task.rh; li = li->next) {
							struct redir_host * nhost = li->o;
							if ( fd_os_cmp(id, len, nhost->id, nhost->len) <= 0 )
								break;
						}
						fd_list_insert_before(li, &h->chain);
					}
					break;

				case AC_REDIRECT_HOST_USAGE:
					/* Parse this AVP */
					CHECK_FCT( fd_msg_parse_dict ( avp, fd_g_config->cnf_dict, NULL ) );
					ASSERT( ahdr->avp_value );
					a_rhu = ahdr->avp_value;
					if (a_rhu->u32 > H_U_MAX) {
						TRACE_DEBUG(INFO, "Received unsupported Redirect-Host-Usage value (%d), defaulting to DONT_CACHE", a_rhu->u32);
					} else {
						task.rhu = a_rhu->u32;
					}
					break;

				case AC_REDIRECT_MAX_CACHE_TIME:
					/* Parse this AVP */
					CHECK_FCT( fd_msg_parse_dict ( avp, fd_g_config->cnf_dict, NULL ) );
					ASSERT( ahdr->avp_value );
					a_rmct = ahdr->avp_value;
					task.rmct = a_rmct->u32;
					break;

			}
		}

		/* Go to next AVP */
		CHECK_FCT(  fd_msg_browse(avp, MSG_BRW_NEXT, &avp, NULL)  );
	}

	/* Check we have received the necessary information */
	if (!a_rc) {
		TRACE_DEBUG(FULL, "Invalid Diameter answer without a Result-Code AVP, Redirect module gave up");
		goto out;
	}

	if (!a_oh) {
		TRACE_DEBUG(FULL, "Invalid Diameter answer without an Origin-Host AVP, Redirect module gave up");
		goto out;
	}

	if (FD_IS_LIST_EMPTY(&task.rh)) {
		TRACE_DEBUG(FULL, "Diameter answer with a DIAMETER_REDIRECT_INDICATION Result-Code AVP but no valid/supported Redirect-Host AVP, Redirect module gave up");
		goto out;
	}

	if (a_rhu && (task.rhu != DONT_CACHE) && !a_rmct) {
		TRACE_DEBUG(FULL, "Invalid Diameter Redirect answer with a Redirect-Host-Usage AVP but no Redirect-Max-Cache-Time, Redirect module gave up");
		goto out;
	}

	/* It looks like we can process the Redirect indication */

	/* Search for the peers we already know */
	for (li = task.rh.next; li != &task.rh; li = li->next) {
		struct redir_host * h = li->o;
		struct peer_hdr * peer;

		CHECK_FCT( fd_peer_getbyid( h->id, h->len, 1, &peer ) );
		if (peer) {
			known ++;
			memcpy(h->id, peer->info.pi_diamid, h->len); /* Overwrite the case so we can search case-sensitive from here on */
			if (fd_peer_get_state(peer) == STATE_OPEN) {
				actives ++;
			}
		}
	}

	TRACE_DEBUG(FULL, "Redirect module: received %d Redirect-Hosts, %d are known peers, %d have an OPEN connection", nbrh, known, actives);

	/* in this version, we only redirect when there are known active peers. TODO: add new peers via fd_peer_add when no active peer is available */

	if (!actives) {
		TRACE_DEBUG(INFO, "Unable to comply to Redirect indication: none of the peers included is in OPEN state");
		goto out;
	}

	/* From this point, we will re-send the query to a different peer, so stop forwarding the answer here */
	*msg = NULL;

	/* Get the query's routing data & add the new error */
	CHECK_FCT( fd_msg_answ_getq(m, &q) );
	CHECK_FCT( fd_msg_rt_get(q, &rtd) );
	CHECK_FCT( fd_msg_source_get( m, &nh, &nhlen ) );
	CHECK_FCT( fd_rtd_error_add(rtd, nh, nhlen, a_oh->os.data, a_oh->os.len, a_rc->u32, NULL, NULL) );

	/* Create a redir_rule  */
	CHECK_FCT( redir_entry_new(&entry, &task.rh, task.rhu, q, nh, nhlen, a_oh->os.data, a_oh->os.len) );

	CHECK_POSIX(  pthread_mutex_lock(&redir_exp_peer_lock)  );
	/* Insert in the split list */
	CHECK_FCT( redir_entry_insert(entry) );
	/* Set the expiry */
	CHECK_FCT( redir_exp_set(entry, task.rmct ?: DEFAULT_EXPIRE_TIME) );
	CHECK_POSIX(  pthread_mutex_unlock(&redir_exp_peer_lock)  );

	/* Now we can get rid of the received answer and send again the query. */
	CHECK_FCT( fd_msg_answ_detach(m) );
	CHECK_FCT( fd_msg_free(m) );

	/* Send it */
	CHECK_FCT( fd_msg_send(&q, NULL, NULL) );

	/* Done! */

out:
	while (!FD_IS_LIST_EMPTY(&task.rh)) {
		struct redir_host * h = task.rh.next->o;
		fd_list_unlink(&h->chain);
		free(h->id);
		free(h);
	}

	return 0;

}

