#!/bin/sh

. /lib/functions.sh
. /usr/share/libubox/jshn.sh

migrate_ipsec() {
	# Skip migration if the 'globals' section already exists
	uci show ipsec.globals 1>/dev/null 2>/dev/null
	[ "$?" = "0" ] && return

	# Rename last ipsec section to 'globals'
	uci -q rename ipsec.@ipsec[-1]=globals
	uci -q set ipsec.globals=globals
}

migrate_ignore_routing_tables() {
	local tables table count

	# Check whether we still have the old config option ( not a 'list')
	count="$(grep -c -E "^[[:space:]]*option ignore_routing_tables" /etc/config/ipsec)"
	[ "$count" = "0" ] && return

	tables="$(uci -q get "ipsec.globals.ignore_routing_tables")"
	uci -q delete "ipsec.globals.ignore_routing_tables"
	for table in $tables; do
		uci -q add_list "ipsec.globals.ignore_routing_tables=${table}"
	done
}

migrate_local_nat_child() {
	local cfg="$1"

	local local_nat value

	config_get local_nat "$cfg" local_nat ""
	[ -z "$local_nat" ] && return

	uci -q delete "ipsec.${cfg}.local_nat"

	# Replace 'local_subnet' with 'local_nat' and do not append.
	# That's how it was previously done in the swanctl start script.
	uci -q delete "ipsec.${cfg}.local_subnet"

	for value in $local_nat; do
		uci -q add_list "ipsec.${cfg}.local_subnet=${value}"
	done
}

migrate_local_nat() {
	config_load ipsec
	config_foreach migrate_local_nat_child tunnel
	config_foreach migrate_local_nat_child transport
}

migrate_gateway_remote() {
	local cfg="$1"

	local gateway

	config_get gateway "$cfg" gateway ""
	[ -z "$gateway" ] && return

	# The option 'any' is default for the 'remote_gateway' option if not set
	# and does not need to be saved.
	[ "$gateway" = "any" ] || {
		uci -q set "ipsec.${cfg}.remote_gateway=${gateway}"
	}

	uci -q delete "ipsec.${cfg}.gateway"
}

migrate_gateway() {
	config_load ipsec
	config_foreach migrate_gateway_remote remote
}

migrate_remote_gateway_remote() {
	local cfg="$1"

	local remote_gateway value

	config_get remote_gateway "$cfg" remote_gateway ""
	[ -z "$remote_gateway" ] && return

	for value in $remote_gateway; do
		uci -q add_list "ipsec.${cfg}.remote_addrs=${value}"
	done

	uci -q delete "ipsec.${cfg}.remote_gateway"
}

migrate_remote_gateway() {
	config_load ipsec
	config_foreach migrate_remote_gateway_remote remote
}

migrate_local_ip_local() {
	local cfg="$1"

	local local_ip value

	config_get local_ip "$cfg" local_ip ""
	[ -z "$local_ip" ] && return

	for value in $local_ip; do
		uci -q add_list "ipsec.${cfg}.local_addrs=${value}"
	done

	uci -q delete "ipsec.${cfg}.local_ip"
}

migrate_local_ip() {
	config_load ipsec
	config_foreach migrate_local_ip_local remote
}

migrate_local_sourceip_vips() {
	local cfg="$1"

	local local_sourceip value

	config_get local_sourceip "$cfg" local_sourceip ""
	[ -z "$local_sourceip" ] && return

	for value in $local_sourceip; do
		uci -q add_list "ipsec.${cfg}.vips=${value}"
	done

	uci -q delete "ipsec.${cfg}.local_sourceip"
}

migrate_local_sourceip() {
	config_load ipsec
	config_foreach migrate_local_sourceip_vips remote
}

