/* SCCP Connectionless Control (SCLC) according to ITU-T Q.713/Q.714 */

/* (C) 2015-2017 by Harald Welte <laforge@gnumonks.org>
 * All Rights reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/* This code is a bit of a hybrid between the ITU-T Q.71x specifications
 * for SCCP (particularly its connection-oriented part), and the IETF
 * RFC 3868 (SUA).  The idea here is to have one shared code base of the
 * state machines for SCCP Connection Oriented, and use those both from
 * SCCP and SUA.
 *
 * To do so, all SCCP messages are translated to SUA messages in the
 * input side, and all generated SUA messages are translated to SCCP on
 * the output side.
 *
 * The Choice of going for SUA messages as the "native" format was based
 * on their easier parseability, and the fact that there are features in
 * SUA which classic SCCP cannot handle (like IP addresses in GT).
 * However, all SCCP features can be expressed in SUA.
 *
 * Missing other features:
 *  * Segmentation/Reassembly support
 *  * T(guard) after (re)start
 *  * freezing of local references
 *  * parsing/encoding of IPv4/IPv6 addresses
 *  * use of multiple Routing Contexts in SUA case
 */

#include <string.h>
#include <errno.h>

#include <osmocom/core/utils.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/timer.h>
#include <osmocom/core/fsm.h>

#include <osmocom/sigtran/sccp_sap.h>
#include <osmocom/sigtran/protocol/sua.h>
#include <osmocom/sccp/sccp_types.h>

#include "xua_internal.h"
#include "ss7_internal.h"
#include "sccp_instance.h"
#include "sccp_internal.h"
#include "sccp_user.h"

/* generate a 'struct xua_msg' of requested type from primitive data */
static struct xua_msg *xua_gen_msg_cl(uint32_t event,
				      struct osmo_scu_prim *prim, int msg_type)
{
	struct xua_msg *xua = xua_msg_alloc();
	struct osmo_scu_unitdata_param *udpar = &prim->u.unitdata;
	uint32_t seq_ctrl = udpar->in_sequence_control;
	uint32_t proto_class;

	if (!xua)
		return NULL;

	if (seq_ctrl == OSMO_SCU_UNITDATA_REQ_P_SEQUENCE_CONTROL_NOT_PRESENT) {
		/* ITU-T Q.711 6.2.1 Class 0:
		 * "The SCCP user can invoke this service by means of the parameter
		 * "sequence control" in the N-UNITDATA request primitive being absent" */
		proto_class = 0;
		/* Erase the mark, we are anyway not using it in protocol_class=0. */
		seq_ctrl = 0;
		/* ITU-T Q.714 (1.1.2.1 Protocol class 0):
		 * "They are transferred independently of each other.
		 *  Therefore, they may be delivered to the SCCP user out-of-sequence."
		 */
		xua->mtp.sls = rand() & 0xf;
	} else {
		/* "ITU-T Q.711 6.2.1 Class 1:
		 * "The SCCP user can invoke this service by means of the parameter
		 * "sequence control" in the N-UNITDATA request primitive being present." */
		proto_class = 1;
		/* ITU-T Q.714 (1.1.2.2 Protocol class 1):
		* "The Signalling Link Selection (SLS) parameter in the MTP-TRANSFER
		* request primitive is chosen by the originating SCCP based on the value
		* of the sequence control parameter. The SLS shall be identical for a
		* stream of NSDUs with the same sequence control parameter".
		* SLS is 4 bits, as described in ITU Q.704 Figure 3.
		*/
		xua->mtp.sls = udpar->in_sequence_control & 0x0f;
	}

	switch (msg_type) {
	case SUA_CL_CLDT:
		xua->hdr = XUA_HDR(SUA_MSGC_CL, SUA_CL_CLDT);
		xua_msg_add_u32(xua, SUA_IEI_ROUTE_CTX, 0); /* FIXME */
		xua_msg_add_u32(xua, SUA_IEI_PROTO_CLASS, proto_class);
		xua_msg_add_sccp_addr(xua, SUA_IEI_SRC_ADDR, &udpar->calling_addr);
		xua_msg_add_sccp_addr(xua, SUA_IEI_DEST_ADDR, &udpar->called_addr);
		xua_msg_add_u32(xua, SUA_IEI_SEQ_CTRL, seq_ctrl);
		/* optional: importance, ... correlation id? */
		if (!prim)
			goto prim_needed;
		xua_msg_add_data(xua, SUA_IEI_DATA, msgb_l2len(prim->oph.msg),
				 msgb_l2(prim->oph.msg));
		break;
	default:
		LOGP(DLSCCP, LOGL_ERROR, "Unknown msg_type %u\n", msg_type);
		xua_msg_free(xua);
		return NULL;
	}
	return xua;

prim_needed:
	xua_msg_free(xua);
	LOGP(DLSCCP, LOGL_ERROR, "%s must be called with valid 'prim' "
	     "pointer for msg_type=%u\n", __func__, msg_type);
	return NULL;
}

