Scripting Fastest VPN Server and UNIX Bash Variables

I'm looking to find the preferred VPN Server for SurfShark. I have done something similar before with NordVPN too. But I cannot get my script to work in a modular parameterised fashion.

The following command line queries all surfshark vpn servers and will return the one in GB with the lowest load.

curl --silent "https://api.surfshark.com/v3/server/clusters" | jq -r -c 'map(select(.countryCode == "GB")) | sort_by(.load) | limit(1;.[]) | [.countryCode, .connectionName, .load] | "(.[1])"'

However, when I try to use variables in a script, I can replace the URL "https://api.surfshark.com/v3/server/clusters" with a variable, but not the countryCode.

Example
vpncmd=https://api.surfshark.com/v3/server/clusters
location='"GB"'
limit=10
capacity=101
echo "$0: Server: Looking for recommended server in '$location' with less than '$capacity%' load"
mycmd=(curl --silent "$vpncmd" | jq -r -c 'map(select(.countryCode =='$location' and .load <= '$capacity')) | sort_by(.load) | limit('$limit';.[]) | [.countryCode, .connectionName, .load] | "(.[1])"')
echo "*** RECOMMENDED SERVER: $mycmd"

Can anyone help with the variable substitution in the subcommand. $vpncmd is substituted, but I cannot find a way to substitute the other parameters in the jq command after the pipe. I've tried using $() to execute the subcommand, and eval. I've also tried using an arrange to pass the command sue to the spaces etc where it could treated as separate input variables. Any help would really be appreciated. I'm clearly doing something really stupid.

OpenWrt's default shell is ash, not bash. The full bash is available as a package. ash does not implement arrays and many other features of bash.

Other than that, this really isn't an OpenWrt question.

2 Likes

It is running on my OpenWRT router so it kind of is relevant. The script requires features of BASH so it will not operate under ash. Bash is declared as the shell within the script and Bash is installed via opkg.
Lots of users connect to the VPN at the router so I'm sure other people will be interested

That part does actually work. It is the fact I cannot replace $limit or $capacity with a shell variable or arg from getops. An ideas how to get BASH to substitute these variables in the subcommand?

on bash under openers the script works - I'll do what you say, but its not replacing the variables. Taking these double quotes out screws its up, it needs those to format the output.
jq: error: syntax error, unexpected INVALID_CHARACTER (Unix shell quoting issues?) at , line 1:
map(select(.countryCode == "GB")) | sort_by(.load) | limit(1;.[]) | [.countryCode, .connectionName, .load] | (.[1])
jq: 1 compile error
Hardcoded it all works....
root: /usr/local/bin/surfshark-server-find: Starting...
root: /usr/local/bin/surfshark-server-find: Server: Looking for recommended server in '"GB"' with less than '101%' load
uk-lon.prod.surfshark.com

Can you substitute a variable within the jq command in order to definite the location or server load?
This fully works on OpenWRT:
curl --silent "https://api.surfshark.com/v3/server/clusters" | jq -r -c 'map(select(.countryCode == "GB")) | sort_by(.load) | limit(1;.[]) | [.countryCode, .connectionName, .load] | "(.[1])"'

But instead of GB, say I wanted to search in the US - how can I get a variable in the command and get Bash to interpret .e.g. $location (where location = US)

This works for me:

