Skip to content
This repository has been archived by the owner on Apr 23, 2023. It is now read-only.

Commit

Permalink
fixed bug with parsing for country and city of server when openvpn pr…
Browse files Browse the repository at this point in the history
…otocol was enabled. Updated alfred-workflow.
  • Loading branch information
atticusmatticus committed Feb 10, 2021
1 parent 9c21997 commit 481f7d5
Show file tree
Hide file tree
Showing 23 changed files with 317 additions and 87 deletions.
Binary file modified MullvadVPN.alfredworkflow
Binary file not shown.
140 changes: 117 additions & 23 deletions src/mullvad.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import subprocess
from datetime import datetime

from workflow import Workflow, MATCH_SUBSTRING
from workflow import Workflow3, MATCH_SUBSTRING
from workflow.background import run_in_background

import mullvad_actions
Expand All @@ -19,6 +19,14 @@
#############################

def execute(cmdList):
""" Execute a terminal command from list of arguments
Arguments:
cmdList -- command line command (list of strings)
Returns:
cmd/err -- output of terminal command (tuple of strings)
"""
newEnv = os.environ.copy()
newEnv['PATH'] = '/usr/local/bin:%s' % newEnv['PATH'] # prepend the path to `mullvad` executable to the system path
cmd, err = subprocess.Popen(cmdList,
Expand All @@ -31,32 +39,84 @@ def execute(cmdList):


def get_auto_connect():
""" Get Mullvad Auto-Connect Status
Arguments:
None
Returns:
String of status -- "Autoconnect: on/off"
"""
return execute(['mullvad', 'auto-connect', 'get']).splitlines()


def get_lan():
""" Get Mullvad Local Network Sharing Status
Arguments:
None
Returns:
String of status -- "Local network sharing setting: allow/block"
"""
return execute(['mullvad', 'lan', 'get']).splitlines()


def get_kill_switch():
""" Get Mullvad Kill-Switch Status
Arguments:
None
Returns:
String of status -- "Network traffic will be allowed/blocked when the VPN is disconnected"
"""
return execute(['mullvad', 'always-require-vpn', 'get']).splitlines()


def get_version():
return [execute(['mullvad', 'version']).splitlines()[1].split()[2], execute(['mullvad', 'version']).splitlines()[2].split()[4]] # version [supported, up-to-date]
""" Get Mullvad Version
Arguments:
None
Returns:
List of strings -- [mullvad supported:"true/false", ]
"""
# TODO: simplify this return segment
mullVersion = execute(['mullvad', 'version'])
# print mullVersion
supported = mullVersion.splitlines()[1].split()[2]
if supported == 'true':
supported = True
elif supported == 'false':
supported = False
# print supported
currentVersion = mullVersion.splitlines()[0].split(':')[1].strip()
# print currentVersion
latestVersion = mullVersion.splitlines()[3].split(':')[1].strip()
# print latestVersion
# print currentVersion==latestVersion
return [supported, currentVersion==latestVersion]


def connection_status():
""" Add workflow item of current connection status
Arguments:
None
Returns:
Item -- Connected/Disconnected/Blocked
"""
for status in get_connection():
# print 'status:', status
# print 'status:', status
stat = str(status.split()[2])
# print 'stat:', stat
# print 'stat:', stat
if stat == 'Connected':
countryString, cityString = get_country_city()
# print '{} to: {} {}'.format(stat, cityString, countryString).decode('utf8')
# print ' '.join(status.split()[4:])+'. Select to Disconnect.'
# print '{} to: {} {}'.format(stat, cityString, countryString).decode('utf8')
# print ' '.join(status.split()[4:])+'. Select to Disconnect.'
wf.add_item('{} to: {} {}'.format(stat, cityString, countryString).decode('utf8'),
subtitle=' '.join(status.split()[4:])+'. Select to Disconnect. Type "Relay" to change.',
subtitle=' '.join(status.split()[4:])+'. Select to Disconnect. Type "relay" to change.',
arg='/usr/local/bin/mullvad disconnect',
valid=True,
icon='icons/mullvad_green.png')
Expand All @@ -75,26 +135,39 @@ def connection_status():


def get_country_city():
countryCodeSearch = '({})'.format(get_protocol()[9])
# print 'countryCodeSearch', countryCodeSearch
cityCodeSearch = '({})'.format(get_protocol()[8][0:3])
# print 'cityCodeSearch', cityCodeSearch
""" Get the current country and city relay information
:returns countryString:
"""
# TODO: make this work for OpenVPN as well as Wireguard
getProt = get_protocol()
# print 'getProt: {}'.format(getProt)
sep = getProt.index(',')
# print 'sep: {}'.format(sep)
countryCodeSearch = '({})'.format(getProt[sep+2:sep+4])
# print 'DEBUG: countryCodeSearch', countryCodeSearch #debug
cityCodeSearch = '({})'.format(getProt[sep-3:sep])
# cityCodeSearch = '({})'.format(get_protocol()[8][0:3])
# print 'DEBUG: cityCodeSearch', cityCodeSearch
countries = wf.cached_data('mullvad_country_list',
get_country_list,
max_age=432000)
# print 'countries', countries
# print 'DEBUG: countries', countries #debug
index = [i for i,s in enumerate(countries) if countryCodeSearch in s][0]
relayList = wf.cached_data('mullvad_relay_list',
get_relay_list,
max_age=432000)
countryString = countries[index].split()[:-1][0]
# print countryString
# print countryString #debug
cityString = ' '.join([city[0] for city in relayList[index][1:] if cityCodeSearch in city[0]][0].split()[:-1])
# print cityString
# print cityString #debug
return countryString, cityString


def get_connection():
""" VPN connection tunnel status
:returns: sentence of tunnel status
:type returns: tuple of a single sentence string
"""
return execute(['mullvad', 'status']).splitlines()


Expand All @@ -105,6 +178,7 @@ def check_connection():
valid=True,
icon='icons/mullvad_yellow.png')


