/* GTP Emulation in TTCN-3
 *
 * (C) 2018 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
 */

module GTP_Emulation {

import from IPL4asp_Types all;
import from General_Types all;
import from Osmocom_Types all;
import from GTPC_Types all;
import from GTPU_Types all;
import from GTPv1C_CodecPort all;
import from GTPv1U_CodecPort all;
import from GTPv1C_CodecPort_CtrlFunct all;
import from GTPv1U_CodecPort_CtrlFunct all;

/***********************************************************************
 * Main Emulation Component
 ***********************************************************************/

const integer GTP0_PORT := 3386;
const integer GTP1C_PORT := 2123;
const integer GTP1U_PORT := 2152;

type record GtpEmulationCfg {
	HostName gtpc_bind_ip optional,
	PortNumber gtpc_bind_port optional,
	HostName gtpu_bind_ip optional,
	PortNumber gtpu_bind_port optional,
	boolean sgsn_role
};

type component GTP_Emulation_CT {
	/* Communication with underlying GTP CodecPort */
	port GTPC_PT GTPC;
	port GTPU_PT GTPU;

	/* Communication with Clients */
	port GTPEM_PT CLIENT;
	port GTPEM_PROC_PT CLIENT_PROC;
	port GTPEM_PT CLIENT_DEFAULT;

	/* Configuration by the user */
	var GtpEmulationCfg g_gtp_cfg;

	/* State */
	var integer g_gtpc_id, g_gtpu_id;
	var OCT1 g_restart_ctr;
	var uint16_t g_c_seq_nr, g_u_seq_nr;
	var TidTableRec TidTable[16];
	var ImsiTableRec ImsiTable[16];
};

type record TidTableRec {
	OCT4 teid,
	GTP_ConnHdlr vc_conn
};

type record ImsiTableRec {
	hexstring imsi,
	GTP_ConnHdlr vc_conn
};

private function f_comp_by_teid(OCT4 teid) runs on GTP_Emulation_CT return GTP_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(TidTable); i := i+1) {
		if (isbound(TidTable[i].teid) and TidTable[i].teid == teid) {
			return TidTable[i].vc_conn;
		}
	}
	setverdict(fail, "No Component for TEID ", teid);
	mtc.stop;
}

private function f_comp_by_imsi(hexstring imsi) runs on GTP_Emulation_CT return GTP_ConnHdlr {
	var integer i;
	for (i := 0; i < sizeof(ImsiTable); i := i+1) {
		if (isbound(ImsiTable[i].imsi) and ImsiTable[i].imsi == imsi) {
			return ImsiTable[i].vc_conn;
		}
	}
	setverdict(fail, "No Component for IMSI ", imsi);
	mtc.stop;
}

private function f_tid_tbl_add(OCT4 teid, GTP_ConnHdlr vc_conn) runs on GTP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(TidTable); i := i+1) {
		if (not isbound(TidTable[i].teid)) {
			TidTable[i].teid := teid;
			TidTable[i].vc_conn := vc_conn;
			return;
		}
	}
	testcase.stop("No Space in TidTable for ", teid);
}

private function f_imsi_tbl_add(hexstring imsi, GTP_ConnHdlr vc_conn) runs on GTP_Emulation_CT {
	var integer i;
	for (i := 0; i < sizeof(ImsiTable); i := i+1) {
		if (not isbound(ImsiTable[i].imsi)) {
			ImsiTable[i].imsi := imsi;
			ImsiTable[i].vc_conn := vc_conn;
			return;
		}
	}
	testcase.stop("No Space in IMSI Table for ", imsi);
}

