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