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.

326 lines
9.7 KiB

  1. #!/usr/bin/env sh
  2. ########
  3. # Custom cyon.ch DNS API for use with [acme.sh](https://github.com/Neilpang/acme.sh)
  4. #
  5. # Usage: acme.sh --issue --dns dns_cyon -d www.domain.com
  6. #
  7. # Dependencies:
  8. # -------------
  9. # - oathtool (When using 2 Factor Authentication)
  10. #
  11. # Issues:
  12. # -------
  13. # Any issues / questions / suggestions can be posted here:
  14. # https://github.com/noplanman/cyon-api/issues
  15. #
  16. # Author: Armando Lüscher <armando@noplanman.ch>
  17. ########
  18. dns_cyon_add() {
  19. _cyon_load_credentials \
  20. && _cyon_load_parameters "$@" \
  21. && _cyon_print_header "add" \
  22. && _cyon_login \
  23. && _cyon_change_domain_env \
  24. && _cyon_add_txt \
  25. && _cyon_logout
  26. }
  27. dns_cyon_rm() {
  28. _cyon_load_credentials \
  29. && _cyon_load_parameters "$@" \
  30. && _cyon_print_header "delete" \
  31. && _cyon_login \
  32. && _cyon_change_domain_env \
  33. && _cyon_delete_txt \
  34. && _cyon_logout
  35. }
  36. #########################
  37. ### PRIVATE FUNCTIONS ###
  38. #########################
  39. _cyon_load_credentials() {
  40. # Convert loaded password to/from base64 as needed.
  41. if [ "${CY_Password_B64}" ]; then
  42. CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64 "multiline")"
  43. elif [ "${CY_Password}" ]; then
  44. CY_Password_B64="$(printf "%s" "${CY_Password}" | _base64)"
  45. fi
  46. if [ -z "${CY_Username}" ] || [ -z "${CY_Password}" ]; then
  47. # Dummy entries to satify script checker.
  48. CY_Username=""
  49. CY_Password=""
  50. CY_OTP_Secret=""
  51. _err ""
  52. _err "You haven't set your cyon.ch login credentials yet."
  53. _err "Please set the required cyon environment variables."
  54. _err ""
  55. return 1
  56. fi
  57. # Save the login credentials to the account.conf file.
  58. _debug "Save credentials to account.conf"
  59. _saveaccountconf CY_Username "${CY_Username}"
  60. _saveaccountconf CY_Password_B64 "$CY_Password_B64"
  61. if [ ! -z "${CY_OTP_Secret}" ]; then
  62. _saveaccountconf CY_OTP_Secret "$CY_OTP_Secret"
  63. else
  64. _clearaccountconf CY_OTP_Secret
  65. fi
  66. }
  67. _cyon_is_idn() {
  68. _idn_temp="$(printf "%s" "${1}" | tr -d "[0-9a-zA-Z.,-_]")"
  69. _idn_temp2="$(printf "%s" "${1}" | grep -o "xn--")"
  70. [ "$_idn_temp" ] || [ "$_idn_temp2" ]
  71. }
  72. # comment on https://stackoverflow.com/a/10797966
  73. _cyon_urlencode() {
  74. curl -Gso /dev/null -w "%{url_effective}" --data-urlencode @- "" | cut -c 3-
  75. }
  76. _cyon_load_parameters() {
  77. # Read the required parameters to add the TXT entry.
  78. fulldomain="$(printf "%s" "${1}" | tr '[A-Z]' '[a-z]')"
  79. fulldomain_idn="${fulldomain}"
  80. # Special case for IDNs, as cyon needs a domain environment change,
  81. # which uses the "pretty" instead of the punycode version.
  82. if _cyon_is_idn "${fulldomain}"; then
  83. if ! _exists idn; then
  84. _err "Please install idn to process IDN names."
  85. _err ""
  86. return 1
  87. fi
  88. fulldomain="$(idn -u "${fulldomain}")"
  89. fulldomain_idn="$(idn -a "${fulldomain}")"
  90. fi
  91. _debug fulldomain "${fulldomain}"
  92. _debug fulldomain_idn "${fulldomain_idn}"
  93. txtvalue="${2}"
  94. _debug txtvalue "${txtvalue}"
  95. # This header is required for curl calls.
  96. _H1="X-Requested-With: XMLHttpRequest"
  97. }
  98. _cyon_print_header() {
  99. if [ "${1}" = "add" ]; then
  100. _info ""
  101. _info "+---------------------------------------------+"
  102. _info "| Adding DNS TXT entry to your cyon.ch domain |"
  103. _info "+---------------------------------------------+"
  104. _info ""
  105. _info " * Full Domain: ${fulldomain}"
  106. _info " * TXT Value: ${txtvalue}"
  107. _info ""
  108. elif [ "${1}" = "delete" ]; then
  109. _info ""
  110. _info "+-------------------------------------------------+"
  111. _info "| Deleting DNS TXT entry from your cyon.ch domain |"
  112. _info "+-------------------------------------------------+"
  113. _info ""
  114. _info " * Full Domain: ${fulldomain}"
  115. _info ""
  116. fi
  117. }
  118. _cyon_get_cookie_header() {
  119. printf "Cookie: %s" "$(cat "$HTTP_HEADER" | grep "cyon=" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')"
  120. }
  121. _cyon_login() {
  122. _info " - Logging in..."
  123. username_encoded="$(printf "%s" "${CY_Username}" | _cyon_urlencode)"
  124. password_encoded="$(printf "%s" "${CY_Password}" | _cyon_urlencode)"
  125. login_url="https://my.cyon.ch/auth/index/dologin-async"
  126. login_data="$(printf "%s" "username=${username_encoded}&password=${password_encoded}&pathname=%2F")"
  127. login_response="$(_post "$login_data" "$login_url")"
  128. _debug login_response "${login_response}"
  129. # Bail if login fails.
  130. if [ "$(printf "%s" "${login_response}" | _cyon_get_response_success)" != "success" ]; then
  131. _err " $(printf "%s" "${login_response}" | _cyon_get_response_message)"
  132. _err ""
  133. return 1
  134. fi
  135. _info " success"
  136. # NECESSARY!! Load the main page after login, to get the new cookie.
  137. _H2="$(_cyon_get_cookie_header)"
  138. _get "https://my.cyon.ch/" >/dev/null
  139. # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
  140. # 2FA authentication with OTP?
  141. if [ ! -z "${CY_OTP_Secret}" ]; then
  142. _info " - Authorising with OTP code..."
  143. if ! _exists oathtool; then
  144. _err "Please install oathtool to use 2 Factor Authentication."
  145. _err ""
  146. return 1
  147. fi
  148. # Get OTP code with the defined secret.
  149. otp_code="$(oathtool --base32 --totp "${CY_OTP_Secret}" 2>/dev/null)"
  150. login_otp_url="https://my.cyon.ch/auth/multi-factor/domultifactorauth-async"
  151. login_otp_data="totpcode=${otp_code}&pathname=%2F&rememberme=0"
  152. login_otp_response="$(_post "$login_otp_data" "$login_otp_url")"
  153. _debug login_otp_response "${login_otp_response}"
  154. # Bail if OTP authentication fails.
  155. if [ "$(printf "%s" "${login_otp_response}" | _cyon_get_response_success)" != "success" ]; then
  156. _err " $(printf "%s" "${login_otp_response}" | _cyon_get_response_message)"
  157. _err ""
  158. return 1
  159. fi
  160. _info " success"
  161. fi
  162. _info ""
  163. }
  164. _cyon_logout() {
  165. _info " - Logging out..."
  166. _get "https://my.cyon.ch/auth/index/dologout" >/dev/null
  167. _info " success"
  168. _info ""
  169. }
  170. _cyon_change_domain_env() {
  171. _info " - Changing domain environment..."
  172. # Get the "example.com" part of the full domain name.
  173. domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')"
  174. _debug "Changing domain environment to ${domain_env}"
  175. domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/domain%3A${domain_env}"
  176. domain_env_response="$(_get "${domain_env_url}")"
  177. _debug domain_env_response "${domain_env_response}"
  178. if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi
  179. domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)"
  180. # Bail if domain environment change fails.
  181. if [ "${domain_env_success}" != "true" ]; then
  182. _err " $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)"
  183. _err ""
  184. return 1
  185. fi
  186. _info " success"
  187. _info ""
  188. }
  189. _cyon_add_txt() {
  190. _info " - Adding DNS TXT entry..."
  191. add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async"
  192. add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}"
  193. add_txt_response="$(_post "$add_txt_data" "$add_txt_url")"
  194. _debug add_txt_response "${add_txt_response}"
  195. if ! _cyon_check_if_2fa_missed "${add_txt_response}"; then return 1; fi
  196. add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)"
  197. add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)"
  198. # Bail if adding TXT entry fails.
  199. if [ "${add_txt_status}" != "true" ]; then
  200. _err " ${add_txt_message}"
  201. _err ""
  202. return 1
  203. fi
  204. _info " success (TXT|${fulldomain_idn}.|${txtvalue})"
  205. _info ""
  206. }
  207. _cyon_delete_txt() {
  208. _info " - Deleting DNS TXT entry..."
  209. list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
  210. list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
  211. _debug list_txt_response "${list_txt_response}"
  212. if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
  213. # Find and delete all acme challenge entries for the $fulldomain.
  214. _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')"
  215. printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do
  216. dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
  217. dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
  218. if [ "${dns_type}" != "TXT" ] || [ "${dns_domain}" != "${fulldomain_idn}." ]; then
  219. continue
  220. fi
  221. hash_encoded="$(printf "%s" "${_hash}" | _cyon_urlencode)"
  222. identifier_encoded="$(printf "%s" "${_identifier}" | _cyon_urlencode)"
  223. delete_txt_url="https://my.cyon.ch/domain/dnseditor/delete-record-async"
  224. delete_txt_data="$(printf "%s" "hash=${hash_encoded}&identifier=${identifier_encoded}")"
  225. delete_txt_response="$(_post "$delete_txt_data" "$delete_txt_url")"
  226. _debug delete_txt_response "${delete_txt_response}"
  227. if ! _cyon_check_if_2fa_missed "${delete_txt_response}"; then return 1; fi
  228. delete_txt_message="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_message)"
  229. delete_txt_status="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_status)"
  230. # Skip if deleting TXT entry fails.
  231. if [ "${delete_txt_status}" != "true" ]; then
  232. _err " ${delete_txt_message} (${_identifier})"
  233. else
  234. _info " success (${_identifier})"
  235. fi
  236. done
  237. _info " done"
  238. _info ""
  239. }
  240. _cyon_get_response_message() {
  241. _egrep_o '"message":"[^"]*"' | cut -d : -f 2 | tr -d '"'
  242. }
  243. _cyon_get_response_status() {
  244. _egrep_o '"status":\w*' | cut -d : -f 2
  245. }
  246. _cyon_get_response_success() {
  247. _egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"'
  248. }
  249. _cyon_check_if_2fa_missed() {
  250. # Did we miss the 2FA?
  251. if test "${1#*multi_factor_form}" != "${1}"; then
  252. _err " Missed OTP authentication!"
  253. _err ""
  254. return 1
  255. fi
  256. }