Add support for multiple APN profiles for subscriber data

Previously the HLR sent in the Insert Subscriber Data call only the
wildcard APN as a single entry.
This violates the spec because the first entry (with the lowest context_id) is
always the default APN, but it is forbidden to have a wildcard APN as default apn.

Introduce a default template/profile which can contain multiple APNs.
This profile is always sent out to the SGSN/MME as part of Insert-Subscriber-Data.
In the future a subscriber might have a profile template name written into the
database which will resolve to a "pdp-profile premium" in the configuration.

To be backward compatible, if the pdp-profile default section is missing,
the HLR will send out only a wildcard APN.

Config example:

hlr
 ps
  pdp-profile default
   profile 1
    apn internet
   profile 2
    apn *

Changes to the apn list will be only handed out to subscribers
when the subscriber do a location update.

Related: SYS#6391
Change-Id: I540132ee5dcfd09f4816e02e702927e1074ca50f
diff --git a/src/hlr_vty.c b/src/hlr_vty.c
index 02e0cde..af57159 100644
--- a/src/hlr_vty.c
+++ b/src/hlr_vty.c
@@ -26,9 +26,12 @@
  */
 
 #include <errno.h>
+#include <string.h>
 
 #include <osmocom/core/talloc.h>
 #include <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+#include <osmocom/gsm/apn.h>
+
 #include <osmocom/vty/vty.h>
 #include <osmocom/vty/stats.h>
 #include <osmocom/vty/command.h>
@@ -103,6 +106,182 @@
 	return CMD_SUCCESS;
 }
 
