#!/bin/sh /etc/rc.common
# Copyright 2019-2023 Stan Grishin (stangri@melmac.ca)
# shellcheck disable=SC1091,SC3043,SC3060

# shellcheck disable=SC2034
START=90
# shellcheck disable=SC2034
STOP=15
# shellcheck disable=SC2034
USE_PROCD=1

if type extra_command 1>/dev/null 2>&1; then
	extra_command 'version' 'Show version information'
else
# shellcheck disable=SC2034
	EXTRA_COMMANDS='version'
fi

readonly PKG_VERSION='dev-test'
readonly packageName='https-dns-proxy'
readonly serviceName="$packageName $PKG_VERSION"
readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m'
readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m'
readonly PROG=/usr/sbin/https-dns-proxy
readonly BOOTSTRAP_CF='1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001'
readonly BOOTSTRAP_GOOGLE='8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844'
readonly DEFAULT_BOOTSTRAP="${BOOTSTRAP_CF},${BOOTSTRAP_GOOGLE}"
readonly canaryDomainsMozilla='use-application-dns.net'
readonly canaryDomainsiCloud='mask.icloud.com mask-h2.icloud.com'

on_boot_trigger=

dnsmasq_restart() { [ -x /etc/init.d/dnsmasq ] || return 1; /etc/init.d/dnsmasq restart >/dev/null 2>&1; }
is_fw4_restart_needed() { [ "$(uci_get "$packageName" 'config' 'force_dns' '1')" = '1' ]; }
is_mac_address() { expr "$1" : '[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]$' >/dev/null; }
is_ipv4() { expr "$1" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' >/dev/null; }
is_ipv6() { ! is_mac_address "$1" && str_contains "$1" ":"; }
is_resolver_working() { resolveip -t 3 one.one.one.one >/dev/null 2>&1; }
output() {
	local msg memmsg logmsg
	local sharedMemoryOutput="/dev/shm/$packageName-output"
	[ -t 1 ] && printf "%b" "$@"
	msg="${1//$serviceName /service }";
	if [ "$(printf "%b" "$msg" | wc -l)" -gt 0 ]; then
		[ -s "$sharedMemoryOutput" ] && memmsg="$(cat "$sharedMemoryOutput")"
		logmsg="$(printf "%b" "${memmsg}${msg}" | sed 's/\x1b\[[0-9;]*m//g')"
		logger -t "$packageName" "$(printf "%b" "$logmsg")"
		rm -f "$sharedMemoryOutput"
	else
		printf "%b" "$msg" >> "$sharedMemoryOutput"
	fi
}
output_ok() { output "$_OK_"; }
output_okn() { output "${_OK_}\\n"; }
output_fail() { output "$_FAIL_"; }
output_failn() { output "${_FAIL_}\\n"; }
str_contains() { [ -n "$1" ] &&[ -n "$2" ] && [ "${1//$2}" != "$1" ]; }
uci_add_list_if_new() {
	local PACKAGE="$1"
	local CONFIG="$2"
	local OPTION="$3"
	local VALUE="$4"
	local i
	[ -n "$PACKAGE" ] && [ -n "$CONFIG" ] && [ -n "$OPTION" ] && [ -n "$VALUE" ] || return 1
	for i in $(uci_get "$PACKAGE" "$CONFIG" "$OPTION"); do
		[ "$i" = "$VALUE" ] && return 0
	done
	uci_add_list "$PACKAGE" "$CONFIG" "$OPTION" "$VALUE"
}
uci_changes() {
	local PACKAGE="$1"
	local CONFIG="$2"
	local OPTION="$3"
	/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} changes "$PACKAGE${CONFIG:+.$CONFIG}${OPTION:+.$OPTION}"
}
version() { echo "$PKG_VERSION"; }

xappend() { PROG_param="$PROG_param $1"; }

append_bool() {
	local section="$1"
	local option="$2"
	local value="$3"
	local default="${4:-0}"
	local _loctmp
	config_get_bool _loctmp "$section" "$option" "$default"
	[ "$_loctmp" -ne 0 ] && xappend "$value"
}

append_parm() {
	local section="$1"
	local option="$2"
	local switch="$3"
	local default="$4"
	local _loctmp
	config_get _loctmp "$section" "$option" "$default"
	[ -n "$_loctmp" ] && xappend "$switch $_loctmp"
}

