diff --git a/README.md b/README.md index b5d5aff0..44b96604 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ You don't have to do anything manually! 1. DirectAdmin API 1. KingHost (https://www.kinghost.com.br/) 1. Zilore (https://zilore.com) - +1. Loopia.se API And: diff --git a/acme.sh b/acme.sh index da8e60c9..a5e4b391 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=2.7.8 +VER=2.7.9 PROJECT_NAME="acme.sh" diff --git a/dnsapi/README.md b/dnsapi/README.md index 045ed0e1..bc1919de 100644 --- a/dnsapi/README.md +++ b/dnsapi/README.md @@ -814,6 +814,28 @@ acme.sh --issue --dns dns_zilore -d example.com -d *.example.com The `Zilore_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. +## 44. Use Loopia.se API +User must provide login credentials to the Loopia API. +The user needs the following permissions: + +- addSubdomain +- updateZoneRecord +- getDomains +- removeSubdomain + +Set the login credentials: +``` +export LOOPIA_User="user@loopiaapi" +export LOOPIA_Password="password" +``` + +And to issue a cert: +``` +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. + # Use custom API If your API is not supported yet, you can write your own DNS API. diff --git a/dnsapi/dns_azure.sh b/dnsapi/dns_azure.sh index e0d9516f..c6893a0c 100644 --- a/dnsapi/dns_azure.sh +++ b/dnsapi/dns_azure.sh @@ -76,10 +76,10 @@ dns_azure_add() { values="{\"value\":[\"$txtvalue\"]}" timestamp="$(_time)" if [ "$_code" = "200" ]; then - vlist="$(echo "$response" | _egrep_o "\"value\"\s*:\s*\[\s*\"[^\"]*\"\s*]" | cut -d : -f 2 | tr -d "[]\"")" + vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"")" _debug "existing TXT found" _debug "$vlist" - existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\s*:\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")" + existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")" if [ -z "$existingts" ]; then # the record was not created by acme.sh. Copy the exisiting entires existingts=$timestamp @@ -172,7 +172,7 @@ dns_azure_rm() { _azure_rest GET "$acmeRecordURI" "" "$accesstoken" timestamp="$(_time)" if [ "$_code" = "200" ]; then - vlist="$(echo "$response" | _egrep_o "\"value\"\s*:\s*\[\s*\"[^\"]*\"\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")" + vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")" values="" comma="" for v in $vlist; do @@ -230,7 +230,7 @@ _azure_rest() { fi _ret="$?" _secure_debug2 "response $response" - _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" _debug "http response code $_code" if [ "$_code" = "401" ]; then # we have an invalid access token set to expired @@ -308,7 +308,7 @@ _get_root() { domain=$1 subscriptionId=$2 accesstoken=$3 - i=2 + i=1 p=1 ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list @@ -328,9 +328,14 @@ _get_root() { 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 \") + _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) + if [ "$i" = 1 ]; then + #create the record at the domain apex (@) if only the domain name was provided as --domain-alias + _sub_domain="@" + else + _sub_domain=$(echo "$domain" | cut -d . -f 1-$p) + fi _domain=$h return 0 fi diff --git a/dnsapi/dns_he.sh b/dnsapi/dns_he.sh index d196fbec..da4a1b81 100755 --- a/dnsapi/dns_he.sh +++ b/dnsapi/dns_he.sh @@ -33,8 +33,9 @@ dns_he_add() { # Fills in the $_zone_id _find_zone "$_full_domain" || return 1 _debug "Zone id \"$_zone_id\" will be used." - - body="email=${HE_Username}&pass=${HE_Password}" + username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)" + body="email=${username_encoded}&pass=${password_encoded}" body="$body&account=" body="$body&menu=edit_zone" body="$body&Type=TXT" @@ -71,7 +72,9 @@ dns_he_rm() { _debug "Zone id \"$_zone_id\" will be used." # Find the record id to clean - body="email=${HE_Username}&pass=${HE_Password}" + username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)" + body="email=${username_encoded}&pass=${password_encoded}" body="$body&hosted_dns_zoneid=$_zone_id" body="$body&menu=edit_zone" body="$body&hosted_dns_editzone=" @@ -112,9 +115,15 @@ dns_he_rm() { _find_zone() { _domain="$1" - body="email=${HE_Username}&pass=${HE_Password}" + username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)" + body="email=${username_encoded}&pass=${password_encoded}" response="$(_post "$body" "https://dns.he.net/")" _debug2 response "$response" + if _contains "$response" '>Incorrect<'; then + _err "Unable to login to dns.he.net please check username and password" + return 1 + fi _table="$(echo "$response" | tr -d "#" | sed "s/ + + removeSubdomain + + + %s + + + %s + + + %s + + + %s + + + ' $LOOPIA_User $LOOPIA_Password "$_domain" "$_sub_domain") + + response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" + + if ! _contains "$response" "OK"; then + _err "Error could not get txt records" + return 1 + fi +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + _debug "get root" + + domain=$1 + i=2 + p=1 + + xml_content=$(printf ' + + getDomains + + + %s + + + %s + + + ' $LOOPIA_User $LOOPIA_Password) + + response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" + while true; do + h=$(echo "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "$h"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 + +} + +_loopia_update_record() { + domain=$1 + sub_domain=$2 + txtval=$3 + + xml_content=$(printf ' + + updateZoneRecord + + + %s + + + %s + + + %s + + + %s + + + + + type + TXT + + + priority + 0 + + + ttl + 60 + + + rdata + %s + + + record_id + 0 + + + + + ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain" "$txtval") + + response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" + + if ! _contains "$response" "OK"; then + _err "Error" + return 1 + fi + return 0 +} + +_loopia_add_record() { + domain=$1 + sub_domain=$2 + + xml_content=$(printf ' + + addSubdomain + + + %s + + + %s + + + %s + + + %s + + + ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain") + + response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" + + if ! _contains "$response" "OK"; then + _err "Error" + return 1 + fi + return 0 +} diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh index 3d99e103..8f07e8c4 100755 --- a/dnsapi/dns_pdns.sh +++ b/dnsapi/dns_pdns.sh @@ -69,15 +69,21 @@ dns_pdns_add() { #fulldomain dns_pdns_rm() { fulldomain=$1 + txtvalue=$2 + + if [ -z "$PDNS_Ttl" ]; then + PDNS_Ttl="$DEFAULT_PDNS_TTL" + fi _debug "Detect root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" return 1 fi + _debug _domain "$_domain" - if ! rm_record "$_domain" "$fulldomain"; then + if ! rm_record "$_domain" "$fulldomain" "$txtvalue"; then return 1 fi @@ -88,9 +94,16 @@ set_record() { _info "Adding record" root=$1 full=$2 - txtvalue=$3 + new_challenge=$3 - if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [{\"name\": \"$full.\", \"type\": \"TXT\", \"content\": \"\\\"$txtvalue\\\"\", \"disabled\": false, \"ttl\": $PDNS_Ttl}]}]}"; then + _record_string="" + _build_record_string "$new_challenge" + _list_existingchallenges + for oldchallenge in $_existing_challenges; do + _build_record_string "$oldchallenge" + done + + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then _err "Set txt record error." return 1 fi @@ -106,14 +119,37 @@ rm_record() { _info "Remove record" root=$1 full=$2 + txtvalue=$3 - if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then - _err "Delete txt record error." - return 1 - fi + #Enumerate existing acme challenges + _list_existingchallenges - if ! notify_slaves "$root"; then - return 1 + if _contains "$_existing_challenges" "$txtvalue"; then + #Delete all challenges (PowerDNS API does not allow to delete content) + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then + _err "Delete txt record error." + return 1 + fi + _record_string="" + #If the only existing challenge was the challenge to delete: nothing to do + if ! [ "$_existing_challenges" = "$txtvalue" ]; then + for oldchallenge in $_existing_challenges; do + #Build up the challenges to re-add, ommitting the one what should be deleted + if ! [ "$oldchallenge" = "$txtvalue" ]; then + _build_record_string "$oldchallenge" + fi + done + #Recreate the existing challenges + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then + _err "Set txt record error." + return 1 + fi + fi + if ! notify_slaves "$root"; then + return 1 + fi + else + _info "Record not found, nothing to remove" fi return 0 @@ -185,3 +221,12 @@ _pdns_rest() { return 0 } + +_build_record_string() { + _record_string="${_record_string:+${_record_string}, }{\"content\": \"\\\"${1}\\\"\", \"disabled\": false}" +} + +_list_existingchallenges() { + _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones/$root" + _existing_challenges=$(echo "$response" | _normalizeJson | _egrep_o "\"name\":\"${fulldomain}[^]]*}" | _egrep_o 'content\":\"\\"[^\\]*' | sed -n 's/^content":"\\"//p') +}