+struct cmd_node ps_node = {
+	PS_NODE,
+	"%s(config-hlr-ps)# ",
+	1,
+};
+
+DEFUN(cfg_ps,
+      cfg_ps_cmd,
+      "ps",
+      "Configure the PS options")
+{
+	vty->node = PS_NODE;
+	return CMD_SUCCESS;
+}
+
+struct cmd_node ps_pdp_profiles_node = {
+	PS_PDP_PROFILES_NODE,
+	"%s(config-hlr-ps-pdp-profiles)# ",
+	1,
+};
+
+DEFUN(cfg_ps_pdp_profiles,
+      cfg_ps_pdp_profiles_cmd,
+      "pdp-profiles default",
+      "Define a PDP profile set.\n"
+      "Define the global default profile.\n")
+{
+	g_hlr->ps.pdp_profile.enabled = true;
+
+	vty->node = PS_PDP_PROFILES_NODE;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_ps_pdp_profiles,
+      cfg_no_ps_pdp_profiles_cmd,
+      "no pdp-profiles default",
+      NO_STR
+      "Delete PDP profile.\n"
+      "Unique identifier for this PDP profile set.\n")
+{
+	g_hlr->ps.pdp_profile.enabled = false;
+	return CMD_SUCCESS;
+}
+
+
+
+struct cmd_node ps_pdp_profiles_profile_node = {
+	PS_PDP_PROFILES_PROFILE_NODE,
+	"%s(config-hlr-ps-pdp-profile)# ",
+	1,
+};
+
+
+/* context_id == 0 means the slot is free */
+struct osmo_gsup_pdp_info *get_pdp_profile(uint8_t context_id)
+{
+	for (int i = 0; i < OSMO_GSUP_MAX_NUM_PDP_INFO; i++) {
+		struct osmo_gsup_pdp_info *info = &g_hlr->ps.pdp_profile.pdp_infos[i];
+		if (info->context_id == context_id)
+			return info;
+	}
+
+	return NULL;
+}
+
+struct osmo_gsup_pdp_info *create_pdp_profile(uint8_t context_id)
+{
+	struct osmo_gsup_pdp_info *info = get_pdp_profile(0);
+	if (!info)
+		return NULL;
+
+	memset(info, 0, sizeof(*info));
+	info->context_id = context_id;
+	info->have_info = 1;
+
+	g_hlr->ps.pdp_profile.num_pdp_infos++;
+	return info;
+}
+
+void destroy_pdp_profile(struct osmo_gsup_pdp_info *info)
+{
+	info->context_id = 0;
+	if (info->apn_enc)
+		talloc_free((void *) info->apn_enc);
+
+	g_hlr->ps.pdp_profile.num_pdp_infos--;
+	memset(info, 0, sizeof(*info));
+}
+
+DEFUN(cfg_ps_pdp_profiles_profile,
+      cfg_ps_pdp_profiles_profile_cmd,
+      "profile <1-10>",
+      "Configure a PDP profile\n"
+      "Unique PDP context identifier. The lowest profile will be used as default context.\n")
+{
+	struct osmo_gsup_pdp_info *info;
+	uint8_t context_id = atoi(argv[0]);
+
+	info = get_pdp_profile(context_id);
+	if (!info) {
+		info = create_pdp_profile(context_id);
+		if (!info) {
+			vty_out(vty, "Failed to create profile %d!%s", context_id, VTY_NEWLINE);
+			return CMD_ERR_INCOMPLETE;
+		}
+	}
+
+	vty->node = PS_PDP_PROFILES_PROFILE_NODE;
+	vty->index = info;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_ps_pdp_profiles_profile,
+      cfg_no_ps_pdp_profiles_profile_cmd,
+      "no profile <1-10>",
+      NO_STR
+      "Delete a PDP profile\n"
+      "Unique PDP context identifier. The lowest profile will be used as default context.\n")
+{
+	struct osmo_gsup_pdp_info *info;
+	uint8_t context_id = atoi(argv[0]);
+
+	info = get_pdp_profile(context_id);
+	if (info)
+		destroy_pdp_profile(info);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ps_pdp_profile_apn, cfg_ps_pdp_profile_apn_cmd,
+	"apn ID",
+	"Configure the APN.\n"
+	"APN name or * for wildcard apn.\n")
+{
+	struct osmo_gsup_pdp_info *info = vty->index;
+	const char *apn_name = argv[0];
+
+	/* apn encoded takes one more byte than strlen() */
+	size_t apn_enc_len = strlen(apn_name) + 1;
+	uint8_t *apn_enc;
+	int ret;
+
+	if (apn_enc_len > APN_MAXLEN) {
+		vty_out(vty, "APN name is too long '%s'. Max is %d!%s", apn_name, APN_MAXLEN, VTY_NEWLINE);
+		return CMD_ERR_INCOMPLETE;
+	}
+
+	info->apn_enc = apn_enc = (uint8_t *) talloc_zero_size(g_hlr, apn_enc_len);
+	ret = info->apn_enc_len = osmo_apn_from_str(apn_enc, apn_enc_len, apn_name);
+	if (ret < 0) {
+		talloc_free(apn_enc);
+		info->apn_enc = NULL;
+		info->apn_enc_len = 0;
+		vty_out(vty, "Invalid APN name %s!", apn_name);
+		return CMD_WARNING;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_ps_pdp_profile_apn, cfg_no_ps_pdp_profile_apn_cmd,
+      "no apn",
+      NO_STR
+      "Delete the APN.\n")
+{
+	struct osmo_gsup_pdp_info *info = vty->index;
+	if (info->apn_enc) {
+		talloc_free((void *) info->apn_enc);
+		info->apn_enc = NULL;
+		info->apn_enc_len = 0;
+	}
+
+	return CMD_SUCCESS;
+}
+
+
 static int config_write_hlr(struct vty *vty)
 {
 	vty_out(vty, "hlr%s", VTY_NEWLINE);
@@ -149,6 +328,37 @@
 	return CMD_SUCCESS;
 }
 
+static int config_write_hlr_ps(struct vty *vty)
+{
+	vty_out(vty, " ps%s", VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+static int config_write_hlr_ps_pdp_profiles(struct vty *vty)
+{
+	char apn[APN_MAXLEN + 1] = {};
+
+	if (!g_hlr->ps.pdp_profile.enabled)
+		return CMD_SUCCESS;
+
+	vty_out(vty, "  pdp-profiles default%s", VTY_NEWLINE);
+	for (int i = 0; i < g_hlr->ps.pdp_profile.num_pdp_infos; i++) {
+		struct osmo_gsup_pdp_info *pdp_info = &g_hlr->ps.pdp_profile.pdp_infos[i];
+		if (!pdp_info->context_id)
+			continue;
+
+		vty_out(vty, "   profile %d%s", pdp_info->context_id, VTY_NEWLINE);
+		if (!pdp_info->have_info)
+			continue;
+
+		if (pdp_info->apn_enc && pdp_info->apn_enc_len) {
+			osmo_apn_to_str(apn, pdp_info->apn_enc, pdp_info->apn_enc_len);
+			vty_out(vty, "    apn %s%s", apn, VTY_NEWLINE);
+		}
+	}
+	return CMD_SUCCESS;
+}
+
 static void show_one_conn(struct vty *vty, const struct osmo_gsup_conn *conn)
 {
 	const struct ipa_server_conn *isc = conn->conn;
@@ -538,6 +748,20 @@
 	install_element(GSUP_NODE, &cfg_hlr_gsup_bind_ip_cmd);
 	install_element(GSUP_NODE, &cfg_hlr_gsup_ipa_name_cmd);
 
+	/* PS */
+	install_node(&ps_node, config_write_hlr_ps);
+	install_element(HLR_NODE, &cfg_ps_cmd);
+
+	install_node(&ps_pdp_profiles_node, config_write_hlr_ps_pdp_profiles);
+	install_element(PS_NODE, &cfg_ps_pdp_profiles_cmd);
+	install_element(PS_NODE, &cfg_no_ps_pdp_profiles_cmd);
+
+	install_node(&ps_pdp_profiles_profile_node, NULL);
+	install_element(PS_PDP_PROFILES_NODE, &cfg_ps_pdp_profiles_profile_cmd);
+	install_element(PS_PDP_PROFILES_NODE, &cfg_no_ps_pdp_profiles_profile_cmd);
+	install_element(PS_PDP_PROFILES_PROFILE_NODE, &cfg_ps_pdp_profile_apn_cmd);
+	install_element(PS_PDP_PROFILES_PROFILE_NODE, &cfg_no_ps_pdp_profile_apn_cmd);
+
 	install_element(HLR_NODE, &cfg_database_cmd);
 
 	install_element(HLR_NODE, &cfg_euse_cmd);