summaryrefslogtreecommitdiffstats
path: root/net/acme-common/files/acme.init
blob: b25384ef6d3be88dffb085aa829609793e4f2bfc (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
#!/bin/sh /etc/rc.common

START=20
USE_PROCD=1
run_dir=/var/run/acme
export CHALLENGE_DIR=$run_dir/challenge
export CERT_DIR=/etc/ssl/acme
LAST_LISTEN_PORT=
NFT_HANDLE=
HOOK=/usr/lib/acme/hook
LOG_TAG=acme

# shellcheck source=net/acme/files/functions.sh
. "$IPKG_INSTROOT/usr/lib/acme/functions.sh"

extra_command "abort" "Abort running certificate issuances/renewals"
extra_command "renew" "Run certificate issuances/renewals"

delete_nft_rule() {
	if [ "$NFT_HANDLE" ]; then
		# $NFT_HANDLE contains the string 'handle XX' so pass it unquoted to nft
		nft delete rule inet fw4 input $NFT_HANDLE
		NFT_HANDLE=
	fi
}

cleanup() {
	log debug "cleaning up"
	delete_nft_rule
}

load_options() {
	section=$1

	config_get staging "$section" staging
	# compatibility for old option name
	if [ -z "$staging" ]; then
		config_get_bool staging "$section" use_staging 0
	fi
	procd_append_param env staging="$staging"
	config_get calias "$section" calias
	procd_append_param env calias="$calias"
	config_get dalias "$section" dalias
	procd_append_param env dalias="$dalias"
	config_get domains "$section" domains
	procd_append_param env domains="$domains"
	main_domain="$(first_arg $domains)"
	procd_append_param env main_domain="$main_domain"
	config_get keylength "$section" keylength
	if [ "$keylength" ]; then
		log warn "Option \"keylength\" is deprecated, please use key_type (e.g., ec256, rsa2048) instead."
		case $keylength in
		ec-*) key_type=${keylength/-/} ;;
		*) key_type=rsa$keylength ;;
		esac
	else
		config_get key_type "$section" key_type ec256
	fi
	procd_append_param env key_type="$key_type"
	config_get acme_server "$section" acme_server
	procd_append_param env acme_server="$acme_server"
	config_get days "$section" days
	procd_append_param env days="$days"
	config_get cert_profile "$section" cert_profile
	procd_append_param env cert_profile="$cert_profile"
	config_get dns_wait "$section" dns_wait
	procd_append_param env dns_wait="$dns_wait"
	config_get webroot "$section" webroot
	if [ "$webroot" ]; then
		log warn "Option \"webroot\" is deprecated, please remove it and change your web server's config so it serves ACME challenge requests from $CHALLENGE_DIR."
		CHALLENGE_DIR=$webroot
	fi
}

first_arg() {
	echo "$1"
}

get_cert() {
	section=$1

	config_get_bool enabled "$section" enabled 1
	[ "$enabled" = 1 ] || return

	# load `listen_port` here rather than in `load_options` so we can
	# return early without leaving a dangling `procd_open_instance`; the
	# check requires loading `validation_method` as well, which in turn
	# requires loading `dns` and `standalone`
	config_get validation_method "$section" validation_method
	config_get dns "$section" dns
	config_get standalone "$section" standalone
	[ -n "$standalone" ] && log warn "Option \"standalone\" is deprecated."
	# if validation_method isn't set then guess it
	if [ -z "$validation_method" ]; then
		if [ -n "$dns" ]; then
			validation_method="dns"
		elif [ "$standalone" = 1 ]; then
			validation_method="standalone"
		else
			validation_method="webroot"
		fi
		log warn "Please set \"option validation_method $validation_method\"."
	fi
	if [ "$validation_method" = "webroot" ]; then
		mkdir -p "$CHALLENGE_DIR"
	fi
	case "$validation_method" in
	standalone)
		config_get listen_port "$section" listen_port 80
		;;
	alpn)
		config_get listen_port "$section" listen_port 443
		;;
	*)
		config_get listen_port "$section" listen_port
		;;
	esac
	if [ "$listen_port" != "$LAST_LISTEN_PORT" ]; then
		delete_nft_rule

		if [ "$listen_port" ]; then
			if ! NFT_HANDLE=$(nft -a -e insert rule inet fw4 input tcp dport "$listen_port" counter accept comment ACME | grep -o 'handle [0-9]\+'); then
				return 1
			fi
			log debug "added nft rule: $NFT_HANDLE"
		fi

		LAST_LISTEN_PORT="$listen_port"
	fi

	procd_open_instance "$section"
	procd_set_param command "$HOOK" get
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_set_param env CHALLENGE_DIR="$CHALLENGE_DIR" CERT_DIR="$CERT_DIR"
	procd_append_param env account_email="$account_email" state_dir="$state_dir" debug="$debug"
	procd_append_param env dns="$dns" validation_method="$validation_method" listen_port="$listen_port"

	load_options "$section"

	load_credentials() {
		# use `eval` to correctly strip quotes around credential values
		eval procd_append_param env "$1"
	}
	config_list_foreach "$section" credentials load_credentials

	procd_close_instance
}

load_globals() {
	[ -z "$account_email" ] || return 1  # only read the first acme section

	section=$1

	config_get account_email "$section" account_email
	if [ -z "$account_email" ]; then
		log err "account_email option is required"
		exit 1
	fi
	export account_email

	config_get state_dir "$section" state_dir
	if [ "$state_dir" ]; then
		log warn "Option \"state_dir\" is deprecated, please remove it. Certificates now exist in $CERT_DIR."
		mkdir -p "$state_dir"
	else
		state_dir=/etc/acme
	fi
	export state_dir

	config_get_bool debug "$section" debug 0
	export debug
}

start_service() {
	grep -q '/etc/init.d/acme' /etc/crontabs/root 2>/dev/null || {
		echo "0 0 * * * /etc/init.d/acme renew" >>/etc/crontabs/root
	}
}

service_started() {
	echo 'Nightly certificate renewal enabled. To renew now, run `service acme renew`.'
}

stop_service() {
	sed -i '\|/etc/init.d/acme|d' /etc/crontabs/root
	running && stop_aborted="Running certificate renewal(s) aborted and a"
}

service_stopped() {
	if enabled; then
		untilboot=' until next boot. To disable permanently, run `service acme disable`'
	fi
	echo "${stop_aborted:-A}utomatic nightly renewal disabled$untilboot."
	echo 'To re-enable nightly renewal, run `service acme start`. To issue/renew now, run `service acme renew`.'
}

service_triggers() {
	procd_add_config_trigger config.change acme \
		/etc/init.d/acme renew
}

load_and_run() {
	trap cleanup EXIT

	config_load acme
	config_foreach load_globals acme

	config_foreach get_cert cert
}

renew() {
	echo "Starting certificate issuance/renewal in the background; see system log for progress."
	echo 'Issuances/renewals can be aborted with `service acme abort`.'
	rc_procd load_and_run
}

abort() {
	procd_lock
	if running "$@"; then
		procd_kill "$(basename ${basescript:-$initscript})" "$1"
		echo "Aborting certificate issuance(s)/renewal(s); see system log for confirmation."
	elif [ -z "$1" ]; then
		echo "No certificate issuances/renewals running to abort!"
		exit 1
	else
		echo "No certificate issuance/renewal \"$1\" running to abort!"
		exit 1
	fi
}