#!/bin/sh /etc/rc.common
# Copyright © 2012-2026 OpenWrt.org

# In recent (relevant) versions of shellcheck busybox is a valid shell type
# shellcheck shell=busybox

# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#

# shellcheck disable=SC2034
START=87
STOP=23
USE_PROCD=1

# Default is hardcoded in other program's configuration, as well as compile
# time options for NUT CGI executable
# /var is typically a tmpfs on OpenWrt and is not persisted across reboots
UPSCGI_CONF_DIR=/var/etc/nut

# Ephemeral configuration for configuring NUT hosts using CGI and upsset
# (persisted using UCI)
UPSCGI_UPSSET_CONF="${UPSCGI_CONF_DIR}/upsset.conf"
# Ephemeral configuration for NUT hosts to display using CGI (persisted
# using UCI)
UPSCGI_HOSTS_CONF="${UPSCGI_CONF_DIR}/hosts.conf"

# IPKG_INSTROOT is intentionally only set when building an image and
# is intentionally empty on a live OpenWrt device

# Shellcheck source paths intentionally point to the location of files of
# the scripts in the development environment (where shellcheck is used), not
# on the live OpenWrt device

# shellcheck source=net/nut/files/functions.sh.functions
. "${IPKG_INSTROOT}"/lib/functions.sh || {
	# Before our sourcing error logging definitions are sourced
	logger -t nut-cgi[$$] "Unable to source 'functions.sh'"
	exit 1
}

# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even
# though it is correctly POSIX compliant
# shellcheck disable=SC1094
# shellcheck source=net/nut/files/nut-common.sh.functions
. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || {
	# Before our sourcing error logging definitions are sourced
	logger -t nut-cgi[$$] "Failed to load nut-common.sh"
	exit 1
}

restore_umask_on_exit

ensure_conf_dir_exists() {
	# If the directory already exists we presume the creator of the directory
	# has set the correct permissions.
	if [ ! -d "$UPSCGI_CONF_DIR" ]; then
		# Make directory for the UPS CGI configuration files, if not present
		# Set permission on dir we create for UPS CGI configuration files
		# All NUT configuration files are here, some which must be readable
		# by other binaries with different group and ownership (therefore must
		# be world readable)
		# NOTE: Ensure sensitive files have more restrictive permissions
		umask 022
		if ! mkdir -p "$UPSCGI_CONF_DIR"; then
			restore_umask
			return 1
		fi
		restore_umask
	fi
	return 0
}

set_upsset_conf_content() {
	local upsset_conf_content="$1"

	umask 133
	if ! printf "%s" "$upsset_conf_content" >"$UPSCGI_UPSSET_CONF"; then
		restore_umask
		log_error "failed to configure upsset; disabling NUT CGI" nut-cgi nut-cgi
		return 1
	fi
	restore_umask
	return 0
}

disable_upsset() {
	ensure_conf_dir_exists || {
		log_error "failed to disable upsset due to invalid directory; disabling NUT CGI" nut-cgi nut-cgi
		return 1
	}
	# Use embedded content in preference to previous symlink method to avoid an
	# attack vector via modified symlink target
	# Prefer variable to heredoc due to complication with if, then around a
	# heredoc when using shellcheck and shfmt together.
	set_upsset_conf_content "# Network UPS Tools - upsset.conf sample file for OpenWrt
#
# This file is provided to ensure that you do not expose your upsd server
# to the world upon installing the CGI programs.  Specifically, it keeps
# the upsset.cgi program from running until you have assured it that you
# have secured your web server's CGI directory.
#
# NOTE: Contents of this file should be pure ASCII (character codes
# not in range would be ignored with a warning message).
#
###
### I_HAVE_SECURED_MY_CGI_DIRECTORY
###
" || return 1
	return 0
}

configure_nut_upscgi_upsset() {
	local cfg="$1"
	local enable

	config_get_bool enable "$cfg" enable 0

	# Use embedded content in preference to previous symlink method to avoid an
	# attack vector via modified symlink target
	# Prefer variable to heredoc due to complication with if..then around a
	# heredoc when using shellcheck and shfmt together.
	[ "$enable" = "1" ] || {
		# Logs an error in the disable_upsset function call if it fails
		disable_upsset || return 1
		# not enabled (disabled) is a valid configuration choice
		return 0
	}
	if ensure_conf_dir_exists; then
		# Leading newline is intentional
		set_upsset_conf_content "
I_HAVE_SECURED_MY_CGI_DIRECTORY
" || {
			upscgi_upsset_failed="true"
			return 1
		}
		return 0
	else
		upscgi_upsset_failed="true"
		# Logs an error in the disable_upsset function call if it fails
		disable_upsset
		return 1
	fi
	# No return 0 here as it would be dead code, and shellcheck would complain
	# about the dead code.
}

log_validation_error() {
	local var="$1"
	local ups="$2"
	log_error "Unsafe characters in '$var' for '$ups'; skipping" nut-cgi nut-cgi
}

