#!/bin/bash
script="opennic-up"
version="1.2"

resolvconf="/etc/resolv.conf"
usefile=0

# pings run in parallel, send $1 pings to each target
multiping() {
  fping -q -p 400 -r 0 -c "$1" "${@:2}" 2>&1
}

# dns lookup nameserver hostname
dnslookup() {
  drill A "$2" @"$1" | awk '$1 == "'"$2"'." && $3 == "IN" && $4 == "A" {print $5; exit}'
}

showhelp() {
    printf -- "usage:  %s [options]\n" "$script"
    printf -- "options:\n"
    printf -- "   -q         quiet, show less information\n"
    printf -- "   -v         display version\n"
    printf -- "   -h         help\n"
    printf -- "   -f <file>  custom resolv.conf file\n"
}

log () {
    if [ "$quiet" -eq 0 ]; then echo "$@" >&2; fi
}

logn () {
    if [ "$quiet" -eq 0 ]; then echo -n "$@" >&2; fi
}

error() {
  printf "ERROR: %s\n" "$1" >&2
}

warning() {
  printf "WARNING: %s\n" "$1" >&2
}

apicurl() {
  curl --silent --connect-timeout 60 --resolve "$apihost:443:$apiip" "$1"  
}

# arguments handling
quiet=0
while getopts ":hvqf:" opt; do
    case "$opt" in
    h)
        showhelp
        exit 0
        ;;
    q)  quiet=1
        ;;
    v)  echo "$script $version"
        exit 0
        ;;
    f)  resolvconf=$OPTARG
        usefile=1
        ;;
    :)  echo "Missing option argument for -$OPTARG" >&2
        exit 1
        ;;
    ?) echo "Invalid argument -$OPTARG" >&2
       showhelp
       exit 1
       ;;
    esac
done

# check needed tools are present
neededlist="awk sort curl fping drill"
for needed in $neededlist; do
  command -v "$needed" >/dev/null 2>&1 && continue
  echo "$needed: is required but cannot be found in the environment path" >&2
  exit 1
done

# source opennic-up config
configpathlist=(/etc ~/.config/opennic-up)
for p in "${configpathlist[@]}"; do
  configfile=$p/opennic-up.conf
  if [ -r "$configfile" ]; then
    . "$configfile"
  fi
done

initdns=${initdns:-"185.121.177.177 169.239.202.202 172.98.193.42"}
# retrieve first responding dns from initdns list
log "Selecting DNS among $initdns..."
respondingdns=$(multiping 2 $initdns | awk -F/ '$5 + 0.0 < 10' | awk '{print $1;exit}')
if [ -z "$respondingdns" ]; then
  # none responding, network may be down, wait for first
  waitdns=$(echo "$initdns" | awk '{print $1}')
  log "Waiting for $waitdns..."
  if fping -q -r 10 "$waitdns"; then respondingdns="$waitdns"; fi
fi
apihost=${apihost:-"api.opennicproject.org"}
if [ -n "$respondingdns" ]; then
  log "Using DNS $respondingdns to retrieve $apihost's IP"
  apiip=$(dnslookup "$respondingdns" "$apihost")
fi
apiip=${apiip:-"116.203.98.109"}
log "Using $apiip as API host"

# record my IP in whitelist if my account login parameters have been provided
if [ -n "$user" ] && [ -n "$auth" ]; then
  logn "Updating whitelist with IP for user: $user "
  wlapiip=${wlapiip:-"161.97.219.82"}
  curl --silent --connect-timeout 60 --insecure "https://$wlapiip/ip/update/?user=$user&auth=$auth"
fi

# query the API: list format, ipv4 only, 200 sites, no server admin sorting, including servers with blocklist and IP whitelisting
apiurl="https://$apihost/geoip/?list&ipv=4&res=200&adm=0&bl&wl"
log "$apiurl"
allhosts=$(apicurl "$apiurl")

if [ -z "$allhosts" ]; then
  error 'API not available'
  exit 1
fi

# filter hosts with more than 90% reliability
myminreliability=${minreliability:-90}
reliable=$(echo "$allhosts" | awk -F# '$3 + 0.0 > '"$myminreliability" | awk -F# '{print $1}')
reliablecount=$(echo "$reliable" | wc -l)

if [ "$reliablecount" -ge 2 ]; then
  #pinging the hosts
  logn "Pinging $reliablecount hosts to determine the top ones..."
  pingresults=$(multiping 20 $reliable)

  # packet loss must be below 10%, then sort the servers by their average response time, eventually keep only the IP column
  responsive=$(echo "$pingresults" | awk -F/ '$5 + 0.0 < 10' | sort -t/ -nk8 | awk '{print $1}')
  responsivecount=$(echo "$responsive" | wc -l)
  log "resulting in $responsivecount responsive hosts"

  mymaxretain=${maxretain:-3}
  if [ "$responsivecount" -ge 2 ]; then
    retain=$((mymaxretain > responsivecount ? responsivecount : mymaxretain))

    # we retain the top servers for our DNS
    log "Selected top $retain hosts:"
    myhosts=$(echo "$responsive" | head -n $retain)
    nameservers=""
    for dns in $myhosts; do
      log "$(echo "$allhosts" | grep "$dns")"
      nameservers=$nameservers"nameserver $dns"$'\n'
    done
    echo -n "$nameservers"

    if [ $usefile -eq 0 ] && command -v nmcli >/dev/null 2>&1; then
      # nmcli: replace with our DNS all active connections
      for id in $(nmcli -terse -fields UUID connection show --active); do
        currentdnss=$(nmcli -terse -fields ipv4.dns connection show "$id" | cut -d: -f2- | tr "," "\n")
        if [ "$(echo "$currentdnss" | sort)" == "$(echo "$myhosts" | sort)" ]; then
            log 'No DNS change'
        else
            #statements
            for dns in $currentdnss; do
              nmcli connection modify "$id" -ipv4.dns "$dns"
            done

            for dns in $myhosts; do
              nmcli connection modify "$id" +ipv4.dns "$dns"
            done
            log "Updating $id"
            nmcli connection up "$id" >/dev/null
            log 'Successful DNS update'
        fi
      done
    else
      # resolv.conf
      touch "$resolvconf"
      currentdnss=$(grep '^nameserver ' "$resolvconf" | cut -d' ' -f2)
      if [ "$(echo "$currentdnss" | sort)" == "$(echo "$myhosts" | sort)" ]; then
          log 'No DNS change'
      else
        if [ -w "$resolvconf" ]; then
          log "Updating $resolvconf"
          otherlines=$(grep -v '^nameserver ' "$resolvconf")
          echo "$otherlines"$'\n'"$nameservers" > "$resolvconf"
          log 'Successful DNS update'
        else
          warning "no write access to '$resolvconf', no change"
        fi
      fi
    fi
  else
    error 'not enough responsive OpenNIC servers available'
    exit 1
  fi
else
  error 'not enough OpenNIC servers available'
  exit 1
fi

