summaryrefslogtreecommitdiffstats
path: root/ui-macos/main.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui-macos/main.py')
-rw-r--r--ui-macos/main.py352
1 files changed, 352 insertions, 0 deletions
diff --git a/ui-macos/main.py b/ui-macos/main.py
new file mode 100644
index 0000000..baa290d
--- /dev/null
+++ b/ui-macos/main.py
@@ -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)