aboutsummarylogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Williams2022-04-21 21:33:11 +0200
committerSimon Williams2022-04-21 21:41:33 +0200
commit78bc1db7a89f1cedbd19af6f49e48e3777eb0a0c (patch)
treee1725cbad80440c54c130d6d11b27dfbe3be41f4
downloadaur-78bc1db7a89f1cedbd19af6f49e48e3777eb0a0c.tar.gz
First commit
-rw-r--r--.SRCINFO14
-rw-r--r--COPYING5
-rw-r--r--PKGBUILD39
-rw-r--r--README.md82
-rwxr-xr-xnordquery.py228
5 files changed, 368 insertions, 0 deletions
diff --git a/.SRCINFO b/.SRCINFO
new file mode 100644
index 000000000000..f36e0a55076a
--- /dev/null
+++ b/.SRCINFO
@@ -0,0 +1,14 @@
+pkgbase = nordquery
+ pkgdesc = A tool to choose NordVPN servers based on filters, written in Python
+ pkgver = 1.0
+ pkgrel = 1
+ url = https://github.com/simonpw/nordquery
+ arch = any
+ license = GPL
+ makedepends = git
+ depends = python>=3.0,
+ depends = python-pycountry
+ source = git+https://github.com/simonpw/nordquery.git
+ md5sums = SKIP
+
+pkgname = nordquery
diff --git a/COPYING b/COPYING
new file mode 100644
index 000000000000..53556406c735
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,5 @@
+This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/PKGBUILD b/PKGBUILD
new file mode 100644
index 000000000000..9d5576d9eda7
--- /dev/null
+++ b/PKGBUILD
@@ -0,0 +1,39 @@
+# Maintainer: Simon Williams <simon@clockcycles.net>
+pkgname=nordquery
+pkgver=1.0
+pkgrel=1
+epoch=
+pkgdesc="A tool to choose NordVPN servers based on filters, written in Python"
+arch=(any)
+url="https://github.com/simonpw/nordquery"
+license=('GPL')
+groups=()
+depends=('python>=3.0', 'python-pycountry')
+makedepends=('git')
+checkdepends=()
+optdepends=()
+provides=()
+conflicts=()
+replaces=()
+backup=()
+options=()
+install=
+changelog=
+source=("git+https://github.com/simonpw/nordquery.git")
+noextract=()
+md5sums=('SKIP')
+
+pkgver() {
+ cd "$srcdir/$pkgname"
+ (
+ set -o pipefail
+ git describe --long 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' ||
+ printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
+ )
+}
+
+package() {
+ cd "$srcdir/$pkgname"
+ install -Dm755 nordquery.py ${pkgdir}/usr/bin/nordquery
+ install -Dm644 COPYING ${pkgdir}/usr/share/licenses/nordquery/COPYING
+}
diff --git a/README.md b/README.md
new file mode 100644
index 000000000000..7f486a5e5eb0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,82 @@
+# nordquery
+
+usage: nordquery [-h] [-u] [-c country] [-f features] [-p protocols] [--list-protocols] [--server-url] [-v]
+
+A tool for choosing NordVPN servers, Version 1.0
+
+## Options:
+
+ `-h, --help` - Show help message and exit
+ `-u, --update` - Update the server database
+ `-c COUNTRY, --country COUNTRY` - Filter by country.
+ `-f FEATURES, --number FEATURES` - Filter by server features, see list below
+ `-p PROTOCOLS, --protocols PROTOCOLS` - Filter by server protocols, see list below
+ `--list-protocols` - List the protocol options available
+ `--server-url` - Override the default server database url (https://nordvpn.com/api/server)
+ `-v, --verbose` - Be verbose
+
+## Countries:
+Countries can be selected using their english name or ISO3166 code, some abbreviations will work but cannot be guaranteed.
+Use `any` to explicitly match any country, overrides the default in configuration file.
+
+## Features:
+The following selections can be made using the -f argument:
+
+- `any` : Don't filter based on features, overrides the config file default
+- `sta` : Standard VPN servers
+- `ded` : Dedicated IP address servers
+- `doub` : Double VPN servers
+- `obf` : Obfuscated servers
+- `p2p` : Servers optimised for P2P usage
+- `tor` : Onion over VPN servers
+
+For details of these servers and their intended usage check the NordVPN documentation.
+
+## Protocols:
+
+The following protocols can be selected with the -p argument:
+
+- `ikev2`
+- `openvpn_udp`
+- `openvpn_tcp`
+- `socks`
+- `proxy`
+- `pptp`
+- `l2tp`
+- `openvpn_xor_udp`
+- `openvpn_xor_tcp`
+- `proxy_cybersec`
+- `proxy_ssl`
+- `proxy_ssl_cybersec`
+- `ikev2_v6`
+- `open_udp_v6`
+- `open_tcp_v6`
+- `wireguard_udp`
+- `openvpn_udp_tls_crypt`
+- `openvpn_tcp_tls_crypt `
+- `openvpn_dedicated_udp`
+- `openvpn_dedicated_tcp`
+- `skylark`
+- `mesh_relay `
+
+`--list-protcols` will list these options.
+For details of these protocols and their usage consult the NordVPN documentation.
+
+## Configuration file:
+
+A user configuration file can be used in this location: /home/[USER]/.config/nordquery/nordquery.conf
+
+The structure is:
+
+`[defaults]`
+`always_update = yes/no` - forces an update of the server database with every query
+`country = xx` or `any` - an ISO3166 code for the default country
+`features = xxx xxx` or `any` - a list of the default features to use, e.g. `sta p2p`
+`db_path` - path to store the server database file, overrides the default (`/home/[USER]/.cache/nordquery`)
+`db_filename` - name for server database file, overrides the default (`server.db`)
+
+These settings will be overriden by command line arguments.
+
+
+Simon Williams 14/04/22
+simon@clockcycles.net
diff --git a/nordquery.py b/nordquery.py
new file mode 100755
index 000000000000..efd7669d79f4
--- /dev/null
+++ b/nordquery.py
@@ -0,0 +1,228 @@
+#! /usr/bin/python
+# nordquery - A tool to find the right NordVPN server, written in Python
+# Copyright (C) 2022 Simon Williams
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import sys, os
+from pathlib import Path
+from urllib import request
+import json
+import argparse
+import configparser
+from pycountry import countries
+import socket
+from timeit import default_timer as timer
+
+features_dict = {'any':'','sta':'Standard VPN servers', 'ded':'Dedicated IP','doub':'Double VPN','obf':'Obfuscated Servers','p2p':'P2P', 'tor':'Onion Over VPN'}
+
+def list_protocols():
+ print('ikev2\nopenvpn_udp\nopenvpn_tcp\nsocks\nproxy\npptp\nl2tp\nopenvpn_xor_udp\nopenvpn_xor_tcp\nproxy_cybersec\n'
+ 'proxy_ssl\nproxy_ssl_cybersec\nikev2_v6\nopen_udp_v6\nopen_tcp_v6\nwireguard_udp\nopenvpn_udp_tls_crypt\n'
+ 'openvpn_tcp_tls_crypt\nopenvpn_dedicated_udp\nopenvpn_dedicated_tcp\nskylark\nmesh_relay')
+ return
+
+def ping(ip,port):
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.settimeout(1)
+ try:
+ s_start = timer()
+ s.connect((ip,port))
+ s.shutdown(socket.SHUT_RD)
+ s_stop = timer()
+ return int((s_stop - s_start)*1000)
+ except:
+ return -1
+ finally:
+ s.close()
+
+def main(argv):
+ db_path = '.cache/nordquery'
+ db_filename = 'servers.db'
+
+ config_path = '.config/nordquery'
+ config_filename = 'nordquery.conf'
+
+ parser = argparse.ArgumentParser(description='A tool to find the right NordVPN server')
+ #usage='%(prog)s [-c COUNTRY -f FEATURES -p PROTOCOL]',)
+
+ parser.add_argument('-c', '--country', help='Filter by country (ISO3166 codes)', type=str, default='')
+ parser.add_argument('-f', '--features', help='Filter by features (any, sta(ndard), doub(le), obf(uscated), p2p, tor',
+ type=str, default='', nargs='+', choices=['any', 'sta', 'ded', 'doub', 'obf', 'p2p', 'tor'], metavar='features')
+ parser.add_argument('-p', '--protocol', help='Filter by supported protocol', type=str, default='', nargs='+',
+ choices=['ikev2','openvpn_udp','openvpn_tcp','socks','proxy','pptp','l2tp','openvpn_xor_udp','openvpn_xor_tcp',
+ 'proxy_cybersec','proxy_ssl','proxy_ssl_cybersec','ikev2_v6','open_udp_v6','open_tcp_v6','wireguard_udp',
+ 'openvpn_udp_tls_crypt','openvpn_tcp_tls_crypt','openvpn_dedicated_udp','openvpn_dedicated_tcp','skylark','mesh_relay'],
+ metavar='protocol')
+ parser.add_argument('--list-protocols', help='Show list of protocols', action='store_true')
+ parser.add_argument('-u', '--update', help='Update the server database', action='store_true')
+ parser.add_argument('--server-url', help='Override the default server info URL', type=str, default='https://nordvpn.com/api/server')
+ parser.add_argument('-v', '--verbose', help='Be verbose', action='store_true')
+
+ args = parser.parse_args()
+
+ if(args.list_protocols):
+ list_protocols()
+ sys.exit()
+
+ ## Parse config file
+ config_fullpath = os.path.join(os.environ['HOME'], config_path)
+
+ confparse = configparser.ConfigParser()
+ if(os.path.exists(os.path.join(config_fullpath, config_filename))): #Check file exists
+ if(args.verbose): print('Config found: ' + os.path.join(config_fullpath, config_filename))
+ confparse.read(os.path.join(config_fullpath, config_filename))
+ try:
+ defaults = confparse['defaults']
+ if(args.verbose):
+ print('Always update:', confparse.get('defaults','always_update',fallback=''))
+ print('Country:', confparse.get('defaults','country',fallback=''))
+ print('Features:', confparse.get('defaults','features',fallback=''))
+ except:
+ print("Warning: Config file invalid")
+ ####
+
+ ## Get server database
+ if(confparse.get('defaults','db_path',fallback='') != ''):
+ db_fullpath = defaults['db_path']
+ else:
+ db_fullpath = os.path.join(os.environ['HOME'], db_path)
+ if(os.path.exists(db_fullpath) == False):
+ if(args.verbose): print('DB path does not exist, making folder ' + db_fullpath)
+ os.mkdir(db_fullpath)
+
+ if(confparse.get('defaults','db_filename',fallback='') != ''):
+ db_filename = defaults['db_filename']
+
+ # Download server file if requested by argument, config or if file doesn't exist
+ if(args.update or
+ (not os.path.exists(os.path.join(db_fullpath, db_filename))) or
+ confparse.get('defaults','always_update',fallback='no') == 'yes'):
+ if(args.verbose): print('Downloading server information from ' + args.server_url)
+ try:
+ data = request.urlopen(args.server_url).read() # Download the server file
+ db = json.loads(data) # Parse the JSON data
+ with open(os.path.join(db_fullpath, db_filename), 'w') as outfile:
+ if(args.verbose): print('Writing DB file to ' + os.path.join(db_fullpath, db_filename))
+ json.dump(db, outfile) # Write the db file to disk
+ except request.URLError:
+ print('Download from:', args.server_url, 'failed')
+ sys.exit()
+ except json.decoder.JSONDecodeError:
+ print('JSON decoding error, download from', args.server_url, 'invalid')
+ sys.exit()
+ except PermissionError:
+ print('You don\'t have permission to write to', str(db_fullpath))
+ sys.exit()
+ else:
+ if(args.verbose): print('Reading server information from ' + os.path.join(db_fullpath, db_filename))
+ try:
+ with open(os.path.join(db_fullpath, db_filename), 'r') as infile:
+ db = json.load(infile) # Load the db file from disk
+ except:
+ print('Reading database file', os.path.join(db_fullpath, db_filename), 'failed')
+ sys.exit()
+
+ ##Build server dictionary
+ if(args.verbose): print('Building server dictionary')
+ db_dict = {}
+ for server in db:
+ db_dict[server.get('id')] = server # Link the server info to ID
+
+ if(args.verbose): print('Database contains ' + str(len(db_dict)) + ' servers')
+
+ ##Parse country argument
+ if(args.country.upper() == 'ANY'):
+ country_code = '' # Match any country
+ elif(args.country != ''):
+ if(args.country.upper() == 'UK'): ##Cleanup UK special case
+ args.country = 'GB'
+ try:
+ country_list = countries.search_fuzzy(args.country) # Attempt to find the country
+ except LookupError:
+ print('Error: country', args.country, 'not found')
+ sys.exit()
+ country_code = country_list[0].alpha_2 # Take the first country from the returned list
+ if(args.verbose): print('Country: ' + country_list[0].name)
+ elif(confparse.get('defaults','country',fallback='ANY').upper() != 'ANY'):
+ country_code = defaults['country'].upper() # The country in config file
+ else:
+ country_code = '' # Match any country
+
+ ##Parse features argument
+ features_input = []
+ if(args.features != ''):
+ features_input = args.features
+ elif(confparse.get('defaults','features',fallback='') != ''):
+ for string in defaults['features'].split(' '):
+ features_input.append(string)
+ else:
+ features_input = ''
+ #print(features_input)
+
+ parsed_features = []
+ for f in features_input:
+ parsed_features.append(features_dict[f])
+ if(args.verbose): print('Features: ' + features_dict[f])
+ #print(parsed_features)
+
+ if(args.verbose and args.protocol != ''): print('Protocols:', args.protocol)
+
+ list_servers = []
+
+ ##Filter the server list
+ for server in db_dict.items():
+ features = []
+ if(parsed_features != []): # Parse the features list
+ for feature in server[1]['categories']:
+ features.append(feature['name'])
+
+ protocols = []
+ if(args.protocol != ''): # Parse the protocol list
+ for protocol in server[1]['features'].keys():
+ if(server[1]['features'][protocol] == True):
+ protocols.append(protocol)
+
+ if((server[1]['flag'] == country_code or country_code == '') and # Match country code
+ (set(parsed_features).issubset(set(features)) or parsed_features == ['']) and # Match features
+ (set(args.protocol).issubset(set(protocols)) or args.protocol == '')): # Match protocols
+ list_servers.append((server[1]['domain'],server[1]['load'],server[1]['ip_address'])) # Add matched server to list
+
+ if(args.verbose):
+ print('Found ' + str(len(list_servers)) + ' servers')
+ for server in list_servers:
+ print(server[0] + ' load: ' + str(server[1]))
+
+ if(list_servers != []):
+ list_servers.sort(key=lambda y: y[1]) # Sort the list of matched servers
+ for server in list_servers:
+ if('Obfuscated Servers' in parsed_features):
+ port = 80 # Obfuscated servers don't open port 443
+ else:
+ port = 443
+ pinged = ping(server[2],port)
+ if(pinged != -1):
+ if(args.verbose): print('Recommended server: ')
+ print(server[0].split('.')[0])
+ if(args.verbose): print('Ping:', pinged, 'ms')
+ break
+ else:
+ print(server[0], 'timed out')
+ else:
+ if(args.verbose): print('No matching server found')
+
+ return
+
+if __name__ == '__main__':
+ main(sys.argv[1:])