diff --git a/README.md b/README.md index a297b99..b33f863 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ You don't have to do anything manually! 1. Alwaysdata.com API 1. Linode.com API 1. FreeDNS (https://freedns.afraid.org/) +1. cyon.ch **More APIs coming soon...** diff --git a/dnsapi/README.md b/dnsapi/README.md index 6a86bf4..fd88d57 100644 --- a/dnsapi/README.md +++ b/dnsapi/README.md @@ -305,6 +305,24 @@ Note that you cannot use acme.sh automatic DNS validation for FreeDNS public dom you create under a FreeDNS public domain. You must own the top level domain in order to automaitcally validate with acme.sh at FreeDNS. +## 16. Use cyon.ch + +You only need to set your cyon.ch login credentials. +If you also have 2 Factor Authentication (OTP) enabled, you need to set your secret token too and have `oathtool` installed. + +``` +export CY_Username="your_cyon_username" +export CY_Password="your_cyon_password" +export CY_OTP_Secret="your_otp_secret" # Only required if using 2FA +``` + +To issue a cert: +``` +acme.sh --issue --dns dns_cyon -d example.com -d www.example.com +``` + +The `CY_Username`, `CY_Password` and `CY_OTP_Secret` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + # Use custom API If your API is not supported yet, you can write your own DNS API. diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh new file mode 100644 index 0000000..c096d8b --- /dev/null +++ b/dnsapi/dns_cyon.sh @@ -0,0 +1,328 @@ +#!/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: +# ------------- +# - oathtool (When using 2 Factor Authentication) +# +# Issues: +# ------- +# Any issues / questions / suggestions can be posted here: +# https://github.com/noplanman/cyon-api/issues +# +# Author: Armando Lüscher +######## + +dns_cyon_add() { + _cyon_load_credentials \ + && _cyon_load_parameters "$@" \ + && _cyon_print_header "add" \ + && _cyon_login \ + && _cyon_change_domain_env \ + && _cyon_add_txt \ + && _cyon_logout +} + +dns_cyon_rm() { + _cyon_load_credentials \ + && _cyon_load_parameters "$@" \ + && _cyon_print_header "delete" \ + && _cyon_login \ + && _cyon_change_domain_env \ + && _cyon_delete_txt \ + && _cyon_logout +} + +######################### +### PRIVATE FUNCTIONS ### +######################### + +_cyon_load_credentials() { + # Convert loaded password to/from base64 as needed. + if [ "${CY_Password_B64}" ]; then + CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64 "multiline")" + elif [ "${CY_Password}" ]; then + CY_Password_B64="$(printf "%s" "${CY_Password}" | _base64)" + fi + + if [ -z "${CY_Username}" ] || [ -z "${CY_Password}" ]; then + # Dummy entries to satify script checker. + CY_Username="" + CY_Password="" + CY_OTP_Secret="" + + _err "" + _err "You haven't set your cyon.ch login credentials yet." + _err "Please set the required cyon environment variables." + _err "" + return 1 + fi + + # Save the login credentials to the account.conf file. + _debug "Save credentials to account.conf" + _saveaccountconf CY_Username "${CY_Username}" + _saveaccountconf CY_Password_B64 "$CY_Password_B64" + if [ ! -z "${CY_OTP_Secret}" ]; then + _saveaccountconf CY_OTP_Secret "$CY_OTP_Secret" + else + _clearaccountconf CY_OTP_Secret + fi +} + +_cyon_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" ] +} + +_cyon_load_parameters() { + # Read the required parameters to add the TXT entry. + # shellcheck disable=SC2018,SC2019 + fulldomain="$(printf "%s" "${1}" | tr "A-Z" "a-z")" + fulldomain_idn="${fulldomain}" + + # Special case for IDNs, as cyon needs a domain environment change, + # which uses the "pretty" instead of the punycode version. + if _cyon_is_idn "${fulldomain}"; then + if ! _exists idn; then + _err "Please install idn to process IDN names." + _err "" + return 1 + fi + + fulldomain="$(idn -u "${fulldomain}")" + fulldomain_idn="$(idn -a "${fulldomain}")" + fi + + _debug fulldomain "${fulldomain}" + _debug fulldomain_idn "${fulldomain_idn}" + + txtvalue="${2}" + _debug txtvalue "${txtvalue}" + + # This header is required for curl calls. + _H1="X-Requested-With: XMLHttpRequest" + export _H1 +} + +_cyon_print_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 "" + elif [ "${1}" = "delete" ]; then + _info "" + _info "+-------------------------------------------------+" + _info "| Deleting DNS TXT entry from your cyon.ch domain |" + _info "+-------------------------------------------------+" + _info "" + _info " * Full Domain: ${fulldomain}" + _info "" + fi +} + +_cyon_get_cookie_header() { + printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')" +} + +_cyon_login() { + _info " - Logging in..." + + username_encoded="$(printf "%s" "${CY_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${CY_Password}" | _url_encode)" + + login_url="https://my.cyon.ch/auth/index/dologin-async" + login_data="$(printf "%s" "username=${username_encoded}&password=${password_encoded}&pathname=%2F")" + + login_response="$(_post "$login_data" "$login_url")" + _debug login_response "${login_response}" + + # Bail if login fails. + if [ "$(printf "%s" "${login_response}" | _cyon_get_response_success)" != "success" ]; then + _err " $(printf "%s" "${login_response}" | _cyon_get_response_message)" + _err "" + return 1 + fi + + _info " success" + + # NECESSARY!! Load the main page after login, to get the new cookie. + _H2="$(_cyon_get_cookie_header)" + export _H2 + + _get "https://my.cyon.ch/" >/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 "${CY_OTP_Secret}" ]; then + _info " - Authorising with OTP code..." + + if ! _exists oathtool; then + _err "Please install oathtool to use 2 Factor Authentication." + _err "" + return 1 + fi + + # Get OTP code with the defined secret. + otp_code="$(oathtool --base32 --totp "${CY_OTP_Secret}" 2>/dev/null)" + + login_otp_url="https://my.cyon.ch/auth/multi-factor/domultifactorauth-async" + login_otp_data="totpcode=${otp_code}&pathname=%2F&rememberme=0" + + login_otp_response="$(_post "$login_otp_data" "$login_otp_url")" + _debug login_otp_response "${login_otp_response}" + + # Bail if OTP authentication fails. + if [ "$(printf "%s" "${login_otp_response}" | _cyon_get_response_success)" != "success" ]; then + _err " $(printf "%s" "${login_otp_response}" | _cyon_get_response_message)" + _err "" + return 1 + fi + + _info " success" + fi + + _info "" +} + +_cyon_logout() { + _info " - Logging out..." + + _get "https://my.cyon.ch/auth/index/dologout" >/dev/null + + _info " success" + _info "" +} + +_cyon_change_domain_env() { + _info " - Changing domain environment..." + + # Get the "example.com" part of the full domain name. + domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')" + _debug "Changing domain environment to ${domain_env}" + + gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")" + _debug gloo_item_key "${gloo_item_key}" + + domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}" + + domain_env_response="$(_get "${domain_env_url}")" + _debug domain_env_response "${domain_env_response}" + + if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi + + domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)" + + # Bail if domain environment change fails. + if [ "${domain_env_success}" != "true" ]; then + _err " $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)" + _err "" + return 1 + fi + + _info " success" + _info "" +} + +_cyon_add_txt() { + _info " - Adding DNS TXT entry..." + + add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async" + add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}" + + add_txt_response="$(_post "$add_txt_data" "$add_txt_url")" + _debug add_txt_response "${add_txt_response}" + + if ! _cyon_check_if_2fa_missed "${add_txt_response}"; then return 1; fi + + add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)" + add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)" + + # Bail if adding TXT entry fails. + if [ "${add_txt_status}" != "true" ]; then + _err " ${add_txt_message}" + _err "" + return 1 + fi + + _info " success (TXT|${fulldomain_idn}.|${txtvalue})" + _info "" +} + +_cyon_delete_txt() { + _info " - Deleting DNS TXT entry..." + + list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async" + + list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')" + _debug list_txt_response "${list_txt_response}" + + if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi + + # Find and delete all acme challenge entries for the $fulldomain. + _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')" + + printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do + dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)" + dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)" + + if [ "${dns_type}" != "TXT" ] || [ "${dns_domain}" != "${fulldomain_idn}." ]; then + continue + fi + + hash_encoded="$(printf "%s" "${_hash}" | _url_encode)" + identifier_encoded="$(printf "%s" "${_identifier}" | _url_encode)" + + delete_txt_url="https://my.cyon.ch/domain/dnseditor/delete-record-async" + delete_txt_data="$(printf "%s" "hash=${hash_encoded}&identifier=${identifier_encoded}")" + + delete_txt_response="$(_post "$delete_txt_data" "$delete_txt_url")" + _debug delete_txt_response "${delete_txt_response}" + + if ! _cyon_check_if_2fa_missed "${delete_txt_response}"; then return 1; fi + + delete_txt_message="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_message)" + delete_txt_status="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_status)" + + # Skip if deleting TXT entry fails. + if [ "${delete_txt_status}" != "true" ]; then + _err " ${delete_txt_message} (${_identifier})" + else + _info " success (${_identifier})" + fi + done + + _info " done" + _info "" +} + +_cyon_get_response_message() { + _egrep_o '"message":"[^"]*"' | cut -d : -f 2 | tr -d '"' +} + +_cyon_get_response_status() { + _egrep_o '"status":\w*' | cut -d : -f 2 +} + +_cyon_get_response_success() { + _egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"' +} + +_cyon_check_if_2fa_missed() { + # Did we miss the 2FA? + if test "${1#*multi_factor_form}" != "${1}"; then + _err " Missed OTP authentication!" + _err "" + return 1 + fi +}