diff --git a/dnsapi/dns_yandex360.sh b/dnsapi/dns_yandex360.sh new file mode 100644 index 00000000..3e879c52 --- /dev/null +++ b/dnsapi/dns_yandex360.sh @@ -0,0 +1,334 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_yandex360_info='Yandex 360 for Business DNS API. +Yandex 360 for Business is a digital environment for effective collaboration. +Site: https://360.yandex.com/ +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_yandex360 +Options: + YANDEX360_CLIENT_ID OAuth 2.0 ClientID + YANDEX360_CLIENT_SECRET OAuth 2.0 Client secret + YANDEX360_ORG_ID Organization ID +OptionsAlt: + YANDEX360_ACCESS_TOKEN OAuth 2.0 Access token. Optional. +Issues: https://github.com/acmesh-official/acme.sh/issues/5213 +Author: +' + +YANDEX360_API_BASE='https://api360.yandex.net/directory/v1/org' +YANDEX360_OAUTH_BASE='https://oauth.yandex.ru' + +######## Public functions ##################### + +dns_yandex360_add() { + fulldomain=$1 + txtvalue=$2 + _info 'Using Yandex 360 DNS API' + + if ! _check_yandex360_variables; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + return 1 + fi + + sub_domain=$(echo "$fulldomain" | sed "s/\.$root_domain$//") + + _debug 'Adding Yandex 360 DNS record for subdomain' "$sub_domain" + dns_api_url="${YANDEX360_API_BASE}/${YANDEX360_ORG_ID}/domains/${root_domain}/dns" + data='{"name":"'"$sub_domain"'","type":"TXT","ttl":60,"text":"'"$txtvalue"'"}' + + response="$(_post "$data" "$dns_api_url" '' 'POST' 'application/json')" + response="$(echo "$response" | _normalizeJson)" + _debug 'Response' "$response" + + if _contains "$response" 'recordId'; then + return 0 + else + return 1 + fi +} + +dns_yandex360_rm() { + fulldomain=$1 + txtvalue=$2 + _info 'Using Yandex 360 DNS API' + + if ! _check_yandex360_variables; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + return 1 + fi + + _debug 'Retrieving 100 records from Yandex 360 DNS' + dns_api_url="${YANDEX360_API_BASE}/${YANDEX360_ORG_ID}/domains/${root_domain}/dns?perPage=100" + response="$(_get "$dns_api_url" '' '')" + response="$(echo "$response" | _normalizeJson)" + _debug 'Response' "$response" + + if ! _contains "$response" "$txtvalue"; then + _info 'DNS record not found. Nothing to remove.' + return 1 + fi + + record_id=$( + echo "$response" | + sed -En 's/.*"recordId":([0-9]+).*"text":"'"${txtvalue}"'".*$/\1/p' + ) + + if [ -z "$record_id" ]; then + _err 'Unable to get record ID to remove' + return 1 + fi + + _debug 'Removing DNS record' "$record_id" + delete_url="${YANDEX360_API_BASE}/${YANDEX360_ORG_ID}/domains/${root_domain}/dns/${record_id}" + + response="$(_post '' "$delete_url" '' 'DELETE')" + response="$(echo "$response" | _normalizeJson)" + _debug 'Response' "$response" + + if _contains "$response" '{}'; then + return 0 + else + return 1 + fi +} + +#################### Private functions below ################################## + +_check_yandex360_variables() { + YANDEX360_CLIENT_ID="${YANDEX360_CLIENT_ID:-$(_readaccountconf_mutable YANDEX360_CLIENT_ID)}" + YANDEX360_CLIENT_SECRET="${YANDEX360_CLIENT_SECRET:-$(_readaccountconf_mutable YANDEX360_CLIENT_SECRET)}" + YANDEX360_ORG_ID="${YANDEX360_ORG_ID:-$(_readaccountconf_mutable YANDEX360_ORG_ID)}" + YANDEX360_ACCESS_TOKEN="${YANDEX360_ACCESS_TOKEN:-$(_readaccountconf_mutable YANDEX360_ACCESS_TOKEN)}" + YANDEX360_REFRESH_TOKEN="${YANDEX360_REFRESH_TOKEN:-$(_readaccountconf_mutable YANDEX360_REFRESH_TOKEN)}" + + if [ -z "$YANDEX360_ORG_ID" ]; then + _err '=========================================' + _err ' ERROR' + _err '=========================================' + _err "A required environment variable YANDEX360_ORG_ID is not set" + _err 'For more details, please visit: https://wiki.acme.sh/dnsapi#30-use-pddyandexru-api' + _err '=========================================' + return 1 + fi + + _saveaccountconf_mutable YANDEX360_ORG_ID "$YANDEX360_ORG_ID" + + if [ -n "$YANDEX360_ACCESS_TOKEN" ]; then + _info '=========================================' + _info ' ATTENTION' + _info '=========================================' + _info 'A manually provided Yandex 360 access token has been detected, which is not recommended.' + _info 'Please note that this token is valid for a limited time after issuance.' + _info 'It is recommended to obtain the token interactively using acme.sh for one-time setup.' + _info 'Subsequent token renewals will be handled automatically.' + _info 'For more details, please visit: https://wiki.acme.sh/dnsapi#30-use-pddyandexru-api' + _info '=========================================' + + _saveaccountconf_mutable YANDEX360_ACCESS_TOKEN "$YANDEX360_ACCESS_TOKEN" + export _H1="Authorization: OAuth $YANDEX360_ACCESS_TOKEN" + return 0 + fi + + if [ -z "$YANDEX360_CLIENT_ID" ] || [ -z "$YANDEX360_CLIENT_SECRET" ]; then + _err '=========================================' + _err ' ERROR' + _err '=========================================' + _err 'The preferred environment variables YANDEX360_CLIENT_ID, YANDEX360_CLIENT_SECRET, and YANDEX360_ORG_ID, or alternatively YANDEX360_ACCESS_TOKEN, is not set.' + _err 'It is recommended to export the first three variables over the latter before running acme.sh.' + _err 'For more details, please visit: https://wiki.acme.sh/dnsapi#30-use-pddyandexru-api' + _err '=========================================' + return 1 + fi + + _saveaccountconf_mutable YANDEX360_CLIENT_ID "$YANDEX360_CLIENT_ID" + _saveaccountconf_mutable YANDEX360_CLIENT_SECRET "$YANDEX360_CLIENT_SECRET" + + if [ -n "$YANDEX360_REFRESH_TOKEN" ]; then + _debug 'Refresh token found. Attempting to refresh access token.' + if _refresh_token; then + return 0 + fi + fi + + if ! _get_token; then + return 1 + fi + + return 0 +} + +_get_token() { + _info "$(_red '=========================================')" + _info "$(_red ' NOTICE')" + _info "$(_red '=========================================')" + _info "$(_red 'Before using the Yandex 360 API, you need to complete an authorization procedure.')" + _info "$(_red 'The initial access token is obtained interactively and is a one-time operation.')" + _info "$(_red 'Subsequent API requests will be handled automatically.')" + _info "$(_red '=========================================')" + + _info 'Initiating device authorization flow' + device_code_url="${YANDEX360_OAUTH_BASE}/device/code" + + hostname=$(uname -n) + data="client_id=$YANDEX360_CLIENT_ID&device_id=acme.sh ${hostname}&device_name=acme.sh ${hostname}" + + response="$(_post "$data" "$device_code_url" '' 'POST')" + response="$(echo "$response" | _normalizeJson)" + _debug 'Response' "$response" + + if ! _contains "$response" 'device_code'; then + _err 'Failed to get device code' + return 1 + fi + + device_code=$( + echo "$response" | + _egrep_o '"device_code":"[^"]*"' | + cut -d: -f2 | + tr -d '"' + ) + _debug 'Device code' "$device_code" + + user_code=$( + echo "$response" | + _egrep_o '"user_code":"[^"]*"' | + cut -d: -f2 | + tr -d '"' + ) + _debug 'User code' "$user_code" + + verification_url=$( + echo "$response" | + _egrep_o '"verification_url":"[^"]*"' | + cut -d: -f2- | + tr -d '"' + ) + _debug 'Verification URL' "$verification_url" + + interval=$( + echo "$response" | + _egrep_o '"interval":[[:space:]]*[0-9]+' | + cut -d: -f2 + ) + _debug 'Polling interval' "$interval" + + _info "$(__red 'Please visit '"$verification_url"' and log in as an organization administrator')" + _info "$(__red 'Once logged in, enter the code: '"$user_code"' on the page from the previous step')" + _info "$(__red 'Waiting for authorization...')" + + _debug 'Polling for token' + token_url="${YANDEX360_OAUTH_BASE}/token" + + while true; do + data="grant_type=device_code&code=$device_code&client_id=$YANDEX360_CLIENT_ID&client_secret=$YANDEX360_CLIENT_SECRET" + + response="$(_post "$data" "$token_url" '' 'POST')" + response="$(echo "$response" | _normalizeJson)" + + if _contains "$response" 'access_token'; then + YANDEX360_ACCESS_TOKEN=$( + echo "$response" | + _egrep_o '"access_token":"[^"]*"' | + cut -d: -f2- | + tr -d '"' + ) + YANDEX360_REFRESH_TOKEN=$( + echo "$response" | + _egrep_o '"refresh_token":"[^"]*"' | + cut -d: -f2- | + tr -d '"' + ) + + _secure_debug 'Response' "$response" + _secure_debug 'Received access token' "$YANDEX360_ACCESS_TOKEN" + _secure_debug 'Received refresh token' "$YANDEX360_REFRESH_TOKEN" + + _saveaccountconf_mutable YANDEX360_REFRESH_TOKEN "$YANDEX360_REFRESH_TOKEN" + + export _H1="Authorization: OAuth $YANDEX360_ACCESS_TOKEN" + + _info 'Access token obtained successfully' + return 0 + elif _contains "$response" 'authorization_pending'; then + _debug 'Response' "$response" + _debug "Authorization pending. Waiting $interval seconds before next attempt." + _sleep "$interval" + else + _debug 'Response' "$response" + _err 'Failed to get access token' + return 1 + fi + done +} + +_refresh_token() { + token_url="${YANDEX360_OAUTH_BASE}/token" + + data="grant_type=refresh_token&refresh_token=$YANDEX360_REFRESH_TOKEN&client_id=$YANDEX360_CLIENT_ID&client_secret=$YANDEX360_CLIENT_SECRET" + + response="$(_post "$data" "$token_url" '' 'POST')" + response="$(echo "$response" | _normalizeJson)" + + if _contains "$response" 'access_token'; then + YANDEX360_ACCESS_TOKEN=$( + echo "$response" | + _egrep_o '"access_token":"[^"]*"' | + cut -d: -f2 | + tr -d '"' + ) + YANDEX360_REFRESH_TOKEN=$( + echo "$response" | + _egrep_o '"refresh_token":"[^"]*"' | + cut -d: -f2- | + tr -d '"' + ) + + _secure_debug 'Response' "$response" + _secure_debug 'Received access token' "$YANDEX360_ACCESS_TOKEN" + _secure_debug 'Received refresh token' "$YANDEX360_REFRESH_TOKEN" + + _saveaccountconf_mutable YANDEX360_REFRESH_TOKEN "$YANDEX360_REFRESH_TOKEN" + + export _H1="Authorization: OAuth $YANDEX360_ACCESS_TOKEN" + + _info 'Access token refreshed successfully' + return 0 + else + _debug 'Response' "$response" + _info 'Failed to refresh token. Will attempt to obtain a new one.' + return 1 + fi +} + +_get_root() { + domain="$1" + i=1 + while true; do + h=$(echo "$domain" | cut -d . -f "$i"-) + _debug "Checking domain: $h" + + if [ -z "$h" ]; then + _err "Could not determine root domain" + return 1 + fi + + dns_api_url="${YANDEX360_API_BASE}/${YANDEX360_ORG_ID}/domains/${h}/dns" + + response="$(_get "$dns_api_url" '' '')" + response="$(echo "$response" | _normalizeJson)" + _debug 'Response' "$response" + + if _contains "$response" '"total":'; then + root_domain="$h" + _debug 'Root domain found' "$root_domain" + return 0 + fi + + i=$(_math "$i" + 1) + done +}