diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 17e98ae3..615e5d8b 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -121,19 +121,19 @@ jobs: - name: Run acmetest run: | if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh @@ -184,19 +184,19 @@ jobs: shell: bash run: | if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh @@ -234,19 +234,19 @@ jobs: copyback: false run: | if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh @@ -285,19 +285,19 @@ jobs: copyback: false run: | if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh @@ -337,19 +337,19 @@ jobs: copyback: false run: | if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh @@ -389,19 +389,19 @@ jobs: copyback: false run: | if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh @@ -445,19 +445,19 @@ jobs: pkg set-mediator -v -I default@1.1 openssl export PATH=/usr/gnu/bin:$PATH if [ "${{ secrets.TokenName1}}" ] ; then - export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" fi if [ "${{ secrets.TokenName2}}" ] ; then - export ${{ secrets.TokenName2}}=${{ secrets.TokenValue2}} + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" fi if [ "${{ secrets.TokenName3}}" ] ; then - export ${{ secrets.TokenName3}}=${{ secrets.TokenValue3}} + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" fi if [ "${{ secrets.TokenName4}}" ] ; then - export ${{ secrets.TokenName4}}=${{ secrets.TokenValue4}} + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" fi if [ "${{ secrets.TokenName5}}" ] ; then - export ${{ secrets.TokenName5}}=${{ secrets.TokenValue5}} + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" fi cd ../acmetest ./letest.sh diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index 6daa9be4..5a0d81ba 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -62,7 +62,7 @@ jobs: nat: | "8080": "80" prepare: | - pkg install -y curl socat + pkg install -y curl socat libnghttp2 usesh: true run: | cd ../acmetest \ diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 33bcf23c..25872c42 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -61,7 +61,6 @@ jobs: nat: | "8080": "80" prepare: | - export PKG_PATH="https://cdn.NetBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r|cut -f '1 2' -d.)/All/" pkg_add curl socat usesh: true copyback: false diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index 7746645a..745a9408 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -66,7 +66,7 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" - prepare: pkg_add socat curl wget + prepare: pkg_add socat curl wget libnghttp2 usesh: true copyback: false run: | diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml index a6ec714c..4bf2ba29 100644 --- a/.github/workflows/Ubuntu.yml +++ b/.github/workflows/Ubuntu.yml @@ -80,9 +80,14 @@ jobs: -p 9000:9000 \ -e "DOCKER_STEPCA_INIT_NAME=Smallstep" \ -e "DOCKER_STEPCA_INIT_DNS_NAMES=localhost,$(hostname -f)" \ + -e "DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT=true" \ + -e "DOCKER_STEPCA_INIT_PASSWORD=test" \ --name stepca \ - smallstep/step-ca \ - && sleep 5 && docker exec stepca step ca provisioner add acme --type ACME \ + smallstep/step-ca:0.23.1 + + sleep 5 + docker exec stepca bash -c "echo test >test" \ + && docker exec stepca step ca provisioner add acme --type ACME --admin-subject step --admin-password-file=/home/step/test \ && docker exec stepca kill -1 1 \ && docker exec stepca cat /home/step/certs/root_ca.crt | sudo bash -c "cat - >>/etc/ssl/certs/ca-certificates.crt" - name: Clone acmetest diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index bd2c01aa..48c44429 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -28,9 +28,9 @@ jobs: id: step_one run: | if [ "$DOCKER_PASSWORD" ] ; then - echo "::set-output name=hasToken::true" + echo "hasToken=true" >>$GITHUB_OUTPUT else - echo "::set-output name=hasToken::false" + echo "hasToken=false" >>$GITHUB_OUTPUT fi - name: Check the value run: echo ${{ steps.step_one.outputs.hasToken }} @@ -43,9 +43,9 @@ jobs: - name: checkout code uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: login to docker hub run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin diff --git a/Dockerfile b/Dockerfile index 79fd1d89..2ad50e6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16.3 +FROM alpine:3.17 RUN apk --no-cache add -f \ openssl \ @@ -12,7 +12,8 @@ RUN apk --no-cache add -f \ oath-toolkit-oathtool \ tar \ libidn \ - jq + jq \ + cronie ENV LE_CONFIG_HOME /acme.sh @@ -25,7 +26,7 @@ COPY ./ /install_acme.sh/ RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null##' | crontab - +RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - RUN for verb in help \ version \ @@ -64,12 +65,10 @@ 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 && sleep infinity &\n \ - wait \n \ + exec crond -n -s -m off \n \ else \n \ exec -- \"\$@\"\n \ -fi" >/entry.sh && chmod +x /entry.sh +fi\n" >/entry.sh && chmod +x /entry.sh VOLUME /acme.sh diff --git a/README.md b/README.md index 30e6e554..73ff3321 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,12 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) - [ruby-china.org](https://ruby-china.org/topics/31983) - [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management) - [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89) -- [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt) - [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty) - [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709) - [Centminmod](https://centminmod.com/letsencrypt-acmetool-https.html) - [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297) -- [archlinux](https://www.archlinux.org/packages/community/any/acme.sh) - [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient) -- [CentOS Web Panel](http://centos-webpanel.com/) +- [CentOS Web Panel](https://control-webpanel.com) - [lnmp.org](https://lnmp.org/) - [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials) @@ -361,10 +359,6 @@ Ok, it's done. # 10. Issue ECC certificates -`Let's Encrypt` can now issue **ECDSA** certificates. - -And we support them too! - Just set the `keylength` parameter with a prefix `ec-`. For example: @@ -385,10 +379,12 @@ Please look at the `keylength` parameter above. Valid values are: -1. **ec-256 (prime256v1, "ECDSA P-256")** +1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)** 2. **ec-384 (secp384r1, "ECDSA P-384")** 3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)** - +4. **2048 (RSA2048)** +5. **3072 (RSA3072)** +6. **4096 (RSA4096)** # 11. Issue Wildcard certificates diff --git a/acme.sh b/acme.sh index 4775531a..e52c626c 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.0.5 +VER=3.0.7 PROJECT_NAME="acme.sh" @@ -53,8 +53,8 @@ CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$ DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" -DEFAULT_ACCOUNT_KEY_LENGTH=2048 -DEFAULT_DOMAIN_KEY_LENGTH=2048 +DEFAULT_ACCOUNT_KEY_LENGTH=ec-256 +DEFAULT_DOMAIN_KEY_LENGTH=ec-256 DEFAULT_OPENSSL_BIN="openssl" @@ -923,8 +923,16 @@ _sed_i() { fi } +if [ "$(echo abc | egrep -o b 2>/dev/null)" = "b" ]; then + __USE_EGREP=1 +else + __USE_EGREP="" +fi + _egrep_o() { - if ! egrep -o "$1" 2>/dev/null; then + if [ "$__USE_EGREP" ]; then + egrep -o "$1" + else sed -n 's/.*\('"$1"'\).*/\1/p' fi } @@ -1637,7 +1645,7 @@ _stat() { #keyfile _isRSA() { keyfile=$1 - if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || ${ACME_OPENSSL_BIN:-openssl} rsa -in "$keyfile" -noout -text | grep "^publicExponent:" >/dev/null 2>&1; then + if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || ${ACME_OPENSSL_BIN:-openssl} rsa -in "$keyfile" -noout -text 2>&1 | grep "^publicExponent:" 2>&1 >/dev/null; then return 0 fi return 1 @@ -1646,7 +1654,7 @@ _isRSA() { #keyfile _isEcc() { keyfile=$1 - if grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || ${ACME_OPENSSL_BIN:-openssl} ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" >/dev/null 2>&1; then + if grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || ${ACME_OPENSSL_BIN:-openssl} ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" 2>&1 >/dev/null; then return 0 fi return 1 @@ -1744,7 +1752,7 @@ _calcjwk() { _debug3 x64 "$x64" xend=$(_math "$xend" + 1) - y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-10000)" + y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-2048)" _debug3 y "$y" y64="$(printf "%s" "$y" | tr -d : | _h2b | _base64 | _url_replace)" @@ -1852,9 +1860,15 @@ _inithttp() { _ACME_CURL="$_ACME_CURL --cacert $CA_BUNDLE " fi - if _contains "$(curl --help 2>&1)" "--globoff"; then + if _contains "$(curl --help 2>&1)" "--globoff" || _contains "$(curl --help curl 2>&1)" "--globoff"; then _ACME_CURL="$_ACME_CURL -g " fi + + #don't use --fail-with-body + ##from curl 7.76: return fail on HTTP errors but keep the body + #if _contains "$(curl --help http 2>&1)" "--fail-with-body"; then + # _ACME_CURL="$_ACME_CURL --fail-with-body " + #fi fi if [ -z "$_ACME_WGET" ] && _exists "wget"; then @@ -1872,11 +1886,11 @@ _inithttp() { elif [ "$CA_BUNDLE" ]; then _ACME_WGET="$_ACME_WGET --ca-certificate=$CA_BUNDLE " fi - fi - #from wget 1.14: do not skip body on 404 error - if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--content-on-error"; then - _ACME_WGET="$_ACME_WGET --content-on-error " + #from wget 1.14: do not skip body on 404 error + if _contains "$(wget --help 2>&1)" "--content-on-error"; then + _ACME_WGET="$_ACME_WGET --content-on-error " + fi fi __HTTP_INITIALIZED=1 @@ -2058,7 +2072,7 @@ _get() { fi _debug "_WGET" "$_WGET" if [ "$onlyheader" ]; then - _wget_out = "$($_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O /dev/null "$url" 2>&1)" + _wget_out="$($_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O /dev/null "$url" 2>&1)" if _contains "$_WGET" " -d "; then # Demultiplex wget debug output echo "$_wget_out" >&2 @@ -2095,9 +2109,20 @@ _head_n() { } _tail_n() { - if ! tail -n "$1" 2>/dev/null; then + if _is_solaris; then #fix for solaris tail -"$1" + else + tail -n "$1" + fi +} + +_tail_c() { + if _is_solaris; then + #fix for solaris + tail -"$1"c + else + tail -c "$1" fi } @@ -2110,6 +2135,7 @@ _send_signed_request() { if [ -z "$keyfile" ]; then keyfile="$ACCOUNT_KEY_PATH" fi + _debug "=======Begin Send Signed Request=======" _debug url "$url" _debug payload "$payload" @@ -2223,6 +2249,20 @@ _send_signed_request() { _debug3 _body "$_body" fi + _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') + if [ "$code" = '503' ]; then + _sleep_overload_retry_sec=$_retryafter + if [ -z "$_sleep_overload_retry_sec" ]; then + _sleep_overload_retry_sec=5 + fi + if [ $_sleep_overload_retry_sec -le 600 ]; then + _info "It seems the CA server is currently overloaded, let's wait and retry. Sleeping $_sleep_overload_retry_sec seconds." + _sleep $_sleep_overload_retry_sec + continue + else + _info "The retryafter=$_retryafter is too large > 600, not retry anymore." + fi + fi if _contains "$_body" "JWS has invalid anti-replay nonce" || _contains "$_body" "JWS has an invalid anti-replay nonce"; then _info "It seems the CA server is busy now, let's wait and retry. Sleeping $_sleep_retry_sec seconds." _CACHED_NONCE="" @@ -2257,7 +2297,7 @@ _setopt() { if [ ! -f "$__conf" ]; then touch "$__conf" fi - if [ -n "$(tail -c 1 <"$__conf")" ]; then + if [ -n "$(_tail_c 1 <"$__conf")" ]; then echo >>"$__conf" fi @@ -2352,6 +2392,26 @@ _readdomainconf() { _read_conf "$DOMAIN_CONF" "$1" } +#_migratedomainconf oldkey newkey base64encode +_migratedomainconf() { + _old_key="$1" + _new_key="$2" + _b64encode="$3" + _value=$(_readdomainconf "$_old_key") + if [ -z "$_value" ]; then + return 1 # oldkey is not found + fi + _savedomainconf "$_new_key" "$_value" "$_b64encode" + _cleardomainconf "$_old_key" + _debug "Domain config $_old_key has been migrated to $_new_key" +} + +#_migratedeployconf oldkey newkey base64encode +_migratedeployconf() { + _migratedomainconf "$1" "SAVED_$2" "$3" || + _migratedomainconf "SAVED_$1" "SAVED_$2" "$3" # try only when oldkey itself is not found +} + #key value base64encode _savedeployconf() { _savedomainconf "SAVED_$1" "$2" "$3" @@ -2366,12 +2426,14 @@ _getdeployconf() { if [ "$_rac_value" ]; then if _startswith "$_rac_value" '"' && _endswith "$_rac_value" '"'; then _debug2 "trim quotation marks" - eval "export $_rac_key=$_rac_value" + eval $_rac_key=$_rac_value + export $_rac_key fi return 0 # do nothing fi - _saved=$(_readdomainconf "SAVED_$_rac_key") - eval "export $_rac_key=\"\$_saved\"" + _saved="$(_readdomainconf "SAVED_$_rac_key")" + eval $_rac_key=\$_saved + export $_rac_key } #_saveaccountconf key value base64encode @@ -2835,12 +2897,14 @@ _initpath() { if _isEccKey "$_ilength"; then DOMAIN_PATH="$domainhomeecc" - else + elif [ -z "$__SELECTED_RSA_KEY" ]; then if [ ! -d "$domainhome" ] && [ -d "$domainhomeecc" ]; then - _info "The domain '$domain' seems to have a ECC cert already, please add '$(__red "--ecc")' parameter if you want to use that cert." + _info "The domain '$domain' seems to have a ECC cert already, lets use ecc cert." + DOMAIN_PATH="$domainhomeecc" fi fi _debug DOMAIN_PATH "$DOMAIN_PATH" + export DOMAIN_PATH fi if [ -z "$DOMAIN_BACKUP_PATH" ]; then @@ -2892,22 +2956,6 @@ _initpath() { } -_exec() { - if [ -z "$_EXEC_TEMP_ERR" ]; then - _EXEC_TEMP_ERR="$(_mktemp)" - fi - - if [ "$_EXEC_TEMP_ERR" ]; then - eval "$@ 2>>$_EXEC_TEMP_ERR" - else - eval "$@" - fi -} - -_exec_err() { - [ "$_EXEC_TEMP_ERR" ] && _err "$(cat "$_EXEC_TEMP_ERR")" && echo "" >"$_EXEC_TEMP_ERR" -} - _apachePath() { _APACHECTL="apachectl" if ! _exists apachectl; then @@ -2920,8 +2968,7 @@ _apachePath() { fi fi - if ! _exec $_APACHECTL -V >/dev/null; then - _exec_err + if ! $_APACHECTL -V >/dev/null; then return 1 fi @@ -2973,8 +3020,7 @@ _restoreApache() { cat "$APACHE_CONF_BACKUP_DIR/$httpdconfname" >"$httpdconf" _debug "Restored: $httpdconf." - if ! _exec $_APACHECTL -t; then - _exec_err + if ! $_APACHECTL -t; then _err "Sorry, restore apache config error, please contact me." return 1 fi @@ -2992,8 +3038,7 @@ _setApache() { #test the conf first _info "Checking if there is an error in the apache config file before starting." - if ! _exec "$_APACHECTL" -t >/dev/null; then - _exec_err + if ! $_APACHECTL -t >/dev/null; then _err "The apache config file has error, please fix it first, then try again." _err "Don't worry, there is nothing changed to your system." return 1 @@ -3054,8 +3099,7 @@ Allow from all chmod 755 "$ACME_DIR" fi - if ! _exec "$_APACHECTL" graceful; then - _exec_err + if ! $_APACHECTL graceful; then _err "$_APACHECTL graceful error, please contact me." _restoreApache return 1 @@ -3140,8 +3184,7 @@ _setNginx() { return 1 fi _info "Check the nginx conf before setting up." - if ! _exec "nginx -t" >/dev/null; then - _exec_err + if ! nginx -t >/dev/null; then return 1 fi @@ -3168,16 +3211,14 @@ location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { fi _debug3 "Modified config:$(cat $FOUND_REAL_NGINX_CONF)" _info "nginx conf is done, let's check it again." - if ! _exec "nginx -t" >/dev/null; then - _exec_err + if ! nginx -t >/dev/null; then _err "It seems that nginx conf was broken, let's restore." cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" return 1 fi _info "Reload nginx" - if ! _exec "nginx -s reload" >/dev/null; then - _exec_err + if ! nginx -s reload >/dev/null; then _err "It seems that nginx reload error, let's restore." cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" return 1 @@ -3302,8 +3343,7 @@ _restoreNginx() { done _info "Reload nginx" - if ! _exec "nginx -s reload" >/dev/null; then - _exec_err + if ! nginx -s reload >/dev/null; then _err "It seems that nginx reload error, please report bug." return 1 fi @@ -3997,7 +4037,7 @@ _ns_purge_cf() { #checks if cf server is available _ns_is_available_cf() { - if _get "https://cloudflare-dns.com" "" 1 >/dev/null 2>&1; then + if _get "https://cloudflare-dns.com" "" 10 >/dev/null; then return 0 else return 1 @@ -4005,7 +4045,7 @@ _ns_is_available_cf() { } _ns_is_available_google() { - if _get "https://dns.google" "" 1 >/dev/null 2>&1; then + if _get "https://dns.google" "" 10 >/dev/null; then return 0 else return 1 @@ -4021,7 +4061,7 @@ _ns_lookup_google() { } _ns_is_available_ali() { - if _get "https://dns.alidns.com" "" 1 >/dev/null 2>&1; then + if _get "https://dns.alidns.com" "" 10 >/dev/null; then return 0 else return 1 @@ -4037,7 +4077,7 @@ _ns_lookup_ali() { } _ns_is_available_dp() { - if _get "https://doh.pub" "" 1 >/dev/null 2>&1; then + if _get "https://doh.pub" "" 10 >/dev/null; then return 0 else return 1 @@ -4584,9 +4624,10 @@ issue() { _d="*.$_d" fi _debug2 _d "$_d" - _authorizations_map="$_d,$response + _authorizations_map="$_d,$response#$_authz_url $_authorizations_map" done + _debug2 _authorizations_map "$_authorizations_map" _index=0 @@ -4638,33 +4679,32 @@ $_authorizations_map" _on_issue_err "$_post_hook" return 1 fi - + _authz_url="$(echo "$_candidates" | sed "s/$_idn_d,//" | _egrep_o "#.*" | sed "s/^#//")" + _debug _authz_url "$_authz_url" if [ -z "$thumbprint" ]; then thumbprint="$(__calc_account_thumbprint)" fi + keyauthorization="" + + if echo "$response" | grep '"status":"valid"' >/dev/null 2>&1; then + _debug "$d is already valid." + keyauthorization="$STATE_VERIFIED" + _debug keyauthorization "$keyauthorization" + fi + entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" - keyauthorization="" - if [ -z "$entry" ]; then - if ! _startswith "$d" '*.'; then - _debug "Not a wildcard domain, lets check whether the validation is already valid." - if echo "$response" | grep '"status":"valid"' >/dev/null 2>&1; then - _debug "$d is already valid." - keyauthorization="$STATE_VERIFIED" - _debug keyauthorization "$keyauthorization" - fi - fi - if [ -z "$keyauthorization" ]; then - _err "Error, can not get domain token entry $d for $vtype" - _supported_vtypes="$(echo "$response" | _egrep_o "\"challenges\":\[[^]]*]" | tr '{' "\n" | grep type | cut -d '"' -f 4 | tr "\n" ' ')" - if [ "$_supported_vtypes" ]; then - _err "The supported validation types are: $_supported_vtypes, but you specified: $vtype" - fi - _clearup - _on_issue_err "$_post_hook" - return 1 + + if [ -z "$keyauthorization" -a -z "$entry" ]; then + _err "Error, can not get domain token entry $d for $vtype" + _supported_vtypes="$(echo "$response" | _egrep_o "\"challenges\":\[[^]]*]" | tr '{' "\n" | grep type | cut -d '"' -f 4 | tr "\n" ' ')" + if [ "$_supported_vtypes" ]; then + _err "The supported validation types are: $_supported_vtypes, but you specified: $vtype" fi + _clearup + _on_issue_err "$_post_hook" + return 1 fi if [ -z "$keyauthorization" ]; then @@ -4690,15 +4730,9 @@ $_authorizations_map" fi keyauthorization="$token.$thumbprint" _debug keyauthorization "$keyauthorization" - - if printf "%s" "$response" | grep '"status":"valid"' >/dev/null 2>&1; then - _debug "$d is already verified." - keyauthorization="$STATE_VERIFIED" - _debug keyauthorization "$keyauthorization" - fi fi - dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot" + dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url" _debug dvlist "$dvlist" vlist="$vlist$dvlist$dvsep" @@ -4715,6 +4749,7 @@ $_authorizations_map" keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + _authz_url=$(echo "$ventry" | cut -d "$sep" -f 6) _debug d "$d" if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then _debug "$d is already verified, skip $vtype." @@ -4840,7 +4875,7 @@ $_authorizations_map" uri=$(echo "$ventry" | cut -d "$sep" -f 3) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - + _authz_url=$(echo "$ventry" | cut -d "$sep" -f 6) if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then _info "$d is already verified, skip $vtype." continue @@ -4850,6 +4885,7 @@ $_authorizations_map" _debug "d" "$d" _debug "keyauthorization" "$keyauthorization" _debug "uri" "$uri" + _debug "_authz_url" "$_authz_url" removelevel="" token="$(printf "%s" "$keyauthorization" | cut -d '.' -f 1)" @@ -4919,18 +4955,6 @@ $_authorizations_map" if ! chmod a+r "$wellknown_path/$token"; then _debug "chmod failed, but we just continue." fi - if [ ! "$usingApache" ]; then - if webroot_owner=$(_stat "$_currentRoot"); then - _debug "Changing owner/group of .well-known to $webroot_owner" - if ! _exec "chown -R \"$webroot_owner\" \"$_currentRoot/.well-known\""; then - _debug "$(cat "$_EXEC_TEMP_ERR")" - _exec_err >/dev/null 2>&1 - fi - else - _debug "not changing owner/group of webroot" - fi - fi - fi elif [ "$vtype" = "$VTYPE_ALPN" ]; then acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" @@ -4969,6 +4993,7 @@ $_authorizations_map" MAX_RETRY_TIMES=30 fi + _debug "Lets check the status of the authz" while true; do waittimes=$(_math "$waittimes" + 1) if [ "$waittimes" -ge "$MAX_RETRY_TIMES" ]; then @@ -5016,9 +5041,9 @@ $_authorizations_map" break fi - if [ "$status" = "pending" ]; then + if _contains "$status" "pending"; then _info "Pending, The CA is processing your order, please just wait. ($waittimes/$MAX_RETRY_TIMES)" - elif [ "$status" = "processing" ]; then + elif _contains "$status" "processing"; then _info "Processing, The CA is processing your order, please just wait. ($waittimes/$MAX_RETRY_TIMES)" else _err "$d:Verify error:$response" @@ -5031,7 +5056,7 @@ $_authorizations_map" _sleep 2 _debug "checking" - _send_signed_request "$uri" + _send_signed_request "$_authz_url" if [ "$?" != "0" ]; then _err "$d:Verify error:$response" @@ -5745,6 +5770,7 @@ deploy() { return 1 fi + _debug2 DOMAIN_CONF "$DOMAIN_CONF" . "$DOMAIN_CONF" _savedomainconf Le_DeployHook "$_hooks" @@ -6104,8 +6130,22 @@ revoke() { uri="${ACME_REVOKE_CERT}" + _info "Try account key first." + if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then + if [ -z "$response" ]; then + _info "Revoke success." + rm -f "$CERT_PATH" + cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked" + cat "$CSR_PATH" >"$CSR_PATH.revoked" + return 0 + else + _err "Revoke error." + _debug "$response" + fi + fi + if [ -f "$CERT_KEY_PATH" ]; then - _info "Try domain key first." + _info "Try domain key." if _send_signed_request "$uri" "$data" "" "$CERT_KEY_PATH"; then if [ -z "$response" ]; then _info "Revoke success." @@ -6121,21 +6161,6 @@ revoke() { else _info "Domain key file doesn't exist." fi - - _info "Try account key." - - if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then - if [ -z "$response" ]; then - _info "Revoke success." - rm -f "$CERT_PATH" - cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked" - cat "$CSR_PATH" >"$CSR_PATH.revoked" - return 0 - else - _err "Revoke error." - _debug "$response" - fi - fi return 1 } @@ -6709,6 +6734,13 @@ _send_notify() { return 0 fi + _nsource="$NOTIFY_SOURCE" + if [ -z "$_nsource" ]; then + _nsource="$(hostname)" + fi + + _nsubject="$_nsubject by $_nsource" + _send_err=0 for _n_hook in $(echo "$_nhooks" | tr ',' " "); do _n_hook_file="$(_findHook "" $_SUB_FOLDER_NOTIFY "$_n_hook")" @@ -6763,11 +6795,12 @@ setnotify() { _nhook="$1" _nlevel="$2" _nmode="$3" + _nsource="$4" _initpath - if [ -z "$_nhook$_nlevel$_nmode" ]; then - _usage "Usage: $PROJECT_ENTRY --set-notify [--notify-hook ] [--notify-level <0|1|2|3>] [--notify-mode <0|1>]" + if [ -z "$_nhook$_nlevel$_nmode$_nsource" ]; then + _usage "Usage: $PROJECT_ENTRY --set-notify [--notify-hook ] [--notify-level <0|1|2|3>] [--notify-mode <0|1>] [--notify-source ]" _usage "$_NOTIFY_WIKI" return 1 fi @@ -6784,6 +6817,12 @@ setnotify() { _saveaccountconf "NOTIFY_MODE" "$NOTIFY_MODE" fi + if [ "$_nsource" ]; then + _info "Set notify source to: $_nsource" + export "NOTIFY_SOURCE=$_nsource" + _saveaccountconf "NOTIFY_SOURCE" "$NOTIFY_SOURCE" + fi + if [ "$_nhook" ]; then _info "Set notify hook to: $_nhook" if [ "$_nhook" = "$NO_VALUE" ]; then @@ -6944,6 +6983,7 @@ Parameters: 0: Bulk mode. Send all the domain's notifications in one message(mail). 1: Cert mode. Send a message for every single cert. --notify-hook Set the notify hook + --notify-source Set the server name in the notification message --revoke-reason <0-10> The reason for revocation, can be used in conjunction with the '--revoke' command. See: $_REVOKE_WIKI @@ -7101,7 +7141,9 @@ _selectServer() { _getCAShortName() { caurl="$1" if [ -z "$caurl" ]; then - caurl="$DEFAULT_CA" + #use letsencrypt as default value if the Le_API is empty + #this case can only come from the old upgrading. + caurl="$CA_LETSENCRYPT_V2" fi if [ "$CA_SSLCOM_ECC" = "$caurl" ]; then caurl="$CA_SSLCOM_RSA" #just hack to get the short name @@ -7218,6 +7260,7 @@ _process() { _notify_hook="" _notify_level="" _notify_mode="" + _notify_source="" _revoke_reason="" _eab_kid="" _eab_hmac_key="" @@ -7463,6 +7506,9 @@ _process() { --keylength | -k) _keylength="$2" shift + if [ "$_keylength" ] && ! _isEccKey "$_keylength"; then + export __SELECTED_RSA_KEY=1 + fi ;; -ak | --accountkeylength) _accountkeylength="$2" @@ -7498,7 +7544,7 @@ _process() { shift ;; --home) - export LE_WORKING_DIR="$2" + export LE_WORKING_DIR="$(echo "$2" | sed 's|/$||')" shift ;; --cert-home | --certhome) @@ -7710,6 +7756,15 @@ _process() { _notify_mode="$_nmode" shift ;; + --notify-source) + _nsource="$2" + if _startswith "$_nsource" "-"; then + _err "'$_nsource' is not valid host name for '$1'" + return 1 + fi + _notify_source="$_nsource" + shift + ;; --revoke-reason) _revoke_reason="$2" if _startswith "$_revoke_reason" "-"; then @@ -7864,7 +7919,7 @@ _process() { createCSR "$_domain" "$_altdomains" "$_ecc" ;; setnotify) - setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" + setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" "$_notify_source" ;; setdefaultca) setdefaultca diff --git a/deploy/docker.sh b/deploy/docker.sh index 3aa1b2cd..c9815d5b 100755 --- a/deploy/docker.sh +++ b/deploy/docker.sh @@ -273,16 +273,27 @@ _check_curl_version() { _minor="$(_getfield "$_cversion" 2 '.')" _debug2 "_minor" "$_minor" - if [ "$_major$_minor" -lt "740" ]; then + if [ "$_major" -ge "8" ]; then + #ok + return 0 + fi + if [ "$_major" = "7" ]; then + if [ "$_minor" -lt "40" ]; then + _err "curl v$_cversion doesn't support unit socket" + _err "Please upgrade to curl 7.40 or later." + return 1 + fi + if [ "$_minor" -lt "50" ]; then + _debug "Use short host name" + export _CURL_NO_HOST=1 + else + export _CURL_NO_HOST= + fi + return 0 + else _err "curl v$_cversion doesn't support unit socket" _err "Please upgrade to curl 7.40 or later." return 1 fi - if [ "$_major$_minor" -lt "750" ]; then - _debug "Use short host name" - export _CURL_NO_HOST=1 - else - export _CURL_NO_HOST= - fi - return 0 + } diff --git a/deploy/gcore_cdn.sh b/deploy/gcore_cdn.sh index f573a3aa..fd17cc25 100644 --- a/deploy/gcore_cdn.sh +++ b/deploy/gcore_cdn.sh @@ -1,10 +1,11 @@ #!/usr/bin/env sh -# Here is the script to deploy the cert to G-Core CDN service (https://gcorelabs.com/ru/) using the G-Core Labs API (https://docs.gcorelabs.com/cdn/). +# Here is the script to deploy the cert to G-Core CDN service (https://gcore.com/) using the G-Core Labs API (https://apidocs.gcore.com/cdn). # Returns 0 when success. # # Written by temoffey # Public domain, 2019 +# Update by DreamOfIce in 2023 #export DEPLOY_GCORE_CDN_USERNAME=myusername #export DEPLOY_GCORE_CDN_PASSWORD=mypassword @@ -56,7 +57,7 @@ gcore_cdn_deploy() { _request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}" _debug _request "$_request" export _H1="Content-Type:application/json" - _response=$(_post "$_request" "https://api.gcdn.co/auth/jwt/login") + _response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login") _debug _response "$_response" _regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$" _debug _regex "$_regex" @@ -69,8 +70,8 @@ gcore_cdn_deploy() { fi _info "Find CDN resource with cname $_cdomain" - export _H2="Authorization:Token $_token" - _response=$(_get "https://api.gcdn.co/resources") + export _H2="Authorization:Bearer $_token" + _response=$(_get "https://api.gcore.com/cdn/resources") _debug _response "$_response" _regex="\"primary_resource\":null}," _debug _regex "$_regex" @@ -102,7 +103,7 @@ gcore_cdn_deploy() { _date=$(date "+%d.%m.%Y %H:%M:%S") _request="{\"name\":\"$_cdomain ($_date)\",\"sslCertificate\":\"$_fullchain\",\"sslPrivateKey\":\"$_key\"}" _debug _request "$_request" - _response=$(_post "$_request" "https://api.gcdn.co/sslData") + _response=$(_post "$_request" "https://api.gcore.com/cdn/sslData") _debug _response "$_response" _regex=".*\"id\":\([0-9]*\).*$" _debug _regex "$_regex" @@ -117,7 +118,7 @@ gcore_cdn_deploy() { _info "Update CDN resource" _request="{\"originGroup\":$_originGroup,\"sslData\":$_sslDataAdd}" _debug _request "$_request" - _response=$(_post "$_request" "https://api.gcdn.co/resources/$_resourceId" '' "PUT") + _response=$(_post "$_request" "https://api.gcore.com/cdn/resources/$_resourceId" '' "PUT") _debug _response "$_response" _regex=".*\"sslData\":\([0-9]*\).*$" _debug _regex "$_regex" @@ -133,7 +134,7 @@ gcore_cdn_deploy() { _info "Not found old SSL certificate" else _info "Delete old SSL certificate" - _response=$(_post '' "https://api.gcdn.co/sslData/$_sslDataOld" '' "DELETE") + _response=$(_post '' "https://api.gcore.com/cdn/sslData/$_sslDataOld" '' "DELETE") _debug _response "$_response" fi diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 89962621..c66e2e19 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -14,7 +14,7 @@ # The following examples are for QNAP NAS running QTS 4.2 # export DEPLOY_SSH_CMD="" # defaults to "ssh -T" # export DEPLOY_SSH_USER="admin" # required -# export DEPLOY_SSH_SERVER="qnap" # defaults to domain name +# export DEPLOY_SSH_SERVER="host1 host2:8022 192.168.0.1:9022" # defaults to domain name, support multiple servers with optional port # export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem" # export DEPLOY_SSH_CERTFILE="/etc/stunnel/stunnel.pem" # export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem" @@ -23,6 +23,8 @@ # export DEPLOY_SSH_BACKUP="" # yes or no, default to yes or previously saved value # export DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy" # path on remote system. Defaults to .acme_ssh_deploy # export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value +# export DEPLOY_SSH_USE_SCP="" yes or no, default to no +# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q" # ######## Public functions ##################### @@ -42,72 +44,134 @@ ssh_deploy() { _debug _cfullchain "$_cfullchain" # USER is required to login by SSH to remote host. + _migratedeployconf Le_Deploy_ssh_user DEPLOY_SSH_USER _getdeployconf DEPLOY_SSH_USER _debug2 DEPLOY_SSH_USER "$DEPLOY_SSH_USER" if [ -z "$DEPLOY_SSH_USER" ]; then - if [ -z "$Le_Deploy_ssh_user" ]; then - _err "DEPLOY_SSH_USER not defined." - return 1 - fi - else - Le_Deploy_ssh_user="$DEPLOY_SSH_USER" - _savedomainconf Le_Deploy_ssh_user "$Le_Deploy_ssh_user" + _err "DEPLOY_SSH_USER not defined." + return 1 fi + _savedeployconf DEPLOY_SSH_USER "$DEPLOY_SSH_USER" # SERVER is optional. If not provided then use _cdomain + _migratedeployconf Le_Deploy_ssh_server DEPLOY_SSH_SERVER _getdeployconf DEPLOY_SSH_SERVER _debug2 DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER" - if [ -n "$DEPLOY_SSH_SERVER" ]; then - Le_Deploy_ssh_server="$DEPLOY_SSH_SERVER" - _savedomainconf Le_Deploy_ssh_server "$Le_Deploy_ssh_server" - elif [ -z "$Le_Deploy_ssh_server" ]; then - Le_Deploy_ssh_server="$_cdomain" + if [ -z "$DEPLOY_SSH_SERVER" ]; then + DEPLOY_SSH_SERVER="$_cdomain" fi + _savedeployconf DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER" # CMD is optional. If not provided then use ssh + _migratedeployconf Le_Deploy_ssh_cmd DEPLOY_SSH_CMD _getdeployconf DEPLOY_SSH_CMD _debug2 DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD" - if [ -n "$DEPLOY_SSH_CMD" ]; then - Le_Deploy_ssh_cmd="$DEPLOY_SSH_CMD" - _savedomainconf Le_Deploy_ssh_cmd "$Le_Deploy_ssh_cmd" - elif [ -z "$Le_Deploy_ssh_cmd" ]; then - Le_Deploy_ssh_cmd="ssh -T" + if [ -z "$DEPLOY_SSH_CMD" ]; then + DEPLOY_SSH_CMD="ssh -T" fi + _savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD" # BACKUP is optional. If not provided then default to previously saved value or yes. + _migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP _getdeployconf DEPLOY_SSH_BACKUP _debug2 DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP" - if [ "$DEPLOY_SSH_BACKUP" = "no" ]; then - Le_Deploy_ssh_backup="no" - elif [ -z "$Le_Deploy_ssh_backup" ] || [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then - Le_Deploy_ssh_backup="yes" + if [ -z "$DEPLOY_SSH_BACKUP" ]; then + DEPLOY_SSH_BACKUP="yes" fi - _savedomainconf Le_Deploy_ssh_backup "$Le_Deploy_ssh_backup" + _savedeployconf DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP" # BACKUP_PATH is optional. If not provided then default to previously saved value or .acme_ssh_deploy + _migratedeployconf Le_Deploy_ssh_backup_path DEPLOY_SSH_BACKUP_PATH _getdeployconf DEPLOY_SSH_BACKUP_PATH _debug2 DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH" - if [ -n "$DEPLOY_SSH_BACKUP_PATH" ]; then - Le_Deploy_ssh_backup_path="$DEPLOY_SSH_BACKUP_PATH" - elif [ -z "$Le_Deploy_ssh_backup_path" ]; then - Le_Deploy_ssh_backup_path=".acme_ssh_deploy" + if [ -z "$DEPLOY_SSH_BACKUP_PATH" ]; then + DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy" fi - _savedomainconf Le_Deploy_ssh_backup_path "$Le_Deploy_ssh_backup_path" + _savedeployconf DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH" # MULTI_CALL is optional. If not provided then default to previously saved # value (which may be undefined... equivalent to "no"). + _migratedeployconf Le_Deploy_ssh_multi_call DEPLOY_SSH_MULTI_CALL _getdeployconf DEPLOY_SSH_MULTI_CALL _debug2 DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL" - if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then - Le_Deploy_ssh_multi_call="yes" - _savedomainconf Le_Deploy_ssh_multi_call "$Le_Deploy_ssh_multi_call" - elif [ "$DEPLOY_SSH_MULTI_CALL" = "no" ]; then - Le_Deploy_ssh_multi_call="" - _cleardomainconf Le_Deploy_ssh_multi_call + if [ -z "$DEPLOY_SSH_MULTI_CALL" ]; then + DEPLOY_SSH_MULTI_CALL="no" + fi + _savedeployconf DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL" + + # KEYFILE is optional. + # If provided then private key will be copied to provided filename. + _migratedeployconf Le_Deploy_ssh_keyfile DEPLOY_SSH_KEYFILE + _getdeployconf DEPLOY_SSH_KEYFILE + _debug2 DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE" + if [ -n "$DEPLOY_SSH_KEYFILE" ]; then + _savedeployconf DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE" fi - _deploy_ssh_servers=$Le_Deploy_ssh_server - for Le_Deploy_ssh_server in $_deploy_ssh_servers; do + # CERTFILE is optional. + # If provided then certificate will be copied or appended to provided filename. + _migratedeployconf Le_Deploy_ssh_certfile DEPLOY_SSH_CERTFILE + _getdeployconf DEPLOY_SSH_CERTFILE + _debug2 DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE" + if [ -n "$DEPLOY_SSH_CERTFILE" ]; then + _savedeployconf DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE" + fi + + # CAFILE is optional. + # If provided then CA intermediate certificate will be copied or appended to provided filename. + _migratedeployconf Le_Deploy_ssh_cafile DEPLOY_SSH_CAFILE + _getdeployconf DEPLOY_SSH_CAFILE + _debug2 DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE" + if [ -n "$DEPLOY_SSH_CAFILE" ]; then + _savedeployconf DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE" + fi + + # FULLCHAIN is optional. + # If provided then fullchain certificate will be copied or appended to provided filename. + _migratedeployconf Le_Deploy_ssh_fullchain DEPLOY_SSH_FULLCHAIN + _getdeployconf DEPLOY_SSH_FULLCHAIN + _debug2 DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN" + if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then + _savedeployconf DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN" + fi + + # REMOTE_CMD is optional. + # If provided then this command will be executed on remote host. + _migratedeployconf Le_Deploy_ssh_remote_cmd DEPLOY_SSH_REMOTE_CMD + _getdeployconf DEPLOY_SSH_REMOTE_CMD + _debug2 DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD" + if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then + _savedeployconf DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD" + fi + + # USE_SCP is optional. If not provided then default to previously saved + # value (which may be undefined... equivalent to "no"). + _getdeployconf DEPLOY_SSH_USE_SCP + _debug2 DEPLOY_SSH_USE_SCP "$DEPLOY_SSH_USE_SCP" + if [ -z "$DEPLOY_SSH_USE_SCP" ]; then + DEPLOY_SSH_USE_SCP="no" + fi + _savedeployconf DEPLOY_SSH_USE_SCP "$DEPLOY_SSH_USE_SCP" + + # SCP_CMD is optional. If not provided then use scp + _getdeployconf DEPLOY_SSH_SCP_CMD + _debug2 DEPLOY_SSH_SCP_CMD "$DEPLOY_SSH_SCP_CMD" + if [ -z "$DEPLOY_SSH_SCP_CMD" ]; then + DEPLOY_SSH_SCP_CMD="scp -q" + fi + _savedeployconf DEPLOY_SSH_SCP_CMD "$DEPLOY_SSH_SCP_CMD" + + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + DEPLOY_SSH_MULTI_CALL="yes" + _info "Using scp as alternate method for copying files. Multicall Mode is implicit" + elif [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + _info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host" + else + _info "Required commands batched and sent in single call to remote host" + fi + + _deploy_ssh_servers="$DEPLOY_SSH_SERVER" + for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do _ssh_deploy done } @@ -117,16 +181,25 @@ _ssh_deploy() { _cmdstr="" _backupprefix="" _backupdir="" + _local_cert_file="" + _local_ca_file="" + _local_full_file="" - _info "Deploy certificates to remote server $Le_Deploy_ssh_user@$Le_Deploy_ssh_server" - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then - _info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host" - else - _info "Required commands batched and sent in single call to remote host" - fi + case $DEPLOY_SSH_SERVER in + *:*) + _host=${DEPLOY_SSH_SERVER%:*} + _port=${DEPLOY_SSH_SERVER##*:} + ;; + *) + _host=$DEPLOY_SSH_SERVER + _port= + ;; + esac - if [ "$Le_Deploy_ssh_backup" = "yes" ]; then - _backupprefix="$Le_Deploy_ssh_backup_path/$_cdomain-backup" + _info "Deploy certificates to remote server $DEPLOY_SSH_USER@$_host:$_port" + + if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then + _backupprefix="$DEPLOY_SSH_BACKUP_PATH/$_cdomain-backup" _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')" # run cleanup on the backup directory, erase all older # than 180 days (15552000 seconds). @@ -138,7 +211,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d _cmdstr="mkdir -p $_backupdir; $_cmdstr" _info "Backup of old certificate files will be placed in remote directory $_backupdir" _info "Backup directories erased after 180 days." - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then return $_err_code fi @@ -146,129 +219,184 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi fi - # KEYFILE is optional. - # If provided then private key will be copied to provided filename. - _getdeployconf DEPLOY_SSH_KEYFILE - _debug2 DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE" if [ -n "$DEPLOY_SSH_KEYFILE" ]; then - Le_Deploy_ssh_keyfile="$DEPLOY_SSH_KEYFILE" - _savedomainconf Le_Deploy_ssh_keyfile "$Le_Deploy_ssh_keyfile" - fi - if [ -n "$Le_Deploy_ssh_keyfile" ]; then - if [ "$Le_Deploy_ssh_backup" = "yes" ]; then + if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_keyfile $_backupdir >/dev/null;" + _cmdstr="$_cmdstr cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi - # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $Le_Deploy_ssh_keyfile;" - _info "will copy private key to remote file $Le_Deploy_ssh_keyfile" - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then - if ! _ssh_remote_cmd "$_cmdstr"; then + + # copy new key into file. + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + if ! _scp_remote_cmd "$_ckey" "$DEPLOY_SSH_KEYFILE"; then return $_err_code fi - _cmdstr="" + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" + _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi fi - # CERTFILE is optional. - # If provided then certificate will be copied or appended to provided filename. - _getdeployconf DEPLOY_SSH_CERTFILE - _debug2 DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE" if [ -n "$DEPLOY_SSH_CERTFILE" ]; then - Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE" - _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile" - fi - if [ -n "$Le_Deploy_ssh_certfile" ]; then _pipe=">" - if [ "$Le_Deploy_ssh_certfile" = "$Le_Deploy_ssh_keyfile" ]; then + if [ "$DEPLOY_SSH_CERTFILE" = "$DEPLOY_SSH_KEYFILE" ]; then # if filename is same as previous file then append. _pipe=">>" - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then + elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_certfile $_backupdir >/dev/null;" + _cmdstr="$_cmdstr cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi + # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $Le_Deploy_ssh_certfile;" - _info "will copy certificate to remote file $Le_Deploy_ssh_certfile" - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then - if ! _ssh_remote_cmd "$_cmdstr"; then + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + _local_cert_file=$(_mktemp) + if [ "$DEPLOY_SSH_CERTFILE" = "$DEPLOY_SSH_KEYFILE" ]; then + cat "$_ckey" >>"$_local_cert_file" + fi + cat "$_ccert" >>"$_local_cert_file" + if ! _scp_remote_cmd "$_local_cert_file" "$DEPLOY_SSH_CERTFILE"; then return $_err_code fi - _cmdstr="" + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;" + _info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi fi - # CAFILE is optional. - # If provided then CA intermediate certificate will be copied or appended to provided filename. - _getdeployconf DEPLOY_SSH_CAFILE - _debug2 DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE" if [ -n "$DEPLOY_SSH_CAFILE" ]; then - Le_Deploy_ssh_cafile="$DEPLOY_SSH_CAFILE" - _savedomainconf Le_Deploy_ssh_cafile "$Le_Deploy_ssh_cafile" - fi - if [ -n "$Le_Deploy_ssh_cafile" ]; then _pipe=">" - if [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_keyfile" ] || - [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_certfile" ]; then + if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_KEYFILE" ] || + [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_CERTFILE" ]; then # if filename is same as previous file then append. _pipe=">>" - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then + elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_cafile $_backupdir >/dev/null;" + _cmdstr="$_cmdstr cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi + # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $Le_Deploy_ssh_cafile;" - _info "will copy CA file to remote file $Le_Deploy_ssh_cafile" - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then - if ! _ssh_remote_cmd "$_cmdstr"; then + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + _local_ca_file=$(_mktemp) + if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_KEYFILE" ]; then + cat "$_ckey" >>"$_local_ca_file" + fi + if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_CERTFILE" ]; then + cat "$_ccert" >>"$_local_ca_file" + fi + cat "$_cca" >>"$_local_ca_file" + if ! _scp_remote_cmd "$_local_ca_file" "$DEPLOY_SSH_CAFILE"; then return $_err_code fi - _cmdstr="" + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;" + _info "will copy CA file to remote file $DEPLOY_SSH_CAFILE" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi fi - # FULLCHAIN is optional. - # If provided then fullchain certificate will be copied or appended to provided filename. - _getdeployconf DEPLOY_SSH_FULLCHAIN - _debug2 DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN" if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then - Le_Deploy_ssh_fullchain="$DEPLOY_SSH_FULLCHAIN" - _savedomainconf Le_Deploy_ssh_fullchain "$Le_Deploy_ssh_fullchain" - fi - if [ -n "$Le_Deploy_ssh_fullchain" ]; then _pipe=">" - if [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_keyfile" ] || - [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_certfile" ] || - [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_cafile" ]; then + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_KEYFILE" ] || + [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CERTFILE" ] || + [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CAFILE" ]; then # if filename is same as previous file then append. _pipe=">>" - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then + elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_fullchain $_backupdir >/dev/null;" + _cmdstr="$_cmdstr cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_FULLCHAIN" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi + # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $Le_Deploy_ssh_fullchain;" - _info "will copy fullchain to remote file $Le_Deploy_ssh_fullchain" - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then - if ! _ssh_remote_cmd "$_cmdstr"; then + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + _local_full_file=$(_mktemp) + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_KEYFILE" ]; then + cat "$_ckey" >>"$_local_full_file" + fi + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CERTFILE" ]; then + cat "$_ccert" >>"$_local_full_file" + fi + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CAFILE" ]; then + cat "$_cca" >>"$_local_full_file" + fi + cat "$_cfullchain" >>"$_local_full_file" + if ! _scp_remote_cmd "$_local_full_file" "$DEPLOY_SSH_FULLCHAIN"; then return $_err_code fi - _cmdstr="" + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;" + _info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi fi - # REMOTE_CMD is optional. - # If provided then this command will be executed on remote host. - _getdeployconf DEPLOY_SSH_REMOTE_CMD - _debug2 DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD" - if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then - Le_Deploy_ssh_remote_cmd="$DEPLOY_SSH_REMOTE_CMD" - _savedomainconf Le_Deploy_ssh_remote_cmd "$Le_Deploy_ssh_remote_cmd" + # cleanup local files if any + if [ -f "$_local_cert_file" ]; then + rm -f "$_local_cert_file" fi - if [ -n "$Le_Deploy_ssh_remote_cmd" ]; then - _cmdstr="$_cmdstr $Le_Deploy_ssh_remote_cmd;" - _info "Will execute remote command $Le_Deploy_ssh_remote_cmd" - if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then + if [ -f "$_local_ca_file" ]; then + rm -f "$_local_ca_file" + fi + if [ -f "$_local_full_file" ]; then + rm -f "$_local_full_file" + fi + + if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then + _cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;" + _info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then return $_err_code fi @@ -282,17 +410,25 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d return $_err_code fi fi + # cleanup in case all is ok return 0 } #cmd _ssh_remote_cmd() { _cmd="$1" + + _ssh_cmd="$DEPLOY_SSH_CMD" + if [ -n "$_port" ]; then + _ssh_cmd="$_ssh_cmd -p $_port" + fi + _secure_debug "Remote commands to execute: $_cmd" - _info "Submitting sequence of commands to remote server by ssh" + _info "Submitting sequence of commands to remote server by $_ssh_cmd" + # quotations in bash cmd below intended. Squash travis spellcheck error # shellcheck disable=SC2029 - $Le_Deploy_ssh_cmd "$Le_Deploy_ssh_user@$Le_Deploy_ssh_server" sh -c "'$_cmd'" + $_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'" _err_code="$?" if [ "$_err_code" != "0" ]; then @@ -301,3 +437,26 @@ _ssh_remote_cmd() { return $_err_code } + +# cmd scp +_scp_remote_cmd() { + _src=$1 + _dest=$2 + + _scp_cmd="$DEPLOY_SSH_SCP_CMD" + if [ -n "$_port" ]; then + _scp_cmd="$_scp_cmd -P $_port" + fi + + _secure_debug "Remote copy source $_src to destination $_dest" + _info "Submitting secure copy by $_scp_cmd" + + $_scp_cmd "$_src" "$DEPLOY_SSH_USER"@"$_host":"$_dest" + _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 c31a5df0..7398b350 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -1,34 +1,35 @@ -#!/usr/bin/env sh +#!/bin/bash -# Here is a script to deploy cert to Synology DSM -# -# It requires following environment variables: -# -# SYNO_Username - Synology Username to login (must be an administrator) -# SYNO_Password - Synology Password to login -# SYNO_Certificate - Certificate description to target for replacement -# -# The following environmental variables may be set if you don't like their -# default values: -# -# SYNO_Scheme - defaults to http -# SYNO_Hostname - defaults to localhost -# SYNO_Port - defaults to 5000 -# SYNO_DID - device ID to skip OTP - defaults to empty -# SYNO_TOTP_SECRET - TOTP secret to generate OTP - defaults to empty -# +################################################################################ +# ACME.sh 3rd party deploy plugin for Synology DSM +################################################################################ +# Authors: Brian Hartvigsen (creator), https://github.com/tresni +# Martin Arndt (contributor), https://troublezone.net/ +# Updated: 2023-07-03 +# Issues: https://github.com/acmesh-official/acme.sh/issues/2727 +################################################################################ +# Usage: +# 1. export SYNO_Username="adminUser" +# 2. export SYNO_Password="adminPassword" +# Optional exports (shown values are the defaults): +# - export SYNO_Certificate="" to replace a specific certificate via description +# - export SYNO_Scheme="http" +# - export SYNO_Hostname="localhost" +# - export SYNO_Port="5000" +# - export SYNO_Device_Name="CertRenewal" - required for skipping 2FA-OTP +# - export SYNO_Device_ID="" - required for skipping 2FA-OTP +# 3. acme.sh --deploy --deploy-hook synology_dsm -d example.com +################################################################################ # Dependencies: -# ------------- -# - jq and curl -# - oathtool (When using 2 Factor Authentication and SYNO_TOTP_SECRET is set) -# -#returns 0 means success, otherwise error. - -######## Public functions ##################### +# - jq & curl +################################################################################ +# Return value: +# 0 means success, otherwise error. +################################################################################ +########## Public functions #################################################### #domain keyfile certfile cafile fullchain synology_dsm_deploy() { - _cdomain="$1" _ckey="$2" _ccert="$3" @@ -36,39 +37,46 @@ synology_dsm_deploy() { _debug _cdomain "$_cdomain" - # Get Username and Password, but don't save until we successfully authenticate + # Get username & password, but don't save until we authenticated successfully _getdeployconf SYNO_Username _getdeployconf SYNO_Password _getdeployconf SYNO_Create _getdeployconf SYNO_DID _getdeployconf SYNO_TOTP_SECRET + _getdeployconf SYNO_Device_Name + _getdeployconf SYNO_Device_ID if [ -z "${SYNO_Username:-}" ] || [ -z "${SYNO_Password:-}" ]; then _err "SYNO_Username & SYNO_Password must be set" return 1 fi + if [ -n "${SYNO_Device_Name:-}" ] && [ -z "${SYNO_Device_ID:-}" ]; then + _err "SYNO_Device_Name set, but SYNO_Device_ID is empty" + return 1 + fi _debug2 SYNO_Username "$SYNO_Username" _secure_debug2 SYNO_Password "$SYNO_Password" + _debug2 SYNO_Create "$SYNO_Create" + _debug2 SYNO_Device_Name "$SYNO_Device_Name" + _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID" - # Optional scheme, hostname, and port for Synology DSM + # Optional scheme, hostname & port for Synology DSM _getdeployconf SYNO_Scheme _getdeployconf SYNO_Hostname _getdeployconf SYNO_Port - # default vaules for scheme, hostname, and port - # defaulting to localhost and http because it's localhost... + # Default values for scheme, hostname & port + # Defaulting to localhost & http, because it's localhost… [ -n "${SYNO_Scheme}" ] || SYNO_Scheme="http" [ -n "${SYNO_Hostname}" ] || SYNO_Hostname="localhost" [ -n "${SYNO_Port}" ] || SYNO_Port="5000" - _savedeployconf SYNO_Scheme "$SYNO_Scheme" _savedeployconf SYNO_Hostname "$SYNO_Hostname" _savedeployconf SYNO_Port "$SYNO_Port" - _debug2 SYNO_Scheme "$SYNO_Scheme" _debug2 SYNO_Hostname "$SYNO_Hostname" _debug2 SYNO_Port "$SYNO_Port" - # Get the certificate description, but don't save it until we verfiy it's real + # Get the certificate description, but don't save it until we verify it's real _getdeployconf SYNO_Certificate _debug SYNO_Certificate "${SYNO_Certificate:-}" @@ -87,49 +95,72 @@ synology_dsm_deploy() { _debug3 response "$response" _debug3 api_version "$api_version" - # Login, get the token from JSON and session id from cookie + # Login, get the session ID & SynoToken from JSON _info "Logging into $SYNO_Hostname:$SYNO_Port" encoded_username="$(printf "%s" "$SYNO_Username" | _url_encode)" encoded_password="$(printf "%s" "$SYNO_Password" | _url_encode)" otp_code="" + # START - DEPRECATED, only kept for legacy compatibility reasons if [ -n "$SYNO_TOTP_SECRET" ]; then + _info "WARNING: Usage of SYNO_TOTP_SECRET is deprecated!" + _info " See synology_dsm.sh script or ACME.sh Wiki page for details:" + _info " https://github.com/acmesh-official/acme.sh/wiki/Synology-NAS-Guide" + DEPRECATED_otp_code="" if _exists oathtool; then - otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)" + DEPRECATED_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 + + if [ -n "$SYNO_DID" ]; then + _H1="Cookie: did=$SYNO_DID" + export _H1 + _debug3 H1 "${_H1}" + fi + + response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DID" "$_base_url/webapi/auth.cgi?enable_syno_token=yes") + _debug3 response "$response" + # END - DEPRECATED, only kept for legacy compatibility reasons + # Get device ID if still empty first, otherwise log in right away + elif [ -z "${SYNO_Device_ID:-}" ]; then + printf "Enter OTP code for user '%s': " "$SYNO_Username" + read -r otp_code + if [ -z "${SYNO_Device_Name:-}" ]; then + printf "Enter device name or leave empty for default (CertRenewal): " + read -r SYNO_Device_Name + [ -n "${SYNO_Device_Name}" ] || SYNO_Device_Name="CertRenewal" + fi + + response=$(_get "$_base_url/webapi/entry.cgi?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&otp_code=$otp_code&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_Device_Name") + _debug3 response "$response" + SYNO_Device_ID=$(echo "$response" | grep "device_id" | sed -n 's/.*"device_id" *: *"\([^"]*\).*/\1/p') + _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID" + else + response=$(_get "$_base_url/webapi/entry.cgi?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_Device_Name&device_id=$SYNO_Device_ID") + _debug3 response "$response" fi - if [ -n "$SYNO_DID" ]; then - _H1="Cookie: did=$SYNO_DID" - export _H1 - _debug3 H1 "${_H1}" - fi - - response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$otp_code&device_name=certrenewal&device_id=$SYNO_DID" "$_base_url/webapi/auth.cgi?enable_syno_token=yes") + sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p') token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p') - _debug3 response "$response" - _debug token "$token" - - if [ -z "$token" ]; then - _err "Unable to authenticate to $SYNO_Hostname:$SYNO_Port using $SYNO_Scheme." - _err "Check your username and password." - _err "If two-factor authentication is enabled for the user, set SYNO_TOTP_SECRET." + _debug "Session ID" "$sid" + _debug SynoToken "$token" + if [ -z "$SYNO_DID" ] && [ -z "$SYNO_Device_ID" ] || [ -z "$sid" ] || [ -z "$token" ]; then + _err "Unable to authenticate to $_base_url - check your username & password." + _err "If two-factor authentication is enabled for the user, set SYNO_Device_ID." return 1 fi - sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p') _H1="X-SYNO-TOKEN: $token" export _H1 _debug2 H1 "${_H1}" - # Now that we know the username and password are good, save them + # Now that we know the username & password are good, save them _savedeployconf SYNO_Username "$SYNO_Username" _savedeployconf SYNO_Password "$SYNO_Password" - _savedeployconf SYNO_DID "$SYNO_DID" - _savedeployconf SYNO_TOTP_SECRET "$SYNO_TOTP_SECRET" + _savedeployconf SYNO_Device_Name "$SYNO_Device_Name" + _savedeployconf SYNO_Device_ID "$SYNO_Device_ID" _info "Getting certificates in Synology DSM" response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1&_sid=$sid" "$_base_url/webapi/entry.cgi") @@ -140,11 +171,11 @@ synology_dsm_deploy() { _debug2 id "$id" if [ -z "$id" ] && [ -z "${SYNO_Create:-}" ]; then - _err "Unable to find certificate: $SYNO_Certificate and \$SYNO_Create is not set" + _err "Unable to find certificate: $SYNO_Certificate & \$SYNO_Create is not set" return 1 fi - # we've verified this certificate description is a thing, so save it + # We've verified this certificate description is a thing, so save it _savedeployconf SYNO_Certificate "$SYNO_Certificate" "base64" _info "Generate form POST request" @@ -156,10 +187,10 @@ synology_dsm_deploy() { content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_Certificate}" if echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\([^{]*\).*/\1/p" | grep -- 'is_default":true' >/dev/null; then - _debug2 default "this is the default certificate" + _debug2 default "This is the default certificate" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}true" else - _debug2 default "this is NOT the default certificate" + _debug2 default "This is NOT the default certificate" fi content="$content${nl}--$delim--${nl}" content="$(printf "%b_" "$content")" @@ -171,13 +202,23 @@ synology_dsm_deploy() { if ! echo "$response" | grep '"error":' >/dev/null; then if echo "$response" | grep '"restart_httpd":true' >/dev/null; then - _info "http services were restarted" + _info "Restarting HTTP services succeeded" else - _info "http services were NOT restarted" + _info "Restarting HTTP services failed" fi + + _logout return 0 else _err "Unable to update certificate, error code $response" + _logout return 1 fi } + +#################### Private functions below ################################## +_logout() { + # Logout to not occupy a permanent session, e.g. in DSM's "Connected Users" widget + response=$(_get "$_base_url/webapi/entry.cgi?api=SYNO.API.Auth&version=$api_version&method=logout") + _debug3 response "$response" +} diff --git a/deploy/truenas.sh b/deploy/truenas.sh index 84cfd5f4..c79e6dac 100644 --- a/deploy/truenas.sh +++ b/deploy/truenas.sh @@ -184,6 +184,27 @@ truenas_deploy() { _info "S3 certificate is not configured or is not the same as TrueNAS web UI" fi + _info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required" + if _exists jq; then + _info "Query all chart release" + _release_list=$(_get "$_api_url/chart/release") + _related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique") + _release_length=$(printf "%s" "$_related_name_list" | jq -r "length") + _info "Found $_release_length related chart release in list: $_related_name_list" + for i in $(seq 0 $((_release_length - 1))); do + _release_name=$(echo "$_related_name_list" | jq -r ".[$i]") + _info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name" + #Read the chart release configuration + _chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")") + #Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress + _updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert ) |= $_cert_id | .config.ingress ") + _update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")" + _debug3 _update_chart_result "$_update_chart_result" + done + else + _info "Tool 'jq' does not exists, skip chart release checking" + fi + _info "Deleting old certificate" _delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")" diff --git a/deploy/vault.sh b/deploy/vault.sh index 399abaee..569faba2 100644 --- a/deploy/vault.sh +++ b/deploy/vault.sh @@ -7,13 +7,16 @@ # # VAULT_PREFIX - this contains the prefix path in vault # VAULT_ADDR - vault requires this to find your vault server +# VAULT_SAVE_TOKEN - set to anything if you want to save the token +# VAULT_RENEW_TOKEN - set to anything if you want to renew the token to default TTL before deploying +# VAULT_KV_V2 - set to anything if you are using v2 of the kv engine # # additionally, you need to ensure that VAULT_TOKEN is avialable # to access the vault server #returns 0 means success, otherwise error. -######## Public functions ##################### +######## Public functions ##################### #domain keyfile certfile cafile fullchain vault_deploy() { @@ -45,6 +48,26 @@ vault_deploy() { fi _savedeployconf VAULT_ADDR "$VAULT_ADDR" + _getdeployconf VAULT_SAVE_TOKEN + _savedeployconf VAULT_SAVE_TOKEN "$VAULT_SAVE_TOKEN" + + _getdeployconf VAULT_RENEW_TOKEN + _savedeployconf VAULT_RENEW_TOKEN "$VAULT_RENEW_TOKEN" + + _getdeployconf VAULT_KV_V2 + _savedeployconf VAULT_KV_V2 "$VAULT_KV_V2" + + _getdeployconf VAULT_TOKEN + if [ -z "$VAULT_TOKEN" ]; then + _err "VAULT_TOKEN needs to be defined" + return 1 + fi + if [ -n "$VAULT_SAVE_TOKEN" ]; then + _savedeployconf VAULT_TOKEN "$VAULT_TOKEN" + fi + + _migratedeployconf FABIO VAULT_FABIO_MODE + # JSON does not allow multiline strings. # So replacing new-lines with "\n" here _ckey=$(sed -z 's/\n/\\n/g' <"$2") @@ -52,26 +75,56 @@ vault_deploy() { _cca=$(sed -z 's/\n/\\n/g' <"$4") _cfullchain=$(sed -z 's/\n/\\n/g' <"$5") - URL="$VAULT_ADDR/v1/$VAULT_PREFIX/$_cdomain" export _H1="X-Vault-Token: $VAULT_TOKEN" - if [ -n "$FABIO" ]; then + if [ -n "$VAULT_RENEW_TOKEN" ]; then + URL="$VAULT_ADDR/v1/auth/token/renew-self" + _info "Renew the Vault token to default TTL" + if ! _post "" "$URL" >/dev/null; then + _err "Failed to renew the Vault token" + return 1 + fi + fi + + URL="$VAULT_ADDR/v1/$VAULT_PREFIX/$_cdomain" + + if [ -n "$VAULT_FABIO_MODE" ]; then + _info "Writing certificate and key to $URL in Fabio mode" if [ -n "$VAULT_KV_V2" ]; then - _post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL" + _post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL" >/dev/null || return 1 else - _post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL" + _post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL" >/dev/null || return 1 fi else if [ -n "$VAULT_KV_V2" ]; then - _post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem" - _post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key" - _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem" - _post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem" + _info "Writing certificate to $URL/cert.pem" + _post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem" >/dev/null || return 1 + _info "Writing key to $URL/cert.key" + _post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key" >/dev/null || return 1 + _info "Writing CA certificate to $URL/ca.pem" + _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem" >/dev/null || return 1 + _info "Writing full-chain certificate to $URL/fullchain.pem" + _post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem" >/dev/null || return 1 else - _post "{\"value\": \"$_ccert\"}" "$URL/cert.pem" - _post "{\"value\": \"$_ckey\"}" "$URL/cert.key" - _post "{\"value\": \"$_cca\"}" "$URL/chain.pem" - _post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem" + _info "Writing certificate to $URL/cert.pem" + _post "{\"value\": \"$_ccert\"}" "$URL/cert.pem" >/dev/null || return 1 + _info "Writing key to $URL/cert.key" + _post "{\"value\": \"$_ckey\"}" "$URL/cert.key" >/dev/null || return 1 + _info "Writing CA certificate to $URL/ca.pem" + _post "{\"value\": \"$_cca\"}" "$URL/ca.pem" >/dev/null || return 1 + _info "Writing full-chain certificate to $URL/fullchain.pem" + _post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem" >/dev/null || return 1 + fi + + # To make it compatible with the wrong ca path `chain.pem` which was used in former versions + if _contains "$(_get "$URL/chain.pem")" "-----BEGIN CERTIFICATE-----"; then + _err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning" + _info "Updating CA certificate to $URL/chain.pem for backward compatibility" + if [ -n "$VAULT_KV_V2" ]; then + _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem" >/dev/null || return 1 + else + _post "{\"value\": \"$_cca\"}" "$URL/chain.pem" >/dev/null || return 1 + fi fi fi diff --git a/deploy/vault_cli.sh b/deploy/vault_cli.sh index cbb8cc59..3ebb8074 100644 --- a/deploy/vault_cli.sh +++ b/deploy/vault_cli.sh @@ -8,6 +8,8 @@ # # VAULT_PREFIX - this contains the prefix path in vault # VAULT_ADDR - vault requires this to find your vault server +# VAULT_SAVE_TOKEN - set to anything if you want to save the token +# VAULT_RENEW_TOKEN - set to anything if you want to renew the token to default TTL before deploying # # additionally, you need to ensure that VAULT_TOKEN is avialable or # `vault auth` has applied the appropriate authorization for the vault binary @@ -33,15 +35,36 @@ vault_cli_deploy() { _debug _cfullchain "$_cfullchain" # validate required env vars + _getdeployconf VAULT_PREFIX if [ -z "$VAULT_PREFIX" ]; then _err "VAULT_PREFIX needs to be defined (contains prefix path in vault)" return 1 fi + _savedeployconf VAULT_PREFIX "$VAULT_PREFIX" + _getdeployconf VAULT_ADDR if [ -z "$VAULT_ADDR" ]; then _err "VAULT_ADDR needs to be defined (contains vault connection address)" return 1 fi + _savedeployconf VAULT_ADDR "$VAULT_ADDR" + + _getdeployconf VAULT_SAVE_TOKEN + _savedeployconf VAULT_SAVE_TOKEN "$VAULT_SAVE_TOKEN" + + _getdeployconf VAULT_RENEW_TOKEN + _savedeployconf VAULT_RENEW_TOKEN "$VAULT_RENEW_TOKEN" + + _getdeployconf VAULT_TOKEN + if [ -z "$VAULT_TOKEN" ]; then + _err "VAULT_TOKEN needs to be defined" + return 1 + fi + if [ -n "$VAULT_SAVE_TOKEN" ]; then + _savedeployconf VAULT_TOKEN "$VAULT_TOKEN" + fi + + _migratedeployconf FABIO VAULT_FABIO_MODE VAULT_CMD=$(command -v vault) if [ ! $? ]; then @@ -49,13 +72,33 @@ vault_cli_deploy() { return 1 fi - if [ -n "$FABIO" ]; then + if [ -n "$VAULT_RENEW_TOKEN" ]; then + _info "Renew the Vault token to default TTL" + if ! $VAULT_CMD token renew; then + _err "Failed to renew the Vault token" + return 1 + fi + fi + + if [ -n "$VAULT_FABIO_MODE" ]; then + _info "Writing certificate and key to ${VAULT_PREFIX}/${_cdomain} in Fabio mode" $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1 else + _info "Writing certificate to ${VAULT_PREFIX}/${_cdomain}/cert.pem" $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1 + _info "Writing key to ${VAULT_PREFIX}/${_cdomain}/cert.key" $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1 - $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1 + _info "Writing CA certificate to ${VAULT_PREFIX}/${_cdomain}/ca.pem" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/ca.pem" value=@"$_cca" || return 1 + _info "Writing full-chain certificate to ${VAULT_PREFIX}/${_cdomain}/fullchain.pem" $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1 + + # To make it compatible with the wrong ca path `chain.pem` which was used in former versions + if $VAULT_CMD kv get "${VAULT_PREFIX}/${_cdomain}/chain.pem" >/dev/null; then + _err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning" + _info "Updating CA certificate to ${VAULT_PREFIX}/${_cdomain}/chain.pem for backward compatibility" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1 + fi fi } diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh index 6accc597..2c6b2e4f 100755 --- a/dnsapi/dns_1984hosting.sh +++ b/dnsapi/dns_1984hosting.sh @@ -1,46 +1,46 @@ #!/usr/bin/env sh -#This file name is "dns_1984hosting.sh" -#So, here must be a method dns_1984hosting_add() -#Which will be called by acme.sh to add the txt record to your api system. -#returns 0 means success, otherwise error. +# This file name is "dns_1984hosting.sh" +# So, here must be a method dns_1984hosting_add() +# Which will be called by acme.sh to add the txt record to your api system. +# returns 0 means success, otherwise error. -#Author: Adrian Fedoreanu -#Report Bugs here: https://github.com/acmesh-official/acme.sh +# Author: Adrian Fedoreanu +# Report Bugs here: https://github.com/acmesh-official/acme.sh # or here... https://github.com/acmesh-official/acme.sh/issues/2851 -# -######## Public functions ##################### + +######## Public functions ##################### # Export 1984HOSTING username and password in following variables # # One984HOSTING_Username=username # One984HOSTING_Password=password # -# sessionid cookie is saved in ~/.acme.sh/account.conf -# username/password need to be set only when changed. +# username/password and csrftoken/sessionid cookies are saved in ~/.acme.sh/account.conf -#Usage: dns_1984hosting_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Usage: dns_1984hosting_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Add a text record. dns_1984hosting_add() { fulldomain=$1 txtvalue=$2 - _info "Add TXT record using 1984Hosting" + _info "Add TXT record using 1984Hosting." _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" if ! _1984hosting_login; then - _err "1984Hosting login failed for user $One984HOSTING_Username. Check $HTTP_HEADER file" + _err "1984Hosting login failed for user $One984HOSTING_Username. Check $HTTP_HEADER file." return 1 fi - _debug "First detect the root zone" + _debug "First detect the root zone." if ! _get_root "$fulldomain"; then - _err "invalid domain" "$fulldomain" + _err "Invalid domain '$fulldomain'." return 1 fi _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _debug "Add TXT record $fulldomain with value '$txtvalue'" + _debug "Add TXT record $fulldomain with value '$txtvalue'." value="$(printf '%s' "$txtvalue" | _url_encode)" url="https://1984.hosting/domains/entry/" @@ -53,93 +53,97 @@ dns_1984hosting_add() { _debug2 postdata "$postdata" _authpost "$postdata" "$url" - response="$(echo "$_response" | _normalizeJson)" - _debug2 response "$response" - - if _contains "$response" '"haserrors": true'; then - _err "1984Hosting failed to add TXT record for $_sub_domain bad RC from _post" + if _contains "$_response" '"haserrors": true'; then + _err "1984Hosting failed to add TXT record for $_sub_domain bad RC from _post." return 1 - elif _contains "$response" "html>"; then - _err "1984Hosting failed to add TXT record for $_sub_domain. Check $HTTP_HEADER file" + elif _contains "$_response" "html>"; then + _err "1984Hosting failed to add TXT record for $_sub_domain. Check $HTTP_HEADER file." return 1 - elif _contains "$response" '"auth": false'; then - _err "1984Hosting failed to add TXT record for $_sub_domain. Invalid or expired cookie" + elif _contains "$_response" '"auth": false'; then + _err "1984Hosting failed to add TXT record for $_sub_domain. Invalid or expired cookie." return 1 fi - _info "Added acme challenge TXT record for $fulldomain at 1984Hosting" + _info "Added acme challenge TXT record for $fulldomain at 1984Hosting." return 0 } -#Usage: fulldomain txtvalue -#Remove the txt record after validation. +# Usage: fulldomain txtvalue +# Remove the txt record after validation. dns_1984hosting_rm() { fulldomain=$1 txtvalue=$2 - _info "Delete TXT record using 1984Hosting" + _info "Delete TXT record using 1984Hosting." _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" if ! _1984hosting_login; then - _err "1984Hosting login failed for user $One984HOSTING_Username. Check $HTTP_HEADER file" + _err "1984Hosting login failed for user $One984HOSTING_Username. Check $HTTP_HEADER file." return 1 fi - _debug "First detect the root zone" + _debug "First detect the root zone." if ! _get_root "$fulldomain"; then - _err "invalid domain" "$fulldomain" + _err "Invalid domain '$fulldomain'." return 1 fi _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _debug "Delete $fulldomain TXT record" + _debug "Delete $fulldomain TXT record." url="https://1984.hosting/domains" if ! _get_zone_id "$url" "$_domain"; then - _err "invalid zone" "$_domain" + _err "Invalid zone '$_domain'." return 1 fi _htmlget "$url/$_zone_id" "$txtvalue" - _debug2 _response "$_response" entry_id="$(echo "$_response" | _egrep_o 'entry_[0-9]+' | sed 's/entry_//')" _debug2 entry_id "$entry_id" if [ -z "$entry_id" ]; then - _err "Error getting TXT entry_id for $1" + _err "Error getting TXT entry_id for $1." return 1 fi _authpost "entry=$entry_id" "$url/delentry/" - response="$(echo "$_response" | _normalizeJson)" - _debug2 response "$response" - - if ! _contains "$response" '"ok": true'; then - _err "1984Hosting failed to delete TXT record for $entry_id bad RC from _post" + if ! _contains "$_response" '"ok": true'; then + _err "1984Hosting failed to delete TXT record for $entry_id bad RC from _post." return 1 fi - _info "Deleted acme challenge TXT record for $fulldomain at 1984Hosting" + _info "Deleted acme challenge TXT record for $fulldomain at 1984Hosting." return 0 } #################### Private functions below ################################## - -# usage: _1984hosting_login username password -# returns 0 success _1984hosting_login() { if ! _check_credentials; then return 1; fi if _check_cookies; then - _debug "Already logged in" + _debug "Already logged in." return 0 fi - _debug "Login to 1984Hosting as user $One984HOSTING_Username" + _debug "Login to 1984Hosting as user $One984HOSTING_Username." username=$(printf '%s' "$One984HOSTING_Username" | _url_encode) password=$(printf '%s' "$One984HOSTING_Password" | _url_encode) url="https://1984.hosting/accounts/checkuserauth/" + _get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken" + csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')" + sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')" + + if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then + _err "One or more cookies are empty: '$csrftoken', '$sessionid'." + return 1 + fi + + export _H1="Cookie: $csrftoken; $sessionid" + export _H2="Referer: https://1984.hosting/accounts/login/" + csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1) + export _H3="X-CSRFToken: $csrf_header" + response="$(_post "username=$username&password=$password&otpkey=" $url)" response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" @@ -149,6 +153,8 @@ _1984hosting_login() { One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')" export One984HOSTING_SESSIONID_COOKIE export One984HOSTING_CSRFTOKEN_COOKIE + _saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username" + _saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password" _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE" _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE" return 0 @@ -157,9 +163,13 @@ _1984hosting_login() { } _check_credentials() { + One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}" + One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}" if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then One984HOSTING_Username="" One984HOSTING_Password="" + _clearaccountconf_mutable One984HOSTING_Username + _clearaccountconf_mutable One984HOSTING_Password _err "You haven't specified 1984Hosting username or password yet." _err "Please export as One984HOSTING_Username / One984HOSTING_Password and try again." return 1 @@ -171,42 +181,43 @@ _check_cookies() { One984HOSTING_SESSIONID_COOKIE="${One984HOSTING_SESSIONID_COOKIE:-$(_readaccountconf_mutable One984HOSTING_SESSIONID_COOKIE)}" One984HOSTING_CSRFTOKEN_COOKIE="${One984HOSTING_CSRFTOKEN_COOKIE:-$(_readaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE)}" if [ -z "$One984HOSTING_SESSIONID_COOKIE" ] || [ -z "$One984HOSTING_CSRFTOKEN_COOKIE" ]; then - _debug "No cached cookie(s) found" + _debug "No cached cookie(s) found." return 1 fi _authget "https://1984.hosting/accounts/loginstatus/" - if _contains "$response" '"ok": true'; then - _debug "Cached cookies still valid" + if _contains "$_response" '"ok": true'; then + _debug "Cached cookies still valid." return 0 fi - _debug "Cached cookies no longer valid" + + _debug "Cached cookies no longer valid. Clearing cookies." One984HOSTING_SESSIONID_COOKIE="" One984HOSTING_CSRFTOKEN_COOKIE="" - _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE" - _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE" + _clearaccountconf_mutable One984HOSTING_SESSIONID_COOKIE + _clearaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE return 1 } -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com +# _acme-challenge.www.domain.com +# Returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com _get_root() { domain="$1" i=1 p=1 while true; do - h=$(printf "%s" "$domain" | cut -d . -f $i-100) + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + # not valid if [ -z "$h" ]; then - #not valid return 1 fi _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) + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$h" return 0 fi @@ -216,46 +227,47 @@ _get_root() { return 1 } -#usage: _get_zone_id url domain.com -#returns zone id for domain.com +# Usage: _get_zone_id url domain.com +# Returns zone id for domain.com _get_zone_id() { url=$1 domain=$2 _htmlget "$url" "$domain" - _debug2 _response "$_response" _zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)" _debug2 _zone_id "$_zone_id" if [ -z "$_zone_id" ]; then - _err "Error getting _zone_id for $2" + _err "Error getting _zone_id for $2." return 1 fi return 0 } -# add extra headers to request +# Add extra headers to request _authget() { - export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE" + export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE" _response=$(_get "$1" | _normalizeJson) _debug2 _response "$_response" } -# truncate huge HTML response -# echo: Argument list too long +# Truncate huge HTML response +# Echo: Argument list too long _htmlget() { - export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE" + export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE" _response=$(_get "$1" | grep "$2") if _contains "$_response" "@$2"; then _response=$(echo "$_response" | grep -v "[@]" | _head_n 1) fi + _debug2 _response "$_response" } -# add extra headers to request +# Add extra headers to request _authpost() { 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 _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE" export _H2="Referer: https://1984.hosting/domains/$_zone_id" export _H3="X-CSRFToken: $csrf_header" - _response=$(_post "$1" "$2") + _response="$(_post "$1" "$2" | _normalizeJson)" + _debug2 _response "$_response" } diff --git a/dnsapi/dns_arvan.sh b/dnsapi/dns_arvan.sh index 4c9217e5..4ca5b685 100644 --- a/dnsapi/dns_arvan.sh +++ b/dnsapi/dns_arvan.sh @@ -1,10 +1,10 @@ #!/usr/bin/env sh -#Arvan_Token="Apikey xxxx" +# Arvan_Token="Apikey xxxx" -ARVAN_API_URL="https://napi.arvancloud.com/cdn/4.0/domains" -#Author: Vahid Fardi -#Report Bugs here: https://github.com/Neilpang/acme.sh +ARVAN_API_URL="https://napi.arvancloud.ir/cdn/4.0/domains" +# Author: Vahid Fardi +# Report Bugs here: https://github.com/Neilpang/acme.sh # ######## Public functions ##################### @@ -18,7 +18,7 @@ dns_arvan_add() { if [ -z "$Arvan_Token" ]; then _err "You didn't specify \"Arvan_Token\" token yet." - _err "You can get yours from here https://npanel.arvancloud.com/profile/api-keys" + _err "You can get yours from here https://npanel.arvancloud.ir/profile/api-keys" return 1 fi #save the api token to the account conf file. @@ -40,7 +40,7 @@ dns_arvan_add() { _info "response id is $response" _info "Added, OK" return 0 - elif _contains "$response" "Record Data is Duplicated"; then + elif _contains "$response" "Record Data is duplicate"; then _info "Already exists, OK" return 0 else @@ -141,6 +141,7 @@ _arvan_rest() { response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")" elif [ "$mtd" = "POST" ]; then export _H2="Content-Type: application/json" + export _H3="Accept: application/json" _debug data "$data" response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")" else diff --git a/dnsapi/dns_bookmyname.sh b/dnsapi/dns_bookmyname.sh new file mode 100644 index 00000000..62548fd0 --- /dev/null +++ b/dnsapi/dns_bookmyname.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env sh + +#Here is a sample custom api script. +#This file name is "dns_bookmyname.sh" +#So, here must be a method dns_bookmyname_add() +#Which will be called by acme.sh to add the txt record to your api system. +#returns 0 means success, otherwise error. +# +#Author: Neilpang +#Report Bugs here: https://github.com/acmesh-official/acme.sh +# +######## Public functions ##################### + +# Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide + +# BookMyName urls: +# https://BOOKMYNAME_USERNAME:BOOKMYNAME_PASSWORD@www.bookmyname.com/dyndns/?hostname=_acme-challenge.domain.tld&type=txt&ttl=300&do=add&value="XXXXXXXX"' +# https://BOOKMYNAME_USERNAME:BOOKMYNAME_PASSWORD@www.bookmyname.com/dyndns/?hostname=_acme-challenge.domain.tld&type=txt&ttl=300&do=remove&value="XXXXXXXX"' + +# Output: +#good: update done, cid 123456, domain id 456789, type txt, ip XXXXXXXX +#good: remove done 1, cid 123456, domain id 456789, ttl 300, type txt, ip XXXXXXXX + +# Be careful, BMN DNS servers can be slow to pick up changes; using dnssleep is thus advised. + +# Usage: +# export BOOKMYNAME_USERNAME="ABCDE-FREE" +# export BOOKMYNAME_PASSWORD="MyPassword" +# /usr/local/ssl/acme.sh/acme.sh --dns dns_bookmyname --dnssleep 600 --issue -d domain.tld + +#Usage: dns_bookmyname_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_bookmyname_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using bookmyname" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + BOOKMYNAME_USERNAME="${BOOKMYNAME_USERNAME:-$(_readaccountconf_mutable BOOKMYNAME_USERNAME)}" + BOOKMYNAME_PASSWORD="${BOOKMYNAME_PASSWORD:-$(_readaccountconf_mutable BOOKMYNAME_PASSWORD)}" + + if [ -z "$BOOKMYNAME_USERNAME" ] || [ -z "$BOOKMYNAME_PASSWORD" ]; then + BOOKMYNAME_USERNAME="" + BOOKMYNAME_PASSWORD="" + _err "You didn't specify BookMyName username and password yet." + _err "Please specify them and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable BOOKMYNAME_USERNAME "$BOOKMYNAME_USERNAME" + _saveaccountconf_mutable BOOKMYNAME_PASSWORD "$BOOKMYNAME_PASSWORD" + + uri="https://${BOOKMYNAME_USERNAME}:${BOOKMYNAME_PASSWORD}@www.bookmyname.com/dyndns/" + data="?hostname=${fulldomain}&type=TXT&ttl=300&do=add&value=${txtvalue}" + result="$(_get "${uri}${data}")" + _debug "Result: $result" + + if ! _startswith "$result" 'good: update done, cid '; then + _err "Can't add $fulldomain" + return 1 + fi + +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_bookmyname_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using bookmyname" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + BOOKMYNAME_USERNAME="${BOOKMYNAME_USERNAME:-$(_readaccountconf_mutable BOOKMYNAME_USERNAME)}" + BOOKMYNAME_PASSWORD="${BOOKMYNAME_PASSWORD:-$(_readaccountconf_mutable BOOKMYNAME_PASSWORD)}" + + uri="https://${BOOKMYNAME_USERNAME}:${BOOKMYNAME_PASSWORD}@www.bookmyname.com/dyndns/" + data="?hostname=${fulldomain}&type=TXT&ttl=300&do=remove&value=${txtvalue}" + result="$(_get "${uri}${data}")" + _debug "Result: $result" + + if ! _startswith "$result" 'good: remove done 1, cid '; then + _info "Can't remove $fulldomain" + fi + +} + +#################### Private functions below ################################## diff --git a/dnsapi/dns_cloudns.sh b/dnsapi/dns_cloudns.sh index b03fd579..8d7fd437 100755 --- a/dnsapi/dns_cloudns.sh +++ b/dnsapi/dns_cloudns.sh @@ -78,7 +78,7 @@ dns_cloudns_rm() { return 1 fi - for i in $(echo "$response" | tr '{' "\n" | grep "$record"); do + for i in $(echo "$response" | tr '{' "\n" | grep -- "$record"); do record_id=$(echo "$i" | tr ',' "\n" | grep -E '^"id"' | sed -re 's/^\"id\"\:\"([0-9]+)\"$/\1/g') if [ -n "$record_id" ]; then diff --git a/dnsapi/dns_gcore.sh b/dnsapi/dns_gcore.sh new file mode 100755 index 00000000..d549a650 --- /dev/null +++ b/dnsapi/dns_gcore.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env sh + +# +#GCORE_Key='773$7b7adaf2a2b32bfb1b83787b4ff32a67eb178e3ada1af733e47b1411f2461f7f4fa7ed7138e2772a46124377bad7384b3bb8d87748f87b3f23db4b8bbe41b2bb' +# + +GCORE_Api="https://api.gcorelabs.com/dns/v2" +GCORE_Doc="https://apidocs.gcore.com/dns" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_gcore_add() { + fulldomain=$1 + txtvalue=$2 + + GCORE_Key="${GCORE_Key:-$(_readaccountconf_mutable GCORE_Key)}" + + if [ -z "$GCORE_Key" ]; then + GCORE_Key="" + _err "You didn't specify a Gcore api key yet." + _err "You can get yours from here $GCORE_Doc" + return 1 + fi + + #save the api key to the account conf file. + _saveaccountconf_mutable GCORE_Key "$GCORE_Key" + + _debug "First detect the zone name" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _zone_name "$_zone_name" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _gcore_rest GET "zones/$_zone_name/$fulldomain/TXT" + payload="" + + if echo "$response" | grep "record is not found" >/dev/null; then + _info "Record doesn't exists" + payload="{\"resource_records\":[{\"content\":[\"$txtvalue\"],\"enabled\":true}],\"ttl\":120}" + elif echo "$response" | grep "$txtvalue" >/dev/null; then + _info "Already exists, OK" + return 0 + elif echo "$response" | tr -d " " | grep \"name\":\""$fulldomain"\",\"type\":\"TXT\" >/dev/null; then + _info "Record with mismatch txtvalue, try update it" + payload=$(echo "$response" | tr -d " " | sed 's/"updated_at":[0-9]\+,//g' | sed 's/"meta":{}}]}/"meta":{}},{"content":['\""$txtvalue"\"'],"enabled":true}]}/') + fi + + # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so + # we can not use updating anymore. + # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + # _debug count "$count" + # if [ "$count" = "0" ]; then + _info "Adding record" + if _gcore_rest PUT "zones/$_zone_name/$fulldomain/TXT" "$payload"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + elif _contains "$response" "rrset is already exists"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 +} + +#fulldomain txtvalue +dns_gcore_rm() { + fulldomain=$1 + txtvalue=$2 + + GCORE_Key="${GCORE_Key:-$(_readaccountconf_mutable GCORE_Key)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _zone_name "$_zone_name" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _gcore_rest GET "zones/$_zone_name/$fulldomain/TXT" + + if echo "$response" | grep "record is not found" >/dev/null; then + _info "No such txt recrod" + return 0 + fi + + if ! echo "$response" | tr -d " " | grep \"name\":\""$fulldomain"\",\"type\":\"TXT\" >/dev/null; then + _err "Error: $response" + return 1 + fi + + if ! echo "$response" | tr -d " " | grep \""$txtvalue"\" >/dev/null; then + _info "No such txt recrod" + return 0 + fi + + count="$(echo "$response" | grep -o "content" | wc -l)" + + if [ "$count" = "1" ]; then + if ! _gcore_rest DELETE "zones/$_zone_name/$fulldomain/TXT"; then + _err "Delete record error. $response" + return 1 + fi + return 0 + fi + + payload="$(echo "$response" | tr -d " " | sed 's/"updated_at":[0-9]\+,//g' | sed 's/{"id":[0-9]\+,"content":\["'"$txtvalue"'"\],"enabled":true,"meta":{}}//' | sed 's/\[,/\[/' | sed 's/,,/,/' | sed 's/,\]/\]/')" + if ! _gcore_rest PUT "zones/$_zone_name/$fulldomain/TXT" "$payload"; then + _err "Delete record error. $response" + fi +} + +#################### Private functions below ################################## +#_acme-challenge.sub.domain.com +#returns +# _sub_domain=_acme-challenge.sub or _acme-challenge +# _domain=domain.com +# _zone_name=domain.com or sub.domain.com +_get_root() { + domain=$1 + i=1 + p=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 ! _gcore_rest GET "zones/$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _zone_name=$h + if [ "$_zone_name" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_gcore_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + key_trimmed=$(echo "$GCORE_Key" | tr -d '"') + + export _H1="Content-Type: application/json" + export _H2="Authorization: APIKey $key_trimmed" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$GCORE_Api/$ep" "" "$m")" + else + response="$(_get "$GCORE_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_gd.sh b/dnsapi/dns_gd.sh index 44c3d279..1729115e 100755 --- a/dnsapi/dns_gd.sh +++ b/dnsapi/dns_gd.sh @@ -22,8 +22,8 @@ dns_gd_add() { if [ -z "$GD_Key" ] || [ -z "$GD_Secret" ]; then GD_Key="" GD_Secret="" - _err "You don't specify godaddy api key and secret yet." - _err "Please create you key and try again." + _err "You didn't specify godaddy api key and secret yet." + _err "Please create your key and try again." return 1 fi @@ -46,7 +46,7 @@ dns_gd_add() { fi if _contains "$response" "$txtvalue"; then - _info "The record is existing, skip" + _info "This record already exists, skipping" return 0 fi diff --git a/dnsapi/dns_googledomains.sh b/dnsapi/dns_googledomains.sh new file mode 100755 index 00000000..63e3073b --- /dev/null +++ b/dnsapi/dns_googledomains.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env sh + +# Author: Alex Leigh +# Created: 2023-03-02 + +#GOOGLEDOMAINS_ACCESS_TOKEN="xxxx" +#GOOGLEDOMAINS_ZONE="xxxx" +GOOGLEDOMAINS_API="https://acmedns.googleapis.com/v1/acmeChallengeSets" + +######## Public functions ######## + +#Usage: dns_googledomains_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_googledomains_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Invoking Google Domains ACME DNS API." + + if ! _dns_googledomains_setup; then + return 1 + fi + + zone="$(_dns_googledomains_get_zone "$fulldomain")" + if [ -z "$zone" ]; then + _err "Could not find a Google Domains-managed zone containing the requested domain." + return 1 + fi + + _debug zone "$zone" + _debug txtvalue "$txtvalue" + + _info "Adding TXT record for $fulldomain." + if _dns_googledomains_api "$zone" ":rotateChallenges" "{\"accessToken\":\"$GOOGLEDOMAINS_ACCESS_TOKEN\",\"recordsToAdd\":[{\"fqdn\":\"$fulldomain\",\"digest\":\"$txtvalue\"}],\"keepExpiredRecords\":true}"; then + if _contains "$response" "$txtvalue"; then + _info "TXT record added." + return 0 + else + _err "Error adding TXT record." + return 1 + fi + fi + + _err "Error adding TXT record." + return 1 +} + +#Usage: dns_googledomains_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_googledomains_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Invoking Google Domains ACME DNS API." + + if ! _dns_googledomains_setup; then + return 1 + fi + + zone="$(_dns_googledomains_get_zone "$fulldomain")" + if [ -z "$zone" ]; then + _err "Could not find a Google Domains-managed domain based on request." + return 1 + fi + + _debug zone "$zone" + _debug txtvalue "$txtvalue" + + _info "Removing TXT record for $fulldomain." + if _dns_googledomains_api "$zone" ":rotateChallenges" "{\"accessToken\":\"$GOOGLEDOMAINS_ACCESS_TOKEN\",\"recordsToRemove\":[{\"fqdn\":\"$fulldomain\",\"digest\":\"$txtvalue\"}],\"keepExpiredRecords\":true}"; then + if _contains "$response" "$txtvalue"; then + _err "Error removing TXT record." + return 1 + else + _info "TXT record removed." + return 0 + fi + fi + + _err "Error removing TXT record." + return 1 +} + +######## Private functions ######## + +_dns_googledomains_setup() { + if [ -n "$GOOGLEDOMAINS_SETUP_COMPLETED" ]; then + return 0 + fi + + GOOGLEDOMAINS_ACCESS_TOKEN="${GOOGLEDOMAINS_ACCESS_TOKEN:-$(_readaccountconf_mutable GOOGLEDOMAINS_ACCESS_TOKEN)}" + GOOGLEDOMAINS_ZONE="${GOOGLEDOMAINS_ZONE:-$(_readaccountconf_mutable GOOGLEDOMAINS_ZONE)}" + + if [ -z "$GOOGLEDOMAINS_ACCESS_TOKEN" ]; then + GOOGLEDOMAINS_ACCESS_TOKEN="" + _err "Google Domains access token was not specified." + _err "Please visit Google Domains Security settings to provision an ACME DNS API access token." + return 1 + fi + + if [ "$GOOGLEDOMAINS_ZONE" ]; then + _savedomainconf GOOGLEDOMAINS_ACCESS_TOKEN "$GOOGLEDOMAINS_ACCESS_TOKEN" + _savedomainconf GOOGLEDOMAINS_ZONE "$GOOGLEDOMAINS_ZONE" + else + _saveaccountconf_mutable GOOGLEDOMAINS_ACCESS_TOKEN "$GOOGLEDOMAINS_ACCESS_TOKEN" + _clearaccountconf_mutable GOOGLEDOMAINS_ZONE + _clearaccountconf GOOGLEDOMAINS_ZONE + fi + + _debug GOOGLEDOMAINS_ACCESS_TOKEN "$GOOGLEDOMAINS_ACCESS_TOKEN" + _debug GOOGLEDOMAINS_ZONE "$GOOGLEDOMAINS_ZONE" + + GOOGLEDOMAINS_SETUP_COMPLETED=1 + return 0 +} + +_dns_googledomains_get_zone() { + domain=$1 + + # Use zone directly if provided + if [ "$GOOGLEDOMAINS_ZONE" ]; then + if ! _dns_googledomains_api "$GOOGLEDOMAINS_ZONE"; then + return 1 + fi + + echo "$GOOGLEDOMAINS_ZONE" + return 0 + fi + + i=2 + while true; do + curr=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug curr "$curr" + + if [ -z "$curr" ]; then + return 1 + fi + + if _dns_googledomains_api "$curr"; then + echo "$curr" + return 0 + fi + + i=$(_math "$i" + 1) + done + + return 1 +} + +_dns_googledomains_api() { + zone=$1 + apimethod=$2 + data="$3" + + if [ -z "$data" ]; then + response="$(_get "$GOOGLEDOMAINS_API/$zone$apimethod")" + else + _debug data "$data" + export _H1="Content-Type: application/json" + response="$(_post "$data" "$GOOGLEDOMAINS_API/$zone$apimethod")" + fi + + _debug response "$response" + + if [ "$?" != "0" ]; then + _err "Error" + return 1 + fi + + if _contains "$response" "\"error\": {"; then + return 1 + fi + + return 0 +} diff --git a/dnsapi/dns_huaweicloud.sh b/dnsapi/dns_huaweicloud.sh index ceda9258..b61c1d43 100644 --- a/dnsapi/dns_huaweicloud.sh +++ b/dnsapi/dns_huaweicloud.sh @@ -23,7 +23,7 @@ dns_huaweicloud_add() { HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}" - HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" + HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}" # Check information if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then @@ -74,7 +74,7 @@ dns_huaweicloud_rm() { HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}" - HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" + HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}" # Check information if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then @@ -98,19 +98,59 @@ dns_huaweicloud_rm() { fi _debug "Zone ID is:" "${zoneid}" - # Remove all records - # Therotically HuaweiCloud does not allow more than one record set - # But remove them recurringly to increase robusty - while [ "${record_id}" != "0" ]; do - _debug "Removing Record" - _rm_record "${token}" "${zoneid}" "${record_id}" - record_id="$(_get_recordset_id "${token}" "${fulldomain}" "${zoneid}")" - done + record_id="$(_get_recordset_id "${token}" "${fulldomain}" "${zoneid}")" + _recursive_rm_record "${token}" "${fulldomain}" "${zoneid}" "${record_id}" + ret="$?" + if [ "${ret}" != "0" ]; then + _err "dns_api(dns_huaweicloud): Error removing record." + return 1 + fi + return 0 } ################### Private functions below ################################## +# _recursive_rm_record +# remove all records from the record set +# +# _token=$1 +# _domain=$2 +# _zoneid=$3 +# _record_id=$4 +# +# Returns 0 on success +_recursive_rm_record() { + _token=$1 + _domain=$2 + _zoneid=$3 + _record_id=$4 + + # Most likely to have problems will huaweicloud side if more than 50 attempts but still cannot fully remove the record set + # Maybe can be removed manually in the dashboard + _retry_cnt=50 + + # Remove all records + # Therotically HuaweiCloud does not allow more than one record set + # But remove them recurringly to increase robusty + + while [ "${_record_id}" != "0" ] && [ "${_retry_cnt}" != "0" ]; do + _debug "Removing Record" + _retry_cnt=$((_retry_cnt - 1)) + _rm_record "${_token}" "${_zoneid}" "${_record_id}" + _record_id="$(_get_recordset_id "${_token}" "${_domain}" "${_zoneid}")" + _debug2 "Checking record exists: record_id=${_record_id}" + done + + # Check if retry count is reached + if [ "${_retry_cnt}" = "0" ]; then + _debug "Failed to remove record after 50 attempts, please try removing it manually in the dashboard" + return 1 + fi + + return 0 +} + # _get_zoneid # # _token=$1 @@ -124,7 +164,7 @@ _get_zoneid() { i=1 while true; do - h=$(printf "%s" "${_domain_string}" | cut -d . -f $i-100) + h=$(printf "%s" "${_domain_string}" | cut -d . -f "$i"-100) if [ -z "$h" ]; then #not valid return 1 @@ -135,11 +175,11 @@ _get_zoneid() { if _contains "${response}" '"id"'; then zoneidlist=$(echo "${response}" | _egrep_o "\"id\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | tr -d " ") zonenamelist=$(echo "${response}" | _egrep_o "\"name\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | tr -d " ") - _debug2 "Return Zone ID(s):" "${zoneidlist}" - _debug2 "Return Zone Name(s):" "${zonenamelist}" + _debug2 "Returned Zone ID(s):" "${zoneidlist}" + _debug2 "Returned Zone Name(s):" "${zonenamelist}" zoneidnum=0 zoneidcount=$(echo "${zoneidlist}" | grep -c '^') - _debug "Retund Zone ID(s) Count:" "${zoneidcount}" + _debug "Returned Zone ID(s) Count:" "${zoneidcount}" while [ "${zoneidnum}" -lt "${zoneidcount}" ]; do zoneidnum=$(_math "$zoneidnum" + 1) _zoneid=$(echo "${zoneidlist}" | sed -n "${zoneidnum}p") @@ -206,8 +246,7 @@ _add_record() { \"type\": \"TXT\", \"ttl\": 1, \"records\": [ - ${_exist_record}, - \"\\\"${_txtvalue}\\\"\" + ${_exist_record},\"\\\"${_txtvalue}\\\"\" ] }" fi @@ -215,19 +254,16 @@ _add_record() { _record_id="$(_get_recordset_id "${_token}" "${_domain}" "${zoneid}")" _debug "Record Set ID is:" "${_record_id}" - # Remove all records - while [ "${_record_id}" != "0" ]; do - _debug "Removing Record" - _rm_record "${_token}" "${zoneid}" "${_record_id}" - _record_id="$(_get_recordset_id "${_token}" "${_domain}" "${zoneid}")" - done - # Add brand new records with all old and new records export _H2="Content-Type: application/json" export _H1="X-Auth-Token: ${_token}" _debug2 "${_post_body}" - _post "${_post_body}" "${dns_api}/v2/zones/${zoneid}/recordsets" >/dev/null + if [ -z "${_exist_record}" ]; then + _post "${_post_body}" "${dns_api}/v2/zones/${zoneid}/recordsets" >/dev/null + else + _post "${_post_body}" "${dns_api}/v2/zones/${zoneid}/recordsets/${_record_id}" false "PUT" >/dev/null + fi _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" if [ "$_code" != "202" ]; then _err "dns_huaweicloud: http code ${_code}" diff --git a/dnsapi/dns_ipv64.sh b/dnsapi/dns_ipv64.sh new file mode 100755 index 00000000..54470119 --- /dev/null +++ b/dnsapi/dns_ipv64.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh + +#Created by Roman Lumetsberger, to use ipv64.net's API to add/remove text records +#2022/11/29 + +# Pass credentials before "acme.sh --issue --dns dns_ipv64 ..." +# -- +# export IPv64_Token="aaaaaaaaaaaaaaaaaaaaaaaaaa" +# -- +# + +IPv64_API="https://ipv64.net/api" + +######## Public functions ###################### + +#Usage: dns_ipv64_add _acme-challenge.domain.ipv64.net "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ipv64_add() { + fulldomain=$1 + txtvalue=$2 + + IPv64_Token="${IPv64_Token:-$(_readaccountconf_mutable IPv64_Token)}" + if [ -z "$IPv64_Token" ]; then + _err "You must export variable: IPv64_Token" + _err "The API Key for your IPv64 account is necessary." + _err "You can look it up in your IPv64 account." + return 1 + fi + + # Now save the credentials. + _saveaccountconf_mutable IPv64_Token "$IPv64_Token" + + if ! _get_root "$fulldomain"; then + _err "invalid domain" "$fulldomain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # convert to lower case + _domain="$(echo "$_domain" | _lower_case)" + _sub_domain="$(echo "$_sub_domain" | _lower_case)" + # Now add the TXT record + _info "Trying to add TXT record" + if _ipv64_rest "POST" "add_record=$_domain&praefix=$_sub_domain&type=TXT&content=$txtvalue"; then + _info "TXT record has been successfully added." + return 0 + else + _err "Errors happened during adding the TXT record, response=$_response" + return 1 + fi + +} + +#Usage: fulldomain txtvalue +#Usage: dns_ipv64_rm _acme-challenge.domain.ipv64.net "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +#Remove the txt record after validation. +dns_ipv64_rm() { + fulldomain=$1 + txtvalue=$2 + + IPv64_Token="${IPv64_Token:-$(_readaccountconf_mutable IPv64_Token)}" + if [ -z "$IPv64_Token" ]; then + _err "You must export variable: IPv64_Token" + _err "The API Key for your IPv64 account is necessary." + _err "You can look it up in your IPv64 account." + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "invalid domain" "$fulldomain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # convert to lower case + _domain="$(echo "$_domain" | _lower_case)" + _sub_domain="$(echo "$_sub_domain" | _lower_case)" + # Now delete the TXT record + _info "Trying to delete TXT record" + if _ipv64_rest "DELETE" "del_record=$_domain&praefix=$_sub_domain&type=TXT&content=$txtvalue"; then + _info "TXT record has been successfully deleted." + return 0 + else + _err "Errors happened during deleting the TXT record, response=$_response" + return 1 + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + i=1 + p=1 + + _ipv64_get "get_domains" + domain_data=$_response + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + #if _contains "$domain_data" "\""$h"\"\:"; then + if _contains "$domain_data" "\"""$h""\"\:"; 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 +} + +#send get request to api +# $1 has to set the api-function +_ipv64_get() { + url="$IPv64_API?$1" + export _H1="Authorization: Bearer $IPv64_Token" + + _response=$(_get "$url") + _response="$(echo "$_response" | _normalizeJson)" + + if _contains "$_response" "429 Too Many Requests"; then + _info "API throttled, sleeping to reset the limit" + _sleep 10 + _response=$(_get "$url") + _response="$(echo "$_response" | _normalizeJson)" + fi +} + +_ipv64_rest() { + url="$IPv64_API" + export _H1="Authorization: Bearer $IPv64_Token" + export _H2="Content-Type: application/x-www-form-urlencoded" + _response=$(_post "$2" "$url" "" "$1") + + if _contains "$_response" "429 Too Many Requests"; then + _info "API throttled, sleeping to reset the limit" + _sleep 10 + _response=$(_post "$2" "$url" "" "$1") + fi + + if ! _contains "$_response" "\"info\":\"success\""; then + return 1 + fi + _debug2 response "$_response" + return 0 +} diff --git a/dnsapi/dns_kappernet.sh b/dnsapi/dns_kappernet.sh index 83a7e5f8..0a8951cb 100644 --- a/dnsapi/dns_kappernet.sh +++ b/dnsapi/dns_kappernet.sh @@ -45,8 +45,8 @@ dns_kappernet_add() { if _kappernet_api GET "action=new&subject=$_domain&data=$data"; then if _contains "$response" "{\"OK\":true"; then - _info "Waiting 120 seconds for DNS to spread the new record" - _sleep 120 + _info "Waiting 1 second for DNS to spread the new record" + _sleep 1 return 0 else _err "Error creating a TXT DNS Record: $fullhostname TXT $txtvalue" diff --git a/dnsapi/dns_kas.sh b/dnsapi/dns_kas.sh index 053abd21..1253cf27 100755 --- a/dnsapi/dns_kas.sh +++ b/dnsapi/dns_kas.sh @@ -215,7 +215,7 @@ _get_record_id() { return 1 fi - _record_id="$(echo "$response" | tr -d '\n\r' | sed "s//\n/g" | grep -i "$_record_name" | grep -i ">TXT<" | sed "s/record_id<\/key>/=>/g" | sed "s/<\/value><\/item>/\n/g" | grep "=>" | sed "s/=>//g")" + _record_id="$(echo "$response" | tr -d '\n\r' | sed "s//\n/g" | grep -i "$_record_name" | grep -i ">TXT<" | sed "s/record_id<\/key>/=>/g" | grep -i "$_txtvalue" | sed "s/<\/value><\/item>/\n/g" | grep "=>" | sed "s/=>//g")" _debug "[KAS] -> Record Id: " "$_record_id" return 0 } diff --git a/dnsapi/dns_leaseweb.sh b/dnsapi/dns_leaseweb.sh index 63f81869..4cd3a8f8 100644 --- a/dnsapi/dns_leaseweb.sh +++ b/dnsapi/dns_leaseweb.sh @@ -6,7 +6,7 @@ #See https://developer.leaseweb.com for more information. ######## Public functions ##################### -LSW_API="https://api.leaseweb.com/hosting/v2/domains/" +LSW_API="https://api.leaseweb.com/hosting/v2/domains" #Usage: dns_leaseweb_add _acme-challenge.www.domain.com dns_leaseweb_add() { diff --git a/dnsapi/dns_loopia.sh b/dnsapi/dns_loopia.sh index 399c7867..60d072e0 100644 --- a/dnsapi/dns_loopia.sh +++ b/dnsapi/dns_loopia.sh @@ -107,7 +107,7 @@ _loopia_load_config() { 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" + _err "Password contains a quotation mark or double quotation marks and this is not supported by dns_loopia.sh" return 1 fi diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh new file mode 100644 index 00000000..8ccc8c29 --- /dev/null +++ b/dnsapi/dns_nanelo.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh + +# Official DNS API for Nanelo.com + +# Provide the required API Key like this: +# NANELO_TOKEN="FmD408PdqT1E269gUK57" + +NANELO_API="https://api.nanelo.com/v1/" + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nanelo_add() { + fulldomain=$1 + txtvalue=$2 + + NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" + if [ -z "$NANELO_TOKEN" ]; then + NANELO_TOKEN="" + _err "You didn't configure a Nanelo API Key yet." + _err "Please set NANELO_TOKEN and try again." + _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" + return 1 + fi + _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" + + _info "Adding TXT record to ${fulldomain}" + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" + if _contains "${response}" 'success'; then + return 0 + fi + _err "Could not create resource record, please check the logs" + _err "${response}" + return 1 +} + +dns_nanelo_rm() { + fulldomain=$1 + txtvalue=$2 + + NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" + if [ -z "$NANELO_TOKEN" ]; then + NANELO_TOKEN="" + _err "You didn't configure a Nanelo API Key yet." + _err "Please set NANELO_TOKEN and try again." + _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" + return 1 + fi + _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" + + _info "Deleting resource record $fulldomain" + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" + if _contains "${response}" 'success'; then + return 0 + fi + _err "Could not delete resource record, please check the logs" + _err "${response}" + return 1 +} diff --git a/dnsapi/dns_openstack.sh b/dnsapi/dns_openstack.sh index 38619e6f..fcc1dc2e 100755 --- a/dnsapi/dns_openstack.sh +++ b/dnsapi/dns_openstack.sh @@ -57,16 +57,16 @@ _dns_openstack_create_recordset() { if [ -z "$_recordset_id" ]; then _info "Creating a new recordset" - if ! _recordset_id=$(openstack recordset create -c id -f value --type TXT --record "$txtvalue" "$_zone_id" "$fulldomain."); then + if ! _recordset_id=$(openstack recordset create -c id -f value --type TXT --record="$txtvalue" "$_zone_id" "$fulldomain."); then _err "No recordset ID found after create" return 1 fi else _info "Updating existing recordset" - # Build new list of --record args for update - _record_args="--record $txtvalue" + # Build new list of --record= args for update + _record_args="--record=$txtvalue" for _rec in $_records; do - _record_args="$_record_args --record $_rec" + _record_args="$_record_args --record=$_rec" done # shellcheck disable=SC2086 if ! _recordset_id=$(openstack recordset set -c id -f value $_record_args "$_zone_id" "$fulldomain."); then @@ -107,13 +107,13 @@ _dns_openstack_delete_recordset() { fi else _info "Found existing records, updating recordset" - # Build new list of --record args for update + # Build new list of --record= args for update _record_args="" for _rec in $_records; do if [ "$_rec" = "$txtvalue" ]; then continue fi - _record_args="$_record_args --record $_rec" + _record_args="$_record_args --record=$_rec" done # shellcheck disable=SC2086 if ! openstack recordset set -c id -f value $_record_args "$_zone_id" "$fulldomain." >/dev/null; then diff --git a/dnsapi/dns_opnsense.sh b/dnsapi/dns_opnsense.sh index c2806a1b..d40cbe28 100755 --- a/dnsapi/dns_opnsense.sh +++ b/dnsapi/dns_opnsense.sh @@ -137,7 +137,7 @@ _get_root() { domain=$1 i=2 p=1 - if _opns_rest "GET" "/domain/searchMasterDomain"; then + if _opns_rest "GET" "/domain/searchPrimaryDomain"; then _domain_response="$response" else return 1 @@ -150,7 +150,7 @@ _get_root() { return 1 fi _debug h "$h" - id=$(echo "$_domain_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"master\",\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2) + id=$(echo "$_domain_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",\"domainname\":\"${h}\"" | cut -d ':' -f 2 | 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 5e35011b..e1a958f6 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -14,6 +14,9 @@ #'ovh-eu' OVH_EU='https://eu.api.ovh.com/1.0' +#'ovh-us' +OVH_US='https://api.us.ovhcloud.com/1.0' + #'ovh-ca': OVH_CA='https://ca.api.ovh.com/1.0' @@ -29,9 +32,6 @@ SYS_EU='https://eu.api.soyoustart.com/1.0' #'soyoustart-ca' SYS_CA='https://ca.api.soyoustart.com/1.0' -#'runabove-ca' -RAV_CA='https://api.runabove.com/1.0' - wiki="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-OVH-domain-api" ovh_success="https://github.com/acmesh-official/acme.sh/wiki/OVH-Success" @@ -45,6 +45,10 @@ _ovh_get_api() { printf "%s" $OVH_EU return ;; + ovh-us | ovhus) + printf "%s" $OVH_US + return + ;; ovh-ca | ovhca) printf "%s" $OVH_CA return @@ -65,14 +69,15 @@ _ovh_get_api() { printf "%s" $SYS_CA return ;; - runabove-ca | runaboveca) - printf "%s" $RAV_CA + # raw API url starts with https:// + https*) + printf "%s" "$1" return ;; *) - _err "Unknown parameter : $1" + _err "Unknown endpoint : $1" return 1 ;; esac diff --git a/dnsapi/dns_pleskxml.sh b/dnsapi/dns_pleskxml.sh index f5986827..799c374c 100644 --- a/dnsapi/dns_pleskxml.sh +++ b/dnsapi/dns_pleskxml.sh @@ -41,7 +41,7 @@ pleskxml_init_checks_done=0 NEWLINE='\ ' -pleskxml_tplt_get_domains="" +pleskxml_tplt_get_domains="" # Get a list of domains that PLESK can manage, so we can check root domain + host for acme.sh # Also used to test credentials and URI. # No params. @@ -145,22 +145,25 @@ dns_pleskxml_rm() { )" if [ -z "$reclist" ]; then - _err "No TXT records found for root domain ${root_domain_name} (Plesk domain ID ${root_domain_id}). Exiting." + _err "No TXT records found for root domain $fulldomain (Plesk domain ID ${root_domain_id}). Exiting." return 1 fi - _debug "Got list of DNS TXT records for root domain '$root_domain_name':" + _debug "Got list of DNS TXT records for root Plesk domain ID ${root_domain_id} of root domain $fulldomain:" _debug "$reclist" + # Extracting the id of the TXT record for the full domain (NOT case-sensitive) and corresponding value recid="$( _value "$reclist" | - grep "${fulldomain}." | + grep -i "${fulldomain}." | grep "${txtvalue}" | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/' )" + _debug "Got id from line: $recid" + if ! _value "$recid" | grep '^[0-9]\{1,\}$' >/dev/null; then - _err "DNS records for root domain '${root_domain_name}' (Plesk ID ${root_domain_id}) + host '${sub_domain_name}' do not contain the TXT record '${txtvalue}'" + _err "DNS records for root domain '${fulldomain}.' (Plesk ID ${root_domain_id}) + host '${sub_domain_name}' do not contain the TXT record '${txtvalue}'" _err "Cannot delete TXT record. Exiting." return 1 fi @@ -251,9 +254,12 @@ _call_api() { # Detect any that isn't "ok". None of the used calls should fail if the API is working correctly. # Also detect if there simply aren't any status lines (null result?) and report that, as well. + # Remove structure from result string, since it might contain values that are related to the status of the domain and not to the API request - statuslines_count_total="$(echo "$pleskxml_prettyprint_result" | grep -c '^ *[^<]* *$')" - statuslines_count_okay="$(echo "$pleskxml_prettyprint_result" | grep -c '^ *ok *$')" + statuslines_count_total="$(echo "$pleskxml_prettyprint_result" | sed '//,/<\/data>/d' | grep -c '^ *[^<]* *$')" + statuslines_count_okay="$(echo "$pleskxml_prettyprint_result" | sed '//,/<\/data>/d' | grep -c '^ *ok *$')" + _debug "statuslines_count_total=$statuslines_count_total." + _debug "statuslines_count_okay=$statuslines_count_okay." if [ -z "$statuslines_count_total" ]; then @@ -375,7 +381,7 @@ _pleskxml_get_root_domain() { # Output will be one line per known domain, containing 2 tages and a single tag # We don't actually need to check for type, name, *and* id, but it guarantees only usable lines are returned. - output="$(_api_response_split "$pleskxml_prettyprint_result" 'domain' 'domain' | sed 's///g;s/<\/ascii-name>/<\/name>/g' | grep '' | grep '')" + output="$(_api_response_split "$pleskxml_prettyprint_result" 'result' 'ok' | sed 's///g;s/<\/ascii-name>/<\/name>/g' | grep '' | grep '')" _debug 'Domains managed by Plesk server are (ignore the hacked output):' _debug "$output" diff --git a/dnsapi/dns_vultr.sh b/dnsapi/dns_vultr.sh index 58f14be1..54e5b6ce 100644 --- a/dnsapi/dns_vultr.sh +++ b/dnsapi/dns_vultr.sh @@ -78,7 +78,7 @@ dns_vultr_rm() { return 1 fi - _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep -- "$txtvalue" | tr ',' '\n' | grep -i 'id' | cut -d : -f 2)" + _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep -- "$txtvalue" | tr ',' '\n' | grep -i 'id' | cut -d : -f 2 | tr -d '"')" _debug _record_id "$_record_id" if [ "$_record_id" ]; then _info "Successfully retrieved the record id for ACME challenge." @@ -116,7 +116,7 @@ _get_root() { return 1 fi - if printf "%s\n" "$response" | grep '^\{.*\}' >/dev/null; then + if printf "%s\n" "$response" | grep -E '^\{.*\}' >/dev/null; then if _contains "$response" "\"domain\":\"$_domain\""; then _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")" return 0 diff --git a/notify/aws_ses.sh b/notify/aws_ses.sh new file mode 100644 index 00000000..30db45ad --- /dev/null +++ b/notify/aws_ses.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env sh + +# +#AWS_ACCESS_KEY_ID="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#AWS_SECRET_ACCESS_KEY="xxxxxxx" +# +#AWS_SES_REGION="us-east-1" +# +#AWS_SES_TO="xxxx@xxx.com" +# +#AWS_SES_FROM="xxxx@cccc.com" +# +#AWS_SES_FROM_NAME="Something something" +#This is the Amazon SES api wrapper for acme.sh +AWS_WIKI="https://docs.aws.amazon.com/ses/latest/dg/send-email-api.html" + +aws_ses_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_statusCode" "$_statusCode" + + AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}" + AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}" + AWS_SES_REGION="${AWS_SES_REGION:-$(_readaccountconf_mutable AWS_SES_REGION)}" + + if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then + _use_container_role || _use_instance_role + fi + + if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then + AWS_ACCESS_KEY_ID="" + AWS_SECRET_ACCESS_KEY="" + _err "You haven't specified the aws SES api key id and and api key secret yet." + _err "Please create your key and try again. see $(__green $AWS_WIKI)" + return 1 + fi + + if [ -z "$AWS_SES_REGION" ]; then + AWS_SES_REGION="" + _err "You haven't specified the aws SES api region yet." + _err "Please specify your region and try again. see https://docs.aws.amazon.com/general/latest/gr/ses.html" + return 1 + fi + _saveaccountconf_mutable AWS_SES_REGION "$AWS_SES_REGION" + + #save for future use, unless using a role which will be fetched as needed + if [ -z "$_using_role" ]; then + _saveaccountconf_mutable AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID" + _saveaccountconf_mutable AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY" + fi + + AWS_SES_TO="${AWS_SES_TO:-$(_readaccountconf_mutable AWS_SES_TO)}" + if [ -z "$AWS_SES_TO" ]; then + AWS_SES_TO="" + _err "You didn't specify an email to AWS_SES_TO receive messages." + return 1 + fi + _saveaccountconf_mutable AWS_SES_TO "$AWS_SES_TO" + + AWS_SES_FROM="${AWS_SES_FROM:-$(_readaccountconf_mutable AWS_SES_FROM)}" + if [ -z "$AWS_SES_FROM" ]; then + AWS_SES_FROM="" + _err "You didn't specify an email to AWS_SES_FROM receive messages." + return 1 + fi + _saveaccountconf_mutable AWS_SES_FROM "$AWS_SES_FROM" + + AWS_SES_FROM_NAME="${AWS_SES_FROM_NAME:-$(_readaccountconf_mutable AWS_SES_FROM_NAME)}" + _saveaccountconf_mutable AWS_SES_FROM_NAME "$AWS_SES_FROM_NAME" + + AWS_SES_SENDFROM="$AWS_SES_FROM_NAME <$AWS_SES_FROM>" + + AWS_SES_ACTION="Action=SendEmail" + AWS_SES_SOURCE="Source=$AWS_SES_SENDFROM" + AWS_SES_TO="Destination.ToAddresses.member.1=$AWS_SES_TO" + AWS_SES_SUBJECT="Message.Subject.Data=$_subject" + AWS_SES_MESSAGE="Message.Body.Text.Data=$_content" + + _data="${AWS_SES_ACTION}&${AWS_SES_SOURCE}&${AWS_SES_TO}&${AWS_SES_SUBJECT}&${AWS_SES_MESSAGE}" + + response="$(aws_rest POST "" "" "$_data")" +} + +_use_metadata() { + _aws_creds="$( + _get "$1" "" 1 | + _normalizeJson | + tr '{,}' '\n' | + while read -r _line; do + _key="$(echo "${_line%%:*}" | tr -d '"')" + _value="${_line#*:}" + _debug3 "_key" "$_key" + _secure_debug3 "_value" "$_value" + case "$_key" in + AccessKeyId) echo "AWS_ACCESS_KEY_ID=$_value" ;; + SecretAccessKey) echo "AWS_SECRET_ACCESS_KEY=$_value" ;; + Token) echo "AWS_SESSION_TOKEN=$_value" ;; + esac + done | + paste -sd' ' - + )" + _secure_debug "_aws_creds" "$_aws_creds" + + if [ -z "$_aws_creds" ]; then + return 1 + fi + + eval "$_aws_creds" + _using_role=true +} + +#method uri qstr data +aws_rest() { + mtd="$1" + ep="$2" + qsr="$3" + data="$4" + + _debug mtd "$mtd" + _debug ep "$ep" + _debug qsr "$qsr" + _debug data "$data" + + CanonicalURI="/$ep" + _debug2 CanonicalURI "$CanonicalURI" + + CanonicalQueryString="$qsr" + _debug2 CanonicalQueryString "$CanonicalQueryString" + + RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")" + _debug2 RequestDate "$RequestDate" + + #RequestDate="20161120T141056Z" ############## + + export _H1="x-amz-date: $RequestDate" + + aws_host="email.$AWS_SES_REGION.amazonaws.com" + CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n" + SignedHeaders="host;x-amz-date" + if [ -n "$AWS_SESSION_TOKEN" ]; then + export _H3="x-amz-security-token: $AWS_SESSION_TOKEN" + CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n" + SignedHeaders="${SignedHeaders};x-amz-security-token" + fi + _debug2 CanonicalHeaders "$CanonicalHeaders" + _debug2 SignedHeaders "$SignedHeaders" + + RequestPayload="$data" + _debug2 RequestPayload "$RequestPayload" + + Hash="sha256" + + CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$(printf "%s" "$RequestPayload" | _digest "$Hash" hex)" + _debug2 CanonicalRequest "$CanonicalRequest" + + HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)" + _debug2 HashedCanonicalRequest "$HashedCanonicalRequest" + + Algorithm="AWS4-HMAC-SHA256" + _debug2 Algorithm "$Algorithm" + + RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)" + _debug2 RequestDateOnly "$RequestDateOnly" + + Region="$AWS_SES_REGION" + Service="ses" + + CredentialScope="$RequestDateOnly/$Region/$Service/aws4_request" + _debug2 CredentialScope "$CredentialScope" + + StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest" + + _debug2 StringToSign "$StringToSign" + + kSecret="AWS4$AWS_SECRET_ACCESS_KEY" + + #kSecret="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" ############################ + + _secure_debug2 kSecret "$kSecret" + + kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")" + _secure_debug2 kSecretH "$kSecretH" + + kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)" + _debug2 kDateH "$kDateH" + + kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)" + _debug2 kRegionH "$kRegionH" + + kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)" + _debug2 kServiceH "$kServiceH" + + kSigningH="$(printf "%s" "aws4_request" | _hmac "$Hash" "$kServiceH" hex)" + _debug2 kSigningH "$kSigningH" + + signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)" + _debug2 signature "$signature" + + Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" + _debug2 Authorization "$Authorization" + + _H2="Authorization: $Authorization" + _debug _H2 "$_H2" + + url="https://$aws_host/$ep" + if [ "$qsr" ]; then + url="https://$aws_host/$ep?$qsr" + fi + + if [ "$mtd" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url")" + fi + + _ret="$?" + _debug2 response "$response" + if [ "$_ret" = "0" ]; then + if _contains "$response" ""]' >/dev/null + echo "$_email" | grep -q -E '^.*[<>"]' } ## @@ -249,7 +249,7 @@ _mime_encoded_word() { _text="$1" # (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that) _ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-" - if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then + if echo "$_text" | grep -q -E "^.*[^$_ascii]"; then # At least one non-ASCII char; convert entire thing to encoded word printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?=" else