module DIAMETER_Emulation {

/* DIAMETER Emulation, runs on top of DIAMETER_CodecPort.  It multiplexes/demultiplexes
 * the individual IMSIs/subscribers, so there can be separate TTCN-3 components handling
 * each of them.
 *
 * The DIAMETER_Emulation.main() function processes DIAMETER primitives from the DIAMETER
 * socket via the DIAMETER_CodecPort, and dispatches them to the per-IMSI components.
 *
 * For each new IMSI, the DiameterOps.create_cb() is called.  It can create
 * or resolve a TTCN-3 component, and returns a component reference to which that IMSI
 * is routed/dispatched.
 *
 * If a pre-existing component wants to register to handle a future inbound IMSI, it can
 * do so by registering an "expect" with the expected IMSI.
 *
 * Inbound DIAMETER messages without IMSI (such as RESET-IND/ACK) are dispatched to
 * the DiameterOps.unitdata_cb() callback, which is registered with an argument to the
 * main() function below.
 *
 * Alternatively, all inbound DIAMETER PDUs can be routed to a single component
 * regardless of the IMSI.  This is called 'raw' mode and can be achieved by
 * setting the 'raw' field in DIAMETEROps to true.
 *
 * (C) 2019 by Harald Welte <laforge@gnumonks.org>
 * All rights reserved.
 *
 * Released under the terms of GNU General Public License, Version 2 or
 * (at your option) any later version.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

import from DIAMETER_CodecPort all;
import from DIAMETER_CodecPort_CtrlFunct all;
import from DIAMETER_Types all;
import from DIAMETER_Templates all;
import from Osmocom_Types all;
import from IPL4asp_Types all;
import from TCCConversion_Functions all;
import from Native_Functions all;
import from SCTP_Templates all;

type hexstring IMSI;

/* notify the recipient that a Capability Exchange happened */
type record DiameterCapabilityExchgInd {
	PDU_DIAMETER rx,
	PDU_DIAMETER tx
};

type component DIAMETER_ConnHdlr {
	port DIAMETER_Conn_PT DIAMETER;
	/* procedure based port to register for incoming connections */
	port DIAMETEREM_PROC_PT DIAMETER_PROC;
}

/* port between individual per-connection components and this dispatcher */
type port DIAMETER_Conn_PT message {
	inout PDU_DIAMETER;
} with { extension "internal" };

/* global test port e.g. for non-imsi/conn specific messages */
type port DIAMETER_PT message {
	inout PDU_DIAMETER, DiameterCapabilityExchgInd;
} with { extension "internal" };


/* represents a single DIAMETER Association */
type record AssociationData {
	DIAMETER_ConnHdlr	comp_ref,
	hexstring	imsi optional
};

/* represents a single DIAMETER message identified by ete_id field */
type record ETEIDData {
	DIAMETER_ConnHdlr	comp_ref,
	UINT32			ete_id optional
};

type component DIAMETER_Emulation_CT {
	/* Port facing to the UDP SUT */
	port DIAMETER_CODEC_PT DIAMETER;
	/* All DIAMETER_ConnHdlr DIAMETER ports connect here
	 * DIAMETER_Emulation_CT.main needs to figure out what messages
	 * to send where with CLIENT.send() to vc_conn */
	port DIAMETER_Conn_PT DIAMETER_CLIENT;
	/* currently tracked connections */
	var AssociationData DiameterAssocTable[256];
	/* Forward reply messages not containing IMSI to correct client port */
	var ETEIDData DiameterETEIDTable[256];
	/* pending expected CRCX */
	var ExpectData DiameterExpectTable[256];
	/* procedure based port to register for incoming connections */
	port DIAMETEREM_PROC_PT DIAMETER_PROC;
	/* test port for unit data messages */
	port DIAMETER_PT DIAMETER_UNIT;

	var charstring g_diameter_id;
	var integer g_diameter_conn_id := -1;
}

type function DIAMETERCreateCallback(PDU_DIAMETER msg, hexstring imsi, charstring id)
runs on DIAMETER_Emulation_CT return DIAMETER_ConnHdlr;

type function DIAMETERUnitdataCallback(PDU_DIAMETER msg)
runs on DIAMETER_Emulation_CT return template PDU_DIAMETER;

type record DIAMETEROps {
	DIAMETERCreateCallback create_cb,
	DIAMETERUnitdataCallback unitdata_cb,
	/* If true, this parameter disables IMSI based routing, so that all incoming
	 * PDUs get routed to a single component connected via the DIAMETER_UNIT port. */
	boolean raw
}

type record DIAMETER_conn_parameters {
	HostName remote_ip,
	PortNumber remote_sctp_port,
	HostName local_ip,
	PortNumber local_sctp_port,
	charstring origin_host,
	charstring origin_realm,
	uint32_t auth_app_id optional,
	uint32_t vendor_app_id optional
}

function tr_DIAMETER_RecvFrom_R(template PDU_DIAMETER msg)
runs on DIAMETER_Emulation_CT return template DIAMETER_RecvFrom {
	var template DIAMETER_RecvFrom mrf := {
		connId := g_diameter_conn_id,
		remName := ?,
		remPort := ?,
		locName := ?,
		locPort := ?,
		msg := msg
	}
	return mrf;
}

private function f_imsi_known(hexstring imsi)
runs on DIAMETER_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		if (DiameterAssocTable[i].imsi == imsi) {
			return true;
		}
	}
	return false;
}

