|
@@ -0,0 +1,355 @@
|
|
|
|
+#!/usr/bin/env sh
|
|
|
|
+
|
|
|
|
+########
|
|
|
|
+# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/Neilpang/acme.sh)
|
|
|
|
+#
|
|
|
|
+# Usage: acme.sh --issue --dns dns_cyon -d www.domain.com
|
|
|
|
+#
|
|
|
|
+# Dependencies:
|
|
|
|
+# -------------
|
|
|
|
+# - jq (get it here: https://stedolan.github.io/jq/download)
|
|
|
|
+# - oathtool (When using 2 Factor Authentication)
|
|
|
|
+#
|
|
|
|
+# Author: Armando Lüscher <armando@noplanman.ch>
|
|
|
|
+########
|
|
|
|
+
|
|
|
|
+########
|
|
|
|
+# Define cyon.ch login credentials:
|
|
|
|
+#
|
|
|
|
+# Either set them here: (uncomment these lines)
|
|
|
|
+#
|
|
|
|
+# cyon_username='your_cyon_username'
|
|
|
|
+# cyon_password='your_cyon_password'
|
|
|
|
+# cyon_otp_secret='your_otp_secret' # Only required if using 2FA
|
|
|
|
+#
|
|
|
|
+# ...or export them as environment variables in your shell:
|
|
|
|
+#
|
|
|
|
+# $ export cyon_username='your_cyon_username'
|
|
|
|
+# $ export cyon_password='your_cyon_password'
|
|
|
|
+# $ export cyon_otp_secret='your_otp_secret' # Only required if using 2FA
|
|
|
|
+#
|
|
|
|
+# *Note:*
|
|
|
|
+# After the first run, the credentials are saved in the "account.conf"
|
|
|
|
+# file, so any hard-coded or environment variables can then be removed.
|
|
|
|
+########
|
|
|
|
+
|
|
|
|
+dns_cyon_add() {
|
|
|
|
+ if ! _exists jq; then
|
|
|
|
+ _fail "Please install jq to use cyon.ch DNS API."
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _load_credentials
|
|
|
|
+ _load_parameters "$@"
|
|
|
|
+
|
|
|
|
+ _info_header "add"
|
|
|
|
+ _login
|
|
|
|
+ _domain_env
|
|
|
|
+ _add_txt
|
|
|
|
+ _cleanup
|
|
|
|
+
|
|
|
|
+ return 0
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+dns_cyon_rm() {
|
|
|
|
+ _load_credentials
|
|
|
|
+ _load_parameters "$@"
|
|
|
|
+
|
|
|
|
+ _info_header "delete"
|
|
|
|
+ _login
|
|
|
|
+ _domain_env
|
|
|
|
+ _delete_txt
|
|
|
|
+ _cleanup
|
|
|
|
+
|
|
|
|
+ return 0
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+#########################
|
|
|
|
+### PRIVATE FUNCTIONS ###
|
|
|
|
+#########################
|
|
|
|
+
|
|
|
|
+_load_credentials() {
|
|
|
|
+ # Convert loaded password to/from base64 as needed.
|
|
|
|
+ if [ "${cyon_password_b64}" ] ; then
|
|
|
|
+ cyon_password="$(echo "${cyon_password_b64}" | _dbase64)"
|
|
|
|
+ elif [ "${cyon_password}" ] ; then
|
|
|
|
+ cyon_password_b64="$(echo "${cyon_password}" | _base64)"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ if [ -z "${cyon_username}" ] || [ -z "${cyon_password}" ] ; then
|
|
|
|
+ _err ""
|
|
|
|
+ _err "You haven't set your cyon.ch login credentials yet."
|
|
|
|
+ _err "Please set the required cyon environment variables."
|
|
|
|
+ _err ""
|
|
|
|
+ exit 1
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ # Save the login credentials to the account.conf file.
|
|
|
|
+ _debug "Save credentials to account.conf"
|
|
|
|
+ _saveaccountconf cyon_username "${cyon_username}"
|
|
|
|
+ _saveaccountconf cyon_password_b64 "$cyon_password_b64"
|
|
|
|
+ if [ ! -z "${cyon_otp_secret}" ] ; then
|
|
|
|
+ _saveaccountconf cyon_otp_secret "$cyon_otp_secret"
|
|
|
|
+ fi
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_is_idn() {
|
|
|
|
+ _idn_temp=$(printf "%s" "$1" | tr -d "[0-9a-zA-Z.,-]")
|
|
|
|
+ _idn_temp2="$(printf "%s" "$1" | grep -o "xn--")"
|
|
|
|
+ [ "$_idn_temp" ] || [ "$_idn_temp2" ]
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_load_parameters() {
|
|
|
|
+ # Read the required parameters to add the TXT entry.
|
|
|
|
+ fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
|
|
|
+ fulldomain_idn="${fulldomain}"
|
|
|
|
+
|
|
|
|
+ # Special case for IDNs, as cyon needs a domain environment change,
|
|
|
|
+ # which uses the "pretty" instead of the punycode version.
|
|
|
|
+ if _is_idn "$1" ; then
|
|
|
|
+ if ! _exists idn; then
|
|
|
|
+ _fail "Please install idn to process IDN names."
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ fulldomain="$(idn -u "${fulldomain}")"
|
|
|
|
+ fulldomain_idn="$(idn -a "${fulldomain}")"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _debug fulldomain "$fulldomain"
|
|
|
|
+ _debug fulldomain_idn "$fulldomain_idn"
|
|
|
|
+
|
|
|
|
+ txtvalue="$2"
|
|
|
|
+ _debug txtvalue "$txtvalue"
|
|
|
|
+
|
|
|
|
+ # Cookiejar required for login session, as cyon.ch has no official API (yet).
|
|
|
|
+ cookiejar=$(tempfile)
|
|
|
|
+ _debug cookiejar "$cookiejar"
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_info_header() {
|
|
|
|
+ if [ "$1" = "add" ]; then
|
|
|
|
+ _info ""
|
|
|
|
+ _info "+---------------------------------------------+"
|
|
|
|
+ _info "| Adding DNS TXT entry to your cyon.ch domain |"
|
|
|
|
+ _info "+---------------------------------------------+"
|
|
|
|
+ _info ""
|
|
|
|
+ _info " * Full Domain: ${fulldomain}"
|
|
|
|
+ _info " * TXT Value: ${txtvalue}"
|
|
|
|
+ _info " * Cookie Jar: ${cookiejar}"
|
|
|
|
+ _info ""
|
|
|
|
+ elif [ "$1" = "delete" ]; then
|
|
|
|
+ _info ""
|
|
|
|
+ _info "+-------------------------------------------------+"
|
|
|
|
+ _info "| Deleting DNS TXT entry from your cyon.ch domain |"
|
|
|
|
+ _info "+-------------------------------------------------+"
|
|
|
|
+ _info ""
|
|
|
|
+ _info " * Full Domain: ${fulldomain}"
|
|
|
|
+ _info " * Cookie Jar: ${cookiejar}"
|
|
|
|
+ _info ""
|
|
|
|
+ fi
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_login() {
|
|
|
|
+ _info " - Logging in..."
|
|
|
|
+ login_response=$(curl \
|
|
|
|
+ "https://my.cyon.ch/auth/index/dologin-async" \
|
|
|
|
+ -s \
|
|
|
|
+ -c "${cookiejar}" \
|
|
|
|
+ -H "X-Requested-With: XMLHttpRequest" \
|
|
|
|
+ --data-urlencode "username=${cyon_username}" \
|
|
|
|
+ --data-urlencode "password=${cyon_password}" \
|
|
|
|
+ --data-urlencode "pathname=/")
|
|
|
|
+
|
|
|
|
+ _debug login_response "${login_response}"
|
|
|
|
+
|
|
|
|
+ # Bail if login fails.
|
|
|
|
+ if [ "$(echo "${login_response}" | jq -r '.onSuccess')" != "success" ]; then
|
|
|
|
+ _fail " $(echo "${login_response}" | jq -r '.message')"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _info " success"
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # NECESSARY!! Load the main page after login, before the OTP check.
|
|
|
|
+ curl "https://my.cyon.ch/" -s --compressed -b "${cookiejar}" >/dev/null
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # 2FA authentication with OTP?
|
|
|
|
+ if [ ! -z "${cyon_otp_secret}" ] ; then
|
|
|
|
+ _info " - Authorising with OTP code..."
|
|
|
|
+
|
|
|
|
+ if ! _exists oathtool; then
|
|
|
|
+ _fail "Please install oathtool to use 2 Factor Authentication."
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ # Get OTP code with the defined secret.
|
|
|
|
+ otp_code=$(oathtool --base32 --totp "${cyon_otp_secret}" 2>/dev/null)
|
|
|
|
+
|
|
|
|
+ otp_response=$(curl \
|
|
|
|
+ "https://my.cyon.ch/auth/multi-factor/domultifactorauth-async" \
|
|
|
|
+ -s \
|
|
|
|
+ --compressed \
|
|
|
|
+ -b "${cookiejar}" \
|
|
|
|
+ -c "${cookiejar}" \
|
|
|
|
+ -H "X-Requested-With: XMLHttpRequest" \
|
|
|
|
+ -d "totpcode=${otp_code}&pathname=%2F&rememberme=0")
|
|
|
|
+
|
|
|
|
+ _debug otp_response "${otp_response}"
|
|
|
|
+
|
|
|
|
+ # Bail if OTP authentication fails.
|
|
|
|
+ if [ "$(echo "${otp_response}" | jq -r '.onSuccess')" != "success" ]; then
|
|
|
|
+ _fail " $(echo "${otp_response}" | jq -r '.message')"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _info " success"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _info ""
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_domain_env() {
|
|
|
|
+ _info " - Changing domain environment..."
|
|
|
|
+
|
|
|
|
+ # Get the "example.com" part of the full domain name.
|
|
|
|
+ domain_env=$(echo "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')
|
|
|
|
+ _debug "Changing domain environment to ${domain_env}"
|
|
|
|
+
|
|
|
|
+ domain_env_response=$(curl \
|
|
|
|
+ "https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/domain%3A${domain_env}" \
|
|
|
|
+ -s \
|
|
|
|
+ --compressed \
|
|
|
|
+ -b "${cookiejar}" \
|
|
|
|
+ -H "X-Requested-With: XMLHttpRequest")
|
|
|
|
+
|
|
|
|
+ _debug domain_env_response "${domain_env_response}"
|
|
|
|
+
|
|
|
|
+ _check_2fa_miss "${domain_env_response}"
|
|
|
|
+
|
|
|
|
+ domain_env_success=$(echo "${domain_env_response}" | jq -r '.authenticated')
|
|
|
|
+
|
|
|
|
+ # Bail if domain environment change fails.
|
|
|
|
+ if [ "${domain_env_success}" != "true" ]; then
|
|
|
|
+ _fail " $(echo "${domain_env_response}" | jq -r '.message')"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _info " success"
|
|
|
|
+ _info ""
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_add_txt() {
|
|
|
|
+ _info " - Adding DNS TXT entry..."
|
|
|
|
+ addtxt_response=$(curl \
|
|
|
|
+ "https://my.cyon.ch/domain/dnseditor/add-record-async" \
|
|
|
|
+ -s \
|
|
|
|
+ --compressed \
|
|
|
|
+ -b "${cookiejar}" \
|
|
|
|
+ -H "X-Requested-With: XMLHttpRequest" \
|
|
|
|
+ -d "zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}")
|
|
|
|
+
|
|
|
|
+ _debug addtxt_response "${addtxt_response}"
|
|
|
|
+
|
|
|
|
+ _check_2fa_miss "${addtxt_response}"
|
|
|
|
+
|
|
|
|
+ addtxt_message=$(echo "${addtxt_response}" | jq -r '.message')
|
|
|
|
+ addtxt_status=$(echo "${addtxt_response}" | jq -r '.status')
|
|
|
|
+
|
|
|
|
+ # Bail if adding TXT entry fails.
|
|
|
|
+ if [ "${addtxt_status}" != "true" ]; then
|
|
|
|
+ if [ "${addtxt_status}" = "null" ]; then
|
|
|
|
+ addtxt_message=$(echo "${addtxt_response}" | jq -r '.error.message')
|
|
|
|
+ fi
|
|
|
|
+ _fail " ${addtxt_message}"
|
|
|
|
+ fi
|
|
|
|
+
|
|
|
|
+ _info " success"
|
|
|
|
+ _info ""
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_delete_txt() {
|
|
|
|
+ _info " - Deleting DNS TXT entry..."
|
|
|
|
+
|
|
|
|
+ list_txt_response=$(curl \
|
|
|
|
+ "https://my.cyon.ch/domain/dnseditor/list-async" \
|
|
|
|
+ -s \
|
|
|
|
+ -b "${cookiejar}" \
|
|
|
|
+ --compressed \
|
|
|
|
+ -H "X-Requested-With: XMLHttpRequest")
|
|
|
|
+
|
|
|
|
+ _debug list_txt_response "${list_txt_response}"
|
|
|
|
+
|
|
|
|
+ _check_2fa_miss "${list_txt_response}"
|
|
|
|
+
|
|
|
|
+ # Find and delete all acme challenge entries for the $fulldomain.
|
|
|
|
+ _dns_entries=$(echo "$list_txt_response" | jq -r --arg fulldomain_idn "${fulldomain_idn}." '
|
|
|
|
+ .rows[] |
|
|
|
|
+ label $out|
|
|
|
|
+ if .[0] != $fulldomain_idn then
|
|
|
|
+ break $out
|
|
|
|
+ else
|
|
|
|
+ .[4]|
|
|
|
|
+ capture("data-hash=\"(?<hash>[^\"]*)\" data-identifier=\"(?<identifier>[^\"]*)\"";"g")|
|
|
|
|
+ .hash + " " + .identifier
|
|
|
|
+ end')
|
|
|
|
+ _dns_entries_cnt=$(echo "${_dns_entries}" | wc -l | grep -o '\d')
|
|
|
|
+
|
|
|
|
+ _info " (entries found: ${_dns_entries_cnt})"
|
|
|
|
+
|
|
|
|
+ _dns_entry_num=0
|
|
|
|
+
|
|
|
|
+ echo "${_dns_entries}" | while read -r _hash _identifier
|
|
|
|
+ do
|
|
|
|
+ ((_dns_entry_num++))
|
|
|
|
+
|
|
|
|
+ delete_txt_response=$(curl \
|
|
|
|
+ "https://my.cyon.ch/domain/dnseditor/delete-record-async" \
|
|
|
|
+ -s \
|
|
|
|
+ --compressed \
|
|
|
|
+ -b "${cookiejar}" \
|
|
|
|
+ -H "X-Requested-With: XMLHttpRequest" \
|
|
|
|
+ --data-urlencode "hash=${_hash}" \
|
|
|
|
+ --data-urlencode "identifier=${_identifier}")
|
|
|
|
+
|
|
|
|
+ _debug delete_txt_response "${delete_txt_response}"
|
|
|
|
+
|
|
|
|
+ _check_2fa_miss "${delete_txt_response}"
|
|
|
|
+
|
|
|
|
+ delete_txt_message=$(echo "${delete_txt_response}" | jq -r '.message')
|
|
|
|
+ delete_txt_status=$(echo "${delete_txt_response}" | jq -r '.status')
|
|
|
|
+
|
|
|
|
+ # Skip if deleting TXT entry fails.
|
|
|
|
+ if [ "${delete_txt_status}" != "true" ]; then
|
|
|
|
+ if [ "${delete_txt_status}" = "null" ]; then
|
|
|
|
+ delete_txt_message=$(echo "${delete_txt_response}" | jq -r '.error.message')
|
|
|
|
+ fi
|
|
|
|
+ _err " [${_dns_entry_num}/${_dns_entries_cnt}] ${delete_txt_message} (${_identifier})"
|
|
|
|
+ else
|
|
|
|
+ _info " [${_dns_entry_num}/${_dns_entries_cnt}] success (${_identifier})"
|
|
|
|
+ fi
|
|
|
|
+ done
|
|
|
|
+
|
|
|
|
+ _info " done"
|
|
|
|
+ _info ""
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_check_2fa_miss() {
|
|
|
|
+ # Did we miss the 2FA?
|
|
|
|
+ if [[ "$1" =~ "multi_factor_form" ]] ; then
|
|
|
|
+ _fail " Missed OTP authentication!"
|
|
|
|
+ fi
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_fail() {
|
|
|
|
+ _err "$1"
|
|
|
|
+ _err ""
|
|
|
|
+ _cleanup
|
|
|
|
+ exit 1
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+_cleanup() {
|
|
|
|
+ _info " - Cleanup."
|
|
|
|
+ _debug "Remove cookie jar: ${cookiejar}"
|
|
|
|
+ rm "${cookiejar}" 2>/dev/null
|
|
|
|
+ _info ""
|
|
|
|
+}
|