diff --git a/Dockerfile b/Dockerfile index c1a2199b..68385d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.6 +FROM alpine:3.9 RUN apk update -f \ && apk --no-cache add -f \ @@ -7,6 +7,7 @@ RUN apk update -f \ bind-tools \ curl \ socat \ + tzdata \ && rm -rf /var/cache/apk/* ENV LE_CONFIG_HOME /acme.sh diff --git a/README.md b/README.md index 4bde4ea4..7c49a34a 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ https://github.com/Neilpang/acmetest - Letsencrypt.org CA(default) - [BuyPass.com CA](https://github.com/Neilpang/acme.sh/wiki/BuyPass.com-CA) +- [Pebble strict Mode](https://github.com/letsencrypt/pebble) # Supported modes @@ -253,7 +254,7 @@ Just set string "apache" as the second argument and it will force use of apache acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com ``` -**This apache mode is only to issue the cert, it will not change your apache config files. +**This apache mode is only to issue the cert, it will not change your apache config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess your apache server, don't worry.** @@ -277,7 +278,7 @@ So, the config is not changed. acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com ``` -**This nginx mode is only to issue the cert, it will not change your nginx config files. +**This nginx mode is only to issue the cert, it will not change your nginx config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess your nginx server, don't worry.** @@ -351,6 +352,12 @@ You don't have to do anything manually! 1. PointDNS API (https://pointhq.com/) 1. Active24.cz API (https://www.active24.cz/) 1. do.de API (https://www.do.de/) +1. Nexcess API (https://www.nexcess.net) +1. Thermo.io API (https://www.thermo.io) +1. Futurehosting API (https://www.futurehosting.com) +1. Rackspace Cloud DNS (https://www.rackspace.com) +1. Online.net API (https://online.net/) +1. MyDevil.net (https://www.mydevil.net/) 1. NederHost API (https://www.nederhost.nl/) And: @@ -529,5 +536,5 @@ Please Star and Fork me. Your donation makes **acme.sh** better: 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) - + [Donate List](https://github.com/Neilpang/acme.sh/wiki/Donate-list) diff --git a/acme.sh b/acme.sh index cfdf5714..005b1333 100755 --- a/acme.sh +++ b/acme.sh @@ -139,6 +139,7 @@ __red() { } _printargs() { + _exitstatus="$?" if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then printf -- "%s" "[$(date)] " fi @@ -148,6 +149,8 @@ _printargs() { printf -- "%s" "$1='$2'" fi printf "\n" + # return the saved exit status + return "$_exitstatus" } _dlg_versions() { @@ -183,6 +186,7 @@ _dlg_versions() { #class _syslog() { + _exitstatus="$?" if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" = "$SYSLOG_LEVEL_NONE" ]; then return fi @@ -196,6 +200,7 @@ _syslog() { fi fi $__logger_i -t "$PROJECT_NAME" -p "$_logclass" "$(_printargs "$@")" >/dev/null 2>&1 + return "$_exitstatus" } _log() { @@ -1188,7 +1193,7 @@ _ss() { if _exists "netstat"; then _debug "Using: netstat" - if netstat -h 2>&1 | grep "\-p proto" >/dev/null; then + if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then #for windows version netstat tool netstat -an -p tcp | grep "LISTENING" | grep ":$_port " else @@ -1822,23 +1827,29 @@ _send_signed_request() { nonceurl="$ACME_NEW_NONCE" if _post "" "$nonceurl" "" "HEAD" "$__request_conent_type"; then _headers="$(cat "$HTTP_HEADER")" + _debug2 _headers "$_headers" + _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" fi fi - if [ -z "$_headers" ]; then + if [ -z "$_CACHED_NONCE" ]; then _debug2 "Get nonce with GET. ACME_DIRECTORY" "$ACME_DIRECTORY" nonceurl="$ACME_DIRECTORY" _headers="$(_get "$nonceurl" "onlyheader")" + _debug2 _headers "$_headers" + _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" fi - + if [ -z "$_CACHED_NONCE" ] && [ "$ACME_NEW_NONCE" ]; then + _debug2 "Get nonce with GET. ACME_NEW_NONCE" "$ACME_NEW_NONCE" + nonceurl="$ACME_NEW_NONCE" + _headers="$(_get "$nonceurl" "onlyheader")" + _debug2 _headers "$_headers" + _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + fi + _debug2 _CACHED_NONCE "$_CACHED_NONCE" if [ "$?" != "0" ]; then _err "Can not connect to $nonceurl to get nonce." return 1 fi - - _debug2 _headers "$_headers" - - _CACHED_NONCE="$(echo "$_headers" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" - _debug2 _CACHED_NONCE "$_CACHED_NONCE" else _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" fi @@ -1882,29 +1893,34 @@ _send_signed_request() { _err "Can not post to $url" return 1 fi - _debug2 original "$response" - response="$(echo "$response" | _normalizeJson)" responseHeaders="$(cat "$HTTP_HEADER")" - _debug2 responseHeaders "$responseHeaders" - _debug2 response "$response" + code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" _debug code "$code" - _CACHED_NONCE="$(echo "$responseHeaders" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" - - _body="$response" - if [ "$needbase64" ]; then - _body="$(echo "$_body" | _dbase64 | tr -d '\0')" - _debug3 _body "$_body" + _debug2 original "$response" + if echo "$responseHeaders" | grep -i "Content-Type: application/json" >/dev/null 2>&1; then + response="$(echo "$response" | _normalizeJson)" fi + _debug2 response "$response" - 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="" - _sleep $_sleep_retry_sec - continue + _CACHED_NONCE="$(echo "$responseHeaders" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + + if ! _startswith "$code" "2"; then + _body="$response" + if [ "$needbase64" ]; then + _body="$(echo "$_body" | _dbase64 multiline)" + _debug3 _body "$_body" + 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="" + _sleep $_sleep_retry_sec + continue + fi fi break done @@ -2050,6 +2066,7 @@ _clearcaconf() { _startserver() { content="$1" ncaddr="$2" + _debug "content" "$content" _debug "ncaddr" "$ncaddr" _debug "startserver: $$" @@ -2076,8 +2093,14 @@ _startserver() { SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}" fi + _content_len="$(printf "%s" "$content" | wc -c)" + _debug _content_len "$_content_len" _debug "_NC" "$_NC $SOCAT_OPTIONS" - $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; echo HTTP/1.0 200 OK; echo ; echo $content; echo;" & + $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ +echo 'HTTP/1.0 200 OK'; \ +echo 'Content-Length\: $_content_len'; \ +echo ''; \ +printf '$content';" & serverproc="$!" } @@ -2919,42 +2942,38 @@ _clearup() { _clearupdns() { _debug "_clearupdns" - _debug "dnsadded" "$dnsadded" - _debug "vlist" "$vlist" - #dnsadded is "0" or "1" means dns-01 method was used for at least one domain - if [ -z "$dnsadded" ] || [ -z "$vlist" ]; then + _debug "dns_entries" "$dns_entries" + + if [ -z "$dns_entries" ]; then _debug "skip dns." return fi _info "Removing DNS records." - ventries=$(echo "$vlist" | tr ',' ' ') - _alias_index=1 - for ventry in $ventries; do - d=$(echo "$ventry" | cut -d "$sep" -f 1) - keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) - vtype=$(echo "$ventry" | cut -d "$sep" -f 4) - _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" - _debug txt "$txt" - if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then - _debug "$d is already verified, skip $vtype." - _alias_index="$(_math "$_alias_index" + 1)" - continue - fi - if [ "$vtype" != "$VTYPE_DNS" ]; then - _debug "Skip $d for $vtype" - continue + for entry in $dns_entries; do + d=$(_getfield "$entry" 1) + txtdomain=$(_getfield "$entry" 2) + aliasDomain=$(_getfield "$entry" 3) + txt=$(_getfield "$entry" 5) + d_api=$(_getfield "$entry" 6) + _debug "d" "$d" + _debug "txtdomain" "$txtdomain" + _debug "aliasDomain" "$aliasDomain" + _debug "txt" "$txt" + _debug "d_api" "$d_api" + if [ "$d_api" = "$txt" ]; then + d_api="" fi - d_api="$(_findHook "$d" dnsapi "$_currentRoot")" - _debug d_api "$d_api" - if [ -z "$d_api" ]; then _info "Not Found domain api file: $d_api" continue fi + if [ "$aliasDomain" ]; then + txtdomain="$aliasDomain" + fi + ( if ! . "$d_api"; then _err "Load file $d_api error. Please check your api file and try again." @@ -2967,24 +2986,6 @@ _clearupdns() { return 1 fi - _dns_root_d="$d" - if _startswith "$_dns_root_d" "*."; then - _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')" - fi - - _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")" - _alias_index="$(_math "$_alias_index" + 1)" - _debug "_d_alias" "$_d_alias" - if [ "$_d_alias" ]; then - if _startswith "$_d_alias" "$DNS_ALIAS_PREFIX"; then - txtdomain="$(echo "$_d_alias" | sed "s/$DNS_ALIAS_PREFIX//")" - else - txtdomain="_acme-challenge.$_d_alias" - fi - else - txtdomain="_acme-challenge.$_dns_root_d" - fi - if ! $rmcommand "$txtdomain" "$txt"; then _err "Error removing txt for domain:$txtdomain" return 1 @@ -3074,6 +3075,7 @@ _on_before_issue() { _info "Standalone mode." if [ -z "$Le_HTTPPort" ]; then Le_HTTPPort=80 + _cleardomainconf "Le_HTTPPort" else _savedomainconf "Le_HTTPPort" "$Le_HTTPPort" fi @@ -3281,7 +3283,7 @@ _regAccount() { fi _debug2 responseHeaders "$responseHeaders" - _accUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" + _accUri="$(echo "$responseHeaders" | grep -i "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" _debug "_accUri" "$_accUri" if [ -z "$_accUri" ]; then _err "Can not find account id url." @@ -3447,12 +3449,119 @@ __trigger_validation() { _t_vtype="$3" _debug2 _t_vtype "$_t_vtype" if [ "$ACME_VERSION" = "2" ]; then - _send_signed_request "$_t_url" "{\"keyAuthorization\": \"$_t_key_authz\"}" + _send_signed_request "$_t_url" "{}" else _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"type\": \"$_t_vtype\", \"keyAuthorization\": \"$_t_key_authz\"}" fi } +#endpoint domain type +_ns_lookup() { + _ns_ep="$1" + _ns_domain="$2" + _ns_type="$3" + _debug2 "_ns_ep" "$_ns_ep" + _debug2 "_ns_domain" "$_ns_domain" + _debug2 "_ns_type" "$_ns_type" + + response="$(_H1="accept: application/dns-json" _get "$_ns_ep?name=$_ns_domain&type=$_ns_type")" + _ret=$? + _debug2 "response" "$response" + if [ "$_ret" != "0" ]; then + return $_ret + fi + _answers="$(echo "$response" | tr '{}' '<>' | _egrep_o '"Answer":\[[^]]*]' | tr '<>' '\n\n')" + _debug2 "_answers" "$_answers" + echo "$_answers" +} + +#domain, type +_ns_lookup_cf() { + _cf_ld="$1" + _cf_ld_type="$2" + _cf_ep="https://cloudflare-dns.com/dns-query" + _ns_lookup "$_cf_ep" "$_cf_ld" "$_cf_ld_type" +} + +#domain, type +_ns_purge_cf() { + _cf_d="$1" + _cf_d_type="$2" + _debug "Cloudflare purge $_cf_d_type record for domain $_cf_d" + _cf_purl="https://1.1.1.1/api/v1/purge?domain=$_cf_d&type=$_cf_d_type" + response="$(_post "" "$_cf_purl")" + _debug2 response "$response" +} + +#txtdomain, alias, txt +__check_txt() { + _c_txtdomain="$1" + _c_aliasdomain="$2" + _c_txt="$3" + _debug "_c_txtdomain" "$_c_txtdomain" + _debug "_c_aliasdomain" "$_c_aliasdomain" + _debug "_c_txt" "$_c_txt" + _answers="$(_ns_lookup_cf "$_c_aliasdomain" TXT)" + _contains "$_answers" "$_c_txt" + +} + +#txtdomain +__purge_txt() { + _p_txtdomain="$1" + _debug _p_txtdomain "$_p_txtdomain" + _ns_purge_cf "$_p_txtdomain" "TXT" +} + +#wait and check each dns entries +_check_dns_entries() { + _success_txt="," + _end_time="$(_time)" + _end_time="$(_math "$_end_time" + 1200)" #let's check no more than 20 minutes. + + while [ "$(_time)" -le "$_end_time" ]; do + _left="" + for entry in $dns_entries; do + d=$(_getfield "$entry" 1) + txtdomain=$(_getfield "$entry" 2) + aliasDomain=$(_getfield "$entry" 3) + txt=$(_getfield "$entry" 5) + d_api=$(_getfield "$entry" 6) + _debug "d" "$d" + _debug "txtdomain" "$txtdomain" + _debug "aliasDomain" "$aliasDomain" + _debug "txt" "$txt" + _debug "d_api" "$d_api" + _info "Checking $d for $aliasDomain" + if _contains "$_success_txt" ",$txt,"; then + _info "Already success, continue next one." + continue + fi + + if __check_txt "$txtdomain" "$aliasDomain" "$txt"; then + _info "Domain $d '$aliasDomain' success." + _success_txt="$_success_txt,$txt," + continue + fi + _left=1 + _info "Not valid yet, let's wait 10 seconds and check next one." + _sleep 10 + __purge_txt "$txtdomain" + if [ "$txtdomain" != "$aliasDomain" ]; then + __purge_txt "$aliasDomain" + fi + done + if [ "$_left" ]; then + _info "Let's wait 10 seconds and check again". + _sleep 10 + else + _info "All success, let's return" + break + fi + done + +} + #webroot, domain domainlist keylength issue() { if [ -z "$2" ]; then @@ -3776,6 +3885,7 @@ $_authorizations_map" done _debug vlist "$vlist" #add entry + dns_entries="" dnsadded="" ventries=$(echo "$vlist" | tr "$dvsep" ' ') _alias_index=1 @@ -3806,8 +3916,10 @@ $_authorizations_map" else txtdomain="_acme-challenge.$_d_alias" fi + dns_entries="${dns_entries}${_dns_root_d}${dvsep}_acme-challenge.$_dns_root_d$dvsep$txtdomain$dvsep$_currentRoot" else txtdomain="_acme-challenge.$_dns_root_d" + dns_entries="${dns_entries}${_dns_root_d}${dvsep}_acme-challenge.$_dns_root_d$dvsep$dvsep$_currentRoot" fi _debug txtdomain "$txtdomain" txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" @@ -3816,7 +3928,9 @@ $_authorizations_map" d_api="$(_findHook "$_dns_root_d" dnsapi "$_currentRoot")" _debug d_api "$d_api" - + dns_entries="$dns_entries$dvsep$txt${dvsep}$d_api +" + _debug2 "$dns_entries" if [ "$d_api" ]; then _info "Found domain api file: $d_api" else @@ -3870,15 +3984,21 @@ $_authorizations_map" fi - if [ "$dnsadded" = '1' ]; then + if [ "$dns_entries" ]; then if [ -z "$Le_DNSSleep" ]; then - Le_DNSSleep="$DEFAULT_DNS_SLEEP" + _info "Let's check each dns records now. Sleep 20 seconds first." + _sleep 20 + if ! _check_dns_entries; then + _err "check dns error." + _on_issue_err "$_post_hook" + _clearup + return 1 + fi else _savedomainconf "Le_DNSSleep" "$Le_DNSSleep" + _info "Sleep $(__green $Le_DNSSleep) seconds for the txt records to take effect" + _sleep "$Le_DNSSleep" fi - - _info "Sleep $(__green $Le_DNSSleep) seconds for the txt records to take effect" - _sleep "$Le_DNSSleep" fi NGINX_RESTORE_VLIST="" @@ -4099,28 +4219,74 @@ $_authorizations_map" der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _url_replace)" if [ "$ACME_VERSION" = "2" ]; then + _info "Lets finalize the order, Le_OrderFinalize: $Le_OrderFinalize" 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." + _err "Sign failed, finalize code is not 200." _err "$response" _on_issue_err "$_post_hook" return 1 fi - Le_LinkCert="$(echo "$response" | tr -d '\r\n' | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" + Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n" | cut -d " " -f 2)" + if [ -z "$Le_LinkOrder" ]; then + _err "Sign error, can not get order link location header" + _err "responseHeaders" "$responseHeaders" + _on_issue_err "$_post_hook" + return 1 + fi + _savedomainconf "Le_LinkOrder" "$Le_LinkOrder" - _tempSignedResponse="$response" - if ! _send_signed_request "$Le_LinkCert" "" "needbase64"; then + _link_cert_retry=0 + _MAX_CERT_RETRY=5 + while [ -z "$Le_LinkCert" ] && [ "$_link_cert_retry" -lt "$_MAX_CERT_RETRY" ]; do + if _contains "$response" "\"status\":\"valid\""; then + _debug "Order status is valid." + Le_LinkCert="$(echo "$response" | tr -d '\r\n' | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" + _debug Le_LinkCert "$Le_LinkCert" + if [ -z "$Le_LinkCert" ]; then + _err "Sign error, can not find Le_LinkCert" + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + break + elif _contains "$response" "\"processing\""; then + _info "Order status is processing, lets sleep and retry." + _sleep 2 + else + _err "Sign error, wrong status" + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + if ! _send_signed_request "$Le_LinkOrder"; then + _err "Sign failed, can not post to Le_LinkOrder cert:$Le_LinkOrder." + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + _link_cert_retry="$(_math $_link_cert_retry + 1)" + done + + if [ -z "$Le_LinkCert" ]; then + _err "Sign failed, can not get Le_LinkCert, retry time limit." + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + _info "Download cert, Le_LinkCert: $Le_LinkCert" + if ! _send_signed_request "$Le_LinkCert"; then _err "Sign failed, can not download cert:$Le_LinkCert." _err "$response" _on_issue_err "$_post_hook" return 1 fi - echo "$response" | _dbase64 "multiline" >"$CERT_PATH" + echo "$response" >"$CERT_PATH" if [ "$(grep -- "$BEGIN_CERT" "$CERT_PATH" | wc -l)" -gt "1" ]; then _debug "Found cert chain" @@ -4131,7 +4297,7 @@ $_authorizations_map" _end_n="$(_math $_end_n + 1)" sed -n "${_end_n},9999p" "$CERT_FULLCHAIN_PATH" >"$CA_CERT_PATH" fi - response="$_tempSignedResponse" + else if ! _send_signed_request "${ACME_NEW_ORDER}" "{\"resource\": \"$ACME_NEW_ORDER_RES\", \"csr\": \"$der\"}" "needbase64"; then _err "Sign failed. $response" @@ -4720,7 +4886,7 @@ _installcert() { export CERT_KEY_PATH export CA_CERT_PATH export CERT_FULLCHAIN_PATH - export Le_Domain + export Le_Domain="$_main_domain" cd "$DOMAIN_PATH" && eval "$_reload_cmd" ); then _info "$(__green "Reload success")" diff --git a/deploy/README.md b/deploy/README.md index 091e9feb..44d53225 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -349,10 +349,10 @@ $ export QINIU_SK="bar" $ acme.sh --deploy -d example.com --deploy-hook qiniu ``` -假如您部署的证书为泛域名证书,您还需要设置 `QINIU_CDN_DOMAIN` 变量,指定实际需要部署的域名: +假如您部署的证书为泛域名证书,您还需要设置 `QINIU_CDN_DOMAIN` 变量,指定实际需要部署的域名(请注意泛域名前的点): ```sh -$ export QINIU_CDN_DOMAIN="cdn.example.com" +$ export QINIU_CDN_DOMAIN=".cdn.example.com" $ acme.sh --deploy -d example.com --deploy-hook qiniu ``` @@ -375,9 +375,19 @@ $ acme.sh --deploy -d example.com --deploy-hook qiniu (Optional), If you are using wildcard certificate, you may need export `QINIU_CDN_DOMAIN` to specify which domain -you want to update: +you want to update (please note the leading dot): ```sh -$ export QINIU_CDN_DOMAIN="cdn.example.com" +$ export QINIU_CDN_DOMAIN=".cdn.example.com" $ acme.sh --deploy -d example.com --deploy-hook qiniu ``` + +## 14. Deploy your cert on MyDevil.net + +Once you have acme.sh installed and certificate issued (see info in [DNS API](../dnsapi/README.md#61-use-mydevilnet)), you can install it by following command: + +```sh +acme.sh --deploy --deploy-hook mydevil -d example.com +``` + +That will remove old certificate and install new one. diff --git a/deploy/mydevil.sh b/deploy/mydevil.sh new file mode 100755 index 00000000..bd9868aa --- /dev/null +++ b/deploy/mydevil.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh + +# MyDevil.net API (2019-02-03) +# +# MyDevil.net already supports automatic Let's Encrypt certificates, +# except for wildcard domains. +# +# This script depends on `devil` command that MyDevil.net provides, +# which means that it works only on server side. +# +# Author: Marcin Konicki +# +######## Public functions ##################### + +# Usage: mydevil_deploy domain keyfile certfile cafile fullchain +mydevil_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + ip="" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + if ! _exists "devil"; then + _err "Could not find 'devil' command." + return 1 + fi + + ip=$(mydevil_get_ip "$_cdomain") + if [ -z "$ip" ]; then + _err "Could not find IP for domain $_cdomain." + return 1 + fi + + # Delete old certificate first + _info "Removing old certificate for $_cdomain at $ip" + devil ssl www del "$ip" "$_cdomain" + + # Add new certificate + _info "Adding new certificate for $_cdomain at $ip" + devil ssl www add "$ip" "$_cfullchain" "$_ckey" "$_cdomain" || return 1 + + return 0 +} + +#################### Private functions below ################################## + +# Usage: ip=$(mydevil_get_ip domain.com) +# echo $ip +mydevil_get_ip() { + devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1 + return 0 +} diff --git a/deploy/qiniu.sh b/deploy/qiniu.sh index 158b8dbf..e46e6fb3 100644 --- a/deploy/qiniu.sh +++ b/deploy/qiniu.sh @@ -87,6 +87,6 @@ qiniu_deploy() { } _make_access_token() { - _token="$(printf "%s\n" "$1" | _hmac "sha1" "$(printf "%s" "$QINIU_SK" | _hex_dump | tr -d " ")" | _base64)" + _token="$(printf "%s\n" "$1" | _hmac "sha1" "$(printf "%s" "$QINIU_SK" | _hex_dump | tr -d " ")" | _base64 | tr -- '+/' '-_')" echo "$QINIU_AK:$_token" } diff --git a/dnsapi/README.md b/dnsapi/README.md index a2bf0c18..4ace7c7b 100644 --- a/dnsapi/README.md +++ b/dnsapi/README.md @@ -1,6 +1,6 @@ # How to use DNS API -If your dns provider doesn't provide api access, you can use our dns alias mode: +If your dns provider doesn't provide api access, you can use our dns alias mode: https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode @@ -891,7 +891,7 @@ acme.sh --issue --dns dns_loopia -d example.com -d *.example.com The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed. ## 45. Use ACME DNS API -ACME DNS is a limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely. +ACME DNS is a limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely. https://github.com/joohoi/acme-dns ``` @@ -1011,7 +1011,6 @@ acme.sh --issue --dns dns_netcup -d example.com -d www.example.com ``` The `NC_Apikey`,`NC_Apipw` and `NC_CID` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - ## 52. Use GratisDNS.dk GratisDNS.dk (https://gratisdns.dk/) does not provide an API to update DNS records (other than IPv4 and IPv6 @@ -1172,7 +1171,115 @@ acme.sh --issue --dns dns_doapi -d example.com -d *.example.com The API token will be saved in `~/.acme.sh/account.conf` and will be reused when needed. -## 61. Use NederHost API +## 61. Use Nexcess API + +First, you'll need to login to the [Nexcess.net Client Portal](https://portal.nexcess.net) and [generate a new API token](https://portal.nexcess.net/api-token). + +Once you have a token, set it in your systems environment: + +``` +export NW_API_TOKEN="YOUR_TOKEN_HERE" +export NW_API_ENDPOINT="https://portal.nexcess.net" +``` + +Finally, we'll issue the certificate: (Nexcess DNS publishes at max every 15 minutes, we recommend setting a 900 second `--dnssleep`) + +``` +acme.sh --issue --dns dns_nw -d example.com --dnssleep 900 +``` + +The `NW_API_TOKEN` and `NW_API_ENDPOINT` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 62. Use Thermo.io API + +First, you'll need to login to the [Thermo.io Client Portal](https://core.thermo.io) and [generate a new API token](https://core.thermo.io/api-token). + +Once you have a token, set it in your systems environment: + +``` +export NW_API_TOKEN="YOUR_TOKEN_HERE" +export NW_API_ENDPOINT="https://core.thermo.io" +``` + +Finally, we'll issue the certificate: (Thermo DNS publishes at max every 15 minutes, we recommend setting a 900 second `--dnssleep`) + +``` +acme.sh --issue --dns dns_nw -d example.com --dnssleep 900 +``` + +The `NW_API_TOKEN` and `NW_API_ENDPOINT` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 63. Use Futurehosting API + +First, you'll need to login to the [Futurehosting Client Portal](https://my.futurehosting.com) and [generate a new API token](https://my.futurehosting.com/api-token). + +Once you have a token, set it in your systems environment: + +``` +export NW_API_TOKEN="YOUR_TOKEN_HERE" +export NW_API_ENDPOINT="https://my.futurehosting.com" +``` + +Finally, we'll issue the certificate: (Futurehosting DNS publishes at max every 15 minutes, we recommend setting a 900 second `--dnssleep`) + +``` +acme.sh --issue --dns dns_nw -d example.com --dnssleep 900 +``` + +The `NW_API_TOKEN` and `NW_API_ENDPOINT` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 64. Use Rackspace API + +Set username and API key, which is available under "My Profile & Settings" + +``` +export RACKSPACE_Username='username' +export RACKSPACE_Apikey='xxx' +``` + +Now, let's issue a cert: + +``` +acme.sh --issue --dns dns_rackspace -d example.com -d www.example.com +``` + +## 65. Use Online API + +First, you'll need to retrive your API key, which is available under https://console.online.net/en/api/access + +``` +export ONLINE_API_KEY='xxx' +``` + +To issue a cert run: + +``` +acme.sh --issue --dns dns_online -d example.com -d www.example.com +``` + +`ONLINE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 66. Use MyDevil.net + +Make sure that you can execute own binaries: + +```sh +devil binexec on +``` + +Install acme.sh, or simply `git clone` it into some directory on your MyDevil host account (in which case you should link to it from your `~/bin` directory). + +If you're not using private IP and depend on default IP provided by host, you may want to edit `crontab` too, and make sure that `acme.sh --cron` is run also after reboot (you can find out how to do that on their wiki pages). + +To issue a new certificate, run: + +```sh +acme.sh --issue --dns dns_mydevil -d example.com -d *.example.com +``` + +After certificate is ready, you can install it with [deploy command](../deploy/README.md#14-deploy-your-cert-on-mydevilnet). + +## 67. Use NederHost API Create an API token in Mijn NederHost. diff --git a/dnsapi/dns_hostingde.sh b/dnsapi/dns_hostingde.sh index b61acb7a..50aa142f 100644 --- a/dnsapi/dns_hostingde.sh +++ b/dnsapi/dns_hostingde.sh @@ -13,6 +13,7 @@ dns_hostingde_add() { txtvalue="${2}" _debug "Calling: _hostingde_addRecord() '${fulldomain}' '${txtvalue}'" _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_addRecord + return $? } dns_hostingde_rm() { @@ -20,6 +21,7 @@ dns_hostingde_rm() { txtvalue="${2}" _debug "Calling: _hostingde_removeRecord() '${fulldomain}' '${txtvalue}'" _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_removeRecord + return $? } #################### own Private functions below ################################## @@ -38,6 +40,18 @@ _hostingde_apiKey() { _saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT" } +_hostingde_parse() { + find="${1}" + if [ "${2}" ]; then + notfind="${2}" + fi + if [ "${notfind}" ]; then + _egrep_o \""${find}\":.*" | grep -v "${notfind}" | cut -d ':' -f 2 | cut -d ',' -f 1 | tr -d ' ' + else + _egrep_o \""${find}\":.*" | cut -d ':' -f 2 | cut -d ',' -f 1 | tr -d ' ' + fi +} + _hostingde_getZoneConfig() { _info "Getting ZoneConfig" curZone="${fulldomain#*.}" @@ -59,18 +73,18 @@ _hostingde_getZoneConfig() { if _contains "${curResult}" '"totalEntries": 1'; then _info "Retrieved zone data." _debug "Zone data: '${curResult}'" - zoneConfigId=$(echo "${curResult}" | _egrep_o '"id":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) - zoneConfigName=$(echo "${curResult}" | _egrep_o '"name":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) - zoneConfigType=$(echo "${curResult}" | grep -v "FindZoneConfigsResult" | _egrep_o '"type":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) - zoneConfigExpire=$(echo "${curResult}" | _egrep_o '"expire":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1) - zoneConfigNegativeTtl=$(echo "${curResult}" | _egrep_o '"negativeTtl":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1) - zoneConfigRefresh=$(echo "${curResult}" | _egrep_o '"refresh":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1) - zoneConfigRetry=$(echo "${curResult}" | _egrep_o '"retry":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1) - zoneConfigTtl=$(echo "${curResult}" | _egrep_o '"ttl":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1) - zoneConfigDnsServerGroupId=$(echo "${curResult}" | _egrep_o '"dnsServerGroupId":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) - zoneConfigEmailAddress=$(echo "${curResult}" | _egrep_o '"emailAddress":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) - zoneConfigDnsSecMode=$(echo "${curResult}" | _egrep_o '"dnsSecMode":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) - if [ "${zoneConfigType}" != "NATIVE" ]; then + zoneConfigId=$(echo "${curResult}" | _hostingde_parse "id") + zoneConfigName=$(echo "${curResult}" | _hostingde_parse "name") + zoneConfigType=$(echo "${curResult}" | _hostingde_parse "type" "FindZoneConfigsResult") + zoneConfigExpire=$(echo "${curResult}" | _hostingde_parse "expire") + zoneConfigNegativeTtl=$(echo "${curResult}" | _hostingde_parse "negativeTtl") + zoneConfigRefresh=$(echo "${curResult}" | _hostingde_parse "refresh") + zoneConfigRetry=$(echo "${curResult}" | _hostingde_parse "retry") + zoneConfigTtl=$(echo "${curResult}" | _hostingde_parse "ttl") + zoneConfigDnsServerGroupId=$(echo "${curResult}" | _hostingde_parse "dnsServerGroupId") + zoneConfigEmailAddress=$(echo "${curResult}" | _hostingde_parse "emailAddress") + zoneConfigDnsSecMode=$(echo "${curResult}" | _hostingde_parse "dnsSecMode") + if [ "${zoneConfigType}" != "\"NATIVE\"" ]; then _err "Zone is not native" returnCode=1 break @@ -89,11 +103,11 @@ _hostingde_getZoneConfig() { _hostingde_getZoneStatus() { _debug "Checking Zone status" - curData="{\"filter\":{\"field\":\"zoneConfigId\",\"value\":\"${zoneConfigId}\"},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}" + curData="{\"filter\":{\"field\":\"zoneConfigId\",\"value\":${zoneConfigId}},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}" curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind")" _debug "Calling zonesFind '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind'" _debug "Result of zonesFind '$curResult'" - zoneStatus=$(echo "${curResult}" | grep -v success | _egrep_o '"status":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) + zoneStatus=$(echo "${curResult}" | _hostingde_parse "status" "success") _debug "zoneStatus '${zoneStatus}'" return 0 } @@ -102,12 +116,12 @@ _hostingde_addRecord() { _info "Adding record to zone" _hostingde_getZoneStatus _debug "Result of zoneStatus: '${zoneStatus}'" - while [ "${zoneStatus}" != "active" ]; do + while [ "${zoneStatus}" != "\"active\"" ]; do _sleep 5 _hostingde_getZoneStatus _debug "Result of zoneStatus: '${zoneStatus}'" done - curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\",\"name\":\"${zoneConfigName}\",\"type\":\"${zoneConfigType}\",\"dnsServerGroupId\":\"${zoneConfigDnsServerGroupId}\",\"dnsSecMode\":\"${zoneConfigDnsSecMode}\",\"emailAddress\":\"${zoneConfigEmailAddress}\",\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}" + curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}" curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")" _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'" _debug "Result of zoneUpdate: '$curResult'" @@ -126,12 +140,12 @@ _hostingde_removeRecord() { _info "Removing record from zone" _hostingde_getZoneStatus _debug "Result of zoneStatus: '$zoneStatus'" - while [ "$zoneStatus" != "active" ]; do + while [ "$zoneStatus" != "\"active\"" ]; do _sleep 5 _hostingde_getZoneStatus _debug "Result of zoneStatus: '$zoneStatus'" done - curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\",\"name\":\"${zoneConfigName}\",\"type\":\"${zoneConfigType}\",\"dnsServerGroupId\":\"${zoneConfigDnsServerGroupId}\",\"dnsSecMode\":\"${zoneConfigDnsSecMode}\",\"emailAddress\":\"${zoneConfigEmailAddress}\",\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}" + curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}" curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")" _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'" _debug "Result of zoneUpdate: '$curResult'" diff --git a/dnsapi/dns_mydevil.sh b/dnsapi/dns_mydevil.sh new file mode 100755 index 00000000..2f398959 --- /dev/null +++ b/dnsapi/dns_mydevil.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env sh + +# MyDevil.net API (2019-02-03) +# +# MyDevil.net already supports automatic Let's Encrypt certificates, +# except for wildcard domains. +# +# This script depends on `devil` command that MyDevil.net provides, +# which means that it works only on server side. +# +# Author: Marcin Konicki +# +######## Public functions ##################### + +#Usage: dns_mydevil_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_mydevil_add() { + fulldomain=$1 + txtvalue=$2 + domain="" + + if ! _exists "devil"; then + _err "Could not find 'devil' command." + return 1 + fi + + _info "Using mydevil" + + domain=$(mydevil_get_domain "$fulldomain") + if [ -z "$domain" ]; then + _err "Invalid domain name: could not find root domain of $fulldomain." + return 1 + fi + + # No need to check if record name exists, `devil` always adds new record. + # In worst case scenario, we end up with multiple identical records. + + _info "Adding $fulldomain record for domain $domain" + if devil dns add "$domain" "$fulldomain" TXT "$txtvalue"; then + _info "Successfully added TXT record, ready for validation." + return 0 + else + _err "Unable to add DNS record." + return 1 + fi +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_mydevil_rm() { + fulldomain=$1 + txtvalue=$2 + domain="" + + if ! _exists "devil"; then + _err "Could not find 'devil' command." + return 1 + fi + + _info "Using mydevil" + + domain=$(mydevil_get_domain "$fulldomain") + if [ -z "$domain" ]; then + _err "Invalid domain name: could not find root domain of $fulldomain." + return 1 + fi + + # catch one or more numbers + num='[0-9][0-9]*' + # catch one or more whitespace + w=$(printf '[\t ][\t ]*') + # catch anything, except newline + any='.*' + # filter to make sure we do not delete other records + validRecords="^${num}${w}${fulldomain}${w}TXT${w}${any}${txtvalue}$" + for id in $(devil dns list "$domain" | tail -n+2 | grep "${validRecords}" | cut -w -s -f 1); do + _info "Removing record $id from domain $domain" + devil dns del "$domain" "$id" || _err "Could not remove DNS record." + done +} + +#################### Private functions below ################################## + +# Usage: domain=$(mydevil_get_domain "_acme-challenge.www.domain.com" || _err "Invalid domain name") +# echo $domain +mydevil_get_domain() { + fulldomain=$1 + domain="" + + for domain in $(devil dns list | cut -w -s -f 1 | tail -n+2); do + if _endswith "$fulldomain" "$domain"; then + printf -- "%s" "$domain" + return 0 + fi + done + + return 1 +} diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh index fbf93c32..6553deb6 100755 --- a/dnsapi/dns_namecheap.sh +++ b/dnsapi/dns_namecheap.sh @@ -76,6 +76,22 @@ dns_namecheap_rm() { # _sub_domain=_acme-challenge.www # _domain=domain.com _get_root() { + fulldomain=$1 + + if ! _get_root_by_getList "$fulldomain"; then + _debug "Failed domain lookup via domains.getList api call. Trying domain lookup via domains.dns.getHosts api." + # The above "getList" api will only return hosts *owned* by the calling user. However, if the calling + # user is not the owner, but still has administrative rights, we must query the getHosts api directly. + # See this comment and the official namecheap response: http://disq.us/p/1q6v9x9 + if ! _get_root_by_getHosts "$fulldomain"; then + return 1 + fi + fi + + return 0 +} + +_get_root_by_getList() { domain=$1 if ! _namecheap_post "namecheap.domains.getList"; then @@ -94,6 +110,10 @@ _get_root() { #not valid return 1 fi + if ! _contains "$h" "\\."; then + #not valid + return 1 + fi if ! _contains "$response" "$h"; then _debug "$h not found" @@ -108,6 +128,31 @@ _get_root() { return 1 } +_get_root_by_getHosts() { + i=100 + p=99 + + while [ $p -ne 0 ]; do + + h=$(printf "%s" "$1" | cut -d . -f $i-100) + if [ -n "$h" ]; then + if _contains "$h" "\\."; then + _debug h "$h" + if _namecheap_set_tld_sld "$h"; then + _sub_domain=$(printf "%s" "$1" | cut -d . -f 1-$p) + _domain="$h" + return 0 + else + _debug "$h not found" + fi + fi + fi + i="$p" + p=$(_math "$p" - 1) + done + return 1 +} + _namecheap_set_publicip() { if [ -z "$NAMECHEAP_SOURCEIP" ]; then diff --git a/dnsapi/dns_nsone.sh b/dnsapi/dns_nsone.sh index 00e186d2..9a998341 100644 --- a/dnsapi/dns_nsone.sh +++ b/dnsapi/dns_nsone.sh @@ -46,7 +46,7 @@ dns_nsone_add() { if [ "$count" = "0" ]; then _info "Adding record" - if _nsone_rest PUT "zones/$_domain/$fulldomain/TXT" "{\"answers\":[{\"answer\":[\"$txtvalue\"]}],\"type\":\"TXT\",\"domain\":\"$fulldomain\",\"zone\":\"$_domain\"}"; then + if _nsone_rest PUT "zones/$_domain/$fulldomain/TXT" "{\"answers\":[{\"answer\":[\"$txtvalue\"]}],\"type\":\"TXT\",\"domain\":\"$fulldomain\",\"zone\":\"$_domain\",\"ttl\":0}"; then if _contains "$response" "$fulldomain"; then _info "Added" #todo: check if the record takes effect @@ -62,7 +62,7 @@ dns_nsone_add() { prev_txt=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain\",\"short_answers\":\[\"[^,]*\]" | _head_n 1 | cut -d: -f3 | cut -d, -f1) _debug "prev_txt" "$prev_txt" - _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}" + _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\",\"ttl\":0}" if [ "$?" = "0" ] && _contains "$response" "$fulldomain"; then _info "Updated!" #todo: check if the record takes effect diff --git a/dnsapi/dns_nw.sh b/dnsapi/dns_nw.sh new file mode 100644 index 00000000..c57d27c2 --- /dev/null +++ b/dnsapi/dns_nw.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env sh +######################################################################## +# NocWorx script for acme.sh +# +# Handles DNS Updates for the Following vendors: +# - Nexcess.net +# - Thermo.io +# - Futurehosting.com +# +# Environment variables: +# +# - NW_API_TOKEN (Your API Token) +# - NW_API_ENDPOINT (One of the following listed below) +# +# Endpoints: +# - https://portal.nexcess.net (default) +# - https://core.thermo.io +# - https://my.futurehosting.com +# +# Note: If you do not have an API token, one can be generated at one +# of the following URLs: +# - https://portal.nexcess.net/api-token +# - https://core.thermo.io/api-token +# - https://my.futurehosting.com/api-token +# +# Author: Frank Laszlo + +NW_API_VERSION="0" + +# dns_nw_add() - Add TXT record +# Usage: dns_nw_add _acme-challenge.subdomain.domain.com "XyZ123..." +dns_nw_add() { + host="${1}" + txtvalue="${2}" + + _debug host "${host}" + _debug txtvalue "${txtvalue}" + + if ! _check_nw_api_creds; then + return 1 + fi + + _info "Using NocWorx (${NW_API_ENDPOINT})" + _debug "Calling: dns_nw_add() '${host}' '${txtvalue}'" + + _debug "Detecting root zone" + if ! _get_root "${host}"; then + _err "Zone for domain does not exist." + return 1 + fi + _debug _zone_id "${_zone_id}" + _debug _sub_domain "${_sub_domain}" + _debug _domain "${_domain}" + + _post_data="{\"zone_id\": \"${_zone_id}\", \"type\": \"TXT\", \"host\": \"${host}\", \"target\": \"${txtvalue}\", \"ttl\": \"300\"}" + + if _rest POST "dns-record" "${_post_data}" && [ -n "${response}" ]; then + _record_id=$(printf "%s\n" "${response}" | _egrep_o "\"record_id\": *[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _record_id "${_record_id}" + + if [ -z "$_record_id" ]; then + _err "Error adding the TXT record." + return 1 + fi + + _info "TXT record successfully added." + return 0 + fi + + return 1 +} + +# dns_nw_rm() - Remove TXT record +# Usage: dns_nw_rm _acme-challenge.subdomain.domain.com "XyZ123..." +dns_nw_rm() { + host="${1}" + txtvalue="${2}" + + _debug host "${host}" + _debug txtvalue "${txtvalue}" + + if ! _check_nw_api_creds; then + return 1 + fi + + _info "Using NocWorx (${NW_API_ENDPOINT})" + _debug "Calling: dns_nw_rm() '${host}'" + + _debug "Detecting root zone" + if ! _get_root "${host}"; then + _err "Zone for domain does not exist." + return 1 + fi + _debug _zone_id "${_zone_id}" + _debug _sub_domain "${_sub_domain}" + _debug _domain "${_domain}" + + _parameters="?zone_id=${_zone_id}" + + if _rest GET "dns-record" "${_parameters}" && [ -n "${response}" ]; then + response="$(echo "${response}" | tr -d "\n" | sed 's/^\[\(.*\)\]$/\1/' | sed -e 's/{"record_id":/|"record_id":/g' | sed 's/|/&{/g' | tr "|" "\n")" + _debug response "${response}" + + record="$(echo "${response}" | _egrep_o "{.*\"host\": *\"${_sub_domain}\", *\"target\": *\"${txtvalue}\".*}")" + _debug record "${record}" + + if [ "${record}" ]; then + _record_id=$(printf "%s\n" "${record}" | _egrep_o "\"record_id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "${_record_id}" ]; then + _debug _record_id "${_record_id}" + + _rest DELETE "dns-record/${_record_id}" + + _info "TXT record successfully deleted." + return 0 + fi + + return 1 + fi + + return 0 + fi + + return 1 +} + +_check_nw_api_creds() { + NW_API_TOKEN="${NW_API_TOKEN:-$(_readaccountconf_mutable NW_API_TOKEN)}" + NW_API_ENDPOINT="${NW_API_ENDPOINT:-$(_readaccountconf_mutable NW_API_ENDPOINT)}" + + if [ -z "${NW_API_ENDPOINT}" ]; then + NW_API_ENDPOINT="https://portal.nexcess.net" + fi + + if [ -z "${NW_API_TOKEN}" ]; then + _err "You have not defined your NW_API_TOKEN." + _err "Please create your token and try again." + _err "If you need to generate a new token, please visit one of the following URLs:" + _err " - https://portal.nexcess.net/api-token" + _err " - https://core.thermo.io/api-token" + _err " - https://my.futurehosting.com/api-token" + + return 1 + fi + + _saveaccountconf_mutable NW_API_TOKEN "${NW_API_TOKEN}" + _saveaccountconf_mutable NW_API_ENDPOINT "${NW_API_ENDPOINT}" +} + +_get_root() { + domain="${1}" + i=2 + p=1 + + if _rest GET "dns-zone"; then + response="$(echo "${response}" | tr -d "\n" | sed 's/^\[\(.*\)\]$/\1/' | sed -e 's/{"zone_id":/|"zone_id":/g' | sed 's/|/&{/g' | tr "|" "\n")" + + _debug response "${response}" + while true; do + h=$(printf "%s" "${domain}" | cut -d . -f $i-100) + _debug h "${h}" + if [ -z "${h}" ]; then + #not valid + return 1 + fi + + hostedzone="$(echo "${response}" | _egrep_o "{.*\"domain\": *\"${h}\".*}")" + if [ "${hostedzone}" ]; then + _zone_id=$(printf "%s\n" "${hostedzone}" | _egrep_o "\"zone_id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "${_zone_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 + fi + return 1 +} + +_rest() { + method="${1}" + ep="/${2}" + data="${3}" + + _debug method "${method}" + _debug ep "${ep}" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + export _H3="Api-Version: ${NW_API_VERSION}" + export _H4="User-Agent: NW-ACME-CLIENT" + export _H5="Authorization: Bearer ${NW_API_TOKEN}" + + if [ "${method}" != "GET" ]; then + _debug data "${data}" + response="$(_post "${data}" "${NW_API_ENDPOINT}${ep}" "" "${method}")" + else + response="$(_get "${NW_API_ENDPOINT}${ep}${data}")" + fi + + if [ "${?}" != "0" ]; then + _err "error ${ep}" + return 1 + fi + _debug2 response "${response}" + return 0 +} diff --git a/dnsapi/dns_online.sh b/dnsapi/dns_online.sh new file mode 100755 index 00000000..9158c268 --- /dev/null +++ b/dnsapi/dns_online.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env sh + +# Online API +# https://console.online.net/en/api/ +# +# Requires Online API key set in ONLINE_API_KEY + +######## Public functions ##################### + +ONLINE_API="https://api.online.net/api/v1" + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_online_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _online_check_config; 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 _real_dns_version "$_real_dns_version" + + _info "Creating temporary zone version" + _online_create_temporary_zone_version + _info "Enabling temporary zone version" + _online_enable_zone "$_temporary_dns_version" + + _info "Adding record" + _online_create_TXT_record "$_real_dns_version" "$_sub_domain" "$txtvalue" + _info "Disabling temporary version" + _online_enable_zone "$_real_dns_version" + _info "Destroying temporary version" + _online_destroy_zone "$_temporary_dns_version" + + _info "Record added." + return 0 +} + +#fulldomain +dns_online_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _online_check_config; 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 _real_dns_version "$_real_dns_version" + + _debug "Getting txt records" + if ! _online_rest GET "domain/$_domain/version/active"; then + return 1 + fi + + rid=$(echo "$response" | _egrep_o "\"id\":[0-9]+,\"name\":\"$_sub_domain\",\"data\":\"\\\u0022$txtvalue\\\u0022\"" | cut -d ':' -f 2 | cut -d ',' -f 1) + _debug rid "$rid" + if [ -z "$rid" ]; then + return 1 + fi + + _info "Creating temporary zone version" + _online_create_temporary_zone_version + _info "Enabling temporary zone version" + _online_enable_zone "$_temporary_dns_version" + + _info "Removing DNS record" + _online_rest DELETE "domain/$_domain/version/$_real_dns_version/zone/$rid" + _info "Disabling temporary version" + _online_enable_zone "$_real_dns_version" + _info "Destroying temporary version" + _online_destroy_zone "$_temporary_dns_version" + + return 0 +} + +#################### Private functions below ################################## + +_online_check_config() { + ONLINE_API_KEY="${ONLINE_API_KEY:-$(_readaccountconf_mutable ONLINE_API_KEY)}" + if [ -z "$ONLINE_API_KEY" ]; then + _err "No API key specified for Online API." + _err "Create your key and export it as ONLINE_API_KEY" + return 1 + fi + if ! _online_rest GET "domain/"; then + _err "Invalid API key specified for Online API." + return 1 + fi + + _saveaccountconf_mutable ONLINE_API_KEY "$ONLINE_API_KEY" + + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _online_rest GET "domain/$h/version/active" + + if ! _contains "$response" "Domain not found" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + _real_dns_version=$(echo "$response" | _egrep_o '"uuid_ref":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + _err "Unable to retrive DNS zone matching this domain" + return 1 +} + +# this function create a temporary zone version +# as online.net does not allow updating an active version +_online_create_temporary_zone_version() { + + _online_rest POST "domain/$_domain/version" "name=acme.sh" + if [ "$?" != "0" ]; then + return 1 + fi + + _temporary_dns_version=$(echo "$response" | _egrep_o '"uuid_ref":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) + + # Creating a dummy record in this temporary version, because online.net doesn't accept enabling an empty version + _online_create_TXT_record "$_temporary_dns_version" "dummy.acme.sh" "dummy" + + return 0 +} + +_online_destroy_zone() { + version_id=$1 + _online_rest DELETE "domain/$_domain/version/$version_id" + + if [ "$?" != "0" ]; then + return 1 + fi + return 0 +} + +_online_enable_zone() { + version_id=$1 + _online_rest PATCH "domain/$_domain/version/$version_id/enable" + + if [ "$?" != "0" ]; then + return 1 + fi + return 0 +} + +_online_create_TXT_record() { + version=$1 + txt_name=$2 + txt_value=$3 + + _online_rest POST "domain/$_domain/version/$version/zone" "type=TXT&name=$txt_name&data=%22$txt_value%22&ttl=60&priority=0" + + # Note : the normal, expected response SHOULD be "Unknown method". + # this happens because the API HTTP response contains a Location: header, that redirect + # to an unknown online.net endpoint. + if [ "$?" != "0" ] || _contains "$response" "Unknown method" || _contains "$response" "\$ref"; then + return 0 + else + _err "error $response" + return 1 + fi +} + +_online_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + _online_url="$ONLINE_API/$ep" + _debug2 _online_url "$_online_url" + export _H1="Authorization: Bearer $ONLINE_API_KEY" + export _H2="X-Pretty-JSON: 1" + if [ "$data" ] || [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$_online_url" "" "$m")" + else + response="$(_get "$_online_url")" + fi + if [ "$?" != "0" ] || _contains "$response" "invalid_grant" || _contains "$response" "Method not allowed"; then + _err "error $response" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_rackspace.sh b/dnsapi/dns_rackspace.sh new file mode 100644 index 00000000..3939fd81 --- /dev/null +++ b/dnsapi/dns_rackspace.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env sh +# +# +#RACKSPACE_Username="" +# +#RACKSPACE_Apikey="" + +RACKSPACE_Endpoint="https://dns.api.rackspacecloud.com/v1.0" + +# 20190213 - The name & id fields swapped in the API response; fix sed +# 20190101 - Duplicating file for new pull request to dev branch +# Original - tcocca:rackspace_dnsapi https://github.com/Neilpang/acme.sh/pull/1297 + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_rackspace_add() { + fulldomain="$1" + _debug fulldomain="$fulldomain" + txtvalue="$2" + _debug txtvalue="$txtvalue" + _rackspace_check_auth || return 1 + _rackspace_check_rootzone || return 1 + _info "Creating TXT record." + if ! _rackspace_rest POST "$RACKSPACE_Tenant/domains/$_domain_id/records" "{\"records\":[{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":300}]}"; then + return 1 + fi + _debug2 response "$response" + if ! _contains "$response" "$txtvalue" >/dev/null; then + _err "Could not add TXT record." + return 1 + fi + return 0 +} + +#fulldomain txtvalue +dns_rackspace_rm() { + fulldomain=$1 + _debug fulldomain="$fulldomain" + txtvalue=$2 + _debug txtvalue="$txtvalue" + _rackspace_check_auth || return 1 + _rackspace_check_rootzone || return 1 + _info "Checking for TXT record." + if ! _get_recordid "$_domain_id" "$fulldomain" "$txtvalue"; then + _err "Could not get TXT record id." + return 1 + fi + if [ "$_dns_record_id" = "" ]; then + _err "TXT record not found." + return 1 + fi + _info "Removing TXT record." + if ! _delete_txt_record "$_domain_id" "$_dns_record_id"; then + _err "Could not remove TXT record $_dns_record_id." + 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_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 + if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains"; then + return 1 + fi + _debug2 response "$response" + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + # Response looks like: + # {"ttl":300,"accountId":12345,"id":1111111,"name":"example.com","emailAddress": ... + _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\([^,]*\),\"name\":\"$h\",.*/\1/p") + _debug2 domain_id "$_domain_id" + if [ -n "$_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 +} + +_get_recordid() { + domainid="$1" + fulldomain="$2" + txtvalue="$3" + if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains/$domainid/records?name=$fulldomain&type=TXT"; then + return 1 + fi + _debug response "$response" + if ! _contains "$response" "$txtvalue"; then + _dns_record_id=0 + return 0 + fi + _dns_record_id=$(echo "$response" | tr '{' "\n" | grep "\"data\":\"$txtvalue\"" | sed -n 's/^.*"id":"\([^"]*\)".*/\1/p') + _debug _dns_record_id "$_dns_record_id" + return 0 +} + +_delete_txt_record() { + domainid="$1" + _dns_record_id="$2" + if ! _rackspace_rest DELETE "$RACKSPACE_Tenant/domains/$domainid/records?id=$_dns_record_id"; then + return 1 + fi + _debug response "$response" + if ! _contains "$response" "RUNNING"; then + return 1 + fi + return 0 +} + +_rackspace_rest() { + m="$1" + ep="$2" + data="$3" + _debug ep "$ep" + export _H1="Accept: application/json" + export _H2="X-Auth-Token: $RACKSPACE_Token" + export _H3="X-Project-Id: $RACKSPACE_Tenant" + export _H4="Content-Type: application/json" + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$RACKSPACE_Endpoint/$ep" "" "$m")" + retcode=$? + else + _info "Getting $RACKSPACE_Endpoint/$ep" + response="$(_get "$RACKSPACE_Endpoint/$ep")" + retcode=$? + fi + + if [ "$retcode" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} + +_rackspace_authorization() { + export _H1="Content-Type: application/json" + data="{\"auth\":{\"RAX-KSKEY:apiKeyCredentials\":{\"username\":\"$RACKSPACE_Username\",\"apiKey\":\"$RACKSPACE_Apikey\"}}}" + _debug data "$data" + response="$(_post "$data" "https://identity.api.rackspacecloud.com/v2.0/tokens" "" "POST")" + retcode=$? + _debug2 response "$response" + if [ "$retcode" != "0" ]; then + _err "Authentication failed." + return 1 + fi + if _contains "$response" "token"; then + RACKSPACE_Token="$(echo "$response" | _normalizeJson | sed -n 's/^.*"token":{.*,"id":"\([^"]*\)",".*/\1/p')" + RACKSPACE_Tenant="$(echo "$response" | _normalizeJson | sed -n 's/^.*"token":{.*,"id":"\([^"]*\)"}.*/\1/p')" + _debug RACKSPACE_Token "$RACKSPACE_Token" + _debug RACKSPACE_Tenant "$RACKSPACE_Tenant" + fi + return 0 +} + +_rackspace_check_auth() { + # retrieve the rackspace creds + RACKSPACE_Username="${RACKSPACE_Username:-$(_readaccountconf_mutable RACKSPACE_Username)}" + RACKSPACE_Apikey="${RACKSPACE_Apikey:-$(_readaccountconf_mutable RACKSPACE_Apikey)}" + # check their vals for null + if [ -z "$RACKSPACE_Username" ] || [ -z "$RACKSPACE_Apikey" ]; then + RACKSPACE_Username="" + RACKSPACE_Apikey="" + _err "You didn't specify a Rackspace username and api key." + _err "Please set those values and try again." + return 1 + fi + # save the username and api key to the account conf file. + _saveaccountconf_mutable RACKSPACE_Username "$RACKSPACE_Username" + _saveaccountconf_mutable RACKSPACE_Apikey "$RACKSPACE_Apikey" + if [ -z "$RACKSPACE_Token" ]; then + _info "Getting authorization token." + if ! _rackspace_authorization; then + _err "Can not get token." + fi + fi +} + +_rackspace_check_rootzone() { + _debug "First detect the root zone" + if ! _get_root_zone "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" +}