diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 24be2c47..8905a651 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,7 +3,7 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: acmesh -ko_fi: # Replace with a single Ko-fi username +ko_fi: neilpang tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 5d032769..22f8b9af 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -25,11 +25,11 @@ jobs: CA: "" CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 - - TEST_ACME_Server: "ZeroSSL.com" - CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - CA: "ZeroSSL RSA Domain Secure Site CA" - CA_EMAIL: "githubtest@acme.sh" - TEST_PREFERRED_CHAIN: "" + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" runs-on: macos-10.15 env: TEST_LOCAL: 1 diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml index 63e3136c..c665652a 100644 --- a/.github/workflows/Linux.yml +++ b/.github/workflows/Linux.yml @@ -25,6 +25,7 @@ jobs: env: TEST_LOCAL: 1 TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + TEST_ACME_Server: "LetsEncrypt.org_test" steps: - uses: actions/checkout@v2 - name: Clone acmetest diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml index 4b529f6a..8d52b3f6 100644 --- a/.github/workflows/MacOS.yml +++ b/.github/workflows/MacOS.yml @@ -25,11 +25,11 @@ jobs: CA: "" CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 - - TEST_ACME_Server: "ZeroSSL.com" - CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - CA: "ZeroSSL RSA Domain Secure Site CA" - CA_EMAIL: "githubtest@acme.sh" - TEST_PREFERRED_CHAIN: "" + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" runs-on: macos-latest env: TEST_LOCAL: 1 diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 77fdcc9a..f8a3826c 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -25,11 +25,11 @@ jobs: CA: "" CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 - - TEST_ACME_Server: "ZeroSSL.com" - CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - CA: "ZeroSSL RSA Domain Secure Site CA" - CA_EMAIL: "githubtest@acme.sh" - TEST_PREFERRED_CHAIN: "" + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" runs-on: macos-10.15 env: TEST_LOCAL: 1 diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index 2d7eeeae..55d32519 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -25,11 +25,11 @@ jobs: CA: "" CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 - - TEST_ACME_Server: "ZeroSSL.com" - CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - CA: "ZeroSSL RSA Domain Secure Site CA" - CA_EMAIL: "githubtest@acme.sh" - TEST_PREFERRED_CHAIN: "" + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" runs-on: windows-latest env: TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} diff --git a/Dockerfile b/Dockerfile index fa11ea8a..049649f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,7 +65,8 @@ RUN for verb in help \ RUN printf "%b" '#!'"/usr/bin/env sh\n \ if [ \"\$1\" = \"daemon\" ]; then \n \ trap \"echo stop && killall crond && exit 0\" SIGTERM SIGINT \n \ - crond && while true; do sleep 1; done;\n \ + crond && sleep infinity &\n \ + wait \n \ else \n \ exec -- \"\$@\"\n \ fi" >/entry.sh && chmod +x /entry.sh diff --git a/README.md b/README.md index 91a18985..4a12d46a 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ https://github.com/acmesh-official/acmetest - Letsencrypt.org CA - [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-CA) - [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) +- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) - [Pebble strict Mode](https://github.com/letsencrypt/pebble) - Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA diff --git a/acme.sh b/acme.sh index 18456968..260733a2 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.0.2 +VER=3.0.5 PROJECT_NAME="acme.sh" @@ -20,8 +20,6 @@ _SUB_FOLDER_DEPLOY="deploy" _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY" -CA_LETSENCRYPT_V1="https://acme-v01.api.letsencrypt.org/directory" - CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory" CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" @@ -34,6 +32,9 @@ _ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email" CA_SSLCOM_RSA="https://acme.ssl.com/sslcom-dv-rsa" CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc" +CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory" +CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory" + DEFAULT_CA=$CA_ZEROSSL DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST @@ -44,9 +45,11 @@ LetsEncrypt.org_test,letsencrypt_test,letsencrypttest BuyPass.com,buypass BuyPass.com_test,buypass_test,buypasstest SSL.com,sslcom +Google.com,google +Google.com_test,googletest,google_test " -CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA" +CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST" DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" @@ -172,6 +175,8 @@ _SERVER_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Server" _PREFERRED_CHAIN_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Preferred-Chain" +_VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity" + _DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck" _DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead." @@ -976,9 +981,9 @@ _base64() { #Usage: multiline _dbase64() { if [ "$1" ]; then - ${ACME_OPENSSL_BIN:-openssl} base64 -d -A - else ${ACME_OPENSSL_BIN:-openssl} base64 -d + else + ${ACME_OPENSSL_BIN:-openssl} base64 -d -A fi } @@ -1141,13 +1146,19 @@ _createkey() { _debug "Use length $length" - if ! touch "$f" >/dev/null 2>&1; then - _f_path="$(dirname "$f")" - _debug _f_path "$_f_path" - if ! mkdir -p "$_f_path"; then - _err "Can not create path: $_f_path" + if ! [ -e "$f" ]; then + if ! touch "$f" >/dev/null 2>&1; then + _f_path="$(dirname "$f")" + _debug _f_path "$_f_path" + if ! mkdir -p "$_f_path"; then + _err "Can not create path: $_f_path" + return 1 + fi + fi + if ! touch "$f" >/dev/null 2>&1; then return 1 fi + chmod 600 "$f" fi if _isEccKey "$length"; then @@ -1495,7 +1506,6 @@ _create_account_key() { else #generate account key if _createkey "$length" "$ACCOUNT_KEY_PATH"; then - chmod 600 "$ACCOUNT_KEY_PATH" _info "Create account key ok." return 0 else @@ -1593,23 +1603,22 @@ _durl_replace_base64() { _time2str() { #BSD - if date -u -r "$1" 2>/dev/null; then + if date -u -r "$1" -j "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then return fi #Linux - if date -u -d@"$1" 2>/dev/null; then + if date -u --date=@"$1" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then return fi #Solaris - if _exists adb; then - _t_s_a=$(echo "0t${1}=Y" | adb) - echo "$_t_s_a" + if printf "%(%Y-%m-%dT%H:%M:%SZ)T\n" $1 2>/dev/null; then + return fi #Busybox - if echo "$1" | awk '{ print strftime("%c", $0); }' 2>/dev/null; then + if echo "$1" | awk '{ print strftime("%Y-%m-%dT%H:%M:%SZ", $0); }' 2>/dev/null; then return fi } @@ -1768,6 +1777,27 @@ _time() { date -u "+%s" } +#support 2 formats: +# 2022-04-01 08:10:33 to 1648800633 +#or 2022-04-01T08:10:33Z to 1648800633 +_date2time() { + #Linux + if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi + + #Solaris + if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi + #Mac/BSD + if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi + _err "Can not parse _date2time $1" + return 1 +} + _utc_date() { date -u "+%Y-%m-%d %H:%M:%S" } @@ -1840,7 +1870,9 @@ _inithttp() { _ACME_WGET="$_ACME_WGET --max-redirect 0 " fi if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then - _ACME_WGET="$_ACME_WGET -d " + if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then + _ACME_WGET="$_ACME_WGET -d " + fi fi if [ "$CA_PATH" ]; then _ACME_WGET="$_ACME_WGET --ca-directory=$CA_PATH " @@ -2657,6 +2689,12 @@ _initAPI() { return 1 } +_clearCA() { + export CA_CONF= + export ACCOUNT_KEY_PATH= + export ACCOUNT_JSON_PATH= +} + #[domain] [keylength or isEcc flag] _initpath() { domain="$1" @@ -3746,7 +3784,7 @@ updateaccount() { _email="$(_getAccountEmail)" - if [ "$ACCOUNT_EMAIL" ]; then + if [ "$_email" ]; then updjson='{"contact": ["mailto:'$_email'"]}' else updjson='{"contact": []}' @@ -3756,7 +3794,7 @@ updateaccount() { if [ "$code" = '200' ]; then echo "$response" >"$ACCOUNT_JSON_PATH" - _info "account update success for $_accUri." + _info "Account update success for $_accUri." else _info "Error. The account was not updated." return 1 @@ -4195,6 +4233,40 @@ _getIdType() { fi } +# beginTime dateTo +# beginTime is full string format("2022-04-01T08:10:33Z"), beginTime can be empty, to use current time +# dateTo can be ether in full string format("2022-04-01T08:10:33Z") or in delta format(+5d or +20h) +_convertValidaty() { + _beginTime="$1" + _dateTo="$2" + _debug2 "_beginTime" "$_beginTime" + _debug2 "_dateTo" "$_dateTo" + + if _startswith "$_dateTo" "+"; then + _v_begin=$(_time) + if [ "$_beginTime" ]; then + _v_begin="$(_date2time "$_beginTime")" + fi + _debug2 "_v_begin" "$_v_begin" + if _endswith "$_dateTo" "h"; then + _v_end=$(_math "$_v_begin + 60 * 60 * $(echo "$_dateTo" | tr -d '+h')") + elif _endswith "$_dateTo" "d"; then + _v_end=$(_math "$_v_begin + 60 * 60 * 24 * $(echo "$_dateTo" | tr -d '+d')") + else + _err "Not recognized format for _dateTo: $_dateTo" + return 1 + fi + _debug2 "_v_end" "$_v_end" + _time2str "$_v_end" + else + if [ "$(_time)" -gt "$(_date2time "$_dateTo")" ]; then + _err "The validaty to is in the past: _dateTo = $_dateTo" + return 1 + fi + echo "$_dateTo" + fi +} + #webroot, domain domainlist keylength issue() { if [ -z "$2" ]; then @@ -4228,6 +4300,8 @@ issue() { _local_addr="${13}" _challenge_alias="${14}" _preferred_chain="${15}" + _valid_from="${16}" + _valid_to="${17}" if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" @@ -4247,11 +4321,24 @@ issue() { Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) _debug Le_NextRenewTime "$Le_NextRenewTime" if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _valid_to_saved=$(_readdomainconf Le_Valid_to) + if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then + _info "The domain is set to be valid to: $_valid_to_saved" + _info "It can not be renewed automatically" + _info "See: $_VALIDITY_WIKI" + return $RENEW_SKIP + fi _saved_domain=$(_readdomainconf Le_Domain) _debug _saved_domain "$_saved_domain" _saved_alt=$(_readdomainconf Le_Alt) _debug _saved_alt "$_saved_alt" - if [ "$_saved_domain,$_saved_alt" = "$_main_domain,$_alt_domains" ]; then + _normized_saved_domains="$(echo "$_saved_domain,$_saved_alt" | tr "," "\n" | sort | tr '\n' ',')" + _debug _normized_saved_domains "$_normized_saved_domains" + + _normized_domains="$(echo "$_main_domain,$_alt_domains" | tr "," "\n" | sort | tr '\n' ',')" + _debug _normized_domains "$_normized_domains" + + if [ "$_normized_saved_domains" = "$_normized_domains" ]; then _info "Domains not changed." _info "Skip, Next renewal time is: $(__green "$(_readdomainconf Le_NextRenewTimeStr)")" _info "Add '$(__red '--force')' to force to renew." @@ -4299,10 +4386,6 @@ issue() { _alt_domains="" fi - if [ "$_key_length" = "$NO_VALUE" ]; then - _key_length="" - fi - if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then _err "_on_before_issue." return 1 @@ -4323,7 +4406,13 @@ issue() { if [ -f "$CSR_PATH" ] && [ ! -f "$CERT_KEY_PATH" ]; then _info "Signing from existing CSR." else + # When renewing from an old version, the empty Le_Keylength means 2048. + # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over + # time but an empty value implies 2048 specifically. _key=$(_readdomainconf Le_Keylength) + if [ -z "$_key" ]; then + _key=2048 + fi _debug "Read key length:$_key" if [ ! -f "$CERT_KEY_PATH" ] || [ "$_key_length" != "$_key" ] || [ "$Le_ForceNewDomainKey" = "1" ]; then if ! createDomainKey "$_main_domain" "$_key_length"; then @@ -4363,12 +4452,52 @@ issue() { _identifiers="$_identifiers,{\"type\":\"$(_getIdType "$d")\",\"value\":\"$(_idn "$d")\"}" done _debug2 _identifiers "$_identifiers" - if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then + _notBefore="" + _notAfter="" + + if [ "$_valid_from" ]; then + _savedomainconf "Le_Valid_From" "$_valid_from" + _debug2 "_valid_from" "$_valid_from" + _notBefore="$(_convertValidaty "" "$_valid_from")" + if [ "$?" != "0" ]; then + _err "Can not parse _valid_from: $_valid_from" + return 1 + fi + if [ "$(_time)" -gt "$(_date2time "$_notBefore")" ]; then + _notBefore="" + fi + else + _cleardomainconf "Le_Valid_From" + fi + _debug2 _notBefore "$_notBefore" + + if [ "$_valid_to" ]; then + _debug2 "_valid_to" "$_valid_to" + _savedomainconf "Le_Valid_To" "$_valid_to" + _notAfter="$(_convertValidaty "$_notBefore" "$_valid_to")" + if [ "$?" != "0" ]; then + _err "Can not parse _valid_to: $_valid_to" + return 1 + fi + else + _cleardomainconf "Le_Valid_To" + fi + _debug2 "_notAfter" "$_notAfter" + + _newOrderObj="{\"identifiers\": [$_identifiers]" + if [ "$_notBefore" ]; then + _newOrderObj="$_newOrderObj,\"notBefore\": \"$_notBefore\"" + fi + if [ "$_notAfter" ]; then + _newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\"" + fi + if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then _err "Create new order error." _clearup _on_issue_err "$_post_hook" return 1 fi + Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n " | cut -d ":" -f 2-)" _debug Le_LinkOrder "$Le_LinkOrder" Le_OrderFinalize="$(echo "$response" | _egrep_o '"finalize" *: *"[^"]*"' | cut -d '"' -f 4)" @@ -4407,7 +4536,7 @@ issue() { response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" - _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2 | tr -d ' "')" + _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2- | tr -d ' "')" if _contains "$response" "\"wildcard\" *: *true"; then _d="*.$_d" fi @@ -4557,6 +4686,7 @@ $_authorizations_map" _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')" fi _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")" + test "$_d_alias" = "$NO_VALUE" && _d_alias="" _alias_index="$(_math "$_alias_index" + 1)" _debug "_d_alias" "$_d_alias" if [ "$_d_alias" ]; then @@ -4851,7 +4981,7 @@ $_authorizations_map" return 1 fi _debug "sleep 2 secs to verify again" - sleep 2 + _sleep 2 _debug "checking" _send_signed_request "$uri" @@ -5028,7 +5158,7 @@ $_authorizations_map" Le_CertCreateTime=$(_time) _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime" - Le_CertCreateTimeStr=$(date -u) + Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime") _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then @@ -5068,13 +5198,20 @@ $_authorizations_map" else _cleardomainconf Le_ForceNewDomainKey fi - - Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) - - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + if [ "$_notAfter" ]; then + Le_NextRenewTime=$(_date2time "$_notAfter") + Le_NextRenewTimeStr="$_notAfter" + if [ "$_valid_to" ] && ! _startswith "$_valid_to" "+"; then + _info "The domain is set to be valid to: $_valid_to" + _info "It can not be renewed automatically" + _info "See: $_VALIDITY_WIKI" + fi + else + Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) + fi _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" - - Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then @@ -5110,17 +5247,20 @@ _split_cert_chain() { fi } -#domain [isEcc] +#domain [isEcc] [server] renew() { Le_Domain="$1" if [ -z "$Le_Domain" ]; then - _usage "Usage: $PROJECT_ENTRY --renew --domain [--ecc]" + _usage "Usage: $PROJECT_ENTRY --renew --domain [--ecc] [--server server]" return 1 fi _isEcc="$2" + _renewServer="$3" + _debug "_renewServer" "$_renewServer" _initpath "$Le_Domain" "$_isEcc" + _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _info "$(__green "Renew: '$Le_Domain'")" if [ ! -f "$DOMAIN_CONF" ]; then @@ -5134,24 +5274,34 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" - if [ -z "$Le_API" ] || [ "$CA_LETSENCRYPT_V1" = "$Le_API" ]; then - #if this is from an old version, Le_API is empty, - #so, we force to use letsencrypt server - Le_API="$CA_LETSENCRYPT_V2" - fi - if [ "$Le_API" ]; then - if [ "$Le_API" != "$ACME_DIRECTORY" ]; then - _clearAPI - fi - export ACME_DIRECTORY="$Le_API" - #reload ca configs - ACCOUNT_KEY_PATH="" - ACCOUNT_JSON_PATH="" - CA_CONF="" - _debug3 "initpath again." - _initpath "$Le_Domain" "$_isEcc" + case "$Le_API" in + "$CA_LETSENCRYPT_V2_TEST") + _info "Switching back to $CA_LETSENCRYPT_V2" + Le_API="$CA_LETSENCRYPT_V2" + ;; + "$CA_BUYPASS_TEST") + _info "Switching back to $CA_BUYPASS" + Le_API="$CA_BUYPASS" + ;; + "$CA_GOOGLE_TEST") + _info "Switching back to $CA_GOOGLE" + Le_API="$CA_GOOGLE" + ;; + esac + + if [ "$_server" ]; then + Le_API="$_server" fi + _info "Renew to Le_API=$Le_API" + + _clearAPI + _clearCA + export ACME_DIRECTORY="$Le_API" + + #reload ca configs + _debug2 "initpath again." + _initpath "$Le_Domain" "$_isEcc" if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then _info "Skip, Next renewal time is: $(__green "$Le_NextRenewTimeStr")" @@ -5175,7 +5325,14 @@ renew() { Le_PostHook="$(_readdomainconf Le_PostHook)" Le_RenewHook="$(_readdomainconf Le_RenewHook)" Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)" - issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" + # When renewing from an old version, the empty Le_Keylength means 2048. + # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over + # time but an empty value implies 2048 specifically. + Le_Keylength="$(_readdomainconf Le_Keylength)" + if [ -z "$Le_Keylength" ]; then + Le_Keylength=2048 + fi + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" res="$?" if [ "$res" != "0" ]; then return "$res" @@ -5202,11 +5359,16 @@ renew() { return "$res" } -#renewAll [stopRenewOnError] +#renewAll [stopRenewOnError] [server] renewAll() { _initpath + _clearCA _stopRenewOnError="$1" _debug "_stopRenewOnError" "$_stopRenewOnError" + + _server="$2" + _debug "_server" "$_server" + _ret="0" _success_msg="" _error_msg="" @@ -5229,7 +5391,7 @@ renewAll() { _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) fi - renew "$d" "$_isEcc" + renew "$d" "$_isEcc" "$_server" ) rc="$?" _debug "Return code: $rc" @@ -5395,10 +5557,13 @@ showcsr() { _initpath _csrsubj=$(_readSubjectFromCSR "$_csrfile") - if [ "$?" != "0" ] || [ -z "$_csrsubj" ]; then + if [ "$?" != "0" ]; then _err "Can not read subject from csr: $_csrfile" return 1 fi + if [ -z "$_csrsubj" ]; then + _info "The Subject is empty" + fi _info "Subject=$_csrsubj" @@ -5611,8 +5776,9 @@ _installcert() { if [ -f "$_real_key" ]; then cat "$CERT_KEY_PATH" >"$_real_key" || return 1 else - cat "$CERT_KEY_PATH" >"$_real_key" || return 1 + touch "$_real_key" || return 1 chmod 600 "$_real_key" + cat "$CERT_KEY_PATH" >"$_real_key" || return 1 fi fi @@ -6610,6 +6776,11 @@ Parameters: If no match, the default offered chain will be used. (default: empty) See: $_PREFERRED_CHAIN_WIKI + --valid-to Request the NotAfter field of the cert. + See: $_VALIDITY_WIKI + --valid-from Request the NotBefore field of the cert. + See: $_VALIDITY_WIKI + -f, --force Force install, force cert renewal or override sudo restrictions. --staging, --test Use staging server, for testing. --debug [0|1|2|3] Output debug info. Defaults to 1 if argument is omitted. @@ -6801,6 +6972,10 @@ _processAccountConf() { } _checkSudo() { + if [ -z "__INTERACTIVE" ]; then + #don't check if it's not in an interactive shell + return 0 + fi if [ "$SUDO_GID" ] && [ "$SUDO_COMMAND" ] && [ "$SUDO_USER" ] && [ "$SUDO_UID" ]; then if [ "$SUDO_USER" = "root" ] && [ "$SUDO_UID" = "0" ]; then #it's root using sudo, no matter it's using sudo or not, just fine @@ -6922,8 +7097,8 @@ _process() { _altdomains="$NO_VALUE" _webroot="" _challenge_alias="" - _keylength="" - _accountkeylength="" + _keylength="$DEFAULT_DOMAIN_KEY_LENGTH" + _accountkeylength="$DEFAULT_ACCOUNT_KEY_LENGTH" _cert_file="" _key_file="" _ca_file="" @@ -6970,6 +7145,8 @@ _process() { _eab_kid="" _eab_hmac_key="" _preferred_chain="" + _valid_from="" + _valid_to="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7277,6 +7454,14 @@ _process() { Le_RenewalDays="$_days" shift ;; + --valid-from) + _valid_from="$2" + shift + ;; + --valid-to) + _valid_to="$2" + shift + ;; --httpport) _httpport="$2" Le_HTTPPort="$_httpport" @@ -7479,6 +7664,7 @@ _process() { if [ "$_server" ]; then _selectServer "$_server" "${_ecc:-$_keylength}" + _server="$ACME_DIRECTORY" fi if [ "${_CMD}" != "install" ]; then @@ -7538,7 +7724,7 @@ _process() { uninstall) uninstall "$_nocron" ;; upgrade) upgrade ;; issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" ;; deploy) deploy "$_domain" "$_deploy_hook" "$_ecc" @@ -7553,10 +7739,10 @@ _process() { installcert "$_domain" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_ecc" ;; renew) - renew "$_domain" "$_ecc" + renew "$_domain" "$_ecc" "$_server" ;; renewAll) - renewAll "$_stopRenewOnError" + renewAll "$_stopRenewOnError" "$_server" ;; revoke) revoke "$_domain" "$_ecc" "$_revoke_reason" diff --git a/deploy/mailcow.sh b/deploy/mailcow.sh index c3535e7e..3492cea4 100644 --- a/deploy/mailcow.sh +++ b/deploy/mailcow.sh @@ -20,18 +20,23 @@ mailcow_deploy() { _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" - _mailcow_path="${DEPLOY_MAILCOW_PATH}" + _getdeployconf DEPLOY_MAILCOW_PATH + _getdeployconf DEPLOY_MAILCOW_RELOAD - if [ -z "$_mailcow_path" ]; then + _debug DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH" + _debug DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD" + + if [ -z "$DEPLOY_MAILCOW_PATH" ]; then _err "Mailcow path is not found, please define DEPLOY_MAILCOW_PATH." return 1 fi - #Tests if _ssl_path is the mailcow root directory. - if [ -f "${_mailcow_path}/generate_config.sh" ]; then - _ssl_path="${_mailcow_path}/data/assets/ssl/" - else - _ssl_path="${_mailcow_path}" + _savedeployconf DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH" + [ -n "$DEPLOY_MAILCOW_RELOAD" ] && _savedeployconf DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD" + + _ssl_path="$DEPLOY_MAILCOW_PATH" + if [ -f "$DEPLOY_MAILCOW_PATH/generate_config.sh" ]; then + _ssl_path="$DEPLOY_MAILCOW_PATH/data/assets/ssl/" fi if [ ! -d "$_ssl_path" ]; then @@ -40,16 +45,15 @@ mailcow_deploy() { fi # ECC or RSA - if [ -z "${Le_Keylength}" ]; then - Le_Keylength="" - fi - if _isEccKey "${Le_Keylength}"; then + length=$(_readdomainconf Le_Keylength) + if _isEccKey "$length"; then _info "ECC key type detected" _cert_name_prefix="ecdsa-" else _info "RSA key type detected" _cert_name_prefix="" fi + _info "Copying key and cert" _real_key="$_ssl_path/${_cert_name_prefix}key.pem" if ! cat "$_ckey" >"$_real_key"; then @@ -63,7 +67,7 @@ mailcow_deploy() { return 1 fi - DEFAULT_MAILCOW_RELOAD="docker restart $(docker ps -qaf name=postfix-mailcow); docker restart $(docker ps -qaf name=nginx-mailcow); docker restart $(docker ps -qaf name=dovecot-mailcow)" + DEFAULT_MAILCOW_RELOAD="docker restart \$(docker ps --quiet --filter name=nginx-mailcow --filter name=dovecot-mailcow)" _reload="${DEPLOY_MAILCOW_RELOAD:-$DEFAULT_MAILCOW_RELOAD}" _info "Run reload: $_reload" diff --git a/deploy/routeros.sh b/deploy/routeros.sh index 9965d65c..c4c9470d 100644 --- a/deploy/routeros.sh +++ b/deploy/routeros.sh @@ -23,6 +23,7 @@ # ```sh # export ROUTER_OS_USERNAME=certuser # export ROUTER_OS_HOST=router.example.com +# export ROUTER_OS_PORT=22 # # acme.sh --deploy -d ftp.example.com --deploy-hook routeros # ``` @@ -48,6 +49,16 @@ # One optional thing to do as well is to create a script that updates # all the required services and run that script in a single command. # +# To adopt parameters to `scp` and/or `ssh` set the optional +# `ROUTER_OS_SSH_CMD` and `ROUTER_OS_SCP_CMD` variables accordingly, +# see ssh(1) and scp(1) for parameters to those commands. +# +# Example: +# ```ssh +# export ROUTER_OS_SSH_CMD="ssh -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts" +# export ROUTER_OS_SCP_CMD="scp -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts" +# ```` +# # returns 0 means success, otherwise error. ######## Public functions ##################### @@ -59,6 +70,7 @@ routeros_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" + _err_code=0 _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" @@ -80,6 +92,27 @@ routeros_deploy() { return 1 fi + _getdeployconf ROUTER_OS_PORT + + if [ -z "$ROUTER_OS_PORT" ]; then + _debug "Using default port 22 as ROUTER_OS_PORT, please set if not correct." + ROUTER_OS_PORT=22 + fi + + _getdeployconf ROUTER_OS_SSH_CMD + + if [ -z "$ROUTER_OS_SSH_CMD" ]; then + _debug "Use default ssh setup." + ROUTER_OS_SSH_CMD="ssh -p $ROUTER_OS_PORT" + fi + + _getdeployconf ROUTER_OS_SCP_CMD + + if [ -z "$ROUTER_OS_SCP_CMD" ]; then + _debug "USe default scp setup." + ROUTER_OS_SCP_CMD="scp -P $ROUTER_OS_PORT" + fi + _getdeployconf ROUTER_OS_ADDITIONAL_SERVICES if [ -z "$ROUTER_OS_ADDITIONAL_SERVICES" ]; then @@ -89,16 +122,26 @@ routeros_deploy() { _savedeployconf ROUTER_OS_HOST "$ROUTER_OS_HOST" _savedeployconf ROUTER_OS_USERNAME "$ROUTER_OS_USERNAME" + _savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT" + _savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD" + _savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD" _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" - _info "Trying to push key '$_ckey' to router" - scp "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key" - _info "Trying to push cert '$_cfullchain' to router" - scp "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer" - DEPLOY_SCRIPT_CMD="/system script add name=\"LE Cert Deploy - $_cdomain\" owner=admin policy=ftp,read,write,password,sensitive \ -source=\"## generated by routeros deploy script in acme.sh;\ -\n/certificate remove [ find name=$_cdomain.cer_0 ];\ + # push key to routeros + if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then + return $_err_code + fi + + # push certificate chain to routeros + if ! _scp_certificate "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"; then + return $_err_code + fi + + DEPLOY_SCRIPT_CMD="/system script add name=\"LE Cert Deploy - $_cdomain\" owner=$ROUTER_OS_USERNAME \ +comment=\"generated by routeros deploy script in acme.sh\" \ +source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\ \n/certificate remove [ find name=$_cdomain.cer_1 ];\ +\n/certificate remove [ find name=$_cdomain.cer_2 ];\ \ndelay 1;\ \n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\ \n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\ @@ -110,12 +153,51 @@ source=\"## generated by routeros deploy script in acme.sh;\ \n$ROUTER_OS_ADDITIONAL_SERVICES;\ \n\" " - # shellcheck disable=SC2029 - ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$DEPLOY_SCRIPT_CMD" - # shellcheck disable=SC2029 - ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "/system script run \"LE Cert Deploy - $_cdomain\"" - # shellcheck disable=SC2029 - ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "/system script remove \"LE Cert Deploy - $_cdomain\"" + + if ! _ssh_remote_cmd "$DEPLOY_SCRIPT_CMD"; then + return $_err_code + fi + + if ! _ssh_remote_cmd "/system script run \"LE Cert Deploy - $_cdomain\""; then + return $_err_code + fi + + if ! _ssh_remote_cmd "/system script remove \"LE Cert Deploy - $_cdomain\""; then + return $_err_code + fi return 0 } + +# inspired by deploy/ssh.sh +_ssh_remote_cmd() { + _cmd="$1" + _secure_debug "Remote commands to execute: $_cmd" + _info "Submitting sequence of commands to routeros" + # quotations in bash cmd below intended. Squash travis spellcheck error + # shellcheck disable=SC2029 + $ROUTER_OS_SSH_CMD "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$_cmd" + _err_code="$?" + + if [ "$_err_code" != "0" ]; then + _err "Error code $_err_code returned from routeros" + fi + + return $_err_code +} + +_scp_certificate() { + _src="$1" + _dst="$2" + _secure_debug "scp '$_src' to '$_dst'" + _info "Push key '$_src' to routeros" + + $ROUTER_OS_SCP_CMD "$_src" "$_dst" + _err_code="$?" + + if [ "$_err_code" != "0" ]; then + _err "Error code $_err_code returned from scp" + fi + + return $_err_code +} diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 66e28f93..f30f82c0 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -94,7 +94,12 @@ synology_dsm_deploy() { otp_code="" if [ -n "$SYNO_TOTP_SECRET" ]; then - otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)" + if _exists oathtool; then + otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)" + else + _err "oathtool could not be found, install oathtool to use SYNO_TOTP_SECRET" + return 1 + fi fi if [ -n "$SYNO_DID" ]; then diff --git a/deploy/truenas.sh b/deploy/truenas.sh index 6f1a31b0..84cfd5f4 100644 --- a/deploy/truenas.sh +++ b/deploy/truenas.sh @@ -38,7 +38,7 @@ truenas_deploy() { _getdeployconf DEPLOY_TRUENAS_APIKEY if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then - _err "TrueNAS Api Key is not found, please define DEPLOY_TRUENAS_APIKEY." + _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable." return 1 fi _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY" @@ -62,15 +62,14 @@ truenas_deploy() { _info "Testing Connection TrueNAS" _response=$(_get "$_api_url/system/state") - _info "TrueNAS System State: $_response." + _info "TrueNAS system state: $_response." if [ -z "$_response" ]; then _err "Unable to authenticate to $_api_url." - _err 'Check your Connection and set DEPLOY_TRUENAS_HOSTNAME="192.168.178.x".' - _err 'or' - _err 'set DEPLOY_TRUENAS_HOSTNAME="".' - _err 'Check your Connection and set DEPLOY_TRUENAS_SCHEME="https".' - _err "Check your Api Key." + _err 'Check your connection settings are correct, e.g.' + _err 'DEPLOY_TRUENAS_HOSTNAME="192.168.x.y" or DEPLOY_TRUENAS_HOSTNAME="truenas.example.com".' + _err 'DEPLOY_TRUENAS_SCHEME="https" or DEPLOY_TRUENAS_SCHEME="http".' + _err "Verify your TrueNAS API key is valid and set correctly, e.g. DEPLOY_TRUENAS_APIKEY=xxxx...." return 1 fi @@ -78,7 +77,7 @@ truenas_deploy() { _savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" _savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME" - _info "Getting active certificate from TrueNAS" + _info "Getting current active certificate from TrueNAS" _response=$(_get "$_api_url/system/general") _active_cert_id=$(echo "$_response" | grep -B2 '"name":' | grep 'id' | tr -d -- '"id: ,') _active_cert_name=$(echo "$_response" | grep '"name":' | sed -n 's/.*: "\(.\{1,\}\)",$/\1/p') @@ -88,14 +87,14 @@ truenas_deploy() { _debug Active_UI_http_redirect "$_param_httpsredirect" if [ "$DEPLOY_TRUENAS_SCHEME" = "http" ] && [ "$_param_httpsredirect" = "true" ]; then - _info "http Redirect active" + _info "HTTP->HTTPS redirection is enabled" _info "Setting DEPLOY_TRUENAS_SCHEME to 'https'" DEPLOY_TRUENAS_SCHEME="https" _api_url="$DEPLOY_TRUENAS_SCHEME://$DEPLOY_TRUENAS_HOSTNAME/api/v2.0" _savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME" fi - _info "Upload new certifikate to TrueNAS" + _info "Uploading new certificate to TrueNAS" _certname="Letsencrypt_$(_utc_date | tr ' ' '_' | tr -d -- ':')" _debug3 _certname "$_certname" @@ -104,30 +103,30 @@ truenas_deploy() { _debug3 _add_cert_result "$_add_cert_result" - _info "Getting Certificate list to get new Cert ID" + _info "Fetching list of installed certificates" _cert_list=$(_get "$_api_url/system/general/ui_certificate_choices") _cert_id=$(echo "$_cert_list" | grep "$_certname" | sed -n 's/.*"\([0-9]\{1,\}\)".*$/\1/p') _debug3 _cert_id "$_cert_id" - _info "Activate Certificate ID: $_cert_id" + _info "Current activate certificate ID: $_cert_id" _activateData="{\"ui_certificate\": \"${_cert_id}\"}" _activate_result="$(_post "$_activateData" "$_api_url/system/general" "" "PUT" "application/json")" _debug3 _activate_result "$_activate_result" - _info "Check if WebDAV certificate is the same as the WEB UI" + _info "Checking if WebDAV certificate is the same as the TrueNAS web UI" _webdav_list=$(_get "$_api_url/webdav") _webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,') if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then - _info "Update the WebDAV Certificate" + _info "Updating the WebDAV certificate" _debug _webdav_cert_id "$_webdav_cert_id" _webdav_data="{\"certssl\": \"${_cert_id}\"}" _activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")" - _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | sed -n 's/.*: \([0-9]\{1,\}\) }$/\1/p') + _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p') if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then - _info "WebDAV Certificate update successfully" + _info "WebDAV certificate updated successfully" else _err "Unable to set WebDAV certificate" _debug3 _activate_webdav_cert "$_activate_webdav_cert" @@ -136,21 +135,21 @@ truenas_deploy() { fi _debug3 _webdav_new_cert_id "$_webdav_new_cert_id" else - _info "WebDAV certificate not set or not the same as Web UI" + _info "WebDAV certificate is not configured or is not the same as TrueNAS web UI" fi - _info "Check if FTP certificate is the same as the WEB UI" + _info "Checking if FTP certificate is the same as the TrueNAS web UI" _ftp_list=$(_get "$_api_url/ftp") _ftp_cert_id=$(echo "$_ftp_list" | grep '"ssltls_certificate":' | tr -d -- '"certislfa:_ ,') if [ "$_ftp_cert_id" = "$_active_cert_id" ]; then - _info "Update the FTP Certificate" + _info "Updating the FTP certificate" _debug _ftp_cert_id "$_ftp_cert_id" _ftp_data="{\"ssltls_certificate\": \"${_cert_id}\"}" _activate_ftp_cert="$(_post "$_ftp_data" "$_api_url/ftp" "" "PUT" "application/json")" - _ftp_new_cert_id=$(echo "$_activate_ftp_cert" | _json_decode | sed -n 's/.*: \([0-9]\{1,\}\) }$/\1/p') + _ftp_new_cert_id=$(echo "$_activate_ftp_cert" | _json_decode | grep '"ssltls_certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p') if [ "$_ftp_new_cert_id" -eq "$_cert_id" ]; then - _info "FTP Certificate update successfully" + _info "FTP certificate updated successfully" else _err "Unable to set FTP certificate" _debug3 _activate_ftp_cert "$_activate_ftp_cert" @@ -159,22 +158,45 @@ truenas_deploy() { fi _debug3 _activate_ftp_cert "$_activate_ftp_cert" else - _info "FTP certificate not set or not the same as Web UI" + _info "FTP certificate is not configured or is not the same as TrueNAS web UI" fi - _info "Delete old Certificate" + _info "Checking if S3 certificate is the same as the TrueNAS web UI" + _s3_list=$(_get "$_api_url/s3") + _s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,') + + if [ "$_s3_cert_id" = "$_active_cert_id" ]; then + _info "Updating the S3 certificate" + _debug _s3_cert_id "$_s3_cert_id" + _s3_data="{\"certificate\": \"${_cert_id}\"}" + _activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")" + _s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p') + if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then + _info "S3 certificate updated successfully" + else + _err "Unable to set S3 certificate" + _debug3 _activate_s3_cert "$_activate_s3_cert" + _debug3 _s3_new_cert_id "$_s3_new_cert_id" + return 1 + fi + _debug3 _activate_s3_cert "$_activate_s3_cert" + else + _info "S3 certificate is not configured or is not the same as TrueNAS web UI" + fi + + _info "Deleting old certificate" _delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")" _debug3 _delete_result "$_delete_result" - _info "Reload WebUI from TrueNAS" + _info "Reloading TrueNAS web UI" _restart_UI=$(_get "$_api_url/system/general/ui_restart") _debug2 _restart_UI "$_restart_UI" if [ -n "$_add_cert_result" ] && [ -n "$_activate_result" ]; then return 0 else - _err "Certupdate was not succesfull, please use --debug" + _err "Certificate update was not succesful, please try again with --debug" return 1 fi } diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh index db0cbe15..6accc597 100755 --- a/dnsapi/dns_1984hosting.sh +++ b/dnsapi/dns_1984hosting.sh @@ -42,7 +42,7 @@ dns_1984hosting_add() { _debug "Add TXT record $fulldomain with value '$txtvalue'" value="$(printf '%s' "$txtvalue" | _url_encode)" - url="https://management.1984hosting.com/domains/entry/" + url="https://1984.hosting/domains/entry/" postdata="entry=new" postdata="$postdata&type=TXT" @@ -95,7 +95,7 @@ dns_1984hosting_rm() { _debug _domain "$_domain" _debug "Delete $fulldomain TXT record" - url="https://management.1984hosting.com/domains" + url="https://1984.hosting/domains" if ! _get_zone_id "$url" "$_domain"; then _err "invalid zone" "$_domain" return 1 @@ -138,7 +138,7 @@ _1984hosting_login() { _debug "Login to 1984Hosting as user $One984HOSTING_Username" username=$(printf '%s' "$One984HOSTING_Username" | _url_encode) password=$(printf '%s' "$One984HOSTING_Password" | _url_encode) - url="https://management.1984hosting.com/accounts/checkuserauth/" + url="https://1984.hosting/accounts/checkuserauth/" response="$(_post "username=$username&password=$password&otpkey=" $url)" response="$(echo "$response" | _normalizeJson)" @@ -175,7 +175,7 @@ _check_cookies() { return 1 fi - _authget "https://management.1984hosting.com/accounts/loginstatus/" + _authget "https://1984.hosting/accounts/loginstatus/" if _contains "$response" '"ok": true'; then _debug "Cached cookies still valid" return 0 @@ -204,7 +204,7 @@ _get_root() { return 1 fi - _authget "https://management.1984hosting.com/domains/soacheck/?zone=$h&nameserver=ns0.1984.is." + _authget "https://1984.hosting/domains/soacheck/?zone=$h&nameserver=ns0.1984.is." if _contains "$_response" "serial" && ! _contains "$_response" "null"; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain="$h" @@ -251,11 +251,11 @@ _htmlget() { # add extra headers to request _authpost() { - url="https://management.1984hosting.com/domains" + url="https://1984.hosting/domains" _get_zone_id "$url" "$_domain" csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")" export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE" - export _H2="Referer: https://management.1984hosting.com/domains/$_zone_id" + export _H2="Referer: https://1984.hosting/domains/$_zone_id" export _H3="X-CSRFToken: $csrf_header" _response=$(_post "$1" "$2") } diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index 14a4594d..376936f5 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -152,7 +152,7 @@ dns_aws_rm() { _get_root() { domain=$1 - i=2 + i=1 p=1 if aws_rest GET "2013-04-01/hostedzone"; then diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 36799dcd..c2430086 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -25,9 +25,15 @@ dns_cf_add() { CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}" if [ "$CF_Token" ]; then - _saveaccountconf_mutable CF_Token "$CF_Token" - _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID" - _saveaccountconf_mutable CF_Zone_ID "$CF_Zone_ID" + if [ "$CF_Zone_ID" ]; then + _savedomainconf CF_Token "$CF_Token" + _savedomainconf CF_Account_ID "$CF_Account_ID" + _savedomainconf CF_Zone_ID "$CF_Zone_ID" + else + _saveaccountconf_mutable CF_Token "$CF_Token" + _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID" + _saveaccountconf_mutable CF_Zone_ID "$CF_Zone_ID" + fi else if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then CF_Key="" diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh new file mode 100644 index 00000000..4b39f365 --- /dev/null +++ b/dnsapi/dns_curanet.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env sh + +#Script to use with curanet.dk, scannet.dk, wannafind.dk, dandomain.dk DNS management. +#Requires api credentials with scope: dns +#Author: Peter L. Hansen +#Version 1.0 + +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 ##################### + +#Usage: dns_curanet_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_curanet_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using curanet" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + CURANET_AUTHCLIENTID="${CURANET_AUTHCLIENTID:-$(_readaccountconf_mutable CURANET_AUTHCLIENTID)}" + CURANET_AUTHSECRET="${CURANET_AUTHSECRET:-$(_readaccountconf_mutable CURANET_AUTHSECRET)}" + if [ -z "$CURANET_AUTHCLIENTID" ] || [ -z "$CURANET_AUTHSECRET" ]; then + CURANET_AUTHCLIENTID="" + CURANET_AUTHSECRET="" + _err "You don't specify curanet api client and secret." + _err "Please create your auth info and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable CURANET_AUTHCLIENTID "$CURANET_AUTHCLIENTID" + _saveaccountconf_mutable CURANET_AUTHSECRET "$CURANET_AUTHSECRET" + + if ! _get_token; then + _err "Unable to get token" + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + export _H1="Content-Type: application/json-patch+json" + export _H2="Accept: application/json" + export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" + data="{\"name\": \"$fulldomain\",\"type\": \"TXT\",\"ttl\": 60,\"priority\": 0,\"data\": \"$txtvalue\"}" + response="$(_post "$data" "$CURANET_REST_URL/${_domain}/Records" "" "")" + + if _contains "$response" "$txtvalue"; then + _debug "TXT record added OK" + else + _err "Unable to add TXT record" + return 1 + fi + + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_curanet_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using curanet" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + CURANET_AUTHCLIENTID="${CURANET_AUTHCLIENTID:-$(_readaccountconf_mutable CURANET_AUTHCLIENTID)}" + CURANET_AUTHSECRET="${CURANET_AUTHSECRET:-$(_readaccountconf_mutable CURANET_AUTHSECRET)}" + + if ! _get_token; then + _err "Unable to get token" + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug "Getting current record list to identify TXT to delete" + + export _H1="Content-Type: application/json" + export _H2="Accept: application/json" + export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" + + response="$(_get "$CURANET_REST_URL/${_domain}/Records" "" "")" + + if ! _contains "$response" "$txtvalue"; then + _err "Unable to delete record (does not contain $txtvalue )" + return 1 + fi + + recordid=$(echo "$response" | _egrep_o "{\"id\":[0-9]+,\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":60,\"priority\":0,\"data\":\"..$txtvalue" | _egrep_o "id\":[0-9]+" | cut -c 5-) + + if [ -z "$recordid" ]; then + _err "Unable to get recordid" + _debug "regex {\"id\":[0-9]+,\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":60,\"priority\":0,\"data\":\"..$txtvalue" + _debug "response $response" + return 1 + fi + + _debug "Deleting recordID $recordid" + response="$(_post "" "$CURANET_REST_URL/${_domain}/Records/$recordid" "" "DELETE")" + return 0 +} + +#################### Private functions below ################################## + +_get_token() { + response="$(_post "grant_type=client_credentials&client_id=$CURANET_AUTHCLIENTID&client_secret=$CURANET_AUTHSECRET&scope=dns" "$CURANET_AUTH_URL" "" "")" + if ! _contains "$response" "access_token"; then + _err "Unable get access token" + return 1 + fi + CURANET_ACCESS_TOKEN=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]+" | cut -c 17-) + + if [ -z "$CURANET_ACCESS_TOKEN" ]; then + _err "Unable to get token" + return 1 + fi + + return 0 + +} + +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + export _H1="Content-Type: application/json" + export _H2="Accept: application/json" + export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" + response="$(_get "$CURANET_REST_URL/$h/Records" "" "")" + + if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then + _domain=$h + return 0 + fi + + i=$(_math "$i" + 1) + done + return 1 +} diff --git a/dnsapi/dns_cx.sh b/dnsapi/dns_cx.sh deleted file mode 100755 index c287d507..00000000 --- a/dnsapi/dns_cx.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# CloudXNS Domain api -# -#CX_Key="1234" -# -#CX_Secret="sADDsdasdgdsf" - -CX_Api="https://www.cloudxns.net/api2" - -#REST_API -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_cx_add() { - fulldomain=$1 - txtvalue=$2 - - CX_Key="${CX_Key:-$(_readaccountconf_mutable CX_Key)}" - CX_Secret="${CX_Secret:-$(_readaccountconf_mutable CX_Secret)}" - if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then - CX_Key="" - CX_Secret="" - _err "You don't specify cloudxns.net api key or secret yet." - _err "Please create you key and try again." - return 1 - fi - - REST_API="$CX_Api" - - #save the api key and email to the account conf file. - _saveaccountconf_mutable CX_Key "$CX_Key" - _saveaccountconf_mutable CX_Secret "$CX_Secret" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - - add_record "$_domain" "$_sub_domain" "$txtvalue" -} - -#fulldomain txtvalue -dns_cx_rm() { - fulldomain=$1 - txtvalue=$2 - CX_Key="${CX_Key:-$(_readaccountconf_mutable CX_Key)}" - CX_Secret="${CX_Secret:-$(_readaccountconf_mutable CX_Secret)}" - REST_API="$CX_Api" - if _get_root "$fulldomain"; then - record_id="" - existing_records "$_domain" "$_sub_domain" "$txtvalue" - if [ "$record_id" ]; then - _rest DELETE "record/$record_id/$_domain_id" "{}" - _info "Deleted record ${fulldomain}" - fi - fi -} - -#usage: root sub -#return if the sub record already exists. -#echos the existing records count. -# '0' means doesn't exist -existing_records() { - _debug "Getting txt records" - root=$1 - sub=$2 - if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100"; then - return 1 - fi - - seg=$(printf "%s\n" "$response" | _egrep_o '"record_id":[^{]*host":"'"$_sub_domain"'"[^}]*\}') - _debug seg "$seg" - if [ -z "$seg" ]; then - return 0 - fi - - if printf "%s" "$response" | grep '"type":"TXT"' >/dev/null; then - record_id=$(printf "%s\n" "$seg" | _egrep_o '"record_id":"[^"]*"' | cut -d : -f 2 | tr -d \" | _head_n 1) - _debug record_id "$record_id" - return 0 - fi - -} - -#add the txt record. -#usage: root sub txtvalue -add_record() { - root=$1 - sub=$2 - txtvalue=$3 - fulldomain="$sub.$root" - - _info "Adding record" - - if ! _rest POST "record" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then - return 1 - fi - - return 0 -} - -#################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -# _domain_id=sdjkglgdfewsdfg -_get_root() { - domain=$1 - i=2 - p=1 - - if ! _rest GET "domain"; then - return 1 - fi - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f $i-100) - _debug h "$h" - if [ -z "$h" ]; then - #not valid - return 1 - fi - - if _contains "$response" "$h."; then - seg=$(printf "%s\n" "$response" | _egrep_o '"id":[^{]*"'"$h"'."[^}]*}') - _debug seg "$seg" - _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") - _debug _domain_id "$_domain_id" - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) - _debug _sub_domain "$_sub_domain" - _domain="$h" - _debug _domain "$_domain" - return 0 - fi - return 1 - fi - p="$i" - i=$(_math "$i" + 1) - done - return 1 -} - -#Usage: method URI data -_rest() { - m=$1 - ep="$2" - _debug ep "$ep" - url="$REST_API/$ep" - _debug url "$url" - - cdate=$(date -u "+%Y-%m-%d %H:%M:%S UTC") - _debug cdate "$cdate" - - data="$3" - _debug data "$data" - - sec="$CX_Key$url$data$cdate$CX_Secret" - _debug sec "$sec" - hmac=$(printf "%s" "$sec" | _digest md5 hex) - _debug hmac "$hmac" - - export _H1="API-KEY: $CX_Key" - export _H2="API-REQUEST-DATE: $cdate" - export _H3="API-HMAC: $hmac" - export _H4="Content-Type: application/json" - - if [ "$data" ]; then - response="$(_post "$data" "$url" "" "$m")" - else - response="$(_get "$url")" - fi - - if [ "$?" != "0" ]; then - _err "error $ep" - return 1 - fi - _debug2 response "$response" - - _contains "$response" '"code":1' - -} diff --git a/dnsapi/dns_fornex.sh b/dnsapi/dns_fornex.sh new file mode 100644 index 00000000..53be307a --- /dev/null +++ b/dnsapi/dns_fornex.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env sh + +#Author: Timur Umarov + +FORNEX_API_URL="https://fornex.com/api/dns/v0.1" + +######## Public functions ##################### + +#Usage: dns_fornex_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_fornex_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _Fornex_API; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Unable to determine root domain" + return 1 + else + _debug _domain "$_domain" + fi + + _info "Adding record" + if _rest POST "$_domain/entry_set/add/" "host=$fulldomain&type=TXT&value=$txtvalue&apikey=$FORNEX_API_KEY"; then + _debug _response "$response" + if _contains "$response" '"ok": true' || _contains "$response" 'Такая запись уже существует.'; then + _info "Added, OK" + return 0 + fi + fi + _err "Add txt record error." + return 1 +} + +#Usage: dns_fornex_rm _acme-challenge.www.domain.com +dns_fornex_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _Fornex_API; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Unable to determine root domain" + return 1 + else + _debug _domain "$_domain" + fi + + _debug "Getting txt records" + _rest GET "$_domain/entry_set.json?apikey=$FORNEX_API_KEY" + + if ! _contains "$response" "$txtvalue"; then + _err "Txt record not found" + return 1 + fi + + _record_id="$(echo "$response" | _egrep_o "{[^{]*\"value\"*:*\"$txtvalue\"[^}]*}" | sed -n -e 's#.*"id": \([0-9]*\).*#\1#p')" + _debug "_record_id" "$_record_id" + if [ -z "$_record_id" ]; then + _err "can not find _record_id" + return 1 + fi + + if ! _rest POST "$_domain/entry_set/$_record_id/delete/" "apikey=$FORNEX_API_KEY"; then + _err "Delete record error." + return 1 + fi + return 0 +} + +#################### Private functions below ################################## + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + + i=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _rest GET "domain_list.json?q=$h&apikey=$FORNEX_API_KEY"; then + return 1 + fi + + if _contains "$response" "\"$h\"" >/dev/null; then + _domain=$h + return 0 + else + _debug "$h not found" + fi + i=$(_math "$i" + 1) + done + + return 1 +} + +_Fornex_API() { + FORNEX_API_KEY="${FORNEX_API_KEY:-$(_readaccountconf_mutable FORNEX_API_KEY)}" + if [ -z "$FORNEX_API_KEY" ]; then + FORNEX_API_KEY="" + + _err "You didn't specify the Fornex API key yet." + _err "Please create your key and try again." + + return 1 + fi + + _saveaccountconf_mutable FORNEX_API_KEY "$FORNEX_API_KEY" +} + +#method method action data +_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Accept: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$FORNEX_API_URL/$ep" "" "$m")" + else + response="$(_get "$FORNEX_API_URL/$ep" | _normalizeJson)" + fi + + _ret="$?" + if [ "$_ret" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_gdnsdk.sh b/dnsapi/dns_gdnsdk.sh deleted file mode 100755 index 90842b25..00000000 --- a/dnsapi/dns_gdnsdk.sh +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env sh -#Author: Herman Sletteng -#Report Bugs here: https://github.com/loial/acme.sh -# -# -# Note, gratisdns requires a login first, so the script needs to handle -# temporary cookies. Since acme.sh _get/_post currently don't directly support -# cookies, I've defined wrapper functions _myget/_mypost to set the headers - -GDNSDK_API="https://admin.gratisdns.com" -######## Public functions ##################### -#Usage: dns_gdnsdk_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_gdnsdk_add() { - fulldomain=$1 - txtvalue=$2 - _info "Using gratisdns.dk" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - if ! _gratisdns_login; then - _err "Login failed!" - return 1 - fi - #finding domain zone - if ! _get_domain; then - _err "No matching root domain for $fulldomain found" - return 1 - fi - # adding entry - _info "Adding the entry" - _mypost "action=dns_primary_record_added_txt&user_domain=$_domain&name=$fulldomain&txtdata=$txtvalue&ttl=1" - if _successful_update; then return 0; fi - _err "Couldn't create entry!" - return 1 -} - -#Usage: fulldomain txtvalue -#Remove the txt record after validation. -dns_gdnsdk_rm() { - fulldomain=$1 - txtvalue=$2 - _info "Using gratisdns.dk" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - if ! _gratisdns_login; then - _err "Login failed!" - return 1 - fi - if ! _get_domain; then - _err "No matching root domain for $fulldomain found" - return 1 - fi - _findentry "$fulldomain" "$txtvalue" - if [ -z "$_id" ]; then - _info "Entry doesn't exist, nothing to delete" - return 0 - fi - _debug "Deleting record..." - _mypost "action=dns_primary_delete_txt&user_domain=$_domain&id=$_id" - # removing entry - - if _successful_update; then return 0; fi - _err "Couldn't delete entry!" - return 1 -} - -#################### Private functions below ################################## - -_checkcredentials() { - GDNSDK_Username="${GDNSDK_Username:-$(_readaccountconf_mutable GDNSDK_Username)}" - GDNSDK_Password="${GDNSDK_Password:-$(_readaccountconf_mutable GDNSDK_Password)}" - - if [ -z "$GDNSDK_Username" ] || [ -z "$GDNSDK_Password" ]; then - GDNSDK_Username="" - GDNSDK_Password="" - _err "You haven't specified gratisdns.dk username and password yet." - _err "Please add credentials and try again." - return 1 - fi - #save the credentials to the account conf file. - _saveaccountconf_mutable GDNSDK_Username "$GDNSDK_Username" - _saveaccountconf_mutable GDNSDK_Password "$GDNSDK_Password" - return 0 -} - -_checkcookie() { - GDNSDK_Cookie="${GDNSDK_Cookie:-$(_readaccountconf_mutable GDNSDK_Cookie)}" - if [ -z "$GDNSDK_Cookie" ]; then - _debug "No cached cookie found" - return 1 - fi - _myget "action=" - if (echo "$_result" | grep -q "logmeout"); then - _debug "Cached cookie still valid" - return 0 - fi - _debug "Cached cookie no longer valid" - GDNSDK_Cookie="" - _saveaccountconf_mutable GDNSDK_Cookie "$GDNSDK_Cookie" - return 1 -} - -_gratisdns_login() { - if ! _checkcredentials; then return 1; fi - - if _checkcookie; then - _debug "Already logged in" - return 0 - fi - _debug "Logging into GratisDNS with user $GDNSDK_Username" - - if ! _mypost "login=$GDNSDK_Username&password=$GDNSDK_Password&action=logmein"; then - _err "GratisDNS login failed for user $GDNSDK_Username bad RC from _post" - return 1 - fi - - GDNSDK_Cookie="$(grep -A 15 '302 Found' "$HTTP_HEADER" | _egrep_o 'Cookie: [^;]*' | _head_n 1 | cut -d ' ' -f2)" - - if [ -z "$GDNSDK_Cookie" ]; then - _err "GratisDNS login failed for user $GDNSDK_Username. Check $HTTP_HEADER file" - return 1 - fi - export GDNSDK_Cookie - _saveaccountconf_mutable GDNSDK_Cookie "$GDNSDK_Cookie" - return 0 -} - -_myget() { - #Adds cookie to request - export _H1="Cookie: $GDNSDK_Cookie" - _result=$(_get "$GDNSDK_API?$1") -} -_mypost() { - #Adds cookie to request - export _H1="Cookie: $GDNSDK_Cookie" - _result=$(_post "$1" "$GDNSDK_API") -} - -_get_domain() { - _myget 'action=dns_primarydns' - _domains=$(echo "$_result" | _egrep_o ' domain="[[:alnum:]._-]+' | sed 's/^.*"//') - if [ -z "$_domains" ]; then - _err "Primary domain list not found!" - return 1 - fi - for _domain in $_domains; do - if (_endswith "$fulldomain" "$_domain"); then - _debug "Root domain: $_domain" - return 0 - fi - done - return 1 -} - -_successful_update() { - if (echo "$_result" | grep -q 'table-success'); then return 0; fi - return 1 -} - -_findentry() { - #args $1: fulldomain, $2: txtvalue - #returns id of dns entry, if it exists - _myget "action=dns_primary_changeDNSsetup&user_domain=$_domain" - _debug3 "_result: $_result" - - _tmp_result=$(echo "$_result" | tr -d '\n\r' | _egrep_o "$1\s*$2[^?]*[^&]*&id=[^&]*") - _debug _tmp_result "$_tmp_result" - if [ -z "${_tmp_result:-}" ]; then - _debug "The variable is _tmp_result is not supposed to be empty, there may be something wrong with the script" - fi - - _id=$(echo "$_tmp_result" | sed 's/^.*=//') - if [ -n "$_id" ]; then - _debug "Entry found with _id=$_id" - return 0 - fi - return 1 -} diff --git a/dnsapi/dns_geoscaling.sh b/dnsapi/dns_geoscaling.sh new file mode 100755 index 00000000..6ccf4daf --- /dev/null +++ b/dnsapi/dns_geoscaling.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env sh + +######################################################################## +# Geoscaling hook script for acme.sh +# +# Environment variables: +# +# - $GEOSCALING_Username (your Geoscaling username - this is usually NOT an amail address) +# - $GEOSCALING_Password (your Geoscaling password) + +#-- dns_geoscaling_add() - Add TXT record -------------------------------------- +# Usage: dns_geoscaling_add _acme-challenge.subdomain.domain.com "XyZ123..." + +dns_geoscaling_add() { + full_domain=$1 + txt_value=$2 + _info "Using DNS-01 Geoscaling DNS2 hook" + + GEOSCALING_Username="${GEOSCALING_Username:-$(_readaccountconf_mutable GEOSCALING_Username)}" + GEOSCALING_Password="${GEOSCALING_Password:-$(_readaccountconf_mutable GEOSCALING_Password)}" + if [ -z "$GEOSCALING_Username" ] || [ -z "$GEOSCALING_Password" ]; then + GEOSCALING_Username= + GEOSCALING_Password= + _err "No auth details provided. Please set user credentials using the \$GEOSCALING_Username and \$GEOSCALING_Password environment variables." + return 1 + fi + _saveaccountconf_mutable GEOSCALING_Username "${GEOSCALING_Username}" + _saveaccountconf_mutable GEOSCALING_Password "${GEOSCALING_Password}" + + # Fills in the $zone_id and $zone_name + find_zone "${full_domain}" || return 1 + _debug "Zone id '${zone_id}' will be used." + + # We're logged in here + + # we should add ${full_domain} minus the trailing ${zone_name} + + prefix=$(echo "${full_domain}" | sed "s|\\.${zone_name}\$||") + + body="id=${zone_id}&name=${prefix}&type=TXT&content=${txt_value}&ttl=300&prio=0" + + do_post "$body" "https://www.geoscaling.com/dns2/ajax/add_record.php" + exit_code="$?" + if [ "${exit_code}" -eq 0 ]; then + _info "TXT record added successfully." + else + _err "Couldn't add the TXT record." + fi + do_logout + return "${exit_code}" +} + +#-- dns_geoscaling_rm() - Remove TXT record ------------------------------------ +# Usage: dns_geoscaling_rm _acme-challenge.subdomain.domain.com "XyZ123..." + +dns_geoscaling_rm() { + full_domain=$1 + txt_value=$2 + _info "Cleaning up after DNS-01 Geoscaling DNS2 hook" + + GEOSCALING_Username="${GEOSCALING_Username:-$(_readaccountconf_mutable GEOSCALING_Username)}" + GEOSCALING_Password="${GEOSCALING_Password:-$(_readaccountconf_mutable GEOSCALING_Password)}" + if [ -z "$GEOSCALING_Username" ] || [ -z "$GEOSCALING_Password" ]; then + GEOSCALING_Username= + GEOSCALING_Password= + _err "No auth details provided. Please set user credentials using the \$GEOSCALING_Username and \$GEOSCALING_Password environment variables." + return 1 + fi + _saveaccountconf_mutable GEOSCALING_Username "${GEOSCALING_Username}" + _saveaccountconf_mutable GEOSCALING_Password "${GEOSCALING_Password}" + + # fills in the $zone_id + find_zone "${full_domain}" || return 1 + _debug "Zone id '${zone_id}' will be used." + + # Here we're logged in + # Find the record id to clean + + # get the domain + response=$(do_get "https://www.geoscaling.com/dns2/index.php?module=domain&id=${zone_id}") + _debug2 "response" "$response" + + table="$(echo "${response}" | tr -d '\n' | sed 's|.*
Basic Records
.*||')" + _debug2 table "${table}" + names=$(echo "${table}" | _egrep_o 'id="[0-9]+\.name">[^<]*' | sed 's|||; s|.*>||') + ids=$(echo "${table}" | _egrep_o 'id="[0-9]+\.name">[^<]*' | sed 's|\.name">.*||; s|id="||') + types=$(echo "${table}" | _egrep_o 'id="[0-9]+\.type">[^<]*' | sed 's|||; s|.*>||') + values=$(echo "${table}" | _egrep_o 'id="[0-9]+\.content">[^<]*' | sed 's|||; s|.*>||') + + _debug2 names "${names}" + _debug2 ids "${ids}" + _debug2 types "${types}" + _debug2 values "${values}" + + # look for line whose name is ${full_domain}, whose type is TXT, and whose value is ${txt_value} + line_num="$(echo "${values}" | grep -F -n -- "${txt_value}" | _head_n 1 | cut -d ':' -f 1)" + _debug2 line_num "${line_num}" + found_id= + if [ -n "$line_num" ]; then + type=$(echo "${types}" | sed -n "${line_num}p") + name=$(echo "${names}" | sed -n "${line_num}p") + id=$(echo "${ids}" | sed -n "${line_num}p") + + _debug2 type "$type" + _debug2 name "$name" + _debug2 id "$id" + _debug2 full_domain "$full_domain" + + if [ "${type}" = "TXT" ] && [ "${name}" = "${full_domain}" ]; then + found_id=${id} + fi + fi + + if [ "${found_id}" = "" ]; then + _err "Can not find record id." + return 0 + fi + + # Remove the record + body="id=${zone_id}&record_id=${found_id}" + response=$(do_post "$body" "https://www.geoscaling.com/dns2/ajax/delete_record.php") + exit_code="$?" + if [ "$exit_code" -eq 0 ]; then + _info "Record removed successfully." + else + _err "Could not clean (remove) up the record. Please go to Geoscaling administration interface and clean it by hand." + fi + do_logout + return "${exit_code}" +} + +########################## PRIVATE FUNCTIONS ########################### + +do_get() { + _url=$1 + export _H1="Cookie: $geoscaling_phpsessid_cookie" + _get "${_url}" +} + +do_post() { + _body=$1 + _url=$2 + export _H1="Cookie: $geoscaling_phpsessid_cookie" + _post "${_body}" "${_url}" +} + +do_login() { + + _info "Logging in..." + + username_encoded="$(printf "%s" "${GEOSCALING_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${GEOSCALING_Password}" | _url_encode)" + body="username=${username_encoded}&password=${password_encoded}" + + response=$(_post "$body" "https://www.geoscaling.com/dns2/index.php?module=auth") + _debug2 response "${response}" + + #retcode=$(grep '^HTTP[^ ]*' "${HTTP_HEADER}" | _head_n 1 | _egrep_o '[0-9]+$') + retcode=$(grep '^HTTP[^ ]*' "${HTTP_HEADER}" | _head_n 1 | cut -d ' ' -f 2) + + if [ "$retcode" != "302" ]; then + _err "Geoscaling login failed for user ${GEOSCALING_Username}. Check ${HTTP_HEADER} file" + return 1 + fi + + geoscaling_phpsessid_cookie="$(grep -i '^set-cookie:' "${HTTP_HEADER}" | _egrep_o 'PHPSESSID=[^;]*;' | tr -d ';')" + return 0 + +} + +do_logout() { + _info "Logging out." + response="$(do_get "https://www.geoscaling.com/dns2/index.php?module=auth")" + _debug2 response "$response" + return 0 +} + +find_zone() { + domain="$1" + + # do login + do_login || return 1 + + # get zones + response="$(do_get "https://www.geoscaling.com/dns2/index.php?module=domains")" + + table="$(echo "${response}" | tr -d '\n' | sed 's|.*
Your domains
.*||')" + _debug2 table "${table}" + zone_names="$(echo "${table}" | _egrep_o '[^<]*' | sed 's|||;s|||')" + _debug2 _matches "${zone_names}" + # Zone names and zone IDs are in same order + zone_ids=$(echo "${table}" | _egrep_o '' | sed 's|.*id=||;s|. .*||') + + _debug2 "These are the zones on this Geoscaling account:" + _debug2 "zone_names" "${zone_names}" + _debug2 "And these are their respective IDs:" + _debug2 "zone_ids" "${zone_ids}" + if [ -z "${zone_names}" ] || [ -z "${zone_ids}" ]; then + _err "Can not get zone names or IDs." + return 1 + fi + # Walk through all possible zone names + strip_counter=1 + while true; do + attempted_zone=$(echo "${domain}" | cut -d . -f ${strip_counter}-) + + # All possible zone names have been tried + if [ -z "${attempted_zone}" ]; then + _err "No zone for domain '${domain}' found." + return 1 + fi + + _debug "Looking for zone '${attempted_zone}'" + + line_num="$(echo "${zone_names}" | grep -n "^${attempted_zone}\$" | _head_n 1 | cut -d : -f 1)" + _debug2 line_num "${line_num}" + if [ "$line_num" ]; then + zone_id=$(echo "${zone_ids}" | sed -n "${line_num}p") + zone_name=$(echo "${zone_names}" | sed -n "${line_num}p") + if [ -z "${zone_id}" ]; then + _err "Can not find zone id." + return 1 + fi + _debug "Found relevant zone '${attempted_zone}' with id '${zone_id}' - will be used for domain '${domain}'." + return 0 + fi + + _debug "Zone '${attempted_zone}' doesn't exist, let's try a less specific zone." + strip_counter=$(_math "${strip_counter}" + 1) + done +} +# vim: et:ts=2:sw=2: diff --git a/dnsapi/dns_ispconfig.sh b/dnsapi/dns_ispconfig.sh index 765e0eb5..6f0e920f 100755 --- a/dnsapi/dns_ispconfig.sh +++ b/dnsapi/dns_ispconfig.sh @@ -32,7 +32,7 @@ dns_ispconfig_rm() { #################### Private functions below ################################## _ISPC_credentials() { - if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -n "${ISPC_Api_Insecure}" ]; then + if [ -z "${ISPC_User}" ] || [ -z "${ISPC_Password}" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then ISPC_User="" ISPC_Password="" ISPC_Api="" diff --git a/dnsapi/dns_loopia.sh b/dnsapi/dns_loopia.sh index 7760b53e..399c7867 100644 --- a/dnsapi/dns_loopia.sh +++ b/dnsapi/dns_loopia.sh @@ -32,8 +32,12 @@ dns_loopia_add() { _info "Adding record" - _loopia_add_sub_domain "$_domain" "$_sub_domain" - _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue" + if ! _loopia_add_sub_domain "$_domain" "$_sub_domain"; then + return 1 + fi + if ! _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"; then + return 1 + fi } @@ -70,12 +74,13 @@ dns_loopia_rm() { %s - ' "$LOOPIA_User" "$LOOPIA_Password" "$_domain" "$_sub_domain") + ' "$LOOPIA_User" "$Encoded_Password" "$_domain" "$_sub_domain") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" "OK"; then - _err "Error could not get txt records" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error could not get txt records: $err_response" return 1 fi } @@ -101,6 +106,12 @@ _loopia_load_config() { return 1 fi + if _contains "$LOOPIA_Password" "'" || _contains "$LOOPIA_Password" '"'; then + _err "Password contains quoute or double quoute and this is not supported by dns_loopia.sh" + return 1 + fi + + Encoded_Password=$(_xml_encode "$LOOPIA_Password") return 0 } @@ -133,11 +144,12 @@ _loopia_get_records() { %s - ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain") + ' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" ""; then - _err "Error" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error: $err_response" return 1 fi return 0 @@ -162,7 +174,7 @@ _get_root() { %s - ' $LOOPIA_User $LOOPIA_Password) + ' "$LOOPIA_User" "$Encoded_Password") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" while true; do @@ -206,32 +218,35 @@ _loopia_add_record() { %s - - - type - TXT - - - priority - 0 - - - ttl - 300 - - - rdata - %s - - + + + + type + TXT + + + priority + 0 + + + ttl + 300 + + + rdata + %s + + + - ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain" "$txtval") + ' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain" "$txtval") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" "OK"; then - _err "Error" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error: $err_response" return 1 fi return 0 @@ -255,7 +270,7 @@ _sub_domain_exists() { %s - ' $LOOPIA_User $LOOPIA_Password "$domain") + ' "$LOOPIA_User" "$Encoded_Password" "$domain") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" @@ -290,13 +305,22 @@ _loopia_add_sub_domain() { %s - ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain") + ' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" "OK"; then - _err "Error" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error: $err_response" return 1 fi return 0 } + +_xml_encode() { + encoded_string=$1 + encoded_string=$(echo "$encoded_string" | sed 's/&/\&/') + encoded_string=$(echo "$encoded_string" | sed 's//\>/') + printf "%s" "$encoded_string" +} diff --git a/dnsapi/dns_mydevil.sh b/dnsapi/dns_mydevil.sh index 2f398959..953290af 100755 --- a/dnsapi/dns_mydevil.sh +++ b/dnsapi/dns_mydevil.sh @@ -74,7 +74,7 @@ dns_mydevil_rm() { validRecords="^${num}${w}${fulldomain}${w}TXT${w}${any}${txtvalue}$" for id in $(devil dns list "$domain" | tail -n+2 | grep "${validRecords}" | cut -w -s -f 1); do _info "Removing record $id from domain $domain" - devil dns del "$domain" "$id" || _err "Could not remove DNS record." + echo "y" | devil dns del "$domain" "$id" || _err "Could not remove DNS record." done } @@ -87,7 +87,9 @@ mydevil_get_domain() { domain="" for domain in $(devil dns list | cut -w -s -f 1 | tail -n+2); do + _debug "Checking domain: $domain" if _endswith "$fulldomain" "$domain"; then + _debug "Fulldomain '$fulldomain' matches '$domain'" printf -- "%s" "$domain" return 0 fi diff --git a/dnsapi/dns_netlify.sh b/dnsapi/dns_netlify.sh index 2ce13e2b..65e803c5 100644 --- a/dnsapi/dns_netlify.sh +++ b/dnsapi/dns_netlify.sh @@ -114,7 +114,7 @@ _get_root() { fi if _contains "$response" "\"name\":\"$h\"" >/dev/null; then - _domain_id=$(echo "$response" | _egrep_o "\"[^\"]*\",\"name\":\"$h" | cut -d , -f 1 | tr -d \") + _domain_id=$(echo "$response" | _egrep_o "\"[^\"]*\",\"name\":\"$h\"" | cut -d , -f 1 | tr -d \") if [ "$_domain_id" ]; then if [ "$i" = 1 ]; then #create the record at the domain apex (@) if only the domain name was provided as --domain-alias diff --git a/dnsapi/dns_opnsense.sh b/dnsapi/dns_opnsense.sh index 26a422f8..eb95902f 100755 --- a/dnsapi/dns_opnsense.sh +++ b/dnsapi/dns_opnsense.sh @@ -150,8 +150,7 @@ _get_root() { return 1 fi _debug h "$h" - id=$(echo "$_domain_response" | _egrep_o "\"[^\"]*\":{\"enabled\":\"1\",\"type\":{\"master\":{\"value\":\"master\",\"selected\":1},\"slave\":{\"value\":\"slave\",\"selected\":0}},\"masterip\":{\"\":{[^}]*}}(,\"allownotifyslave\":{\"\":{[^}]*}},|,)\"domainname\":\"${h}\"" | cut -d ':' -f 1 | cut -d '"' -f 2) - + id=$(echo "$_domain_response" | _egrep_o "\"[^\"]*\":{\"enabled\":\"1\",\"type\":{\"master\":{\"value\":\"master\",\"selected\":1},\"slave\":{\"value\":\"slave\",\"selected\":0}},\"masterip\":{\"[^\"]*\":{[^}]*}},\"transferkeyalgo\":{[^{]*{[^{]*{[^{]*{[^{]*{[^{]*{[^{]*{[^{]*{[^}]*}},\"transferkey\":\"[^\"]*\"(,\"allownotifyslave\":{\"\":{[^}]*}},|,)\"domainname\":\"${h}\"" | cut -d ':' -f 1 | cut -d '"' -f 2) if [ -n "$id" ]; then _debug id "$id" _host=$(printf "%s" "$domain" | cut -d . -f 1-$p) diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index e65babbd..b382e52f 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -198,6 +198,8 @@ dns_ovh_rm() { if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then return 1 fi + _ovh_rest POST "domain/zone/$_domain/refresh" + _debug "Refresh:$response" return 0 fi done diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh index 437e5e5c..6a8d0e18 100644 --- a/dnsapi/dns_simply.sh +++ b/dnsapi/dns_simply.sh @@ -5,8 +5,8 @@ #SIMPLY_AccountName="accountname" #SIMPLY_ApiKey="apikey" # -#SIMPLY_Api="https://api.simply.com/1/[ACCOUNTNAME]/[APIKEY]" -SIMPLY_Api_Default="https://api.simply.com/1" +#SIMPLY_Api="https://api.simply.com/2/" +SIMPLY_Api_Default="https://api.simply.com/2" #This is used for determining success of REST call SIMPLY_SUCCESS_CODE='"status":200' @@ -237,12 +237,18 @@ _simply_rest() { _debug2 ep "$ep" _debug2 m "$m" - export _H1="Content-Type: application/json" + basicauth=$(printf "%s:%s" "$SIMPLY_AccountName" "$SIMPLY_ApiKey" | _base64) + + if [ "$basicauth" ]; then + export _H1="Authorization: Basic $basicauth" + fi + + export _H2="Content-Type: application/json" if [ "$m" != "GET" ]; then - response="$(_post "$data" "$SIMPLY_Api/$SIMPLY_AccountName/$SIMPLY_ApiKey/$ep" "" "$m")" + response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")" else - response="$(_get "$SIMPLY_Api/$SIMPLY_AccountName/$SIMPLY_ApiKey/$ep")" + response="$(_get "$SIMPLY_Api/$ep")" fi if [ "$?" != "0" ]; then diff --git a/dnsapi/dns_udr.sh b/dnsapi/dns_udr.sh new file mode 100644 index 00000000..caada826 --- /dev/null +++ b/dnsapi/dns_udr.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env sh + +# united-domains Reselling (https://www.ud-reselling.com/) DNS API +# Author: Andreas Scherer (https://github.com/andischerer) +# Created: 2021-02-01 +# +# Set the environment variables as below: +# +# export UDR_USER="your_username_goes_here" +# export UDR_PASS="some_password_goes_here" +# + +UDR_API="https://api.domainreselling.de/api/call.cgi" +UDR_TTL="30" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "some_long_string_of_characters_go_here_from_lets_encrypt" +dns_udr_add() { + fulldomain=$1 + txtvalue=$2 + + UDR_USER="${UDR_USER:-$(_readaccountconf_mutable UDR_USER)}" + UDR_PASS="${UDR_PASS:-$(_readaccountconf_mutable UDR_PASS)}" + if [ -z "$UDR_USER" ] || [ -z "$UDR_PASS" ]; then + UDR_USER="" + UDR_PASS="" + _err "You didn't specify an UD-Reselling username and password yet" + return 1 + fi + # save the username and password to the account conf file. + _saveaccountconf_mutable UDR_USER "$UDR_USER" + _saveaccountconf_mutable UDR_PASS "$UDR_PASS" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _dnszone "${_dnszone}" + + _debug "Getting txt records" + if ! _udr_rest "QueryDNSZoneRRList" "dnszone=${_dnszone}"; then + return 1 + fi + + rr="${fulldomain}. ${UDR_TTL} IN TXT ${txtvalue}" + _debug resource_record "${rr}" + if _contains "$response" "$rr" >/dev/null; then + _err "Error, it would appear that this record already exists. Please review existing TXT records for this domain." + return 1 + fi + + _info "Adding record" + if ! _udr_rest "UpdateDNSZone" "dnszone=${_dnszone}&addrr0=${rr}"; then + _err "Adding the record did not succeed, please verify/check." + return 1 + fi + + _info "Added, OK" + return 0 +} + +dns_udr_rm() { + fulldomain=$1 + txtvalue=$2 + + UDR_USER="${UDR_USER:-$(_readaccountconf_mutable UDR_USER)}" + UDR_PASS="${UDR_PASS:-$(_readaccountconf_mutable UDR_PASS)}" + if [ -z "$UDR_USER" ] || [ -z "$UDR_PASS" ]; then + UDR_USER="" + UDR_PASS="" + _err "You didn't specify an UD-Reselling username and password yet" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _dnszone "${_dnszone}" + + _debug "Getting txt records" + if ! _udr_rest "QueryDNSZoneRRList" "dnszone=${_dnszone}"; then + return 1 + fi + + rr="${fulldomain}. ${UDR_TTL} IN TXT ${txtvalue}" + _debug resource_record "${rr}" + if _contains "$response" "$rr" >/dev/null; then + if ! _udr_rest "UpdateDNSZone" "dnszone=${_dnszone}&delrr0=${rr}"; then + _err "Deleting the record did not succeed, please verify/check." + return 1 + fi + _info "Removed, OK" + return 0 + else + _info "Text record is not present, will not delete anything." + return 0 + fi +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + + if ! _udr_rest "QueryDNSZoneList" ""; then + return 1 + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "${response}" "${h}." >/dev/null; then + _dnszone=$(echo "$response" | _egrep_o "${h}") + if [ "$_dnszone" ]; then + return 0 + fi + return 1 + fi + i=$(_math "$i" + 1) + done + return 1 +} + +_udr_rest() { + if [ -n "$2" ]; then + data="command=$1&$2" + else + data="command=$1" + fi + + _debug data "${data}" + response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")" + + _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + _debug response_code "$_code" + _debug response_description "$_description" + + if [ ! "$_code" = "200" ]; then + _err "DNS-API-Error: $_description" + return 1 + fi + + return 0 +} diff --git a/dnsapi/dns_vercel.sh b/dnsapi/dns_vercel.sh new file mode 100644 index 00000000..7bf6b0e5 --- /dev/null +++ b/dnsapi/dns_vercel.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env sh + +# Vercel DNS API +# +# This is your API token which can be acquired on the account page. +# https://vercel.com/account/tokens +# +# VERCEL_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje" + +VERCEL_API="https://api.vercel.com" + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_vercel_add() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + VERCEL_TOKEN="${VERCEL_TOKEN:-$(_readaccountconf_mutable VERCEL_TOKEN)}" + + if [ -z "$VERCEL_TOKEN" ]; then + VERCEL_TOKEN="" + _err "You have not set the Vercel API token yet." + _err "Please visit https://vercel.com/account/tokens to generate it." + return 1 + fi + + _saveaccountconf_mutable VERCEL_TOKEN "$VERCEL_TOKEN" + + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _vercel_rest POST "v2/domains/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\"}"; then + if printf -- "%s" "$response" | grep "\"uid\":\"" >/dev/null; then + _info "Added" + return 0 + else + _err "Unexpected response while adding text record." + return 1 + fi + fi + _err "Add txt record error." +} + +dns_vercel_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _vercel_rest GET "v2/domains/$_domain/records" + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\"" | wc -l | tr -d " ") + + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + _record_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"slug\":\"[^,]*\",\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\",\"value\":\"$txtvalue\"" | cut -d: -f2 | cut -d, -f1 | tr -d '"') + + if [ "$_record_id" ]; then + echo "$_record_id" | while read -r item; do + if _vercel_rest DELETE "v2/domains/$_domain/records/$item"; then + _info "removed record" "$item" + return 0 + else + _err "failed to remove record" "$item" + return 1 + fi + done + fi + fi +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + ep="$2" + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _vercel_rest GET "v4/domains/$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_vercel_rest() { + m="$1" + ep="$2" + data="$3" + + path="$VERCEL_API/$ep" + + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $VERCEL_TOKEN" + + if [ "$m" != "GET" ]; then + _secure_debug2 data "$data" + response="$(_post "$data" "$path" "" "$m")" + else + response="$(_get "$path")" + fi + _ret="$?" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + _secure_debug2 response "$response" + if [ "$_ret" != "0" ]; then + _err "error $ep" + return 1 + fi + + response="$(printf "%s" "$response" | _normalizeJson)" + return 0 +} diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index 231c34b3..bcf256ff 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -24,7 +24,7 @@ dns_world4you_add() { fi export _H1="Cookie: W4YSESSID=$sessid" - form=$(_get "$WORLD4YOU_API/dashboard/paketuebersicht") + form=$(_get "$WORLD4YOU_API/") _get_paketnr "$fqdn" "$form" paketnr="$PAKETNR" if [ -z "$paketnr" ]; then @@ -54,15 +54,14 @@ dns_world4you_add() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | tr '\n' '\t' | sed 's/.*

[^\t]*\t *\([^\t]*\)\t.*/\1/') - if _contains "$msg" '^<\!DOCTYPE html>'; then - msg='Unknown error' - fi - _err "Unable to add record: $msg" - if _contains "$msg" '^<\!DOCTYPE html>'; then + msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>\|^\s*//g') + if [ "$msg" = '' ]; then + _err "Unable to add record: Unknown error" echo "$ret" >'error-01.html' echo "$res" >'error-02.html' _err "View error-01.html and error-02.html for debugging" + else + _err "Unable to add record: my.world4you.com: $msg" fi return 1 fi @@ -87,7 +86,7 @@ dns_world4you_rm() { fi export _H1="Cookie: W4YSESSID=$sessid" - form=$(_get "$WORLD4YOU_API/dashboard/paketuebersicht") + form=$(_get "$WORLD4YOU_API/") _get_paketnr "$fqdn" "$form" paketnr="$PAKETNR" if [ -z "$paketnr" ]; then @@ -119,15 +118,14 @@ dns_world4you_rm() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | tr '\n' '\t' | sed 's/.*

[^\t]*\t *\([^\t]*\)\t.*/\1/') - if _contains "$msg" '^<\!DOCTYPE html>'; then - msg='Unknown error' - fi - _err "Unable to remove record: $msg" - if _contains "$msg" '^<\!DOCTYPE html>'; then + msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>\|^\s*//g') + if [ "$msg" = '' ]; then + _err "Unable to remove record: Unknown error" echo "$ret" >'error-01.html' echo "$res" >'error-02.html' _err "View error-01.html and error-02.html for debugging" + else + _err "Unable to remove record: my.world4you.com: $msg" fi return 1 fi @@ -184,7 +182,7 @@ _get_paketnr() { fqdn="$1" form="$2" - domains=$(echo "$form" | grep '^ *[A-Za-z0-9_\.-]*\.[A-Za-z0-9_-]*$' | sed 's/^ *\(.*\)$/\1/') + domains=$(echo "$form" | grep 'header-paket-domain' | sed 's/<[^>]*>//g' | sed 's/^.*>\([^>]*\)$/\1/') domain='' for domain in $domains; do if _contains "$fqdn" "$domain\$"; then @@ -199,6 +197,6 @@ _get_paketnr() { TLD="$domain" _debug domain "$domain" RECORD=$(echo "$fqdn" | cut -c"1-$((${#fqdn} - ${#TLD} - 1))") - PAKETNR=$(echo "$form" | grep "data-textfilter=\".* $domain " | _head_n 1 | sed 's/^.* \([0-9]*\) .*$/\1/') + PAKETNR=$(echo "$form" | grep "data-textfilter=\".* $domain " | _tail_n 1 | sed "s|.*$WORLD4YOU_API/\\([0-9]*\\)/.*|\\1|") return 0 } diff --git a/notify/callmebotWhatsApp.sh b/notify/callmebotWhatsApp.sh new file mode 100644 index 00000000..1c15b283 --- /dev/null +++ b/notify/callmebotWhatsApp.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +#Support CallMeBot Whatsapp webhooks + +#CALLMEBOT_YOUR_PHONE_NO="" +#CALLMEBOT_API_KEY="" + +callmebotWhatsApp_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_statusCode" "$_statusCode" + + CALLMEBOT_YOUR_PHONE_NO="${CALLMEBOT_YOUR_PHONE_NO:-$(_readaccountconf_mutable CALLMEBOT_YOUR_PHONE_NO)}" + if [ -z "$CALLMEBOT_YOUR_PHONE_NO" ]; then + CALLMEBOT_YOUR_PHONE_NO="" + _err "You didn't specify a Slack webhook url CALLMEBOT_YOUR_PHONE_NO yet." + return 1 + fi + _saveaccountconf_mutable CALLMEBOT_YOUR_PHONE_NO "$CALLMEBOT_YOUR_PHONE_NO" + + CALLMEBOT_API_KEY="${CALLMEBOT_API_KEY:-$(_readaccountconf_mutable CALLMEBOT_API_KEY)}" + if [ "$CALLMEBOT_API_KEY" ]; then + _saveaccountconf_mutable CALLMEBOT_API_KEY "$CALLMEBOT_API_KEY" + fi + + _waUrl="https://api.callmebot.com/whatsapp.php" + + _Phone_No="$(printf "%s" "$CALLMEBOT_YOUR_PHONE_NO" | _url_encode)" + _apikey="$(printf "%s" "$CALLMEBOT_API_KEY" | _url_encode)" + _message="$(printf "*%s*\\n%s" "$_subject" "$_content" | _url_encode)" + + _finalUrl="$_waUrl?phone=$_Phone_No&apikey=$_apikey&text=$_message" + response="$(_get "$_finalUrl")" + + if [ "$?" = "0" ] && _contains ".

Message queued. You will receive it in a few seconds."; then + _info "wa send success." + return 0 + fi + _err "wa send error." + _debug "URL" "$_finalUrl" + _debug "Response" "$response" + return 1 +} diff --git a/notify/discord.sh b/notify/discord.sh new file mode 100644 index 00000000..58362a4e --- /dev/null +++ b/notify/discord.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env sh + +#Support Discord webhooks + +# Required: +#DISCORD_WEBHOOK_URL="" +# Optional: +#DISCORD_USERNAME="" +#DISCORD_AVATAR_URL="" + +discord_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_statusCode" "$_statusCode" + + DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-$(_readaccountconf_mutable DISCORD_WEBHOOK_URL)}" + if [ -z "$DISCORD_WEBHOOK_URL" ]; then + DISCORD_WEBHOOK_URL="" + _err "You didn't specify a Discord webhook url DISCORD_WEBHOOK_URL yet." + return 1 + fi + _saveaccountconf_mutable DISCORD_WEBHOOK_URL "$DISCORD_WEBHOOK_URL" + + DISCORD_USERNAME="${DISCORD_USERNAME:-$(_readaccountconf_mutable DISCORD_USERNAME)}" + if [ "$DISCORD_USERNAME" ]; then + _saveaccountconf_mutable DISCORD_USERNAME "$DISCORD_USERNAME" + fi + + DISCORD_AVATAR_URL="${DISCORD_AVATAR_URL:-$(_readaccountconf_mutable DISCORD_AVATAR_URL)}" + if [ "$DISCORD_AVATAR_URL" ]; then + _saveaccountconf_mutable DISCORD_AVATAR_URL "$DISCORD_AVATAR_URL" + fi + + export _H1="Content-Type: application/json" + + _content="$(printf "**%s**\n%s" "$_subject" "$_content" | _json_encode)" + _data="{\"content\": \"$_content\" " + if [ "$DISCORD_USERNAME" ]; then + _data="$_data, \"username\": \"$DISCORD_USERNAME\" " + fi + if [ "$DISCORD_AVATAR_URL" ]; then + _data="$_data, \"avatar_url\": \"$DISCORD_AVATAR_URL\" " + fi + _data="$_data}" + + if _post "$_data" "$DISCORD_WEBHOOK_URL?wait=true"; then + # shellcheck disable=SC2154 + if [ "$response" ]; then + _info "discord send success." + return 0 + fi + fi + _err "discord send error." + _err "$response" + return 1 +} diff --git a/notify/weixin_work.sh b/notify/weixin_work.sh new file mode 100644 index 00000000..bf3e9ad6 --- /dev/null +++ b/notify/weixin_work.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env sh + +#Support weixin work webhooks api + +#WEIXIN_WORK_WEBHOOK="xxxx" + +#optional +#WEIXIN_WORK_KEYWORD="yyyy" + +#`WEIXIN_WORK_SIGNING_KEY`="SEC08ffdbd403cbc3fc8a65xxxxxxxxxxxxxxxxxxxx" + +# subject content statusCode +weixin_work_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_subject" "$_subject" + _debug "_content" "$_content" + _debug "_statusCode" "$_statusCode" + + WEIXIN_WORK_WEBHOOK="${WEIXIN_WORK_WEBHOOK:-$(_readaccountconf_mutable WEIXIN_WORK_WEBHOOK)}" + if [ -z "$WEIXIN_WORK_WEBHOOK" ]; then + WEIXIN_WORK_WEBHOOK="" + _err "You didn't specify a weixin_work webhooks WEIXIN_WORK_WEBHOOK yet." + _err "You can get yours from https://work.weixin.qq.com/api/doc/90000/90136/91770" + return 1 + fi + _saveaccountconf_mutable WEIXIN_WORK_WEBHOOK "$WEIXIN_WORK_WEBHOOK" + + WEIXIN_WORK_KEYWORD="${WEIXIN_WORK_KEYWORD:-$(_readaccountconf_mutable WEIXIN_WORK_KEYWORD)}" + if [ "$WEIXIN_WORK_KEYWORD" ]; then + _saveaccountconf_mutable WEIXIN_WORK_KEYWORD "$WEIXIN_WORK_KEYWORD" + fi + + _content=$(echo "$_content" | _json_encode) + _subject=$(echo "$_subject" | _json_encode) + _data="{\"msgtype\": \"text\", \"text\": {\"content\": \"[$WEIXIN_WORK_KEYWORD]\n$_subject\n$_content\"}}" + + response="$(_post "$_data" "$WEIXIN_WORK_WEBHOOK" "" "POST" "application/json")" + + if [ "$?" = "0" ] && _contains "$response" "errmsg\":\"ok"; then + _info "weixin_work webhooks event fired success." + return 0 + fi + + _err "weixin_work webhooks event fired error." + _err "$response" + return 1 +}