def set_kill_switch():
for status in get_kill_switch():
if status == 'Network traffic will be blocked when the VPN is disconnected':
Expand All @@ -119,11 +193,11 @@ def set_kill_switch():


def get_protocol():
return execute(['mullvad','relay','get']).split()
return execute(['mullvad','relay','get'])


def protocol_status():
status = get_protocol()[2]
status = get_connection()[0].split()[4]
wf.add_item('Tunnel-protocol: {}'.format(status),
subtitle='Change tunnel-protocol',
autocomplete='protocol',
Expand Down Expand Up @@ -206,25 +280,39 @@ def add_time_account():


def update_relay_list():
execute(['mullvad', 'relay', 'update']) #TODO add this to its own subroutine that gets run in the background
# TODO add this to its own subroutine that gets run in the background
execute(['mullvad', 'relay', 'update'])


def list_relay_countries(wf, query):
""" List countries with servers
Arguments:
query -- "relay"
"""
# TODO: does `query` need to be here?
# print query
for country in filter_relay_countries(wf, query):
countryName = country.split(' (')[0]
countryCode = country.split('(')[1].split(')')[0]
wf.add_item(country,
subtitle='List cities in {}'.format(countryName),
valid=False, # TABing and RETURN have the same effect. take you to city selection
valid=False, # TABing and RETURN have the same effect, take you to city selection
autocomplete='country:{} '.format(countryCode),
icon='icons/chevron-right-dark.png') #TODO lock icon? or maybe just chevron
icon='icons/chevron-right-dark.png') # TODO lock icon, or maybe just chevron


def filter_relay_countries(wf, query):
""" List contries based on fuzzy match of query
Returns:
List of countries as strings
"""
# print query
countries = wf.cached_data('mullvad_country_list',
get_country_list,
max_age=432000)
# print query
queryFilter = query.split()
# print query, queryFilter
if len(queryFilter) > 1:
return wf.filter(queryFilter[1], countries, match_on=MATCH_SUBSTRING)
return countries
Expand Down Expand Up @@ -258,6 +346,12 @@ def get_relay_list():


def list_relay_cities(wf, query):
""" List cities of country
Argument:
query -- country:`countryCode` where `countryCode` is a two letter abbreviation of a country from list_relay_countries()
Returns:
List of Items of cities
"""
countryCode = query.split(':')[1].split()[0]
for city in filter_relay_cities(wf, countryCode, query):
cityCode = city.split('(')[1].split(')')[0]
Expand Down Expand Up @@ -308,14 +402,14 @@ def main(wf):
# extract query
query = wf.args[0] if len(wf.args) else None # if there's an argument(s) `query` is the first one. Otherwise it's `None`

