/* Gb-proxy message patching */

/* (C) 2014 by On-Waves
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <osmocom/sgsn/gb_proxy.h>

#include <osmocom/sgsn/gprs_utils.h>
#include <osmocom/sgsn/gprs_gb_parse.h>

#include <osmocom/sgsn/debug.h>

#include <osmocom/gprs/protocol/gsm_08_18.h>
#include <osmocom/core/rate_ctr.h>
#include <osmocom/gsm/apn.h>

extern void *tall_sgsn_ctx;

/* patch RA identifier in place */
static void gbproxy_patch_raid(struct gsm48_ra_id *raid_enc, struct gbproxy_peer *peer,
			       int to_bss, const char *log_text)
{
	struct gbproxy_patch_state *state = &peer->patch_state;
	struct osmo_plmn_id old_plmn;
	struct gprs_ra_id raid;
	enum gbproxy_peer_ctr counter =
		to_bss ?
		GBPROX_PEER_CTR_RAID_PATCHED_SGSN :
		GBPROX_PEER_CTR_RAID_PATCHED_BSS;

	if (!state->local_plmn.mcc || !state->local_plmn.mnc)
		return;

	gsm48_parse_ra(&raid, (uint8_t *)raid_enc);

	old_plmn = (struct osmo_plmn_id){
		.mcc = raid.mcc,
		.mnc = raid.mnc,
		.mnc_3_digits = raid.mnc_3_digits,
	};

	if (!to_bss) {
		/* BSS -> SGSN */
		if (state->local_plmn.mcc)
			raid.mcc = peer->cfg->core_plmn.mcc;

		if (state->local_plmn.mnc) {
			raid.mnc = peer->cfg->core_plmn.mnc;
			raid.mnc_3_digits = peer->cfg->core_plmn.mnc_3_digits;
		}
	} else {
		/* SGSN -> BSS */
		if (state->local_plmn.mcc)
			raid.mcc = state->local_plmn.mcc;

		if (state->local_plmn.mnc) {
			raid.mnc = state->local_plmn.mnc;
			raid.mnc_3_digits = state->local_plmn.mnc_3_digits;
		}
	}

	LOGP(DGPRS, LOGL_DEBUG,
	     "Patching %s to %s: "
	     "%s-%d-%d -> %s\n",
	     log_text,
	     to_bss ? "BSS" : "SGSN",
	     osmo_plmn_name(&old_plmn), raid.lac, raid.rac,
	     osmo_rai_name(&raid));

	gsm48_encode_ra(raid_enc, &raid);
	rate_ctr_inc(&peer->ctrg->ctr[counter]);
}

static void gbproxy_patch_apn_ie(struct msgb *msg,
				 uint8_t *apn_ie, size_t apn_ie_len,
				 struct gbproxy_peer *peer,
				 size_t *new_apn_ie_len, const char *log_text)
{
	struct apn_ie_hdr {
		uint8_t iei;
		uint8_t apn_len;
		uint8_t apn[0];
	} *hdr = (void *)apn_ie;

	size_t apn_len = hdr->apn_len;
	uint8_t *apn = hdr->apn;

	OSMO_ASSERT(apn_ie_len == apn_len + sizeof(struct apn_ie_hdr));
	OSMO_ASSERT(apn_ie_len > 2 && apn_ie_len <= 102);

	if (peer->cfg->core_apn_size == 0) {
		char str1[110];
		/* Remove the IE */
		LOGP(DGPRS, LOGL_DEBUG,
		     "Patching %s to SGSN: Removing APN '%s'\n",
		     log_text,
		     osmo_apn_to_str(str1, apn, apn_len));

		*new_apn_ie_len = 0;
		msgb_resize_area(msg, apn_ie, apn_ie_len, 0);
	} else {
		/* Resize the IE */
		char str1[110];
		char str2[110];

		OSMO_ASSERT(peer->cfg->core_apn_size <= 100);

		LOGP(DGPRS, LOGL_DEBUG,
		     "Patching %s to SGSN: "
		     "Replacing APN '%s' -> '%s'\n",
		     log_text,
		     osmo_apn_to_str(str1, apn, apn_len),
		     osmo_apn_to_str(str2, peer->cfg->core_apn,
				       peer->cfg->core_apn_size));

		*new_apn_ie_len = peer->cfg->core_apn_size + 2;
		msgb_resize_area(msg, apn, apn_len, peer->cfg->core_apn_size);
		memcpy(apn, peer->cfg->core_apn, peer->cfg->core_apn_size);
		hdr->apn_len = peer->cfg->core_apn_size;
	}

	rate_ctr_inc(&peer->ctrg->ctr[GBPROX_PEER_CTR_APN_PATCHED]);
}