migrate_to_child() {
	local tunnel_sections=""
	local transport_sections=""
	local remote_sections=""

	local section

	collect_tunnel() {
		tunnel_sections="${tunnel_sections} $1"
	}
	collect_transport() {
		transport_sections="${transport_sections} $1"
	}
	collect_remote() {
		remote_sections="${remote_sections} $1"
	}

	config_load "ipsec"
	config_foreach collect_tunnel "tunnel"
	config_foreach collect_transport "transport"
	config_foreach collect_remote "remote"

	migrate_simple() {
		local cfg="$1"
		local mode="$2"

		uci -q set ipsec.${cfg}=child
		uci -q set ipsec.${cfg}.mode="$mode"
	}

	for section in $tunnel_sections; do
		migrate_simple "$section" "tunnel"
	done

	for section in $transport_sections; do
		migrate_simple "$section" "transport"
	done

	for section in $remote_sections; do
		local tunnel_refs=""
		local transport_refs=""

		config_get tunnel_refs "$section" tunnel ""
		config_get transport_refs "$section" transport ""

		if [ -n "$tunnel_refs" ] || [ -n "$transport_refs" ]; then
			uci -q delete ipsec.${section}.tunnel
			uci -q delete ipsec.${section}.transport
			for ref in $tunnel_refs $transport_refs; do
				uci -q add_list ipsec.${section}.child="$ref"
			done
		fi
	done
}

migrate_debug() {
	local debug

	config_load "ipsec"
	config_get debug "globals" debug ""

	[ -z "$debug" ] && return

	# create syslog section if not exist
	if [ -z "$(uci -q get ipsec.syslog)" ]; then
		uci -q set ipsec.syslog="syslog"
	fi

	uci -q set ipsec.syslog.dmn="$debug"
	uci -q set ipsec.syslog.mgr="$debug"
	uci -q set ipsec.syslog.job="$debug"
	uci -q set ipsec.syslog.cfg="$debug"
	uci -q set ipsec.syslog.knl="$debug"
	uci -q set ipsec.syslog.net="$debug"
	uci -q set ipsec.syslog.asn="$debug"
	uci -q set ipsec.syslog.lib="$debug"
	uci -q set ipsec.syslog.tls="$debug"
	uci -q set ipsec.syslog.tnc="$debug"
	uci -q set ipsec.syslog.imc="$debug"
	uci -q set ipsec.syslog.imv="$debug"
	uci -q set ipsec.syslog.pts="$debug"
	uci -q set ipsec.syslog.app="$debug"
	uci -q set ipsec.syslog.wch="$debug"
	uci -q set ipsec.syslog.ike="$debug"
	uci -q set ipsec.syslog.chd="$debug"
	uci -q set ipsec.syslog.enc="$debug"
	uci -q set ipsec.syslog.esp="$debug"

	uci -q delete ipsec.globals.debug
}

migrate_dpd_action_value() {
	local cfg="$1"

	local dpdaction

	config_get dpdaction "$cfg" dpdaction ""
	[ -z "$dpdaction" ] && return

	[ "$dpdaction" = "hold" ] && {
		uci -q set ipsec.${cfg}.dpd_action="trap"
	}

	[ "$dpdaction" = "restart" ] && {
		uci -q set ipsec.${cfg}.dpd_action="restart"
	}

	# * 'none' is the default value for the *old* dpdaction. This value
	#   is no longer available in the new 'dpd_action'. It should be
	#   converted to 'dpd_delay=0' and 'dpd_action=clear' to restore the
	#   old behavior.
	# * 'clear' is default value if 'dpd_action' is not specified
	#   This option does not need to be migrated, as it is the default
	#   for the *new* option.

	uci -q delete ipsec.${cfg}.dpdaction
}

migrate_dpd_action() {
	config_load ipsec
	config_foreach migrate_dpd_action_value child
}

