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.

355 lines
9.5 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. # - jq (get it here: https://stedolan.github.io/jq/download)
  10. # - oathtool (When using 2 Factor Authentication)
  11. #
  12. # Author: Armando Lüscher <armando@noplanman.ch>
  13. ########
  14. ########
  15. # Define cyon.ch login credentials:
  16. #
  17. # Either set them here: (uncomment these lines)
  18. #
  19. # cyon_username='your_cyon_username'
  20. # cyon_password='your_cyon_password'
  21. # cyon_otp_secret='your_otp_secret' # Only required if using 2FA
  22. #
  23. # ...or export them as environment variables in your shell:
  24. #
  25. # $ export cyon_username='your_cyon_username'
  26. # $ export cyon_password='your_cyon_password'
  27. # $ export cyon_otp_secret='your_otp_secret' # Only required if using 2FA
  28. #
  29. # *Note:*
  30. # After the first run, the credentials are saved in the "account.conf"
  31. # file, so any hard-coded or environment variables can then be removed.
  32. ########
  33. dns_cyon_add() {
  34. if ! _exists jq; then
  35. _fail "Please install jq to use cyon.ch DNS API."
  36. fi
  37. _load_credentials
  38. _load_parameters "$@"
  39. _info_header "add"
  40. _login
  41. _domain_env
  42. _add_txt
  43. _cleanup
  44. return 0
  45. }
  46. dns_cyon_rm() {
  47. _load_credentials
  48. _load_parameters "$@"
  49. _info_header "delete"
  50. _login
  51. _domain_env
  52. _delete_txt
  53. _cleanup
  54. return 0
  55. }
  56. #########################
  57. ### PRIVATE FUNCTIONS ###
  58. #########################
  59. _load_credentials() {
  60. # Convert loaded password to/from base64 as needed.
  61. if [ "${cyon_password_b64}" ] ; then
  62. cyon_password="$(echo "${cyon_password_b64}" | _dbase64)"
  63. elif [ "${cyon_password}" ] ; then
  64. cyon_password_b64="$(echo "${cyon_password}" | _base64)"
  65. fi
  66. if [ -z "${cyon_username}" ] || [ -z "${cyon_password}" ] ; then
  67. _err ""
  68. _err "You haven't set your cyon.ch login credentials yet."
  69. _err "Please set the required cyon environment variables."
  70. _err ""
  71. exit 1
  72. fi
  73. # Save the login credentials to the account.conf file.
  74. _debug "Save credentials to account.conf"
  75. _saveaccountconf cyon_username "${cyon_username}"
  76. _saveaccountconf cyon_password_b64 "$cyon_password_b64"
  77. if [ ! -z "${cyon_otp_secret}" ] ; then
  78. _saveaccountconf cyon_otp_secret "$cyon_otp_secret"
  79. fi
  80. }
  81. _is_idn() {
  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. _load_parameters() {
  87. # Read the required parameters to add the TXT entry.
  88. fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
  89. fulldomain_idn="${fulldomain}"
  90. # Special case for IDNs, as cyon needs a domain environment change,
  91. # which uses the "pretty" instead of the punycode version.
  92. if _is_idn "$1" ; then
  93. if ! _exists idn; then
  94. _fail "Please install idn to process IDN names."
  95. fi
  96. fulldomain="$(idn -u "${fulldomain}")"
  97. fulldomain_idn="$(idn -a "${fulldomain}")"
  98. fi
  99. _debug fulldomain "$fulldomain"
  100. _debug fulldomain_idn "$fulldomain_idn"
  101. txtvalue="$2"
  102. _debug txtvalue "$txtvalue"
  103. # Cookiejar required for login session, as cyon.ch has no official API (yet).
  104. cookiejar=$(tempfile)
  105. _debug cookiejar "$cookiejar"
  106. }
  107. _info_header() {
  108. if [ "$1" = "add" ]; then
  109. _info ""
  110. _info "+---------------------------------------------+"
  111. _info "| Adding DNS TXT entry to your cyon.ch domain |"
  112. _info "+---------------------------------------------+"
  113. _info ""
  114. _info " * Full Domain: ${fulldomain}"
  115. _info " * TXT Value: ${txtvalue}"
  116. _info " * Cookie Jar: ${cookiejar}"
  117. _info ""
  118. elif [ "$1" = "delete" ]; then
  119. _info ""
  120. _info "+-------------------------------------------------+"
  121. _info "| Deleting DNS TXT entry from your cyon.ch domain |"
  122. _info "+-------------------------------------------------+"
  123. _info ""
  124. _info " * Full Domain: ${fulldomain}"
  125. _info " * Cookie Jar: ${cookiejar}"
  126. _info ""
  127. fi
  128. }
  129. _login() {
  130. _info " - Logging in..."
  131. login_response=$(curl \
  132. "https://my.cyon.ch/auth/index/dologin-async" \
  133. -s \
  134. -c "${cookiejar}" \
  135. -H "X-Requested-With: XMLHttpRequest" \
  136. --data-urlencode "username=${cyon_username}" \
  137. --data-urlencode "password=${cyon_password}" \
  138. --data-urlencode "pathname=/")
  139. _debug login_response "${login_response}"
  140. # Bail if login fails.
  141. if [ "$(echo "${login_response}" | jq -r '.onSuccess')" != "success" ]; then
  142. _fail " $(echo "${login_response}" | jq -r '.message')"
  143. fi
  144. _info " success"
  145. # NECESSARY!! Load the main page after login, before the OTP check.
  146. curl "https://my.cyon.ch/" -s --compressed -b "${cookiejar}" >/dev/null
  147. # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
  148. # 2FA authentication with OTP?
  149. if [ ! -z "${cyon_otp_secret}" ] ; then
  150. _info " - Authorising with OTP code..."
  151. if ! _exists oathtool; then
  152. _fail "Please install oathtool to use 2 Factor Authentication."
  153. fi
  154. # Get OTP code with the defined secret.
  155. otp_code=$(oathtool --base32 --totp "${cyon_otp_secret}" 2>/dev/null)
  156. otp_response=$(curl \
  157. "https://my.cyon.ch/auth/multi-factor/domultifactorauth-async" \
  158. -s \
  159. --compressed \
  160. -b "${cookiejar}" \
  161. -c "${cookiejar}" \
  162. -H "X-Requested-With: XMLHttpRequest" \
  163. -d "totpcode=${otp_code}&pathname=%2F&rememberme=0")
  164. _debug otp_response "${otp_response}"
  165. # Bail if OTP authentication fails.
  166. if [ "$(echo "${otp_response}" | jq -r '.onSuccess')" != "success" ]; then
  167. _fail " $(echo "${otp_response}" | jq -r '.message')"
  168. fi
  169. _info " success"
  170. fi
  171. _info ""
  172. }
  173. _domain_env() {
  174. _info " - Changing domain environment..."
  175. # Get the "example.com" part of the full domain name.
  176. domain_env=$(echo "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')
  177. _debug "Changing domain environment to ${domain_env}"
  178. domain_env_response=$(curl \
  179. "https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/domain%3A${domain_env}" \
  180. -s \
  181. --compressed \
  182. -b "${cookiejar}" \
  183. -H "X-Requested-With: XMLHttpRequest")
  184. _debug domain_env_response "${domain_env_response}"
  185. _check_2fa_miss "${domain_env_response}"
  186. domain_env_success=$(echo "${domain_env_response}" | jq -r '.authenticated')
  187. # Bail if domain environment change fails.
  188. if [ "${domain_env_success}" != "true" ]; then
  189. _fail " $(echo "${domain_env_response}" | jq -r '.message')"
  190. fi
  191. _info " success"
  192. _info ""
  193. }
  194. _add_txt() {
  195. _info " - Adding DNS TXT entry..."
  196. addtxt_response=$(curl \
  197. "https://my.cyon.ch/domain/dnseditor/add-record-async" \
  198. -s \
  199. --compressed \
  200. -b "${cookiejar}" \
  201. -H "X-Requested-With: XMLHttpRequest" \
  202. -d "zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}")
  203. _debug addtxt_response "${addtxt_response}"
  204. _check_2fa_miss "${addtxt_response}"
  205. addtxt_message=$(echo "${addtxt_response}" | jq -r '.message')
  206. addtxt_status=$(echo "${addtxt_response}" | jq -r '.status')
  207. # Bail if adding TXT entry fails.
  208. if [ "${addtxt_status}" != "true" ]; then
  209. if [ "${addtxt_status}" = "null" ]; then
  210. addtxt_message=$(echo "${addtxt_response}" | jq -r '.error.message')
  211. fi
  212. _fail " ${addtxt_message}"
  213. fi
  214. _info " success"
  215. _info ""
  216. }
  217. _delete_txt() {
  218. _info " - Deleting DNS TXT entry..."
  219. list_txt_response=$(curl \
  220. "https://my.cyon.ch/domain/dnseditor/list-async" \
  221. -s \
  222. -b "${cookiejar}" \
  223. --compressed \
  224. -H "X-Requested-With: XMLHttpRequest")
  225. _debug list_txt_response "${list_txt_response}"
  226. _check_2fa_miss "${list_txt_response}"
  227. # Find and delete all acme challenge entries for the $fulldomain.
  228. _dns_entries=$(echo "$list_txt_response" | jq -r --arg fulldomain_idn "${fulldomain_idn}." '
  229. .rows[] |
  230. label $out|
  231. if .[0] != $fulldomain_idn then
  232. break $out
  233. else
  234. .[4]|
  235. capture("data-hash=\"(?<hash>[^\"]*)\" data-identifier=\"(?<identifier>[^\"]*)\"";"g")|
  236. .hash + " " + .identifier
  237. end')
  238. _dns_entries_cnt=$(echo "${_dns_entries}" | wc -l | grep -o '\d')
  239. _info " (entries found: ${_dns_entries_cnt})"
  240. _dns_entry_num=0
  241. echo "${_dns_entries}" | while read -r _hash _identifier
  242. do
  243. ((_dns_entry_num++))
  244. delete_txt_response=$(curl \
  245. "https://my.cyon.ch/domain/dnseditor/delete-record-async" \
  246. -s \
  247. --compressed \
  248. -b "${cookiejar}" \
  249. -H "X-Requested-With: XMLHttpRequest" \
  250. --data-urlencode "hash=${_hash}" \
  251. --data-urlencode "identifier=${_identifier}")
  252. _debug delete_txt_response "${delete_txt_response}"
  253. _check_2fa_miss "${delete_txt_response}"
  254. delete_txt_message=$(echo "${delete_txt_response}" | jq -r '.message')
  255. delete_txt_status=$(echo "${delete_txt_response}" | jq -r '.status')
  256. # Skip if deleting TXT entry fails.
  257. if [ "${delete_txt_status}" != "true" ]; then
  258. if [ "${delete_txt_status}" = "null" ]; then
  259. delete_txt_message=$(echo "${delete_txt_response}" | jq -r '.error.message')
  260. fi
  261. _err " [${_dns_entry_num}/${_dns_entries_cnt}] ${delete_txt_message} (${_identifier})"
  262. else
  263. _info " [${_dns_entry_num}/${_dns_entries_cnt}] success (${_identifier})"
  264. fi
  265. done
  266. _info " done"
  267. _info ""
  268. }
  269. _check_2fa_miss() {
  270. # Did we miss the 2FA?
  271. if [[ "$1" =~ "multi_factor_form" ]] ; then
  272. _fail " Missed OTP authentication!"
  273. fi
  274. }
  275. _fail() {
  276. _err "$1"
  277. _err ""
  278. _cleanup
  279. exit 1
  280. }
  281. _cleanup() {
  282. _info " - Cleanup."
  283. _debug "Remove cookie jar: ${cookiejar}"
  284. rm "${cookiejar}" 2>/dev/null
  285. _info ""
  286. }