[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
This commit is contained in:
Robey Pointer 2004-08-27 00:57:40 +00:00
parent 34d975b972
commit c86c4f3949
6 changed files with 249 additions and 170 deletions

1
README
View File

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

View File

@ -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():

View File

@ -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' ]

View File

@ -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 <pkey.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:

154
paramiko/server.py Normal file
View File

@ -0,0 +1,154 @@
#!/usr/bin/python
# Copyright (C) 2003-2004 Robey Pointer <robey@lag.net>
#
# 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 <pkey.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

View File

@ -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 <Transport.get_allowed_auths>},
L{check_auth_none <Transport.check_auth_none>},
L{check_auth_password <Transport.check_auth_password>}, and
L{check_auth_publickey <Transport.check_auth_publickey>} to control the
authentication process.
L{get_allowed_auths <ServerInterface.get_allowed_auths>},
L{check_auth_none <ServerInterface.check_auth_none>},
L{check_auth_password <ServerInterface.check_auth_password>}, and
L{check_auth_publickey <ServerInterface.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 <ServerInterface.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