summaryrefslogtreecommitdiffstats
path: root/net/nut/files/nut-cgi.init
blob: 9aea83c94df9c38f4c515c690aa6de1fdc2c1958 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/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
}