diff --git a/acme.sh b/acme.sh index 5ea331c9..2a3138cb 100755 --- a/acme.sh +++ b/acme.sh @@ -63,6 +63,7 @@ END_CSR="-----END CERTIFICATE REQUEST-----" BEGIN_CERT="-----BEGIN CERTIFICATE-----" END_CERT="-----END CERTIFICATE-----" +CONTENT_TYPE_JSON="application/jose+json" RENEW_SKIP=2 ECC_SEP="_" @@ -848,6 +849,16 @@ _dbase64() { fi } +#file +_checkcert() { + _cf="$1" + if [ "$DEBUG" ]; then + openssl x509 -noout -text -in "$_cf" + else + openssl x509 -noout -text -in "$_cf" >/dev/null 2>&1 + fi +} + #Usage: hashalg [outputhex] #Output Base64-encoded digest _digest() { @@ -1591,12 +1602,13 @@ _inithttp() { } -# body url [needbase64] [POST|PUT] +# body url [needbase64] [POST|PUT] [ContentType] _post() { body="$1" _post_url="$2" needbase64="$3" httpmethod="$4" + _postContentType="$5" if [ -z "$httpmethod" ]; then httpmethod="POST" @@ -1612,6 +1624,9 @@ _post() { if [ "$HTTPS_INSECURE" ]; then _CURL="$_CURL --insecure " fi + if [ "$_postContentType" ]; then + _CURL="$_CURL -H \"Content-Type: $_postContentType\" " + 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" "$_post_url" | _base64)" @@ -1634,15 +1649,31 @@ _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" "$_post_url" 2>"$HTTP_HEADER" | _base64)" + if [ "$_postContentType" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --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" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER" | _base64)" + fi else - 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)" + if [ "$_postContentType" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --method $httpmethod --body-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" "$_post_url" 2>"$HTTP_HEADER" | _base64)" + fi 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" "$_post_url" 2>"$HTTP_HEADER")" + if [ "$_postContentType" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --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" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" + fi else - 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")" + if [ "$_postContentType" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --method $httpmethod --body-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" "$_post_url" 2>"$HTTP_HEADER")" + fi fi fi _ret="$?" @@ -1765,7 +1796,7 @@ _send_signed_request() { if [ "$ACME_NEW_NONCE" ]; then _debug2 "Get nonce. ACME_NEW_NONCE" "$ACME_NEW_NONCE" nonceurl="$ACME_NEW_NONCE" - if _post "" "$nonceurl" "" "HEAD"; then + if _post "" "$nonceurl" "" "HEAD" "$CONTENT_TYPE_JSON"; then _headers="$(cat "$HTTP_HEADER")" fi fi @@ -1820,7 +1851,7 @@ _send_signed_request() { fi _debug3 body "$body" - response="$(_post "$body" "$url" "$needbase64")" + response="$(_post "$body" "$url" "$needbase64" "POST" "$CONTENT_TYPE_JSON")" _CACHED_NONCE="" if [ "$?" != "0" ]; then @@ -1841,7 +1872,7 @@ _send_signed_request() { _body="$response" if [ "$needbase64" ]; then - _body="$(echo "$_body" | _dbase64)" + _body="$(echo "$_body" | _dbase64 | tr -d '\0')" _debug3 _body "$_body" fi @@ -4068,6 +4099,13 @@ $_authorizations_map" _debug "Le_LinkCert" "$Le_LinkCert" _savedomainconf "Le_LinkCert" "$Le_LinkCert" + if [ -z "$Le_LinkCert" ] || ! _checkcert "$CERT_PATH"; then + response="$(echo "$response" | _dbase64 "multiline" | tr -d '\0' | _normalizeJson)" + _err "Sign failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" + _on_issue_err "$_post_hook" + return 1 + fi + if [ "$Le_LinkCert" ]; then _info "$(__green "Cert success.")" cat "$CERT_PATH" @@ -4078,28 +4116,18 @@ $_authorizations_map" _info "Your cert key is in $(__green " $CERT_KEY_PATH ")" fi - if [ "$ACME_VERSION" != "2" ]; then - cp "$CERT_PATH" "$CERT_FULLCHAIN_PATH" - fi - if [ ! "$USER_PATH" ] || [ ! "$IN_CRON" ]; then USER_PATH="$PATH" _saveaccountconf "USER_PATH" "$USER_PATH" fi fi - if [ -z "$Le_LinkCert" ]; then - response="$(echo "$response" | _dbase64 "multiline" | _normalizeJson)" - _err "Sign failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" - _on_issue_err "$_post_hook" - return 1 - fi - _cleardomainconf "Le_Vlist" if [ "$ACME_VERSION" = "2" ]; then _debug "v2 chain." else + cp "$CERT_PATH" "$CERT_FULLCHAIN_PATH" Le_LinkIssuer=$(grep -i '^Link' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | cut -d ';' -f 1 | tr -d '<>') if [ "$Le_LinkIssuer" ]; then @@ -4123,6 +4151,10 @@ $_authorizations_map" echo "$BEGIN_CERT" >"$CA_CERT_PATH" _base64 "multiline" <"$CA_CERT_PATH.der" >>"$CA_CERT_PATH" echo "$END_CERT" >>"$CA_CERT_PATH" + if !_checkcert "$CA_CERT_PATH"; then + _err "Can not get the ca cert." + break + fi cat "$CA_CERT_PATH" >>"$CERT_FULLCHAIN_PATH" rm -f "$CA_CERT_PATH.der" break @@ -5367,7 +5399,6 @@ Parameters: --webroot, -w /path/to/webroot Specifies the web root folder for web root mode. --standalone Use standalone mode. --stateless Use stateless mode, see: $_STATELESS_WIKI - --tls Use standalone tls mode. --apache Use apache mode. --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file] Use dns mode or dns api. --dnssleep [$DEFAULT_DNS_SLEEP] The time in seconds to wait for all the txt records to take effect in dns api mode. Default $DEFAULT_DNS_SLEEP seconds. @@ -5397,7 +5428,6 @@ Parameters: --accountkey Specifies the account key path, Only valid for the '--install' command. --days Specifies the days to renew the cert when using '--issue' command. The max value is $MAX_RENEW days. --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. - --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. --listraw Only used for '--list' command, list the certs in raw format. --stopRenewOnError, -se Only valid for '--renew-all' command. Stop if one cert has error in renewal. @@ -5748,14 +5778,6 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; - --tls) - wvalue="$W_TLS" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; --dns) wvalue="dns" if [ "$2" ] && ! _startswith "$2" "-"; then @@ -5851,12 +5873,6 @@ _process() { Le_HTTPPort="$_httpport" shift ;; - --tlsport) - _tlsport="$2" - Le_TLSPort="$_tlsport" - shift - ;; - --listraw) _listraw="raw" ;; diff --git a/dnsapi/dns_freedns.sh b/dnsapi/dns_freedns.sh index afd8b796..0d8fae73 100755 --- a/dnsapi/dns_freedns.sh +++ b/dnsapi/dns_freedns.sh @@ -53,8 +53,9 @@ dns_freedns_add() { i="$(_math "$i" - 1)" sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" - _debug top_domain "$top_domain" - _debug sub_domain "$sub_domain" + _debug "top_domain: $top_domain" + _debug "sub_domain: $sub_domain" + # Sometimes FreeDNS does not return the subdomain page but rather # returns a page regarding becoming a premium member. This usually # happens after a period of inactivity. Immediately trying again @@ -63,6 +64,7 @@ dns_freedns_add() { attempts=2 while [ "$attempts" -gt "0" ]; do attempts="$(_math "$attempts" - 1)" + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" if [ "$?" != "0" ]; then if [ "$using_cached_cookies" = "true" ]; then @@ -71,10 +73,9 @@ dns_freedns_add() { fi return 1 fi - _debug2 htmlpage "$htmlpage" subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '
' | sed 's//@/g' | tr '@' '\n' | grep edit.php | grep "$top_domain")" - _debug2 subdomain_csv "$subdomain_csv" + _debug3 "subdomain_csv: $subdomain_csv" # The above beauty ends with striping out rows that do not have an # href to edit.php and do not have the top domain we are looking for. @@ -85,55 +86,25 @@ dns_freedns_add() { lines="$(echo "$subdomain_csv" | wc -l)" i=0 found=0 + DNSdomainid="" while [ "$i" -lt "$lines" ]; do i="$(_math "$i" + 1)" line="$(echo "$subdomain_csv" | sed -n "${i}p")" - _debug2 line "$line" + _debug2 "line: $line" if [ $found = 0 ] && _contains "$line" "$top_domain"; then # this line will contain DNSdomainid for the top_domain DNSdomainid="$(echo "$line" | _egrep_o "edit_domain_id *= *.*>" | cut -d = -f 2 | cut -d '>' -f 1)" - _debug2 DNSdomainid "$DNSdomainid" + _debug2 "DNSdomainid: $DNSdomainid" found=1 - else - # lines contain DNS records for all subdomains - DNSname="$(echo "$line" | _egrep_o 'edit.php.*' | cut -d '>' -f 2 | cut -d '<' -f 1)" - _debug2 DNSname "$DNSname" - DNStype="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" - _debug2 DNStype "$DNStype" - if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then - DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)" - # Now get current value for the TXT record. This method may - # not produce accurate results as the value field is truncated - # on this webpage. To get full value we would need to load - # another page. However we don't really need this so long as - # there is only one TXT record for the acme challenge subdomain. - DNSvalue="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" - _debug2 DNSvalue "$DNSvalue" - if [ $found != 0 ]; then - break - # we are breaking out of the loop at the first match of DNS name - # and DNS type (if we are past finding the domainid). This assumes - # that there is only ever one TXT record for the LetsEncrypt/acme - # challenge subdomain. This seems to be a reasonable assumption - # as the acme client deletes the TXT record on successful validation. - fi - else - DNSname="" - DNStype="" - fi + break fi done - _debug "DNSname: $DNSname DNStype: $DNStype DNSdomainid: $DNSdomainid DNSdataid: $DNSdataid" - _debug "DNSvalue: $DNSvalue" - if [ -z "$DNSdomainid" ]; then # If domain ID is empty then something went wrong (top level # domain not found at FreeDNS). if [ "$attempts" = "0" ]; then # exhausted maximum retry attempts - _debug "$htmlpage" - _debug "$subdomain_csv" _err "Domain $top_domain not found at FreeDNS" return 1 fi @@ -145,33 +116,10 @@ dns_freedns_add() { _info "Retry loading subdomain page ($attempts attempts remaining)" done - if [ -z "$DNSdataid" ]; then - # If data ID is empty then specific subdomain does not exist yet, need - # to create it this should always be the case as the acme client - # deletes the entry after domain is validated. - _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" - return $? - else - if [ "$txtvalue" = "$DNSvalue" ]; then - # if value in TXT record matches value requested then DNS record - # does not need to be updated. But... - # Testing value match fails. Website is truncating the value field. - # So for now we will always go down the else path. Though in theory - # should never come here anyway as the acme client deletes - # the TXT record on successful validation, so we should not even - # have found a TXT record !! - _info "No update necessary for $fulldomain at FreeDNS" - return 0 - else - # Delete the old TXT record (with the wrong value) - if _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"; then - # And add in new TXT record with the value provided - _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" - fi - return $? - fi - fi - return 0 + # Add in new TXT record with the value provided + _debug "Adding TXT record for $fulldomain, $txtvalue" + _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" + return $? } #Usage: fulldomain txtvalue @@ -205,7 +153,7 @@ dns_freedns_rm() { fi subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '' | sed 's//@/g' | tr '@' '\n' | grep edit.php | grep "$fulldomain")" - _debug2 subdomain_csv "$subdomain_csv" + _debug3 "subdomain_csv: $subdomain_csv" # The above beauty ends with striping out rows that do not have an # href to edit.php and do not have the domain name we are looking for. @@ -216,35 +164,51 @@ dns_freedns_rm() { lines="$(echo "$subdomain_csv" | wc -l)" i=0 found=0 + DNSdataid="" while [ "$i" -lt "$lines" ]; do i="$(_math "$i" + 1)" line="$(echo "$subdomain_csv" | sed -n "${i}p")" - _debug2 line "$line" + _debug3 "line: $line" DNSname="$(echo "$line" | _egrep_o 'edit.php.*' | cut -d '>' -f 2 | cut -d '<' -f 1)" - _debug2 DNSname "$DNSname" - DNStype="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" - _debug2 DNStype "$DNStype" - if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then - DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)" - _debug2 DNSdataid "$DNSdataid" - DNSvalue="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" - _debug2 DNSvalue "$DNSvalue" - # if [ "$DNSvalue" = "$txtvalue" ]; then - # Testing value match fails. Website is truncating the value - # field. So for now we will assume that there is only one TXT - # field for the sub domain and just delete it. Currently this - # is a safe assumption. - _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" - return $? - # fi + _debug2 "DNSname: $DNSname" + if [ "$DNSname" = "$fulldomain" ]; then + DNStype="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" + _debug2 "DNStype: $DNStype" + if [ "$DNStype" = "TXT" ]; then + DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)" + _debug2 "DNSdataid: $DNSdataid" + DNSvalue="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" + if _startswith "$DNSvalue" """; then + # remove the quotation from the start + DNSvalue="$(echo "$DNSvalue" | cut -c 7-)" + fi + if _endswith "$DNSvalue" "..."; then + # value was truncated, remove the dot dot dot from the end + DNSvalue="$(echo "$DNSvalue" | sed 's/...$//')" + elif _endswith "$DNSvalue" """; then + # else remove the closing quotation from the end + DNSvalue="$(echo "$DNSvalue" | sed 's/......$//')" + fi + _debug2 "DNSvalue: $DNSvalue" + + if [ -n "$DNSdataid" ] && _startswith "$txtvalue" "$DNSvalue"; then + # Found a match. But note... Website is truncating the + # value field so we are only testing that part that is not + # truncated. This should be accurate enough. + _debug "Deleting TXT record for $fulldomain, $txtvalue" + _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" + return $? + fi + + fi fi done done # If we get this far we did not find a match (after two attempts) # Not necessarily an error, but log anyway. - _debug2 "$subdomain_csv" - _info "Cannot delete TXT record for $fulldomain/$txtvalue. Does not exist at FreeDNS" + _debug3 "$subdomain_csv" + _info "Cannot delete TXT record for $fulldomain, $txtvalue. Does not exist at FreeDNS" return 0 } @@ -272,7 +236,7 @@ _freedns_login() { # if cookies is not empty then logon successful if [ -z "$cookies" ]; then - _debug "$htmlpage" + _debug3 "htmlpage: $htmlpage" _err "FreeDNS login failed for user $username. Check $HTTP_HEADER file" return 1 fi @@ -301,7 +265,7 @@ _freedns_retrieve_subdomain_page() { return 1 fi - _debug2 "$htmlpage" + _debug3 "htmlpage: $htmlpage" printf "%s" "$htmlpage" return 0 @@ -323,17 +287,17 @@ _freedns_add_txt_record() { _err "FreeDNS failed to add TXT record for $subdomain bad RC from _post" return 1 elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then - _debug "$htmlpage" + _debug3 "htmlpage: $htmlpage" _err "FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file" return 1 elif _contains "$htmlpage" "security code was incorrect"; then - _debug "$htmlpage" + _debug3 "htmlpage: $htmlpage" _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested security code" _err "Note that you cannot use automatic DNS validation for FreeDNS public domains" return 1 fi - _debug2 "$htmlpage" + _debug3 "htmlpage: $htmlpage" _info "Added acme challenge TXT record for $fulldomain at FreeDNS" return 0 } @@ -352,7 +316,7 @@ _freedns_delete_txt_record() { _err "FreeDNS failed to delete TXT record for $data_id bad RC from _get" return 1 elif ! _contains "$htmlheader" "200 OK"; then - _debug "$htmlheader" + _debug2 "htmlheader: $htmlheader" _err "FreeDNS failed to delete TXT record $data_id" return 1 fi