From 34ea1ed8b7ec3188af378100dc43616e8d304856 Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Sat, 22 Jan 2011 16:52:07 -0800 Subject: [PATCH] MacOS precompiled app package for sshuttle-0.45 --- Sshuttle VPN.app/Contents/Info.plist | 40 + Sshuttle VPN.app/Contents/MacOS/Sshuttle | Bin 0 -> 8896 bytes .../Resources/English.lproj/MainMenu.nib | Bin 0 -> 27923 bytes .../Contents/Resources/UserDefaults.plist | 10 + Sshuttle VPN.app/Contents/Resources/app.icns | Bin 0 -> 110343 bytes .../Contents/Resources/askpass.py | 28 + .../Contents/Resources/chicken-tiny-bw.png | Bin 0 -> 821 bytes .../Contents/Resources/chicken-tiny-err.png | Bin 0 -> 789 bytes .../Contents/Resources/chicken-tiny.png | Bin 0 -> 810 bytes Sshuttle VPN.app/Contents/Resources/main.py | 352 +++++ Sshuttle VPN.app/Contents/Resources/models.py | 131 ++ Sshuttle VPN.app/Contents/Resources/my.py | 62 + .../Contents/Resources/sshuttle/assembler.py | 26 + .../Contents/Resources/sshuttle/client.py | 356 +++++ .../Resources/sshuttle/compat/__init__.py | 0 .../Resources/sshuttle/compat/ssubprocess.py | 1305 +++++++++++++++++ .../Contents/Resources/sshuttle/firewall.py | 304 ++++ .../Contents/Resources/sshuttle/helpers.py | 37 + .../Contents/Resources/sshuttle/hostwatch.py | 277 ++++ .../Contents/Resources/sshuttle/main.py | 122 ++ .../Contents/Resources/sshuttle/options.py | 201 +++ .../Contents/Resources/sshuttle/server.py | 176 +++ .../Contents/Resources/sshuttle/ssh.py | 95 ++ .../Contents/Resources/sshuttle/sshuttle | 122 ++ .../Contents/Resources/sshuttle/ssnet.py | 520 +++++++ .../Contents/Resources/sshuttle/ssyslog.py | 16 + Sshuttle VPN.app/Contents/Resources/stupid.py | 14 + 27 files changed, 4194 insertions(+) create mode 100644 Sshuttle VPN.app/Contents/Info.plist create mode 100755 Sshuttle VPN.app/Contents/MacOS/Sshuttle create mode 100644 Sshuttle VPN.app/Contents/Resources/English.lproj/MainMenu.nib create mode 100644 Sshuttle VPN.app/Contents/Resources/UserDefaults.plist create mode 100644 Sshuttle VPN.app/Contents/Resources/app.icns create mode 100644 Sshuttle VPN.app/Contents/Resources/askpass.py create mode 100644 Sshuttle VPN.app/Contents/Resources/chicken-tiny-bw.png create mode 100644 Sshuttle VPN.app/Contents/Resources/chicken-tiny-err.png create mode 100644 Sshuttle VPN.app/Contents/Resources/chicken-tiny.png create mode 100644 Sshuttle VPN.app/Contents/Resources/main.py create mode 100644 Sshuttle VPN.app/Contents/Resources/models.py create mode 100644 Sshuttle VPN.app/Contents/Resources/my.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/assembler.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/client.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.py create mode 100755 Sshuttle VPN.app/Contents/Resources/sshuttle/main.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/options.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/server.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py create mode 100755 Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py create mode 100644 Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.py create mode 100644 Sshuttle VPN.app/Contents/Resources/stupid.py diff --git a/Sshuttle VPN.app/Contents/Info.plist b/Sshuttle VPN.app/Contents/Info.plist new file mode 100644 index 0000000..b495531 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + Sshuttle VPN + CFBundleExecutable + Sshuttle + CFBundleIconFile + app.icns + CFBundleIdentifier + ca.apenwarr.Sshuttle + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Sshuttle VPN + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.0.0 + CFBundleSignature + ???? + CFBundleVersion + 0.0.0 + LSUIElement + 1 + LSHasLocalizedDisplayName + + NSAppleScriptEnabled + + NSHumanReadableCopyright + GNU LGPL Version 2 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/Sshuttle VPN.app/Contents/MacOS/Sshuttle b/Sshuttle VPN.app/Contents/MacOS/Sshuttle new file mode 100755 index 0000000000000000000000000000000000000000..76f25e262404f6d207968cda7a7d64738f8bd8c0 GIT binary patch literal 8896 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

