#!/usr/bin/env bash
# Build lines with fixed IPs from a rotating Hybrid line and verify them later.

TARGET=http://api.ipify.org
OUTFILE=pinned.txt
DEFAULT_KIND=

show_help() {
  cat >&2 <<'TXT'
Build lines with fixed IPs:
  hybrid-pin.sh [-t socks5|http] HOST:PORT:LOGIN:PASSWORD [HOW_MANY]

Verify saved lines:
  hybrid-pin.sh verify [-t socks5|http] [FILE]

HOW_MANY defaults to 10, the result goes to pinned.txt.
A socks5:// or http:// prefix on the line overrides -t. With neither, SOCKS5 is used.
TXT
  exit 1
}

split_line() {
  local text=$1 kind=$DEFAULT_KIND
  LEAD=
  if [[ $text =~ ^(socks5h?|http)://(.*)$ ]]; then
    kind=${BASH_REMATCH[1]}
    LEAD=$kind://
    text=${BASH_REMATCH[2]}
  fi
  IFS=: read -r GW_HOST GW_PORT ACCOUNT SECRET <<< "$text"
  case $kind in
    http) GATEWAY=http://$GW_HOST:$GW_PORT ;;
    *) GATEWAY=socks5h://$GW_HOST:$GW_PORT ;;
  esac
}

exit_ip() {
  curl -s -m 15 -x "$GATEWAY" -U "$1:$SECRET" "$TARGET" < /dev/null
}

progress() {
  [ -t 2 ] || return 0
  local part=$1 whole=$2 size=32 on line
  on=$((part * size / whole))
  line=$(printf '%*s' "$on" '' | tr ' ' '#')$(printf '%*s' $((size - on)) '' | tr ' ' '.')
  printf '\r[%s] %3d%%  %d of %d' "$line" $((part * 100 / whole)) "$part" "$whole" >&2
}

clear_bar() {
  if [ -t 2 ]; then printf '\r\033[K' >&2; fi
}

build() {
  [ -n "$1" ] || show_help
  local want=${2:-10} have=0 asked=0 replied=0 used=' ' addr
  case $want in ''|*[!0-9]*|0) show_help ;; esac
  split_line "$1"
  : > "$OUTFILE"
  progress 0 "$want"
  while [ "$have" -lt "$want" ] && [ "$asked" -lt $((want * 3)) ]; do
    asked=$((asked + 1))
    addr=$(exit_ip "$ACCOUNT")
    if ! [[ $addr =~ ^[0-9.]+$ ]]; then
      if [ "$replied" = 0 ] && [ "$asked" -ge 3 ]; then
        clear_bar
        echo "No reply through $GATEWAY. Check the line, its port and the protocol." >&2
        exit 1
      fi
      continue
    fi
    replied=1
    [[ $used == *" $addr "* ]] && continue
    used="$used$addr "
    [ "$(exit_ip "$ACCOUNT-ip-$addr")" = "$addr" ] || continue
    echo "$LEAD$GW_HOST:$GW_PORT:$ACCOUNT-ip-$addr:$SECRET" >> "$OUTFILE"
    have=$((have + 1))
    progress "$have" "$want"
  done
  clear_bar
  echo "Saved $have of $want lines with fixed IPs to $OUTFILE"
}

verify() {
  local file=${1:-$OUTFILE} whole seen=0 same=0 row pinned real
  [ -r "$file" ] || { echo "File not readable: $file" >&2; exit 1; }
  whole=$(grep -c . "$file")
  [ "$whole" -gt 0 ] || { echo "$file has no lines" >&2; exit 1; }
  progress 0 "$whole"
  while IFS= read -r row || [ -n "$row" ]; do
    row=${row%$'\r'}
    [ -n "$row" ] || continue
    split_line "$row"
    pinned='no IP in login'
    [[ $ACCOUNT =~ -ip-([0-9.]+)$ ]] && pinned=${BASH_REMATCH[1]}
    real=$(exit_ip "$ACCOUNT")
    seen=$((seen + 1))
    clear_bar
    if [ "$real" = "$pinned" ]; then
      same=$((same + 1))
      printf '%-15s  same\n' "$pinned"
    else
      printf '%-15s  differs, got %s\n' "$pinned" "${real:-no reply}"
    fi
    progress "$seen" "$whole"
  done < "$file"
  clear_bar
  echo "Verified $seen lines: $same same, $((seen - same)) differ"
}

MODE=build
if [ "$1" = verify ]; then
  MODE=verify
  shift
fi
while getopts 't:' flag; do
  case $flag in
    t) DEFAULT_KIND=$OPTARG ;;
    *) show_help ;;
  esac
done
shift $((OPTIND - 1))
case $DEFAULT_KIND in ''|socks5|http) ;; *) show_help ;; esac

if [ "$MODE" = verify ]; then
  verify "$1"
else
  build "$1" "$2"
fi