migrate_close_action_value() {
	local cfg="$1"

	local closeaction

	# Check whether we need to migrate at all.
	config_get closeaction "$cfg" closeaction ""
	[ -z "$closeaction" ] && return

	uci -q delete ipsec.${cfg}.closeaction
	[ "$closeaction" = "none" ] || [ "$closeaction" = "clear" ] && {
		uci -q set ipsec.${cfg}.close_action="none"
	}

	[ "$closeaction" = "hold" ] && {
		uci -q set ipsec.${cfg}.close_action="trap"
	}

	[ "$closeaction" = "restart" ] && {
		uci -q set ipsec.${cfg}.close_action="start"
	}
}

migrate_close_action() {
	config_load ipsec
	config_foreach migrate_close_action_value child
}

migrate_start_action_value() {
	local cfg="$1"

	local startaction

	# Check whether we need to migrate at all.
	config_get startaction "$cfg" startaction ""
	[ -z "$startaction" ] && return

	uci -q delete ipsec.${cfg}.startaction
	[ "$startaction" = "add" ] && {
		uci -q set ipsec.${cfg}.start_action="none"
	}

	[ "$startaction" = "route" ] && {
		uci -q set ipsec.${cfg}.start_action="trap"
	}

	[ "$startaction" = "start" ] && {
		uci -q set ipsec.${cfg}.start_action="start"
	}
	# * 'none' is default value if 'start_action' is not specified
	# * 'ignore' is the default value for the old startaction. This value
	#   is no longer available in the new 'start_action'. There is not
	#   equivalent for this option!
}

migrate_start_action() {
	config_load ipsec
	config_foreach migrate_start_action_value child
}

migrate_option_child_values() {
	local cfg="$1"

	local value

	config_get value "$cfg" rekeytime ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.rekeytime=rekey_time"
	}

	config_get value "$cfg" lifetime ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.lifetime=life_time"
	}

	config_get value "$cfg" rekeybytes ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.rekeybytes=rekey_bytes"
	}

	config_get value "$cfg" lifebytes ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.lifebytes=life_bytes"
	}

	config_get value "$cfg" rekeypackets ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.rekeypackets=rekey_packets"
	}

	config_get value "$cfg" lifepackets ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.lifepackets=life_packets"
	}
}

migrate_option_remote_values() {
	local cfg="$1"

	local value

	config_get value "$cfg" rekeytime ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.rekeytime=rekey_time"
	}

	config_get value "$cfg" overtime ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.overtime=over_time"
	}

	config_get value "$cfg" dpddelay ""
	[ -z "$value" ] || {
		uci -q rename "ipsec.${cfg}.dpddelay=dpd_delay"
	}
}

migrate_options() {
	config_load ipsec
	config_foreach migrate_option_child_values child
	config_foreach migrate_option_remote_values remote
}

migrate_remote_to_connection() {
	local sections=""
	local section

	collect_remote_type() {
		sections="$sections $1"
	}

	config_load ipsec
	config_foreach collect_remote_type remote

	for section in $sections; do
		uci -q set "ipsec.$section=connection"
	done
}

migrate_mschapv2_secret_value() {
	local cfg="$1"

	local id

	# The 'id' option of the new 'secret' section type is a list element.
	config_get id "$cfg" id ""
	[ -z "$id" ] && return

	uci -q delete "ipsec.${cfg}.id"
	uci -q add_list "ipsec.${cfg}.id=${id}"

	# Rename the section type and set the secret kind
	uci -q set "ipsec.${cfg}=secret"
	uci -q set "ipsec.${cfg}.type=eap"
}

migrate_mschapv2_secrets() {
	config_load ipsec
	config_foreach migrate_mschapv2_secret_value mschapv2_secrets
}

