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.

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