#!/bin/bash
readonly script="opennic-up" version="1.2.4"
resolvconf="/etc/resolv.conf"
usefile=0

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

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

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

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

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

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

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

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

check_command() {
  command -v "$1" >/dev/null 2>&1 || error "$1: is required but cannot be found in the environment path"
}

# 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
for needed in awk sort curl fping drill; do
  check_command "$needed"
done

# source opennic-up config
for p in /etc ~/.config/opennic-up; do
  configfile=$p/opennic-up.conf
  [ -r "$configfile" ] && . "$configfile"
done

initdns=${initdns:-"192.3.165.37 147.182.243.49 137.184.12.79"}
# 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}')
echo "$respondingdns"
if [ -z "$respondingdns" ]; then
  # none responding, network may be down, wait for first
  waitdns=$(echo "$initdns" | awk '{print $1}')
  log "Waiting for $waitdns..."
  fping -q -r 10 "$waitdns" && respondingdns="$waitdns"
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
  log "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" >/dev/null
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")

[ -z "$allhosts" ] && error 'API not available'

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

[ "$reliablecount" -lt 1 ] && error 'Not enough OpenNIC servers available'

#pinging the hosts
logn "Pinging $reliablecount hosts to determine the top ones..."
pingresults=$(multiping 15 $reliable)

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

mymaxretain=${maxretain:-3}
[ "$responsivecount" -lt 1 ] && error 'Not enough responsive OpenNIC servers available'
retain=$((mymaxretain > responsivecount ? responsivecount : mymaxretain))

# we retain the top servers for our DNS
log "Selected top $retain hosts:"
myhosts=$(head -n $retain <<< "$responsive")
nameservers=""
for dns in $myhosts; do
  log "$(grep -F "$dns" <<< "$allhosts")"
  nameservers+="nameserver $dns"$'\n'
done
printf "%s" "$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
        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