append_counter() {
	local section="$1"
	local option="$2"
	local switch="$3"
	local default="${4:-0}"
	local _loctmp i
	config_get _loctmp "$section" "$option" "$default"
# shellcheck disable=SC2086,SC2154
	for i in $(seq 1 $_loctmp); do
		xappend '-v'
	done
}

append_bootstrap() {
	local section="$1"
	local option="$2"
	local switch="$3"
	local default="$4"
	local _old_ifs="$IFS"
	local _loctmp _newtmp i
	config_get _loctmp "$section" "$option" "$default"
	[ -z "$_loctmp" ] && return 0
	IFS=" ,"
	for i in $_loctmp; do
		if { [ "$ipv6_resolvers_only" -eq 0 ] && is_ipv4 "$i"; } || \
			{ [ "$ipv6_resolvers_only" -ne 0 ] && is_ipv6 "$i"; }; then
			[ -z "$_newtmp" ] && _newtmp="$i" || _newtmp="${_newtmp},${i}"
		fi
	done
	IFS="$_old_ifs"
	[ -n "$_newtmp" ] && xappend "$switch $_newtmp"
	[ "$ipv6_resolvers_only" -eq 0 ] && xappend '-4'
}

boot() {
	ubus -t 30 wait_for network.interface 2>/dev/null
	on_boot_trigger=1
	rc_procd start_service 'on_boot' && service_started 'on_boot'
	is_resolver_working || { rc_procd stop_service 'on_failed_health_check' && service_stopped 'on_failed_health_check'; }
}

start_instance() {
	local cfg="$1" param="$2"
	local PROG_param 
	local listen_addr listen_port ipv6_resolvers_only p url iface

	config_get url "$cfg" 'resolver_url'
	config_get_bool ipv6_resolvers_only "$cfg" 'use_ipv6_resolvers_only' '0'
	append_parm "$cfg" 'resolver_url' '-r'
	append_parm "$cfg" 'listen_addr' '-a' '127.0.0.1'
	append_parm "$cfg" 'listen_port' '-p' "$port"
	append_parm "$cfg" 'dscp_codepoint' '-c'
	append_bootstrap "$cfg" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP"
	append_parm "$cfg" 'user' '-u' 'nobody'
	append_parm "$cfg" 'group' '-g' 'nogroup'
	append_parm "$cfg" 'ca_certs_file' '-C'
	append_parm "$cfg" 'polling_interval' '-i'
	append_parm "$cfg" 'proxy_server' '-t'
	append_parm "$cfg" 'logfile' '-l'
	append_bool "$cfg" 'use_http1' '-x'
	append_counter "$cfg" 'verbosity' '-v' '0'

	procd_open_instance
# shellcheck disable=SC2086
	procd_set_param command $PROG $PROG_param
	procd_set_param stderr 1
	procd_set_param stdout 1
	procd_set_param respawn
	procd_open_data
	json_add_object mdns
		procd_add_mdns_service "$packageName" 'udp' "$port" "DNS over HTTPS proxy"
	json_close_object
	if [ "$force_dns" -ne '0' ]; then
		json_add_array firewall
		for iface in $procd_fw_src_interfaces; do
			for p in $force_dns_port; do
				if netstat -tuln | grep 'LISTEN' | grep ":${p}" >/dev/null 2>&1 || [ "$p" = '53' ]; then
					json_add_object ''
					json_add_string type redirect
					json_add_string target DNAT
					json_add_string src "$iface"
					json_add_string proto 'tcp udp'
					json_add_string src_dport "$p"
					json_add_string dest_port "$p"
					json_add_string family any
					json_add_boolean reflection 0
					json_close_object
				else
					json_add_object ''
					json_add_string type rule
					json_add_string src "$iface"
					json_add_string dest '*'
					json_add_string proto 'tcp udp'
					json_add_string dest_port "$p"
					json_add_string target REJECT
					json_close_object
				fi
			done
		done
		json_close_array
	fi
	procd_close_data
	procd_close_instance

# shellcheck disable=SC2181
	if [ "$?" -eq 0 ]; then
		config_get listen_addr "$cfg" 'listen_addr' '127.0.0.1'
		config_get listen_port "$cfg" 'listen_port' "$port"
		if [ "$dnsmasq_config_update" = '*' ]; then
			config_load 'dhcp'
			config_foreach dnsmasq_doh_server 'dnsmasq' 'add' "${listen_addr}" "${listen_port}"
		elif [ -n "$dnsmasq_config_update" ]; then
			for i in $dnsmasq_config_update; do
				if [ -n "$(uci_get 'dhcp' "@dnsmasq[$i]")" ]; then
					dnsmasq_doh_server "@dnsmasq[$i]" 'add' "${listen_addr}" "${listen_port}"
				elif [ -n "$(uci_get 'dhcp' "$i")" ]; then
					dnsmasq_doh_server "${i}" 'add' "${listen_addr}" "${listen_port}"
				fi
			done
		fi
		output_ok
		port="$((port+1))"
		force_dns='0'
	else
		output_fail
	fi
}

