From aba7e37a383fc3d7ffedc6d9e433f65223ac5fe2 Mon Sep 17 00:00:00 2001 From: Robey Pointer Date: Fri, 3 Sep 2004 22:39:20 +0000 Subject: [PATCH] [project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-70] clean up server interface; no longer need to subclass Channel - export AUTH_*, OPEN_FAILED_*, and the new OPEN_SUCCEEDED into the paramiko namespace instead of making people dig into paramiko.Transport.AUTH_* etc. - move all of the check_* methods from Channel to ServerInterface so apps don't need to subclass Channel anymore just to run an ssh server - ServerInterface.check_channel_request() returns an error code now, not a new Channel object - fix demo_server.py to follow all these changes - fix a bunch of places where i used "string" in docstrings but meant "str" - added Channel.get_id() --- README | 2 + demo_server.py | 37 ++++--- paramiko/__init__.py | 3 + paramiko/auth_transport.py | 43 ++++---- paramiko/channel.py | 138 +++++++++----------------- paramiko/common.py | 13 +++ paramiko/server.py | 195 ++++++++++++++++++++++++++++--------- paramiko/sftp.py | 6 +- paramiko/transport.py | 64 ++++++------ 9 files changed, 293 insertions(+), 208 deletions(-) diff --git a/README b/README index 8958519..b5f06d5 100644 --- a/README +++ b/README @@ -155,3 +155,5 @@ v0.9 FEAROW * multi-part auth not supported (ie, need username AND pk) * server mode needs better documentation * sftp server mode + +ivysaur? diff --git a/demo_server.py b/demo_server.py index 8d88996..2f08f59 100755 --- a/demo_server.py +++ b/demo_server.py @@ -20,38 +20,34 @@ class Server (paramiko.ServerInterface): data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hpfAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMCKDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iTUWT10hcuO4Ks8=' good_pub_key = paramiko.RSAKey(data=base64.decodestring(data)) + def __init__(self): + self.event = threading.Event() + def check_channel_request(self, kind, chanid): if kind == 'session': - return ServerChannel(chanid) - return paramiko.Transport.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): if (username == 'robey') and (password == 'foo'): - return paramiko.Transport.AUTH_SUCCESSFUL - return paramiko.Transport.AUTH_FAILED + return paramiko.AUTH_SUCCESSFUL + return paramiko.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 paramiko.Transport.AUTH_SUCCESSFUL - return paramiko.Transport.AUTH_FAILED + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED def get_allowed_auths(self, username): return 'password,publickey' - -class ServerChannel (paramiko.Channel): - "Channel descendant that pretends to understand pty and shell requests" - - def __init__(self, chanid): - paramiko.Channel.__init__(self, chanid) - self.event = threading.Event() - - def check_pty_request(self, term, width, height, pixelwidth, pixelheight, modes): + def check_channel_shell_request(self, channel): + self.event.set() return True - def check_shell_request(self): - self.event.set() + def check_channel_pty_request(self, channel, term, width, height, pixelwidth, + pixelheight, modes): return True @@ -85,7 +81,8 @@ try: print '(Failed to load moduli -- gex will be unsupported.)' raise t.add_server_key(host_key) - t.start_server(event, Server()) + server = Server() + t.start_server(event, server) while 1: event.wait(0.1) if not t.is_active(): @@ -101,8 +98,8 @@ try: print '*** No channel.' sys.exit(1) print 'Authenticated!' - chan.event.wait(10) - if not chan.event.isSet(): + server.event.wait(10) + if not server.event.isSet(): print '*** Client never asked for a shell.' sys.exit(1) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 3af2125..3bfc90c 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -79,6 +79,9 @@ SFTP = sftp.SFTP ServerInterface = server.ServerInterface SecurityOptions = transport.SecurityOptions +from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ + OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, \ + OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, OPEN_FAILED_RESOURCE_SHORTAGE __all__ = [ 'Transport', 'SecurityOptions', diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py index 55f6363..c1919cd 100644 --- a/paramiko/auth_transport.py +++ b/paramiko/auth_transport.py @@ -47,8 +47,6 @@ class Transport (BaseTransport): another shell window). """ - AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) - def __init__(self, sock): BaseTransport.__init__(self, sock) self.username = None @@ -60,20 +58,22 @@ class Transport (BaseTransport): self.auth_complete = 0 def __repr__(self): + out = '' - out = '' diff --git a/paramiko/common.py b/paramiko/common.py index 8f42fe2..2347464 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -35,7 +35,19 @@ MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) +# authentication request return codes: + +AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) + + # channel request failed reasons: + +(OPEN_SUCCEEDED, + OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, + OPEN_FAILED_CONNECT_FAILED, + OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, + OPEN_FAILED_RESOURCE_SHORTAGE) = range(0, 5) + CONNECTION_FAILED_CODE = { 1: 'Administratively prohibited', 2: 'Connect failed', @@ -43,6 +55,7 @@ CONNECTION_FAILED_CODE = { 4: 'Resource shortage' } + DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 diff --git a/paramiko/server.py b/paramiko/server.py index 2a9b015..25924b6 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -22,6 +22,7 @@ L{ServerInterface} is an interface to override for server support. """ +from common import * from auth_transport import Transport class ServerInterface (object): @@ -37,31 +38,46 @@ class ServerInterface (object): 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. + return C{OPEN_SUCCEEDED} or an error code. 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. + If you allow channel requests (and an ssh server that didn't would be + useless), you should also override some of the channel request methods + below, which are used to determine which services will be allowed on + a given channel: + - L{check_channel_pty_request} + - L{check_channel_shell_request} + - L{check_channel_subsystem_request} + - L{check_channel_window_change_request} - The default implementation always returns C{None}, rejecting any - channel requests. A useful server must override this method. + The C{chanid} parameter is a small number that uniquely identifies the + channel within a L{Transport}. A L{Channel} object is not created + unless this method returns C{OPEN_SUCCEEDED} -- once a + L{Channel} object is created, you can call L{Channel.get_id} to + retrieve the channel ID. + + The return value should either be C{OPEN_SUCCEEDED} (or + C{0}) to allow the channel request, or one of the following error + codes to reject it: + - C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED} + - C{OPEN_FAILED_CONNECT_FAILED} + - C{OPEN_FAILED_UNKNOWN_CHANNEL_TYPE} + - C{OPEN_FAILED_RESOURCE_SHORTAGE} + + The default implementation always returns + C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}. @param kind: the kind of channel the client would like to open (usually C{"session"}). - @type kind: string + @type kind: str @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: a success or failure code (listed above). + @rtype: int """ - return None + return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def get_allowed_auths(self, username): """ @@ -76,9 +92,9 @@ class ServerInterface (object): The default implementation always returns C{"password"}. @param username: the username requesting authentication. - @type username: string + @type username: str @return: a comma-separated list of authentication types - @rtype: string + @rtype: str """ return 'password' @@ -87,46 +103,46 @@ class ServerInterface (object): 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 + Return L{AUTH_FAILED} if the client must authenticate, or + L{AUTH_SUCCESSFUL} if it's okay for the client to not authenticate. - The default implementation always returns L{Transport.AUTH_FAILED}. + The default implementation always returns L{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. + @type username: str + @return: L{AUTH_FAILED} if the authentication fails; + L{AUTH_SUCCESSFUL} if it succeeds. @rtype: int """ - return Transport.AUTH_FAILED + return 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 + Return L{AUTH_FAILED} if the password is not accepted, + L{AUTH_SUCCESSFUL} if the password is accepted and completes + the authentication, or L{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}. + The default implementation always returns L{AUTH_FAILED}. @param username: the username of the authenticating client. - @type username: string + @type username: str @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 + @type password: str + @return: L{AUTH_FAILED} if the authentication fails; + L{AUTH_SUCCESSFUL} if it succeeds; + L{AUTH_PARTIALLY_SUCCESSFUL} if the password auth is successful, but authentication must continue. @rtype: int """ - return Transport.AUTH_FAILED + return AUTH_FAILED def check_auth_publickey(self, username, key): """ @@ -135,24 +151,115 @@ class ServerInterface (object): 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 + Return L{AUTH_FAILED} if the key is not accepted, + L{AUTH_SUCCESSFUL} if the key is accepted and completes the + authentication, or L{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}. + The default implementation always returns L{AUTH_FAILED}. @param username: the username of the authenticating client. - @type username: string + @type username: str @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 + @return: L{AUTH_FAILED} if the client can't authenticate + with this key; L{AUTH_SUCCESSFUL} if it can; + L{AUTH_PARTIALLY_SUCCESSFUL} if it can authenticate with this key but must continue with authentication. @rtype: int """ - return Transport.AUTH_FAILED + return AUTH_FAILED + + + ### Channel requests + + + def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, + modes): + """ + Determine if a pseudo-terminal of the given dimensions (usually + requested for shell access) can be provided on the given channel. + + The default implementation always returns C{False}. + + @param channel: the L{Channel} the pty request arrived on. + @type channel: L{Channel} + @param term: type of terminal requested (for example, C{"vt100"}). + @type term: str + @param width: width of screen in characters. + @type width: int + @param height: height of screen in characters. + @type height: int + @param pixelwidth: width of screen in pixels, if known (may be C{0} if + unknown). + @type pixelwidth: int + @param pixelheight: height of screen in pixels, if known (may be C{0} + if unknown). + @type pixelheight: int + @return: C{True} if the psuedo-terminal has been allocated; C{False} + otherwise. + @rtype: boolean + """ + return False + + def check_channel_shell_request(self, channel): + """ + Determine if a shell will be provided to the client on the given + channel. If this method returns C{True}, the channel should be + connected to the stdin/stdout of a shell (or something that acts like + a shell). + + The default implementation always returns C{False}. + + @param channel: the L{Channel} the pty request arrived on. + @type channel: L{Channel} + @return: C{True} if this channel is now hooked up to a shell; C{False} + if a shell can't or won't be provided. + @rtype: boolean + """ + return False + + def check_channel_subsystem_request(self, channel, name): + """ + Determine if a requested subsystem will be provided to the client on + the given channel. If this method returns C{True}, all future I/O + through this channel will be assumed to be connected to the requested + subsystem. An example of a subsystem is C{sftp}. + + The default implementation always returns C{False}. + + @param channel: the L{Channel} the pty request arrived on. + @type channel: L{Channel} + @param name: name of the requested subsystem. + @type name: str + @return: C{True} if this channel is now hooked up to the requested + subsystem; C{False} if that subsystem can't or won't be provided. + @rtype: boolean + """ + return False + + def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight): + """ + Determine if the pseudo-terminal on the given channel can be resized. + This only makes sense if a pty was previously allocated on it. + + The default implementation always returns C{False}. + + @param channel: the L{Channel} the pty request arrived on. + @type channel: L{Channel} + @param width: width of screen in characters. + @type width: int + @param height: height of screen in characters. + @type height: int + @param pixelwidth: width of screen in pixels, if known (may be C{0} if + unknown). + @type pixelwidth: int + @param pixelheight: height of screen in pixels, if known (may be C{0} + if unknown). + @type pixelheight: int + @return: C{True} if the terminal was resized; C{False} if not. + """ + return False diff --git a/paramiko/sftp.py b/paramiko/sftp.py index fb977d7..95214f1 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -36,7 +36,7 @@ _FX_OK = 0 _FX_EOF, _FX_NO_SUCH_FILE, _FX_PERMISSION_DENIED, _FX_FAILURE, _FX_BAD_MESSAGE, \ _FX_NO_CONNECTION, _FX_CONNECTION_LOST, _FX_OP_UNSUPPORTED = range(1, 9) -VERSION = 3 +_VERSION = 3 class SFTPAttributes (object): @@ -238,12 +238,12 @@ class SFTP (object): else: self.logger = logging.getLogger('paramiko.sftp') # protocol: (maybe should move to a different method) - self._send_packet(_CMD_INIT, struct.pack('>I', VERSION)) + self._send_packet(_CMD_INIT, struct.pack('>I', _VERSION)) t, data = self._read_packet() if t != _CMD_VERSION: raise SFTPError('Incompatible sftp protocol') version = struct.unpack('>I', data[:4])[0] -# if version != VERSION: +# if version != _VERSION: # raise SFTPError('Incompatible sftp protocol') def from_transport(selfclass, t): diff --git a/paramiko/transport.py b/paramiko/transport.py index 90de98c..1548671 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -64,6 +64,8 @@ class SecurityOptions (object): If you try to add an algorithm that paramiko doesn't recognize, C{ValueError} will be raised. If you try to assign something besides a tuple to one of the fields, L{TypeError} will be raised. + + @since: ivysaur """ __slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', '_transport' ] @@ -74,7 +76,7 @@ class SecurityOptions (object): """ Returns a string representation of this object, for debugging. - @rtype: string + @rtype: str """ return '' % repr(self._transport) @@ -162,9 +164,6 @@ class BaseTransport (threading.Thread): REKEY_PACKETS = pow(2, 30) REKEY_BYTES = pow(2, 30) - OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, \ - OPEN_FAILED_RESOURCE_SHORTAGE = range(1, 5) - _modulus_pack = None def __init__(self, sock): @@ -176,7 +175,7 @@ class BaseTransport (threading.Thread): If the object is not actually a socket, it must have the following methods: - - C{send(string)}: Writes from 1 to C{len(string)} bytes, and + - C{send(str)}: Writes from 1 to C{len(str)} bytes, and returns an int representing the number of bytes written. Returns 0 or raises C{EOFError} if the stream has been closed. - C{recv(int)}: Reads from 1 to C{int} bytes and returns them as a @@ -264,17 +263,19 @@ class BaseTransport (threading.Thread): """ Returns a string representation of this object, for debugging. - @rtype: string + @rtype: str """ + out = '' - out = '} @@ -1042,6 +1048,9 @@ class BaseTransport (threading.Thread): chanid = m.get_int() if self.channels.has_key(chanid): self._channel_handler_table[ptype](self.channels[chanid], m) + else: + self._log(ERROR, 'Channel request for unknown channel %d' % chanid) + self.active = False else: self._log(WARNING, 'Oops, unhandled type %d' % ptype) msg = Message() @@ -1127,7 +1136,9 @@ class BaseTransport (threading.Thread): 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 - self._preferred_kex.remove('diffie-hellman-group-exchange-sha1') + pkex = list(self.get_security_options().kex) + pkex.remove('diffie-hellman-group-exchange-sha1') + self.get_security_options().kex = pkex available_server_keys = filter(self.server_key_dict.keys().__contains__, self._preferred_keys) else: @@ -1394,7 +1405,7 @@ class BaseTransport (threading.Thread): if not self.server_mode: self._log(DEBUG, 'Rejecting "%s" channel request from server.' % kind) reject = True - reason = self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + reason = OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED else: try: self.lock.acquire() @@ -1402,14 +1413,10 @@ class BaseTransport (threading.Thread): self.channel_counter += 1 finally: self.lock.release() - chan = self.server_object.check_channel_request(kind, my_chanid) - if (chan is None) or (type(chan) is int): + reason = self.server_object.check_channel_request(kind, my_chanid) + if reason != OPEN_SUCCEEDED: self._log(DEBUG, 'Rejecting "%s" channel request from client.' % kind) reject = True - if type(chan) is int: - reason = chan - else: - reason = self.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED if reject: msg = Message() msg.add_byte(chr(MSG_CHANNEL_OPEN_FAILURE)) @@ -1419,6 +1426,7 @@ class BaseTransport (threading.Thread): msg.add_string('en') self._send_message(msg) return + chan = Channel(my_chanid) try: self.lock.acquire() self.channels[my_chanid] = chan