#!/bin/sh /etc/rc.common
# Copyright (c) 2012-2021, Matthias Schiffer <mschiffer@universe-factory.net>

set -o pipefail

START=95

USE_PROCD=1


# Backwards compatiblity with OpenWrt 19.07 and older
if ! command -v extra_command >/dev/null; then
	EXTRA_HELP='
'
	EXTRA_COMMANDS=''

	extra_command() {
		local cmd="$1"
		local help="$2"

		local extra="$(printf '\t%-16s%s' "${cmd}" "${help}")"
		EXTRA_HELP="${EXTRA_HELP}${extra}
"
		EXTRA_COMMANDS="${EXTRA_COMMANDS} ${cmd}"
	}
fi

extra_command 'show_key' 'show the public key of an instance'
extra_command 'generate_key' 'generate key for an instance and store in UCI'


LIST_SEP="
"
TMP_FASTD=/tmp/fastd
FASTD_COMMAND=/usr/bin/fastd


section_enabled() {
	config_get_bool enabled "$1" 'enabled' 0
	[ $enabled -gt 0 ]
}

error() {
	echo "${initscript}:" "$@" 1>&2
}

instance_usage() {
	echo "Usage: ${initscript} $1 instance" 1>&2
	exit 1
}

get_key_instance() {
	local s="$1"

	config_get secret "$s" secret
	if [ "$secret" = 'generate' ]; then
		secret=`"$FASTD_COMMAND" --generate-key --machine-readable`
		uci -q set fastd."$s".secret="$secret" && uci -q commit fastd
	fi

	echo "$secret"
}


