diff options
author | Jeremy T. Bouse <jbouse@debian.org> | 2009-11-27 16:25:55 -0500 |
---|---|---|
committer | Jeremy T. Bouse <jbouse@debian.org> | 2009-11-27 16:25:55 -0500 |
commit | e299181a5dda25aed4879ebcbe1359604448b3ae (patch) | |
tree | f2d18804fcc3367ca9c5e977c1618679b5a5e363 /paramiko | |
parent | ed280d5ac360e2af796e9bd973d7b4df89f0c449 (diff) | |
download | python-paramiko-e299181a5dda25aed4879ebcbe1359604448b3ae.tar python-paramiko-e299181a5dda25aed4879ebcbe1359604448b3ae.tar.gz |
Imported Upstream version 1.7.6upstream/1.7.6
Diffstat (limited to 'paramiko')
34 files changed, 412 insertions, 305 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 9a8caec..ac0d559 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2008 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2009 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -47,9 +47,9 @@ released under the GNU Lesser General Public License (LGPL). Website: U{http://www.lag.net/paramiko/} -@version: 1.7.4 (Desmond) +@version: 1.7.6 (Fanny) @author: Robey Pointer -@contact: robey@lag.net +@contact: robeypointer@gmail.com @license: GNU Lesser General Public License (LGPL) """ @@ -59,10 +59,10 @@ if sys.version_info < (2, 2): raise RuntimeError('You need python 2.2 for this module.') -__author__ = "Robey Pointer <robey@lag.net>" -__date__ = "06 Jul 2008" -__version__ = "1.7.4 (Desmond)" -__version_info__ = (1, 7, 4) +__author__ = "Robey Pointer <robeypointer@gmail.com>" +__date__ = "1 Nov 2009" +__version__ = "1.7.6 (Fanny)" +__version_info__ = (1, 7, 6) __license__ = "GNU Lesser General Public License (LGPL)" diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 39a0194..0f2e4f6 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -363,7 +363,7 @@ class AuthHandler (object): self.transport._log(DEBUG, 'Methods: ' + str(authlist)) self.transport.saved_exception = PartialAuthentication(authlist) elif self.auth_method not in authlist: - self.transport._log(INFO, 'Authentication type (%s) not permitted.' % self.auth_method) + self.transport._log(DEBUG, 'Authentication type (%s) not permitted.' % self.auth_method) self.transport._log(DEBUG, 'Allowed methods: ' + str(authlist)) self.transport.saved_exception = BadAuthenticationType('Bad authentication type', authlist) else: diff --git a/paramiko/ber.py b/paramiko/ber.py index 9d8ddfa..19568dd 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py index ae3d9d6..b19d74b 100644 --- a/paramiko/buffered_pipe.py +++ b/paramiko/buffered_pipe.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/channel.py b/paramiko/channel.py index 910a03c..4694eef 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -90,6 +90,7 @@ class Channel (object): self.logger = util.get_logger('paramiko.transport') self._pipe = None self.event = threading.Event() + self.event_ready = False self.combine_stderr = False self.exit_status = -1 self.origin_addr = None @@ -152,7 +153,7 @@ class Channel (object): # pixel height, width (usually useless) m.add_int(0).add_int(0) m.add_string('') - self.event.clear() + self._event_pending() self.transport._send_user_message(m) self._wait_for_event() @@ -179,7 +180,7 @@ class Channel (object): m.add_int(self.remote_chanid) m.add_string('shell') m.add_boolean(1) - self.event.clear() + self._event_pending() self.transport._send_user_message(m) self._wait_for_event() @@ -207,7 +208,7 @@ class Channel (object): m.add_string('exec') m.add_boolean(True) m.add_string(command) - self.event.clear() + self._event_pending() self.transport._send_user_message(m) self._wait_for_event() @@ -234,7 +235,7 @@ class Channel (object): m.add_string('subsystem') m.add_boolean(True) m.add_string(subsystem) - self.event.clear() + self._event_pending() self.transport._send_user_message(m) self._wait_for_event() @@ -261,7 +262,7 @@ class Channel (object): m.add_int(width) m.add_int(height) m.add_int(0).add_int(0) - self.event.clear() + self._event_pending() self.transport._send_user_message(m) self._wait_for_event() @@ -291,10 +292,8 @@ class Channel (object): @since: 1.2 """ - while True: - if self.closed or self.status_event.isSet(): - break - self.status_event.wait(0.1) + self.status_event.wait() + assert self.status_event.isSet() return self.exit_status def send_exit_status(self, status): @@ -376,7 +375,7 @@ class Channel (object): m.add_string(auth_protocol) m.add_string(auth_cookie) m.add_int(screen_number) - self.event.clear() + self._event_pending() self.transport._send_user_message(m) self._wait_for_event() self.transport._set_x11_handler(handler) @@ -919,9 +918,10 @@ class Channel (object): self.out_max_packet_size = max(max_packet_size, MIN_PACKET_SIZE) self.active = 1 self._log(DEBUG, 'Max packet out: %d bytes' % max_packet_size) - + def _request_success(self, m): self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid) + self.event_ready = True self.event.set() return @@ -1069,17 +1069,19 @@ class Channel (object): def _log(self, level, msg, *args): self.logger.log(level, "[chan " + self._name + "] " + msg, *args) + def _event_pending(self): + self.event.clear() + self.event_ready = False + def _wait_for_event(self): - while True: - self.event.wait(0.1) - if self.event.isSet(): - return - if self.closed: - e = self.transport.get_exception() - if e is None: - e = SSHException('Channel closed.') - raise e - return + self.event.wait() + assert self.event.isSet() + if self.event_ready: + return + e = self.transport.get_exception() + if e is None: + e = SSHException('Channel closed.') + raise e def _set_closed(self): # you are holding the lock. @@ -1087,6 +1089,9 @@ class Channel (object): self.in_buffer.close() self.in_stderr_buffer.close() self.out_buffer_cv.notifyAll() + # Notify any waiters that we are closed + self.event.set() + self.status_event.set() if self._pipe is not None: self._pipe.set_forever() diff --git a/paramiko/client.py b/paramiko/client.py index 7870ea9..023b405 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -36,6 +36,8 @@ from paramiko.ssh_exception import SSHException, BadHostKeyException from paramiko.transport import Transport +SSH_PORT = 22 + class MissingHostKeyPolicy (object): """ Interface for defining the policy that L{SSHClient} should use when the @@ -43,10 +45,10 @@ class MissingHostKeyPolicy (object): application's keys. Pre-made classes implement policies for automatically adding the key to the application's L{HostKeys} object (L{AutoAddPolicy}), and for automatically rejecting the key (L{RejectPolicy}). - + This function may be used to ask the user to verify the key, for example. """ - + def missing_host_key(self, client, hostname, key): """ Called when an L{SSHClient} receives a server key for a server that @@ -62,7 +64,7 @@ class AutoAddPolicy (MissingHostKeyPolicy): Policy for automatically adding the hostname and new host key to the local L{HostKeys} object, and saving it. This is used by L{SSHClient}. """ - + def missing_host_key(self, client, hostname, key): client._host_keys.add(hostname, key.get_name(), key) if client._host_keys_filename is not None: @@ -76,7 +78,7 @@ class RejectPolicy (MissingHostKeyPolicy): Policy for automatically rejecting the unknown hostname & key. This is used by L{SSHClient}. """ - + def missing_host_key(self, client, hostname, key): client._log(DEBUG, 'Rejecting %s host key for %s: %s' % (key.get_name(), hostname, hexlify(key.get_fingerprint()))) @@ -98,16 +100,16 @@ class SSHClient (object): A high-level representation of a session with an SSH server. This class wraps L{Transport}, L{Channel}, and L{SFTPClient} to take care of most aspects of authenticating and opening channels. A typical use case is:: - + client = SSHClient() client.load_system_host_keys() client.connect('ssh.example.com') stdin, stdout, stderr = client.exec_command('ls -l') - + You may pass in explicit overrides for authentication and server host key checking. The default mechanism is to try to use local key files or an SSH agent (if one is running). - + @since: 1.6 """ @@ -121,16 +123,16 @@ class SSHClient (object): self._log_channel = None self._policy = RejectPolicy() self._transport = None - + def load_system_host_keys(self, filename=None): """ Load host keys from a system (read-only) file. Host keys read with this method will not be saved back by L{save_host_keys}. - + This method can be called multiple times. Each new set of host keys will be merged with the existing set (new replacing old if there are conflicts). - + If C{filename} is left as C{None}, an attempt will be made to read keys from the user's local "known hosts" file, as used by OpenSSH, and no exception will be raised if the file can't be read. This is @@ -138,7 +140,7 @@ class SSHClient (object): @param filename: the filename to read, or C{None} @type filename: str - + @raise IOError: if a filename was provided and the file could not be read """ @@ -151,7 +153,7 @@ class SSHClient (object): pass return self._system_host_keys.load(filename) - + def load_host_keys(self, filename): """ Load host keys from a local host-key file. Host keys read with this @@ -159,11 +161,11 @@ class SSHClient (object): but will be saved back by L{save_host_keys} (so they can be modified). The missing host key policy L{AutoAddPolicy} adds keys to this set and saves them, when connecting to a previously-unknown server. - + This method can be called multiple times. Each new set of host keys will be merged with the existing set (new replacing old if there are conflicts). When automatically saving, the last hostname is used. - + @param filename: the filename to read @type filename: str @@ -171,16 +173,16 @@ class SSHClient (object): """ self._host_keys_filename = filename self._host_keys.load(filename) - + def save_host_keys(self, filename): """ Save the host keys back to a file. Only the host keys loaded with L{load_host_keys} (plus any added directly) will be saved -- not any host keys loaded with L{load_system_host_keys}. - + @param filename: the filename to save to @type filename: str - + @raise IOError: if the file could not be written """ f = open(filename, 'w') @@ -189,17 +191,17 @@ class SSHClient (object): for keytype, key in keys.iteritems(): f.write('%s %s %s\n' % (hostname, keytype, key.get_base64())) f.close() - + def get_host_keys(self): """ Get the local L{HostKeys} object. This can be used to examine the local host keys or change them. - + @return: the local host keys @rtype: L{HostKeys} """ return self._host_keys - + def set_log_channel(self, name): """ Set the channel for logging. The default is C{"paramiko.transport"} @@ -209,21 +211,21 @@ class SSHClient (object): @type name: str """ self._log_channel = name - + def set_missing_host_key_policy(self, policy): """ Set the policy to use when connecting to a server that doesn't have a host key in either the system or local L{HostKeys} objects. The default policy is to reject all unknown servers (using L{RejectPolicy}). You may substitute L{AutoAddPolicy} or write your own policy class. - + @param policy: the policy to use when receiving a host key from a previously-unknown server @type policy: L{MissingHostKeyPolicy} """ self._policy = policy - def connect(self, hostname, port=22, username=None, password=None, pkey=None, + def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, key_filename=None, timeout=None, allow_agent=True, look_for_keys=True): """ Connect to an SSH server and authenticate to it. The server's host key @@ -232,14 +234,14 @@ class SSHClient (object): is not found in either set of host keys, the missing host key policy is used (see L{set_missing_host_key_policy}). The default policy is to reject the key and raise an L{SSHException}. - + Authentication is attempted in the following order of priority: - + - The C{pkey} or C{key_filename} passed in (if any) - Any key we can find through an SSH agent - Any "id_rsa" or "id_dsa" key discoverable in C{~/.ssh/} - Plain username/password auth, if a password was given - + If a private key requires a password to unlock it, and a password is passed in, that password will be used to attempt to unlock the key. @@ -273,39 +275,49 @@ class SSHClient (object): establishing an SSH session @raise socket.error: if a socket error occurred while connecting """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): + if socktype == socket.SOCK_STREAM: + af = family + addr = sockaddr + break + else: + raise SSHException('No suitable address family for %s' % hostname) + sock = socket.socket(af, socket.SOCK_STREAM) if timeout is not None: try: sock.settimeout(timeout) except: pass - - sock.connect((hostname, port)) + sock.connect(addr) t = self._transport = Transport(sock) if self._log_channel is not None: t.set_log_channel(self._log_channel) t.start_client() ResourceManager.register(self, t) - + server_key = t.get_remote_server_key() keytype = server_key.get_name() - - our_server_key = self._system_host_keys.get(hostname, {}).get(keytype, None) + + if port == SSH_PORT: + server_hostkey_name = hostname + else: + server_hostkey_name = "[%s]:%d" % (hostname, port) + our_server_key = self._system_host_keys.get(server_hostkey_name, {}).get(keytype, None) if our_server_key is None: - our_server_key = self._host_keys.get(hostname, {}).get(keytype, None) + our_server_key = self._host_keys.get(server_hostkey_name, {}).get(keytype, None) if our_server_key is None: # will raise exception if the key is rejected; let that fall out - self._policy.missing_host_key(self, hostname, server_key) + self._policy.missing_host_key(self, server_hostkey_name, server_key) # if the callback returns, assume the key is ok our_server_key = server_key - + if server_key != our_server_key: raise BadHostKeyException(hostname, server_key, our_server_key) if username is None: username = getpass.getuser() - + if key_filename is None: key_filenames = [] elif isinstance(key_filename, (str, unicode)): @@ -313,7 +325,7 @@ class SSHClient (object): else: key_filenames = key_filename self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys) - + def close(self): """ Close this SSHClient and its underlying L{Transport}. @@ -329,7 +341,7 @@ class SSHClient (object): the requested command is executed. The command's input and output streams are returned as python C{file}-like objects representing stdin, stdout, and stderr. - + @param command: the command to execute @type command: str @param bufsize: interpreted the same way as by the built-in C{file()} function in python @@ -351,7 +363,7 @@ class SSHClient (object): Start an interactive shell session on the SSH server. A new L{Channel} is opened and connected to a pseudo-terminal using the requested terminal type and size. - + @param term: the terminal type to emulate (for example, C{"vt100"}) @type term: str @param width: the width (in characters) of the terminal window @@ -360,47 +372,47 @@ class SSHClient (object): @type height: int @return: a new channel connected to the remote shell @rtype: L{Channel} - + @raise SSHException: if the server fails to invoke a shell """ chan = self._transport.open_session() chan.get_pty(term, width, height) chan.invoke_shell() return chan - + def open_sftp(self): """ Open an SFTP session on the SSH server. - + @return: a new SFTP session object @rtype: L{SFTPClient} """ return self._transport.open_sftp_client() - + def get_transport(self): """ Return the underlying L{Transport} object for this SSH connection. This can be used to perform lower-level tasks, like opening specific kinds of channels. - + @return: the Transport for this connection @rtype: L{Transport} """ return self._transport - + def _auth(self, username, password, pkey, key_filenames, allow_agent, look_for_keys): """ Try, in order: - + - The key passed in, if one was passed in. - Any key we can find through an SSH agent (if allowed). - Any "id_rsa" or "id_dsa" key discoverable in ~/.ssh/ (if allowed). - Plain username/password auth, if a password was given. - + (The password might be needed to unlock a private key.) """ saved_exception = None - + if pkey is not None: try: self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint())) @@ -418,7 +430,7 @@ class SSHClient (object): return except SSHException, e: saved_exception = e - + if allow_agent: for key in Agent().get_keys(): try: @@ -442,7 +454,7 @@ class SSHClient (object): keyfiles.append((RSAKey, rsa_key)) if os.path.isfile(dsa_key): keyfiles.append((DSSKey, dsa_key)) - + if not look_for_keys: keyfiles = [] @@ -456,7 +468,7 @@ class SSHClient (object): saved_exception = e except IOError, e: saved_exception = e - + if password is not None: try: self._transport.auth_password(username, password) diff --git a/paramiko/common.py b/paramiko/common.py index f4a4d81..7a37463 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/compress.py b/paramiko/compress.py index 08fffb1..40b430f 100644 --- a/paramiko/compress.py +++ b/paramiko/compress.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/config.py b/paramiko/config.py index 1e3d680..2a2cbff 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -26,28 +26,28 @@ import fnmatch class SSHConfig (object): """ Representation of config information as stored in the format used by - OpenSSH. Queries can be made via L{lookup}. The format is described in - OpenSSH's C{ssh_config} man page. This class is provided primarily as a - convenience to posix users (since the OpenSSH format is a de-facto + OpenSSH. Queries can be made via L{lookup}. The format is described in + OpenSSH's C{ssh_config} man page. This class is provided primarily as a + convenience to posix users (since the OpenSSH format is a de-facto standard on posix) but should work fine on Windows too. - + @since: 1.6 """ - + def __init__(self): """ Create a new OpenSSH config object. """ self._config = [ { 'host': '*' } ] - + def parse(self, file_obj): """ Read an OpenSSH config from the given file object. - + @param file_obj: a file-like object to read the config file from @type file_obj: file """ - config = self._config[0] + configs = [self._config[0]] for line in file_obj: line = line.rstrip('\n').lstrip() if (line == '') or (line[0] == '#'): @@ -66,15 +66,20 @@ class SSHConfig (object): value = line[i:].lstrip() if key == 'host': - # do we have a pre-existing host config to append to? - matches = [c for c in self._config if c['host'] == value] - if len(matches) > 0: - config = matches[0] - else: - config = { 'host': value } - self._config.append(config) + del configs[:] + # the value may be multiple hosts, space-delimited + for host in value.split(): + # do we have a pre-existing host config to append to? + matches = [c for c in self._config if c['host'] == host] + if len(matches) > 0: + configs.append(matches[0]) + else: + config = { 'host': host } + self._config.append(config) + configs.append(config) else: - config[key] = value + for config in configs: + config[key] = value def lookup(self, hostname): """ @@ -83,13 +88,13 @@ class SSHConfig (object): The host-matching rules of OpenSSH's C{ssh_config} man page are used, which means that all configuration options from matching host specifications are merged, with more specific hostmasks taking - precedence. In other words, if C{"Port"} is set under C{"Host *"} + precedence. In other words, if C{"Port"} is set under C{"Host *"} and also C{"Host *.example.com"}, and the lookup is for C{"ssh.example.com"}, then the port entry for C{"Host *.example.com"} will win out. The keys in the returned dict are all normalized to lowercase (look for - C{"port"}, not C{"Port"}. No other processing is done to the keys or + C{"port"}, not C{"Port"}. No other processing is done to the keys or values. @param hostname: the hostname to lookup diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 9f381d2..eecfa69 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/file.py b/paramiko/file.py index 7db4401..d4aec8e 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 0c0ac8c..9ceef43 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -1,4 +1,4 @@ -# Copyright (C) 2006-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -33,7 +33,7 @@ class HostKeyEntry: """ Representation of a line in an OpenSSH-style "known hosts" file. """ - + def __init__(self, hostnames=None, key=None): self.valid = (hostnames is not None) and (key is not None) self.hostnames = hostnames @@ -53,9 +53,10 @@ class HostKeyEntry: @type line: str """ fields = line.split(' ') - if len(fields) != 3: + if len(fields) < 3: # Bad number of fields return None + fields = fields[:3] names, keytype, key = fields names = names.split(',') @@ -82,7 +83,7 @@ class HostKeyEntry: return '%s %s %s\n' % (','.join(self.hostnames), self.key.get_name(), self.key.get_base64()) return None - + def __repr__(self): return '<HostKeyEntry %r: %r>' % (self.hostnames, self.key) @@ -92,18 +93,18 @@ class HostKeys (UserDict.DictMixin): Representation of an openssh-style "known hosts" file. Host keys can be read from one or more files, and then individual hosts can be looked up to verify server keys during SSH negotiation. - + A HostKeys object can be treated like a dict; any dict lookup is equivalent to calling L{lookup}. - + @since: 1.5.3 """ - + def __init__(self, filename=None): """ Create a new HostKeys object, optionally loading keys from an openssh style host-key file. - + @param filename: filename to load host keys from, or C{None} @type filename: str """ @@ -111,12 +112,12 @@ class HostKeys (UserDict.DictMixin): self._entries = [] if filename is not None: self.load(filename) - + def add(self, hostname, keytype, key): """ Add a host key entry to the table. Any existing entry for a C{(hostname, keytype)} pair will be replaced. - + @param hostname: the hostname (or IP) to add @type hostname: str @param keytype: key type (C{"ssh-rsa"} or C{"ssh-dss"}) @@ -129,21 +130,21 @@ class HostKeys (UserDict.DictMixin): e.key = key return self._entries.append(HostKeyEntry([hostname], key)) - + def load(self, filename): """ Read a file of known SSH host keys, in the format used by openssh. This type of file unfortunately doesn't exist on Windows, but on posix, it will usually be stored in C{os.path.expanduser("~/.ssh/known_hosts")}. - + If this method is called multiple times, the host keys are merged, not cleared. So multiple calls to C{load} will just call L{add}, replacing any existing entries and adding new ones. - + @param filename: name of the file to read host keys from @type filename: str - + @raise IOError: if there was an error reading the file """ f = open(filename, 'r') @@ -155,19 +156,19 @@ class HostKeys (UserDict.DictMixin): if e is not None: self._entries.append(e) f.close() - + def save(self, filename): """ Save host keys into a file, in the format used by openssh. The order of keys in the file will be preserved when possible (if these keys were loaded from a file originally). The single exception is that combined lines will be split into individual key lines, which is arguably a bug. - + @param filename: name of the file to write @type filename: str - + @raise IOError: if there was an error writing the file - + @since: 1.6.1 """ f = open(filename, 'w') @@ -182,7 +183,7 @@ class HostKeys (UserDict.DictMixin): Find a hostkey entry for a given hostname or IP. If no entry is found, C{None} is returned. Otherwise a dictionary of keytype to key is returned. The keytype will be either C{"ssh-rsa"} or C{"ssh-dss"}. - + @param hostname: the hostname (or IP) to lookup @type hostname: str @return: keys associated with this host (or C{None}) @@ -193,13 +194,13 @@ class HostKeys (UserDict.DictMixin): self._hostname = hostname self._entries = entries self._hostkeys = hostkeys - + def __getitem__(self, key): for e in self._entries: if e.key.get_name() == key: return e.key raise KeyError(key) - + def __setitem__(self, key, val): for e in self._entries: if e.key is None: @@ -213,7 +214,7 @@ class HostKeys (UserDict.DictMixin): e = HostKeyEntry([hostname], val) self._entries.append(e) self._hostkeys._entries.append(e) - + def keys(self): return [e.key.get_name() for e in self._entries if e.key is not None] @@ -225,12 +226,12 @@ class HostKeys (UserDict.DictMixin): if len(entries) == 0: return None return SubDict(hostname, entries, self) - + def check(self, hostname, key): """ Return True if the given key is associated with the given hostname in this dictionary. - + @param hostname: hostname (or IP) of the SSH server @type hostname: str @param key: the key to check @@ -252,13 +253,13 @@ class HostKeys (UserDict.DictMixin): Remove all host keys from the dictionary. """ self._entries = [] - + def __getitem__(self, key): ret = self.lookup(key) if ret is None: raise KeyError(key) return ret - + def __setitem__(self, hostname, entry): # don't use this please. if len(entry) == 0: @@ -273,7 +274,7 @@ class HostKeys (UserDict.DictMixin): found = True if not found: self._entries.append(HostKeyEntry([hostname], entry[key_type])) - + def keys(self): # python 2.4 sets would be nice here. ret = [] @@ -293,7 +294,7 @@ class HostKeys (UserDict.DictMixin): """ Return a "hashed" form of the hostname, as used by openssh when storing hashed hostnames in the known_hosts file. - + @param hostname: the hostname to hash @type hostname: str @param salt: optional salt to use when hashing (must be 20 bytes long) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 63a0c99..c6be638 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 843a6d8..4228dd9 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/logging22.py b/paramiko/logging22.py index 9bf7656..ed1d891 100644 --- a/paramiko/logging22.py +++ b/paramiko/logging22.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/message.py b/paramiko/message.py index 1a5151c..366c43c 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -111,7 +111,7 @@ class Message (object): """ b = self.packet.read(n) if len(b) < n: - return '\x00'*n + return b + '\x00' * (n - len(b)) return b def get_byte(self): diff --git a/paramiko/packet.py b/paramiko/packet.py index 4bde2f7..9072fbe 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -268,11 +268,11 @@ class Packetizer (object): Read a line from the socket. We assume no data is pending after the line, so it's okay to attempt large reads. """ - buf = '' + buf = self.__remainder while not '\n' in buf: buf += self._read_timeout(timeout) n = buf.index('\n') - self.__remainder += buf[n+1:] + self.__remainder = buf[n+1:] buf = buf[:n] if (len(buf) > 0) and (buf[-1] == '\r'): buf = buf[:-1] diff --git a/paramiko/pipe.py b/paramiko/pipe.py index 1cfed2d..37191ef 100644 --- a/paramiko/pipe.py +++ b/paramiko/pipe.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 4e8b26b..bb8c83c 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/primes.py b/paramiko/primes.py index 7b35736..1cf7905 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/resource.py b/paramiko/resource.py index a089754..0d5c82f 100644 --- a/paramiko/resource.py +++ b/paramiko/resource.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/rng_posix.py b/paramiko/rng_posix.py index 1e6d72c..c4c9691 100644 --- a/paramiko/rng_posix.py +++ b/paramiko/rng_posix.py @@ -43,7 +43,7 @@ def open_rng_device(device_path=None): f = None g = None - + if device_path is None: device_path = "/dev/urandom" @@ -54,7 +54,7 @@ def open_rng_device(device_path=None): f = open(device_path, "rb", 0) except EnvironmentError: raise error("Unable to open /dev/urandom") - + # Open a second file descriptor for sanity checking later. try: g = open(device_path, "rb", 0) @@ -65,17 +65,17 @@ def open_rng_device(device_path=None): st = os.fstat(f.fileno()) # f if stat.S_ISREG(st.st_mode) or not stat.S_ISCHR(st.st_mode): raise error("/dev/urandom is not a character special device") - + st = os.fstat(g.fileno()) # g if stat.S_ISREG(st.st_mode) or not stat.S_ISCHR(st.st_mode): raise error("/dev/urandom is not a character special device") - + # Check that /dev/urandom always returns the number of bytes requested x = f.read(20) y = g.read(20) if len(x) != 20 or len(y) != 20: raise error("Error reading from /dev/urandom: input truncated") - + # Check that different reads return different data if x == y: raise error("/dev/urandom is broken; returning identical data: %r == %r" % (x, y)) diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index d72d175..a665279 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/server.py b/paramiko/server.py index bcaa4be..6424b63 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/sftp.py b/paramiko/sftp.py index 2296d85..a0b08e0 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index 9c92862..26290be 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2006 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2006 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index b3d2d56..1f11075 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -23,6 +23,7 @@ Client-mode SFTP support. from binascii import hexlify import errno import os +import stat import threading import time import weakref @@ -104,27 +105,31 @@ class SFTPClient (BaseSFTP): chan.invoke_subsystem('sftp') return cls(chan) from_transport = classmethod(from_transport) - + def _log(self, level, msg, *args): - super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args))) + if isinstance(msg, list): + for m in msg: + super(SFTPClient, self)._log(level, "[chan %s] " + m, *([ self.sock.get_name() ] + list(args))) + else: + super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args))) def close(self): """ Close the SFTP session and its underlying channel. - + @since: 1.4 """ self._log(INFO, 'sftp session closed.') self.sock.close() - + def get_channel(self): """ Return the underlying L{Channel} object for this SFTP session. This might be useful for doing things like setting a timeout on the channel. - + @return: the SSH channel @rtype: L{Channel} - + @since: 1.7.1 """ return self.sock @@ -143,14 +148,14 @@ class SFTPClient (BaseSFTP): @rtype: list of str """ return [f.filename for f in self.listdir_attr(path)] - + def listdir_attr(self, path='.'): """ Return a list containing L{SFTPAttributes} objects corresponding to files in the given C{path}. The list is in arbitrary order. It does not include the special entries C{'.'} and C{'..'} even if they are present in the folder. - + The returned L{SFTPAttributes} objects will each have an additional field: C{longname}, which may contain a formatted string of the file's attributes, in unix format. The content of this string will probably @@ -160,7 +165,7 @@ class SFTPClient (BaseSFTP): @type path: str @return: list of attributes @rtype: list of L{SFTPAttributes} - + @since: 1.2 """ path = self._adjust_cwd(path) @@ -201,7 +206,7 @@ class SFTPClient (BaseSFTP): existing file), C{'a+'} for reading/appending. The python C{'b'} flag is ignored, since SSH treats all files as binary. The C{'U'} flag is supported in a compatible way. - + Since 1.5.2, an C{'x'} flag indicates that the operation should only succeed if the file was created and did not previously exist. This has no direct mapping to python's file flags, but is commonly known as the @@ -271,7 +276,7 @@ class SFTPClient (BaseSFTP): @type oldpath: str @param newpath: new name for the file or folder @type newpath: str - + @raise IOError: if C{newpath} is a folder, or something else goes wrong """ @@ -384,7 +389,7 @@ class SFTPClient (BaseSFTP): attr = SFTPAttributes() attr.st_mode = mode self._request(CMD_SETSTAT, path, attr) - + def chown(self, path, uid, gid): """ Change the owner (C{uid}) and group (C{gid}) of a file. As with @@ -433,7 +438,7 @@ class SFTPClient (BaseSFTP): Change the size of the file specified by C{path}. This usually extends or shrinks the size of the file, just like the C{truncate()} method on python file objects. - + @param path: path of the file to modify @type path: str @param size: the new size of the file @@ -479,7 +484,7 @@ class SFTPClient (BaseSFTP): @type path: str @return: normalized form of the given path @rtype: str - + @raise IOError: if the path can't be resolved on the server """ path = self._adjust_cwd(path) @@ -491,45 +496,51 @@ class SFTPClient (BaseSFTP): if count != 1: raise SFTPError('Realpath returned %d results' % count) return _to_unicode(msg.get_string()) - + def chdir(self, path): """ Change the "current directory" of this SFTP session. Since SFTP doesn't really have the concept of a current working directory, this is emulated by paramiko. Once you use this method to set a working directory, all operations on this SFTPClient object will be relative - to that path. - + to that path. You can pass in C{None} to stop using a current working + directory. + @param path: new current working directory @type path: str - + @raise IOError: if the requested path doesn't exist on the server - + @since: 1.4 """ - self._cwd = self.normalize(path) - + if path is None: + self._cwd = None + return + if not stat.S_ISDIR(self.stat(path).st_mode): + raise SFTPError(errno.ENOTDIR, "%s: %s" % (os.strerror(errno.ENOTDIR), path)) + self._cwd = self.normalize(path).encode('utf-8') + def getcwd(self): """ Return the "current working directory" for this SFTP session, as emulated by paramiko. If no directory has been set with L{chdir}, this method will return C{None}. - + @return: the current working directory on the server, or C{None} @rtype: str - + @since: 1.4 """ return self._cwd - + def put(self, localpath, remotepath, callback=None): """ Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. Any exception raised by operations will be passed through. This method is primarily provided as a convenience. - + The SFTP operations use pipelining for speed. - + @param localpath: the local file to copy @type localpath: str @param remotepath: the destination path on the SFTP server @@ -541,35 +552,39 @@ class SFTPClient (BaseSFTP): @return: an object containing attributes about the given file (since 1.7.4) @rtype: SFTPAttributes - + @since: 1.4 """ file_size = os.stat(localpath).st_size fl = file(localpath, 'rb') - fr = self.file(remotepath, 'wb') - fr.set_pipelined(True) - size = 0 - while True: - data = fl.read(32768) - if len(data) == 0: - break - fr.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - fl.close() - fr.close() + try: + fr = self.file(remotepath, 'wb') + fr.set_pipelined(True) + size = 0 + try: + while True: + data = fl.read(32768) + if len(data) == 0: + break + fr.write(data) + size += len(data) + if callback is not None: + callback(size, file_size) + finally: + fr.close() + finally: + fl.close() s = self.stat(remotepath) if s.st_size != size: raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) return s - + def get(self, remotepath, localpath, callback=None): """ Copy a remote file (C{remotepath}) from the SFTP server to the local host as C{localpath}. Any exception raised by operations will be passed through. This method is primarily provided as a convenience. - + @param remotepath: the remote file to copy @type remotepath: str @param localpath: the destination path on the local host @@ -578,24 +593,28 @@ class SFTPClient (BaseSFTP): transferred so far and the total bytes to be transferred (since 1.7.4) @type callback: function(int, int) - + @since: 1.4 """ fr = self.file(remotepath, 'rb') file_size = self.stat(remotepath).st_size fr.prefetch() - fl = file(localpath, 'wb') - size = 0 - while True: - data = fr.read(32768) - if len(data) == 0: - break - fl.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - fl.close() - fr.close() + try: + fl = file(localpath, 'wb') + try: + size = 0 + while True: + data = fr.read(32768) + if len(data) == 0: + break + fl.write(data) + size += len(data) + if callback is not None: + callback(size, file_size) + finally: + fl.close() + finally: + fr.close() s = os.stat(localpath) if s.st_size != size: raise IOError('size mismatch in get! %d != %d' % (s.st_size, size)) @@ -607,7 +626,7 @@ class SFTPClient (BaseSFTP): def _request(self, t, *arg): num = self._async_request(type(None), t, *arg) return self._read_response(num) - + def _async_request(self, fileobj, t, *arg): # this method may be called from other threads (prefetch) self._lock.acquire() @@ -684,7 +703,7 @@ class SFTPClient (BaseSFTP): raise IOError(errno.EACCES, text) else: raise IOError(text) - + def _adjust_cwd(self, path): """ Return an adjusted path if we're emulating a "current working diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index cfa7db1..8c5c7ac 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py index e976f43..a6cd44a 100644 --- a/paramiko/sftp_handle.py +++ b/paramiko/sftp_handle.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index 099ac12..7cc6c0c 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/sftp_si.py b/paramiko/sftp_si.py index 47dd25d..401a4e9 100644 --- a/paramiko/sftp_si.py +++ b/paramiko/sftp_si.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index e3120bb..68924d0 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # diff --git a/paramiko/transport.py b/paramiko/transport.py index a18e05b..50e78e7 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -50,8 +50,12 @@ from paramiko.ssh_exception import SSHException, BadAuthenticationType, ChannelE # i believe this on the standards track. # PyCrypt compiled for Win32 can be downloaded from the HashTar homepage: # http://nitace.bsd.uchicago.edu:8080/hashtar -from Crypto.Cipher import Blowfish, AES, DES3 +from Crypto.Cipher import Blowfish, AES, DES3, ARC4 from Crypto.Hash import SHA, MD5 +try: + from Crypto.Util import Counter +except ImportError: + from paramiko.util import Counter # for thread cleanup @@ -99,7 +103,7 @@ class SecurityOptions (object): def _get_kex(self): return self._transport._preferred_kex - + def _get_compression(self): return self._transport._preferred_compression @@ -125,7 +129,7 @@ class SecurityOptions (object): def _set_kex(self, x): self._set('_preferred_kex', '_kex_info', x) - + def _set_compression(self, x): self._set('_preferred_compression', '_compression_info', x) @@ -152,14 +156,14 @@ class ChannelMap (object): self._map[chanid] = chan finally: self._lock.release() - + def get(self, chanid): self._lock.acquire() try: return self._map.get(chanid, None) finally: self._lock.release() - + def delete(self, chanid): self._lock.acquire() try: @@ -169,14 +173,14 @@ class ChannelMap (object): pass finally: self._lock.release() - + def values(self): self._lock.acquire() try: return self._map.values() finally: self._lock.release() - + def __len__(self): self._lock.acquire() try: @@ -194,19 +198,24 @@ class Transport (threading.Thread): """ _PROTO_ID = '2.0' - _CLIENT_ID = 'paramiko_1.7.4' + _CLIENT_ID = 'paramiko_1.7.6' - _preferred_ciphers = ( 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ) + _preferred_ciphers = ( 'aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc', + 'arcfour128', 'arcfour256' ) _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_compression = ( 'none', ) - + _cipher_info = { + 'aes128-ctr': { 'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16 }, + 'aes256-ctr': { 'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32 }, 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 }, 'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 }, 'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 }, '3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 }, + 'arcfour128': { 'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16 }, + 'arcfour256': { 'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32 }, } _mac_info = { @@ -225,7 +234,7 @@ class Transport (threading.Thread): 'diffie-hellman-group1-sha1': KexGroup1, 'diffie-hellman-group-exchange-sha1': KexGex, } - + _compression_info = { # zlib@openssh.com is just zlib, but only turned on after a successful # authentication. openssh servers may only offer this type because @@ -266,7 +275,7 @@ class Transport (threading.Thread): @param sock: a socket or socket-like object to create the session over. @type sock: socket """ - if type(sock) is str: + if isinstance(sock, (str, unicode)): # convert "host:port" into (host, port) hl = sock.split(':', 1) if len(hl) == 1: @@ -276,10 +285,18 @@ class Transport (threading.Thread): if type(sock) is tuple: # connect to the given (host, port) hostname, port = sock - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): + if socktype == socket.SOCK_STREAM: + af = family + addr = sockaddr + break + else: + raise SSHException('No suitable address family for %s' % hostname) + sock = socket.socket(af, socket.SOCK_STREAM) sock.connect((hostname, port)) # okay, normal socket-ish flow here... threading.Thread.__init__(self) + self.setDaemon(True) self.randpool = randpool self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB @@ -302,7 +319,7 @@ class Transport (threading.Thread): self.session_id = None self.host_key_type = None self.host_key = None - + # state used during negotiation self.kex_engine = None self.H = None @@ -328,6 +345,7 @@ class Transport (threading.Thread): self.saved_exception = None self.clear_to_send = threading.Event() self.clear_to_send_lock = threading.Lock() + self.clear_to_send_timeout = 30.0 self.log_name = 'paramiko.transport' self.logger = util.get_logger(self.log_name) self.packetizer.set_log(self.logger) @@ -365,7 +383,7 @@ class Transport (threading.Thread): out += ' (connecting)' out += '>' return out - + def atfork(self): """ Terminate this Transport without closing the session. On posix @@ -373,7 +391,7 @@ class Transport (threading.Thread): and child will share the underlying socket, but only one process can use the connection (without corrupting the session). Use this method to clean up a Transport object without disrupting the other process. - + @since: 1.5.3 """ self.sock.close() @@ -396,11 +414,11 @@ class Transport (threading.Thread): Negotiate a new SSH2 session as a client. This is the first step after creating a new L{Transport}. A separate thread is created for protocol negotiation. - + If an event is passed in, this method returns immediately. When negotiation is done (successful or not), the given C{Event} will be triggered. On failure, L{is_active} will return C{False}. - + (Since 1.4) If C{event} is C{None}, this method will not return until negotation is done. On success, the method returns normally. Otherwise an SSHException is raised. @@ -410,7 +428,7 @@ class Transport (threading.Thread): L{auth_publickey <Transport.auth_publickey>}. @note: L{connect} is a simpler method for connecting as a client. - + @note: After calling this method (or L{start_server} or L{connect}), you should no longer directly read from or write to the original socket object. @@ -447,11 +465,11 @@ class Transport (threading.Thread): 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 separate thread is created for protocol negotiation. - + If an event is passed in, this method returns immediately. When negotiation is done (successful or not), the given C{Event} will be triggered. On failure, L{is_active} will return C{False}. - + (Since 1.4) If C{event} is C{None}, this method will not return until negotation is done. On success, the method returns normally. Otherwise an SSHException is raised. @@ -514,7 +532,7 @@ class Transport (threading.Thread): we are. Because this is used for signing, the key must contain private key info, not just the public half. Only one key of each type (RSA or DSS) is kept. - + @param key: the host key to add, usually an L{RSAKey <rsakey.RSAKey>} or L{DSSKey <dsskey.DSSKey>}. @type key: L{PKey <pkey.PKey>} @@ -564,7 +582,7 @@ class Transport (threading.Thread): @return: True if a moduli file was successfully loaded; False otherwise. @rtype: bool - + @note: This has no effect when used in client mode. """ Transport._modulus_pack = ModulusPack(randpool) @@ -605,7 +623,7 @@ class Transport (threading.Thread): C{str(key)} for the key string. @raise SSHException: if no session is currently active. - + @return: public key of the remote server @rtype: L{PKey <pkey.PKey>} """ @@ -630,7 +648,7 @@ class Transport (threading.Thread): @return: a new L{Channel} @rtype: L{Channel} - + @raise SSHException: if the request is rejected or the session ends prematurely """ @@ -646,25 +664,25 @@ class Transport (threading.Thread): @type src_addr: (str, int) @return: a new L{Channel} @rtype: L{Channel} - + @raise SSHException: if the request is rejected or the session ends prematurely """ return self.open_channel('x11', src_addr=src_addr) - + def open_forwarded_tcpip_channel(self, (src_addr, src_port), (dest_addr, dest_port)): """ Request a new channel back to the client, of type C{"forwarded-tcpip"}. This is used after a client has requested port forwarding, for sending incoming connections back to the client. - + @param src_addr: originator's address @param src_port: originator's port @param dest_addr: local (server) connected address @param dest_port: local (server) connected port """ return self.open_channel('forwarded-tcpip', (dest_addr, dest_port), (src_addr, src_port)) - + def open_channel(self, kind, dest_addr=None, src_addr=None): """ Request a new channel to the server. L{Channel}s are socket-like @@ -739,19 +757,19 @@ class Transport (threading.Thread): """ Ask the server to forward TCP connections from a listening port on the server, across this SSH session. - + If a handler is given, that handler is called from a different thread whenever a forwarded connection arrives. The handler parameters are:: - + handler(channel, (origin_addr, origin_port), (server_addr, server_port)) - + where C{server_addr} and C{server_port} are the address and port that the server was listening on. - + If no handler is set, the default behavior is to send new incoming forwarded connections into the accept queue, to be picked up via L{accept}. - + @param address: the address to bind when forwarding @type address: str @param port: the port to forward, or 0 to ask the server to allocate @@ -761,7 +779,7 @@ class Transport (threading.Thread): @type handler: function(Channel, (str, int), (str, int)) @return: the port # allocated by the server @rtype: int - + @raise SSHException: if the server refused the TCP forward request """ if not self.active: @@ -785,7 +803,7 @@ class Transport (threading.Thread): Ask the server to cancel a previous port-forwarding request. No more connections to the given address & port will be forwarded across this ssh connection. - + @param address: the address to stop forwarding @type address: str @param port: the port to stop forwarding @@ -795,7 +813,7 @@ class Transport (threading.Thread): return self._tcp_handler = None self.global_request('cancel-tcpip-forward', (address, port), wait=True) - + def open_sftp_client(self): """ Create an SFTP client channel from an open transport. On success, @@ -858,7 +876,7 @@ class Transport (threading.Thread): C{interval} seconds without sending any data over the connection, a "keepalive" packet will be sent (and ignored by the remote host). This can be useful to keep connections alive over a NAT, for example. - + @param interval: seconds to wait before sending a keepalive packet (or 0 to disable keepalives). @type interval: int @@ -909,7 +927,7 @@ class Transport (threading.Thread): Return the next channel opened by the client over this transport, in server mode. If no channel is opened before the given timeout, C{None} is returned. - + @param timeout: seconds to wait for a channel, or C{None} to wait forever @type timeout: int @@ -961,7 +979,7 @@ class Transport (threading.Thread): @param pkey: a private key to use for authentication, if you want to use private key authentication; otherwise C{None}. @type pkey: L{PKey<pkey.PKey>} - + @raise SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. """ @@ -989,17 +1007,17 @@ class Transport (threading.Thread): self.auth_publickey(username, pkey) return - + def get_exception(self): """ Return any exception that happened during the last server request. This can be used to fetch more specific error information after using calls like L{start_client}. The exception (if any) is cleared after this call. - + @return: an exception, or C{None} if there is no stored exception. @rtype: Exception - + @since: 1.1 """ self.lock.acquire() @@ -1031,7 +1049,7 @@ class Transport (threading.Thread): self.subsystem_table[name] = (handler, larg, kwarg) finally: self.lock.release() - + def is_authenticated(self): """ Return true if this session is active and authenticated. @@ -1042,7 +1060,7 @@ class Transport (threading.Thread): @rtype: bool """ return self.active and (self.auth_handler is not None) and self.auth_handler.is_authenticated() - + def get_username(self): """ Return the username this connection is authenticated for. If the @@ -1062,7 +1080,7 @@ class Transport (threading.Thread): This will almost always fail. It may be useful for determining the list of authentication types supported by the server, by catching the L{BadAuthenticationType} exception raised. - + @param username: the username to authenticate as @type username: string @return: list of auth types permissible for the next stage of @@ -1073,7 +1091,7 @@ class Transport (threading.Thread): by the server for this user @raise SSHException: if the authentication failed due to a network error - + @since: 1.5 """ if (not self.active) or (not self.initial_kex_done): @@ -1087,7 +1105,7 @@ class Transport (threading.Thread): """ Authenticate to the server using a password. The username and password are sent over an encrypted link. - + If an C{event} is passed in, this method will return immediately, and the event will be triggered once authentication succeeds or fails. On success, L{is_authenticated} will return C{True}. On failure, you may @@ -1096,7 +1114,7 @@ class Transport (threading.Thread): Since 1.1, if no event is passed, this method will block until the authentication succeeds or fails. On failure, an exception is raised. Otherwise, the method simply returns. - + Since 1.5, if no event is passed and C{fallback} is C{True} (the default), if the server doesn't support plain password authentication but does support so-called "keyboard-interactive" mode, an attempt @@ -1105,11 +1123,11 @@ class Transport (threading.Thread): made. This is useful for some recent Gentoo and Debian distributions, which turn off plain password authentication in a misguided belief that interactive authentication is "more secure". (It's not.) - + If the server requires multi-step authentication (which is very rare), this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. - + @param username: the username to authenticate as @type username: str @param password: the password to authenticate with @@ -1124,7 +1142,7 @@ class Transport (threading.Thread): @return: list of auth types permissible for the next stage of authentication (normally empty) @rtype: list - + @raise BadAuthenticationType: if password authentication isn't allowed by the server for this user (and no event was passed in) @raise AuthenticationException: if the authentication failed (and no @@ -1170,12 +1188,12 @@ class Transport (threading.Thread): """ Authenticate to the server using a private key. The key is used to sign data from the server, so it must include the private part. - + If an C{event} is passed in, this method will return immediately, and the event will be triggered once authentication succeeds or fails. On success, L{is_authenticated} will return C{True}. On failure, you may use L{get_exception} to get more detailed error information. - + Since 1.1, if no event is passed, this method will block until the authentication succeeds or fails. On failure, an exception is raised. Otherwise, the method simply returns. @@ -1194,7 +1212,7 @@ class Transport (threading.Thread): @return: list of auth types permissible for the next stage of authentication (normally empty) @rtype: list - + @raise BadAuthenticationType: if public-key authentication isn't allowed by the server for this user (and no event was passed in) @raise AuthenticationException: if the authentication failed (and no @@ -1214,18 +1232,18 @@ class Transport (threading.Thread): # caller wants to wait for event themselves return [] return self.auth_handler.wait_for_response(my_event) - + def auth_interactive(self, username, handler, submethods=''): """ Authenticate to the server interactively. A handler is used to answer arbitrary questions from the server. On many servers, this is just a dumb wrapper around PAM. - + This method will block until the authentication succeeds or fails, peroidically calling the handler asynchronously to get answers to authentication questions. The handler may be called more than once if the server continues to ask questions. - + The handler is expected to be a callable that will handle calls of the form: C{handler(title, instructions, prompt_list)}. The C{title} is meant to be a dialog-window title, and the C{instructions} are user @@ -1233,13 +1251,13 @@ class Transport (threading.Thread): prompts, each prompt being a tuple of C{(str, bool)}. The string is the prompt and the boolean indicates whether the user text should be echoed. - + A sample call would thus be: C{handler('title', 'instructions', [('Password:', False)])}. - + The handler should return a list or tuple of answers to the server's questions. - + If the server requires multi-step authentication (which is very rare), this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. @@ -1253,12 +1271,12 @@ class Transport (threading.Thread): @return: list of auth types permissible for the next stage of authentication (normally empty). @rtype: list - + @raise BadAuthenticationType: if public-key authentication isn't allowed by the server for this user @raise AuthenticationException: if the authentication failed @raise SSHException: if there was a network error - + @since: 1.5 """ if (not self.active) or (not self.initial_kex_done): @@ -1307,43 +1325,43 @@ class Transport (threading.Thread): @type hexdump: bool """ self.packetizer.set_hexdump(hexdump) - + def get_hexdump(self): """ Return C{True} if the transport is currently logging hex dumps of protocol traffic. - + @return: C{True} if hex dumps are being logged @rtype: bool - + @since: 1.4 """ return self.packetizer.get_hexdump() - + def use_compression(self, compress=True): """ Turn on/off compression. This will only have an affect before starting the transport (ie before calling L{connect}, etc). By default, compression is off since it negatively affects interactive sessions. - + @param compress: C{True} to ask the remote client/server to compress traffic; C{False} to refuse compression @type compress: bool - + @since: 1.5.2 """ if compress: self._preferred_compression = ( 'zlib@openssh.com', 'zlib', 'none' ) else: self._preferred_compression = ( 'none', ) - + def getpeername(self): """ Return the address of the remote side of this Transport, if possible. This is effectively a wrapper around C{'getpeername'} on the underlying socket. If the socket-like object has no C{'getpeername'} method, then C{("unknown", 0)} is returned. - + @return: the address if the remote host, if known @rtype: tuple(str, int) """ @@ -1359,7 +1377,7 @@ class Transport (threading.Thread): ### internals... - + def _log(self, level, msg, *args): if issubclass(type(msg), list): for m in msg: @@ -1392,6 +1410,7 @@ class Transport (threading.Thread): send a message, but block if we're in key negotiation. this is used for user-initiated requests. """ + start = time.time() while True: self.clear_to_send.wait(0.1) if not self.active: @@ -1401,6 +1420,8 @@ class Transport (threading.Thread): if self.clear_to_send.isSet(): break self.clear_to_send_lock.release() + if time.time() > start + self.clear_to_send_timeout: + raise SSHException('Key-exchange timed out waiting for key negotiation') try: self._send_message(data) finally: @@ -1422,7 +1443,7 @@ class Transport (threading.Thread): if key is None: raise SSHException('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 SSHException('Signature verification (%s) failed.' % self.host_key_type) self.host_key = key def _compute_key(self, id, nbytes): @@ -1446,7 +1467,19 @@ class Transport (threading.Thread): def _get_cipher(self, name, key, iv): if name not in self._cipher_info: raise SSHException('Unknown client cipher ' + name) - return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv) + if name in ('arcfour128', 'arcfour256'): + # arcfour cipher + cipher = self._cipher_info[name]['class'].new(key) + # as per RFC 4345, the first 1536 bytes of keystream + # generated by the cipher MUST be discarded + cipher.encrypt(" " * 1536) + return cipher + elif name.endswith("-ctr"): + # CTR modes, we need a counter + counter = Counter.new(nbits=self._cipher_info[name]['block-size'] * 8, initial_value=util.inflate_long(iv, True)) + return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv, counter) + else: + return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv) def _set_x11_handler(self, handler): # only called if a channel has turned on x11 forwarding @@ -1465,13 +1498,13 @@ class Transport (threading.Thread): self.server_accept_cv.notify() finally: self.lock.release() - + def run(self): # (use the exposed "run" method, because if we specify a thread target # of a private method, threading.Thread will keep a reference to it # indefinitely, creating a GC cycle and not letting Transport ever be - # GC'd. it's a bug in Thread.) - + # GC'd. it's a bug in Thread.) + # active=True occurs before the thread is launched, to avoid a race _active_threads.append(self) if self.server_mode: @@ -1587,7 +1620,7 @@ class Transport (threading.Thread): def _check_banner(self): # this is slow, but we only have to do it once - for i in range(5): + for i in range(100): # give them 15 seconds for the first line, then just 2 seconds # each additional line. (some sites have very high latency.) if i == 0: @@ -1896,7 +1929,7 @@ class Transport (threading.Thread): self.global_response = m if self.completion_event is not None: self.completion_event.set() - + def _parse_request_failure(self, m): self._log(DEBUG, 'Global request denied.') self.global_response = None @@ -1985,7 +2018,7 @@ class Transport (threading.Thread): origin_addr = m.get_string() origin_port = m.get_int() reason = self.server_object.check_channel_direct_tcpip_request( - my_chanid, (origin_addr, origin_port), + my_chanid, (origin_addr, origin_port), (dest_addr, dest_port)) else: reason = self.server_object.check_channel_request(kind, my_chanid) @@ -2001,7 +2034,7 @@ class Transport (threading.Thread): msg.add_string('en') self._send_message(msg) return - + chan = Channel(my_chanid) self.lock.acquire() try: diff --git a/paramiko/util.py b/paramiko/util.py index 8abdc0c..0d6a534 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -22,6 +22,7 @@ Useful functions used by the rest of paramiko. from __future__ import generators +import array from binascii import hexlify, unhexlify import sys import struct @@ -135,6 +136,8 @@ def safe_string(s): def bit_length(n): norm = deflate_long(n, 0) hbyte = ord(norm[0]) + if hbyte == 0: + return 1 bitlen = len(norm) * 8 while not (hbyte & 0x80): hbyte <<= 1 @@ -184,10 +187,10 @@ def load_host_keys(filename): return a compound dict of C{hostname -> keytype ->} L{PKey <paramiko.pkey.PKey>}. The hostname may be an IP address or DNS name. The keytype will be either C{"ssh-rsa"} or C{"ssh-dss"}. - + This type of file unfortunately doesn't exist on Windows, but on posix, it will usually be stored in C{os.path.expanduser("~/.ssh/known_hosts")}. - + Since 1.5.3, this is just a wrapper around L{HostKeys}. @param filename: name of the file to read host keys from @@ -268,3 +271,32 @@ def get_logger(name): return l +class Counter (object): + """Stateful counter for CTR mode crypto""" + def __init__(self, nbits, initial_value=1L, overflow=0L): + self.blocksize = nbits / 8 + self.overflow = overflow + # start with value - 1 so we don't have to store intermediate values when counting + # could the iv be 0? + if initial_value == 0: + self.value = array.array('c', '\xFF' * self.blocksize) + else: + x = deflate_long(initial_value - 1, add_sign_padding=False) + self.value = array.array('c', '\x00' * (self.blocksize - len(x)) + x) + + def __call__(self): + """Increament the counter and return the new value""" + i = self.blocksize - 1 + while i > -1: + c = self.value[i] = chr((ord(self.value[i]) + 1) % 256) + if c != '\x00': + return self.value.tostring() + i -= 1 + # counter reset + x = deflate_long(self.overflow, add_sign_padding=False) + self.value = array.array('c', '\x00' * (self.blocksize - len(x)) + x) + return self.value.tostring() + + def new(cls, nbits, initial_value=1L, overflow=0L): + return cls(nbits, initial_value=initial_value, overflow=overflow) + new = classmethod(new) |