I 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 z 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 RM*2|?bM9l5{~s~z{{ipL3T*%Y literal 0 HcmV?d00001 diff --git a/Sshuttle VPN.app/Contents/Resources/UserDefaults.plist b/Sshuttle VPN.app/Contents/Resources/UserDefaults.plist new file mode 100644 index 0000000..7467434 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/UserDefaults.plist @@ -0,0 +1,10 @@ + + + + + startAtLogin + + autoReconnect + + + diff --git a/Sshuttle VPN.app/Contents/Resources/app.icns b/Sshuttle VPN.app/Contents/Resources/app.icns new file mode 100644 index 0000000000000000000000000000000000000000..620b97776ec2abb96b8360c5ab6cbe4a0254b85a GIT binary patch literal 110343 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 literal 0 HcmV?d00001 diff --git a/Sshuttle VPN.app/Contents/Resources/askpass.py b/Sshuttle VPN.app/Contents/Resources/askpass.py new file mode 100644 index 0000000..9690c0d --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/askpass.py @@ -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) diff --git a/Sshuttle VPN.app/Contents/Resources/chicken-tiny-bw.png b/Sshuttle VPN.app/Contents/Resources/chicken-tiny-bw.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef418d8b7b3bac47654eec95ea557198be4346f GIT binary patch literal 821 zcmV-51Iqk~P)H{>_t)3gH)^$-Z`(Gy-7btVP)ecIYVk&+u>s%` z04x9x&(F_yn$6~NZ*MP=QbGs;#u$PiaDCrjYqeTy#u#X=!8wNz0)!A4V*mhYnl_7~ z`0deX;Ogq?tK;Kigkgv*%TN>r;y8ws5}i&5j4>EvkYyRNEJL2>NYfNqmcbZtq|sCxj0D5cN#_xHa!I5=2YSy_48bzKM{ptS}e1VRW%DNz&!(ljmV_4&5bGep2YQY}#zt zeIEdL{-y^{a!?+|ar}$c8c`JeR20Quw@vytv(bE!=C?{k00000NkvXXu0mjfj-rhn literal 0 HcmV?d00001 diff --git a/Sshuttle VPN.app/Contents/Resources/chicken-tiny-err.png b/Sshuttle VPN.app/Contents/Resources/chicken-tiny-err.png new file mode 100644 index 0000000000000000000000000000000000000000..29845134b4fc045ecb814ee86c1e06b3a7d4a361 GIT binary patch literal 789 zcmV+w1M2*VP)-goCs-+P_OyqRR2I87RAZ6p+NA%Y;q?+-*N z1eaBCp&K`@S}EO$2yV2Xh%N*{BvK0%8<6;sB zB0=NX-ox+zJBM>Ec3ze-W=tvNyZK#HLneP`H;Ed}*6PRB4uQ(a<)+@Nym;oh=7BMt z87rTyTjxu^^&f11IA-sJ=s3>g(bC?zYHw(yUwDQ<_V7dQbF%EZF~(H&#u$6GQbFMD zei&N^aU3_cgC44ko_uy`{=RViN9HScAj)AfyO&?N(D~B3_&vVoJ@hBVejh(=;AAH) zZ~4jL+F9T89{VSt@A(fL_sdtm*2pQ0d_-Pmtw-ea#3yUDa`k1UR6uQa@ddFFu@S+R zt7w@8dCLi|`>uQFZ)4wmB&eM~r;$00oJPqEMn3TOM8!hkFpwW?T&eo>$k){fUoiaI%eqUq)eovLtuJSm?V~Ax)kD$L zeO4tU(n1Q{+z5z7NYJb~rTE=w<@qR%uWdrN9=}ma4I5)d>&4PjRqTDQHy4k%Iwb_s zbf6HX5Mu`B7&%e)k2O}BZ*Dc+?ngQ9F8APJFL=kZa{m1{7N&|ss*RQ_h2W2~c{RJ7 zM87PquDrh?|KF(~F1tGl;P?C(`Zt#2EbUloz=|AHE0 z#`jn2ulTkaA9u?K7NjfP{`$2n%UU~zW?6PiEGn_Wx_ootyCg~1+TG4i+u8mFN}nH0 T1cqW300000NkvXXu0mjfW>0+s literal 0 HcmV?d00001 diff --git a/Sshuttle VPN.app/Contents/Resources/chicken-tiny.png b/Sshuttle VPN.app/Contents/Resources/chicken-tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..b1d9ab0c89f3c5baf8b1830e9a3a2f23b0c6b9f2 GIT binary patch literal 810 zcmV+_1J(SAP)2sRH%#Z&w!!wl2<*>{m0@{g?igA`rCfF8Vs1LTM{^H9784rx6kZyHNdRl+B>Z_{n;hLd9;4mkzb7Y}(}EYv5#76M&OdpoAHH}UT?&P| z`7LO2+S$`hxl|gELZjs{pl2AS9+(UNOv&>>sBd%2((N5xUK5%m;ubKlL$!;m= zqEiUK@c};o4jmNffqrwiT4DJpG`f1b`RUPv9D!3_vQt4}l;6gaF(c5C-+Mb0sP)zIP*&$v|s3 z^F`hldTf2ENZhhC(rJ+E0cEES3kgIKVB2*l3;-Dc)wzVYe%C*yl>YAvN{c4SB*Kr3 zXl-kYaj>nGcODrDMy!;E>(&8cfad^#SMSEHnfl)=bpBI099x`}mKf6*H>huLApR?7 zNNwo}vZ)n{)heWCr<-OTKa&%-qn}r7`5OTFf6=cMkFotHhSS#;91&c-5uU9$;tv4g zd;5({CesjA8isU|U4AW2Z>Ov0060tSXN^E2Y*^Qt81sIEQj}@b)`|#&*?Z#6`gma;s5{u07*qoM6N<$f{p5RssI20 literal 0 HcmV?d00001 diff --git a/Sshuttle VPN.app/Contents/Resources/main.py b/Sshuttle VPN.app/Contents/Resources/main.py new file mode 100644 index 0000000..baa290d --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/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) diff --git a/Sshuttle VPN.app/Contents/Resources/models.py b/Sshuttle VPN.app/Contents/Resources/models.py new file mode 100644 index 0000000..858975e --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/models.py @@ -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() diff --git a/Sshuttle VPN.app/Contents/Resources/my.py b/Sshuttle VPN.app/Contents/Resources/my.py new file mode 100644 index 0000000..cd701eb --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/my.py @@ -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 diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/assembler.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/assembler.py new file mode 100644 index 0000000..c478e37 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/assembler.py @@ -0,0 +1,26 @@ +import sys, zlib + +z = zlib.decompressobj() +mainmod = sys.modules[__name__] +while 1: + name = sys.stdin.readline().strip() + if name: + nbytes = int(sys.stdin.readline()) + if verbosity >= 2: + sys.stderr.write('server: assembling %r (%d bytes)\n' + % (name, nbytes)) + content = z.decompress(sys.stdin.read(nbytes)) + exec compile(content, name, "exec") + + # FIXME: this crushes everything into a single module namespace, + # then makes each of the module names point at this one. Gross. + assert(name.endswith('.py')) + modname = name[:-3] + mainmod.__dict__[modname] = mainmod + else: + break + +verbose = verbosity +sys.stderr.flush() +sys.stdout.flush() +main() diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py new file mode 100644 index 0000000..dbd11de --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/client.py @@ -0,0 +1,356 @@ +import struct, socket, select, errno, re, signal +import compat.ssubprocess as ssubprocess +import helpers, ssnet, ssh, ssyslog +from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper +from helpers import * + +_extra_fd = os.open('/dev/null', os.O_RDONLY) + +def _islocal(ip): + sock = socket.socket() + try: + try: + sock.bind((ip, 0)) + except socket.error, e: + if e.args[0] == errno.EADDRNOTAVAIL: + return False # not a local IP + else: + raise + finally: + sock.close() + return True # it's a local IP, or there would have been an error + + +def got_signal(signum, frame): + log('exiting on signal %d\n' % signum) + sys.exit(1) + + +_pidname = None +def check_daemon(pidfile): + global _pidname + _pidname = os.path.abspath(pidfile) + try: + oldpid = open(_pidname).read(1024) + except IOError, e: + if e.errno == errno.ENOENT: + return # no pidfile, ok + else: + raise Fatal("can't read %s: %s" % (_pidname, e)) + if not oldpid: + os.unlink(_pidname) + return # invalid pidfile, ok + oldpid = int(oldpid.strip() or 0) + if oldpid <= 0: + os.unlink(_pidname) + return # invalid pidfile, ok + try: + os.kill(oldpid, 0) + except OSError, e: + if e.errno == errno.ESRCH: + os.unlink(_pidname) + return # outdated pidfile, ok + elif e.errno == errno.EPERM: + pass + else: + raise + raise Fatal("%s: sshuttle is already running (pid=%d)" + % (_pidname, oldpid)) + + +def daemonize(): + if os.fork(): + os._exit(0) + os.setsid() + if os.fork(): + os._exit(0) + + outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666) + try: + os.write(outfd, '%d\n' % os.getpid()) + finally: + os.close(outfd) + os.chdir("/") + + # Normal exit when killed, or try/finally won't work and the pidfile won't + # be deleted. + signal.signal(signal.SIGTERM, got_signal) + + si = open('/dev/null', 'r+') + os.dup2(si.fileno(), 0) + os.dup2(si.fileno(), 1) + si.close() + + ssyslog.stderr_to_syslog() + + +def daemon_cleanup(): + try: + os.unlink(_pidname) + except OSError, e: + if e.errno == errno.ENOENT: + pass + else: + raise + + +def original_dst(sock): + try: + SO_ORIGINAL_DST = 80 + SOCKADDR_MIN = 16 + sockaddr_in = sock.getsockopt(socket.SOL_IP, + SO_ORIGINAL_DST, SOCKADDR_MIN) + (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) + assert(socket.htons(proto) == socket.AF_INET) + ip = '%d.%d.%d.%d' % (a,b,c,d) + return (ip,port) + except socket.error, e: + if e.args[0] == errno.ENOPROTOOPT: + return sock.getsockname() + raise + + +class FirewallClient: + def __init__(self, port, subnets_include, subnets_exclude): + self.port = port + self.auto_nets = [] + self.subnets_include = subnets_include + self.subnets_exclude = subnets_exclude + argvbase = ([sys.argv[0]] + + ['-v'] * (helpers.verbose or 0) + + ['--firewall', str(port)]) + if ssyslog._p: + argvbase += ['--syslog'] + argv_tries = [ + ['sudo', '-p', '[local sudo] Password: '] + argvbase, + ['su', '-c', ' '.join(argvbase)], + argvbase + ] + + # we can't use stdin/stdout=subprocess.PIPE here, as we normally would, + # because stupid Linux 'su' requires that stdin be attached to a tty. + # Instead, attach a *bidirectional* socket to its stdout, and use + # that for talking in both directions. + (s1,s2) = socket.socketpair() + def setup(): + # run in the child process + s2.close() + e = None + if os.getuid() == 0: + argv_tries = argv_tries[-1:] # last entry only + for argv in argv_tries: + try: + if argv[0] == 'su': + sys.stderr.write('[local su] ') + self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) + e = None + break + except OSError, e: + pass + self.argv = argv + s1.close() + self.pfile = s2.makefile('wb+') + if e: + log('Spawning firewall manager: %r\n' % self.argv) + raise Fatal(e) + line = self.pfile.readline() + self.check() + if line != 'READY\n': + raise Fatal('%r expected READY, got %r' % (self.argv, line)) + + def check(self): + rv = self.p.poll() + if rv: + raise Fatal('%r returned %d' % (self.argv, rv)) + + def start(self): + self.pfile.write('ROUTES\n') + for (ip,width) in self.subnets_include+self.auto_nets: + self.pfile.write('%d,0,%s\n' % (width, ip)) + for (ip,width) in self.subnets_exclude: + self.pfile.write('%d,1,%s\n' % (width, ip)) + self.pfile.write('GO\n') + self.pfile.flush() + line = self.pfile.readline() + self.check() + if line != 'STARTED\n': + raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) + + def sethostip(self, hostname, ip): + assert(not re.search(r'[^-\w]', hostname)) + assert(not re.search(r'[^0-9.]', ip)) + self.pfile.write('HOST %s,%s\n' % (hostname, ip)) + self.pfile.flush() + + def done(self): + self.pfile.close() + rv = self.p.wait() + if rv: + raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) + + +def _main(listener, fw, ssh_cmd, remotename, python, seed_hosts, auto_nets, + syslog, daemon): + handlers = [] + if helpers.verbose >= 1: + helpers.logprefix = 'c : ' + else: + helpers.logprefix = 'client: ' + debug1('connecting to server...\n') + + try: + (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, + stderr=ssyslog._p and ssyslog._p.stdin) + except socket.error, e: + if e.args[0] == errno.EPIPE: + raise Fatal("failed to establish ssh session (1)") + else: + raise + mux = Mux(serversock, serversock) + handlers.append(mux) + + expected = 'SSHUTTLE0001' + try: + initstring = serversock.recv(len(expected)) + except socket.error, e: + if e.args[0] == errno.ECONNRESET: + raise Fatal("failed to establish ssh session (2)") + else: + raise + + rv = serverproc.poll() + if rv: + raise Fatal('server died with error code %d' % rv) + + if initstring != expected: + raise Fatal('expected server init string %r; got %r' + % (expected, initstring)) + debug1('connected.\n') + print 'Connected.' + sys.stdout.flush() + if daemon: + daemonize() + log('daemonizing (%s).\n' % _pidname) + elif syslog: + debug1('switching to syslog.\n') + ssyslog.stderr_to_syslog() + + def onroutes(routestr): + if auto_nets: + for line in routestr.strip().split('\n'): + (ip,width) = line.split(',', 1) + fw.auto_nets.append((ip,int(width))) + + # we definitely want to do this *after* starting ssh, or we might end + # up intercepting the ssh connection! + # + # Moreover, now that we have the --auto-nets option, we have to wait + # for the server to send us that message anyway. Even if we haven't + # set --auto-nets, we might as well wait for the message first, then + # ignore its contents. + mux.got_routes = None + fw.start() + mux.got_routes = onroutes + + def onhostlist(hostlist): + debug2('got host list: %r\n' % hostlist) + for line in hostlist.strip().split(): + if line: + name,ip = line.split(',', 1) + fw.sethostip(name, ip) + mux.got_host_list = onhostlist + + def onaccept(): + global _extra_fd + try: + sock,srcip = listener.accept() + except socket.error, e: + if e.args[0] in [errno.EMFILE, errno.ENFILE]: + debug1('Rejected incoming connection: too many open files!\n') + # free up an fd so we can eat the connection + os.close(_extra_fd) + try: + sock,srcip = listener.accept() + sock.close() + finally: + _extra_fd = os.open('/dev/null', os.O_RDONLY) + return + else: + raise + dstip = original_dst(sock) + debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], + dstip[0],dstip[1])) + if dstip[1] == listener.getsockname()[1] and _islocal(dstip[0]): + debug1("-- ignored: that's my address!\n") + sock.close() + return + chan = mux.next_channel() + mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip) + outwrap = MuxWrapper(mux, chan) + handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) + handlers.append(Handler([listener], onaccept)) + + if seed_hosts != None: + debug1('seed_hosts: %r\n' % seed_hosts) + mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts)) + + while 1: + rv = serverproc.poll() + if rv: + raise Fatal('server died with error code %d' % rv) + + ssnet.runonce(handlers, mux) + mux.callback() + mux.check_fullness() + + +def main(listenip, ssh_cmd, remotename, python, seed_hosts, auto_nets, + subnets_include, subnets_exclude, syslog, daemon, pidfile): + if syslog: + ssyslog.start_syslog() + if daemon: + try: + check_daemon(pidfile) + except Fatal, e: + log("%s\n" % e) + return 5 + debug1('Starting sshuttle proxy.\n') + listener = socket.socket() + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if listenip[1]: + ports = [listenip[1]] + else: + ports = xrange(12300,9000,-1) + last_e = None + bound = False + debug2('Binding:') + for port in ports: + debug2(' %d' % port) + try: + listener.bind((listenip[0], port)) + bound = True + break + except socket.error, e: + last_e = e + debug2('\n') + if not bound: + assert(last_e) + raise last_e + listener.listen(10) + listenip = listener.getsockname() + debug1('Listening on %r.\n' % (listenip,)) + + fw = FirewallClient(listenip[1], subnets_include, subnets_exclude) + + try: + return _main(listener, fw, ssh_cmd, remotename, + python, seed_hosts, auto_nets, syslog, daemon) + finally: + try: + if daemon: + # it's not our child anymore; can't waitpid + fw.p.returncode = 0 + fw.done() + finally: + if daemon: + daemon_cleanup() diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.py new file mode 100644 index 0000000..ee6b8da --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/compat/ssubprocess.py @@ -0,0 +1,1305 @@ +# subprocess - Subprocesses with accessible I/O streams +# +# For more information about this module, see PEP 324. +# +# This module should remain compatible with Python 2.2, see PEP 291. +# +# Copyright (c) 2003-2005 by Peter Astrand +# +# Licensed to PSF under a Contributor Agreement. +# See http://www.python.org/2.4/license for licensing details. + +r"""subprocess - Subprocesses with accessible I/O streams + +This module allows you to spawn processes, connect to their +input/output/error pipes, and obtain their return codes. This module +intends to replace several other, older modules and functions, like: + +os.system +os.spawn* +os.popen* +popen2.* +commands.* + +Information about how the subprocess module can be used to replace these +modules and functions can be found below. + + + +Using the subprocess module +=========================== +This module defines one class called Popen: + +class Popen(args, bufsize=0, executable=None, + stdin=None, stdout=None, stderr=None, + preexec_fn=None, close_fds=False, shell=False, + cwd=None, env=None, universal_newlines=False, + startupinfo=None, creationflags=0): + + +Arguments are: + +args should be a string, or a sequence of program arguments. The +program to execute is normally the first item in the args sequence or +string, but can be explicitly set by using the executable argument. + +On UNIX, with shell=False (default): In this case, the Popen class +uses os.execvp() to execute the child program. args should normally +be a sequence. A string will be treated as a sequence with the string +as the only item (the program to execute). + +On UNIX, with shell=True: If args is a string, it specifies the +command string to execute through the shell. If args is a sequence, +the first item specifies the command string, and any additional items +will be treated as additional shell arguments. + +On Windows: the Popen class uses CreateProcess() to execute the child +program, which operates on strings. If args is a sequence, it will be +converted to a string using the list2cmdline method. Please note that +not all MS Windows applications interpret the command line the same +way: The list2cmdline is designed for applications using the same +rules as the MS C runtime. + +bufsize, if given, has the same meaning as the corresponding argument +to the built-in open() function: 0 means unbuffered, 1 means line +buffered, any other positive value means use a buffer of +(approximately) that size. A negative bufsize means to use the system +default, which usually means fully buffered. The default value for +bufsize is 0 (unbuffered). + +stdin, stdout and stderr specify the executed programs' standard +input, standard output and standard error file handles, respectively. +Valid values are PIPE, an existing file descriptor (a positive +integer), an existing file object, and None. PIPE indicates that a +new pipe to the child should be created. With None, no redirection +will occur; the child's file handles will be inherited from the +parent. Additionally, stderr can be STDOUT, which indicates that the +stderr data from the applications should be captured into the same +file handle as for stdout. + +If preexec_fn is set to a callable object, this object will be called +in the child process just before the child is executed. + +If close_fds is true, all file descriptors except 0, 1 and 2 will be +closed before the child process is executed. + +if shell is true, the specified command will be executed through the +shell. + +If cwd is not None, the current directory will be changed to cwd +before the child is executed. + +If env is not None, it defines the environment variables for the new +process. + +If universal_newlines is true, the file objects stdout and stderr are +opened as a text files, but lines may be terminated by any of '\n', +the Unix end-of-line convention, '\r', the Macintosh convention or +'\r\n', the Windows convention. All of these external representations +are seen as '\n' by the Python program. Note: This feature is only +available if Python is built with universal newline support (the +default). Also, the newlines attribute of the file objects stdout, +stdin and stderr are not updated by the communicate() method. + +The startupinfo and creationflags, if given, will be passed to the +underlying CreateProcess() function. They can specify things such as +appearance of the main window and priority for the new process. +(Windows only) + + +This module also defines two shortcut functions: + +call(*popenargs, **kwargs): + Run command with arguments. Wait for command to complete, then + return the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + retcode = call(["ls", "-l"]) + +check_call(*popenargs, **kwargs): + Run command with arguments. Wait for command to complete. If the + exit code was zero then return, otherwise raise + CalledProcessError. The CalledProcessError object will have the + return code in the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + check_call(["ls", "-l"]) + +Exceptions +---------- +Exceptions raised in the child process, before the new program has +started to execute, will be re-raised in the parent. Additionally, +the exception object will have one extra attribute called +'child_traceback', which is a string containing traceback information +from the childs point of view. + +The most common exception raised is OSError. This occurs, for +example, when trying to execute a non-existent file. Applications +should prepare for OSErrors. + +A ValueError will be raised if Popen is called with invalid arguments. + +check_call() will raise CalledProcessError, if the called process +returns a non-zero return code. + + +Security +-------- +Unlike some other popen functions, this implementation will never call +/bin/sh implicitly. This means that all characters, including shell +metacharacters, can safely be passed to child processes. + + +Popen objects +============= +Instances of the Popen class have the following methods: + +poll() + Check if child process has terminated. Returns returncode + attribute. + +wait() + Wait for child process to terminate. Returns returncode attribute. + +communicate(input=None) + Interact with process: Send data to stdin. Read data from stdout + and stderr, until end-of-file is reached. Wait for process to + terminate. The optional input argument should be a string to be + sent to the child process, or None, if no data should be sent to + the child. + + communicate() returns a tuple (stdout, stderr). + + Note: The data read is buffered in memory, so do not use this + method if the data size is large or unlimited. + +The following attributes are also available: + +stdin + If the stdin argument is PIPE, this attribute is a file object + that provides input to the child process. Otherwise, it is None. + +stdout + If the stdout argument is PIPE, this attribute is a file object + that provides output from the child process. Otherwise, it is + None. + +stderr + If the stderr argument is PIPE, this attribute is file object that + provides error output from the child process. Otherwise, it is + None. + +pid + The process ID of the child process. + +returncode + The child return code. A None value indicates that the process + hasn't terminated yet. A negative value -N indicates that the + child was terminated by signal N (UNIX only). + + +Replacing older functions with the subprocess module +==================================================== +In this section, "a ==> b" means that b can be used as a replacement +for a. + +Note: All functions in this section fail (more or less) silently if +the executed program cannot be found; this module raises an OSError +exception. + +In the following examples, we assume that the subprocess module is +imported with "from subprocess import *". + + +Replacing /bin/sh shell backquote +--------------------------------- +output=`mycmd myarg` +==> +output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0] + + +Replacing shell pipe line +------------------------- +output=`dmesg | grep hda` +==> +p1 = Popen(["dmesg"], stdout=PIPE) +p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) +output = p2.communicate()[0] + + +Replacing os.system() +--------------------- +sts = os.system("mycmd" + " myarg") +==> +p = Popen("mycmd" + " myarg", shell=True) +pid, sts = os.waitpid(p.pid, 0) + +Note: + +* Calling the program through the shell is usually not required. + +* It's easier to look at the returncode attribute than the + exitstatus. + +A more real-world example would look like this: + +try: + retcode = call("mycmd" + " myarg", shell=True) + if retcode < 0: + print >>sys.stderr, "Child was terminated by signal", -retcode + else: + print >>sys.stderr, "Child returned", retcode +except OSError, e: + print >>sys.stderr, "Execution failed:", e + + +Replacing os.spawn* +------------------- +P_NOWAIT example: + +pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg") +==> +pid = Popen(["/bin/mycmd", "myarg"]).pid + + +P_WAIT example: + +retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg") +==> +retcode = call(["/bin/mycmd", "myarg"]) + + +Vector example: + +os.spawnvp(os.P_NOWAIT, path, args) +==> +Popen([path] + args[1:]) + + +Environment example: + +os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env) +==> +Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"}) + + +Replacing os.popen* +------------------- +pipe = os.popen(cmd, mode='r', bufsize) +==> +pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout + +pipe = os.popen(cmd, mode='w', bufsize) +==> +pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin + + +(child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize) +==> +p = Popen(cmd, shell=True, bufsize=bufsize, + stdin=PIPE, stdout=PIPE, close_fds=True) +(child_stdin, child_stdout) = (p.stdin, p.stdout) + + +(child_stdin, + child_stdout, + child_stderr) = os.popen3(cmd, mode, bufsize) +==> +p = Popen(cmd, shell=True, bufsize=bufsize, + stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) +(child_stdin, + child_stdout, + child_stderr) = (p.stdin, p.stdout, p.stderr) + + +(child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize) +==> +p = Popen(cmd, shell=True, bufsize=bufsize, + stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) +(child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout) + + +Replacing popen2.* +------------------ +Note: If the cmd argument to popen2 functions is a string, the command +is executed through /bin/sh. If it is a list, the command is directly +executed. + +(child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode) +==> +p = Popen(["somestring"], shell=True, bufsize=bufsize + stdin=PIPE, stdout=PIPE, close_fds=True) +(child_stdout, child_stdin) = (p.stdout, p.stdin) + + +(child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode) +==> +p = Popen(["mycmd", "myarg"], bufsize=bufsize, + stdin=PIPE, stdout=PIPE, close_fds=True) +(child_stdout, child_stdin) = (p.stdout, p.stdin) + +The popen2.Popen3 and popen2.Popen4 basically works as subprocess.Popen, +except that: + +* subprocess.Popen raises an exception if the execution fails +* the capturestderr argument is replaced with the stderr argument. +* stdin=PIPE and stdout=PIPE must be specified. +* popen2 closes all filedescriptors by default, but you have to specify + close_fds=True with subprocess.Popen. +""" + +import sys +mswindows = (sys.platform == "win32") + +import os +import types +import traceback +import gc +import signal + +# Exception classes used by this module. +class CalledProcessError(Exception): + """This exception is raised when a process run by check_call() returns + a non-zero exit status. The exit status will be stored in the + returncode attribute.""" + def __init__(self, returncode, cmd): + self.returncode = returncode + self.cmd = cmd + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + + +if mswindows: + import threading + import msvcrt + if 0: # <-- change this to use pywin32 instead of the _subprocess driver + import pywintypes + from win32api import GetStdHandle, STD_INPUT_HANDLE, \ + STD_OUTPUT_HANDLE, STD_ERROR_HANDLE + from win32api import GetCurrentProcess, DuplicateHandle, \ + GetModuleFileName, GetVersion + from win32con import DUPLICATE_SAME_ACCESS, SW_HIDE + from win32pipe import CreatePipe + from win32process import CreateProcess, STARTUPINFO, \ + GetExitCodeProcess, STARTF_USESTDHANDLES, \ + STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE + from win32process import TerminateProcess + from win32event import WaitForSingleObject, INFINITE, WAIT_OBJECT_0 + else: + from _subprocess import * + class STARTUPINFO: + dwFlags = 0 + hStdInput = None + hStdOutput = None + hStdError = None + wShowWindow = 0 + class pywintypes: + error = IOError +else: + import select + import errno + import fcntl + import pickle + +__all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "CalledProcessError"] + +try: + MAXFD = os.sysconf("SC_OPEN_MAX") +except: + MAXFD = 256 + +# True/False does not exist on 2.2.0 +#try: +# False +#except NameError: +# False = 0 +# True = 1 + +_active = [] + +def _cleanup(): + for inst in _active[:]: + if inst._internal_poll(_deadstate=sys.maxint) >= 0: + try: + _active.remove(inst) + except ValueError: + # This can happen if two threads create a new Popen instance. + # It's harmless that it was already removed, so ignore. + pass + +PIPE = -1 +STDOUT = -2 + + +def call(*popenargs, **kwargs): + """Run command with arguments. Wait for command to complete, then + return the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + retcode = call(["ls", "-l"]) + """ + return Popen(*popenargs, **kwargs).wait() + + +def check_call(*popenargs, **kwargs): + """Run command with arguments. Wait for command to complete. If + the exit code was zero then return, otherwise raise + CalledProcessError. The CalledProcessError object will have the + return code in the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + check_call(["ls", "-l"]) + """ + retcode = call(*popenargs, **kwargs) + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + if retcode: + raise CalledProcessError(retcode, cmd) + return retcode + + +def list2cmdline(seq): + """ + Translate a sequence of arguments into a command line + string, using the same rules as the MS C runtime: + + 1) Arguments are delimited by white space, which is either a + space or a tab. + + 2) A string surrounded by double quotation marks is + interpreted as a single argument, regardless of white space + or pipe characters contained within. A quoted string can be + embedded in an argument. + + 3) A double quotation mark preceded by a backslash is + interpreted as a literal double quotation mark. + + 4) Backslashes are interpreted literally, unless they + immediately precede a double quotation mark. + + 5) If backslashes immediately precede a double quotation mark, + every pair of backslashes is interpreted as a literal + backslash. If the number of backslashes is odd, the last + backslash escapes the next double quotation mark as + described in rule 3. + """ + + # See + # http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp + result = [] + needquote = False + for arg in seq: + bs_buf = [] + + # Add a space to separate this argument from the others + if result: + result.append(' ') + + needquote = (" " in arg) or ("\t" in arg) or ("|" in arg) or not arg + if needquote: + result.append('"') + + for c in arg: + if c == '\\': + # Don't know if we need to double yet. + bs_buf.append(c) + elif c == '"': + # Double backslashes. + result.append('\\' * len(bs_buf)*2) + bs_buf = [] + result.append('\\"') + else: + # Normal char + if bs_buf: + result.extend(bs_buf) + bs_buf = [] + result.append(c) + + # Add remaining backslashes, if any. + if bs_buf: + result.extend(bs_buf) + + if needquote: + result.extend(bs_buf) + result.append('"') + + return ''.join(result) + + +def _closerange(start, max): + try: + os.closerange(start, max) + except AttributeError: + for i in xrange(start, max): + try: + os.close(i) + except: + pass + + +class Popen(object): + def __init__(self, args, bufsize=0, executable=None, + stdin=None, stdout=None, stderr=None, + preexec_fn=None, close_fds=False, shell=False, + cwd=None, env=None, universal_newlines=False, + startupinfo=None, creationflags=0): + """Create new Popen instance.""" + _cleanup() + + self._child_created = False + if not isinstance(bufsize, (int, long)): + raise TypeError("bufsize must be an integer") + + if mswindows: + if preexec_fn is not None: + raise ValueError("preexec_fn is not supported on Windows " + "platforms") + if close_fds and (stdin is not None or stdout is not None or + stderr is not None): + raise ValueError("close_fds is not supported on Windows " + "platforms if you redirect stdin/stdout/stderr") + else: + # POSIX + if startupinfo is not None: + raise ValueError("startupinfo is only supported on Windows " + "platforms") + if creationflags != 0: + raise ValueError("creationflags is only supported on Windows " + "platforms") + + self.stdin = None + self.stdout = None + self.stderr = None + self.pid = None + self.returncode = None + self.universal_newlines = universal_newlines + + # Input and output objects. The general principle is like + # this: + # + # Parent Child + # ------ ----- + # p2cwrite ---stdin---> p2cread + # c2pread <--stdout--- c2pwrite + # errread <--stderr--- errwrite + # + # On POSIX, the child objects are file descriptors. On + # Windows, these are Windows file handles. The parent objects + # are file descriptors on both platforms. The parent objects + # are None when not using PIPEs. The child objects are None + # when not redirecting. + + (p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) = self._get_handles(stdin, stdout, stderr) + + self._execute_child(args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) + + # On Windows, you cannot just redirect one or two handles: You + # either have to redirect all three or none. If the subprocess + # user has only redirected one or two handles, we are + # automatically creating PIPEs for the rest. We should close + # these after the process is started. See bug #1124861. + if mswindows: + if stdin is None and p2cwrite is not None: + os.close(p2cwrite) + p2cwrite = None + if stdout is None and c2pread is not None: + os.close(c2pread) + c2pread = None + if stderr is None and errread is not None: + os.close(errread) + errread = None + + if p2cwrite is not None: + self.stdin = os.fdopen(p2cwrite, 'wb', bufsize) + if c2pread is not None: + if universal_newlines: + self.stdout = os.fdopen(c2pread, 'rU', bufsize) + else: + self.stdout = os.fdopen(c2pread, 'rb', bufsize) + if errread is not None: + if universal_newlines: + self.stderr = os.fdopen(errread, 'rU', bufsize) + else: + self.stderr = os.fdopen(errread, 'rb', bufsize) + + + def _translate_newlines(self, data): + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") + return data + + + def __del__(self, sys=sys): + if not self._child_created: + # We didn't get to successfully create a child process. + return + # In case the child hasn't been waited on, check if it's done. + self._internal_poll(_deadstate=sys.maxint) + if self.returncode is None and _active is not None: + # Child is still running, keep us alive until we can wait on it. + _active.append(self) + + + def communicate(self, input=None): + """Interact with process: Send data to stdin. Read data from + stdout and stderr, until end-of-file is reached. Wait for + process to terminate. The optional input argument should be a + string to be sent to the child process, or None, if no data + should be sent to the child. + + communicate() returns a tuple (stdout, stderr).""" + + # Optimization: If we are only using one pipe, or no pipe at + # all, using select() or threads is unnecessary. + if [self.stdin, self.stdout, self.stderr].count(None) >= 2: + stdout = None + stderr = None + if self.stdin: + if input: + self.stdin.write(input) + self.stdin.close() + elif self.stdout: + stdout = self.stdout.read() + self.stdout.close() + elif self.stderr: + stderr = self.stderr.read() + self.stderr.close() + self.wait() + return (stdout, stderr) + + return self._communicate(input) + + + def poll(self): + return self._internal_poll() + + + if mswindows: + # + # Windows methods + # + def _get_handles(self, stdin, stdout, stderr): + """Construct and return tupel with IO objects: + p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite + """ + if stdin is None and stdout is None and stderr is None: + return (None, None, None, None, None, None) + + p2cread, p2cwrite = None, None + c2pread, c2pwrite = None, None + errread, errwrite = None, None + + if stdin is None: + p2cread = GetStdHandle(STD_INPUT_HANDLE) + if p2cread is not None: + pass + elif stdin is None or stdin == PIPE: + p2cread, p2cwrite = CreatePipe(None, 0) + # Detach and turn into fd + p2cwrite = p2cwrite.Detach() + p2cwrite = msvcrt.open_osfhandle(p2cwrite, 0) + elif isinstance(stdin, int): + p2cread = msvcrt.get_osfhandle(stdin) + else: + # Assuming file-like object + p2cread = msvcrt.get_osfhandle(stdin.fileno()) + p2cread = self._make_inheritable(p2cread) + + if stdout is None: + c2pwrite = GetStdHandle(STD_OUTPUT_HANDLE) + if c2pwrite is not None: + pass + elif stdout is None or stdout == PIPE: + c2pread, c2pwrite = CreatePipe(None, 0) + # Detach and turn into fd + c2pread = c2pread.Detach() + c2pread = msvcrt.open_osfhandle(c2pread, 0) + elif isinstance(stdout, int): + c2pwrite = msvcrt.get_osfhandle(stdout) + else: + # Assuming file-like object + c2pwrite = msvcrt.get_osfhandle(stdout.fileno()) + c2pwrite = self._make_inheritable(c2pwrite) + + if stderr is None: + errwrite = GetStdHandle(STD_ERROR_HANDLE) + if errwrite is not None: + pass + elif stderr is None or stderr == PIPE: + errread, errwrite = CreatePipe(None, 0) + # Detach and turn into fd + errread = errread.Detach() + errread = msvcrt.open_osfhandle(errread, 0) + elif stderr == STDOUT: + errwrite = c2pwrite + elif isinstance(stderr, int): + errwrite = msvcrt.get_osfhandle(stderr) + else: + # Assuming file-like object + errwrite = msvcrt.get_osfhandle(stderr.fileno()) + errwrite = self._make_inheritable(errwrite) + + return (p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) + + + def _make_inheritable(self, handle): + """Return a duplicate of handle, which is inheritable""" + return DuplicateHandle(GetCurrentProcess(), handle, + GetCurrentProcess(), 0, 1, + DUPLICATE_SAME_ACCESS) + + + def _find_w9xpopen(self): + """Find and return absolut path to w9xpopen.exe""" + w9xpopen = os.path.join(os.path.dirname(GetModuleFileName(0)), + "w9xpopen.exe") + if not os.path.exists(w9xpopen): + # Eeek - file-not-found - possibly an embedding + # situation - see if we can locate it in sys.exec_prefix + w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix), + "w9xpopen.exe") + if not os.path.exists(w9xpopen): + raise RuntimeError("Cannot locate w9xpopen.exe, which is " + "needed for Popen to work with your " + "shell or platform.") + return w9xpopen + + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Execute program (MS Windows version)""" + + if not isinstance(args, types.StringTypes): + args = list2cmdline(args) + + # Process startup details + if startupinfo is None: + startupinfo = STARTUPINFO() + if None not in (p2cread, c2pwrite, errwrite): + startupinfo.dwFlags |= STARTF_USESTDHANDLES + startupinfo.hStdInput = p2cread + startupinfo.hStdOutput = c2pwrite + startupinfo.hStdError = errwrite + + if shell: + startupinfo.dwFlags |= STARTF_USESHOWWINDOW + startupinfo.wShowWindow = SW_HIDE + comspec = os.environ.get("COMSPEC", "cmd.exe") + args = comspec + " /c " + args + if (GetVersion() >= 0x80000000L or + os.path.basename(comspec).lower() == "command.com"): + # Win9x, or using command.com on NT. We need to + # use the w9xpopen intermediate program. For more + # information, see KB Q150956 + # (http://web.archive.org/web/20011105084002/http://support.microsoft.com/support/kb/articles/Q150/9/56.asp) + w9xpopen = self._find_w9xpopen() + args = '"%s" %s' % (w9xpopen, args) + # Not passing CREATE_NEW_CONSOLE has been known to + # cause random failures on win9x. Specifically a + # dialog: "Your program accessed mem currently in + # use at xxx" and a hopeful warning about the + # stability of your system. Cost is Ctrl+C wont + # kill children. + creationflags |= CREATE_NEW_CONSOLE + + # Start the process + try: + hp, ht, pid, tid = CreateProcess(executable, args, + # no special security + None, None, + int(not close_fds), + creationflags, + env, + cwd, + startupinfo) + except pywintypes.error, e: + # Translate pywintypes.error to WindowsError, which is + # a subclass of OSError. FIXME: We should really + # translate errno using _sys_errlist (or simliar), but + # how can this be done from Python? + raise WindowsError(*e.args) + + # Retain the process handle, but close the thread handle + self._child_created = True + self._handle = hp + self.pid = pid + ht.Close() + + # Child is launched. Close the parent's copy of those pipe + # handles that only the child should have open. You need + # to make sure that no handles to the write end of the + # output pipe are maintained in this process or else the + # pipe will not close when the child process exits and the + # ReadFile will hang. + if p2cread is not None: + p2cread.Close() + if c2pwrite is not None: + c2pwrite.Close() + if errwrite is not None: + errwrite.Close() + + + def _internal_poll(self, _deadstate=None): + """Check if child process has terminated. Returns returncode + attribute.""" + if self.returncode is None: + if WaitForSingleObject(self._handle, 0) == WAIT_OBJECT_0: + self.returncode = GetExitCodeProcess(self._handle) + return self.returncode + + + def wait(self): + """Wait for child process to terminate. Returns returncode + attribute.""" + if self.returncode is None: + obj = WaitForSingleObject(self._handle, INFINITE) + self.returncode = GetExitCodeProcess(self._handle) + return self.returncode + + + def _readerthread(self, fh, buffer): + buffer.append(fh.read()) + + + def _communicate(self, input): + stdout = None # Return + stderr = None # Return + + if self.stdout: + stdout = [] + stdout_thread = threading.Thread(target=self._readerthread, + args=(self.stdout, stdout)) + stdout_thread.setDaemon(True) + stdout_thread.start() + if self.stderr: + stderr = [] + stderr_thread = threading.Thread(target=self._readerthread, + args=(self.stderr, stderr)) + stderr_thread.setDaemon(True) + stderr_thread.start() + + if self.stdin: + if input is not None: + self.stdin.write(input) + self.stdin.close() + + if self.stdout: + stdout_thread.join() + if self.stderr: + stderr_thread.join() + + # All data exchanged. Translate lists into strings. + if stdout is not None: + stdout = stdout[0] + if stderr is not None: + stderr = stderr[0] + + # Translate newlines, if requested. We cannot let the file + # object do the translation: It is based on stdio, which is + # impossible to combine with select (unless forcing no + # buffering). + if self.universal_newlines and hasattr(file, 'newlines'): + if stdout: + stdout = self._translate_newlines(stdout) + if stderr: + stderr = self._translate_newlines(stderr) + + self.wait() + return (stdout, stderr) + + def send_signal(self, sig): + """Send a signal to the process + """ + if sig == signal.SIGTERM: + self.terminate() + else: + raise ValueError("Only SIGTERM is supported on Windows") + + def terminate(self): + """Terminates the process + """ + TerminateProcess(self._handle, 1) + + kill = terminate + + else: + # + # POSIX methods + # + def _get_handles(self, stdin, stdout, stderr): + """Construct and return tupel with IO objects: + p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite + """ + p2cread, p2cwrite = None, None + c2pread, c2pwrite = None, None + errread, errwrite = None, None + + if stdin is None: + pass + elif stdin == PIPE: + p2cread, p2cwrite = os.pipe() + elif isinstance(stdin, int): + p2cread = stdin + else: + # Assuming file-like object + p2cread = stdin.fileno() + + if stdout is None: + pass + elif stdout == PIPE: + c2pread, c2pwrite = os.pipe() + elif isinstance(stdout, int): + c2pwrite = stdout + else: + # Assuming file-like object + c2pwrite = stdout.fileno() + + if stderr is None: + pass + elif stderr == PIPE: + errread, errwrite = os.pipe() + elif stderr == STDOUT: + errwrite = c2pwrite + elif isinstance(stderr, int): + errwrite = stderr + else: + # Assuming file-like object + errwrite = stderr.fileno() + + return (p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) + + + def _set_cloexec_flag(self, fd): + try: + cloexec_flag = fcntl.FD_CLOEXEC + except AttributeError: + cloexec_flag = 1 + + old = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag) + + + def _close_fds(self, but): + _closerange(3, but) + _closerange(but + 1, MAXFD) + + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Execute program (POSIX version)""" + + if isinstance(args, types.StringTypes): + args = [args] + else: + args = list(args) + + if shell: + args = ["/bin/sh", "-c"] + args + + if executable is None: + executable = args[0] + + # For transferring possible exec failure from child to parent + # The first char specifies the exception type: 0 means + # OSError, 1 means some other error. + errpipe_read, errpipe_write = os.pipe() + self._set_cloexec_flag(errpipe_write) + + gc_was_enabled = gc.isenabled() + # Disable gc to avoid bug where gc -> file_dealloc -> + # write to stderr -> hang. http://bugs.python.org/issue1336 + gc.disable() + try: + self.pid = os.fork() + except: + if gc_was_enabled: + gc.enable() + raise + self._child_created = True + if self.pid == 0: + # Child + try: + # Close parent's pipe ends + if p2cwrite is not None: + os.close(p2cwrite) + if c2pread is not None: + os.close(c2pread) + if errread is not None: + os.close(errread) + os.close(errpipe_read) + + # Dup fds for child + if p2cread is not None: + os.dup2(p2cread, 0) + if c2pwrite is not None: + os.dup2(c2pwrite, 1) + if errwrite is not None: + os.dup2(errwrite, 2) + + # Close pipe fds. Make sure we don't close the same + # fd more than once, or standard fds. + if p2cread is not None and p2cread not in (0,): + os.close(p2cread) + if c2pwrite is not None and c2pwrite not in (p2cread, 1): + os.close(c2pwrite) + if errwrite is not None and errwrite not in (p2cread, c2pwrite, 2): + os.close(errwrite) + + # Close all other fds, if asked for + if close_fds: + self._close_fds(but=errpipe_write) + + if cwd is not None: + os.chdir(cwd) + + if preexec_fn: + preexec_fn() + + if env is None: + os.execvp(executable, args) + else: + os.execvpe(executable, args, env) + + except: + exc_type, exc_value, tb = sys.exc_info() + # Save the traceback and attach it to the exception object + exc_lines = traceback.format_exception(exc_type, + exc_value, + tb) + exc_value.child_traceback = ''.join(exc_lines) + os.write(errpipe_write, pickle.dumps(exc_value)) + + # This exitcode won't be reported to applications, so it + # really doesn't matter what we return. + os._exit(255) + + # Parent + if gc_was_enabled: + gc.enable() + os.close(errpipe_write) + if p2cread is not None and p2cwrite is not None: + os.close(p2cread) + if c2pwrite is not None and c2pread is not None: + os.close(c2pwrite) + if errwrite is not None and errread is not None: + os.close(errwrite) + + # Wait for exec to fail or succeed; possibly raising exception + data = os.read(errpipe_read, 1048576) # Exceptions limited to 1 MB + os.close(errpipe_read) + if data != "": + os.waitpid(self.pid, 0) + child_exception = pickle.loads(data) + raise child_exception + + + def _handle_exitstatus(self, sts): + if os.WIFSIGNALED(sts): + self.returncode = -os.WTERMSIG(sts) + elif os.WIFEXITED(sts): + self.returncode = os.WEXITSTATUS(sts) + else: + # Should never happen + raise RuntimeError("Unknown child exit status!") + + + def _internal_poll(self, _deadstate=None): + """Check if child process has terminated. Returns returncode + attribute.""" + if self.returncode is None: + try: + pid, sts = os.waitpid(self.pid, os.WNOHANG) + if pid == self.pid: + self._handle_exitstatus(sts) + except os.error: + if _deadstate is not None: + self.returncode = _deadstate + return self.returncode + + + def wait(self): + """Wait for child process to terminate. Returns returncode + attribute.""" + if self.returncode is None: + pid, sts = os.waitpid(self.pid, 0) + self._handle_exitstatus(sts) + return self.returncode + + + def _communicate(self, input): + read_set = [] + write_set = [] + stdout = None # Return + stderr = None # Return + + if self.stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + self.stdin.flush() + if input: + write_set.append(self.stdin) + else: + self.stdin.close() + if self.stdout: + read_set.append(self.stdout) + stdout = [] + if self.stderr: + read_set.append(self.stderr) + stderr = [] + + input_offset = 0 + while read_set or write_set: + try: + rlist, wlist, xlist = select.select(read_set, write_set, []) + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + + if self.stdin in wlist: + # When select has indicated that the file is writable, + # we can write up to PIPE_BUF bytes without risk + # blocking. POSIX defines PIPE_BUF >= 512 + chunk = input[input_offset : input_offset + 512] + bytes_written = os.write(self.stdin.fileno(), chunk) + input_offset += bytes_written + if input_offset >= len(input): + self.stdin.close() + write_set.remove(self.stdin) + + if self.stdout in rlist: + data = os.read(self.stdout.fileno(), 1024) + if data == "": + self.stdout.close() + read_set.remove(self.stdout) + stdout.append(data) + + if self.stderr in rlist: + data = os.read(self.stderr.fileno(), 1024) + if data == "": + self.stderr.close() + read_set.remove(self.stderr) + stderr.append(data) + + # All data exchanged. Translate lists into strings. + if stdout is not None: + stdout = ''.join(stdout) + if stderr is not None: + stderr = ''.join(stderr) + + # Translate newlines, if requested. We cannot let the file + # object do the translation: It is based on stdio, which is + # impossible to combine with select (unless forcing no + # buffering). + if self.universal_newlines and hasattr(file, 'newlines'): + if stdout: + stdout = self._translate_newlines(stdout) + if stderr: + stderr = self._translate_newlines(stderr) + + self.wait() + return (stdout, stderr) + + def send_signal(self, sig): + """Send a signal to the process + """ + os.kill(self.pid, sig) + + def terminate(self): + """Terminate the process with SIGTERM + """ + self.send_signal(signal.SIGTERM) + + def kill(self): + """Kill the process with SIGKILL + """ + self.send_signal(signal.SIGKILL) + + +def _demo_posix(): + # + # Example 1: Simple redirection: Get process list + # + plist = Popen(["ps"], stdout=PIPE).communicate()[0] + print "Process list:" + print plist + + # + # Example 2: Change uid before executing child + # + if os.getuid() == 0: + p = Popen(["id"], preexec_fn=lambda: os.setuid(100)) + p.wait() + + # + # Example 3: Connecting several subprocesses + # + print "Looking for 'hda'..." + p1 = Popen(["dmesg"], stdout=PIPE) + p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) + print repr(p2.communicate()[0]) + + # + # Example 4: Catch execution error + # + print + print "Trying a weird file..." + try: + print Popen(["/this/path/does/not/exist"]).communicate() + except OSError, e: + if e.errno == errno.ENOENT: + print "The file didn't exist. I thought so..." + print "Child traceback:" + print e.child_traceback + else: + print "Error", e.errno + else: + print >>sys.stderr, "Gosh. No error." + + +def _demo_windows(): + # + # Example 1: Connecting several subprocesses + # + print "Looking for 'PROMPT' in set output..." + p1 = Popen("set", stdout=PIPE, shell=True) + p2 = Popen('find "PROMPT"', stdin=p1.stdout, stdout=PIPE) + print repr(p2.communicate()[0]) + + # + # Example 2: Simple execution of program + # + print "Executing calc..." + p = Popen("calc") + p.wait() + + +if 0 and __name__ == "__main__": + if mswindows: + _demo_windows() + else: + _demo_posix() diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py new file mode 100644 index 0000000..044ac52 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/firewall.py @@ -0,0 +1,304 @@ +import re, errno +import compat.ssubprocess as ssubprocess +import helpers, ssyslog +from helpers import * + + +def ipt_chain_exists(name): + argv = ['iptables', '-t', 'nat', '-nL'] + p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) + for line in p.stdout: + if line.startswith('Chain %s ' % name): + return True + rv = p.wait() + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + + +def ipt(*args): + argv = ['iptables', '-t', 'nat'] + list(args) + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv) + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + + +# We name the chain based on the transproxy port number so that it's possible +# to run multiple copies of sshuttle at the same time. Of course, the +# multiple copies shouldn't have overlapping subnets, or only the most- +# recently-started one will win (because we use "-I OUTPUT 1" instead of +# "-A OUTPUT"). +def do_iptables(port, subnets): + chain = 'sshuttle-%s' % port + + # basic cleanup/setup of chains + if ipt_chain_exists(chain): + ipt('-D', 'OUTPUT', '-j', chain) + ipt('-D', 'PREROUTING', '-j', chain) + ipt('-F', chain) + ipt('-X', chain) + + if subnets: + ipt('-N', chain) + ipt('-F', chain) + ipt('-I', 'OUTPUT', '1', '-j', chain) + ipt('-I', 'PREROUTING', '1', '-j', chain) + + # create new subnet entries. Note that we're sorting in a very + # particular order: we need to go from most-specific (largest swidth) + # to least-specific, and at any given level of specificity, we want + # excludes to come first. That's why the columns are in such a non- + # intuitive order. + for swidth,sexclude,snet in sorted(subnets, reverse=True): + if sexclude: + ipt('-A', chain, '-j', 'RETURN', + '--dest', '%s/%s' % (snet,swidth), + '-p', 'tcp') + else: + ipt('-A', chain, '-j', 'REDIRECT', + '--dest', '%s/%s' % (snet,swidth), + '-p', 'tcp', + '--to-ports', str(port), + '-m', 'ttl', '!', '--ttl', '42' # to prevent infinite loops + ) + + +def ipfw_rule_exists(n): + argv = ['ipfw', 'list'] + p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) + found = False + for line in p.stdout: + if line.startswith('%05d ' % n): + if not ('ipttl 42 setup keep-state' in line + or ('skipto %d' % (n+1)) in line + or 'check-state' in line): + log('non-sshuttle ipfw rule: %r\n' % line.strip()) + raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n) + found = True + rv = p.wait() + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + return found + + +_oldctls = {} +def _fill_oldctls(prefix): + argv = ['sysctl', prefix] + p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) + for line in p.stdout: + assert(line[-1] == '\n') + (k,v) = line[:-1].split(': ', 1) + _oldctls[k] = v + rv = p.wait() + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + if not line: + raise Fatal('%r returned no data' % (argv,)) + + +def _sysctl_set(name, val): + argv = ['sysctl', '-w', '%s=%s' % (name, val)] + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv, stdout = open('/dev/null', 'w')) + + +_changedctls = [] +def sysctl_set(name, val): + PREFIX = 'net.inet.ip' + assert(name.startswith(PREFIX + '.')) + val = str(val) + if not _oldctls: + _fill_oldctls(PREFIX) + if not (name in _oldctls): + debug1('>> No such sysctl: %r\n' % name) + return + oldval = _oldctls[name] + if val != oldval: + _changedctls.append(name) + return _sysctl_set(name, val) + + +def ipfw(*args): + argv = ['ipfw', '-q'] + list(args) + debug1('>> %s\n' % ' '.join(argv)) + rv = ssubprocess.call(argv) + if rv: + raise Fatal('%r returned %d' % (argv, rv)) + + +def do_ipfw(port, subnets): + sport = str(port) + xsport = str(port+1) + + # cleanup any existing rules + if ipfw_rule_exists(port): + ipfw('delete', sport) + + while _changedctls: + name = _changedctls.pop() + oldval = _oldctls[name] + _sysctl_set(name, oldval) + + if subnets: + sysctl_set('net.inet.ip.fw.enable', 1) + sysctl_set('net.inet.ip.scopedroute', 0) + + ipfw('add', sport, 'check-state', 'ip', + 'from', 'any', 'to', 'any') + + # create new subnet entries + for swidth,sexclude,snet in sorted(subnets, reverse=True): + if sexclude: + ipfw('add', sport, 'skipto', xsport, + 'log', 'tcp', + 'from', 'any', 'to', '%s/%s' % (snet,swidth)) + else: + ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port, + 'log', 'tcp', + 'from', 'any', 'to', '%s/%s' % (snet,swidth), + 'not', 'ipttl', '42', 'keep-state', 'setup') + + +def program_exists(name): + paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) + for p in paths: + fn = '%s/%s' % (p, name) + if os.path.exists(fn): + return not os.path.isdir(fn) and os.access(fn, os.X_OK) + +hostmap = {} +def rewrite_etc_hosts(port): + HOSTSFILE='/etc/hosts' + BAKFILE='%s.sbak' % HOSTSFILE + APPEND='# sshuttle-firewall-%d AUTOCREATED' % port + old_content = '' + st = None + try: + old_content = open(HOSTSFILE).read() + st = os.stat(HOSTSFILE) + except IOError, e: + if e.errno == errno.ENOENT: + pass + else: + raise + if old_content.strip() and not os.path.exists(BAKFILE): + os.link(HOSTSFILE, BAKFILE) + tmpname = "%s.%d.tmp" % (HOSTSFILE, port) + f = open(tmpname, 'w') + for line in old_content.rstrip().split('\n'): + if line.find(APPEND) >= 0: + continue + f.write('%s\n' % line) + for (name,ip) in sorted(hostmap.items()): + f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND)) + f.close() + + if st: + os.chown(tmpname, st.st_uid, st.st_gid) + os.chmod(tmpname, st.st_mode) + else: + os.chown(tmpname, 0, 0) + os.chmod(tmpname, 0644) + os.rename(tmpname, HOSTSFILE) + + +def restore_etc_hosts(port): + global hostmap + hostmap = {} + rewrite_etc_hosts(port) + + +# This is some voodoo for setting up the kernel's transparent +# proxying stuff. If subnets is empty, we just delete our sshuttle rules; +# otherwise we delete it, then make them from scratch. +# +# This code is supposed to clean up after itself by deleting its rules on +# exit. In case that fails, it's not the end of the world; future runs will +# supercede it in the transproxy list, at least, so the leftover rules +# are hopefully harmless. +def main(port, syslog): + assert(port > 0) + assert(port <= 65535) + + if os.getuid() != 0: + raise Fatal('you must be root (or enable su/sudo) to set the firewall') + + if program_exists('ipfw'): + do_it = do_ipfw + elif program_exists('iptables'): + do_it = do_iptables + else: + raise Fatal("can't find either ipfw or iptables; check your PATH") + + # because of limitations of the 'su' command, the *real* stdin/stdout + # are both attached to stdout initially. Clone stdout into stdin so we + # can read from it. + os.dup2(1, 0) + + if syslog: + ssyslog.start_syslog() + ssyslog.stderr_to_syslog() + + debug1('firewall manager ready.\n') + sys.stdout.write('READY\n') + sys.stdout.flush() + + # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, + # I'll die automatically. + os.setsid() + + # we wait until we get some input before creating the rules. That way, + # sshuttle can launch us as early as possible (and get sudo password + # authentication as early in the startup process as possible). + line = sys.stdin.readline(128) + if not line: + return # parent died; nothing to do + + subnets = [] + if line != 'ROUTES\n': + raise Fatal('firewall: expected ROUTES but got %r' % line) + while 1: + line = sys.stdin.readline(128) + if not line: + raise Fatal('firewall: expected route but got %r' % line) + elif line == 'GO\n': + break + try: + (width,exclude,ip) = line.strip().split(',', 2) + except: + raise Fatal('firewall: expected route or GO but got %r' % line) + subnets.append((int(width), bool(int(exclude)), ip)) + + try: + if line: + debug1('firewall manager: starting transproxy.\n') + do_it(port, subnets) + sys.stdout.write('STARTED\n') + + try: + sys.stdout.flush() + except IOError: + # the parent process died for some reason; he's surely been loud + # enough, so no reason to report another error + return + + # Now we wait until EOF or any other kind of exception. We need + # to stay running so that we don't need a *second* password + # authentication at shutdown time - that cleanup is important! + while 1: + line = sys.stdin.readline(128) + if line.startswith('HOST '): + (name,ip) = line[5:].strip().split(',', 1) + hostmap[name] = ip + rewrite_etc_hosts(port) + elif line: + raise Fatal('expected EOF, got %r' % line) + else: + break + finally: + try: + debug1('firewall manager: undoing changes.\n') + except: + pass + do_it(port, []) + restore_etc_hosts(port) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py new file mode 100644 index 0000000..18871a2 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/helpers.py @@ -0,0 +1,37 @@ +import sys, os + +logprefix = '' +verbose = 0 + +def log(s): + try: + sys.stdout.flush() + sys.stderr.write(logprefix + s) + sys.stderr.flush() + except IOError: + # this could happen if stderr gets forcibly disconnected, eg. because + # our tty closes. That sucks, but it's no reason to abort the program. + pass + +def debug1(s): + if verbose >= 1: + log(s) + +def debug2(s): + if verbose >= 2: + log(s) + +def debug3(s): + if verbose >= 3: + log(s) + + +class Fatal(Exception): + pass + + +def list_contains_any(l, sub): + for i in sub: + if i in l: + return True + return False diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.py new file mode 100644 index 0000000..d77a58f --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/hostwatch.py @@ -0,0 +1,277 @@ +import time, socket, re, select, errno +if not globals().get('skip_imports'): + import compat.ssubprocess as ssubprocess + import helpers + from helpers import * + +POLL_TIME = 60*15 +NETSTAT_POLL_TIME = 30 +CACHEFILE=os.path.expanduser('~/.sshuttle.hosts') + + +_nmb_ok = True +_smb_ok = True +hostnames = {} +queue = {} +null = open('/dev/null', 'rb+') + + +def _is_ip(s): + return re.match(r'\d+\.\d+\.\d+\.\d+$', s) + + +def write_host_cache(): + tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) + try: + f = open(tmpname, 'wb') + for name,ip in sorted(hostnames.items()): + f.write('%s,%s\n' % (name, ip)) + f.close() + os.rename(tmpname, CACHEFILE) + finally: + try: + os.unlink(tmpname) + except: + pass + + +def read_host_cache(): + try: + f = open(CACHEFILE) + except IOError, e: + if e.errno == errno.ENOENT: + return + else: + raise + for line in f: + words = line.strip().split(',') + if len(words) == 2: + (name,ip) = words + name = re.sub(r'[^-\w]', '-', name).strip() + ip = re.sub(r'[^0-9.]', '', ip).strip() + if name and ip: + found_host(name, ip) + + +def found_host(hostname, ip): + hostname = re.sub(r'\..*', '', hostname) + hostname = re.sub(r'[^-\w]', '_', hostname) + if (ip.startswith('127.') or ip.startswith('255.') + or hostname == 'localhost'): + return + oldip = hostnames.get(hostname) + if oldip != ip: + hostnames[hostname] = ip + debug1('Found: %s: %s\n' % (hostname, ip)) + sys.stdout.write('%s,%s\n' % (hostname, ip)) + write_host_cache() + + +def _check_etc_hosts(): + debug2(' > hosts\n') + for line in open('/etc/hosts'): + line = re.sub(r'#.*', '', line) + words = line.strip().split() + if not words: + continue + ip = words[0] + names = words[1:] + if _is_ip(ip): + debug3('< %s %r\n' % (ip, names)) + for n in names: + check_host(n) + found_host(n, ip) + + +def _check_revdns(ip): + debug2(' > rev: %s\n' % ip) + try: + r = socket.gethostbyaddr(ip) + debug3('< %s\n' % r[0]) + check_host(r[0]) + found_host(r[0], ip) + except socket.herror, e: + pass + + +def _check_dns(hostname): + debug2(' > dns: %s\n' % hostname) + try: + ip = socket.gethostbyname(hostname) + debug3('< %s\n' % ip) + check_host(ip) + found_host(hostname, ip) + except socket.gaierror, e: + pass + + +def _check_netstat(): + debug2(' > netstat\n') + argv = ['netstat', '-n'] + try: + p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) + content = p.stdout.read() + p.wait() + except OSError, e: + log('%r failed: %r\n' % (argv, e)) + return + + for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content): + debug3('< %s\n' % ip) + check_host(ip) + + +def _check_smb(hostname): + return + global _smb_ok + if not _smb_ok: + return + argv = ['smbclient', '-U', '%', '-L', hostname] + debug2(' > smb: %s\n' % hostname) + try: + p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) + lines = p.stdout.readlines() + p.wait() + except OSError, e: + log('%r failed: %r\n' % (argv, e)) + _smb_ok = False + return + + lines.reverse() + + # junk at top + while lines: + line = lines.pop().strip() + if re.match(r'Server\s+', line): + break + + # server list section: + # Server Comment + # ------ ------- + while lines: + line = lines.pop().strip() + if not line or re.match(r'-+\s+-+', line): + continue + if re.match(r'Workgroup\s+Master', line): + break + words = line.split() + hostname = words[0].lower() + debug3('< %s\n' % hostname) + check_host(hostname) + + # workgroup list section: + # Workgroup Master + # --------- ------ + while lines: + line = lines.pop().strip() + if re.match(r'-+\s+', line): + continue + if not line: + break + words = line.split() + (workgroup, hostname) = (words[0].lower(), words[1].lower()) + debug3('< group(%s) -> %s\n' % (workgroup, hostname)) + check_host(hostname) + check_workgroup(workgroup) + + if lines: + assert(0) + + +def _check_nmb(hostname, is_workgroup, is_master): + return + global _nmb_ok + if not _nmb_ok: + return + argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname] + debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) + try: + p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) + lines = p.stdout.readlines() + rv = p.wait() + except OSError, e: + log('%r failed: %r\n' % (argv, e)) + _nmb_ok = False + return + if rv: + log('%r returned %d\n' % (argv, rv)) + return + for line in lines: + m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line) + if m: + g = m.groups() + (ip, name) = (g[0], g[1].lower()) + debug3('< %s -> %s\n' % (name, ip)) + if is_workgroup: + _enqueue(_check_smb, ip) + else: + found_host(name, ip) + check_host(name) + + +def check_host(hostname): + if _is_ip(hostname): + _enqueue(_check_revdns, hostname) + else: + _enqueue(_check_dns, hostname) + _enqueue(_check_smb, hostname) + _enqueue(_check_nmb, hostname, False, False) + + +def check_workgroup(hostname): + _enqueue(_check_nmb, hostname, True, False) + _enqueue(_check_nmb, hostname, True, True) + + +def _enqueue(op, *args): + t = (op,args) + if queue.get(t) == None: + queue[t] = 0 + + +def _stdin_still_ok(timeout): + r,w,x = select.select([sys.stdin.fileno()], [], [], timeout) + if r: + b = os.read(sys.stdin.fileno(), 4096) + if not b: + return False + return True + + +def hw_main(seed_hosts): + if helpers.verbose >= 2: + helpers.logprefix = 'HH: ' + else: + helpers.logprefix = 'hostwatch: ' + + read_host_cache() + + _enqueue(_check_etc_hosts) + _enqueue(_check_netstat) + check_host('localhost') + check_host(socket.gethostname()) + check_workgroup('workgroup') + check_workgroup('-') + for h in seed_hosts: + check_host(h) + + while 1: + now = time.time() + for t,last_polled in queue.items(): + (op,args) = t + if not _stdin_still_ok(0): + break + maxtime = POLL_TIME + if op == _check_netstat: + maxtime = NETSTAT_POLL_TIME + if now - last_polled > maxtime: + queue[t] = time.time() + op(*args) + try: + sys.stdout.flush() + except IOError: + break + + # FIXME: use a smarter timeout based on oldest last_polled + if not _stdin_still_ok(1): + break diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py new file mode 100755 index 0000000..66954f8 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/main.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +import sys, os, re +import helpers, options, client, server, firewall, hostwatch +import compat.ssubprocess as ssubprocess +from helpers import * + + +# list of: +# 1.2.3.4/5 or just 1.2.3.4 +def parse_subnets(subnets_str): + subnets = [] + for s in subnets_str: + m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP subnet format' % s) + (a,b,c,d,width) = m.groups() + (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0)) + if width == None: + width = 32 + else: + width = int(width) + if a > 255 or b > 255 or c > 255 or d > 255: + raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) + if width > 32: + raise Fatal('*/%d is greater than the maximum of 32' % width) + subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width)) + return subnets + + +# 1.2.3.4:567 or just 1.2.3.4 or just 567 +def parse_ipport(s): + s = str(s) + m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP:port format' % s) + (a,b,c,d,port) = m.groups() + (a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0), + int(port or 0)) + if a > 255 or b > 255 or c > 255 or d > 255: + raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) + if port > 65535: + raise Fatal('*:%d is greater than the maximum of 65535' % port) + if a == None: + a = b = c = d = 0 + return ('%d.%d.%d.%d' % (a,b,c,d), port) + + +optspec = """ +sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] +sshuttle --server +sshuttle --firewall +sshuttle --hostwatch +-- +l,listen= transproxy to this ip address and port number [127.0.0.1:0] +H,auto-hosts scan for remote hostnames and update local /etc/hosts +N,auto-nets automatically determine subnets to route +python= path to python interpreter on the remote server [python] +r,remote= ssh hostname (and optional username) of remote sshuttle server +x,exclude= exclude this subnet (can be used more than once) +v,verbose increase debug message verbosity +e,ssh-cmd= the command to use to connect to the remote [ssh] +seed-hosts= with -H, use these hostnames for initial scan (comma-separated) +D,daemon run in the background as a daemon +syslog send log messages to syslog (default if you use --daemon) +pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] +server (internal use only) +firewall (internal use only) +hostwatch (internal use only) +""" +o = options.Options('sshuttle', optspec) +(opt, flags, extra) = o.parse(sys.argv[1:]) + +if opt.daemon: + opt.syslog = 1 +helpers.verbose = opt.verbose + +try: + if opt.server: + if len(extra) != 0: + o.fatal('no arguments expected') + sys.exit(server.main()) + elif opt.firewall: + if len(extra) != 1: + o.fatal('exactly one argument expected') + sys.exit(firewall.main(int(extra[0]), opt.syslog)) + elif opt.hostwatch: + sys.exit(hostwatch.hw_main(extra)) + else: + if len(extra) < 1 and not opt.auto_nets: + o.fatal('at least one subnet (or -N) expected') + includes = extra + excludes = ['127.0.0.0/8'] + for k,v in flags: + if k in ('-x','--exclude'): + excludes.append(v) + remotename = opt.remote + if remotename == '' or remotename == '-': + remotename = None + if opt.seed_hosts and not opt.auto_hosts: + o.fatal('--seed-hosts only works if you also use -H') + if opt.seed_hosts: + sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) + elif opt.auto_hosts: + sh = [] + else: + sh = None + sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'), + opt.ssh_cmd, + remotename, + opt.python, + sh, + opt.auto_nets, + parse_subnets(includes), + parse_subnets(excludes), + opt.syslog, opt.daemon, opt.pidfile)) +except Fatal, e: + log('fatal: %s\n' % e) + sys.exit(99) +except KeyboardInterrupt: + log('\n') + log('Keyboard interrupt: exiting.\n') + sys.exit(1) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py new file mode 100644 index 0000000..25322fb --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/options.py @@ -0,0 +1,201 @@ +"""Command-line options parser. +With the help of an options spec string, easily parse command-line options. +""" +import sys, os, textwrap, getopt, re, struct + +class OptDict: + def __init__(self): + self._opts = {} + + def __setitem__(self, k, v): + if k.startswith('no-') or k.startswith('no_'): + k = k[3:] + v = not v + self._opts[k] = v + + def __getitem__(self, k): + if k.startswith('no-') or k.startswith('no_'): + return not self._opts[k[3:]] + return self._opts[k] + + def __getattr__(self, k): + return self[k] + + +def _default_onabort(msg): + sys.exit(97) + + +def _intify(v): + try: + vv = int(v or '') + if str(vv) == v: + return vv + except ValueError: + pass + return v + + +def _atoi(v): + try: + return int(v or 0) + except ValueError: + return 0 + + +def _remove_negative_kv(k, v): + if k.startswith('no-') or k.startswith('no_'): + return k[3:], not v + return k,v + +def _remove_negative_k(k): + return _remove_negative_kv(k, None)[0] + + +def _tty_width(): + s = struct.pack("HHHH", 0, 0, 0, 0) + try: + import fcntl, termios + s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s) + except (IOError, ImportError): + return _atoi(os.environ.get('WIDTH')) or 70 + (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s) + return xsize or 70 + + +class Options: + """Option parser. + When constructed, two strings are mandatory. The first one is the command + name showed before error messages. The second one is a string called an + optspec that specifies the synopsis and option flags and their description. + For more information about optspecs, consult the bup-options(1) man page. + + Two optional arguments specify an alternative parsing function and an + alternative behaviour on abort (after having output the usage string). + + By default, the parser function is getopt.gnu_getopt, and the abort + behaviour is to exit the program. + """ + def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt, + onabort=_default_onabort): + self.exe = exe + self.optspec = optspec + self._onabort = onabort + self.optfunc = optfunc + self._aliases = {} + self._shortopts = 'h?' + self._longopts = ['help'] + self._hasparms = {} + self._defaults = {} + self._usagestr = self._gen_usage() + + def _gen_usage(self): + out = [] + lines = self.optspec.strip().split('\n') + lines.reverse() + first_syn = True + while lines: + l = lines.pop() + if l == '--': break + out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l)) + first_syn = False + out.append('\n') + last_was_option = False + while lines: + l = lines.pop() + if l.startswith(' '): + out.append('%s%s\n' % (last_was_option and '\n' or '', + l.lstrip())) + last_was_option = False + elif l: + (flags, extra) = l.split(' ', 1) + extra = extra.strip() + if flags.endswith('='): + flags = flags[:-1] + has_parm = 1 + else: + has_parm = 0 + g = re.search(r'\[([^\]]*)\]$', extra) + if g: + defval = g.group(1) + else: + defval = None + flagl = flags.split(',') + flagl_nice = [] + for f in flagl: + f,dvi = _remove_negative_kv(f, _intify(defval)) + self._aliases[f] = _remove_negative_k(flagl[0]) + self._hasparms[f] = has_parm + self._defaults[f] = dvi + if len(f) == 1: + self._shortopts += f + (has_parm and ':' or '') + flagl_nice.append('-' + f) + else: + f_nice = re.sub(r'\W', '_', f) + self._aliases[f_nice] = _remove_negative_k(flagl[0]) + self._longopts.append(f + (has_parm and '=' or '')) + self._longopts.append('no-' + f) + flagl_nice.append('--' + f) + flags_nice = ', '.join(flagl_nice) + if has_parm: + flags_nice += ' ...' + prefix = ' %-20s ' % flags_nice + argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(), + initial_indent=prefix, + subsequent_indent=' '*28)) + out.append(argtext + '\n') + last_was_option = True + else: + out.append('\n') + last_was_option = False + return ''.join(out).rstrip() + '\n' + + def usage(self, msg=""): + """Print usage string to stderr and abort.""" + sys.stderr.write(self._usagestr) + e = self._onabort and self._onabort(msg) or None + if e: + raise e + + def fatal(self, s): + """Print an error message to stderr and abort with usage string.""" + msg = 'error: %s\n' % s + sys.stderr.write(msg) + return self.usage(msg) + + def parse(self, args): + """Parse a list of arguments and return (options, flags, extra). + + In the returned tuple, "options" is an OptDict with known options, + "flags" is a list of option flags that were used on the command-line, + and "extra" is a list of positional arguments. + """ + try: + (flags,extra) = self.optfunc(args, self._shortopts, self._longopts) + except getopt.GetoptError, e: + self.fatal(e) + + opt = OptDict() + + for k,v in self._defaults.iteritems(): + k = self._aliases[k] + opt[k] = v + + for (k,v) in flags: + k = k.lstrip('-') + if k in ('h', '?', 'help'): + self.usage() + if k.startswith('no-'): + k = self._aliases[k[3:]] + v = 0 + else: + k = self._aliases[k] + if not self._hasparms[k]: + assert(v == '') + v = (opt._opts.get(k) or 0) + 1 + else: + v = _intify(v) + opt[k] = v + for (f1,f2) in self._aliases.iteritems(): + opt[f1] = opt._opts.get(f2) + return (opt,flags,extra) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py new file mode 100644 index 0000000..24dd462 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/server.py @@ -0,0 +1,176 @@ +import re, struct, socket, select, traceback +if not globals().get('skip_imports'): + import ssnet, helpers, hostwatch + import compat.ssubprocess as ssubprocess + from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper + from helpers import * + + +def _ipmatch(ipstr): + if ipstr == 'default': + ipstr = '0.0.0.0/0' + m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) + if m: + g = m.groups() + ips = g[0] + width = int(g[4] or 32) + if g[1] == None: + ips += '.0.0.0' + width = min(width, 8) + elif g[2] == None: + ips += '.0.0' + width = min(width, 16) + elif g[3] == None: + ips += '.0' + width = min(width, 24) + return (struct.unpack('!I', socket.inet_aton(ips))[0], width) + + +def _ipstr(ip, width): + if width >= 32: + return ip + else: + return "%s/%d" % (ip, width) + + +def _maskbits(netmask): + if not netmask: + return 32 + for i in range(32): + if netmask[0] & _shl(1, i): + return 32-i + return 0 + + +def _shl(n, bits): + return n * int(2**bits) + + +def _list_routes(): + argv = ['netstat', '-rn'] + p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) + routes = [] + for line in p.stdout: + cols = re.split(r'\s+', line) + ipw = _ipmatch(cols[0]) + if not ipw: + continue # some lines won't be parseable; never mind + maskw = _ipmatch(cols[2]) # linux only + mask = _maskbits(maskw) # returns 32 if maskw is null + width = min(ipw[1], mask) + ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width) + routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width)) + rv = p.wait() + if rv != 0: + log('WARNING: %r returned %d\n' % (argv, rv)) + log('WARNING: That prevents --auto-nets from working.\n') + return routes + + +def list_routes(): + for (ip,width) in _list_routes(): + if not ip.startswith('0.') and not ip.startswith('127.'): + yield (ip,width) + + +def _exc_dump(): + exc_info = sys.exc_info() + return ''.join(traceback.format_exception(*exc_info)) + + +def start_hostwatch(seed_hosts): + s1,s2 = socket.socketpair() + pid = os.fork() + if not pid: + # child + rv = 99 + try: + try: + s2.close() + os.dup2(s1.fileno(), 1) + os.dup2(s1.fileno(), 0) + s1.close() + rv = hostwatch.hw_main(seed_hosts) or 0 + except Exception, e: + log('%s\n' % _exc_dump()) + rv = 98 + finally: + os._exit(rv) + s1.close() + return pid,s2 + + +class Hostwatch: + def __init__(self): + self.pid = 0 + self.sock = None + + +def main(): + if helpers.verbose >= 1: + helpers.logprefix = ' s: ' + else: + helpers.logprefix = 'server: ' + + routes = list(list_routes()) + debug1('available routes:\n') + for r in routes: + debug1(' %s/%d\n' % r) + + # synchronization header + sys.stdout.write('SSHUTTLE0001') + sys.stdout.flush() + + handlers = [] + mux = Mux(socket.fromfd(sys.stdin.fileno(), + socket.AF_INET, socket.SOCK_STREAM), + socket.fromfd(sys.stdout.fileno(), + socket.AF_INET, socket.SOCK_STREAM)) + handlers.append(mux) + routepkt = '' + for r in routes: + routepkt += '%s,%d\n' % r + mux.send(0, ssnet.CMD_ROUTES, routepkt) + + hw = Hostwatch() + hw.leftover = '' + + def hostwatch_ready(): + assert(hw.pid) + content = hw.sock.recv(4096) + if content: + lines = (hw.leftover + content).split('\n') + if lines[-1]: + # no terminating newline: entry isn't complete yet! + hw.leftover = lines.pop() + lines.append('') + else: + hw.leftover = '' + mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines)) + else: + raise Fatal('hostwatch process died') + + def got_host_req(data): + if not hw.pid: + (hw.pid,hw.sock) = start_hostwatch(data.strip().split()) + handlers.append(Handler(socks = [hw.sock], + callback = hostwatch_ready)) + mux.got_host_req = got_host_req + + def new_channel(channel, data): + (dstip,dstport) = data.split(',', 1) + dstport = int(dstport) + outwrap = ssnet.connect_dst(dstip,dstport) + handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) + mux.new_channel = new_channel + + while mux.ok: + if hw.pid: + assert(hw.pid > 0) + (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) + if rpid: + raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) + + ssnet.runonce(handlers, mux) + mux.check_fullness() + mux.callback() diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py new file mode 100644 index 0000000..ac7f411 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssh.py @@ -0,0 +1,95 @@ +import sys, os, re, socket, zlib +import compat.ssubprocess as ssubprocess +import helpers +from helpers import * + + +def readfile(name): + basedir = os.path.dirname(os.path.abspath(sys.argv[0])) + path = [basedir] + sys.path + for d in path: + fullname = os.path.join(d, name) + if os.path.exists(fullname): + return open(fullname, 'rb').read() + raise Exception("can't find file %r in any of %r" % (name, path)) + + +def empackage(z, filename): + (path,basename) = os.path.split(filename) + content = z.compress(readfile(filename)) + content += z.flush(zlib.Z_SYNC_FLUSH) + return '%s\n%d\n%s' % (basename,len(content), content) + + +def connect(ssh_cmd, rhostport, python, stderr): + main_exe = sys.argv[0] + portl = [] + + rhostIsIPv6 = False + if (rhostport or '').count(':') > 1: + rhostIsIPv6 = True + if rhostport.count(']') or rhostport.count('['): + result = rhostport.split(']') + rhost = result[0].strip('[') + if len(result) > 1: + result[1] = result[1].strip(':') + if result[1] is not '': + portl = ['-p', str(int(result[1]))] + else: # can't disambiguate IPv6 colons and a port number. pass the hostname through. + rhost = rhostport + else: # IPv4 + l = (rhostport or '').split(':', 1) + rhost = l[0] + if len(l) > 1: + portl = ['-p', str(int(l[1]))] + + if rhost == '-': + rhost = None + + ipv6flag = [] + if rhostIsIPv6: + ipv6flag = ['-6'] + + z = zlib.compressobj(1) + content = readfile('assembler.py') + content2 = (empackage(z, 'helpers.py') + + empackage(z, 'compat/ssubprocess.py') + + empackage(z, 'ssnet.py') + + empackage(z, 'hostwatch.py') + + empackage(z, 'server.py') + + "\n") + + pyscript = r""" + import sys; + skip_imports=1; + verbosity=%d; + exec compile(sys.stdin.read(%d), "assembler.py", "exec") + """ % (helpers.verbose or 0, len(content)) + pyscript = re.sub(r'\s+', ' ', pyscript.strip()) + + + if not rhost: + argv = [python, '-c', pyscript] + else: + if ssh_cmd: + sshl = ssh_cmd.split(' ') + else: + sshl = ['ssh'] + argv = (sshl + + portl + + ipv6flag + + [rhost, '--', "'%s' -c '%s'" % (python, pyscript)]) + (s1,s2) = socket.socketpair() + def setup(): + # runs in the child process + s2.close() + s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) + s1.close() + debug2('executing: %r\n' % argv) + p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, + close_fds=True, stderr=stderr) + os.close(s1a) + os.close(s1b) + s2.sendall(content) + s2.sendall(content2) + return p, s2 diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle b/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle new file mode 100755 index 0000000..66954f8 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/sshuttle @@ -0,0 +1,122 @@ +#!/usr/bin/env python +import sys, os, re +import helpers, options, client, server, firewall, hostwatch +import compat.ssubprocess as ssubprocess +from helpers import * + + +# list of: +# 1.2.3.4/5 or just 1.2.3.4 +def parse_subnets(subnets_str): + subnets = [] + for s in subnets_str: + m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP subnet format' % s) + (a,b,c,d,width) = m.groups() + (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0)) + if width == None: + width = 32 + else: + width = int(width) + if a > 255 or b > 255 or c > 255 or d > 255: + raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) + if width > 32: + raise Fatal('*/%d is greater than the maximum of 32' % width) + subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width)) + return subnets + + +# 1.2.3.4:567 or just 1.2.3.4 or just 567 +def parse_ipport(s): + s = str(s) + m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s) + if not m: + raise Fatal('%r is not a valid IP:port format' % s) + (a,b,c,d,port) = m.groups() + (a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0), + int(port or 0)) + if a > 255 or b > 255 or c > 255 or d > 255: + raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) + if port > 65535: + raise Fatal('*:%d is greater than the maximum of 65535' % port) + if a == None: + a = b = c = d = 0 + return ('%d.%d.%d.%d' % (a,b,c,d), port) + + +optspec = """ +sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] +sshuttle --server +sshuttle --firewall +sshuttle --hostwatch +-- +l,listen= transproxy to this ip address and port number [127.0.0.1:0] +H,auto-hosts scan for remote hostnames and update local /etc/hosts +N,auto-nets automatically determine subnets to route +python= path to python interpreter on the remote server [python] +r,remote= ssh hostname (and optional username) of remote sshuttle server +x,exclude= exclude this subnet (can be used more than once) +v,verbose increase debug message verbosity +e,ssh-cmd= the command to use to connect to the remote [ssh] +seed-hosts= with -H, use these hostnames for initial scan (comma-separated) +D,daemon run in the background as a daemon +syslog send log messages to syslog (default if you use --daemon) +pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] +server (internal use only) +firewall (internal use only) +hostwatch (internal use only) +""" +o = options.Options('sshuttle', optspec) +(opt, flags, extra) = o.parse(sys.argv[1:]) + +if opt.daemon: + opt.syslog = 1 +helpers.verbose = opt.verbose + +try: + if opt.server: + if len(extra) != 0: + o.fatal('no arguments expected') + sys.exit(server.main()) + elif opt.firewall: + if len(extra) != 1: + o.fatal('exactly one argument expected') + sys.exit(firewall.main(int(extra[0]), opt.syslog)) + elif opt.hostwatch: + sys.exit(hostwatch.hw_main(extra)) + else: + if len(extra) < 1 and not opt.auto_nets: + o.fatal('at least one subnet (or -N) expected') + includes = extra + excludes = ['127.0.0.0/8'] + for k,v in flags: + if k in ('-x','--exclude'): + excludes.append(v) + remotename = opt.remote + if remotename == '' or remotename == '-': + remotename = None + if opt.seed_hosts and not opt.auto_hosts: + o.fatal('--seed-hosts only works if you also use -H') + if opt.seed_hosts: + sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) + elif opt.auto_hosts: + sh = [] + else: + sh = None + sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'), + opt.ssh_cmd, + remotename, + opt.python, + sh, + opt.auto_nets, + parse_subnets(includes), + parse_subnets(excludes), + opt.syslog, opt.daemon, opt.pidfile)) +except Fatal, e: + log('fatal: %s\n' % e) + sys.exit(99) +except KeyboardInterrupt: + log('\n') + log('Keyboard interrupt: exiting.\n') + sys.exit(1) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py new file mode 100644 index 0000000..62fa378 --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssnet.py @@ -0,0 +1,520 @@ +import struct, socket, errno, select +if not globals().get('skip_imports'): + from helpers import * + +# these don't exist in the socket module in python 2.3! +SHUT_RD = 0 +SHUT_WR = 1 +SHUT_RDWR = 2 + + +HDR_LEN = 8 + + +CMD_EXIT = 0x4200 +CMD_PING = 0x4201 +CMD_PONG = 0x4202 +CMD_CONNECT = 0x4203 +CMD_STOP_SENDING = 0x4204 +CMD_EOF = 0x4205 +CMD_DATA = 0x4206 +CMD_ROUTES = 0x4207 +CMD_HOST_REQ = 0x4208 +CMD_HOST_LIST = 0x4209 + +cmd_to_name = { + CMD_EXIT: 'EXIT', + CMD_PING: 'PING', + CMD_PONG: 'PONG', + CMD_CONNECT: 'CONNECT', + CMD_STOP_SENDING: 'STOP_SENDING', + CMD_EOF: 'EOF', + CMD_DATA: 'DATA', + CMD_ROUTES: 'ROUTES', + CMD_HOST_REQ: 'HOST_REQ', + CMD_HOST_LIST: 'HOST_LIST', +} + + + +def _add(l, elem): + if not elem in l: + l.append(elem) + + +def _fds(l): + out = [] + for i in l: + try: + out.append(i.fileno()) + except AttributeError: + out.append(i) + out.sort() + return out + + +def _nb_clean(func, *args): + try: + return func(*args) + except OSError, e: + if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): + raise + else: + debug3('%s: err was: %s\n' % (func.__name__, e)) + return None + + +def _try_peername(sock): + try: + pn = sock.getpeername() + if pn: + return '%s:%s' % (pn[0], pn[1]) + except socket.error, e: + if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK): + raise + return 'unknown' + + +_swcount = 0 +class SockWrapper: + def __init__(self, rsock, wsock, connect_to=None, peername=None): + global _swcount + _swcount += 1 + debug3('creating new SockWrapper (%d now exist\n)' % _swcount) + self.exc = None + self.rsock = rsock + self.wsock = wsock + self.shut_read = self.shut_write = False + self.buf = [] + self.connect_to = connect_to + self.peername = peername or _try_peername(self.rsock) + self.try_connect() + + def __del__(self): + global _swcount + _swcount -= 1 + debug1('%r: deleting (%d remain)\n' % (self, _swcount)) + if self.exc: + debug1('%r: error was: %r\n' % (self, self.exc)) + + def __repr__(self): + if self.rsock == self.wsock: + fds = '#%d' % self.rsock.fileno() + else: + fds = '#%d,%d' % (self.rsock.fileno(), self.wsock.fileno()) + return 'SW%s:%s' % (fds, self.peername) + + def seterr(self, e): + if not self.exc: + self.exc = e + self.nowrite() + self.noread() + + def try_connect(self): + if self.connect_to and self.shut_write: + self.noread() + self.connect_to = None + if not self.connect_to: + return # already connected + self.rsock.setblocking(False) + debug3('%r: trying connect to %r\n' % (self, self.connect_to)) + try: + self.rsock.connect(self.connect_to) + # connected successfully (Linux) + self.connect_to = None + except socket.error, e: + debug3('%r: connect result: %r\n' % (self, e)) + if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: + pass # not connected yet + elif e.args[0] == errno.EISCONN: + # connected successfully (BSD) + self.connect_to = None + elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT, + errno.EHOSTUNREACH, errno.ENETUNREACH, + errno.EACCES, errno.EPERM]: + # a "normal" kind of error + self.connect_to = None + self.seterr(e) + else: + raise # error we've never heard of?! barf completely. + + def noread(self): + if not self.shut_read: + debug2('%r: done reading\n' % self) + self.shut_read = True + #self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway + + def nowrite(self): + if not self.shut_write: + debug2('%r: done writing\n' % self) + self.shut_write = True + try: + self.wsock.shutdown(SHUT_WR) + except socket.error, e: + self.seterr('nowrite: %s' % e) + + def too_full(self): + return False # fullness is determined by the socket's select() state + + def uwrite(self, buf): + if self.connect_to: + return 0 # still connecting + self.wsock.setblocking(False) + try: + return _nb_clean(os.write, self.wsock.fileno(), buf) + except OSError, e: + if e.errno == errno.EPIPE: + debug1('%r: uwrite: got EPIPE\n' % self) + self.nowrite() + return 0 + else: + # unexpected error... stream is dead + self.seterr('uwrite: %s' % e) + return 0 + + def write(self, buf): + assert(buf) + return self.uwrite(buf) + + def uread(self): + if self.connect_to: + return None # still connecting + if self.shut_read: + return + self.rsock.setblocking(False) + try: + return _nb_clean(os.read, self.rsock.fileno(), 65536) + except OSError, e: + self.seterr('uread: %s' % e) + return '' # unexpected error... we'll call it EOF + + def fill(self): + if self.buf: + return + rb = self.uread() + if rb: + self.buf.append(rb) + if rb == '': # empty string means EOF; None means temporarily empty + self.noread() + + def copy_to(self, outwrap): + if self.buf and self.buf[0]: + wrote = outwrap.write(self.buf[0]) + self.buf[0] = self.buf[0][wrote:] + while self.buf and not self.buf[0]: + self.buf.pop(0) + if not self.buf and self.shut_read: + outwrap.nowrite() + + +class Handler: + def __init__(self, socks = None, callback = None): + self.ok = True + self.socks = socks or [] + if callback: + self.callback = callback + + def pre_select(self, r, w, x): + for i in self.socks: + _add(r, i) + + def callback(self): + log('--no callback defined-- %r\n' % self) + (r,w,x) = select.select(self.socks, [], [], 0) + for s in r: + v = s.recv(4096) + if not v: + log('--closed-- %r\n' % self) + self.socks = [] + self.ok = False + + +class Proxy(Handler): + def __init__(self, wrap1, wrap2): + Handler.__init__(self, [wrap1.rsock, wrap1.wsock, + wrap2.rsock, wrap2.wsock]) + self.wrap1 = wrap1 + self.wrap2 = wrap2 + + def pre_select(self, r, w, x): + if self.wrap1.shut_write: self.wrap2.noread() + if self.wrap2.shut_write: self.wrap1.noread() + + if self.wrap1.connect_to: + _add(w, self.wrap1.rsock) + elif self.wrap1.buf: + if not self.wrap2.too_full(): + _add(w, self.wrap2.wsock) + elif not self.wrap1.shut_read: + _add(r, self.wrap1.rsock) + + if self.wrap2.connect_to: + _add(w, self.wrap2.rsock) + elif self.wrap2.buf: + if not self.wrap1.too_full(): + _add(w, self.wrap1.wsock) + elif not self.wrap2.shut_read: + _add(r, self.wrap2.rsock) + + def callback(self): + self.wrap1.try_connect() + self.wrap2.try_connect() + self.wrap1.fill() + self.wrap2.fill() + self.wrap1.copy_to(self.wrap2) + self.wrap2.copy_to(self.wrap1) + if self.wrap1.buf and self.wrap2.shut_write: + self.wrap1.buf = [] + self.wrap1.noread() + if self.wrap2.buf and self.wrap1.shut_write: + self.wrap2.buf = [] + self.wrap2.noread() + if (self.wrap1.shut_read and self.wrap2.shut_read and + not self.wrap1.buf and not self.wrap2.buf): + self.ok = False + self.wrap1.nowrite() + self.wrap2.nowrite() + + +class Mux(Handler): + def __init__(self, rsock, wsock): + Handler.__init__(self, [rsock, wsock]) + self.rsock = rsock + self.wsock = wsock + self.new_channel = self.got_routes = None + self.got_host_req = self.got_host_list = None + self.channels = {} + self.chani = 0 + self.want = 0 + self.inbuf = '' + self.outbuf = [] + self.fullness = 0 + self.too_full = False + self.send(0, CMD_PING, 'chicken') + + def next_channel(self): + # channel 0 is special, so we never allocate it + for timeout in xrange(1024): + self.chani += 1 + if self.chani > 65535: + self.chani = 1 + if not self.channels.get(self.chani): + return self.chani + + def amount_queued(self): + total = 0 + for b in self.outbuf: + total += len(b) + return total + + def check_fullness(self): + if self.fullness > 32768: + if not self.too_full: + self.send(0, CMD_PING, 'rttest') + self.too_full = True + #ob = [] + #for b in self.outbuf: + # (s1,s2,c) = struct.unpack('!ccH', b[:4]) + # ob.append(c) + #log('outbuf: %d %r\n' % (self.amount_queued(), ob)) + + def send(self, channel, cmd, data): + data = str(data) + assert(len(data) <= 65535) + p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data + self.outbuf.append(p) + debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n' + % (channel, cmd_to_name.get(cmd,hex(cmd)), + len(data), self.fullness)) + self.fullness += len(data) + + def got_packet(self, channel, cmd, data): + debug2('< channel=%d cmd=%s len=%d\n' + % (channel, cmd_to_name.get(cmd,hex(cmd)), len(data))) + if cmd == CMD_PING: + self.send(0, CMD_PONG, data) + elif cmd == CMD_PONG: + debug2('received PING response\n') + self.too_full = False + self.fullness = 0 + elif cmd == CMD_EXIT: + self.ok = False + elif cmd == CMD_CONNECT: + assert(not self.channels.get(channel)) + if self.new_channel: + self.new_channel(channel, data) + elif cmd == CMD_ROUTES: + if self.got_routes: + self.got_routes(data) + else: + raise Exception('got CMD_ROUTES without got_routes?') + elif cmd == CMD_HOST_REQ: + if self.got_host_req: + self.got_host_req(data) + else: + raise Exception('got CMD_HOST_REQ without got_host_req?') + elif cmd == CMD_HOST_LIST: + if self.got_host_list: + self.got_host_list(data) + else: + raise Exception('got CMD_HOST_LIST without got_host_list?') + else: + callback = self.channels.get(channel) + if not callback: + log('warning: closed channel %d got cmd=%s len=%d\n' + % (channel, cmd_to_name.get(cmd,hex(cmd)), len(data))) + else: + callback(cmd, data) + + def flush(self): + self.wsock.setblocking(False) + if self.outbuf and self.outbuf[0]: + wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0]) + debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0]))) + if wrote: + self.outbuf[0] = self.outbuf[0][wrote:] + while self.outbuf and not self.outbuf[0]: + self.outbuf[0:1] = [] + + def fill(self): + self.rsock.setblocking(False) + try: + b = _nb_clean(os.read, self.rsock.fileno(), 32768) + except OSError, e: + raise Fatal('other end: %r' % e) + #log('<<< %r\n' % b) + if b == '': # EOF + self.ok = False + if b: + self.inbuf += b + + def handle(self): + self.fill() + #log('inbuf is: (%d,%d) %r\n' + # % (self.want, len(self.inbuf), self.inbuf)) + while 1: + if len(self.inbuf) >= (self.want or HDR_LEN): + (s1,s2,channel,cmd,datalen) = \ + struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) + assert(s1 == 'S') + assert(s2 == 'S') + self.want = datalen + HDR_LEN + if self.want and len(self.inbuf) >= self.want: + data = self.inbuf[HDR_LEN:self.want] + self.inbuf = self.inbuf[self.want:] + self.want = 0 + self.got_packet(channel, cmd, data) + else: + break + + def pre_select(self, r, w, x): + _add(r, self.rsock) + if self.outbuf: + _add(w, self.wsock) + + def callback(self): + (r,w,x) = select.select([self.rsock], [self.wsock], [], 0) + if self.rsock in r: + self.handle() + if self.outbuf and self.wsock in w: + self.flush() + + +class MuxWrapper(SockWrapper): + def __init__(self, mux, channel): + SockWrapper.__init__(self, mux.rsock, mux.wsock) + self.mux = mux + self.channel = channel + self.mux.channels[channel] = self.got_packet + self.socks = [] + debug2('new channel: %d\n' % channel) + + def __del__(self): + self.nowrite() + SockWrapper.__del__(self) + + def __repr__(self): + return 'SW%r:Mux#%d' % (self.peername,self.channel) + + def noread(self): + if not self.shut_read: + self.shut_read = True + self.mux.send(self.channel, CMD_STOP_SENDING, '') + self.maybe_close() + + def nowrite(self): + if not self.shut_write: + self.shut_write = True + self.mux.send(self.channel, CMD_EOF, '') + self.maybe_close() + + def maybe_close(self): + if self.shut_read and self.shut_write: + # remove the mux's reference to us. The python garbage collector + # will then be able to reap our object. + self.mux.channels[self.channel] = None + + def too_full(self): + return self.mux.too_full + + def uwrite(self, buf): + if self.mux.too_full: + return 0 # too much already enqueued + if len(buf) > 2048: + buf = buf[:2048] + self.mux.send(self.channel, CMD_DATA, buf) + return len(buf) + + def uread(self): + if self.shut_read: + return '' # EOF + else: + return None # no data available right now + + def got_packet(self, cmd, data): + if cmd == CMD_EOF: + self.noread() + elif cmd == CMD_STOP_SENDING: + self.nowrite() + elif cmd == CMD_DATA: + self.buf.append(data) + else: + raise Exception('unknown command %d (%d bytes)' + % (cmd, len(data))) + + +def connect_dst(ip, port): + debug2('Connecting to %s:%d\n' % (ip, port)) + outsock = socket.socket() + outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) + return SockWrapper(outsock, outsock, + connect_to = (ip,port), + peername = '%s:%d' % (ip,port)) + + +def runonce(handlers, mux): + r = [] + w = [] + x = [] + to_remove = filter(lambda s: not s.ok, handlers) + for h in to_remove: + handlers.remove(h) + + for s in handlers: + s.pre_select(r,w,x) + debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n' + % (len(handlers), _fds(r), _fds(w), _fds(x), + mux.fullness, mux.too_full)) + (r,w,x) = select.select(r,w,x) + debug2(' Ready: %d r=%r w=%r x=%r\n' + % (len(handlers), _fds(r), _fds(w), _fds(x))) + ready = r+w+x + did = {} + for h in handlers: + for s in h.socks: + if s in ready: + h.callback() + did[s] = 1 + for s in ready: + if not s in did: + raise Fatal('socket %r was not used by any handler' % s) diff --git a/Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.py b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.py new file mode 100644 index 0000000..0fa768c --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/sshuttle/ssyslog.py @@ -0,0 +1,16 @@ +import sys, os +from compat import ssubprocess + + +_p = None +def start_syslog(): + global _p + _p = ssubprocess.Popen(['logger', + '-p', 'daemon.notice', + '-t', 'sshuttle'], stdin=ssubprocess.PIPE) + + +def stderr_to_syslog(): + sys.stdout.flush() + sys.stderr.flush() + os.dup2(_p.stdin.fileno(), 2) diff --git a/Sshuttle VPN.app/Contents/Resources/stupid.py b/Sshuttle VPN.app/Contents/Resources/stupid.py new file mode 100644 index 0000000..fdb1e0b --- /dev/null +++ b/Sshuttle VPN.app/Contents/Resources/stupid.py @@ -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()