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.

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