diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index b0326332..729874ce 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -65,7 +65,7 @@ jobs: run: | docker run --rm -itd --name=pebble \ -e PEBBLE_VA_ALWAYS_VALID=1 \ - -p 14000:14000 -p 15000:15000 letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict + -p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest -config /test/config/pebble-config.json -strict - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest diff --git a/Dockerfile b/Dockerfile index 7523f0af..d8f8b265 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.22 RUN apk --no-cache add -f \ openssl \ diff --git a/acme.sh b/acme.sh index 95e7fed4..d5d806b1 100755 --- a/acme.sh +++ b/acme.sh @@ -1897,6 +1897,11 @@ _inithttp() { if [ -z "$_ACME_CURL" ] && _exists "curl"; then _ACME_CURL="curl --silent --dump-header $HTTP_HEADER " + if [ "$ACME_USE_IPV6_REQUESTS" ]; then + _ACME_CURL="$_ACME_CURL --ipv6 " + elif [ "$ACME_USE_IPV4_REQUESTS" ]; then + _ACME_CURL="$_ACME_CURL --ipv4 " + fi if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_CURL="$_ACME_CURL -L " fi @@ -1924,6 +1929,11 @@ _inithttp() { if [ -z "$_ACME_WGET" ] && _exists "wget"; then _ACME_WGET="wget -q" + if [ "$ACME_USE_IPV6_REQUESTS" ]; then + _ACME_WGET="$_ACME_WGET --inet6-only " + elif [ "$ACME_USE_IPV4_REQUESTS" ]; then + _ACME_WGET="$_ACME_WGET --inet4-only " + fi if [ "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_WGET="$_ACME_WGET --max-redirect 0 " fi @@ -7237,6 +7247,8 @@ Parameters: --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted. --listen-v4 Force standalone/tls server to listen at ipv4. --listen-v6 Force standalone/tls server to listen at ipv6. + --request-v4 Force client requests to use ipv4 to connect to the CA server. + --request-v6 Force client requests to use ipv6 to connect to the CA server. --openssl-bin Specifies a custom openssl bin location. --use-wget Force to use wget, if you have both curl and wget installed. --yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode. @@ -7355,6 +7367,24 @@ _processAccountConf() { _saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET" fi + if [ "$_request_v6" ]; then + _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6" + _clearaccountconf "ACME_USE_IPV4_REQUESTS" + ACME_USE_IPV4_REQUESTS= + elif [ "$_request_v4" ]; then + _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4" + _clearaccountconf "ACME_USE_IPV6_REQUESTS" + ACME_USE_IPV6_REQUESTS= + elif [ "$ACME_USE_IPV6_REQUESTS" ]; then + _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS" + _clearaccountconf "ACME_USE_IPV4_REQUESTS" + ACME_USE_IPV4_REQUESTS= + elif [ "$ACME_USE_IPV4_REQUESTS" ]; then + _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS" + _clearaccountconf "ACME_USE_IPV6_REQUESTS" + ACME_USE_IPV6_REQUESTS= + fi + } _checkSudo() { @@ -7521,6 +7551,8 @@ _process() { _local_address="" _log_level="" _auto_upgrade="" + _request_v4="" + _request_v6="" _listen_v4="" _listen_v6="" _openssl_bin="" @@ -7995,6 +8027,18 @@ _process() { fi AUTO_UPGRADE="$_auto_upgrade" ;; + --request-v4) + _request_v4="1" + ACME_USE_IPV4_REQUESTS="1" + _request_v6="" + ACME_USE_IPV6_REQUESTS="" + ;; + --request-v6) + _request_v6="1" + ACME_USE_IPV6_REQUESTS="1" + _request_v4="" + ACME_USE_IPV4_REQUESTS="" + ;; --listen-v4) _listen_v4="1" Le_Listen_V4="$_listen_v4" diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index d334853e..df34f927 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -71,7 +71,7 @@ with Client(uri="$_ws_uri") as c: fullchain = file.read() with open('$2', 'r') as file: privatekey = file.read() - ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True) + ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey}, job=True) print("R:" + str(ret["id"])) sys.exit(0) else: diff --git a/deploy/unifi.sh b/deploy/unifi.sh index 1f274236..1d13e04f 100644 --- a/deploy/unifi.sh +++ b/deploy/unifi.sh @@ -143,8 +143,10 @@ unifi_deploy() { # correct file ownership according to the directory, the keystore is placed in _unifi_keystore_dir=$(dirname "${_unifi_keystore}") - _unifi_keystore_dir_owner=$(find "${_unifi_keystore_dir}" -maxdepth 0 -printf '%u\n') - _unifi_keystore_owner=$(find "${_unifi_keystore}" -maxdepth 0 -printf '%u\n') + # shellcheck disable=SC2012 + _unifi_keystore_dir_owner=$(ls -ld "${_unifi_keystore_dir}" | awk '{print $3}') + # shellcheck disable=SC2012 + _unifi_keystore_owner=$(ls -l "${_unifi_keystore}" | awk '{print $3}') if ! [ "${_unifi_keystore_owner}" = "${_unifi_keystore_dir_owner}" ]; then _debug "Changing keystore owner to ${_unifi_keystore_dir_owner}" chown "$_unifi_keystore_dir_owner" "${_unifi_keystore}" >/dev/null 2>&1 # fail quietly if we're not running as root diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh index f57afa1f..0ef03fea 100644 --- a/dnsapi/dns_curanet.sh +++ b/dnsapi/dns_curanet.sh @@ -15,7 +15,7 @@ CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains" CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token" CURANET_ACCESS_TOKEN="" -######## Public functions ##################### +######## Public functions #################### #Usage: dns_curanet_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_curanet_add() { @@ -154,7 +154,7 @@ _get_root() { export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" response="$(_get "$CURANET_REST_URL/$h/Records" "" "")" - if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then + if [ ! "$(echo "$response" | _egrep_o "Entity not found|Bad Request")" ]; then _domain=$h return 0 fi diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index 1ab47a89..0c42989b 100644 --- a/dnsapi/dns_nanelo.sh +++ b/dnsapi/dns_nanelo.sh @@ -27,8 +27,16 @@ dns_nanelo_add() { fi _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _info "Adding TXT record to ${fulldomain}" - response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" + response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/addrecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")" if _contains "${response}" 'success'; then return 0 fi @@ -51,8 +59,16 @@ dns_nanelo_rm() { fi _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" + _debug "First, let's detect the root zone:" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _info "Deleting resource record $fulldomain" - response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" + response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/deleterecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")" if _contains "${response}" 'success'; then return 0 fi @@ -60,3 +76,45 @@ dns_nanelo_rm() { _err "${response}" return 1 } + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com + +_get_root() { + fulldomain=$1 + + # Fetch all zones from Nanelo + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/getzones")" || return 1 + + # Extract "zones" array into space-separated list + zones=$(echo "$response" | + tr -d ' \n' | + sed -n 's/.*"zones":\[\([^]]*\)\].*/\1/p' | + tr -d '"' | + tr , ' ') + _debug zones "$zones" + + bestzone="" + for z in $zones; do + case "$fulldomain" in + *."$z" | "$z") + if [ ${#z} -gt ${#bestzone} ]; then + bestzone=$z + fi + ;; + esac + done + + if [ -z "$bestzone" ]; then + _err "No matching zone found for $fulldomain" + return 1 + fi + + _domain="$bestzone" + _sub_domain=$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//") + + return 0 +} diff --git a/notify/ntfy.sh b/notify/ntfy.sh index 21e39559..3a788a84 100644 --- a/notify/ntfy.sh +++ b/notify/ntfy.sh @@ -14,6 +14,13 @@ ntfy_send() { _debug "_content" "$_content" _debug "_statusCode" "$_statusCode" + _priority_default="default" + _priority_error="high" + + _tag_success="white_check_mark" + _tag_error="warning" + _tag_info="information_source" + NTFY_URL="${NTFY_URL:-$(_readaccountconf_mutable NTFY_URL)}" if [ "$NTFY_URL" ]; then _saveaccountconf_mutable NTFY_URL "$NTFY_URL" @@ -30,7 +37,26 @@ ntfy_send() { export _H1="Authorization: Bearer $NTFY_TOKEN" fi - _data="${_subject}. $_content" + case "$_statusCode" in + 0) + _priority="$_priority_default" + _tag="$_tag_success" + ;; + 1) + _priority="$_priority_error" + _tag="$_tag_error" + ;; + 2) + _priority="$_priority_default" + _tag="$_tag_info" + ;; + esac + + export _H2="Priority: $_priority" + export _H3="Tags: $_tag" + export _H4="Title: $PROJECT_NAME: $_subject" + + _data="$_content" response="$(_post "$_data" "$NTFY_URL/$NTFY_TOPIC" "" "POST" "")" if [ "$?" = "0" ] && _contains "$response" "expires"; then diff --git a/notify/opsgenie.sh b/notify/opsgenie.sh new file mode 100644 index 00000000..d352a18c --- /dev/null +++ b/notify/opsgenie.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env sh + +#Support OpsGenie API integration + +#OPSGENIE_API_KEY="" Required, opsgenie api key +#OPSGENIE_REGION="" Optional, opsgenie region, can be EU or US (default: US) +#OPSGENIE_PRIORITY_SUCCESS="" Optional, opsgenie priority for success (default: P5) +#OPSGENIE_PRIORITY_ERROR="" Optional, opsgenie priority for error (default: P2) +#OPSGENIE_PRIORITY_SKIP="" Optional, opsgenie priority for renew skipped (default: P5) + +_OPSGENIE_AVAIL_REGION="US,EU" +_OPSGENIE_AVAIL_PRIORITIES="P1,P2,P3,P4,P5" + +opsgenie_send() { + _subject="$1" + _content="$2" + _status_code="$3" #0: success, 1: error, 2($RENEW_SKIP): skipped + + OPSGENIE_API_KEY="${OPSGENIE_API_KEY:-$(_readaccountconf_mutable OPSGENIE_API_KEY)}" + if [ -z "$OPSGENIE_API_KEY" ]; then + OPSGENIE_API_KEY="" + _err "You didn't specify an OpsGenie API key OPSGENIE_API_KEY yet." + return 1 + fi + _saveaccountconf_mutable OPSGENIE_API_KEY "$OPSGENIE_API_KEY" + export _H1="Authorization: GenieKey $OPSGENIE_API_KEY" + + OPSGENIE_REGION="${OPSGENIE_REGION:-$(_readaccountconf_mutable OPSGENIE_REGION)}" + if [ -z "$OPSGENIE_REGION" ]; then + OPSGENIE_REGION="US" + _info "The OPSGENIE_REGION is not set, so use the default US as regeion." + elif ! _hasfield "$_OPSGENIE_AVAIL_REGION" "$OPSGENIE_REGION"; then + _err "The OPSGENIE_REGION \"$OPSGENIE_REGION\" is not available, should be one of $_OPSGENIE_AVAIL_REGION" + OPSGENIE_REGION="" + return 1 + else + _saveaccountconf_mutable OPSGENIE_REGION "$OPSGENIE_REGION" + fi + + OPSGENIE_PRIORITY_SUCCESS="${OPSGENIE_PRIORITY_SUCCESS:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS)}" + if [ -z "$OPSGENIE_PRIORITY_SUCCESS" ]; then + OPSGENIE_PRIORITY_SUCCESS="P5" + _info "The OPSGENIE_PRIORITY_SUCCESS is not set, so use the default P5 as priority." + elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SUCCESS"; then + _err "The OPSGENIE_PRIORITY_SUCCESS \"$OPSGENIE_PRIORITY_SUCCESS\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES" + OPSGENIE_PRIORITY_SUCCESS="" + return 1 + else + _saveaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS "$OPSGENIE_PRIORITY_SUCCESS" + fi + + OPSGENIE_PRIORITY_ERROR="${OPSGENIE_PRIORITY_ERROR:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_ERROR)}" + if [ -z "$OPSGENIE_PRIORITY_ERROR" ]; then + OPSGENIE_PRIORITY_ERROR="P2" + _info "The OPSGENIE_PRIORITY_ERROR is not set, so use the default P2 as priority." + elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_ERROR"; then + _err "The OPSGENIE_PRIORITY_ERROR \"$OPSGENIE_PRIORITY_ERROR\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES" + OPSGENIE_PRIORITY_ERROR="" + return 1 + else + _saveaccountconf_mutable OPSGENIE_PRIORITY_ERROR "$OPSGENIE_PRIORITY_ERROR" + fi + + OPSGENIE_PRIORITY_SKIP="${OPSGENIE_PRIORITY_SKIP:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SKIP)}" + if [ -z "$OPSGENIE_PRIORITY_SKIP" ]; then + OPSGENIE_PRIORITY_SKIP="P5" + _info "The OPSGENIE_PRIORITY_SKIP is not set, so use the default P5 as priority." + elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SKIP"; then + _err "The OPSGENIE_PRIORITY_SKIP \"$OPSGENIE_PRIORITY_SKIP\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES" + OPSGENIE_PRIORITY_SKIP="" + return 1 + else + _saveaccountconf_mutable OPSGENIE_PRIORITY_SKIP "$OPSGENIE_PRIORITY_SKIP" + fi + + case "$OPSGENIE_REGION" in + "US") + _opsgenie_url="https://api.opsgenie.com/v2/alerts" + ;; + "EU") + _opsgenie_url="https://api.eu.opsgenie.com/v2/alerts" + ;; + *) + _err "opsgenie region error." + return 1 + ;; + esac + + case $_status_code in + 0) + _priority=$OPSGENIE_PRIORITY_SUCCESS + ;; + 1) + _priority=$OPSGENIE_PRIORITY_ERROR + ;; + 2) + _priority=$OPSGENIE_PRIORITY_SKIP + ;; + *) + _priority=$OPSGENIE_PRIORITY_ERROR + ;; + esac + + _subject_json=$(echo "$_subject" | _json_encode) + _content_json=$(echo "$_content" | _json_encode) + _subject_underscore=$(echo "$_subject" | sed 's/ /_/g') + _alias_json=$(echo "acme.sh-$(hostname)-$_subject_underscore-$(date +%Y%m%d)" | base64 --wrap=0 | _json_encode) + + _data="{ + \"message\": \"$_subject_json\", + \"alias\": \"$_alias_json\", + \"description\": \"$_content_json\", + \"tags\": [ + \"acme.sh\", + \"host:$(hostname)\" + ], + \"entity\": \"$(hostname -f)\", + \"priority\": \"$_priority\" +}" + + if response=$(_post "$_data" "$_opsgenie_url" "" "" "application/json"); then + if ! _contains "$response" error; then + _info "opsgenie send success." + return 0 + fi + fi + _err "opsgenie send error." + _err "$response" + return 1 +}