private function f_comp_known(DIAMETER_ConnHdlr client)
runs on DIAMETER_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		if (DiameterAssocTable[i].comp_ref == client) {
			return true;
		}
	}
	return false;
}

private function f_comp_by_imsi(hexstring imsi)
runs on DIAMETER_Emulation_CT return DIAMETER_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		if (DiameterAssocTable[i].imsi == imsi) {
			return DiameterAssocTable[i].comp_ref;
		}
	}
	setverdict(fail, "DIAMETER Association Table not found by IMSI", imsi);
	mtc.stop;
}

private function f_imsi_by_comp(DIAMETER_ConnHdlr client)
runs on DIAMETER_Emulation_CT return hexstring {
	var integer i;
	for (i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		if (DiameterAssocTable[i].comp_ref == client) {
			return DiameterAssocTable[i].imsi;
		}
	}
	setverdict(fail, "DIAMETER Association Table not found by component ", client);
	mtc.stop;
}

private function f_imsi_table_add(DIAMETER_ConnHdlr comp_ref, hexstring imsi)
runs on DIAMETER_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		if (not isvalue(DiameterAssocTable[i].imsi)) {
			DiameterAssocTable[i].imsi := imsi;
			DiameterAssocTable[i].comp_ref := comp_ref;
			return;
		}
	}
	testcase.stop("DIAMETER Association Table full!");
}

private function f_imsi_table_del(DIAMETER_ConnHdlr comp_ref, hexstring imsi)
runs on DIAMETER_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		if (DiameterAssocTable[i].comp_ref == comp_ref and
		    DiameterAssocTable[i].imsi == imsi) {
			DiameterAssocTable[i].imsi := omit;
			DiameterAssocTable[i].comp_ref := null;
			return;
		}
	}
	setverdict(fail, "DIAMETER Association Table: Couldn't find to-be-deleted entry!");
	mtc.stop;
}

private function f_imsi_table_init()
runs on DIAMETER_Emulation_CT {
	for (var integer i := 0; i < sizeof(DiameterAssocTable); i := i+1) {
		DiameterAssocTable[i].comp_ref := null;
		DiameterAssocTable[i].imsi := omit;
	}
}

/* End-to-End ID table matching. */
private function f_ete_id_known(UINT32 ete_id)
runs on DIAMETER_Emulation_CT return boolean {
	var integer i;
	for (i := 0; i < sizeof(DiameterETEIDTable); i := i+1) {
		if (DiameterETEIDTable[i].ete_id == ete_id) {
			return true;
		}
	}
	return false;
}

