#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_opnsense_info='OPNsense Server
Site: docs.opnsense.org/development/api.html
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_opnsense
Options:
 OPNs_Host Server Hostname. E.g. "opnsense.example.com"
 OPNs_Port Port. Default: "443".
 OPNs_Key API Key
 OPNs_Token API Token
 OPNs_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
Issues: github.com/acmesh-official/acme.sh/issues/2480
'

########  Public functions #####################
#Usage: add _acme-challenge.www.domain.com "123456789ABCDEF0000000000000000000000000000000000000"
#fulldomain
#txtvalue
OPNs_DefaultPort=443
OPNs_DefaultApi_Insecure=0

dns_opnsense_add() {
  fulldomain=$1
  txtvalue=$2

  _opns_check_auth || return 1

  if ! set_record "$fulldomain" "$txtvalue"; then
    return 1
  fi

  return 0
}

#fulldomain
dns_opnsense_rm() {
  fulldomain=$1
  txtvalue=$2

  _opns_check_auth || return 1

  if ! rm_record "$fulldomain" "$txtvalue"; then
    return 1
  fi

  return 0
}

set_record() {
  fulldomain=$1
  new_challenge=$2
  _info "Adding record $fulldomain with challenge: $new_challenge"

  _debug "Detect root zone"
  if ! _get_root "$fulldomain"; then
    _err "invalid domain"
    return 1
  fi

  _debug _domain "$_domain"
  _debug _host "$_host"
  _debug _domainid "$_domainid"
  _return_str=""
  _record_string=""
  _build_record_string "$_domainid" "$_host" "$new_challenge"
  _uuid=""
  if _existingchallenge "$_domain" "$_host" "$new_challenge"; then
    # Update
    if _opns_rest "POST" "/record/setRecord/${_uuid}" "$_record_string"; then
      _return_str="$response"
    else
      return 1
    fi

  else
    #create
    if _opns_rest "POST" "/record/addRecord" "$_record_string"; then
      _return_str="$response"
    else
      return 1
    fi
  fi

  if echo "$_return_str" | _egrep_o "\"result\":\"saved\"" >/dev/null; then
    _opns_rest "POST" "/service/reconfigure" "{}"
    _debug "Record created"
  else
    _err "Error creating record $_record_string"
    return 1
  fi

  return 0
}

rm_record() {
  fulldomain=$1
  new_challenge="$2"
  _info "Remove record $fulldomain with challenge: $new_challenge"

  _debug "Detect root zone"
  if ! _get_root "$fulldomain"; then
    _err "invalid domain"
    return 1
  fi

  _debug _domain "$_domain"
  _debug _host "$_host"
  _debug _domainid "$_domainid"
  _uuid=""
  if _existingchallenge "$_domain" "$_host" "$new_challenge"; then
    # Delete
    if _opns_rest "POST" "/record/delRecord/${_uuid}" "\{\}"; then
      if echo "$response" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then
        _debug "Record deleted"
        _opns_rest "POST" "/service/reconfigure" "{}"
        _debug "Service reconfigured"
      else
        _err "Error deleting record $_host from domain $fulldomain"
        return 1
      fi
    else
      _err "Error requesting deletion of record $_host from domain $fulldomain"
      return 1
    fi
  else
    _info "Record not found, nothing to remove"
  fi

  return 0
}

