diff --git a/README.md b/README.md index d535631..b5022c9 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - An ACME protocol client written purely in Shell (Unix shell) language. - Full ACME protocol implementation. +- Support ACME v1 and ACME v2 +- Support ACME v2 wildcard certs - Simple, powerful and very easy to use. You only need 3 minutes to learn it. - Bash, dash and sh compatible. - Simplest shell script for Let's Encrypt free certificate client. @@ -127,7 +129,7 @@ Ok, you are ready to issue certs now. Show help message: -``` +```sh root@v1:~# acme.sh -h ``` @@ -164,16 +166,16 @@ You must have at least one domain there. You must point and bind all the domains to the same webroot dir: `/home/wwwroot/example.com`. -Generated/issued certs will be placed in `~/.acme.sh/example.com/` +The certs will be placed in `~/.acme.sh/example.com/` -The issued cert will be renewed automatically every **60** days. +The certs will be renewed automatically every **60** days. More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert -# 3. Install the issued cert to Apache/Nginx etc. +# 3. Install the cert to Apache/Nginx etc. -After you issue a cert, you probably want to install/copy the cert to your Apache/Nginx or other servers. +After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers. You **MUST** use this command to copy the certs to the target files, **DO NOT** use the certs files in **~/.acme.sh/** folder, they are for internal use only, the folder structure may change in the future. **Apache** example: @@ -195,9 +197,9 @@ acme.sh --install-cert -d example.com \ Only the domain is required, all the other parameters are optional. -The ownership and permission info of existing files are preserved. You may want to precreate the files to have defined ownership and permission. +The ownership and permission info of existing files are preserved. You can pre-create the files to define the ownership and permission. -Install/copy the issued cert/key to the production Apache or Nginx path. +Install/copy the cert/key to the production Apache or Nginx path. The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`. @@ -240,7 +242,7 @@ Particularly, if you are running an Apache server, you should use Apache mode in Just set string "apache" as the second argument and it will force use of apache plugin automatically. -``` +```sh acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com ``` @@ -260,47 +262,13 @@ It will configure nginx server automatically to verify the domain and then resto So, the config is not changed. -``` +```sh acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com ``` More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert -# 8. Use DNS mode: - -Support the `dns-01` challenge. - -```bash -acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com -``` - -You should get an output like below: - -``` -Add the following txt record: -Domain:_acme-challenge.example.com -Txt value:9ihDbjYfTExAYeDs4DBUeuTo18KBzwvTEjUnSwd32-c - -Add the following txt record: -Domain:_acme-challenge.www.example.com -Txt value:9ihDbjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - -Please add those txt records to the domains. Waiting for the dns to take effect. -``` - -Then just rerun with `renew` argument: - -```bash -acme.sh --renew -d example.com -``` - -Ok, it's finished. - -**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.** - -**Please use dns api mode instead.** - -# 9. Automatic DNS API integration +# 8. Automatic DNS API integration If your DNS provider supports API access, we can use that API to automatically issue the certs. @@ -343,8 +311,10 @@ You don't have to do anything manually! 1. INWX (https://www.inwx.de/) 1. Servercow (https://servercow.de) 1. Namesilo (https://www.namesilo.com) +1. InternetX autoDNS API (https://internetx.com) +1. Azure DNS +1. selectel.com(selectel.ru) DNS API 1. DNSEver(https://www.dnsever.com) - And: 1. lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api @@ -358,6 +328,39 @@ If your DNS provider is not on the supported list above, you can write your own For more details: [How to use DNS API](dnsapi) +# 9. Use DNS manual mode: + +If your dns provider doesn't support any api access, you will have to add the txt record by your hand. + +```bash +acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com +``` + +You should get an output like below: + +```sh +Add the following txt record: +Domain:_acme-challenge.example.com +Txt value:9ihDbjYfTExAYeDs4DBUeuTo18KBzwvTEjUnSwd32-c + +Add the following txt record: +Domain:_acme-challenge.www.example.com +Txt value:9ihDbjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +Please add those txt records to the domains. Waiting for the dns to take effect. +``` + +Then just rerun with `renew` argument: + +```bash +acme.sh --renew -d example.com +``` + +Ok, it's done. + +**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.** + +**Please use dns api mode instead.** # 10. Issue ECC certificates @@ -390,36 +393,60 @@ Valid values are: 3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)** -# 11. How to renew the issued certs + +# 11. Issue Wildcard certificates + +It's simple, just give a wildcard domain as the `-d` parameter. + +```sh +acme.sh --issue -d example.com -d *.example.com --dns dns_cf +``` + + + +# 12. How to renew the certs No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days. -However, you can also force to renew any cert: +However, you can also force to renew a cert: -``` +```sh acme.sh --renew -d example.com --force ``` or, for ECC cert: -``` +```sh acme.sh --renew -d example.com --force --ecc ``` -# 12. How to upgrade `acme.sh` +# 13. How to stop cert renewal + +To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: + +```sh +acme.sh --remove -d example.com [--ecc] +``` + +The cert/key file is not removed from the disk. + +You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself. + + +# 14. How to upgrade `acme.sh` acme.sh is in constant development, so it's strongly recommended to use the latest code. You can update acme.sh to the latest code: -``` +```sh acme.sh --upgrade ``` You can also enable auto upgrade: -``` +```sh acme.sh --upgrade --auto-upgrade ``` @@ -427,31 +454,30 @@ Then **acme.sh** will be kept up to date automatically. Disable auto upgrade: -``` +```sh acme.sh --upgrade --auto-upgrade 0 ``` -# 13. Issue a cert from an existing CSR +# 15. Issue a cert from an existing CSR https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR -# 14. Under the Hood +# 16. Under the Hood Speak ACME language using shell, directly to "Let's Encrypt". TODO: -# 15. Acknowledgments +# 17. Acknowledgments 1. Acme-tiny: https://github.com/diafygi/acme-tiny 2. ACME protocol: https://github.com/ietf-wg-acme/acme -3. Certbot: https://github.com/certbot/certbot -# 16. License & Others +# 18. License & Others License is GPLv3 @@ -460,7 +486,7 @@ Please Star and Fork me. [Issues](https://github.com/Neilpang/acme.sh/issues) and [pull requests](https://github.com/Neilpang/acme.sh/pulls) are welcome. -# 17. Donate +# 19. Donate Your donation makes **acme.sh** better: 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) diff --git a/acme.sh b/acme.sh index 472975a..a21b24e 100755 --- a/acme.sh +++ b/acme.sh @@ -13,8 +13,14 @@ _SCRIPT_="$0" _SUB_FOLDERS="dnsapi deploy" -_OLD_CA_HOST="https://acme-v01.api.letsencrypt.org" -DEFAULT_CA="https://acme-v01.api.letsencrypt.org/directory" +LETSENCRYPT_CA_V1="https://acme-v01.api.letsencrypt.org/directory" +LETSENCRYPT_STAGING_CA_V1="https://acme-staging.api.letsencrypt.org/directory" + +LETSENCRYPT_CA_V2="https://acme-v02.api.letsencrypt.org/directory" +LETSENCRYPT_STAGING_CA_V2="https://acme-staging-v02.api.letsencrypt.org/directory" + +DEFAULT_CA=$LETSENCRYPT_CA_V1 +DEFAULT_STAGING_CA=$LETSENCRYPT_STAGING_CA_V1 DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" DEFAULT_ACCOUNT_EMAIL="" @@ -24,13 +30,13 @@ DEFAULT_DOMAIN_KEY_LENGTH=2048 DEFAULT_OPENSSL_BIN="openssl" -STAGE_CA="https://acme-staging.api.letsencrypt.org/directory" +_OLD_CA_HOST="https://acme-v01.api.letsencrypt.org" _OLD_STAGE_CA_HOST="https://acme-staging.api.letsencrypt.org" VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" VTYPE_TLS="tls-sni-01" -#VTYPE_TLS2="tls-sni-02" +VTYPE_TLS2="tls-sni-02" LOCAL_ANY_ADDRESS="0.0.0.0" @@ -991,7 +997,7 @@ _createkey() { _is_idn() { _is_idn_d="$1" _debug2 _is_idn_d "$_is_idn_d" - _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '.,-') + _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-') _debug2 _idn_temp "$_idn_temp" [ "$_idn_temp" ] } @@ -1044,13 +1050,14 @@ _createcsr() { if [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then #single domain _info "Single domain" "$domain" + printf -- "\nsubjectAltName=DNS:$domain" >>"$csrconf" else domainlist="$(_idn "$domainlist")" _debug2 domainlist "$domainlist" if _contains "$domainlist" ","; then - alt="DNS:$(echo "$domainlist" | sed "s/,/,DNS:/g")" + alt="DNS:$domain,DNS:$(echo "$domainlist" | sed "s/,,/,/g" | sed "s/,/,DNS:/g")" else - alt="DNS:$domainlist" + alt="DNS:$domain,DNS:$domainlist" fi #multi _info "Multi domain" "$alt" @@ -1421,7 +1428,7 @@ _calcjwk() { JWK_HEADER='{"alg": "RS256", "jwk": '$jwk'}' JWK_HEADERPLACE_PART1='{"nonce": "' - JWK_HEADERPLACE_PART2='", "alg": "RS256", "jwk": '$jwk'}' + JWK_HEADERPLACE_PART2='", "alg": "RS256"' elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then _debug "EC key" crv="$(${ACME_OPENSSL_BIN:-openssl} ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" | cut -d ":" -f 2 | tr -d " \r\n")" @@ -1490,7 +1497,7 @@ _calcjwk() { JWK_HEADER='{"alg": "ES'$__ECC_KEY_LEN'", "jwk": '$jwk'}' JWK_HEADERPLACE_PART1='{"nonce": "' - JWK_HEADERPLACE_PART2='", "alg": "ES'$__ECC_KEY_LEN'", "jwk": '$jwk'}' + JWK_HEADERPLACE_PART2='", "alg": "ES'$__ECC_KEY_LEN'"' else _err "Only RSA or EC key is supported." return 1 @@ -1580,7 +1587,7 @@ _inithttp() { # body url [needbase64] [POST|PUT] _post() { body="$1" - url="$2" + _post_url="$2" needbase64="$3" httpmethod="$4" @@ -1588,7 +1595,7 @@ _post() { httpmethod="POST" fi _debug $httpmethod - _debug "url" "$url" + _debug "_post_url" "$_post_url" _debug2 "body" "$body" _inithttp @@ -1600,9 +1607,9 @@ _post() { fi _debug "_CURL" "$_CURL" if [ "$needbase64" ]; then - response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$url" | _base64)" + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" else - response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$url")" + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" fi _ret="$?" if [ "$_ret" != "0" ]; then @@ -1620,15 +1627,15 @@ _post() { _debug "_WGET" "$_WGET" if [ "$needbase64" ]; then if [ "$httpmethod" = "POST" ]; then - response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$url" 2>"$HTTP_HEADER" | _base64)" + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER" | _base64)" else - response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$url" 2>"$HTTP_HEADER" | _base64)" + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$_post_url" 2>"$HTTP_HEADER" | _base64)" fi else if [ "$httpmethod" = "POST" ]; then - response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$url" 2>"$HTTP_HEADER")" + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" else - response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$url" 2>"$HTTP_HEADER")" + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$_post_url" 2>"$HTTP_HEADER")" fi fi _ret="$?" @@ -1656,7 +1663,7 @@ _get() { onlyheader="$2" t="$3" _debug url "$url" - _debug "timeout" "$t" + _debug "timeout=$t" _inithttp @@ -1776,7 +1783,15 @@ _send_signed_request() { nonce="$_CACHED_NONCE" _debug2 nonce "$nonce" - protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2" + if [ "$ACME_VERSION" = "2" ]; then + if [ "$url" = "$ACME_NEW_ACCOUNT" ] || [ "$url" = "$ACME_REVOKE_CERT" ]; then + protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' + else + protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"kid\": \"${ACCOUNT_URL}\""'}' + fi + else + protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' + fi _debug3 protected "$protected" protected64="$(printf "%s" "$protected" | _base64 | _url_replace)" @@ -1791,7 +1806,11 @@ _send_signed_request() { sig="$(printf "%s" "$_sig_t" | _url_replace)" _debug3 sig "$sig" - body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + if [ "$ACME_VERSION" = "2" ]; then + body="{\"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + else + body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + fi _debug3 body "$body" response="$(_post "$body" "$url" "$needbase64")" @@ -2170,9 +2189,15 @@ _initAPI() { _debug2 "response" "$response" ACME_KEY_CHANGE=$(echo "$response" | _egrep_o 'key-change" *: *"[^"]*"' | cut -d '"' -f 3) + if [ -z "$ACME_KEY_CHANGE" ]; then + ACME_KEY_CHANGE=$(echo "$response" | _egrep_o 'keyChange" *: *"[^"]*"' | cut -d '"' -f 3) + fi export ACME_KEY_CHANGE ACME_NEW_AUTHZ=$(echo "$response" | _egrep_o 'new-authz" *: *"[^"]*"' | cut -d '"' -f 3) + if [ -z "$ACME_NEW_AUTHZ" ]; then + ACME_NEW_AUTHZ=$(echo "$response" | _egrep_o 'newAuthz" *: *"[^"]*"' | cut -d '"' -f 3) + fi export ACME_NEW_AUTHZ ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'new-cert" *: *"[^"]*"' | cut -d '"' -f 3) @@ -2180,6 +2205,9 @@ _initAPI() { if [ -z "$ACME_NEW_ORDER" ]; then ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'new-order" *: *"[^"]*"' | cut -d '"' -f 3) ACME_NEW_ORDER_RES="new-order" + if [ -z "$ACME_NEW_ORDER" ]; then + ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'newOrder" *: *"[^"]*"' | cut -d '"' -f 3) + fi fi export ACME_NEW_ORDER export ACME_NEW_ORDER_RES @@ -2189,17 +2217,32 @@ _initAPI() { if [ -z "$ACME_NEW_ACCOUNT" ]; then ACME_NEW_ACCOUNT=$(echo "$response" | _egrep_o 'new-account" *: *"[^"]*"' | cut -d '"' -f 3) ACME_NEW_ACCOUNT_RES="new-account" + if [ -z "$ACME_NEW_ACCOUNT" ]; then + ACME_NEW_ACCOUNT=$(echo "$response" | _egrep_o 'newAccount" *: *"[^"]*"' | cut -d '"' -f 3) + if [ "$ACME_NEW_ACCOUNT" ]; then + export ACME_VERSION=2 + fi + fi fi export ACME_NEW_ACCOUNT export ACME_NEW_ACCOUNT_RES ACME_REVOKE_CERT=$(echo "$response" | _egrep_o 'revoke-cert" *: *"[^"]*"' | cut -d '"' -f 3) + if [ -z "$ACME_REVOKE_CERT" ]; then + ACME_REVOKE_CERT=$(echo "$response" | _egrep_o 'revokeCert" *: *"[^"]*"' | cut -d '"' -f 3) + fi export ACME_REVOKE_CERT ACME_NEW_NONCE=$(echo "$response" | _egrep_o 'new-nonce" *: *"[^"]*"' | cut -d '"' -f 3) + if [ -z "$ACME_NEW_NONCE" ]; then + ACME_NEW_NONCE=$(echo "$response" | _egrep_o 'newNonce" *: *"[^"]*"' | cut -d '"' -f 3) + fi export ACME_NEW_NONCE ACME_AGREEMENT=$(echo "$response" | _egrep_o 'terms-of-service" *: *"[^"]*"' | cut -d '"' -f 3) + if [ -z "$ACME_AGREEMENT" ]; then + ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3) + fi export ACME_AGREEMENT _debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE" @@ -2208,12 +2251,16 @@ _initAPI() { _debug "ACME_NEW_ACCOUNT" "$ACME_NEW_ACCOUNT" _debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT" _debug "ACME_AGREEMENT" "$ACME_AGREEMENT" + _debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE" + _debug "ACME_VERSION" "$ACME_VERSION" fi } #[domain] [keylength or isEcc flag] _initpath() { + domain="$1" + _ilength="$2" __initHome @@ -2232,11 +2279,16 @@ _initpath() { CA_HOME="$DEFAULT_CA_HOME" fi + if [ "$ACME_VERSION" = "2" ]; then + DEFAULT_CA="$LETSENCRYPT_CA_V2" + DEFAULT_STAGING_CA="$LETSENCRYPT_STAGING_CA_V2" + fi + if [ -z "$ACME_DIRECTORY" ]; then if [ -z "$STAGE" ]; then ACME_DIRECTORY="$DEFAULT_CA" else - ACME_DIRECTORY="$STAGE_CA" + ACME_DIRECTORY="$DEFAULT_STAGING_CA" _info "Using stage ACME_DIRECTORY: $ACME_DIRECTORY" fi fi @@ -2296,13 +2348,10 @@ _initpath() { ACME_OPENSSL_BIN="$DEFAULT_OPENSSL_BIN" fi - if [ -z "$1" ]; then + if [ -z "$domain" ]; then return 0 fi - domain="$1" - _ilength="$2" - if [ -z "$DOMAIN_PATH" ]; then domainhome="$CERT_HOME/$domain" domainhomeecc="$CERT_HOME/$domain$ECC_SUFFIX" @@ -2548,10 +2597,7 @@ _setNginx() { _d="$1" _croot="$2" _thumbpt="$3" - if ! _exists "nginx"; then - _err "nginx command is not found." - return 1 - fi + FOUND_REAL_NGINX_CONF="" FOUND_REAL_NGINX_CONF_LN="" BACKUP_NGINX_CONF="" @@ -2561,6 +2607,10 @@ _setNginx() { if [ -z "$_start_f" ]; then _debug "find start conf from nginx command" if [ -z "$NGINX_CONF" ]; then + if ! _exists "nginx"; then + _err "nginx command is not found." + return 1 + fi NGINX_CONF="$(nginx -V 2>&1 | _egrep_o "--conf-path=[^ ]* " | tr -d " ")" _debug NGINX_CONF "$NGINX_CONF" NGINX_CONF="$(echo "$NGINX_CONF" | cut -d = -f 2)" @@ -2605,6 +2655,10 @@ _setNginx() { return 1 fi + if ! _exists "nginx"; then + _err "nginx command is not found." + return 1 + fi _info "Check the nginx conf before setting up." if ! _exec "nginx -t" >/dev/null; then _exec_err @@ -2697,7 +2751,7 @@ _isRealNginxConf() { for _fln in $(tr "\t" ' ' <"$2" | grep -n "^ *server_name.* $1" | cut -d : -f 1); do _debug _fln "$_fln" if [ "$_fln" ]; then - _start=$(tr "\t" ' ' <"$2" | _head_n "$_fln" | grep -n "^ *server *{" | _tail_n 1) + _start=$(tr "\t" ' ' <"$2" | _head_n "$_fln" | grep -n "^ *server *" | grep -v server_name | _tail_n 1) _debug "_start" "$_start" _start_n=$(echo "$_start" | cut -d : -f 1) _start_nn=$(_math $_start_n + 1) @@ -2706,8 +2760,8 @@ _isRealNginxConf() { _left="$(sed -n "${_start_nn},99999p" "$2")" _debug2 _left "$_left" - if echo "$_left" | tr "\t" ' ' | grep -n "^ *server *{" >/dev/null; then - _end=$(echo "$_left" | tr "\t" ' ' | grep -n "^ *server *{" | _head_n 1) + if echo "$_left" | tr "\t" ' ' | grep -n "^ *server *" >/dev/null; then + _end=$(echo "$_left" | tr "\t" ' ' | grep -n "^ *server *" | _head_n 1) _debug "_end" "$_end" _end_n=$(echo "$_end" | cut -d : -f 1) _debug "_end_n" "$_end_n" @@ -2718,8 +2772,20 @@ _isRealNginxConf() { _debug "_seg_n" "$_seg_n" - if [ "$(echo "$_seg_n" | _egrep_o "^ *ssl *on *;")" ] \ - || [ "$(echo "$_seg_n" | _egrep_o "listen .* ssl[ |;]")" ]; then + _skip_ssl=1 + for _listen_i in $(echo "$_seg_n" | tr "\t" ' ' | grep "^ *listen" | tr -d " "); do + if [ "$_listen_i" ]; then + if [ "$(echo "$_listen_i" | _egrep_o "listen.*ssl[ |;]")" ]; then + _debug2 "$_listen_i is ssl" + else + _debug2 "$_listen_i is plain text" + _skip_ssl="" + break + fi + fi + done + + if [ "$_skip_ssl" = "1" ]; then _debug "ssl on, skip" else FOUND_REAL_NGINX_CONF_LN=$_fln @@ -2818,7 +2884,11 @@ _clearupdns() { return 1 fi - txtdomain="_acme-challenge.$d" + _dns_root_d="$d" + if _startswith "$_dns_root_d" "*."; then + _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')" + fi + txtdomain="_acme-challenge.$_dns_root_d" if ! $rmcommand "$txtdomain" "$txt"; then _err "Error removing txt for domain:$txtdomain" @@ -2951,6 +3021,7 @@ _on_issue_err() { _chk_post_hook="$1" _chk_vlist="$2" _debug _on_issue_err + if [ "$LOG_FILE" ]; then _err "Please check log file for more details: $LOG_FILE" else @@ -3052,6 +3123,8 @@ _regAccount() { _initpath _reg_length="$1" _debug3 _regAccount "$_regAccount" + _initAPI + mkdir -p "$CA_DIR" if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH" @@ -3073,11 +3146,18 @@ _regAccount() { if ! _calcjwk "$ACCOUNT_KEY_PATH"; then return 1 fi - _initAPI - _reg_res="$ACME_NEW_ACCOUNT_RES" - regjson='{"resource": "'$_reg_res'", "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' - if [ "$ACCOUNT_EMAIL" ]; then - regjson='{"resource": "'$_reg_res'", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' + + if [ "$ACME_VERSION" = "2" ]; then + regjson='{"termsOfServiceAgreed": true}' + if [ "$ACCOUNT_EMAIL" ]; then + regjson='{"contact": ["mailto: '$ACCOUNT_EMAIL'"], "termsOfServiceAgreed": true}' + fi + else + _reg_res="$ACME_NEW_ACCOUNT_RES" + regjson='{"resource": "'$_reg_res'", "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' + if [ "$ACCOUNT_EMAIL" ]; then + regjson='{"resource": "'$_reg_res'", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' + fi fi _info "Registering account" @@ -3090,7 +3170,7 @@ _regAccount() { if [ "$code" = "" ] || [ "$code" = '201' ]; then echo "$response" >"$ACCOUNT_JSON_PATH" _info "Registered" - elif [ "$code" = '409' ]; then + elif [ "$code" = '409' ] || [ "$code" = '200' ]; then _info "Already registered" else _err "Register account Error: $response" @@ -3100,8 +3180,8 @@ _regAccount() { _accUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" _debug "_accUri" "$_accUri" _savecaconf "ACCOUNT_URL" "$_accUri" + export ACCOUNT_URL="$ACCOUNT_URL" - echo "$response" >"$ACCOUNT_JSON_PATH" CA_KEY_HASH="$(__calcAccountKeyHash)" _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" _savecaconf CA_KEY_HASH "$CA_KEY_HASH" @@ -3113,7 +3193,6 @@ _regAccount() { ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)" _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" - } #Implement deactivate account @@ -3149,7 +3228,12 @@ deactivateaccount() { fi _initAPI - if _send_signed_request "$_accUri" "{\"resource\": \"reg\", \"status\":\"deactivated\"}" && _contains "$response" '"deactivated"'; then + if [ "$ACME_VERSION" = "2" ]; then + _djson="{\"status\":\"deactivated\"}" + else + _djson="{\"resource\": \"reg\", \"status\":\"deactivated\"}" + fi + if _send_signed_request "$_accUri" "$_djson" && _contains "$response" '"deactivated"'; then _info "Deactivate account success for $_accUri." _accid=$(echo "$response" | _egrep_o "\"id\" *: *[^,]*," | cut -d : -f 2 | tr -d ' ,') elif [ "$code" = "403" ]; then @@ -3237,7 +3321,7 @@ __get_domain_new_authz() { _err "new-authz retry reach the max $_Max_new_authz_retry_times times." fi - if [ ! -z "$code" ] && [ ! "$code" = '201' ]; then + if [ "$code" ] && [ "$code" != '201' ]; then _err "new-authz error: $response" return 1 fi @@ -3251,7 +3335,11 @@ __trigger_validation() { _debug2 _t_url "$_t_url" _t_key_authz="$2" _debug2 _t_key_authz "$_t_key_authz" - _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$_t_key_authz\"}" + if [ "$ACME_VERSION" = "2" ]; then + _send_signed_request "$_t_url" "{\"keyAuthorization\": \"$_t_key_authz\"}" + else + _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$_t_key_authz\"}" + fi } #webroot, domain domainlist keylength @@ -3267,6 +3355,7 @@ issue() { _web_roots="$1" _main_domain="$2" _alt_domains="$3" + if _contains "$_main_domain" ","; then _main_domain=$(echo "$2,$3" | cut -d , -f 1) _alt_domains=$(echo "$2,$3" | cut -d , -f 2- | sed "s/,${NO_VALUE}$//") @@ -3393,32 +3482,109 @@ issue() { sep='#' dvsep=',' if [ -z "$vlist" ]; then + if [ "$ACME_VERSION" = "2" ]; then + #make new order request + _identifiers="{\"type\":\"dns\",\"value\":\"$_main_domain\"}" + for d in $(echo "$_alt_domains" | tr ',' ' '); do + if [ "$d" ]; then + _identifiers="$_identifiers,{\"type\":\"dns\",\"value\":\"$d\"}" + fi + done + _debug2 _identifiers "$_identifiers" + if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then + _err "Create new order error." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + Le_OrderFinalize="$(echo "$response" | tr -d '\r\n' | _egrep_o '"finalize" *: *"[^"]*"' | cut -d '"' -f 4)" + _debug Le_OrderFinalize "$Le_OrderFinalize" + if [ -z "$Le_OrderFinalize" ]; then + _err "Create new order error. Le_OrderFinalize not found. $response" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + #for dns manual mode + _savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize" + + _authorizations_seg="$(echo "$response" | tr -d '\r\n' | _egrep_o '"authorizations" *: *\[[^\]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')" + _debug2 _authorizations_seg "$_authorizations_seg" + if [ -z "$_authorizations_seg" ]; then + _err "_authorizations_seg not found." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + #domain and authz map + _authorizations_map="" + for _authz_url in $(echo "$_authorizations_seg" | tr ',' ' '); do + _debug2 "_authz_url" "$_authz_url" + if ! response="$(_get "$_authz_url")"; then + _err "get to authz error." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2 | tr -d ' "')" + if _contains "$response" "\"wildcard\" *: *true"; then + _d="*.$_d" + fi + _debug2 _d "$_d" + _authorizations_map="$_d,$response +$_authorizations_map" + done + _debug2 _authorizations_map "$_authorizations_map" + fi + alldomains=$(echo "$_main_domain,$_alt_domains" | tr ',' ' ') - _index=1 + _index=0 _currentRoot="" for d in $alldomains; do _info "Getting webroot for domain" "$d" + _index=$(_math $_index + 1) _w="$(echo $_web_roots | cut -d , -f $_index)" _debug _w "$_w" if [ "$_w" ]; then _currentRoot="$_w" fi _debug "_currentRoot" "$_currentRoot" - _index=$(_math $_index + 1) vtype="$VTYPE_HTTP" + #todo, v2 wildcard force to use dns if _startswith "$_currentRoot" "dns"; then vtype="$VTYPE_DNS" fi if [ "$_currentRoot" = "$W_TLS" ]; then - vtype="$VTYPE_TLS" + if [ "$ACME_VERSION" = "2" ]; then + vtype="$VTYPE_TLS2" + else + vtype="$VTYPE_TLS" + fi fi - if ! __get_domain_new_authz "$d"; then - _clearup - _on_issue_err "$_post_hook" - return 1 + if [ "$ACME_VERSION" = "2" ]; then + response="$(echo "$_authorizations_map" | grep "^$d," | sed "s/$d,//")" + _debug2 "response" "$response" + if [ -z "$response" ]; then + _err "get to authz error." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + else + if ! __get_domain_new_authz "$d"; then + _clearup + _on_issue_err "$_post_hook" + return 1 + fi fi if [ -z "$thumbprint" ]; then @@ -3428,7 +3594,7 @@ issue() { entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then - _err "Error, can not get domain token $d" + _err "Error, can not get domain token entry $d" _clearup _on_issue_err "$_post_hook" return 1 @@ -3436,14 +3602,30 @@ issue() { token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" _debug token "$token" - uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d '"' -f 4)" + if [ -z "$token" ]; then + _err "Error, can not get domain token $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + if [ "$ACME_VERSION" = "2" ]; then + uri="$(printf "%s\n" "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" + else + uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d '"' -f 4)" + fi _debug uri "$uri" + if [ -z "$uri" ]; then + _err "Error, can not get domain uri. $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi keyauthorization="$token.$thumbprint" _debug keyauthorization "$keyauthorization" if printf "%s" "$response" | grep '"status":"valid"' >/dev/null 2>&1; then - _debug "$d is already verified, skip." + _debug "$d is already verified." keyauthorization="$STATE_VERIFIED" _debug keyauthorization "$keyauthorization" fi @@ -3463,7 +3645,7 @@ issue() { keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - + _debug d "$d" if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then _debug "$d is already verified, skip $vtype." continue @@ -3471,12 +3653,16 @@ issue() { if [ "$vtype" = "$VTYPE_DNS" ]; then dnsadded='0' - txtdomain="_acme-challenge.$d" + _dns_root_d="$d" + if _startswith "$_dns_root_d" "*."; then + _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')" + fi + txtdomain="_acme-challenge.$_dns_root_d" _debug txtdomain "$txtdomain" txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" _debug txt "$txt" - d_api="$(_findHook "$d" dnsapi "$_currentRoot")" + d_api="$(_findHook "$_dns_root_d" dnsapi "$_currentRoot")" _debug d_api "$d_api" @@ -3685,12 +3871,16 @@ issue() { return 1 fi - if [ ! -z "$code" ] && [ ! "$code" = '202' ]; then - _err "$d:Challenge error: $response" - _clearupwebbroot "$_currentRoot" "$removelevel" "$token" - _clearup - _on_issue_err "$_post_hook" "$vlist" - return 1 + if [ "$code" ] && [ "$code" != '202' ]; then + if [ "$ACME_VERSION" = "2" ] && [ "$code" = '200' ]; then + _debug "trigger validation code: $code" + else + _err "$d:Challenge error: $response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi fi waittimes=0 @@ -3773,18 +3963,42 @@ issue() { _info "Verify finished, start to sign." der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _url_replace)" - if ! _send_signed_request "${ACME_NEW_ORDER}" "{\"resource\": \"$ACME_NEW_ORDER_RES\", \"csr\": \"$der\"}" "needbase64"; then - _err "Sign failed." - _on_issue_err "$_post_hook" - return 1 - fi + if [ "$ACME_VERSION" = "2" ]; then + if ! _send_signed_request "${Le_OrderFinalize}" "{\"csr\": \"$der\"}"; then + _err "Sign failed." + _on_issue_err "$_post_hook" + return 1 + fi + if [ "$code" != "200" ]; then + _err "Sign failed, code is not 200." + _on_issue_err "$_post_hook" + return 1 + fi + Le_LinkCert="$(echo "$response" | tr -d '\r\n' | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" - _rcert="$response" - Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" - _debug "Le_LinkCert" "$Le_LinkCert" - _savedomainconf "Le_LinkCert" "$Le_LinkCert" + if ! _get "$Le_LinkCert" >"$CERT_PATH"; then + _err "Sign failed, code is not 200." + _on_issue_err "$_post_hook" + return 1 + fi - if [ "$Le_LinkCert" ]; then + if [ "$(grep -- "$BEGIN_CERT" "$CERT_PATH" | wc -l)" -gt "1" ]; then + _debug "Found cert chain" + cat "$CERT_PATH" >"$CERT_FULLCHAIN_PATH" + _end_n="$(grep -n -- "$END_CERT" "$CERT_FULLCHAIN_PATH" | _head_n 1 | cut -d : -f 1)" + _debug _end_n "$_end_n" + sed -n "1,${_end_n}p" "$CERT_FULLCHAIN_PATH" >"$CERT_PATH" + _end_n="$(_math $_end_n + 1)" + sed -n "${_end_n},9999p" "$CERT_FULLCHAIN_PATH" >"$CA_CERT_PATH" + fi + else + if ! _send_signed_request "${ACME_NEW_ORDER}" "{\"resource\": \"$ACME_NEW_ORDER_RES\", \"csr\": \"$der\"}" "needbase64"; then + _err "Sign failed." + _on_issue_err "$_post_hook" + return 1 + fi + _rcert="$response" + Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" echo "$BEGIN_CERT" >"$CERT_PATH" #if ! _get "$Le_LinkCert" | _base64 "multiline" >> "$CERT_PATH" ; then @@ -3798,6 +4012,12 @@ issue() { fi echo "$END_CERT" >>"$CERT_PATH" + fi + + _debug "Le_LinkCert" "$Le_LinkCert" + _savedomainconf "Le_LinkCert" "$Le_LinkCert" + + if [ "$Le_LinkCert" ]; then _info "$(__green "Cert success.")" cat "$CERT_PATH" @@ -3824,40 +4044,49 @@ issue() { _cleardomainconf "Le_Vlist" - Le_LinkIssuer=$(grep -i '^Link' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | cut -d ';' -f 1 | tr -d '<>') - if ! _contains "$Le_LinkIssuer" ":"; then - _info "$(__red "Relative issuer link found.")" - Le_LinkIssuer="$_ACME_SERVER_HOST$Le_LinkIssuer" - fi - _debug Le_LinkIssuer "$Le_LinkIssuer" - _savedomainconf "Le_LinkIssuer" "$Le_LinkIssuer" - - if [ "$Le_LinkIssuer" ]; then - _link_issuer_retry=0 - _MAX_ISSUER_RETRY=5 - while [ "$_link_issuer_retry" -lt "$_MAX_ISSUER_RETRY" ]; do - _debug _link_issuer_retry "$_link_issuer_retry" - if _get "$Le_LinkIssuer" >"$CA_CERT_PATH.der"; then - echo "$BEGIN_CERT" >"$CA_CERT_PATH" - _base64 "multiline" <"$CA_CERT_PATH.der" >>"$CA_CERT_PATH" - echo "$END_CERT" >>"$CA_CERT_PATH" - - _info "The intermediate CA cert is in $(__green " $CA_CERT_PATH ")" - cat "$CA_CERT_PATH" >>"$CERT_FULLCHAIN_PATH" - _info "And the full chain certs is there: $(__green " $CERT_FULLCHAIN_PATH ")" - - rm -f "$CA_CERT_PATH.der" - break - fi - _link_issuer_retry=$(_math $_link_issuer_retry + 1) - _sleep "$_link_issuer_retry" - done - if [ "$_link_issuer_retry" = "$_MAX_ISSUER_RETRY" ]; then - _err "Max retry for issuer ca cert is reached." - fi + if [ "$ACME_VERSION" = "2" ]; then + _debug "v2 chain." else - _debug "No Le_LinkIssuer header found." + Le_LinkIssuer=$(grep -i '^Link' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | cut -d ';' -f 1 | tr -d '<>') + + if [ "$Le_LinkIssuer" ]; then + if ! _contains "$Le_LinkIssuer" ":"; then + _info "$(__red "Relative issuer link found.")" + Le_LinkIssuer="$_ACME_SERVER_HOST$Le_LinkIssuer" + fi + _debug Le_LinkIssuer "$Le_LinkIssuer" + _savedomainconf "Le_LinkIssuer" "$Le_LinkIssuer" + + _link_issuer_retry=0 + _MAX_ISSUER_RETRY=5 + while [ "$_link_issuer_retry" -lt "$_MAX_ISSUER_RETRY" ]; do + _debug _link_issuer_retry "$_link_issuer_retry" + if [ "$ACME_VERSION" = "2" ]; then + if _get "$Le_LinkIssuer" >"$CA_CERT_PATH"; then + break + fi + else + if _get "$Le_LinkIssuer" >"$CA_CERT_PATH.der"; then + echo "$BEGIN_CERT" >"$CA_CERT_PATH" + _base64 "multiline" <"$CA_CERT_PATH.der" >>"$CA_CERT_PATH" + echo "$END_CERT" >>"$CA_CERT_PATH" + cat "$CA_CERT_PATH" >>"$CERT_FULLCHAIN_PATH" + rm -f "$CA_CERT_PATH.der" + break + fi + fi + _link_issuer_retry=$(_math $_link_issuer_retry + 1) + _sleep "$_link_issuer_retry" + done + if [ "$_link_issuer_retry" = "$_MAX_ISSUER_RETRY" ]; then + _err "Max retry for issuer ca cert is reached." + fi + else + _debug "No Le_LinkIssuer header found." + fi fi + [ -f "$CA_CERT_PATH" ] && _info "The intermediate CA cert is in $(__green " $CA_CERT_PATH ")" + [ -f "$CERT_FULLCHAIN_PATH" ] && _info "And the full chain certs is there: $(__green " $CERT_FULLCHAIN_PATH ")" Le_CertCreateTime=$(_time) _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime" @@ -3957,7 +4186,7 @@ renew() { _savedomainconf Le_API "$Le_API" fi if [ "$_OLD_STAGE_CA_HOST" = "$Le_API" ]; then - export Le_API="$STAGE_CA" + export Le_API="$DEFAULT_STAGING_CA" _savedomainconf Le_API "$Le_API" fi export ACME_DIRECTORY="$Le_API" @@ -4045,8 +4274,6 @@ signcsr() { return 1 fi - _initpath - _csrsubj=$(_readSubjectFromCSR "$_csrfile") if [ "$?" != "0" ]; then _err "Can not read subject from csr: $_csrfile" @@ -4083,6 +4310,9 @@ signcsr() { return 1 fi + if [ -z "$ACME_VERSION" ] && _contains "$_csrsubj,$_csrdomainlist" "*."; then + export ACME_VERSION=2 + fi _initpath "$_csrsubj" "$_csrkeylength" mkdir -p "$DOMAIN_PATH" @@ -4448,7 +4678,11 @@ revoke() { _initAPI - data="{\"resource\": \"revoke-cert\", \"certificate\": \"$cert\"}" + if [ "$ACME_VERSION" = "2" ]; then + data="{\"certificate\": \"$cert\"}" + else + data="{\"resource\": \"revoke-cert\", \"certificate\": \"$cert\"}" + fi uri="${ACME_REVOKE_CERT}" if [ -f "$CERT_KEY_PATH" ]; then @@ -4519,27 +4753,56 @@ _deactivate() { _d_type="$2" _initpath - if ! __get_domain_new_authz "$_d_domain"; then - _err "Can not get domain new authz token." - return 1 + if [ "$ACME_VERSION" = "2" ]; then + _identifiers="{\"type\":\"dns\",\"value\":\"$_d_domain\"}" + if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then + _err "Can not get domain new order." + return 1 + fi + _authorizations_seg="$(echo "$response" | tr -d '\r\n' | _egrep_o '"authorizations" *: *\[[^\]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')" + _debug2 _authorizations_seg "$_authorizations_seg" + if [ -z "$_authorizations_seg" ]; then + _err "_authorizations_seg not found." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + authzUri="$_authorizations_seg" + _debug2 "authzUri" "$authzUri" + if ! response="$(_get "$authzUri")"; then + _err "get to authz error." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + _URL_NAME="url" + else + if ! __get_domain_new_authz "$_d_domain"; then + _err "Can not get domain new authz token." + return 1 + fi + + authzUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" + _debug "authzUri" "$authzUri" + if [ "$code" ] && [ ! "$code" = '201' ]; then + _err "new-authz error: $response" + return 1 + fi + _URL_NAME="uri" fi - authzUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" - _debug "authzUri" "$authzUri" - - if [ "$code" ] && [ ! "$code" = '201' ]; then - _err "new-authz error: $response" - return 1 - fi - - entries="$(echo "$response" | _egrep_o '{ *"type":"[^"]*", *"status": *"valid", *"uri"[^}]*')" + entries="$(echo "$response" | _egrep_o "{ *\"type\":\"[^\"]*\", *\"status\": *\"valid\", *\"$_URL_NAME\"[^}]*")" if [ -z "$entries" ]; then _info "No valid entries found." if [ -z "$thumbprint" ]; then thumbprint="$(__calc_account_thumbprint)" fi _debug "Trigger validation." - vtype="$VTYPE_HTTP" + vtype="$VTYPE_DNS" entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then @@ -4549,7 +4812,7 @@ _deactivate() { token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" _debug token "$token" - uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d : -f 2,3 | tr -d '"')" + uri="$(printf "%s\n" "$entry" | _egrep_o "\"$_URL_NAME\":\"[^\"]*" | cut -d : -f 2,3 | tr -d '"')" _debug uri "$uri" keyauthorization="$token.$thumbprint" @@ -4575,7 +4838,7 @@ _deactivate() { _debug _vtype "$_vtype" _info "Found $_vtype" - uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d : -f 2,3 | tr -d '"')" + uri="$(printf "%s\n" "$entry" | _egrep_o "\"$_URL_NAME\":\"[^\"]*" | cut -d : -f 2,3 | tr -d '"')" _debug uri "$uri" if [ "$_d_type" ] && [ "$_d_type" != "$_vtype" ]; then @@ -4585,7 +4848,13 @@ _deactivate() { _info "Deactivate: $_vtype" - if _send_signed_request "$authzUri" "{\"resource\": \"authz\", \"status\":\"deactivated\"}" && _contains "$response" '"deactivated"'; then + if [ "$ACME_VERSION" = "2" ]; then + _djson="{\"status\":\"deactivated\"}" + else + _djson="{\"resource\": \"authz\", \"status\":\"deactivated\"}" + fi + + if _send_signed_request "$authzUri" "$_djson" && _contains "$response" '"deactivated"'; then _info "Deactivate: $_vtype success." else _err "Can not deactivate $_vtype." @@ -4894,8 +5163,14 @@ install() { if [ -z "$NO_DETECT_SH" ]; then #Modify shebang if _exists bash; then + _bash_path="$(bash -c "command -v bash 2>/dev/null")" + if [ -z "$_bash_path" ]; then + _bash_path="$(bash -c 'echo $SHELL')" + fi + fi + if [ "$_bash_path" ]; then _info "Good, bash is found, so change the shebang to use bash as preferred." - _shebang='#!'"$(env bash -c "command -v bash")" + _shebang='#!'"$_bash_path" _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang" for subf in $_SUB_FOLDERS; do if [ -d "$LE_WORKING_DIR/$subf" ]; then @@ -5000,7 +5275,7 @@ Commands: --renew, -r Renew a cert. --renew-all Renew all the certs. --revoke Revoke a cert. - --remove Remove the cert from $PROJECT + --remove Remove the cert from list of certs known to $PROJECT_NAME. --list List all the certs. --showcsr Show the content of a csr. --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. @@ -5307,6 +5582,10 @@ _process() { return 1 fi + if _startswith "$_dvalue" "*."; then + _debug "Wildcard domain" + export ACME_VERSION=2 + fi if [ -z "$_domain" ]; then _domain="$_dvalue" else @@ -5510,7 +5789,7 @@ _process() { HTTPS_INSECURE="1" ;; --ca-bundle) - _ca_bundle="$(_readlink -f "$2")" + _ca_bundle="$(_readlink "$2")" CA_BUNDLE="$_ca_bundle" shift ;; diff --git a/dnsapi/README.md b/dnsapi/README.md index f029b72..40efc9e 100644 --- a/dnsapi/README.md +++ b/dnsapi/README.md @@ -679,6 +679,60 @@ acme.sh --issue --dns dns_dnsever -d example.com -d www.example.com ``` The DNSEVER_ID and DNSEVER_PW will be saved in ~/.acme.sh/account.conf and will be reused when needed. +## 36. Use autoDNS (InternetX) + +[InternetX](https://www.internetx.com/) offers an [xml api](https://help.internetx.com/display/API/AutoDNS+XML-API) with your standard login credentials, set them like so: + +``` +export AUTODNS_USER="yourusername" +export AUTODNS_PASSWORD="password" +export AUTODNS_CONTEXT="context" +``` + +Then you can issue your certificates with: + +``` +acme.sh --issue --dns dns_autodns -d example.com -d www.example.com +``` + +The `AUTODNS_USER`, `AUTODNS_PASSWORD` and `AUTODNS_CONTEXT` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 37. Use Azure DNS + +You have to create a service principal first. See:[How to use Azure DNS](../../../wiki/How-to-use-Azure-DNS) + +``` +export AZUREDNS_SUBSCRIPTIONID="12345678-9abc-def0-1234-567890abcdef" +export AZUREDNS_TENANTID="11111111-2222-3333-4444-555555555555" +export AZUREDNS_APPID="3b5033b5-7a66-43a5-b3b9-a36b9e7c25ed" +export AZUREDNS_CLIENTSECRET="1b0224ef-34d4-5af9-110f-77f527d561bd" +``` + +Then you can issue your certificates with: + +``` +acme.sh --issue --dns dns_azure -d example.com -d www.example.com +``` + +`AZUREDNS_SUBSCRIPTIONID`, `AZUREDNS_TENANTID`,`AZUREDNS_APPID` and `AZUREDNS_CLIENTSECRET` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 38. Use selectel.com(selectel.ru) domain API to automatically issue cert + +First you need to login to your account to get your API key from: https://my.selectel.ru/profile/apikeys. + +```sh +export SL_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" + +``` + +Ok, let's issue a cert now: +``` +acme.sh --issue --dns dns_selectel -d example.com -d www.example.com +``` + +The `SL_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + + # Use custom API If your API is not supported yet, you can write your own DNS API. diff --git a/dnsapi/dns_autodns.sh b/dnsapi/dns_autodns.sh new file mode 100644 index 0000000..9253448 --- /dev/null +++ b/dnsapi/dns_autodns.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env sh +# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*- + +# This is the InternetX autoDNS xml api wrapper for acme.sh +# Author: auerswald@gmail.com +# Created: 2018-01-14 +# +# export AUTODNS_USER="username" +# export AUTODNS_PASSWORD="password" +# export AUTODNS_CONTEXT="context" +# +# Usage: +# acme.sh --issue --dns dns_autodns -d example.com + +AUTODNS_API="https://gateway.autodns.com" + +# Arguments: +# txtdomain +# txt +dns_autodns_add() { + fulldomain="$1" + txtvalue="$2" + + AUTODNS_USER="${AUTODNS_USER:-$(_readaccountconf_mutable AUTODNS_USER)}" + AUTODNS_PASSWORD="${AUTODNS_PASSWORD:-$(_readaccountconf_mutable AUTODNS_PASSWORD)}" + AUTODNS_CONTEXT="${AUTODNS_CONTEXT:-$(_readaccountconf_mutable AUTODNS_CONTEXT)}" + + if [ -z "$AUTODNS_USER" ] || [ -z "$AUTODNS_CONTEXT" ] || [ -z "$AUTODNS_PASSWORD" ]; then + _err "You don't specify autodns user, password and context." + return 1 + fi + + _saveaccountconf_mutable AUTODNS_USER "$AUTODNS_USER" + _saveaccountconf_mutable AUTODNS_PASSWORD "$AUTODNS_PASSWORD" + _saveaccountconf_mutable AUTODNS_CONTEXT "$AUTODNS_CONTEXT" + + _debug "First detect the root zone" + + if ! _get_autodns_zone "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _zone "$_zone" + _debug _system_ns "$_system_ns" + + _info "Adding TXT record" + + autodns_response="$(_autodns_zone_update "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")" + + if [ "$?" -eq "0" ]; then + _info "Added, OK" + return 0 + fi + + return 1 +} + +# Arguments: +# txtdomain +# txt +dns_autodns_rm() { + fulldomain="$1" + txtvalue="$2" + + AUTODNS_USER="${AUTODNS_USER:-$(_readaccountconf_mutable AUTODNS_USER)}" + AUTODNS_PASSWORD="${AUTODNS_PASSWORD:-$(_readaccountconf_mutable AUTODNS_PASSWORD)}" + AUTODNS_CONTEXT="${AUTODNS_CONTEXT:-$(_readaccountconf_mutable AUTODNS_CONTEXT)}" + + if [ -z "$AUTODNS_USER" ] || [ -z "$AUTODNS_CONTEXT" ] || [ -z "$AUTODNS_PASSWORD" ]; then + _err "You don't specify autodns user, password and context." + return 1 + fi + + _debug "First detect the root zone" + + if ! _get_autodns_zone "$fulldomain"; then + _err "zone not found" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _zone "$_zone" + _debug _system_ns "$_system_ns" + + _info "Delete TXT record" + + autodns_response="$(_autodns_zone_cleanup "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")" + + if [ "$?" -eq "0" ]; then + _info "Deleted, OK" + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## + +# Arguments: +# fulldomain +# Returns: +# _sub_domain=_acme-challenge.www +# _zone=domain.com +# _system_ns +_get_autodns_zone() { + domain="$1" + + i=2 + 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 + + autodns_response="$(_autodns_zone_inquire "$h")" + + if [ "$?" -ne "0" ]; then + _err "invalid domain" + return 1 + fi + + if _contains "$autodns_response" "1" >/dev/null; then + _zone="$(echo "$autodns_response" | _egrep_o '[^<]*' | cut -d '>' -f 2 | cut -d '<' -f 1)" + _system_ns="$(echo "$autodns_response" | _egrep_o '[^<]*' | cut -d '>' -f 2 | cut -d '<' -f 1)" + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + + return 1 +} + +_build_request_auth_xml() { + printf " + %s + %s + %s + " "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT" +} + +# Arguments: +# zone +_build_zone_inquire_xml() { + printf " + + %s + + 0205 + + 1 + 1 + + + name + eq + %s + + + " "$(_build_request_auth_xml)" "$1" +} + +# Arguments: +# zone +# subdomain +# txtvalue +# system_ns +_build_zone_update_xml() { + printf " + + %s + + 0202001 + + + %s + 600 + TXT + %s + + + + %s + %s + + + " "$(_build_request_auth_xml)" "$2" "$3" "$1" "$4" +} + +# Arguments: +# zone +_autodns_zone_inquire() { + request_data="$(_build_zone_inquire_xml "$1")" + autodns_response="$(_autodns_api_call "$request_data")" + ret="$?" + + printf "%s" "$autodns_response" + return "$ret" +} + +# Arguments: +# zone +# subdomain +# txtvalue +# system_ns +_autodns_zone_update() { + request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")" + autodns_response="$(_autodns_api_call "$request_data")" + ret="$?" + + printf "%s" "$autodns_response" + return "$ret" +} + +# Arguments: +# zone +# subdomain +# txtvalue +# system_ns +_autodns_zone_cleanup() { + request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")" + # replace 'rr_add>' with 'rr_rem>' in request_data + request_data="$(printf -- "%s" "$request_data" | sed 's/rr_add>/rr_rem>/g')" + autodns_response="$(_autodns_api_call "$request_data")" + ret="$?" + + printf "%s" "$autodns_response" + return "$ret" +} + +# Arguments: +# request_data +_autodns_api_call() { + request_data="$1" + + _debug request_data "$request_data" + + autodns_response="$(_post "$request_data" "$AUTODNS_API")" + ret="$?" + + _debug autodns_response "$autodns_response" + + if [ "$ret" -ne "0" ]; then + _err "error" + return 1 + fi + + if _contains "$autodns_response" "success" >/dev/null; then + _info "success" + printf "%s" "$autodns_response" + return 0 + fi + + return 1 +} diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index 5a71651..ed31746 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -19,18 +19,19 @@ dns_aws_add() { fulldomain=$1 txtvalue=$2 + 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)}" if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" _err "You don't specify aws route53 api key id and and api key secret yet." - _err "Please create you key and try again. see $(__green $AWS_WIKI)" + _err "Please create your key and try again. see $(__green $AWS_WIKI)" return 1 fi - if [ -z "$AWS_SESSION_TOKEN" ]; then - _saveaccountconf AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID" - _saveaccountconf AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY" - fi + #save for future use + _saveaccountconf_mutable AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID" + _saveaccountconf_mutable AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY" _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -56,6 +57,8 @@ dns_aws_rm() { fulldomain=$1 txtvalue=$2 + 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)}" _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" diff --git a/dnsapi/dns_azure.sh b/dnsapi/dns_azure.sh new file mode 100644 index 0000000..0834ede --- /dev/null +++ b/dnsapi/dns_azure.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env sh + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +# +# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate +# +dns_azure_add() { + fulldomain=$1 + txtvalue=$2 + + AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" + AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" + AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" + AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" + + if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Subscription ID " + return 1 + fi + + if [ -z "$AZUREDNS_TENANTID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Tenant ID " + return 1 + fi + + if [ -z "$AZUREDNS_APPID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure App ID" + return 1 + fi + + if [ -z "$AZUREDNS_CLIENTSECRET" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Client Secret" + return 1 + fi + #save account details to account conf file. + _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID" + _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" + _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" + _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" + + accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + + if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01" + _debug "$acmeRecordURI" + body="{\"properties\": {\"TTL\": 3600, \"TXTRecords\": [{\"value\": [\"$txtvalue\"]}]}}" + _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken" + if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then + _info "validation record added" + else + _err "error adding validation record ($_code)" + return 1 + fi +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +# +# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/delete +# +dns_azure_rm() { + fulldomain=$1 + txtvalue=$2 + + AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" + AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" + AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" + AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" + + if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Subscription ID " + return 1 + fi + + if [ -z "$AZUREDNS_TENANTID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Tenant ID " + return 1 + fi + + if [ -z "$AZUREDNS_APPID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure App ID" + return 1 + fi + + if [ -z "$AZUREDNS_CLIENTSECRET" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Client Secret" + return 1 + fi + + accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + + if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01" + _debug "$acmeRecordURI" + body="{\"properties\": {\"TTL\": 3600, \"TXTRecords\": [{\"value\": [\"$txtvalue\"]}]}}" + _azure_rest DELETE "$acmeRecordURI" "" "$accesstoken" + if [ "$_code" = "200" ] || [ "$_code" = '204' ]; then + _info "validation record removed" + else + _err "error removing validation record ($_code)" + return 1 + fi +} + +################### Private functions below ################################## + +_azure_rest() { + m=$1 + ep="$2" + data="$3" + accesstoken="$4" + + export _H1="authorization: Bearer $accesstoken" + export _H2="accept: application/json" + export _H3="Content-Type: application/json" + + _debug "$ep" + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$ep" "" "$m")" + else + response="$(_get "$ep")" + fi + _debug2 response "$response" + + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug2 "http response code $_code" + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + return 0 +} + +## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token +_azure_getaccess_token() { + TENANTID=$1 + clientID=$2 + clientSecret=$3 + + export _H1="accept: application/json" + export _H2="Content-Type: application/x-www-form-urlencoded" + + body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials" + _debug data "$body" + response="$(_post "$body" "https://login.windows.net/$TENANTID/oauth2/token" "" "POST")" + accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + _debug2 "response $response" + + if [ -z "$accesstoken" ]; then + _err "no acccess token received" + return 1 + fi + if [ "$?" != "0" ]; then + _err "error $response" + return 1 + fi + printf "%s" "$accesstoken" + return 0 +} + +_get_root() { + domain=$1 + subscriptionId=$2 + accesstoken=$3 + i=2 + p=1 + + ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list + ## returns up to 100 zones in one response therefore handling more results is not not implemented + ## (ZoneListResult with continuation token for the next page of results) + ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways + ## + _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?api-version=2017-09-01" "" "$accesstoken" + + # Find matching domain name is Json response + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug2 "Checking domain: $h" + if [ -z "$h" ]; then + #not valid + _err "Invalid domain" + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(echo "$response" | _egrep_o "\{\"id\":\"[^\"]*$h\"" | head -n 1 | cut -d : -f 2 | tr -d \") + if [ "$_domain_id" ]; 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 +} diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 57a2e88..68264a4 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -51,33 +51,36 @@ dns_cf_add() { return 1 fi - count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) - _debug count "$count" - if [ "$count" = "0" ]; then - _info "Adding record" - if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then - if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then - _info "Added, OK" - return 0 - else - _err "Add txt record error." - return 1 - fi - fi - _err "Add txt record error." - else - _info "Updating record" - record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) - _debug "record_id" "$record_id" - - _cf_rest PUT "zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}" - if [ "$?" = "0" ]; then - _info "Updated, OK" + # 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 _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then + _info "Added, OK" return 0 + else + _err "Add txt record error." + return 1 fi - _err "Update error" - return 1 fi + _err "Add txt record error." + return 1 + # else + # _info "Updating record" + # record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) + # _debug "record_id" "$record_id" + # + # _cf_rest PUT "zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}" + # if [ "$?" = "0" ]; then + # _info "Updated, OK" + # return 0 + # fi + # _err "Update error" + # return 1 + # fi } diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 74440bd..5dfba7d 100644 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -35,53 +35,9 @@ dns_inwx_add() { fi _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _debug "Getting txt records" - xml_content=$(printf ' - - nameserver.info - - - - - - domain - - %s - - - - type - - TXT - - - - name - - %s - - - - - - - ' "$_domain" "$_sub_domain") - response="$(_post "$xml_content" "$INWX_Api" "" "POST")" - - if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then - _err "Error could net get txt records" - return 1 - fi - - if ! printf "%s" "$response" | grep "count" >/dev/null; then - _info "Adding record" - _inwx_add_record "$_domain" "$_sub_domain" "$txtvalue" - else - _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+') - _info "Updating record" - _inwx_update_record "$_record_id" "$txtvalue" - fi + _info "Adding record" + _inwx_add_record "$_domain" "$_sub_domain" "$txtvalue" } @@ -147,7 +103,7 @@ dns_inwx_rm() { ' "$_domain" "$_sub_domain") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" - if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then + if ! _contains "$response" "Command completed successfully"; then _err "Error could not get txt records" return 1 fi diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index eaa90bd..6009473 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -78,13 +78,7 @@ _ovh_get_api() { esac } -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_ovh_add() { - fulldomain=$1 - txtvalue=$2 - +_initAuth() { if [ -z "$OVH_AK" ] || [ -z "$OVH_AS" ]; then OVH_AK="" OVH_AS="" @@ -119,14 +113,26 @@ dns_ovh_add() { _info "Checking authentication" - response="$(_ovh_rest GET "domain")" - if _contains "$response" "INVALID_CREDENTIAL"; then + if ! _ovh_rest GET "domain" || _contains "$response" "INVALID_CREDENTIAL"; then _err "The consumer key is invalid: $OVH_CK" _err "Please retry to create a new one." _clearaccountconf OVH_CK return 1 fi _info "Consumer key is ok." + return 0 +} + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ovh_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _initAuth; then + return 1 + fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -137,49 +143,58 @@ dns_ovh_add() { _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _debug "Getting txt records" - _ovh_rest GET "domain/zone/$_domain/record?fieldType=TXT&subDomain=$_sub_domain" - - if _contains "$response" '\[\]' || _contains "$response" "This service does not exist"; then - _info "Adding record" - if _ovh_rest POST "domain/zone/$_domain/record" "{\"fieldType\":\"TXT\",\"subDomain\":\"$_sub_domain\",\"target\":\"$txtvalue\",\"ttl\":60}"; then - if _contains "$response" "$txtvalue"; then - _ovh_rest POST "domain/zone/$_domain/refresh" - _debug "Refresh:$response" - _info "Added, sleeping 10 seconds" - sleep 10 - return 0 - fi + _info "Adding record" + if _ovh_rest POST "domain/zone/$_domain/record" "{\"fieldType\":\"TXT\",\"subDomain\":\"$_sub_domain\",\"target\":\"$txtvalue\",\"ttl\":60}"; then + if _contains "$response" "$txtvalue"; then + _ovh_rest POST "domain/zone/$_domain/refresh" + _debug "Refresh:$response" + _info "Added, sleep 10 seconds." + _sleep 10 + return 0 fi - _err "Add txt record error." - else - _info "Updating record" - record_id=$(printf "%s" "$response" | tr -d "[]" | cut -d , -f 1) - if [ -z "$record_id" ]; then - _err "Can not get record id." - return 1 - fi - _debug "record_id" "$record_id" - - if _ovh_rest PUT "domain/zone/$_domain/record/$record_id" "{\"target\":\"$txtvalue\",\"subDomain\":\"$_sub_domain\",\"ttl\":60}"; then - if _contains "$response" "null"; then - _ovh_rest POST "domain/zone/$_domain/refresh" - _debug "Refresh:$response" - _info "Updated, sleeping 10 seconds" - sleep 10 - return 0 - fi - fi - _err "Update error" - return 1 fi + _err "Add txt record error." + return 1 } #fulldomain dns_ovh_rm() { fulldomain=$1 + txtvalue=$2 + if ! _initAuth; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug "Getting txt records" + if ! _ovh_rest GET "domain/zone/$_domain/record?fieldType=TXT&subDomain=$_sub_domain"; then + return 1 + fi + + for rid in $(echo "$response" | tr '][,' ' '); do + _debug rid "$rid" + if ! _ovh_rest GET "domain/zone/$_domain/record/$rid"; then + return 1 + fi + if _contains "$response" "\"target\":\"$txtvalue\""; then + _debug "Found txt id:$rid" + if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then + return 1 + fi + return 0 + fi + done + + return 1 } #################### Private functions below ################################## @@ -191,7 +206,7 @@ _ovh_authentication() { _H3="" _H4="" - _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}' + _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}' response="$(_post "$_ovhdata" "$OVH_API/auth/credential")" _debug3 response "$response" @@ -279,15 +294,15 @@ _ovh_rest() { export _H3="X-Ovh-Timestamp: $_ovh_t" export _H4="X-Ovh-Consumer: $OVH_CK" export _H5="Content-Type: application/json;charset=utf-8" - if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ]; then + if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then _debug data "$data" response="$(_post "$data" "$_ovh_url" "" "$m")" else response="$(_get "$_ovh_url")" fi - if [ "$?" != "0" ]; then - _err "error $ep" + if [ "$?" != "0" ] || _contains "$response" "INVALID_CREDENTIAL"; then + _err "error $response" return 1 fi _debug2 response "$response" diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh new file mode 100644 index 0000000..94252d8 --- /dev/null +++ b/dnsapi/dns_selectel.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env sh + +# +#SL_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# + +SL_Api="https://api.selectel.ru/domains/v1" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_selectel_add() { + fulldomain=$1 + txtvalue=$2 + + SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}" + + if [ -z "$SL_Key" ]; then + SL_Key="" + _err "You don't specify selectel.ru api key yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key to the account conf file. + _saveaccountconf_mutable SL_Key "$SL_Key" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _sl_rest POST "/$_domain_id/records/" "{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"$fulldomain\", \"content\": \"$txtvalue\"}"; then + if _contains "$response" "$txtvalue" || _contains "$response" "record_already_exists"; then + _info "Added, OK" + return 0 + fi + fi + _err "Add txt record error." + return 1 +} + +#fulldomain txtvalue +dns_selectel_rm() { + fulldomain=$1 + txtvalue=$2 + + SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}" + + if [ -z "$SL_Key" ]; then + SL_Key="" + _err "You don't specify slectel api key yet." + _err "Please create you key and try again." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _sl_rest GET "/${_domain_id}/records/" + + if ! _contains "$response" "$txtvalue"; then + _err "Txt record not found" + return 1 + fi + + _record_seg="$(echo "$response" | _egrep_o "\"content\" *: *\"$txtvalue\"[^}]*}")" + _debug2 "_record_seg" "$_record_seg" + if [ -z "$_record_seg" ]; then + _err "can not find _record_seg" + return 1 + fi + + _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2)" + _debug2 "_record_id" "$_record_id" + if [ -z "$_record_id" ]; then + _err "can not find _record_id" + return 1 + fi + + if ! _sl_rest DELETE "/$_domain_id/records/$_record_id"; then + _err "Delete record error." + return 1 + fi + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + + if ! _sl_rest GET "/"; then + return 1 + fi + + i=2 + 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 _contains "$response" "\"name\": \"$h\","; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + _debug "Getting domain id for $h" + if ! _sl_rest GET "/$h"; then + return 1 + fi + _domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_sl_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="X-Token: $SL_Key" + export _H2="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$SL_Api/$ep" "" "$m")" + else + response="$(_get "$SL_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_yandex.sh b/dnsapi/dns_yandex.sh index eb60d5a..7ebb15d 100755 --- a/dnsapi/dns_yandex.sh +++ b/dnsapi/dns_yandex.sh @@ -18,7 +18,6 @@ dns_yandex_add() { curDomain=$(_PDD_get_domain "$fulldomain") _debug "Found suitable domain in pdd: $curDomain" - curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")" curData="domain=${curDomain}&type=TXT&subdomain=${curSubdomain}&ttl=360&content=${txtvalue}" curUri="https://pddimp.yandex.ru/api2/admin/dns/add" curResult="$(_post "${curData}" "${curUri}")" @@ -36,7 +35,6 @@ dns_yandex_rm() { curDomain=$(_PDD_get_domain "$fulldomain") _debug "Found suitable domain in pdd: $curDomain" - curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")" curUri="https://pddimp.yandex.ru/api2/admin/dns/del" curData="domain=${curDomain}&record_id=${record_id}" @@ -61,7 +59,7 @@ _PDD_get_domain() { __last=1 fi - __all_domains="$__all_domains $(echo "$res1" | sed -e "s@,@\n@g" | grep '"name"' | cut -d: -f2 | sed -e 's@"@@g')" + __all_domains="$__all_domains $(echo "$res1" | tr "," "\n" | grep '"name"' | cut -d: -f2 | sed -e 's@"@@g')" __page=$(_math $__page + 1) done @@ -72,6 +70,8 @@ _PDD_get_domain() { _debug "finding zone for domain $__t" for d in $__all_domains; do if [ "$d" = "$__t" ]; then + p=$(_math $k - 1) + curSubdomain="$(echo "$fulldomain" | cut -d . -f "1-$p")" echo "$__t" return fi @@ -98,7 +98,6 @@ pdd_get_record_id() { curDomain=$(_PDD_get_domain "$fulldomain") _debug "Found suitable domain in pdd: $curDomain" - curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")" curUri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=${curDomain}" curResult="$(_get "${curUri}" | _normalizeJson)"