vpncmd="https://api.surfshark.com/v3/server/clusters"
location="GB"
limit="10"
capacity="101"
mycmd="$(curl --silent "${vpncmd}" \
| jq -r -c "map(select(.countryCode == \"${location}\" \
and .load <= \"${capacity}\")) \
| sort_by(.load) \
| limit(${limit};.[]) \
| [.countryCode, .connectionName, .load] \
| (.[1])")"
echo "${mycmd}"

You don't even need bash, just curl and jq.

2 Likes

It's not working for you as you're trying to do variable expansion within single quotes. In bash, variables will not be expanded when they're in single quotes.

1 Like

Ahhhh - I'll try that

Hi vgaetera - there's other parts of my script that need bash - trying what you have given me now - thank you

1 Like

Star - Thank you so much!

Can't believe I missed that - I'v been here hours

#!/bin/ash
  
myvar="some test data"

# expands
echo "this is my test data: $myvar"

# expands
echo "this is my test data: '$myvar'"

# does not expand
echo 'this is my test data: $myvar'

# does not expand
echo 'this is my test data: "$myvar"'

Sorry for the delay - been trying to get it all working. The overall script is below. What I have noticed is that I need to encapsulate the location (-l in args) with double quotes or it doesn't work. This is jg is called in the function get_servers

Also, I use bash because I check location against an array. That said, It would be better if I could extract the country codes from the list of available server i.e. US, UK. NL etc. This I think can be done using jg, but haven't quite worked it out yet...output=$(curl --silent "https://api.surfshark.com/v3/server/clusters" | jq -s -c '.[] | map(.countryCode) | reduce .[] as $i ({}; setpath([$i]; getpath([$i]) + 1) )
| to_entries | .[] | { "ISO Country Code": .key, "No of Servers" } '

Would really welcome any feedback, comments or tidying up!

# Script: surfshark-server-find
# Author: Ian Jones ian@ianjones.org.uk
# Date: December 2017

logger -s "$0: Starting..."

# Dependencies check
if ((BASH_VERSINFO[0] < 4))
then 
  2>&1 echo "Error: This script requires at least Bash 4.0"
  exit 1
fi
type curl >/dev/null 2>&1 || { 2>&1 echo "Missing dependency: This script requires curl"; exit 1; }
type jq >/dev/null 2>&1 || { 2>&1 echo "Missing dependency: This script requires jq 1.5"; exit 1; }


# Reset in case getopts has been used previously in the shell.
OPTIND=1

#Declare variables
vpncmd=https://api.surfshark.com/v3/server/clusters
location="GB"
bare=false
technology=openvpn_udp
capacity="50" # Default capacity value is 30%
limit="10" # Default limit value is 10

usage() {
   cat <<EOF

Usage: [-r] [-b -l -c -n -v -o -t]
where:
    -r output the recommended server for your location and exit
    -l 2-letter ISO 3166-1 country code (ex: us, uk, de)
    -c current server load, integer between 1-100 (defaults to 30)
    -n limits number of results, integer between 1-100 (defaults to 20)
    -o output list of SurfShark country identifiers
    -b returns a single server name for specified country code
    -t server type (ex: openvpn_udp, openvpn_tcp)

EOF
   exit 0
}

get_servers() {
  if [[ "$bare" == false ]];
  then
    if [[ "$limit" -gt 1 ]];
    then
      plural="s"
    else
      plural=""  
    fi
    logger -s "$0: Looking for up to $limit server$plural located in ${location^^} with server load lower than $capacity%..."
      
      shellcmd="$(curl --silent "${vpncmd}" \
      | jq -r -c "map(select(.countryCode == \"${location}\" \
      and .load <= \"${capacity}\")) \
      | sort_by(.load) \
      | limit(${limit};.[]) \
      | [.countryCode, .connectionName, .load] \
      | (.[1])")"
      echo "${shellcmd}"

   logger -s "$0: Server: $shellcmd"
   logger -s "$0: Completed"
  fi
}


iso_codes=(
  'ad' 'ae' 'af' 'ag' 'ai' 'al' 'am' 'ao' 'aq' 'ar' 'as' 'at' 'au' 'aw' 'ax'
  'az' 'ba' 'bb' 'bd' 'be' 'bf' 'bg' 'bh' 'bi' 'bj' 'bl' 'bm' 'bn' 'bo' 'bq'
  'br' 'bs' 'bt' 'bv' 'bw' 'by' 'bz' 'ca' 'cc' 'cd' 'cf' 'cg' 'ch' 'ci' 'ck'
  'cl' 'cm' 'cn' 'co' 'cr' 'cu' 'cv' 'cw' 'cx' 'cy' 'cz' 'de' 'dj' 'dk' 'dm'
  'do' 'dz' 'ec' 'ee' 'eg' 'eh' 'er' 'es' 'et' 'fi' 'fj' 'fk' 'fm' 'fo' 'fr'
  'ga' 'gb' 'gd' 'ge' 'gf' 'gg' 'gh' 'gi' 'gl' 'gm' 'gn' 'gp' 'gq' 'gr' 'gs'
  'gt' 'gu' 'gw' 'gy' 'hk' 'hm' 'hn' 'hr' 'ht' 'hu' 'id' 'ie' 'il' 'im' 'in'
  'io' 'iq' 'ir' 'is' 'it' 'je' 'jm' 'jo' 'jp' 'ke' 'kg' 'kh' 'ki' 'km' 'kn'
  'kp' 'kr' 'kw' 'ky' 'kz' 'la' 'lb' 'lc' 'li' 'lk' 'lr' 'ls' 'lt' 'lu' 'lv'
  'ly' 'ma' 'mc' 'md' 'me' 'mf' 'mg' 'mh' 'mk' 'ml' 'mm' 'mn' 'mo' 'mp' 'mq'
  'mr' 'ms' 'mt' 'mu' 'mv' 'mw' 'mx' 'my' 'mz' 'na' 'nc' 'ne' 'nf' 'ng' 'ni'
  'nl' 'no' 'np' 'nr' 'nu' 'nz' 'om' 'pa' 'pe' 'pf' 'pg' 'ph' 'pk' 'pl' 'pm'
  'pn' 'pr' 'ps' 'pt' 'pw' 'py' 'qa' 're' 'ro' 'rs' 'ru' 'rw' 'sa' 'sb' 'sc'
  'sd' 'se' 'sg' 'sh' 'si' 'sj' 'sk' 'sl' 'sm' 'sn' 'so' 'sr' 'ss' 'st' 'sv'
  'sx' 'sy' 'sz' 'tc' 'td' 'tf' 'tg' 'th' 'tj' 'tk' 'tl' 'tm' 'tn' 'to' 'tr'
  'tt' 'tv' 'tw' 'tz' 'ua' 'ug' 'uk' 'um' 'us' 'uy' 'uz' 'va' 'vc' 've' 'vg'
  'vi' 'vn' 'vu' 'wf' 'ws' 'ye' 'yt' 'za' 'zm' 'zw'
  )


array_contains () { 
  local array="$1[@]"
  local seeking=$2
  local in=1
  for element in "${!array}"; do
    if [[ $element == "$seeking" ]]; then
      in=0
      break
    fi
  done
  return $in
}

while getopts ":l:c:n:t:orbh" opt; do
    case "$opt" in
    h)
      usage
      exit 0
      ;;
    o)
      output=$(curl --silent "https://api.surfshark.com/v3/server/clusters" | jq -s -c '.[] | map(.countryCode) | reduce .[] as $i ({}; setpath([$i]; getpath([$i]) + 1) ) | to_entries | .[] | { "ISO Country Code": .key, "No of Servers"
: .value }')
      logger -s "$0: Server: $output"
      echo "$output"
      logger -s "$0: Completed"
      exit 0
      ;;
    t)
      technology=$OPTARG >&2;
      ;;
    b)
      bare=true
      ;;
    l)
      location=printf("$OPTARG") >&2;
      array_contains iso_codes "${OPTARG,,}"  && location=${OPTARG,,} >&2 || \
        { 
          >&2 echo
          >&2 echo "Invalid iso country code parameter."
          >&2 echo "Please provide a valid ISO 3166-1 country code to -l."
          >&2 echo "(ex: -l us)"
          exit 1
        }
      ;;
    c)
      if [[ "$OPTARG" =~ ^[0-9]+$ ]] && [ "$OPTARG" -ge 1 -a "$OPTARG" -le 100 ]; 
      then 
        capacity=$OPTARG >&2; 
      else
        {
          >&2 echo
          >&2 echo "Invalid capacity parameter."
          >&2 echo "Please provide an integer between 1-100 to -c."
          >&2 echo "(ex: -c 90)"
          exit 1
        }
      fi
      ;;
    n)
      if [[ "$OPTARG" =~ ^[0-9]+$ ]] && [ "$OPTARG" -ge 1 -a "$OPTARG" -le 100 ]; 
      then 
        limit=$OPTARG >&2; 
      else
        { 
          >&2 echo "Invalid limit parameter."
          >&2 echo "Please provide an integer between 1-100 to -n."
          >&2 echo "(ex: -n 50)"
          exit 1
        }
      fi
      ;;
    r)
      #Set Limit to 1 and utilisation to 101 for Recommended Server
      limit="1"
      capacity="101"
      logger -s "$0: Server: Looking for recommended server in '$location' with less than '$capacity%' load"

      shellcmd="$(curl --silent "${vpncmd}" \
      | jq -r -c "map(select(.countryCode == \"${location}\" \
      and .load <= \"${capacity}\")) \
      | sort_by(.load) \
      | limit(${limit};.[]) \
      | [.countryCode, .connectionName, .load] \
      | (.[1])")"
      echo "${shellcmd}"

      logger -s "$0: Server: $shellcmd"
      logger -s "$0: Completed"
      exit 0
      ;;

    :)
      >&2 echo
      >&2 echo "Option -$OPTARG requires an argument."
      >&2 usage
      ;;
    \?)
      >&2 echo
      >&2 echo "Invalid option: -$OPTARG"
      >&2 usage
      ;;
    esac
done

get_servers

exit 0

you need to wrap your code in ``` at the beginning and end otherwise it's unreadable.

1 Like

sorry done thanks - one reason its not working is getups is lower case so I need to do a tr on location

I don't really have the inclination to try to read and understand your code. Perhaps you'll find the following helpful.

#!/bin/bash
  
surfshark=$(curl --silent https://api.surfshark.com/v3/server/clusters)
countrycodes=$(echo "$surfshark" | jq -r -c '.[].countryCode' | sort | uniq)

for i in $countrycodes; do

        servers=$(echo "$surfshark" | jq -r -c ".[] | select( .countryCode == \"$i\" ) | .connectionName")

        echo "Servers for country code: $i"
        for j in $servers; do
                echo $j
        done

        lowest_load=$(echo "$surfshark" | jq -r -c "[ .[] | select( .countryCode == \"$i\" ).load  ] | min")
        echo "lowest load = $lowest_load"

        server_lowest_load=$(echo "$surfshark" |  jq -r -c ".[] | select( (.countryCode == \"$i\") and (.load == $lowest_load) ) | .connectionName")
        echo "server with lowest load: $server_lowest_load"

        echo

done
2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.