validate_host() {
	local ups="$1"
	local upsname="$2"
	local hostname="$3"
	local port="$4"
	local displayname="$5"

	# We do not unset upscgi_hosts_exists on error as we do not want to block
	# monitoring hosts with the needed configuration as a result of those that
	# do not
	for var in upsname hostname port displayname; do
		case "$var" in
		upsname)
			if ! check_safe_uci_name "$upsname"; then
				log_validation_error "$var" "$ups"
				return 1
			fi
			;;
		hostname)
			if ! check_safe_hostname_or_ip "$hostname"; then
				log_validation_error "$var" "$ups"
				return 1
			fi
			;;
		displayname)
			# We accept a minimally useful set of characters for a display name
			# We specifically omit characters like $ % " and '
			# Some AI code reviewers get confused by \(\) and \space in the case
			# pattern, but this is correct for busybox ash in modern OpenWrt
			# and does not allow \ through the validation.
			case "$displayname" in
			*[!a-zA-Z0-9+!\(\)\ _=:.,-]*)
				log_validation_error "$var" "$ups"
				return 1
				;;
			esac
			;;
		port)
			if ! check_port "$port"; then
				log_validation_error "$var" "$ups"
				return 1
			fi
			;;
		esac
	done
	return 0
}

# Must be called from service_reload
nut_upscgi_add_host() {
	local ups="$1"
	local upsname hostname port system displayname

	config_get upsname "$ups" upsname "$ups"
	config_get hostname "$ups" hostname localhost
	config_get port "$ups" port
	config_get displayname "$ups" displayname "$ups"

	# We do not error exit as that would abort processing of other hosts
	validate_host "$ups" "$upsname" "$hostname" "$port" "$displayname" || return

	system="${upsname}@$hostname"
	if [ -n "$port" ]; then
		system="$system:$port"
	fi

	if ! printf "%s\n" "MONITOR $system \"$displayname\"" >>"${UPSCGI_HOSTS_CONF}.new"; then
		log_error "Failed to add MONITOR to CGI host.conf for '$ups'" nut-cgi nut-cgi
		# Failed printf could result in inconsistent UPSCGI_HOSTS_CONF.new state, so recreate it as
		# empty so later hosts can still potentially be properly configured
		if rm -f "${UPSCGI_HOSTS_CONF}.new"; then
			return
		else
			# Cascade failure. Bail.
			log_error_exit "Cascade failure in nut_upscgi_add_host. Bailing." nut-cgi nut-cgi
		fi
	fi
	upscgi_hosts_exists="true"
	return 0
}

service_reload() {
	# Callers should not need to see the variables below outside this function
	# Conversely, functions called by this function are able to see and set
	# the variables below, which act as 'pseudo-globals' to called functions.
	local upscgi_hosts_exists="false"
	local upscgi_upsset_failed="false"

	config_load nut_cgi || {
		log_config_load_error "nut_cgi" "nut-cgi" "nut-cgi"
		exit 1
	}

	# NUT configuration does not exist if NUT CGI cannot be configured, so
	# stop service and bailout.
	ensure_conf_dir_exists || {
		service_stop
		return 1
	}

	rm -f "${UPSCGI_HOSTS_CONF}.new"
	umask 133
	touch "${UPSCGI_HOSTS_CONF}.new" || {
		restore_umask
		service_stop
		return 1
	}
	restore_umask

	# We do not exit on error here as we do not want to block monitoring hosts
	# with the needed configuration as a result of those that do not
	config_foreach nut_upscgi_add_host host

	# If new host configuration was successfully created
	if [ "$upscgi_hosts_exists" = "true" ] && [ -s "${UPSCGI_HOSTS_CONF}.new" ]; then
		# Move the new configuration file to active use,
		# removing the old configuration in the process
		mv -f "${UPSCGI_HOSTS_CONF}.new" "${UPSCGI_HOSTS_CONF}"
	elif [ "$upscgi_hosts_exists" = "true" ]; then
		rm -f "${UPSCGI_HOSTS_CONF}.new"
	else
		# If no hosts are configured, use an empty hosts.conf (no configuration)
		rm -f "${UPSCGI_HOSTS_CONF}"
		rm -f "${UPSCGI_HOSTS_CONF}.new"
		if ! touch "${UPSCGI_HOSTS_CONF}"; then
			log_error "Failed to create empty hosts.conf" nut-cgi nut-cgi
			return 1
		fi
		# Empty hosts is not necessarily an error, but we log for information
		log_msg "No configured hosts" nut-cgi nut-cgi info
	fi

	# NUT CGI host file configuration is independent of upsset configuration
	config_foreach configure_nut_upscgi_upsset upsset
	if [ "$upscgi_upsset_failed" = "true" ]; then
		disable_upsset
		return 1
	fi
	return 0
}

start_service() {
	service_reload || return 1
	return 0
}

reload_service() {
	service_reload || {
		stop_service
		return 1
	}
	return 0
}

stop_service() {
	# Remove the hosts configuration file
	# Since this is only a configuration generation service, the only way to
	# stop serving (without stopping the web server for all uses) is to remove
	# the NUT CGI configuration
	rm -f "$UPSCGI_HOSTS_CONF"
	rm -f "$UPSCGI_HOSTS_CONF.new"

	disable_upsset || return 1
	return 0
}

service_triggers() {
	# Add a reload trigger on changes to the nut_cgi UCI config
	procd_add_reload_trigger "nut_cgi" || return 1
	return 0
}
