#!/usr/bin/env sh # shellcheck disable=SC2034 dns_netcup_info='netcup.eu Domains: netcup.de netcup.net Site: netcup.eu/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_netcup Options: NC_Apikey API Key. The new netcup REST API key (64 characters) or the legacy CCP API key NC_Apipw API Password. Only used for the legacy CCP API NC_CID Customer Number. Only used for the legacy CCP API NC_Apikey_Legacy Legacy CCP API Key. Only used for domains not manageable via the REST API when NC_Apikey holds a new netcup REST API key. Optional. Author: linux-insideDE ' NC_Apikey="${NC_Apikey:-$(_readaccountconf_mutable NC_Apikey)}" NC_Apipw="${NC_Apipw:-$(_readaccountconf_mutable NC_Apipw)}" NC_CID="${NC_CID:-$(_readaccountconf_mutable NC_CID)}" NC_Apikey_Legacy="${NC_Apikey_Legacy:-$(_readaccountconf_mutable NC_Apikey_Legacy)}" end="https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" _nc_endrest="https://api.netcup.com/v1" client="" dns_netcup_add() { fulldomain=$1 txtvalue=$2 _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" if ! _nc_check_credentials; then return 1 fi _saveaccountconf_mutable NC_Apikey "$NC_Apikey" if [ -n "$NC_Apipw" ]; then _saveaccountconf_mutable NC_Apipw "$NC_Apipw" fi if [ -n "$NC_CID" ]; then _saveaccountconf_mutable NC_CID "$NC_CID" fi if [ -n "$NC_Apikey_Legacy" ]; then _saveaccountconf_mutable NC_Apikey_Legacy "$NC_Apikey_Legacy" fi if _nc_is_rest_key; then _nc_rest_add "$fulldomain" "$txtvalue" else _nc_apikey="$NC_Apikey" _nc_legacy_add "$fulldomain" "$txtvalue" fi } dns_netcup_rm() { fulldomain=$1 txtvalue=$2 _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" if ! _nc_check_credentials; then return 1 fi if _nc_is_rest_key; then _nc_rest_rm "$fulldomain" "$txtvalue" else _nc_apikey="$NC_Apikey" _nc_legacy_rm "$fulldomain" "$txtvalue" fi } #################### New netcup REST API (api.netcup.com) #################### _nc_rest_add() { fulldomain=$1 txtvalue=$2 if ! _nc_rest_get_domain "$fulldomain"; then return 1 fi _debug _domain_id "$_domain_id" _debug _dns_managed "$_dns_managed" if [ "$_dns_managed" = "false" ]; then _nc_rest_use_legacy "$_domain" || return 1 _nc_legacy_add "$fulldomain" "$txtvalue" return fi if [ "$_dns_managed" != "true" ]; then _err "Unable to read isDnsManaged for $_domain from the netcup REST API response: $response" return 1 fi case "$fulldomain" in _acme-challenge.*) ;; acmetestXyzRandomName.*) # The synthetic record of the DNS-API-Test, which expects add and # rm to succeed. The REST API can only manage _acme-challenge # records, so skip it. Real records are never treated as a no-op. _info "Skipping the DNS-API-Test record $fulldomain, the netcup REST API can only manage _acme-challenge records." return 0 ;; *) # e.g. a challenge alias given in the "=" form without the prefix if [ -n "$NC_Apikey_Legacy" ] && [ -n "$NC_Apipw" ] && [ -n "$NC_CID" ]; then _debug "The netcup REST API can only create _acme-challenge records, using the legacy CCP API for $fulldomain" _nc_apikey="$NC_Apikey_Legacy" _nc_legacy_add "$fulldomain" "$txtvalue" return fi _err "The netcup REST API can only create _acme-challenge records, unable to create $fulldomain." _err "Set NC_Apikey_Legacy, NC_Apipw and NC_CID to manage it via the legacy CCP API." return 1 ;; esac _nc_rest_get_scope "$fulldomain" "$_domain" _debug _scope "$_scope" if ! _nc_rest POST "domain/$_domain_id/acme/challenge" "{\"scope\": \"$_scope\", \"value\": \"$txtvalue\"}" || ! _contains "$response" '"success": *true'; then _err "Unable to add the challenge record: $response" return 1 fi # The challenge record is added to the zone right away, but deploying # the zone to the nameservers happens in the background, so poll until # the record has actually been deployed (up to about 60 seconds). _nc_tries=0 while true; do if _nc_rest GET "domain/$_domain_id/acme/challenge/$_scope/$txtvalue" && _contains "$response" '"status": *"deployed"'; then _info "The challenge record has been deployed" return 0 fi _nc_tries=$(_math "$_nc_tries" + 1) if [ "$_nc_tries" -ge 12 ]; then break fi _debug "The challenge record has not been deployed yet, waiting 5 more seconds" _sleep 5 done _info "The challenge record has still not been deployed after 60 seconds, continuing anyway" return 0 } _nc_rest_rm() { fulldomain=$1 txtvalue=$2 if ! _nc_rest_get_domain "$fulldomain"; then return 1 fi if [ "$_dns_managed" = "false" ]; then _nc_rest_use_legacy "$_domain" || return 1 _nc_legacy_rm "$fulldomain" "$txtvalue" return fi if [ "$_dns_managed" != "true" ]; then _err "Unable to read isDnsManaged for $_domain from the netcup REST API response: $response" return 1 fi case "$fulldomain" in _acme-challenge.*) ;; acmetestXyzRandomName.*) # See _nc_rest_add. _info "Skipping the DNS-API-Test record $fulldomain, the netcup REST API can only manage _acme-challenge records." return 0 ;; *) if [ -n "$NC_Apikey_Legacy" ] && [ -n "$NC_Apipw" ] && [ -n "$NC_CID" ]; then _debug "The netcup REST API can only remove _acme-challenge records, using the legacy CCP API for $fulldomain" _nc_apikey="$NC_Apikey_Legacy" _nc_legacy_rm "$fulldomain" "$txtvalue" return fi _err "The netcup REST API can only remove _acme-challenge records, unable to remove $fulldomain." _err "Set NC_Apikey_Legacy, NC_Apipw and NC_CID to manage it via the legacy CCP API." return 1 ;; esac _nc_rest_get_scope "$fulldomain" "$_domain" if ! _nc_rest DELETE "domain/$_domain_id/acme/challenge/$_scope/$txtvalue"; then _err "Unable to remove the challenge record: $response" return 1 fi _nc_status=$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') _debug _nc_status "$_nc_status" case "$_nc_status" in 204) return 0 ;; 404) _info "The challenge record was not found, nothing to remove" return 0 ;; *) _err "Unable to remove the challenge record: $response" return 1 ;; esac } # fulldomain # Sets _domain_id, _domain and _dns_managed of the domain the record # belongs to, walking up the name, longest match first. For a challenge # record the leftmost label is the prefix and can never be a zone, so # the walk starts one label in. Other names (e.g. a challenge alias in # the "=" form) may be a zone apex themselves. _nc_rest_get_domain() { case "$1" in _acme-challenge.*) i=2 ;; *) i=1 ;; esac while true; do h=$(printf "%s" "$1" | cut -d . -f "$i"-100) if [ -z "$h" ]; then _nc_nozone "$1" return 1 fi _debug h "$h" if ! _nc_rest GET "domain?fqdn=$h"; then return 1 fi if _contains "$response" '"success": *true'; then if _contains "$response" '"fqdn"'; then # split the response so that first/last match cannot differ # between the egrep and sed implementations of _egrep_o _domain_id=$(printf "%s" "$response" | tr '{,' '\n' | _egrep_o '"id": *[0-9][0-9]*' | _head_n 1 | tr -dc '0-9') _dns_managed=$(printf "%s" "$response" | tr '{,' '\n' | _egrep_o '"isDnsManaged": *[a-z][a-z]*' | _head_n 1 | sed 's/.*: *//') _domain="$h" if [ -n "$_domain_id" ]; then return 0 fi _err "Unable to parse the domain id from the netcup REST API response: $response" return 1 fi # an empty result, $h is not a domain of this account: walk on elif _contains "$response" '"code": *"resourceDoesNotExist"'; then # the API reports a domain that is not in this account with # success:false and this error code: walk on _debug "$h is not a domain of this account" else # e.g. an invalid API key; do not walk on, it would end in a # misleading "no zone found" error _err "The netcup REST API request failed: $response" _err "Note: NC_Apikey was detected as a netcup REST API key because it is 64 characters long." return 1 fi i=$(_math "$i" + 1) done } # fulldomain domain # Sets _scope to the host part of the challenge relative to the domain. # The REST API prepends _acme-challenge. to the scope itself, so the # prefix is stripped from the record name (the callers guarantee it is # present). _nc_rest_get_scope() { _scope="${1#_acme-challenge.}" if [ "$_scope" = "$2" ]; then _scope="@" else _scope="${_scope%".$2"}" fi } # domain # Selects the legacy credentials for a domain whose DNS cannot be # managed via the new netcup REST API. _nc_rest_use_legacy() { _debug "The DNS of $1 cannot be managed via the REST API, using the legacy CCP API" if [ -z "$NC_Apikey_Legacy" ] || [ -z "$NC_Apipw" ] || [ -z "$NC_CID" ]; then _err "The DNS of the domain $1 cannot be managed via the new netcup REST API." _err "Set NC_Apikey_Legacy, NC_Apipw and NC_CID to your legacy CCP API credentials to manage it." return 1 fi _nc_apikey="$NC_Apikey_Legacy" } # method endpoint [data] # The response is returned in the global variable $response. _nc_rest() { m=$1 ep=$2 data=$3 _debug2 "REST $m $ep" export _H1="Authorization: Bearer $NC_Apikey" # blank the remaining header slots so that auth headers of another # dns hook cannot ride into the netcup REST API in a multi-provider # issuance export _H2="" export _H3="" export _H4="" export _H5="" if [ "$m" = "GET" ]; then response=$(_get "$_nc_endrest/$ep") else _debug2 data "$data" response=$(_post "$data" "$_nc_endrest/$ep" "" "$m" "application/json") fi _nc_ret="$?" _debug2 response "$response" return "$_nc_ret" } #################### Legacy CCP API (ccp.netcup.net) #################### _nc_legacy_add() { fulldomain=$1 txtvalue=$2 if ! _nc_legacy_login; then return 1 fi domain="" exit=$(echo "$fulldomain" | tr -dc '.' | wc -c) exit=$(_math "$exit" + 1) i=$exit _nc_last=$(_nc_lastlevel "$i") _nc_found="" while [ "$exit" -ge "$_nc_last" ] do tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") if [ "$(_math "$i" - "$exit")" -eq 0 ]; then domain="$tmp" else domain="$tmp.$domain" fi if [ "$(_math "$i" - "$exit")" -ge 1 ]; then msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"false\", \"state\": \"yes\"} ]}}}" "$end" "" "POST") _debug "$msg" if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then _err "$msg" return 1 else _nc_found=1 break fi fi fi exit=$(_math "$exit" - 1) done if [ -z "$_nc_found" ]; then _err "$msg" _nc_nozone "$fulldomain" return 1 fi _nc_legacy_logout } _nc_legacy_rm() { fulldomain=$1 txtvalue=$2 if ! _nc_legacy_login; then return 1 fi domain="" exit=$(echo "$fulldomain" | tr -dc '.' | wc -c) exit=$(_math "$exit" + 1) i=$exit rec="" _nc_last=$(_nc_lastlevel "$i") _nc_found="" while [ "$exit" -ge "$_nc_last" ] do tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") if [ "$(_math "$i" - "$exit")" -eq 0 ]; then domain="$tmp" else domain="$tmp.$domain" fi if [ "$(_math "$i" - "$exit")" -ge 1 ]; then msg=$(_post "{\"action\": \"infoDnsRecords\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\", \"domainname\": \"$domain\"}}" "$end" "" "POST") rec=$(echo "$msg" | sed 's/\[//g' | sed 's/\]//g' | sed 's/{\"serverrequestid\".*\"dnsrecords\"://g' | sed 's/},{/};{/g' | sed 's/{//g' | sed 's/}//g') _debug "$msg" if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then _err "$msg" return 1 else _nc_found=1 break fi fi fi exit=$(_math "$exit" - 1) done if [ -z "$_nc_found" ]; then _err "$msg" _nc_nozone "$fulldomain" return 1 fi ida=0000 idv=0001 ids=0000000000 i=1 while [ "$i" -ne 0 ] do specrec=$(_getfield "$rec" "$i" ";") idv="$ida" ida=$(_getfield "$specrec" "1" "," | sed 's/\"id\":\"//g' | sed 's/\"//g') txtv=$(_getfield "$specrec" "5" "," | sed 's/\"destination\":\"//g' | sed 's/\"//g') i=$(_math "$i" + 1) if [ "$txtvalue" = "$txtv" ]; then i=0 ids="$ida" fi if [ "$ida" = "$idv" ]; then i=0 fi done msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"$ids\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"TRUE\", \"state\": \"yes\"} ]}}}" "$end" "" "POST") _debug "$msg" if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then _err "$msg" return 1 fi _nc_legacy_logout } _nc_legacy_login() { # never send the REST API Bearer header (or auth headers of another # dns hook) to the legacy CCP API export _H1="" export _H2="" export _H3="" export _H4="" export _H5="" tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4) _debug "$tmp" if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then _err "$tmp" return 1 fi } _nc_legacy_logout() { tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") _debug "$tmp" if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then _err "$tmp" return 1 fi } #################### Shared helpers #################### _nc_check_credentials() { if [ -z "$NC_Apikey" ]; then _err "No Credentials given" _err "Set NC_Apikey to your netcup REST API key (64 characters) or your legacy CCP API key." return 1 fi if ! _nc_is_rest_key; then if [ -z "$NC_Apipw" ] || [ -z "$NC_CID" ]; then _err "No Credentials given" _err "The legacy CCP API needs NC_Apikey, NC_Apipw and NC_CID." return 1 fi fi } # New netcup REST API keys are 64 characters long, legacy CCP API keys # are 50, so the key length selects the API. _nc_is_rest_key() { [ "${#NC_Apikey}" -eq 64 ] } # The legacy zone lookup walks the challenge name from the right, one # label at a time. The leftmost label is the challenge prefix, so the # full name itself can never be a zone: asking netcup for it only # returns 4013 "Validation Error", which would then mask the real 5028 # "zone could not be found". Stop one label short, unless the name is # too short to have a challenge prefix at all (manual invocation). # levels _nc_lastlevel() { if [ "$1" -ge 3 ]; then echo 2 else echo 1 fi } # fulldomain _nc_nozone() { _err "No DNS zone for $1 was found at netcup." _err "Check that the domain belongs to the account of the configured credentials and that its DNS is hosted at netcup." }