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.

347 lines
9.2 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. _cleanup
  40. return 0
  41. }
  42. dns_cyon_rm() {
  43. _load_credentials
  44. _load_parameters "$@"
  45. _info_header "delete"
  46. _login
  47. _domain_env
  48. _delete_txt
  49. _cleanup
  50. return 0
  51. }
  52. #########################
  53. ### PRIVATE FUNCTIONS ###
  54. #########################
  55. _load_credentials() {
  56. # Convert loaded password to/from base64 as needed.
  57. if [ "${cyon_password_b64}" ]; then
  58. cyon_password="$(echo "${cyon_password_b64}" | _dbase64)"
  59. elif [ "${cyon_password}" ]; then
  60. cyon_password_b64="$(echo "${cyon_password}" | _base64)"
  61. fi
  62. if [ -z "${cyon_username}" ] || [ -z "${cyon_password}" ]; then
  63. cyon_username=""
  64. cyon_password=""
  65. cyon_otp_secret=""
  66. _err ""
  67. _err "You haven't set your cyon.ch login credentials yet."
  68. _err "Please set the required cyon environment variables."
  69. _err ""
  70. exit 1
  71. fi
  72. # Save the login credentials to the account.conf file.
  73. _debug "Save credentials to account.conf"
  74. _saveaccountconf cyon_username "${cyon_username}"
  75. _saveaccountconf cyon_password_b64 "$cyon_password_b64"
  76. if [ ! -z "${cyon_otp_secret}" ]; then
  77. _saveaccountconf cyon_otp_secret "$cyon_otp_secret"
  78. fi
  79. }
  80. _is_idn() {
  81. _idn_temp=$(printf "%s" "$1" | tr -d "[0-9a-zA-Z.,-]")
  82. _idn_temp2="$(printf "%s" "$1" | grep -o "xn--")"
  83. [ "$_idn_temp" ] || [ "$_idn_temp2" ]
  84. }
  85. _load_parameters() {
  86. # Read the required parameters to add the TXT entry.
  87. fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
  88. fulldomain_idn="${fulldomain}"
  89. # Special case for IDNs, as cyon needs a domain environment change,
  90. # which uses the "pretty" instead of the punycode version.
  91. if _is_idn "$1"; then
  92. if ! _exists idn; then
  93. _fail "Please install idn to process IDN names."
  94. fi
  95. fulldomain="$(idn -u "${fulldomain}")"
  96. fulldomain_idn="$(idn -a "${fulldomain}")"
  97. fi
  98. _debug fulldomain "$fulldomain"
  99. _debug fulldomain_idn "$fulldomain_idn"
  100. txtvalue="$2"
  101. _debug txtvalue "$txtvalue"
  102. # Cookiejar required for login session, as cyon.ch has no official API (yet).
  103. cookiejar=$(tempfile)
  104. _debug cookiejar "$cookiejar"
  105. }
  106. _info_header() {
  107. if [ "$1" = "add" ]; then
  108. _info ""
  109. _info "+---------------------------------------------+"
  110. _info "| Adding DNS TXT entry to your cyon.ch domain |"
  111. _info "+---------------------------------------------+"
  112. _info ""
  113. _info " * Full Domain: ${fulldomain}"
  114. _info " * TXT Value: ${txtvalue}"
  115. _info " * Cookie Jar: ${cookiejar}"
  116. _info ""
  117. elif [ "$1" = "delete" ]; then
  118. _info ""
  119. _info "+-------------------------------------------------+"
  120. _info "| Deleting DNS TXT entry from your cyon.ch domain |"
  121. _info "+-------------------------------------------------+"
  122. _info ""
  123. _info " * Full Domain: ${fulldomain}"
  124. _info " * Cookie Jar: ${cookiejar}"
  125. _info ""
  126. fi
  127. }
  128. _login() {
  129. _info " - Logging in..."
  130. login_response=$(curl \
  131. "https://my.cyon.ch/auth/index/dologin-async" \
  132. -s \
  133. -c "${cookiejar}" \
  134. -H "X-Requested-With: XMLHttpRequest" \
  135. --data-urlencode "username=${cyon_username}" \
  136. --data-urlencode "password=${cyon_password}" \
  137. --data-urlencode "pathname=/")
  138. _debug login_response "${login_response}"
  139. # Bail if login fails.
  140. if [ "$(echo "${login_response}" | _get_response_success)" != "success" ]; then
  141. _fail " $(echo "${login_response}" | _get_response_message)"
  142. fi
  143. _info " success"
  144. # NECESSARY!! Load the main page after login, before the OTP check.
  145. curl "https://my.cyon.ch/" -s --compressed -b "${cookiejar}" >/dev/null
  146. # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
  147. # 2FA authentication with OTP?
  148. if [ ! -z "${cyon_otp_secret}" ]; then
  149. _info " - Authorising with OTP code..."
  150. if ! _exists oathtool; then
  151. _fail "Please install oathtool to use 2 Factor Authentication."
  152. fi
  153. # Get OTP code with the defined secret.
  154. otp_code=$(oathtool --base32 --totp "${cyon_otp_secret}" 2>/dev/null)
  155. otp_response=$(curl \
  156. "https://my.cyon.ch/auth/multi-factor/domultifactorauth-async" \
  157. -s \
  158. --compressed \
  159. -b "${cookiejar}" \
  160. -c "${cookiejar}" \
  161. -H "X-Requested-With: XMLHttpRequest" \
  162. -d "totpcode=${otp_code}&pathname=%2F&rememberme=0")
  163. _debug otp_response "${otp_response}"
  164. # Bail if OTP authentication fails.
  165. if [ "$(echo "${otp_response}" | _get_response_success)" != "success" ]; then
  166. _fail " $(echo "${otp_response}" | _get_response_message)"
  167. fi
  168. _info " success"
  169. fi
  170. _info ""
  171. }
  172. _domain_env() {
  173. _info " - Changing domain environment..."
  174. # Get the "example.com" part of the full domain name.
  175. domain_env=$(echo "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')
  176. _debug "Changing domain environment to ${domain_env}"
  177. domain_env_response=$(curl \
  178. "https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/domain%3A${domain_env}" \
  179. -s \
  180. --compressed \
  181. -b "${cookiejar}" \
  182. -H "X-Requested-With: XMLHttpRequest")
  183. _debug domain_env_response "${domain_env_response}"
  184. _check_2fa_miss "${domain_env_response}"
  185. domain_env_success=$(echo "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)
  186. # Bail if domain environment change fails.
  187. if [ "${domain_env_success}" != "true" ]; then
  188. _fail " $(echo "${domain_env_response}" | _get_response_message)"
  189. fi
  190. _info " success"
  191. _info ""
  192. }
  193. _add_txt() {
  194. _info " - Adding DNS TXT entry..."
  195. addtxt_response=$(curl \
  196. "https://my.cyon.ch/domain/dnseditor/add-record-async" \
  197. -s \
  198. --compressed \
  199. -b "${cookiejar}" \
  200. -H "X-Requested-With: XMLHttpRequest" \
  201. -d "zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}")
  202. _debug addtxt_response "${addtxt_response}"
  203. _check_2fa_miss "${addtxt_response}"
  204. addtxt_message=$(echo "${addtxt_response}" | _get_response_message)
  205. addtxt_status=$(echo "${addtxt_response}" | _get_response_status)
  206. # Bail if adding TXT entry fails.
  207. if [ "${addtxt_status}" != "true" ]; then
  208. _fail " ${addtxt_message}"
  209. fi
  210. _info " success"
  211. _info ""
  212. }
  213. _delete_txt() {
  214. _info " - Deleting DNS TXT entry..."
  215. list_txt_response=$(curl \
  216. "https://my.cyon.ch/domain/dnseditor/list-async" \
  217. -s \
  218. -b "${cookiejar}" \
  219. --compressed \
  220. -H "X-Requested-With: XMLHttpRequest" | \
  221. sed -e 's/data-hash/\\ndata-hash/g')
  222. _debug list_txt_response "${list_txt_response}"
  223. _check_2fa_miss "${list_txt_response}"
  224. # Find and delete all acme challenge entries for the $fulldomain.
  225. _dns_entries=$(echo -e "$list_txt_response" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')
  226. echo "${_dns_entries}" | while read -r _hash _identifier; do
  227. dns_type="$(echo "$_identifier" | cut -d'|' -f1)"
  228. dns_domain="$(echo "$_identifier" | cut -d'|' -f2)"
  229. if [ "${dns_type}" != "TXT" ] || [ "${dns_domain}" != "${fulldomain_idn}." ]; then
  230. continue
  231. fi
  232. delete_txt_response=$(curl \
  233. "https://my.cyon.ch/domain/dnseditor/delete-record-async" \
  234. -s \
  235. --compressed \
  236. -b "${cookiejar}" \
  237. -H "X-Requested-With: XMLHttpRequest" \
  238. --data-urlencode "hash=${_hash}" \
  239. --data-urlencode "identifier=${_identifier}")
  240. _debug delete_txt_response "${delete_txt_response}"
  241. _check_2fa_miss "${delete_txt_response}"
  242. delete_txt_message=$(echo "${delete_txt_response}" | _get_response_message)
  243. delete_txt_status=$(echo "${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_2fa_miss() {
  264. # Did we miss the 2FA?
  265. if test "${1#*multi_factor_form}" != "$1"; then
  266. _fail " Missed OTP authentication!"
  267. fi
  268. }
  269. _fail() {
  270. _err "$1"
  271. _err ""
  272. _cleanup
  273. exit 1
  274. }
  275. _cleanup() {
  276. _info " - Cleanup."
  277. _debug "Remove cookie jar: ${cookiejar}"
  278. rm "${cookiejar}" 2>/dev/null
  279. _info ""
  280. }