diff --git a/.travis.yml b/.travis.yml index 4ab35a4..28e7a80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: python python: +- 2.6 - 2.7 +- 3.4 - 3.5 - pypy diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..969c119 --- /dev/null +++ b/conftest.py @@ -0,0 +1,10 @@ +import sys + +if sys.version_info >= (3, 0): + good_python = sys.version_info >= (3, 5) +else: + good_python = sys.version_info >= (2, 7) + +collect_ignore = [] +if not good_python: + collect_ignore.append("sshuttle/tests/client") diff --git a/docs/requirements.rst b/docs/requirements.rst index 0c66538..c35824f 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -62,6 +62,7 @@ cmd.exe with Administrator access. See :doc:`windows` for more information. Server side Requirements ------------------------ +Server requirements are more relaxed, however it is recommended that you use Python 2.7 or Python 3.5. diff --git a/sshuttle/assembler.py b/sshuttle/assembler.py index 330528d..137331e 100644 --- a/sshuttle/assembler.py +++ b/sshuttle/assembler.py @@ -4,19 +4,20 @@ import imp z = zlib.decompressobj() while 1: - name = stdin.readline().strip() + name = sys.stdin.readline().strip() if name: name = name.decode("ASCII") - nbytes = int(stdin.readline()) + nbytes = int(sys.stdin.readline()) if verbosity >= 2: sys.stderr.write('server: assembling %r (%d bytes)\n' % (name, nbytes)) - content = z.decompress(stdin.read(nbytes)) + content = z.decompress(sys.stdin.read(nbytes)) module = imp.new_module(name) - parent, _, parent_name = name.rpartition(".") - if parent != "": + parents = name.rsplit(".", 1) + if len(parents) == 2: + parent, parent_name = parents setattr(sys.modules[parent], parent_name, module) code = compile(content, name, "exec") diff --git a/sshuttle/helpers.py b/sshuttle/helpers.py index 4e80e33..67a6013 100644 --- a/sshuttle/helpers.py +++ b/sshuttle/helpers.py @@ -5,6 +5,16 @@ import errno logprefix = '' verbose = 0 +if sys.version_info[0] == 3: + binary_type = bytes + + def b(s): + return s.encode("ASCII") +else: + binary_type = str + + def b(s): + return s def log(s): global logprefix @@ -70,7 +80,8 @@ def islocal(ip, family): try: try: sock.bind((ip, 0)) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] if e.args[0] == errno.EADDRNOTAVAIL: return False # not a local IP else: diff --git a/sshuttle/hostwatch.py b/sshuttle/hostwatch.py index 95a539a..be210f9 100644 --- a/sshuttle/hostwatch.py +++ b/sshuttle/hostwatch.py @@ -22,7 +22,8 @@ hostnames = {} queue = {} try: null = open('/dev/null', 'wb') -except IOError as e: +except IOError: + _, e = sys.exc_info()[:2] log('warning: %s\n' % e) null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096) @@ -38,7 +39,7 @@ def write_host_cache(): for name, ip in sorted(hostnames.items()): f.write(('%s,%s\n' % (name, ip)).encode("ASCII")) f.close() - os.chmod(tmpname, 0o600) + os.chmod(tmpname, 384) # 600 in octal, 'rw-------' os.rename(tmpname, CACHEFILE) finally: try: @@ -50,7 +51,8 @@ def write_host_cache(): def read_host_cache(): try: f = open(CACHEFILE) - except IOError as e: + except IOError: + _, e = sys.exc_info()[:2] if e.errno == errno.ENOENT: return else: @@ -124,7 +126,8 @@ def _check_netstat(): p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) content = p.stdout.read().decode("ASCII") p.wait() - except OSError as e: + except OSError: + _, e = sys.exc_info()[:2] log('%r failed: %r\n' % (argv, e)) return @@ -144,7 +147,8 @@ def _check_smb(hostname): p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) lines = p.stdout.readlines() p.wait() - except OSError as e: + except OSError: + _, e = sys.exc_info()[:2] log('%r failed: %r\n' % (argv, e)) _smb_ok = False return @@ -201,7 +205,8 @@ def _check_nmb(hostname, is_workgroup, is_master): p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) lines = p.stdout.readlines() rv = p.wait() - except OSError as e: + except OSError: + _, e = sys.exc_info()[:2] log('%r failed: %r\n' % (argv, e)) _nmb_ok = False return diff --git a/sshuttle/server.py b/sshuttle/server.py index 78252b8..a47c760 100644 --- a/sshuttle/server.py +++ b/sshuttle/server.py @@ -12,29 +12,29 @@ import sshuttle.helpers as helpers import sshuttle.hostwatch as hostwatch import subprocess as ssubprocess from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper -from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, \ +from sshuttle.helpers import b, log, debug1, debug2, debug3, Fatal, \ resolvconf_random_nameserver def _ipmatch(ipstr): # FIXME: IPv4 only - if ipstr == b'default': - ipstr = b'0.0.0.0/0' - m = re.match(b'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) + if ipstr == 'default': + ipstr = '0.0.0.0/0' + m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) if m: g = m.groups() ips = g[0] width = int(g[4] or 32) if g[1] is None: - ips += b'.0.0.0' + ips += '.0.0.0' width = min(width, 8) elif g[2] is None: - ips += b'.0.0' + ips += '.0.0' width = min(width, 16) elif g[3] is None: - ips += b'.0' + ips += '.0' width = min(width, 24) - ips = ips.decode("ASCII") + ips = ips return (struct.unpack('!I', socket.inet_aton(ips))[0], width) @@ -66,7 +66,7 @@ def _list_routes(): p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) routes = [] for line in p.stdout: - cols = re.split(b'\s+', line) + cols = re.split(r'\s+', line.decode("ASCII")) ipw = _ipmatch(cols[0]) if not ipw: continue # some lines won't be parseable; never mind @@ -152,7 +152,8 @@ class DnsProxy(Handler): try: sock.send(self.request) self.socks.append(sock) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] if e.args[0] in ssnet.NET_ERRS: # might have been spurious; try again. # Note: these errors sometimes are reported by recv(), @@ -169,7 +170,8 @@ class DnsProxy(Handler): try: data = sock.recv(4096) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] self.socks.remove(sock) del self.peers[sock] @@ -204,14 +206,16 @@ class UdpProxy(Handler): debug2('UDP: sending to %r port %d\n' % dstip) try: self.sock.sendto(data, dstip) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e)) return def callback(self, sock): try: data, peer = sock.recvfrom(4096) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e)) return debug2('UDP response: %d bytes\n' % len(data)) @@ -244,26 +248,26 @@ def main(latency_control): socket.fromfd(sys.stdout.fileno(), socket.AF_INET, socket.SOCK_STREAM)) handlers.append(mux) - routepkt = b'' + routepkt = '' for r in routes: - routepkt += b'%d,%s,%d\n' % (r[0], r[1].encode("ASCII"), r[2]) - mux.send(0, ssnet.CMD_ROUTES, routepkt) + routepkt += '%d,%s,%d\n' % r + mux.send(0, ssnet.CMD_ROUTES, b(routepkt)) hw = Hostwatch() - hw.leftover = b'' + hw.leftover = b('') def hostwatch_ready(sock): assert(hw.pid) content = hw.sock.recv(4096) if content: - lines = (hw.leftover + content).split(b'\n') + lines = (hw.leftover + content).split(b('\n')) if lines[-1]: # no terminating newline: entry isn't complete yet! hw.leftover = lines.pop() - lines.append(b'') + lines.append(b('')) else: - hw.leftover = b'' - mux.send(0, ssnet.CMD_HOST_LIST, b'\n'.join(lines)) + hw.leftover = b('') + mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines)) else: raise Fatal('hostwatch process died') @@ -275,7 +279,7 @@ def main(latency_control): mux.got_host_req = got_host_req def new_channel(channel, data): - (family, dstip, dstport) = data.split(b',', 2) + (family, dstip, dstport) = data.decode("ASCII").split(',', 2) family = int(family) dstport = int(dstport) outwrap = ssnet.connect_dst(family, dstip, dstport) diff --git a/sshuttle/ssh.py b/sshuttle/ssh.py index cc89c0d..1a571d5 100644 --- a/sshuttle/ssh.py +++ b/sshuttle/ssh.py @@ -95,10 +95,10 @@ def connect(ssh_cmd, rhostport, python, stderr, options): b"\n") pyscript = r""" - import sys; + import sys, os; verbosity=%d; - stdin=getattr(sys.stdin,"buffer",sys.stdin); - exec(compile(stdin.read(%d), "assembler.py", "exec")) + sys.stdin = os.fdopen(0, "rb"); + exec(compile(sys.stdin.read(%d), "assembler.py", "exec")) """ % (helpers.verbose or 0, len(content)) pyscript = re.sub(r'\s+', ' ', pyscript.strip()) diff --git a/sshuttle/ssnet.py b/sshuttle/ssnet.py index 218e7b5..fe1de1e 100644 --- a/sshuttle/ssnet.py +++ b/sshuttle/ssnet.py @@ -1,9 +1,10 @@ +import sys import struct import socket import errno import select import os -from sshuttle.helpers import log, debug1, debug2, debug3, Fatal +from sshuttle.helpers import b, binary_type, log, debug1, debug2, debug3, Fatal MAX_CHANNEL = 65535 @@ -75,7 +76,8 @@ def _fds(l): def _nb_clean(func, *args): try: return func(*args) - except OSError as e: + except OSError: + _, e = sys.exc_info()[:2] if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): raise else: @@ -88,7 +90,8 @@ def _try_peername(sock): pn = sock.getpeername() if pn: return '%s:%s' % (pn[0], pn[1]) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK): raise return 'unknown' @@ -144,7 +147,8 @@ class SockWrapper: self.rsock.connect(self.connect_to) # connected successfully (Linux) self.connect_to = None - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] debug3('%r: connect result: %s\n' % (self, e)) if e.args[0] == errno.EINVAL: # this is what happens when you call connect() on a socket @@ -191,7 +195,8 @@ class SockWrapper: self.shut_write = True try: self.wsock.shutdown(SHUT_WR) - except socket.error as e: + except socket.error: + _, e = sys.exc_info()[:2] self.seterr('nowrite: %s' % e) def too_full(self): @@ -203,7 +208,8 @@ class SockWrapper: self.wsock.setblocking(False) try: return _nb_clean(os.write, self.wsock.fileno(), buf) - except OSError as e: + except OSError: + _, e = sys.exc_info()[:2] if e.errno == errno.EPIPE: debug1('%r: uwrite: got EPIPE\n' % self) self.nowrite() @@ -225,9 +231,10 @@ class SockWrapper: self.rsock.setblocking(False) try: return _nb_clean(os.read, self.rsock.fileno(), 65536) - except OSError as e: + except OSError: + _, e = sys.exc_info()[:2] self.seterr('uread: %s' % e) - return b'' # unexpected error... we'll call it EOF + return b('') # unexpected error... we'll call it EOF def fill(self): if self.buf: @@ -235,7 +242,7 @@ class SockWrapper: rb = self.uread() if rb: self.buf.append(rb) - if rb == b'': # empty string means EOF; None means temporarily empty + if rb == b(''): # empty string means EOF; None means temporarily empty self.noread() def copy_to(self, outwrap): @@ -333,11 +340,11 @@ class Mux(Handler): self.channels = {} self.chani = 0 self.want = 0 - self.inbuf = b'' + self.inbuf = b('') self.outbuf = [] self.fullness = 0 self.too_full = False - self.send(0, CMD_PING, b'chicken') + self.send(0, CMD_PING, b('chicken')) def next_channel(self): # channel 0 is special, so we never allocate it @@ -357,7 +364,7 @@ class Mux(Handler): def check_fullness(self): if self.fullness > 32768: if not self.too_full: - self.send(0, CMD_PING, b'rttest') + self.send(0, CMD_PING, b('rttest')) self.too_full = True # ob = [] # for b in self.outbuf: @@ -366,9 +373,9 @@ class Mux(Handler): # log('outbuf: %d %r\n' % (self.amount_queued(), ob)) def send(self, channel, cmd, data): - assert isinstance(data, bytes) + assert isinstance(data, binary_type) assert len(data) <= 65535 - p = struct.pack('!ccHHH', b'S', b'S', channel, cmd, len(data)) + data + p = struct.pack('!ccHHH', b('S'), b('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)), @@ -434,14 +441,15 @@ class Mux(Handler): def fill(self): self.rsock.setblocking(False) try: - b = _nb_clean(os.read, self.rsock.fileno(), 32768) - except OSError as e: + read = _nb_clean(os.read, self.rsock.fileno(), 32768) + except OSError: + _, e = sys.exc_info()[:2] raise Fatal('other end: %r' % e) # log('<<< %r\n' % b) - if b == b'': # EOF + if read == b(''): # EOF self.ok = False - if b: - self.inbuf += b + if read: + self.inbuf += read def handle(self): self.fill() @@ -451,8 +459,8 @@ class Mux(Handler): if len(self.inbuf) >= (self.want or HDR_LEN): (s1, s2, channel, cmd, datalen) = \ struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) - assert(s1 == b'S') - assert(s2 == b'S') + assert(s1 == b('S')) + assert(s2 == b('S')) self.want = datalen + HDR_LEN if self.want and len(self.inbuf) >= self.want: data = self.inbuf[HDR_LEN:self.want] @@ -496,14 +504,14 @@ class MuxWrapper(SockWrapper): if not self.shut_read: debug2('%r: done reading\n' % self) self.shut_read = True - self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b'') + self.mux.send(self.channel, CMD_TCP_STOP_SENDING, b('')) self.maybe_close() def nowrite(self): if not self.shut_write: debug2('%r: done writing\n' % self) self.shut_write = True - self.mux.send(self.channel, CMD_TCP_EOF, b'') + self.mux.send(self.channel, CMD_TCP_EOF, b('')) self.maybe_close() def maybe_close(self): @@ -526,7 +534,7 @@ class MuxWrapper(SockWrapper): def uread(self): if self.shut_read: - return b'' # EOF + return b('') # EOF else: return None # no data available right now diff --git a/sshuttle/tests/server/test_server.py b/sshuttle/tests/server/test_server.py index bf6985f..6b437f7 100644 --- a/sshuttle/tests/server/test_server.py +++ b/sshuttle/tests/server/test_server.py @@ -5,9 +5,9 @@ from mock import patch, Mock, call def test__ipmatch(): - assert sshuttle.server._ipmatch(b"1.2.3.4") is not None - assert sshuttle.server._ipmatch(b"::1") is None # ipv6 not supported - assert sshuttle.server._ipmatch(b"42 Example Street, Melbourne") is None + assert sshuttle.server._ipmatch("1.2.3.4") is not None + assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported + assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None def test__ipstr(): @@ -16,7 +16,7 @@ def test__ipstr(): def test__maskbits(): - netmask = sshuttle.server._ipmatch(b"255.255.255.0") + netmask = sshuttle.server._ipmatch("255.255.255.0") sshuttle.server._maskbits(netmask) diff --git a/tox.ini b/tox.ini index e2e93f8..2b66655 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,16 @@ [tox] downloadcache = {toxworkdir}/cache/ envlist = + py26, py27, + py34, py35, [testenv] basepython = + py26: python2.6 py27: python2.7 + py34: python3.4 py35: python3.5 commands = py.test