mirror of
https://github.com/sshuttle/sshuttle.git
synced 2024-11-28 19:03:26 +01:00
Merge branch 'macapp'
A cute little GUI for sshuttle on MacOS, written using pyobjc. * macapp: ui-macos: call the main binary MacOS/Sshuttle. ui-macos/git-export.do: write the generated app to a branch. ui-macos/default.app.do: get rid of some duplicated files. ui-macos: Actually prompt for passwords instead of assuming a default. ui-macos: Don't enable connecting for hosts with Custom but zero subnets. ui-macos: Smoother log messages in the log window. ui-macos: Notice when we've connected; make debug logs optional. ui-macos: Much better connection status reporting. ui-macos/run.do: a shortcut for running debug.app. ui-macos/*: "a series of unfortunate events."
This commit is contained in:
commit
668441adb6
@ -226,6 +226,8 @@ def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets,
|
|||||||
raise Fatal('expected server init string %r; got %r'
|
raise Fatal('expected server init string %r; got %r'
|
||||||
% (expected, initstring))
|
% (expected, initstring))
|
||||||
debug1('connected.\n')
|
debug1('connected.\n')
|
||||||
|
print 'Connected.'
|
||||||
|
sys.stdout.flush()
|
||||||
if daemon:
|
if daemon:
|
||||||
daemonize()
|
daemonize()
|
||||||
log('daemonizing (%s).\n' % _pidname)
|
log('daemonizing (%s).\n' % _pidname)
|
||||||
|
8
ui-macos/.gitignore
vendored
Normal file
8
ui-macos/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
*.pyc
|
||||||
|
*~
|
||||||
|
/*.nib
|
||||||
|
/debug.app
|
||||||
|
/sources.list
|
||||||
|
/Sshuttle VPN.app
|
||||||
|
/*.tar.gz
|
||||||
|
/*.zip
|
40
ui-macos/Info.plist
Normal file
40
ui-macos/Info.plist
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Sshuttle VPN</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Sshuttle</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>app.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>ca.apenwarr.Sshuttle</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Sshuttle VPN</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSHasLocalizedDisplayName</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSAppleScriptEnabled</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>GNU LGPL Version 2</string>
|
||||||
|
<key>NSMainNibFile</key>
|
||||||
|
<string>MainMenu</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
2340
ui-macos/MainMenu.xib
Normal file
2340
ui-macos/MainMenu.xib
Normal file
File diff suppressed because it is too large
Load Diff
10
ui-macos/UserDefaults.plist
Normal file
10
ui-macos/UserDefaults.plist
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>startAtLogin</key>
|
||||||
|
<false/>
|
||||||
|
<key>autoReconnect</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
1
ui-macos/all.do
Normal file
1
ui-macos/all.do
Normal file
@ -0,0 +1 @@
|
|||||||
|
redo-ifchange debug.app dist
|
BIN
ui-macos/app.icns
Normal file
BIN
ui-macos/app.icns
Normal file
Binary file not shown.
28
ui-macos/askpass.py
Normal file
28
ui-macos/askpass.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import sys, os, re, subprocess
|
||||||
|
|
||||||
|
def askpass(prompt):
|
||||||
|
prompt = prompt.replace('"', "'")
|
||||||
|
|
||||||
|
if 'yes/no' in prompt:
|
||||||
|
return "yes"
|
||||||
|
|
||||||
|
script="""
|
||||||
|
tell application "Finder"
|
||||||
|
activate
|
||||||
|
display dialog "%s" \
|
||||||
|
with title "Sshuttle SSH Connection" \
|
||||||
|
default answer "" \
|
||||||
|
with icon caution \
|
||||||
|
with hidden answer
|
||||||
|
end tell
|
||||||
|
""" % prompt
|
||||||
|
|
||||||
|
p = subprocess.Popen(['osascript', '-e', script], stdout=subprocess.PIPE)
|
||||||
|
out = p.stdout.read()
|
||||||
|
rv = p.wait()
|
||||||
|
if rv:
|
||||||
|
return None
|
||||||
|
g = re.match("text returned:(.*), button returned:.*", out)
|
||||||
|
if not g:
|
||||||
|
return None
|
||||||
|
return g.group(1)
|
1
ui-macos/bits/.gitignore
vendored
Normal file
1
ui-macos/bits/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/runpython
|
1
ui-macos/bits/PkgInfo
Normal file
1
ui-macos/bits/PkgInfo
Normal file
@ -0,0 +1 @@
|
|||||||
|
APPL????
|
23
ui-macos/bits/runpython.c
Normal file
23
ui-macos/bits/runpython.c
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* This rather pointless program acts like the python interpreter, except
|
||||||
|
* it's intended to sit inside a MacOS .app package, so that its argv[0]
|
||||||
|
* will point inside the package.
|
||||||
|
*
|
||||||
|
* NSApplicationMain() looks for Info.plist using the path in argv[0], which
|
||||||
|
* goes wrong if your interpreter is /usr/bin/python.
|
||||||
|
*/
|
||||||
|
#include <Python.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
char *path = strdup(argv[0]), *cptr;
|
||||||
|
char *args[] = {argv[0], "../Resources/main.py", NULL};
|
||||||
|
cptr = strrchr(path, '/');
|
||||||
|
if (cptr)
|
||||||
|
*cptr = 0;
|
||||||
|
chdir(path);
|
||||||
|
free(path);
|
||||||
|
return Py_Main(2, args);
|
||||||
|
}
|
5
ui-macos/bits/runpython.do
Normal file
5
ui-macos/bits/runpython.do
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
exec >&2
|
||||||
|
redo-ifchange runpython.c
|
||||||
|
gcc -Wall -o $3 runpython.c \
|
||||||
|
-I/usr/include/python2.5 \
|
||||||
|
-lpython2.5
|
BIN
ui-macos/chicken-tiny-bw.png
Normal file
BIN
ui-macos/chicken-tiny-bw.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 821 B |
BIN
ui-macos/chicken-tiny-err.png
Normal file
BIN
ui-macos/chicken-tiny-err.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 789 B |
BIN
ui-macos/chicken-tiny.png
Normal file
BIN
ui-macos/chicken-tiny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 810 B |
4
ui-macos/clean.do
Normal file
4
ui-macos/clean.do
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
exec >&2
|
||||||
|
find -name '*~' | xargs rm -f
|
||||||
|
rm -rf *.app *.zip *.tar.gz
|
||||||
|
rm -f bits/runpython *.nib sources.list
|
15
ui-macos/debug.app.do
Normal file
15
ui-macos/debug.app.do
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
redo-ifchange bits/runpython MainMenu.nib
|
||||||
|
rm -rf debug.app
|
||||||
|
mkdir debug.app debug.app/Contents
|
||||||
|
cd debug.app/Contents
|
||||||
|
ln -s ../.. Resources
|
||||||
|
ln -s ../.. English.lproj
|
||||||
|
ln -s ../../Info.plist .
|
||||||
|
ln -s ../../app.icns .
|
||||||
|
|
||||||
|
mkdir MacOS
|
||||||
|
cd MacOS
|
||||||
|
ln -s ../../../bits/runpython Sshuttle
|
||||||
|
|
||||||
|
cd ../../..
|
||||||
|
redo-ifchange $(find debug.app -type f)
|
28
ui-macos/default.app.do
Normal file
28
ui-macos/default.app.do
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
TOP=$PWD
|
||||||
|
redo-ifchange sources.list
|
||||||
|
redo-ifchange Info.plist bits/runpython \
|
||||||
|
$(while read name newname; do echo "$name"; done <sources.list)
|
||||||
|
|
||||||
|
rm -rf "$1.app"
|
||||||
|
mkdir "$1.app" "$1.app/Contents"
|
||||||
|
cd "$1.app/Contents"
|
||||||
|
|
||||||
|
cp "$TOP/Info.plist" .
|
||||||
|
|
||||||
|
mkdir MacOS
|
||||||
|
cp "$TOP/bits/runpython" MacOS/Sshuttle
|
||||||
|
|
||||||
|
mkdir Resources
|
||||||
|
|
||||||
|
cd "$TOP"
|
||||||
|
while read name newname; do
|
||||||
|
[ -z "$name" ] && continue
|
||||||
|
: "${newname:=$name}"
|
||||||
|
outname=$1.app/Contents/Resources/$newname
|
||||||
|
outdir=$(dirname "$outname")
|
||||||
|
[ -d "$outdir" ] || mkdir "$outdir"
|
||||||
|
cp "${name-$newname}" "$outname"
|
||||||
|
done <sources.list
|
||||||
|
|
||||||
|
cd "$1.app"
|
||||||
|
redo-ifchange $(find . -type f)
|
5
ui-macos/default.app.tar.gz.do
Normal file
5
ui-macos/default.app.tar.gz.do
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
exec >&2
|
||||||
|
IFS="
|
||||||
|
"
|
||||||
|
redo-ifchange $1.app
|
||||||
|
tar -czf $3 $1.app/
|
5
ui-macos/default.app.zip.do
Normal file
5
ui-macos/default.app.zip.do
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
exec >&2
|
||||||
|
IFS="
|
||||||
|
"
|
||||||
|
redo-ifchange $1.app
|
||||||
|
zip -q -r $3 $1.app/
|
2
ui-macos/default.nib.do
Normal file
2
ui-macos/default.nib.do
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
redo-ifchange $1.xib
|
||||||
|
ibtool --compile $3 $1.xib
|
1
ui-macos/dist.do
Normal file
1
ui-macos/dist.do
Normal file
@ -0,0 +1 @@
|
|||||||
|
redo-ifchange "Sshuttle VPN.app.zip" "Sshuttle VPN.app.tar.gz"
|
19
ui-macos/git-export.do
Normal file
19
ui-macos/git-export.do
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# update a local branch with pregenerated output files, so people can download
|
||||||
|
# the completed tarballs from github. Since we don't have any real binaries,
|
||||||
|
# our final distribution package contains mostly blobs from the source code,
|
||||||
|
# so this doesn't cost us much extra space in the repo.
|
||||||
|
BRANCH=dist/macos
|
||||||
|
redo-ifchange 'Sshuttle VPN.app'
|
||||||
|
git update-ref refs/heads/$BRANCH origin/$BRANCH '' 2>/dev/null || true
|
||||||
|
|
||||||
|
export GIT_INDEX_FILE=$PWD/gitindex.tmp
|
||||||
|
rm -f "$GIT_INDEX_FILE"
|
||||||
|
git add -f 'Sshuttle VPN.app'
|
||||||
|
|
||||||
|
MSG="MacOS precompiled app package for $(git describe)"
|
||||||
|
TREE=$(git write-tree --prefix=ui-macos)
|
||||||
|
git show-ref refs/heads/$BRANCH >/dev/null && PARENT="-p refs/heads/$BRANCH"
|
||||||
|
COMMITID=$(echo "$MSG" | git commit-tree $TREE $PARENT)
|
||||||
|
|
||||||
|
git update-ref refs/heads/$BRANCH $COMMITID
|
||||||
|
rm -f "$GIT_INDEX_FILE"
|
352
ui-macos/main.py
Normal file
352
ui-macos/main.py
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import sys, os, pty
|
||||||
|
from AppKit import *
|
||||||
|
import my, models, askpass
|
||||||
|
|
||||||
|
def sshuttle_args(host, auto_nets, auto_hosts, nets, debug):
|
||||||
|
argv = [my.bundle_path('sshuttle/sshuttle', ''), '-r', host]
|
||||||
|
assert(argv[0])
|
||||||
|
if debug:
|
||||||
|
argv.append('-v')
|
||||||
|
if auto_nets:
|
||||||
|
argv.append('--auto-nets')
|
||||||
|
if auto_hosts:
|
||||||
|
argv.append('--auto-hosts')
|
||||||
|
argv += nets
|
||||||
|
return argv
|
||||||
|
|
||||||
|
|
||||||
|
class _Callback(NSObject):
|
||||||
|
def initWithFunc_(self, func):
|
||||||
|
self = super(_Callback, self).init()
|
||||||
|
self.func = func
|
||||||
|
return self
|
||||||
|
def func_(self, obj):
|
||||||
|
return self.func(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Callback:
|
||||||
|
def __init__(self, func):
|
||||||
|
self.obj = _Callback.alloc().initWithFunc_(func)
|
||||||
|
self.sel = self.obj.func_
|
||||||
|
|
||||||
|
|
||||||
|
class Runner:
|
||||||
|
def __init__(self, argv, logfunc, promptfunc, serverobj):
|
||||||
|
print 'in __init__'
|
||||||
|
self.id = argv
|
||||||
|
self.rv = None
|
||||||
|
self.pid = None
|
||||||
|
self.fd = None
|
||||||
|
self.logfunc = logfunc
|
||||||
|
self.promptfunc = promptfunc
|
||||||
|
self.serverobj = serverobj
|
||||||
|
self.buf = ''
|
||||||
|
self.logfunc('\nConnecting to %s.\n' % self.serverobj.host())
|
||||||
|
print 'will run: %r' % argv
|
||||||
|
self.serverobj.setConnected_(False)
|
||||||
|
pid,fd = pty.fork()
|
||||||
|
if pid == 0:
|
||||||
|
# child
|
||||||
|
try:
|
||||||
|
os.execvp(argv[0], argv)
|
||||||
|
except Exception, e:
|
||||||
|
sys.stderr.write('failed to start: %r\n' % e)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
os._exit(42)
|
||||||
|
# parent
|
||||||
|
self.pid = pid
|
||||||
|
self.file = NSFileHandle.alloc()\
|
||||||
|
.initWithFileDescriptor_closeOnDealloc_(fd, True)
|
||||||
|
self.cb = Callback(self.gotdata)
|
||||||
|
NSNotificationCenter.defaultCenter()\
|
||||||
|
.addObserver_selector_name_object_(self.cb.obj, self.cb.sel,
|
||||||
|
NSFileHandleDataAvailableNotification, self.file)
|
||||||
|
self.file.waitForDataInBackgroundAndNotify()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
def _try_wait(self, options):
|
||||||
|
if self.rv == None and self.pid > 0:
|
||||||
|
pid,code = os.waitpid(self.pid, options)
|
||||||
|
if pid == self.pid:
|
||||||
|
if os.WIFEXITED(code):
|
||||||
|
self.rv = os.WEXITSTATUS(code)
|
||||||
|
else:
|
||||||
|
self.rv = -os.WSTOPSIG(code)
|
||||||
|
self.serverobj.setConnected_(False)
|
||||||
|
self.serverobj.setError_('VPN process died')
|
||||||
|
self.logfunc('Disconnected.\n')
|
||||||
|
print 'wait_result: %r' % self.rv
|
||||||
|
return self.rv
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
return self._try_wait(0)
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
return self._try_wait(os.WNOHANG)
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
assert(self.pid > 0)
|
||||||
|
print 'killing: pid=%r rv=%r' % (self.pid, self.rv)
|
||||||
|
if self.rv == None:
|
||||||
|
self.logfunc('Disconnecting from %s.\n' % self.serverobj.host())
|
||||||
|
os.kill(self.pid, 15)
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
def gotdata(self, notification):
|
||||||
|
print 'gotdata!'
|
||||||
|
d = str(self.file.availableData())
|
||||||
|
if d:
|
||||||
|
self.logfunc(d)
|
||||||
|
self.buf = self.buf + d
|
||||||
|
if 'Connected.\r\n' in self.buf:
|
||||||
|
self.serverobj.setConnected_(True)
|
||||||
|
self.buf = self.buf[-4096:]
|
||||||
|
if self.buf.strip().endswith(':'):
|
||||||
|
lastline = self.buf.rstrip().split('\n')[-1]
|
||||||
|
resp = self.promptfunc(lastline)
|
||||||
|
add = ' (response)\n'
|
||||||
|
self.buf += add
|
||||||
|
self.logfunc(add)
|
||||||
|
self.file.writeData_(my.Data(resp + '\n'))
|
||||||
|
self.file.waitForDataInBackgroundAndNotify()
|
||||||
|
self.poll()
|
||||||
|
#print 'gotdata done!'
|
||||||
|
|
||||||
|
|
||||||
|
class SshuttleApp(NSObject):
|
||||||
|
def initialize(self):
|
||||||
|
d = my.PList('UserDefaults')
|
||||||
|
my.Defaults().registerDefaults_(d)
|
||||||
|
|
||||||
|
|
||||||
|
class SshuttleController(NSObject):
|
||||||
|
# Interface builder outlets
|
||||||
|
startAtLoginField = objc.IBOutlet()
|
||||||
|
autoReconnectField = objc.IBOutlet()
|
||||||
|
debugField = objc.IBOutlet()
|
||||||
|
routingField = objc.IBOutlet()
|
||||||
|
prefsWindow = objc.IBOutlet()
|
||||||
|
serversController = objc.IBOutlet()
|
||||||
|
logField = objc.IBOutlet()
|
||||||
|
|
||||||
|
servers = []
|
||||||
|
conns = {}
|
||||||
|
|
||||||
|
def _connect(self, server):
|
||||||
|
host = server.host()
|
||||||
|
print 'connecting %r' % host
|
||||||
|
self.fill_menu()
|
||||||
|
def logfunc(msg):
|
||||||
|
print 'log! (%d bytes)' % len(msg)
|
||||||
|
self.logField.textStorage()\
|
||||||
|
.appendAttributedString_(NSAttributedString.alloc()\
|
||||||
|
.initWithString_(msg))
|
||||||
|
self.logField.didChangeText()
|
||||||
|
def promptfunc(prompt):
|
||||||
|
print 'prompt! %r' % prompt
|
||||||
|
return askpass.askpass(prompt)
|
||||||
|
nets_mode = server.autoNets()
|
||||||
|
if nets_mode == models.NET_MANUAL:
|
||||||
|
manual_nets = ["%s/%d" % (i.subnet(), i.width())
|
||||||
|
for i in server.nets()]
|
||||||
|
elif nets_mode == models.NET_ALL:
|
||||||
|
manual_nets = ['0/0']
|
||||||
|
else:
|
||||||
|
manual_nets = []
|
||||||
|
conn = Runner(sshuttle_args(host,
|
||||||
|
auto_nets = nets_mode == models.NET_AUTO,
|
||||||
|
auto_hosts = server.autoHosts(),
|
||||||
|
nets = manual_nets,
|
||||||
|
debug = self.debugField.state()),
|
||||||
|
logfunc=logfunc, promptfunc=promptfunc,
|
||||||
|
serverobj=server)
|
||||||
|
self.conns[host] = conn
|
||||||
|
|
||||||
|
def _disconnect(self, server):
|
||||||
|
host = server.host()
|
||||||
|
print 'disconnecting %r' % host
|
||||||
|
conn = self.conns.get(host)
|
||||||
|
if conn:
|
||||||
|
conn.kill()
|
||||||
|
self.fill_menu()
|
||||||
|
self.logField.textStorage().setAttributedString_(
|
||||||
|
NSAttributedString.alloc().initWithString_(''))
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_connect(self, sender):
|
||||||
|
server = sender.representedObject()
|
||||||
|
server.setWantConnect_(True)
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_disconnect(self, sender):
|
||||||
|
server = sender.representedObject()
|
||||||
|
server.setWantConnect_(False)
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_show(self, sender):
|
||||||
|
self.prefsWindow.makeKeyAndOrderFront_(self)
|
||||||
|
NSApp.activateIgnoringOtherApps_(True)
|
||||||
|
|
||||||
|
@objc.IBAction
|
||||||
|
def cmd_quit(self, sender):
|
||||||
|
NSApp.performSelector_withObject_afterDelay_(NSApp.terminate_,
|
||||||
|
None, 0.0)
|
||||||
|
|
||||||
|
def fill_menu(self):
|
||||||
|
menu = self.menu
|
||||||
|
menu.removeAllItems()
|
||||||
|
|
||||||
|
def additem(name, func, obj):
|
||||||
|
it = menu.addItemWithTitle_action_keyEquivalent_(name, None, "")
|
||||||
|
it.setRepresentedObject_(obj)
|
||||||
|
it.setTarget_(self)
|
||||||
|
it.setAction_(func)
|
||||||
|
def addnote(name):
|
||||||
|
additem(name, None, None)
|
||||||
|
|
||||||
|
any_inprogress = None
|
||||||
|
any_conn = None
|
||||||
|
any_err = None
|
||||||
|
if len(self.servers):
|
||||||
|
for i in self.servers:
|
||||||
|
host = i.host()
|
||||||
|
want = i.wantConnect()
|
||||||
|
connected = i.connected()
|
||||||
|
numnets = len(list(i.nets()))
|
||||||
|
if not host:
|
||||||
|
additem('Connect Untitled', None, i)
|
||||||
|
elif i.autoNets() == models.NET_MANUAL and not numnets:
|
||||||
|
additem('Connect %s (no routes)' % host, None, i)
|
||||||
|
elif want:
|
||||||
|
any_conn = i
|
||||||
|
additem('Disconnect %s' % host, self.cmd_disconnect, i)
|
||||||
|
else:
|
||||||
|
additem('Connect %s' % host, self.cmd_connect, i)
|
||||||
|
if not want:
|
||||||
|
msg = 'Off'
|
||||||
|
elif i.error():
|
||||||
|
msg = 'ERROR - try reconnecting'
|
||||||
|
any_err = i
|
||||||
|
elif connected:
|
||||||
|
msg = 'Connected'
|
||||||
|
else:
|
||||||
|
msg = 'Connecting...'
|
||||||
|
any_inprogress = i
|
||||||
|
addnote(' State: %s' % msg)
|
||||||
|
if i.autoNets() == 0:
|
||||||
|
addnote(' Routes: All')
|
||||||
|
elif i.autoNets() == 2:
|
||||||
|
addnote(' Routes: Auto')
|
||||||
|
else:
|
||||||
|
addnote(' Routes: Custom')
|
||||||
|
else:
|
||||||
|
addnote('No servers defined yet')
|
||||||
|
|
||||||
|
menu.addItem_(NSMenuItem.separatorItem())
|
||||||
|
additem('Preferences...', self.cmd_show, None)
|
||||||
|
additem('Quit Sshuttle VPN', self.cmd_quit, None)
|
||||||
|
|
||||||
|
if any_err:
|
||||||
|
self.statusitem.setImage_(self.img_err)
|
||||||
|
self.statusitem.setTitle_('Error!')
|
||||||
|
elif any_conn:
|
||||||
|
self.statusitem.setImage_(self.img_running)
|
||||||
|
if any_inprogress:
|
||||||
|
self.statusitem.setTitle_('Connecting...')
|
||||||
|
else:
|
||||||
|
self.statusitem.setTitle_('')
|
||||||
|
else:
|
||||||
|
self.statusitem.setImage_(self.img_idle)
|
||||||
|
self.statusitem.setTitle_('')
|
||||||
|
|
||||||
|
def load_servers(self):
|
||||||
|
l = my.Defaults().arrayForKey_('servers') or []
|
||||||
|
sl = []
|
||||||
|
for s in l:
|
||||||
|
host = s.get('host', None)
|
||||||
|
if not host: continue
|
||||||
|
|
||||||
|
nets = s.get('nets', [])
|
||||||
|
nl = []
|
||||||
|
for n in nets:
|
||||||
|
subnet = n[0]
|
||||||
|
width = n[1]
|
||||||
|
net = models.SshuttleNet.alloc().init()
|
||||||
|
net.setSubnet_(subnet)
|
||||||
|
net.setWidth_(width)
|
||||||
|
nl.append(net)
|
||||||
|
|
||||||
|
autoNets = s.get('autoNets', 1)
|
||||||
|
autoHosts = s.get('autoHosts', 1)
|
||||||
|
srv = models.SshuttleServer.alloc().init()
|
||||||
|
srv.setHost_(host)
|
||||||
|
srv.setAutoNets_(autoNets)
|
||||||
|
srv.setAutoHosts_(autoHosts)
|
||||||
|
srv.setNets_(nl)
|
||||||
|
sl.append(srv)
|
||||||
|
self.serversController.addObjects_(sl)
|
||||||
|
self.serversController.setSelectionIndex_(0)
|
||||||
|
|
||||||
|
def save_servers(self):
|
||||||
|
l = []
|
||||||
|
for s in self.servers:
|
||||||
|
host = s.host()
|
||||||
|
if not host: continue
|
||||||
|
nets = []
|
||||||
|
for n in s.nets():
|
||||||
|
subnet = n.subnet()
|
||||||
|
if not subnet: continue
|
||||||
|
nets.append((subnet, n.width()))
|
||||||
|
d = dict(host=s.host(),
|
||||||
|
nets=nets,
|
||||||
|
autoNets=s.autoNets(),
|
||||||
|
autoHosts=s.autoHosts())
|
||||||
|
l.append(d)
|
||||||
|
my.Defaults().setObject_forKey_(l, 'servers')
|
||||||
|
self.fill_menu()
|
||||||
|
|
||||||
|
def awakeFromNib(self):
|
||||||
|
self.routingField.removeAllItems()
|
||||||
|
tf = self.routingField.addItemWithTitle_
|
||||||
|
tf('Send all traffic through this server')
|
||||||
|
tf('Determine automatically')
|
||||||
|
tf('Custom...')
|
||||||
|
|
||||||
|
# Hmm, even when I mark this as !enabled in the .nib, it still comes
|
||||||
|
# through as enabled. So let's just disable it here (since we don't
|
||||||
|
# support this feature yet).
|
||||||
|
self.startAtLoginField.setEnabled_(False)
|
||||||
|
self.startAtLoginField.setState_(False)
|
||||||
|
self.autoReconnectField.setEnabled_(False)
|
||||||
|
self.autoReconnectField.setState_(False)
|
||||||
|
|
||||||
|
self.load_servers()
|
||||||
|
|
||||||
|
# Initialize our menu item
|
||||||
|
self.menu = NSMenu.alloc().initWithTitle_('Sshuttle')
|
||||||
|
bar = NSStatusBar.systemStatusBar()
|
||||||
|
statusitem = bar.statusItemWithLength_(NSVariableStatusItemLength)
|
||||||
|
self.statusitem = statusitem
|
||||||
|
self.img_idle = my.Image('chicken-tiny-bw', 'png')
|
||||||
|
self.img_running = my.Image('chicken-tiny', 'png')
|
||||||
|
self.img_err = my.Image('chicken-tiny-err', 'png')
|
||||||
|
statusitem.setImage_(self.img_idle)
|
||||||
|
statusitem.setHighlightMode_(True)
|
||||||
|
statusitem.setMenu_(self.menu)
|
||||||
|
self.fill_menu()
|
||||||
|
|
||||||
|
models.configchange_callback = my.DelayedCallback(self.save_servers)
|
||||||
|
|
||||||
|
def sc(server):
|
||||||
|
if server.wantConnect():
|
||||||
|
self._connect(server)
|
||||||
|
else:
|
||||||
|
self._disconnect(server)
|
||||||
|
models.setconnect_callback = sc
|
||||||
|
|
||||||
|
|
||||||
|
# Note: NSApplicationMain calls sys.exit(), so this never returns.
|
||||||
|
NSApplicationMain(sys.argv)
|
131
ui-macos/models.py
Normal file
131
ui-macos/models.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
from AppKit import *
|
||||||
|
import my
|
||||||
|
|
||||||
|
|
||||||
|
configchange_callback = setconnect_callback = None
|
||||||
|
|
||||||
|
|
||||||
|
def config_changed():
|
||||||
|
if configchange_callback:
|
||||||
|
configchange_callback()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ip(v):
|
||||||
|
parts = v.split('.')[:4]
|
||||||
|
if len(parts) < 4:
|
||||||
|
parts += ['0'] * (4 - len(parts))
|
||||||
|
for i in range(4):
|
||||||
|
n = my.atoi(parts[i])
|
||||||
|
if n < 0:
|
||||||
|
n = 0
|
||||||
|
elif n > 255:
|
||||||
|
n = 255
|
||||||
|
parts[i] = str(n)
|
||||||
|
return '.'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_width(v):
|
||||||
|
n = my.atoi(v)
|
||||||
|
if n < 0:
|
||||||
|
n = 0
|
||||||
|
elif n > 32:
|
||||||
|
n = 32
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
class SshuttleNet(NSObject):
|
||||||
|
def subnet(self):
|
||||||
|
return getattr(self, '_k_subnet', None)
|
||||||
|
def setSubnet_(self, v):
|
||||||
|
self._k_subnet = v
|
||||||
|
config_changed()
|
||||||
|
@objc.accessor
|
||||||
|
def validateSubnet_error_(self, value, error):
|
||||||
|
#print 'validateSubnet!'
|
||||||
|
return True, _validate_ip(value), error
|
||||||
|
|
||||||
|
def width(self):
|
||||||
|
return getattr(self, '_k_width', 24)
|
||||||
|
def setWidth_(self, v):
|
||||||
|
self._k_width = v
|
||||||
|
config_changed()
|
||||||
|
@objc.accessor
|
||||||
|
def validateWidth_error_(self, value, error):
|
||||||
|
#print 'validateWidth!'
|
||||||
|
return True, _validate_width(value), error
|
||||||
|
|
||||||
|
NET_ALL = 0
|
||||||
|
NET_AUTO = 1
|
||||||
|
NET_MANUAL = 2
|
||||||
|
|
||||||
|
class SshuttleServer(NSObject):
|
||||||
|
def init(self):
|
||||||
|
self = super(SshuttleServer, self).init()
|
||||||
|
config_changed()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def wantConnect(self):
|
||||||
|
return getattr(self, '_k_wantconnect', False)
|
||||||
|
def setWantConnect_(self, v):
|
||||||
|
self._k_wantconnect = v
|
||||||
|
self.setError_(None)
|
||||||
|
config_changed()
|
||||||
|
if setconnect_callback: setconnect_callback(self)
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
return getattr(self, '_k_connected', False)
|
||||||
|
def setConnected_(self, v):
|
||||||
|
print 'setConnected of %r to %r' % (self, v)
|
||||||
|
self._k_connected = v
|
||||||
|
if v: self.setError_(None) # connected ok, so no error
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
return getattr(self, '_k_error', None)
|
||||||
|
def setError_(self, v):
|
||||||
|
self._k_error = v
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def isValid(self):
|
||||||
|
if not self.host():
|
||||||
|
return False
|
||||||
|
if self.autoNets() == NET_MANUAL and not len(list(self.nets())):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def host(self):
|
||||||
|
return getattr(self, '_k_host', None)
|
||||||
|
def setHost_(self, v):
|
||||||
|
self._k_host = v
|
||||||
|
config_changed()
|
||||||
|
@objc.accessor
|
||||||
|
def validateHost_error_(self, value, error):
|
||||||
|
#print 'validatehost! %r %r %r' % (self, value, error)
|
||||||
|
while value.startswith('-'):
|
||||||
|
value = value[1:]
|
||||||
|
return True, value, error
|
||||||
|
|
||||||
|
def nets(self):
|
||||||
|
return getattr(self, '_k_nets', [])
|
||||||
|
def setNets_(self, v):
|
||||||
|
self._k_nets = v
|
||||||
|
config_changed()
|
||||||
|
def netsHidden(self):
|
||||||
|
#print 'checking netsHidden'
|
||||||
|
return self.autoNets() != NET_MANUAL
|
||||||
|
def setNetsHidden_(self, v):
|
||||||
|
config_changed()
|
||||||
|
#print 'setting netsHidden to %r' % v
|
||||||
|
|
||||||
|
def autoNets(self):
|
||||||
|
return getattr(self, '_k_autoNets', NET_AUTO)
|
||||||
|
def setAutoNets_(self, v):
|
||||||
|
self._k_autoNets = v
|
||||||
|
self.setNetsHidden_(-1)
|
||||||
|
config_changed()
|
||||||
|
|
||||||
|
def autoHosts(self):
|
||||||
|
return getattr(self, '_k_autoHosts', True)
|
||||||
|
def setAutoHosts_(self, v):
|
||||||
|
self._k_autoHosts = v
|
||||||
|
config_changed()
|
62
ui-macos/my.py
Normal file
62
ui-macos/my.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import sys, os
|
||||||
|
from AppKit import *
|
||||||
|
import PyObjCTools.AppHelper
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_path(name, typ):
|
||||||
|
if typ:
|
||||||
|
return NSBundle.mainBundle().pathForResource_ofType_(name, typ)
|
||||||
|
else:
|
||||||
|
return os.path.join(NSBundle.mainBundle().resourcePath(), name)
|
||||||
|
|
||||||
|
|
||||||
|
# Load an NSData using a python string
|
||||||
|
def Data(s):
|
||||||
|
return NSData.alloc().initWithBytes_length_(s, len(s))
|
||||||
|
|
||||||
|
|
||||||
|
# Load a property list from a file in the application bundle.
|
||||||
|
def PList(name):
|
||||||
|
path = bundle_path(name, 'plist')
|
||||||
|
return NSDictionary.dictionaryWithContentsOfFile_(path)
|
||||||
|
|
||||||
|
|
||||||
|
# Load an NSImage from a file in the application bundle.
|
||||||
|
def Image(name, ext):
|
||||||
|
bytes = open(bundle_path(name, ext)).read()
|
||||||
|
img = NSImage.alloc().initWithData_(Data(bytes))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# Return the NSUserDefaults shared object.
|
||||||
|
def Defaults():
|
||||||
|
return NSUserDefaults.standardUserDefaults()
|
||||||
|
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# f = DelayedCallback(func, args...)
|
||||||
|
# later:
|
||||||
|
# f()
|
||||||
|
#
|
||||||
|
# When you call f(), it will schedule a call to func() next time the
|
||||||
|
# ObjC event loop iterates. Multiple calls to f() in a single iteration
|
||||||
|
# will only result in one call to func().
|
||||||
|
#
|
||||||
|
def DelayedCallback(func, *args, **kwargs):
|
||||||
|
flag = [0]
|
||||||
|
def _go():
|
||||||
|
if flag[0]:
|
||||||
|
print 'running %r (flag=%r)' % (func, flag)
|
||||||
|
flag[0] = 0
|
||||||
|
func(*args, **kwargs)
|
||||||
|
def call():
|
||||||
|
flag[0] += 1
|
||||||
|
PyObjCTools.AppHelper.callAfter(_go)
|
||||||
|
return call
|
||||||
|
|
||||||
|
|
||||||
|
def atoi(s):
|
||||||
|
try:
|
||||||
|
return int(s)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
3
ui-macos/run.do
Normal file
3
ui-macos/run.do
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
redo-ifchange debug.app
|
||||||
|
exec >&2
|
||||||
|
./debug.app/Contents/MacOS/run
|
14
ui-macos/sources.list.do
Normal file
14
ui-macos/sources.list.do
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
redo-always
|
||||||
|
exec >$3
|
||||||
|
cat <<-EOF
|
||||||
|
app.icns
|
||||||
|
MainMenu.nib English.lproj/MainMenu.nib
|
||||||
|
UserDefaults.plist
|
||||||
|
chicken-tiny.png
|
||||||
|
chicken-tiny-bw.png
|
||||||
|
chicken-tiny-err.png
|
||||||
|
EOF
|
||||||
|
for d in *.py sshuttle/*.py sshuttle/sshuttle sshuttle/compat/*.py; do
|
||||||
|
echo $d
|
||||||
|
done
|
||||||
|
redo-stamp <$3
|
1
ui-macos/sshuttle
Symbolic link
1
ui-macos/sshuttle
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
..
|
14
ui-macos/stupid.py
Normal file
14
ui-macos/stupid.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
pid = os.fork()
|
||||||
|
if pid == 0:
|
||||||
|
# child
|
||||||
|
try:
|
||||||
|
os.setsid()
|
||||||
|
#os.execvp('sudo', ['sudo', 'SSH_ASKPASS=%s' % os.path.abspath('askpass.py'), 'ssh', 'afterlife', 'ls'])
|
||||||
|
os.execvp('ssh', ['ssh', 'afterlife', 'ls'])
|
||||||
|
finally:
|
||||||
|
os._exit(44)
|
||||||
|
else:
|
||||||
|
# parent
|
||||||
|
os.wait()
|
Loading…
Reference in New Issue
Block a user