Backward compatibility with Python 2.4 (server)

It is often the case that the user has no administrative control over
the server that is being used. As such it is important to support as
many versions as possible, at least on the remote server end. These
fixes will allow sshuttle to be used with servers that have only
python 2.4 or python 2.6 installed while hopefully not breaking the
compatibility with 2.7 and 3.5.
This commit is contained in:
vieira 2015-12-05 03:40:26 +00:00 committed by Brian May
parent 6e15e69029
commit 4241381d82
11 changed files with 111 additions and 65 deletions

View File

@ -1,6 +1,8 @@
language: python language: python
python: python:
- 2.6
- 2.7 - 2.7
- 3.4
- 3.5 - 3.5
- pypy - pypy

10
conftest.py Normal file
View File

@ -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")

View File

@ -62,6 +62,7 @@ cmd.exe with Administrator access. See :doc:`windows` for more information.
Server side Requirements Server side Requirements
------------------------ ------------------------
Server requirements are more relaxed, however it is recommended that you use
Python 2.7 or Python 3.5. Python 2.7 or Python 3.5.

View File

@ -4,19 +4,20 @@ import imp
z = zlib.decompressobj() z = zlib.decompressobj()
while 1: while 1:
name = stdin.readline().strip() name = sys.stdin.readline().strip()
if name: if name:
name = name.decode("ASCII") name = name.decode("ASCII")
nbytes = int(stdin.readline()) nbytes = int(sys.stdin.readline())
if verbosity >= 2: if verbosity >= 2:
sys.stderr.write('server: assembling %r (%d bytes)\n' sys.stderr.write('server: assembling %r (%d bytes)\n'
% (name, nbytes)) % (name, nbytes))
content = z.decompress(stdin.read(nbytes)) content = z.decompress(sys.stdin.read(nbytes))
module = imp.new_module(name) module = imp.new_module(name)
parent, _, parent_name = name.rpartition(".") parents = name.rsplit(".", 1)
if parent != "": if len(parents) == 2:
parent, parent_name = parents
setattr(sys.modules[parent], parent_name, module) setattr(sys.modules[parent], parent_name, module)
code = compile(content, name, "exec") code = compile(content, name, "exec")

View File

@ -5,6 +5,16 @@ import errno
logprefix = '' logprefix = ''
verbose = 0 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): def log(s):
global logprefix global logprefix
@ -70,7 +80,8 @@ def islocal(ip, family):
try: try:
try: try:
sock.bind((ip, 0)) sock.bind((ip, 0))
except socket.error as e: except socket.error:
_, e = sys.exc_info()[:2]
if e.args[0] == errno.EADDRNOTAVAIL: if e.args[0] == errno.EADDRNOTAVAIL:
return False # not a local IP return False # not a local IP
else: else:

View File

@ -22,7 +22,8 @@ hostnames = {}
queue = {} queue = {}
try: try:
null = open('/dev/null', 'wb') null = open('/dev/null', 'wb')
except IOError as e: except IOError:
_, e = sys.exc_info()[:2]
log('warning: %s\n' % e) log('warning: %s\n' % e)
null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096) 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()): for name, ip in sorted(hostnames.items()):
f.write(('%s,%s\n' % (name, ip)).encode("ASCII")) f.write(('%s,%s\n' % (name, ip)).encode("ASCII"))
f.close() f.close()
os.chmod(tmpname, 0o600) os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
os.rename(tmpname, CACHEFILE) os.rename(tmpname, CACHEFILE)
finally: finally:
try: try:
@ -50,7 +51,8 @@ def write_host_cache():
def read_host_cache(): def read_host_cache():
try: try:
f = open(CACHEFILE) f = open(CACHEFILE)
except IOError as e: except IOError:
_, e = sys.exc_info()[:2]
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
return return
else: else:
@ -124,7 +126,8 @@ def _check_netstat():
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
content = p.stdout.read().decode("ASCII") content = p.stdout.read().decode("ASCII")
p.wait() p.wait()
except OSError as e: except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e)) log('%r failed: %r\n' % (argv, e))
return return
@ -144,7 +147,8 @@ def _check_smb(hostname):
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
lines = p.stdout.readlines() lines = p.stdout.readlines()
p.wait() p.wait()
except OSError as e: except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e)) log('%r failed: %r\n' % (argv, e))
_smb_ok = False _smb_ok = False
return return
@ -201,7 +205,8 @@ def _check_nmb(hostname, is_workgroup, is_master):
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null)
lines = p.stdout.readlines() lines = p.stdout.readlines()
rv = p.wait() rv = p.wait()
except OSError as e: except OSError:
_, e = sys.exc_info()[:2]
log('%r failed: %r\n' % (argv, e)) log('%r failed: %r\n' % (argv, e))
_nmb_ok = False _nmb_ok = False
return return

