#!/usr/bin/bash

# shellcheck disable=SC2034
dns_arubabusiness_info='ArubaBusiness
Site: business.aruba.it
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness
Options:
 AB_Key Your ArubaBusiness API Key
 AB_User Your account user
 AB_Pass Your account password
'

#
# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type.
# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges
#

ARUBABUSINESS_API='https://api.arubabusiness.it'

######## Public functions ########

#
# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333
#
# Add a new TXT record whose name and value match the given domain and value
#
# Variables
#   _full_domain: $1 - the name of the TXT record
#   _txt_value: $2 - the value of the TXT record
#   _body
#   dns_details
#   domain_id
#   dns_record_id
#   response
#
dns_arubabusiness_add() {
  _full_domain=$1
  _txt_value=$2

  if ! _ab_authenticate; then
    return 1
  fi

  if ! _ab_domain_id "$_full_domain"; then
    return 1
  fi

  if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
    # This is very unlikely, but allow the process to use the existing record
    _info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)"
    return 0
  fi

  _body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }"

  _debug "Adding TXT record with name: $_full_domain and value: $_txt_value"

  if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then
    _err "Failed to add TXT record with name: $_full_domain"
    return 1
  fi

  _info "Sleeping 10 seconds to let ArubaBusiness do its magic"
  _sleep 10

  # Refresh dns details and check that the record was really added
  if ! _ab_dns_details "$root_domain"; then
    return 1
  fi

  if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
    # This should never happen
    _err "The TXT record with name: $_full_domain was not set"
    _err "Please check that the dns records are clean"
    return 1
  fi

  _info "Added TXT record with id: $dns_record_id"
  return 0
}

#
# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333
#
# Remove the TXT record whose name and value match the given domain and value
#
# Variables
#   _full_domain: $1 - the name of the TXT record
#   _txt_value: $2 - the value of the TXT record
#   dns_details
#   dns_record_id
#
dns_arubabusiness_rm() {
  _full_domain=$1
  _txt_value=$2

  if ! _ab_authenticate; then
    return 1
  fi

  if ! _ab_domain_id "$_full_domain"; then
    return 1
  fi

  if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then
    _err "Could not retrieve the record id for: $_full_domain"
    return 1
  fi

  _debug "Deleting TXT record: $dns_record_id"
  if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then
    _err "Failed to delete TXT record: $dns_record_id"
    return 1
  fi

  _info "Deleted TXT record: $dns_record_id"
  return 0
}

######## Private functions ########

#
# Usage: _ab_domain_id _acme-challenge.www.domain.com
#
# Split the input domain into subdomain + root domain and get the id of the root domain
#
# Variables
#   _full_domain: $1 - the domain whose root needs to be extracted
#   _domain_sections
#   _current_index
#   _candidate_subdomain
#   _candidate_domain
#   sub_domain
#   root_domain
#   domain_id
#   dns_details: a json containing all dns records registered on the root domain
#
# Example
#   _get_root _acme-challenge.www.domain.com
#
# Should return
#   sub_domain=_acme-challenge.www
#   root_domain=domain.com
#   domain_id=123123123123
#   dns_details="{JSON_CONTENT}"
#
_ab_domain_id() {
  _full_domain=$1

  _info "Attempting to retrieve root domain details for: $_full_domain"

  _domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1)

  if [ "$_domain_sections" -lt 1 ]; then
    _err "Invalid input $_full_domain"
    return 1
  fi

  _current_index=1
  while true; do
    _candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi)
    _candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections")

    if ! _ab_dns_details "$_candidate_domain"; then
      _debug2 "Could not fetch dns details for: $_candidate_domain"
      _current_index=$(_math "$_current_index" + 1)

      # Fail if there are no candidates left
      if [ "$_current_index" -gt "$_domain_sections" ]; then
        _err "Could not determine the root domain for: $_full_domain"
        return 1
      fi
    else
      sub_domain="$_candidate_subdomain"
      root_domain="$_candidate_domain"
      # Extract the domain id, which is an integer and contains no commas
      domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')"

      if [ -z "$domain_id" ]; then
        _err "Could not determine the domain id for: $root_domain"
        return 1
      fi

      _debug "Retrieved root domain id: $domain_id"
      return 0
    fi
  done
}

#
# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}"
#
# Extract the record id of the first TXT record whose name and content match the input values
#
# Variables
#   _record_name: $1
#   _txt_value: $2
#   _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain'
#   _record_ids
#   _record_names
#   _record_types
#   _record_contents
#   _record_ids_count
#   _record_names_count
#   _record_types_count
#   _record_contents_count
#   _i
#   dns_record_id
#
# Notes
#   TXT correspond to record type 5
#   ArubaBusiness appends a terminating dot (.) to the record name
#   The content field may contain the following character sequence: \"
#   All record names are always converted to lowercase
#
_ab_dns_record_id() {
  _record_name=$1
  _txt_value=$2
  _dns_details=$3

  _record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case)

  # Extract the record ids, which are integers and contain no commas, colons or spaces
  # The first id is skipped because it refers to the domain id
  _record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')

  # Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes
  # The first name is skipped because it refers to the domain name
  _record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ')

  # Extract the record types, which  are integers (except for the first one) and contain no commas, colons or spaces
  # The first type is skipped because it refers to the domain type
  _record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')

  # Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals
  # Note: There is no domain related entry here
  # Note: A " character is appended at the end of each content to make it easier to process the list later
  _record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#')

  _info "IDS: $_record_ids"
  _info "NAMES: $_record_names"
  _info "TYPEs: $_record_types"
  _info "CONTENTS: $_record_contents"

  _record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l)
  _record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l)
  _record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l)
  _record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l)

  _info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"

  if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then
    _err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
    return 1
  fi

  _info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value"

  _i=1
  while [ "$_i" -le "$_record_ids_count" ]; do
    _current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i")
    _current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i")
    _current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i")

    if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then
      dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i")
      _info "Found matching record with id: $dns_record_id"
      return 0
    else
      _debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'"
    fi
    _i=$(_math "$_i" + 1)
  done

  _debug2 "No matching record was found in $_dns_details"
  return 1
}