if not query:
if not query: # starting screen of information.
if wf.cached_data('mullvad_version',
get_version,
max_age=86400)[1] == 'false':
max_age=86400)[1] == False:
update_mullvad()
if wf.cached_data('mullvad_version',
get_version,
max_age=86400)[0] == 'false':
max_age=86400)[0] == False:
unsupported_mullvad()
if wf.cached_data('mullvad_account',
get_account,
Expand Down Expand Up @@ -411,5 +505,5 @@ def main(wf):
#############################

if __name__ == '__main__':
wf = Workflow(update_settings={'github_slug': GITHUB_SLUG})
wf = Workflow3(update_settings={'github_slug': GITHUB_SLUG})
sys.exit(wf.run(main))
Binary file modified src/mullvad.pyc
Binary file not shown.
Binary file modified src/mullvad_actions.pyc
Binary file not shown.
2 changes: 1 addition & 1 deletion src/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9
0.10
Empty file removed src/workflow/.alfredversionchecked
Empty file.
Binary file modified src/workflow/__init__.pyc
Binary file not shown.
Binary file removed src/workflow/__pycache__/__init__.cpython-37.pyc
Binary file not shown.
Binary file removed src/workflow/__pycache__/workflow.cpython-37.pyc
Binary file not shown.
5 changes: 1 addition & 4 deletions src/workflow/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ def _job_pid(name):
if _process_exists(pid):
return pid

try:
os.unlink(pidfile)
except Exception: # pragma: no cover
pass
os.unlink(pidfile)


def is_running(name):
Expand Down
Binary file modified src/workflow/background.pyc
Binary file not shown.
18 changes: 10 additions & 8 deletions src/workflow/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ def install_notifier():
# z.extractall(destdir)
tgz = tarfile.open(archive, 'r:gz')
tgz.extractall(destdir)
assert os.path.exists(n), \
'Notify.app could not be installed in %s' % destdir
if not os.path.exists(n): # pragma: nocover
raise RuntimeError('Notify.app could not be installed in ' + destdir)

# Replace applet icon
icon = notifier_icon_path()
Expand Down Expand Up @@ -253,8 +253,9 @@ def png_to_icns(png_path, icns_path):
try:
iconset = os.path.join(tempdir, 'Icon.iconset')

assert not os.path.exists(iconset), \
'iconset already exists: ' + iconset
if os.path.exists(iconset): # pragma: nocover
raise RuntimeError('iconset already exists: ' + iconset)

os.makedirs(iconset)

# Copy source icon to icon set and generate all the other
Expand Down Expand Up @@ -283,8 +284,9 @@ def png_to_icns(png_path, icns_path):
if retcode != 0:
raise RuntimeError('iconset exited with %d' % retcode)

assert os.path.exists(icns_path), \
'generated ICNS file not found: ' + repr(icns_path)
if not os.path.exists(icns_path): # pragma: nocover
raise ValueError(
'generated ICNS file not found: ' + repr(icns_path))
finally:
try:
shutil.rmtree(tempdir)
Expand Down Expand Up @@ -332,8 +334,8 @@ def ustr(s):
print('converting {0!r} to {1!r} ...'.format(o.png, icns),
file=sys.stderr)

assert not os.path.exists(icns), \
'destination file already exists: ' + icns
if os.path.exists(icns):
raise ValueError('destination file already exists: ' + icns)

png_to_icns(o.png, icns)
sys.exit(0)
Expand Down
12 changes: 5 additions & 7 deletions src/workflow/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class Version(object):
"""

#: Match version and pre-release/build information in version strings
match_version = re.compile(r'([0-9\.]+)(.+)?').match
match_version = re.compile(r'([0-9][0-9\.]*)(.+)?').match

def __init__(self, vstr):
"""Create new `Version` object.
Expand All @@ -247,7 +247,7 @@ def _parse(self, vstr):
else:
m = self.match_version(vstr)
if not m:
raise ValueError('invalid version number: {!r}'.format(vstr))
raise ValueError('invalid version number: ' + vstr)

version, suffix = m.groups()
parts = self._parse_dotted_string(version)
Expand All @@ -257,7 +257,7 @@ def _parse(self, vstr):
if len(parts):
self.patch = parts.pop(0)
if not len(parts) == 0:
raise ValueError('version number too long: {!r}'.format(vstr))
raise ValueError('version number too long: ' + vstr)

if suffix:
# Build info
Expand All @@ -268,11 +268,9 @@ def _parse(self, vstr):
if suffix:
if not suffix.startswith('-'):
raise ValueError(
'suffix must start with - : {0}'.format(suffix))
'suffix must start with - : ' + suffix)
self.suffix = suffix[1:]

# wf().logger.debug('version str `{}` -> {}'.format(vstr, repr(self)))

def _parse_dotted_string(self, s):
"""Parse string ``s`` into list of ints and strings."""
parsed = []
Expand Down Expand Up @@ -521,7 +519,7 @@ def install_update():
path = retrieve_download(Download.from_dict(dl))

wf().logger.info('installing updated workflow ...')
subprocess.call(['open', path])
subprocess.call(['open', path]) # nosec

wf().cache_data(key, no_update)
return True
Expand Down
Binary file modified src/workflow/update.pyc
Binary file not shown.
Loading

0 comments on commit 481f7d5

Please sign in to comment.