#!/bin/sh
# Mosquitto subnet ACL generation script
# Called from init script to generate subnet ACL files

. /lib/functions.sh
. /lib/functions/network.sh

get_interface_subnets() {
	local interface="$1"
	local subnets=""
	local ipaddr
	local prefix
	local ifstatus

	network_flush_cache
	network_get_ipaddr ipaddr "$interface"
	network_get_subnet prefix "$interface"

	if [ -n "$prefix" ]; then
		subnets="$(echo "$prefix" | cut -d'/' -f1,2)"
	elif [ -n "$ipaddr" ]; then
		local netmask
		local cidr
		network_get_netmask netmask "$interface"
		[ -n "$netmask" ] && cidr=$(ipcalc.sh "$ipaddr" "$netmask" | grep NETWORK | cut -d'=' -f2)
		[ -n "$cidr" ] && subnets="$cidr"
	fi

	local ipv6_prefixes
	network_get_prefix_assignments6 ipv6_prefixes "$interface"
	[ -n "$ipv6_prefixes" ] && subnets="$subnets $ipv6_prefixes"

	ifstatus=$(ubus -t 5 call network.interface.${interface} status 2>/dev/null)
	if [ -n "$ifstatus" ]; then
		local targets
		local masks
		targets=$(echo "$ifstatus" | jsonfilter -e '@.route[@.nexthop="::" && @.mask!=0 && @.mask!=128].target')
		masks=$(echo "$ifstatus" | jsonfilter -e '@.route[@.nexthop="::" && @.mask!=0 && @.mask!=128].mask')

		if [ -n "$targets" ] && [ -n "$masks" ]; then
			local target_list="$targets"
			local mask_list="$masks"
			for target in $target_list; do
				local mask=$(echo "$mask_list" | head -1)
				mask_list=$(echo "$mask_list" | tail -n +2)
				[ -n "$target" ] && [ -n "$mask" ] && subnets="$subnets ${target}/${mask}"
			done
		else
			local addrs
			local addr_masks
			addrs=$(echo "$ifstatus" | jsonfilter -e '@["ipv6-address"][@.mask!=128].address')
			addr_masks=$(echo "$ifstatus" | jsonfilter -e '@["ipv6-address"][@.mask!=128].mask')

			if [ -n "$addrs" ] && [ -n "$addr_masks" ]; then
				local addr_list="$addrs"
				local mask_list="$addr_masks"
				for addr in $addr_list; do
					local mask=$(echo "$mask_list" | head -1)
					mask_list=$(echo "$mask_list" | tail -n +2)
					[ -n "$addr" ] && [ -n "$mask" ] && subnets="$subnets ${addr}/${mask}"
				done
			fi
		fi
	fi

	echo "$subnets"
}

generate_subnet_acl() {
	local listener_cfg="$1"
	local acl_file="$2"

	mkdir -p "$(dirname "$acl_file")"
	{
		echo "# Subnet ACL file generated from UCI config"
		echo ""
	} > "$acl_file"

	config_list_foreach "$listener_cfg" subnet_acl_rules process_subnet_rule "$acl_file" || \
		process_listener_subnet_rules "$listener_cfg" "$acl_file"
}

process_subnet_rule() {
	local rule_name="$1"
	local acl_file="$2"
	local enabled
	local username
	local action

	config_get enabled "$rule_name" enabled "1"
	[ "$enabled" = "0" ] && return 0

	config_get username "$rule_name" username
	config_get action "$rule_name" action

	[ -z "$username" -o -z "$action" ] && return 1
	[ "$action" != "allow" -a "$action" != "deny" ] && return 1

	local all_subnets=""
	local has_interfaces=0
	local has_subnets=0
	config_list_foreach "$rule_name" interface process_interface_for_rule "$rule_name" && has_interfaces=1
	config_list_foreach "$rule_name" subnet process_subnet_for_rule "$rule_name" && has_subnets=1

	if [ "$has_interfaces" = "0" -a "$has_subnets" = "0" ]; then
		local interface
		local subnet
		config_get interface "$rule_name" interface
		config_get subnet "$rule_name" subnet

		[ -n "$interface" ] && all_subnets=$(get_interface_subnets "$interface")
		[ -z "$all_subnets" -a -n "$subnet" ] && all_subnets="$subnet"
		[ -z "$all_subnets" ] && return 1
	else
		all_subnets="$RULE_SUBNETS"
		RULE_SUBNETS=""
	fi

	for net in $all_subnets; do
		echo "subnet $action $username $net" >> "$acl_file"
	done
}