/* generate xua_msg, encode it and send it to SCRC */
static int xua_gen_encode_and_send(struct osmo_sccp_user *scu, uint32_t event,
				   struct osmo_scu_prim *prim, int msg_type)
{
	struct xua_msg *xua;
	int rc;

	xua = xua_gen_msg_cl(event, prim, msg_type);
	if (!xua)
		return -1;

	rc = sccp_scrc_rx_sclc_msg(scu->inst, xua);
	xua_msg_free(xua);
	return rc;
}

/*! Main entrance function for primitives from SCCP User.
 * The caller is required to free oph->msg, otherwise the same as sccp_sclc_user_sap_down().
 *  \param[in] scu SCCP User who is sending the primitive
 *  \param[on] oph Osmocom primitive header of the primitive
 *  \returns 0 on success; negative in case of error */
int sccp_sclc_user_sap_down_nofree(struct osmo_sccp_user *scu, struct osmo_prim_hdr *oph)
{
	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;

	/* we get called from osmo_sccp_user_sap_down() which already
	 * has debug-logged the primitive */

	switch (OSMO_PRIM_HDR(&prim->oph)) {
	case OSMO_PRIM(OSMO_SCU_PRIM_N_UNITDATA, PRIM_OP_REQUEST):
		/* Connectionless by-passes this altogether */
		return xua_gen_encode_and_send(scu, -1, prim, SUA_CL_CLDT);
	default:
		LOGPSCU(scu, LOGL_ERROR, "Received unknown SCCP User primitive %s from user\n",
			osmo_scu_prim_name(&prim->oph));
		return -1;
	}
}

/*! Main entrance function for primitives from SCCP User.
 * Implies a msgb_free(oph->msg), otherwise the same as sccp_sclc_user_sap_down_nofree().
 *  \param[in] scu SCCP User who is sending the primitive
 *  \param[on] oph Osmocom primitive header of the primitive
 *  \returns 0 on success; negative in case of error */
int sccp_sclc_user_sap_down(struct osmo_sccp_user *scu, struct osmo_prim_hdr *oph)
{
	struct osmo_scu_prim *prim = (struct osmo_scu_prim *) oph;
	struct msgb *msg = prim->oph.msg;
	int rc = sccp_sclc_user_sap_down_nofree(scu, oph);
	msgb_free(msg);
	return rc;
}

/* Process an incoming CLDT message (from a remote peer) */
static int sclc_rx_cldt(struct osmo_sccp_instance *inst, struct xua_msg *xua)
{
	struct osmo_scu_prim *prim;
	struct osmo_scu_unitdata_param *param;
	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
	struct msgb *upmsg;
	struct osmo_sccp_user *scu;
	uint32_t protocol_class;
	int rc;

	if (!data_ie) {
		LOGPSCI(inst, LOGL_ERROR, "SCCP/SUA CLDT without user data?!?\n");
		return -1;
	}

	upmsg = sccp_msgb_alloc(__func__);
	/* fill primitive */
	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
	param = &prim->u.unitdata;
	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
			OSMO_SCU_PRIM_N_UNITDATA,
			PRIM_OP_INDICATION, upmsg);

	rc = sua_addr_parse(&param->called_addr, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid DEST_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		msgb_free(upmsg);
		return -EINVAL;
	}

	rc = sua_addr_parse(&param->calling_addr, xua, SUA_IEI_SRC_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid SRC_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		msgb_free(upmsg);
		return -EINVAL;
	}

	param->in_sequence_control = xua_msg_get_u32(xua, SUA_IEI_SEQ_CTRL);
	protocol_class = xua_msg_get_u32(xua, SUA_IEI_PROTO_CLASS);
	param->return_option = protocol_class & 0x80;
	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);

	scu = sccp_user_find(inst, param->called_addr.ssn,
			     param->called_addr.pc);
	if (!scu) {
		/* FIXME: Send destination unreachable? */
		_LOGPSCI(inst, DLSUA, LOGL_NOTICE, "Received SUA message for unequipped SSN %u\n",
			 param->called_addr.ssn);
		msgb_free(upmsg);
		return 0;
	}

	/* copy data */
	upmsg->l2h = msgb_put(upmsg, data_ie->len);
	memcpy(upmsg->l2h, data_ie->dat, data_ie->len);

	/* send to user SAP */
	sccp_user_prim_up(scu, prim);

	/* xua_msg is free'd by our caller */
	return 0;
}

