diff --git a/MANIFEST b/MANIFEST index 68d7bde..e53d8e7 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,4 +1,5 @@ README +auth_transport.py ber.py channel.py dsskey.py @@ -11,3 +12,5 @@ setup.py transport.py util.py demo.py +demo_server.py +demo_host_key diff --git a/NOTES b/NOTES index e722b14..de3d2fc 100644 --- a/NOTES +++ b/NOTES @@ -58,6 +58,9 @@ from Channel: exec_command invoke_subsystem resize_pty + [server:] + check_pty_request + check_shell_request from ChannelFile: next diff --git a/auth_transport.py b/auth_transport.py index 78ce8d7..13b4a23 100644 --- a/auth_transport.py +++ b/auth_transport.py @@ -4,7 +4,7 @@ from transport import BaseTransport from transport import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, \ MSG_USERAUTH_SUCCESS, MSG_USERAUTH_BANNER from message import Message -from secsh import SSHException +from secsh import SecshException from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ @@ -34,7 +34,7 @@ class Transport(BaseTransport): def auth_key(self, username, key, event): if (not self.active) or (not self.initial_kex_done): # we should never try to send the password unless we're on a secure link - raise SSHException('No existing session') + raise SecshException('No existing session') try: self.lock.acquire() self.auth_event = event @@ -49,7 +49,7 @@ class Transport(BaseTransport): 'authenticate using a password; event is triggered on success or fail' if (not self.active) or (not self.initial_kex_done): # we should never try to send the password unless we're on a secure link - raise SSHException('No existing session') + raise SecshException('No existing session') try: self.lock.acquire() self.auth_event = event @@ -108,7 +108,7 @@ class Transport(BaseTransport): m.add_string(str(self.private_key)) m.add_string(self.private_key.sign_ssh_session(self.randpool, self.H, self.username)) else: - raise SSHException('Unknown auth method "%s"' % self.auth_method) + raise SecshException('Unknown auth method "%s"' % self.auth_method) self.send_message(m) else: self.log(DEBUG, 'Service request "%s" accepted (?)' % service) diff --git a/channel.py b/channel.py index 8f53d37..8bbe22a 100644 --- a/channel.py +++ b/channel.py @@ -1,5 +1,5 @@ from message import Message -from secsh import SSHException +from secsh import SecshException from transport import MSG_CHANNEL_REQUEST, MSG_CHANNEL_CLOSE, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, \ MSG_CHANNEL_EOF, MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE @@ -100,6 +100,22 @@ class Channel(object): finally: self.lock.release() + def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): + "override me! return True if a pty of the given dimensions (for shell access, usually) can be provided" + return False + + def check_shell_request(self): + "override me! return True if shell access will be provided" + return False + + def check_subsystem_request(self, name): + "override me! return True if the given subsystem can be provided" + return False + + def check_window_change_request(self, width, height, pixelwidth, pixelheight): + "override me! return True if the pty was resized" + return False + def handle_request(self, m): key = m.get_string() want_reply = m.get_boolean() @@ -110,10 +126,25 @@ class Channel(object): elif key == 'xon-xoff': # ignore ok = True - elif (key == 'pty-req') or (key == 'shell'): - if self.transport.server_mode: - # humor them - ok = True + elif key == 'pty-req': + term = m.get_string() + width = m.get_int() + height = m.get_int() + pixelwidth = m.get_int() + pixelheight = m.get_int() + modes = m.get_string() + ok = self.check_pty_request(term, width, height, pixelwidth, pixelheight, modes) + elif key == 'shell': + ok = self.check_shell_request() + elif key == 'subsystem': + name = m.get_string() + ok = self.check_subsystem_request(name) + elif key == 'window-change': + width = m.get_int() + height = m.get_int() + pixelwidth = m.get_int() + pixelheight = m.get_int() + ok = self.check_window_change_request(width, height, pixelwidth, pixelheight) else: self.log(DEBUG, 'Unhandled channel request "%s"' % key) ok = False @@ -155,7 +186,7 @@ class Channel(object): def get_pty(self, term='vt100', width=80, height=24): if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') + raise SecshException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) @@ -171,7 +202,7 @@ class Channel(object): def invoke_shell(self): if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') + raise SecshException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) @@ -181,7 +212,7 @@ class Channel(object): def exec_command(self, command): if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') + raise SecshException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) @@ -192,7 +223,7 @@ class Channel(object): def invoke_subsystem(self, subsystem): if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') + raise SecshException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) @@ -203,7 +234,7 @@ class Channel(object): def resize_pty(self, width=80, height=24): if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') + raise SecshException('Channel is not open') m = Message() m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) @@ -500,9 +531,6 @@ class ChannelFile(object): XXX Todo: the channel and its file-wrappers should be able to be closed or garbage-collected independently, for compatibility with real sockets and their file-wrappers. Currently, closing does nothing but flush the buffer. - XXX Todo: translation of the various forms of newline is not implemented, - let alone the universal newline. Line buffering (for writing) is - implemented, though, which makes little sense without text mode support. """ def __init__(self, channel, mode = "r", buf_size = -1): @@ -528,6 +556,9 @@ class ChannelFile(object): self.newlines = None self.softspace = False + def __repr__(self): + return '' + def __iter__(self): return self diff --git a/demo.py b/demo.py index 069077d..50ae3f9 100755 --- a/demo.py +++ b/demo.py @@ -66,7 +66,7 @@ except Exception, e: try: event = threading.Event() t = secsh.Transport(sock) - t.ultra_debug = 1 + t.ultra_debug = 0 t.start_client(event) # print repr(t) event.wait(10) diff --git a/demo-host-key b/demo_host_key similarity index 100% rename from demo-host-key rename to demo_host_key diff --git a/demo-server.py b/demo_server.py similarity index 72% rename from demo-server.py rename to demo_server.py index b0f8326..a4cf01c 100755 --- a/demo-server.py +++ b/demo_server.py @@ -1,25 +1,25 @@ #!/usr/bin/python -import sys, os, socket, threading, logging, traceback, time +import sys, os, socket, threading, logging, traceback import secsh # setup logging l = logging.getLogger("secsh") l.setLevel(logging.DEBUG) if len(l.handlers) == 0: - f = open('demo-server.log', 'w') + f = open('demo_server.log', 'w') lh = logging.StreamHandler(f) lh.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s] %(name)s: %(message)s', '%Y%m%d:%H%M%S')) l.addHandler(lh) host_key = secsh.RSAKey() -host_key.read_private_key_file('demo-host-key') +host_key.read_private_key_file('demo_host_key') class ServerTransport(secsh.Transport): def check_channel_request(self, kind, chanid): if kind == 'session': - return secsh.Channel(chanid) + return ServerChannel(chanid) return self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): @@ -27,6 +27,20 @@ class ServerTransport(secsh.Transport): return self.AUTH_SUCCESSFUL return self.AUTH_FAILED +class ServerChannel(secsh.Channel): + "Channel descendant that pretends to understand pty and shell requests" + + def __init__(self, chanid): + secsh.Channel.__init__(self, chanid) + self.event = threading.Event() + + def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): + return True + + def check_shell_request(self): + self.event.set() + return True + # now connect try: @@ -50,7 +64,7 @@ try: event = threading.Event() t = ServerTransport(client) t.add_server_key(host_key) - t.ultra_debug = 1 + t.ultra_debug = 0 t.start_server(event) # print repr(t) event.wait(10) @@ -60,7 +74,11 @@ try: # print repr(t) chan = t.accept() - time.sleep(2) + chan.event.wait(10) + if not chan.event.isSet(): + print '*** Client never asked for a shell.' + sys.exit(1) + chan.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') chan.send('We are on fire all the time! Hooray! Candy corn for everyone!\r\n') chan.send('Happy birthday to Robot Dave!\r\n\r\n') diff --git a/kex_gex.py b/kex_gex.py index 2b6e11c..ef1bd9f 100644 --- a/kex_gex.py +++ b/kex_gex.py @@ -5,8 +5,8 @@ # LOT more on the server side). from message import Message -from util import inflate_long, deflate_long, generate_prime -from secsh import SSHException +from util import inflate_long, deflate_long, generate_prime, bit_length +from secsh import SecshException from transport import MSG_NEWKEYS from Crypto.Hash import SHA from Crypto.Util import number @@ -49,17 +49,7 @@ class KexGex(object): return self.parse_kexdh_gex_init(m) elif ptype == MSG_KEXDH_GEX_REPLY: return self.parse_kexdh_gex_reply(m) - raise SSHException('KexGex asked to handle packet type %d' % ptype) - - def bit_length(n): - norm = deflate_long(n, 0) - hbyte = ord(norm[0]) - bitlen = len(norm) * 8 - while not (hbyte & 0x80): - hbyte <<= 1 - bitlen -= 1 - return bitlen - bit_length = staticmethod(bit_length) + raise SecshException('KexGex asked to handle packet type %d' % ptype) def generate_x(self): # generate an "x" (1 < x < (p-1)/2). @@ -116,9 +106,9 @@ class KexGex(object): self.p = m.get_mpint() self.g = m.get_mpint() # reject if p's bit length < 1024 or > 8192 - bitlen = self.bit_length(self.p) + bitlen = bit_length(self.p) if (bitlen < 1024) or (bitlen > 8192): - raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen) + raise SecshException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen) self.transport.log(DEBUG, 'Got server p (%d bits)' % bitlen) self.generate_x() # now compute e = g^x mod p @@ -132,7 +122,7 @@ class KexGex(object): def parse_kexdh_gex_init(self, m): self.e = m.get_mpint() if (self.e < 1) or (self.e > self.p - 1): - raise SSHException('Client kex "e" is out of range') + raise SecshException('Client kex "e" is out of range') self.generate_x() K = pow(self.e, self.x, P) key = str(self.transport.get_server_key()) @@ -164,7 +154,7 @@ class KexGex(object): self.f = m.get_mpint() sig = m.get_string() if (self.f < 1) or (self.f > self.p - 1): - raise SSHException('Server kex "f" is out of range') + raise SecshException('Server kex "f" is out of range') K = pow(self.f, self.x, self.p) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) hm = Message().add(self.transport.local_version).add(self.transport.remote_version) diff --git a/kex_group1.py b/kex_group1.py index 7d65c38..ab16ee4 100644 --- a/kex_group1.py +++ b/kex_group1.py @@ -5,7 +5,7 @@ # "g" generator. from message import Message, inflate_long -from secsh import SSHException +from secsh import SecshException from transport import MSG_NEWKEYS from Crypto.Hash import SHA from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL @@ -59,14 +59,14 @@ class KexGroup1(object): return self.parse_kexdh_init(m) elif not self.transport.server_mode and (ptype == MSG_KEXDH_REPLY): return self.parse_kexdh_reply(m) - raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) + raise SecshException('KexGroup1 asked to handle packet type %d' % ptype) def parse_kexdh_reply(self, m): # client mode host_key = m.get_string() self.f = m.get_mpint() if (self.f < 1) or (self.f > P - 1): - raise SSHException('Server kex "f" is out of range') + raise SecshException('Server kex "f" is out of range') sig = m.get_string() K = pow(self.f, self.x, P) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) @@ -82,7 +82,7 @@ class KexGroup1(object): # server mode self.e = m.get_mpint() if (self.e < 1) or (self.e > P - 1): - raise SSHException('Client kex "e" is out of range') + raise SecshException('Client kex "e" is out of range') K = pow(self.e, self.x, P) key = str(self.transport.get_server_key()) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) diff --git a/secsh.py b/secsh.py index 6f12494..526a966 100644 --- a/secsh.py +++ b/secsh.py @@ -5,8 +5,7 @@ import sys if (sys.version_info[0] < 2) or ((sys.version_info[0] == 2) and (sys.version_info[1] < 3)): raise RuntimeError('You need python 2.3 for this module.') -# FIXME rename -class SSHException(Exception): +class SecshException(Exception): pass @@ -17,7 +16,7 @@ from dsskey import DSSKey __author__ = "Robey Pointer " -__date__ = "18 Sep 2003" -__version__ = "0.1-bulbasaur" +__date__ = "9 Nov 2003" +__version__ = "0.1-charmander" __credits__ = "Huzzah!" diff --git a/setup.py b/setup.py index 64b3401..9eb1c9d 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,25 @@ from distutils.core import setup longdesc = ''' -This is a library for making client-side SSH2 connections (server-side is -coming soon). All major ciphers and hash methods are supported. +This is a library for making SSH2 connections (client or server). +Emphasis is on using SSH2 as an alternative to SSL for making secure +connections between pyton scripts. All major ciphers and hash methods +are supported. Required packages: pyCrypto ''' setup(name = "secsh", - version = "0.1-bulbasaur", + version = "0.1-charmander", description = "SSH2 protocol library", author = "Robey Pointer", author_email = "robey@lag.net", url = "http://www.lag.net/~robey/secsh/", - py_modules = [ 'secsh', 'transport', 'channel', 'message', 'util', 'ber', - 'kex_group1', 'kex_gex', 'rsakey', 'dsskey' ], - scripts = [ 'demo.py' ], - download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-bulbasaur.zip', + py_modules = [ 'secsh', 'transport', 'auth_transport', 'channel', + 'message', 'util', 'ber', 'kex_group1', 'kex_gex', + 'rsakey', 'dsskey' ], + download_url = 'http://www.lag.net/~robey/secsh/secsh-0.1-charmander.zip', license = 'LGPL', platforms = 'Posix; MacOS X; Windows', classifiers = [ 'Development Status :: 3 - Alpha', diff --git a/transport.py b/transport.py index c5ff252..611e577 100644 --- a/transport.py +++ b/transport.py @@ -14,7 +14,7 @@ MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ import sys, os, string, threading, socket, logging, struct from message import Message from channel import Channel -from secsh import SSHException +from secsh import SecshException from util import format_binary, safe_string, inflate_long, deflate_long, tb_strings from rsakey import RSAKey from dsskey import DSSKey @@ -79,8 +79,7 @@ class BaseTransport(threading.Thread): preferred_ciphers = [ 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ] preferred_macs = [ 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ] preferred_keys = [ 'ssh-rsa', 'ssh-dss' ] - preferred_kex = [ 'diffie-hellman-group1-sha1' ] -# preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ] + preferred_kex = [ 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ] cipher_info = { 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 }, @@ -203,7 +202,7 @@ class BaseTransport(threading.Thread): def get_remote_server_key(self): 'returns (type, key) where type is like "ssh-rsa" and key is an opaque string' if (not self.active) or (not self.initial_kex_done): - raise SSHException('No existing session') + raise SecshException('No existing session') key_msg = Message(self.host_key) key_type = key_msg.get_string() return key_type, self.host_key @@ -324,7 +323,7 @@ class BaseTransport(threading.Thread): # leftover contains decrypted bytes from the first block (after the length field) leftover = header[4:] if (packet_size - len(leftover)) % self.block_size_in != 0: - raise SSHException('Invalid packet blocking') + raise SecshException('Invalid packet blocking') buffer = self.read_all(packet_size + self.remote_mac_len - len(leftover)) packet = buffer[:packet_size - len(leftover)] post_packet = buffer[packet_size - len(leftover):] @@ -338,7 +337,7 @@ class BaseTransport(threading.Thread): mac_payload = struct.pack('>II', self.sequence_number_in, packet_size) + packet my_mac = HMAC.HMAC(self.mac_key_in, mac_payload, self.remote_mac_engine).digest()[:self.remote_mac_len] if my_mac != mac: - raise SSHException('Mismatched MAC') + raise SecshException('Mismatched MAC') padding = ord(packet[0]) payload = packet[1:packet_size - padding + 1] randpool.add_event(packet[packet_size - padding + 1]) @@ -361,7 +360,7 @@ class BaseTransport(threading.Thread): # comply, then just drop the connection self.received_packets_overflow += 1 if self.received_packets_overflow >= 20: - raise SSHException('Remote transport is ignoring rekey requests') + raise SecshException('Remote transport is ignoring rekey requests') return ord(payload[0]), msg @@ -380,9 +379,9 @@ class BaseTransport(threading.Thread): else: key = None if (key == None) or not key.valid: - raise SSHException('Unknown host key type') + raise SecshException('Unknown host key type') if not key.verify_ssh_sig(self.H, Message(sig)): - raise SSHException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type) + raise SecshException('Signature verification (%s) failed. Boo. Robey should debug this.' % self.host_key_type) self.host_key = host_key def compute_key(self, id, nbytes): @@ -405,7 +404,7 @@ class BaseTransport(threading.Thread): def get_cipher(self, name, key, iv): if not self.cipher_info.has_key(name): - raise SSHException('Unknown client cipher ' + name) + raise SecshException('Unknown client cipher ' + name) return self.cipher_info[name]['class'].new(key, self.cipher_info[name]['mode'], iv) def run(self): @@ -430,7 +429,7 @@ class BaseTransport(threading.Thread): continue if self.expected_packet != 0: if ptype != self.expected_packet: - raise SSHException('Expecting packet %d, got %d' % (self.expected_packet, ptype)) + raise SecshException('Expecting packet %d, got %d' % (self.expected_packet, ptype)) self.expected_packet = 0 if (ptype >= 30) and (ptype <= 39): self.kex_engine.parse_next(ptype, m) @@ -448,7 +447,7 @@ class BaseTransport(threading.Thread): msg.add_byte(chr(MSG_UNIMPLEMENTED)) msg.add_int(m.seqno) self.send_message(msg) - except SSHException, e: + except SecshException, e: self.log(DEBUG, 'Exception: ' + str(e)) self.log(DEBUG, tb_strings()) except EOFError, e: @@ -480,7 +479,7 @@ class BaseTransport(threading.Thread): return 1 def negotiate_keys(self, m): - # throws SSHException on anything unusual + # throws SecshException on anything unusual if self.local_kex_init == None: # remote side wants to renegotiate self.send_kex_init() @@ -500,7 +499,7 @@ class BaseTransport(threading.Thread): break self.log(DEBUG, 'Banner: ' + buffer) if buffer[:4] != 'SSH-': - raise SSHException('Indecipherable protocol version "' + buffer + '"') + raise SecshException('Indecipherable protocol version "' + buffer + '"') # save this server version string for later self.remote_version = buffer # pull off any attached comment @@ -512,15 +511,19 @@ class BaseTransport(threading.Thread): # parse out version string and make sure it matches segs = buffer.split('-', 2) if len(segs) < 3: - raise SSHException('Invalid SSH banner') + raise SecshException('Invalid SSH banner') version = segs[1] client = segs[2] if version != '1.99' and version != '2.0': - raise SSHException('Incompatible version (%s instead of 2.0)' % (version,)) + raise SecshException('Incompatible version (%s instead of 2.0)' % (version,)) self.log(INFO, 'Connected (version %s, client %s)' % (version, client)) def send_kex_init(self): # send a really wimpy kex-init packet that says we're a bare-bones ssh client + if self.server_mode: + # FIXME: can't do group-exchange (gex) yet -- too slow + if 'diffie-hellman-group-exchange-sha1' in self.preferred_kex: + self.preferred_kex.remove('diffie-hellman-group-exchange-sha1') m = Message() m.add_byte(chr(MSG_KEXINIT)) m.add_bytes(randpool.get_bytes(16)) @@ -563,7 +566,7 @@ class BaseTransport(threading.Thread): # no compression support (yet?) if (not('none' in client_compress_algo_list) or not('none' in server_compress_algo_list)): - raise SSHException('Incompatible ssh peer.') + raise SecshException('Incompatible ssh peer.') # as a server, we pick the first item in the client's list that we support. # as a client, we pick the first item in our list that the server supports. @@ -572,7 +575,7 @@ class BaseTransport(threading.Thread): else: agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex) if len(agreed_kex) == 0: - raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)') + raise SecshException('Incompatible ssh peer (no acceptable kex algorithm)') self.kex_engine = self.kex_info[agreed_kex[0]](self) if self.server_mode: @@ -580,10 +583,10 @@ class BaseTransport(threading.Thread): else: agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys) if len(agreed_keys) == 0: - raise SSHException('Incompatible ssh peer (no acceptable host key)') + raise SecshException('Incompatible ssh peer (no acceptable host key)') self.host_key_type = agreed_keys[0] if self.server_mode and (self.get_server_key() is None): - raise SSHException('Incompatible ssh peer (can\'t match requested host key type)') + raise SecshException('Incompatible ssh peer (can\'t match requested host key type)') if self.server_mode: agreed_local_ciphers = filter(self.preferred_ciphers.__contains__, @@ -596,7 +599,7 @@ class BaseTransport(threading.Thread): agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__, self.preferred_ciphers) if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0): - raise SSHException('Incompatible ssh server (no acceptable ciphers)') + raise SecshException('Incompatible ssh server (no acceptable ciphers)') self.local_cipher = agreed_local_ciphers[0] self.remote_cipher = agreed_remote_ciphers[0] self.log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher)) @@ -608,7 +611,7 @@ class BaseTransport(threading.Thread): agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs) agreed_remote_macs = filter(server_mac_algo_list.__contains__, self.preferred_macs) if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0): - raise SSHException('Incompatible ssh server (no acceptable macs)') + raise SecshException('Incompatible ssh server (no acceptable macs)') self.local_mac = agreed_local_macs[0] self.remote_mac = agreed_remote_macs[0]