#
# Usage: _ab_dns_details domain.com
#
# Retrieve dns info for the given input domain
#
# Variables
#   _domain: $1
#   dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0)
#   response
#
_ab_dns_details() {
  _domain=$1

  if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then
    return 1
  fi

  dns_details="$response"
  return 0
}

#
# Usage: _ab_authenticate
#
# Read account conf, update domain conf and perform user authentication to acquire an access token
#
# Variables
#   AB_Key
#   AB_User
#   AB_Pass
#   AB_Token
#
_ab_authenticate() {
  AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}"
  AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}"
  AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}"

  if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then
    AB_Key=""
    AB_User=""
    AB_Pass=""
    _err "Either the ArubaBusiness API key, the user or the password has not been defined yet."
    _err "Please configure them and try again."
    return 1
  fi

  _saveaccountconf_mutable AB_Key "$AB_Key"
  _saveaccountconf_mutable AB_User "$AB_User"
  _saveaccountconf_mutable AB_Pass "$AB_Pass"

  if ! _ab_get_token || [ -z "$AB_Token" ]; then
    _err "Failed to acquire an access token"
    return 1
  fi

  return 0
}

#
# Usage: _ab_get_token
#
# Try acquiring a temporary access token. The token should have a 24h lifespan
#
# Variables
#   _ab_user_enc
#   _ab_pass_enc
#   _ab_authdata
#   AB_User
#   AB_Pass
#   AB_Token
#   response
#   _H2
#
_ab_get_token() {
  _ab_user_enc=$(printf "%s" "$AB_User" | _url_encode)
  _ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode)
  _ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc"

  _H2="Content-Type: application/x-www-form-urlencoded"

  if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then
    _err "Authentication failure"
    return 1
  fi

  AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')"

  if [ -z "$AB_Token" ]; then
    _err "Could not extract access token"
    return 1
  fi

  _debug "Acquired access token"
  return 0
}

#
# Usage: _ab_rest POST "example/endpoint" "password=123"
#
# Perform a REST request using the given method, endpoint and data
#
# Variables
#   _method: $1 - The http method
#   _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API)
#   _data: $3 - The body of the request (optional)
#   _key_trimmed
#   _token_trimmed
#   _ret_code
#   AB_Key
#   AB_Token
#   ARUBABUSINESS_API
#   _H1
#   _H2
#   _H3
#   _H4
#
_ab_rest() {
  _method=$1
  _endpoint="$2"
  _data="$3"

  _key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"')
  _token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"')

  _H1="Accept: application/json"

  if [ -z "$_H2" ]; then
    # Default to application/json
    _H2="Content-Type: application/json"
  fi

  if [ "$_key_trimmed" ]; then
    _H3="Authorization-Key: $_key_trimmed"
  else
    _err "Missing Api Key"
    _ab_cleanup_headers
    return 1
  fi

  if [ "$_token_trimmed" ]; then
    _H4="Authorization: Bearer $_token_trimmed"
  else
    _debug "No access token set"
  fi

  if [ "$_method" != "GET" ]; then
    response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")"
  else
    response="$(_get "$ARUBABUSINESS_API/$_endpoint")"
  fi

  _ret_code=$?

  if [ "$_ret_code" = "0" ] && _ab_call_is_success; then
    # Normalize the json response
    response="$(printf "%s" "$response" | _normalizeJson)"
    _ret_code=0
  else
    _err "Failed to call endpoint: $_endpoint"
    _ret_code=1
  fi

  _ab_cleanup_headers

  return $_ret_code
}

#
# Usage: _ab_cleanup_headers
#
# Unset header variables to avoid interfering with other calls
#
# Variables
#   _H1
#   _H2
#   _H3
#   _H4
#
_ab_cleanup_headers() {
  # Cleanup request headers
  unset _H1 _H2 _H3 _H4 _H5

  # Cleanup response headers
  if [ -f "$HTTP_HEADER" ]; then
    : >"$HTTP_HEADER"
  fi
}

#
# Usage: _ab_call_is_success
#
# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled)
#
# Variables
#   _status
#   _http_status
#   _success_http_codes
#   HTTP_HEADER
#
_ab_call_is_success() {
  _success_http_codes="200 201 202 204"
  if [ -f "$HTTP_HEADER" ]; then
    _http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)
    for _status in $_success_http_codes; do
      if [ "$_status" = "$_http_status" ]; then
        return 0
      fi
    done
  fi

  return 1
}