####################  Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _domainid=domid
#_domain=domain.com
_get_root() {
  domain=$1
  i=2
  p=1
  if _opns_rest "GET" "/domain/searchPrimaryDomain"; then
    _domain_response="$response"
  else
    return 1
  fi

  while true; do
    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
    if [ -z "$h" ]; then
      #not valid
      return 1
    fi
    _debug h "$h"
    lines=$(echo "$_domain_response" | sed 's/{/\n/g')
    for line in $lines; do
      id=$(echo "$line" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",.*\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
      if [ -n "$id" ]; then
        _debug id "$id"
        _host=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
        _domain="${h}"
        _domainid="${id}"
        return 0
      fi
    done
    p=$i
    i=$(_math "$i" + 1)
  done
  _debug "$domain not found"

  return 1
}

_opns_rest() {
  method=$1
  ep=$2
  data=$3
  #Percent encode user and token
  key=$(echo "$OPNs_Key" | tr -d "\n\r" | _url_encode)
  token=$(echo "$OPNs_Token" | tr -d "\n\r" | _url_encode)

  opnsense_url="https://${key}:${token}@${OPNs_Host}:${OPNs_Port:-$OPNs_DefaultPort}/api/bind${ep}"
  export _H1="Content-Type: application/json"
  _debug2 "Try to call api: https://${OPNs_Host}:${OPNs_Port:-$OPNs_DefaultPort}/api/bind${ep}"
  if [ ! "$method" = "GET" ]; then
    _debug data "$data"
    export _H1="Content-Type: application/json"
    response="$(_post "$data" "$opnsense_url" "" "$method")"
  else
    export _H1=""
    response="$(_get "$opnsense_url")"
  fi

  if [ "$?" != "0" ]; then
    _err "error $ep"
    return 1
  fi
  _debug2 response "$response"

  return 0
}

_build_record_string() {
  _record_string="{\"record\":{\"enabled\":\"1\",\"domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"}}"
}

_existingchallenge() {
  if _opns_rest "GET" "/record/searchRecord"; then
    _record_response="$response"
  else
    return 1
  fi
  _uuid=""
  _uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"[01]\",\"domain\":\"[a-z0-9\-]*\",\"%domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2)

  if [ -n "$_uuid" ]; then
    _debug uuid "$_uuid"
    return 0
  fi
  _debug "${2}.${1} record not found"

  return 1
}

_opns_check_auth() {
  OPNs_Host="${OPNs_Host:-$(_readaccountconf_mutable OPNs_Host)}"
  OPNs_Port="${OPNs_Port:-$(_readaccountconf_mutable OPNs_Port)}"
  OPNs_Key="${OPNs_Key:-$(_readaccountconf_mutable OPNs_Key)}"
  OPNs_Token="${OPNs_Token:-$(_readaccountconf_mutable OPNs_Token)}"
  OPNs_Api_Insecure="${OPNs_Api_Insecure:-$(_readaccountconf_mutable OPNs_Api_Insecure)}"

  if [ -z "$OPNs_Host" ]; then
    _err "You don't specify OPNsense address."
    return 1
  else
    _saveaccountconf_mutable OPNs_Host "$OPNs_Host"
  fi

  if ! printf '%s' "$OPNs_Port" | grep '^[0-9]*$' >/dev/null; then
    _err 'OPNs_Port specified but not numeric value'
    return 1
  elif [ -z "$OPNs_Port" ]; then
    _info "OPNSense port not specified. Defaulting to using port $OPNs_DefaultPort"
  else
    _saveaccountconf_mutable OPNs_Port "$OPNs_Port"
  fi

  if ! printf '%s' "$OPNs_Api_Insecure" | grep '^[01]$' >/dev/null; then
    _err 'OPNs_Api_Insecure specified but not 0/1 value'
    return 1
  elif [ -n "$OPNs_Api_Insecure" ]; then
    _saveaccountconf_mutable OPNs_Api_Insecure "$OPNs_Api_Insecure"
  fi
  export HTTPS_INSECURE="${OPNs_Api_Insecure:-$OPNs_DefaultApi_Insecure}"

  if [ -z "$OPNs_Key" ]; then
    _err "you have not specified your OPNsense api key id."
    _err "Please set OPNs_Key and try again."
    return 1
  else
    _saveaccountconf_mutable OPNs_Key "$OPNs_Key"
  fi

  if [ -z "$OPNs_Token" ]; then
    _err "you have not specified your OPNsense token."
    _err "Please create OPNs_Token and try again."
    return 1
  else
    _saveaccountconf_mutable OPNs_Token "$OPNs_Token"
  fi

  if ! _opns_rest "GET" "/general/get"; then
    _err "Call to OPNsense API interface failed. Unable to access OPNsense API."
    return 1
  fi
  return 0
}