private function f_comp_by_ete_id(UINT32 ete_id)
runs on DIAMETER_Emulation_CT return DIAMETER_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(DiameterETEIDTable); i := i+1) {
		if (DiameterETEIDTable[i].ete_id == ete_id) {
			return DiameterETEIDTable[i].comp_ref;
		}
	}
	setverdict(fail, "DIAMETER ETEID Table not found by ete_id", ete_id);
	mtc.stop;
}

private function f_eteid_table_add(DIAMETER_ConnHdlr comp_ref, UINT32 ete_id)
runs on DIAMETER_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(DiameterETEIDTable); i := i+1) {
		if (not isvalue(DiameterETEIDTable[i].ete_id)) {
			DiameterETEIDTable[i].ete_id := ete_id;
			DiameterETEIDTable[i].comp_ref := comp_ref;
			return;
		}
	}
	testcase.stop("DIAMETER ETEID Table full!");
}

private function f_eteid_table_del(DIAMETER_ConnHdlr comp_ref, UINT32 ete_id)
runs on DIAMETER_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(DiameterETEIDTable); i := i+1) {
		if (DiameterETEIDTable[i].comp_ref == comp_ref and
		    DiameterETEIDTable[i].ete_id == ete_id) {
			DiameterETEIDTable[i].ete_id := omit;
			DiameterETEIDTable[i].comp_ref := null;
			return;
		}
	}
	setverdict(fail, "DIAMETER ETEID Table: Couldn't find to-be-deleted entry!");
	mtc.stop;
}


private function f_eteid_table_init()
runs on DIAMETER_Emulation_CT {
	for (var integer i := 0; i < sizeof(DiameterETEIDTable); i := i+1) {
		DiameterETEIDTable[i].comp_ref := null;
		DiameterETEIDTable[i].ete_id := omit;
	}
}

function f_DIAMETER_get_imsi(PDU_DIAMETER pdu) return template (omit) IMSI
{
	var template (omit) AVP imsi_avp;

	imsi_avp := f_DIAMETER_get_avp(pdu, c_AVP_Code_BASE_NONE_User_Name);
	if (istemplatekind(imsi_avp, "omit")) {
		var template (omit) AVP sid_avp;
		sid_avp := f_DIAMETER_get_avp(pdu, c_AVP_Code_DCC_NONE_Subscription_Id);
		if (istemplatekind(sid_avp, "omit")) {
			return omit;
		}
		var AVP_Grouped grp := valueof(sid_avp.avp_data.avp_DCC_NONE_Subscription_Id);
		if (not match(grp[0], tr_AVP_SubcrIdType(END_USER_IMSI))) {
			return omit;
		}
		return str2hex(oct2char(grp[1].avp.avp_data.avp_DCC_NONE_Subscription_Id_Data));
	} else {
		var octetstring imsi_oct := valueof(imsi_avp.avp_data.avp_BASE_NONE_User_Name);
		var charstring imsi_str := oct2char(imsi_oct);
		/* Username may be a NAI instead of IMSI: "<IMSI>@nai.epc.mnc<MNC>.mcc<MCC>.3gppnetwork.org" */
		var integer pos := f_strstr(imsi_str, "@");
		if (pos != -1) {
			imsi_str := substr(imsi_str, 0, pos);
		}
		return str2hex(imsi_str);
	}
}

private function f_diameter_xceive(template (value) PDU_DIAMETER tx,
				   template PDU_DIAMETER rx_t := ?)