process_interface_for_rule() {
	local subnets=$(get_interface_subnets "$1")
	[ -n "$subnets" ] && RULE_SUBNETS="$RULE_SUBNETS $subnets"
}

process_subnet_for_rule() {
	RULE_SUBNETS="$RULE_SUBNETS $1"
}

process_listener_subnet_rules() {
	local listener_cfg="$1"
	local acl_file="$2"
	local username
	local action
	local interface
	local subnet
	local subnets

	config_get username "$listener_cfg" subnet_username
	config_get action "$listener_cfg" subnet_action "allow"
	config_get interface "$listener_cfg" subnet_interface
	config_get subnet "$listener_cfg" subnet_cidr

	[ -z "$username" ] && return 0
	[ -n "$interface" ] && subnets=$(get_interface_subnets "$interface")
	[ -z "$subnets" -a -n "$subnet" ] && subnets="$subnet"

	for net in $subnets; do
		echo "subnet $action $username $net" >> "$acl_file"
	done
}

# Process a single listener's subnet ACL
process_listener_acl() {
	local listener_cfg="$1"
	local subnet_acl_file="$2"
	local ACL_FILES_CHANGED=0

	config_get subnet_acl_rules "$listener_cfg" subnet_acl_rules
	[ -z "$subnet_acl_rules" ] && return 0

	[ -z "$subnet_acl_file" ] && subnet_acl_file="/tmp/mosquitto.${listener_cfg}_subnet.acl"

	temp_acl_file="${subnet_acl_file}.new"
	generate_subnet_acl "$listener_cfg" "$temp_acl_file"

	if [ -f "$subnet_acl_file" ]; then
		old_hash=$(sha256sum "$subnet_acl_file" | cut -d' ' -f1)
		new_hash=$(sha256sum "$temp_acl_file" | cut -d' ' -f1)

		if [ "$old_hash" != "$new_hash" ]; then
			logger -t mosquitto-acl -p daemon.info "Subnet ACL file $subnet_acl_file content changed"
			mv "$temp_acl_file" "$subnet_acl_file"
			ACL_FILES_CHANGED=1
		else
			logger -t mosquitto-acl -p daemon.debug "Subnet ACL file $subnet_acl_file unchanged"
			rm -f "$temp_acl_file"
		fi
	else
		logger -t mosquitto-acl -p daemon.info "Creating new subnet ACL file $subnet_acl_file"
		mv "$temp_acl_file" "$subnet_acl_file"
		ACL_FILES_CHANGED=1
	fi

	return $ACL_FILES_CHANGED
}

# Main function: regenerate ACL files and check for changes
generate_all_subnet_acls() {
	local ACL_FILES_CHANGED=0

	config_load mosquitto

	for listener_cfg in $(uci show mosquitto 2>/dev/null | grep "=listener" | cut -d. -f2 | cut -d= -f1); do
		if process_listener_acl "$listener_cfg"; then
			ACL_FILES_CHANGED=1
		fi
	done

	return $ACL_FILES_CHANGED
}

# If called with listener_cfg argument, process only that listener
# Otherwise process all listeners
if [ -n "$1" ]; then
	config_load mosquitto
	process_listener_acl "$1" "$2"
	exit $?
else
	generate_all_subnet_acls
	exit $?
fi
