123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- #!/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
- cyon_username=""
- cyon_password=""
- cyon_otp_secret=""
- _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=$((_dns_entry_num + 1))
- 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 test "${1#*multi_factor_form}" != "$1"; 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 ""
- }
|