runs on DIAMETER_Emulation_CT return PDU_DIAMETER {
	timer T := 10.0;
	var DIAMETER_RecvFrom mrf;

	DIAMETER.send(t_DIAMETER_Send(g_diameter_conn_id, tx));
	T.start;
	alt {
	[] DIAMETER.receive(tr_DIAMETER_RecvFrom_R(rx_t)) -> value mrf { }
	[] DIAMETER.receive(tr_DIAMETER_RecvFrom_R(?)) -> value mrf {
		setverdict(fail, "Rx unexpected DIAMETER PDU: ", mrf);
		mtc.stop;
		}
	[] DIAMETER.receive(tr_SctpAssocChange) { repeat; }
	[] DIAMETER.receive(tr_SctpPeerAddrChange) { repeat; }
	[] T.timeout {
		setverdict(fail, "Timeout waiting for ", rx_t);
		mtc.stop;
		}
	}
	return mrf.msg;
}

function main(DIAMETEROps ops, DIAMETER_conn_parameters p, charstring id) runs on DIAMETER_Emulation_CT {
	var boolean server_mode := p.remote_sctp_port == -1;
	var Result res;
	g_diameter_id := id;
	f_imsi_table_init();
	f_eteid_table_init();
	f_expect_table_init();

	map(self:DIAMETER, system:DIAMETER_CODEC_PT);
	if (server_mode) {
		res := DIAMETER_CodecPort_CtrlFunct.f_IPL4_listen(DIAMETER, p.local_ip, p.local_sctp_port,
								  { sctp := valueof(ts_SctpTuple) });
	} else {
		res := DIAMETER_CodecPort_CtrlFunct.f_IPL4_connect(DIAMETER, p.remote_ip, p.remote_sctp_port,
								p.local_ip, p.local_sctp_port, -1,
								{ sctp := valueof(ts_SctpTuple) });
	}
	if (not ispresent(res.connId)) {
		setverdict(fail, "Could not connect DIAMETER socket, check your configuration");
		mtc.stop;
	}
	g_diameter_conn_id := res.connId;

	/* If in client mode, send CER immediately */
	if (not server_mode) {
		var template (value) PDU_DIAMETER req;
		var PDU_DIAMETER rsp;

		req := ts_DIA_CER(f_inet_addr(p.local_ip), p.vendor_app_id,
				  orig_host := p.origin_host, orig_realm := p.origin_realm);
		rsp := f_diameter_xceive(req, tr_DIAMETER_A(Capabilities_Exchange, req.application_id));
		/* notify our user that the CER->CEA exchange has happened */
		DIAMETER_UNIT.send(DiameterCapabilityExchgInd:{rx := rsp, tx := valueof(req)});
	}

	while (true) {
		var DIAMETER_ConnHdlr vc_conn;
		var template IMSI imsi_t;
		var hexstring imsi;
		var UINT32 ete_id;
		var DIAMETER_RecvFrom mrf;
		var PDU_DIAMETER msg;
		var charstring vlr_name, mme_name;
		var PortEvent port_evt;

		alt {
		[] DIAMETER.receive(PortEvent:{connOpened := ?}) -> value port_evt {
			g_diameter_conn_id := port_evt.connOpened.connId;
			}
		[] DIAMETER.receive(PortEvent:?) { }
		/* DIAMETER from client */
		[] DIAMETER_CLIENT.receive(PDU_DIAMETER:?) -> value msg sender vc_conn {
			/* Pass message through */
			/* TODO: check which ConnectionID client has allocated + store in table? */
			DIAMETER.send(t_DIAMETER_Send(g_diameter_conn_id, msg));
			}

		/* handle CER/CEA handshake */
		[] DIAMETER.receive(tr_DIAMETER_RecvFrom_R(tr_DIAMETER_R(cmd_code := Capabilities_Exchange))) -> value mrf {
			var template (value) PDU_DIAMETER resp;
			resp := f_ts_DIA_CEA(mrf.msg.hop_by_hop_id, mrf.msg.end_to_end_id, p.origin_host,
					   p.origin_realm, f_inet_addr(p.local_ip), p.auth_app_id, p.vendor_app_id);
			DIAMETER.send(t_DIAMETER_Send(g_diameter_conn_id, resp));
			/* notify our user that the CER->CEA exchange has happened */
			DIAMETER_UNIT.send(DiameterCapabilityExchgInd:{rx:=mrf.msg, tx:=valueof(resp)});
			}
		/* handle DWR/DWA ping-pong */
		[] DIAMETER.receive(tr_DIAMETER_RecvFrom_R(tr_DIA_DWR)) -> value mrf {
			var template (value) PDU_DIAMETER resp;
			resp := ts_DIA_DWA('00000001'O, p.origin_host, p.origin_realm,
					   hbh_id := mrf.msg.hop_by_hop_id,
					   ete_id := mrf.msg.end_to_end_id);
			DIAMETER.send(t_DIAMETER_Send(g_diameter_conn_id, valueof(resp)));
			}

		/* DIAMETER from the test suite */
		[ops.raw] DIAMETER_UNIT.receive(PDU_DIAMETER:?) -> value msg {
			DIAMETER.send(t_DIAMETER_Send(g_diameter_conn_id, msg));
			}
		/* DIAMETER from remote peer (raw mode) */
		[ops.raw] DIAMETER.receive(tr_DIAMETER_RecvFrom_R(?)) -> value mrf {
			DIAMETER_UNIT.send(mrf.msg);
			}
		/* DIAMETER from remote peer (IMSI based routing) */
		[not ops.raw] DIAMETER.receive(tr_DIAMETER_RecvFrom_R(?)) -> value mrf {
			imsi_t := f_DIAMETER_get_imsi(mrf.msg);
			ete_id := mrf.msg.end_to_end_id;
			if (f_ete_id_known(ete_id)) {
				vc_conn := f_comp_by_ete_id(ete_id);
				/* The ete_id is a single-time expect: */
				f_eteid_table_del(vc_conn, ete_id);
				DIAMETER_CLIENT.send(mrf.msg) to vc_conn;
			} else if (isvalue(imsi_t)) {
				imsi := valueof(imsi_t);
				if (f_imsi_known(imsi)) {
					vc_conn := f_comp_by_imsi(imsi);
					DIAMETER_CLIENT.send(mrf.msg) to vc_conn;
				} else {
					vc_conn := ops.create_cb.apply(mrf.msg, imsi, id);
					f_imsi_table_add(vc_conn, imsi);
					DIAMETER_CLIENT.send(mrf.msg) to vc_conn;
				}
			} else {
				/* message contained no IMSI; is not IMSI-oriented */
				var template PDU_DIAMETER resp := ops.unitdata_cb.apply(mrf.msg);
				if (isvalue(resp)) {
					DIAMETER.send(t_DIAMETER_Send(g_diameter_conn_id, valueof(resp)));
				}
			}
			}
		[] DIAMETER.receive(tr_SctpAssocChange) { }
		[] DIAMETER.receive(tr_SctpPeerAddrChange) { }
		[] DIAMETER_PROC.getcall(DIAMETEREM_register_imsi:{?,?}) -> param(imsi, vc_conn) {
			f_create_expect(imsi, vc_conn);
			DIAMETER_PROC.reply(DIAMETEREM_register_imsi:{imsi, vc_conn}) to vc_conn;
			}
		[] DIAMETER_PROC.getcall(DIAMETEREM_register_eteid:{?,?}) -> param(ete_id, vc_conn) {
			f_eteid_table_add(vc_conn, ete_id);
			DIAMETER_PROC.reply(DIAMETEREM_register_eteid:{ete_id, vc_conn}) to vc_conn;
			}

		}

	}
}