View File

@ -12,29 +12,29 @@ import sshuttle.helpers as helpers
import sshuttle.hostwatch as hostwatch import sshuttle.hostwatch as hostwatch
import subprocess as ssubprocess import subprocess as ssubprocess
from sshuttle.ssnet import Handler, Proxy, Mux, MuxWrapper 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 resolvconf_random_nameserver
def _ipmatch(ipstr): def _ipmatch(ipstr):
# FIXME: IPv4 only # FIXME: IPv4 only
if ipstr == b'default': if ipstr == 'default':
ipstr = b'0.0.0.0/0' ipstr = '0.0.0.0/0'
m = re.match(b'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) m = re.match('^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr)
if m: if m:
g = m.groups() g = m.groups()
ips = g[0] ips = g[0]
width = int(g[4] or 32) width = int(g[4] or 32)
if g[1] is None: if g[1] is None:
ips += b'.0.0.0' ips += '.0.0.0'
width = min(width, 8) width = min(width, 8)
elif g[2] is None: elif g[2] is None:
ips += b'.0.0' ips += '.0.0'
width = min(width, 16) width = min(width, 16)
elif g[3] is None: elif g[3] is None:
ips += b'.0' ips += '.0'
width = min(width, 24) width = min(width, 24)
ips = ips.decode("ASCII") ips = ips
return (struct.unpack('!I', socket.inet_aton(ips))[0], width) return (struct.unpack('!I', socket.inet_aton(ips))[0], width)
@ -66,7 +66,7 @@ def _list_routes():
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE)
routes = [] routes = []
for line in p.stdout: for line in p.stdout:
cols = re.split(b'\s+', line) cols = re.split(r'\s+', line.decode("ASCII"))
ipw = _ipmatch(cols[0]) ipw = _ipmatch(cols[0])
if not ipw: if not ipw:
continue # some lines won't be parseable; never mind continue # some lines won't be parseable; never mind
@ -152,7 +152,8 @@ class DnsProxy(Handler):
try: try:
sock.send(self.request) sock.send(self.request)
self.socks.append(sock) 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: if e.args[0] in ssnet.NET_ERRS:
# might have been spurious; try again. # might have been spurious; try again.
# Note: these errors sometimes are reported by recv(), # Note: these errors sometimes are reported by recv(),
@ -169,7 +170,8 @@ class DnsProxy(Handler):
try: try:
data = sock.recv(4096) data = sock.recv(4096)
except socket.error as e: except socket.error:
_, e = sys.exc_info()[:2]
self.socks.remove(sock) self.socks.remove(sock)
del self.peers[sock] del self.peers[sock]
@ -204,14 +206,16 @@ class UdpProxy(Handler):
debug2('UDP: sending to %r port %d\n' % dstip) debug2('UDP: sending to %r port %d\n' % dstip)
try: try:
self.sock.sendto(data, dstip) 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)) log('UDP send to %r port %d: %s\n' % (dstip[0], dstip[1], e))
return return
def callback(self, sock): def callback(self, sock):
try: try:
data, peer = sock.recvfrom(4096) 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)) log('UDP recv from %r port %d: %s\n' % (peer[0], peer[1], e))
return return
debug2('UDP response: %d bytes\n' % len(data)) debug2('UDP response: %d bytes\n' % len(data))
@ -244,26 +248,26 @@ def main(latency_control):
socket.fromfd(sys.stdout.fileno(), socket.fromfd(sys.stdout.fileno(),
socket.AF_INET, socket.SOCK_STREAM)) socket.AF_INET, socket.SOCK_STREAM))
handlers.append(mux) handlers.append(mux)
routepkt = b'' routepkt = ''
for r in routes: for r in routes:
routepkt += b'%d,%s,%d\n' % (r[0], r[1].encode("ASCII"), r[2]) routepkt += '%d,%s,%d\n' % r
mux.send(0, ssnet.CMD_ROUTES, routepkt) mux.send(0, ssnet.CMD_ROUTES, b(routepkt))
hw = Hostwatch() hw = Hostwatch()
hw.leftover = b'' hw.leftover = b('')
def hostwatch_ready(sock): def hostwatch_ready(sock):
assert(hw.pid) assert(hw.pid)
content = hw.sock.recv(4096) content = hw.sock.recv(4096)
if content: if content:
lines = (hw.leftover + content).split(b'\n') lines = (hw.leftover + content).split(b('\n'))
if lines[-1]: if lines[-1]:
# no terminating newline: entry isn't complete yet! # no terminating newline: entry isn't complete yet!
hw.leftover = lines.pop() hw.leftover = lines.pop()
lines.append(b'') lines.append(b(''))
else: else:
hw.leftover = b'' hw.leftover = b('')
mux.send(0, ssnet.CMD_HOST_LIST, b'\n'.join(lines)) mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
else: else:
raise Fatal('hostwatch process died') raise Fatal('hostwatch process died')
@ -275,7 +279,7 @@ def main(latency_control):
mux.got_host_req = got_host_req mux.got_host_req = got_host_req
def new_channel(channel, data): def new_channel(channel, data):
(family, dstip, dstport) = data.split(b',', 2) (family, dstip, dstport) = data.decode("ASCII").split(',', 2)
family = int(family) family = int(family)
dstport = int(dstport) dstport = int(dstport)
outwrap = ssnet.connect_dst(family, dstip, dstport) outwrap = ssnet.connect_dst(family, dstip, dstport)

