#!/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 }