migrate_psk_secret() {
	local conn="$1"

	local authentication_method
	local pre_shared_key
	local local_identifier
	local remote_identifier

	config_get authentication_method "$conn" authentication_method ""
	[ "$authentication_method" = "psk" ] || return

	config_get pre_shared_key "$conn" pre_shared_key ""
	[ -z "$pre_shared_key" ] && return

	uci -q set "ipsec.${conn}_secret=secret"
	uci -q set "ipsec.${conn}_secret.type=ike"
	uci -q set "ipsec.${conn}_secret.secret=$pre_shared_key"

	# Replicate the old inline secret generation: 'id1' is the local
	# identifier, 'id2' the remote identifier. Both are only emitted if
	# the local identifier is set.
	config_get local_identifier "$conn" local_identifier ""
	if [ -n "$local_identifier" ]; then
		uci -q add_list "ipsec.${conn}_secret.id=${local_identifier}"
		config_get remote_identifier "$conn" remote_identifier ""
		[ -n "$remote_identifier" ] &&
			uci -q add_list "ipsec.${conn}_secret.id=${remote_identifier}"
	fi

	uci -q delete "ipsec.${conn}.pre_shared_key"
}

migrate_psk_secrets() {
	config_load ipsec
	config_foreach migrate_psk_secret connection
}

migrate_pubkey_authority() {
	local conn="$1"

	local authentication_method
	local ca_cert

	config_get authentication_method "$conn" authentication_method ""
	[ "$authentication_method" = "pubkey" ] || return

	config_get ca_cert "$conn" ca_cert ""
	[ -z "$ca_cert" ] && return

	uci -q set "ipsec.${conn}_authority=authority"
	uci -q set "ipsec.${conn}_authority.cacert=$ca_cert"

	uci -q delete "ipsec.${conn}.ca_cert"
}

migrate_pubkey_authorities() {
	config_load ipsec
	config_foreach migrate_pubkey_authority connection
}

migrate_connection_local_remote() {
	local conn="$1"

	local authentication_method
	local local_auth_method
	local local_section="${conn}_local"
	local remote_section="${conn}_remote"
	local value
	local cacert

	config_get authentication_method "$conn" authentication_method ""

	# The old connection had a single 'authentication_method' for both the
	# local and the remote side. Move it into the new 'auth' option of the
	# dedicated 'local' and 'remote' sections. The local side of an EAP
	# connection authenticates with a certificate.
	local_auth_method="$authentication_method"
	case "$authentication_method" in
	eap-mschapv2 | eap-tls)
		local_auth_method="pubkey"
		;;
	esac

	uci -q set "ipsec.${local_section}=local"
	uci -q set "ipsec.${local_section}.auth=${local_auth_method:-pubkey}"

	config_get value "$conn" local_identifier ""
	[ -z "$value" ] || uci -q set "ipsec.${local_section}.id=${value}"

	config_get value "$conn" local_cert ""
	[ -z "$value" ] || uci -q set "ipsec.${local_section}.certs=${value}"

	config_get value "$conn" local_key ""
	[ -z "$value" ] || uci -q set "ipsec.${local_section}.key=${value}"

	uci -q add_list "ipsec.${conn}.local=${local_section}"

	uci -q set "ipsec.${remote_section}=remote"
	uci -q set "ipsec.${remote_section}.auth=${authentication_method:-pubkey}"

	config_get value "$conn" remote_identifier ""
	[ -z "$value" ] || uci -q set "ipsec.${remote_section}.id=${value}"

	for cacert in $(config_get "$conn" remote_ca_certs); do
		uci -q add_list "ipsec.${remote_section}.cacerts=${cacert}"
	done

	case "$authentication_method" in
	eap-mschapv2 | eap-tls)
		config_get value "$conn" eap_id ""
		[ -z "$value" ] || uci -q set "ipsec.${remote_section}.eap_id=${value}"
		;;
	esac

	uci -q add_list "ipsec.${conn}.remote=${remote_section}"

	uci -q delete "ipsec.${conn}.authentication_method"
	uci -q delete "ipsec.${conn}.local_identifier"
	uci -q delete "ipsec.${conn}.local_cert"
	uci -q delete "ipsec.${conn}.local_key"
	uci -q delete "ipsec.${conn}.remote_identifier"
	uci -q delete "ipsec.${conn}.remote_ca_certs"
	uci -q delete "ipsec.${conn}.eap_id"
}

