4 # Custom cyon.ch DNS API for use with [acme.sh](https://github.com/acmesh-official/acme.sh)
6 # Usage: acme.sh --issue --dns dns_cyon -d www.domain.com
10 # - oathtool (When using 2 Factor Authentication)
14 # Any issues / questions / suggestions can be posted here:
15 # https://github.com/noplanman/cyon-api/issues
17 # Author: Armando Lüscher <armando@noplanman.ch>
21 _cyon_load_credentials
&&
22 _cyon_load_parameters
"$@" &&
23 _cyon_print_header
"add" &&
25 _cyon_change_domain_env
&&
31 _cyon_load_credentials
&&
32 _cyon_load_parameters
"$@" &&
33 _cyon_print_header
"delete" &&
35 _cyon_change_domain_env
&&
40 #########################
41 ### PRIVATE FUNCTIONS ###
42 #########################
44 _cyon_load_credentials
() {
45 # Convert loaded password to/from base64 as needed.
46 if [ "${CY_Password_B64}" ]; then
47 CY_Password
="$(printf "%s
" "${CY_Password_B64}" | _dbase64 "multiline
")"
48 elif [ "${CY_Password}" ]; then
49 CY_Password_B64
="$(printf "%s
" "${CY_Password}" | _base64)"
52 if [ -z "${CY_Username}" ] ||
[ -z "${CY_Password}" ]; then
53 # Dummy entries to satisfy script checker.
59 _err
"You haven't set your cyon.ch login credentials yet."
60 _err
"Please set the required cyon environment variables."
65 # Save the login credentials to the account.conf file.
66 _debug
"Save credentials to account.conf"
67 _saveaccountconf CY_Username
"${CY_Username}"
68 _saveaccountconf CY_Password_B64
"$CY_Password_B64"
69 if [ -n "${CY_OTP_Secret}" ]; then
70 _saveaccountconf CY_OTP_Secret
"$CY_OTP_Secret"
72 _clearaccountconf CY_OTP_Secret
77 _idn_temp
="$(printf "%s
" "${1}" | tr -d "0-9a-zA-Z.
,-_")"
78 _idn_temp2
="$(printf "%s
" "${1}" | grep -o "xn--
")"
79 [ "$_idn_temp" ] ||
[ "$_idn_temp2" ]
82 _cyon_load_parameters
() {
83 # Read the required parameters to add the TXT entry.
84 # shellcheck disable=SC2018,SC2019
85 fulldomain
="$(printf "%s
" "${1}" | tr "A-Z
" "a-z
")"
86 fulldomain_idn
="${fulldomain}"
88 # Special case for IDNs, as cyon needs a domain environment change,
89 # which uses the "pretty" instead of the punycode version.
90 if _cyon_is_idn
"${fulldomain}"; then
91 if ! _exists idn
; then
92 _err
"Please install idn to process IDN names."
97 fulldomain
="$(idn -u "${fulldomain}")"
98 fulldomain_idn
="$(idn -a "${fulldomain}")"
101 _debug fulldomain
"${fulldomain}"
102 _debug fulldomain_idn
"${fulldomain_idn}"
105 _debug txtvalue
"${txtvalue}"
107 # This header is required for curl calls.
108 _H1
="X-Requested-With: XMLHttpRequest"
112 _cyon_print_header
() {
113 if [ "${1}" = "add" ]; then
115 _info
"+---------------------------------------------+"
116 _info
"| Adding DNS TXT entry to your cyon.ch domain |"
117 _info
"+---------------------------------------------+"
119 _info
" * Full Domain: ${fulldomain}"
120 _info
" * TXT Value: ${txtvalue}"
122 elif [ "${1}" = "delete" ]; then
124 _info
"+-------------------------------------------------+"
125 _info
"| Deleting DNS TXT entry from your cyon.ch domain |"
126 _info
"+-------------------------------------------------+"
128 _info
" * Full Domain: ${fulldomain}"
133 _cyon_get_cookie_header
() {
134 printf "Cookie: %s" "$(grep "cyon
=" "$HTTP_HEADER" | grep "^Set-Cookie
:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')"
138 _info
" - Logging in..."
140 username_encoded
="$(printf "%s
" "${CY_Username}" | _url_encode)"
141 password_encoded
="$(printf "%s
" "${CY_Password}" | _url_encode)"
143 login_url
="https://my.cyon.ch/auth/index/dologin-async"
144 login_data
="$(printf "%s
" "username
=${username_encoded}&password
=${password_encoded}&pathname
=%2F
")"
146 login_response
="$(_post "$login_data" "$login_url")"
147 _debug login_response
"${login_response}"
149 # Bail if login fails.
150 if [ "$(printf "%s
" "${login_response}" | _cyon_get_response_success)" != "success" ]; then
151 _err
" $(printf "%s
" "${login_response}" | _cyon_get_response_message)"
158 # NECESSARY!! Load the main page after login, to get the new cookie.
159 _H2
="$(_cyon_get_cookie_header)"
162 _get
"https://my.cyon.ch/" >/dev
/null
164 # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
166 # 2FA authentication with OTP?
167 if [ -n "${CY_OTP_Secret}" ]; then
168 _info
" - Authorising with OTP code..."
170 if ! _exists oathtool
; then
171 _err
"Please install oathtool to use 2 Factor Authentication."
176 # Get OTP code with the defined secret.
177 otp_code
="$(oathtool --base32 --totp "${CY_OTP_Secret}" 2>/dev/null)"
179 login_otp_url
="https://my.cyon.ch/auth/multi-factor/domultifactorauth-async"
180 login_otp_data
="totpcode=${otp_code}&pathname=%2F&rememberme=0"
182 login_otp_response
="$(_post "$login_otp_data" "$login_otp_url")"
183 _debug login_otp_response
"${login_otp_response}"
185 # Bail if OTP authentication fails.
186 if [ "$(printf "%s
" "${login_otp_response}" | _cyon_get_response_success)" != "success" ]; then
187 _err
" $(printf "%s
" "${login_otp_response}" | _cyon_get_response_message)"
199 _info
" - Logging out..."
201 _get
"https://my.cyon.ch/auth/index/dologout" >/dev
/null
207 _cyon_change_domain_env
() {
208 _info
" - Changing domain environment..."
210 # Get the "example.com" part of the full domain name.
211 domain_env
="$(printf "%s
" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')"
212 _debug
"Changing domain environment to ${domain_env}"
214 gloo_item_key
="$(_get "https
://my.cyon.ch
/domain
/" | tr '\n' ' ' | sed -E -e "s
/.
*data-domain
=\"${domain_env}\"[^
<]*data-itemkey
=\"([^
\"]*).
*/\
1/")"
215 _debug gloo_item_key
"${gloo_item_key}"
217 domain_env_url
="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}"
219 domain_env_response
="$(_get "${domain_env_url}")"
220 _debug domain_env_response
"${domain_env_response}"
222 if ! _cyon_check_if_2fa_missed
"${domain_env_response}"; then return 1; fi
224 domain_env_success
="$(printf "%s
" "${domain_env_response}" | _egrep_o '"authenticated
":\w*' | cut -d : -f 2)"
226 # Bail if domain environment change fails.
227 if [ "${domain_env_success}" != "true" ]; then
228 _err
" $(printf "%s
" "${domain_env_response}" | _cyon_get_response_message)"
238 _info
" - Adding DNS TXT entry..."
240 add_txt_url
="https://my.cyon.ch/domain/dnseditor/add-record-async"
241 add_txt_data
="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}"
243 add_txt_response
="$(_post "$add_txt_data" "$add_txt_url")"
244 _debug add_txt_response
"${add_txt_response}"
246 if ! _cyon_check_if_2fa_missed
"${add_txt_response}"; then return 1; fi
248 add_txt_message
="$(printf "%s
" "${add_txt_response}" | _cyon_get_response_message)"
249 add_txt_status
="$(printf "%s
" "${add_txt_response}" | _cyon_get_response_status)"
251 # Bail if adding TXT entry fails.
252 if [ "${add_txt_status}" != "true" ]; then
253 _err
" ${add_txt_message}"
258 _info
" success (TXT|${fulldomain_idn}.|${txtvalue})"
263 _info
" - Deleting DNS TXT entry..."
265 list_txt_url
="https://my.cyon.ch/domain/dnseditor/list-async"
267 list_txt_response
="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
268 _debug list_txt_response
"${list_txt_response}"
270 if ! _cyon_check_if_2fa_missed
"${list_txt_response}"; then return 1; fi
272 # Find and delete all acme challenge entries for the $fulldomain.
273 _dns_entries
="$(printf "%b
\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\
([^
"]*\)\\" data-identifier
=\\"\([^"]*\
)\\".*/\1 \2/p')"
275 printf "%s" "${_dns_entries}" |
while read -r _hash _identifier
; do
276 dns_type
="$(printf "%s
" "$_identifier" | cut -d'|' -f1)"
277 dns_domain
="$(printf "%s
" "$_identifier" | cut -d'|' -f2)"
279 if [ "${dns_type}" != "TXT" ] || [ "${dns_domain}" != "${fulldomain_idn}." ]; then
283 hash_encoded
="$(printf "%s
" "${_hash}" | _url_encode)"
284 identifier_encoded
="$(printf "%s
" "${_identifier}" | _url_encode)"
286 delete_txt_url
="https://my.cyon.ch/domain/dnseditor/delete-record-async"
287 delete_txt_data
="$(printf "%s
" "hash=${hash_encoded}&identifier
=${identifier_encoded}")"
289 delete_txt_response
="$(_post "$delete_txt_data" "$delete_txt_url")"
290 _debug delete_txt_response
"${delete_txt_response}"
292 if ! _cyon_check_if_2fa_missed
"${delete_txt_response}"; then return 1; fi
294 delete_txt_message
="$(printf "%s
" "${delete_txt_response}" | _cyon_get_response_message)"
295 delete_txt_status
="$(printf "%s
" "${delete_txt_response}" | _cyon_get_response_status)"
297 # Skip if deleting TXT entry fails.
298 if [ "${delete_txt_status}" != "true" ]; then
299 _err
" ${delete_txt_message} (${_identifier})"
301 _info
" success (${_identifier})"
309 _cyon_get_response_message
() {
310 _egrep_o
'"message":"[^"]*"' | cut
-d : -f 2 |
tr -d '"'
313 _cyon_get_response_status
() {
314 _egrep_o
'"status":\w*' | cut
-d : -f 2
317 _cyon_get_response_success
() {
318 _egrep_o
'"onSuccess":"[^"]*"' | cut
-d : -f 2 |
tr -d '"'
321 _cyon_check_if_2fa_missed
() {
322 # Did we miss the 2FA?
323 if test "${1#*multi_factor_form}" != "${1}"; then
324 _err
" Missed OTP authentication!"