/* "E2E ID Expect" Handling */
type record ExpectDataE2EID {
	UINT32 ete_id optional,
	DIAMETER_ConnHdlr vc_conn
}

signature DIAMETEREM_register_eteid(in UINT32 ete_id, in DIAMETER_ConnHdlr hdlr);

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_diameter_expect_eteid(UINT32 ete_id) runs on DIAMETER_ConnHdlr {
	DIAMETER_PROC.call(DIAMETEREM_register_eteid:{ete_id, self}) {
		[] DIAMETER_PROC.getreply(DIAMETEREM_register_eteid:{?,?}) {};
	}
}

/* "IMSI Expect" Handling */

type record ExpectData {
	hexstring imsi optional,
	DIAMETER_ConnHdlr vc_conn
}

signature DIAMETEREM_register_imsi(in hexstring imsi, in DIAMETER_ConnHdlr hdlr);

/* Function that can be used as create_cb and will use the expect table */
function ExpectedCreateCallback(PDU_DIAMETER msg, hexstring imsi, charstring id)
runs on DIAMETER_Emulation_CT return DIAMETER_ConnHdlr {
	var DIAMETER_ConnHdlr ret := null;
	var integer i;

	for (i := 0; i < sizeof(DiameterExpectTable); i := i+1) {
		if (not ispresent(DiameterExpectTable[i].imsi)) {
			continue;
		}
		if (imsi == DiameterExpectTable[i].imsi) {
			ret := DiameterExpectTable[i].vc_conn;
			/* Release this entry */
			DiameterExpectTable[i].imsi := omit;
			DiameterExpectTable[i].vc_conn := null;
			log("Found Expect[", i, "] for ", msg, " handled at ", ret);
			return ret;
		}
	}
	setverdict(fail, "Couldn't find Expect for ", msg);
	mtc.stop;
}

