From c86c4f3949e2cc6db3c09828b9518e27c6c3a304 Mon Sep 17 00:00:00 2001 From: Robey Pointer Date: Fri, 27 Aug 2004 00:57:40 +0000 Subject: [PATCH] [project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-66] new ServerInterface class, outbound rekey works, etc. a bunch of changes that i'm too lazy to split out into individual patches: * all the server overrides from transport.py have been moved into a separate class ServerInterface, so server code doesn't have to subclass the whole paramiko library * updated demo_server to subclass ServerInterface * when re-keying during a session, block other messages until the new keys are activated (openssh doensn't like any other traffic during a rekey) * re-key when outbound limits are tripped too (was only counting inbound traffic) * don't log scary things on EOF --- README | 1 - demo_server.py | 19 +++-- paramiko/__init__.py | 3 + paramiko/auth_transport.py | 106 ++----------------------- paramiko/server.py | 154 +++++++++++++++++++++++++++++++++++++ paramiko/transport.py | 136 ++++++++++++++++++-------------- 6 files changed, 249 insertions(+), 170 deletions(-) create mode 100644 paramiko/server.py diff --git a/README b/README index 173533c..8958519 100644 --- a/README +++ b/README @@ -155,4 +155,3 @@ v0.9 FEAROW * multi-part auth not supported (ie, need username AND pk) * server mode needs better documentation * sftp server mode -* make invoke_subsystem, etc wait for a reply diff --git a/demo_server.py b/demo_server.py index 5d6cbb5..8d88996 100755 --- a/demo_server.py +++ b/demo_server.py @@ -15,7 +15,7 @@ host_key.read_private_key_file('demo_dss_key') print 'Read key: ' + paramiko.util.hexify(host_key.get_fingerprint()) -class ServerTransport(paramiko.Transport): +class Server (paramiko.ServerInterface): # 'data' is the output of base64.encodestring(str(key)) data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hpfAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMCKDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iTUWT10hcuO4Ks8=' good_pub_key = paramiko.RSAKey(data=base64.decodestring(data)) @@ -23,24 +23,24 @@ class ServerTransport(paramiko.Transport): def check_channel_request(self, kind, chanid): if kind == 'session': return ServerChannel(chanid) - return self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + return paramiko.Transport.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): if (username == 'robey') and (password == 'foo'): - return self.AUTH_SUCCESSFUL - return self.AUTH_FAILED + return paramiko.Transport.AUTH_SUCCESSFUL + return paramiko.Transport.AUTH_FAILED def check_auth_publickey(self, username, key): print 'Auth attempt with key: ' + paramiko.util.hexify(key.get_fingerprint()) if (username == 'robey') and (key == self.good_pub_key): - return self.AUTH_SUCCESSFUL - return self.AUTH_FAILED + return paramiko.Transport.AUTH_SUCCESSFUL + return paramiko.Transport.AUTH_FAILED def get_allowed_auths(self, username): return 'password,publickey' -class ServerChannel(paramiko.Channel): +class ServerChannel (paramiko.Channel): "Channel descendant that pretends to understand pty and shell requests" def __init__(self, chanid): @@ -61,7 +61,6 @@ try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 2200)) except Exception, e: - print '*** Bind failed: ' + str(e) traceback.print_exc() sys.exit(1) @@ -79,14 +78,14 @@ print 'Got a connection!' try: event = threading.Event() - t = ServerTransport(client) + t = paramiko.Transport(client) try: t.load_server_moduli() except: print '(Failed to load moduli -- gex will be unsupported.)' raise t.add_server_key(host_key) - t.start_server(event) + t.start_server(event, Server()) while 1: event.wait(0.1) if not t.is_active(): diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 5d2925e..c987af2 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -76,6 +76,7 @@ SSHException = ssh_exception.SSHException Message = message.Message PasswordRequiredException = ssh_exception.PasswordRequiredException SFTP = sftp.SFTP +ServerInterface = server.ServerInterface __all__ = [ 'Transport', @@ -86,6 +87,7 @@ __all__ = [ 'Transport', 'SSHException', 'PasswordRequiredException', 'SFTP', + 'ServerInterface', 'transport', 'auth_transport', 'channel', @@ -95,4 +97,5 @@ __all__ = [ 'Transport', 'message', 'ssh_exception', 'sftp', + 'server', 'util' ] diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py index 048e3f0..55f6363 100644 --- a/paramiko/auth_transport.py +++ b/paramiko/auth_transport.py @@ -156,102 +156,6 @@ class Transport (BaseTransport): finally: self.lock.release() - def get_allowed_auths(self, username): - """ - I{(subclass override)} - Return a list of authentication methods supported by the server. - This list is sent to clients attempting to authenticate, to inform them - of authentication methods that might be successful. - - The "list" is actually a string of comma-separated names of types of - authentication. Possible values are C{"password"}, C{"publickey"}, - and C{"none"}. - - The default implementation always returns C{"password"}. - - @param username: the username requesting authentication. - @type username: string - @return: a comma-separated list of authentication types - @rtype: string - """ - return 'password' - - def check_auth_none(self, username): - """ - I{(subclass override)} - Determine if a client may open channels with no (further) - authentication. You should override this method in server mode. - - Return C{AUTH_FAILED} if the client must authenticate, or - C{AUTH_SUCCESSFUL} if it's okay for the client to not authenticate. - - The default implementation always returns C{AUTH_FAILED}. - - @param username: the username of the client. - @type username: string - @return: C{AUTH_FAILED} if the authentication fails; C{AUTH_SUCCESSFUL} - if it succeeds. - @rtype: int - """ - return self.AUTH_FAILED - - def check_auth_password(self, username, password): - """ - I{(subclass override)} - Determine if a given username and password supplied by the client is - acceptable for use in authentication. You should override this method - in server mode. - - Return C{AUTH_FAILED} if the password is not accepted, - C{AUTH_SUCCESSFUL} if the password is accepted and completes the - authentication, or C{AUTH_PARTIALLY_SUCCESSFUL} if your authentication - is stateful, and this key is accepted for authentication, but more - authentication is required. (In this latter case, L{get_allowed_auths} - will be called to report to the client what options it has for - continuing the authentication.) - - The default implementation always returns C{AUTH_FAILED}. - - @param username: the username of the authenticating client. - @type username: string - @param password: the password given by the client. - @type password: string - @return: C{AUTH_FAILED} if the authentication fails; C{AUTH_SUCCESSFUL} - if it succeeds; C{AUTH_PARTIALLY_SUCCESSFUL} if the password auth is - successful, but authentication must continue. - @rtype: int - """ - return self.AUTH_FAILED - - def check_auth_publickey(self, username, key): - """ - I{(subclass override)} - Determine if a given key supplied by the client is acceptable for use - in authentication. You should override this method in server mode to - check the username and key and decide if you would accept a signature - made using this key. - - Return C{AUTH_FAILED} if the key is not accepted, C{AUTH_SUCCESSFUL} - if the key is accepted and completes the authentication, or - C{AUTH_PARTIALLY_SUCCESSFUL} if your authentication is stateful, and - this key is accepted for authentication, but more authentication is - required. (In this latter case, L{get_allowed_auths} will be called - to report to the client what options it has for continuing the - authentication.) - - The default implementation always returns C{AUTH_FAILED}. - - @param username: the username of the authenticating client. - @type username: string - @param key: the key object provided by the client. - @type key: L{PKey } - @return: C{AUTH_FAILED} if the client can't authenticate with this key; - C{AUTH_SUCCESSFUL} if it can; C{AUTH_PARTIALLY_SUCCESSFUL} if it can - authenticate with this key but must continue with authentication. - @rtype: int - """ - return self.AUTH_FAILED - ### internals... @@ -355,7 +259,7 @@ class Transport (BaseTransport): self.auth_username = username if method == 'none': - result = self.check_auth_none(username) + result = self.server_object.check_auth_none(username) elif method == 'password': changereq = m.get_boolean() password = m.get_string().decode('UTF-8') @@ -366,7 +270,7 @@ class Transport (BaseTransport): newpassword = m.get_string().decode('UTF-8') result = self.AUTH_FAILED else: - result = self.check_auth_password(username, password) + result = self.server_object.check_auth_password(username, password) elif method == 'publickey': sig_attached = m.get_boolean() keytype = m.get_string() @@ -377,7 +281,7 @@ class Transport (BaseTransport): self._disconnect_no_more_auth() return # first check if this key is okay... if not, we can skip the verify - result = self.check_auth_publickey(username, key) + result = self.server_object.check_auth_publickey(username, key) if result != self.AUTH_FAILED: # key is okay, verify it if not sig_attached: @@ -395,7 +299,7 @@ class Transport (BaseTransport): self._log(DEBUG, 'Auth rejected: invalid signature') result = self.AUTH_FAILED else: - result = self.check_auth_none(username) + result = self.server_object.check_auth_none(username) # okay, send result m = Message() if result == self.AUTH_SUCCESSFUL: @@ -405,7 +309,7 @@ class Transport (BaseTransport): else: self._log(DEBUG, 'Auth rejected.') m.add_byte(chr(MSG_USERAUTH_FAILURE)) - m.add_string(self.get_allowed_auths(username)) + m.add_string(self.server_object.get_allowed_auths(username)) if result == self.AUTH_PARTIALLY_SUCCESSFUL: m.add_boolean(1) else: diff --git a/paramiko/server.py b/paramiko/server.py new file mode 100644 index 0000000..6abcffb --- /dev/null +++ b/paramiko/server.py @@ -0,0 +1,154 @@ +#!/usr/bin/python + +# Copyright (C) 2003-2004 Robey Pointer +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +L{ServerInterface} is an interface to override for server support. +""" + +from auth_transport import Transport + +class ServerInterface (object): + """ + This class defines an interface for controlling the behavior of paramiko + in server mode. + """ + + def check_channel_request(self, kind, chanid): + """ + Determine if a channel request of a given type will be granted, and + return a suitable L{Channel} object. This method is called in server + mode when the client requests a channel, after authentication is + complete. + + You will generally want to subclass L{Channel} to override some of the + methods for handling client requests (such as connecting to a subsystem + opening a shell) to determine what you want to allow or disallow. For + this reason, L{check_channel_request} must return a new object of that + type. The C{chanid} parameter is passed so that you can use it in + L{Channel}'s constructor. + + The default implementation always returns C{None}, rejecting any + channel requests. A useful server must override this method. + + @param kind: the kind of channel the client would like to open + (usually C{"session"}). + @type kind: string + @param chanid: ID of the channel, required to create a new L{Channel} + object. + @type chanid: int + @return: a new L{Channel} object (or subclass thereof), or C{None} to + refuse the request. + @rtype: L{Channel} + """ + return None + + def get_allowed_auths(self, username): + """ + Return a list of authentication methods supported by the server. + This list is sent to clients attempting to authenticate, to inform them + of authentication methods that might be successful. + + The "list" is actually a string of comma-separated names of types of + authentication. Possible values are C{"password"}, C{"publickey"}, + and C{"none"}. + + The default implementation always returns C{"password"}. + + @param username: the username requesting authentication. + @type username: string + @return: a comma-separated list of authentication types + @rtype: string + """ + return 'password' + + def check_auth_none(self, username): + """ + Determine if a client may open channels with no (further) + authentication. + + Return L{Transport.AUTH_FAILED} if the client must authenticate, or + L{Transport.AUTH_SUCCESSFUL} if it's okay for the client to not + authenticate. + + The default implementation always returns L{Transport.AUTH_FAILED}. + + @param username: the username of the client. + @type username: string + @return: L{Transport.AUTH_FAILED} if the authentication fails; + L{Transport.AUTH_SUCCESSFUL} if it succeeds. + @rtype: int + """ + return Transport.AUTH_FAILED + + def check_auth_password(self, username, password): + """ + Determine if a given username and password supplied by the client is + acceptable for use in authentication. + + Return L{Transport.AUTH_FAILED} if the password is not accepted, + L{Transport.AUTH_SUCCESSFUL} if the password is accepted and completes + the authentication, or L{Transport.AUTH_PARTIALLY_SUCCESSFUL} if your + authentication is stateful, and this key is accepted for + authentication, but more authentication is required. (In this latter + case, L{get_allowed_auths} will be called to report to the client what + options it has for continuing the authentication.) + + The default implementation always returns L{Transport.AUTH_FAILED}. + + @param username: the username of the authenticating client. + @type username: string + @param password: the password given by the client. + @type password: string + @return: L{Transport.AUTH_FAILED} if the authentication fails; + L{Transport.AUTH_SUCCESSFUL} if it succeeds; + L{Transport.AUTH_PARTIALLY_SUCCESSFUL} if the password auth is + successful, but authentication must continue. + @rtype: int + """ + return Transport.AUTH_FAILED + + def check_auth_publickey(self, username, key): + """ + Determine if a given key supplied by the client is acceptable for use + in authentication. You should override this method in server mode to + check the username and key and decide if you would accept a signature + made using this key. + + Return L{Transport.AUTH_FAILED} if the key is not accepted, + L{Transport.AUTH_SUCCESSFUL} if the key is accepted and completes the + authentication, or L{Transport.AUTH_PARTIALLY_SUCCESSFUL} if your + authentication is stateful, and this key is accepted for + authentication, but more authentication is required. (In this latter + case, L{get_allowed_auths} will be called to report to the client what + options it has for continuing the authentication.) + + The default implementation always returns L{Transport.AUTH_FAILED}. + + @param username: the username of the authenticating client. + @type username: string + @param key: the key object provided by the client. + @type key: L{PKey } + @return: L{Transport.AUTH_FAILED} if the client can't authenticate + with this key; L{Transport.AUTH_SUCCESSFUL} if it can; + L{Transport.AUTH_PARTIALLY_SUCCESSFUL} if it can authenticate with + this key but must continue with authentication. + @rtype: int + """ + return Transport.AUTH_FAILED diff --git a/paramiko/transport.py b/paramiko/transport.py index 06f7580..7931a11 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -138,6 +138,9 @@ class BaseTransport (threading.Thread): self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB try: + # we set the timeout so we can check self.active periodically to + # see if we should bail. socket.timeout exception is never + # propagated. self.sock.settimeout(0.1) except AttributeError: pass @@ -155,7 +158,7 @@ class BaseTransport (threading.Thread): self.expected_packet = 0 self.active = False self.initial_kex_done = False - self.write_lock = threading.Lock() # lock around outbound writes (packet computation) + self.write_lock = threading.RLock() # lock around outbound writes (packet computation) self.lock = threading.Lock() # synchronization (always higher level than write_lock) self.channels = { } # (id -> Channel) self.channel_events = { } # (id -> Event) @@ -165,10 +168,13 @@ class BaseTransport (threading.Thread): self.max_packet_size = 32768 self.ultra_debug = False self.saved_exception = None + self.clear_to_send = threading.Event() # used for noticing when to re-key: self.received_bytes = 0 self.received_packets = 0 self.received_packets_overflow = 0 + self.sent_bytes = 0 + self.sent_packets = 0 # user-defined event callbacks: self.completion_event = None # keepalives: @@ -176,6 +182,7 @@ class BaseTransport (threading.Thread): self.keepalive_last = time.time() # server mode: self.server_mode = 0 + self.server_object = None self.server_key_dict = { } self.server_accepts = [ ] self.server_accept_cv = threading.Condition(self.lock) @@ -223,7 +230,7 @@ class BaseTransport (threading.Thread): self.completion_event = event self.start() - def start_server(self, event=None): + def start_server(self, event=None, server=None): """ Negotiate a new SSH2 session as a server. This is the first step after creating a new L{Transport} and setting up your server host key(s). A @@ -235,15 +242,16 @@ class BaseTransport (threading.Thread): After a successful negotiation, the client will need to authenticate. Override the methods - L{get_allowed_auths }, - L{check_auth_none }, - L{check_auth_password }, and - L{check_auth_publickey } to control the - authentication process. + L{get_allowed_auths }, + L{check_auth_none }, + L{check_auth_password }, and + L{check_auth_publickey } in the + given C{server} object to control the authentication process. After a successful authentication, the client should request to open - a channel. Override L{check_channel_request} to allow channels to - be opened. + a channel. Override + L{check_channel_request } in the + given C{server} object to allow channels to be opened. @note: After calling this method (or L{start_client} or L{connect}), you should no longer directly read from or write to the original socket @@ -251,8 +259,14 @@ class BaseTransport (threading.Thread): @param event: an event to trigger when negotiation is complete. @type event: threading.Event + @param server: an object used to perform authentication and create + L{Channel}s. + @type server: L{server.ServerInterface} """ + if server is None: + server = ServerInterface() self.server_mode = 1 + self.server_object = server self.completion_event = event self.start() @@ -422,7 +436,7 @@ class BaseTransport (threading.Thread): self.channel_events[chanid] = event = threading.Event() chan._set_transport(self) chan._set_window(self.window_size, self.max_packet_size) - self._send_message(m) + self._send_user_message(m) finally: self.lock.release() while 1: @@ -457,7 +471,7 @@ class BaseTransport (threading.Thread): if bytes is None: bytes = (ord(randpool.get_bytes(1)) % 32) + 10 m.add_bytes(randpool.get_bytes(bytes)) - self._send_message(m) + self._send_user_message(m) def renegotiate_keys(self): """ @@ -528,7 +542,7 @@ class BaseTransport (threading.Thread): for item in data: m.add(item) self._log(DEBUG, 'Sending global request "%s"' % kind) - self._send_message(m) + self._send_user_message(m) if not wait: return True while True: @@ -539,36 +553,6 @@ class BaseTransport (threading.Thread): break return self.global_response - def check_channel_request(self, kind, chanid): - """ - I{(subclass override)} - Determine if a channel request of a given type will be granted, and - return a suitable L{Channel} object. This method is called in server - mode when the client requests a channel, after authentication is - complete. - - In server mode, you will generally want to subclass L{Channel} to - override some of the methods for handling client requests (such as - connecting to a subsystem or opening a shell) to determine what you - want to allow or disallow. For this reason, L{check_channel_request} - must return a new object of that type. The C{chanid} parameter is - passed so that you can use it in L{Channel}'s constructor. - - The default implementation always returns C{None}, rejecting any - channel requests. A useful server must override this method. - - @param kind: the kind of channel the client would like to open - (usually C{"session"}). - @type kind: string - @param chanid: ID of the channel, required to create a new L{Channel} - object. - @type chanid: int - @return: a new L{Channel} object (or subclass thereof), or C{None} to - refuse the request. - @rtype: L{Channel} - """ - return None - def check_global_request(self, kind, msg): """ I{(subclass override)} @@ -771,7 +755,11 @@ class BaseTransport (threading.Thread): def _write_all(self, out): self.keepalive_last = time.time() while len(out) > 0: - n = self.sock.send(out) + try: + n = self.sock.send(out) + except: + # could be: (32, 'Broken pipe') + n = -1 if n < 0: raise EOFError() if n == len(out): @@ -810,9 +798,33 @@ class BaseTransport (threading.Thread): self.sequence_number_out += 1L self.sequence_number_out %= 0x100000000L self._write_all(out) + + self.sent_bytes += len(out) + self.sent_packets += 1 + if ((self.sent_packets >= self.REKEY_PACKETS) or (self.sent_bytes >= self.REKEY_BYTES)) \ + and (self.local_kex_init is None): + # only ask once for rekeying + self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes sent)' % + (self.sent_packets, self.sent_bytes)) + self.received_packets_overflow = 0 + self._send_kex_init() finally: self.write_lock.release() + def _send_user_message(self, data): + """ + send a message, but block if we're in key negotiation. this is used + for user-initiated requests. + """ + while 1: + self.clear_to_send.wait(0.1) + if not self.active: + self._log(DEBUG, 'Dropping user packet because connection is dead.') + return + if self.clear_to_send.isSet(): + break + self._send_message(data) + def _read_message(self): "only one thread will ever be in this function" header = self._read_all(self.block_size_in) @@ -850,19 +862,19 @@ class BaseTransport (threading.Thread): # check for rekey self.received_bytes += packet_size + self.remote_mac_len + 4 self.received_packets += 1 - if (self.received_packets >= self.REKEY_PACKETS) or (self.received_bytes >= self.REKEY_BYTES): + if self.local_kex_init is not None: + # we've asked to rekey -- give them 20 packets to comply before + # dropping the connection + self.received_packets_overflow += 1 + if self.received_packets_overflow >= 20: + raise SSHException('Remote transport is ignoring rekey requests') + elif (self.received_packets >= self.REKEY_PACKETS) or \ + (self.received_bytes >= self.REKEY_BYTES): # only ask once for rekeying - if self.local_kex_init is None: - self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes)' % (self.received_packets, - self.received_bytes)) - self.received_packets_overflow = 0 - self._send_kex_init() - else: - # we've asked to rekey already -- give them 20 packets to - # 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') + self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes received)' % + (self.received_packets, self.received_bytes)) + self.received_packets_overflow = 0 + self._send_kex_init() cmd = ord(payload[0]) self._log(DEBUG, 'Read packet $%x, length %d' % (cmd, len(payload))) @@ -964,8 +976,8 @@ class BaseTransport (threading.Thread): self._log(ERROR, util.tb_strings()) self.saved_exception = e except EOFError, e: - self._log(DEBUG, 'EOF') - self._log(DEBUG, util.tb_strings()) + self._log(DEBUG, 'EOF in transport thread') + #self._log(DEBUG, util.tb_strings()) self.saved_exception = e except Exception, e: self._log(ERROR, 'Unknown exception: ' + str(e)) @@ -990,6 +1002,7 @@ class BaseTransport (threading.Thread): def _negotiate_keys(self, m): # throws SSHException on anything unusual + self.clear_to_send.clear() if self.local_kex_init == None: # remote side wants to renegotiate self._send_kex_init() @@ -1033,6 +1046,7 @@ class BaseTransport (threading.Thread): announce to the other side that we'd like to negotiate keys, and what kind of key negotiation we support. """ + self.clear_to_send.clear() if self.server_mode: if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self.preferred_kex): # can't do group-exchange if we don't have a pack of potential primes @@ -1066,6 +1080,8 @@ class BaseTransport (threading.Thread): self.received_bytes = 0 self.received_packets = 0 self.received_packets_overflow = 0 + self.sent_bytes = 0 + self.sent_packets = 0 cookie = m.get_bytes(16) kex_algo_list = m.get_list() @@ -1211,6 +1227,8 @@ class BaseTransport (threading.Thread): # send an event? if self.completion_event != None: self.completion_event.set() + # it's now okay to send data again (if this was a re-key) + self.clear_to_send.set() return def _parse_disconnect(self, m): @@ -1307,7 +1325,7 @@ class BaseTransport (threading.Thread): self.channel_counter += 1 finally: self.lock.release() - chan = self.check_channel_request(kind, my_chanid) + chan = self.server_object.check_channel_request(kind, my_chanid) if (chan is None) or (type(chan) is int): self._log(DEBUG, 'Rejecting "%s" channel request from client.' % kind) reject = True @@ -1373,3 +1391,5 @@ class BaseTransport (threading.Thread): MSG_CHANNEL_EOF: Channel._handle_eof, MSG_CHANNEL_CLOSE: Channel._handle_close, } + +from server import ServerInterface