function f_gtpc_extract_imsi(PDU_GTPC gtp) return template (omit) hexstring {
	if (ischosen(gtp.gtpc_pdu.createPDPContextRequest)) {
		return gtp.gtpc_pdu.createPDPContextRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestSGSN)) {
		if (ispresent(gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestSGSN.imsi.digits)) {
			return gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestSGSN.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestGGSN)) {
		if (ispresent(gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestGGSN.imsi.digits)) {
			return gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestGGSN.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestCGW)) {
		if (ispresent(gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestCGW.imsi.digits)) {
			return gtp.gtpc_pdu.updatePDPContextRequest.updatePDPContextRequestCGW.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.pdu_NotificationRequest)) {
		return gtp.gtpc_pdu.pdu_NotificationRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.sendRouteingInformationForGPRSRequest)) {
		return gtp.gtpc_pdu.sendRouteingInformationForGPRSRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.sendRouteingInformationForGPRSResponse)) {
		return gtp.gtpc_pdu.sendRouteingInformationForGPRSResponse.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.failureReportRequest)) {
		return gtp.gtpc_pdu.failureReportRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.noteMS_GPRSPresentRequest)) {
		return gtp.gtpc_pdu.noteMS_GPRSPresentRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.identificationResponse) ) {
		if (ispresent(gtp.gtpc_pdu.identificationResponse.imsi.digits)) {
			return gtp.gtpc_pdu.identificationResponse.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.sgsn_ContextRequest)) {
		if (ispresent(gtp.gtpc_pdu.sgsn_ContextRequest.imsi.digits)) {
			return gtp.gtpc_pdu.sgsn_ContextRequest.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.sgsn_ContextResponse)) {
		if (ispresent(gtp.gtpc_pdu.sgsn_ContextResponse.imsi.digits)) {
			return gtp.gtpc_pdu.sgsn_ContextResponse.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.forwardRelocationRequest)) {
		if (ispresent(gtp.gtpc_pdu.forwardRelocationRequest.imsi.digits)) {
			return gtp.gtpc_pdu.forwardRelocationRequest.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.relocationCancelRequest)) {
		if (ispresent(gtp.gtpc_pdu.relocationCancelRequest.imsi.digits)) {
			return gtp.gtpc_pdu.relocationCancelRequest.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.uERegistrationQueryRequest)) {
		return gtp.gtpc_pdu.uERegistrationQueryRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.uERegistrationQueryResponse)) {
		return gtp.gtpc_pdu.uERegistrationQueryResponse.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.mBMSNotificationRequest)) {
		return gtp.gtpc_pdu.mBMSNotificationRequest.imsi.digits;
	} else if (ischosen(gtp.gtpc_pdu.createMBMSContextRequest)) {
		if (ispresent(gtp.gtpc_pdu.createMBMSContextRequest.imsi.digits)) {
			return gtp.gtpc_pdu.createMBMSContextRequest.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.deleteMBMSContextRequest)) {
		if (ispresent(gtp.gtpc_pdu.deleteMBMSContextRequest.imsi.digits)) {
			return gtp.gtpc_pdu.deleteMBMSContextRequest.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.mS_InfoChangeNotificationRequest)) {
		if (ispresent(gtp.gtpc_pdu.mS_InfoChangeNotificationRequest.imsi.digits)) {
			return gtp.gtpc_pdu.mS_InfoChangeNotificationRequest.imsi.digits;
		} else {
			return omit;
		}
	} else if (ischosen(gtp.gtpc_pdu.mS_InfoChangeNotificationResponse)) {
		if (ispresent(gtp.gtpc_pdu.mS_InfoChangeNotificationResponse.imsi.digits)) {
			return gtp.gtpc_pdu.mS_InfoChangeNotificationResponse.imsi.digits;
		} else {
			return omit;
		}
	} else {
		return omit;
	}
}

private function f_init(GtpEmulationCfg cfg) runs on GTP_Emulation_CT {
	var Result res;

	if (isvalue(cfg.gtpc_bind_ip) and isvalue(cfg.gtpc_bind_port)) {
		map(self:GTPC, system:GTPC);
		res := GTPv1C_CodecPort_CtrlFunct.f_IPL4_listen(GTPC, cfg.gtpc_bind_ip,
							cfg.gtpc_bind_port, {udp:={}});
		g_gtpc_id := res.connId;
	}

	if (isvalue(cfg.gtpu_bind_ip) and isvalue(cfg.gtpu_bind_port)) {
		map(self:GTPU, system:GTPU);
		res := GTPv1U_CodecPort_CtrlFunct.f_GTPU_listen(GTPU, cfg.gtpu_bind_ip,
							     cfg.gtpu_bind_port, {udp:={}});
		g_gtpu_id := res.connId;
	}

	g_restart_ctr := f_rnd_octstring(1);
	g_c_seq_nr := f_rnd_int(65535);
	g_u_seq_nr := f_rnd_int(65535);
	g_gtp_cfg := cfg;
}