static int gbproxy_patch_tlli(uint8_t *tlli_enc,
			      struct gbproxy_peer *peer,
			      uint32_t new_tlli,
			      int to_bss, const char *log_text)
{
	uint32_t tlli_be;
	uint32_t tlli;
	enum gbproxy_peer_ctr counter =
		to_bss ?
		GBPROX_PEER_CTR_TLLI_PATCHED_SGSN :
		GBPROX_PEER_CTR_TLLI_PATCHED_BSS;

	memcpy(&tlli_be, tlli_enc, sizeof(tlli_be));
	tlli = ntohl(tlli_be);

	if (tlli == new_tlli)
		return 0;

	LOGP(DGPRS, LOGL_DEBUG,
	     "Patching %ss: "
	     "Replacing %08x -> %08x\n",
	     log_text, tlli, new_tlli);

	tlli_be = htonl(new_tlli);
	memcpy(tlli_enc, &tlli_be, sizeof(tlli_be));

	rate_ctr_inc(&peer->ctrg->ctr[counter]);

	return 1;
}

static int gbproxy_patch_ptmsi(uint8_t *ptmsi_enc,
			       struct gbproxy_peer *peer,
			       uint32_t new_ptmsi,
			       int to_bss, const char *log_text)
{
	uint32_t ptmsi_be;
	uint32_t ptmsi;
	enum gbproxy_peer_ctr counter =
		to_bss ?
		GBPROX_PEER_CTR_PTMSI_PATCHED_SGSN :
		GBPROX_PEER_CTR_PTMSI_PATCHED_BSS;
	memcpy(&ptmsi_be, ptmsi_enc, sizeof(ptmsi_be));
	ptmsi = ntohl(ptmsi_be);

	if (ptmsi == new_ptmsi)
		return 0;

	LOGP(DGPRS, LOGL_DEBUG,
	     "Patching %ss: "
	     "Replacing %08x -> %08x\n",
	     log_text, ptmsi, new_ptmsi);

	ptmsi_be = htonl(new_ptmsi);
	memcpy(ptmsi_enc, &ptmsi_be, sizeof(ptmsi_be));

	rate_ctr_inc(&peer->ctrg->ctr[counter]);

	return 1;
}

int gbproxy_patch_llc(struct msgb *msg, uint8_t *llc, size_t llc_len,
		     struct gbproxy_peer *peer,
		     struct gbproxy_link_info *link_info, int *len_change,
		     struct gprs_gb_parse_context *parse_ctx)
{
	struct gprs_llc_hdr_parsed *ghp = &parse_ctx->llc_hdr_parsed;
	int have_patched = 0;
	int fcs;
	struct gbproxy_config *cfg = peer->cfg;

	if (parse_ctx->ptmsi_enc && link_info &&
	    !parse_ctx->old_raid_is_foreign && peer->cfg->patch_ptmsi) {
		uint32_t ptmsi;
		if (parse_ctx->to_bss)
			ptmsi = link_info->tlli.ptmsi;
		else
			ptmsi = link_info->sgsn_tlli.ptmsi;

		if (ptmsi != GSM_RESERVED_TMSI) {
			if (gbproxy_patch_ptmsi(parse_ctx->ptmsi_enc, peer,
						ptmsi, parse_ctx->to_bss, "P-TMSI"))
				have_patched = 1;
		} else {
			/* TODO: invalidate old RAI if present (see below) */
		}
	}

	if (parse_ctx->new_ptmsi_enc && link_info && cfg->patch_ptmsi) {
		uint32_t ptmsi;
		if (parse_ctx->to_bss)
			ptmsi = link_info->tlli.ptmsi;
		else
			ptmsi = link_info->sgsn_tlli.ptmsi;

		OSMO_ASSERT(ptmsi);
		if (gbproxy_patch_ptmsi(parse_ctx->new_ptmsi_enc, peer,
					ptmsi, parse_ctx->to_bss, "new P-TMSI"))
			have_patched = 1;
	}

