diff --git a/paramiko/__init__.py b/paramiko/__init__.py index c987af2..3af2125 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -77,9 +77,11 @@ Message = message.Message PasswordRequiredException = ssh_exception.PasswordRequiredException SFTP = sftp.SFTP ServerInterface = server.ServerInterface +SecurityOptions = transport.SecurityOptions __all__ = [ 'Transport', + 'SecurityOptions', 'Channel', 'RSAKey', 'DSSKey', diff --git a/paramiko/channel.py b/paramiko/channel.py index 6aaed9e..67e9ea4 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -116,6 +116,8 @@ class Channel (object): @type width: int @param height: height (in characters) of the terminal screen @type height: int + @return: C{True} if the operation succeeded; C{False} if not. + @rtype: bool """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') @@ -144,6 +146,9 @@ class Channel (object): Request an interactive shell session on this channel. If the server allows it, the channel will then be directly connected to the stdin and stdout of the shell. + + @return: C{True} if the operation succeeded; C{False} if not. + @rtype: bool """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') @@ -169,6 +174,8 @@ class Channel (object): @param command: a shell command to execute. @type command: string + @return: C{True} if the operation succeeded; C{False} if not. + @rtype: bool """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') @@ -195,6 +202,8 @@ class Channel (object): @param subsystem: name of the subsystem being requested. @type subsystem: string + @return: C{True} if the operation succeeded; C{False} if not. + @rtype: bool """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') @@ -222,6 +231,8 @@ class Channel (object): @type width: int @param height: new height (in characters) of the terminal screen @type height: int + @return: C{True} if the operation succeeded; C{False} if not. + @rtype: bool """ if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') diff --git a/paramiko/server.py b/paramiko/server.py index 6abcffb..2a9b015 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -28,6 +28,10 @@ class ServerInterface (object): """ This class defines an interface for controlling the behavior of paramiko in server mode. + + Methods on this class are called from paramiko's primary thread, so you + shouldn't do too much work in them. (Certainly nothing that blocks or + sleeps.) """ def check_channel_request(self, kind, chanid): diff --git a/paramiko/sftp.py b/paramiko/sftp.py index fc8793c..fb977d7 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -123,8 +123,10 @@ class SFTPError (Exception): class SFTPFile (BufferedFile): - # some sftp servers will choke if you send read/write requests larger than - # this size. + """ + Some sftp servers will choke if you send read/write requests larger than + this size. + """ MAX_REQUEST_SIZE = 32768 def __init__(self, sftp, handle, mode='r', bufsize=-1): @@ -245,10 +247,20 @@ class SFTP (object): # raise SFTPError('Incompatible sftp protocol') def from_transport(selfclass, t): + """ + Create an SFTP client channel from an open L{Transport}. + + @param t: an open L{Transport} which is already authenticated. + @type t: L{Transport} + @return: a new L{SFTP} object, referring to an sftp session (channel) + across the transport. + @rtype: L{SFTP} + """ chan = t.open_session() if chan is None: return None - chan.invoke_subsystem('sftp') + if not chan.invoke_subsystem('sftp'): + raise SFTPError('Failed to invoke sftp subsystem') return selfclass(chan) from_transport = classmethod(from_transport) diff --git a/paramiko/transport.py b/paramiko/transport.py index 7931a11..11531c5 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -53,6 +53,50 @@ import atexit atexit.register(_join_lingering_threads) +class SecurityOptions (object): + """ + Simple object containing the security preferences of an ssh transport. + These are lists of acceptable ciphers, digests, key types, and key + exchange algorithms, listed in order of preference. + """ + __slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', '_transport' ] + + def __init__(self, transport): + self._transport = transport + + def _get_ciphers(self): + return self._transport._preferred_ciphers + + def _set_ciphers(self, x): + self._transport._preferred_ciphers = x + + def _get_digests(self): + return self._transport._preferred_macs + + def _set_digests(self, x): + self._transport._preferred_macs = x + + def _get_key_types(self): + return self._transport._preferred_keys + + def _set_key_types(self, x): + self._transport._preferred_keys = x + + def _get_kex(self): + return self._transport._preferred_kex + + def _set_kex(self, x): + self._transport._preferred_kex = x + + ciphers = property(_get_ciphers, _set_ciphers, None, + "Symmetric encryption ciphers") + digests = property(_get_digests, _set_digests, None, + "Digest (one-way hash) algorithms") + key_types = property(_get_key_types, _set_key_types, None, + "Public-key algorithms") + kex = property(_get_kex, _set_kex, None, "Key exchange algorithms") + + class BaseTransport (threading.Thread): """ Handles protocol negotiation, key exchange, encryption, and the creation @@ -62,10 +106,10 @@ class BaseTransport (threading.Thread): _PROTO_ID = '2.0' _CLIENT_ID = 'pyssh_1.1' - 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', 'diffie-hellman-group-exchange-sha1' ] + _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', 'diffie-hellman-group-exchange-sha1' ] _cipher_info = { 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 }, @@ -205,6 +249,18 @@ class BaseTransport (threading.Thread): out += '>' return out + def get_security_options(self): + """ + Return a L{SecurityOptions} object which can be used to tweak the + encryption algorithms this transport will permit, and the order of + preference for them. + + @return: an object that can be used to change the preferred algorithms + for encryption, digest (hash), public key, and key exchange. + @rtype: L{SecurityOptions} + """ + return SecurityOptions(self) + def start_client(self, event=None): """ Negotiate a new SSH2 session as a client. This is the first step after @@ -640,7 +696,7 @@ class BaseTransport (threading.Thread): @since: doduo """ if hostkeytype is not None: - self.preferred_keys = [ hostkeytype ] + self._preferred_keys = [ hostkeytype ] event = threading.Event() self.start_client(event) @@ -1048,23 +1104,23 @@ class BaseTransport (threading.Thread): """ 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): + 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') + self._preferred_kex.remove('diffie-hellman-group-exchange-sha1') available_server_keys = filter(self.server_key_dict.keys().__contains__, - self.preferred_keys) + self._preferred_keys) else: - available_server_keys = self.preferred_keys + available_server_keys = self._preferred_keys m = Message() m.add_byte(chr(MSG_KEXINIT)) m.add_bytes(randpool.get_bytes(16)) - m.add(','.join(self.preferred_kex)) + m.add(','.join(self._preferred_kex)) m.add(','.join(available_server_keys)) - m.add(','.join(self.preferred_ciphers)) - m.add(','.join(self.preferred_ciphers)) - m.add(','.join(self.preferred_macs)) - m.add(','.join(self.preferred_macs)) + m.add(','.join(self._preferred_ciphers)) + m.add(','.join(self._preferred_ciphers)) + m.add(','.join(self._preferred_macs)) + m.add(','.join(self._preferred_macs)) m.add('none') m.add('none') m.add('') @@ -1105,19 +1161,19 @@ class BaseTransport (threading.Thread): # 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. if self.server_mode: - agreed_kex = filter(self.preferred_kex.__contains__, kex_algo_list) + agreed_kex = filter(self._preferred_kex.__contains__, kex_algo_list) else: - agreed_kex = filter(kex_algo_list.__contains__, self.preferred_kex) + agreed_kex = filter(kex_algo_list.__contains__, self._preferred_kex) if len(agreed_kex) == 0: raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)') self.kex_engine = self._kex_info[agreed_kex[0]](self) if self.server_mode: available_server_keys = filter(self.server_key_dict.keys().__contains__, - self.preferred_keys) + self._preferred_keys) agreed_keys = filter(available_server_keys.__contains__, server_key_algo_list) else: - agreed_keys = filter(server_key_algo_list.__contains__, self.preferred_keys) + 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)') self.host_key_type = agreed_keys[0] @@ -1125,15 +1181,15 @@ class BaseTransport (threading.Thread): raise SSHException('Incompatible ssh peer (can\'t match requested host key type)') if self.server_mode: - agreed_local_ciphers = filter(self.preferred_ciphers.__contains__, + agreed_local_ciphers = filter(self._preferred_ciphers.__contains__, server_encrypt_algo_list) - agreed_remote_ciphers = filter(self.preferred_ciphers.__contains__, + agreed_remote_ciphers = filter(self._preferred_ciphers.__contains__, client_encrypt_algo_list) else: agreed_local_ciphers = filter(client_encrypt_algo_list.__contains__, - self.preferred_ciphers) + self._preferred_ciphers) agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__, - self.preferred_ciphers) + self._preferred_ciphers) if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0): raise SSHException('Incompatible ssh server (no acceptable ciphers)') self.local_cipher = agreed_local_ciphers[0] @@ -1141,11 +1197,11 @@ class BaseTransport (threading.Thread): self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher)) if self.server_mode: - agreed_remote_macs = filter(self.preferred_macs.__contains__, client_mac_algo_list) - agreed_local_macs = filter(self.preferred_macs.__contains__, server_mac_algo_list) + agreed_remote_macs = filter(self._preferred_macs.__contains__, client_mac_algo_list) + agreed_local_macs = filter(self._preferred_macs.__contains__, server_mac_algo_list) else: - agreed_local_macs = filter(client_mac_algo_list.__contains__, self.preferred_macs) - agreed_remote_macs = filter(server_mac_algo_list.__contains__, self.preferred_macs) + 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)') self.local_mac = agreed_local_macs[0]