start_service() {
	local param="$1"
	local canaryDomains canary_domains_icloud canary_domains_mozilla
	local dnsmasq_config_update force_dns force_dns_port 
	local procd_fw_src_interfaces

	local port=5053
	output "Starting $serviceName instances ${param:+$param }"
	config_load "$packageName"
	config_get_bool canary_domains_icloud  'config' 'canary_domains_icloud' '1'
	config_get_bool canary_domains_mozilla 'config' 'canary_domains_mozilla' '1'
	config_get_bool force_dns          'config' 'force_dns' '1'
	config_get dnsmasq_config_update   'config' 'dnsmasq_config_update' '*'
	config_get force_dns_port          'config' 'force_dns_port' '53 853'
	config_get procd_fw_src_interfaces 'config' 'procd_fw_src_interfaces' 'lan'
	if [ "$canary_domains_icloud" -ne 0 ]; then
		canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsiCloud}"
	fi
	if [ "$canary_domains_mozilla" -ne 0 ]; then
		canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsMozilla}"
	fi
	dhcp_backup 'create'
	config_load "$packageName"
	config_foreach start_instance "$packageName" "$param"
	output "\\n"
	if [ -n "$(uci_changes dhcp)" ]; then
		output "Updating dnsmasq config "
		if uci_commit 'dhcp'; then
			output_okn
			param='on_config_update'
		else
			output_failn
		fi
	fi
	case "$param" in
		on_boot|on_config_update|on_hotplug)
			output "Restarting dnsmasq ${param:+$param }"
			if dnsmasq_restart; then
				output_okn
			else
				output_failn
			fi
		;;
	esac
}

stop_service() {
	local param="$1"
	local canaryDomains canary_domains_icloud canary_domains_mozilla
	local dnsmasq_config_update
	local s=0
	output "Stopping $serviceName ${param:+$param }"
	config_load "$packageName"
	config_get dnsmasq_config_update        'config' 'dnsmasq_config_update' '*'
	config_get_bool canary_domains_icloud   'config' 'canary_domains_icloud' '1'
	config_get_bool canary_domains_mozilla  'config' 'canary_domains_mozilla' '1'
	if [ "$canary_domains_icloud" -ne 0 ]; then
		canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsiCloud}"
	fi
	if [ "$canary_domains_mozilla" -ne 0 ]; then
		canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsMozilla}"
	fi
	dhcp_backup 'restore'
	if [ -n "$(uci_changes dhcp)" ]; then
		uci_commit 'dhcp'
		dnsmasq_restart || s=1
	fi
# shellcheck disable=SC2015
	[ "$s" = '0' ] && output_okn || output_failn
}

service_triggers() {
	local wan wan6 i
	local procd_trigger_wan6
	if [ "$on_boot_trigger" = '1' ]; then
		output "Setting $serviceName raw_trigger for 'interface.*.up'"
		procd_add_raw_trigger "interface.*.up" 3000 "/etc/init.d/${packageName}" restart 'on_interface_up'
		output_okn
	else
		config_load "$packageName"
		config_get_bool procd_trigger_wan6 'config' 'procd_trigger_wan6' '0'
		. /lib/functions/network.sh
		network_flush_cache
		network_find_wan wan
		wan="${wan:-wan}"
		if [ "$procd_trigger_wan6" -ne 0 ]; then
			network_find_wan6 wan6
			wan6="${wan6:-wan6}"
		fi
		for i in $wan $wan6; do
			procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" restart 'on_interface_trigger'
		done
	fi
	procd_add_config_trigger "config.change" "$packageName" "/etc/init.d/${packageName}" reload 'on_config_change'
}

