]> git.proxmox.com Git - proxmox-acme.git/blob - src/proxmox-acme
ff9fec863a3dc1f4e8a3145b4ea9137bea942451
[proxmox-acme.git] / src / proxmox-acme
1 #!/usr/bin/env sh
2
3 VER=0.9
4
5 PROJECT_NAME="ProxmoxACME"
6
7 USER_AGENT="$PROJECT_NAME/$VER"
8
9 DNS_PLUGIN_PATH="/usr/share/proxmox-acme/dnsapi"
10 HTTP_HEADER="$(mktemp)"
11
12 _base64() {
13 openssl base64 -e | tr -d '\r\n'
14 }
15
16 _dbase64() {
17 openssl base64 -d
18 }
19
20 # Usage: hashalg [outputhex]
21 # Output Base64-encoded digest
22 _digest() {
23 alg="$1"
24
25 if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
26 if [ "$2" ]; then
27 openssl dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
28 else
29 openssl dgst -"$alg" -binary | _base64
30 fi
31 fi
32 }
33
34 _upper_case() {
35 # shellcheck disable=SC2018,SC2019
36 tr 'a-z' 'A-Z'
37 }
38
39 _lower_case() {
40 # shellcheck disable=SC2018,SC2019
41 tr 'A-Z' 'a-z'
42 }
43
44 _startswith() {
45 _str="$1"
46 _sub="$2"
47 echo "$_str" | grep "^$_sub" >/dev/null 2>&1
48 }
49
50 _endswith() {
51 _str="$1"
52 _sub="$2"
53 echo "$_str" | grep -- "$_sub\$" >/dev/null 2>&1
54 }
55
56 _contains() {
57 _str="$1"
58 _sub="$2"
59 echo "$_str" | grep -- "$_sub" >/dev/null 2>&1
60 }
61
62 # str index [sep]
63 _getfield() {
64 _str="$1"
65 _findex="$2"
66 _sep="$3"
67
68 if [ -z "$_sep" ]; then
69 _sep=","
70 fi
71
72 _ffi="$_findex"
73 while [ "$_ffi" -gt "0" ]; do
74 _fv="$(echo "$_str" | cut -d "$_sep" -f "$_ffi")"
75 if [ "$_fv" ]; then
76 printf -- "%s" "$_fv"
77 return 0
78 fi
79 _ffi="$(_math "$_ffi" - 1)"
80 done
81
82 printf -- "%s" "$_str"
83
84 }
85
86 _exists() {
87 cmd="$1"
88 if eval type type >/dev/null 2>&1; then
89 type "$cmd" >/dev/null 2>&1
90 else command
91 command -v "$cmd" >/dev/null 2>&1
92 fi
93 ret="$?"
94 return $ret
95 }
96
97 # a + b
98 _math() {
99 _m_opts="$@"
100 printf "%s" "$(($_m_opts))"
101 }
102
103 _egrep_o() {
104 if ! egrep -o "$1" 2>/dev/null; then
105 sed -n 's/.*\('"$1"'\).*/\1/p'
106 fi
107 }
108
109 # body url [needbase64] [POST|PUT|DELETE] [ContentType]
110 _post() {
111 body="$1"
112 _post_url="$2"
113 needbase64="$3"
114 httpmethod="$4"
115 _postContentType="$5"
116
117 if [ -z "$httpmethod" ]; then
118 httpmethod="POST"
119 fi
120
121 _CURL="curl -L --silent --dump-header $HTTP_HEADER -g "
122 if [ "$HTTPS_INSECURE" ]; then
123 _CURL="$_CURL --insecure "
124 fi
125 if [ "$httpmethod" = "HEAD" ]; then
126 _CURL="$_CURL -I "
127 fi
128 if [ "$needbase64" ]; then
129 if [ "$body" ]; then
130 if [ "$_postContentType" ]; then
131 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
132 else
133 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
134 fi
135 else
136 if [ "$_postContentType" ]; then
137 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)"
138 else
139 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)"
140 fi
141 fi
142 else
143 if [ "$body" ]; then
144 if [ "$_postContentType" ]; then
145 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
146 else
147 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
148 fi
149 else
150 if [ "$_postContentType" ]; then
151 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")"
152 else
153 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")"
154 fi
155 fi
156 fi
157 _ret="$?"
158 if [ "$_ret" != "0" ]; then
159 _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
160 fi
161 printf "%s" "$response"
162 return $_ret
163 }
164
165 # url getheader timeout
166 _get() {
167 url="$1"
168 onlyheader="$2"
169 t="$3"
170
171 _CURL="curl -L --silent --dump-header $HTTP_HEADER -g "
172 if [ "$HTTPS_INSECURE" ]; then
173 _CURL="$_CURL --insecure "
174 fi
175 if [ "$t" ]; then
176 _CURL="$_CURL --connect-timeout $t"
177 fi
178 if [ "$onlyheader" ]; then
179 $_CURL -I --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url"
180 else
181 $_CURL --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url"
182 fi
183 ret=$?
184 if [ "$ret" != "0" ]; then
185 _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret"
186 fi
187 return $ret
188 }
189
190 _head_n() {
191 head -n "$1"
192 }
193
194 _tail_n() {
195 tail -n "$1"
196 }
197
198 # stdin output hexstr splited by one space
199 # input:"abc"
200 # output: " 61 62 63"
201 _hex_dump() {
202 od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n"
203 }
204
205 # stdin stdout
206 _url_encode() {
207 _hex_str=$(_hex_dump)
208 for _hex_code in $_hex_str; do
209 #upper case
210 case "${_hex_code}" in
211 "41")
212 printf "%s" "A"
213 ;;
214 "42")
215 printf "%s" "B"
216 ;;
217 "43")
218 printf "%s" "C"
219 ;;
220 "44")
221 printf "%s" "D"
222 ;;
223 "45")
224 printf "%s" "E"
225 ;;
226 "46")
227 printf "%s" "F"
228 ;;
229 "47")
230 printf "%s" "G"
231 ;;
232 "48")
233 printf "%s" "H"
234 ;;
235 "49")
236 printf "%s" "I"
237 ;;
238 "4a")
239 printf "%s" "J"
240 ;;
241 "4b")
242 printf "%s" "K"
243 ;;
244 "4c")
245 printf "%s" "L"
246 ;;
247 "4d")
248 printf "%s" "M"
249 ;;
250 "4e")
251 printf "%s" "N"
252 ;;
253 "4f")
254 printf "%s" "O"
255 ;;
256 "50")
257 printf "%s" "P"
258 ;;
259 "51")
260 printf "%s" "Q"
261 ;;
262 "52")
263 printf "%s" "R"
264 ;;
265 "53")
266 printf "%s" "S"
267 ;;
268 "54")
269 printf "%s" "T"
270 ;;
271 "55")
272 printf "%s" "U"
273 ;;
274 "56")
275 printf "%s" "V"
276 ;;
277 "57")
278 printf "%s" "W"
279 ;;
280 "58")
281 printf "%s" "X"
282 ;;
283 "59")
284 printf "%s" "Y"
285 ;;
286 "5a")
287 printf "%s" "Z"
288 ;;
289
290 #lower case
291 "61")
292 printf "%s" "a"
293 ;;
294 "62")
295 printf "%s" "b"
296 ;;
297 "63")
298 printf "%s" "c"
299 ;;
300 "64")
301 printf "%s" "d"
302 ;;
303 "65")
304 printf "%s" "e"
305 ;;
306 "66")
307 printf "%s" "f"
308 ;;
309 "67")
310 printf "%s" "g"
311 ;;
312 "68")
313 printf "%s" "h"
314 ;;
315 "69")
316 printf "%s" "i"
317 ;;
318 "6a")
319 printf "%s" "j"
320 ;;
321 "6b")
322 printf "%s" "k"
323 ;;
324 "6c")
325 printf "%s" "l"
326 ;;
327 "6d")
328 printf "%s" "m"
329 ;;
330 "6e")
331 printf "%s" "n"
332 ;;
333 "6f")
334 printf "%s" "o"
335 ;;
336 "70")
337 printf "%s" "p"
338 ;;
339 "71")
340 printf "%s" "q"
341 ;;
342 "72")
343 printf "%s" "r"
344 ;;
345 "73")
346 printf "%s" "s"
347 ;;
348 "74")
349 printf "%s" "t"
350 ;;
351 "75")
352 printf "%s" "u"
353 ;;
354 "76")
355 printf "%s" "v"
356 ;;
357 "77")
358 printf "%s" "w"
359 ;;
360 "78")
361 printf "%s" "x"
362 ;;
363 "79")
364 printf "%s" "y"
365 ;;
366 "7a")
367 printf "%s" "z"
368 ;;
369
370 #numbers
371 "30")
372 printf "%s" "0"
373 ;;
374 "31")
375 printf "%s" "1"
376 ;;
377 "32")
378 printf "%s" "2"
379 ;;
380 "33")
381 printf "%s" "3"
382 ;;
383 "34")
384 printf "%s" "4"
385 ;;
386 "35")
387 printf "%s" "5"
388 ;;
389 "36")
390 printf "%s" "6"
391 ;;
392 "37")
393 printf "%s" "7"
394 ;;
395 "38")
396 printf "%s" "8"
397 ;;
398 "39")
399 printf "%s" "9"
400 ;;
401 "2d")
402 printf "%s" "-"
403 ;;
404 "5f")
405 printf "%s" "_"
406 ;;
407 "2e")
408 printf "%s" "."
409 ;;
410 "7e")
411 printf "%s" "~"
412 ;;
413
414 #other hex
415 *)
416 printf '%%%s' "$_hex_code"
417 ;;
418 esac
419 done
420 }
421
422 # Usage: hashalg secret_hex [outputhex]
423 # Output binary hmac
424 _hmac() {
425 alg="$1"
426 secret_hex="$2"
427 outputhex="$3"
428
429 if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then
430 if [ "$outputhex" ]; then
431 (openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)") | cut -d = -f 2 | tr -d ' '
432 else
433 openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)" -binary
434 fi
435 fi
436 }
437
438 # domain
439 _is_idn() {
440 _is_idn_d="$1"
441 _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-_')
442 [ "$_idn_temp" ]
443 }
444
445 # aa.com
446 _idn() {
447 __idn_d="$1"
448 if ! _is_idn "$__idn_d"; then
449 printf "%s" "$__idn_d"
450 return 0
451 fi
452
453 if _exists idn; then
454 idn "$__idn_d" | tr -d "\r\n"
455 else
456 _err "Please install idn to process IDN names."
457 fi
458 }
459
460 _normalizeJson() {
461 sed "s/\" *: *\([\"{\[]\)/\":\1/g" | sed "s/^ *\([^ ]\)/\1/" | tr -d "\r\n"
462 }
463
464 # options file
465 _sed_i() {
466 sed -i "$1" "$2"
467 }
468
469 # sleep sec
470 _sleep() {
471 sleep "$1"
472 }
473
474 _stat() {
475 stat -c '%U:%G' "$1" 2>/dev/null
476 }
477
478 _time() {
479 date -u "+%s"
480 }
481
482 _utc_date() {
483 date -u "+%Y-%m-%d %H:%M:%S"
484 }
485
486 # stubbed/aliased:
487 __green() {
488 printf -- "%b" "$1"
489 }
490
491 __red() {
492 printf -- "%b" "$1"
493 }
494
495 _log() {
496 return
497 }
498
499 _info() {
500 printf -- "%s" "[$(date)] " >&1
501 echo "$1"
502 }
503
504 _err() {
505 printf -- "%s" "[$(date)] " >&2
506 if [ -z "$2" ]; then
507 __red "$1" >&2
508 else
509 __red "$1='$2'" >&2
510 fi
511 printf "\n" >&2
512 return 1
513 }
514
515 # key
516 _readaccountconf() {
517 echo "$1"
518 }
519
520 # key
521 _readaccountconf_mutable() {
522 _readaccountconf "$1"
523 }
524
525 # no-ops:
526 _clearaccountconf() {
527 return
528 }
529
530 _cleardomainconf() {
531 return
532 }
533
534 _debug() {
535 return
536 }
537
538 _debug2() {
539 return
540 }
541
542 _debug3() {
543 return
544 }
545
546 _secure_debug() {
547 return
548 }
549
550 _secure_debug2() {
551 return
552 }
553
554 _secure_debug3() {
555 return
556 }
557
558 _saveaccountconf() {
559 return
560 }
561
562 _saveaccountconf_mutable() {
563 return
564 }
565
566 _save_conf() {
567 return
568 }
569
570 _savedomainconf() {
571 return
572 }
573
574 _source_plugin_config() {
575 return
576 }
577
578 # Proxmox implementation to inject the DNSAPI variables
579 _load_plugin_config() {
580 tmp_str="${plugin_conf_string//[^,]}"
581 index="$(_math ${#tmp_str} + 1)"
582 while [ "$index" -gt "0" ]
583 do
584 field=$(_getfield $plugin_conf_string "$index" ",")
585 ADDR=(${field/=/ })
586 key="${ADDR[0]}"
587 value="${ADDR[1]}"
588
589 # decode base64 encoded values
590 value=$(echo $value | /usr/bin/openssl base64 -d -A)
591
592 # acme.sh uses eval insted of export
593 export "$key"="$value"
594 index="$(_math "$index" - 1)"
595 done
596 }
597
598 # call setup and teardown direct
599 # the parameter must be set in the correct order
600 # $1 <String> DNS Plugin name
601 # $2 <String> Fully Qualified Domain Name
602 # $3 <String> value for TXT record
603 # $4 <String> DNS plugin auth and config parameter separated by ","
604
605 setup() {
606 dns_plugin="dns_$1"
607 dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh"
608 fqdn="_acme-challenge.$2"
609 txtvalue=$3
610 plugin_conf_string=$4
611
612 _load_plugin_config
613
614 if ! . "$dns_plugin_path"; then
615 _err "Load file $dns_plugin error."
616 return 1
617 fi
618
619 addcommand="${dns_plugin}_add"
620 if ! _exists "$addcommand"; then
621 _err "It seems that your api file is not correct, it must have a function named: $addcommand"
622 return 1
623 fi
624
625 if ! $addcommand "$fqdn" "$txtvalue"; then
626 _err "Error add txt for domain:$fulldomain"
627 return 1
628 fi
629 }
630
631 teardown() {
632 dns_plugin="dns_$1"
633 dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh"
634 fqdn="_acme-challenge.$2"
635 txtvalue=$3
636 plugin_conf_string=$4
637
638 _load_plugin_config
639
640 if ! . "$dns_plugin_path"; then
641 _err "Load file $dns_plugin error."
642 return 1
643 fi
644
645 rmcommand="${dns_plugin}_rm"
646 if ! _exists "$rmcommand"; then
647 _err "It seems that your api file is not correct, it must have a function named: $rmcommand"
648 return 1
649 fi
650
651 if ! $rmcommand "$fqdn" "$txtvalue"; then
652 _err "Error add txt for domain:$fulldomain"
653 return 1
654 fi
655 }
656
657 "$@"