View File

@ -95,10 +95,10 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
b"\n") b"\n")
pyscript = r""" pyscript = r"""
import sys; import sys, os;
verbosity=%d; verbosity=%d;
stdin=getattr(sys.stdin,"buffer",sys.stdin); sys.stdin = os.fdopen(0, "rb");
exec(compile(stdin.read(%d), "assembler.py", "exec")) exec(compile(sys.stdin.read(%d), "assembler.py", "exec"))
""" % (helpers.verbose or 0, len(content)) """ % (helpers.verbose or 0, len(content))
pyscript = re.sub(r'\s+', ' ', pyscript.strip()) pyscript = re.sub(r'\s+', ' ', pyscript.strip())

View File

@ -1,9 +1,10 @@
import sys
import struct import struct
import socket import socket
import errno import errno
import select import select
import os 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 MAX_CHANNEL = 65535
@ -75,7 +76,8 @@ def _fds(l):
def _nb_clean(func, *args): def _nb_clean(func, *args):
try: try:
return func(*args) return func(*args)
except OSError as e: except OSError:
_, e = sys.exc_info()[:2]
if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise raise
else: else:
@ -88,7 +90,8 @@ def _try_peername(sock):
pn = sock.getpeername() pn = sock.getpeername()
if pn: if pn:
return '%s:%s' % (pn[0], pn[1]) 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): if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK):
raise raise
return 'unknown' return 'unknown'
@ -144,7 +147,8 @@ class SockWrapper:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux) # connected successfully (Linux)
self.connect_to = None 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)) debug3('%r: connect result: %s\n' % (self, e))
if e.args[0] == errno.EINVAL: if e.args[0] == errno.EINVAL:
# this is what happens when you call connect() on a socket # this is what happens when you call connect() on a socket
@ -191,7 +195,8 @@ class SockWrapper:
self.shut_write = True self.shut_write = True
try: try:
self.wsock.shutdown(SHUT_WR) self.wsock.shutdown(SHUT_WR)
except socket.error as e: except socket.error:
_, e = sys.exc_info()[:2]
self.seterr('nowrite: %s' % e) self.seterr('nowrite: %s' % e)
def too_full(self): def too_full(self):
@ -203,7 +208,8 @@ class SockWrapper:
self.wsock.setblocking(False) self.wsock.setblocking(False)
try: try:
return _nb_clean(os.write, self.wsock.fileno(), buf) 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: if e.errno == errno.EPIPE:
debug1('%r: uwrite: got EPIPE\n' % self) debug1('%r: uwrite: got EPIPE\n' % self)
self.nowrite() self.nowrite()
@ -225,9 +231,10 @@ class SockWrapper:
self.rsock.setblocking(False) self.rsock.setblocking(False)
try: try:
return _nb_clean(os.read, self.rsock.fileno(), 65536) 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) 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): def fill(self):
if self.buf: if self.buf:
@ -235,7 +242,7 @@ class SockWrapper:
rb = self.uread() rb = self.uread()
if rb: if rb:
self.buf.append(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() self.noread()
def copy_to(self, outwrap): def copy_to(self, outwrap):
@ -333,11 +340,11 @@ class Mux(Handler):
self.channels = {} self.channels = {}
self.chani = 0 self.chani = 0
self.want = 0 self.want = 0
self.inbuf = b'' self.inbuf = b('')
self.outbuf = [] self.outbuf = []
self.fullness = 0 self.fullness = 0
self.too_full = False self.too_full = False
self.send(0, CMD_PING, b'chicken') self.send(0, CMD_PING, b('chicken'))
def next_channel(self): def next_channel(self):
# channel 0 is special, so we never allocate it # channel 0 is special, so we never allocate it
@ -357,7 +364,7 @@ class Mux(Handler):
def check_fullness(self): def check_fullness(self):
if self.fullness > 32768: if self.fullness > 32768:
if not self.too_full: if not self.too_full:
self.send(0, CMD_PING, b'rttest') self.send(0, CMD_PING, b('rttest'))
self.too_full = True self.too_full = True
# ob = [] # ob = []
# for b in self.outbuf: # for b in self.outbuf:
@ -366,9 +373,9 @@ class Mux(Handler):
# log('outbuf: %d %r\n' % (self.amount_queued(), ob)) # log('outbuf: %d %r\n' % (self.amount_queued(), ob))
def send(self, channel, cmd, data): def send(self, channel, cmd, data):
assert isinstance(data, bytes) assert isinstance(data, binary_type)
assert len(data) <= 65535 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) self.outbuf.append(p)
debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n' debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n'
% (channel, cmd_to_name.get(cmd, hex(cmd)), % (channel, cmd_to_name.get(cmd, hex(cmd)),
@ -434,14 +441,15 @@ class Mux(Handler):
def fill(self): def fill(self):
self.rsock.setblocking(False) self.rsock.setblocking(False)
try: try:
b = _nb_clean(os.read, self.rsock.fileno(), 32768) read = _nb_clean(os.read, self.rsock.fileno(), 32768)
except OSError as e: except OSError:
_, e = sys.exc_info()[:2]
raise Fatal('other end: %r' % e) raise Fatal('other end: %r' % e)
# log('<<< %r\n' % b) # log('<<< %r\n' % b)
if b == b'': # EOF if read == b(''): # EOF
self.ok = False self.ok = False
if b: if read:
self.inbuf += b self.inbuf += read
def handle(self): def handle(self):
self.fill() self.fill()
@ -451,8 +459,8 @@ class Mux(Handler):
if len(self.inbuf) >= (self.want or HDR_LEN): if len(self.inbuf) >= (self.want or HDR_LEN):
(s1, s2, channel, cmd, datalen) = \ (s1, s2, channel, cmd, datalen) = \
struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) struct.unpack('!ccHHH', self.inbuf[:HDR_LEN])
assert(s1 == b'S') assert(s1 == b('S'))
assert(s2 == b'S') assert(s2 == b('S'))
self.want = datalen + HDR_LEN self.want = datalen + HDR_LEN
if self.want and len(self.inbuf) >= self.want: if self.want and len(self.inbuf) >= self.want:
data = self.inbuf[HDR_LEN:self.want] data = self.inbuf[HDR_LEN:self.want]
@ -496,14 +504,14 @@ class MuxWrapper(SockWrapper):
if not self.shut_read: if not self.shut_read:
debug2('%r: done reading\n' % self) debug2('%r: done reading\n' % self)
self.shut_read = True 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() self.maybe_close()
def nowrite(self): def nowrite(self):
if not self.shut_write: if not self.shut_write:
debug2('%r: done writing\n' % self) debug2('%r: done writing\n' % self)
self.shut_write = True 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() self.maybe_close()
def maybe_close(self): def maybe_close(self):
@ -526,7 +534,7 @@ class MuxWrapper(SockWrapper):
def uread(self): def uread(self):
if self.shut_read: if self.shut_read:
return b'' # EOF return b('') # EOF
else: else:
return None # no data available right now return None # no data available right now

View File

@ -5,9 +5,9 @@ from mock import patch, Mock, call
def test__ipmatch(): def test__ipmatch():
assert sshuttle.server._ipmatch(b"1.2.3.4") is not None assert sshuttle.server._ipmatch("1.2.3.4") is not None
assert sshuttle.server._ipmatch(b"::1") is None # ipv6 not supported assert sshuttle.server._ipmatch("::1") is None # ipv6 not supported
assert sshuttle.server._ipmatch(b"42 Example Street, Melbourne") is None assert sshuttle.server._ipmatch("42 Example Street, Melbourne") is None
def test__ipstr(): def test__ipstr():
@ -16,7 +16,7 @@ def test__ipstr():
def test__maskbits(): def test__maskbits():
netmask = sshuttle.server._ipmatch(b"255.255.255.0") netmask = sshuttle.server._ipmatch("255.255.255.0")
sshuttle.server._maskbits(netmask) sshuttle.server._maskbits(netmask)

View File

@ -1,12 +1,16 @@
[tox] [tox]
downloadcache = {toxworkdir}/cache/ downloadcache = {toxworkdir}/cache/
envlist = envlist =
py26,
py27, py27,
py34,
py35, py35,
[testenv] [testenv]
basepython = basepython =
py26: python2.6
py27: python2.7 py27: python2.7
py34: python3.4
py35: python3.5 py35: python3.5
commands = commands =
py.test py.test