	if (parse_ctx->raid_enc) {
		gbproxy_patch_raid((struct gsm48_ra_id *)parse_ctx->raid_enc, peer, parse_ctx->to_bss,
				   parse_ctx->llc_msg_name);
		have_patched = 1;
	}

	if (parse_ctx->old_raid_enc && !parse_ctx->old_raid_is_foreign) {
		/* TODO: Patch to invalid if P-TMSI unknown. */
		gbproxy_patch_raid((struct gsm48_ra_id *)parse_ctx->old_raid_enc, peer, parse_ctx->to_bss,
				   parse_ctx->llc_msg_name);
		have_patched = 1;
	}

	if (parse_ctx->apn_ie &&
	    cfg->core_apn &&
	    !parse_ctx->to_bss &&
	    gbproxy_imsi_matches(cfg, GBPROX_MATCH_PATCHING, link_info) &&
	    cfg->core_apn) {
		size_t new_len;
		gbproxy_patch_apn_ie(msg,
				     parse_ctx->apn_ie, parse_ctx->apn_ie_len,
				     peer, &new_len, parse_ctx->llc_msg_name);
		*len_change += (int)new_len - (int)parse_ctx->apn_ie_len;

		have_patched = 1;
	}

	if (have_patched) {
		llc_len += *len_change;
		ghp->crc_length += *len_change;

		/* Fix FCS */
		fcs = gprs_llc_fcs(llc, ghp->crc_length);
		LOGP(DLLC, LOGL_DEBUG, "Updated LLC message, CRC: %06x -> %06x\n",
		     ghp->fcs, fcs);

		llc[llc_len - 3] = fcs & 0xff;
		llc[llc_len - 2] = (fcs >> 8) & 0xff;
		llc[llc_len - 1] = (fcs >> 16) & 0xff;
	}

	return have_patched;
}

/* patch BSSGP message to use core_plmn.mcc/mnc on the SGSN side */
void gbproxy_patch_bssgp(struct msgb *msg, uint8_t *bssgp, size_t bssgp_len,
			 struct gbproxy_peer *peer,
			 struct gbproxy_link_info *link_info, int *len_change,
			 struct gprs_gb_parse_context *parse_ctx)
{
	const char *err_info = NULL;
	int err_ctr = -1;

	if (parse_ctx->bssgp_raid_enc)
		gbproxy_patch_raid((struct gsm48_ra_id *)parse_ctx->bssgp_raid_enc, peer,
				   parse_ctx->to_bss, "BSSGP");

	if (parse_ctx->need_decryption &&
	    (peer->cfg->patch_ptmsi || peer->cfg->core_apn)) {
		/* Patching LLC messages has been requested
		 * explicitly, but the message (including the
		 * type) is encrypted, so we possibly fail to
		 * patch the LLC part of the message. */
		err_ctr = GBPROX_PEER_CTR_PATCH_CRYPT_ERR;
		err_info = "GMM message is encrypted";
		goto patch_error;
	}

	if (!link_info && parse_ctx->tlli_enc && parse_ctx->to_bss) {
		/* Happens with unknown (not cached) TLLI coming from
		 * the SGSN */
		/* TODO: What shall be done with the message in this case? */
		err_ctr = GBPROX_PEER_CTR_TLLI_UNKNOWN;
		err_info = "TLLI sent by the SGSN is unknown";
		goto patch_error;
	}

	if (!link_info)
		return;

	if (parse_ctx->tlli_enc && peer->cfg->patch_ptmsi) {
		uint32_t tlli = gbproxy_map_tlli(parse_ctx->tlli,
						 link_info, parse_ctx->to_bss);

		if (tlli) {
			gbproxy_patch_tlli(parse_ctx->tlli_enc, peer, tlli,
					   parse_ctx->to_bss, "TLLI");
			parse_ctx->tlli = tlli;
		} else {
			/* Internal error */
			err_ctr = GBPROX_PEER_CTR_PATCH_ERR;
			err_info = "Replacement TLLI is 0";
			goto patch_error;
		}
	}