service_started() { is_fw4_restart_needed && procd_set_config_changed firewall; }
service_stopped() { is_fw4_restart_needed && procd_set_config_changed firewall; }
restart() { procd_send_signal "$packageName"; rc_procd start_service "$*"; }

dnsmasq_doh_server() {
	local cfg="$1" param="$2" address="${3:-127.0.0.1}" port="$4" i
	case "$param" in
		add)
			if [ "$force_dns" -ne 0 ]; then
				for i in $canaryDomains; do
					uci_add_list_if_new 'dhcp' "$cfg" 'server' "/${i}/"
				done
			fi
			case $address in
				0.0.0.0|::ffff:0.0.0.0) address='127.0.0.1';;
				::) address='::1';;
			esac
			uci_add_list_if_new 'dhcp' "$cfg" 'server' "${address}#${port}"
			uci_add_list_if_new 'dhcp' "$cfg" 'doh_server' "${address}#${port}"
		;;
		remove)
			for i in $(uci_get 'dhcp' "$cfg" 'doh_server'); do
				uci_remove_list 'dhcp' "$cfg" 'server' "$i"
				uci_remove_list 'dhcp' "$cfg" 'doh_server' "$i"
			done
			for i in $canaryDomains; do
				uci_remove_list 'dhcp' "$cfg" 'server' "/${i}/"
				uci_remove_list 'dhcp' "$cfg" 'doh_server' "/${i}/"
			done
		;;
	esac
}

dnsmasq_create_server_backup() {
	local cfg="$1" i
	[ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1
	if [ -z "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" ]; then
		if [ -z "$(uci_get 'dhcp' "$cfg" 'noresolv')" ]; then
			uci_set 'dhcp' "$cfg" 'doh_backup_noresolv' '-1'
		else
			uci_set 'dhcp' "$cfg" 'doh_backup_noresolv' "$(uci_get 'dhcp' "$cfg" noresolv)"
		fi
		uci_set 'dhcp' "$cfg" 'noresolv' 1
	fi
	if [ -z "$(uci_get 'dhcp' "$cfg" 'doh_backup_server')" ]; then
		if [ -z "$(uci_get 'dhcp' "$cfg" 'server')" ]; then
			uci_add_list 'dhcp' "$cfg" 'doh_backup_server' ""
		fi
		for i in $(uci_get 'dhcp' "$cfg" 'server'); do
			uci_add_list 'dhcp' "$cfg" 'doh_backup_server' "$i"
			if [ "$i" = "$(echo "$i" | tr -d /\#)" ]; then
				uci_remove_list 'dhcp' "$cfg" 'server' "$i"
			fi
		done
	fi
	return 0
}

dnsmasq_restore_server_backup() {
	local cfg="$1" i
	[ -n "$(uci_get 'dhcp' "$cfg")" ] || return 0
	if [ -n "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" ]; then
		if [ "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" = "-1" ]; then
			uci_remove 'dhcp' "$cfg" 'noresolv'
		else
			uci_set 'dhcp' "$cfg" 'noresolv' "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')"
		fi
		uci_remove 'dhcp' "$cfg" 'doh_backup_noresolv'
	fi
	if uci_get 'dhcp' "$cfg" 'doh_backup_server' >/dev/null 2>&1; then
		dnsmasq_doh_server "$cfg" 'remove'
		for i in $(uci_get 'dhcp' "$cfg" 'doh_backup_server'); do
			uci_add_list_if_new 'dhcp' "$cfg" 'server' "$i"
		done
		uci_remove 'dhcp' "$cfg" 'doh_backup_server'
	fi
}

dhcp_backup() {
	local i
	config_load 'dhcp'
	case "$1" in
		create)
			if [ "$dnsmasq_config_update" = "*" ]; then
				config_foreach dnsmasq_create_server_backup 'dnsmasq'
			elif [ -n "$dnsmasq_config_update" ]; then
				for i in $dnsmasq_config_update; do
					if [ -n "$(uci_get 'dhcp' "@dnsmasq[$i]")" ]; then
						dnsmasq_create_server_backup "@dnsmasq[$i]"
					elif [ -n "$(uci_get 'dhcp' "$i")" ]; then
						dnsmasq_create_server_backup "$i"
					fi
				done
			fi
			;;
		restore)
			config_foreach dnsmasq_restore_server_backup 'dnsmasq'
			;;
	esac
}