function main(GtpEmulationCfg cfg) runs on GTP_Emulation_CT {
	var Gtp1cUnitdata g1c_ud;
	var Gtp1uUnitdata g1u_ud;
	var GTP_ConnHdlr vc_conn;
	var hexstring imsi;
	var OCT4 teid;

	f_init(cfg);

	while (true) {
	alt {
	/* route inbound GTP-C based on IMSI or TEID */
	[] GTPC.receive(Gtp1cUnitdata:?) -> value g1c_ud {
		var template hexstring imsi_t := f_gtpc_extract_imsi(g1c_ud.gtpc);
		if (isvalue(imsi_t)) {
			vc_conn := f_comp_by_imsi(valueof(imsi_t));
			CLIENT.send(g1c_ud) to vc_conn;
		} else if(g1c_ud.gtpc.teid != int2oct(0, 4)) {
			vc_conn := f_comp_by_teid(g1c_ud.gtpc.teid);
			CLIENT.send(g1c_ud) to vc_conn;
		} else {
			/* Check if a default port is set: */
			if (CLIENT_DEFAULT.checkstate("Connected")) {
				CLIENT_DEFAULT.send(g1c_ud);
			} else {
				/* Send to all clients */
				var integer i;
				for (i := 0; i < sizeof(TidTable); i := i+1) {
					if (isbound(TidTable[i].teid) and TidTable[i].teid == teid) {
						CLIENT.send(g1c_ud) to TidTable[i].vc_conn;
					}
				}
			}
		}
		}
	[] GTPU.receive(Gtp1uUnitdata:?) -> value g1u_ud {
		vc_conn := f_comp_by_teid(g1u_ud.gtpu.teid);
		CLIENT.send(g1u_ud) to vc_conn;
		}

	/* transparently forward any GTP-C / GTP-U from clients to peer[s] */
	[] CLIENT.receive(Gtp1cUnitdata:?) -> value g1c_ud sender vc_conn {
		GTPC.send(g1c_ud);
		}
	[] CLIENT.receive(Gtp1uUnitdata:?) -> value g1u_ud sender vc_conn {
		GTPU.send(g1u_ud);
		}
	[] CLIENT_DEFAULT.receive(Gtp1cUnitdata:?) -> value g1c_ud sender vc_conn {
		GTPC.send(g1c_ud);
		}

	[] CLIENT_PROC.getcall(GTPEM_register_imsi:{?}) -> param(imsi) sender vc_conn {
		f_imsi_tbl_add(imsi, vc_conn);
		CLIENT_PROC.reply(GTPEM_register_imsi:{imsi}) to vc_conn;
		}

	[] CLIENT_PROC.getcall(GTPEM_register_teid:{?}) -> param(teid) sender vc_conn {
		f_tid_tbl_add(teid, vc_conn);
		CLIENT_PROC.reply(GTPEM_register_teid:{teid}) to vc_conn;
		}

	}
	}
}


/***********************************************************************
 * Interaction between Main and Client Components
 ***********************************************************************/
type port GTPEM_PT message {
	inout Gtp1cUnitdata, Gtp1uUnitdata;
} with { extension "internal" };

signature GTPEM_register_imsi(hexstring imsi);
signature GTPEM_register_teid(OCT4 teid);

type port GTPEM_PROC_PT procedure {
	inout GTPEM_register_imsi, GTPEM_register_teid;
} with { extension "internal" };

/***********************************************************************
 * Client Component
 ***********************************************************************/
/* maximum number of GTP ports a GTP_ConnHdlr component can manage. This allows
 * connection one GTP_ConnHdlr to several GTP_Emulation(s). */
const integer NUM_MAX_GTP := 4;

type component GTP_ConnHdlr {
	port GTPEM_PT GTP[NUM_MAX_GTP];
	port GTPEM_PROC_PT GTP_PROC[NUM_MAX_GTP];
};

function f_gtp_register_imsi(hexstring imsi, integer gtp_idx := 0) runs on GTP_ConnHdlr {
	GTP_PROC[gtp_idx].call(GTPEM_register_imsi:{imsi}) {
		[] GTP_PROC[gtp_idx].getreply(GTPEM_register_imsi:{imsi});
	}
}

function f_gtp_register_teid(OCT4 teid, integer gtp_idx := 0) runs on GTP_ConnHdlr {
	GTP_PROC[gtp_idx].call(GTPEM_register_teid:{teid}) {
		[] GTP_PROC[gtp_idx].getreply(GTPEM_register_teid:{teid});
	}
}

function f_gtp_teid_random() return OCT4 {
	return f_rnd_octstring(4);
}
}