	if (parse_ctx->bssgp_ptmsi_enc && peer->cfg->patch_ptmsi) {
		uint32_t ptmsi;
		if (parse_ctx->to_bss)
			ptmsi = link_info->tlli.ptmsi;
		else
			ptmsi = link_info->sgsn_tlli.ptmsi;

		if (ptmsi != GSM_RESERVED_TMSI)
			gbproxy_patch_ptmsi(
				parse_ctx->bssgp_ptmsi_enc, peer,
				ptmsi, parse_ctx->to_bss, "BSSGP P-TMSI");
	}

	if (parse_ctx->llc) {
		uint8_t *llc = parse_ctx->llc;
		size_t llc_len = parse_ctx->llc_len;
		int llc_len_change = 0;

		gbproxy_patch_llc(msg, llc, llc_len, peer, link_info,
				  &llc_len_change, parse_ctx);
		/* Note that the APN might have been resized here, but no
		 * pointer int the parse_ctx will refer to an adress after the
		 * APN. So it's possible to patch first and do the TLLI
		 * handling afterwards. */

		if (llc_len_change) {
			llc_len += llc_len_change;

			/* Fix LLC IE len */
			/* TODO: This is a kludge, but the a pointer to the
			 * start of the IE is not available here */
			if (llc[-2] == BSSGP_IE_LLC_PDU && llc[-1] & 0x80) {
				/* most probably a one byte length */
				if (llc_len > 127) {
					err_info = "Cannot increase size";
					err_ctr = GBPROX_PEER_CTR_PATCH_ERR;
					goto patch_error;
				}
				llc[-1] = llc_len | 0x80;
			} else {
				llc[-2] = (llc_len >> 8) & 0x7f;
				llc[-1] = llc_len & 0xff;
			}
			*len_change += llc_len_change;
		}
		/* Note that the tp struct might contain invalid pointers here
		 * if the LLC field has changed its size */
		parse_ctx->llc_len = llc_len;
	}
	return;

patch_error:
	OSMO_ASSERT(err_ctr >= 0);
	rate_ctr_inc(&peer->ctrg->ctr[err_ctr]);
	LOGP(DGPRS, LOGL_ERROR,
	     "NSEI=%u(%s) failed to patch BSSGP message as requested: %s.\n",
	     msgb_nsei(msg), parse_ctx->to_bss ? "SGSN" : "BSS",
	     err_info);
}

void gbproxy_clear_patch_filter(struct gbproxy_match *match)
{
	if (match->enable) {
		regfree(&match->re_comp);
		match->enable = false;
	}
	talloc_free(match->re_str);
	match->re_str = NULL;
}

int gbproxy_set_patch_filter(struct gbproxy_match *match, const char *filter,
		const char **err_msg)
{
	static char err_buf[300];
	int rc;

	gbproxy_clear_patch_filter(match);

	if (!filter)
		return 0;

	rc = regcomp(&match->re_comp, filter,
		     REG_EXTENDED | REG_NOSUB | REG_ICASE);

	if (rc == 0) {
		match->enable = true;
		match->re_str = talloc_strdup(tall_sgsn_ctx, filter);
		return 0;
	}

	if (err_msg) {
		regerror(rc, &match->re_comp,
			 err_buf, sizeof(err_buf));
		*err_msg = err_buf;
	}

	return -1;
}

int gbproxy_check_imsi(struct gbproxy_match *match,
		       const uint8_t *imsi, size_t imsi_len)
{
	char mi_buf[200];
	int rc;

	if (!match->enable)
		return 1;

	rc = gprs_is_mi_imsi(imsi, imsi_len);
	if (rc > 0)
		rc = gsm48_mi_to_string(mi_buf, sizeof(mi_buf), imsi, imsi_len);
	if (rc <= 0) {
		LOGP(DGPRS, LOGL_NOTICE, "Invalid IMSI %s\n",
		     osmo_hexdump(imsi, imsi_len));
		return -1;
	}

	LOGP(DGPRS, LOGL_DEBUG, "Checking IMSI '%s' (%d)\n", mi_buf, rc);

	rc = regexec(&match->re_comp, mi_buf, 0, NULL, 0);
	if (rc == REG_NOMATCH) {
		LOGP(DGPRS, LOGL_INFO,
		       "IMSI '%s' doesn't match pattern '%s'\n",
		       mi_buf, match->re_str);
		return 0;
	}

	return 1;
}