static int sclc_rx_cldr(struct osmo_sccp_instance *inst, struct xua_msg *xua)
{
	struct osmo_scu_prim *prim;
	struct osmo_scu_notice_param *param;
	struct xua_msg_part *data_ie = xua_msg_find_tag(xua, SUA_IEI_DATA);
	struct msgb *upmsg;
	struct osmo_sccp_user *scu;
	int rc;

	if (!data_ie) {
		LOGPSCI(inst, LOGL_ERROR, "SCCP/SUA CLDR without user data?!?\n");
		return -1;
	}

	upmsg = sccp_msgb_alloc(__func__);
	/* fill primitive */
	prim = (struct osmo_scu_prim *) msgb_put(upmsg, sizeof(*prim));
	param = &prim->u.notice;
	osmo_prim_init(&prim->oph, SCCP_SAP_USER,
			OSMO_SCU_PRIM_N_NOTICE,
			PRIM_OP_INDICATION, upmsg);

	rc = sua_addr_parse(&param->called_addr, xua, SUA_IEI_DEST_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid DEST_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		msgb_free(upmsg);
		return -EINVAL;
	}

	rc = sua_addr_parse(&param->calling_addr, xua, SUA_IEI_SRC_ADDR);
	if (rc < 0) {
		LOGPSCI(inst, LOGL_ERROR, "XUA Message %s without valid SRC_ADDR\n",
			xua_hdr_dump(xua, &xua_dialect_sua));
		msgb_free(upmsg);
		return -EINVAL;
	}

	param->importance = xua_msg_get_u32(xua, SUA_IEI_IMPORTANCE);
	param->cause = xua_msg_get_u32(xua, SUA_IEI_CAUSE) & ~SUA_CAUSE_T_MASK;

	scu = sccp_user_find(inst, param->called_addr.ssn,
			     param->called_addr.pc);
	if (!scu) {
		/* FIXME: Send destination unreachable? */
		_LOGPSCI(inst, DLSUA, LOGL_NOTICE, "Received CLDR for unequipped SSN %u\n",
			 param->called_addr.ssn);
		msgb_free(upmsg);
		return 0;
	}

	/* copy data */
	upmsg->l2h = msgb_put(upmsg, data_ie->len);
	memcpy(upmsg->l2h, data_ie->dat, data_ie->len);

	/* send to user SAP */
	sccp_user_prim_up(scu, prim);

	/* xua_msg is free'd by our caller */
	return 0;
}

/*! \brief SCRC -> SCLC (connectionless message)
 *  \param[in] inst SCCP Instance in which we operate
 *  \param[in] xua SUA connectionless message
 *  \returns 0 on success; negative on error */
int sccp_sclc_rx_from_scrc(struct osmo_sccp_instance *inst,
			   struct xua_msg *xua)
{
	int rc = -1;

	OSMO_ASSERT(xua->hdr.msg_class == SUA_MSGC_CL);

	switch (xua->hdr.msg_type) {
	case SUA_CL_CLDT:
		rc = sclc_rx_cldt(inst, xua);
		break;
	case SUA_CL_CLDR:
		rc = sclc_rx_cldr(inst, xua);
		break;
	default:
		_LOGPSCI(inst, DLSUA, LOGL_NOTICE, "Received unknown/unsupported message %s\n",
			 xua_hdr_dump(xua, &xua_dialect_sua));
		break;
	}

	return rc;
}

/* generate a return/refusal message (SUA CLDR == SCCP UDTS) based on
 * the incoming message.  We need to flip all identities between sender
 * and receiver */