migrate_connections_local_remote() {
	config_load ipsec
	config_foreach migrate_connection_local_remote connection
}

migrate_subnet_to_ts() {
	local cfg="$1"
	local old_option="$2"
	local new_option="$3"

	local ts value

	config_get ts "$cfg" "$old_option" ""
	[ -z "$ts" ] && return

	uci -q delete "ipsec.${cfg}.${old_option}"

	for value in $ts; do
		uci -q add_list "ipsec.${cfg}.${new_option}=${value}"
	done
}

migrate_child_subnet_to_ts() {
	config_load ipsec
	config_foreach migrate_subnet_to_ts child local_subnet local_ts
	config_foreach migrate_subnet_to_ts child remote_subnet remote_ts
}

session_uci() {
	local session="$1"
	local method="$2"

	json_add_string config ipsec
	json_add_string ubus_rpc_session "$session"
	ubus call uci "$method" "$(json_dump)" 2>/dev/null
}

# Stage the migrated configuration as pending changes of the given LuCI
# session. rpcd does not expose where it keeps the changes of a session,
# so replace the configuration section by section through its uci ubus
# API.
stage_session() {
	local session="$1"

	local data section sections

	# The migrated configuration including the changes staged above.
	# Do not name anonymous sections, so that they stay anonymous.
	data="$(uci -q -S -N export ipsec)" || return 1

	json_init
	session_uci "$session" revert >/dev/null || return 1

	json_init
	sections="$(session_uci "$session" get)" || return 1
	json_load "$sections"
	json_select values
	json_get_keys sections

	for section in $sections; do
		json_init
		json_add_string section "$section"
		session_uci "$session" delete >/dev/null || return 1
	done

	# Add the sections of the migrated configuration. Run in a subshell,
	# as the config, option and list handlers of /lib/functions.sh are
	# replaced for evaluating the export.
	(
		sec_type=
		cur_list=

		add_section() {
			[ -n "$sec_type" ] || return 0
			[ -n "$cur_list" ] && json_close_array
			json_close_object
			session_uci "$session" add >/dev/null || exit 1
		}

		config() {
			add_section
			sec_type="$1"
			cur_list=
			json_init
			json_add_string type "$1"
			[ -n "$2" ] && json_add_string name "$2"
			json_add_object values
		}

		option() {
			[ -n "$cur_list" ] && json_close_array
			cur_list=
			json_add_string "$1" "$2"
		}

		list() {
			[ "$cur_list" = "$1" ] || {
				[ -n "$cur_list" ] && json_close_array
				cur_list="$1"
				json_add_array "$1"
			}
			json_add_string "" "$2"
		}

		eval "$data"
		add_section
	)
}

main() {
	local session="$1"

	[ -f /etc/config/ipsec ] || {
		echo "File '/etc/config/ipsec' does not exists."
		exit 1
	}

	# Make a backup copy of the old file.
	cp /etc/config/ipsec /etc/config/ipsec_bak

	migrate_ipsec
	migrate_ignore_routing_tables
	migrate_local_nat
	migrate_gateway
	migrate_remote_gateway
	migrate_local_ip
	migrate_local_sourceip
	migrate_to_child
	migrate_debug
	migrate_dpd_action
	migrate_close_action
	migrate_start_action
	migrate_options
	migrate_remote_to_connection
	migrate_mschapv2_secrets
	migrate_psk_secrets
	migrate_pubkey_authorities
	migrate_connections_local_remote
	migrate_child_subnet_to_ts

	# If LuCI provides a session ID, hand the migration over to it as
	# pending changes, allowing to review and apply it in LuCI without an
	# immediate restart.
	[ -n "$session" ] && {
		if ! stage_session "$session"; then
			json_init
			session_uci "$session" revert >/dev/null
			uci -q revert ipsec
			echo "Failed to stage the migrated configuration in the LuCI session." >&2
			exit 1
		fi

		uci -q revert ipsec
	}

	exit 0
}

main "$@"