escape_string() {
	local t=${1//\\/\\\\}
	echo -n "\"${t//\"/\\\"}\""
}

guard_value() {
	local t=${1//[^-a-z0-9\[\].:]/}
	echo -n "$t"
}

guard_quotes() {
	local t=${1//[^-a-zA-Z0-9\[\].:\"% ]/}
	local quotes=${t//[^\"]/}
	if [ "${#quotes}" = 0 -o "${#quotes}" = 2 ]; then
		echo -n "$t"
	fi
}

yes_no() {
	case "$1" in
		1|yes|on|true|enabled)
			echo -n yes;;
		0|no|off|false|disabled)
			echo -n no;;
		*)
			guard_value "$1";;
	esac
}

config_string_bind='bind $(guard_quotes "$value");'
config_string_config='include $(escape_string "$value");'
config_string_config_peer='include peer $(escape_string "$value");'
config_string_config_peer_dir='include peers from $(escape_string "$value");'
config_string_drop_capabilities='drop capabilities $(yes_no "$value");'
config_string_forward='forward $(yes_no "$value");'
config_string_group='group $(escape_string "$value");'
config_string_hide_ip_addresses='hide ip addresses $(yes_no "$value");'
config_string_hide_mac_addresses='hide mac addresses $(yes_no "$value");'
config_string_interface='interface $(escape_string "$value");'
config_string_method='method $(escape_string "$value");'
config_string_mode='mode $(guard_value "$value");'
config_string_mtu='mtu $(guard_value "$value");'
config_string_offload_l2tp='offload l2tp $(yes_no "$value");'
config_string_peer_limit='peer limit $(guard_value "$value");'
config_string_packet_mark='packet mark $(guard_value "$value");'
config_string_persist_interface='persist interface $(yes_no "$value");'
config_string_status_socket='status socket $(escape_string "$value");'
config_string_syslog_level='log to syslog level $(guard_value "$value");'
config_string_user='user $(escape_string "$value");'

config_string_on_pre_up='on pre-up $(escape_string "$value");'
config_string_on_post_down='on post-down $(escape_string "$value");'
config_string_on_up='on up $(escape_string "$value");'
config_string_on_down='on down $(escape_string "$value");'
config_string_on_connect='on connect $(escape_string "$value");'
config_string_on_establish='on establish $(escape_string "$value");'
config_string_on_disestablish='on disestablish $(escape_string "$value");'
config_string_on_verify='on verify $(escape_string "$value");'

config_string_peer='peer $(escape_string "$value") {'
config_string_peer_group='peer group $(escape_string "$value") {'

peer_string_float='float $(yes_no "$value");'
peer_string_interface='interface $(escape_string "$value");'
peer_string_key='key $(escape_string "$value");'
peer_string_mtu='mtu $(guard_value "$value");'
peer_string_remote='remote $(guard_quotes "$value");'

generate_option() {
	local __string=$(eval echo \"\$$2\")
	local value="$1";
	eval echo "\"$__string\""
}

append_option() {
	local v; local len; local s="$1"; local prefix="$2"; local p="$3"

	config_get len "$s" "${p}_LENGTH"

	if [ -z "$len" ]; then
		config_get v "$s" "$p"
		[ -n "$v" ] && generate_option "$v" "${prefix}_string_${p}"
	else
		config_list_foreach "$s" "$p" generate_option "${prefix}_string_${p}"
	fi
}

append_options() {
	local p; local s="$1"; local prefix="$2"; shift; shift
	for p in $*; do
		append_option "$s" "$prefix" "$p"
	done
}


generate_config_secret() {
	echo "secret $(escape_string "$1");"
}


generate_peer_config() {
	local peer="$1"

	# These options are deprecated
	config_get address "$peer" address
	config_get hostname "$peer" hostname
	config_get address_family "$peer" address_family
	config_get port "$peer" port

	if [ "$address" -o "$hostname" ]; then
		if [ -z "$port" ]; then
			error "peer $peer: address or hostname, but no port given"
			return 1
		fi

		if [ "$hostname" ]; then
			generate_option peer_string_remote "$address_family \"$hostname\" port $port"
		fi

		if [ "$address" ]; then
			generate_option peer_string_remote "$address port $port"
		fi
	fi

	append_options "$peer" peer \
		float interface key mtu remote
}

create_peer_config() {
	local peer="$1"; local net="$2"; local group="$3"; local path="$4"

	config_get peer_net "$peer" net
	config_get peer_group "$peer" group
	[ "$group" = "$peer_group" ] || return 0

	if [ "$net" != "$peer_net" ]; then
		[ -z "$group" ] || error "warning: the peer group of peer '$peer' doesn't match its net, the peer will be ignored"
		return 0
	fi

	section_enabled "$peer" || return 0

	generate_peer_config "$peer" >"$path/$peer"
}

update_peer_group() {
	local net="$1"; local group_dir="$2"; local group="$3"; local update_only="$4"
	local path="$TMP_FASTD/fastd.$net.peers/$group_dir"

	rm -rf "$path"
	mkdir -p "$path"

	config_foreach create_peer_config 'peer' "$net" "$group" "$path"

	if [ -z "$update_only" ]; then
		generate_option "$path" config_string_config_peer_dir
	fi

	config_foreach generate_peer_group_config 'peer_group' "$net" "$group_dir" "$update_only" "$group"
}

generate_peer_group_config() {
	local group="$1"; local net="$2"; local group_dir="$3%$group"; local update_only="$4"; local parent="$5"

	config_get group_net "$group" net
	config_get group_parent "$group" parent
	[ "$parent" = "$group_parent" ] || return 0

	if [ "$net" != "$group_net" ]; then
		[ -z "$parent" ] || error "warning: the parent of peer group '$group' doesn't match its net, the peer group will be ignored"
		return 0
	fi

	section_enabled "$group" || return 0

	if [ -z "$update_only" ]; then
		generate_option "$group" config_string_peer_group
		append_options "$group" config \
			config config_peer config_peer_dir method peer_limit \
			on_up on_down on_connect on_establish on_disestablish on_verify
	fi

	update_peer_group "$net" "$group_dir" "$group" "$update_only"

	if [ -z "$update_only" ]; then
		echo '}'
	fi
}

update_peer_groups() {
	local net="$1"; local update_only="$2"

	update_peer_group "$net" 'peers' '' "$update_only"
}

generate_config() {
	local s="$1"

	generate_option 'info' config_string_syslog_level

	append_options "$s" config \
		bind config config_peer config_peer_dir drop_capabilities method syslog_level mode interface mtu peer_limit \
		user group status_socket forward hide_ip_addresses hide_mac_addresses offload_l2tp packet_mark \
		persist_interface on_pre_up on_post_down on_up on_down on_connect on_establish on_disestablish on_verify

	config_get mode "$s" mode

	update_peer_groups "$s"
}


generate_key_instance() {
	local s="$1"

	config_get secret "$s" secret
	if [ -z "$secret" -o "$secret" = 'generate' ]; then
		secret=`fastd --generate-key --machine-readable`
		uci -q set fastd."$s".secret="$secret" && uci -q commit fastd
	fi

	generate_config_secret "$secret" | "$FASTD_COMMAND" --config - --show-key --machine-readable
}

show_key_instance() {
	local s="$1"

	local secret=`get_key_instance "$s"`
	if [ -z "$secret" ]; then
		error "$s: secret is not set"
		return 1
	fi

	generate_config_secret "$secret" | "$FASTD_COMMAND" --config - --show-key --machine-readable
}

start_instance() {
	local s="$1"

	section_enabled "$s" || return 1

	config_get mode "$s" mode
	if [ -z "$mode" ]; then
		error "$s: mode is not set"
		return 1
	fi

	local secret=`get_key_instance "$s"`
	if [ -z "$secret" ]; then
		error "$s: secret is not set"
		return 1
	fi

	local cfgfile="$TMP_FASTD/fastd.$s.conf"
	local cfgfiletmp="${cfgfile}.$$"

	rm -f "$cfgfiletmp"
	mkdir -p "$TMP_FASTD"
	touch "$cfgfiletmp"
	chmod 600 "$cfgfiletmp"
	generate_config_secret "$secret" > "$cfgfiletmp" || return 1
	generate_config "$s" >> "$cfgfiletmp" || return 1
	mv -f "$cfgfiletmp" "$cfgfile"

	procd_open_instance "$s"

	procd_set_param command "$FASTD_COMMAND" --config "$cfgfile"
	procd_set_param file "$cfgfile"
	procd_set_param respawn

	procd_close_instance
}

reload_instance() {
	local s="$1"

	update_peer_groups "$s" true

	rc_procd start_service "$s"
	procd_send_signal fastd "$s"
}

start_service() {
	local instance="$1"
	local exists

	config_load 'fastd'
	if [ -z "$instance" ]; then
		config_foreach start_instance 'fastd'
	else
		config_get exists "$instance" 'TYPE'
		if [ "$exists" != 'fastd' ]; then
			echo >&2 "Invalid instance name '$instance'"
			return 1
		fi

		start_instance "$instance"
	fi

	return 0
}

reload_service() {
	local instance="$1"
	local exists

	config_load 'fastd'
	if [ -z "$instance" ]; then
		config_foreach reload_instance 'fastd'
	else
		config_get exists "$instance" 'TYPE'
		if [ "$exists" != 'fastd' ]; then
			echo >&2 "Invalid instance name '$instance'"
			return 1
		fi

		reload_instance "$instance"
	fi

	return 0
}

show_key() {
	[ $# -eq 1 ] || instance_usage 'show_key'

	local instance="$1"
	local exists

	config_load 'fastd'
	config_get exists "$instance" 'TYPE'
	if [ "$exists" != 'fastd' ]; then
		echo >&2 "Invalid instance name '$instance'"
		return 1
	fi

	show_key_instance "$instance"
}

generate_key() {
	[ $# -eq 1 ] || instance_usage 'generate_key'

	local instance="$1"
	local exists

	config_load 'fastd'
	config_get exists "$instance" 'TYPE'
	if [ "$exists" != 'fastd' ]; then
		echo >&2 "Invalid instance name '$instance'"
		return 1
	fi

	generate_key_instance "$instance"
}