static struct xua_msg *gen_ret_msg(struct osmo_sccp_instance *inst,
				   const struct xua_msg *xua_in,
				   uint32_t ret_cause)
{
	struct xua_msg *xua_out = xua_msg_alloc();
	struct osmo_sccp_addr called;

	xua_out->hdr = XUA_HDR(SUA_MSGC_CL, SUA_CL_CLDR);
	xua_msg_add_u32(xua_out, SUA_IEI_ROUTE_CTX, inst->route_ctx);
	xua_msg_add_u32(xua_out, SUA_IEI_CAUSE,
			SUA_CAUSE_T_RETURN | ret_cause);
	/* Swap Calling and Called Party */
	xua_msg_copy_part(xua_out, SUA_IEI_SRC_ADDR, xua_in, SUA_IEI_DEST_ADDR);
	xua_msg_copy_part(xua_out, SUA_IEI_DEST_ADDR, xua_in, SUA_IEI_SRC_ADDR);
	/* TODO: Optional: Hop Count */
	/* Optional: Importance */
	xua_msg_copy_part(xua_out, SUA_IEI_IMPORTANCE,
			  xua_in, SUA_IEI_IMPORTANCE);
	/* Optional: Message Priority */
	xua_msg_copy_part(xua_out, SUA_IEI_MSG_PRIO, xua_in, SUA_IEI_MSG_PRIO);
	/* Optional: Correlation ID */
	xua_msg_copy_part(xua_out, SUA_IEI_CORR_ID, xua_in, SUA_IEI_CORR_ID);
	/* Optional: Segmentation */
	xua_msg_copy_part(xua_out, SUA_IEI_SEGMENTATION,
			  xua_in, SUA_IEI_SEGMENTATION);
	/* Optional: Data */
	xua_msg_copy_part(xua_out, SUA_IEI_DATA, xua_in, SUA_IEI_DATA);

	OSMO_ASSERT(sua_addr_parse(&called, xua_out, SUA_IEI_DEST_ADDR) == 0);

	/* Route on PC + SSN ? */
	if (called.ri == OSMO_SCCP_RI_SSN_PC) {
		/* if no PC, copy OPC into called addr */
		if (!(called.presence & OSMO_SCCP_ADDR_T_PC)) {
			struct osmo_sccp_addr calling;
			OSMO_ASSERT(sua_addr_parse(&calling, xua_out, SUA_IEI_SRC_ADDR) == 0);
			called.presence |= OSMO_SCCP_ADDR_T_PC;
			called.pc = calling.pc;
			/* Re-encode / replace called address */
			xua_msg_free_tag(xua_out, SUA_IEI_DEST_ADDR);
			xua_msg_add_sccp_addr(xua_out, SUA_IEI_DEST_ADDR,
					      &called);
		}
	}
	return xua_out;
}

/*! \brief SCRC -> SCLC (Routing Failure
 *  \param[in] inst SCCP Instance in which we operate
 *  \param[in] xua_in Message that failed to be routed
 *  \param[in] cause SCCP Return Cause */
void sccp_sclc_rx_scrc_rout_fail(struct osmo_sccp_instance *inst,
				 struct xua_msg *xua_in, uint32_t cause)
{
	struct xua_msg *xua_out;

	LOGPSCI(inst, LOGL_NOTICE, "SCRC Routing Failure (%s) for CL message %s\n",
		osmo_sccp_return_cause_name(cause),
		xua_hdr_dump(xua_in, &xua_dialect_sua));

	/* Figure C.12/Q.714 (Sheet 8) Node 9 */
	switch (xua_in->hdr.msg_type) {
	case SUA_CL_CLDT:
		xua_out = gen_ret_msg(inst, xua_in, cause);
		/* TODO: Message Return Option? */
		if (!osmo_ss7_pc_is_local(inst->ss7, xua_in->mtp.opc)) {
			/* non-local originator: send UDTS */
			/* TODO: Assign SLS */
			sccp_scrc_rx_sclc_msg(inst, xua_out);
		} else {
			/* local originator: send N-NOTICE to user */
			/* TODO: N-NOTICE.ind SCLC -> SCU */
			sclc_rx_cldr(inst, xua_out);
		}
		xua_msg_free(xua_out);
		break;
	case SUA_CL_CLDR:
		/* do nothing */
		break;
	}
}