private function f_create_expect(hexstring imsi, DIAMETER_ConnHdlr hdlr)
runs on DIAMETER_Emulation_CT {
	var integer i;

	/* Check an entry like this is not already presnt */
	for (i := 0; i < sizeof(DiameterExpectTable); i := i+1) {
		if (imsi == DiameterExpectTable[i].imsi) {
			setverdict(fail, "IMSI already present", imsi);
			mtc.stop;
		}
	}
	for (i := 0; i < sizeof(DiameterExpectTable); i := i+1) {
		if (not ispresent(DiameterExpectTable[i].imsi)) {
			DiameterExpectTable[i].imsi := imsi;
			DiameterExpectTable[i].vc_conn := hdlr;
			log("Created Expect[", i, "] for ", imsi, " to be handled at ", hdlr);
			return;
		}
	}
	testcase.stop("No space left in DiameterExpectTable")
}

/* client/conn_hdlr side function to use procedure port to create expect in emulation */
function f_diameter_expect_imsi(hexstring imsi) runs on DIAMETER_ConnHdlr {
	DIAMETER_PROC.call(DIAMETEREM_register_imsi:{imsi, self}) {
		[] DIAMETER_PROC.getreply(DIAMETEREM_register_imsi:{?,?}) {};
	}
}

private function f_expect_table_init()
runs on DIAMETER_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(DiameterExpectTable); i := i + 1) {
		DiameterExpectTable[i].imsi := omit;
	}
}

function DummyUnitdataCallback(PDU_DIAMETER msg)
runs on DIAMETER_Emulation_CT return template PDU_DIAMETER {
	log("Ignoring DIAMETER ", msg);
	return omit;
}

type port DIAMETEREM_PROC_PT procedure {
	inout DIAMETEREM_register_imsi;
	inout DIAMETEREM_register_eteid;
} with { extension "internal" };

function f_diameter_wait_capability(DIAMETER_PT pt)
{
	/* Wait for the Capability Exchange with the DUT */
	timer T := 10.0;
	T.start;
	alt {
	[] pt.receive(DiameterCapabilityExchgInd:?) {}
	[] pt.receive {
		setverdict(fail, "Unexpected receive waiting for DiameterCapabilityExchgInd");
		mtc.stop;
		}
	[] T.timeout {
		setverdict(fail, "Timeout waiting for DiameterCapabilityExchgInd");
		mtc.stop;
		}
	}
}


}