#!/usr/bin/bash

timeout_limit=2
timeout_count=0

services=(
    "plain:https://ifconfig.me/ip"
    "plain:https://ipinfo.io/ip"
    "plain:https://icanhazip.com"
    "plain:https://api.ipify.org"
    "plain:https://ident.me"
    "plain:https://checkip.amazonaws.com"
    "plain:https://ipecho.net/plain"
    "plain:https://ipapi.co/ip"
    "plain:https://wtfismyip.com/text"
    "plain:https://ifconfig.co/ip"

    "dns:opendns"
    "dns:cloudflare"
    "dns:google"

    "json:https://jsonip.com"
    "json:https://ifconfig.co/json"
    "json:https://ipinfo.io/json"
    "json:https://ipapi.co/json"
    "json:https://api64.ipify.org?format=json"
)

dns_opendns=(dig +short myip.opendns.com @resolver1.opendns.com)
dns_cloudflare=(dig +short txt ch whoami.cloudflare @1.0.0.1)
dns_google=(dig +short txt o-o.myaddr.l.google.com @ns1.google.com)

ipv4_regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'

shuffle_services() {
    local i j tmp

    for ((i=${#services[@]}-1; i>0; i--)); do
        j=$((RANDOM % (i + 1)))

        tmp="${services[i]}"
        services[i]="${services[j]}"
        services[j]="$tmp"
    done
}

record_timeout() {
    ((timeout_count++))

    logger -t getmyip \
        "Timeout count: ${timeout_count}/${timeout_limit}"

    if (( timeout_count >= timeout_limit )); then
        logger -t getmyip "Too many timeouts, exiting"
        echo "Failed: too many timeouts." >&2
        exit 1
    fi
}

is_valid_ip() {
    local ip="$1"

    if [[ $ip =~ $ipv4_regex ]]; then
        IFS='.' read -r -a octets <<< "$ip"

        for octet in "${octets[@]}"; do
            (( octet <= 255 )) || return 1
        done

        return 0
    fi

    if command -v python3 >/dev/null 2>&1; then
        python3 - "$ip" <<'EOF' >/dev/null 2>&1
import ipaddress
import sys

try:
    ipaddress.ip_address(sys.argv[1])
    sys.exit(0)
except Exception:
    sys.exit(1)
EOF
        return $?
    fi

    return 1
}

extract_plain_ip() {
    local ip

    ip=$(echo "$1" | tr -d '\r' | head -n1 | xargs)

    [[ $ip =~ \<.*\> ]] && return 1

    if is_valid_ip "$ip"; then
        printf '%s\n' "$ip"
        return 0
    fi

    return 1
}

extract_json_ip() {
    local data="$1"
    local ip=""

    if command -v jq >/dev/null 2>&1; then
        ip=$(jq -r '
            .ip //
            .query //
            .data //
            .result //
            empty
        ' <<< "$data" | head -n1)
    else
        ip=$(echo "$data" |
            grep -oE '"(ip|query)"[[:space:]]*:[[:space:]]*"[^"]+"' |
            head -n1 |
            sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/')
    fi

    ip=$(echo "$ip" | xargs)

    if is_valid_ip "$ip"; then
        printf '%s\n' "$ip"
        return 0
    fi

    return 1
}

query_plain_service() {
    local url="$1"
    local response rc ip

    response=$(timeout 12 curl \
        -fsSL \
        --connect-timeout 5 \
        --max-time 10 \
        -H "Accept: text/plain" \
        "$url" 2>/dev/null)

    rc=$?

    case "$rc" in
        0)
            ;;
        28|124)
            logger -t getmyip "Timeout from $url (rc=$rc)"
            return 2
            ;;
        *)
            logger -t getmyip "Failure from $url (rc=$rc)"
            return 1
            ;;
    esac

    ip=$(extract_plain_ip "$response") || return 1

    printf '%s\n' "$ip"
    return 0
}

query_json_service() {
    local url="$1"
    local response rc ip

    response=$(timeout 12 curl \
        -fsSL \
        --connect-timeout 5 \
        --max-time 10 \
        -H "Accept: application/json" \
        "$url" 2>/dev/null)

    rc=$?

    case "$rc" in
        0)
            ;;
        28|124)
            logger -t getmyip "Timeout from $url (rc=$rc)"
            return 2
            ;;
        *)
            logger -t getmyip "Failure from $url (rc=$rc)"
            return 1
            ;;
    esac

    ip=$(extract_json_ip "$response") || return 1

    printf '%s\n' "$ip"
    return 0
}

query_dns_service() {
    local provider="$1"
    local response rc ip

    case "$provider" in
        opendns)
            response=$(timeout 6 "${dns_opendns[@]}" 2>/dev/null)
            ;;
        cloudflare)
            response=$(timeout 6 "${dns_cloudflare[@]}" 2>/dev/null)
            ;;
        google)
            response=$(timeout 6 "${dns_google[@]}" 2>/dev/null)
            ;;
        *)
            return 1
            ;;
    esac

    rc=$?

    case "$rc" in
        0)
            ;;
        124)
            logger -t getmyip "DNS timeout ($provider)"
            return 2
            ;;
        *)
            logger -t getmyip "DNS failure ($provider) rc=$rc"
            return 1
            ;;
    esac

    ip=$(echo "$response" |
        tr -d '"' |
        grep -oE '[0-9A-Fa-f:.]+' |
        head -n1)

    if is_valid_ip "$ip"; then
        printf '%s\n' "$ip"
        return 0
    fi

    return 1
}

get_public_ip() {
    local entry type target ip rc

    shuffle_services

    for entry in "${services[@]}"; do

        type="${entry%%:*}"
        target="${entry#*:}"

        case "$type" in
            plain)
                ip=$(query_plain_service "$target")
                rc=$?
                ;;

            json)
                ip=$(query_json_service "$target")
                rc=$?
                ;;

            dns)
                ip=$(query_dns_service "$target")
                rc=$?
                ;;

            *)
                continue
                ;;
        esac

        if (( rc == 2 )); then
            record_timeout
            continue
        fi

        if (( rc != 0 )); then
            continue
        fi

        if [[ -n "$ip" ]] && is_valid_ip "$ip"; then
            logger -t getmyip \
                "Success using $entry -> $ip"

            echo "$ip"
            return 0
        fi
    done

    logger -t getmyip \
        "Failed to retrieve public IP using all services"

    echo "Failed to retrieve public IP using all services." >&2
    return 1
}

get_public_ip