You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
6.9 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. #!/usr/bin/env sh
  2. # shellcheck disable=SC2034
  3. dns_hetzner_info='Hetzner.com
  4. Site: Hetzner.com
  5. Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
  6. Options:
  7. HETZNER_Token API Token
  8. Issues: github.com/acmesh-official/acme.sh/issues/2943
  9. '
  10. HETZNER_Api="https://dns.hetzner.com/api/v1"
  11. ######## Public functions #####################
  12. # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
  13. # Used to add txt record
  14. # Ref: https://dns.hetzner.com/api-docs/
  15. dns_hetzner_add() {
  16. full_domain=$1
  17. txt_value=$2
  18. HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
  19. if [ -z "$HETZNER_Token" ]; then
  20. HETZNER_Token=""
  21. _err "You didn't specify a Hetzner api token."
  22. _err "You can get yours from here https://dns.hetzner.com/settings/api-token."
  23. return 1
  24. fi
  25. #save the api key and email to the account conf file.
  26. _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
  27. _debug "First detect the root zone"
  28. if ! _get_root "$full_domain"; then
  29. _err "Invalid domain"
  30. return 1
  31. fi
  32. _debug _domain_id "$_domain_id"
  33. _debug _sub_domain "$_sub_domain"
  34. _debug _domain "$_domain"
  35. _debug "Getting TXT records"
  36. if ! _find_record "$_sub_domain" "$txt_value"; then
  37. return 1
  38. fi
  39. if [ -z "$_record_id" ]; then
  40. _info "Adding record"
  41. if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
  42. if _contains "$response" "$txt_value"; then
  43. _info "Record added, OK"
  44. _sleep 2
  45. return 0
  46. fi
  47. fi
  48. _err "Add txt record error${_response_error}"
  49. return 1
  50. else
  51. _info "Found record id: $_record_id."
  52. _info "Record found, do nothing."
  53. return 0
  54. # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
  55. #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
  56. # if _contains "$response" "$txt_value"; then
  57. # _info "Modified, OK"
  58. # return 0
  59. # fi
  60. #fi
  61. #_err "Add txt record error (modify)."
  62. #return 1
  63. fi
  64. }
  65. # Usage: full_domain txt_value
  66. # Used to remove the txt record after validation
  67. dns_hetzner_rm() {
  68. full_domain=$1
  69. txt_value=$2
  70. HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
  71. _debug "First detect the root zone"
  72. if ! _get_root "$full_domain"; then
  73. _err "Invalid domain"
  74. return 1
  75. fi
  76. _debug _domain_id "$_domain_id"
  77. _debug _sub_domain "$_sub_domain"
  78. _debug _domain "$_domain"
  79. _debug "Getting TXT records"
  80. if ! _find_record "$_sub_domain" "$txt_value"; then
  81. return 1
  82. fi
  83. if [ -z "$_record_id" ]; then
  84. _info "Remove not needed. Record not found."
  85. else
  86. if ! _hetzner_rest DELETE "records/$_record_id"; then
  87. _err "Delete record error${_response_error}"
  88. return 1
  89. fi
  90. _sleep 2
  91. _info "Record deleted"
  92. fi
  93. }
  94. #################### Private functions below ##################################
  95. #returns
  96. # _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error
  97. _find_record() {
  98. unset _record_id
  99. _record_name=$1
  100. _record_value=$2
  101. if [ -z "$_record_value" ]; then
  102. _record_value='[^"]*'
  103. fi
  104. _debug "Getting all records"
  105. _hetzner_rest GET "records?zone_id=${_domain_id}"
  106. if _response_has_error; then
  107. _err "Error${_response_error}"
  108. return 1
  109. else
  110. _record_id=$(
  111. echo "$response" |
  112. grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" |
  113. grep "\"value\":\"$_record_value\"" |
  114. while read -r record; do
  115. # test for type and
  116. if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
  117. echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
  118. break
  119. fi
  120. done
  121. )
  122. fi
  123. }
  124. #_acme-challenge.www.domain.com
  125. #returns
  126. # _sub_domain=_acme-challenge.www
  127. # _domain=domain.com
  128. # _domain_id=sdjkglgdfewsdfg
  129. _get_root() {
  130. domain=$1
  131. i=1
  132. p=1
  133. domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
  134. domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
  135. _debug "Reading zone_id for '$domain_without_acme' from config..."
  136. HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
  137. if [ "$HETZNER_Zone_ID" ]; then
  138. _debug "Found, using: $HETZNER_Zone_ID"
  139. if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
  140. _debug "Zone with id '$HETZNER_Zone_ID' does not exist."
  141. _cleardomainconf "$domain_param_name"
  142. unset HETZNER_Zone_ID
  143. else
  144. if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
  145. _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
  146. if [ "$_domain" ]; then
  147. _cut_length=$((${#domain} - ${#_domain} - 1))
  148. _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
  149. _domain_id="$HETZNER_Zone_ID"
  150. return 0
  151. else
  152. return 1
  153. fi
  154. else
  155. return 1
  156. fi
  157. fi
  158. fi
  159. _debug "Trying to get zone id by domain name for '$domain_without_acme'."
  160. while true; do
  161. h=$(printf "%s" "$domain" | cut -d . -f $i-100)
  162. if [ -z "$h" ]; then
  163. #not valid
  164. return 1
  165. fi
  166. _debug h "$h"
  167. _hetzner_rest GET "zones?name=$h"
  168. if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
  169. _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
  170. if [ "$_domain_id" ]; then
  171. _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
  172. _domain=$h
  173. HETZNER_Zone_ID=$_domain_id
  174. _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
  175. return 0
  176. fi
  177. return 1
  178. fi
  179. p=$i
  180. i=$(_math "$i" + 1)
  181. done
  182. return 1
  183. }
  184. #returns
  185. # _response_error
  186. _response_has_error() {
  187. unset _response_error
  188. err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')"
  189. if [ -n "$err_part" ]; then
  190. err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
  191. err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
  192. if [ -n "$err_code" ] && [ -n "$err_message" ]; then
  193. _response_error=" - message: ${err_message}, code: ${err_code}"
  194. return 0
  195. fi
  196. fi
  197. return 1
  198. }
  199. #returns
  200. # response
  201. _hetzner_rest() {
  202. m=$1
  203. ep="$2"
  204. data="$3"
  205. _debug "$ep"
  206. key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
  207. export _H1="Content-TType: application/json"
  208. export _H2="Auth-API-Token: $key_trimmed"
  209. if [ "$m" != "GET" ]; then
  210. _debug data "$data"
  211. response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
  212. else
  213. response="$(_get "$HETZNER_Api/$ep")"
  214. fi
  215. if [ "$?" != "0" ] || _response_has_error; then
  216. _debug "Error$_response_error"
  217. return 1
  218. fi
  219. _debug2 response "$response"
  220. return 0
  221. }