├── README └── nordvpn-server-find /README: -------------------------------------------------------------------------------- 1 | Small script to find the fastest NordVPN servers, filtering by location and 2 | current capacity (i.e. server load). 3 | 4 | Super useful if you use a third-party VPN client (like Viscosity) and want to 5 | quickly find out the fastest server to connect to. 6 | 7 | The script calls an undocumented NordVPN API endpoint for quick and reliable 8 | results. 9 | 10 | USAGE 11 | 12 | nordvpn-server-find -r|(-l LOCATION [-c CAPACITY=30] [-n LIMIT=20] [-q]) 13 | 14 | -r recommended Just output the recommended server for your location and exit, 15 | ignoring other options 16 | -q quiet Just output the best result for the given location, ignoring 17 | the -n and the -c option 18 | -l location 2-letter ISO 3166-1 country code (ex: us, uk, de) 19 | -c capacity Current server load, integer between 1-100 (defaults to 30) 20 | -n limit Limits number of results, integer between 1-100 (defaults to 21 | 20) 22 | 23 | The -r and -q flags can be useful for scripting. For example, on macOS this 24 | script gets Viscosity to automatically connect to the recommended server for 25 | your location: 26 | 27 | #!/usr/bin/osascript 28 | tell application "Viscosity" 29 | connect "$(nordvpn-server-find -r).tcp" 30 | end tell 31 | 32 | If -q is given, manually set capacity and limit are ignored. If -r is given all 33 | other options are ignored. 34 | 35 | DEPENDENCIES 36 | 37 | - Bash 4 38 | - jq 1.5 39 | 40 | BUGS 41 | 42 | Please report here: https://github.com/mrzool/nordvpn-server-find/issues 43 | 44 | AUTHOR 45 | 46 | Mattia Tezzele 47 | 48 | LICENSE 49 | 50 | This program is distributed under the GNU General Public License. 51 | -------------------------------------------------------------------------------- /nordvpn-server-find: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Script: nordvpn-server-find 3 | # Repository URL: https://github.com/mrzool/nordvpn-server-find 4 | # First release: December 2017 5 | # Last updated: April 2022 6 | # Author: Mattia Tezzele 7 | 8 | # Dependencies check 9 | if ((BASH_VERSINFO[0] < 4)); then 10 | echo 2>&1 "Error: This script requires at least Bash 4.0" 11 | exit 1 12 | fi 13 | 14 | type jq >/dev/null 2>&1 || { 15 | echo 2>&1 "Missing dependency: This script requires jq 1.5" 16 | exit 1 17 | } 18 | 19 | # Reset in case getopts has been used previously in the shell. 20 | OPTIND=1 21 | 22 | quiet=0 23 | location=false 24 | capacity=30 # Default capacity value is 30% 25 | limit=20 # Default limit value is 20 26 | 27 | iso_codes=( 28 | 'ad' 'ae' 'af' 'ag' 'ai' 'al' 'am' 'ao' 'aq' 'ar' 'as' 'at' 'au' 'aw' 'ax' 29 | 'az' 'ba' 'bb' 'bd' 'be' 'bf' 'bg' 'bh' 'bi' 'bj' 'bl' 'bm' 'bn' 'bo' 'bq' 30 | 'br' 'bs' 'bt' 'bv' 'bw' 'by' 'bz' 'ca' 'cc' 'cd' 'cf' 'cg' 'ch' 'ci' 'ck' 31 | 'cl' 'cm' 'cn' 'co' 'cr' 'cu' 'cv' 'cw' 'cx' 'cy' 'cz' 'de' 'dj' 'dk' 'dm' 32 | 'do' 'dz' 'ec' 'ee' 'eg' 'eh' 'er' 'es' 'et' 'fi' 'fj' 'fk' 'fm' 'fo' 'fr' 33 | 'ga' 'gb' 'gd' 'ge' 'gf' 'gg' 'gh' 'gi' 'gl' 'gm' 'gn' 'gp' 'gq' 'gr' 'gs' 34 | 'gt' 'gu' 'gw' 'gy' 'hk' 'hm' 'hn' 'hr' 'ht' 'hu' 'id' 'ie' 'il' 'im' 'in' 35 | 'io' 'iq' 'ir' 'is' 'it' 'je' 'jm' 'jo' 'jp' 'ke' 'kg' 'kh' 'ki' 'km' 'kn' 36 | 'kp' 'kr' 'kw' 'ky' 'kz' 'la' 'lb' 'lc' 'li' 'lk' 'lr' 'ls' 'lt' 'lu' 'lv' 37 | 'ly' 'ma' 'mc' 'md' 'me' 'mf' 'mg' 'mh' 'mk' 'ml' 'mm' 'mn' 'mo' 'mp' 'mq' 38 | 'mr' 'ms' 'mt' 'mu' 'mv' 'mw' 'mx' 'my' 'mz' 'na' 'nc' 'ne' 'nf' 'ng' 'ni' 39 | 'nl' 'no' 'np' 'nr' 'nu' 'nz' 'om' 'pa' 'pe' 'pf' 'pg' 'ph' 'pk' 'pl' 'pm' 40 | 'pn' 'pr' 'ps' 'pt' 'pw' 'py' 'qa' 're' 'ro' 'rs' 'ru' 'rw' 'sa' 'sb' 'sc' 41 | 'sd' 'se' 'sg' 'sh' 'si' 'sj' 'sk' 'sl' 'sm' 'sn' 'so' 'sr' 'ss' 'st' 'sv' 42 | 'sx' 'sy' 'sz' 'tc' 'td' 'tf' 'tg' 'th' 'tj' 'tk' 'tl' 'tm' 'tn' 'to' 'tr' 43 | 'tt' 'tv' 'tw' 'tz' 'ua' 'ug' 'uk' 'um' 'us' 'uy' 'uz' 'va' 'vc' 've' 'vg' 44 | 'vi' 'vn' 'vu' 'wf' 'ws' 'ye' 'yt' 'za' 'zm' 'zw' 45 | ) 46 | 47 | array_contains() { 48 | local array="$1[@]" 49 | local seeking=$2 50 | local in=1 51 | for element in "${!array}"; do 52 | if [[ $element == "$seeking" ]]; then 53 | in=0 54 | break 55 | fi 56 | done 57 | return $in 58 | } 59 | 60 | while getopts ":l:c:n:rhq" opt; do 61 | case "$opt" in 62 | h) 63 | echo 64 | echo "nordvpn-server-find -r|(-l LOCATION [-c CAPACITY=30] [-n LIMIT=20] [-q])" 65 | echo 66 | echo "-r $(tput smul)recommended$(tput rmul) just output the recommended server for your location and exit, ignoring other options" 67 | echo "-q $(tput smul)quiet$(tput rmul) just output the best result for the given location, ignoring the -n and the -c option" 68 | echo "-l $(tput smul)location$(tput rmul) 2-letter ISO 3166-1 country code (ex: us, uk, de)" 69 | echo "-c $(tput smul)capacity$(tput rmul) current server load, integer between 1-100 (defaults to 30)" 70 | echo "-n $(tput smul)limit$(tput rmul) limits number of results, integer between 1-100 (defaults to 20)" 71 | echo 72 | exit 0 73 | ;; 74 | q) 75 | quiet=1 76 | limit=1 77 | ;; 78 | r) 79 | rec_server=$(curl --silent "https://nordvpn.com/wp-admin/admin-ajax.php?action=servers_recommendations" | jq -r '.[0].hostname') 80 | echo "$rec_server" 81 | exit 0 82 | ;; 83 | l) 84 | array_contains iso_codes "${OPTARG,,}" && location=${OPTARG,,} >&2 || 85 | { 86 | echo >&2 "Invalid location parameter." 87 | echo >&2 "Please provide a valid ISO 3166-1 country code to -l." 88 | echo >&2 "(ex: -l us)" 89 | exit 1 90 | } 91 | ;; 92 | c) 93 | if [[ "$OPTARG" =~ ^[0-9]+$ ]] && [ "$OPTARG" -ge 1 -a "$OPTARG" -le 100 ]; then 94 | capacity=$OPTARG >&2 95 | else 96 | { 97 | echo >&2 "Invalid capacity parameter." 98 | echo >&2 "Please provide an integer between 1-100 to -c." 99 | echo >&2 "(ex: -c 90)" 100 | exit 1 101 | } 102 | fi 103 | ;; 104 | n) 105 | if [[ $quiet -eq 1 ]]; then 106 | limit=1 107 | elif [[ "$OPTARG" =~ ^[0-9]+$ ]] && [ "$OPTARG" -ge 1 -a "$OPTARG" -le 100 ]; then 108 | limit=$OPTARG >&2 109 | else 110 | { 111 | echo >&2 "Invalid limit parameter." 112 | echo >&2 "Please provide an integer between 1-100 to -n." 113 | echo >&2 "(ex: -n 50)" 114 | exit 1 115 | } 116 | fi 117 | ;; 118 | :) 119 | echo >&2 "Option -$OPTARG requires an argument." 120 | echo >&2 "Use -h to show the help." 121 | exit 1 122 | ;; 123 | \?) 124 | echo >&2 "Invalid option: -$OPTARG" 125 | echo >&2 "Use -h to show the help." 126 | exit 1 127 | ;; 128 | esac 129 | done 130 | 131 | if [[ "$location" == false ]]; then 132 | echo >&2 "A location parameter is required." 133 | echo >&2 "Please provide a valid location parameter using the -l option." 134 | echo >&2 "(ex: -l us)" 135 | echo >&2 "Use -h to show the help." 136 | exit 1 137 | fi 138 | 139 | if [[ $quiet -ne 1 ]]; then 140 | # Output the following only if writing to a terminal 141 | if [ -t 1 ]; then 142 | echo 143 | echo "Looking for servers located in $(tput bold)${location^^}$(tput sgr0) with server load lower than $(tput bold)$capacity%$(tput sgr0)..." 144 | echo 145 | fi 146 | fi 147 | 148 | servers=$(curl --silent https://nordvpn.com/api/server/stats) 149 | 150 | # Declare and populate array 151 | declare -A results 152 | while IFS="=" read -r key value; do 153 | results[$key]="$value" 154 | done < <(jq --compact-output -r --arg location "$location" --arg capacity "$capacity" --arg limit "$limit" \ 155 | '[. | 156 | to_entries[] | 157 | {key: .key, value: .value.percent} | 158 | select(.value <= ($capacity|tonumber)) | 159 | select(.key|contains($location))] | 160 | sort_by(.value) | 161 | from_entries | 162 | to_entries | 163 | map("\(.key)=\(.value|tostring)") | 164 | limit(($limit|tonumber);.[])' <<<"$servers") 165 | 166 | # Print out results 167 | 168 | if [ ${#results[@]} -eq 0 ]; then 169 | if [[ $quiet -eq 1 ]]; then 170 | exit -1 171 | fi 172 | # Colored output only if writing to a terminal 173 | if [ -t 1 ]; then 174 | echo >&2 "$(tput setaf 1)No servers found :(" 175 | else 176 | echo >&2 "No servers found :(" 177 | fi 178 | else 179 | for key in "${!results[@]}"; do 180 | if [[ $quiet -eq 1 ]]; then 181 | echo "$key" 182 | else 183 | echo -e "$(tput setaf 6 && tput bold)$key $(tput sgr0) ${results[$key]}%" 184 | fi 185 | done | 186 | # if [ -t 1] checks if script is writing to a terminal. 187 | # If not, strip ANSI color codes from text stream. 188 | awk '{print $NF,$0}' | sort -n | cut -f2- -d' ' | column -t | 189 | if [ -t 1 ]; then cat; else sed -r "s/[[:cntrl:]]\[[0-9]{1,3}m//g"; fi 190 | fi 191 | --------------------------------------------------------------------------------