From 448613e33c395917771c2751e62888a74ff663b3 Mon Sep 17 00:00:00 2001 From: John Begeman Date: Wed, 9 Jan 2013 12:39:33 -0500 Subject: Add listdir_iter to sftp_client.SFTPClient --- paramiko/sftp_client.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 8cb8ceaf..b546363d 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -193,6 +193,75 @@ class SFTPClient (BaseSFTP): self._request(CMD_CLOSE, handle) return filelist + def listdir_iter(self, path='.', read_ahead_requests=50): + """ + Generator yielding L{SFTPAttributes} objects corresponding to + files in the given C{path}. Files are yielded 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 + depend on the SFTP server implementation. + + @param path: path to list (defaults to C{'.'}) + @type path: str + @return: Yields L{SFTPAttributes} + @rtype: L{SFTPAttributes} + + @since: 1.9 + """ + path = self._adjust_cwd(path) + self._log(DEBUG, 'listdir(%r)' % path) + t, msg = self._request(CMD_OPENDIR, path) + + if t != CMD_HANDLE: + raise SFTPError('Expected handle') + + handle = msg.get_string() + + nums = list() + while True: + try: + # Assume the handle IDs we're getting will be in sequence... + # Send out a bunch of readdir requests so that we can read the responses later on + # Section 6.7 of the SSH file transfer RFC explains this + # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt + for i in range(read_ahead_requests): + num = self._async_request(type(None), CMD_READDIR, handle) + nums.append(num) + + + # For each of our sent requests + # Read and parse the corresponding packets + # If we're at the end of our queued requests, then fire off some more requests + # Exit the loop when we've reached the end of the directory handle + for num in nums: + # Avoid using self._request as it does a bunch of shit we don't care about in scanning directories + t, pkt_data = self._read_packet() + msg = Message(pkt_data) + new_num = msg.get_int() + if num == new_num: + if t == CMD_STATUS: + self._convert_status(msg) + count = msg.get_int() + for i in range(count): + filename = msg.get_string() + longname = msg.get_string() + attr = SFTPAttributes._from_msg(msg, filename, longname) + if (filename != '.') and (filename != '..'): + now = datetime.datetime.now() + yield attr + + # If we've hit the end of our queued requests, reset nums. + nums = list() + + except EOFError as e: + self._request(CMD_CLOSE, handle) + return + + def open(self, filename, mode='r', bufsize=-1): """ Open a file on the remote server. The arguments are the same as for -- cgit v1.2.3 From 6313ad0c3a942f2c3eb471e8fed265028a469234 Mon Sep 17 00:00:00 2001 From: John Begeman Date: Wed, 9 Jan 2013 15:02:21 -0500 Subject: Remove some debug stuff I inadvertently left in and remove some confusing comments --- paramiko/sftp_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index b546363d..4c69da26 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -224,7 +224,6 @@ class SFTPClient (BaseSFTP): nums = list() while True: try: - # Assume the handle IDs we're getting will be in sequence... # Send out a bunch of readdir requests so that we can read the responses later on # Section 6.7 of the SSH file transfer RFC explains this # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt @@ -238,7 +237,6 @@ class SFTPClient (BaseSFTP): # If we're at the end of our queued requests, then fire off some more requests # Exit the loop when we've reached the end of the directory handle for num in nums: - # Avoid using self._request as it does a bunch of shit we don't care about in scanning directories t, pkt_data = self._read_packet() msg = Message(pkt_data) new_num = msg.get_int() @@ -251,7 +249,6 @@ class SFTPClient (BaseSFTP): longname = msg.get_string() attr = SFTPAttributes._from_msg(msg, filename, longname) if (filename != '.') and (filename != '..'): - now = datetime.datetime.now() yield attr # If we've hit the end of our queued requests, reset nums. -- cgit v1.2.3 From 683b3c22893be899d2fdbf1ada35e61fba8a3d70 Mon Sep 17 00:00:00 2001 From: John Begeman Date: Wed, 9 Jan 2013 15:26:55 -0500 Subject: Add python 2.5 support except block to listdir_iter --- paramiko/sftp_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 4c69da26..5c2eb8bb 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -254,7 +254,7 @@ class SFTPClient (BaseSFTP): # If we've hit the end of our queued requests, reset nums. nums = list() - except EOFError as e: + except EOFError: self._request(CMD_CLOSE, handle) return -- cgit v1.2.3 From c70238250e25843629fd8c53e42bc3a8d34b9cfe Mon Sep 17 00:00:00 2001 From: aszlig Date: Wed, 2 Oct 2013 10:38:13 +0200 Subject: SSHClient: Allow to use ECDSA private keys. Nowadays, ECDSA keys became more widespread, so it might be a good idea to support those private keys as well. Signed-off-by: aszlig --- paramiko/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index c5a2d1ac..f314a4cb 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -30,6 +30,7 @@ from paramiko.agent import Agent from paramiko.common import * from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey +from paramiko.ecdsakey import ECDSAKey from paramiko.hostkeys import HostKeys from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey @@ -457,7 +458,7 @@ class SSHClient (object): if not two_factor: for key_filename in key_filenames: - for pkey_class in (RSAKey, DSSKey): + for pkey_class in (RSAKey, DSSKey, ECDSAKey): try: key = pkey_class.from_private_key_file(key_filename, password) self._log(DEBUG, 'Trying key %s from %s' % (hexlify(key.get_fingerprint()), key_filename)) -- cgit v1.2.3 From ad33bb186f1fa5750b363be66348b4fa6fbfdd4a Mon Sep 17 00:00:00 2001 From: aszlig Date: Wed, 2 Oct 2013 11:00:38 +0200 Subject: SSHClient: Also look for id_ecdsa in ~/.ssh. I'm not using keys in ~/.ssh at all, so I missed adding ECDSA support there as well. Signed-off-by: aszlig --- paramiko/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/paramiko/client.py b/paramiko/client.py index f314a4cb..0af54c8d 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -490,17 +490,23 @@ class SSHClient (object): keyfiles = [] rsa_key = os.path.expanduser('~/.ssh/id_rsa') dsa_key = os.path.expanduser('~/.ssh/id_dsa') + ecdsa_key = os.path.expanduser('~/.ssh/id_ecdsa') if os.path.isfile(rsa_key): keyfiles.append((RSAKey, rsa_key)) if os.path.isfile(dsa_key): keyfiles.append((DSSKey, dsa_key)) + if os.path.isfile(ecdsa_key): + keyfiles.append((ECDSAKey, ecdsa_key)) # look in ~/ssh/ for windows users: rsa_key = os.path.expanduser('~/ssh/id_rsa') dsa_key = os.path.expanduser('~/ssh/id_dsa') + ecdsa_key = os.path.expanduser('~/ssh/id_ecdsa') if os.path.isfile(rsa_key): keyfiles.append((RSAKey, rsa_key)) if os.path.isfile(dsa_key): keyfiles.append((DSSKey, dsa_key)) + if os.path.isfile(ecdsa_key): + keyfiles.append((ECDSAKey, ecdsa_key)) if not look_for_keys: keyfiles = [] -- cgit v1.2.3 From 27b800bf3f1c0ee7063221538d2913cec7c048c1 Mon Sep 17 00:00:00 2001 From: Torsten Landschoff Date: Fri, 12 Aug 2011 11:25:36 +0200 Subject: Issue #22: Try IPv4 as well as IPv6 when port is not open on IPv6. With this change, paramiko tries the next address family when the connection gets refused or the target port is unreachable. --- paramiko/client.py | 53 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index c5a2d1ac..da08ad0a 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -25,6 +25,7 @@ import getpass import os import socket import warnings +from errno import ECONNREFUSED, EHOSTUNREACH from paramiko.agent import Agent from paramiko.common import * @@ -231,6 +232,29 @@ class SSHClient (object): """ self._policy = policy + def _families_and_addresses(self, hostname, port): + """ + Yield pairs of address families and addresses to try for connecting. + + @param hostname: the server to connect to + @type hostname: str + @param port: the server port to connect to + @type port: int + @rtype: generator + """ + guess = True + addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) + for (family, socktype, proto, canonname, sockaddr) in addrinfos: + if socktype == socket.SOCK_STREAM: + yield family, sockaddr + guess = False + + # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( + # We only do this if we did not get a single result marked as socktype == SOCK_STREAM. + if guess: + for family, _, _, _, sockaddr in addrinfos: + yield family, sockaddr + 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, compress=False, sock=None): @@ -288,21 +312,22 @@ class SSHClient (object): @raise socket.error: if a socket error occurred while connecting """ if not sock: - 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: - # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( - af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) - sock = socket.socket(af, socket.SOCK_STREAM) - if timeout is not None: + for af, addr in self._families_and_addresses(hostname, port): try: - sock.settimeout(timeout) - except: - pass - retry_on_signal(lambda: sock.connect(addr)) + sock = socket.socket(af, socket.SOCK_STREAM) + if timeout is not None: + try: + sock.settimeout(timeout) + except: + pass + retry_on_signal(lambda: sock.connect(addr)) + # Break out of the loop on success + break + except socket.error, e: + # If the port is not open on IPv6 for example, we may still try IPv4. + # Likewise if the host is not reachable using that address family. + if e.errno not in (ECONNREFUSED, EHOSTUNREACH): + raise t = self._transport = Transport(sock) t.use_compression(compress=compress) -- cgit v1.2.3 From 68cf5ab063f27e295ec4c63cf015d505d32841b3 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 10:50:19 -0700 Subject: Removed an unused import. --- paramiko/kex_gex.py | 1 - 1 file changed, 1 deletion(-) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index c0455a1f..71218aca 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -23,7 +23,6 @@ client side, and a B{lot} more on the server side. """ from Crypto.Hash import SHA -from Crypto.Util import number from paramiko.common import * from paramiko import util -- cgit v1.2.3 From 5335d9dc0a6266217d288e7aaf2091739980d841 Mon Sep 17 00:00:00 2001 From: David Pursehouse Date: Fri, 15 Nov 2013 19:31:56 +0900 Subject: Change logging level of "Secsh channel" logs Log "Secsh channel %d opened" with level DEBUG. Log "Secsh channel %d open FAILED" with level ERROR. Logging these with level INFO causes a lot of noise on the console when using paramiko in an application that makes a lot of connections. --- paramiko/transport.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 3155d3f8..1fd7c2f3 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1998,7 +1998,7 @@ class Transport (threading.Thread): self.lock.acquire() try: chan._set_remote_channel(server_chanid, server_window_size, server_max_packet_size) - self._log(INFO, 'Secsh channel %d opened.' % chanid) + self._log(DEBUG, 'Secsh channel %d opened.' % chanid) if chanid in self.channel_events: self.channel_events[chanid].set() del self.channel_events[chanid] @@ -2012,7 +2012,7 @@ class Transport (threading.Thread): reason_str = m.get_string() lang = m.get_string() reason_text = CONNECTION_FAILED_CODE.get(reason, '(unknown code)') - self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) + self._log(ERROR, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) self.lock.acquire() try: self.saved_exception = ChannelException(reason, reason_text) @@ -2109,7 +2109,7 @@ class Transport (threading.Thread): m.add_int(self.window_size) m.add_int(self.max_packet_size) self._send_message(m) - self._log(INFO, 'Secsh channel %d (%s) opened.', my_chanid, kind) + self._log(DEBUG, 'Secsh channel %d (%s) opened.', my_chanid, kind) if kind == 'auth-agent@openssh.com': self._forward_agent_handler(chan) elif kind == 'x11': -- cgit v1.2.3 From e7f41de2f2dac5d03404f35edc5514f12e42c49f Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Tue, 11 Feb 2014 13:01:49 +0100 Subject: Merge branch scottkmaxwell:py3-support-without-py25 into SebastianDeiss:gssapi-py3-support --- .travis.yml | 2 + README | 4 +- demos/demo.py | 53 +++--- demos/demo_keygen.py | 23 ++- demos/demo_server.py | 43 ++--- demos/demo_sftp.py | 37 ++-- demos/demo_simple.py | 23 ++- demos/forward.py | 16 +- demos/interactive.py | 5 +- demos/rforward.py | 10 +- dev-requirements.txt | 4 +- paramiko/__init__.py | 70 +++---- paramiko/_winapi.py | 436 +++++++++++++++++++++---------------------- paramiko/agent.py | 30 +-- paramiko/auth_handler.py | 78 ++++---- paramiko/ber.py | 30 +-- paramiko/buffered_pipe.py | 25 ++- paramiko/channel.py | 58 +++--- paramiko/client.py | 31 ++-- paramiko/common.py | 45 ++++- paramiko/config.py | 2 +- paramiko/dsskey.py | 27 +-- paramiko/ecdsakey.py | 34 ++-- paramiko/file.py | 98 ++++++---- paramiko/hostkeys.py | 91 +++++---- paramiko/kex_gex.py | 32 ++-- paramiko/kex_group1.py | 33 ++-- paramiko/message.py | 109 ++++++++--- paramiko/packet.py | 42 +++-- paramiko/pipe.py | 5 +- paramiko/pkey.py | 50 ++--- paramiko/primes.py | 31 ++-- paramiko/proxy.py | 9 +- paramiko/py3compat.py | 160 ++++++++++++++++ paramiko/rsakey.py | 24 ++- paramiko/server.py | 4 +- paramiko/sftp.py | 15 +- paramiko/sftp_attr.py | 14 +- paramiko/sftp_client.py | 81 ++++---- paramiko/sftp_file.py | 14 +- paramiko/sftp_handle.py | 4 +- paramiko/sftp_server.py | 83 ++++----- paramiko/transport.py | 183 +++++++++--------- paramiko/util.py | 96 +++++----- paramiko/win_pageant.py | 5 +- setup.py | 2 +- test.py | 35 ++-- tests/__init__.py | 0 tests/loop.py | 8 +- tests/stub_sftp.py | 32 ++-- tests/test_auth.py | 54 +++--- tests/test_buffered_pipe.py | 31 ++-- tests/test_client.py | 126 +++++++------ tests/test_file.py | 17 +- tests/test_hostkeys.py | 56 +++--- tests/test_kex.py | 129 ++++++------- tests/test_message.py | 77 ++++---- tests/test_packetizer.py | 35 ++-- tests/test_pkey.py | 191 +++++++++---------- tests/test_sftp.py | 440 ++++++++++++++++++++++---------------------- tests/test_sftp_big.py | 313 +++++++++++++++---------------- tests/test_transport.py | 223 +++++++++++----------- tests/test_util.py | 67 ++++--- tests/util.py | 7 + tox.ini | 2 +- 65 files changed, 2243 insertions(+), 1871 deletions(-) create mode 100644 paramiko/py3compat.py create mode 100644 tests/__init__.py diff --git a/.travis.yml b/.travis.yml index 88b69936..291acf96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: python python: - "2.6" - "2.7" + - "3.2" + - "3.3" install: # Self-install for setup.py-driven deps - pip install -e . diff --git a/README b/README index 09334675..666b31a7 100644 --- a/README +++ b/README @@ -15,7 +15,7 @@ What ---- "paramiko" is a combination of the esperanto words for "paranoid" and -"friend". it's a module for python 2.5+ that implements the SSH2 protocol +"friend". it's a module for python 2.6+ that implements the SSH2 protocol for secure (encrypted and authenticated) connections to remote machines. unlike SSL (aka TLS), SSH2 protocol does not require hierarchical certificates signed by a powerful central authority. you may know SSH2 as @@ -34,7 +34,7 @@ that should have come with this archive. Requirements ------------ - - python 2.5 or better + - python 2.6 or better - pycrypto 2.1 or better - ecdsa 0.9 or better diff --git a/demos/demo.py b/demos/demo.py index aa4bdaa5..fff61784 100755 --- a/demos/demo.py +++ b/demos/demo.py @@ -28,9 +28,13 @@ import socket import sys import time import traceback +from paramiko.py3compat import input import paramiko -import interactive +try: + import interactive +except ImportError: + from . import interactive def agent_auth(transport, username): @@ -45,24 +49,24 @@ def agent_auth(transport, username): return for key in agent_keys: - print 'Trying ssh-agent key %s' % hexlify(key.get_fingerprint()), + print('Trying ssh-agent key %s' % hexlify(key.get_fingerprint())) try: transport.auth_publickey(username, key) - print '... success!' + print('... success!') return except paramiko.SSHException: - print '... nope.' + print('... nope.') def manual_auth(username, hostname): default_auth = 'p' - auth = raw_input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth) + auth = input('Auth by (p)assword, (r)sa key, or (d)ss key? [%s] ' % default_auth) if len(auth) == 0: auth = default_auth if auth == 'r': default_path = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa') - path = raw_input('RSA key [%s]: ' % default_path) + path = input('RSA key [%s]: ' % default_path) if len(path) == 0: path = default_path try: @@ -73,7 +77,7 @@ def manual_auth(username, hostname): t.auth_publickey(username, key) elif auth == 'd': default_path = os.path.join(os.environ['HOME'], '.ssh', 'id_dsa') - path = raw_input('DSS key [%s]: ' % default_path) + path = input('DSS key [%s]: ' % default_path) if len(path) == 0: path = default_path try: @@ -96,9 +100,9 @@ if len(sys.argv) > 1: if hostname.find('@') >= 0: username, hostname = hostname.split('@') else: - hostname = raw_input('Hostname: ') + hostname = input('Hostname: ') if len(hostname) == 0: - print '*** Hostname required.' + print('*** Hostname required.') sys.exit(1) port = 22 if hostname.find(':') >= 0: @@ -109,8 +113,8 @@ if hostname.find(':') >= 0: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((hostname, port)) -except Exception, e: - print '*** Connect failed: ' + str(e) +except Exception as e: + print('*** Connect failed: ' + str(e)) traceback.print_exc() sys.exit(1) @@ -119,7 +123,7 @@ try: try: t.start_client() except paramiko.SSHException: - print '*** SSH negotiation failed.' + print('*** SSH negotiation failed.') sys.exit(1) try: @@ -128,25 +132,25 @@ try: try: keys = paramiko.util.load_host_keys(os.path.expanduser('~/ssh/known_hosts')) except IOError: - print '*** Unable to open host keys file' + print('*** Unable to open host keys file') keys = {} # check server's host key -- this is important. key = t.get_remote_server_key() - if not keys.has_key(hostname): - print '*** WARNING: Unknown host key!' - elif not keys[hostname].has_key(key.get_name()): - print '*** WARNING: Unknown host key!' + if hostname not in keys: + print('*** WARNING: Unknown host key!') + elif key.get_name() not in keys[hostname]: + print('*** WARNING: Unknown host key!') elif keys[hostname][key.get_name()] != key: - print '*** WARNING: Host key has changed!!!' + print('*** WARNING: Host key has changed!!!') sys.exit(1) else: - print '*** Host key OK.' + print('*** Host key OK.') # get username if username == '': default_username = getpass.getuser() - username = raw_input('Username [%s]: ' % default_username) + username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username @@ -154,21 +158,20 @@ try: if not t.is_authenticated(): manual_auth(username, hostname) if not t.is_authenticated(): - print '*** Authentication failed. :(' + print('*** Authentication failed. :(') t.close() sys.exit(1) chan = t.open_session() chan.get_pty() chan.invoke_shell() - print '*** Here we go!' - print + print('*** Here we go!\n') interactive.interactive_shell(chan) chan.close() t.close() -except Exception, e: - print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e) +except Exception as e: + print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) traceback.print_exc() try: t.close() diff --git a/demos/demo_keygen.py b/demos/demo_keygen.py index bdd7388d..860ee4e9 100755 --- a/demos/demo_keygen.py +++ b/demos/demo_keygen.py @@ -17,9 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -from __future__ import with_statement -import string import sys from binascii import hexlify @@ -28,6 +26,7 @@ from optparse import OptionParser from paramiko import DSSKey from paramiko import RSAKey from paramiko.ssh_exception import SSHException +from paramiko.py3compat import u usage=""" %prog [-v] [-b bits] -t type [-N new_passphrase] [-f output_keyfile]""" @@ -47,16 +46,16 @@ key_dispatch_table = { def progress(arg=None): if not arg: - print '0%\x08\x08\x08', + sys.stdout.write('0%\x08\x08\x08 ') sys.stdout.flush() elif arg[0] == 'p': - print '25%\x08\x08\x08\x08', + sys.stdout.write('25%\x08\x08\x08\x08 ') sys.stdout.flush() elif arg[0] == 'h': - print '50%\x08\x08\x08\x08', + sys.stdout.write('50%\x08\x08\x08\x08 ') sys.stdout.flush() elif arg[0] == 'x': - print '75%\x08\x08\x08\x08', + sys.stdout.write('75%\x08\x08\x08\x08 ') sys.stdout.flush() if __name__ == '__main__': @@ -92,8 +91,8 @@ if __name__ == '__main__': parser.print_help() sys.exit(0) - for o in default_values.keys(): - globals()[o] = getattr(options, o, default_values[string.lower(o)]) + for o in list(default_values.keys()): + globals()[o] = getattr(options, o, default_values[o.lower()]) if options.newphrase: phrase = getattr(options, 'newphrase') @@ -106,7 +105,7 @@ if __name__ == '__main__': if ktype == 'dsa' and bits > 1024: raise SSHException("DSA Keys must be 1024 bits") - if not key_dispatch_table.has_key(ktype): + if ktype not in key_dispatch_table: raise SSHException("Unknown %s algorithm to generate keys pair" % ktype) # generating private key @@ -121,7 +120,7 @@ if __name__ == '__main__': f.write(" %s" % comment) if options.verbose: - print "done." + print("done.") - hash = hexlify(pub.get_fingerprint()) - print "Fingerprint: %d %s %s.pub (%s)" % (bits, ":".join([ hash[i:2+i] for i in range(0, len(hash), 2)]), filename, string.upper(ktype)) + hash = u(hexlify(pub.get_fingerprint())) + print("Fingerprint: %d %s %s.pub (%s)" % (bits, ":".join([ hash[i:2+i] for i in range(0, len(hash), 2)]), filename, ktype.upper())) diff --git a/demos/demo_server.py b/demos/demo_server.py index 915b0c67..bb35258b 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -27,6 +27,7 @@ import threading import traceback import paramiko +from paramiko.py3compat import b, u, decodebytes # setup logging @@ -35,17 +36,17 @@ paramiko.util.log_to_file('demo_server.log') host_key = paramiko.RSAKey(filename='test_rsa.key') #host_key = paramiko.DSSKey(filename='test_dss.key') -print 'Read key: ' + hexlify(host_key.get_fingerprint()) +print('Read key: ' + u(hexlify(host_key.get_fingerprint()))) class Server (paramiko.ServerInterface): # 'data' is the output of base64.encodestring(str(key)) # (using the "user_rsa_key" files) - data = 'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + \ - 'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + \ - 'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + \ - 'UWT10hcuO4Ks8=' - good_pub_key = paramiko.RSAKey(data=base64.decodestring(data)) + data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' + b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' + b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' + b'UWT10hcuO4Ks8=') + good_pub_key = paramiko.RSAKey(data=decodebytes(data)) def __init__(self): self.event = threading.Event() @@ -61,7 +62,7 @@ class Server (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - print 'Auth attempt with key: ' + hexlify(key.get_fingerprint()) + print('Auth attempt with key: ' + u(hexlify(key.get_fingerprint()))) if (username == 'robey') and (key == self.good_pub_key): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -83,47 +84,47 @@ try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 2200)) -except Exception, e: - print '*** Bind failed: ' + str(e) +except Exception as e: + print('*** Bind failed: ' + str(e)) traceback.print_exc() sys.exit(1) try: sock.listen(100) - print 'Listening for connection ...' + print('Listening for connection ...') client, addr = sock.accept() -except Exception, e: - print '*** Listen/accept failed: ' + str(e) +except Exception as e: + print('*** Listen/accept failed: ' + str(e)) traceback.print_exc() sys.exit(1) -print 'Got a connection!' +print('Got a connection!') try: t = paramiko.Transport(client) try: t.load_server_moduli() except: - print '(Failed to load moduli -- gex will be unsupported.)' + print('(Failed to load moduli -- gex will be unsupported.)') raise t.add_server_key(host_key) server = Server() try: t.start_server(server=server) - except paramiko.SSHException, x: - print '*** SSH negotiation failed.' + except paramiko.SSHException: + print('*** SSH negotiation failed.') sys.exit(1) # wait for auth chan = t.accept(20) if chan is None: - print '*** No channel.' + print('*** No channel.') sys.exit(1) - print 'Authenticated!' + print('Authenticated!') server.event.wait(10) if not server.event.isSet(): - print '*** Client never asked for a shell.' + print('*** Client never asked for a shell.') sys.exit(1) chan.send('\r\n\r\nWelcome to my dorky little BBS!\r\n\r\n') @@ -135,8 +136,8 @@ try: chan.send('\r\nI don\'t like you, ' + username + '.\r\n') chan.close() -except Exception, e: - print '*** Caught exception: ' + str(e.__class__) + ': ' + str(e) +except Exception as e: + print('*** Caught exception: ' + str(e.__class__) + ': ' + str(e)) traceback.print_exc() try: t.close() diff --git a/demos/demo_sftp.py b/demos/demo_sftp.py index 7c4aaba0..a34f2b19 100755 --- a/demos/demo_sftp.py +++ b/demos/demo_sftp.py @@ -28,6 +28,7 @@ import sys import traceback import paramiko +from paramiko.py3compat import input # setup logging @@ -40,9 +41,9 @@ if len(sys.argv) > 1: if hostname.find('@') >= 0: username, hostname = hostname.split('@') else: - hostname = raw_input('Hostname: ') + hostname = input('Hostname: ') if len(hostname) == 0: - print '*** Hostname required.' + print('*** Hostname required.') sys.exit(1) port = 22 if hostname.find(':') >= 0: @@ -53,7 +54,7 @@ if hostname.find(':') >= 0: # get username if username == '': default_username = getpass.getuser() - username = raw_input('Username [%s]: ' % default_username) + username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) @@ -69,13 +70,13 @@ except IOError: # try ~/ssh/ too, because windows can't have a folder named ~/.ssh/ host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/ssh/known_hosts')) except IOError: - print '*** Unable to open host keys file' + print('*** Unable to open host keys file') host_keys = {} -if host_keys.has_key(hostname): +if hostname in host_keys: hostkeytype = host_keys[hostname].keys()[0] hostkey = host_keys[hostname][hostkeytype] - print 'Using host key of type %s' % hostkeytype + print('Using host key of type %s' % hostkeytype) # now, connect and use paramiko Transport to negotiate SSH2 across the connection @@ -86,22 +87,26 @@ try: # dirlist on remote host dirlist = sftp.listdir('.') - print "Dirlist:", dirlist + print("Dirlist: %s" % dirlist) # copy this demo onto the server try: sftp.mkdir("demo_sftp_folder") except IOError: - print '(assuming demo_sftp_folder/ already exists)' - sftp.open('demo_sftp_folder/README', 'w').write('This was created by demo_sftp.py.\n') - data = open('demo_sftp.py', 'r').read() + print('(assuming demo_sftp_folder/ already exists)') + with sftp.open('demo_sftp_folder/README', 'w') as f: + f.write('This was created by demo_sftp.py.\n') + with open('demo_sftp.py', 'r') as f: + data = f.read() sftp.open('demo_sftp_folder/demo_sftp.py', 'w').write(data) - print 'created demo_sftp_folder/ on the server' + print('created demo_sftp_folder/ on the server') # copy the README back here - data = sftp.open('demo_sftp_folder/README', 'r').read() - open('README_demo_sftp', 'w').write(data) - print 'copied README back here' + with sftp.open('demo_sftp_folder/README', 'r') as f: + data = f.read() + with open('README_demo_sftp', 'w') as f: + f.write(data) + print('copied README back here') # BETTER: use the get() and put() methods sftp.put('demo_sftp.py', 'demo_sftp_folder/demo_sftp.py') @@ -109,8 +114,8 @@ try: t.close() -except Exception, e: - print '*** Caught exception: %s: %s' % (e.__class__, e) +except Exception as e: + print('*** Caught exception: %s: %s' % (e.__class__, e)) traceback.print_exc() try: t.close() diff --git a/demos/demo_simple.py b/demos/demo_simple.py index 50f344a7..ae631e43 100755 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -25,9 +25,13 @@ import os import socket import sys import traceback +from paramiko.py3compat import input import paramiko -import interactive +try: + import interactive +except ImportError: + from . import interactive # setup logging @@ -40,9 +44,9 @@ if len(sys.argv) > 1: if hostname.find('@') >= 0: username, hostname = hostname.split('@') else: - hostname = raw_input('Hostname: ') + hostname = input('Hostname: ') if len(hostname) == 0: - print '*** Hostname required.' + print('*** Hostname required.') sys.exit(1) port = 22 if hostname.find(':') >= 0: @@ -53,7 +57,7 @@ if hostname.find(':') >= 0: # get username if username == '': default_username = getpass.getuser() - username = raw_input('Username [%s]: ' % default_username) + username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) @@ -64,18 +68,17 @@ try: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) - print '*** Connecting...' + print('*** Connecting...') client.connect(hostname, port, username, password) chan = client.invoke_shell() - print repr(client.get_transport()) - print '*** Here we go!' - print + print(repr(client.get_transport())) + print('*** Here we go!\n') interactive.interactive_shell(chan) chan.close() client.close() -except Exception, e: - print '*** Caught exception: %s: %s' % (e.__class__, e) +except Exception as e: + print('*** Caught exception: %s: %s' % (e.__class__, e)) traceback.print_exc() try: client.close() diff --git a/demos/forward.py b/demos/forward.py index 5048c775..96e1700d 100644 --- a/demos/forward.py +++ b/demos/forward.py @@ -30,7 +30,11 @@ import getpass import os import socket import select -import SocketServer +try: + import SocketServer +except ImportError: + import socketserver as SocketServer + import sys from optparse import OptionParser @@ -54,7 +58,7 @@ class Handler (SocketServer.BaseRequestHandler): chan = self.ssh_transport.open_channel('direct-tcpip', (self.chain_host, self.chain_port), self.request.getpeername()) - except Exception, e: + except Exception as e: verbose('Incoming request to %s:%d failed: %s' % (self.chain_host, self.chain_port, repr(e))) @@ -98,7 +102,7 @@ def forward_tunnel(local_port, remote_host, remote_port, transport): def verbose(s): if g_verbose: - print s + print(s) HELP = """\ @@ -165,8 +169,8 @@ def main(): try: client.connect(server[0], server[1], username=options.user, key_filename=options.keyfile, look_for_keys=options.look_for_keys, password=password) - except Exception, e: - print '*** Failed to connect to %s:%d: %r' % (server[0], server[1], e) + except Exception as e: + print('*** Failed to connect to %s:%d: %r' % (server[0], server[1], e)) sys.exit(1) verbose('Now forwarding port %d to %s:%d ...' % (options.port, remote[0], remote[1])) @@ -174,7 +178,7 @@ def main(): try: forward_tunnel(options.port, remote[0], remote[1], client.get_transport()) except KeyboardInterrupt: - print 'C-c: Port forwarding stopped.' + print('C-c: Port forwarding stopped.') sys.exit(0) diff --git a/demos/interactive.py b/demos/interactive.py index f3be74d2..7138cd6c 100644 --- a/demos/interactive.py +++ b/demos/interactive.py @@ -19,6 +19,7 @@ import socket import sys +from paramiko.py3compat import u # windows does not have termios... try: @@ -49,9 +50,9 @@ def posix_shell(chan): r, w, e = select.select([chan, sys.stdin], [], []) if chan in r: try: - x = chan.recv(1024) + x = u(chan.recv(1024)) if len(x) == 0: - print '\r\n*** EOF\r\n', + sys.stdout.write('\r\n*** EOF\r\n') break sys.stdout.write(x) sys.stdout.flush() diff --git a/demos/rforward.py b/demos/rforward.py index 4a5d2e43..ae70670c 100755 --- a/demos/rforward.py +++ b/demos/rforward.py @@ -46,7 +46,7 @@ def handler(chan, host, port): sock = socket.socket() try: sock.connect((host, port)) - except Exception, e: + except Exception as e: verbose('Forwarding request to %s:%d failed: %r' % (host, port, e)) return @@ -82,7 +82,7 @@ def reverse_forward_tunnel(server_port, remote_host, remote_port, transport): def verbose(s): if g_verbose: - print s + print(s) HELP = """\ @@ -150,8 +150,8 @@ def main(): try: client.connect(server[0], server[1], username=options.user, key_filename=options.keyfile, look_for_keys=options.look_for_keys, password=password) - except Exception, e: - print '*** Failed to connect to %s:%d: %r' % (server[0], server[1], e) + except Exception as e: + print('*** Failed to connect to %s:%d: %r' % (server[0], server[1], e)) sys.exit(1) verbose('Now forwarding remote port %d to %s:%d ...' % (options.port, remote[0], remote[1])) @@ -159,7 +159,7 @@ def main(): try: reverse_forward_tunnel(options.port, remote[0], remote[1], client.get_transport()) except KeyboardInterrupt: - print 'C-c: Port forwarding stopped.' + print('C-c: Port forwarding stopped.') sys.exit(0) diff --git a/dev-requirements.txt b/dev-requirements.txt index f706c46f..cdd120a9 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,2 @@ -tox>=1.4,<1.5 -epydoc>=3.0,<3.1 +tox>=1.4 +epydoc>=3.0 diff --git a/paramiko/__init__.py b/paramiko/__init__.py index c996302a..8a814061 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -18,7 +18,7 @@ """ I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend") -is a module for python 2.5 or greater that implements the SSH2 protocol for +is a module for python 2.6 or greater that implements the SSH2 protocol for secure (encrypted and authenticated) connections to remote machines. Unlike SSL (aka TLS), the SSH2 protocol does not require hierarchical certificates signed by a powerful central authority. You may know SSH2 as the protocol that @@ -52,58 +52,58 @@ Mailing list: U{paramiko@librelist.com} import sys -if sys.version_info < (2, 5): - raise RuntimeError('You need python 2.5+ for this module.') +if sys.version_info < (2, 6): + raise RuntimeError('You need python 2.6+ for this module.') __author__ = "Jeff Forcier " -__version__ = "1.12.1" +__version__ = "1.13.0" __version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" -from transport import SecurityOptions, Transport -from client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy -from auth_handler import AuthHandler -from channel import Channel, ChannelFile -from ssh_exception import SSHException, PasswordRequiredException, \ +from paramiko.transport import SecurityOptions, Transport +from paramiko.client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy +from paramiko.auth_handler import AuthHandler +from paramiko.channel import Channel, ChannelFile +from paramiko.ssh_exception import SSHException, PasswordRequiredException, \ BadAuthenticationType, ChannelException, BadHostKeyException, \ AuthenticationException, ProxyCommandFailure -from server import ServerInterface, SubsystemHandler, InteractiveQuery -from rsakey import RSAKey -from dsskey import DSSKey -from ecdsakey import ECDSAKey -from sftp import SFTPError, BaseSFTP -from sftp_client import SFTP, SFTPClient -from sftp_server import SFTPServer -from sftp_attr import SFTPAttributes -from sftp_handle import SFTPHandle -from sftp_si import SFTPServerInterface -from sftp_file import SFTPFile -from message import Message -from packet import Packetizer -from file import BufferedFile -from agent import Agent, AgentKey -from pkey import PKey -from hostkeys import HostKeys -from config import SSHConfig -from proxy import ProxyCommand +from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery +from paramiko.rsakey import RSAKey +from paramiko.dsskey import DSSKey +from paramiko.ecdsakey import ECDSAKey +from paramiko.sftp import SFTPError, BaseSFTP +from paramiko.sftp_client import SFTP, SFTPClient +from paramiko.sftp_server import SFTPServer +from paramiko.sftp_attr import SFTPAttributes +from paramiko.sftp_handle import SFTPHandle +from paramiko.sftp_si import SFTPServerInterface +from paramiko.sftp_file import SFTPFile +from paramiko.message import Message +from paramiko.packet import Packetizer +from paramiko.file import BufferedFile +from paramiko.agent import Agent, AgentKey +from paramiko.pkey import PKey +from paramiko.hostkeys import HostKeys +from paramiko.config import SSHConfig +from paramiko.proxy import ProxyCommand # fix module names for epydoc -for c in locals().values(): +for c in list(locals().values()): if issubclass(type(c), type) or type(c).__name__ == 'classobj': # classobj for exceptions :/ c.__module__ = __name__ del c -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 +from paramiko.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 -from sftp import SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, \ - SFTP_BAD_MESSAGE, SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED +from paramiko.sftp import SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, \ + SFTP_BAD_MESSAGE, SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED -from common import io_sleep +from paramiko.common import io_sleep __all__ = [ 'Transport', 'SSHClient', diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index f141b005..25d1a308 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -8,77 +8,81 @@ in jaraco.windows and asking the author to port the fixes back here. import ctypes import ctypes.wintypes -import __builtin__ +from paramiko.py3compat import u +try: + import builtins +except ImportError: + import __builtin__ as builtins ###################### # jaraco.windows.error def format_system_message(errno): - """ - Call FormatMessage with a system error number to retrieve - the descriptive error message. - """ - # first some flags used by FormatMessageW - ALLOCATE_BUFFER = 0x100 - ARGUMENT_ARRAY = 0x2000 - FROM_HMODULE = 0x800 - FROM_STRING = 0x400 - FROM_SYSTEM = 0x1000 - IGNORE_INSERTS = 0x200 - - # Let FormatMessageW allocate the buffer (we'll free it below) - # Also, let it know we want a system error message. - flags = ALLOCATE_BUFFER | FROM_SYSTEM - source = None - message_id = errno - language_id = 0 - result_buffer = ctypes.wintypes.LPWSTR() - buffer_size = 0 - arguments = None - bytes = ctypes.windll.kernel32.FormatMessageW( - flags, - source, - message_id, - language_id, - ctypes.byref(result_buffer), - buffer_size, - arguments, - ) - # note the following will cause an infinite loop if GetLastError - # repeatedly returns an error that cannot be formatted, although - # this should not happen. - handle_nonzero_success(bytes) - message = result_buffer.value - ctypes.windll.kernel32.LocalFree(result_buffer) - return message - - -class WindowsError(__builtin__.WindowsError): - "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" - - def __init__(self, value=None): - if value is None: - value = ctypes.windll.kernel32.GetLastError() - strerror = format_system_message(value) - super(WindowsError, self).__init__(value, strerror) - - @property - def message(self): - return self.strerror - - @property - def code(self): - return self.winerror - - def __str__(self): - return self.message - - def __repr__(self): - return '{self.__class__.__name__}({self.winerror})'.format(**vars()) + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + ARGUMENT_ARRAY = 0x2000 + FROM_HMODULE = 0x800 + FROM_STRING = 0x400 + FROM_SYSTEM = 0x1000 + IGNORE_INSERTS = 0x200 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + format_bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(format_bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(builtins.WindowsError): + "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + super(WindowsError, self).__init__(value, strerror) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return '{self.__class__.__name__}({self.winerror})'.format(**vars()) def handle_nonzero_success(result): - if result == 0: - raise WindowsError() + if result == 0: + raise WindowsError() ##################### @@ -86,12 +90,12 @@ def handle_nonzero_success(result): CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW CreateFileMapping.argtypes = [ - ctypes.wintypes.HANDLE, - ctypes.c_void_p, - ctypes.wintypes.DWORD, - ctypes.wintypes.DWORD, - ctypes.wintypes.DWORD, - ctypes.wintypes.LPWSTR, + ctypes.wintypes.HANDLE, + ctypes.c_void_p, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.LPWSTR, ] CreateFileMapping.restype = ctypes.wintypes.HANDLE @@ -99,171 +103,171 @@ MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile MapViewOfFile.restype = ctypes.wintypes.HANDLE class MemoryMap(object): - """ - A memory map object which can have security attributes overrideden. - """ - def __init__(self, name, length, security_attributes=None): - self.name = name - self.length = length - self.security_attributes = security_attributes - self.pos = 0 - - def __enter__(self): - p_SA = ( - ctypes.byref(self.security_attributes) - if self.security_attributes else None - ) - INVALID_HANDLE_VALUE = -1 - PAGE_READWRITE = 0x4 - FILE_MAP_WRITE = 0x2 - filemap = ctypes.windll.kernel32.CreateFileMappingW( - INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, - unicode(self.name)) - handle_nonzero_success(filemap) - if filemap == INVALID_HANDLE_VALUE: - raise Exception("Failed to create file mapping") - self.filemap = filemap - self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) - return self - - def seek(self, pos): - self.pos = pos - - def write(self, msg): - ctypes.windll.msvcrt.memcpy(self.view + self.pos, msg, len(msg)) - self.pos += len(msg) - - def read(self, n): - """ - Read n bytes from mapped view. - """ - out = ctypes.create_string_buffer(n) - ctypes.windll.msvcrt.memcpy(out, self.view + self.pos, n) - self.pos += n - return out.raw - - def __exit__(self, exc_type, exc_val, tb): - ctypes.windll.kernel32.UnmapViewOfFile(self.view) - ctypes.windll.kernel32.CloseHandle(self.filemap) + """ + A memory map object which can have security attributes overrideden. + """ + def __init__(self, name, length, security_attributes=None): + self.name = name + self.length = length + self.security_attributes = security_attributes + self.pos = 0 + + def __enter__(self): + p_SA = ( + ctypes.byref(self.security_attributes) + if self.security_attributes else None + ) + INVALID_HANDLE_VALUE = -1 + PAGE_READWRITE = 0x4 + FILE_MAP_WRITE = 0x2 + filemap = ctypes.windll.kernel32.CreateFileMappingW( + INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, + u(self.name)) + handle_nonzero_success(filemap) + if filemap == INVALID_HANDLE_VALUE: + raise Exception("Failed to create file mapping") + self.filemap = filemap + self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) + return self + + def seek(self, pos): + self.pos = pos + + def write(self, msg): + ctypes.windll.msvcrt.memcpy(self.view + self.pos, msg, len(msg)) + self.pos += len(msg) + + def read(self, n): + """ + Read n bytes from mapped view. + """ + out = ctypes.create_string_buffer(n) + ctypes.windll.msvcrt.memcpy(out, self.view + self.pos, n) + self.pos += n + return out.raw + + def __exit__(self, exc_type, exc_val, tb): + ctypes.windll.kernel32.UnmapViewOfFile(self.view) + ctypes.windll.kernel32.CloseHandle(self.filemap) ######################### # jaraco.windows.security class TokenInformationClass: - TokenUser = 1 + TokenUser = 1 class TOKEN_USER(ctypes.Structure): - num = 1 - _fields_ = [ - ('SID', ctypes.c_void_p), - ('ATTRIBUTES', ctypes.wintypes.DWORD), - ] + num = 1 + _fields_ = [ + ('SID', ctypes.c_void_p), + ('ATTRIBUTES', ctypes.wintypes.DWORD), + ] class SECURITY_DESCRIPTOR(ctypes.Structure): - """ - typedef struct _SECURITY_DESCRIPTOR - { - UCHAR Revision; - UCHAR Sbz1; - SECURITY_DESCRIPTOR_CONTROL Control; - PSID Owner; - PSID Group; - PACL Sacl; - PACL Dacl; - } SECURITY_DESCRIPTOR; - """ - SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT - REVISION = 1 - - _fields_ = [ - ('Revision', ctypes.c_ubyte), - ('Sbz1', ctypes.c_ubyte), - ('Control', SECURITY_DESCRIPTOR_CONTROL), - ('Owner', ctypes.c_void_p), - ('Group', ctypes.c_void_p), - ('Sacl', ctypes.c_void_p), - ('Dacl', ctypes.c_void_p), - ] + """ + typedef struct _SECURITY_DESCRIPTOR + { + UCHAR Revision; + UCHAR Sbz1; + SECURITY_DESCRIPTOR_CONTROL Control; + PSID Owner; + PSID Group; + PACL Sacl; + PACL Dacl; + } SECURITY_DESCRIPTOR; + """ + SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT + REVISION = 1 + + _fields_ = [ + ('Revision', ctypes.c_ubyte), + ('Sbz1', ctypes.c_ubyte), + ('Control', SECURITY_DESCRIPTOR_CONTROL), + ('Owner', ctypes.c_void_p), + ('Group', ctypes.c_void_p), + ('Sacl', ctypes.c_void_p), + ('Dacl', ctypes.c_void_p), + ] class SECURITY_ATTRIBUTES(ctypes.Structure): - """ - typedef struct _SECURITY_ATTRIBUTES { - DWORD nLength; - LPVOID lpSecurityDescriptor; - BOOL bInheritHandle; - } SECURITY_ATTRIBUTES; - """ - _fields_ = [ - ('nLength', ctypes.wintypes.DWORD), - ('lpSecurityDescriptor', ctypes.c_void_p), - ('bInheritHandle', ctypes.wintypes.BOOL), - ] - - def __init__(self, *args, **kwargs): - super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) - self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) - - def _get_descriptor(self): - return self._descriptor - def _set_descriptor(self, descriptor): - self._descriptor = descriptor - self.lpSecurityDescriptor = ctypes.addressof(descriptor) - descriptor = property(_get_descriptor, _set_descriptor) + """ + typedef struct _SECURITY_ATTRIBUTES { + DWORD nLength; + LPVOID lpSecurityDescriptor; + BOOL bInheritHandle; + } SECURITY_ATTRIBUTES; + """ + _fields_ = [ + ('nLength', ctypes.wintypes.DWORD), + ('lpSecurityDescriptor', ctypes.c_void_p), + ('bInheritHandle', ctypes.wintypes.BOOL), + ] + + def __init__(self, *args, **kwargs): + super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) + self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) + + def _get_descriptor(self): + return self._descriptor + def _set_descriptor(self, descriptor): + self._descriptor = descriptor + self.lpSecurityDescriptor = ctypes.addressof(descriptor) + descriptor = property(_get_descriptor, _set_descriptor) def GetTokenInformation(token, information_class): - """ - Given a token, get the token information for it. - """ - data_size = ctypes.wintypes.DWORD() - ctypes.windll.advapi32.GetTokenInformation(token, information_class.num, - 0, 0, ctypes.byref(data_size)) - data = ctypes.create_string_buffer(data_size.value) - handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token, - information_class.num, - ctypes.byref(data), ctypes.sizeof(data), - ctypes.byref(data_size))) - return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents + """ + Given a token, get the token information for it. + """ + data_size = ctypes.wintypes.DWORD() + ctypes.windll.advapi32.GetTokenInformation(token, information_class.num, + 0, 0, ctypes.byref(data_size)) + data = ctypes.create_string_buffer(data_size.value) + handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token, + information_class.num, + ctypes.byref(data), ctypes.sizeof(data), + ctypes.byref(data_size))) + return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents class TokenAccess: - TOKEN_QUERY = 0x8 + TOKEN_QUERY = 0x8 def OpenProcessToken(proc_handle, access): - result = ctypes.wintypes.HANDLE() - proc_handle = ctypes.wintypes.HANDLE(proc_handle) - handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken( - proc_handle, access, ctypes.byref(result))) - return result + result = ctypes.wintypes.HANDLE() + proc_handle = ctypes.wintypes.HANDLE(proc_handle) + handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken( + proc_handle, access, ctypes.byref(result))) + return result def get_current_user(): - """ - Return a TOKEN_USER for the owner of this process. - """ - process = OpenProcessToken( - ctypes.windll.kernel32.GetCurrentProcess(), - TokenAccess.TOKEN_QUERY, - ) - return GetTokenInformation(process, TOKEN_USER) + """ + Return a TOKEN_USER for the owner of this process. + """ + process = OpenProcessToken( + ctypes.windll.kernel32.GetCurrentProcess(), + TokenAccess.TOKEN_QUERY, + ) + return GetTokenInformation(process, TOKEN_USER) def get_security_attributes_for_user(user=None): - """ - Return a SECURITY_ATTRIBUTES structure with the SID set to the - specified user (uses current user if none is specified). - """ - if user is None: - user = get_current_user() - - assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" - - SD = SECURITY_DESCRIPTOR() - SA = SECURITY_ATTRIBUTES() - # by attaching the actual security descriptor, it will be garbage- - # collected with the security attributes - SA.descriptor = SD - SA.bInheritHandle = 1 - - ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), - SECURITY_DESCRIPTOR.REVISION) - ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), - user.SID, 0) - return SA + """ + Return a SECURITY_ATTRIBUTES structure with the SID set to the + specified user (uses current user if none is specified). + """ + if user is None: + user = get_current_user() + + assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" + + SD = SECURITY_DESCRIPTOR() + SA = SECURITY_ATTRIBUTES() + # by attaching the actual security descriptor, it will be garbage- + # collected with the security attributes + SA.descriptor = SD + SA.bInheritHandle = 1 + + ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), + SECURITY_DESCRIPTOR.REVISION) + ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), + user.SID, 0) + return SA diff --git a/paramiko/agent.py b/paramiko/agent.py index 23a5a2e4..8f2a486a 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -34,11 +34,14 @@ from paramiko.ssh_exception import SSHException from paramiko.message import Message from paramiko.pkey import PKey from paramiko.channel import Channel -from paramiko.common import io_sleep +from paramiko.common import * from paramiko.util import retry_on_signal -SSH2_AGENTC_REQUEST_IDENTITIES, SSH2_AGENT_IDENTITIES_ANSWER, \ - SSH2_AGENTC_SIGN_REQUEST, SSH2_AGENT_SIGN_RESPONSE = range(11, 15) +cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11) +SSH2_AGENT_IDENTITIES_ANSWER = 12 +cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13) +SSH2_AGENT_SIGN_RESPONSE = 14 + class AgentSSH(object): """ @@ -68,12 +71,12 @@ class AgentSSH(object): def _connect(self, conn): self._conn = conn - ptype, result = self._send_message(chr(SSH2_AGENTC_REQUEST_IDENTITIES)) + ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES) if ptype != SSH2_AGENT_IDENTITIES_ANSWER: raise SSHException('could not get keys from ssh-agent') keys = [] for i in range(result.get_int()): - keys.append(AgentKey(self, result.get_string())) + keys.append(AgentKey(self, result.get_binary())) result.get_string() self._keys = tuple(keys) @@ -83,7 +86,7 @@ class AgentSSH(object): self._keys = () def _send_message(self, msg): - msg = str(msg) + msg = asbytes(msg) self._conn.send(struct.pack('>I', len(msg)) + msg) l = self._read_all(4) msg = Message(self._read_all(struct.unpack('>I', l)[0])) @@ -215,7 +218,7 @@ class AgentClientProxy(object): # probably a dangling env var: the ssh agent is gone return elif sys.platform == 'win32': - import win_pageant + import paramiko.win_pageant as win_pageant if win_pageant.can_talk_to_agent(): conn = win_pageant.PageantConnection() else: @@ -334,7 +337,7 @@ class Agent(AgentSSH): # probably a dangling env var: the ssh agent is gone return elif sys.platform == 'win32': - import win_pageant + from . import win_pageant if win_pageant.can_talk_to_agent(): conn = win_pageant.PageantConnection() else: @@ -360,21 +363,24 @@ class AgentKey(PKey): def __init__(self, agent, blob): self.agent = agent self.blob = blob - self.name = Message(blob).get_string() + self.name = Message(blob).get_text() - def __str__(self): + def asbytes(self): return self.blob + def __str__(self): + return self.asbytes() + def get_name(self): return self.name def sign_ssh_data(self, rng, data): msg = Message() - msg.add_byte(chr(SSH2_AGENTC_SIGN_REQUEST)) + msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_string(self.blob) msg.add_string(data) msg.add_int(0) ptype, result = self.agent._send_message(msg) if ptype != SSH2_AGENT_SIGN_RESPONSE: raise SSHException('key cannot be used for signing') - return result.get_string() + return result.get_binary() diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index acb7c8b8..5046cac5 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -93,7 +93,7 @@ class AuthHandler (object): self._request_auth() finally: self.transport.lock.release() - + def auth_interactive(self, username, handler, event, submethods=''): """ response_list = handler(title, instructions, prompt_list) @@ -108,7 +108,7 @@ class AuthHandler (object): self._request_auth() finally: self.transport.lock.release() - + def abort(self): if self.auth_event is not None: self.auth_event.set() @@ -119,13 +119,13 @@ class AuthHandler (object): def _request_auth(self): m = Message() - m.add_byte(chr(MSG_SERVICE_REQUEST)) + m.add_byte(cMSG_SERVICE_REQUEST) m.add_string('ssh-userauth') self.transport._send_message(m) def _disconnect_service_not_available(self): m = Message() - m.add_byte(chr(MSG_DISCONNECT)) + m.add_byte(cMSG_DISCONNECT) m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) m.add_string('Service not available') m.add_string('en') @@ -134,7 +134,7 @@ class AuthHandler (object): def _disconnect_no_more_auth(self): m = Message() - m.add_byte(chr(MSG_DISCONNECT)) + m.add_byte(cMSG_DISCONNECT) m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) m.add_string('No more auth methods available') m.add_string('en') @@ -144,14 +144,14 @@ class AuthHandler (object): def _get_session_blob(self, key, service, username): m = Message() m.add_string(self.transport.session_id) - m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_byte(cMSG_USERAUTH_REQUEST) m.add_string(username) m.add_string(service) m.add_string('publickey') m.add_boolean(1) m.add_string(key.get_name()) - m.add_string(str(key)) - return str(m) + m.add_string(key) + return m.asbytes() def wait_for_response(self, event): while True: @@ -175,11 +175,11 @@ class AuthHandler (object): return [] def _parse_service_request(self, m): - service = m.get_string() + service = m.get_text() if self.transport.server_mode and (service == 'ssh-userauth'): # accepted m = Message() - m.add_byte(chr(MSG_SERVICE_ACCEPT)) + m.add_byte(cMSG_SERVICE_ACCEPT) m.add_string(service) self.transport._send_message(m) return @@ -187,27 +187,25 @@ class AuthHandler (object): self._disconnect_service_not_available() def _parse_service_accept(self, m): - service = m.get_string() + service = m.get_text() if service == 'ssh-userauth': self.transport._log(DEBUG, 'userauth is OK') m = Message() - m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_byte(cMSG_USERAUTH_REQUEST) m.add_string(self.username) m.add_string('ssh-connection') m.add_string(self.auth_method) if self.auth_method == 'password': m.add_boolean(False) - password = self.password - if isinstance(password, unicode): - password = password.encode('UTF-8') + password = bytestring(self.password) m.add_string(password) elif self.auth_method == 'publickey': m.add_boolean(True) m.add_string(self.private_key.get_name()) - m.add_string(str(self.private_key)) + m.add_string(self.private_key) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) sig = self.private_key.sign_ssh_data(self.transport.rng, blob) - m.add_string(str(sig)) + m.add_string(sig) elif self.auth_method == 'keyboard-interactive': m.add_string('') m.add_string(self.submethods) @@ -224,11 +222,11 @@ class AuthHandler (object): m = Message() if result == AUTH_SUCCESSFUL: self.transport._log(INFO, 'Auth granted (%s).' % method) - m.add_byte(chr(MSG_USERAUTH_SUCCESS)) + m.add_byte(cMSG_USERAUTH_SUCCESS) self.authenticated = True else: self.transport._log(INFO, 'Auth rejected (%s).' % method) - m.add_byte(chr(MSG_USERAUTH_FAILURE)) + m.add_byte(cMSG_USERAUTH_FAILURE) m.add_string(self.transport.server_object.get_allowed_auths(username)) if result == AUTH_PARTIALLY_SUCCESSFUL: m.add_boolean(1) @@ -244,10 +242,10 @@ class AuthHandler (object): def _interactive_query(self, q): # make interactive query instead of response m = Message() - m.add_byte(chr(MSG_USERAUTH_INFO_REQUEST)) + m.add_byte(cMSG_USERAUTH_INFO_REQUEST) m.add_string(q.name) m.add_string(q.instructions) - m.add_string('') + m.add_string(bytes()) m.add_int(len(q.prompts)) for p in q.prompts: m.add_string(p[0]) @@ -258,7 +256,7 @@ class AuthHandler (object): if not self.transport.server_mode: # er, uh... what? m = Message() - m.add_byte(chr(MSG_USERAUTH_FAILURE)) + m.add_byte(cMSG_USERAUTH_FAILURE) m.add_string('none') m.add_boolean(0) self.transport._send_message(m) @@ -266,9 +264,9 @@ class AuthHandler (object): if self.authenticated: # ignore return - username = m.get_string() - service = m.get_string() - method = m.get_string() + username = m.get_text() + service = m.get_text() + method = m.get_text() self.transport._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username)) if service != 'ssh-connection': self._disconnect_service_not_available() @@ -283,7 +281,7 @@ class AuthHandler (object): result = self.transport.server_object.check_auth_none(username) elif method == 'password': changereq = m.get_boolean() - password = m.get_string() + password = m.get_binary() try: password = password.decode('UTF-8') except UnicodeError: @@ -294,7 +292,7 @@ class AuthHandler (object): # always treated as failure, since we don't support changing passwords, but collect # the list of valid auth types from the callback anyway self.transport._log(DEBUG, 'Auth request to change passwords (rejected)') - newpassword = m.get_string() + newpassword = m.get_binary() try: newpassword = newpassword.decode('UTF-8', 'replace') except UnicodeError: @@ -304,11 +302,11 @@ class AuthHandler (object): result = self.transport.server_object.check_auth_password(username, password) elif method == 'publickey': sig_attached = m.get_boolean() - keytype = m.get_string() - keyblob = m.get_string() + keytype = m.get_text() + keyblob = m.get_binary() try: key = self.transport._key_info[keytype](Message(keyblob)) - except SSHException, e: + except SSHException as e: self.transport._log(INFO, 'Auth rejected: public key: %s' % str(e)) key = None except: @@ -325,12 +323,12 @@ class AuthHandler (object): # client wants to know if this key is acceptable, before it # signs anything... send special "ok" message m = Message() - m.add_byte(chr(MSG_USERAUTH_PK_OK)) + m.add_byte(cMSG_USERAUTH_PK_OK) m.add_string(keytype) m.add_string(keyblob) self.transport._send_message(m) return - sig = Message(m.get_string()) + sig = Message(m.get_binary()) blob = self._get_session_blob(key, service, username) if not key.verify_ssh_sig(blob, sig): self.transport._log(INFO, 'Auth rejected: invalid signature') @@ -376,23 +374,23 @@ class AuthHandler (object): def _parse_userauth_banner(self, m): banner = m.get_string() lang = m.get_string() - self.transport._log(INFO, 'Auth banner: ' + banner) + self.transport._log(INFO, 'Auth banner: %s' % banner) # who cares. def _parse_userauth_info_request(self, m): if self.auth_method != 'keyboard-interactive': raise SSHException('Illegal info request from server') - title = m.get_string() - instructions = m.get_string() - m.get_string() # lang + title = m.get_text() + instructions = m.get_text() + m.get_binary() # lang prompts = m.get_int() prompt_list = [] for i in range(prompts): - prompt_list.append((m.get_string(), m.get_boolean())) + prompt_list.append((m.get_text(), m.get_boolean())) response_list = self.interactive_handler(title, instructions, prompt_list) m = Message() - m.add_byte(chr(MSG_USERAUTH_INFO_RESPONSE)) + m.add_byte(cMSG_USERAUTH_INFO_RESPONSE) m.add_int(len(response_list)) for r in response_list: m.add_string(r) @@ -404,14 +402,14 @@ class AuthHandler (object): n = m.get_int() responses = [] for i in range(n): - responses.append(m.get_string()) + responses.append(m.get_text()) result = self.transport.server_object.check_auth_interactive_response(responses) if isinstance(type(result), InteractiveQuery): # make interactive query instead of response self._interactive_query(result) return self._send_auth_result(self.auth_username, 'keyboard-interactive', result) - + _handler_table = { MSG_SERVICE_REQUEST: _parse_service_request, diff --git a/paramiko/ber.py b/paramiko/ber.py index 3941581c..c4f35210 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -17,7 +17,8 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -import util +import paramiko.util as util +from paramiko.common import * class BERException (Exception): @@ -29,13 +30,16 @@ class BER(object): Robey's tiny little attempt at a BER decoder. """ - def __init__(self, content=''): - self.content = content + def __init__(self, content=bytes()): + self.content = b(content) self.idx = 0 - def __str__(self): + def asbytes(self): return self.content + def __str__(self): + return self.asbytes() + def __repr__(self): return 'BER(\'' + repr(self.content) + '\')' @@ -45,13 +49,13 @@ class BER(object): def decode_next(self): if self.idx >= len(self.content): return None - ident = ord(self.content[self.idx]) + ident = byte_ord(self.content[self.idx]) self.idx += 1 if (ident & 31) == 31: # identifier > 30 ident = 0 while self.idx < len(self.content): - t = ord(self.content[self.idx]) + t = byte_ord(self.content[self.idx]) self.idx += 1 ident = (ident << 7) | (t & 0x7f) if not (t & 0x80): @@ -59,7 +63,7 @@ class BER(object): if self.idx >= len(self.content): return None # now fetch length - size = ord(self.content[self.idx]) + size = byte_ord(self.content[self.idx]) self.idx += 1 if size & 0x80: # more complimicated... @@ -98,20 +102,20 @@ class BER(object): def encode_tlv(self, ident, val): # no need to support ident > 31 here - self.content += chr(ident) + self.content += byte_chr(ident) if len(val) > 0x7f: lenstr = util.deflate_long(len(val)) - self.content += chr(0x80 + len(lenstr)) + lenstr + self.content += byte_chr(0x80 + len(lenstr)) + lenstr else: - self.content += chr(len(val)) + self.content += byte_chr(len(val)) self.content += val def encode(self, x): if type(x) is bool: if x: - self.encode_tlv(1, '\xff') + self.encode_tlv(1, max_byte) else: - self.encode_tlv(1, '\x00') + self.encode_tlv(1, zero_byte) elif (type(x) is int) or (type(x) is long): self.encode_tlv(2, util.deflate_long(x)) elif type(x) is str: @@ -125,5 +129,5 @@ class BER(object): b = BER() for item in data: b.encode(item) - return str(b) + return b.asbytes() encode_sequence = staticmethod(encode_sequence) diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py index 4ef5cf74..02408a20 100644 --- a/paramiko/buffered_pipe.py +++ b/paramiko/buffered_pipe.py @@ -25,6 +25,7 @@ read operations are blocking and can have a timeout set. import array import threading import time +from paramiko.common import * class PipeTimeout (IOError): @@ -48,6 +49,20 @@ class BufferedPipe (object): self._buffer = array.array('B') self._closed = False + if PY2: + def _buffer_frombytes(self, data): + self._buffer.fromstring(data) + + def _buffer_tobytes(self, limit=None): + return self._buffer[:limit].tostring() + else: + def _buffer_frombytes(self, data): + self._buffer.frombytes(data) + + def _buffer_tobytes(self, limit=None): + return self._buffer[:limit].tobytes() + + def set_event(self, event): """ Set an event on this buffer. When data is ready to be read (or the @@ -75,7 +90,7 @@ class BufferedPipe (object): try: if self._event is not None: self._event.set() - self._buffer.fromstring(data) + self._buffer_frombytes(b(data)) self._cv.notifyAll() finally: self._lock.release() @@ -121,7 +136,7 @@ class BufferedPipe (object): @raise PipeTimeout: if a timeout was specified and no data was ready before that timeout """ - out = '' + out = bytes() self._lock.acquire() try: if len(self._buffer) == 0: @@ -142,12 +157,12 @@ class BufferedPipe (object): # something's in the buffer and we have the lock! if len(self._buffer) <= nbytes: - out = self._buffer.tostring() + out = self._buffer_tobytes() del self._buffer[:] if (self._event is not None) and not self._closed: self._event.clear() else: - out = self._buffer[:nbytes].tostring() + out = self._buffer_tobytes(nbytes) del self._buffer[:nbytes] finally: self._lock.release() @@ -163,7 +178,7 @@ class BufferedPipe (object): """ self._lock.acquire() try: - out = self._buffer.tostring() + out = self._buffer_tobytes() del self._buffer[:] if (self._event is not None) and not self._closed: self._event.clear() diff --git a/paramiko/channel.py b/paramiko/channel.py index d1e6333c..9980fcea 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -148,7 +148,7 @@ class Channel (object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('pty-req') m.add_boolean(True) @@ -157,7 +157,7 @@ class Channel (object): m.add_int(height) m.add_int(width_pixels) m.add_int(height_pixels) - m.add_string('') + m.add_string(bytes()) self._event_pending() self.transport._send_user_message(m) self._wait_for_event() @@ -181,7 +181,7 @@ class Channel (object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('shell') m.add_boolean(1) @@ -208,7 +208,7 @@ class Channel (object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('exec') m.add_boolean(True) @@ -235,7 +235,7 @@ class Channel (object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('subsystem') m.add_boolean(True) @@ -264,7 +264,7 @@ class Channel (object): if self.closed or self.eof_received or self.eof_sent or not self.active: raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('window-change') m.add_boolean(False) @@ -319,7 +319,7 @@ class Channel (object): # in many cases, the channel will not still be open here. # that's fine. m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('exit-status') m.add_boolean(False) @@ -375,7 +375,7 @@ class Channel (object): auth_cookie = binascii.hexlify(self.transport.rng.read(16)) m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('x11-req') m.add_boolean(True) @@ -406,7 +406,7 @@ class Channel (object): raise SSHException('Channel is not open') m = Message() - m.add_byte(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('auth-agent-req@openssh.com') m.add_boolean(False) @@ -477,7 +477,7 @@ class Channel (object): @since: 1.1 """ - data = '' + data = bytes() self.lock.acquire() try: old = self.combine_stderr @@ -491,7 +491,7 @@ class Channel (object): self._feed(data) return old - + ### socket API @@ -615,14 +615,14 @@ class Channel (object): """ try: out = self.in_buffer.read(nbytes, self.timeout) - except PipeTimeout, e: + except PipeTimeout: raise socket.timeout() ack = self._check_add_window(len(out)) # no need to hold the channel lock when sending this if ack > 0: m = Message() - m.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST)) + m.add_byte(cMSG_CHANNEL_WINDOW_ADJUST) m.add_int(self.remote_chanid) m.add_int(ack) self.transport._send_user_message(m) @@ -665,14 +665,14 @@ class Channel (object): """ try: out = self.in_stderr_buffer.read(nbytes, self.timeout) - except PipeTimeout, e: + except PipeTimeout: raise socket.timeout() ack = self._check_add_window(len(out)) # no need to hold the channel lock when sending this if ack > 0: m = Message() - m.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST)) + m.add_byte(cMSG_CHANNEL_WINDOW_ADJUST) m.add_int(self.remote_chanid) m.add_int(ack) self.transport._send_user_message(m) @@ -724,7 +724,7 @@ class Channel (object): # eof or similar return 0 m = Message() - m.add_byte(chr(MSG_CHANNEL_DATA)) + m.add_byte(cMSG_CHANNEL_DATA) m.add_int(self.remote_chanid) m.add_string(s[:size]) finally: @@ -761,7 +761,7 @@ class Channel (object): # eof or similar return 0 m = Message() - m.add_byte(chr(MSG_CHANNEL_EXTENDED_DATA)) + m.add_byte(cMSG_CHANNEL_EXTENDED_DATA) m.add_int(self.remote_chanid) m.add_int(1) m.add_string(s[:size]) @@ -969,16 +969,16 @@ class Channel (object): self.transport._send_user_message(m) def _feed(self, m): - if type(m) is str: + if isinstance(m, bytes_types): # passed from _feed_extended s = m else: - s = m.get_string() + s = m.get_binary() self.in_buffer.feed(s) def _feed_extended(self, m): code = m.get_int() - s = m.get_string() + s = m.get_binary() if code != 1: self._log(ERROR, 'unknown extended_data type %d; discarding' % code) return @@ -999,7 +999,7 @@ class Channel (object): self.lock.release() def _handle_request(self, m): - key = m.get_string() + key = m.get_text() want_reply = m.get_boolean() server = self.transport.server_object ok = False @@ -1035,13 +1035,13 @@ class Channel (object): else: ok = server.check_channel_env_request(self, name, value) elif key == 'exec': - cmd = m.get_string() + cmd = m.get_text() if server is None: ok = False else: ok = server.check_channel_exec_request(self, cmd) elif key == 'subsystem': - name = m.get_string() + name = m.get_text() if server is None: ok = False else: @@ -1058,8 +1058,8 @@ class Channel (object): pixelheight) elif key == 'x11-req': single_connection = m.get_boolean() - auth_proto = m.get_string() - auth_cookie = m.get_string() + auth_proto = m.get_text() + auth_cookie = m.get_binary() screen_number = m.get_int() if server is None: ok = False @@ -1077,9 +1077,9 @@ class Channel (object): if want_reply: m = Message() if ok: - m.add_byte(chr(MSG_CHANNEL_SUCCESS)) + m.add_byte(cMSG_CHANNEL_SUCCESS) else: - m.add_byte(chr(MSG_CHANNEL_FAILURE)) + m.add_byte(cMSG_CHANNEL_FAILURE) m.add_int(self.remote_chanid) self.transport._send_user_message(m) @@ -1145,7 +1145,7 @@ class Channel (object): if self.eof_sent: return None m = Message() - m.add_byte(chr(MSG_CHANNEL_EOF)) + m.add_byte(cMSG_CHANNEL_EOF) m.add_int(self.remote_chanid) self.eof_sent = True self._log(DEBUG, 'EOF sent (%s)', self._name) @@ -1157,7 +1157,7 @@ class Channel (object): return None, None m1 = self._send_eof() m2 = Message() - m2.add_byte(chr(MSG_CHANNEL_CLOSE)) + m2.add_byte(cMSG_CHANNEL_CLOSE) m2.add_int(self.remote_chanid) self._set_closed() # can't unlink from the Transport yet -- the remote side may still diff --git a/paramiko/client.py b/paramiko/client.py index be896091..40ef7f0b 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -192,11 +192,10 @@ class SSHClient (object): if self._host_keys_filename is not None: self.load_host_keys(self._host_keys_filename) - f = open(filename, 'w') - for hostname, keys in self._host_keys.iteritems(): - for keytype, key in keys.iteritems(): - f.write('%s %s %s\n' % (hostname, keytype, key.get_base64())) - f.close() + with open(filename, 'w') as f: + for hostname, keys in self._host_keys.items(): + for keytype, key in keys.items(): + f.write('%s %s %s\n' % (hostname, keytype, key.get_base64())) def get_host_keys(self): """ @@ -335,7 +334,7 @@ class SSHClient (object): if key_filename is None: key_filenames = [] - elif isinstance(key_filename, (str, unicode)): + elif isinstance(key_filename, string_types): key_filenames = [ key_filename ] else: key_filenames = key_filename @@ -378,12 +377,12 @@ class SSHClient (object): chan.settimeout(timeout) chan.exec_command(command) stdin = chan.makefile('wb', bufsize) - stdout = chan.makefile('rb', bufsize) - stderr = chan.makefile_stderr('rb', bufsize) + stdout = chan.makefile('r', bufsize) + stderr = chan.makefile_stderr('r', bufsize) return stdin, stdout, stderr def invoke_shell(self, term='vt100', width=80, height=24, width_pixels=0, - height_pixels=0): + height_pixels=0): """ Start an interactive shell session on the SSH server. A new L{Channel} is opened and connected to a pseudo-terminal using the requested @@ -452,7 +451,7 @@ class SSHClient (object): two_factor = (allowed_types == ['password']) if not two_factor: return - except SSHException, e: + except SSHException as e: saved_exception = e if not two_factor: @@ -466,7 +465,7 @@ class SSHClient (object): if not two_factor: return break - except SSHException, e: + except SSHException as e: saved_exception = e if not two_factor and allow_agent: @@ -482,7 +481,7 @@ class SSHClient (object): if not two_factor: return break - except SSHException, e: + except SSHException as e: saved_exception = e if not two_factor: @@ -514,17 +513,15 @@ class SSHClient (object): if not two_factor: return break - except SSHException, e: - saved_exception = e - except IOError, e: + except (SSHException, IOError) as e: saved_exception = e if password is not None: try: self._transport.auth_password(username, password) return - except SSHException, e: - saved_exception = e + except SSHException: + saved_exception = sys.exc_info()[1] elif two_factor: raise SSHException('Two-factor authentication requires a password') diff --git a/paramiko/common.py b/paramiko/common.py index 3d7ca588..e30df73a 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -19,12 +19,13 @@ """ Common constants and global variables. """ +from paramiko.py3compat import * MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, MSG_SERVICE_REQUEST, \ MSG_SERVICE_ACCEPT = range(1, 7) MSG_KEXINIT, MSG_NEWKEYS = range(20, 22) MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ - MSG_USERAUTH_BANNER = range(50, 54) + MSG_USERAUTH_BANNER = range(50, 54) MSG_USERAUTH_PK_OK = 60 MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) @@ -33,6 +34,10 @@ MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \ MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) +for key in list(locals().keys()): + if key.startswith('MSG_'): + locals()['c' + key] = byte_chr(locals()[key]) +del key # for debugging: MSG_NAMES = { @@ -69,7 +74,7 @@ MSG_NAMES = { MSG_CHANNEL_REQUEST: 'channel-request', MSG_CHANNEL_SUCCESS: 'channel-success', MSG_CHANNEL_FAILURE: 'channel-failure' - } +} # authentication request return codes: @@ -118,6 +123,42 @@ else: import logging PY22 = False +zero_byte = byte_chr(0) +one_byte = byte_chr(1) +four_byte = byte_chr(4) +max_byte = byte_chr(0xff) +cr_byte = byte_chr(13) +linefeed_byte = byte_chr(10) +crlf = cr_byte + linefeed_byte + +if PY2: + cr_byte_value = cr_byte + linefeed_byte_value = linefeed_byte +else: + cr_byte_value = 13 + linefeed_byte_value = 10 + + +def asbytes(s): + if not isinstance(s, bytes_types): + if isinstance(s, string_types): + s = b(s) + else: + try: + s = s.asbytes() + except Exception: + raise Exception('Unknown type') + return s + +xffffffff = long(0xffffffff) +x80000000 = long(0x80000000) +o666 = 438 +o660 = 432 +o644 = 420 +o600 = 384 +o777 = 511 +o700 = 448 +o70 = 56 DEBUG = logging.DEBUG INFO = logging.INFO diff --git a/paramiko/config.py b/paramiko/config.py index 1705de76..51415be1 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -173,7 +173,7 @@ class SSHConfig (object): ret = {} for match in matches: - for key, value in match['config'].iteritems(): + for key, value in match['config'].items(): if key not in ret: # Create a copy of the original value, # else it will reference the original list diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index f6ecb2a7..a02a4ddb 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -56,7 +56,7 @@ class DSSKey (PKey): else: if msg is None: raise SSHException('Key object may not be empty') - if msg.get_string() != 'ssh-dss': + if msg.get_text() != 'ssh-dss': raise SSHException('Invalid key') self.p = msg.get_mpint() self.q = msg.get_mpint() @@ -64,14 +64,17 @@ class DSSKey (PKey): self.y = msg.get_mpint() self.size = util.bit_length(self.p) - def __str__(self): + def asbytes(self): m = Message() m.add_string('ssh-dss') m.add_mpint(self.p) m.add_mpint(self.q) m.add_mpint(self.g) m.add_mpint(self.y) - return str(m) + return m.asbytes() + + def __str__(self): + return self.asbytes() def __hash__(self): h = hash(self.get_name()) @@ -107,21 +110,21 @@ class DSSKey (PKey): rstr = util.deflate_long(r, 0) sstr = util.deflate_long(s, 0) if len(rstr) < 20: - rstr = '\x00' * (20 - len(rstr)) + rstr + rstr = zero_byte * (20 - len(rstr)) + rstr if len(sstr) < 20: - sstr = '\x00' * (20 - len(sstr)) + sstr + sstr = zero_byte * (20 - len(sstr)) + sstr m.add_string(rstr + sstr) return m def verify_ssh_sig(self, data, msg): - if len(str(msg)) == 40: + if len(msg.asbytes()) == 40: # spies.com bug: signature has no header - sig = str(msg) + sig = msg.asbytes() else: - kind = msg.get_string() + kind = msg.get_text() if kind != 'ssh-dss': return 0 - sig = msg.get_string() + sig = msg.get_binary() # pull out (r, s) which are NOT encoded as mpints sigR = util.inflate_long(sig[:20], 1) @@ -140,7 +143,7 @@ class DSSKey (PKey): b.encode(keylist) except BERException: raise SSHException('Unable to create ber encoding of key') - return str(b) + return b.asbytes() def write_private_key_file(self, filename, password=None): self._write_private_key_file('DSA', filename, self._encode_key(), password) @@ -184,8 +187,8 @@ class DSSKey (PKey): # DSAPrivateKey = { version = 0, p, q, g, y, x } try: keylist = BER(data).decode() - except BERException, x: - raise SSHException('Unable to parse key file: ' + str(x)) + except BERException as e: + raise SSHException('Unable to parse key file: ' + str(e)) if (type(keylist) is not list) or (len(keylist) < 6) or (keylist[0] != 0): raise SSHException('not a valid DSA private key file (bad ber encoding)') self.p = keylist[1] diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index ac840ab7..3ecf0a58 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -56,30 +56,33 @@ class ECDSAKey (PKey): else: if msg is None: raise SSHException('Key object may not be empty') - if msg.get_string() != 'ecdsa-sha2-nistp256': + if msg.get_text() != 'ecdsa-sha2-nistp256': raise SSHException('Invalid key') - curvename = msg.get_string() + curvename = msg.get_text() if curvename != 'nistp256': raise SSHException("Can't handle curve of type %s" % curvename) - pointinfo = msg.get_string() - if pointinfo[0] != "\x04": - raise SSHException('Point compression is being used: %s'% + pointinfo = msg.get_binary() + if pointinfo[0:1] != four_byte: + raise SSHException('Point compression is being used: %s' % binascii.hexlify(pointinfo)) self.verifying_key = VerifyingKey.from_string(pointinfo[1:], - curve=curves.NIST256p) + curve=curves.NIST256p) self.size = 256 - def __str__(self): + def asbytes(self): key = self.verifying_key m = Message() m.add_string('ecdsa-sha2-nistp256') m.add_string('nistp256') - point_str = "\x04" + key.to_string() + point_str = four_byte + key.to_string() m.add_string(point_str) - return str(m) + return m.asbytes() + + def __str__(self): + return self.asbytes() def __hash__(self): h = hash(self.get_name()) @@ -106,9 +109,9 @@ class ECDSAKey (PKey): return m def verify_ssh_sig(self, data, msg): - if msg.get_string() != 'ecdsa-sha2-nistp256': + if msg.get_text() != 'ecdsa-sha2-nistp256': return False - sig = msg.get_string() + sig = msg.get_binary() # verify the signature by SHA'ing the data and encrypting it # using the public key. @@ -154,14 +157,13 @@ class ECDSAKey (PKey): data = self._read_private_key('EC', file_obj, password) self._decode_key(data) - ALLOWED_PADDINGS = ['\x01', '\x02\x02', '\x03\x03\x03', '\x04\x04\x04\x04', - '\x05\x05\x05\x05\x05', '\x06\x06\x06\x06\x06\x06', - '\x07\x07\x07\x07\x07\x07\x07'] + ALLOWED_PADDINGS = [one_byte, byte_chr(2) * 2, byte_chr(3) * 3, byte_chr(4) * 4, + byte_chr(5) * 5, byte_chr(6) * 6, byte_chr(7) * 7] def _decode_key(self, data): s, padding = der.remove_sequence(data) if padding: if padding not in self.ALLOWED_PADDINGS: - raise ValueError, "weird padding: %s" % (binascii.hexlify(empty)) + raise ValueError("weird padding: %s" % u(binascii.hexlify(data))) data = data[:-len(padding)] key = SigningKey.from_der(data) self.signing_key = key @@ -172,7 +174,7 @@ class ECDSAKey (PKey): msg = Message() msg.add_mpint(r) msg.add_mpint(s) - return str(msg) + return msg.asbytes() def _sigdecode(self, sig, order): msg = Message(sig) diff --git a/paramiko/file.py b/paramiko/file.py index 5fd81cfe..c9f191a4 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -20,7 +20,7 @@ BufferedFile. """ -from cStringIO import StringIO +from paramiko.common import * class BufferedFile (object): @@ -47,8 +47,8 @@ class BufferedFile (object): self.newlines = None self._flags = 0 self._bufsize = self._DEFAULT_BUFSIZE - self._wbuffer = StringIO() - self._rbuffer = '' + self._wbuffer = BytesIO() + self._rbuffer = bytes() self._at_trailing_cr = False self._closed = False # pos - position within the file, according to the user @@ -89,24 +89,41 @@ class BufferedFile (object): buffering is not turned on. """ self._write_all(self._wbuffer.getvalue()) - self._wbuffer = StringIO() + self._wbuffer = BytesIO() return - def next(self): - """ - Returns the next line from the input, or raises L{StopIteration} when - EOF is hit. Unlike python file objects, it's okay to mix calls to - C{next} and L{readline}. + if PY2: + def next(self): + """ + Returns the next line from the input, or raises L{StopIteration} when + EOF is hit. Unlike python file objects, it's okay to mix calls to + C{next} and L{readline}. - @raise StopIteration: when the end of the file is reached. + @raise StopIteration: when the end of the file is reached. - @return: a line read from the file. - @rtype: str - """ - line = self.readline() - if not line: - raise StopIteration - return line + @return: a line read from the file. + @rtype: str + """ + line = self.readline() + if not line: + raise StopIteration + return line + else: + def __next__(self): + """ + Returns the next line from the input, or raises L{StopIteration} when + EOF is hit. Unlike python file objects, it's okay to mix calls to + C{next} and L{readline}. + + @raise StopIteration: when the end of the file is reached. + + @return: a line read from the file. + @rtype: str + """ + line = self.readline() + if not line: + raise StopIteration + return line def read(self, size=None): """ @@ -127,7 +144,7 @@ class BufferedFile (object): if (size is None) or (size < 0): # go for broke result = self._rbuffer - self._rbuffer = '' + self._rbuffer = bytes() self._pos += len(result) while True: try: @@ -139,12 +156,12 @@ class BufferedFile (object): result += new_data self._realpos += len(new_data) self._pos += len(new_data) - return result + return result if self._flags & self.FLAG_BINARY else u(result) if size <= len(self._rbuffer): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result + return result if self._flags & self.FLAG_BINARY else u(result) while len(self._rbuffer) < size: read_size = size - len(self._rbuffer) if self._flags & self.FLAG_BUFFERED: @@ -160,7 +177,7 @@ class BufferedFile (object): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result + return result if self._flags & self.FLAG_BINARY else u(result) def readline(self, size=None): """ @@ -190,11 +207,11 @@ class BufferedFile (object): if self._at_trailing_cr and (self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (len(line) > 0): # edge case: the newline may be '\r\n' and we may have read # only the first '\r' last time. - if line[0] == '\n': + if line[0] == linefeed_byte_value: line = line[1:] - self._record_newline('\r\n') + self._record_newline(crlf) else: - self._record_newline('\r') + self._record_newline(cr_byte) self._at_trailing_cr = False # check size before looking for a linefeed, in case we already have # enough. @@ -204,42 +221,42 @@ class BufferedFile (object): self._rbuffer = line[size:] line = line[:size] self._pos += len(line) - return line + return line if self._flags & self.FLAG_BINARY else u(line) n = size - len(line) else: n = self._bufsize - if ('\n' in line) or ((self._flags & self.FLAG_UNIVERSAL_NEWLINE) and ('\r' in line)): + if (linefeed_byte in line) or ((self._flags & self.FLAG_UNIVERSAL_NEWLINE) and (cr_byte in line)): break try: new_data = self._read(n) except EOFError: new_data = None if (new_data is None) or (len(new_data) == 0): - self._rbuffer = '' + self._rbuffer = bytes() self._pos += len(line) - return line + return line if self._flags & self.FLAG_BINARY else u(line) line += new_data self._realpos += len(new_data) # find the newline - pos = line.find('\n') + pos = line.find(linefeed_byte) if self._flags & self.FLAG_UNIVERSAL_NEWLINE: - rpos = line.find('\r') + rpos = line.find(cr_byte) if (rpos >= 0) and ((rpos < pos) or (pos < 0)): pos = rpos xpos = pos + 1 - if (line[pos] == '\r') and (xpos < len(line)) and (line[xpos] == '\n'): + if (line[pos] == cr_byte_value) and (xpos < len(line)) and (line[xpos] == linefeed_byte_value): xpos += 1 self._rbuffer = line[xpos:] lf = line[pos:xpos] - line = line[:pos] + '\n' - if (len(self._rbuffer) == 0) and (lf == '\r'): + line = line[:pos] + linefeed_byte + if (len(self._rbuffer) == 0) and (lf == cr_byte): # we could read the line up to a '\r' and there could still be a # '\n' following that we read next time. note that and eat it. self._at_trailing_cr = True else: self._record_newline(lf) self._pos += len(line) - return line + return line if self._flags & self.FLAG_BINARY else u(line) def readlines(self, sizehint=None): """ @@ -254,14 +271,14 @@ class BufferedFile (object): @rtype: list """ lines = [] - bytes = 0 + byte_count = 0 while True: line = self.readline() if len(line) == 0: break lines.append(line) - bytes += len(line) - if (sizehint is not None) and (bytes >= sizehint): + byte_count += len(line) + if (sizehint is not None) and (byte_count >= sizehint): break return lines @@ -306,6 +323,7 @@ class BufferedFile (object): @param data: data to write. @type data: str """ + data = b(data) if self._closed: raise IOError('File is closed') if not (self._flags & self.FLAG_WRITE): @@ -316,12 +334,12 @@ class BufferedFile (object): self._wbuffer.write(data) if self._flags & self.FLAG_LINE_BUFFERED: # only scan the new data for linefeed, to avoid wasting time. - last_newline_pos = data.rfind('\n') + last_newline_pos = data.rfind(linefeed_byte) if last_newline_pos >= 0: wbuf = self._wbuffer.getvalue() last_newline_pos += len(wbuf) - len(data) self._write_all(wbuf[:last_newline_pos + 1]) - self._wbuffer = StringIO() + self._wbuffer = BytesIO() self._wbuffer.write(wbuf[last_newline_pos + 1:]) return # even if we're line buffering, if the buffer has grown past the @@ -454,7 +472,7 @@ class BufferedFile (object): return if self.newlines is None: self.newlines = newline - elif (type(self.newlines) is str) and (self.newlines != newline): + elif self.newlines != newline and isinstance(self.newlines, bytes_types): self.newlines = (self.newlines, newline) elif newline not in self.newlines: self.newlines += (newline,) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 9bcf0d55..cf586a29 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -23,7 +23,10 @@ L{HostKeys} import base64 import binascii from Crypto.Hash import SHA, HMAC -import UserDict +try: + from collections import MutableMapping +except ImportError: + from UserDict import DictMixin as MutableMapping from paramiko.common import * from paramiko.dsskey import DSSKey @@ -78,18 +81,19 @@ class HostKeyEntry: # Decide what kind of key we're looking at and create an object # to hold it accordingly. try: + key = b(key) if keytype == 'ssh-rsa': - key = RSAKey(data=base64.decodestring(key)) + key = RSAKey(data=decodebytes(key)) elif keytype == 'ssh-dss': - key = DSSKey(data=base64.decodestring(key)) + key = DSSKey(data=decodebytes(key)) elif keytype == 'ecdsa-sha2-nistp256': - key = ECDSAKey(data=base64.decodestring(key)) + key = ECDSAKey(data=decodebytes(key)) else: log.info("Unable to handle key of type %s" % (keytype,)) return None - except binascii.Error, e: - raise InvalidHostKey(line, e) + except binascii.Error as e: + raise InvalidHostKey(line, sys.exc_info()[1]) return cls(names, key) from_line = classmethod(from_line) @@ -109,7 +113,7 @@ class HostKeyEntry: return '' % (self.hostnames, self.key) -class HostKeys (UserDict.DictMixin): +class HostKeys (MutableMapping): """ 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 @@ -168,20 +172,19 @@ class HostKeys (UserDict.DictMixin): @raise IOError: if there was an error reading the file """ - f = open(filename, 'r') - for lineno, line in enumerate(f): - line = line.strip() - if (len(line) == 0) or (line[0] == '#'): - continue - e = HostKeyEntry.from_line(line, lineno) - if e is not None: - _hostnames = e.hostnames - for h in _hostnames: - if self.check(h, e.key): - e.hostnames.remove(h) - if len(e.hostnames): - self._entries.append(e) - f.close() + with open(filename, 'r') as f: + for lineno, line in enumerate(f): + line = line.strip() + if (len(line) == 0) or (line[0] == '#'): + continue + e = HostKeyEntry.from_line(line, lineno) + if e is not None: + _hostnames = e.hostnames + for h in _hostnames: + if self.check(h, e.key): + e.hostnames.remove(h) + if len(e.hostnames): + self._entries.append(e) def save(self, filename): """ @@ -197,12 +200,11 @@ class HostKeys (UserDict.DictMixin): @since: 1.6.1 """ - f = open(filename, 'w') - for e in self._entries: - line = e.to_line() - if line: - f.write(line) - f.close() + with open(filename, 'w') as f: + for e in self._entries: + line = e.to_line() + if line: + f.write(line) def lookup(self, hostname): """ @@ -215,12 +217,26 @@ class HostKeys (UserDict.DictMixin): @return: keys associated with this host (or C{None}) @rtype: dict(str, L{PKey}) """ - class SubDict (UserDict.DictMixin): + class SubDict (MutableMapping): def __init__(self, hostname, entries, hostkeys): self._hostname = hostname self._entries = entries self._hostkeys = hostkeys + def __iter__(self): + for k in self.keys(): + yield k + + def __len__(self): + return len(self.keys()) + + def __delitem__(self, key): + for e in list(self._entries): + if e.key.get_name() == key: + self._entries.remove(e) + else: + raise KeyError(key) + def __getitem__(self, key): for e in self._entries: if e.key.get_name() == key: @@ -272,7 +288,7 @@ class HostKeys (UserDict.DictMixin): host_key = k.get(key.get_name(), None) if host_key is None: return False - return str(host_key) == str(key) + return host_key.asbytes() == key.asbytes() def clear(self): """ @@ -280,6 +296,17 @@ class HostKeys (UserDict.DictMixin): """ self._entries = [] + def __iter__(self): + for k in self.keys(): + yield k + + def __len__(self): + return len(self.keys()) + + def __delitem__(self, key): + k = self[key] + pass + def __getitem__(self, key): ret = self.lookup(key) if ret is None: @@ -333,10 +360,10 @@ class HostKeys (UserDict.DictMixin): else: if salt.startswith('|1|'): salt = salt.split('|')[2] - salt = base64.decodestring(salt) + salt = decodebytes(b(salt)) assert len(salt) == SHA.digest_size - hmac = HMAC.HMAC(salt, hostname, SHA).digest() - hostkey = '|1|%s|%s' % (base64.encodestring(salt), base64.encodestring(hmac)) + hmac = HMAC.HMAC(salt, b(hostname), SHA).digest() + hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) return hostkey.replace('\n', '') hash_host = staticmethod(hash_host) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index c0455a1f..02494539 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -33,6 +33,8 @@ from paramiko.ssh_exception import SSHException _MSG_KEXDH_GEX_REQUEST_OLD, _MSG_KEXDH_GEX_GROUP, _MSG_KEXDH_GEX_INIT, \ _MSG_KEXDH_GEX_REPLY, _MSG_KEXDH_GEX_REQUEST = range(30, 35) +c_MSG_KEXDH_GEX_REQUEST_OLD, c_MSG_KEXDH_GEX_GROUP, c_MSG_KEXDH_GEX_INIT, \ + c_MSG_KEXDH_GEX_REPLY, c_MSG_KEXDH_GEX_REQUEST = [byte_chr(c) for c in range(30, 35)] class KexGex (object): @@ -62,11 +64,11 @@ class KexGex (object): m = Message() if _test_old_style: # only used for unit tests: we shouldn't ever send this - m.add_byte(chr(_MSG_KEXDH_GEX_REQUEST_OLD)) + m.add_byte(c_MSG_KEXDH_GEX_REQUEST_OLD) m.add_int(self.preferred_bits) self.old_style = True else: - m.add_byte(chr(_MSG_KEXDH_GEX_REQUEST)) + m.add_byte(c_MSG_KEXDH_GEX_REQUEST) m.add_int(self.min_bits) m.add_int(self.preferred_bits) m.add_int(self.max_bits) @@ -89,20 +91,20 @@ class KexGex (object): ### internals... - + def _generate_x(self): # generate an "x" (1 < x < (p-1)/2). q = (self.p - 1) // 2 qnorm = util.deflate_long(q, 0) - qhbyte = ord(qnorm[0]) - bytes = len(qnorm) + qhbyte = byte_ord(qnorm[0]) + byte_count = len(qnorm) qmask = 0xff while not (qhbyte & 0x80): qhbyte <<= 1 qmask >>= 1 while True: - x_bytes = self.transport.rng.read(bytes) - x_bytes = chr(ord(x_bytes[0]) & qmask) + x_bytes[1:] + x_bytes = self.transport.rng.read(byte_count) + x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): break @@ -135,7 +137,7 @@ class KexGex (object): self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits)) self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) m = Message() - m.add_byte(chr(_MSG_KEXDH_GEX_GROUP)) + m.add_byte(c_MSG_KEXDH_GEX_GROUP) m.add_mpint(self.p) m.add_mpint(self.g) self.transport._send_message(m) @@ -156,7 +158,7 @@ class KexGex (object): self.transport._log(DEBUG, 'Picking p (~ %d bits)' % (self.preferred_bits,)) self.g, self.p = pack.get_modulus(self.min_bits, self.preferred_bits, self.max_bits) m = Message() - m.add_byte(chr(_MSG_KEXDH_GEX_GROUP)) + m.add_byte(c_MSG_KEXDH_GEX_GROUP) m.add_mpint(self.p) m.add_mpint(self.g) self.transport._send_message(m) @@ -175,7 +177,7 @@ class KexGex (object): # now compute e = g^x mod p self.e = pow(self.g, self.x, self.p) m = Message() - m.add_byte(chr(_MSG_KEXDH_GEX_INIT)) + m.add_byte(c_MSG_KEXDH_GEX_INIT) m.add_mpint(self.e) self.transport._send_message(m) self.transport._expect_packet(_MSG_KEXDH_GEX_REPLY) @@ -187,7 +189,7 @@ class KexGex (object): self._generate_x() self.f = pow(self.g, self.x, self.p) K = pow(self.e, self.x, self.p) - key = str(self.transport.get_server_key()) + key = self.transport.get_server_key().asbytes() # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) hm = Message() hm.add(self.transport.remote_version, self.transport.local_version, @@ -203,16 +205,16 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(str(hm)).digest() + H = SHA.new(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) # send reply m = Message() - m.add_byte(chr(_MSG_KEXDH_GEX_REPLY)) + m.add_byte(c_MSG_KEXDH_GEX_REPLY) m.add_string(key) m.add_mpint(self.f) - m.add_string(str(sig)) + m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound() @@ -238,6 +240,6 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(str(hm)).digest()) + self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 6e89b6dc..05693a1f 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -30,11 +30,14 @@ from paramiko.ssh_exception import SSHException _MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) +c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)] # draft-ietf-secsh-transport-09.txt, page 17 -P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL +P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF G = 2 +b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 +b0000000000000000 = zero_byte * 8 class KexGroup1(object): @@ -42,9 +45,9 @@ class KexGroup1(object): def __init__(self, transport): self.transport = transport - self.x = 0L - self.e = 0L - self.f = 0L + self.x = long(0) + self.e = long(0) + self.f = long(0) def start_kex(self): self._generate_x() @@ -56,7 +59,7 @@ class KexGroup1(object): # compute e = g^x mod p (where g=2), and send it self.e = pow(G, self.x, P) m = Message() - m.add_byte(chr(_MSG_KEXDH_INIT)) + m.add_byte(c_MSG_KEXDH_INIT) m.add_mpint(self.e) self.transport._send_message(m) self.transport._expect_packet(_MSG_KEXDH_REPLY) @@ -67,7 +70,7 @@ class KexGroup1(object): elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY): return self._parse_kexdh_reply(m) raise SSHException('KexGroup1 asked to handle packet type %d' % ptype) - + ### internals... @@ -80,9 +83,9 @@ class KexGroup1(object): # larger than q (but this is a tiny tiny subset of potential x). while 1: x_bytes = self.transport.rng.read(128) - x_bytes = chr(ord(x_bytes[0]) & 0x7f) + x_bytes[1:] - if (x_bytes[:8] != '\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF') and \ - (x_bytes[:8] != '\x00\x00\x00\x00\x00\x00\x00\x00'): + x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] + if (x_bytes[:8] != b7fffffffffffffff) and \ + (x_bytes[:8] != b0000000000000000): break self.x = util.inflate_long(x_bytes) @@ -92,7 +95,7 @@ class KexGroup1(object): self.f = m.get_mpint() if (self.f < 1) or (self.f > P - 1): raise SSHException('Server kex "f" is out of range') - sig = m.get_string() + sig = m.get_binary() K = pow(self.f, self.x, P) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() @@ -102,7 +105,7 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(str(hm)).digest()) + self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() @@ -112,7 +115,7 @@ class KexGroup1(object): if (self.e < 1) or (self.e > P - 1): raise SSHException('Client kex "e" is out of range') K = pow(self.e, self.x, P) - key = str(self.transport.get_server_key()) + key = self.transport.get_server_key().asbytes() # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() hm.add(self.transport.remote_version, self.transport.local_version, @@ -121,15 +124,15 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(str(hm)).digest() + H = SHA.new(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) # send reply m = Message() - m.add_byte(chr(_MSG_KEXDH_REPLY)) + m.add_byte(c_MSG_KEXDH_REPLY) m.add_string(key) m.add_mpint(self.f) - m.add_string(str(sig)) + m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound() diff --git a/paramiko/message.py b/paramiko/message.py index c0e8692b..45086213 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -21,9 +21,9 @@ Implementation of an SSH2 "message". """ import struct -import cStringIO from paramiko import util +from paramiko.common import * class Message (object): @@ -37,6 +37,8 @@ class Message (object): paramiko doesn't support yet. """ + big_int = long(0xff000000) + def __init__(self, content=None): """ Create a new SSH2 Message. @@ -46,18 +48,12 @@ class Message (object): @type content: string """ if content != None: - self.packet = cStringIO.StringIO(content) + self.packet = BytesIO(content) else: - self.packet = cStringIO.StringIO() + self.packet = BytesIO() def __str__(self): - """ - Return the byte stream content of this Message, as a string. - - @return: the contents of this Message. - @rtype: string - """ - return self.packet.getvalue() + return self.asbytes() def __repr__(self): """ @@ -67,6 +63,15 @@ class Message (object): """ return 'paramiko.Message(' + repr(self.packet.getvalue()) + ')' + def asbytes(self): + """ + Return the byte stream content of this Message, as bytes. + + @return: the contents of this Message. + @rtype: bytes + """ + return self.packet.getvalue() + def rewind(self): """ Rewind the message to the beginning as if no items had been parsed @@ -112,7 +117,7 @@ class Message (object): b = self.packet.read(n) max_pad_size = 1<<20 # Limit padding to 1 MB if len(b) < n and n < max_pad_size: - return b + '\x00' * (n - len(b)) + return b + zero_byte * (n - len(b)) return b def get_byte(self): @@ -134,12 +139,25 @@ class Message (object): @rtype: bool """ b = self.get_bytes(1) - return b != '\x00' + return b != zero_byte def get_int(self): """ Fetch an int from the stream. + @return: a 32-bit unsigned integer. + @rtype: int + """ + byte = self.get_bytes(1) + if byte == max_byte: + return util.inflate_long(self.get_binary()) + byte += self.get_bytes(3) + return struct.unpack('>I', byte)[0] + + def get_size(self): + """ + Fetch an int from the stream. + @return: a 32-bit unsigned integer. @rtype: int """ @@ -161,7 +179,7 @@ class Message (object): @return: an arbitrary-length integer. @rtype: long """ - return util.inflate_long(self.get_string()) + return util.inflate_long(self.get_binary()) def get_string(self): """ @@ -172,7 +190,30 @@ class Message (object): @return: a string. @rtype: string """ - return self.get_bytes(self.get_int()) + return self.get_bytes(self.get_size()) + + def get_text(self): + """ + Fetch a string from the stream. This could be a byte string and may + contain unprintable characters. (It's not unheard of for a string to + contain another byte-stream Message.) + + @return: a string. + @rtype: string + """ + return u(self.get_bytes(self.get_size())) + #return self.get_bytes(self.get_size()) + + def get_binary(self): + """ + Fetch a string from the stream. This could be a byte string and may + contain unprintable characters. (It's not unheard of for a string to + contain another byte-stream Message.) + + @return: a string. + @rtype: string + """ + return self.get_bytes(self.get_size()) def get_list(self): """ @@ -182,7 +223,7 @@ class Message (object): @return: a list of strings. @rtype: list of strings """ - return self.get_string().split(',') + return self.get_text().split(',') def add_bytes(self, b): """ @@ -212,12 +253,12 @@ class Message (object): @type b: bool """ if b: - self.add_byte('\x01') + self.packet.write(one_byte) else: - self.add_byte('\x00') + self.packet.write(zero_byte) return self - def add_int(self, n): + def add_size(self, n): """ Add an integer to the stream. @@ -227,6 +268,20 @@ class Message (object): self.packet.write(struct.pack('>I', n)) return self + def add_int(self, n): + """ + Add an integer to the stream. + + @param n: integer to add + @type n: int + """ + if n >= Message.big_int: + self.packet.write(max_byte) + self.add_string(util.deflate_long(n)) + else: + self.packet.write(struct.pack('>I', n)) + return self + def add_int64(self, n): """ Add a 64-bit int to the stream. @@ -255,7 +310,8 @@ class Message (object): @param s: string to add @type s: str """ - self.add_int(len(s)) + s = asbytes(s) + self.add_size(len(s)) self.packet.write(s) return self @@ -272,21 +328,14 @@ class Message (object): return self def _add(self, i): - if type(i) is str: - return self.add_string(i) - elif type(i) is int: - return self.add_int(i) - elif type(i) is long: - if i > 0xffffffffL: - return self.add_mpint(i) - else: - return self.add_int(i) - elif type(i) is bool: + if type(i) is bool: return self.add_boolean(i) + elif isinstance(i, integer_types): + return self.add_int(i) elif type(i) is list: return self.add_list(i) else: - raise Exception('Unknown type') + return self.add_string(i) def add(self, *seq): """ diff --git a/paramiko/packet.py b/paramiko/packet.py index 6ab7363d..b941c75b 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -38,6 +38,7 @@ try: except ImportError: from Crypto.Hash.HMAC import HMAC + def compute_hmac(key, message, digest_class): return HMAC(key, message, digest_class).digest() @@ -66,7 +67,7 @@ class Packetizer (object): self.__dump_packets = False self.__need_rekey = False self.__init_count = 0 - self.__remainder = '' + self.__remainder = bytes() # used for noticing when to re-key: self.__sent_bytes = 0 @@ -86,12 +87,12 @@ class Packetizer (object): self.__sdctr_out = False self.__mac_engine_out = None self.__mac_engine_in = None - self.__mac_key_out = '' - self.__mac_key_in = '' + self.__mac_key_out = bytes() + self.__mac_key_in = bytes() self.__compress_engine_out = None self.__compress_engine_in = None - self.__sequence_number_out = 0L - self.__sequence_number_in = 0L + self.__sequence_number_out = 0 + self.__sequence_number_in = 0 # lock around outbound writes (packet computation) self.__write_lock = threading.RLock() @@ -152,6 +153,7 @@ class Packetizer (object): def close(self): self.__closed = True + self.__socket.close() def set_hexdump(self, hexdump): self.__dump_packets = hexdump @@ -196,7 +198,7 @@ class Packetizer (object): @raise EOFError: if the socket was closed before all the bytes could be read """ - out = '' + out = bytes() # handle over-reading from reading the banner line if len(self.__remainder) > 0: out = self.__remainder[:n] @@ -214,7 +216,7 @@ class Packetizer (object): n -= len(x) except socket.timeout: got_timeout = True - except socket.error, e: + except socket.error as e: # on Linux, sometimes instead of socket.timeout, we get # EAGAIN. this is a bug in recent (> 2.6.9) kernels but # we need to work around it. @@ -243,7 +245,7 @@ class Packetizer (object): n = self.__socket.send(out) except socket.timeout: retry_write = True - except socket.error, e: + except socket.error as e: if (type(e.args) is tuple) and (len(e.args) > 0) and (e.args[0] == errno.EAGAIN): retry_write = True elif (type(e.args) is tuple) and (len(e.args) > 0) and (e.args[0] == errno.EINTR): @@ -273,22 +275,22 @@ class Packetizer (object): line, so it's okay to attempt large reads. """ buf = self.__remainder - while not '\n' in buf: + while not linefeed_byte in buf: buf += self._read_timeout(timeout) - n = buf.index('\n') + n = buf.index(linefeed_byte) self.__remainder = buf[n+1:] buf = buf[:n] - if (len(buf) > 0) and (buf[-1] == '\r'): + if (len(buf) > 0) and (buf[-1] == cr_byte_value): buf = buf[:-1] - return buf + return u(buf) def send_message(self, data): """ Write a block of data using the current cipher, as an SSH block. """ # encrypt this sucka - data = str(data) - cmd = ord(data[0]) + data = asbytes(data) + cmd = byte_ord(data[0]) if cmd in MSG_NAMES: cmd_name = MSG_NAMES[cmd] else: @@ -310,7 +312,7 @@ class Packetizer (object): if self.__block_engine_out != None: payload = struct.pack('>I', self.__sequence_number_out) + packet out += compute_hmac(self.__mac_key_out, payload, self.__mac_engine_out)[:self.__mac_size_out] - self.__sequence_number_out = (self.__sequence_number_out + 1) & 0xffffffffL + self.__sequence_number_out = (self.__sequence_number_out + 1) & xffffffff self.write_all(out) self.__sent_bytes += len(out) @@ -359,7 +361,7 @@ class Packetizer (object): my_mac = compute_hmac(self.__mac_key_in, mac_payload, self.__mac_engine_in)[:self.__mac_size_in] if my_mac != mac: raise SSHException('Mismatched MAC') - padding = ord(packet[0]) + padding = byte_ord(packet[0]) payload = packet[1:packet_size - padding] if self.__dump_packets: @@ -370,7 +372,7 @@ class Packetizer (object): msg = Message(payload[1:]) msg.seqno = self.__sequence_number_in - self.__sequence_number_in = (self.__sequence_number_in + 1) & 0xffffffffL + self.__sequence_number_in = (self.__sequence_number_in + 1) & xffffffff # check for rekey raw_packet_size = packet_size + self.__mac_size_in + 4 @@ -393,7 +395,7 @@ class Packetizer (object): self.__received_packets_overflow = 0 self._trigger_rekey() - cmd = ord(payload[0]) + cmd = byte_ord(payload[0]) if cmd in MSG_NAMES: cmd_name = MSG_NAMES[cmd] else: @@ -468,7 +470,7 @@ class Packetizer (object): break except socket.timeout: pass - except EnvironmentError, e: + except EnvironmentError as e: if ((type(e.args) is tuple) and (len(e.args) > 0) and (e.args[0] == errno.EINTR)): pass @@ -490,7 +492,7 @@ class Packetizer (object): if self.__sdctr_out or self.__block_engine_out is None: # cute trick i caught openssh doing: if we're not encrypting or SDCTR mode (RFC4344), # don't waste random bytes for the padding - packet += (chr(0) * padding) + packet += (zero_byte * padding) else: packet += rng.read(padding) return packet diff --git a/paramiko/pipe.py b/paramiko/pipe.py index db43d549..619b807e 100644 --- a/paramiko/pipe.py +++ b/paramiko/pipe.py @@ -27,6 +27,7 @@ will trigger as readable in select(). import sys import os import socket +from paramiko.py3compat import b def make_pipe (): @@ -63,7 +64,7 @@ class PosixPipe (object): if self._set or self._closed: return self._set = True - os.write(self._wfd, '*') + os.write(self._wfd, b'*') def set_forever (self): self._forever = True @@ -109,7 +110,7 @@ class WindowsPipe (object): if self._set or self._closed: return self._set = True - self._wsock.send('*') + self._wsock.send(b'*') def set_forever (self): self._forever = True diff --git a/paramiko/pkey.py b/paramiko/pkey.py index b1199df8..0a174ca7 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -63,7 +63,7 @@ class PKey (object): """ pass - def __str__(self): + def asbytes(self): """ Return a string of an SSH L{Message} made up of the public part(s) of this key. This string is suitable for passing to L{__init__} to @@ -72,7 +72,10 @@ class PKey (object): @return: string representation of an SSH key message. @rtype: str """ - return '' + return bytes() + + def __str__(self): + return self.asbytes() def __cmp__(self, other): """ @@ -90,7 +93,10 @@ class PKey (object): ho = hash(other) if hs != ho: return cmp(hs, ho) - return cmp(str(self), str(other)) + return cmp(self.asbytes(), other.asbytes()) + + def __eq__(self, other): + return hash(self) == hash(other) def get_name(self): """ @@ -131,7 +137,7 @@ class PKey (object): format. @rtype: str """ - return MD5.new(str(self)).digest() + return MD5.new(self.asbytes()).digest() def get_base64(self): """ @@ -142,7 +148,7 @@ class PKey (object): @return: a base64 string containing the public part of the key. @rtype: str """ - return base64.encodestring(str(self)).replace('\n', '') + return u(encodebytes(self.asbytes())).replace('\n', '') def sign_ssh_data(self, rng, data): """ @@ -156,7 +162,7 @@ class PKey (object): @return: an SSH signature message. @rtype: L{Message} """ - return '' + return bytes() def verify_ssh_sig(self, data, msg): """ @@ -276,9 +282,8 @@ class PKey (object): encrypted, and C{password} is C{None}. @raise SSHException: if the key file is invalid. """ - f = open(filename, 'r') - data = self._read_private_key(tag, f, password) - f.close() + with open(filename, 'r') as f: + data = self._read_private_key(tag, f, password) return data def _read_private_key(self, tag, f, password=None): @@ -303,8 +308,8 @@ class PKey (object): end += 1 # if we trudged to the end of the file, just try to cope. try: - data = base64.decodestring(''.join(lines[start:end])) - except base64.binascii.Error, e: + data = decodebytes(b(''.join(lines[start:end]))) + except base64.binascii.Error as e: raise SSHException('base64 decoding error: ' + str(e)) if 'proc-type' not in headers: # unencryped: done @@ -315,7 +320,7 @@ class PKey (object): try: encryption_type, saltstr = headers['dek-info'].split(',') except: - raise SSHException('Can\'t parse DEK-info in private key file') + raise SSHException("Can't parse DEK-info in private key file") if encryption_type not in self._CIPHER_TABLE: raise SSHException('Unknown private key cipher "%s"' % encryption_type) # if no password was passed in, raise an exception pointing out that we need one @@ -324,7 +329,7 @@ class PKey (object): cipher = self._CIPHER_TABLE[encryption_type]['cipher'] keysize = self._CIPHER_TABLE[encryption_type]['keysize'] mode = self._CIPHER_TABLE[encryption_type]['mode'] - salt = unhexlify(saltstr) + salt = unhexlify(b(saltstr)) key = util.generate_key_bytes(MD5, salt, password, keysize) return cipher.new(key, mode, salt).decrypt(data) @@ -346,33 +351,32 @@ class PKey (object): @raise IOError: if there was an error writing the file. """ - f = open(filename, 'w', 0600) - # grrr... the mode doesn't always take hold - os.chmod(filename, 0600) - self._write_private_key(tag, f, data, password) - f.close() + with open(filename, 'w', o600) as f: + # grrr... the mode doesn't always take hold + os.chmod(filename, o600) + self._write_private_key(tag, f, data, password) def _write_private_key(self, tag, f, data, password=None): f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag) if password is not None: # since we only support one cipher here, use it - cipher_name = self._CIPHER_TABLE.keys()[0] + cipher_name = list(self._CIPHER_TABLE.keys())[0] cipher = self._CIPHER_TABLE[cipher_name]['cipher'] keysize = self._CIPHER_TABLE[cipher_name]['keysize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = rng.read(8) + salt = rng.read(16) key = util.generate_key_bytes(MD5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize #data += rng.read(n) # that would make more sense ^, but it confuses openssh. - data += '\0' * n + data += zero_byte * n data = cipher.new(key, mode, salt).encrypt(data) f.write('Proc-Type: 4,ENCRYPTED\n') - f.write('DEK-Info: %s,%s\n' % (cipher_name, hexlify(salt).upper())) + f.write('DEK-Info: %s,%s\n' % (cipher_name, u(hexlify(salt)).upper())) f.write('\n') - s = base64.encodestring(data) + s = u(encodebytes(data)) # re-wrap to 64-char lines s = ''.join(s.split('\n')) s = '\n'.join([s[i : i+64] for i in range(0, len(s), 64)]) diff --git a/paramiko/primes.py b/paramiko/primes.py index 9419cd6b..d10a15fb 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -24,6 +24,7 @@ from Crypto.Util import number from paramiko import util from paramiko.ssh_exception import SSHException +from paramiko.common import * def _generate_prime(bits, rng): @@ -33,7 +34,7 @@ def _generate_prime(bits, rng): # loop catches the case where we increment n into a higher bit-range x = rng.read((bits+7) // 8) if hbyte_mask > 0: - x = chr(ord(x[0]) & hbyte_mask) + x[1:] + x = byte_mask(x[0], hbyte_mask) + x[1:] n = util.inflate_long(x, 1) n |= 1 n |= (1 << (bits - 1)) @@ -46,7 +47,7 @@ def _generate_prime(bits, rng): def _roll_random(rng, n): "returns a random # from 0 to N-1" bits = util.bit_length(n-1) - bytes = (bits + 7) // 8 + byte_count = (bits + 7) // 8 hbyte_mask = pow(2, bits % 8) - 1 # so here's the plan: @@ -56,9 +57,9 @@ def _roll_random(rng, n): # fits, so i can't guarantee that this loop will ever finish, but the odds # of it looping forever should be infinitesimal. while True: - x = rng.read(bytes) + x = rng.read(byte_count) if hbyte_mask > 0: - x = chr(ord(x[0]) & hbyte_mask) + x[1:] + x = byte_mask(x[0], hbyte_mask) + x[1:] num = util.inflate_long(x, 1) if num < n: break @@ -112,20 +113,18 @@ class ModulusPack (object): @raise IOError: passed from any file operations that fail. """ self.pack = {} - f = open(filename, 'r') - for line in f: - line = line.strip() - if (len(line) == 0) or (line[0] == '#'): - continue - try: - self._parse_modulus(line) - except: - continue - f.close() + with open(filename, 'r') as f: + for line in f: + line = line.strip() + if (len(line) == 0) or (line[0] == '#'): + continue + try: + self._parse_modulus(line) + except: + continue def get_modulus(self, min, prefer, max): - bitsizes = self.pack.keys() - bitsizes.sort() + bitsizes = sorted(self.pack.keys()) if len(bitsizes) == 0: raise SSHException('no moduli available') good = -1 diff --git a/paramiko/proxy.py b/paramiko/proxy.py index 218b76e2..72927c04 100644 --- a/paramiko/proxy.py +++ b/paramiko/proxy.py @@ -21,6 +21,7 @@ L{ProxyCommand}. """ import os +import sys from shlex import split as shlsplit import signal from subprocess import Popen, PIPE @@ -59,12 +60,12 @@ class ProxyCommand(object): """ try: self.process.stdin.write(content) - except IOError, e: + except IOError as e: # There was a problem with the child process. It probably # died and we can't proceed. The best option here is to # raise an exception informing the user that the informed # ProxyCommand is not working. - raise BadProxyCommand(' '.join(self.cmd), e.strerror) + raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) return len(content) def recv(self, size): @@ -79,8 +80,8 @@ class ProxyCommand(object): """ try: return os.read(self.process.stdout.fileno(), size) - except IOError, e: - raise BadProxyCommand(' '.join(self.cmd), e.strerror) + except IOError as e: + raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) def close(self): os.kill(self.process.pid, signal.SIGTERM) diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py new file mode 100644 index 00000000..22285992 --- /dev/null +++ b/paramiko/py3compat.py @@ -0,0 +1,160 @@ +import sys +import base64 + +__all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input', + 'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask', + 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next'] + +PY2 = sys.version_info[0] < 3 + +if PY2: + string_types = basestring + text_type = unicode + bytes_types = str + bytes = str + integer_types = (int, long) + long = long + input = raw_input + decodebytes = base64.decodestring + encodebytes = base64.encodestring + + + def bytestring(s): # NOQA + if isinstance(s, unicode): + return s.encode('utf-8') + return s + + + byte_ord = ord # NOQA + byte_chr = chr # NOQA + + + def byte_mask(c, mask): + return chr(ord(c) & mask) + + + def b(s, encoding='utf8'): # NOQA + """cast unicode or bytes to bytes""" + if isinstance(s, str): + return s + elif isinstance(s, unicode): + return s.encode(encoding) + else: + raise TypeError("Expected unicode or bytes, got %r" % s) + + + def u(s, encoding='utf8'): # NOQA + """cast bytes or unicode to unicode""" + if isinstance(s, str): + return s.decode(encoding) + elif isinstance(s, unicode): + return s + else: + raise TypeError("Expected unicode or bytes, got %r" % s) + + + def b2s(s): + return s + + + try: + import cStringIO + + StringIO = cStringIO.StringIO # NOQA + except ImportError: + import StringIO + + StringIO = StringIO.StringIO # NOQA + + BytesIO = StringIO + + + def is_callable(c): # NOQA + return callable(c) + + + def get_next(c): # NOQA + return c.next + + + def next(c): + return c.next() + + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + + + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) # NOQA + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) # NOQA + del X +else: + import collections + import struct + string_types = str + text_type = str + bytes = bytes + bytes_types = bytes + integer_types = int + class long(int): + pass + input = input + decodebytes = base64.decodebytes + encodebytes = base64.encodebytes + + def bytestring(s): + return s + + def byte_ord(c): + assert isinstance(c, int) + return c + + def byte_chr(c): + assert isinstance(c, int) + return struct.pack('B', c) + + def byte_mask(c, mask): + assert isinstance(c, int) + return struct.pack('B', c & mask) + + def b(s, encoding='utf8'): + """cast unicode or bytes to bytes""" + if isinstance(s, bytes): + return s + elif isinstance(s, str): + return s.encode(encoding) + else: + raise TypeError("Expected unicode or bytes, got %r" % s) + + def u(s, encoding='utf8'): + """cast bytes or unicode to unicode""" + if isinstance(s, bytes): + return s.decode(encoding) + elif isinstance(s, str): + return s + else: + raise TypeError("Expected unicode or bytes, got %r" % s) + + def b2s(s): + return s.decode() if isinstance(s, bytes) else s + + import io + StringIO = io.StringIO # NOQA + BytesIO = io.BytesIO # NOQA + + def is_callable(c): + return isinstance(c, collections.Callable) + + def get_next(c): + return c.__next__ + + next = next + + MAXSIZE = sys.maxsize # NOQA diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index c7500f85..65f1463d 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -31,6 +31,8 @@ from paramiko.ber import BER, BERException from paramiko.pkey import PKey from paramiko.ssh_exception import SSHException +SHA1_DIGESTINFO = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' + class RSAKey (PKey): """ @@ -57,18 +59,21 @@ class RSAKey (PKey): else: if msg is None: raise SSHException('Key object may not be empty') - if msg.get_string() != 'ssh-rsa': + if msg.get_text() != 'ssh-rsa': raise SSHException('Invalid key') self.e = msg.get_mpint() self.n = msg.get_mpint() self.size = util.bit_length(self.n) - def __str__(self): + def asbytes(self): m = Message() m.add_string('ssh-rsa') m.add_mpint(self.e) m.add_mpint(self.n) - return str(m) + return m.asbytes() + + def __str__(self): + return self.asbytes() def __hash__(self): h = hash(self.get_name()) @@ -88,16 +93,16 @@ class RSAKey (PKey): def sign_ssh_data(self, rpool, data): digest = SHA.new(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) - sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), '')[0], 0) + sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) m = Message() m.add_string('ssh-rsa') m.add_string(sig) return m def verify_ssh_sig(self, data, msg): - if msg.get_string() != 'ssh-rsa': + if msg.get_text() != 'ssh-rsa': return False - sig = util.inflate_long(msg.get_string(), True) + sig = util.inflate_long(msg.get_binary(), True) # verify the signature by SHA'ing the data and encrypting it using the # public key. some wackiness ensues where we "pkcs1imify" the 20-byte # hash into a string as long as the RSA key. @@ -116,7 +121,7 @@ class RSAKey (PKey): b.encode(keylist) except BERException: raise SSHException('Unable to create ber encoding of key') - return str(b) + return b.asbytes() def write_private_key_file(self, filename, password=None): self._write_private_key_file('RSA', filename, self._encode_key(), password) @@ -154,10 +159,9 @@ class RSAKey (PKey): turn a 20-byte SHA1 hash into a blob of data as large as the key's N, using PKCS1's \"emsa-pkcs1-v1_5\" encoding. totally bizarre. """ - SHA1_DIGESTINFO = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' size = len(util.deflate_long(self.n, 0)) - filler = '\xff' * (size - len(SHA1_DIGESTINFO) - len(data) - 3) - return '\x00\x01' + filler + '\x00' + SHA1_DIGESTINFO + data + filler = max_byte * (size - len(SHA1_DIGESTINFO) - len(data) - 3) + return zero_byte + one_byte + filler + zero_byte + SHA1_DIGESTINFO + data def _from_private_key_file(self, filename, password): data = self._read_private_key_file('RSA', filename, password) diff --git a/paramiko/server.py b/paramiko/server.py index fdb40942..a922201d 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -48,7 +48,7 @@ class InteractiveQuery (object): self.instructions = instructions self.prompts = [] for x in prompts: - if (type(x) is str) or (type(x) is unicode): + if isinstance(x, string_types): self.add_prompt(x) else: self.add_prompt(x[0], x[1]) @@ -623,7 +623,7 @@ class SubsystemHandler (threading.Thread): try: self.__transport._log(DEBUG, 'Starting handler for subsystem %s' % self.__name) self.start_subsystem(self.__name, self.__transport, self.__channel) - except Exception, e: + except Exception as e: self.__transport._log(ERROR, 'Exception in subsystem handler for "%s": %s' % (self.__name, str(e))) self.__transport._log(ERROR, util.tb_strings()) diff --git a/paramiko/sftp.py b/paramiko/sftp.py index a97c300f..3e05de9f 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -86,7 +86,7 @@ CMD_NAMES = { CMD_ATTRS: 'attrs', CMD_EXTENDED: 'extended', CMD_EXTENDED_REPLY: 'extended_reply' - } +} class SFTPError (Exception): @@ -125,7 +125,7 @@ class BaseSFTP (object): msg = Message() msg.add_int(_VERSION) msg.add(*extension_pairs) - self._send_packet(CMD_VERSION, str(msg)) + self._send_packet(CMD_VERSION, msg) return version def _log(self, level, msg, *args): @@ -142,7 +142,7 @@ class BaseSFTP (object): return def _read_all(self, n): - out = '' + out = bytes() while n > 0: if isinstance(self.sock, socket.socket): # sometimes sftp is used directly over a socket instead of @@ -166,7 +166,8 @@ class BaseSFTP (object): def _send_packet(self, t, packet): #self._log(DEBUG2, 'write: %s (len=%d)' % (CMD_NAMES.get(t, '0x%02x' % t), len(packet))) - out = struct.pack('>I', len(packet) + 1) + chr(t) + packet + packet = asbytes(packet) + out = struct.pack('>I', len(packet) + 1) + byte_chr(t) + packet if self.ultra_debug: self._log(DEBUG, util.format_binary(out, 'OUT: ')) self._write_all(out) @@ -175,14 +176,14 @@ class BaseSFTP (object): x = self._read_all(4) # most sftp servers won't accept packets larger than about 32k, so # anything with the high byte set (> 16MB) is just garbage. - if x[0] != '\x00': + if byte_ord(x[0]): raise SFTPError('Garbage packet received') size = struct.unpack('>I', x)[0] data = self._read_all(size) if self.ultra_debug: self._log(DEBUG, util.format_binary(data, 'IN: ')); if size > 0: - t = ord(data[0]) + t = byte_ord(data[0]) #self._log(DEBUG2, 'read: %s (len=%d)' % (CMD_NAMES.get(t), '0x%02x' % t, len(data)-1)) return t, data[1:] - return 0, '' + return 0, bytes() diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index b459b04b..0ed030d8 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -44,7 +44,7 @@ class SFTPAttributes (object): FLAG_UIDGID = 2 FLAG_PERMISSIONS = 4 FLAG_AMTIME = 8 - FLAG_EXTENDED = 0x80000000L + FLAG_EXTENDED = x80000000 def __init__(self): """ @@ -143,7 +143,7 @@ class SFTPAttributes (object): msg.add_int(long(self.st_mtime)) if self._flags & self.FLAG_EXTENDED: msg.add_int(len(self.attr)) - for key, val in self.attr.iteritems(): + for key, val in self.attr.items(): msg.add_string(key) msg.add_string(val) return @@ -158,7 +158,7 @@ class SFTPAttributes (object): out += 'mode=' + oct(self.st_mode) + ' ' if (self.st_atime is not None) and (self.st_mtime is not None): out += 'atime=%d mtime=%d ' % (self.st_atime, self.st_mtime) - for k, v in self.attr.iteritems(): + for k, v in self.attr.items(): out += '"%s"=%r ' % (str(k), v) out += ']' return out @@ -194,13 +194,13 @@ class SFTPAttributes (object): ks = 's' else: ks = '?' - ks += self._rwx((self.st_mode & 0700) >> 6, self.st_mode & stat.S_ISUID) - ks += self._rwx((self.st_mode & 070) >> 3, self.st_mode & stat.S_ISGID) + ks += self._rwx((self.st_mode & o700) >> 6, self.st_mode & stat.S_ISUID) + ks += self._rwx((self.st_mode & o70) >> 3, self.st_mode & stat.S_ISGID) ks += self._rwx(self.st_mode & 7, self.st_mode & stat.S_ISVTX, True) else: ks = '?---------' # compute display date - if (self.st_mtime is None) or (self.st_mtime == 0xffffffffL): + if (self.st_mtime is None) or (self.st_mtime == xffffffff): # shouldn't really happen datestr = '(unknown date)' else: @@ -221,3 +221,5 @@ class SFTPAttributes (object): return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename) + def asbytes(self): + return b(str(self)) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index d9215743..0f55c6a8 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -42,12 +42,13 @@ def _to_unicode(s): """ try: return s.encode('ascii') - except UnicodeError: + except (UnicodeError, AttributeError): try: return s.decode('utf-8') except UnicodeError: return s +b_slash = b'/' class SFTPClient (BaseSFTP): """ @@ -85,7 +86,7 @@ class SFTPClient (BaseSFTP): self.ultra_debug = transport.get_hexdump() try: server_version = self._send_version() - except EOFError, x: + except EOFError: raise SSHException('EOF during negotiation') self._log(INFO, 'Opened sftp connection (server version %d)' % server_version) @@ -173,20 +174,20 @@ class SFTPClient (BaseSFTP): t, msg = self._request(CMD_OPENDIR, path) if t != CMD_HANDLE: raise SFTPError('Expected handle') - handle = msg.get_string() + handle = msg.get_binary() filelist = [] while True: try: t, msg = self._request(CMD_READDIR, handle) - except EOFError, e: + except EOFError: # done with handle break if t != CMD_NAME: raise SFTPError('Expected name response') count = msg.get_int() for i in range(count): - filename = _to_unicode(msg.get_string()) - longname = _to_unicode(msg.get_string()) + filename = msg.get_text() + longname = msg.get_text() attr = SFTPAttributes._from_msg(msg, filename, longname) if (filename != '.') and (filename != '..'): filelist.append(attr) @@ -245,7 +246,7 @@ class SFTPClient (BaseSFTP): t, msg = self._request(CMD_OPEN, filename, imode, attrblock) if t != CMD_HANDLE: raise SFTPError('Expected handle') - handle = msg.get_string() + handle = msg.get_binary() self._log(DEBUG, 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle))) return SFTPFile(self, handle, mode, bufsize) @@ -285,7 +286,7 @@ class SFTPClient (BaseSFTP): self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath)) self._request(CMD_RENAME, oldpath, newpath) - def mkdir(self, path, mode=0777): + def mkdir(self, path, mode=o777): """ Create a folder (directory) named C{path} with numeric mode C{mode}. The default mode is 0777 (octal). On some systems, mode is ignored. @@ -369,8 +370,7 @@ class SFTPClient (BaseSFTP): """ dest = self._adjust_cwd(dest) self._log(DEBUG, 'symlink(%r, %r)' % (source, dest)) - if type(source) is unicode: - source = source.encode('utf-8') + source = bytestring(source) self._request(CMD_SYMLINK, source, dest) def chmod(self, path, mode): @@ -495,9 +495,9 @@ class SFTPClient (BaseSFTP): count = msg.get_int() if count != 1: raise SFTPError('Realpath returned %d results' % count) - return _to_unicode(msg.get_string()) + return msg.get_text() - def chdir(self, path): + def chdir(self, path=None): """ Change the "current directory" of this SFTP session. Since SFTP doesn't really have the concept of a current working directory, this @@ -518,7 +518,7 @@ class SFTPClient (BaseSFTP): 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') + self._cwd = b(self.normalize(path)) def getcwd(self): """ @@ -531,7 +531,7 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ - return self._cwd + return self._cwd and u(self._cwd) def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): """ @@ -561,10 +561,9 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ - fr = self.file(remotepath, 'wb') - fr.set_pipelined(True) - size = 0 - try: + with self.file(remotepath, 'wb') as fr: + fr.set_pipelined(True) + size = 0 while True: data = fl.read(32768) fr.write(data) @@ -573,8 +572,6 @@ class SFTPClient (BaseSFTP): callback(size, file_size) if len(data) == 0: break - finally: - fr.close() if confirm: s = self.stat(remotepath) if s.st_size != size: @@ -610,11 +607,8 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ file_size = os.stat(localpath).st_size - fl = file(localpath, 'rb') - try: + with open(localpath, 'rb') as fl: return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) - finally: - fl.close() def getfo(self, remotepath, fl, callback=None): """ @@ -636,10 +630,9 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ - fr = self.file(remotepath, 'rb') - file_size = self.stat(remotepath).st_size - fr.prefetch() - try: + with self.open(remotepath, 'rb') as fr: + file_size = self.stat(remotepath).st_size + fr.prefetch() size = 0 while True: data = fr.read(32768) @@ -649,8 +642,6 @@ class SFTPClient (BaseSFTP): callback(size, file_size) if len(data) == 0: break - finally: - fr.close() return size def get(self, remotepath, localpath, callback=None): @@ -671,11 +662,8 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ file_size = self.stat(remotepath).st_size - fl = file(localpath, 'wb') - try: + with open(localpath, 'wb') as fl: size = self.getfo(remotepath, fl, callback) - finally: - fl.close() s = os.stat(localpath) if s.st_size != size: raise IOError('size mismatch in get! %d != %d' % (s.st_size, size)) @@ -695,11 +683,11 @@ class SFTPClient (BaseSFTP): msg = Message() msg.add_int(self.request_number) for item in arg: - if isinstance(item, int): - msg.add_int(item) - elif isinstance(item, long): + if isinstance(item, long): msg.add_int64(item) - elif isinstance(item, str): + elif isinstance(item, int): + msg.add_int(item) + elif isinstance(item, (string_types, bytes_types)): msg.add_string(item) elif isinstance(item, SFTPAttributes): item._pack(msg) @@ -707,7 +695,7 @@ class SFTPClient (BaseSFTP): raise Exception('unknown type for %r type %r' % (item, type(item))) num = self.request_number self._expecting[num] = fileobj - self._send_packet(t, str(msg)) + self._send_packet(t, msg) self.request_number += 1 finally: self._lock.release() @@ -717,8 +705,8 @@ class SFTPClient (BaseSFTP): while True: try: t, data = self._read_packet() - except EOFError, e: - raise SSHException('Server connection dropped: %s' % (str(e),)) + except EOFError as e: + raise SSHException('Server connection dropped: %s' % str(e)) msg = Message(data) num = msg.get_int() if num not in self._expecting: @@ -752,7 +740,7 @@ class SFTPClient (BaseSFTP): Raises EOFError or IOError on error status; otherwise does nothing. """ code = msg.get_int() - text = msg.get_string() + text = msg.get_text() if code == SFTP_OK: return elif code == SFTP_EOF: @@ -770,16 +758,15 @@ class SFTPClient (BaseSFTP): Return an adjusted path if we're emulating a "current working directory" for the server. """ - if type(path) is unicode: - path = path.encode('utf-8') + path = b(path) if self._cwd is None: return path - if (len(path) > 0) and (path[0] == '/'): + if len(path) and path[0:1] == b_slash: # absolute path return path - if self._cwd == '/': + if self._cwd == b_slash: return self._cwd + path - return self._cwd + '/' + path + return self._cwd + b_slash + path class SFTP (SFTPClient): diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 4ec936d0..f0f7e382 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -94,7 +94,7 @@ class SFTPFile (BufferedFile): k = [i for i in self._prefetch_reads if i[0] <= offset] if len(k) == 0: return False - k.sort(lambda x, y: cmp(x[0], y[0])) + k.sort(key=lambda x: x[0]) buf_offset, buf_size = k[-1] if buf_offset + buf_size <= offset: # prefetch request ends before this one begins @@ -165,7 +165,7 @@ class SFTPFile (BufferedFile): def _write(self, data): # may write less than requested if it would exceed max packet size chunk = min(len(data), self.MAX_REQUEST_SIZE) - self._reqs.append(self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), str(data[:chunk]))) + self._reqs.append(self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), data[:chunk])) if not self.pipelined or (len(self._reqs) > 100 and self.sftp.sock.recv_ready()): while len(self._reqs): req = self._reqs.popleft() @@ -218,7 +218,7 @@ class SFTPFile (BufferedFile): self._realpos = self._pos else: self._realpos = self._pos = self._get_size() + offset - self._rbuffer = '' + self._rbuffer = bytes() def stat(self): """ @@ -348,8 +348,8 @@ class SFTPFile (BufferedFile): """ t, msg = self.sftp._request(CMD_EXTENDED, 'check-file', self.handle, hash_algorithm, long(offset), long(length), block_size) - ext = msg.get_string() - alg = msg.get_string() + ext = msg.get_text() + alg = msg.get_text() data = msg.get_remainder() return data @@ -464,8 +464,8 @@ class SFTPFile (BufferedFile): # save exception and re-raise it on next file operation try: self.sftp._convert_status(msg) - except Exception, x: - self._saved_exception = x + except Exception as e: + self._saved_exception = e return if t != CMD_DATA: raise SFTPError('Expected data') diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py index 29d3d0d8..9fc21008 100644 --- a/paramiko/sftp_handle.py +++ b/paramiko/sftp_handle.py @@ -100,7 +100,7 @@ class SFTPHandle (object): readfile.seek(offset) self.__tell = offset data = readfile.read(length) - except IOError, e: + except IOError as e: self.__tell = None return SFTPServer.convert_errno(e.errno) self.__tell += len(data) @@ -139,7 +139,7 @@ class SFTPHandle (object): self.__tell = offset writefile.write(data) writefile.flush() - except IOError, e: + except IOError as e: self.__tell = None return SFTPServer.convert_errno(e.errno) if self.__tell is not None: diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index 1833c2e8..4539edea 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -92,7 +92,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): except EOFError: self._log(DEBUG, 'EOF -- end of session') return - except Exception, e: + except Exception as e: self._log(DEBUG, 'Exception on channel: ' + str(e)) self._log(DEBUG, util.tb_strings()) return @@ -100,7 +100,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): request_number = msg.get_int() try: self._process(t, request_number, msg) - except Exception, e: + except Exception as e: self._log(DEBUG, 'Exception in server processing: ' + str(e)) self._log(DEBUG, util.tb_strings()) # send some kind of failure message, at least @@ -113,9 +113,9 @@ class SFTPServer (BaseSFTP, SubsystemHandler): self.server.session_ended() super(SFTPServer, self).finish_subsystem() # close any file handles that were left open (so we can return them to the OS quickly) - for f in self.file_table.itervalues(): + for f in self.file_table.values(): f.close() - for f in self.folder_table.itervalues(): + for f in self.folder_table.values(): f.close() self.file_table = {} self.folder_table = {} @@ -166,7 +166,8 @@ class SFTPServer (BaseSFTP, SubsystemHandler): if attr._flags & attr.FLAG_AMTIME: os.utime(filename, (attr.st_atime, attr.st_mtime)) if attr._flags & attr.FLAG_SIZE: - open(filename, 'w+').truncate(attr.st_size) + with open(filename, 'w+') as f: + f.truncate(attr.st_size) set_file_attr = staticmethod(set_file_attr) @@ -177,24 +178,24 @@ class SFTPServer (BaseSFTP, SubsystemHandler): msg = Message() msg.add_int(request_number) for item in arg: - if type(item) is int: - msg.add_int(item) - elif type(item) is long: + if isinstance(item, long): msg.add_int64(item) - elif type(item) is str: + elif isinstance(item, int): + msg.add_int(item) + elif isinstance(item, (string_types, bytes_types)): msg.add_string(item) elif type(item) is SFTPAttributes: item._pack(msg) else: raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item))) - self._send_packet(t, str(msg)) + self._send_packet(t, msg) def _send_handle_response(self, request_number, handle, folder=False): if not issubclass(type(handle), SFTPHandle): # must be error code self._send_status(request_number, handle) return - handle._set_name('hx%d' % self.next_handle) + handle._set_name(b('hx%d' % self.next_handle)) self.next_handle += 1 if folder: self.folder_table[handle._get_name()] = handle @@ -232,16 +233,16 @@ class SFTPServer (BaseSFTP, SubsystemHandler): msg.add_int(len(flist)) for attr in flist: msg.add_string(attr.filename) - msg.add_string(str(attr)) + msg.add_string(attr) attr._pack(msg) - self._send_packet(CMD_NAME, str(msg)) + self._send_packet(CMD_NAME, msg) def _check_file(self, request_number, msg): # this extension actually comes from v6 protocol, but since it's an # extension, i feel like we can reasonably support it backported. # it's very useful for verifying uploaded files or checking for # rsync-like differences between local and remote files. - handle = msg.get_string() + handle = msg.get_binary() alg_list = msg.get_list() start = msg.get_int64() length = msg.get_int64() @@ -270,7 +271,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): self._send_status(request_number, SFTP_FAILURE, 'Block size too small') return - sum_out = '' + sum_out = bytes() offset = start while offset < start + length: blocklen = min(block_size, start + length - offset) @@ -280,7 +281,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): hash_obj = alg.new() while count < blocklen: data = f.read(offset, chunklen) - if not type(data) is str: + if not isinstance(data, bytes_types): self._send_status(request_number, data, 'Unable to hash file') return hash_obj.update(data) @@ -293,7 +294,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): msg.add_string('check-file') msg.add_string(algname) msg.add_bytes(sum_out) - self._send_packet(CMD_EXTENDED_REPLY, str(msg)) + self._send_packet(CMD_EXTENDED_REPLY, msg) def _convert_pflags(self, pflags): "convert SFTP-style open() flags to python's os.open() flags" @@ -316,12 +317,12 @@ class SFTPServer (BaseSFTP, SubsystemHandler): def _process(self, t, request_number, msg): self._log(DEBUG, 'Request: %s' % CMD_NAMES[t]) if t == CMD_OPEN: - path = msg.get_string() + path = msg.get_text() flags = self._convert_pflags(msg.get_int()) attr = SFTPAttributes._from_msg(msg) self._send_handle_response(request_number, self.server.open(path, flags, attr)) elif t == CMD_CLOSE: - handle = msg.get_string() + handle = msg.get_binary() if handle in self.folder_table: del self.folder_table[handle] self._send_status(request_number, SFTP_OK) @@ -333,14 +334,14 @@ class SFTPServer (BaseSFTP, SubsystemHandler): return self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') elif t == CMD_READ: - handle = msg.get_string() + handle = msg.get_binary() offset = msg.get_int64() length = msg.get_int() if handle not in self.file_table: self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') return data = self.file_table[handle].read(offset, length) - if type(data) is str: + if isinstance(data, (bytes_types, string_types)): if len(data) == 0: self._send_status(request_number, SFTP_EOF) else: @@ -348,54 +349,54 @@ class SFTPServer (BaseSFTP, SubsystemHandler): else: self._send_status(request_number, data) elif t == CMD_WRITE: - handle = msg.get_string() + handle = msg.get_binary() offset = msg.get_int64() - data = msg.get_string() + data = msg.get_binary() if handle not in self.file_table: self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') return self._send_status(request_number, self.file_table[handle].write(offset, data)) elif t == CMD_REMOVE: - path = msg.get_string() + path = msg.get_text() self._send_status(request_number, self.server.remove(path)) elif t == CMD_RENAME: - oldpath = msg.get_string() - newpath = msg.get_string() + oldpath = msg.get_text() + newpath = msg.get_text() self._send_status(request_number, self.server.rename(oldpath, newpath)) elif t == CMD_MKDIR: - path = msg.get_string() + path = msg.get_text() attr = SFTPAttributes._from_msg(msg) self._send_status(request_number, self.server.mkdir(path, attr)) elif t == CMD_RMDIR: - path = msg.get_string() + path = msg.get_text() self._send_status(request_number, self.server.rmdir(path)) elif t == CMD_OPENDIR: - path = msg.get_string() + path = msg.get_text() self._open_folder(request_number, path) return elif t == CMD_READDIR: - handle = msg.get_string() + handle = msg.get_binary() if handle not in self.folder_table: self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') return folder = self.folder_table[handle] self._read_folder(request_number, folder) elif t == CMD_STAT: - path = msg.get_string() + path = msg.get_text() resp = self.server.stat(path) if issubclass(type(resp), SFTPAttributes): self._response(request_number, CMD_ATTRS, resp) else: self._send_status(request_number, resp) elif t == CMD_LSTAT: - path = msg.get_string() + path = msg.get_text() resp = self.server.lstat(path) if issubclass(type(resp), SFTPAttributes): self._response(request_number, CMD_ATTRS, resp) else: self._send_status(request_number, resp) elif t == CMD_FSTAT: - handle = msg.get_string() + handle = msg.get_binary() if handle not in self.file_table: self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') return @@ -405,34 +406,34 @@ class SFTPServer (BaseSFTP, SubsystemHandler): else: self._send_status(request_number, resp) elif t == CMD_SETSTAT: - path = msg.get_string() + path = msg.get_text() attr = SFTPAttributes._from_msg(msg) self._send_status(request_number, self.server.chattr(path, attr)) elif t == CMD_FSETSTAT: - handle = msg.get_string() + handle = msg.get_binary() attr = SFTPAttributes._from_msg(msg) if handle not in self.file_table: self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle') return self._send_status(request_number, self.file_table[handle].chattr(attr)) elif t == CMD_READLINK: - path = msg.get_string() + path = msg.get_text() resp = self.server.readlink(path) - if type(resp) is str: + if isinstance(resp, (bytes_types, string_types)): self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes()) else: self._send_status(request_number, resp) elif t == CMD_SYMLINK: # the sftp 2 draft is incorrect here! path always follows target_path - target_path = msg.get_string() - path = msg.get_string() + target_path = msg.get_text() + path = msg.get_text() self._send_status(request_number, self.server.symlink(target_path, path)) elif t == CMD_REALPATH: - path = msg.get_string() + path = msg.get_text() rpath = self.server.canonicalize(path) self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes()) elif t == CMD_EXTENDED: - tag = msg.get_string() + tag = msg.get_text() if tag == 'check-file': self._check_file(request_number, msg) else: diff --git a/paramiko/transport.py b/paramiko/transport.py index 3155d3f8..6ab9274c 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -79,7 +79,8 @@ class SecurityOptions (object): C{ValueError} will be raised. If you try to assign something besides a tuple to one of the fields, C{TypeError} will be raised. """ - __slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', 'compression', '_transport' ] + #__slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', 'compression', '_transport' ] + __slots__ = '_transport' def __init__(self, transport): self._transport = transport @@ -112,8 +113,8 @@ class SecurityOptions (object): x = tuple(x) if type(x) is not tuple: raise TypeError('expected tuple or list') - possible = getattr(self._transport, orig).keys() - forbidden = filter(lambda n: n not in possible, x) + possible = list(getattr(self._transport, orig).keys()) + forbidden = [n for n in x if n not in possible] if len(forbidden) > 0: raise ValueError('unknown cipher') setattr(self._transport, name, x) @@ -177,7 +178,7 @@ class ChannelMap (object): def values(self): self._lock.acquire() try: - return self._map.values() + return list(self._map.values()) finally: self._lock.release() @@ -276,7 +277,7 @@ class Transport (threading.Thread): @param sock: a socket or socket-like object to create the session over. @type sock: socket """ - if isinstance(sock, (str, unicode)): + if isinstance(sock, string_types): # convert "host:port" into (host, port) hl = sock.split(':', 1) if len(hl) == 1: @@ -294,7 +295,7 @@ class Transport (threading.Thread): sock = socket.socket(af, socket.SOCK_STREAM) try: retry_on_signal(lambda: sock.connect((hostname, port))) - except socket.error, e: + except socket.error as e: reason = str(e) else: break @@ -376,7 +377,7 @@ class Transport (threading.Thread): @rtype: str """ - out = '= 0: comment = buf[i+1:] buf = buf[:i] @@ -1721,13 +1724,13 @@ class Transport (threading.Thread): 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) + available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, + self._preferred_keys)) else: available_server_keys = self._preferred_keys m = Message() - m.add_byte(chr(MSG_KEXINIT)) + m.add_byte(cMSG_KEXINIT) m.add_bytes(rng.read(16)) m.add_list(self._preferred_kex) m.add_list(available_server_keys) @@ -1737,12 +1740,12 @@ class Transport (threading.Thread): m.add_list(self._preferred_macs) m.add_list(self._preferred_compression) m.add_list(self._preferred_compression) - m.add_string('') - m.add_string('') + m.add_string(bytes()) + m.add_string(bytes()) m.add_boolean(False) m.add_int(0) # save a copy for later (needed to compute a hash) - self.local_kex_init = str(m) + self.local_kex_init = m.asbytes() self._send_message(m) def _parse_kex_init(self, m): @@ -1774,19 +1777,19 @@ class Transport (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 = list(filter(self._preferred_kex.__contains__, kex_algo_list)) else: - agreed_kex = filter(kex_algo_list.__contains__, self._preferred_kex) + agreed_kex = list(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) - agreed_keys = filter(available_server_keys.__contains__, server_key_algo_list) + available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, + self._preferred_keys)) + agreed_keys = list(filter(available_server_keys.__contains__, server_key_algo_list)) else: - agreed_keys = filter(server_key_algo_list.__contains__, self._preferred_keys) + agreed_keys = list(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] @@ -1794,15 +1797,15 @@ class Transport (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__, - server_encrypt_algo_list) - agreed_remote_ciphers = filter(self._preferred_ciphers.__contains__, - client_encrypt_algo_list) + agreed_local_ciphers = list(filter(self._preferred_ciphers.__contains__, + server_encrypt_algo_list)) + agreed_remote_ciphers = list(filter(self._preferred_ciphers.__contains__, + client_encrypt_algo_list)) else: - agreed_local_ciphers = filter(client_encrypt_algo_list.__contains__, - self._preferred_ciphers) - agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__, - self._preferred_ciphers) + agreed_local_ciphers = list(filter(client_encrypt_algo_list.__contains__, + self._preferred_ciphers)) + agreed_remote_ciphers = list(filter(server_encrypt_algo_list.__contains__, + 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] @@ -1810,22 +1813,22 @@ class Transport (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 = list(filter(self._preferred_macs.__contains__, client_mac_algo_list)) + agreed_local_macs = list(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 = list(filter(client_mac_algo_list.__contains__, self._preferred_macs)) + agreed_remote_macs = list(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] self.remote_mac = agreed_remote_macs[0] if self.server_mode: - agreed_remote_compression = filter(self._preferred_compression.__contains__, client_compress_algo_list) - agreed_local_compression = filter(self._preferred_compression.__contains__, server_compress_algo_list) + agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list)) + agreed_local_compression = list(filter(self._preferred_compression.__contains__, server_compress_algo_list)) else: - agreed_local_compression = filter(client_compress_algo_list.__contains__, self._preferred_compression) - agreed_remote_compression = filter(server_compress_algo_list.__contains__, self._preferred_compression) + agreed_local_compression = list(filter(client_compress_algo_list.__contains__, self._preferred_compression)) + agreed_remote_compression = list(filter(server_compress_algo_list.__contains__, self._preferred_compression)) if (len(agreed_local_compression) == 0) or (len(agreed_remote_compression) == 0): raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression)) self.local_compression = agreed_local_compression[0] @@ -1840,7 +1843,7 @@ class Transport (threading.Thread): # actually some extra bytes (one NUL byte in openssh's case) added to # the end of the packet but not parsed. turns out we need to throw # away those bytes because they aren't part of the hash. - self.remote_kex_init = chr(MSG_KEXINIT) + m.get_so_far() + self.remote_kex_init = cMSG_KEXINIT + m.get_so_far() def _activate_inbound(self): "switch on newly negotiated encryption parameters for inbound traffic" @@ -1869,7 +1872,7 @@ class Transport (threading.Thread): def _activate_outbound(self): "switch on newly negotiated encryption parameters for outbound traffic" m = Message() - m.add_byte(chr(MSG_NEWKEYS)) + m.add_byte(cMSG_NEWKEYS) self._send_message(m) block_size = self._cipher_info[self.local_cipher]['block-size'] if self.server_mode: @@ -1938,24 +1941,24 @@ class Transport (threading.Thread): def _parse_disconnect(self, m): code = m.get_int() - desc = m.get_string() + desc = m.get_text() self._log(INFO, 'Disconnect (code %d): %s' % (code, desc)) def _parse_global_request(self, m): - kind = m.get_string() + kind = m.get_text() self._log(DEBUG, 'Received global request "%s"' % kind) want_reply = m.get_boolean() if not self.server_mode: self._log(DEBUG, 'Rejecting "%s" global request from server.' % kind) ok = False elif kind == 'tcpip-forward': - address = m.get_string() + address = m.get_text() port = m.get_int() ok = self.server_object.check_port_forward_request(address, port) if ok != False: ok = (ok,) elif kind == 'cancel-tcpip-forward': - address = m.get_string() + address = m.get_text() port = m.get_int() self.server_object.cancel_port_forward_request(address, port) ok = True @@ -1968,10 +1971,10 @@ class Transport (threading.Thread): if want_reply: msg = Message() if ok: - msg.add_byte(chr(MSG_REQUEST_SUCCESS)) + msg.add_byte(cMSG_REQUEST_SUCCESS) msg.add(*extra) else: - msg.add_byte(chr(MSG_REQUEST_FAILURE)) + msg.add_byte(cMSG_REQUEST_FAILURE) self._send_message(msg) def _parse_request_success(self, m): @@ -2009,8 +2012,8 @@ class Transport (threading.Thread): def _parse_channel_open_failure(self, m): chanid = m.get_int() reason = m.get_int() - reason_str = m.get_string() - lang = m.get_string() + reason_str = m.get_text() + lang = m.get_text() reason_text = CONNECTION_FAILED_CODE.get(reason, '(unknown code)') self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) self.lock.acquire() @@ -2026,7 +2029,7 @@ class Transport (threading.Thread): return def _parse_channel_open(self, m): - kind = m.get_string() + kind = m.get_text() chanid = m.get_int() initial_window_size = m.get_int() max_packet_size = m.get_int() @@ -2039,7 +2042,7 @@ class Transport (threading.Thread): finally: self.lock.release() elif (kind == 'x11') and (self._x11_handler is not None): - origin_addr = m.get_string() + origin_addr = m.get_text() origin_port = m.get_int() self._log(DEBUG, 'Incoming x11 connection from %s:%d' % (origin_addr, origin_port)) self.lock.acquire() @@ -2048,9 +2051,9 @@ class Transport (threading.Thread): finally: self.lock.release() elif (kind == 'forwarded-tcpip') and (self._tcp_handler is not None): - server_addr = m.get_string() + server_addr = m.get_text() server_port = m.get_int() - origin_addr = m.get_string() + origin_addr = m.get_text() origin_port = m.get_int() self._log(DEBUG, 'Incoming tcp forwarded connection from %s:%d' % (origin_addr, origin_port)) self.lock.acquire() @@ -2070,9 +2073,9 @@ class Transport (threading.Thread): self.lock.release() if kind == 'direct-tcpip': # handle direct-tcpip requests comming from the client - dest_addr = m.get_string() + dest_addr = m.get_text() dest_port = m.get_int() - origin_addr = m.get_string() + origin_addr = m.get_text() origin_port = m.get_int() reason = self.server_object.check_channel_direct_tcpip_request( my_chanid, (origin_addr, origin_port), @@ -2084,7 +2087,7 @@ class Transport (threading.Thread): reject = True if reject: msg = Message() - msg.add_byte(chr(MSG_CHANNEL_OPEN_FAILURE)) + msg.add_byte(cMSG_CHANNEL_OPEN_FAILURE) msg.add_int(chanid) msg.add_int(reason) msg.add_string('') @@ -2103,7 +2106,7 @@ class Transport (threading.Thread): finally: self.lock.release() m = Message() - m.add_byte(chr(MSG_CHANNEL_OPEN_SUCCESS)) + m.add_byte(cMSG_CHANNEL_OPEN_SUCCESS) m.add_int(chanid) m.add_int(my_chanid) m.add_int(self.window_size) diff --git a/paramiko/util.py b/paramiko/util.py index 85ee6b06..e9f6250c 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -48,60 +48,53 @@ if sys.version_info < (2,3): def inflate_long(s, always_positive=False): "turns a normalized byte string into a long-int (adapted from Crypto.Util.number)" - out = 0L + out = long(0) negative = 0 - if not always_positive and (len(s) > 0) and (ord(s[0]) >= 0x80): + if not always_positive and (len(s) > 0) and (byte_ord(s[0]) >= 0x80): negative = 1 if len(s) % 4: - filler = '\x00' + filler = zero_byte if negative: - filler = '\xff' + filler = max_byte s = filler * (4 - len(s) % 4) + s for i in range(0, len(s), 4): out = (out << 32) + struct.unpack('>I', s[i:i+4])[0] if negative: - out -= (1L << (8 * len(s))) + out -= (long(1) << (8 * len(s))) return out +deflate_zero = zero_byte if PY2 else 0 +deflate_ff = max_byte if PY2 else 0xff + def deflate_long(n, add_sign_padding=True): "turns a long-int into a normalized byte string (adapted from Crypto.Util.number)" # after much testing, this algorithm was deemed to be the fastest - s = '' + s = bytes() n = long(n) while (n != 0) and (n != -1): - s = struct.pack('>I', n & 0xffffffffL) + s + s = struct.pack('>I', n & xffffffff) + s n = n >> 32 # strip off leading zeros, FFs for i in enumerate(s): - if (n == 0) and (i[1] != '\000'): + if (n == 0) and (i[1] != deflate_zero): break - if (n == -1) and (i[1] != '\xff'): + if (n == -1) and (i[1] != deflate_ff): break else: # degenerate case, n was either 0 or -1 i = (0,) if n == 0: - s = '\000' + s = zero_byte else: - s = '\xff' + s = max_byte s = s[i[0]:] if add_sign_padding: - if (n == 0) and (ord(s[0]) >= 0x80): - s = '\x00' + s - if (n == -1) and (ord(s[0]) < 0x80): - s = '\xff' + s + if (n == 0) and (byte_ord(s[0]) >= 0x80): + s = zero_byte + s + if (n == -1) and (byte_ord(s[0]) < 0x80): + s = max_byte + s return s -def format_binary_weird(data): - out = '' - for i in enumerate(data): - out += '%02X' % ord(i[1]) - if i[0] % 2: - out += ' ' - if i[0] % 16 == 15: - out += '\n' - return out - def format_binary(data, prefix=''): x = 0 out = [] @@ -113,8 +106,8 @@ def format_binary(data, prefix=''): return [prefix + x for x in out] def format_binary_line(data): - left = ' '.join(['%02X' % ord(c) for c in data]) - right = ''.join([('.%c..' % c)[(ord(c)+63)//95] for c in data]) + left = ' '.join(['%02X' % byte_ord(c) for c in data]) + right = ''.join([('.%c..' % c)[(byte_ord(c)+63)//95] for c in data]) return '%-50s %s' % (left, right) def hexify(s): @@ -126,24 +119,27 @@ def unhexify(s): def safe_string(s): out = '' for c in s: - if (ord(c) >= 32) and (ord(c) <= 127): + if (byte_ord(c) >= 32) and (byte_ord(c) <= 127): out += c else: - out += '%%%02X' % ord(c) + out += '%%%02X' % byte_ord(c) return out # ''.join([['%%%02X' % ord(c), c][(ord(c) >= 32) and (ord(c) <= 127)] for c in 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 - bitlen -= 1 - return bitlen + try: + return n.bitlength() + except AttributeError: + norm = deflate_long(n, 0) + hbyte = byte_ord(norm[0]) + if hbyte == 0: + return 1 + bitlen = len(norm) * 8 + while not (hbyte & 0x80): + hbyte <<= 1 + bitlen -= 1 + return bitlen def tb_strings(): return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') @@ -158,7 +154,7 @@ def generate_key_bytes(hashclass, salt, key, nbytes): hashing function (like C{MD5} or C{SHA}). @type hashclass: L{Crypto.Hash} @param salt: data to salt the hash with. - @type salt: string + @type salt: byte string @param key: human-entered password or passphrase. @type key: string @param nbytes: number of bytes to generate. @@ -166,15 +162,15 @@ def generate_key_bytes(hashclass, salt, key, nbytes): @return: key data @rtype: string """ - keydata = '' - digest = '' + keydata = bytes() + digest = bytes() if len(salt) > 8: salt = salt[:8] while nbytes > 0: hash_obj = hashclass.new() if len(digest) > 0: hash_obj.update(digest) - hash_obj.update(key) + hash_obj.update(b(key)) hash_obj.update(salt) digest = hash_obj.digest() size = min(nbytes, len(digest)) @@ -276,36 +272,36 @@ def retry_on_signal(function): while True: try: return function() - except EnvironmentError, e: + except EnvironmentError as e: if e.errno != errno.EINTR: raise class Counter (object): """Stateful counter for CTR mode crypto""" - def __init__(self, nbits, initial_value=1L, overflow=0L): + def __init__(self, nbits, initial_value=long(1), overflow=long(0)): 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) + self.value = array.array('c', max_byte * self.blocksize) else: x = deflate_long(initial_value - 1, add_sign_padding=False) - self.value = array.array('c', '\x00' * (self.blocksize - len(x)) + x) + self.value = array.array('c', zero_byte * (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': + c = self.value[i] = byte_chr((byte_ord(self.value[i]) + 1) % 256) + if c != zero_byte: 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) + self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x) return self.value.tostring() - def new(cls, nbits, initial_value=1L, overflow=0L): + def new(cls, nbits, initial_value=long(1), overflow=long(0)): return cls(nbits, initial_value=initial_value, overflow=overflow) new = classmethod(new) diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py index de1cd64b..3c23f2ff 100644 --- a/paramiko/win_pageant.py +++ b/paramiko/win_pageant.py @@ -21,13 +21,12 @@ Functions for communicating with Pageant, the basic windows ssh agent program. """ -from __future__ import with_statement - import struct import threading import array import platform import ctypes.wintypes +from paramiko.util import * from . import _winapi @@ -82,7 +81,7 @@ def _query_pageant(msg): with pymap: pymap.write(msg) # Create an array buffer containing the mapped filename - char_buffer = array.array("c", map_name + '\0') + char_buffer = array.array("c", b(map_name) + zero_byte) char_buffer_address, char_buffer_size = char_buffer.buffer_info() # Create a string to use for the SendMessage function call cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, diff --git a/setup.py b/setup.py index f50cfd23..be404b1b 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ if sys.platform == 'darwin': setup(name = "paramiko", - version = "1.12.1", + version = "1.13.0", description = "SSH2 protocol library", author = "Jeff Forcier", author_email = "jeff@bitprophet.org", diff --git a/test.py b/test.py index 6702e53a..bd966d1e 100755 --- a/test.py +++ b/test.py @@ -29,22 +29,21 @@ import unittest from optparse import OptionParser import paramiko import threading +from paramiko.py3compat import PY2 sys.path.append('tests') -from test_message import MessageTest -from test_file import BufferedFileTest -from test_buffered_pipe import BufferedPipeTest -from test_util import UtilTest -from test_hostkeys import HostKeysTest -from test_pkey import KeyTest -from test_kex import KexTest -from test_packetizer import PacketizerTest -from test_auth import AuthTest -from test_transport import TransportTest -from test_sftp import SFTPTest -from test_sftp_big import BigSFTPTest -from test_client import SSHClientTest +from tests.test_message import MessageTest +from tests.test_file import BufferedFileTest +from tests.test_buffered_pipe import BufferedPipeTest +from tests.test_util import UtilTest +from tests.test_hostkeys import HostKeysTest +from tests.test_pkey import KeyTest +from tests.test_kex import KexTest +from tests.test_packetizer import PacketizerTest +from tests.test_auth import AuthTest +from tests.test_transport import TransportTest +from tests.test_client import SSHClientTest default_host = 'localhost' default_user = os.environ.get('USER', 'nobody') @@ -109,13 +108,16 @@ def main(): paramiko.util.log_to_file('test.log') if options.use_sftp: + from tests.test_sftp import SFTPTest if options.use_loopback_sftp: SFTPTest.init_loopback() else: SFTPTest.init(options.hostname, options.username, options.keyfile, options.password) if not options.use_big_file: SFTPTest.set_big_file_test(False) - + if options.use_big_file: + from tests.test_sftp_big import BigSFTPTest + suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(MessageTest)) suite.addTest(unittest.makeSuite(BufferedFileTest)) @@ -147,7 +149,10 @@ def main(): # TODO: make that not a problem, jeez for thread in threading.enumerate(): if thread is not threading.currentThread(): - thread._Thread__stop() + if PY2: + thread._Thread__stop() + else: + thread._stop() # Exit correctly if not result.wasSuccessful(): sys.exit(1) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/loop.py b/tests/loop.py index 91c216d2..6e933f83 100644 --- a/tests/loop.py +++ b/tests/loop.py @@ -21,6 +21,7 @@ """ import threading, socket +from paramiko.common import * class LoopSocket (object): @@ -31,7 +32,7 @@ class LoopSocket (object): """ def __init__(self): - self.__in_buffer = '' + self.__in_buffer = bytes() self.__lock = threading.Lock() self.__cv = threading.Condition(self.__lock) self.__timeout = None @@ -41,11 +42,12 @@ class LoopSocket (object): self.__unlink() try: self.__lock.acquire() - self.__in_buffer = '' + self.__in_buffer = bytes() finally: self.__lock.release() def send(self, data): + data = asbytes(data) if self.__mate is None: # EOF raise EOFError() @@ -57,7 +59,7 @@ class LoopSocket (object): try: if self.__mate is None: # EOF - return '' + return bytes() if len(self.__in_buffer) == 0: self.__cv.wait(self.__timeout) if len(self.__in_buffer) == 0: diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py index 3021d816..58e4be26 100644 --- a/tests/stub_sftp.py +++ b/tests/stub_sftp.py @@ -21,8 +21,10 @@ A stub SFTP server for loopback SFTP testing. """ import os +import sys from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \ SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED +from paramiko.common import * class StubServer (ServerInterface): @@ -38,7 +40,7 @@ class StubSFTPHandle (SFTPHandle): def stat(self): try: return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno())) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) def chattr(self, attr): @@ -47,7 +49,7 @@ class StubSFTPHandle (SFTPHandle): try: SFTPServer.set_file_attr(self.filename, attr) return SFTP_OK - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) @@ -69,21 +71,21 @@ class StubSFTPServer (SFTPServerInterface): attr.filename = fname out.append(attr) return out - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) def stat(self, path): path = self._realpath(path) try: return SFTPAttributes.from_stat(os.stat(path)) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) def lstat(self, path): path = self._realpath(path) try: return SFTPAttributes.from_stat(os.lstat(path)) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) def open(self, path, flags, attr): @@ -97,8 +99,8 @@ class StubSFTPServer (SFTPServerInterface): else: # os.open() defaults to 0777 which is # an odd default mode for files - fd = os.open(path, flags, 0666) - except OSError, e: + fd = os.open(path, flags, o666) + except OSError as e: return SFTPServer.convert_errno(e.errno) if (flags & os.O_CREAT) and (attr is not None): attr._flags &= ~attr.FLAG_PERMISSIONS @@ -118,7 +120,7 @@ class StubSFTPServer (SFTPServerInterface): fstr = 'rb' try: f = os.fdopen(fd, fstr) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) fobj = StubSFTPHandle(flags) fobj.filename = path @@ -130,7 +132,7 @@ class StubSFTPServer (SFTPServerInterface): path = self._realpath(path) try: os.remove(path) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) return SFTP_OK @@ -139,7 +141,7 @@ class StubSFTPServer (SFTPServerInterface): newpath = self._realpath(newpath) try: os.rename(oldpath, newpath) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) return SFTP_OK @@ -149,7 +151,7 @@ class StubSFTPServer (SFTPServerInterface): os.mkdir(path) if attr is not None: SFTPServer.set_file_attr(path, attr) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) return SFTP_OK @@ -157,7 +159,7 @@ class StubSFTPServer (SFTPServerInterface): path = self._realpath(path) try: os.rmdir(path) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) return SFTP_OK @@ -165,7 +167,7 @@ class StubSFTPServer (SFTPServerInterface): path = self._realpath(path) try: SFTPServer.set_file_attr(path, attr) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) return SFTP_OK @@ -185,7 +187,7 @@ class StubSFTPServer (SFTPServerInterface): target_path = '' try: os.symlink(target_path, path) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) return SFTP_OK @@ -193,7 +195,7 @@ class StubSFTPServer (SFTPServerInterface): path = self._realpath(path) try: symlink = os.readlink(path) - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) # if it's absolute, remove the root if os.path.isabs(symlink): diff --git a/tests/test_auth.py b/tests/test_auth.py index 61fe63f4..d26b1807 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -29,14 +29,18 @@ from paramiko import Transport, ServerInterface, RSAKey, DSSKey, \ AuthenticationException from paramiko import AUTH_FAILED, AUTH_PARTIALLY_SUCCESSFUL, AUTH_SUCCESSFUL from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED -from loop import LoopSocket +from paramiko.py3compat import u +from tests.loop import LoopSocket +from tests.util import test_path + +_pwd = u('\u2022') class NullServer (ServerInterface): paranoid_did_password = False paranoid_did_public_key = False - paranoid_key = DSSKey.from_private_key_file('tests/test_dss.key') - + paranoid_key = DSSKey.from_private_key_file(test_path('test_dss.key')) + def get_allowed_auths(self, username): if username == 'slowdive': return 'publickey,password' @@ -64,7 +68,7 @@ class NullServer (ServerInterface): if self.paranoid_did_public_key: return AUTH_SUCCESSFUL return AUTH_PARTIALLY_SUCCESSFUL - if (username == 'utf8') and (password == u'\u2022'): + if (username == 'utf8') and (password == _pwd): return AUTH_SUCCESSFUL if (username == 'non-utf8') and (password == '\xff'): return AUTH_SUCCESSFUL @@ -110,18 +114,18 @@ class AuthTest (unittest.TestCase): self.sockc.close() def start_server(self): - host_key = RSAKey.from_private_key_file('tests/test_rsa.key') - self.public_host_key = RSAKey(data=str(host_key)) + host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + self.public_host_key = RSAKey(data=host_key.asbytes()) self.ts.add_server_key(host_key) self.event = threading.Event() self.server = NullServer() - self.assert_(not self.event.isSet()) + self.assertTrue(not self.event.isSet()) self.ts.start_server(self.event, self.server) def verify_finished(self): self.event.wait(1.0) - self.assert_(self.event.isSet()) - self.assert_(self.ts.is_active()) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) def test_1_bad_auth_type(self): """ @@ -132,11 +136,11 @@ class AuthTest (unittest.TestCase): try: self.tc.connect(hostkey=self.public_host_key, username='unknown', password='error') - self.assert_(False) + self.assertTrue(False) except: etype, evalue, etb = sys.exc_info() - self.assertEquals(BadAuthenticationType, etype) - self.assertEquals(['publickey'], evalue.allowed_types) + self.assertEqual(BadAuthenticationType, etype) + self.assertEqual(['publickey'], evalue.allowed_types) def test_2_bad_password(self): """ @@ -147,10 +151,10 @@ class AuthTest (unittest.TestCase): self.tc.connect(hostkey=self.public_host_key) try: self.tc.auth_password(username='slowdive', password='error') - self.assert_(False) + self.assertTrue(False) except: etype, evalue, etb = sys.exc_info() - self.assert_(issubclass(etype, AuthenticationException)) + self.assertTrue(issubclass(etype, AuthenticationException)) self.tc.auth_password(username='slowdive', password='pygmalion') self.verify_finished() @@ -161,10 +165,10 @@ class AuthTest (unittest.TestCase): self.start_server() self.tc.connect(hostkey=self.public_host_key) remain = self.tc.auth_password(username='paranoid', password='paranoid') - self.assertEquals(['publickey'], remain) - key = DSSKey.from_private_key_file('tests/test_dss.key') + self.assertEqual(['publickey'], remain) + key = DSSKey.from_private_key_file(test_path('test_dss.key')) remain = self.tc.auth_publickey(username='paranoid', key=key) - self.assertEquals([], remain) + self.assertEqual([], remain) self.verify_finished() def test_4_interactive_auth(self): @@ -180,9 +184,9 @@ class AuthTest (unittest.TestCase): self.got_prompts = prompts return ['cat'] remain = self.tc.auth_interactive('commie', handler) - self.assertEquals(self.got_title, 'password') - self.assertEquals(self.got_prompts, [('Password', False)]) - self.assertEquals([], remain) + self.assertEqual(self.got_title, 'password') + self.assertEqual(self.got_prompts, [('Password', False)]) + self.assertEqual([], remain) self.verify_finished() def test_5_interactive_auth_fallback(self): @@ -193,7 +197,7 @@ class AuthTest (unittest.TestCase): self.start_server() self.tc.connect(hostkey=self.public_host_key) remain = self.tc.auth_password('commie', 'cat') - self.assertEquals([], remain) + self.assertEqual([], remain) self.verify_finished() def test_6_auth_utf8(self): @@ -202,8 +206,8 @@ class AuthTest (unittest.TestCase): """ self.start_server() self.tc.connect(hostkey=self.public_host_key) - remain = self.tc.auth_password('utf8', u'\u2022') - self.assertEquals([], remain) + remain = self.tc.auth_password('utf8', _pwd) + self.assertEqual([], remain) self.verify_finished() def test_7_auth_non_utf8(self): @@ -214,7 +218,7 @@ class AuthTest (unittest.TestCase): self.start_server() self.tc.connect(hostkey=self.public_host_key) remain = self.tc.auth_password('non-utf8', '\xff') - self.assertEquals([], remain) + self.assertEqual([], remain) self.verify_finished() def test_8_auth_gets_disconnected(self): @@ -228,4 +232,4 @@ class AuthTest (unittest.TestCase): remain = self.tc.auth_password('bad-server', 'hello') except: etype, evalue, etb = sys.exc_info() - self.assert_(issubclass(etype, AuthenticationException)) + self.assertTrue(issubclass(etype, AuthenticationException)) diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index 47ece936..b9d2bef4 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -25,8 +25,9 @@ import time import unittest from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe +from paramiko.py3compat import b -from util import ParamikoTest +from tests.util import ParamikoTest def delay_thread(pipe): @@ -44,39 +45,39 @@ def close_thread(pipe): class BufferedPipeTest(ParamikoTest): def test_1_buffered_pipe(self): p = BufferedPipe() - self.assert_(not p.read_ready()) + self.assertTrue(not p.read_ready()) p.feed('hello.') - self.assert_(p.read_ready()) + self.assertTrue(p.read_ready()) data = p.read(6) - self.assertEquals('hello.', data) + self.assertEqual(b'hello.', data) p.feed('plus/minus') - self.assertEquals('plu', p.read(3)) - self.assertEquals('s/m', p.read(3)) - self.assertEquals('inus', p.read(4)) + self.assertEqual(b'plu', p.read(3)) + self.assertEqual(b's/m', p.read(3)) + self.assertEqual(b'inus', p.read(4)) p.close() - self.assert_(not p.read_ready()) - self.assertEquals('', p.read(1)) + self.assertTrue(not p.read_ready()) + self.assertEqual(b'', p.read(1)) def test_2_delay(self): p = BufferedPipe() - self.assert_(not p.read_ready()) + self.assertTrue(not p.read_ready()) threading.Thread(target=delay_thread, args=(p,)).start() - self.assertEquals('a', p.read(1, 0.1)) + self.assertEqual(b'a', p.read(1, 0.1)) try: p.read(1, 0.1) - self.assert_(False) + self.assertTrue(False) except PipeTimeout: pass - self.assertEquals('b', p.read(1, 1.0)) - self.assertEquals('', p.read(1)) + self.assertEqual(b'b', p.read(1, 1.0)) + self.assertEqual(b'', p.read(1)) def test_3_close_while_reading(self): p = BufferedPipe() threading.Thread(target=close_thread, args=(p,)).start() data = p.read(1, 1.0) - self.assertEquals('', data) + self.assertEqual(b'', data) def test_4_or_pipe(self): p = pipe.make_pipe() diff --git a/tests/test_client.py b/tests/test_client.py index fae1d329..97150979 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -20,16 +20,14 @@ Some unit tests for SSHClient. """ -from __future__ import with_statement # Python 2.5 support import socket +from tempfile import mkstemp import threading -import time import unittest import weakref import warnings import os -from binascii import hexlify - +from tests.util import test_path import paramiko @@ -46,7 +44,7 @@ class NullServer (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - if (key.get_name() == 'ssh-dss') and (hexlify(key.get_fingerprint()) == '4478f0b9a23cc5182009ff755bc1d26c'): + if (key.get_name() == 'ssh-dss') and key.get_fingerprint() == b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c': return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -67,8 +65,6 @@ class SSHClientTest (unittest.TestCase): self.sockl.listen(1) self.addr, self.port = self.sockl.getsockname() self.event = threading.Event() - thread = threading.Thread(target=self._run) - thread.start() def tearDown(self): for attr in "tc ts socks sockl".split(): @@ -78,28 +74,28 @@ class SSHClientTest (unittest.TestCase): def _run(self): self.socks, addr = self.sockl.accept() self.ts = paramiko.Transport(self.socks) - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) self.ts.add_server_key(host_key) server = NullServer() self.ts.start_server(self.event, server) - def test_1_client(self): """ verify that the SSHClient stuff works too. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.event.wait(1.0) - self.assert_(self.event.isSet()) - self.assert_(self.ts.is_active()) - self.assertEquals('slowdive', self.ts.get_username()) - self.assertEquals(True, self.ts.is_authenticated()) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) + self.assertEqual('slowdive', self.ts.get_username()) + self.assertEqual(True, self.ts.is_authenticated()) stdin, stdout, stderr = self.tc.exec_command('yes') schan = self.ts.accept(1.0) @@ -108,10 +104,10 @@ class SSHClientTest (unittest.TestCase): schan.send_stderr('This is on stderr.\n') schan.close() - self.assertEquals('Hello there.\n', stdout.readline()) - self.assertEquals('', stdout.readline()) - self.assertEquals('This is on stderr.\n', stderr.readline()) - self.assertEquals('', stderr.readline()) + self.assertEqual('Hello there.\n', stdout.readline()) + self.assertEqual('', stdout.readline()) + self.assertEqual('This is on stderr.\n', stderr.readline()) + self.assertEqual('', stderr.readline()) stdin.close() stdout.close() @@ -121,18 +117,19 @@ class SSHClientTest (unittest.TestCase): """ verify that SSHClient works with a DSA key. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename='tests/test_dss.key') + self.tc.connect(self.addr, self.port, username='slowdive', key_filename=test_path('test_dss.key')) self.event.wait(1.0) - self.assert_(self.event.isSet()) - self.assert_(self.ts.is_active()) - self.assertEquals('slowdive', self.ts.get_username()) - self.assertEquals(True, self.ts.is_authenticated()) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) + self.assertEqual('slowdive', self.ts.get_username()) + self.assertEqual(True, self.ts.is_authenticated()) stdin, stdout, stderr = self.tc.exec_command('yes') schan = self.ts.accept(1.0) @@ -141,10 +138,10 @@ class SSHClientTest (unittest.TestCase): schan.send_stderr('This is on stderr.\n') schan.close() - self.assertEquals('Hello there.\n', stdout.readline()) - self.assertEquals('', stdout.readline()) - self.assertEquals('This is on stderr.\n', stderr.readline()) - self.assertEquals('', stderr.readline()) + self.assertEqual('Hello there.\n', stdout.readline()) + self.assertEqual('', stdout.readline()) + self.assertEqual('This is on stderr.\n', stderr.readline()) + self.assertEqual('', stderr.readline()) stdin.close() stdout.close() @@ -154,38 +151,40 @@ class SSHClientTest (unittest.TestCase): """ verify that SSHClient accepts and tries multiple key files. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename=[ 'tests/test_rsa.key', 'tests/test_dss.key' ]) + self.tc.connect(self.addr, self.port, username='slowdive', key_filename=[ test_path('test_rsa.key'), test_path('test_dss.key') ]) self.event.wait(1.0) - self.assert_(self.event.isSet()) - self.assert_(self.ts.is_active()) - self.assertEquals('slowdive', self.ts.get_username()) - self.assertEquals(True, self.ts.is_authenticated()) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) + self.assertEqual('slowdive', self.ts.get_username()) + self.assertEqual(True, self.ts.is_authenticated()) def test_4_auto_add_policy(self): """ verify that SSHClient's AutoAddPolicy works. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - self.assertEquals(0, len(self.tc.get_host_keys())) + self.assertEqual(0, len(self.tc.get_host_keys())) self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.event.wait(1.0) - self.assert_(self.event.isSet()) - self.assert_(self.ts.is_active()) - self.assertEquals('slowdive', self.ts.get_username()) - self.assertEquals(True, self.ts.is_authenticated()) - self.assertEquals(1, len(self.tc.get_host_keys())) - self.assertEquals(public_host_key, self.tc.get_host_keys()['[%s]:%d' % (self.addr, self.port)]['ssh-rsa']) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) + self.assertEqual('slowdive', self.ts.get_username()) + self.assertEqual(True, self.ts.is_authenticated()) + self.assertEqual(1, len(self.tc.get_host_keys())) + self.assertEqual(public_host_key, self.tc.get_host_keys()['[%s]:%d' % (self.addr, self.port)]['ssh-rsa']) def test_5_save_host_keys(self): """ @@ -193,9 +192,10 @@ class SSHClientTest (unittest.TestCase): """ warnings.filterwarnings('ignore', 'tempnam.*') - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) - localname = os.tempnam() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + fd, localname = mkstemp() + os.close(fd) client = paramiko.SSHClient() self.assertEquals(0, len(client.get_host_keys())) @@ -218,24 +218,32 @@ class SSHClientTest (unittest.TestCase): verify that when an SSHClient is collected, its transport (and the transport's packetizer) is closed. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - self.assertEquals(0, len(self.tc.get_host_keys())) + self.assertEqual(0, len(self.tc.get_host_keys())) self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.event.wait(1.0) - self.assert_(self.event.isSet()) - self.assert_(self.ts.is_active()) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) p = weakref.ref(self.tc._transport.packetizer) - self.assert_(p() is not None) + self.assertTrue(p() is not None) + self.tc.close() del self.tc + # hrm, sometimes p isn't cleared right away. why is that? - st = time.time() - while (time.time() - st < 5.0) and (p() is not None): - time.sleep(0.1) - self.assert_(p() is None) + #st = time.time() + #while (time.time() - st < 5.0) and (p() is not None): + # time.sleep(0.1) + + # instead of dumbly waiting for the GC to collect, force a collection + # to see whether the SSHClient object is deallocated correctly + import gc + gc.collect() + self.assertTrue(p() is None) diff --git a/tests/test_file.py b/tests/test_file.py index 6cb35070..33a49130 100755 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -22,6 +22,7 @@ Some unit tests for the BufferedFile abstraction. import unittest from paramiko.file import BufferedFile +from paramiko.common import * class LoopbackFile (BufferedFile): @@ -31,7 +32,7 @@ class LoopbackFile (BufferedFile): def __init__(self, mode='r', bufsize=-1): BufferedFile.__init__(self) self._set_mode(mode, bufsize) - self.buffer = '' + self.buffer = bytes() def _read(self, size): if len(self.buffer) == 0: @@ -53,7 +54,7 @@ class BufferedFileTest (unittest.TestCase): f = LoopbackFile('r') try: f.write('hi') - self.assert_(False, 'no exception on write to read-only file') + self.assertTrue(False, 'no exception on write to read-only file') except: pass f.close() @@ -61,7 +62,7 @@ class BufferedFileTest (unittest.TestCase): f = LoopbackFile('w') try: f.read(1) - self.assert_(False, 'no exception to read from write-only file') + self.assertTrue(False, 'no exception to read from write-only file') except: pass f.close() @@ -80,12 +81,12 @@ class BufferedFileTest (unittest.TestCase): f.close() try: f.readline() - self.assert_(False, 'no exception on readline of closed file') + self.assertTrue(False, 'no exception on readline of closed file') except IOError: pass - self.assert_('\n' in f.newlines) - self.assert_('\r\n' in f.newlines) - self.assert_('\r' not in f.newlines) + self.assertTrue(linefeed_byte in f.newlines) + self.assertTrue(crlf in f.newlines) + self.assertTrue(cr_byte not in f.newlines) def test_3_lf(self): """ @@ -97,7 +98,7 @@ class BufferedFileTest (unittest.TestCase): f.write('\nSecond.\r\n') self.assertEqual(f.readline(), 'Second.\n') f.close() - self.assertEqual(f.newlines, '\r\n') + self.assertEqual(f.newlines, crlf) def test_4_write(self): """ diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py index 44070cbe..9a7e3689 100644 --- a/tests/test_hostkeys.py +++ b/tests/test_hostkeys.py @@ -25,6 +25,7 @@ from binascii import hexlify import os import unittest import paramiko +from paramiko.py3compat import b, decodebytes test_hosts_file = """\ @@ -36,12 +37,12 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ 5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M= """ -keyblob = """\ +keyblob = b"""\ AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31MBGQ3GQ/Fc7SX6gkpXkwcZryoi4k\ NFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW5ymME3bQ4J/k1IKxCtz/bAlAqFgK\ oc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=""" -keyblob_dss = """\ +keyblob_dss = b"""\ AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/\ h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF60\ 8EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIE\ @@ -55,51 +56,50 @@ Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg\ class HostKeysTest (unittest.TestCase): def setUp(self): - f = open('hostfile.temp', 'w') - f.write(test_hosts_file) - f.close() + with open('hostfile.temp', 'w') as f: + f.write(test_hosts_file) def tearDown(self): os.unlink('hostfile.temp') def test_1_load(self): hostdict = paramiko.HostKeys('hostfile.temp') - self.assertEquals(2, len(hostdict)) - self.assertEquals(1, len(hostdict.values()[0])) - self.assertEquals(1, len(hostdict.values()[1])) + self.assertEqual(2, len(hostdict)) + self.assertEqual(1, len(list(hostdict.values())[0])) + self.assertEqual(1, len(list(hostdict.values())[1])) fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper() - self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp) + self.assertEqual(b'E6684DB30E109B67B70FF1DC5C7F1363', fp) def test_2_add(self): hostdict = paramiko.HostKeys('hostfile.temp') hh = '|1|BMsIC6cUIP2zBuXR3t2LRcJYjzM=|hpkJMysjTk/+zzUUzxQEa2ieq6c=' - key = paramiko.RSAKey(data=base64.decodestring(keyblob)) + key = paramiko.RSAKey(data=decodebytes(keyblob)) hostdict.add(hh, 'ssh-rsa', key) - self.assertEquals(3, len(hostdict)) + self.assertEqual(3, len(list(hostdict))) x = hostdict['foo.example.com'] fp = hexlify(x['ssh-rsa'].get_fingerprint()).upper() - self.assertEquals('7EC91BB336CB6D810B124B1353C32396', fp) - self.assert_(hostdict.check('foo.example.com', key)) + self.assertEqual(b'7EC91BB336CB6D810B124B1353C32396', fp) + self.assertTrue(hostdict.check('foo.example.com', key)) def test_3_dict(self): hostdict = paramiko.HostKeys('hostfile.temp') - self.assert_('secure.example.com' in hostdict) - self.assert_('not.example.com' not in hostdict) - self.assert_(hostdict.has_key('secure.example.com')) - self.assert_(not hostdict.has_key('not.example.com')) + self.assertTrue('secure.example.com' in hostdict) + self.assertTrue('not.example.com' not in hostdict) + self.assertTrue('secure.example.com' in hostdict) + self.assertTrue('not.example.com' not in hostdict) x = hostdict.get('secure.example.com', None) - self.assert_(x is not None) + self.assertTrue(x is not None) fp = hexlify(x['ssh-rsa'].get_fingerprint()).upper() - self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp) + self.assertEqual(b'E6684DB30E109B67B70FF1DC5C7F1363', fp) i = 0 for key in hostdict: i += 1 - self.assertEquals(2, i) + self.assertEqual(2, i) def test_4_dict_set(self): hostdict = paramiko.HostKeys('hostfile.temp') - key = paramiko.RSAKey(data=base64.decodestring(keyblob)) - key_dss = paramiko.DSSKey(data=base64.decodestring(keyblob_dss)) + key = paramiko.RSAKey(data=decodebytes(keyblob)) + key_dss = paramiko.DSSKey(data=decodebytes(keyblob_dss)) hostdict['secure.example.com'] = { 'ssh-rsa': key, 'ssh-dss': key_dss @@ -107,11 +107,11 @@ class HostKeysTest (unittest.TestCase): hostdict['fake.example.com'] = {} hostdict['fake.example.com']['ssh-rsa'] = key - self.assertEquals(3, len(hostdict)) - self.assertEquals(2, len(hostdict.values()[0])) - self.assertEquals(1, len(hostdict.values()[1])) - self.assertEquals(1, len(hostdict.values()[2])) + self.assertEqual(3, len(hostdict)) + self.assertEqual(2, len(list(hostdict.values())[0])) + self.assertEqual(1, len(list(hostdict.values())[1])) + self.assertEqual(1, len(list(hostdict.values())[2])) fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper() - self.assertEquals('7EC91BB336CB6D810B124B1353C32396', fp) + self.assertEqual(b'7EC91BB336CB6D810B124B1353C32396', fp) fp = hexlify(hostdict['secure.example.com']['ssh-dss'].get_fingerprint()).upper() - self.assertEquals('4478F0B9A23CC5182009FF755BC1D26C', fp) + self.assertEqual(b'4478F0B9A23CC5182009FF755BC1D26C', fp) diff --git a/tests/test_kex.py b/tests/test_kex.py index 39d2e17e..e69c051b 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -26,22 +26,25 @@ import paramiko.util from paramiko.kex_group1 import KexGroup1 from paramiko.kex_gex import KexGex from paramiko import Message +from paramiko.common import * class FakeRng (object): def read(self, n): - return chr(0xcc) * n + return byte_chr(0xcc) * n class FakeKey (object): def __str__(self): return 'fake-key' + def asbytes(self): + return b'fake-key' def sign_ssh_data(self, rng, H): - return 'fake-sig' + return b'fake-sig' class FakeModulusPack (object): - P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFFL + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF G = 2 def get_modulus(self, min, ask, max): return self.G, self.P @@ -75,7 +78,7 @@ class FakeTransport (object): class KexTest (unittest.TestCase): - K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504L + K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 def setUp(self): pass @@ -88,9 +91,9 @@ class KexTest (unittest.TestCase): transport.server_mode = False kex = KexGroup1(transport) kex.start_kex() - x = '1E000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_group1._MSG_KEXDH_REPLY,), transport._expect) + x = b'1E000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_group1._MSG_KEXDH_REPLY,), transport._expect) # fake "reply" msg = Message() @@ -99,47 +102,47 @@ class KexTest (unittest.TestCase): msg.add_string('fake-sig') msg.rewind() kex.parse_next(paramiko.kex_group1._MSG_KEXDH_REPLY, msg) - H = '03079780F3D3AD0B3C6DB30C8D21685F367A86D2' - self.assertEquals(self.K, transport._K) - self.assertEquals(H, hexlify(transport._H).upper()) - self.assertEquals(('fake-host-key', 'fake-sig'), transport._verify) - self.assert_(transport._activated) + H = b'03079780F3D3AD0B3C6DB30C8D21685F367A86D2' + self.assertEqual(self.K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) + self.assertTrue(transport._activated) def test_2_group1_server(self): transport = FakeTransport() transport.server_mode = True kex = KexGroup1(transport) kex.start_kex() - self.assertEquals((paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect) + self.assertEqual((paramiko.kex_group1._MSG_KEXDH_INIT,), transport._expect) msg = Message() msg.add_mpint(69) msg.rewind() kex.parse_next(paramiko.kex_group1._MSG_KEXDH_INIT, msg) - H = 'B16BF34DD10945EDE84E9C1EF24A14BFDC843389' - x = '1F0000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' - self.assertEquals(self.K, transport._K) - self.assertEquals(H, hexlify(transport._H).upper()) - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assert_(transport._activated) + H = b'B16BF34DD10945EDE84E9C1EF24A14BFDC843389' + x = b'1F0000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' + self.assertEqual(self.K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertTrue(transport._activated) def test_3_gex_client(self): transport = FakeTransport() transport.server_mode = False kex = KexGex(transport) kex.start_kex() - x = '22000004000000080000002000' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) + x = b'22000004000000080000002000' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) msg = Message() msg.add_mpint(FakeModulusPack.P) msg.add_mpint(FakeModulusPack.G) msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg) - x = '20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) + x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) msg = Message() msg.add_string('fake-host-key') @@ -147,29 +150,29 @@ class KexTest (unittest.TestCase): msg.add_string('fake-sig') msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg) - H = 'A265563F2FA87F1A89BF007EE90D58BE2E4A4BD0' - self.assertEquals(self.K, transport._K) - self.assertEquals(H, hexlify(transport._H).upper()) - self.assertEquals(('fake-host-key', 'fake-sig'), transport._verify) - self.assert_(transport._activated) + H = b'A265563F2FA87F1A89BF007EE90D58BE2E4A4BD0' + self.assertEqual(self.K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) + self.assertTrue(transport._activated) def test_4_gex_old_client(self): transport = FakeTransport() transport.server_mode = False kex = KexGex(transport) kex.start_kex(_test_old_style=True) - x = '1E00000800' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) + x = b'1E00000800' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) msg = Message() msg.add_mpint(FakeModulusPack.P) msg.add_mpint(FakeModulusPack.G) msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg) - x = '20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) + x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) msg = Message() msg.add_string('fake-host-key') @@ -177,18 +180,18 @@ class KexTest (unittest.TestCase): msg.add_string('fake-sig') msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg) - H = '807F87B269EF7AC5EC7E75676808776A27D5864C' - self.assertEquals(self.K, transport._K) - self.assertEquals(H, hexlify(transport._H).upper()) - self.assertEquals(('fake-host-key', 'fake-sig'), transport._verify) - self.assert_(transport._activated) + H = b'807F87B269EF7AC5EC7E75676808776A27D5864C' + self.assertEqual(self.K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) + self.assertTrue(transport._activated) def test_5_gex_server(self): transport = FakeTransport() transport.server_mode = True kex = KexGex(transport) kex.start_kex() - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) msg = Message() msg.add_int(1024) @@ -196,45 +199,45 @@ class KexTest (unittest.TestCase): msg.add_int(4096) msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg) - x = '1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) + x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) msg = Message() msg.add_mpint(12345) msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg) - K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581L - H = 'CE754197C21BF3452863B4F44D0B3951F12516EF' - x = '210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' - self.assertEquals(K, transport._K) - self.assertEquals(H, hexlify(transport._H).upper()) - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assert_(transport._activated) + K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 + H = b'CE754197C21BF3452863B4F44D0B3951F12516EF' + x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' + self.assertEqual(K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertTrue(transport._activated) def test_6_gex_server_with_old_client(self): transport = FakeTransport() transport.server_mode = True kex = KexGex(transport) kex.start_kex() - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) msg = Message() msg.add_int(2048) msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg) - x = '1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assertEquals((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) + x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) msg = Message() msg.add_mpint(12345) msg.rewind() kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg) - K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581L - H = 'B41A06B2E59043CEFC1AE16EC31F1E2D12EC455B' - x = '210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' - self.assertEquals(K, transport._K) - self.assertEquals(H, hexlify(transport._H).upper()) - self.assertEquals(x, hexlify(str(transport._message)).upper()) - self.assert_(transport._activated) + K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 + H = b'B41A06B2E59043CEFC1AE16EC31F1E2D12EC455B' + x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' + self.assertEqual(K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertTrue(transport._activated) diff --git a/tests/test_message.py b/tests/test_message.py index ad622a27..4da52cfb 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -22,14 +22,15 @@ Some unit tests for ssh protocol message blocks. import unittest from paramiko.message import Message +from paramiko.common import * class MessageTest (unittest.TestCase): - __a = '\x00\x00\x00\x17\x07\x60\xe0\x90\x00\x00\x00\x01q\x00\x00\x00\x05hello\x00\x00\x03\xe8' + ('x' * 1000) - __b = '\x01\x00\xf3\x00\x3f\x00\x00\x00\x10huey,dewey,louie' - __c = '\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x01\x11\x00\x00\x00\x07\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x06\x9a\x1b\x2c\x3d\x4e\xf7' - __d = '\x00\x00\x00\x05\x00\x00\x00\x05\x11\x22\x33\x44\x55\x01\x00\x00\x00\x03cat\x00\x00\x00\x03a,b' + __a = b'\x00\x00\x00\x17\x07\x60\xe0\x90\x00\x00\x00\x01\x71\x00\x00\x00\x05\x68\x65\x6c\x6c\x6f\x00\x00\x03\xe8' + b'x' * 1000 + __b = b'\x01\x00\xf3\x00\x3f\x00\x00\x00\x10\x68\x75\x65\x79\x2c\x64\x65\x77\x65\x79\x2c\x6c\x6f\x75\x69\x65' + __c = b'\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x01\x11\x00\x00\x00\x07\x00\xf5\xe4\xd3\xc2\xb1\x09\x00\x00\x00\x06\x9a\x1b\x2c\x3d\x4e\xf7' + __d = b'\x00\x00\x00\x05\xff\x00\x00\x00\x05\x11\x22\x33\x44\x55\xff\x00\x00\x00\x0a\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x63\x61\x74\x00\x00\x00\x03\x61\x2c\x62' def test_1_encode(self): msg = Message() @@ -38,63 +39,65 @@ class MessageTest (unittest.TestCase): msg.add_string('q') msg.add_string('hello') msg.add_string('x' * 1000) - self.assertEquals(str(msg), self.__a) + self.assertEqual(msg.asbytes(), self.__a) msg = Message() msg.add_boolean(True) msg.add_boolean(False) - msg.add_byte('\xf3') - msg.add_bytes('\x00\x3f') + msg.add_byte(byte_chr(0xf3)) + + msg.add_bytes(zero_byte + byte_chr(0x3f)) msg.add_list(['huey', 'dewey', 'louie']) - self.assertEquals(str(msg), self.__b) + self.assertEqual(msg.asbytes(), self.__b) msg = Message() msg.add_int64(5) - msg.add_int64(0xf5e4d3c2b109L) + msg.add_int64(0xf5e4d3c2b109) msg.add_mpint(17) - msg.add_mpint(0xf5e4d3c2b109L) - msg.add_mpint(-0x65e4d3c2b109L) - self.assertEquals(str(msg), self.__c) + msg.add_mpint(0xf5e4d3c2b109) + msg.add_mpint(-0x65e4d3c2b109) + self.assertEqual(msg.asbytes(), self.__c) def test_2_decode(self): msg = Message(self.__a) - self.assertEquals(msg.get_int(), 23) - self.assertEquals(msg.get_int(), 123789456) - self.assertEquals(msg.get_string(), 'q') - self.assertEquals(msg.get_string(), 'hello') - self.assertEquals(msg.get_string(), 'x' * 1000) + self.assertEqual(msg.get_int(), 23) + self.assertEqual(msg.get_int(), 123789456) + self.assertEqual(msg.get_text(), 'q') + self.assertEqual(msg.get_text(), 'hello') + self.assertEqual(msg.get_text(), 'x' * 1000) msg = Message(self.__b) - self.assertEquals(msg.get_boolean(), True) - self.assertEquals(msg.get_boolean(), False) - self.assertEquals(msg.get_byte(), '\xf3') - self.assertEquals(msg.get_bytes(2), '\x00\x3f') - self.assertEquals(msg.get_list(), ['huey', 'dewey', 'louie']) + self.assertEqual(msg.get_boolean(), True) + self.assertEqual(msg.get_boolean(), False) + self.assertEqual(msg.get_byte(), byte_chr(0xf3)) + self.assertEqual(msg.get_bytes(2), zero_byte + byte_chr(0x3f)) + self.assertEqual(msg.get_list(), ['huey', 'dewey', 'louie']) msg = Message(self.__c) - self.assertEquals(msg.get_int64(), 5) - self.assertEquals(msg.get_int64(), 0xf5e4d3c2b109L) - self.assertEquals(msg.get_mpint(), 17) - self.assertEquals(msg.get_mpint(), 0xf5e4d3c2b109L) - self.assertEquals(msg.get_mpint(), -0x65e4d3c2b109L) + self.assertEqual(msg.get_int64(), 5) + self.assertEqual(msg.get_int64(), 0xf5e4d3c2b109) + self.assertEqual(msg.get_mpint(), 17) + self.assertEqual(msg.get_mpint(), 0xf5e4d3c2b109) + self.assertEqual(msg.get_mpint(), -0x65e4d3c2b109) def test_3_add(self): msg = Message() msg.add(5) - msg.add(0x1122334455L) + msg.add(0x1122334455) + msg.add(0xf00000000000000000) msg.add(True) msg.add('cat') msg.add(['a', 'b']) - self.assertEquals(str(msg), self.__d) + self.assertEqual(msg.asbytes(), self.__d) def test_4_misc(self): msg = Message(self.__d) - self.assertEquals(msg.get_int(), 5) - self.assertEquals(msg.get_mpint(), 0x1122334455L) - self.assertEquals(msg.get_so_far(), self.__d[:13]) - self.assertEquals(msg.get_remainder(), self.__d[13:]) + self.assertEqual(msg.get_int(), 5) + self.assertEqual(msg.get_int(), 0x1122334455) + self.assertEqual(msg.get_int(), 0xf00000000000000000) + self.assertEqual(msg.get_so_far(), self.__d[:29]) + self.assertEqual(msg.get_remainder(), self.__d[29:]) msg.rewind() - self.assertEquals(msg.get_int(), 5) - self.assertEquals(msg.get_so_far(), self.__d[:4]) - self.assertEquals(msg.get_remainder(), self.__d[4:]) - + self.assertEqual(msg.get_int(), 5) + self.assertEqual(msg.get_so_far(), self.__d[:4]) + self.assertEqual(msg.get_remainder(), self.__d[4:]) diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py index 1f5bec05..5c36fed6 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -21,10 +21,15 @@ Some unit tests for the ssh2 protocol in Transport. """ import unittest -from loop import LoopSocket +from tests.loop import LoopSocket from Crypto.Cipher import AES from Crypto.Hash import SHA, HMAC from paramiko import Message, Packetizer, util +from paramiko.common import * + +x55 = byte_chr(0x55) +x1f = byte_chr(0x1f) + class PacketizerTest (unittest.TestCase): @@ -35,22 +40,22 @@ class PacketizerTest (unittest.TestCase): p = Packetizer(wsock) p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) - cipher = AES.new('\x00' * 16, AES.MODE_CBC, '\x55' * 16) - p.set_outbound_cipher(cipher, 16, SHA, 12, '\x1f' * 20) + cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) + p.set_outbound_cipher(cipher, 16, SHA, 12, x1f * 20) # message has to be at least 16 bytes long, so we'll have at least one # block of data encrypted that contains zero random padding bytes m = Message() - m.add_byte(chr(100)) + m.add_byte(byte_chr(100)) m.add_int(100) m.add_int(1) m.add_int(900) p.send_message(m) data = rsock.recv(100) # 32 + 12 bytes of MAC = 44 - self.assertEquals(44, len(data)) - self.assertEquals('\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0', data[:16]) - + self.assertEqual(44, len(data)) + self.assertEqual(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0', data[:16]) + def test_2_read (self): rsock = LoopSocket() wsock = LoopSocket() @@ -58,13 +63,11 @@ class PacketizerTest (unittest.TestCase): p = Packetizer(rsock) p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) - cipher = AES.new('\x00' * 16, AES.MODE_CBC, '\x55' * 16) - p.set_inbound_cipher(cipher, 16, SHA, 12, '\x1f' * 20) - - wsock.send('C\x91\x97\xbd[P\xac%\x87\xc2\xc4k\xc7\xe98\xc0' + \ - '\x90\xd2\x16V\rqsa8|L=\xfb\x97}\xe2n\x03\xb1\xa0\xc2\x1c\xd6AAL\xb4Y') + cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) + p.set_inbound_cipher(cipher, 16, SHA, 12, x1f * 20) + wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59') cmd, m = p.read_message() - self.assertEquals(100, cmd) - self.assertEquals(100, m.get_int()) - self.assertEquals(1, m.get_int()) - self.assertEquals(900, m.get_int()) + self.assertEqual(100, cmd) + self.assertEqual(100, m.get_int()) + self.assertEqual(1, m.get_int()) + self.assertEqual(900, m.get_int()) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 8e8c4aa7..2e565a5f 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -20,11 +20,11 @@ Some unit tests for public/private key objects. """ -from binascii import hexlify, unhexlify -import StringIO +from binascii import hexlify import unittest from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util -from paramiko.common import rng +from paramiko.common import rng, StringIO, byte_chr, b, bytes +from tests.util import test_path # from openssh's ssh-keygen PUB_RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=' @@ -77,6 +77,9 @@ ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g== -----END EC PRIVATE KEY----- """ +x1234 = b'\x01\x02\x03\x04' + + class KeyTest (unittest.TestCase): def setUp(self): @@ -87,164 +90,164 @@ class KeyTest (unittest.TestCase): def test_1_generate_key_bytes(self): from Crypto.Hash import MD5 - key = util.generate_key_bytes(MD5, '\x01\x02\x03\x04', 'happy birthday', 30) - exp = unhexlify('61E1F272F4C1C4561586BD322498C0E924672780F47BB37DDA7D54019E64') - self.assertEquals(exp, key) + key = util.generate_key_bytes(MD5, x1234, 'happy birthday', 30) + exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64' + self.assertEqual(exp, key) def test_2_load_rsa(self): - key = RSAKey.from_private_key_file('tests/test_rsa.key') - self.assertEquals('ssh-rsa', key.get_name()) - exp_rsa = FINGER_RSA.split()[1].replace(':', '') + key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + self.assertEqual('ssh-rsa', key.get_name()) + exp_rsa = b(FINGER_RSA.split()[1].replace(':', '')) my_rsa = hexlify(key.get_fingerprint()) - self.assertEquals(exp_rsa, my_rsa) - self.assertEquals(PUB_RSA.split()[1], key.get_base64()) - self.assertEquals(1024, key.get_bits()) + self.assertEqual(exp_rsa, my_rsa) + self.assertEqual(PUB_RSA.split()[1], key.get_base64()) + self.assertEqual(1024, key.get_bits()) - s = StringIO.StringIO() + s = StringIO() key.write_private_key(s) - self.assertEquals(RSA_PRIVATE_OUT, s.getvalue()) + self.assertEqual(RSA_PRIVATE_OUT, s.getvalue()) s.seek(0) key2 = RSAKey.from_private_key(s) - self.assertEquals(key, key2) + self.assertEqual(key, key2) def test_3_load_rsa_password(self): - key = RSAKey.from_private_key_file('tests/test_rsa_password.key', 'television') - self.assertEquals('ssh-rsa', key.get_name()) - exp_rsa = FINGER_RSA.split()[1].replace(':', '') + key = RSAKey.from_private_key_file(test_path('test_rsa_password.key'), 'television') + self.assertEqual('ssh-rsa', key.get_name()) + exp_rsa = b(FINGER_RSA.split()[1].replace(':', '')) my_rsa = hexlify(key.get_fingerprint()) - self.assertEquals(exp_rsa, my_rsa) - self.assertEquals(PUB_RSA.split()[1], key.get_base64()) - self.assertEquals(1024, key.get_bits()) + self.assertEqual(exp_rsa, my_rsa) + self.assertEqual(PUB_RSA.split()[1], key.get_base64()) + self.assertEqual(1024, key.get_bits()) def test_4_load_dss(self): - key = DSSKey.from_private_key_file('tests/test_dss.key') - self.assertEquals('ssh-dss', key.get_name()) - exp_dss = FINGER_DSS.split()[1].replace(':', '') + key = DSSKey.from_private_key_file(test_path('test_dss.key')) + self.assertEqual('ssh-dss', key.get_name()) + exp_dss = b(FINGER_DSS.split()[1].replace(':', '')) my_dss = hexlify(key.get_fingerprint()) - self.assertEquals(exp_dss, my_dss) - self.assertEquals(PUB_DSS.split()[1], key.get_base64()) - self.assertEquals(1024, key.get_bits()) + self.assertEqual(exp_dss, my_dss) + self.assertEqual(PUB_DSS.split()[1], key.get_base64()) + self.assertEqual(1024, key.get_bits()) - s = StringIO.StringIO() + s = StringIO() key.write_private_key(s) - self.assertEquals(DSS_PRIVATE_OUT, s.getvalue()) + self.assertEqual(DSS_PRIVATE_OUT, s.getvalue()) s.seek(0) key2 = DSSKey.from_private_key(s) - self.assertEquals(key, key2) + self.assertEqual(key, key2) def test_5_load_dss_password(self): - key = DSSKey.from_private_key_file('tests/test_dss_password.key', 'television') - self.assertEquals('ssh-dss', key.get_name()) - exp_dss = FINGER_DSS.split()[1].replace(':', '') + key = DSSKey.from_private_key_file(test_path('test_dss_password.key'), 'television') + self.assertEqual('ssh-dss', key.get_name()) + exp_dss = b(FINGER_DSS.split()[1].replace(':', '')) my_dss = hexlify(key.get_fingerprint()) - self.assertEquals(exp_dss, my_dss) - self.assertEquals(PUB_DSS.split()[1], key.get_base64()) - self.assertEquals(1024, key.get_bits()) + self.assertEqual(exp_dss, my_dss) + self.assertEqual(PUB_DSS.split()[1], key.get_base64()) + self.assertEqual(1024, key.get_bits()) def test_6_compare_rsa(self): # verify that the private & public keys compare equal - key = RSAKey.from_private_key_file('tests/test_rsa.key') - self.assertEquals(key, key) - pub = RSAKey(data=str(key)) - self.assert_(key.can_sign()) - self.assert_(not pub.can_sign()) - self.assertEquals(key, pub) + key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + self.assertEqual(key, key) + pub = RSAKey(data=key.asbytes()) + self.assertTrue(key.can_sign()) + self.assertTrue(not pub.can_sign()) + self.assertEqual(key, pub) def test_7_compare_dss(self): # verify that the private & public keys compare equal - key = DSSKey.from_private_key_file('tests/test_dss.key') - self.assertEquals(key, key) - pub = DSSKey(data=str(key)) - self.assert_(key.can_sign()) - self.assert_(not pub.can_sign()) - self.assertEquals(key, pub) + key = DSSKey.from_private_key_file(test_path('test_dss.key')) + self.assertEqual(key, key) + pub = DSSKey(data=key.asbytes()) + self.assertTrue(key.can_sign()) + self.assertTrue(not pub.can_sign()) + self.assertEqual(key, pub) def test_8_sign_rsa(self): # verify that the rsa private key can sign and verify - key = RSAKey.from_private_key_file('tests/test_rsa.key') - msg = key.sign_ssh_data(rng, 'ice weasels') - self.assert_(type(msg) is Message) + key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + msg = key.sign_ssh_data(rng, b'ice weasels') + self.assertTrue(type(msg) is Message) msg.rewind() - self.assertEquals('ssh-rsa', msg.get_string()) - sig = ''.join([chr(int(x, 16)) for x in SIGNED_RSA.split(':')]) - self.assertEquals(sig, msg.get_string()) + self.assertEqual('ssh-rsa', msg.get_text()) + sig = bytes().join([byte_chr(int(x, 16)) for x in SIGNED_RSA.split(':')]) + self.assertEqual(sig, msg.get_binary()) msg.rewind() - pub = RSAKey(data=str(key)) - self.assert_(pub.verify_ssh_sig('ice weasels', msg)) + pub = RSAKey(data=key.asbytes()) + self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg)) def test_9_sign_dss(self): # verify that the dss private key can sign and verify - key = DSSKey.from_private_key_file('tests/test_dss.key') - msg = key.sign_ssh_data(rng, 'ice weasels') - self.assert_(type(msg) is Message) + key = DSSKey.from_private_key_file(test_path('test_dss.key')) + msg = key.sign_ssh_data(rng, b'ice weasels') + self.assertTrue(type(msg) is Message) msg.rewind() - self.assertEquals('ssh-dss', msg.get_string()) + self.assertEqual('ssh-dss', msg.get_text()) # can't do the same test as we do for RSA, because DSS signatures # are usually different each time. but we can test verification # anyway so it's ok. - self.assertEquals(40, len(msg.get_string())) + self.assertEqual(40, len(msg.get_binary())) msg.rewind() - pub = DSSKey(data=str(key)) - self.assert_(pub.verify_ssh_sig('ice weasels', msg)) + pub = DSSKey(data=key.asbytes()) + self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg)) def test_A_generate_rsa(self): key = RSAKey.generate(1024) - msg = key.sign_ssh_data(rng, 'jerri blank') + msg = key.sign_ssh_data(rng, b'jerri blank') msg.rewind() - self.assert_(key.verify_ssh_sig('jerri blank', msg)) + self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) def test_B_generate_dss(self): key = DSSKey.generate(1024) - msg = key.sign_ssh_data(rng, 'jerri blank') + msg = key.sign_ssh_data(rng, b'jerri blank') msg.rewind() - self.assert_(key.verify_ssh_sig('jerri blank', msg)) + self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) def test_10_load_ecdsa(self): - key = ECDSAKey.from_private_key_file('tests/test_ecdsa.key') - self.assertEquals('ecdsa-sha2-nistp256', key.get_name()) - exp_ecdsa = FINGER_ECDSA.split()[1].replace(':', '') + key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key')) + self.assertEqual('ecdsa-sha2-nistp256', key.get_name()) + exp_ecdsa = b(FINGER_ECDSA.split()[1].replace(':', '')) my_ecdsa = hexlify(key.get_fingerprint()) - self.assertEquals(exp_ecdsa, my_ecdsa) - self.assertEquals(PUB_ECDSA.split()[1], key.get_base64()) - self.assertEquals(256, key.get_bits()) + self.assertEqual(exp_ecdsa, my_ecdsa) + self.assertEqual(PUB_ECDSA.split()[1], key.get_base64()) + self.assertEqual(256, key.get_bits()) - s = StringIO.StringIO() + s = StringIO() key.write_private_key(s) - self.assertEquals(ECDSA_PRIVATE_OUT, s.getvalue()) + self.assertEqual(ECDSA_PRIVATE_OUT, s.getvalue()) s.seek(0) key2 = ECDSAKey.from_private_key(s) - self.assertEquals(key, key2) + self.assertEqual(key, key2) def test_11_load_ecdsa_password(self): - key = ECDSAKey.from_private_key_file('tests/test_ecdsa_password.key', 'television') - self.assertEquals('ecdsa-sha2-nistp256', key.get_name()) - exp_ecdsa = FINGER_ECDSA.split()[1].replace(':', '') + key = ECDSAKey.from_private_key_file(test_path('test_ecdsa_password.key'), b'television') + self.assertEqual('ecdsa-sha2-nistp256', key.get_name()) + exp_ecdsa = b(FINGER_ECDSA.split()[1].replace(':', '')) my_ecdsa = hexlify(key.get_fingerprint()) - self.assertEquals(exp_ecdsa, my_ecdsa) - self.assertEquals(PUB_ECDSA.split()[1], key.get_base64()) - self.assertEquals(256, key.get_bits()) + self.assertEqual(exp_ecdsa, my_ecdsa) + self.assertEqual(PUB_ECDSA.split()[1], key.get_base64()) + self.assertEqual(256, key.get_bits()) def test_12_compare_ecdsa(self): # verify that the private & public keys compare equal - key = ECDSAKey.from_private_key_file('tests/test_ecdsa.key') - self.assertEquals(key, key) - pub = ECDSAKey(data=str(key)) - self.assert_(key.can_sign()) - self.assert_(not pub.can_sign()) - self.assertEquals(key, pub) + key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key')) + self.assertEqual(key, key) + pub = ECDSAKey(data=key.asbytes()) + self.assertTrue(key.can_sign()) + self.assertTrue(not pub.can_sign()) + self.assertEqual(key, pub) def test_13_sign_ecdsa(self): # verify that the rsa private key can sign and verify - key = ECDSAKey.from_private_key_file('tests/test_ecdsa.key') - msg = key.sign_ssh_data(rng, 'ice weasels') - self.assert_(type(msg) is Message) + key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key')) + msg = key.sign_ssh_data(rng, b'ice weasels') + self.assertTrue(type(msg) is Message) msg.rewind() - self.assertEquals('ecdsa-sha2-nistp256', msg.get_string()) + self.assertEqual('ecdsa-sha2-nistp256', msg.get_text()) # ECDSA signatures, like DSS signatures, tend to be different # each time, so we can't compare against a "known correct" # signature. # Even the length of the signature can change. msg.rewind() - pub = ECDSAKey(data=str(key)) - self.assert_(pub.verify_ssh_sig('ice weasels', msg)) + pub = ECDSAKey(data=key.asbytes()) + self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg)) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index cc512c18..f8fab1ce 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -23,19 +23,18 @@ a real actual sftp server is contacted, and a new folder is created there to do test file operations in (so no existing files will be harmed). """ -from __future__ import with_statement - from binascii import hexlify import os import warnings -import sys import threading import unittest -import StringIO +from tempfile import mkstemp import paramiko -from stub_sftp import StubServer, StubSFTPServer -from loop import LoopSocket +from paramiko.common import * +from tests.stub_sftp import StubServer, StubSFTPServer +from tests.loop import LoopSocket +from tests.util import test_path from paramiko.sftp_attr import SFTPAttributes ARTICLE = ''' @@ -70,6 +69,10 @@ FOLDER = os.environ.get('TEST_FOLDER', 'temp-testing000') sftp = None tc = None g_big_file_test = True +# we need to use eval(compile()) here because Py3.2 doesn't support the 'u' marker for unicode +# this test is the only line in the entire program that has to be treated specially to support Py3.2 +unicode_folder = eval(compile(r"u'\u00fcnic\u00f8de'" if PY2 else r"'\u00fcnic\u00f8de'", 'test_sftp.py', 'eval')) +utf8_folder = b'/\xc3\xbcnic\xc3\xb8\x64\x65' def get_sftp(): @@ -121,7 +124,7 @@ class SFTPTest (unittest.TestCase): tc = paramiko.Transport(sockc) ts = paramiko.Transport(socks) - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) ts.add_server_key(host_key) event = threading.Event() server = StubServer() @@ -140,7 +143,7 @@ class SFTPTest (unittest.TestCase): def setUp(self): global FOLDER - for i in xrange(1000): + for i in range(1000): FOLDER = FOLDER[:-3] + '%03d' % i try: sftp.mkdir(FOLDER) @@ -149,6 +152,7 @@ class SFTPTest (unittest.TestCase): pass def tearDown(self): + #sftp.chdir() sftp.rmdir(FOLDER) def test_1_file(self): @@ -158,8 +162,8 @@ class SFTPTest (unittest.TestCase): f = sftp.open(FOLDER + '/test', 'w') try: self.assertEqual(f.stat().st_size, 0) - f.close() finally: + f.close() sftp.remove(FOLDER + '/test') def test_2_close(self): @@ -180,10 +184,9 @@ class SFTPTest (unittest.TestCase): """ verify that a file can be created and written, and the size is correct. """ - f = sftp.open(FOLDER + '/duck.txt', 'w') try: - f.write(ARTICLE) - f.close() + with sftp.open(FOLDER + '/duck.txt', 'w') as f: + f.write(ARTICLE) self.assertEqual(sftp.stat(FOLDER + '/duck.txt').st_size, 1483) finally: sftp.remove(FOLDER + '/duck.txt') @@ -203,19 +206,17 @@ class SFTPTest (unittest.TestCase): """ verify that a file can be opened for append, and tell() still works. """ - f = sftp.open(FOLDER + '/append.txt', 'w') try: - f.write('first line\nsecond line\n') - self.assertEqual(f.tell(), 23) - f.close() - - f = sftp.open(FOLDER + '/append.txt', 'a+') - f.write('third line!!!\n') - self.assertEqual(f.tell(), 37) - self.assertEqual(f.stat().st_size, 37) - f.seek(-26, f.SEEK_CUR) - self.assertEqual(f.readline(), 'second line\n') - f.close() + with sftp.open(FOLDER + '/append.txt', 'w') as f: + f.write('first line\nsecond line\n') + self.assertEqual(f.tell(), 23) + + with sftp.open(FOLDER + '/append.txt', 'a+') as f: + f.write('third line!!!\n') + self.assertEqual(f.tell(), 37) + self.assertEqual(f.stat().st_size, 37) + f.seek(-26, f.SEEK_CUR) + self.assertEqual(f.readline(), 'second line\n') finally: sftp.remove(FOLDER + '/append.txt') @@ -223,20 +224,18 @@ class SFTPTest (unittest.TestCase): """ verify that renaming a file works. """ - f = sftp.open(FOLDER + '/first.txt', 'w') try: - f.write('content!\n') - f.close() + with sftp.open(FOLDER + '/first.txt', 'w') as f: + f.write('content!\n') sftp.rename(FOLDER + '/first.txt', FOLDER + '/second.txt') try: - f = sftp.open(FOLDER + '/first.txt', 'r') - self.assert_(False, 'no exception on reading nonexistent file') + sftp.open(FOLDER + '/first.txt', 'r') + self.assertTrue(False, 'no exception on reading nonexistent file') except IOError: pass - f = sftp.open(FOLDER + '/second.txt', 'r') - f.seek(-6, f.SEEK_END) - self.assertEqual(f.read(4), 'tent') - f.close() + with sftp.open(FOLDER + '/second.txt', 'r') as f: + f.seek(-6, f.SEEK_END) + self.assertEqual(u(f.read(4)), 'tent') finally: try: sftp.remove(FOLDER + '/first.txt') @@ -253,14 +252,13 @@ class SFTPTest (unittest.TestCase): remove the folder and verify that we can't create a file in it anymore. """ sftp.mkdir(FOLDER + '/subfolder') - f = sftp.open(FOLDER + '/subfolder/test', 'w') - f.close() + sftp.open(FOLDER + '/subfolder/test', 'w').close() sftp.remove(FOLDER + '/subfolder/test') sftp.rmdir(FOLDER + '/subfolder') try: - f = sftp.open(FOLDER + '/subfolder/test') + sftp.open(FOLDER + '/subfolder/test') # shouldn't be able to create that file - self.assert_(False, 'no exception at dummy file creation') + self.assertTrue(False, 'no exception at dummy file creation') except IOError: pass @@ -270,21 +268,16 @@ class SFTPTest (unittest.TestCase): and those files show up in sftp.listdir. """ try: - f = sftp.open(FOLDER + '/duck.txt', 'w') - f.close() - - f = sftp.open(FOLDER + '/fish.txt', 'w') - f.close() - - f = sftp.open(FOLDER + '/tertiary.py', 'w') - f.close() + sftp.open(FOLDER + '/duck.txt', 'w').close() + sftp.open(FOLDER + '/fish.txt', 'w').close() + sftp.open(FOLDER + '/tertiary.py', 'w').close() x = sftp.listdir(FOLDER) self.assertEqual(len(x), 3) - self.assert_('duck.txt' in x) - self.assert_('fish.txt' in x) - self.assert_('tertiary.py' in x) - self.assert_('random' not in x) + self.assertTrue('duck.txt' in x) + self.assertTrue('fish.txt' in x) + self.assertTrue('tertiary.py' in x) + self.assertTrue('random' not in x) finally: sftp.remove(FOLDER + '/duck.txt') sftp.remove(FOLDER + '/fish.txt') @@ -294,22 +287,21 @@ class SFTPTest (unittest.TestCase): """ verify that the setstat functions (chown, chmod, utime, truncate) work. """ - f = sftp.open(FOLDER + '/special', 'w') try: - f.write('x' * 1024) - f.close() + with sftp.open(FOLDER + '/special', 'w') as f: + f.write('x' * 1024) stat = sftp.stat(FOLDER + '/special') - sftp.chmod(FOLDER + '/special', (stat.st_mode & ~0777) | 0600) + sftp.chmod(FOLDER + '/special', (stat.st_mode & ~o777) | o600) stat = sftp.stat(FOLDER + '/special') - expected_mode = 0600 + expected_mode = o600 if sys.platform == 'win32': # chmod not really functional on windows - expected_mode = 0666 + expected_mode = o666 if sys.platform == 'cygwin': # even worse. - expected_mode = 0644 - self.assertEqual(stat.st_mode & 0777, expected_mode) + expected_mode = o644 + self.assertEqual(stat.st_mode & o777, expected_mode) self.assertEqual(stat.st_size, 1024) mtime = stat.st_mtime - 3600 @@ -333,40 +325,38 @@ class SFTPTest (unittest.TestCase): verify that the fsetstat functions (chown, chmod, utime, truncate) work on open files. """ - f = sftp.open(FOLDER + '/special', 'w') try: - f.write('x' * 1024) - f.close() - - f = sftp.open(FOLDER + '/special', 'r+') - stat = f.stat() - f.chmod((stat.st_mode & ~0777) | 0600) - stat = f.stat() - - expected_mode = 0600 - if sys.platform == 'win32': - # chmod not really functional on windows - expected_mode = 0666 - if sys.platform == 'cygwin': - # even worse. - expected_mode = 0644 - self.assertEqual(stat.st_mode & 0777, expected_mode) - self.assertEqual(stat.st_size, 1024) - - mtime = stat.st_mtime - 3600 - atime = stat.st_atime - 1800 - f.utime((atime, mtime)) - stat = f.stat() - self.assertEqual(stat.st_mtime, mtime) - if sys.platform not in ('win32', 'cygwin'): - self.assertEqual(stat.st_atime, atime) - - # can't really test chown, since we'd have to know a valid uid. - - f.truncate(512) - stat = f.stat() - self.assertEqual(stat.st_size, 512) - f.close() + with sftp.open(FOLDER + '/special', 'w') as f: + f.write('x' * 1024) + + with sftp.open(FOLDER + '/special', 'r+') as f: + stat = f.stat() + f.chmod((stat.st_mode & ~o777) | o600) + stat = f.stat() + + expected_mode = o600 + if sys.platform == 'win32': + # chmod not really functional on windows + expected_mode = o666 + if sys.platform == 'cygwin': + # even worse. + expected_mode = o644 + self.assertEqual(stat.st_mode & o777, expected_mode) + self.assertEqual(stat.st_size, 1024) + + mtime = stat.st_mtime - 3600 + atime = stat.st_atime - 1800 + f.utime((atime, mtime)) + stat = f.stat() + self.assertEqual(stat.st_mtime, mtime) + if sys.platform not in ('win32', 'cygwin'): + self.assertEqual(stat.st_atime, atime) + + # can't really test chown, since we'd have to know a valid uid. + + f.truncate(512) + stat = f.stat() + self.assertEqual(stat.st_size, 512) finally: sftp.remove(FOLDER + '/special') @@ -378,25 +368,23 @@ class SFTPTest (unittest.TestCase): buffering is reset on 'seek'. """ try: - f = sftp.open(FOLDER + '/duck.txt', 'w') - f.write(ARTICLE) - f.close() + with sftp.open(FOLDER + '/duck.txt', 'w') as f: + f.write(ARTICLE) - f = sftp.open(FOLDER + '/duck.txt', 'r+') - line_number = 0 - loc = 0 - pos_list = [] - for line in f: - line_number += 1 - pos_list.append(loc) - loc = f.tell() - f.seek(pos_list[6], f.SEEK_SET) - self.assertEqual(f.readline(), 'Nouzilly, France.\n') - f.seek(pos_list[17], f.SEEK_SET) - self.assertEqual(f.readline()[:4], 'duck') - f.seek(pos_list[10], f.SEEK_SET) - self.assertEqual(f.readline(), 'duck types were equally resistant to exogenous insulin compared with chicken.\n') - f.close() + with sftp.open(FOLDER + '/duck.txt', 'r+') as f: + line_number = 0 + loc = 0 + pos_list = [] + for line in f: + line_number += 1 + pos_list.append(loc) + loc = f.tell() + f.seek(pos_list[6], f.SEEK_SET) + self.assertEqual(f.readline(), 'Nouzilly, France.\n') + f.seek(pos_list[17], f.SEEK_SET) + self.assertEqual(f.readline()[:4], 'duck') + f.seek(pos_list[10], f.SEEK_SET) + self.assertEqual(f.readline(), 'duck types were equally resistant to exogenous insulin compared with chicken.\n') finally: sftp.remove(FOLDER + '/duck.txt') @@ -405,17 +393,15 @@ class SFTPTest (unittest.TestCase): create a text file, seek back and change part of it, and verify that the changes worked. """ - f = sftp.open(FOLDER + '/testing.txt', 'w') try: - f.write('hello kitty.\n') - f.seek(-5, f.SEEK_CUR) - f.write('dd') - f.close() + with sftp.open(FOLDER + '/testing.txt', 'w') as f: + f.write('hello kitty.\n') + f.seek(-5, f.SEEK_CUR) + f.write('dd') self.assertEqual(sftp.stat(FOLDER + '/testing.txt').st_size, 13) - f = sftp.open(FOLDER + '/testing.txt', 'r') - data = f.read(20) - f.close() + with sftp.open(FOLDER + '/testing.txt', 'r') as f: + data = f.read(20) self.assertEqual(data, 'hello kiddy.\n') finally: sftp.remove(FOLDER + '/testing.txt') @@ -428,16 +414,14 @@ class SFTPTest (unittest.TestCase): # skip symlink tests on windows return - f = sftp.open(FOLDER + '/original.txt', 'w') try: - f.write('original\n') - f.close() + with sftp.open(FOLDER + '/original.txt', 'w') as f: + f.write('original\n') sftp.symlink('original.txt', FOLDER + '/link.txt') self.assertEqual(sftp.readlink(FOLDER + '/link.txt'), 'original.txt') - f = sftp.open(FOLDER + '/link.txt', 'r') - self.assertEqual(f.readlines(), ['original\n']) - f.close() + with sftp.open(FOLDER + '/link.txt', 'r') as f: + self.assertEqual(f.readlines(), ['original\n']) cwd = sftp.normalize('.') if cwd[-1] == '/': @@ -450,7 +434,7 @@ class SFTPTest (unittest.TestCase): self.assertEqual(sftp.stat(FOLDER + '/link.txt').st_size, 9) # the sftp server may be hiding extra path members from us, so the # length may be longer than we expect: - self.assert_(sftp.lstat(FOLDER + '/link2.txt').st_size >= len(abs_path)) + self.assertTrue(sftp.lstat(FOLDER + '/link2.txt').st_size >= len(abs_path)) self.assertEqual(sftp.stat(FOLDER + '/link2.txt').st_size, 9) self.assertEqual(sftp.stat(FOLDER + '/original.txt').st_size, 9) finally: @@ -471,18 +455,16 @@ class SFTPTest (unittest.TestCase): """ verify that buffered writes are automatically flushed on seek. """ - f = sftp.open(FOLDER + '/happy.txt', 'w', 1) try: - f.write('full line.\n') - f.write('partial') - f.seek(9, f.SEEK_SET) - f.write('?\n') - f.close() - - f = sftp.open(FOLDER + '/happy.txt', 'r') - self.assertEqual(f.readline(), 'full line?\n') - self.assertEqual(f.read(7), 'partial') - f.close() + with sftp.open(FOLDER + '/happy.txt', 'w', 1) as f: + f.write('full line.\n') + f.write('partial') + f.seek(9, f.SEEK_SET) + f.write('?\n') + + with sftp.open(FOLDER + '/happy.txt', 'r') as f: + self.assertEqual(f.readline(), 'full line?\n') + self.assertEqual(f.read(7), 'partial') finally: try: sftp.remove(FOLDER + '/happy.txt') @@ -495,10 +477,10 @@ class SFTPTest (unittest.TestCase): error. """ pwd = sftp.normalize('.') - self.assert_(len(pwd) > 0) + self.assertTrue(len(pwd) > 0) f = sftp.normalize('./' + FOLDER) - self.assert_(len(f) > 0) - self.assertEquals(os.path.join(pwd, FOLDER), f) + self.assertTrue(len(f) > 0) + self.assertEqual(os.path.join(pwd, FOLDER), f) def test_F_mkdir(self): """ @@ -507,19 +489,19 @@ class SFTPTest (unittest.TestCase): try: sftp.mkdir(FOLDER + '/subfolder') except: - self.assert_(False, 'exception creating subfolder') + self.assertTrue(False, 'exception creating subfolder') try: sftp.mkdir(FOLDER + '/subfolder') - self.assert_(False, 'no exception overwriting subfolder') + self.assertTrue(False, 'no exception overwriting subfolder') except IOError: pass try: sftp.rmdir(FOLDER + '/subfolder') except: - self.assert_(False, 'exception removing subfolder') + self.assertTrue(False, 'exception removing subfolder') try: sftp.rmdir(FOLDER + '/subfolder') - self.assert_(False, 'no exception removing nonexistent subfolder') + self.assertTrue(False, 'no exception removing nonexistent subfolder') except IOError: pass @@ -534,17 +516,16 @@ class SFTPTest (unittest.TestCase): sftp.mkdir(FOLDER + '/alpha') sftp.chdir(FOLDER + '/alpha') sftp.mkdir('beta') - self.assertEquals(root + FOLDER + '/alpha', sftp.getcwd()) - self.assertEquals(['beta'], sftp.listdir('.')) + self.assertEqual(root + FOLDER + '/alpha', sftp.getcwd()) + self.assertEqual(['beta'], sftp.listdir('.')) sftp.chdir('beta') - f = sftp.open('fish', 'w') - f.write('hello\n') - f.close() + with sftp.open('fish', 'w') as f: + f.write('hello\n') sftp.chdir('..') - self.assertEquals(['fish'], sftp.listdir('beta')) + self.assertEqual(['fish'], sftp.listdir('beta')) sftp.chdir('..') - self.assertEquals(['fish'], sftp.listdir('alpha/beta')) + self.assertEqual(['fish'], sftp.listdir('alpha/beta')) finally: sftp.chdir(root) try: @@ -566,30 +547,29 @@ class SFTPTest (unittest.TestCase): """ warnings.filterwarnings('ignore', 'tempnam.*') - localname = os.tempnam() - text = 'All I wanted was a plastic bunny rabbit.\n' - f = open(localname, 'wb') - f.write(text) - f.close() + fd, localname = mkstemp() + os.close(fd) + text = b'All I wanted was a plastic bunny rabbit.\n' + with open(localname, 'wb') as f: + f.write(text) saved_progress = [] def progress_callback(x, y): saved_progress.append((x, y)) sftp.put(localname, FOLDER + '/bunny.txt', progress_callback) - f = sftp.open(FOLDER + '/bunny.txt', 'r') - self.assertEquals(text, f.read(128)) - f.close() - self.assertEquals((41, 41), saved_progress[-1]) + with sftp.open(FOLDER + '/bunny.txt', 'rb') as f: + self.assertEqual(text, f.read(128)) + self.assertEqual((41, 41), saved_progress[-1]) os.unlink(localname) - localname = os.tempnam() + fd, localname = mkstemp() + os.close(fd) saved_progress = [] sftp.get(FOLDER + '/bunny.txt', localname, progress_callback) - f = open(localname, 'rb') - self.assertEquals(text, f.read(128)) - f.close() - self.assertEquals((41, 41), saved_progress[-1]) + with open(localname, 'rb') as f: + self.assertEqual(text, f.read(128)) + self.assertEqual((41, 41), saved_progress[-1]) os.unlink(localname) sftp.unlink(FOLDER + '/bunny.txt') @@ -600,20 +580,18 @@ class SFTPTest (unittest.TestCase): (it's an sftp extension that we support, and may be the only ones who support it.) """ - f = sftp.open(FOLDER + '/kitty.txt', 'w') - f.write('here kitty kitty' * 64) - f.close() + with sftp.open(FOLDER + '/kitty.txt', 'w') as f: + f.write('here kitty kitty' * 64) try: - f = sftp.open(FOLDER + '/kitty.txt', 'r') - sum = f.check('sha1') - self.assertEquals('91059CFC6615941378D413CB5ADAF4C5EB293402', hexlify(sum).upper()) - sum = f.check('md5', 0, 512) - self.assertEquals('93DE4788FCA28D471516963A1FE3856A', hexlify(sum).upper()) - sum = f.check('md5', 0, 0, 510) - self.assertEquals('EB3B45B8CD55A0707D99B177544A319F373183D241432BB2157AB9E46358C4AC90370B5CADE5D90336FC1716F90B36D6', - hexlify(sum).upper()) - f.close() + with sftp.open(FOLDER + '/kitty.txt', 'r') as f: + sum = f.check('sha1') + self.assertEqual('91059CFC6615941378D413CB5ADAF4C5EB293402', u(hexlify(sum)).upper()) + sum = f.check('md5', 0, 512) + self.assertEqual('93DE4788FCA28D471516963A1FE3856A', u(hexlify(sum)).upper()) + sum = f.check('md5', 0, 0, 510) + self.assertEqual('EB3B45B8CD55A0707D99B177544A319F373183D241432BB2157AB9E46358C4AC90370B5CADE5D90336FC1716F90B36D6', + u(hexlify(sum)).upper()) finally: sftp.unlink(FOLDER + '/kitty.txt') @@ -621,12 +599,11 @@ class SFTPTest (unittest.TestCase): """ verify that the 'x' flag works when opening a file. """ - f = sftp.open(FOLDER + '/unusual.txt', 'wx') - f.close() + sftp.open(FOLDER + '/unusual.txt', 'wx').close() try: try: - f = sftp.open(FOLDER + '/unusual.txt', 'wx') + sftp.open(FOLDER + '/unusual.txt', 'wx') self.fail('expected exception') except IOError: pass @@ -637,44 +614,39 @@ class SFTPTest (unittest.TestCase): """ verify that unicode strings are encoded into utf8 correctly. """ - f = sftp.open(FOLDER + '/something', 'w') - f.write('okay') - f.close() + with sftp.open(FOLDER + '/something', 'w') as f: + f.write('okay') try: - sftp.rename(FOLDER + '/something', FOLDER + u'/\u00fcnic\u00f8de') - sftp.open(FOLDER + '/\xc3\xbcnic\xc3\xb8\x64\x65', 'r') - except Exception, e: - self.fail('exception ' + e) - sftp.unlink(FOLDER + '/\xc3\xbcnic\xc3\xb8\x64\x65') + sftp.rename(FOLDER + '/something', FOLDER + '/' + unicode_folder) + sftp.open(b(FOLDER) + utf8_folder, 'r') + except Exception as e: + self.fail('exception ' + str(e)) + sftp.unlink(b(FOLDER) + utf8_folder) def test_L_utf8_chdir(self): - sftp.mkdir(FOLDER + u'\u00fcnic\u00f8de') + sftp.mkdir(FOLDER + '/' + unicode_folder) try: - sftp.chdir(FOLDER + u'\u00fcnic\u00f8de') - f = sftp.open('something', 'w') - f.write('okay') - f.close() + sftp.chdir(FOLDER + '/' + unicode_folder) + with sftp.open('something', 'w') as f: + f.write('okay') sftp.unlink('something') finally: - sftp.chdir(None) - sftp.rmdir(FOLDER + u'\u00fcnic\u00f8de') + sftp.chdir() + sftp.rmdir(FOLDER + '/' + unicode_folder) def test_M_bad_readv(self): """ verify that readv at the end of the file doesn't essplode. """ - f = sftp.open(FOLDER + '/zero', 'w') - f.close() + sftp.open(FOLDER + '/zero', 'w').close() try: - f = sftp.open(FOLDER + '/zero', 'r') - f.readv([(0, 12)]) - f.close() + with sftp.open(FOLDER + '/zero', 'r') as f: + f.readv([(0, 12)]) - f = sftp.open(FOLDER + '/zero', 'r') - f.prefetch() - f.read(100) - f.close() + with sftp.open(FOLDER + '/zero', 'r') as f: + f.prefetch() + f.read(100) finally: sftp.unlink(FOLDER + '/zero') @@ -684,45 +656,61 @@ class SFTPTest (unittest.TestCase): """ warnings.filterwarnings('ignore', 'tempnam.*') - localname = os.tempnam() + fd, localname = mkstemp() + os.close(fd) text = 'All I wanted was a plastic bunny rabbit.\n' - f = open(localname, 'wb') - f.write(text) - f.close() + with open(localname, 'w') as f: + f.write(text) saved_progress = [] def progress_callback(x, y): saved_progress.append((x, y)) res = sftp.put(localname, FOLDER + '/bunny.txt', progress_callback, False) - self.assertEquals(SFTPAttributes().attr, res.attr) + self.assertEqual(SFTPAttributes().attr, res.attr) - f = sftp.open(FOLDER + '/bunny.txt', 'r') - self.assertEquals(text, f.read(128)) - f.close() - self.assertEquals((41, 41), saved_progress[-1]) + with sftp.open(FOLDER + '/bunny.txt', 'r') as f: + self.assertEqual(text, f.read(128)) + self.assertEqual((41, 41), saved_progress[-1]) os.unlink(localname) sftp.unlink(FOLDER + '/bunny.txt') + def test_O_getcwd(self): + """ + verify that chdir/getcwd work. + """ + self.assertEqual(None, sftp.getcwd()) + root = sftp.normalize('.') + if root[-1] != '/': + root += '/' + try: + sftp.mkdir(FOLDER + '/alpha') + sftp.chdir(FOLDER + '/alpha') + self.assertEqual('/' + FOLDER + '/alpha', sftp.getcwd()) + finally: + sftp.chdir(root) + try: + sftp.rmdir(FOLDER + '/alpha') + except: + pass + def XXX_test_M_seek_append(self): """ verify that seek does't affect writes during append. does not work except through paramiko. :( openssh fails. """ - f = sftp.open(FOLDER + '/append.txt', 'a') try: - f.write('first line\nsecond line\n') - f.seek(11, f.SEEK_SET) - f.write('third line\n') - f.close() - - f = sftp.open(FOLDER + '/append.txt', 'r') - self.assertEqual(f.stat().st_size, 34) - self.assertEqual(f.readline(), 'first line\n') - self.assertEqual(f.readline(), 'second line\n') - self.assertEqual(f.readline(), 'third line\n') - f.close() + with sftp.open(FOLDER + '/append.txt', 'a') as f: + f.write('first line\nsecond line\n') + f.seek(11, f.SEEK_SET) + f.write('third line\n') + + with sftp.open(FOLDER + '/append.txt', 'r') as f: + self.assertEqual(f.stat().st_size, 34) + self.assertEqual(f.readline(), 'first line\n') + self.assertEqual(f.readline(), 'second line\n') + self.assertEqual(f.readline(), 'third line\n') finally: sftp.remove(FOLDER + '/append.txt') @@ -731,10 +719,16 @@ class SFTPTest (unittest.TestCase): Send an empty file and confirm it is sent. """ target = FOLDER + '/empty file.txt' - stream = StringIO.StringIO() + stream = StringIO() try: attrs = sftp.putfo(stream, target) # the returned attributes should not be null self.assertNotEqual(attrs, None) finally: sftp.remove(target) + + +if __name__ == '__main__': + SFTPTest.init_loopback() + from unittest import main + main() diff --git a/tests/test_sftp_big.py b/tests/test_sftp_big.py index 04b15b0d..6870c6b4 100644 --- a/tests/test_sftp_big.py +++ b/tests/test_sftp_big.py @@ -33,9 +33,10 @@ import time import unittest import paramiko -from stub_sftp import StubServer, StubSFTPServer -from loop import LoopSocket -from test_sftp import get_sftp +from paramiko.common import * +from tests.stub_sftp import StubServer, StubSFTPServer +from tests.loop import LoopSocket +from tests.test_sftp import get_sftp FOLDER = os.environ.get('TEST_FOLDER', 'temp-testing000') @@ -45,7 +46,7 @@ class BigSFTPTest (unittest.TestCase): def setUp(self): global FOLDER sftp = get_sftp() - for i in xrange(1000): + for i in range(1000): FOLDER = FOLDER[:-3] + '%03d' % i try: sftp.mkdir(FOLDER) @@ -65,19 +66,17 @@ class BigSFTPTest (unittest.TestCase): numfiles = 100 try: for i in range(numfiles): - f = sftp.open('%s/file%d.txt' % (FOLDER, i), 'w', 1) - f.write('this is file #%d.\n' % i) - f.close() - sftp.chmod('%s/file%d.txt' % (FOLDER, i), 0660) + with sftp.open('%s/file%d.txt' % (FOLDER, i), 'w', 1) as f: + f.write('this is file #%d.\n' % i) + sftp.chmod('%s/file%d.txt' % (FOLDER, i), o660) # now make sure every file is there, by creating a list of filenmes # and reading them in random order. - numlist = range(numfiles) + numlist = list(range(numfiles)) while len(numlist) > 0: r = numlist[random.randint(0, len(numlist) - 1)] - f = sftp.open('%s/file%d.txt' % (FOLDER, r)) - self.assertEqual(f.readline(), 'this is file #%d.\n' % r) - f.close() + with sftp.open('%s/file%d.txt' % (FOLDER, r)) as f: + self.assertEqual(f.readline(), 'this is file #%d.\n' % r) numlist.remove(r) finally: for i in range(numfiles): @@ -94,12 +93,11 @@ class BigSFTPTest (unittest.TestCase): kblob = (1024 * 'x') start = time.time() try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'w') as f: + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) @@ -107,11 +105,10 @@ class BigSFTPTest (unittest.TestCase): sys.stderr.write('%ds ' % round(end - start)) start = time.time() - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - for n in range(1024): - data = f.read(1024) - self.assertEqual(data, kblob) - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'r') as f: + for n in range(1024): + data = f.read(1024) + self.assertEqual(data, kblob) end = time.time() sys.stderr.write('%ds ' % round(end - start)) @@ -123,16 +120,15 @@ class BigSFTPTest (unittest.TestCase): write a 1MB file, with no linefeeds, using pipelining. """ sftp = get_sftp() - kblob = ''.join([struct.pack('>H', n) for n in xrange(512)]) + kblob = bytes().join([struct.pack('>H', n) for n in range(512)]) start = time.time() try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - f.set_pipelined(True) - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'wb') as f: + f.set_pipelined(True) + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) @@ -140,22 +136,21 @@ class BigSFTPTest (unittest.TestCase): sys.stderr.write('%ds ' % round(end - start)) start = time.time() - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - f.prefetch() + with sftp.open('%s/hongry.txt' % FOLDER, 'rb') as f: + f.prefetch() - # read on odd boundaries to make sure the bytes aren't getting scrambled - n = 0 - k2blob = kblob + kblob - chunk = 629 - size = 1024 * 1024 - while n < size: - if n + chunk > size: - chunk = size - n - data = f.read(chunk) - offset = n % 1024 - self.assertEqual(data, k2blob[offset:offset + chunk]) - n += chunk - f.close() + # read on odd boundaries to make sure the bytes aren't getting scrambled + n = 0 + k2blob = kblob + kblob + chunk = 629 + size = 1024 * 1024 + while n < size: + if n + chunk > size: + chunk = size - n + data = f.read(chunk) + offset = n % 1024 + self.assertEqual(data, k2blob[offset:offset + chunk]) + n += chunk end = time.time() sys.stderr.write('%ds ' % round(end - start)) @@ -164,15 +159,14 @@ class BigSFTPTest (unittest.TestCase): def test_4_prefetch_seek(self): sftp = get_sftp() - kblob = ''.join([struct.pack('>H', n) for n in xrange(512)]) + kblob = bytes().join([struct.pack('>H', n) for n in range(512)]) try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - f.set_pipelined(True) - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'wb') as f: + f.set_pipelined(True) + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) @@ -180,21 +174,20 @@ class BigSFTPTest (unittest.TestCase): start = time.time() k2blob = kblob + kblob chunk = 793 - for i in xrange(10): - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - f.prefetch() - base_offset = (512 * 1024) + 17 * random.randint(1000, 2000) - offsets = [base_offset + j * chunk for j in xrange(100)] - # randomly seek around and read them out - for j in xrange(100): - offset = offsets[random.randint(0, len(offsets) - 1)] - offsets.remove(offset) - f.seek(offset) - data = f.read(chunk) - n_offset = offset % 1024 - self.assertEqual(data, k2blob[n_offset:n_offset + chunk]) - offset += chunk - f.close() + for i in range(10): + with sftp.open('%s/hongry.txt' % FOLDER, 'rb') as f: + f.prefetch() + base_offset = (512 * 1024) + 17 * random.randint(1000, 2000) + offsets = [base_offset + j * chunk for j in range(100)] + # randomly seek around and read them out + for j in range(100): + offset = offsets[random.randint(0, len(offsets) - 1)] + offsets.remove(offset) + f.seek(offset) + data = f.read(chunk) + n_offset = offset % 1024 + self.assertEqual(data, k2blob[n_offset:n_offset + chunk]) + offset += chunk end = time.time() sys.stderr.write('%ds ' % round(end - start)) finally: @@ -202,15 +195,14 @@ class BigSFTPTest (unittest.TestCase): def test_5_readv_seek(self): sftp = get_sftp() - kblob = ''.join([struct.pack('>H', n) for n in xrange(512)]) + kblob = bytes().join([struct.pack('>H', n) for n in range(512)]) try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - f.set_pipelined(True) - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'wb') as f: + f.set_pipelined(True) + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) @@ -218,22 +210,21 @@ class BigSFTPTest (unittest.TestCase): start = time.time() k2blob = kblob + kblob chunk = 793 - for i in xrange(10): - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - base_offset = (512 * 1024) + 17 * random.randint(1000, 2000) - # make a bunch of offsets and put them in random order - offsets = [base_offset + j * chunk for j in xrange(100)] - readv_list = [] - for j in xrange(100): - o = offsets[random.randint(0, len(offsets) - 1)] - offsets.remove(o) - readv_list.append((o, chunk)) - ret = f.readv(readv_list) - for i in xrange(len(readv_list)): - offset = readv_list[i][0] - n_offset = offset % 1024 - self.assertEqual(ret.next(), k2blob[n_offset:n_offset + chunk]) - f.close() + for i in range(10): + with sftp.open('%s/hongry.txt' % FOLDER, 'rb') as f: + base_offset = (512 * 1024) + 17 * random.randint(1000, 2000) + # make a bunch of offsets and put them in random order + offsets = [base_offset + j * chunk for j in range(100)] + readv_list = [] + for j in range(100): + o = offsets[random.randint(0, len(offsets) - 1)] + offsets.remove(o) + readv_list.append((o, chunk)) + ret = f.readv(readv_list) + for i in range(len(readv_list)): + offset = readv_list[i][0] + n_offset = offset % 1024 + self.assertEqual(next(ret), k2blob[n_offset:n_offset + chunk]) end = time.time() sys.stderr.write('%ds ' % round(end - start)) finally: @@ -247,28 +238,26 @@ class BigSFTPTest (unittest.TestCase): sftp = get_sftp() kblob = (1024 * 'x') try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - f.set_pipelined(True) - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'w') as f: + f.set_pipelined(True) + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) for i in range(10): - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') + with sftp.open('%s/hongry.txt' % FOLDER, 'r') as f: + f.prefetch() + with sftp.open('%s/hongry.txt' % FOLDER, 'r') as f: f.prefetch() - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - f.prefetch() - for n in range(1024): - data = f.read(1024) - self.assertEqual(data, kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + for n in range(1024): + data = f.read(1024) + self.assertEqual(data, kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') finally: sftp.remove('%s/hongry.txt' % FOLDER) @@ -278,35 +267,33 @@ class BigSFTPTest (unittest.TestCase): verify that prefetch and readv don't conflict with each other. """ sftp = get_sftp() - kblob = ''.join([struct.pack('>H', n) for n in xrange(512)]) + kblob = bytes().join([struct.pack('>H', n) for n in range(512)]) try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - f.set_pipelined(True) - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'wb') as f: + f.set_pipelined(True) + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - f.prefetch() - data = f.read(1024) - self.assertEqual(data, kblob) - - chunk_size = 793 - base_offset = 512 * 1024 - k2blob = kblob + kblob - chunks = [(base_offset + (chunk_size * i), chunk_size) for i in range(20)] - for data in f.readv(chunks): - offset = base_offset % 1024 - self.assertEqual(chunk_size, len(data)) - self.assertEqual(k2blob[offset:offset + chunk_size], data) - base_offset += chunk_size - - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'rb') as f: + f.prefetch() + data = f.read(1024) + self.assertEqual(data, kblob) + + chunk_size = 793 + base_offset = 512 * 1024 + k2blob = kblob + kblob + chunks = [(base_offset + (chunk_size * i), chunk_size) for i in range(20)] + for data in f.readv(chunks): + offset = base_offset % 1024 + self.assertEqual(chunk_size, len(data)) + self.assertEqual(k2blob[offset:offset + chunk_size], data) + base_offset += chunk_size + sys.stderr.write(' ') finally: sftp.remove('%s/hongry.txt' % FOLDER) @@ -317,26 +304,24 @@ class BigSFTPTest (unittest.TestCase): returned as a single blob. """ sftp = get_sftp() - kblob = ''.join([struct.pack('>H', n) for n in xrange(512)]) + kblob = bytes().join([struct.pack('>H', n) for n in range(512)]) try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w') - f.set_pipelined(True) - for n in range(1024): - f.write(kblob) - if n % 128 == 0: - sys.stderr.write('.') - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'wb') as f: + f.set_pipelined(True) + for n in range(1024): + f.write(kblob) + if n % 128 == 0: + sys.stderr.write('.') sys.stderr.write(' ') self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) - f = sftp.open('%s/hongry.txt' % FOLDER, 'r') - data = list(f.readv([(23 * 1024, 128 * 1024)])) - self.assertEqual(1, len(data)) - data = data[0] - self.assertEqual(128 * 1024, len(data)) + with sftp.open('%s/hongry.txt' % FOLDER, 'rb') as f: + data = list(f.readv([(23 * 1024, 128 * 1024)])) + self.assertEqual(1, len(data)) + data = data[0] + self.assertEqual(128 * 1024, len(data)) - f.close() sys.stderr.write(' ') finally: sftp.remove('%s/hongry.txt' % FOLDER) @@ -348,9 +333,8 @@ class BigSFTPTest (unittest.TestCase): sftp = get_sftp() mblob = (1024 * 1024 * 'x') try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w', 128 * 1024) - f.write(mblob) - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'w', 128 * 1024) as f: + f.write(mblob) self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) finally: @@ -365,21 +349,26 @@ class BigSFTPTest (unittest.TestCase): t.packetizer.REKEY_BYTES = 512 * 1024 k32blob = (32 * 1024 * 'x') try: - f = sftp.open('%s/hongry.txt' % FOLDER, 'w', 128 * 1024) - for i in xrange(32): - f.write(k32blob) - f.close() - + with sftp.open('%s/hongry.txt' % FOLDER, 'w', 128 * 1024) as f: + for i in range(32): + f.write(k32blob) + self.assertEqual(sftp.stat('%s/hongry.txt' % FOLDER).st_size, 1024 * 1024) - self.assertNotEquals(t.H, t.session_id) + self.assertNotEqual(t.H, t.session_id) # try to read it too. - f = sftp.open('%s/hongry.txt' % FOLDER, 'r', 128 * 1024) - f.prefetch() - total = 0 - while total < 1024 * 1024: - total += len(f.read(32 * 1024)) - f.close() + with sftp.open('%s/hongry.txt' % FOLDER, 'r', 128 * 1024) as f: + f.prefetch() + total = 0 + while total < 1024 * 1024: + total += len(f.read(32 * 1024)) finally: sftp.remove('%s/hongry.txt' % FOLDER) t.packetizer.REKEY_BYTES = pow(2, 30) + + +if __name__ == '__main__': + from tests.test_sftp import SFTPTest + SFTPTest.init_loopback() + from unittest import main + main() diff --git a/tests/test_transport.py b/tests/test_transport.py index e8f7f366..876759c8 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -20,7 +20,7 @@ Some unit tests for the ssh2 protocol in Transport. """ -from binascii import hexlify, unhexlify +from binascii import hexlify import select import socket import sys @@ -33,10 +33,10 @@ from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey SSHException, BadAuthenticationType, InteractiveQuery, ChannelException from paramiko import AUTH_FAILED, AUTH_PARTIALLY_SUCCESSFUL, AUTH_SUCCESSFUL from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED -from paramiko.common import MSG_KEXINIT, MSG_CHANNEL_WINDOW_ADJUST +from paramiko.common import MSG_KEXINIT, MSG_CHANNEL_WINDOW_ADJUST, b, bytes from paramiko.message import Message -from loop import LoopSocket -from util import ParamikoTest +from tests.loop import LoopSocket +from tests.util import ParamikoTest, test_path LONG_BANNER = """\ @@ -55,7 +55,7 @@ Maybe. class NullServer (ServerInterface): paranoid_did_password = False paranoid_did_public_key = False - paranoid_key = DSSKey.from_private_key_file('tests/test_dss.key') + paranoid_key = DSSKey.from_private_key_file(test_path('test_dss.key')) def get_allowed_auths(self, username): if username == 'slowdive': @@ -121,8 +121,8 @@ class TransportTest(ParamikoTest): self.sockc.close() def setup_test_server(self, client_options=None, server_options=None): - host_key = RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = RSAKey(data=str(host_key)) + host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = RSAKey(data=host_key.asbytes()) self.ts.add_server_key(host_key) if client_options is not None: @@ -132,37 +132,37 @@ class TransportTest(ParamikoTest): event = threading.Event() self.server = NullServer() - self.assert_(not event.isSet()) + self.assertTrue(not event.isSet()) self.ts.start_server(event, self.server) self.tc.connect(hostkey=public_host_key, username='slowdive', password='pygmalion') event.wait(1.0) - self.assert_(event.isSet()) - self.assert_(self.ts.is_active()) + self.assertTrue(event.isSet()) + self.assertTrue(self.ts.is_active()) def test_1_security_options(self): o = self.tc.get_security_options() - self.assertEquals(type(o), SecurityOptions) - self.assert_(('aes256-cbc', 'blowfish-cbc') != o.ciphers) + self.assertEqual(type(o), SecurityOptions) + self.assertTrue(('aes256-cbc', 'blowfish-cbc') != o.ciphers) o.ciphers = ('aes256-cbc', 'blowfish-cbc') - self.assertEquals(('aes256-cbc', 'blowfish-cbc'), o.ciphers) + self.assertEqual(('aes256-cbc', 'blowfish-cbc'), o.ciphers) try: o.ciphers = ('aes256-cbc', 'made-up-cipher') - self.assert_(False) + self.assertTrue(False) except ValueError: pass try: o.ciphers = 23 - self.assert_(False) + self.assertTrue(False) except TypeError: pass def test_2_compute_key(self): - self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929L - self.tc.H = unhexlify('0C8307CDE6856FF30BA93684EB0F04C2520E9ED3') + self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929 + self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3' self.tc.session_id = self.tc.H key = self.tc._compute_key('C', 32) - self.assertEquals('207E66594CA87C44ECCBA3B3CD39FDDB378E6FDB0F97C54B2AA0CFBF900CD995', + self.assertEqual(b'207E66594CA87C44ECCBA3B3CD39FDDB378E6FDB0F97C54B2AA0CFBF900CD995', hexlify(key).upper()) def test_3_simple(self): @@ -171,44 +171,44 @@ class TransportTest(ParamikoTest): loopback sockets. this is hardly "simple" but it's simpler than the later tests. :) """ - host_key = RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = RSAKey(data=str(host_key)) + host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = RSAKey(data=host_key.asbytes()) self.ts.add_server_key(host_key) event = threading.Event() server = NullServer() - self.assert_(not event.isSet()) - self.assertEquals(None, self.tc.get_username()) - self.assertEquals(None, self.ts.get_username()) - self.assertEquals(False, self.tc.is_authenticated()) - self.assertEquals(False, self.ts.is_authenticated()) + self.assertTrue(not event.isSet()) + self.assertEqual(None, self.tc.get_username()) + self.assertEqual(None, self.ts.get_username()) + self.assertEqual(False, self.tc.is_authenticated()) + self.assertEqual(False, self.ts.is_authenticated()) self.ts.start_server(event, server) self.tc.connect(hostkey=public_host_key, username='slowdive', password='pygmalion') event.wait(1.0) - self.assert_(event.isSet()) - self.assert_(self.ts.is_active()) - self.assertEquals('slowdive', self.tc.get_username()) - self.assertEquals('slowdive', self.ts.get_username()) - self.assertEquals(True, self.tc.is_authenticated()) - self.assertEquals(True, self.ts.is_authenticated()) + self.assertTrue(event.isSet()) + self.assertTrue(self.ts.is_active()) + self.assertEqual('slowdive', self.tc.get_username()) + self.assertEqual('slowdive', self.ts.get_username()) + self.assertEqual(True, self.tc.is_authenticated()) + self.assertEqual(True, self.ts.is_authenticated()) def test_3a_long_banner(self): """ verify that a long banner doesn't mess up the handshake. """ - host_key = RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = RSAKey(data=str(host_key)) + host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = RSAKey(data=host_key.asbytes()) self.ts.add_server_key(host_key) event = threading.Event() server = NullServer() - self.assert_(not event.isSet()) + self.assertTrue(not event.isSet()) self.socks.send(LONG_BANNER) self.ts.start_server(event, server) self.tc.connect(hostkey=public_host_key, username='slowdive', password='pygmalion') event.wait(1.0) - self.assert_(event.isSet()) - self.assert_(self.ts.is_active()) + self.assertTrue(event.isSet()) + self.assertTrue(self.ts.is_active()) def test_4_special(self): """ @@ -219,10 +219,10 @@ class TransportTest(ParamikoTest): options.ciphers = ('aes256-cbc',) options.digests = ('hmac-md5-96',) self.setup_test_server(client_options=force_algorithms) - self.assertEquals('aes256-cbc', self.tc.local_cipher) - self.assertEquals('aes256-cbc', self.tc.remote_cipher) - self.assertEquals(12, self.tc.packetizer.get_mac_size_out()) - self.assertEquals(12, self.tc.packetizer.get_mac_size_in()) + self.assertEqual('aes256-cbc', self.tc.local_cipher) + self.assertEqual('aes256-cbc', self.tc.remote_cipher) + self.assertEqual(12, self.tc.packetizer.get_mac_size_out()) + self.assertEqual(12, self.tc.packetizer.get_mac_size_in()) self.tc.send_ignore(1024) self.tc.renegotiate_keys() @@ -233,10 +233,10 @@ class TransportTest(ParamikoTest): verify that the keepalive will be sent. """ self.setup_test_server() - self.assertEquals(None, getattr(self.server, '_global_request', None)) + self.assertEqual(None, getattr(self.server, '_global_request', None)) self.tc.set_keepalive(1) time.sleep(2) - self.assertEquals('keepalive@lag.net', self.server._global_request) + self.assertEqual('keepalive@lag.net', self.server._global_request) def test_6_exec_command(self): """ @@ -248,8 +248,8 @@ class TransportTest(ParamikoTest): schan = self.ts.accept(1.0) try: chan.exec_command('no') - self.assert_(False) - except SSHException, x: + self.assertTrue(False) + except SSHException: pass chan = self.tc.open_session() @@ -260,11 +260,11 @@ class TransportTest(ParamikoTest): schan.close() f = chan.makefile() - self.assertEquals('Hello there.\n', f.readline()) - self.assertEquals('', f.readline()) + self.assertEqual('Hello there.\n', f.readline()) + self.assertEqual('', f.readline()) f = chan.makefile_stderr() - self.assertEquals('This is on stderr.\n', f.readline()) - self.assertEquals('', f.readline()) + self.assertEqual('This is on stderr.\n', f.readline()) + self.assertEqual('', f.readline()) # now try it with combined stdout/stderr chan = self.tc.open_session() @@ -276,9 +276,9 @@ class TransportTest(ParamikoTest): chan.set_combine_stderr(True) f = chan.makefile() - self.assertEquals('Hello there.\n', f.readline()) - self.assertEquals('This is on stderr.\n', f.readline()) - self.assertEquals('', f.readline()) + self.assertEqual('Hello there.\n', f.readline()) + self.assertEqual('This is on stderr.\n', f.readline()) + self.assertEqual('', f.readline()) def test_7_invoke_shell(self): """ @@ -290,9 +290,9 @@ class TransportTest(ParamikoTest): schan = self.ts.accept(1.0) chan.send('communist j. cat\n') f = schan.makefile() - self.assertEquals('communist j. cat\n', f.readline()) + self.assertEqual('communist j. cat\n', f.readline()) chan.close() - self.assertEquals('', f.readline()) + self.assertEqual('', f.readline()) def test_8_channel_exception(self): """ @@ -302,8 +302,8 @@ class TransportTest(ParamikoTest): try: chan = self.tc.open_channel('bogus') self.fail('expected exception') - except ChannelException, x: - self.assert_(x.code == OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED) + except ChannelException as e: + self.assertTrue(e.code == OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED) def test_9_exit_status(self): """ @@ -315,7 +315,7 @@ class TransportTest(ParamikoTest): schan = self.ts.accept(1.0) chan.exec_command('yes') schan.send('Hello there.\n') - self.assert_(not chan.exit_status_ready()) + self.assertTrue(not chan.exit_status_ready()) # trigger an EOF schan.shutdown_read() schan.shutdown_write() @@ -323,15 +323,15 @@ class TransportTest(ParamikoTest): schan.close() f = chan.makefile() - self.assertEquals('Hello there.\n', f.readline()) - self.assertEquals('', f.readline()) + self.assertEqual('Hello there.\n', f.readline()) + self.assertEqual('', f.readline()) count = 0 while not chan.exit_status_ready(): time.sleep(0.1) count += 1 if count > 50: raise Exception("timeout") - self.assertEquals(23, chan.recv_exit_status()) + self.assertEqual(23, chan.recv_exit_status()) chan.close() def test_A_select(self): @@ -345,9 +345,9 @@ class TransportTest(ParamikoTest): # nothing should be ready r, w, e = select.select([chan], [], [], 0.1) - self.assertEquals([], r) - self.assertEquals([], w) - self.assertEquals([], e) + self.assertEqual([], r) + self.assertEqual([], w) + self.assertEqual([], e) schan.send('hello\n') @@ -357,17 +357,17 @@ class TransportTest(ParamikoTest): if chan in r: break time.sleep(0.1) - self.assertEquals([chan], r) - self.assertEquals([], w) - self.assertEquals([], e) + self.assertEqual([chan], r) + self.assertEqual([], w) + self.assertEqual([], e) - self.assertEquals('hello\n', chan.recv(6)) + self.assertEqual(b'hello\n', chan.recv(6)) # and, should be dead again now r, w, e = select.select([chan], [], [], 0.1) - self.assertEquals([], r) - self.assertEquals([], w) - self.assertEquals([], e) + self.assertEqual([], r) + self.assertEqual([], w) + self.assertEqual([], e) schan.close() @@ -377,17 +377,17 @@ class TransportTest(ParamikoTest): if chan in r: break time.sleep(0.1) - self.assertEquals([chan], r) - self.assertEquals([], w) - self.assertEquals([], e) - self.assertEquals('', chan.recv(16)) + self.assertEqual([chan], r) + self.assertEqual([], w) + self.assertEqual([], e) + self.assertEqual(bytes(), chan.recv(16)) # make sure the pipe is still open for now... p = chan._pipe - self.assertEquals(False, p._closed) + self.assertEqual(False, p._closed) chan.close() # ...and now is closed. - self.assertEquals(True, p._closed) + self.assertEqual(True, p._closed) def test_B_renegotiate(self): """ @@ -399,17 +399,17 @@ class TransportTest(ParamikoTest): chan.exec_command('yes') schan = self.ts.accept(1.0) - self.assertEquals(self.tc.H, self.tc.session_id) + self.assertEqual(self.tc.H, self.tc.session_id) for i in range(20): chan.send('x' * 1024) chan.close() # allow a few seconds for the rekeying to complete - for i in xrange(50): + for i in range(50): if self.tc.H != self.tc.session_id: break time.sleep(0.1) - self.assertNotEquals(self.tc.H, self.tc.session_id) + self.assertNotEqual(self.tc.H, self.tc.session_id) schan.close() @@ -428,8 +428,8 @@ class TransportTest(ParamikoTest): chan.send('x' * 1024) bytes2 = self.tc.packetizer._Packetizer__sent_bytes # tests show this is actually compressed to *52 bytes*! including packet overhead! nice!! :) - self.assert_(bytes2 - bytes < 1024) - self.assertEquals(52, bytes2 - bytes) + self.assertTrue(bytes2 - bytes < 1024) + self.assertEqual(52, bytes2 - bytes) chan.close() schan.close() @@ -444,24 +444,25 @@ class TransportTest(ParamikoTest): schan = self.ts.accept(1.0) requested = [] - def handler(c, (addr, port)): + def handler(c, addr_port): + addr, port = addr_port requested.append((addr, port)) self.tc._queue_incoming_channel(c) - self.assertEquals(None, getattr(self.server, '_x11_screen_number', None)) + self.assertEqual(None, getattr(self.server, '_x11_screen_number', None)) cookie = chan.request_x11(0, single_connection=True, handler=handler) - self.assertEquals(0, self.server._x11_screen_number) - self.assertEquals('MIT-MAGIC-COOKIE-1', self.server._x11_auth_protocol) - self.assertEquals(cookie, self.server._x11_auth_cookie) - self.assertEquals(True, self.server._x11_single_connection) + self.assertEqual(0, self.server._x11_screen_number) + self.assertEqual('MIT-MAGIC-COOKIE-1', self.server._x11_auth_protocol) + self.assertEqual(cookie, self.server._x11_auth_cookie) + self.assertEqual(True, self.server._x11_single_connection) x11_server = self.ts.open_x11_channel(('localhost', 6093)) x11_client = self.tc.accept() - self.assertEquals('localhost', requested[0][0]) - self.assertEquals(6093, requested[0][1]) + self.assertEqual('localhost', requested[0][0]) + self.assertEqual(6093, requested[0][1]) x11_server.send('hello') - self.assertEquals('hello', x11_client.recv(5)) + self.assertEqual(b'hello', x11_client.recv(5)) x11_server.close() x11_client.close() @@ -479,13 +480,13 @@ class TransportTest(ParamikoTest): schan = self.ts.accept(1.0) requested = [] - def handler(c, (origin_addr, origin_port), (server_addr, server_port)): - requested.append((origin_addr, origin_port)) - requested.append((server_addr, server_port)) + def handler(c, origin_addr_port, server_addr_port): + requested.append(origin_addr_port) + requested.append(server_addr_port) self.tc._queue_incoming_channel(c) port = self.tc.request_port_forward('127.0.0.1', 0, handler) - self.assertEquals(port, self.server._listen.getsockname()[1]) + self.assertEqual(port, self.server._listen.getsockname()[1]) cs = socket.socket() cs.connect(('127.0.0.1', port)) @@ -494,7 +495,7 @@ class TransportTest(ParamikoTest): cch = self.tc.accept() sch.send('hello') - self.assertEquals('hello', cch.recv(5)) + self.assertEqual(b'hello', cch.recv(5)) sch.close() cch.close() ss.close() @@ -526,12 +527,12 @@ class TransportTest(ParamikoTest): cch.connect(self.server._tcpip_dest) ss, _ = greeting_server.accept() - ss.send('Hello!\n') + ss.send(b'Hello!\n') ss.close() sch.send(cch.recv(8192)) sch.close() - self.assertEquals('Hello!\n', cs.recv(7)) + self.assertEqual(b'Hello!\n', cs.recv(7)) cs.close() def test_G_stderr_select(self): @@ -546,9 +547,9 @@ class TransportTest(ParamikoTest): # nothing should be ready r, w, e = select.select([chan], [], [], 0.1) - self.assertEquals([], r) - self.assertEquals([], w) - self.assertEquals([], e) + self.assertEqual([], r) + self.assertEqual([], w) + self.assertEqual([], e) schan.send_stderr('hello\n') @@ -558,17 +559,17 @@ class TransportTest(ParamikoTest): if chan in r: break time.sleep(0.1) - self.assertEquals([chan], r) - self.assertEquals([], w) - self.assertEquals([], e) + self.assertEqual([chan], r) + self.assertEqual([], w) + self.assertEqual([], e) - self.assertEquals('hello\n', chan.recv_stderr(6)) + self.assertEqual(b'hello\n', chan.recv_stderr(6)) # and, should be dead again now r, w, e = select.select([chan], [], [], 0.1) - self.assertEquals([], r) - self.assertEquals([], w) - self.assertEquals([], e) + self.assertEqual([], r) + self.assertEqual([], w) + self.assertEqual([], e) schan.close() chan.close() @@ -582,7 +583,7 @@ class TransportTest(ParamikoTest): chan.invoke_shell() schan = self.ts.accept(1.0) - self.assertEquals(chan.send_ready(), True) + self.assertEqual(chan.send_ready(), True) total = 0 K = '*' * 1024 while total < 1024 * 1024: @@ -590,11 +591,11 @@ class TransportTest(ParamikoTest): total += len(K) if not chan.send_ready(): break - self.assert_(total < 1024 * 1024) + self.assertTrue(total < 1024 * 1024) schan.close() chan.close() - self.assertEquals(chan.send_ready(), True) + self.assertEqual(chan.send_ready(), True) def test_I_rekey_deadlock(self): """ @@ -657,7 +658,7 @@ class TransportTest(ParamikoTest): def run(self): try: - for i in xrange(1, 1+self.iterations): + for i in range(1, 1+self.iterations): if self.done_event.isSet(): break self.watchdog_event.set() @@ -706,7 +707,7 @@ class TransportTest(ParamikoTest): # Simulate in-transit MSG_CHANNEL_WINDOW_ADJUST by sending it # before responding to the incoming MSG_KEXINIT. m2 = Message() - m2.add_byte(chr(MSG_CHANNEL_WINDOW_ADJUST)) + m2.add_byte(cMSG_CHANNEL_WINDOW_ADJUST) m2.add_int(chan.remote_chanid) m2.add_int(1) # bytes to add self._send_message(m2) diff --git a/tests/test_util.py b/tests/test_util.py index 12677a9b..4f85c391 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -21,15 +21,15 @@ Some unit tests for utility functions. """ from binascii import hexlify -import cStringIO import errno import os import unittest from Crypto.Hash import SHA import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config +from paramiko.py3compat import StringIO, byte_ord, b -from util import ParamikoTest +from tests.util import ParamikoTest test_config_file = """\ Host * @@ -65,7 +65,7 @@ class UtilTest(ParamikoTest): """ verify that all the classes can be imported from paramiko. """ - symbols = globals().keys() + symbols = list(globals().keys()) self.assertTrue('Transport' in symbols) self.assertTrue('SSHClient' in symbols) self.assertTrue('MissingHostKeyPolicy' in symbols) @@ -101,9 +101,9 @@ class UtilTest(ParamikoTest): def test_2_parse_config(self): global test_config_file - f = cStringIO.StringIO(test_config_file) + f = StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) - self.assertEquals(config._config, + self.assertEqual(config._config, [{'host': ['*'], 'config': {}}, {'host': ['*'], 'config': {'identityfile': ['~/.ssh/id_rsa'], 'user': 'robey'}}, {'host': ['*.example.com'], 'config': {'user': 'bjork', 'port': '3333'}}, {'host': ['*'], 'config': {'crazy': 'something dumb '}}, @@ -111,7 +111,7 @@ class UtilTest(ParamikoTest): def test_3_host_config(self): global test_config_file - f = cStringIO.StringIO(test_config_file) + f = StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) for host, values in { @@ -131,27 +131,26 @@ class UtilTest(ParamikoTest): hostname=host, identityfile=[os.path.expanduser("~/.ssh/id_rsa")] ) - self.assertEquals( + self.assertEqual( paramiko.util.lookup_ssh_host_config(host, config), values ) def test_4_generate_key_bytes(self): - x = paramiko.util.generate_key_bytes(SHA, 'ABCDEFGH', 'This is my secret passphrase.', 64) - hex = ''.join(['%02x' % ord(c) for c in x]) - self.assertEquals(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b') + x = paramiko.util.generate_key_bytes(SHA, b'ABCDEFGH', 'This is my secret passphrase.', 64) + hex = ''.join(['%02x' % byte_ord(c) for c in x]) + self.assertEqual(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b') def test_5_host_keys(self): - f = open('hostfile.temp', 'w') - f.write(test_hosts_file) - f.close() + with open('hostfile.temp', 'w') as f: + f.write(test_hosts_file) try: hostdict = paramiko.util.load_host_keys('hostfile.temp') - self.assertEquals(2, len(hostdict)) - self.assertEquals(1, len(hostdict.values()[0])) - self.assertEquals(1, len(hostdict.values()[1])) + self.assertEqual(2, len(hostdict)) + self.assertEqual(1, len(list(hostdict.values())[0])) + self.assertEqual(1, len(list(hostdict.values())[1])) fp = hexlify(hostdict['secure.example.com']['ssh-rsa'].get_fingerprint()).upper() - self.assertEquals('E6684DB30E109B67B70FF1DC5C7F1363', fp) + self.assertEqual(b'E6684DB30E109B67B70FF1DC5C7F1363', fp) finally: os.unlink('hostfile.temp') @@ -159,7 +158,7 @@ class UtilTest(ParamikoTest): from paramiko.common import rng # just verify that we can pull out 32 bytes and not get an exception. x = rng.read(32) - self.assertEquals(len(x), 32) + self.assertEqual(len(x), 32) def test_7_host_config_expose_issue_33(self): test_config_file = """ @@ -172,16 +171,16 @@ Host *.example.com Host * Port 3333 """ - f = cStringIO.StringIO(test_config_file) + f = StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) host = 'www13.example.com' - self.assertEquals( + self.assertEqual( paramiko.util.lookup_ssh_host_config(host, config), {'hostname': host, 'port': '22'} ) def test_8_eintr_retry(self): - self.assertEquals('foo', paramiko.util.retry_on_signal(lambda: 'foo')) + self.assertEqual('foo', paramiko.util.retry_on_signal(lambda: 'foo')) # Variables that are set by raises_intr intr_errors_remaining = [3] @@ -192,8 +191,8 @@ Host * intr_errors_remaining[0] -= 1 raise IOError(errno.EINTR, 'file', 'interrupted system call') self.assertTrue(paramiko.util.retry_on_signal(raises_intr) is None) - self.assertEquals(0, intr_errors_remaining[0]) - self.assertEquals(4, call_count[0]) + self.assertEqual(0, intr_errors_remaining[0]) + self.assertEqual(4, call_count[0]) def raises_ioerror_not_eintr(): raise IOError(errno.ENOENT, 'file', 'file not found') @@ -216,10 +215,10 @@ Host space-delimited Host equals-delimited ProxyCommand=foo bar=biz baz """ - f = cStringIO.StringIO(conf) + f = StringIO(conf) config = paramiko.util.parse_ssh_config(f) for host in ('space-delimited', 'equals-delimited'): - self.assertEquals( + self.assertEqual( host_config(host, config)['proxycommand'], 'foo bar=biz baz' ) @@ -228,7 +227,7 @@ Host equals-delimited """ ProxyCommand should perform interpolation on the value """ - config = paramiko.util.parse_ssh_config(cStringIO.StringIO(""" + config = paramiko.util.parse_ssh_config(StringIO(""" Host specific Port 37 ProxyCommand host %h port %p lol @@ -245,7 +244,7 @@ Host * ('specific', "host specific port 37 lol"), ('portonly', "host portonly port 155"), ): - self.assertEquals( + self.assertEqual( host_config(host, config)['proxycommand'], val ) @@ -264,10 +263,10 @@ Host www13.* Host * Port 3333 """ - f = cStringIO.StringIO(test_config_file) + f = StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) host = 'www13.example.com' - self.assertEquals( + self.assertEqual( paramiko.util.lookup_ssh_host_config(host, config), {'hostname': host, 'port': '8080'} ) @@ -293,9 +292,9 @@ ProxyCommand foo=bar:%h-%p 'foo=bar:proxy-without-equal-divisor-22'} }.items(): - f = cStringIO.StringIO(test_config_file) + f = StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) - self.assertEquals( + self.assertEqual( paramiko.util.lookup_ssh_host_config(host, config), values ) @@ -323,9 +322,9 @@ IdentityFile id_dsa22 'identityfile': ['id_dsa0', 'id_dsa1', 'id_dsa22']} }.items(): - f = cStringIO.StringIO(test_config_file) + f = StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) - self.assertEquals( + self.assertEqual( paramiko.util.lookup_ssh_host_config(host, config), values ) @@ -338,5 +337,5 @@ IdentityFile id_dsa22 AddressFamily inet IdentityFile something_%l_using_fqdn """ - config = paramiko.util.parse_ssh_config(cStringIO.StringIO(test_config)) + config = paramiko.util.parse_ssh_config(StringIO(test_config)) assert config.lookup('meh') # will die during lookup() if bug regresses diff --git a/tests/util.py b/tests/util.py index 2e0be087..66d2696c 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,5 +1,8 @@ +import os import unittest +root_path = os.path.dirname(os.path.realpath(__file__)) + class ParamikoTest(unittest.TestCase): # for Python 2.3 and below @@ -8,3 +11,7 @@ class ParamikoTest(unittest.TestCase): if not hasattr(unittest.TestCase, 'assertFalse'): assertFalse = unittest.TestCase.failIf + +def test_path(filename): + return os.path.join(root_path, filename) + diff --git a/tox.ini b/tox.ini index e2a8dcf4..c63c93d3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27 +envlist = py25,py26,py27,py32,py33 [testenv] commands = pip install --use-mirrors -q -r dev-requirements.txt -- cgit v1.2.3 From 3e1f9f09b1da0397f82e4ee9e1886f5271705e29 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Tue, 11 Feb 2014 13:08:11 +0100 Subject: GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication with Python 3 support Add Python 3 support for the GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. This patch supersedes pull request #250. --- .gitignore | 1 + README | 18 ++ demos/demo_server.py | 37 ++- demos/demo_sftp.py | 19 +- demos/demo_simple.py | 19 +- paramiko/__init__.py | 1 + paramiko/auth_handler.py | 203 ++++++++++++- paramiko/client.py | 84 +++++- paramiko/common.py | 13 +- paramiko/kex_group1.py | 29 +- paramiko/kex_group14.py | 33 +++ paramiko/kex_gss.py | 649 +++++++++++++++++++++++++++++++++++++++++ paramiko/server.py | 80 +++++ paramiko/ssh_gss.py | 619 +++++++++++++++++++++++++++++++++++++++ paramiko/transport.py | 116 +++++++- sites/_shared_static/logo.png | Bin 0 -> 6401 bytes sites/shared_conf.py | 41 +++ sites/www/_templates/rss.xml | 19 ++ sites/www/blog.py | 140 +++++++++ sites/www/blog.rst | 16 + sites/www/blog/first-post.rst | 7 + sites/www/blog/second-post.rst | 7 + sites/www/changelog.rst | 114 ++++++++ sites/www/conf.py | 35 +++ sites/www/contact.rst | 11 + sites/www/contributing.rst | 19 ++ sites/www/index.rst | 38 +++ sites/www/installing.rst | 105 +++++++ test.py | 46 +++ tests/test_gssapi.py | 167 +++++++++++ tests/test_kex_gss.py | 143 +++++++++ tests/test_ssh_gss.py | 136 +++++++++ 32 files changed, 2915 insertions(+), 50 deletions(-) mode change 100755 => 100644 demos/demo_sftp.py create mode 100644 paramiko/kex_group14.py create mode 100644 paramiko/kex_gss.py create mode 100644 paramiko/ssh_gss.py create mode 100644 sites/_shared_static/logo.png create mode 100644 sites/shared_conf.py create mode 100644 sites/www/_templates/rss.xml create mode 100644 sites/www/blog.py create mode 100644 sites/www/blog.rst create mode 100644 sites/www/blog/first-post.rst create mode 100644 sites/www/blog/second-post.rst create mode 100644 sites/www/changelog.rst create mode 100644 sites/www/conf.py create mode 100644 sites/www/contact.rst create mode 100644 sites/www/contributing.rst create mode 100644 sites/www/index.rst create mode 100644 sites/www/installing.rst create mode 100644 tests/test_gssapi.py create mode 100644 tests/test_kex_gss.py create mode 100644 tests/test_ssh_gss.py diff --git a/.gitignore b/.gitignore index 4b578950..bdf72de4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ paramiko.egg-info/ test.log docs/ +demos/*.log diff --git a/README b/README index 666b31a7..2712e9ff 100644 --- a/README +++ b/README @@ -71,6 +71,24 @@ Bugs & Support Please file bug reports at https://github.com/paramiko/paramiko/. There is currently no mailing list but we plan to create a new one ASAP. +Kerberos Support +---------------- + +If you want paramiko to do kerberos authentication or key exchange using GSS-API or SSPI, you +need the following python packages: + +- pyasn1 0.1.7 or better +- python-gssapi 0.4.0 or better (Unix) +- pywin32 2.1.8 or better (Windows) + +So you have to install pyasn1 and python-gssapi on Unix or pywin32 on Windows. +To enable GSS-API / SSPI authentication or key exchange see the demos or paramiko docs. +Note: If you use Microsoft SSPI for kerberos authentication and credential +delegation in paramiko, make sure that the target host is trusted for +delegation in the active directory configuration. For details see: +http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx + + Demo ---- diff --git a/demos/demo_server.py b/demos/demo_server.py index bb35258b..74e4677e 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -66,9 +66,39 @@ class Server (paramiko.ServerInterface): if (username == 'robey') and (key == self.good_pub_key): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + """ + @note: We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a certain + platform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + @see: U{krb5_kuserok() man page } + """ + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + GSSAPICleanupCredentials = False + return UseGSSAPI def get_allowed_auths(self, username): - return 'password,publickey' + return 'gssapi-keyex,gssapi-with-mic,password,publickey' def check_channel_shell_request(self, channel): self.event.set() @@ -79,6 +109,8 @@ class Server (paramiko.ServerInterface): return True +DoGSSAPIKeyExchange = True + # now connect try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -101,7 +133,8 @@ except Exception as e: print('Got a connection!') try: - t = paramiko.Transport(client) + t = paramiko.Transport(client, gss_kex=DoGSSAPIKeyExchange) + t.set_gss_host(socket.getfqdn("")) try: t.load_server_moduli() except: diff --git a/demos/demo_sftp.py b/demos/demo_sftp.py old mode 100755 new mode 100644 index a34f2b19..2cb44701 --- a/demos/demo_sftp.py +++ b/demos/demo_sftp.py @@ -34,6 +34,11 @@ from paramiko.py3compat import input # setup logging paramiko.util.log_to_file('demo_sftp.log') +# Paramiko client configuration +UseGSSAPI = True # enable GSS-API / SSPI authentication +DoGSSAPIKeyExchange = True +Port = 22 + # get hostname username = '' if len(sys.argv) > 1: @@ -45,10 +50,10 @@ else: if len(hostname) == 0: print('*** Hostname required.') sys.exit(1) -port = 22 + if hostname.find(':') >= 0: hostname, portstr = hostname.split(':') - port = int(portstr) + Port = int(portstr) # get username @@ -57,7 +62,10 @@ if username == '': username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username -password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +if not UseGSSAPI: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +else: + password = None # get host key, if we know one @@ -81,8 +89,9 @@ if hostname in host_keys: # now, connect and use paramiko Transport to negotiate SSH2 across the connection try: - t = paramiko.Transport((hostname, port)) - t.connect(username=username, password=password, hostkey=hostkey) + t = paramiko.Transport((hostname, Port)) + t.connect(hostkey, username, password, gss_host=socket.getfqdn(hostname), + gss_auth=UseGSSAPI, gss_kex=DoGSSAPIKeyExchange) sftp = paramiko.SFTPClient.from_transport(t) # dirlist on remote host diff --git a/demos/demo_simple.py b/demos/demo_simple.py index ae631e43..100e15f5 100755 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -36,6 +36,10 @@ except ImportError: # setup logging paramiko.util.log_to_file('demo_simple.log') +# Paramiko client configuration +UseGSSAPI = True # enable GSS-API / SSPI authentication +DoGSSAPIKeyExchange = True +Port = 22 # get hostname username = '' @@ -48,10 +52,10 @@ else: if len(hostname) == 0: print('*** Hostname required.') sys.exit(1) -port = 22 + if hostname.find(':') >= 0: hostname, portstr = hostname.split(':') - port = int(portstr) + Port = int(portstr) # get username @@ -60,7 +64,8 @@ if username == '': username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username -password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +if not UseGSSAPI: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) # now, connect and use paramiko Client to negotiate SSH2 across the connection @@ -69,7 +74,13 @@ try: client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) print('*** Connecting...') - client.connect(hostname, port, username, password) + if not UseGSSAPI: + client.connect(hostname, Port, username, password) + else: + # SSPI works only with the FQDN of the target host + hostname = socket.getfqdn(hostname) + client.connect(hostname, Port, username, gss_auth=UseGSSAPI, + gss_kex=DoGSSAPIKeyExchange) chan = client.invoke_shell() print(repr(client.get_transport())) print('*** Here we go!\n') diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 8a814061..51329271 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -65,6 +65,7 @@ __license__ = "GNU Lesser General Public License (LGPL)" from paramiko.transport import SecurityOptions, Transport from paramiko.client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy from paramiko.auth_handler import AuthHandler +from paramiko.ssh_gss import GSSAuth from paramiko.channel import Channel, ChannelFile from paramiko.ssh_exception import SSHException, PasswordRequiredException, \ BadAuthenticationType, ChannelException, BadHostKeyException, \ diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 5046cac5..1910a7f1 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -32,13 +32,14 @@ from paramiko.message import Message from paramiko.ssh_exception import SSHException, AuthenticationException, \ BadAuthenticationType, PartialAuthentication from paramiko.server import InteractiveQuery +from paramiko.ssh_gss import GSSAuth class AuthHandler (object): """ Internal class to handle the mechanics of authentication. """ - + def __init__(self, transport): self.transport = weakref.proxy(transport) self.username = None @@ -52,7 +53,10 @@ class AuthHandler (object): # for server mode: self.auth_username = None self.auth_fail_count = 0 - + # for GSSAPI + self.gss_host = None + self.gss_deleg_creds = True + def is_authenticated(self): return self.authenticated @@ -109,6 +113,28 @@ class AuthHandler (object): finally: self.transport.lock.release() + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'gssapi-with-mic' + self.username = username + self.gss_host = gss_host + self.gss_deleg_creds = gss_deleg_creds + self._request_auth() + finally: + self.transport.lock.release() + + def auth_gssapi_keyex(self, username, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'gssapi-keyex' + self.username = username + self._request_auth() + finally: + self.transport.lock.release() + def abort(self): if self.auth_event is not None: self.auth_event.set() @@ -209,6 +235,76 @@ class AuthHandler (object): elif self.auth_method == 'keyboard-interactive': m.add_string('') m.add_string(self.submethods) + elif self.auth_method == "gssapi-with-mic": + sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds) + m.add_bytes(sshgss.ssh_gss_oids()) + # send the supported GSSAPI OIDs to the server + self.transport._send_message(m) + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_RESPONSE: + """ + Read the mechanism selected by the server. + We send just the Kerberos V5 OID, so the server can only + respond with this OID. + """ + mech = m.get_string() + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(sshgss.ssh_init_sec_context(self.gss_host, + mech, + self.username,)) + self.transport._send_message(m) + while True: + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_TOKEN: + srv_token = m.get_string() + next_token = sshgss.ssh_init_sec_context(self.gss_host, + mech, + self.username, + srv_token) + """ + After this step the GSSAPI should not return any + token. If it does, we keep sending the token to the + server until no more token is returned. + """ + if next_token is None: + break + else: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(next_token) + self.transport.send_message(m) + else: + raise SSHException("Received Package: %s" % MSG_NAMES[ptype]) + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_MIC) + # send the MIC to the server + m.add_string(sshgss.ssh_get_mic(self.transport.session_id)) + elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK: + """ + RFC 4462 says we are not required to implement GSS-API error + messages. + @see: U{RFC 4462 Section 3.8} + """ + raise SSHException("Server returned an error token") + elif ptype == MSG_USERAUTH_GSSAPI_ERROR: + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + lang_tag = m.get_string() # we don't care! + raise SSHException("GSS-API Error:\nMajor Status: %s\n\ + Minor Status: %s\ \nError Message:\ + %s\n") % (str(maj_status), + str(min_status), + err_msg) + else: + raise SSHException("Received Package: %s" % MSG_NAMES[ptype]) + elif self.auth_method == 'gssapi-keyex' and\ + self.transport.gss_kex_used: + kexgss = self.transport.kexgss_ctxt + kexgss.set_username(self.username) + mic_token = kexgss.ssh_get_mic(self.transport.session_id) + m.add_string(mic_token) elif self.auth_method == 'none': pass else: @@ -276,6 +372,8 @@ class AuthHandler (object): self._disconnect_no_more_auth() return self.auth_username = username + # check if GSS-API authentication is enabled + gss_auth = self.transport.server_object.enable_auth_gssapi() if method == 'none': result = self.transport.server_object.check_auth_none(username) @@ -341,6 +439,107 @@ class AuthHandler (object): # make interactive query instead of response self._interactive_query(result) return + elif method == "gssapi-with-mic" and gss_auth: + sshgss = GSSAuth(method) + """ + OpenSSH sends just one OID. It's the Kerveros V5 OID and that's + the only OID we support. + """ + # read the number of OID mechanisms supported by the client + mechs = m.get_int() + """ + We can't accept more than one OID, so if the SSH client send more + than one disconnect + """ + if mechs > 1: + self.transport._log(INFO, + 'Disconnect: Received more than one GSS-API OID mechanism') + self._disconnect_no_more_auth() + desired_mech = m.get_string() + mech_ok = sshgss.ssh_check_mech(desired_mech) + # if we don't support the mechanism, disconnect. + if not mech_ok: + self.transport._log(INFO, + 'Disconnect: Received an invalid GSS-API OID mechanism') + self._disconnect_no_more_auth() + # send the Kerberos V5 GSSAPI OID to the client + supported_mech = sshgss.ssh_gss_oids("server") + """ + RFC 4462 says we are not required to implement GSS-API error + messages. + @see: U{RFC 4462 Section 3.8 } + """ + while True: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE) + m.add_bytes(supported_mech) + self.transport._send_message(m) + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_TOKEN: + client_token = m.get_string() + # use the client token as input to establish a secure + # context. + try: + token = sshgss.ssh_accept_sec_context(self.gss_host, + client_token, + username) + except Exception: + result = AUTH_FAILED + self._send_auth_result(username, method, result) + raise + if token is not None: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(token) + self.transport._send_message(m) + else: + raise SSHException("Client asked to handle paket %s" + %MSG_NAMES[ptype]) + # check MIC + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_MIC: + break + mic_token = m.get_string() + try: + retval = sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + username) + except Exception: + result = AUTH_FAILED + self._send_auth_result(username, method, result) + raise + if retval == 0: + """ + @todo: Implement client credential saving + The OpenSSH server is able to create a TGT with the delegated + client credentials, but this is not supported by GSS-API. + """ + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_with_mic(username, + result) + else: + result = AUTH_FAILED + elif method == "gssapi-keyex" and gss_auth: + mic_token = m.get_string() + sshgss = self.transport.kexgss_ctxt + if sshgss is None: + # If there is no valid context, we reject the authentication + result = AUTH_FAILED + self._send_auth_result(username, method, result) + try: + retval = sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + self.auth_username) + except Exception: + result = AUTH_FAILED + self._send_auth_result(username, method, result) + raise + if retval == 0: + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_keyex(username, + result) + else: + result = AUTH_FAILED else: result = self.transport.server_object.check_auth_none(username) # okay, send result diff --git a/paramiko/client.py b/paramiko/client.py index 40ef7f0b..b786d17b 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -232,7 +232,8 @@ class SSHClient (object): 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, - compress=False, sock=None): + compress=False, sock=None, gss_auth=False, gss_kex=False, + gss_deleg_creds=True, gss_host=None): """ Connect to an SSH server and authenticate to it. The server's host key is checked against the system host keys (see L{load_system_host_keys}) @@ -278,6 +279,14 @@ class SSHClient (object): @param sock: an open socket or socket-like object (such as a L{Channel}) to use for communication to the target host @type sock: socket + @param gss_auth: C{True} if you want to use GSS-API authentication + @type gss_auth: Boolean + @param gss_kex: Perform GSS-API Key Exchange and user authentication + @type gss_kex: Boolean + @param gss_deleg_creds: Delegate GSS-API client credentials or not + @type gss_deleg_creds: Boolean + @param gss_host: The targets name in the kerberos database. default: hostname + @type gss_host: String @raise BadHostKeyException: if the server's host key could not be verified @@ -303,8 +312,14 @@ class SSHClient (object): pass retry_on_signal(lambda: sock.connect(addr)) - t = self._transport = Transport(sock) + t = self._transport = Transport(sock, gss_kex, gss_deleg_creds) t.use_compression(compress=compress) + if gss_kex and gss_host is None: + t.set_gss_host(hostname) + elif gss_kex and gss_host is not None: + t.set_gss_host(gss_host) + else: + pass if self._log_channel is not None: t.set_log_channel(self._log_channel) t.start_client() @@ -317,17 +332,27 @@ class SSHClient (object): 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(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, 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 GSS-API Key Exchange is performed we are not required to check the + host key, because the host is authenticated via GSS-API / SSPI as well + as out client. + """ + if not self._transport.use_gss_kex: + 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(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, 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() @@ -338,7 +363,10 @@ class SSHClient (object): key_filenames = [ key_filename ] else: key_filenames = key_filename - self._auth(username, password, pkey, key_filenames, allow_agent, look_for_keys) + if gss_host is None: + gss_host = hostname + self._auth(username, password, pkey, key_filenames, allow_agent, + look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host) def close(self): """ @@ -428,7 +456,8 @@ class SSHClient (object): """ return self._transport - def _auth(self, username, password, pkey, key_filenames, allow_agent, look_for_keys): + def _auth(self, username, password, pkey, key_filenames, allow_agent, + look_for_keys, gss_auth, gss_kex, gss_deleg_creds, gss_host): """ Try, in order: @@ -525,6 +554,31 @@ class SSHClient (object): elif two_factor: raise SSHException('Two-factor authentication requires a password') + """ + Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key + Exchange is not performed, because if we use GSS-API for the key + exchange, there is already a fully established GSS-API context, so + why should we do that again? + """ + if gss_auth and not self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_with_mic(username, gss_host, + gss_deleg_creds) + return + except SSHException as e: + saved_exception = e + + """ + If GSS-API support and GSS-PI Key Exchange was performed, we attempt + authentication with gssapi-keyex. + """ + if gss_auth and gss_kex and self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_keyex(username) + return + except SSHException as e: + saved_exception = e + # if we got an auth-failed exception earlier, re-raise it if saved_exception is not None: raise saved_exception diff --git a/paramiko/common.py b/paramiko/common.py index e30df73a..de7e38d2 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -28,6 +28,9 @@ MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ MSG_USERAUTH_BANNER = range(50, 54) MSG_USERAUTH_PK_OK = 60 MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) +MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62) +MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\ +MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67) MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ @@ -54,6 +57,8 @@ MSG_NAMES = { 32: 'kex32', 33: 'kex33', 34: 'kex34', + 40: 'kex40', + 41: 'kex41', MSG_USERAUTH_REQUEST: 'userauth-request', MSG_USERAUTH_FAILURE: 'userauth-failure', MSG_USERAUTH_SUCCESS: 'userauth-success', @@ -73,7 +78,13 @@ MSG_NAMES = { MSG_CHANNEL_CLOSE: 'channel-close', MSG_CHANNEL_REQUEST: 'channel-request', MSG_CHANNEL_SUCCESS: 'channel-success', - MSG_CHANNEL_FAILURE: 'channel-failure' + MSG_CHANNEL_FAILURE: 'channel-failure', + MSG_USERAUTH_GSSAPI_RESPONSE: 'userauth-gssapi-response', + MSG_USERAUTH_GSSAPI_TOKEN: 'userauth-gssapi-token', + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE: 'userauth-gssapi-exchange-complete', + MSG_USERAUTH_GSSAPI_ERROR: 'userauth-gssapi-error', + MSG_USERAUTH_GSSAPI_ERRTOK: 'userauth-gssapi-error-token', + MSG_USERAUTH_GSSAPI_MIC: 'userauth-gssapi-mic' } diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 05693a1f..25eb2566 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -32,15 +32,16 @@ from paramiko.ssh_exception import SSHException _MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)] -# draft-ietf-secsh-transport-09.txt, page 17 -P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF -G = 2 - -b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 -b0000000000000000 = zero_byte * 8 class KexGroup1(object): + # draft-ietf-secsh-transport-09.txt, page 17 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF + G = 2 + + b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 + b0000000000000000 = zero_byte * 8 + name = 'diffie-hellman-group1-sha1' def __init__(self, transport): @@ -53,11 +54,11 @@ class KexGroup1(object): self._generate_x() if self.transport.server_mode: # compute f = g^x mod p, but don't send it yet - self.f = pow(G, self.x, P) + self.f = pow(self.G, self.x, self.P) self.transport._expect_packet(_MSG_KEXDH_INIT) return # compute e = g^x mod p (where g=2), and send it - self.e = pow(G, self.x, P) + self.e = pow(self.G, self.x, self.P) m = Message() m.add_byte(c_MSG_KEXDH_INIT) m.add_mpint(self.e) @@ -84,8 +85,8 @@ class KexGroup1(object): while 1: x_bytes = self.transport.rng.read(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] - if (x_bytes[:8] != b7fffffffffffffff) and \ - (x_bytes[:8] != b0000000000000000): + if (x_bytes[:8] != self.b7fffffffffffffff) and \ + (x_bytes[:8] != self.b0000000000000000): break self.x = util.inflate_long(x_bytes) @@ -93,10 +94,10 @@ class KexGroup1(object): # client mode host_key = m.get_string() self.f = m.get_mpint() - if (self.f < 1) or (self.f > P - 1): + if (self.f < 1) or (self.f > self.P - 1): raise SSHException('Server kex "f" is out of range') sig = m.get_binary() - K = pow(self.f, self.x, P) + K = pow(self.f, self.x, self.P) # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() hm.add(self.transport.local_version, self.transport.remote_version, @@ -112,9 +113,9 @@ class KexGroup1(object): def _parse_kexdh_init(self, m): # server mode self.e = m.get_mpint() - if (self.e < 1) or (self.e > P - 1): + if (self.e < 1) or (self.e > self.P - 1): raise SSHException('Client kex "e" is out of range') - K = pow(self.e, self.x, P) + K = pow(self.e, self.x, self.P) key = self.transport.get_server_key().asbytes() # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) hm = Message() diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py new file mode 100644 index 00000000..a914aeaf --- /dev/null +++ b/paramiko/kex_group14.py @@ -0,0 +1,33 @@ +# Copyright (C) 2013 Torsten Landschoff +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of +2048 bit key halves, using a known "p" prime and "g" generator. +""" + +from paramiko.kex_group1 import KexGroup1 + + +class KexGroup14(KexGroup1): + + # http://tools.ietf.org/html/rfc3526#section-3 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF + G = 2 + + name = 'diffie-hellman-group14-sha1' diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py new file mode 100644 index 00000000..1fb7aea6 --- /dev/null +++ b/paramiko/kex_gss.py @@ -0,0 +1,649 @@ +# Copyright (C) 2003-2007 Robey Pointer +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + + +""" +This module provides GSS-API / SSPI Key Exchange for Paramiko as defined in +RFC 4462 with the following restrictions: +Credential delegation is not supported in server mode, +To Use this module, you need the following additional python packages: +U{pyasn1 >= 0.1.7 }, +U{python-gssapi >= 0.4.0 (Unix) }, +U{pywin32 2.1.8 (Windows) }. + +@summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module +@version: 0.1 +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) +@see: L{ssh_gss} + +Created on 12.12.2013 +""" + + +from Crypto.Hash import SHA +from paramiko.common import * +from paramiko import util +from paramiko.message import Message +from paramiko.ssh_exception import SSHException + + +MSG_KEXGSS_INIT, MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_HOSTKEY,\ +MSG_KEXGSS_ERROR = range(30, 35) +MSG_KEXGSS_GROUPREQ, MSG_KEXGSS_GROUP = range(40, 42) +c_MSG_KEXGSS_INIT, c_MSG_KEXGSS_CONTINUE, c_MSG_KEXGSS_COMPLETE,\ +c_MSG_KEXGSS_HOSTKEY, c_MSG_KEXGSS_ERROR = [byte_chr(c) for c in range(30, 35)] +c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)] + + +class KexGSSGroup1(object): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange + as defined in U{RFC 4462 Section 2 } + + @note: RFC 4462 says we are not required to implement GSS-API error + messages. + If an error occurs an exception will be thrown and the connection + will be terminated. + @see: U{RFC 4462 Section 2.2 } + """ + # draft-ietf-secsh-transport-09.txt, page 17 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF + G = 2 + b7fffffffffffffff = byte_chr(0x7f) + max_byte * 7 + b0000000000000000 = zero_byte * 8 + NAME = "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + def __init__(self, transport): + self.transport = transport + self.kexgss = self.transport.kexgss_ctxt + self.gss_host = None + self.x = 0 + self.e = 0 + self.f = 0 + + def start_kex(self): + """ + Start the GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange. + """ + self.transport.gss_kex_used = True + self._generate_x() + if self.transport.server_mode: + # compute f = g^x mod p, but don't send it yet + self.f = pow(self.G, self.x, self.P) + self.transport._expect_packet(MSG_KEXGSS_INIT) + return + # compute e = g^x mod p (where g=2), and send it + self.e = pow(self.G, self.x, self.P) + # Initialize GSS-API Key Exchange + self.gss_host = self.transport.gss_host + m = Message() + m.add_byte(c_MSG_KEXGSS_INIT) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def parse_next(self, ptype, m): + """ + Parse the next packet. + + @param ptype: The type of the incomming packet + @type ptype: Char + @param m: The paket content + @type m: L{Message} + """ + if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT): + return self._parse_kexgss_init(m) + elif not self.transport.server_mode and (ptype == MSG_KEXGSS_HOSTKEY): + return self._parse_kexgss_hostkey(m) + elif self.transport.server_mode and (ptype == MSG_KEXGSS_CONTINUE): + return self._parse_kexgss_continue(m) + elif not self.transport.server_mode and (ptype == MSG_KEXGSS_COMPLETE): + return self._parse_kexgss_complete(m) + elif ptype == MSG_KEXGSS_ERROR: + return self._parse_kexgss_error(m) + raise SSHException('GSS KexGroup1 asked to handle packet type %d' + % ptype) + + # ## internals... + + def _generate_x(self): + """ + generate an "x" (1 < x < q), where q is (p-1)/2. + p is a 128-byte (1024-bit) number, where the first 64 bits are 1. + therefore q can be approximated as a 2^1023. we drop the subset of + potential x where the first 63 bits are 1, because some of those will be + larger than q (but this is a tiny tiny subset of potential x). + """ + while 1: + x_bytes = self.transport.rng.read(128) + x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] + if (x_bytes[:8] != self.b7fffffffffffffff) and \ + (x_bytes[:8] != self.b0000000000000000): + break + self.x = util.inflate_long(x_bytes) + + def _parse_kexgss_hostkey(self, m): + """ + Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + @type m: L{Message} + """ + # client mode + host_key = m.get_string() + self.transport.host_key = host_key + sig = m.get_string() + self.transport._verify_key(host_key, sig) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE) + + def _parse_kexgss_continue(self, m): + """ + Parse the SSH2_MSG_KEXGSS_CONTINUE message. + + @param m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + @type m: L{Message} + """ + if not self.transport.server_mode: + srv_token = m.get_string() + m = Message() + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host, + recv_token=srv_token)) + self.transport.send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + else: + pass + + def _parse_kexgss_complete(self, m): + """ + Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + @type m: L{Message} + """ + # client mode + if self.transport.host_key is None: + self.transport.host_key = NullHostKey() + self.f = m.get_mpint() + if (self.f < 1) or (self.f > self.P - 1): + raise SSHException('Server kex "f" is out of range') + mic_token = m.get_string() + """ + This must be TRUE, if there is a GSS-API token in this + message. + """ + bool = m.get_boolean() + srv_token = None + if bool: + srv_token = m.get_string() + K = pow(self.f, self.x, self.P) + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add(self.transport.local_version, self.transport.remote_version, + self.transport.local_kex_init, self.transport.remote_kex_init) + hm.add_string(self.transport.host_key.__str__()) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + self.transport._set_K_H(K, SHA.new(str(hm)).digest()) + if srv_token is not None: + self.kexgss.ssh_init_sec_context(target=self.gss_host, + recv_token=srv_token) + self.kexgss.ssh_check_mic(mic_token, + self.transport.session_id) + else: + self.kexgss.ssh_check_mic(mic_token, + self.transport.session_id) + self.transport._activate_outbound() + + def _parse_kexgss_init(self, m): + """ + Parse the SSH2_MSG_KEXGSS_INIT message (server mode). + + @param m: The content of the SSH2_MSG_KEXGSS_INIT message + @type m: L{Message} + """ + # server mode + client_token = m.get_string() + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.P - 1): + raise SSHException('Client kex "e" is out of range') + K = pow(self.e, self.x, self.P) + self.transport.host_key = NullHostKey() + key = self.transport.host_key.__str__() + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add(self.transport.remote_version, self.transport.local_version, + self.transport.remote_kex_init, self.transport.local_kex_init) + hm.add_string(key) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = SHA.new(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host, + client_token) + m = Message() + if self.kexgss._gss_srv_ctxt_status: + mic_token = self.kexgss.ssh_get_mic(self.transport.session_id, + gss_kex=True) + m.add_byte(c_MSG_KEXGSS_COMPLETE) + m.add_mpint(self.f) + m.add_string(mic_token) + if srv_token is not None: + m.add_boolean(True) + m.add_string(srv_token) + else: + m.add_boolean(False) + self.transport._send_message(m) + self.transport._activate_outbound() + else: + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(srv_token) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def _parse_kexgss_error(self, m): + """ + Parse the SSH2_MSG_KEXGSS_ERROR message (client mode). + The server may send a GSS-API error message. if it does, we display + the error by throwing an exception (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_ERROR message + @type m: L{Message} + @raise SSHException: Contains GSS-API major and minor status as well as + the error message and the language tag of the + message + """ + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + lang_tag = m.get_string() # we don't care about the language! + raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\ + \nError Message: %s\n") % (str(maj_status), + str(min_status), + err_msg) + + +class KexGSSGroup14(KexGSSGroup1): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange + as defined in U{RFC 4462 Section 2 } + + @note: RFC 4462 says we are not required to implement GSS-API error + messages. + If an error occurs an exception will be thrown and the connection + will be terminated. + @see: U{RFC 4462 Section 2.2 } + """ + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF + G = 2 + NAME = "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + +class KexGSSGex(object): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange + as defined in U{RFC 4462 Section 2 } + + @note: RFC 4462 says we are not required to implement GSS-API error + messages. + If an error occurs an exception will be thrown and the connection + will be terminated. + @see: U{RFC 4462 Section 2.2 } + """ + NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" + min_bits = 1024 + max_bits = 8192 + preferred_bits = 2048 + + def __init__(self, transport): + self.transport = transport + self.kexgss = self.transport.kexgss_ctxt + self.gss_host = None + self.p = None + self.q = None + self.g = None + self.x = None + self.e = None + self.f = None + self.old_style = False + + def start_kex(self): + """ + Start the GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange + """ + self.transport.gss_kex_used = True + if self.transport.server_mode: + self.transport._expect_packet(MSG_KEXGSS_GROUPREQ) + return + # request a bit range: we accept (min_bits) to (max_bits), but prefer + # (preferred_bits). according to the spec, we shouldn't pull the + # minimum up above 1024. + self.gss_host = self.transport.gss_host + m = Message() + m.add_byte(c_MSG_KEXGSS_GROUPREQ) + m.add_int(self.min_bits) + m.add_int(self.preferred_bits) + m.add_int(self.max_bits) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_GROUP) + + def parse_next(self, ptype, m): + """ + Parse the next packet. + + @param ptype: The type of the incomming packet + @type ptype: Char + @param m: The paket content + @type m: L{Message} + """ + if ptype == MSG_KEXGSS_GROUPREQ: + return self._parse_kexgss_groupreq(m) + elif ptype == MSG_KEXGSS_GROUP: + return self._parse_kexgss_group(m) + elif ptype == MSG_KEXGSS_INIT: + return self._parse_kexgss_gex_init(m) + elif ptype == MSG_KEXGSS_HOSTKEY: + return self._parse_kexgss_hostkey(m) + elif ptype == MSG_KEXGSS_CONTINUE: + return self._parse_kexgss_continue(m) + elif ptype == MSG_KEXGSS_COMPLETE: + return self._parse_kexgss_complete(m) + elif ptype == MSG_KEXGSS_ERROR: + return self._parse_kexgss_error(m) + raise SSHException('KexGex asked to handle packet type %d' % ptype) + + # ## internals... + + def _generate_x(self): + # generate an "x" (1 < x < (p-1)/2). + q = (self.p - 1) // 2 + qnorm = util.deflate_long(q, 0) + qhbyte = byte_ord(qnorm[0]) + byte_count = len(qnorm) + qmask = 0xff + while not (qhbyte & 0x80): + qhbyte <<= 1 + qmask >>= 1 + while True: + x_bytes = self.transport.rng.read(byte_count) + x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] + x = util.inflate_long(x_bytes, 1) + if (x > 1) and (x < q): + break + self.x = x + + def _parse_kexgss_groupreq(self, m): + """ + Parse the SSH2_MSG_KEXGSS_GROUPREQ message (server mode). + + @param m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message + @type m: L{Message} + """ + minbits = m.get_int() + preferredbits = m.get_int() + maxbits = m.get_int() + # smoosh the user's preferred size into our own limits + if preferredbits > self.max_bits: + preferredbits = self.max_bits + if preferredbits < self.min_bits: + preferredbits = self.min_bits + # fix min/max if they're inconsistent. technically, we could just pout + # and hang up, but there's no harm in giving them the benefit of the + # doubt and just picking a bitsize for them. + if minbits > preferredbits: + minbits = preferredbits + if maxbits < preferredbits: + maxbits = preferredbits + # now save a copy + self.min_bits = minbits + self.preferred_bits = preferredbits + self.max_bits = maxbits + # generate prime + pack = self.transport._get_modulus_pack() + if pack is None: + raise SSHException('Can\'t do server-side gex with no modulus pack') + self.transport._log(DEBUG, 'Picking p (%d <= %d <= %d bits)' % (minbits, preferredbits, maxbits)) + self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) + m = Message() + m.add_byte(c_MSG_KEXGSS_GROUP) + m.add_mpint(self.p) + m.add_mpint(self.g) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_INIT) + + def _parse_kexgss_group(self, m): + """ + Parse the SSH2_MSG_KEXGSS_GROUP message (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_GROUP message + @type m: L{Message} + """ + self.p = m.get_mpint() + self.g = m.get_mpint() + # reject if p's bit length < 1024 or > 8192 + bitlen = util.bit_length(self.p) + if (bitlen < 1024) or (bitlen > 8192): + raise SSHException('Server-generated gex p (don\'t ask) is out of range (%d bits)' % bitlen) + self.transport._log(DEBUG, 'Got server p (%d bits)' % bitlen) + self._generate_x() + # now compute e = g^x mod p + self.e = pow(self.g, self.x, self.p) + m = Message() + m.add_byte(c_MSG_KEXGSS_INIT) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def _parse_kexgss_gex_init(self, m): + """ + Parse the SSH2_MSG_KEXGSS_INIT message (server mode). + + @param m: The content of the SSH2_MSG_KEXGSS_INIT message + @type m: L{Message} + """ + client_token = m.get_string() + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.p - 1): + raise SSHException('Client kex "e" is out of range') + self._generate_x() + self.f = pow(self.g, self.x, self.p) + K = pow(self.e, self.x, self.p) + self.transport.host_key = NullHostKey() + key = self.transport.host_key.__str__() + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) + hm = Message() + hm.add(self.transport.remote_version, self.transport.local_version, + self.transport.remote_kex_init, self.transport.local_kex_init, + key) + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = SHA.new(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host, + client_token) + m = Message() + if self.kexgss._gss_srv_ctxt_status: + mic_token = self.kexgss.ssh_get_mic(self.transport.session_id, + gss_kex=True) + m.add_byte(c_MSG_KEXGSS_COMPLETE) + m.add_mpint(self.f) + m.add_string(mic_token) + if srv_token is not None: + m.add_boolean(True) + m.add_string(srv_token) + else: + m.add_boolean(False) + self.transport._send_message(m) + self.transport._activate_outbound() + else: + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(srv_token) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + + def _parse_kexgss_hostkey(self, m): + """ + Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + @type m: L{Message} + """ + # client mode + host_key = m.get_string() + self.transport.host_key = host_key + sig = m.get_string() + self.transport._verify_key(host_key, sig) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE) + + def _parse_kexgss_continue(self, m): + """ + Parse the SSH2_MSG_KEXGSS_CONTINUE message. + + @param m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + @type m: L{Message} + """ + if not self.transport.server_mode: + srv_token = m.get_string() + m = Message() + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host, + recv_token=srv_token)) + self.transport.send_message(m) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR) + else: + pass + + def _parse_kexgss_complete(self, m): + """ + Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + @type m: L{Message} + """ + if self.transport.host_key is None: + self.transport.host_key = NullHostKey() + self.f = m.get_mpint() + mic_token = m.get_string() + """ + This must be TRUE, if there is a GSS-API token in this + message. + """ + bool = m.get_boolean() + srv_token = None + if bool: + srv_token = m.get_string() + if (self.f < 1) or (self.f > self.p - 1): + raise SSHException('Server kex "f" is out of range') + K = pow(self.f, self.x, self.p) + # okay, build up the hash H of (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) + hm = Message() + hm.add(self.transport.local_version, self.transport.remote_version, + self.transport.local_kex_init, self.transport.remote_kex_init, + self.transport.host_key.__str__()) + if not self.old_style: + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + if not self.old_style: + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = SHA.new(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + if srv_token is not None: + self.kexgss.ssh_init_sec_context(target=self.gss_host, + recv_token=srv_token) + self.kexgss.ssh_check_mic(mic_token, + self.transport.session_id) + else: + self.kexgss.ssh_check_mic(mic_token, + self.transport.session_id) + self.transport._activate_outbound() + + def _parse_kexgss_error(self, m): + """ + Parse the SSH2_MSG_KEXGSS_ERROR message (client mode). + The server may send a GSS-API error message. if it does, we display + the error by throwing an exception (client mode). + + @param m: The content of the SSH2_MSG_KEXGSS_ERROR message + @type m: L{Message} + @raise SSHException: Contains GSS-API major and minor status as well as + the error message and the language tag of the + message + """ + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + lang_tag = m.get_string() # we don't care about the language! + raise SSHException("GSS-API Error:\nMajor Status: %s\nMinor Status: %s\ + \nError Message: %s\n") % (str(maj_status), + str(min_status), + err_msg) + + +class NullHostKey(object): + """ + This class represents the Null Host Key for GSS-API Key Exchange + as defined in U{RFC 4462 Section 5 } + """ + def __init__(self): + self.key = "" + + def __str__(self): + return self.key + + def get_name(self): + return self.key diff --git a/paramiko/server.py b/paramiko/server.py index a922201d..38decf5b 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -276,6 +276,86 @@ class ServerInterface (object): @rtype: int or L{InteractiveQuery} """ return AUTH_FAILED + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=AUTH_FAILED, + cc_file=None): + """ + Authenticate the given user to the server if he is a valid krb5 + principal. + + @param username: The username of the authenticating client + @type username: String + @param gss_authenticated: The result of the krb5 authentication + @type gss_authenticated: Integer + @param cc_filename: The krb5 client credentials cache filename + @type cc_filename: String + @return: L{AUTH_FAILED} if the user is not authenticated otherwise + L{AUTH_SUCCESSFUL} + @rtype: Integer + @note: Kerberos credential delegation is not supported. + @see: L{ssh_gss} + @note: We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + plattform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + @see: U{http://www.unix.com/man-page/all/3/krb5_kuserok/} + """ + if gss_authenticated == AUTH_SUCCESSFUL: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=AUTH_FAILED, + cc_file=None): + """ + Authenticate the given user to the server if he is a valid krb5 + principal and GSS-API Key Exchange was performed. + If GSS-API Key Exchange was not performed, this authentication method + won't be available. + + @param username: The username of the authenticating client + @type username: String + @param gss_authenticated: The result of the krb5 authentication + @type gss_authenticated: Integer + @param cc_filename: The krb5 client credentials cache filename + @type cc_filename: String + @return: L{AUTH_FAILED} if the user is not authenticated otherwise + L{AUTH_SUCCESSFUL} + @rtype: Integer + @note: Kerberos credential delegation is not supported. + @see: L{ssh_gss}, L{kex_gss} + @note: We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a certain + platform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + @see: U{http://www.unix.com/man-page/all/3/krb5_kuserok/} + """ + if gss_authenticated == AUTH_SUCCESSFUL: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def enable_auth_gssapi(self): + """ + Overwrite this function in your SSH server to enable GSSAPI + authentication. + The default implementation always returns false. + + @return: True if GSSAPI authentication is enabled otherwise false + @rtype: Boolean + @see: ssh_gss + """ + UseGSSAPI = False + GSSAPICleanupCredentials = False + return UseGSSAPI def check_port_forward_request(self, address, port): """ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py new file mode 100644 index 00000000..72beeaae --- /dev/null +++ b/paramiko/ssh_gss.py @@ -0,0 +1,619 @@ +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +This module provides GSS-API / SSPI authentication for Paramiko as defined in +RFC 4462 with the following restrictions: +Credential delegation is not supported in server mode, +GSS-API key exchange is supported, but not implemented in Paramiko. +To Use this module, you need the following additional python packages: +U{pyasn1 >= 0.1.7 }, +U{python-gssapi >= 0.4.0 (Unix) }, +U{pywin32 2.1.8 (Windows) }. + +@summary: SSH2 GSS-API / SSPI authentication module +@version: 0.1 +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) +@see: L{kex_gss} + +Created on 07.11.2013 +""" + +import struct +import os +try: + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder, decoder +except ImportError: + class ObjectIdentifier(object): + def __init__(self, *args): + raise NotImplementedError("Module pyasn1 not importable") + + class decoder(object): + def decode(self): + raise NotImplementedError("Module pyasn1 not importable") + +from paramiko.common import MSG_USERAUTH_REQUEST +from paramiko.ssh_exception import SSHException + +""" +@var _API: constraint for the used API +@type _API: String +""" +_API = "MIT" + +try: + import gssapi +except ImportError: + try: + import sspicon + import sspi + _API = "SSPI" + except ImportError: + _API = None + + +def GSSAuth(auth_method, gss_deleg_creds=True): + """ + Provide SSH2 GSS-API / SSPI authentication for Paramiko. + + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not. + We delegate credentials by default. + @type gss_deleg_creds: Boolean + @return: Either an L{_SSH_GSSAPI} (Unix) object or an + L{_SSH_SSPI} (Windows) object + @rtype: Object + + @raise ImportError: If no GSS-API / SSPI module could be imported. + + @see: U{RFC 4462 } + @note: Check for the available API and return either an L{_SSH_GSSAPI} + (MIT GSSAPI) object or an L{_SSH_SSPI} (MS SSPI) object. If you + get python-gssapi working on Windows, python-gssapi + will be used and a L{_SSH_GSSAPI} object will be returned. + If there is no supported API available, + C{None} will be returned. + """ + if _API == "MIT": + return _SSH_GSSAPI(auth_method, gss_deleg_creds) + elif _API == "SSPI" and os.name == "nt": + return _SSH_SSPI(auth_method, gss_deleg_creds) + else: + raise ImportError("Unable to import a GSS-API / SSPI module!") + + +class _SSH_GSSAuth(object): + """ + Contains the shared variables and methods of L{_SSH_GSSAPI} and + L{_SSH_SSPI}. + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not + @type gss_deleg_creds: Boolean + """ + self._auth_method = auth_method + self._gss_deleg_creds = gss_deleg_creds + self._gss_host = None + self._username = None + self._session_id = None + self._service = "ssh-connection" + """ + OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication, + so we also support the krb5 mechanism only. + """ + self._krb5_mech = "1.2.840.113554.1.2.2" + + # client mode + self._gss_ctxt = None + self._gss_ctxt_status = False + + # server mode + self._gss_srv_ctxt = None + self._gss_srv_ctxt_status = False + self.cc_file = None + + def set_service(self, service): + """ + This is just a setter to use a non default service. + I added this method, because RFC 4462 doesn't specify "ssh-connection" + as the only service value. + + @param service: The desired SSH service + @type service: String + @rtype: Void + """ + if service.find("ssh-"): + self._service = service + + def set_username(self, username): + """ + Setter for C{username}. If GSS-API Key Exchange is performed, the + username is not set by C{ssh_init_sec_context}. + + @param username: The name of the user who attempts to login + @type username: String + @rtype: Void + """ + self._username = username + + def ssh_gss_oids(self, mode="client"): + """ + This method returns a single OID, because we only support the + Kerberos V5 mechanism. + + @param mode: Client for client mode and server for server mode + @param mode: String + @return: A byte sequence containing the number of supported + OIDs, the length of the OID and the actual OID encoded with + DER + @note: In server mode we just return the OID length and the DER encoded + OID. + @rtype: Bytes + """ + OIDs = self._make_uint32(1) + krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech)) + OID_len = self._make_uint32(len(krb5_OID)) + if mode == "server": + return OID_len + krb5_OID + return OIDs + OID_len + krb5_OID + + def ssh_check_mech(self, desired_mech): + """ + Check if the given OID is the Kerberos V5 OID (server mode). + + @param desired_mech: The desired GSS-API mechanism of the client + @type desired_mech: String + @return: C{True} if the given OID is supported, otherwise C{False} + @rtype: Boolean + """ + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + return False + return True + + # Internals + #-------------------------------------------------------------------------- + def _make_uint32(self, integer): + """ + Create a 32 bit unsigned integer (The byte sequence of an integer). + + @param integer: The integer value to convert + @type integer: Integer + @return: The byte sequence of an 32 bit integer + @rtype: Bytes + """ + return struct.pack("!I", integer) + + def _ssh_build_mic(self, session_id, username, service, auth_method): + """ + Create the SSH2 MIC filed for gssapi-with-mic. + + @param session_id: The SSH session ID + @type session_id: String + @param username: The name of the user who attempts to login + @type username: String + @param service: The requested SSH service + @type service: String + @param auth_method: The requested SSH authentication mechanism + @type auth_method: String + @return: The MIC as defined in RFC 4462. The contents of the + MIC field are: + string session_identifier, + byte SSH_MSG_USERAUTH_REQUEST, + string user-name, + string service (ssh-connection), + string authentication-method + (gssapi-with-mic or gssapi-keyex) + @rtype: Bytes + """ + mic = self._make_uint32(len(session_id)) + mic += session_id + mic += struct.pack('B', MSG_USERAUTH_REQUEST) + mic += self._make_uint32(len(username)) + mic += str.encode(username) + mic += self._make_uint32(len(service)) + mic += str.encode(service) + mic += self._make_uint32(len(auth_method)) + mic += str.encode(auth_method) + return mic + + +class _SSH_GSSAPI(_SSH_GSSAuth): + """ + Implementation of the GSS-API MIT Kerberos Authentication for SSH2. + + @see: L{GSSAuth} + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not + @type gss_deleg_creds: Boolean + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG, + gssapi.C_DELEG_FLAG) + else: + self._gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG) + + def ssh_init_sec_context(self, target, desired_mech=None, + username=None, recv_token=None): + """ + Initialize a GSS-API context. + + @param username: The name of the user who attempts to login + @type username: String + @param target: The hostname of the target to connect to + @type target: String + @param desired_mech: The negotiated GSS-API mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + @type desired_mech: String + @param recv_token: The GSS-API token received from the Server + @type recv_token: String + @raise SSHException: Is raised if the desired mechanism of the client + is not supported + @return: A C{String} if the GSS-API has returned a token or C{None} if + no token was returned + @rtype: String or None + """ + self._username = username + self._gss_host = target + targ_name = gssapi.Name("host@" + self._gss_host, + gssapi.C_NT_HOSTBASED_SERVICE) + ctx = gssapi.Context() + ctx.flags = self._gss_flags + if desired_mech is None: + krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) + else: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + else: + krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) + token = None + if recv_token is None: + self._gss_ctxt = gssapi.InitContext(peer_name=targ_name, + mech_type=krb5_mech, + req_flags=ctx.flags) + token = self._gss_ctxt.step(token) + else: + token = self._gss_ctxt.step(recv_token) + self._gss_ctxt_status = self._gss_ctxt.established + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + @param session_id: The SSH session ID + @type session_id: String + @param gss_kex: Generate the MIC for GSS-API Key Exchange or not + @type gss_kex: Boolean + @return: gssapi-with-mic: + Returns the MIC token from GSS-API for the message we created + with C{_ssh_build_mic}. + gssapi-keyex: + Returns the MIC token from GSS-API with the SSH session ID as + message. + @rtype: String + @see: L{_ssh_build_mic} + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_token = self._gss_ctxt.get_mic(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.get_mic(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, recv_token, username=None): + """ + Accept a GSS-API context (server mode). + + @param hostname: The servers hostname + @type hostname: String + @param username: The name of the user who attempts to login + @type username: String + @param recv_token: The GSS-API Token received from the server, if it's + not the initial call + @type recv_token: String + @return: A C{String} if the GSS-API has returned a token or C{None} if + no token was returned + @rtype: String or None + """ + # hostname and username are not required for GSSAPI, but for SSPI + self._gss_host = hostname + self._username = username + if self._gss_srv_ctxt is None: + self._gss_srv_ctxt = gssapi.AcceptContext() + token = self._gss_srv_ctxt.step(recv_token) + self._gss_srv_ctxt_status = self._gss_srv_ctxt.established + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + @param mic_token: The MIC token received from the client + @type mic_token: String + @param session_id: The SSH session ID + @type session_id: String + @param username: The name of the user who attempts to login + @type username: String + @return: 0 if the MIC check was successful and 1 if it fails + @rtype: Integer + """ + self._session_id = session_id + self._username = username + if self._username is not None: + # server mode + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_status = self._gss_srv_ctxt.verify_mic(mic_field, + mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + mic_status = self._gss_ctxt.verify_mic(self._session_id, + mic_token) + return mic_status + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + @return: C{True} if credentials are delegated, otherwise C{False} + @rtype: Boolean + """ + if self._gss_srv_ctxt.delegated_cred is not None: + return True + return False + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentials if credentials are delegated + (server mode). + + @param client_token: The GSS-API token received form the client + @type client_token: String + @raise NotImplementedError: Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError + + +class _SSH_SSPI(_SSH_GSSAuth): + """ + Implementation of the Microsoft SSPI Kerberos Authentication for SSH2. + + @see: L{GSSAuth} + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + @param auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + @type auth_method: String + @param gss_deleg_creds: Delegate client credentials or not + @type gss_deleg_creds: Boolean + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\ + sspicon.ISC_REQ_MUTUAL_AUTH |\ + sspicon.ISC_REQ_DELEGATE + else: + self._gss_flags = sspicon.ISC_REQ_INTEGRITY |\ + sspicon.ISC_REQ_MUTUAL_AUTH + + def ssh_init_sec_context(self, target, desired_mech=None, + username=None, recv_token=None): + """ + Initialize a SSPI context. + + @param username: The name of the user who attempts to login + @type username: String + @param target: The FQDN of the target to connect to + @type target: String + @param desired_mech: The negotiated SSPI mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + @type desired_mech: String + @param recv_token: The SSPI token received from the Server + @type recv_token: String + @raise SSHException: Is raised if the desired mechanism of the client + is not supported + @return: A C{String} if the SSPI has returned a token or C{None} if + no token was returned + @rtype: String or None + """ + self._username = username + self._gss_host = target + targ_name = "host/" + self._gss_host + if desired_mech is not None: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + if recv_token is None: + self._gss_ctxt = sspi.ClientAuth("Kerberos", + scflags=self._gss_flags, + targetspn=targ_name) + error, token = self._gss_ctxt.authorize(recv_token) + token = token[0].Buffer + if error == 0: + """ + if the status is GSS_COMPLETE (error = 0) the context is fully + established an we can set _gss_ctxt_status to True. + """ + self._gss_ctxt_status = True + token = None + """ + You won't get another token if the context is fully established, + so i set token to None instead of "" + """ + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + @param session_id: The SSH session ID + @type session_id: String + @param gss_kex: Generate the MIC for Key Exchange with SSPI or not + @type gss_kex: Boolean + @return: gssapi-with-mic: + Returns the MIC token from SSPI for the message we created + with C{_ssh_build_mic}. + gssapi-keyex: + Returns the MIC token from SSPI with the SSH session ID as + message. + @rtype: String + @see: L{_ssh_build_mic} + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_token = self._gss_ctxt.sign(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.sign(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, username, recv_token): + """ + Accept a SSPI context (server mode). + + @param hostname: The servers FQDN + @type hostname: String + @param username: The name of the user who attempts to login + @type username: String + @param recv_token: The SSPI Token received from the server, if it's not + the initial call + @type recv_token: String + @return: A C{String} if the SSPI has returned a token or C{None} if + no token was returned + @rtype: String or None + """ + self._gss_host = hostname + self._username = username + targ_name = "host/" + self._gss_host + self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name) + error, token = self._gss_srv_ctxt.authorize(recv_token) + token = token[0].Buffer + if error == 0: + self._gss_srv_ctxt_status = True + token = None + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + @param mic_token: The MIC token received from the client + @type mic_token: String + @param session_id: The SSH session ID + @type session_id: String + @param username: The name of the user who attempts to login + @type username: String + @return: 0 if the MIC check was successful + @rtype: Integer + """ + self._session_id = session_id + self._username = username + mic_status = 1 + if username is not None: + # server mode + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + mic_status = self._gss_srv_ctxt.verify(mic_field, + mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + mic_status = self._gss_ctxt.verify(self._session_id, + mic_token) + """ + The SSPI method C{verify} has no return value, so if no SSPI error + is returned, set C{mic_status} to 0. + """ + mic_status = 0 + return mic_status + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + @return: C{True} if credentials are delegated, otherwise C{False} + @rtype: Boolean + """ + return ( + self._gss_flags & sspicon.ISC_REQ_DELEGATE + ) and ( + self._gss_srv_ctxt_status or (self._gss_flags) + ) + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentails if credentials are delegated + (server mode). + + @param client_token: The SSPI token received form the client + @type client_token: String + @raise NotImplementedError: Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError diff --git a/paramiko/transport.py b/paramiko/transport.py index 6ab9274c..d46a6c6f 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -32,12 +32,15 @@ import weakref import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler +from paramiko.ssh_gss import GSSAuth from paramiko.channel import Channel from paramiko.common import * from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey from paramiko.kex_gex import KexGex from paramiko.kex_group1 import KexGroup1 +from paramiko.kex_group14 import KexGroup14 +from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey from paramiko.message import Message from paramiko.packet import Packetizer, NeedRekeyException from paramiko.primes import ModulusPack @@ -205,7 +208,7 @@ class Transport (threading.Thread): 'arcfour128', 'arcfour256' ) _preferred_macs = ( 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ) _preferred_keys = ( 'ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256' ) - _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ) + _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' ) _preferred_compression = ( 'none', ) _cipher_info = { @@ -234,7 +237,11 @@ class Transport (threading.Thread): _kex_info = { 'diffie-hellman-group1-sha1': KexGroup1, + 'diffie-hellman-group14-sha1': KexGroup14, 'diffie-hellman-group-exchange-sha1': KexGex, + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1, + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14, + 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex } _compression_info = { @@ -249,7 +256,7 @@ class Transport (threading.Thread): _modulus_pack = None - def __init__(self, sock): + def __init__(self, sock, gss_kex=False, gss_deleg_creds=True): """ Create a new SSH session over an existing socket, or socket-like object. This only creates the Transport object; it doesn't begin the @@ -328,6 +335,21 @@ class Transport (threading.Thread): self.host_key_type = None self.host_key = None + # GSS-API / SSPI Key Exchange + self.use_gss_kex = gss_kex + # This will be set to True if GSS-API Key Exchange was performed + self.gss_kex_used = False + self.kexgss_ctxt = None + self.gss_host = None + if gss_kex: + self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds) + self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group1-sha1', + 'diffie-hellman-group14-sha1') + # state used during negotiation self.kex_engine = None self.H = None @@ -418,6 +440,18 @@ class Transport (threading.Thread): """ return SecurityOptions(self) + def set_gss_host(self, gss_host): + """ + Setter for C{gss_host} if GSS-API Key Exchange is performed. + + @param gss_host: The targets name in the kerberos database + Default: The name of the host to connect to + @type gss_host: String + @rtype: Void + """ + # We need the FQDN to get this working with SSPI + self.gss_host = socket.getfqdn(gss_host) + def start_client(self, event=None): """ Negotiate a new SSH2 session as a client. This is the first step after @@ -970,7 +1004,8 @@ class Transport (threading.Thread): self.lock.release() return chan - def connect(self, hostkey=None, username='', password=None, pkey=None): + def connect(self, hostkey=None, username='', password=None, pkey=None, + gss_host=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True): """ Negotiate an SSH2 session, and optionally verify the server's host key and authenticate using a password or private key. This is a shortcut @@ -1000,6 +1035,14 @@ 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} + @param gss_host: The host to authenticate to + @type gss_host: str + @param gss_auth: Enable or Disable GSSAPI Authentication + @type gss_auth: Boolean + @param gss_kex: Perform GSS-API Key Exchange and user authentication + @type gss_kex: Boolean + @param gss_deleg_creds: Use delegated credentails with GSSAPI + @type gss_deleg_cred: Boolean @raise SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. @@ -1010,7 +1053,9 @@ class Transport (threading.Thread): self.start_client() # check host key if we were given one - if (hostkey is not None): + # If GSS-API Key Exchange was performed, we are not required to check + # the host key. + if (hostkey is not None) and not gss_kex: key = self.get_remote_server_key() if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()): self._log(DEBUG, 'Bad host key from server') @@ -1019,13 +1064,19 @@ class Transport (threading.Thread): raise SSHException('Bad host key from server') self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name()) - if (pkey is not None) or (password is not None): + if (pkey is not None) or (password is not None) or gss_auth or gss_kex: if password is not None: self._log(DEBUG, 'Attempting password auth...') self.auth_password(username, password) - else: + elif pkey is not None: self._log(DEBUG, 'Attempting public-key auth...') self.auth_publickey(username, pkey) + elif gss_auth: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)') + self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds) + else: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)') + self.auth_gssapi_keyex(username) return @@ -1308,6 +1359,57 @@ class Transport (threading.Thread): self.auth_handler.auth_interactive(username, handler, my_event, submethods) return self.auth_handler.wait_for_response(my_event) + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds): + """ + Authenticate to the Server using GSS-API / SSPI. + + @param username: The username to authenticate as + @type username: String + @param gss_host: The target host + @type gss_host: String + @param gss_deleg_creds: Delegate credentials or not + @type gss_deleg_creds: Boolean + @return: list of auth types permissible for the next stage of + authentication (normally empty) + @rtype: list + @raise BadAuthenticationType: if gssapi-with-mic isn't + allowed by the server (and no event was passed in) + @raise AuthenticationException: if the authentication failed (and no + event was passed in) + @raise SSHException: if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException('No existing session') + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds, my_event) + return self.auth_handler.wait_for_response(my_event) + + def auth_gssapi_keyex(self, username): + """ + Authenticate to the Server with GSS-API / SSPI if GSS-API Key Exchange + was the used key exchange method. + + @param username: The username to authenticate as + @type username: String + @return: list of auth types permissible for the next stage of + authentication (normally empty) + @rtype: list + @raise BadAuthenticationType: if GSS-API Key Exchange was not performed + (and no event was passed in) + @raise AuthenticationException: if the authentication failed (and no + event was passed in) + @raise SSHException: if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException('No existing session') + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_gssapi_keyex(username, my_event) + return self.auth_handler.wait_for_response(my_event) + def set_log_channel(self, name): """ Set the channel for this transport's logging. The default is @@ -1582,7 +1684,7 @@ class Transport (threading.Thread): if ptype not in self._expected_packet: raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype)) self._expected_packet = tuple() - if (ptype >= 30) and (ptype <= 39): + if (ptype >= 30) and (ptype <= 41): self.kex_engine.parse_next(ptype, m) continue diff --git a/sites/_shared_static/logo.png b/sites/_shared_static/logo.png new file mode 100644 index 00000000..bc76697e Binary files /dev/null and b/sites/_shared_static/logo.png differ diff --git a/sites/shared_conf.py b/sites/shared_conf.py new file mode 100644 index 00000000..86ecdfe8 --- /dev/null +++ b/sites/shared_conf.py @@ -0,0 +1,41 @@ +from datetime import datetime +import os +import sys + +import alabaster + + +# Alabaster theme + mini-extension +html_theme_path = [alabaster.get_path()] +extensions = ['alabaster'] +# Paths relative to invoking conf.py - not this shared file +html_static_path = ['../_shared_static'] +html_theme = 'alabaster' +html_theme_options = { + 'description': "A Python implementation of SSHv2.", + 'github_user': 'paramiko', + 'github_repo': 'paramiko', + 'gittip_user': 'bitprophet', + 'analytics_id': 'UA-18486793-2', + + 'link': '#3782BE', + 'link_hover': '#3782BE', +} +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'searchbox.html', + 'donate.html', + ] +} + +# Regular settings +project = u'Paramiko' +year = datetime.now().year +copyright = u'%d Jeff Forcier' % year +master_doc = 'index' +templates_path = ['_templates'] +exclude_trees = ['_build'] +source_suffix = '.rst' +default_role = 'obj' diff --git a/sites/www/_templates/rss.xml b/sites/www/_templates/rss.xml new file mode 100644 index 00000000..f6f9cbd1 --- /dev/null +++ b/sites/www/_templates/rss.xml @@ -0,0 +1,19 @@ + + + + + {{ title }} + {{ link }} + {{ description }} + {{ date }} + {% for link, title, desc, date in posts %} + + {{ link }} + {{ link }} + <![CDATA[{{ title }}]]> + + {{ date }} + + {% endfor %} + + diff --git a/sites/www/blog.py b/sites/www/blog.py new file mode 100644 index 00000000..3b129ebf --- /dev/null +++ b/sites/www/blog.py @@ -0,0 +1,140 @@ +from collections import namedtuple +from datetime import datetime +import time +import email.utils + +from sphinx.util.compat import Directive +from docutils import nodes + + +class BlogDateDirective(Directive): + """ + Used to parse/attach date info to blog post documents. + + No nodes generated, since none are needed. + """ + has_content = True + + def run(self): + # Tag parent document with parsed date value. + self.state.document.blog_date = datetime.strptime( + self.content[0], "%Y-%m-%d" + ) + # Don't actually insert any nodes, we're already done. + return [] + +class blog_post_list(nodes.General, nodes.Element): + pass + +class BlogPostListDirective(Directive): + """ + Simply spits out a 'blog_post_list' temporary node for replacement. + + Gets replaced at doctree-resolved time - only then will all blog post + documents be written out (& their date directives executed). + """ + def run(self): + return [blog_post_list('')] + + +Post = namedtuple('Post', 'name doc title date opener') + +def get_posts(app): + # Obtain blog posts + post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) + posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) + # Obtain common data used for list page & RSS + data = [] + for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True): + # Welp. No "nice" way to get post title. Thanks Sphinx. + title = doc[0][0][0] + # Date. This may or may not end up reflecting the required + # *input* format, but doing it here gives us flexibility. + date = doc.blog_date + # 1st paragraph as opener. TODO: allow a role or something marking + # where to actually pull from? + opener = doc.traverse(nodes.paragraph)[0] + data.append(Post(post, doc, title, date, opener)) + return data + +def replace_blog_post_lists(app, doctree, fromdocname): + """ + Replace blog_post_list nodes with ordered list-o-links to posts. + """ + # Obtain blog posts + post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) + posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) + # Build "list" of links/etc + post_links = [] + for post, doc, title, date, opener in get_posts(app): + # Link itself + uri = app.builder.get_relative_uri(fromdocname, post) + link = nodes.reference('', '', refdocname=post, refuri=uri) + # Title, bolded. TODO: use 'topic' or something maybe? + link.append(nodes.strong('', title)) + date = date.strftime("%Y-%m-%d") + # Meh @ not having great docutils nodes which map to this. + html = '
%s
' % date + timestamp = nodes.raw(text=html, format='html') + # NOTE: may group these within another element later if styling + # necessitates it + group = [timestamp, nodes.paragraph('', '', link), opener] + post_links.extend(group) + + # Replace temp node(s) w/ expanded list-o-links + for node in doctree.traverse(blog_post_list): + node.replace_self(post_links) + +def rss_timestamp(timestamp): + # Use horribly inappropriate module for its magical daylight-savings-aware + # timezone madness. Props to Tinkerer for the idea. + return email.utils.formatdate( + time.mktime(timestamp.timetuple()), + localtime=True + ) + +def generate_rss(app): + # Meh at having to run this subroutine like 3x per build. Not worth trying + # to be clever for now tho. + posts_ = get_posts(app) + # LOL URLs + root = app.config.rss_link + if not root.endswith('/'): + root += '/' + # Oh boy + posts = [ + ( + root + app.builder.get_target_uri(x.name), + x.title, + str(x.opener[0]), # Grab inner text element from paragraph + rss_timestamp(x.date), + ) + for x in posts_ + ] + location = 'blog/rss.xml' + context = { + 'title': app.config.project, + 'link': root, + 'atom': root + location, + 'description': app.config.rss_description, + # 'posts' is sorted by date already + 'date': rss_timestamp(posts_[0].date), + 'posts': posts, + } + yield (location, context, 'rss.xml') + +def setup(app): + # Link in RSS feed back to main website, e.g. 'http://paramiko.org' + app.add_config_value('rss_link', None, '') + # Ditto for RSS description field + app.add_config_value('rss_description', None, '') + # Interprets date metadata in blog post documents + app.add_directive('date', BlogDateDirective) + # Inserts blog post list node (in e.g. a listing page) for replacement + # below + app.add_node(blog_post_list) + app.add_directive('blog-posts', BlogPostListDirective) + # Performs abovementioned replacement + app.connect('doctree-resolved', replace_blog_post_lists) + # Generates RSS page from whole cloth at page generation step + app.connect('html-collect-pages', generate_rss) diff --git a/sites/www/blog.rst b/sites/www/blog.rst new file mode 100644 index 00000000..af9651e4 --- /dev/null +++ b/sites/www/blog.rst @@ -0,0 +1,16 @@ +==== +Blog +==== + +.. blog-posts directive gets replaced with an ordered list of blog posts. + +.. blog-posts:: + + +.. The following toctree ensures blog posts get processed. + +.. toctree:: + :hidden: + :glob: + + blog/* diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst new file mode 100644 index 00000000..7b075073 --- /dev/null +++ b/sites/www/blog/first-post.rst @@ -0,0 +1,7 @@ +=========== +First post! +=========== + +A blog post. + +.. date:: 2013-12-04 diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst new file mode 100644 index 00000000..c4463f33 --- /dev/null +++ b/sites/www/blog/second-post.rst @@ -0,0 +1,7 @@ +=========== +Another one +=========== + +.. date:: 2013-12-05 + +Indeed! diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst new file mode 100644 index 00000000..2680086e --- /dev/null +++ b/sites/www/changelog.rst @@ -0,0 +1,114 @@ +========= +Changelog +========= + +* :feature:`250` GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and user authentication. +* :bug:`193` (and its attentant PRs :issue:`230` & :issue:`253`): Fix SSH agent + problems present on Windows. Thanks to David Hobbs for initial report and to + Aarni Koskela & Olle Lundberg for the patches. +* :release:`1.12.1 <2014-01-08>` +* :release:`1.11.3 <2014-01-08>` 176 +* :release:`1.10.5 <2014-01-08>` 176 +* :bug:`225` Note ecdsa requirement in README. Thanks to Amaury Rodriguez for + the catch. +* :bug:`176` Fix AttributeError bugs in known_hosts file (re)loading. Thanks + to Nathan Scowcroft for the patch & Martin Blumenstingl for the initial test + case. +* :release:`1.12.0 <2013-09-27>` +* :release:`1.11.2 <2013-09-27>` +* :release:`1.10.4 <2013-09-27>` 199, 200, 179 +* :feature:`152` Add tentative support for ECDSA keys. *This adds the ecdsa + module as a new dependency of Paramiko.* The module is available at + [warner/python-ecdsa on Github](https://github.com/warner/python-ecdsa) and + [ecdsa on PyPI](https://pypi.python.org/pypi/ecdsa). + + * Note that you might still run into problems with key negotiation -- + Paramiko picks the first key that the server offers, which might not be + what you have in your known_hosts file. + * Mega thanks to Ethan Glasser-Camp for the patch. + +* :feature:`136` Add server-side support for the SSH protocol's 'env' command. + Thanks to Benjamin Pollack for the patch. +* :bug:`156` Fix potential deadlock condition when using Channel objects as + sockets (e.g. when using SSH gatewaying). Thanks to Steven Noonan and Frank + Arnold for catch & patch. +* :bug:`179` Fix a missing variable causing errors when an ssh_config file has + a non-default AddressFamily set. Thanks to Ed Marshall & Tomaz Muraus for + catch & patch. +* :bug:`200` Fix an exception-causing typo in ``demo_simple.py``. Thanks to Alex + Buchanan for catch & Dave Foster for patch. +* :bug:`199` Typo fix in the license header cross-project. Thanks to Armin + Ronacher for catch & patch. +* :release:`1.11.1 <2013-09-20>` +* :release:`1.10.3 <2013-09-20>` +* :bug:`162` Clean up HMAC module import to avoid deadlocks in certain uses of + SSHClient. Thanks to Gernot Hillier for the catch & suggested fix. +* :bug:`36` Fix the port-forwarding demo to avoid file descriptor errors. + Thanks to Jonathan Halcrow for catch & patch. +* :bug:`168` Update config handling to properly handle multiple 'localforward' + and 'remoteforward' keys. Thanks to Emre Yılmaz for the patch. +* :release:`1.11.0 <2013-07-26>` +* :release:`1.10.2 <2013-07-26>` +* :bug:`98 major` On Windows, when interacting with the PuTTY PAgeant, Paramiko + now creates the shared memory map with explicit Security Attributes of the + user, which is the same technique employed by the canonical PuTTY library to + avoid permissions issues when Paramiko is running under a different UAC + context than the PuTTY Ageant process. Thanks to Jason R. Coombs for the + patch. +* :support:`100` Remove use of PyWin32 in ``win_pageant`` module. Module was + already dependent on ctypes for constructing appropriate structures and had + ctypes implementations of all functionality. Thanks to Jason R. Coombs for + the patch. +* :bug:`87 major` Ensure updates to ``known_hosts`` files account for any + updates to said files after Paramiko initially read them. (Includes related + fix to guard against duplicate entries during subsequent ``known_hosts`` + loads.) Thanks to ``@sunweaver`` for the contribution. +* :bug:`153` (also :issue:`67`) Warn on parse failure when reading known_hosts + file. Thanks to ``@glasserc`` for patch. +* :bug:`146` Indentation fixes for readability. Thanks to Abhinav Upadhyay for + catch & patch. +* :release:`1.10.1 <2013-04-05>` +* :bug:`142` (`Fabric #811 `_) + SFTP put of empty file will still return the attributes of the put file. + Thanks to Jason R. Coombs for the patch. +* :bug:`154` (`Fabric #876 `_) + Forwarded SSH agent connections left stale local pipes lying around, which + could cause local (and sometimes remote or network) resource starvation when + running many agent-using remote commands. Thanks to Kevin Tegtmeier for catch + & patch. +* :release:`1.10.0 <2013-03-01>` +* :feature:`66` Batch SFTP writes to help speed up file transfers. Thanks to + Olle Lundberg for the patch. +* :bug:`133 major` Fix handling of window-change events to be on-spec and not + attempt to wait for a response from the remote sshd; this fixes problems with + less common targets such as some Cisco devices. Thanks to Phillip Heller for + catch & patch. +* :feature:`93` Overhaul SSH config parsing to be in line with ``man + ssh_config`` (& the behavior of ``ssh`` itself), including addition of parameter + expansion within config values. Thanks to Olle Lundberg for the patch. +* :feature:`110` Honor SSH config ``AddressFamily`` setting when looking up + local host's FQDN. Thanks to John Hensley for the patch. +* :feature:`128` Defer FQDN resolution until needed, when parsing SSH config + files. Thanks to Parantapa Bhattacharya for catch & patch. +* :bug:`102 major` Forego random padding for packets when running under + ``*-ctr`` ciphers. This corrects some slowdowns on platforms where random + byte generation is inefficient (e.g. Windows). Thanks to ``@warthog618`` for + catch & patch, and Michael van der Kolff for code/technique review. +* :feature:`127` Turn ``SFTPFile`` into a context manager. Thanks to Michael + Williamson for the patch. +* :feature:`116` Limit ``Message.get_bytes`` to an upper bound of 1MB to protect + against potential DoS vectors. Thanks to ``@mvschaik`` for catch & patch. +* :feature:`115` Add convenience ``get_pty`` kwarg to ``Client.exec_command`` so + users not manually controlling a channel object can still toggle PTY + creation. Thanks to Michael van der Kolff for the patch. +* :feature:`71` Add ``SFTPClient.putfo`` and ``.getfo`` methods to allow direct + uploading/downloading of file-like objects. Thanks to Eric Buehl for the + patch. +* :feature:`113` Add ``timeout`` parameter to ``SSHClient.exec_command`` for + easier setting of the command's internal channel object's timeout. Thanks to + Cernov Vladimir for the patch. +* :support:`94` Remove duplication of SSH port constant. Thanks to Olle + Lundberg for the catch. +* :feature:`80` Expose the internal "is closed" property of the file transfer + class ``BufferedFile`` as ``.closed``, better conforming to Python's file + interface. Thanks to ``@smunaut`` and James Hiscock for catch & patch. diff --git a/sites/www/conf.py b/sites/www/conf.py new file mode 100644 index 00000000..481acdff --- /dev/null +++ b/sites/www/conf.py @@ -0,0 +1,35 @@ +# Obtain shared config values +import sys +import os +from os.path import abspath, join, dirname + +sys.path.append(abspath(join(dirname(__file__), '..'))) +from shared_conf import * + +# Local blog extension +sys.path.append(abspath('.')) +extensions.append('blog') +rss_link = 'http://paramiko.org' +rss_description = 'Paramiko project news' + +# Releases changelog extension +extensions.append('releases') +releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s" +releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" + +# Intersphinx for referencing API/usage docs +extensions.append('sphinx.ext.intersphinx') +# Default is 'local' building, but reference the public docs site when building +# under RTD. +target = join(dirname(__file__), '..', 'docs', '_build') +if os.environ.get('READTHEDOCS') == 'True': + # TODO: switch to docs.paramiko.org post go-live of sphinx API docs + target = 'http://paramiko-docs.readthedocs.org/en/latest/' +#intersphinx_mapping = { +# 'docs': (target, None), +#} + +# Sister-site links to API docs +html_theme_options['extra_nav_links'] = { + "API Docs": 'http://docs.paramiko.org', +} diff --git a/sites/www/contact.rst b/sites/www/contact.rst new file mode 100644 index 00000000..2b6583f5 --- /dev/null +++ b/sites/www/contact.rst @@ -0,0 +1,11 @@ +======= +Contact +======= + +You can get in touch with the developer & user community in any of the +following ways: + +* IRC: ``#paramiko`` on Freenode +* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage + `_ for usage details). +* This website - a blog section is forthcoming. diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst new file mode 100644 index 00000000..2b752cc5 --- /dev/null +++ b/sites/www/contributing.rst @@ -0,0 +1,19 @@ +============ +Contributing +============ + +How to get the code +=================== + +Our primary Git repository is on Github at `paramiko/paramiko +`_; please follow their instructions for +cloning to your local system. (If you intend to submit patches/pull requests, +we recommend forking first, then cloning your fork. Github has excellent +documentation for all this.) + + +How to submit bug reports or new code +===================================== + +Please see `this project-agnostic contribution guide +`_ - we follow it explicitly. diff --git a/sites/www/index.rst b/sites/www/index.rst new file mode 100644 index 00000000..7fefedd2 --- /dev/null +++ b/sites/www/index.rst @@ -0,0 +1,38 @@ +Welcome to Paramiko! +==================== + +Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_, +providing both client and server functionality. While it leverages a Python C +extension for low level cryptography (`PyCrypto `_), +Paramiko itself is a pure Python interface around SSH networking concepts. + +This website covers project information for Paramiko such as the changelog, +contribution guidelines, development roadmap, news/blog, and so forth. Detailed +usage and API documentation can be found at our code documentation site, +`docs.paramiko.org `_. + +.. toctree:: + changelog + installing + contributing + contact + +.. Hide blog in hidden toctree for now (to avoid warnings.) + +.. toctree:: + :hidden: + + blog + + +.. rubric:: Footnotes + +.. [#] + SSH is defined in RFCs + `4251 `_, + `4252 `_, + `4253 `_, and + `4254 `_; + the primary working implementation of the protocol is the `OpenSSH project + `_. Paramiko implements a large portion of the SSH + feature set, but there are occasional gaps. diff --git a/sites/www/installing.rst b/sites/www/installing.rst new file mode 100644 index 00000000..0d4dc1ac --- /dev/null +++ b/sites/www/installing.rst @@ -0,0 +1,105 @@ +========== +Installing +========== + +Paramiko itself +=============== + +The recommended way to get Invoke is to **install the latest stable release** +via `pip `_:: + + $ pip install paramiko + +.. note:: + Users who want the bleeding edge can install the development version via + ``pip install paramiko==dev``. + +We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming +soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on +Python 2.4 still, but there is no longer any support guarantee. + +Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the +PyCrypto C extension. `ecdsa` is easily installable from wherever you +obtained Paramiko's package; PyCrypto may require more work. Read on for +details. + +PyCrypto +======== + +`PyCrypto `_ provides the low-level +(C-based) encryption algorithms we need to implement the SSH protocol. There +are a couple gotchas associated with installing PyCrypto: its compatibility +with Python's package tools, and the fact that it is a C-based extension. + +.. _pycrypto-and-pip: + +Possible gotcha on older Python and/or pip versions +--------------------------------------------------- + +We strongly recommend using ``pip`` to as it is newer and generally better than +``easy_install``. However, a combination of bugs in specific (now rather old) +versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto. +Specifically: + +* Python = 2.5.x +* PyCrypto >= 2.1 (required for most modern versions of Paramiko) +* ``pip`` < 0.8.1 + +When all three criteria are met, you may encounter ``No such file or +directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install +PyCrypto``. + +The fix is to make sure at least one of the above criteria is not met, by doing +the following (in order of preference): + +* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``. +* Upgrade to Python 2.6 or above. +* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1, + and install PyCrypto 2.0.1 (the oldest version on PyPI which works with + Paramiko 1.7.6/1.7.7) + + +C extension +----------- + +Unless you are installing from a precompiled source such as a Debian apt +repository or RedHat RPM, or using :ref:`pypm `, you will also need the +ability to build Python C-based modules from source in order to install +PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will +need the traditional C build toolchain installed (e.g. Developer Tools / XCode +Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux +-- basically, anything with ``gcc``, ``make`` and so forth) as well as the +Python development libraries, often named ``python-dev`` or similar. + +For **Windows** users we recommend using :ref:`pypm`, installing a C +development environment such as `Cygwin `_ or obtaining a +precompiled Win32 PyCrypto package from `voidspace's Python modules page +`_. + +.. note:: + Some Windows users whose Python is 64-bit have found that the PyCrypto + dependency ``winrandom`` may not install properly, leading to ImportErrors. + In this scenario, you'll probably need to compile ``winrandom`` yourself + via e.g. MS Visual Studio. See `Fabric #194 + `_ for info. + + +.. _pypm: + +ActivePython and PyPM +===================== + +Windows users who already have ActiveState's `ActivePython +`_ distribution installed +may find Paramiko is best installed with `its package manager, PyPM +`_. Below is example output from an +installation of Paramiko via ``pypm``:: + + C:\> pypm install paramiko + The following packages will be installed into "%APPDATA%\Python" (2.7): + paramiko-1.7.8 pycrypto-2.4 + Get: [pypm-free.activestate.com] paramiko 1.7.8 + Get: [pypm-free.activestate.com] pycrypto 2.4 + Installing paramiko-1.7.8 + Installing pycrypto-2.4 + C:\> diff --git a/test.py b/test.py index bd966d1e..a954ef27 100755 --- a/test.py +++ b/test.py @@ -44,6 +44,22 @@ from tests.test_packetizer import PacketizerTest from tests.test_auth import AuthTest from tests.test_transport import TransportTest from tests.test_client import SSHClientTest +from test_message import MessageTest +from test_file import BufferedFileTest +from test_buffered_pipe import BufferedPipeTest +from test_util import UtilTest +from test_hostkeys import HostKeysTest +from test_pkey import KeyTest +from test_kex import KexTest +from test_packetizer import PacketizerTest +from test_auth import AuthTest +from test_transport import TransportTest +from test_sftp import SFTPTest +from test_sftp_big import BigSFTPTest +from test_client import SSHClientTest +from test_gssapi import GSSAPITest +from test_ssh_gss import GSSAuthTest +from test_kex_gss import GSSKexTest default_host = 'localhost' default_user = os.environ.get('USER', 'nobody') @@ -85,6 +101,17 @@ def main(): help='skip SFTP client/server tests, which can be slow') parser.add_option('--no-big-file', action='store_false', dest='use_big_file', default=True, help='skip big file SFTP tests, which are slow as molasses') + parser.add_option('--gssapi-test', action='store_true', dest='gssapi_test', default=False, + help='Test the used APIs for GSS-API / SSPI authentication') + parser.add_option('--test-gssauth', action='store_true', dest='test_gssauth', default=False, + help='Test GSS-API / SSPI authentication for SSHv2. To test this, you need kerberos a infrastructure.\ + Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\ + copy the used key to another file and set the environment variable KRB5_KTNAME to this file.') + parser.add_option('--test-gssapi-keyex', action='store_true', dest='test_gsskex', default=False, + help='Test GSS-API / SSPI authenticated iffie-Hellman Key Exchange and user\ + authentication. To test this, you need kerberos a infrastructure.\ + Note: Paramiko needs access to your krb5.keytab file. Make it readable for Paramiko or\ + copy the used key to another file and set the environment variable KRB5_KTNAME to this file.') parser.add_option('-R', action='store_false', dest='use_loopback_sftp', default=True, help='perform SFTP tests against a remote server (by default, SFTP tests ' + 'are done through a loopback socket)') @@ -101,6 +128,16 @@ def main(): parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, metavar='', help='[with -R] (optional) password to unlock the private key for remote sftp tests') + parser.add_option('--krb5_principal', dest='krb5_principal', type='string', + metavar='', + help='The krb5 principal (your username) for GSS-API / SSPI authentication') + parser.add_option('--targ_name', dest='targ_name', type='string', + metavar='', + help='Target name for GSS-API / SSPI authentication.\ + This is the hosts name you are running the test on in the kerberos database.') + parser.add_option('--server_mode', action='store_true', dest='server_mode', default=False, + help='Usage with --gssapi-test. Test the available GSS-API / SSPI server mode to.\ + Note: you need to have access to the kerberos keytab file.') options, args = parser.parse_args() @@ -136,6 +173,15 @@ def main(): suite.addTest(unittest.makeSuite(SFTPTest)) if options.use_big_file: suite.addTest(unittest.makeSuite(BigSFTPTest)) + if options.gssapi_test: + GSSAPITest.init(options.targ_name, options.server_mode) + suite.addTest(unittest.makeSuite(GSSAPITest)) + if options.test_gssauth: + GSSAuthTest.init(options.krb5_principal, options.targ_name) + suite.addTest(unittest.makeSuite(GSSAuthTest)) + if options.test_gsskex: + GSSKexTest.init(options.krb5_principal, options.targ_name) + suite.addTest(unittest.makeSuite(GSSKexTest)) verbosity = 1 if options.verbose: verbosity = 2 diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py new file mode 100644 index 00000000..e9ef99a9 --- /dev/null +++ b/tests/test_gssapi.py @@ -0,0 +1,167 @@ +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +Test the used APIs for GSS-API / SSPI authentication + +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) + +Created on 04.12.2013 +""" + +import unittest +import socket + + +class GSSAPITest(unittest.TestCase): + + def init(hostname=None, srv_mode=False): + global krb5_mech, targ_name, server_mode + krb5_mech = "1.2.840.113554.1.2.2" + targ_name = hostname + server_mode = srv_mode + + init = staticmethod(init) + + def test_1_pyasn1(self): + """ + Test the used methods of pyasn1. + """ + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder, decoder + oid = encoder.encode(ObjectIdentifier(krb5_mech)) + mech, __ = decoder.decode(oid) + self.assertEquals(krb5_mech, mech.__str__()) + + def test_2_gssapi_sspi(self): + """ + Test the used methods of python-gssapi or sspi, sspicon from pywin32. + """ + _API = "MIT" + try: + import gssapi + except ImportError: + import sspicon + import sspi + _API = "SSPI" + + c_token = None + gss_ctxt_status = False + mic_msg = b"G'day Mate!" + + if _API == "MIT": + if server_mode: + gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG, + gssapi.C_DELEG_FLAG) + else: + gss_flags = (gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_DELEG_FLAG) + """ + Initialize a GSS-API context. + """ + ctx = gssapi.Context() + ctx.flags = gss_flags + krb5_oid = gssapi.OID.mech_from_string(krb5_mech) + target_name = gssapi.Name("host@" + targ_name, + gssapi.C_NT_HOSTBASED_SERVICE) + gss_ctxt = gssapi.InitContext(peer_name=target_name, + mech_type=krb5_oid, + req_flags=ctx.flags) + if server_mode: + c_token = gss_ctxt.step(c_token) + gss_ctxt_status = gss_ctxt.established + self.assertEquals(False, gss_ctxt_status) + """ + Accept a GSS-API context. + """ + gss_srv_ctxt = gssapi.AcceptContext() + s_token = gss_srv_ctxt.step(c_token) + gss_ctxt_status = gss_srv_ctxt.established + self.assertNotEquals(None, s_token) + self.assertEquals(True, gss_ctxt_status) + """ + Establish the client context + """ + c_token = gss_ctxt.step(s_token) + self.assertEquals(None, c_token) + else: + while not gss_ctxt.established: + c_token = gss_ctxt.step(c_token) + self.assertNotEquals(None, c_token) + """ + Build MIC + """ + mic_token = gss_ctxt.get_mic(mic_msg) + + if server_mode: + """ + Check MIC + """ + status = gss_srv_ctxt.verify_mic(mic_msg, mic_token) + self.assertEquals(0, status) + else: + gss_flags = sspicon.ISC_REQ_INTEGRITY |\ + sspicon.ISC_REQ_MUTUAL_AUTH |\ + sspicon.ISC_REQ_DELEGATE + """ + Initialize a GSS-API context. + """ + target_name = "host/" + socket.getfqdn(targ_name) + gss_ctxt = sspi.ClientAuth("Kerberos", + scflags=gss_flags, + targetspn=target_name) + if server_mode: + error, token = gss_ctxt.authorize(c_token) + c_token = token[0].Buffer + self.assertEquals(0, error) + """ + Accept a GSS-API context. + """ + gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=target_name) + error, token = gss_srv_ctxt.authorize(c_token) + s_token = token[0].Buffer + """ + Establish the context. + """ + error, token = gss_ctxt.authorize(s_token) + c_token = token[0].Buffer + self.assertEquals(None, c_token) + self.assertEquals(0, error) + """ + Build MIC + """ + mic_token = gss_ctxt.sign(mic_msg) + """ + Check MIC + """ + gss_srv_ctxt.verify(mic_msg, mic_token) + else: + error, token = gss_ctxt.authorize(c_token) + c_token = token[0].Buffer + self.assertNotEquals(0, error) diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py new file mode 100644 index 00000000..e160eb35 --- /dev/null +++ b/tests/test_kex_gss.py @@ -0,0 +1,143 @@ +# Copyright (C) 2003-2007 Robey Pointer +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +Unit Tests for the GSS-API / SSPI SSHv2 Diffie-Hellman Key Exchange and user +authentication + +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2003-2009 Robey Pointer, (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) + +Created on 08.01.2014 +""" + + +import socket +import threading +import unittest + +import paramiko + + +class NullServer (paramiko.ServerInterface): + + def get_allowed_auths(self, username): + return 'gssapi-keyex' + + def check_auth_gssapi_keyex(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + return UseGSSAPI + + def check_channel_request(self, kind, chanid): + return paramiko.OPEN_SUCCEEDED + + def check_channel_exec_request(self, channel, command): + if command != 'yes': + return False + return True + + +class GSSKexTest(unittest.TestCase): + + def init(username, hostname): + global krb5_principal, targ_name + krb5_principal = username + targ_name = hostname + + init = staticmethod(init) + + def setUp(self): + self.username = krb5_principal + self.hostname = socket.getfqdn(targ_name) + self.sockl = socket.socket() + self.sockl.bind((targ_name, 0)) + self.sockl.listen(1) + self.addr, self.port = self.sockl.getsockname() + self.event = threading.Event() + thread = threading.Thread(target=self._run) + thread.start() + + def tearDown(self): + for attr in "tc ts socks sockl".split(): + if hasattr(self, attr): + getattr(self, attr).close() + + def _run(self): + self.socks, addr = self.sockl.accept() + self.ts = paramiko.Transport(self.socks, True) + host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + self.ts.add_server_key(host_key) + self.ts.set_gss_host(targ_name) + try: + self.ts.load_server_moduli() + except: + print ('(Failed to load moduli -- gex will be unsupported.)') + server = NullServer() + self.ts.start_server(self.event, server) + + def test_1_gsskex_and_auth(self): + """ + Verify that Paramiko can handle SSHv2 GSS-API / SSPI authenticated + Diffie-Hellman Key Exchange and user authentication with the GSS-API + context created during key exchange. + """ + host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + + self.tc = paramiko.SSHClient() + self.tc.get_host_keys().add('[%s]:%d' % (self.hostname, self.port), + 'ssh-rsa', public_host_key) + self.tc.connect(self.hostname, self.port, username=self.username, + gss_auth=True, gss_kex=True) + + self.event.wait(1.0) + self.assert_(self.event.isSet()) + self.assert_(self.ts.is_active()) + self.assertEquals(self.username, self.ts.get_username()) + self.assertEquals(True, self.ts.is_authenticated()) + + stdin, stdout, stderr = self.tc.exec_command('yes') + schan = self.ts.accept(1.0) + + schan.send('Hello there.\n') + schan.send_stderr('This is on stderr.\n') + schan.close() + + self.assertEquals('Hello there.\n', stdout.readline()) + self.assertEquals('', stdout.readline()) + self.assertEquals('This is on stderr.\n', stderr.readline()) + self.assertEquals('', stderr.readline()) + + stdin.close() + stdout.close() + stderr.close() diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py new file mode 100644 index 00000000..98e280ec --- /dev/null +++ b/tests/test_ssh_gss.py @@ -0,0 +1,136 @@ +# Copyright (C) 2003-2007 Robey Pointer +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +Unit Tests for the GSS-API / SSPI SSHv2 Authentication (gssapi-with-mic) + +@author: Sebastian Deiss +@contact: U{https://github.com/SebastianDeiss/paramiko/issues} +@organization: science + computing ag + (U{EMail}) +@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag + } +@license: GNU Lesser General Public License (LGPL) + +Created on 04.12.2013 +""" + +import socket +import threading +import unittest + +import paramiko + + +class NullServer (paramiko.ServerInterface): + + def get_allowed_auths(self, username): + return 'gssapi-with-mic' + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + if gss_authenticated == paramiko.AUTH_SUCCESSFUL: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def enable_auth_gssapi(self): + UseGSSAPI = True + GSSAPICleanupCredentials = True + return UseGSSAPI + + def check_channel_request(self, kind, chanid): + return paramiko.OPEN_SUCCEEDED + + def check_channel_exec_request(self, channel, command): + if command != 'yes': + return False + return True + + +class GSSAuthTest(unittest.TestCase): + + def init(username, hostname): + global krb5_principal, targ_name + krb5_principal = username + targ_name = hostname + + init = staticmethod(init) + + def setUp(self): + self.username = krb5_principal + self.hostname = socket.getfqdn(targ_name) + self.sockl = socket.socket() + self.sockl.bind((targ_name, 0)) + self.sockl.listen(1) + self.addr, self.port = self.sockl.getsockname() + self.event = threading.Event() + thread = threading.Thread(target=self._run) + thread.start() + + def tearDown(self): + for attr in "tc ts socks sockl".split(): + if hasattr(self, attr): + getattr(self, attr).close() + + def _run(self): + self.socks, addr = self.sockl.accept() + self.ts = paramiko.Transport(self.socks) + host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + self.ts.add_server_key(host_key) + server = NullServer() + self.ts.start_server(self.event, server) + + def test_1_gss_auth(self): + """ + Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication + (gssapi-with-mic) in client and server mode. + """ + host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + + self.tc = paramiko.SSHClient() + self.tc.get_host_keys().add('[%s]:%d' % (self.hostname, self.port), + 'ssh-rsa', public_host_key) + self.tc.connect(self.hostname, self.port, username=self.username, + gss_auth=True) + + self.event.wait(1.0) + self.assert_(self.event.isSet()) + self.assert_(self.ts.is_active()) + self.assertEquals(self.username, self.ts.get_username()) + self.assertEquals(True, self.ts.is_authenticated()) + + stdin, stdout, stderr = self.tc.exec_command('yes') + schan = self.ts.accept(1.0) + + schan.send('Hello there.\n') + schan.send_stderr('This is on stderr.\n') + schan.close() + + self.assertEquals('Hello there.\n', stdout.readline()) + self.assertEquals('', stdout.readline()) + self.assertEquals('This is on stderr.\n', stderr.readline()) + self.assertEquals('', stderr.readline()) + + stdin.close() + stdout.close() + stderr.close() -- cgit v1.2.3 From 604980e9dfeb671da27178c5261ae39dfb5e2f32 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Mon, 17 Feb 2014 10:49:05 +0100 Subject: Improved handling of failed GSS-API authentication attempts Previously an attribute error occurred or a SSHException was thrown if the GSS-API authentication failed. If GSS-API authentication fails now or the remote host does not support GSS-API, paramiko tries other authentication methods. --- demos/demo_simple.py | 12 ++++++++---- paramiko/auth_handler.py | 3 +++ paramiko/client.py | 50 ++++++++++++++++++++++++------------------------ paramiko/transport.py | 18 ++++++++--------- 4 files changed, 45 insertions(+), 38 deletions(-) diff --git a/demos/demo_simple.py b/demos/demo_simple.py index 100e15f5..a9d363da 100755 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -64,7 +64,7 @@ if username == '': username = input('Username [%s]: ' % default_username) if len(username) == 0: username = default_username -if not UseGSSAPI: +if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) @@ -74,13 +74,17 @@ try: client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy()) print('*** Connecting...') - if not UseGSSAPI: + if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): client.connect(hostname, Port, username, password) else: # SSPI works only with the FQDN of the target host hostname = socket.getfqdn(hostname) - client.connect(hostname, Port, username, gss_auth=UseGSSAPI, - gss_kex=DoGSSAPIKeyExchange) + try: + client.connect(hostname, Port, username, gss_auth=UseGSSAPI, + gss_kex=DoGSSAPIKeyExchange) + except Exception: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) + client.connect(hostname, Port, username, password) chan = client.invoke_shell() print(repr(client.get_transport())) print('*** Here we go!\n') diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 1910a7f1..81a7946d 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -297,6 +297,9 @@ class AuthHandler (object): %s\n") % (str(maj_status), str(min_status), err_msg) + elif ptype == MSG_USERAUTH_FAILURE: + self._parse_userauth_failure(m) + return else: raise SSHException("Received Package: %s" % MSG_NAMES[ptype]) elif self.auth_method == 'gssapi-keyex' and\ diff --git a/paramiko/client.py b/paramiko/client.py index b786d17b..f4149510 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -473,6 +473,31 @@ class SSHClient (object): two_factor = False allowed_types = [] + """ + If GSS-API support and GSS-PI Key Exchange was performed, we attempt + authentication with gssapi-keyex. + """ + if gss_kex and self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_keyex(username) + return + except Exception as e: + saved_exception = e + + """ + Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key + Exchange is not performed, because if we use GSS-API for the key + exchange, there is already a fully established GSS-API context, so + why should we do that again? + """ + if gss_auth: + try: + self._transport.auth_gssapi_with_mic(username, gss_host, + gss_deleg_creds) + return + except Exception as e: + saved_exception = e + if pkey is not None: try: self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint())) @@ -554,31 +579,6 @@ class SSHClient (object): elif two_factor: raise SSHException('Two-factor authentication requires a password') - """ - Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key - Exchange is not performed, because if we use GSS-API for the key - exchange, there is already a fully established GSS-API context, so - why should we do that again? - """ - if gss_auth and not self._transport.gss_kex_used: - try: - self._transport.auth_gssapi_with_mic(username, gss_host, - gss_deleg_creds) - return - except SSHException as e: - saved_exception = e - - """ - If GSS-API support and GSS-PI Key Exchange was performed, we attempt - authentication with gssapi-keyex. - """ - if gss_auth and gss_kex and self._transport.gss_kex_used: - try: - self._transport.auth_gssapi_keyex(username) - return - except SSHException as e: - saved_exception = e - # if we got an auth-failed exception earlier, re-raise it if saved_exception is not None: raise saved_exception diff --git a/paramiko/transport.py b/paramiko/transport.py index d46a6c6f..fcf074ae 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -341,7 +341,7 @@ class Transport (threading.Thread): self.gss_kex_used = False self.kexgss_ctxt = None self.gss_host = None - if gss_kex: + if self.use_gss_kex: self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds) self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==', 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', @@ -1065,18 +1065,18 @@ class Transport (threading.Thread): self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name()) if (pkey is not None) or (password is not None) or gss_auth or gss_kex: - if password is not None: - self._log(DEBUG, 'Attempting password auth...') - self.auth_password(username, password) - elif pkey is not None: - self._log(DEBUG, 'Attempting public-key auth...') - self.auth_publickey(username, pkey) - elif gss_auth: + if gss_auth: self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)') self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds) - else: + elif gss_kex: self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)') self.auth_gssapi_keyex(username) + elif pkey is not None: + self._log(DEBUG, 'Attempting public-key auth...') + self.auth_publickey(username, pkey) + else: + self._log(DEBUG, 'Attempting password auth...') + self.auth_password(username, password) return -- cgit v1.2.3 From 7c3e505d92d804c7cd5f376cab297bf3552f5aa3 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Tue, 18 Feb 2014 11:47:33 +0100 Subject: Improve Exception handling If an GSS-API / SSPI error occurs you get a status code and an error message, but you may also want the name of the remote host. That's what this patch adds. --- paramiko/ssh_gss.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 72beeaae..417dab6e 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -44,6 +44,7 @@ Created on 07.11.2013 import struct import os +import sys try: from pyasn1.type.univ import ObjectIdentifier from pyasn1.codec.der import encoder, decoder @@ -310,13 +311,17 @@ class _SSH_GSSAPI(_SSH_GSSAuth): else: krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) token = None - if recv_token is None: - self._gss_ctxt = gssapi.InitContext(peer_name=targ_name, - mech_type=krb5_mech, - req_flags=ctx.flags) - token = self._gss_ctxt.step(token) - else: - token = self._gss_ctxt.step(recv_token) + try: + if recv_token is None: + self._gss_ctxt = gssapi.InitContext(peer_name=targ_name, + mech_type=krb5_mech, + req_flags=ctx.flags) + token = self._gss_ctxt.step(token) + else: + token = self._gss_ctxt.step(recv_token) + except gssapi.GSSException: + raise gssapi.GSSException("{0} Target: {1}".format(sys.exc_info()[1], + self._gss_host)) self._gss_ctxt_status = self._gss_ctxt.established return token @@ -476,17 +481,22 @@ class _SSH_SSPI(_SSH_GSSAuth): """ self._username = username self._gss_host = target + error = 0 targ_name = "host/" + self._gss_host if desired_mech is not None: mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: raise SSHException("Unsupported mechanism OID.") - if recv_token is None: - self._gss_ctxt = sspi.ClientAuth("Kerberos", - scflags=self._gss_flags, - targetspn=targ_name) - error, token = self._gss_ctxt.authorize(recv_token) - token = token[0].Buffer + try: + if recv_token is None: + self._gss_ctxt = sspi.ClientAuth("Kerberos", + scflags=self._gss_flags, + targetspn=targ_name) + error, token = self._gss_ctxt.authorize(recv_token) + token = token[0].Buffer + except: + raise Exception("{0}, Target: {1}".format(sys.exc_info()[1], + self._gss_host)) if error == 0: """ if the status is GSS_COMPLETE (error = 0) the context is fully -- cgit v1.2.3 From 2e2324b47b3dacb09ee945728ae3ea16ab4f9ff7 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Thu, 27 Feb 2014 09:27:00 +0100 Subject: Add userauth-banner handling for GSS-API authentication Previously GSS-API authentication (gssapi-with-mic) failed if the server sent a userauth-banner. --- paramiko/auth_handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 81a7946d..ee5fb174 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -241,6 +241,9 @@ class AuthHandler (object): # send the supported GSSAPI OIDs to the server self.transport._send_message(m) ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_BANNER: + self._parse_userauth_banner(m) + ptype, m = self.transport.packetizer.read_message() if ptype == MSG_USERAUTH_GSSAPI_RESPONSE: """ Read the mechanism selected by the server. -- cgit v1.2.3 From b0b6a827b96453cfe9bddb343f94c5c2f4243f7b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 13 Mar 2014 21:05:32 -0700 Subject: Update docs to reflect Python 2.6+, 3.3+ compat --- sites/www/index.rst | 2 +- sites/www/installing.rst | 34 +++------------------------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/sites/www/index.rst b/sites/www/index.rst index 7fefedd2..a8e72624 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -1,7 +1,7 @@ Welcome to Paramiko! ==================== -Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_, +Paramiko is a Python (2.6+, 3.3+) implementation of the SSHv2 protocol [#]_, providing both client and server functionality. While it leverages a Python C extension for low level cryptography (`PyCrypto `_), Paramiko itself is a pure Python interface around SSH networking concepts. diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 0d4dc1ac..955a0a59 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -14,9 +14,9 @@ via `pip `_:: Users who want the bleeding edge can install the development version via ``pip install paramiko==dev``. -We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming -soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on -Python 2.4 still, but there is no longer any support guarantee. +We currently support **Python 2.6, 2.7 and 3.3** (Python **3.2** should also +work but has a less-strong compatibility guarantee from us.) Users on Python +2.5 or older are urged to upgrade. Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the PyCrypto C extension. `ecdsa` is easily installable from wherever you @@ -31,34 +31,6 @@ PyCrypto are a couple gotchas associated with installing PyCrypto: its compatibility with Python's package tools, and the fact that it is a C-based extension. -.. _pycrypto-and-pip: - -Possible gotcha on older Python and/or pip versions ---------------------------------------------------- - -We strongly recommend using ``pip`` to as it is newer and generally better than -``easy_install``. However, a combination of bugs in specific (now rather old) -versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto. -Specifically: - -* Python = 2.5.x -* PyCrypto >= 2.1 (required for most modern versions of Paramiko) -* ``pip`` < 0.8.1 - -When all three criteria are met, you may encounter ``No such file or -directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install -PyCrypto``. - -The fix is to make sure at least one of the above criteria is not met, by doing -the following (in order of preference): - -* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``. -* Upgrade to Python 2.6 or above. -* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1, - and install PyCrypto 2.0.1 (the oldest version on PyPI which works with - Paramiko 1.7.6/1.7.7) - - C extension ----------- -- cgit v1.2.3 From 4742f4c1c130d436282e419756030a1f3f710774 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 13 Mar 2014 21:06:14 -0700 Subject: Update copyright in README to be more accurate --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 77866b8f..005fd6f3 100644 --- a/README +++ b/README @@ -5,7 +5,7 @@ paramiko :Paramiko: Python SSH module :Copyright: Copyright (c) 2003-2009 Robey Pointer -:Copyright: Copyright (c) 2014 Jeff Forcier +:Copyright: Copyright (c) 2013-2014 Jeff Forcier :License: LGPL :Homepage: https://github.com/paramiko/paramiko/ :API docs: http://docs.paramiko.org -- cgit v1.2.3 From d99342d119fcf268c57d025fa9a9fd1124f39de0 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 13 Mar 2014 21:06:57 -0700 Subject: README py3 update --- README | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README b/README index 005fd6f3..9305bfb3 100644 --- a/README +++ b/README @@ -34,7 +34,8 @@ that should have come with this archive. Requirements ------------ - - python 2.6 or better + - Python 2.6 or better - this includes Python + 3.3 and higher as well. - pycrypto 2.1 or better - ecdsa 0.9 or better -- cgit v1.2.3 From 5407b1a27468d5abedde93b51aad5bd97bd046c4 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Mon, 17 Mar 2014 13:50:13 +0100 Subject: Add a flag to check if GSS-API / SSPI is available. --- paramiko/__init__.py | 2 +- paramiko/ssh_gss.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 51329271..8304cf6d 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -65,7 +65,7 @@ __license__ = "GNU Lesser General Public License (LGPL)" from paramiko.transport import SecurityOptions, Transport from paramiko.client import SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, WarningPolicy from paramiko.auth_handler import AuthHandler -from paramiko.ssh_gss import GSSAuth +from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE from paramiko.channel import Channel, ChannelFile from paramiko.ssh_exception import SSHException, PasswordRequiredException, \ BadAuthenticationType, ChannelException, BadHostKeyException, \ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 417dab6e..d4b897a7 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -45,10 +45,19 @@ Created on 07.11.2013 import struct import os import sys + +''' +@var GSS_AUTH_AVAILABLE: Constraint that indicates if GSS-API / SSPI is + Available. +@type GSS_AUTH_AVAILABLE: Boolean +''' +GSS_AUTH_AVAILABLE = True + try: from pyasn1.type.univ import ObjectIdentifier from pyasn1.codec.der import encoder, decoder except ImportError: + GSS_AUTH_AVAILABLE = False class ObjectIdentifier(object): def __init__(self, *args): raise NotImplementedError("Module pyasn1 not importable") @@ -61,7 +70,7 @@ from paramiko.common import MSG_USERAUTH_REQUEST from paramiko.ssh_exception import SSHException """ -@var _API: constraint for the used API +@var _API: Constraint for the used API @type _API: String """ _API = "MIT" @@ -74,6 +83,7 @@ except ImportError: import sspi _API = "SSPI" except ImportError: + GSS_AUTH_AVAILABLE = False _API = None -- cgit v1.2.3 From 6fac5df53598e6dedb806121ae40261d7491a341 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 21 Mar 2014 17:28:19 -0700 Subject: Start an FAQ and answer it with a new install section --- sites/www/faq.rst | 9 +++++++++ sites/www/index.rst | 1 + sites/www/installing.rst | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 sites/www/faq.rst diff --git a/sites/www/faq.rst b/sites/www/faq.rst new file mode 100644 index 00000000..a7e80014 --- /dev/null +++ b/sites/www/faq.rst @@ -0,0 +1,9 @@ +=================================== +Frequently Asked/Answered Questions +=================================== + +Which version should I use? I see multiple active releases. +=========================================================== + +Please see :ref:`the installation docs ` which have an explicit +section about this topic. diff --git a/sites/www/index.rst b/sites/www/index.rst index a8e72624..0864decd 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -13,6 +13,7 @@ usage and API documentation can be found at our code documentation site, .. toctree:: changelog + faq installing contributing contact diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 955a0a59..7d61632d 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -2,6 +2,8 @@ Installing ========== +.. _paramiko-itself: + Paramiko itself =============== @@ -23,6 +25,28 @@ PyCrypto C extension. `ecdsa` is easily installable from wherever you obtained Paramiko's package; PyCrypto may require more work. Read on for details. +.. _release-lines: + +Release lines +------------- + +Users desiring stability may wish to pin themselves to a specific release line +once they first start using Paramiko; to assist in this, we guarantee bugfixes +for at least the last 2-3 releases including the latest stable one. This currently means Paramiko **1.11** through **1.13**. + +If you're unsure which version to install, we have suggestions: + +* **Completely new users** should always default to the **latest stable + release** (as above, whatever is newest / whatever shows up with ``pip + install paramiko``.) +* **Users upgrading from a much older version** (e.g. the 1.7.x line) should + probably get the **oldest actively supported line** (see the paragraph above + this list for what that currently is.) +* **Everybody else** is hopefully already "on" a given version and can + carefully upgrade to whichever version they care to, when their release line + stops being supported. + + PyCrypto ======== -- cgit v1.2.3 From 7feeb272a059ac189de1c2c375baca0916c6fc54 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 21 Mar 2014 17:28:25 -0700 Subject: Uggh how did this slip in --- sites/www/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 7d61632d..74c5c6e8 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -7,7 +7,7 @@ Installing Paramiko itself =============== -The recommended way to get Invoke is to **install the latest stable release** +The recommended way to get Paramiko is to **install the latest stable release** via `pip `_:: $ pip install paramiko -- cgit v1.2.3 From a8110d8006f8ee3e520c2ee4c3ef1133b0fedf15 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 21 Mar 2014 17:32:36 -0700 Subject: Real title too large for sidebar --- sites/www/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/index.rst b/sites/www/index.rst index 0864decd..cb3961ce 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -13,7 +13,7 @@ usage and API documentation can be found at our code documentation site, .. toctree:: changelog - faq + FAQs installing contributing contact -- cgit v1.2.3 From bbb8e8ca4a48d18a7a366513fc202ea006e752df Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 22 Mar 2014 18:17:44 -0700 Subject: Fixes #275 -- upload wheels as a part of the release process Requires teh latest version of invocations from git --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index c9844780..38282b8e 100644 --- a/tasks.py +++ b/tasks.py @@ -43,7 +43,7 @@ def release(ctx): rmtree(target, ignore_errors=True) copytree(docs_build, target) # Publish - publish(ctx) + publish(ctx, wheel=True) ns = Collection(test, coverage, release, docs=docs, www=www) -- cgit v1.2.3 From 9c2640fb9f4fcd700f10fde6f0efdf9bab1cbe53 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Wed, 26 Mar 2014 11:43:33 +0100 Subject: Fix Git conflicts --- paramiko/ber.py | 5 ----- paramiko/py3compat.py | 4 ---- 2 files changed, 9 deletions(-) diff --git a/paramiko/ber.py b/paramiko/ber.py index af4e2138..05152303 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -128,11 +128,6 @@ class BER(object): def encode_sequence(data): ber = BER() for item in data: -<<<<<<< HEAD - b.encode(item) - return b.asbytes() -======= ber.encode(item) return ber.asbytes() ->>>>>>> master encode_sequence = staticmethod(encode_sequence) diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index 2fae079b..8842b988 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -113,13 +113,9 @@ else: return s def byte_ord(c): -<<<<<<< HEAD - assert isinstance(c, int) -======= # In case we're handed a string instead of an int. if not isinstance(c, int): c = ord(c) ->>>>>>> master return c def byte_chr(c): -- cgit v1.2.3 From c8391cb90e02cf8c4c769f3c523ad9f6767ca92c Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Wed, 26 Mar 2014 13:47:29 +0100 Subject: Adapt GSS-API implementation to the merged code. --- paramiko/auth_handler.py | 8 +++++++- paramiko/common.py | 6 ++++++ paramiko/kex_gss.py | 1 + paramiko/ssh_gss.py | 10 +++++++--- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index a048e9b3..9c431c45 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -28,7 +28,13 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \ cMSG_USERAUTH_INFO_REQUEST, WARNING, AUTH_FAILED, cMSG_USERAUTH_PK_OK, \ cMSG_USERAUTH_INFO_RESPONSE, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT, \ MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS, MSG_USERAUTH_FAILURE, \ - MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE + MSG_USERAUTH_BANNER, MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE, \ + cMSG_USERAUTH_GSSAPI_RESPONSE, cMSG_USERAUTH_GSSAPI_TOKEN, \ + cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, cMSG_USERAUTH_GSSAPI_ERROR, \ + cMSG_USERAUTH_GSSAPI_ERRTOK, cMSG_USERAUTH_GSSAPI_MIC,\ + MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, \ + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR, \ + MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC from paramiko.message import Message from paramiko.py3compat import bytestring diff --git a/paramiko/common.py b/paramiko/common.py index ed8d9a09..3da43cf2 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -53,6 +53,12 @@ cMSG_USERAUTH_BANNER = byte_chr(MSG_USERAUTH_BANNER) cMSG_USERAUTH_PK_OK = byte_chr(MSG_USERAUTH_PK_OK) cMSG_USERAUTH_INFO_REQUEST = byte_chr(MSG_USERAUTH_INFO_REQUEST) cMSG_USERAUTH_INFO_RESPONSE = byte_chr(MSG_USERAUTH_INFO_RESPONSE) +cMSG_USERAUTH_GSSAPI_RESPONSE = byte_chr(MSG_USERAUTH_GSSAPI_RESPONSE) +cMSG_USERAUTH_GSSAPI_TOKEN = byte_chr(MSG_USERAUTH_GSSAPI_TOKEN) +cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE = byte_chr(MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE) +cMSG_USERAUTH_GSSAPI_ERROR = byte_chr(MSG_USERAUTH_GSSAPI_ERROR) +cMSG_USERAUTH_GSSAPI_ERRTOK = byte_chr(MSG_USERAUTH_GSSAPI_ERRTOK) +cMSG_USERAUTH_GSSAPI_MIC = byte_chr(MSG_USERAUTH_GSSAPI_MIC) cMSG_GLOBAL_REQUEST = byte_chr(MSG_GLOBAL_REQUEST) cMSG_REQUEST_SUCCESS = byte_chr(MSG_REQUEST_SUCCESS) cMSG_REQUEST_FAILURE = byte_chr(MSG_REQUEST_FAILURE) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 1fb7aea6..1a810b8b 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -48,6 +48,7 @@ from Crypto.Hash import SHA from paramiko.common import * from paramiko import util from paramiko.message import Message +from paramiko.py3compat import byte_chr, long, byte_mask, byte_ord from paramiko.ssh_exception import SSHException diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index d4b897a7..35d654af 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -66,6 +66,10 @@ except ImportError: def decode(self): raise NotImplementedError("Module pyasn1 not importable") + class encoder(object): + def encode(self): + raise NotImplementedError("Module pyasn1 not importable") + from paramiko.common import MSG_USERAUTH_REQUEST from paramiko.ssh_exception import SSHException @@ -251,11 +255,11 @@ class _SSH_GSSAuth(object): mic += session_id mic += struct.pack('B', MSG_USERAUTH_REQUEST) mic += self._make_uint32(len(username)) - mic += str.encode(username) + mic += username.encode() mic += self._make_uint32(len(service)) - mic += str.encode(service) + mic += service.encode() mic += self._make_uint32(len(auth_method)) - mic += str.encode(auth_method) + mic += auth_method.encode() return mic -- cgit v1.2.3 From a935505b0ad0664f74d07433db40449592a86bf6 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Thu, 27 Mar 2014 11:14:54 +0100 Subject: Change GSS-API epydoc docstrings to Sphinx --- paramiko/auth_handler.py | 6 +- paramiko/kex_gss.py | 100 +++++++-------- paramiko/server.py | 80 ++++++------ paramiko/ssh_gss.py | 295 +++++++++++++++++++-------------------------- paramiko/transport.py | 43 +++---- sites/docs/api/kex_gss.rst | 5 + sites/docs/api/ssh_gss.rst | 14 +++ sites/docs/index.rst | 2 + 8 files changed, 248 insertions(+), 297 deletions(-) create mode 100644 sites/docs/api/kex_gss.rst create mode 100644 sites/docs/api/ssh_gss.rst diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 9c431c45..8532d1f9 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -295,7 +295,7 @@ class AuthHandler (object): """ RFC 4462 says we are not required to implement GSS-API error messages. - @see: U{RFC 4462 Section 3.8} + :see: `RFC 4462 Section 3.8 `_ """ raise SSHException("Server returned an error token") elif ptype == MSG_USERAUTH_GSSAPI_ERROR: @@ -481,7 +481,7 @@ class AuthHandler (object): """ RFC 4462 says we are not required to implement GSS-API error messages. - @see: U{RFC 4462 Section 3.8 } + :see: `RFC 4462 Section 3.8 `_ """ while True: m = Message() @@ -524,7 +524,7 @@ class AuthHandler (object): raise if retval == 0: """ - @todo: Implement client credential saving + :todo: Implement client credential saving The OpenSSH server is able to create a TGT with the delegated client credentials, but this is not supported by GSS-API. """ diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 1a810b8b..02f943ba 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -25,20 +25,20 @@ This module provides GSS-API / SSPI Key Exchange for Paramiko as defined in RFC 4462 with the following restrictions: Credential delegation is not supported in server mode, To Use this module, you need the following additional python packages: -U{pyasn1 >= 0.1.7 }, -U{python-gssapi >= 0.4.0 (Unix) }, -U{pywin32 2.1.8 (Windows) }. - -@summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module -@version: 0.1 -@author: Sebastian Deiss -@contact: U{https://github.com/SebastianDeiss/paramiko/issues} -@organization: science + computing ag - (U{EMail}) -@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag - } -@license: GNU Lesser General Public License (LGPL) -@see: L{ssh_gss} +`pyasn1 >= 0.1.7 `_, +`python-gssapi >= 0.4.0 (Unix) `_, +`pywin32 2.1.8 (Windows) `_. + +:summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module +:version: 0.1 +:author: Sebastian Deiss +:contact: https://github.com/SebastianDeiss/paramiko/issues +:organization: science + computing ag + `EMail `_ +:copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 `science + computing ag + `_ +:license: GNU Lesser General Public License (LGPL) +:see: `.ssh_gss` Created on 12.12.2013 """ @@ -63,13 +63,13 @@ c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)] class KexGSSGroup1(object): """ GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange - as defined in U{RFC 4462 Section 2 } + as defined in `RFC 4462 Section 2 `_ - @note: RFC 4462 says we are not required to implement GSS-API error + :note: RFC 4462 says we are not required to implement GSS-API error messages. If an error occurs an exception will be thrown and the connection will be terminated. - @see: U{RFC 4462 Section 2.2 } + :see: `RFC 4462 Section 2.2 `_ """ # draft-ietf-secsh-transport-09.txt, page 17 P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF @@ -115,10 +115,8 @@ class KexGSSGroup1(object): """ Parse the next packet. - @param ptype: The type of the incomming packet - @type ptype: Char - @param m: The paket content - @type m: L{Message} + :param char ptype: The type of the incomming packet + :param `.Message` m: The paket content """ if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT): return self._parse_kexgss_init(m) @@ -155,8 +153,7 @@ class KexGSSGroup1(object): """ Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message - @type m: L{Message} + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message """ # client mode host_key = m.get_string() @@ -170,8 +167,7 @@ class KexGSSGroup1(object): """ Parse the SSH2_MSG_KEXGSS_CONTINUE message. - @param m: The content of the SSH2_MSG_KEXGSS_CONTINUE message - @type m: L{Message} + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message """ if not self.transport.server_mode: srv_token = m.get_string() @@ -190,8 +186,7 @@ class KexGSSGroup1(object): """ Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_COMPLETE message - @type m: L{Message} + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message """ # client mode if self.transport.host_key is None: @@ -232,8 +227,7 @@ class KexGSSGroup1(object): """ Parse the SSH2_MSG_KEXGSS_INIT message (server mode). - @param m: The content of the SSH2_MSG_KEXGSS_INIT message - @type m: L{Message} + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_INIT message """ # server mode client_token = m.get_string() @@ -283,9 +277,8 @@ class KexGSSGroup1(object): The server may send a GSS-API error message. if it does, we display the error by throwing an exception (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_ERROR message - @type m: L{Message} - @raise SSHException: Contains GSS-API major and minor status as well as + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message + :raise SSHException: Contains GSS-API major and minor status as well as the error message and the language tag of the message """ @@ -302,13 +295,13 @@ class KexGSSGroup1(object): class KexGSSGroup14(KexGSSGroup1): """ GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange - as defined in U{RFC 4462 Section 2 } + as defined in `RFC 4462 Section 2 `_ - @note: RFC 4462 says we are not required to implement GSS-API error + :note: RFC 4462 says we are not required to implement GSS-API error messages. If an error occurs an exception will be thrown and the connection will be terminated. - @see: U{RFC 4462 Section 2.2 } + :see: `RFC 4462 Section 2.2 `_ """ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF G = 2 @@ -318,13 +311,13 @@ class KexGSSGroup14(KexGSSGroup1): class KexGSSGex(object): """ GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange - as defined in U{RFC 4462 Section 2 } + as defined in `RFC 4462 Section 2 `_ - @note: RFC 4462 says we are not required to implement GSS-API error + :note: RFC 4462 says we are not required to implement GSS-API error messages. If an error occurs an exception will be thrown and the connection will be terminated. - @see: U{RFC 4462 Section 2.2 } + :see: `RFC 4462 Section 2.2 `_ """ NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" min_bits = 1024 @@ -367,10 +360,8 @@ class KexGSSGex(object): """ Parse the next packet. - @param ptype: The type of the incomming packet - @type ptype: Char - @param m: The paket content - @type m: L{Message} + :param char ptype: The type of the incomming packet + :param `.Message` m: The paket content """ if ptype == MSG_KEXGSS_GROUPREQ: return self._parse_kexgss_groupreq(m) @@ -412,8 +403,7 @@ class KexGSSGex(object): """ Parse the SSH2_MSG_KEXGSS_GROUPREQ message (server mode). - @param m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message - @type m: L{Message} + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_GROUPREQ message """ minbits = m.get_int() preferredbits = m.get_int() @@ -451,8 +441,7 @@ class KexGSSGex(object): """ Parse the SSH2_MSG_KEXGSS_GROUP message (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_GROUP message - @type m: L{Message} + :param `Message` m: The content of the SSH2_MSG_KEXGSS_GROUP message """ self.p = m.get_mpint() self.g = m.get_mpint() @@ -478,8 +467,7 @@ class KexGSSGex(object): """ Parse the SSH2_MSG_KEXGSS_INIT message (server mode). - @param m: The content of the SSH2_MSG_KEXGSS_INIT message - @type m: L{Message} + :param `Message` m: The content of the SSH2_MSG_KEXGSS_INIT message """ client_token = m.get_string() self.e = m.get_mpint() @@ -533,8 +521,7 @@ class KexGSSGex(object): """ Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message - @type m: L{Message} + :param `Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message """ # client mode host_key = m.get_string() @@ -548,8 +535,7 @@ class KexGSSGex(object): """ Parse the SSH2_MSG_KEXGSS_CONTINUE message. - @param m: The content of the SSH2_MSG_KEXGSS_CONTINUE message - @type m: L{Message} + :param `Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message """ if not self.transport.server_mode: srv_token = m.get_string() @@ -568,8 +554,7 @@ class KexGSSGex(object): """ Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_COMPLETE message - @type m: L{Message} + :param `Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message """ if self.transport.host_key is None: self.transport.host_key = NullHostKey() @@ -619,9 +604,8 @@ class KexGSSGex(object): The server may send a GSS-API error message. if it does, we display the error by throwing an exception (client mode). - @param m: The content of the SSH2_MSG_KEXGSS_ERROR message - @type m: L{Message} - @raise SSHException: Contains GSS-API major and minor status as well as + :param `Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message + :raise SSHException: Contains GSS-API major and minor status as well as the error message and the language tag of the message """ @@ -638,7 +622,7 @@ class KexGSSGex(object): class NullHostKey(object): """ This class represents the Null Host Key for GSS-API Key Exchange - as defined in U{RFC 4462 Section 5 } + as defined in `RFC 4462 Section 5 `_ """ def __init__(self): self.key = "" diff --git a/paramiko/server.py b/paramiko/server.py index 25c39063..cf396b15 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -237,26 +237,23 @@ class ServerInterface (object): Authenticate the given user to the server if he is a valid krb5 principal. - @param username: The username of the authenticating client - @type username: String - @param gss_authenticated: The result of the krb5 authentication - @type gss_authenticated: Integer - @param cc_filename: The krb5 client credentials cache filename - @type cc_filename: String - @return: L{AUTH_FAILED} if the user is not authenticated otherwise - L{AUTH_SUCCESSFUL} - @rtype: Integer - @note: Kerberos credential delegation is not supported. - @see: L{ssh_gss} - @note: We are just checking in L{AuthHandler} that the given user is - a valid krb5 principal! - We don't check if the krb5 principal is allowed to log in on - the server, because there is no way to do that in python. So - if you develop your own SSH server with paramiko for a cetain - plattform like Linux, you should call C{krb5_kuserok()} in your - local kerberos library to make sure that the krb5_principal has - an account on the server and is allowed to log in as a user. - @see: U{http://www.unix.com/man-page/all/3/krb5_kuserok/} + :param str username: The username of the authenticating client + :param int gss_authenticated: The result of the krb5 authentication + :param str cc_filename: The krb5 client credentials cache filename + :return: `.AUTH_FAILED` if the user is not authenticated otherwise + `.AUTH_SUCCESSFUL` + :rtype: int + :note: Kerberos credential delegation is not supported. + :see: `.ssh_gss` + :note: : We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + plattform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/` """ if gss_authenticated == AUTH_SUCCESSFUL: return AUTH_SUCCESSFUL @@ -271,26 +268,23 @@ class ServerInterface (object): If GSS-API Key Exchange was not performed, this authentication method won't be available. - @param username: The username of the authenticating client - @type username: String - @param gss_authenticated: The result of the krb5 authentication - @type gss_authenticated: Integer - @param cc_filename: The krb5 client credentials cache filename - @type cc_filename: String - @return: L{AUTH_FAILED} if the user is not authenticated otherwise - L{AUTH_SUCCESSFUL} - @rtype: Integer - @note: Kerberos credential delegation is not supported. - @see: L{ssh_gss}, L{kex_gss} - @note: We are just checking in L{AuthHandler} that the given user is - a valid krb5 principal! - We don't check if the krb5 principal is allowed to log in on - the server, because there is no way to do that in python. So - if you develop your own SSH server with paramiko for a certain - platform like Linux, you should call C{krb5_kuserok()} in your - local kerberos library to make sure that the krb5_principal has - an account on the server and is allowed to log in as a user. - @see: U{http://www.unix.com/man-page/all/3/krb5_kuserok/} + :param str username: The username of the authenticating client + :param int gss_authenticated: The result of the krb5 authentication + :param str cc_filename: The krb5 client credentials cache filename + :return: `.AUTH_FAILED` if the user is not authenticated otherwise + `.AUTH_SUCCESSFUL` + :rtype: int + :note: Kerberos credential delegation is not supported. + :see: `.ssh_gss` `.kex_gss` + :note: : We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + plattform like Linux, you should call C{krb5_kuserok()} in your + local kerberos library to make sure that the krb5_principal has + an account on the server and is allowed to log in as a user. + :see: `http://www.unix.com/man-page/all/3/krb5_kuserok/` """ if gss_authenticated == AUTH_SUCCESSFUL: return AUTH_SUCCESSFUL @@ -302,9 +296,9 @@ class ServerInterface (object): authentication. The default implementation always returns false. - @return: True if GSSAPI authentication is enabled otherwise false - @rtype: Boolean - @see: ssh_gss + :return: True if GSSAPI authentication is enabled otherwise false + :rtype: Boolean + :see: : `.ssh_gss` """ UseGSSAPI = False GSSAPICleanupCredentials = False diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 35d654af..4dfdec11 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -24,20 +24,20 @@ RFC 4462 with the following restrictions: Credential delegation is not supported in server mode, GSS-API key exchange is supported, but not implemented in Paramiko. To Use this module, you need the following additional python packages: -U{pyasn1 >= 0.1.7 }, -U{python-gssapi >= 0.4.0 (Unix) }, -U{pywin32 2.1.8 (Windows) }. - -@summary: SSH2 GSS-API / SSPI authentication module -@version: 0.1 -@author: Sebastian Deiss -@contact: U{https://github.com/SebastianDeiss/paramiko/issues} -@organization: science + computing ag - (U{EMail}) -@copyright: (C) 2013-2014 U{science + computing ag - } -@license: GNU Lesser General Public License (LGPL) -@see: L{kex_gss} +`pyasn1 >= 0.1.7 `_, +`python-gssapi >= 0.4.0 (Unix) `_, +`pywin32 2.1.8 (Windows) `_. + +:summary: SSH2 GSS-API / SSPI authentication module +:version: 0.1 +:author: Sebastian Deiss +:contact: https://github.com/SebastianDeiss/paramiko/issues +:organization: science + computing ag + `EMail `_ +:copyright: (C) 2013-2014 `science + computing ag + `_ +:license: GNU Lesser General Public License (LGPL) +:see: `.kex_gss` Created on 07.11.2013 """ @@ -47,9 +47,8 @@ import os import sys ''' -@var GSS_AUTH_AVAILABLE: Constraint that indicates if GSS-API / SSPI is +:var bool GSS_AUTH_AVAILABLE: Constraint that indicates if GSS-API / SSPI is Available. -@type GSS_AUTH_AVAILABLE: Boolean ''' GSS_AUTH_AVAILABLE = True @@ -74,8 +73,7 @@ from paramiko.common import MSG_USERAUTH_REQUEST from paramiko.ssh_exception import SSHException """ -@var _API: Constraint for the used API -@type _API: String +:var str _API: Constraint for the used API """ _API = "MIT" @@ -95,25 +93,23 @@ def GSSAuth(auth_method, gss_deleg_creds=True): """ Provide SSH2 GSS-API / SSPI authentication for Paramiko. - @param auth_method: The name of the SSH authentication mechanism - (gssapi-with-mic or gss-keyex) - @type auth_method: String - @param gss_deleg_creds: Delegate client credentials or not. - We delegate credentials by default. - @type gss_deleg_creds: Boolean - @return: Either an L{_SSH_GSSAPI} (Unix) object or an - L{_SSH_SSPI} (Windows) object - @rtype: Object - - @raise ImportError: If no GSS-API / SSPI module could be imported. - - @see: U{RFC 4462 } - @note: Check for the available API and return either an L{_SSH_GSSAPI} - (MIT GSSAPI) object or an L{_SSH_SSPI} (MS SSPI) object. If you + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not. + We delegate credentials by default. + :return: Either an `._SSH_GSSAPI` (Unix) object or an + `_SSH_SSPI` (Windows) object + :rtype: Object + + :raise ImportError: If no GSS-API / SSPI module could be imported. + + :see: `RFC 4462 `_ + :note: Check for the available API and return either an `._SSH_GSSAPI` + (MIT GSSAPI) object or an `._SSH_SSPI` (MS SSPI) object. If you get python-gssapi working on Windows, python-gssapi - will be used and a L{_SSH_GSSAPI} object will be returned. + will be used and a `._SSH_GSSAPI` object will be returned. If there is no supported API available, - C{None} will be returned. + ``None`` will be returned. """ if _API == "MIT": return _SSH_GSSAPI(auth_method, gss_deleg_creds) @@ -125,16 +121,14 @@ def GSSAuth(auth_method, gss_deleg_creds=True): class _SSH_GSSAuth(object): """ - Contains the shared variables and methods of L{_SSH_GSSAPI} and - L{_SSH_SSPI}. + Contains the shared variables and methods of `._SSH_GSSAPI` and + `._SSH_SSPI`. """ def __init__(self, auth_method, gss_deleg_creds): """ - @param auth_method: The name of the SSH authentication mechanism - (gssapi-with-mic or gss-keyex) - @type auth_method: String - @param gss_deleg_creds: Delegate client credentials or not - @type gss_deleg_creds: Boolean + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not """ self._auth_method = auth_method self._gss_deleg_creds = gss_deleg_creds @@ -163,9 +157,8 @@ class _SSH_GSSAuth(object): I added this method, because RFC 4462 doesn't specify "ssh-connection" as the only service value. - @param service: The desired SSH service - @type service: String - @rtype: Void + :param str service: The desired SSH service + :rtype: Void """ if service.find("ssh-"): self._service = service @@ -175,9 +168,8 @@ class _SSH_GSSAuth(object): Setter for C{username}. If GSS-API Key Exchange is performed, the username is not set by C{ssh_init_sec_context}. - @param username: The name of the user who attempts to login - @type username: String - @rtype: Void + :param str username: The name of the user who attempts to login + :rtype: Void """ self._username = username @@ -186,14 +178,13 @@ class _SSH_GSSAuth(object): This method returns a single OID, because we only support the Kerberos V5 mechanism. - @param mode: Client for client mode and server for server mode - @param mode: String - @return: A byte sequence containing the number of supported + :param str mode: Client for client mode and server for server mode + :return: A byte sequence containing the number of supported OIDs, the length of the OID and the actual OID encoded with DER - @note: In server mode we just return the OID length and the DER encoded + :rtype: Bytes + :note: In server mode we just return the OID length and the DER encoded OID. - @rtype: Bytes """ OIDs = self._make_uint32(1) krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech)) @@ -206,10 +197,9 @@ class _SSH_GSSAuth(object): """ Check if the given OID is the Kerberos V5 OID (server mode). - @param desired_mech: The desired GSS-API mechanism of the client - @type desired_mech: String - @return: C{True} if the given OID is supported, otherwise C{False} - @rtype: Boolean + :param str desired_mech: The desired GSS-API mechanism of the client + :return: ``True`` if the given OID is supported, otherwise C{False} + :rtype: Boolean """ mech, __ = decoder.decode(desired_mech) if mech.__str__() != self._krb5_mech: @@ -222,10 +212,9 @@ class _SSH_GSSAuth(object): """ Create a 32 bit unsigned integer (The byte sequence of an integer). - @param integer: The integer value to convert - @type integer: Integer - @return: The byte sequence of an 32 bit integer - @rtype: Bytes + :param int integer: The integer value to convert + :return: The byte sequence of an 32 bit integer + :rtype: Bytes """ return struct.pack("!I", integer) @@ -233,15 +222,11 @@ class _SSH_GSSAuth(object): """ Create the SSH2 MIC filed for gssapi-with-mic. - @param session_id: The SSH session ID - @type session_id: String - @param username: The name of the user who attempts to login - @type username: String - @param service: The requested SSH service - @type service: String - @param auth_method: The requested SSH authentication mechanism - @type auth_method: String - @return: The MIC as defined in RFC 4462. The contents of the + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :param str service: The requested SSH service + :param str auth_method: The requested SSH authentication mechanism + :return: The MIC as defined in RFC 4462. The contents of the MIC field are: string session_identifier, byte SSH_MSG_USERAUTH_REQUEST, @@ -249,7 +234,7 @@ class _SSH_GSSAuth(object): string service (ssh-connection), string authentication-method (gssapi-with-mic or gssapi-keyex) - @rtype: Bytes + :rtype: Bytes """ mic = self._make_uint32(len(session_id)) mic += session_id @@ -267,15 +252,13 @@ class _SSH_GSSAPI(_SSH_GSSAuth): """ Implementation of the GSS-API MIT Kerberos Authentication for SSH2. - @see: L{GSSAuth} + :see: `.GSSAuth` """ def __init__(self, auth_method, gss_deleg_creds): """ - @param auth_method: The name of the SSH authentication mechanism - (gssapi-with-mic or gss-keyex) - @type auth_method: String - @param gss_deleg_creds: Delegate client credentials or not - @type gss_deleg_creds: Boolean + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not """ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) @@ -294,21 +277,17 @@ class _SSH_GSSAPI(_SSH_GSSAuth): """ Initialize a GSS-API context. - @param username: The name of the user who attempts to login - @type username: String - @param target: The hostname of the target to connect to - @type target: String - @param desired_mech: The negotiated GSS-API mechanism - ("pseudo negotiated" mechanism, because we - support just the krb5 mechanism :-)) - @type desired_mech: String - @param recv_token: The GSS-API token received from the Server - @type recv_token: String - @raise SSHException: Is raised if the desired mechanism of the client + :param str username: The name of the user who attempts to login + :param str target: The hostname of the target to connect to + :param str desired_mech: The negotiated GSS-API mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param str recv_token: The GSS-API token received from the Server + :raise SSHException: Is raised if the desired mechanism of the client is not supported - @return: A C{String} if the GSS-API has returned a token or C{None} if + :return: A ``String`` if the GSS-API has returned a token or ``None`` if no token was returned - @rtype: String or None + :rtype: String or None """ self._username = username self._gss_host = target @@ -343,18 +322,16 @@ class _SSH_GSSAPI(_SSH_GSSAuth): """ Create the MIC token for a SSH2 message. - @param session_id: The SSH session ID - @type session_id: String - @param gss_kex: Generate the MIC for GSS-API Key Exchange or not - @type gss_kex: Boolean - @return: gssapi-with-mic: + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not + :return: gssapi-with-mic: Returns the MIC token from GSS-API for the message we created - with C{_ssh_build_mic}. + with ``_ssh_build_mic``. gssapi-keyex: Returns the MIC token from GSS-API with the SSH session ID as message. - @rtype: String - @see: L{_ssh_build_mic} + :rtype: String + :see: `._ssh_build_mic` """ self._session_id = session_id if not gss_kex: @@ -372,16 +349,13 @@ class _SSH_GSSAPI(_SSH_GSSAuth): """ Accept a GSS-API context (server mode). - @param hostname: The servers hostname - @type hostname: String - @param username: The name of the user who attempts to login - @type username: String - @param recv_token: The GSS-API Token received from the server, if it's - not the initial call - @type recv_token: String - @return: A C{String} if the GSS-API has returned a token or C{None} if - no token was returned - @rtype: String or None + :param str hostname: The servers hostname + :param str username: The name of the user who attempts to login + :param str recv_token: The GSS-API Token received from the server, + if it's not the initial call. + :return: A ``String`` if the GSS-API has returned a token or ``None`` + if no token was returned + :rtype: String or None """ # hostname and username are not required for GSSAPI, but for SSPI self._gss_host = hostname @@ -396,14 +370,11 @@ class _SSH_GSSAPI(_SSH_GSSAuth): """ Verify the MIC token for a SSH2 message. - @param mic_token: The MIC token received from the client - @type mic_token: String - @param session_id: The SSH session ID - @type session_id: String - @param username: The name of the user who attempts to login - @type username: String - @return: 0 if the MIC check was successful and 1 if it fails - @rtype: Integer + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: 0 if the MIC check was successful and 1 if it fails + :rtype: int """ self._session_id = session_id self._username = username @@ -427,8 +398,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth): """ Checks if credentials are delegated (server mode). - @return: C{True} if credentials are delegated, otherwise C{False} - @rtype: Boolean + :return: ``True`` if credentials are delegated, otherwise ``False`` + :rtype: bool """ if self._gss_srv_ctxt.delegated_cred is not None: return True @@ -440,9 +411,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth): to store the client credentials if credentials are delegated (server mode). - @param client_token: The GSS-API token received form the client - @type client_token: String - @raise NotImplementedError: Credential delegation is currently not + :param str client_token: The GSS-API token received form the client + :raise NotImplementedError: Credential delegation is currently not supported in server mode """ raise NotImplementedError @@ -452,15 +422,13 @@ class _SSH_SSPI(_SSH_GSSAuth): """ Implementation of the Microsoft SSPI Kerberos Authentication for SSH2. - @see: L{GSSAuth} + :see: `.GSSAuth` """ def __init__(self, auth_method, gss_deleg_creds): """ - @param auth_method: The name of the SSH authentication mechanism - (gssapi-with-mic or gss-keyex) - @type auth_method: String - @param gss_deleg_creds: Delegate client credentials or not - @type gss_deleg_creds: Boolean + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not """ _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) @@ -477,21 +445,17 @@ class _SSH_SSPI(_SSH_GSSAuth): """ Initialize a SSPI context. - @param username: The name of the user who attempts to login - @type username: String - @param target: The FQDN of the target to connect to - @type target: String - @param desired_mech: The negotiated SSPI mechanism - ("pseudo negotiated" mechanism, because we - support just the krb5 mechanism :-)) - @type desired_mech: String - @param recv_token: The SSPI token received from the Server - @type recv_token: String - @raise SSHException: Is raised if the desired mechanism of the client + :param str username: The name of the user who attempts to login + :param str target: The FQDN of the target to connect to + :param str desired_mech: The negotiated SSPI mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param recv_token: The SSPI token received from the Server + :raise SSHException: Is raised if the desired mechanism of the client is not supported - @return: A C{String} if the SSPI has returned a token or C{None} if + :return: A ``String`` if the SSPI has returned a token or ``None`` if no token was returned - @rtype: String or None + :rtype: String or None """ self._username = username self._gss_host = target @@ -528,18 +492,16 @@ class _SSH_SSPI(_SSH_GSSAuth): """ Create the MIC token for a SSH2 message. - @param session_id: The SSH session ID - @type session_id: String - @param gss_kex: Generate the MIC for Key Exchange with SSPI or not - @type gss_kex: Boolean - @return: gssapi-with-mic: + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for Key Exchange with SSPI or not + :return: gssapi-with-mic: Returns the MIC token from SSPI for the message we created - with C{_ssh_build_mic}. + with ``_ssh_build_mic``. gssapi-keyex: Returns the MIC token from SSPI with the SSH session ID as message. - @rtype: String - @see: L{_ssh_build_mic} + :rtype: String + :see: `._ssh_build_mic` """ self._session_id = session_id if not gss_kex: @@ -557,16 +519,13 @@ class _SSH_SSPI(_SSH_GSSAuth): """ Accept a SSPI context (server mode). - @param hostname: The servers FQDN - @type hostname: String - @param username: The name of the user who attempts to login - @type username: String - @param recv_token: The SSPI Token received from the server, if it's not - the initial call - @type recv_token: String - @return: A C{String} if the SSPI has returned a token or C{None} if + :param str hostname: The servers FQDN + :param str username: The name of the user who attempts to login + :param str recv_token: The SSPI Token received from the server, + if it's not the initial call. + :return: A ``String`` if the SSPI has returned a token or ``None`` if no token was returned - @rtype: String or None + :rtype: String or None """ self._gss_host = hostname self._username = username @@ -583,14 +542,11 @@ class _SSH_SSPI(_SSH_GSSAuth): """ Verify the MIC token for a SSH2 message. - @param mic_token: The MIC token received from the client - @type mic_token: String - @param session_id: The SSH session ID - @type session_id: String - @param username: The name of the user who attempts to login - @type username: String - @return: 0 if the MIC check was successful - @rtype: Integer + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: 0 if the MIC check was successful + :rtype: int """ self._session_id = session_id self._username = username @@ -620,8 +576,8 @@ class _SSH_SSPI(_SSH_GSSAuth): """ Checks if credentials are delegated (server mode). - @return: C{True} if credentials are delegated, otherwise C{False} - @rtype: Boolean + :return: ``True`` if credentials are delegated, otherwise ``False`` + :rtype: Boolean """ return ( self._gss_flags & sspicon.ISC_REQ_DELEGATE @@ -635,9 +591,8 @@ class _SSH_SSPI(_SSH_GSSAuth): to store the client credentails if credentials are delegated (server mode). - @param client_token: The SSPI token received form the client - @type client_token: String - @raise NotImplementedError: Credential delegation is currently not + :param str client_token: The SSPI token received form the client + :raise NotImplementedError: Credential delegation is currently not supported in server mode """ raise NotImplementedError diff --git a/paramiko/transport.py b/paramiko/transport.py index 393b1d35..86b5e7e4 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -326,10 +326,9 @@ class Transport (threading.Thread): """ Setter for C{gss_host} if GSS-API Key Exchange is performed. - @param gss_host: The targets name in the kerberos database - Default: The name of the host to connect to - @type gss_host: String - @rtype: Void + :param str gss_host: The targets name in the kerberos database + Default: The name of the host to connect to + :rtype: Void """ # We need the FQDN to get this working with SSPI self.gss_host = socket.getfqdn(gss_host) @@ -1226,20 +1225,17 @@ class Transport (threading.Thread): """ Authenticate to the Server using GSS-API / SSPI. - @param username: The username to authenticate as - @type username: String - @param gss_host: The target host - @type gss_host: String - @param gss_deleg_creds: Delegate credentials or not - @type gss_deleg_creds: Boolean - @return: list of auth types permissible for the next stage of + :param str username: The username to authenticate as + :param str gss_host: The target host + :param bool gss_deleg_creds: Delegate credentials or not + :return: list of auth types permissible for the next stage of authentication (normally empty) - @rtype: list - @raise BadAuthenticationType: if gssapi-with-mic isn't + :rtype: list + :raise BadAuthenticationType: if gssapi-with-mic isn't allowed by the server (and no event was passed in) - @raise AuthenticationException: if the authentication failed (and no + :raise AuthenticationException: if the authentication failed (and no event was passed in) - @raise SSHException: if there was a network error + :raise SSHException: if there was a network error """ if (not self.active) or (not self.initial_kex_done): # we should never try to authenticate unless we're on a secure link @@ -1254,16 +1250,17 @@ class Transport (threading.Thread): Authenticate to the Server with GSS-API / SSPI if GSS-API Key Exchange was the used key exchange method. - @param username: The username to authenticate as - @type username: String - @return: list of auth types permissible for the next stage of - authentication (normally empty) - @rtype: list - @raise BadAuthenticationType: if GSS-API Key Exchange was not performed + :param str username: The username to authenticate as + :param str gss_host: The target host + :param bool gss_deleg_creds: Delegate credentials or not + :return: list of auth types permissible for the next stage of + authentication (normally empty) + :rtype: list + :raise BadAuthenticationType: if GSS-API Key Exchange was not performed (and no event was passed in) - @raise AuthenticationException: if the authentication failed (and no + :raise AuthenticationException: if the authentication failed (and no event was passed in) - @raise SSHException: if there was a network error + :raise SSHException: if there was a network error """ if (not self.active) or (not self.initial_kex_done): # we should never try to authenticate unless we're on a secure link diff --git a/sites/docs/api/kex_gss.rst b/sites/docs/api/kex_gss.rst new file mode 100644 index 00000000..a662be01 --- /dev/null +++ b/sites/docs/api/kex_gss.rst @@ -0,0 +1,5 @@ +GSS-API Key Exchange Module +=========================== + +.. automodule:: paramiko.kex_gss + :member-order: bysource diff --git a/sites/docs/api/ssh_gss.rst b/sites/docs/api/ssh_gss.rst new file mode 100644 index 00000000..1b08c7f8 --- /dev/null +++ b/sites/docs/api/ssh_gss.rst @@ -0,0 +1,14 @@ +Paramiko GSS-API Interface +========================== + +.. automodule:: paramiko.ssh_gss + :member-order: bysource + +.. autoclass:: _SSH_GSSAuth + :member-order: bysource + +.. autoclass:: _SSH_GSSAPI + :member-order: bysource + +.. autoclass:: _SSH_SSPI + :member-order: bysource diff --git a/sites/docs/index.rst b/sites/docs/index.rst index f336b393..87265d95 100644 --- a/sites/docs/index.rst +++ b/sites/docs/index.rst @@ -50,6 +50,8 @@ Authentication & keys api/agent api/hostkeys api/keys + api/ssh_gss + api/kex_gss Other primary functions -- cgit v1.2.3 From 5a430def22aa5cbd755f347c8714e4140d6cdcab Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 27 Mar 2014 14:02:03 -0700 Subject: Forgot to explicitly note python 2.5 drop in changelog for py3 --- sites/www/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 02fee80b..4563877d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -15,7 +15,8 @@ Changelog * :release:`1.11.5 <2014-03-13>` * :release:`1.10.7 <2014-03-13>` * :feature:`16` **Python 3 support!** Our test suite passes under Python 3, and - it (& Fabric's test suite) continues to pass under Python 2. + it (& Fabric's test suite) continues to pass under Python 2. **Python 2.5 is + no longer supported with this change!** The merged code was built on many contributors' efforts, both code & feedback. In no particular order, we thank Daniel Goertzen, Ivan Kolodyazhny, -- cgit v1.2.3 From 4d3e0b711a98c440810004cb599a00d0f72978d7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 29 Mar 2014 16:55:01 -0700 Subject: Switched hash functions from PyCrypto to hashlib. There's a few advantages to this: 1) It's probably fast, OpenSSL, which typically backs hashlib, receives far more attention for optimizaitons than PyCrypto. 2) It's the first step to supporting PyPy, where PyCrypto doesn't run. --- paramiko/dsskey.py | 7 ++++--- paramiko/ecdsakey.py | 9 +++++---- paramiko/hostkeys.py | 10 ++++++---- paramiko/kex_gex.py | 6 +++--- paramiko/kex_group1.py | 6 +++--- paramiko/packet.py | 9 ++------- paramiko/pkey.py | 8 ++++---- paramiko/rsakey.py | 7 ++++--- paramiko/sftp_server.py | 18 +++++++++--------- paramiko/transport.py | 22 +++++++++++----------- paramiko/util.py | 9 ++++----- tests/test_packetizer.py | 9 ++++++--- tests/test_pkey.py | 8 +++++--- tests/test_util.py | 5 +++-- 14 files changed, 69 insertions(+), 64 deletions(-) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index c26966e8..04281eab 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -20,8 +20,9 @@ DSS keys. """ +from hashlib import sha1 + from Crypto.PublicKey import DSA -from Crypto.Hash import SHA from paramiko import util from paramiko.common import zero_byte, rng @@ -96,7 +97,7 @@ class DSSKey (PKey): return self.x is not None def sign_ssh_data(self, rng, data): - digest = SHA.new(data).digest() + digest = sha1(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) # generate a suitable k qsize = len(util.deflate_long(self.q, 0)) @@ -130,7 +131,7 @@ class DSSKey (PKey): # pull out (r, s) which are NOT encoded as mpints sigR = util.inflate_long(sig[:20], 1) sigS = util.inflate_long(sig[20:], 1) - sigM = util.inflate_long(SHA.new(data).digest(), 1) + sigM = util.inflate_long(sha1(data).digest(), 1) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6ae2d277..7e2ee7ff 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,11 +21,12 @@ L{ECDSAKey} """ import binascii +from hashlib import sha256 + from ecdsa import SigningKey, VerifyingKey, der, curves -from Crypto.Hash import SHA256 from ecdsa.test_pyecdsa import ECDSA -from paramiko.common import four_byte, one_byte +from paramiko.common import four_byte, one_byte from paramiko.message import Message from paramiko.pkey import PKey from paramiko.py3compat import byte_chr, u @@ -98,7 +99,7 @@ class ECDSAKey (PKey): return self.signing_key is not None def sign_ssh_data(self, rpool, data): - digest = SHA256.new(data).digest() + digest = sha256(data).digest() sig = self.signing_key.sign_digest(digest, entropy=rpool.read, sigencode=self._sigencode) m = Message() @@ -113,7 +114,7 @@ class ECDSAKey (PKey): # verify the signature by SHA'ing the data and encrypting it # using the public key. - hash_obj = SHA256.new(data).digest() + hash_obj = sha256(data).digest() return self.verifying_key.verify_digest(sig, hash_obj, sigdecode=self._sigdecode) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index f32fbeb6..e1e7a182 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -18,7 +18,9 @@ import binascii -from Crypto.Hash import SHA, HMAC +from hashlib import sha1 +from hmac import HMAC + from paramiko.common import rng from paramiko.py3compat import b, u, encodebytes, decodebytes @@ -262,13 +264,13 @@ class HostKeys (MutableMapping): :return: the hashed hostname as a `str` """ if salt is None: - salt = rng.read(SHA.digest_size) + salt = rng.read(sha1().digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] salt = decodebytes(b(salt)) - assert len(salt) == SHA.digest_size - hmac = HMAC.HMAC(salt, b(hostname), SHA).digest() + assert len(salt) == sha1().digest_size + hmac = HMAC(salt, b(hostname), sha1).digest() hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) return hostkey.replace('\n', '') hash_host = staticmethod(hash_host) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 02e507b7..26b22435 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -22,7 +22,7 @@ generator "g" are provided by the server. A bit more work is required on the client side, and a B{lot} more on the server side. """ -from Crypto.Hash import SHA +from hashlib import sha1 from paramiko import util from paramiko.common import DEBUG @@ -203,7 +203,7 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) @@ -238,6 +238,6 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) + self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 3dfb7f18..f792eff0 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -21,7 +21,7 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of 1024 bit key halves, using a known "p" prime and "g" generator. """ -from Crypto.Hash import SHA +from hashlib import sha1 from paramiko import util from paramiko.common import max_byte, zero_byte @@ -105,7 +105,7 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(hm.asbytes()).digest()) + self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() @@ -124,7 +124,7 @@ class KexGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) diff --git a/paramiko/packet.py b/paramiko/packet.py index 0f51df5e..5cffe950 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -25,6 +25,7 @@ import socket import struct import threading import time +from hmac import HMAC from paramiko import util from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \ @@ -34,12 +35,6 @@ from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message -try: - from r_hmac import HMAC -except ImportError: - from Crypto.Hash.HMAC import HMAC - - def compute_hmac(key, message, digest_class): return HMAC(key, message, digest_class).digest() @@ -359,7 +354,7 @@ class Packetizer (object): raise SSHException('Mismatched MAC') padding = byte_ord(packet[0]) payload = packet[1:packet_size - padding] - + if self.__dump_packets: self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c8f84e0a..7d5da409 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -23,8 +23,8 @@ Common API for all public keys. import base64 from binascii import hexlify, unhexlify import os +from hashlib import md5 -from Crypto.Hash import MD5 from Crypto.Cipher import DES3, AES from paramiko import util @@ -126,7 +126,7 @@ class PKey (object): a 16-byte `string ` (binary) of the MD5 fingerprint, in SSH format. """ - return MD5.new(self.asbytes()).digest() + return md5(self.asbytes()).digest() def get_base64(self): """ @@ -300,7 +300,7 @@ class PKey (object): keysize = self._CIPHER_TABLE[encryption_type]['keysize'] mode = self._CIPHER_TABLE[encryption_type]['mode'] salt = unhexlify(b(saltstr)) - key = util.generate_key_bytes(MD5, salt, password, keysize) + key = util.generate_key_bytes(md5, salt, password, keysize) return cipher.new(key, mode, salt).decrypt(data) def _write_private_key_file(self, tag, filename, data, password=None): @@ -332,7 +332,7 @@ class PKey (object): blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] salt = rng.read(16) - key = util.generate_key_bytes(MD5, salt, password, keysize) + key = util.generate_key_bytes(md5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize #data += rng.read(n) diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index c93f3218..fc09b5cb 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -20,8 +20,9 @@ RSA keys. """ +from hashlib import sha1 + from Crypto.PublicKey import RSA -from Crypto.Hash import SHA from paramiko import util from paramiko.common import rng, max_byte, zero_byte, one_byte @@ -91,7 +92,7 @@ class RSAKey (PKey): return self.d is not None def sign_ssh_data(self, rpool, data): - digest = SHA.new(data).digest() + digest = sha1(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) m = Message() @@ -106,7 +107,7 @@ class RSAKey (PKey): # verify the signature by SHA'ing the data and encrypting it using the # public key. some wackiness ensues where we "pkcs1imify" the 20-byte # hash into a string as long as the RSA key. - hash_obj = util.inflate_long(self._pkcs1imify(SHA.new(data).digest()), True) + hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash_obj, (sig,)) diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index dadfd026..2d8d1909 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -22,9 +22,9 @@ Server-mode SFTP support. import os import errno - -from Crypto.Hash import MD5, SHA import sys +from hashlib import md5, sha1 + from paramiko import util from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \ SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE @@ -45,8 +45,8 @@ from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME, CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED _hash_class = { - 'sha1': SHA, - 'md5': MD5, + 'sha1': sha1, + 'md5': md5, } @@ -82,14 +82,14 @@ class SFTPServer (BaseSFTP, SubsystemHandler): self.file_table = {} self.folder_table = {} self.server = sftp_si(server, *largs, **kwargs) - + def _log(self, level, msg): if issubclass(type(msg), list): for m in msg: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m) else: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg) - + def start_subsystem(self, name, transport, channel): self.sock = channel self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel)) @@ -157,7 +157,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): This is meant to be a handy helper function for translating SFTP file requests into local file operations. - + :param str filename: name of the file to alter (should usually be an absolute path). :param .SFTPAttributes attr: attributes to change. @@ -281,7 +281,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): # don't try to read more than about 64KB at a time chunklen = min(blocklen, 65536) count = 0 - hash_obj = alg.new() + hash_obj = alg() while count < blocklen: data = f.read(offset, chunklen) if not isinstance(data, bytes_types): @@ -298,7 +298,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): msg.add_string(algname) msg.add_bytes(sum_out) self._send_packet(CMD_EXTENDED_REPLY, msg) - + def _convert_pflags(self, pflags): """convert SFTP-style open() flags to Python's os.open() flags""" if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): diff --git a/paramiko/transport.py b/paramiko/transport.py index 1471b543..437cd278 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -25,6 +25,7 @@ import sys import threading import time import weakref +from hashlib import md5, sha1 import paramiko from paramiko import util @@ -59,7 +60,6 @@ from paramiko.util import retry_on_signal from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 -from Crypto.Hash import SHA, MD5 try: from Crypto.Util import Counter except ImportError: @@ -107,10 +107,10 @@ class Transport (threading.Thread): } _mac_info = { - 'hmac-sha1': {'class': SHA, 'size': 20}, - 'hmac-sha1-96': {'class': SHA, 'size': 12}, - 'hmac-md5': {'class': MD5, 'size': 16}, - 'hmac-md5-96': {'class': MD5, 'size': 12}, + 'hmac-sha1': {'class': sha1, 'size': 20}, + 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-md5': {'class': md5, 'size': 16}, + 'hmac-md5-96': {'class': md5, 'size': 12}, } _key_info = { @@ -1338,13 +1338,13 @@ class Transport (threading.Thread): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = SHA.new(m.asbytes()).digest() + out = sofar = sha1(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = SHA.new(m.asbytes()).digest() + digest = sha1(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1719,9 +1719,9 @@ class Transport (threading.Thread): # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) else: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) self.packetizer.set_inbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) compress_in = self._compression_info[self.remote_compression][1] if (compress_in is not None) and ((self.remote_compression != 'zlib@openssh.com') or self.authenticated): @@ -1746,9 +1746,9 @@ class Transport (threading.Thread): # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) else: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) sdctr = self.local_cipher.endswith('-ctr') self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key, sdctr) compress_out = self._compression_info[self.local_compression][0] diff --git a/paramiko/util.py b/paramiko/util.py index dbcbbae4..f4ee3adc 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -143,15 +143,14 @@ def tb_strings(): return ''.join(traceback.format_exception(*sys.exc_info())).split('\n') -def generate_key_bytes(hashclass, salt, key, nbytes): +def generate_key_bytes(hash_alg, salt, key, nbytes): """ Given a password, passphrase, or other human-source key, scramble it through a secure hash into some keyworthy bytes. This specific algorithm is used for encrypting/decrypting private key files. - :param class hashclass: - class from `Crypto.Hash` that can be used as a secure hashing function - (like ``MD5`` or ``SHA``). + :param function hash_alg: A function which creates a new hash object, such + as ``hashlib.sha256``. :param salt: data to salt the hash with. :type salt: byte string :param str key: human-entered password or passphrase. @@ -163,7 +162,7 @@ def generate_key_bytes(hashclass, salt, key, nbytes): if len(salt) > 8: salt = salt[:8] while nbytes > 0: - hash_obj = hashclass.new() + hash_obj = hash_alg() if len(digest) > 0: hash_obj.update(digest) hash_obj.update(b(key)) diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py index d4d5544e..a8c0f973 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -21,9 +21,12 @@ Some unit tests for the ssh2 protocol in Transport. """ import unittest +from hashlib import sha1 + from tests.loop import LoopSocket + from Crypto.Cipher import AES -from Crypto.Hash import SHA + from paramiko import Message, Packetizer, util from paramiko.common import byte_chr, zero_byte @@ -41,7 +44,7 @@ class PacketizerTest (unittest.TestCase): p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_outbound_cipher(cipher, 16, SHA, 12, x1f * 20) + p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) # message has to be at least 16 bytes long, so we'll have at least one # block of data encrypted that contains zero random padding bytes @@ -64,7 +67,7 @@ class PacketizerTest (unittest.TestCase): p.set_log(util.get_logger('paramiko.transport')) p.set_hexdump(True) cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) - p.set_inbound_cipher(cipher, 16, SHA, 12, x1f * 20) + p.set_inbound_cipher(cipher, 16, sha1, 12, x1f * 20) wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59') cmd, m = p.read_message() self.assertEqual(100, cmd) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 6ff68fc2..c457f0ee 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -20,11 +20,14 @@ Some unit tests for public/private key objects. """ -from binascii import hexlify import unittest +from binascii import hexlify +from hashlib import md5 + from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes from paramiko.common import rng + from tests.util import test_path # from openssh's ssh-keygen @@ -90,8 +93,7 @@ class KeyTest (unittest.TestCase): pass def test_1_generate_key_bytes(self): - from Crypto.Hash import MD5 - key = util.generate_key_bytes(MD5, x1234, 'happy birthday', 30) + key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30) exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64' self.assertEqual(exp, key) diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..af6eceb5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -23,7 +23,8 @@ Some unit tests for utility functions. from binascii import hexlify import errno import os -from Crypto.Hash import SHA +from hashlib import sha1 + import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config from paramiko.py3compat import StringIO, byte_ord @@ -136,7 +137,7 @@ class UtilTest(ParamikoTest): ) def test_4_generate_key_bytes(self): - x = paramiko.util.generate_key_bytes(SHA, b'ABCDEFGH', 'This is my secret passphrase.', 64) + x = paramiko.util.generate_key_bytes(sha1, b'ABCDEFGH', 'This is my secret passphrase.', 64) hex = ''.join(['%02x' % byte_ord(c) for c in x]) self.assertEqual(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b') -- cgit v1.2.3 From 23528069ec2d14464c8cb5754cca8e039692f255 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 29 Mar 2014 17:17:20 -0700 Subject: Remove unused function --- paramiko/primes.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/paramiko/primes.py b/paramiko/primes.py index 58d158c8..5279b1a5 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -20,31 +20,11 @@ Utility functions for dealing with primes. """ -from Crypto.Util import number - from paramiko import util from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException -def _generate_prime(bits, rng): - """primtive attempt at prime generation""" - hbyte_mask = pow(2, bits % 8) - 1 - while True: - # loop catches the case where we increment n into a higher bit-range - x = rng.read((bits + 7) // 8) - if hbyte_mask > 0: - x = byte_mask(x[0], hbyte_mask) + x[1:] - n = util.inflate_long(x, 1) - n |= 1 - n |= (1 << (bits - 1)) - while not number.isPrime(n): - n += 2 - if util.bit_length(n) == bits: - break - return n - - def _roll_random(rng, n): """returns a random # from 0 to N-1""" bits = util.bit_length(n - 1) -- cgit v1.2.3 From 6f211115f49edcea7d23b764d7cf3a84ff12f5f0 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 29 Mar 2014 19:22:36 -0700 Subject: Switch from using PyCrypto's Random to using os.urandom. There's several reasons for this change: 1) It's faster for reads up to 1024 bytes (nearly 10x faster for 16 byte reads) 2) It receives considerably more security review since it's in the kernel. 3) It's yet another step towards running on PyPy. 4) Using userspace CSPRNGs is considered something of an anti-pattern. See: http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/ http://webcache.googleusercontent.com/search?q=cache:2nTvpCgKZXIJ:www.2uo.de/myths-about-urandom/+&cd=3&hl=en&ct=clnk&gl=us --- paramiko/agent.py | 2 +- paramiko/auth_handler.py | 2 +- paramiko/channel.py | 5 +++-- paramiko/common.py | 5 ----- paramiko/dsskey.py | 16 +++++++++------- paramiko/ecdsakey.py | 11 +++++++---- paramiko/hostkeys.py | 6 ++++-- paramiko/kex_gex.py | 8 +++++--- paramiko/kex_group1.py | 6 ++++-- paramiko/packet.py | 5 +++-- paramiko/pkey.py | 9 ++++----- paramiko/primes.py | 15 ++++++++------- paramiko/rsakey.py | 14 ++++++++------ paramiko/transport.py | 18 ++++++------------ test.py | 6 +++--- tests/test_kex.py | 17 +++++++++-------- tests/test_pkey.py | 13 +++++++------ tests/test_util.py | 6 ------ 18 files changed, 82 insertions(+), 82 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 2b11337f..5a08d452 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -364,7 +364,7 @@ class AgentKey(PKey): def get_name(self): return self.name - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): msg = Message() msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) msg.add_string(self.blob) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index c00ad41c..57babef0 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -206,7 +206,7 @@ class AuthHandler (object): m.add_string(self.private_key.get_name()) m.add_string(self.private_key) blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) - sig = self.private_key.sign_ssh_data(self.transport.rng, blob) + sig = self.private_key.sign_ssh_data(blob) m.add_string(sig) elif self.auth_method == 'keyboard-interactive': m.add_string('') diff --git a/paramiko/channel.py b/paramiko/channel.py index e10ddbac..583809d5 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -21,9 +21,10 @@ Abstraction for an SSH2 channel. """ import binascii +import os +import socket import time import threading -import socket from paramiko import util from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \ @@ -358,7 +359,7 @@ class Channel (object): if auth_protocol is None: auth_protocol = 'MIT-MAGIC-COOKIE-1' if auth_cookie is None: - auth_cookie = binascii.hexlify(self.transport.rng.read(16)) + auth_cookie = binascii.hexlify(os.urandom(16)) m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) diff --git a/paramiko/common.py b/paramiko/common.py index 9a5e2ee1..18298922 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -126,11 +126,6 @@ CONNECTION_FAILED_CODE = { DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 -from Crypto import Random - -# keep a crypto-strong PRNG nearby -rng = Random.new() - zero_byte = byte_chr(0) one_byte = byte_chr(1) four_byte = byte_chr(4) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index c26966e8..446353a0 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -20,11 +20,13 @@ DSS keys. """ +import os + from Crypto.PublicKey import DSA from Crypto.Hash import SHA from paramiko import util -from paramiko.common import zero_byte, rng +from paramiko.common import zero_byte from paramiko.py3compat import long from paramiko.ssh_exception import SSHException from paramiko.message import Message @@ -91,17 +93,17 @@ class DSSKey (PKey): def get_bits(self): return self.size - + def can_sign(self): return self.x is not None - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): digest = SHA.new(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) # generate a suitable k qsize = len(util.deflate_long(self.q, 0)) while True: - k = util.inflate_long(rng.read(qsize), 1) + k = util.inflate_long(os.urandom(qsize), 1) if (k > 2) and (k < self.q): break r, s = dss.sign(util.inflate_long(digest, 1), k) @@ -163,7 +165,7 @@ class DSSKey (PKey): by ``pyCrypto.PublicKey``). :return: new `.DSSKey` private key """ - dsa = DSA.generate(bits, rng.read, progress_func) + dsa = DSA.generate(bits, os.urandom, progress_func) key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x return key @@ -174,11 +176,11 @@ class DSSKey (PKey): def _from_private_key_file(self, filename, password): data = self._read_private_key_file('DSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('DSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6ae2d277..bb5b780d 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,11 +21,14 @@ L{ECDSAKey} """ import binascii +import os + from ecdsa import SigningKey, VerifyingKey, der, curves -from Crypto.Hash import SHA256 from ecdsa.test_pyecdsa import ECDSA -from paramiko.common import four_byte, one_byte +from Crypto.Hash import SHA256 + +from paramiko.common import four_byte, one_byte from paramiko.message import Message from paramiko.pkey import PKey from paramiko.py3compat import byte_chr, u @@ -97,9 +100,9 @@ class ECDSAKey (PKey): def can_sign(self): return self.signing_key is not None - def sign_ssh_data(self, rpool, data): + def sign_ssh_data(self, data): digest = SHA256.new(data).digest() - sig = self.signing_key.sign_digest(digest, entropy=rpool.read, + sig = self.signing_key.sign_digest(digest, entropy=os.urandom, sigencode=self._sigencode) m = Message() m.add_string('ecdsa-sha2-nistp256') diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index f32fbeb6..743165c7 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -18,8 +18,10 @@ import binascii +import os + from Crypto.Hash import SHA, HMAC -from paramiko.common import rng + from paramiko.py3compat import b, u, encodebytes, decodebytes try: @@ -262,7 +264,7 @@ class HostKeys (MutableMapping): :return: the hashed hostname as a `str` """ if salt is None: - salt = rng.read(SHA.digest_size) + salt = os.urandom(SHA.digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 02e507b7..415f58e3 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -22,6 +22,8 @@ generator "g" are provided by the server. A bit more work is required on the client side, and a B{lot} more on the server side. """ +import os + from Crypto.Hash import SHA from paramiko import util @@ -101,7 +103,7 @@ class KexGex (object): qhbyte <<= 1 qmask >>= 1 while True: - x_bytes = self.transport.rng.read(byte_count) + x_bytes = os.urandom(byte_count) x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): @@ -206,7 +208,7 @@ class KexGex (object): H = SHA.new(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_GEX_REPLY) @@ -215,7 +217,7 @@ class KexGex (object): m.add_string(sig) self.transport._send_message(m) self.transport._activate_outbound() - + def _parse_kexdh_gex_reply(self, m): host_key = m.get_string() self.f = m.get_mpint() diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 3dfb7f18..bc88202c 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -21,6 +21,8 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of 1024 bit key halves, using a known "p" prime and "g" generator. """ +import os + from Crypto.Hash import SHA from paramiko import util @@ -82,7 +84,7 @@ class KexGroup1(object): # potential x where the first 63 bits are 1, because some of those will be # larger than q (but this is a tiny tiny subset of potential x). while 1: - x_bytes = self.transport.rng.read(128) + x_bytes = os.urandom(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] if (x_bytes[:8] != b7fffffffffffffff and x_bytes[:8] != b0000000000000000): @@ -127,7 +129,7 @@ class KexGroup1(object): H = SHA.new(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(H) # send reply m = Message() m.add_byte(c_MSG_KEXDH_REPLY) diff --git a/paramiko/packet.py b/paramiko/packet.py index 0f51df5e..0e41b851 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -21,6 +21,7 @@ Packet handling """ import errno +import os import socket import struct import threading @@ -28,7 +29,7 @@ import time from paramiko import util from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \ - DEBUG, xffffffff, zero_byte, rng + DEBUG, xffffffff, zero_byte from paramiko.py3compat import u, byte_ord from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message @@ -455,7 +456,7 @@ class Packetizer (object): # don't waste random bytes for the padding packet += (zero_byte * padding) else: - packet += rng.read(padding) + packet += os.urandom(padding) return packet def _trigger_rekey(self): diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c8f84e0a..1313bdf3 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -28,7 +28,7 @@ from Crypto.Hash import MD5 from Crypto.Cipher import DES3, AES from paramiko import util -from paramiko.common import o600, rng, zero_byte +from paramiko.common import o600, zero_byte from paramiko.py3compat import u, encodebytes, decodebytes, b from paramiko.ssh_exception import SSHException, PasswordRequiredException @@ -138,12 +138,11 @@ class PKey (object): """ return u(encodebytes(self.asbytes())).replace('\n', '') - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): """ Sign a blob of data with this private key, and return a `.Message` representing an SSH signature message. - :param .Crypto.Util.rng.RandomPool rng: a secure random number generator. :param str data: the data to sign. :return: an SSH signature `message <.Message>`. """ @@ -331,11 +330,11 @@ class PKey (object): keysize = self._CIPHER_TABLE[cipher_name]['keysize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = rng.read(16) + salt = os.urandom(16) key = util.generate_key_bytes(MD5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize - #data += rng.read(n) + #data += os.urandom(n) # that would make more sense ^, but it confuses openssh. data += zero_byte * n data = cipher.new(key, mode, salt).encrypt(data) diff --git a/paramiko/primes.py b/paramiko/primes.py index 58d158c8..33cd6510 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -20,6 +20,8 @@ Utility functions for dealing with primes. """ +import os + from Crypto.Util import number from paramiko import util @@ -27,12 +29,12 @@ from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException -def _generate_prime(bits, rng): +def _generate_prime(bits): """primtive attempt at prime generation""" hbyte_mask = pow(2, bits % 8) - 1 while True: # loop catches the case where we increment n into a higher bit-range - x = rng.read((bits + 7) // 8) + x = os.urandom((bits + 7) // 8) if hbyte_mask > 0: x = byte_mask(x[0], hbyte_mask) + x[1:] n = util.inflate_long(x, 1) @@ -45,7 +47,7 @@ def _generate_prime(bits, rng): return n -def _roll_random(rng, n): +def _roll_random(n): """returns a random # from 0 to N-1""" bits = util.bit_length(n - 1) byte_count = (bits + 7) // 8 @@ -58,7 +60,7 @@ def _roll_random(rng, n): # fits, so i can't guarantee that this loop will ever finish, but the odds # of it looping forever should be infinitesimal. while True: - x = rng.read(byte_count) + x = os.urandom(byte_count) if hbyte_mask > 0: x = byte_mask(x[0], hbyte_mask) + x[1:] num = util.inflate_long(x, 1) @@ -73,11 +75,10 @@ class ModulusPack (object): on systems that have such a file. """ - def __init__(self, rpool): + def __init__(self): # pack is a hash of: bits -> [ (generator, modulus) ... ] self.pack = {} self.discarded = [] - self.rng = rpool def _parse_modulus(self, line): timestamp, mod_type, tests, tries, size, generator, modulus = line.split() @@ -147,5 +148,5 @@ class ModulusPack (object): if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize - n = _roll_random(self.rng, len(self.pack[good])) + n = _roll_random(len(self.pack[good])) return self.pack[good][n] diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index c93f3218..a6f97bff 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -20,11 +20,13 @@ RSA keys. """ +import os + from Crypto.PublicKey import RSA from Crypto.Hash import SHA from paramiko import util -from paramiko.common import rng, max_byte, zero_byte, one_byte +from paramiko.common import max_byte, zero_byte, one_byte from paramiko.message import Message from paramiko.ber import BER, BERException from paramiko.pkey import PKey @@ -90,7 +92,7 @@ class RSAKey (PKey): def can_sign(self): return self.d is not None - def sign_ssh_data(self, rpool, data): + def sign_ssh_data(self, data): digest = SHA.new(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), bytes())[0], 0) @@ -125,7 +127,7 @@ class RSAKey (PKey): def write_private_key_file(self, filename, password=None): self._write_private_key_file('RSA', filename, self._encode_key(), password) - + def write_private_key(self, file_obj, password=None): self._write_private_key('RSA', file_obj, self._encode_key(), password) @@ -140,7 +142,7 @@ class RSAKey (PKey): by ``pyCrypto.PublicKey``). :return: new `.RSAKey` private key """ - rsa = RSA.generate(bits, rng.read, progress_func) + rsa = RSA.generate(bits, os.urandom, progress_func) key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p @@ -162,11 +164,11 @@ class RSAKey (PKey): def _from_private_key_file(self, filename, password): data = self._read_private_key_file('RSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('RSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } diff --git a/paramiko/transport.py b/paramiko/transport.py index 1471b543..a0a752ef 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -20,6 +20,7 @@ Core protocol implementation """ +import os import socket import sys import threading @@ -30,7 +31,7 @@ import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler from paramiko.channel import Channel -from paramiko.common import rng, xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ +from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \ cMSG_NEWKEYS, MSG_NEWKEYS, cMSG_REQUEST_SUCCESS, cMSG_REQUEST_FAILURE, \ @@ -57,7 +58,6 @@ from paramiko.ssh_exception import (SSHException, BadAuthenticationType, ChannelException, ProxyCommandFailure) from paramiko.util import retry_on_signal -from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 from Crypto.Hash import SHA, MD5 try: @@ -192,7 +192,6 @@ class Transport (threading.Thread): # okay, normal socket-ish flow here... threading.Thread.__init__(self) self.setDaemon(True) - self.rng = rng self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB try: @@ -339,7 +338,6 @@ class Transport (threading.Thread): # synchronous, wait for a result self.completion_event = event = threading.Event() self.start() - Random.atfork() while True: event.wait(0.1) if not self.active: @@ -475,7 +473,7 @@ class Transport (threading.Thread): .. note:: This has no effect when used in client mode. """ - Transport._modulus_pack = ModulusPack(rng) + Transport._modulus_pack = ModulusPack() # places to look for the openssh "moduli" file file_list = ['/etc/ssh/moduli', '/usr/local/etc/moduli'] if filename is not None: @@ -732,8 +730,8 @@ class Transport (threading.Thread): m = Message() m.add_byte(cMSG_IGNORE) if byte_count is None: - byte_count = (byte_ord(rng.read(1)) % 32) + 10 - m.add_bytes(rng.read(byte_count)) + byte_count = (byte_ord(os.urandom(1)) % 32) + 10 + m.add_bytes(os.urandom(byte_count)) self._send_user_message(m) def renegotiate_keys(self): @@ -1402,10 +1400,6 @@ class Transport (threading.Thread): # interpreter shutdown. self.sys = sys - # Required to prevent RNG errors when running inside many subprocess - # containers. - Random.atfork() - # active=True occurs before the thread is launched, to avoid a race _active_threads.append(self) if self.server_mode: @@ -1590,7 +1584,7 @@ class Transport (threading.Thread): m = Message() m.add_byte(cMSG_KEXINIT) - m.add_bytes(rng.read(16)) + m.add_bytes(os.urandom(16)) m.add_list(self._preferred_kex) m.add_list(available_server_keys) m.add_list(self._preferred_ciphers) diff --git a/test.py b/test.py index bd966d1e..2b3d4ed4 100755 --- a/test.py +++ b/test.py @@ -101,12 +101,12 @@ def main(): parser.add_option('-P', '--sftp-passwd', dest='password', type='string', default=default_passwd, metavar='', help='[with -R] (optional) password to unlock the private key for remote sftp tests') - + options, args = parser.parse_args() - + # setup logging paramiko.util.log_to_file('test.log') - + if options.use_sftp: from tests.test_sftp import SFTPTest if options.use_loopback_sftp: diff --git a/tests/test_kex.py b/tests/test_kex.py index c522be46..56f1b7c7 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -21,7 +21,9 @@ Some unit tests for the key exchange protocols. """ from binascii import hexlify +import os import unittest + import paramiko.util from paramiko.kex_group1 import KexGroup1 from paramiko.kex_gex import KexGex @@ -29,9 +31,8 @@ from paramiko import Message from paramiko.common import byte_chr -class FakeRng (object): - def read(self, n): - return byte_chr(0xcc) * n +def dummy_urandom(n): + return byte_chr(0xcc) * n class FakeKey (object): @@ -41,7 +42,7 @@ class FakeKey (object): def asbytes(self): return b'fake-key' - def sign_ssh_data(self, rng, H): + def sign_ssh_data(self, H): return b'fake-sig' @@ -53,8 +54,7 @@ class FakeModulusPack (object): return self.G, self.P -class FakeTransport (object): - rng = FakeRng() +class FakeTransport(object): local_version = 'SSH-2.0-paramiko_1.0' remote_version = 'SSH-2.0-lame' local_kex_init = 'local-kex-init' @@ -91,10 +91,11 @@ class KexTest (unittest.TestCase): K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 def setUp(self): - pass + self._original_urandom = os.urandom + os.urandom = dummy_urandom def tearDown(self): - pass + os.urandom = self._original_urandom def test_1_group1_client(self): transport = FakeTransport() diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 6ff68fc2..b0ceefe7 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -22,9 +22,10 @@ Some unit tests for public/private key objects. from binascii import hexlify import unittest + from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes -from paramiko.common import rng + from tests.util import test_path # from openssh's ssh-keygen @@ -166,7 +167,7 @@ class KeyTest (unittest.TestCase): def test_8_sign_rsa(self): # verify that the rsa private key can sign and verify key = RSAKey.from_private_key_file(test_path('test_rsa.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ssh-rsa', msg.get_text()) @@ -179,7 +180,7 @@ class KeyTest (unittest.TestCase): def test_9_sign_dss(self): # verify that the dss private key can sign and verify key = DSSKey.from_private_key_file(test_path('test_dss.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ssh-dss', msg.get_text()) @@ -193,13 +194,13 @@ class KeyTest (unittest.TestCase): def test_A_generate_rsa(self): key = RSAKey.generate(1024) - msg = key.sign_ssh_data(rng, b'jerri blank') + msg = key.sign_ssh_data(b'jerri blank') msg.rewind() self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) def test_B_generate_dss(self): key = DSSKey.generate(1024) - msg = key.sign_ssh_data(rng, b'jerri blank') + msg = key.sign_ssh_data(b'jerri blank') msg.rewind() self.assertTrue(key.verify_ssh_sig(b'jerri blank', msg)) @@ -240,7 +241,7 @@ class KeyTest (unittest.TestCase): def test_13_sign_ecdsa(self): # verify that the rsa private key can sign and verify key = ECDSAKey.from_private_key_file(test_path('test_ecdsa.key')) - msg = key.sign_ssh_data(rng, b'ice weasels') + msg = key.sign_ssh_data(b'ice weasels') self.assertTrue(type(msg) is Message) msg.rewind() self.assertEqual('ecdsa-sha2-nistp256', msg.get_text()) diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..d3911f49 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -153,12 +153,6 @@ class UtilTest(ParamikoTest): finally: os.unlink('hostfile.temp') - def test_6_random(self): - from paramiko.common import rng - # just verify that we can pull out 32 bytes and not get an exception. - x = rng.read(32) - self.assertEqual(len(x), 32) - def test_7_host_config_expose_issue_33(self): test_config_file = """ Host www13.* -- cgit v1.2.3 From 77b1aaccc6dcc17108da17dc609c81bdd4e9a0e5 Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Mon, 31 Mar 2014 12:01:32 +1100 Subject: Don't validate points in known_hosts ECDSA keys Point validation is really expensive and apparently unnecessary when we already trust the keys in our known_hosts file. With my current known_hosts file of 657 keys, this reduces the time taken to complete a remote task in fabric from 24 seconds to 7 seconds. This requires python-ecdsa >= 0.11. Closes #270. --- paramiko/ecdsakey.py | 6 ++++-- paramiko/hostkeys.py | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6ae2d277..490373b7 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -38,7 +38,8 @@ class ECDSAKey (PKey): data. """ - def __init__(self, msg=None, data=None, filename=None, password=None, vals=None, file_obj=None): + def __init__(self, msg=None, data=None, filename=None, password=None, + vals=None, file_obj=None, validate_point=True): self.verifying_key = None self.signing_key = None if file_obj is not None: @@ -65,7 +66,8 @@ class ECDSAKey (PKey): raise SSHException('Point compression is being used: %s' % binascii.hexlify(pointinfo)) self.verifying_key = VerifyingKey.from_string(pointinfo[1:], - curve=curves.NIST256p) + curve=curves.NIST256p, + validate_point=validate_point) self.size = 256 def asbytes(self): diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index f32fbeb6..30031fad 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -324,7 +324,7 @@ class HostKeyEntry: elif keytype == 'ssh-dss': key = DSSKey(data=decodebytes(key)) elif keytype == 'ecdsa-sha2-nistp256': - key = ECDSAKey(data=decodebytes(key)) + key = ECDSAKey(data=decodebytes(key), validate_point=False) else: log.info("Unable to handle key of type %s" % (keytype,)) return None diff --git a/setup.py b/setup.py index 2910a7fe..3b0ecdd8 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ try: from setuptools import setup kw = { 'install_requires': ['pycrypto >= 2.1, != 2.4', - 'ecdsa', + 'ecdsa >= 0.11', ], } except ImportError: -- cgit v1.2.3 From 6c6969c1882e62d6249264d2df46ff452eb53e7e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 31 Mar 2014 16:09:45 -0700 Subject: The ecdsa module already defaults to using urandom. --- paramiko/ecdsakey.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index bb5b780d..23bf0fe0 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -102,8 +102,7 @@ class ECDSAKey (PKey): def sign_ssh_data(self, data): digest = SHA256.new(data).digest() - sig = self.signing_key.sign_digest(digest, entropy=os.urandom, - sigencode=self._sigencode) + sig = self.signing_key.sign_digest(digest, sigencode=self._sigencode) m = Message() m.add_string('ecdsa-sha2-nistp256') m.add_string(sig) -- cgit v1.2.3 From fded67e7120d79c619e541ccd3ba19898c194b5b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 1 Apr 2014 08:04:25 -0700 Subject: Use deterministic signatures for ECDSA keys. This is now considered the preffered approach across the board for ECDSA. This is because with the traditional, random "k" parameter for ECDSA, any entropy problems at all, even a single bit, about "k", results in a complete compromise (see https://en.wikipedia.org/wiki/ECDSA#Security). The deterministic algorithm doesn't have this downside. --- paramiko/ecdsakey.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6ae2d277..0d47a3ba 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -21,6 +21,8 @@ L{ECDSAKey} """ import binascii +import hashlib + from ecdsa import SigningKey, VerifyingKey, der, curves from Crypto.Hash import SHA256 from ecdsa.test_pyecdsa import ECDSA @@ -98,9 +100,8 @@ class ECDSAKey (PKey): return self.signing_key is not None def sign_ssh_data(self, rpool, data): - digest = SHA256.new(data).digest() - sig = self.signing_key.sign_digest(digest, entropy=rpool.read, - sigencode=self._sigencode) + sig = self.signing_key.sign_deterministic( + data, sigencode=self._sigencode, hashfunc=hashlib.sha256) m = Message() m.add_string('ecdsa-sha2-nistp256') m.add_string(sig) -- cgit v1.2.3 From 80aff93d3f0040f5886e983a6ce781717f7703a4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 1 Apr 2014 12:36:21 -0700 Subject: Fix broken tag-tree links in changelog --- sites/www/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sites/www/conf.py b/sites/www/conf.py index 1c6c9254..0c8af16c 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -14,7 +14,8 @@ rss_description = 'Paramiko project news' # Releases changelog extension extensions.append('releases') -releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s" +# Paramiko 1.x tags start with 'v'. Meh. +releases_release_uri = "https://github.com/paramiko/paramiko/tree/v%s" releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" # Intersphinx for referencing API/usage docs -- cgit v1.2.3 From b81025e3d2ddb71fd28632b33b3bf9b5cc56c826 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 6 Apr 2014 12:36:50 -0700 Subject: Formatting --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 2910a7fe..4a858326 100644 --- a/setup.py +++ b/setup.py @@ -40,9 +40,10 @@ import sys try: from setuptools import setup kw = { - 'install_requires': ['pycrypto >= 2.1, != 2.4', - 'ecdsa', - ], + 'install_requires': [ + 'pycrypto >= 2.1, != 2.4', + 'ecdsa', + ], } except ImportError: from distutils.core import setup -- cgit v1.2.3 From b85a09673a31719b76b3998270137f0189c226e5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 6 Apr 2014 16:19:03 -0700 Subject: Use newer alabaster w/ showhidden in sidebar TOC Lets us not have 2x TOCs on landing page --- dev-requirements.txt | 2 +- sites/www/index.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 5744f331..91ae8549 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -5,5 +5,5 @@ tox>=1.4,<1.5 invoke>=0.7.0 invocations>=0.5.0 sphinx>=1.1.3 -alabaster>=0.3.1 +alabaster>=0.4.0 releases>=0.5.2 diff --git a/sites/www/index.rst b/sites/www/index.rst index cb3961ce..03189cfa 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -12,6 +12,8 @@ usage and API documentation can be found at our code documentation site, `docs.paramiko.org `_. .. toctree:: + :hidden: + changelog FAQs installing -- cgit v1.2.3 From 1103416d8386e7965bae0d51d596efc4f5a75670 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 6 Apr 2014 16:24:16 -0700 Subject: Put blog into a branch --- sites/www/blog.py | 140 ----------------------------------------- sites/www/blog.rst | 16 ----- sites/www/blog/first-post.rst | 7 --- sites/www/blog/second-post.rst | 7 --- sites/www/conf.py | 6 -- sites/www/index.rst | 9 +-- 6 files changed, 2 insertions(+), 183 deletions(-) delete mode 100644 sites/www/blog.py delete mode 100644 sites/www/blog.rst delete mode 100644 sites/www/blog/first-post.rst delete mode 100644 sites/www/blog/second-post.rst diff --git a/sites/www/blog.py b/sites/www/blog.py deleted file mode 100644 index 3b129ebf..00000000 --- a/sites/www/blog.py +++ /dev/null @@ -1,140 +0,0 @@ -from collections import namedtuple -from datetime import datetime -import time -import email.utils - -from sphinx.util.compat import Directive -from docutils import nodes - - -class BlogDateDirective(Directive): - """ - Used to parse/attach date info to blog post documents. - - No nodes generated, since none are needed. - """ - has_content = True - - def run(self): - # Tag parent document with parsed date value. - self.state.document.blog_date = datetime.strptime( - self.content[0], "%Y-%m-%d" - ) - # Don't actually insert any nodes, we're already done. - return [] - -class blog_post_list(nodes.General, nodes.Element): - pass - -class BlogPostListDirective(Directive): - """ - Simply spits out a 'blog_post_list' temporary node for replacement. - - Gets replaced at doctree-resolved time - only then will all blog post - documents be written out (& their date directives executed). - """ - def run(self): - return [blog_post_list('')] - - -Post = namedtuple('Post', 'name doc title date opener') - -def get_posts(app): - # Obtain blog posts - post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) - posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) - # Obtain common data used for list page & RSS - data = [] - for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True): - # Welp. No "nice" way to get post title. Thanks Sphinx. - title = doc[0][0][0] - # Date. This may or may not end up reflecting the required - # *input* format, but doing it here gives us flexibility. - date = doc.blog_date - # 1st paragraph as opener. TODO: allow a role or something marking - # where to actually pull from? - opener = doc.traverse(nodes.paragraph)[0] - data.append(Post(post, doc, title, date, opener)) - return data - -def replace_blog_post_lists(app, doctree, fromdocname): - """ - Replace blog_post_list nodes with ordered list-o-links to posts. - """ - # Obtain blog posts - post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) - posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) - # Build "list" of links/etc - post_links = [] - for post, doc, title, date, opener in get_posts(app): - # Link itself - uri = app.builder.get_relative_uri(fromdocname, post) - link = nodes.reference('', '', refdocname=post, refuri=uri) - # Title, bolded. TODO: use 'topic' or something maybe? - link.append(nodes.strong('', title)) - date = date.strftime("%Y-%m-%d") - # Meh @ not having great docutils nodes which map to this. - html = '
%s
' % date - timestamp = nodes.raw(text=html, format='html') - # NOTE: may group these within another element later if styling - # necessitates it - group = [timestamp, nodes.paragraph('', '', link), opener] - post_links.extend(group) - - # Replace temp node(s) w/ expanded list-o-links - for node in doctree.traverse(blog_post_list): - node.replace_self(post_links) - -def rss_timestamp(timestamp): - # Use horribly inappropriate module for its magical daylight-savings-aware - # timezone madness. Props to Tinkerer for the idea. - return email.utils.formatdate( - time.mktime(timestamp.timetuple()), - localtime=True - ) - -def generate_rss(app): - # Meh at having to run this subroutine like 3x per build. Not worth trying - # to be clever for now tho. - posts_ = get_posts(app) - # LOL URLs - root = app.config.rss_link - if not root.endswith('/'): - root += '/' - # Oh boy - posts = [ - ( - root + app.builder.get_target_uri(x.name), - x.title, - str(x.opener[0]), # Grab inner text element from paragraph - rss_timestamp(x.date), - ) - for x in posts_ - ] - location = 'blog/rss.xml' - context = { - 'title': app.config.project, - 'link': root, - 'atom': root + location, - 'description': app.config.rss_description, - # 'posts' is sorted by date already - 'date': rss_timestamp(posts_[0].date), - 'posts': posts, - } - yield (location, context, 'rss.xml') - -def setup(app): - # Link in RSS feed back to main website, e.g. 'http://paramiko.org' - app.add_config_value('rss_link', None, '') - # Ditto for RSS description field - app.add_config_value('rss_description', None, '') - # Interprets date metadata in blog post documents - app.add_directive('date', BlogDateDirective) - # Inserts blog post list node (in e.g. a listing page) for replacement - # below - app.add_node(blog_post_list) - app.add_directive('blog-posts', BlogPostListDirective) - # Performs abovementioned replacement - app.connect('doctree-resolved', replace_blog_post_lists) - # Generates RSS page from whole cloth at page generation step - app.connect('html-collect-pages', generate_rss) diff --git a/sites/www/blog.rst b/sites/www/blog.rst deleted file mode 100644 index af9651e4..00000000 --- a/sites/www/blog.rst +++ /dev/null @@ -1,16 +0,0 @@ -==== -Blog -==== - -.. blog-posts directive gets replaced with an ordered list of blog posts. - -.. blog-posts:: - - -.. The following toctree ensures blog posts get processed. - -.. toctree:: - :hidden: - :glob: - - blog/* diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst deleted file mode 100644 index 7b075073..00000000 --- a/sites/www/blog/first-post.rst +++ /dev/null @@ -1,7 +0,0 @@ -=========== -First post! -=========== - -A blog post. - -.. date:: 2013-12-04 diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst deleted file mode 100644 index c4463f33..00000000 --- a/sites/www/blog/second-post.rst +++ /dev/null @@ -1,7 +0,0 @@ -=========== -Another one -=========== - -.. date:: 2013-12-05 - -Indeed! diff --git a/sites/www/conf.py b/sites/www/conf.py index 0c8af16c..5047fa67 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -6,12 +6,6 @@ from os.path import abspath, join, dirname sys.path.append(abspath(join(dirname(__file__), '..'))) from shared_conf import * -# Local blog extension -sys.path.append(abspath('.')) -extensions.append('blog') -rss_link = 'http://paramiko.org' -rss_description = 'Paramiko project news' - # Releases changelog extension extensions.append('releases') # Paramiko 1.x tags start with 'v'. Meh. diff --git a/sites/www/index.rst b/sites/www/index.rst index 03189cfa..77e5fcbb 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -11,6 +11,8 @@ contribution guidelines, development roadmap, news/blog, and so forth. Detailed usage and API documentation can be found at our code documentation site, `docs.paramiko.org `_. +Please see the sidebar to the left to bebin. + .. toctree:: :hidden: @@ -20,13 +22,6 @@ usage and API documentation can be found at our code documentation site, contributing contact -.. Hide blog in hidden toctree for now (to avoid warnings.) - -.. toctree:: - :hidden: - - blog - .. rubric:: Footnotes -- cgit v1.2.3 From 8b9e60f4ce66c913cd7a8371349b07e476e0e6e6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 6 Apr 2014 16:25:02 -0700 Subject: Wow. Just wow. --- sites/www/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/index.rst b/sites/www/index.rst index 77e5fcbb..1b609709 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -11,7 +11,7 @@ contribution guidelines, development roadmap, news/blog, and so forth. Detailed usage and API documentation can be found at our code documentation site, `docs.paramiko.org `_. -Please see the sidebar to the left to bebin. +Please see the sidebar to the left to begin. .. toctree:: :hidden: -- cgit v1.2.3 From 57e647341f416c879ae3841c9a7be50c52a21327 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 6 Apr 2014 18:52:58 -0700 Subject: Nuke Fab-oriented link color override --- sites/shared_conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sites/shared_conf.py b/sites/shared_conf.py index c61ca638..c265fc49 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -14,9 +14,6 @@ html_theme_options = { 'github_repo': 'paramiko', 'gittip_user': 'bitprophet', 'analytics_id': 'UA-18486793-2', - - 'link': '#3782BE', - 'link_hover': '#3782BE', } html_sidebars = { '**': [ -- cgit v1.2.3 From 8e3561b5b123a993b7f19d0b1ce5c5e67e370cbc Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Mon, 7 Apr 2014 10:31:04 +0200 Subject: Don't raise if libgssapi_krb5.so is unavailable This change fixes a paramiko crash in case the package 'gssapi' is installed but can't load libgssapi_krb5.so. --- paramiko/ssh_gss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 4dfdec11..58a64a56 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -79,7 +79,7 @@ _API = "MIT" try: import gssapi -except ImportError: +except (ImportError, OSError): try: import sspicon import sspi -- cgit v1.2.3 From ed4f077b816cc05109bd409d052271291fee26fc Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Mon, 14 Apr 2014 10:40:56 -0400 Subject: Remove python 2.5 from tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 55e3fe64..43c391dd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27,py32,py33 +envlist = py26,py27,py32,py33 [testenv] commands = pip install --use-mirrors -q -r tox-requirements.txt -- cgit v1.2.3 From be7c679942b9b3a1838cce692f87e1c3d45092cf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Apr 2014 10:48:33 -0400 Subject: Errything uses intersphinx to Python --- sites/docs/conf.py | 7 +------ sites/shared_conf.py | 7 ++++++- sites/www/conf.py | 4 +--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/sites/docs/conf.py b/sites/docs/conf.py index f9355715..5674fed1 100644 --- a/sites/docs/conf.py +++ b/sites/docs/conf.py @@ -5,16 +5,11 @@ sys.path.append(os.path.abspath('../..')) from shared_conf import * # Enable autodoc, intersphinx -extensions.extend(['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']) +extensions.extend(['sphinx.ext.autodoc']) # Autodoc settings autodoc_default_flags = ['members', 'special-members'] -# Intersphinx connection to stdlib -intersphinx_mapping = { - 'python': ('http://docs.python.org/2.6', None), -} - # Sister-site links to WWW html_theme_options['extra_nav_links'] = { "Main website": 'http://www.paramiko.org', diff --git a/sites/shared_conf.py b/sites/shared_conf.py index c265fc49..e0afe92e 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -5,7 +5,7 @@ import alabaster # Alabaster theme + mini-extension html_theme_path = [alabaster.get_path()] -extensions = ['alabaster'] +extensions = ['alabaster', 'sphinx.ext.intersphinx'] # Paths relative to invoking conf.py - not this shared file html_theme = 'alabaster' html_theme_options = { @@ -24,6 +24,11 @@ html_sidebars = { ] } +# Everything intersphinx's to Python +intersphinx_mapping = { + 'python': ('http://docs.python.org/2.6', None), +} + # Regular settings project = 'Paramiko' year = datetime.now().year diff --git a/sites/www/conf.py b/sites/www/conf.py index 5047fa67..bdb5929a 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -20,9 +20,7 @@ target = join(dirname(__file__), '..', 'docs', '_build') if os.environ.get('READTHEDOCS') == 'True': # TODO: switch to docs.paramiko.org post go-live of sphinx API docs target = 'http://docs.paramiko.org/en/latest/' -intersphinx_mapping = { - 'docs': (target, None), -} +intersphinx_mapping['docs'] = (target, None) # Sister-site links to API docs html_theme_options['extra_nav_links'] = { -- cgit v1.2.3 From 160e2c08e0b7652a92d879c0e481ce72cddafef7 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Apr 2014 10:48:55 -0400 Subject: Changelog, closes #295 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 4563877d..eff8c2e8 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`295` Swap out a bunch of PyCrypto hash functions with use of + `hashlib` * :support:`290` (also :issue:`292`) Add support for building universal (Python 2+3 compatible) wheel files during the release process. Courtesy of Alex Gaynor. -- cgit v1.2.3 From 1e0e296b05a3e63b33291cfe3d688a435f592c3c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Apr 2014 10:50:12 -0400 Subject: Derp --- sites/www/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index eff8c2e8..5f019bb8 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -3,7 +3,7 @@ Changelog ========= * :support:`295` Swap out a bunch of PyCrypto hash functions with use of - `hashlib` + `hashlib`. Thanks to Alex Gaynor. * :support:`290` (also :issue:`292`) Add support for building universal (Python 2+3 compatible) wheel files during the release process. Courtesy of Alex Gaynor. -- cgit v1.2.3 From 9e2e9812247adbedf0eab84ad5cdd80c458d68f9 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Apr 2014 11:05:25 -0400 Subject: Changelog, closes #297 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5f019bb8..653502ca 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`297` Replace PyCrypto's ``Random`` with `os.urandom` for improved + speed and security. Thanks again to Alex. * :support:`295` Swap out a bunch of PyCrypto hash functions with use of `hashlib`. Thanks to Alex Gaynor. * :support:`290` (also :issue:`292`) Add support for building universal -- cgit v1.2.3 From e96e2653a2ca0a465d2773b1fe468c0f87e758bc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Apr 2014 11:29:41 -0400 Subject: Changelog, closes #299 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 653502ca..2c4c1cf7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`299` Use deterministic signatures for ECDSA keys for improved + security. Thanks to Alex Gaynor. * :support:`297` Replace PyCrypto's ``Random`` with `os.urandom` for improved speed and security. Thanks again to Alex. * :support:`295` Swap out a bunch of PyCrypto hash functions with use of -- cgit v1.2.3 From d02ae566014c6d79f61f7538c979d6560c1de629 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 14 Apr 2014 18:28:03 -0400 Subject: Note changelog location in contribution docs --- sites/www/contributing.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst index 2b752cc5..634c2b26 100644 --- a/sites/www/contributing.rst +++ b/sites/www/contributing.rst @@ -17,3 +17,6 @@ How to submit bug reports or new code Please see `this project-agnostic contribution guide `_ - we follow it explicitly. + +Our current changelog is located in ``sites/www/changelog.rst`` - the top +level files like ``ChangeLog.*`` and ``NEWS`` are historical only. -- cgit v1.2.3 From c14de1d93516c9aec18749cb501febf0b2cee530 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 15 Apr 2014 15:04:46 -0400 Subject: Show Travis status in website sidebar --- sites/shared_conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/shared_conf.py b/sites/shared_conf.py index e0afe92e..69908388 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -14,6 +14,7 @@ html_theme_options = { 'github_repo': 'paramiko', 'gittip_user': 'bitprophet', 'analytics_id': 'UA-18486793-2', + 'travis_button': True, } html_sidebars = { '**': [ -- cgit v1.2.3 From 8a836942d545ffa44ed139099dfea4f96d336add Mon Sep 17 00:00:00 2001 From: Yan Kalchevskiy Date: Tue, 16 Jul 2013 14:02:24 +0700 Subject: Add support quoted values for SSHConfig (#157) --- paramiko/config.py | 54 ++++++++++++++++++++++++++++++++----------- tests/test_util.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 77fa13d7..28dc5231 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -53,6 +53,27 @@ class SSHConfig (object): :param file file_obj: a file-like object to read the config file from """ + def get_hosts(val): + i, length = 0, len(val) + hosts = [] + while i < length: + if val[i] == '"': + end = val.find('"', i + 1) + if end < 0: + raise Exception("Unparsable host %s" % val) + hosts.append(val[i + 1:end]) + i = end + 1 + elif not val[i].isspace(): + end = i + 1 + while end < length and not val[end].isspace(): + end += 1 + hosts.append(val[i:end]) + i = end + 1 + else: + i += 1 + + return hosts + host = {"host": ['*'], "config": {}} for line in file_obj: line = line.rstrip('\n').lstrip() @@ -75,22 +96,27 @@ class SSHConfig (object): raise Exception('Unparsable line: %r' % line) key = line[:i].lower() value = line[i:].lstrip() - + if key == 'host': self._config.append(host) - value = value.split() - host = {key: value, 'config': {}} - #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be - # specified multiple times and they should be tried in order - # of specification. - - elif key in ['identityfile', 'localforward', 'remoteforward']: - if key in host['config']: - host['config'][key].append(value) - else: - host['config'][key] = [value] - elif key not in host['config']: - host['config'].update({key: value}) + host = { + 'host': get_hosts(value), + 'config': {} + } + else: + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + + #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be + # specified multiple times and they should be tried in order + # of specification. + if key in ['identityfile', 'localforward', 'remoteforward']: + if key in host['config']: + host['config'][key].append(value) + else: + host['config'][key] = [value] + elif key not in host['config']: + host['config'][key] = value self._config.append(host) def lookup(self, hostname): diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..4e67e071 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -333,3 +333,71 @@ IdentityFile something_%l_using_fqdn """ config = paramiko.util.parse_ssh_config(StringIO(test_config)) assert config.lookup('meh') # will die during lookup() if bug regresses + + def test_quoted_host_names(self): + test_config_file = """\ +Host "param pam" param "pam" + Port 1111 + +Host "param2" + Port 2222 + +Host param3 parara + Port 3333 + +Host param4 "p a r" "p" "par" para + Port 4444 +""" + res = { + 'param pam': {'hostname': 'param pam', 'port': '1111'}, + 'param': {'hostname': 'param', 'port': '1111'}, + 'pam': {'hostname': 'pam', 'port': '1111'}, + + 'param2': {'hostname': 'param2', 'port': '2222'}, + + 'param3': {'hostname': 'param3', 'port': '3333'}, + 'parara': {'hostname': 'parara', 'port': '3333'}, + + 'param4': {'hostname': 'param4', 'port': '4444'}, + 'p a r': {'hostname': 'p a r', 'port': '4444'}, + 'p': {'hostname': 'p', 'port': '4444'}, + 'par': {'hostname': 'par', 'port': '4444'}, + 'para': {'hostname': 'para', 'port': '4444'}, + } + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + for host, values in res.items(): + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_quoted_params_in_config(self): + test_config_file = """\ +Host "param pam" param "pam" + IdentityFile id_rsa + +Host "param2" + IdentityFile "test rsa key" + +Host param3 parara + IdentityFile id_rsa + IdentityFile "test rsa key" +""" + res = { + 'param pam': {'hostname': 'param pam', 'identityfile': ['id_rsa']}, + 'param': {'hostname': 'param', 'identityfile': ['id_rsa']}, + 'pam': {'hostname': 'pam', 'identityfile': ['id_rsa']}, + + 'param2': {'hostname': 'param2', 'identityfile': ['test rsa key']}, + + 'param3': {'hostname': 'param3', 'identityfile': ['id_rsa', 'test rsa key']}, + 'parara': {'hostname': 'parara', 'identityfile': ['id_rsa', 'test rsa key']}, + } + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + for host, values in res.items(): + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) -- cgit v1.2.3 From 59b7cc7a0296090f5b7a58226e74d4b293d00103 Mon Sep 17 00:00:00 2001 From: Yan Kalchevskiy Date: Wed, 22 Jan 2014 16:30:20 +0700 Subject: Simplify handling of the configuration file. --- paramiko/config.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 28dc5231..dce472ee 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -27,7 +27,6 @@ import re import socket SSH_PORT = 22 -proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) class SSHConfig (object): @@ -41,6 +40,8 @@ class SSHConfig (object): .. versionadded:: 1.6 """ + SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)') + def __init__(self): """ Create a new OpenSSH config object. @@ -79,23 +80,12 @@ class SSHConfig (object): line = line.rstrip('\n').lstrip() if (line == '') or (line[0] == '#'): continue - if '=' in line: - # Ensure ProxyCommand gets properly split - if line.lower().strip().startswith('proxycommand'): - match = proxy_re.match(line) - key, value = match.group(1).lower(), match.group(2) - else: - key, value = line.split('=', 1) - key = key.strip().lower() - else: - # find first whitespace, and split there - i = 0 - while (i < len(line)) and not line[i].isspace(): - i += 1 - if i == len(line): - raise Exception('Unparsable line: %r' % line) - key = line[:i].lower() - value = line[i:].lstrip() + + match = re.match(self.SETTINGS_REGEX, line) + if not match: + raise Exception("Unparsable line %s" % line) + key = match.group(1).lower() + value = match.group(2) if key == 'host': self._config.append(host) -- cgit v1.2.3 From f258d1e207a21d4342dbc431fcc4a4dafe893e80 Mon Sep 17 00:00:00 2001 From: Yan Kalchevksiy Date: Sat, 15 Feb 2014 17:20:31 +0700 Subject: Moved get_hosts function into method. --- paramiko/config.py | 46 +++++++++++++++++++++++++--------------------- tests/test_util.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index dce472ee..5abf181f 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -54,26 +54,6 @@ class SSHConfig (object): :param file file_obj: a file-like object to read the config file from """ - def get_hosts(val): - i, length = 0, len(val) - hosts = [] - while i < length: - if val[i] == '"': - end = val.find('"', i + 1) - if end < 0: - raise Exception("Unparsable host %s" % val) - hosts.append(val[i + 1:end]) - i = end + 1 - elif not val[i].isspace(): - end = i + 1 - while end < length and not val[end].isspace(): - end += 1 - hosts.append(val[i:end]) - i = end + 1 - else: - i += 1 - - return hosts host = {"host": ['*'], "config": {}} for line in file_obj: @@ -90,7 +70,7 @@ class SSHConfig (object): if key == 'host': self._config.append(host) host = { - 'host': get_hosts(value), + 'host': self._get_hosts(value), 'config': {} } else: @@ -222,6 +202,30 @@ class SSHConfig (object): config[k] = config[k].replace(find, str(replace)) return config + def _get_hosts(self, host): + """ + Return a list of host_names from host value. + """ + i, length = 0, len(host) + hosts = [] + while i < length: + if host[i] == '"': + end = host.find('"', i + 1) + if end < 0: + raise Exception("Unparsable host %s" % host) + hosts.append(host[i + 1:end]) + i = end + 1 + elif not host[i].isspace(): + end = i + 1 + while end < length and not host[end].isspace() and host[end] != '"': + end += 1 + hosts.append(host[i:end]) + i = end + else: + i += 1 + + return hosts + class LazyFqdn(object): """ diff --git a/tests/test_util.py b/tests/test_util.py index 4e67e071..8142d416 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -401,3 +401,35 @@ Host param3 parara paramiko.util.lookup_ssh_host_config(host, config), values ) + + def test_quoted_host_in_config(self): + conf = SSHConfig() + correct_data = { + 'param': ['param'], + '"param"': ['param'], + + 'param pam': ['param', 'pam'], + '"param" "pam"': ['param', 'pam'], + '"param" pam': ['param', 'pam'], + 'param "pam"': ['param', 'pam'], + + 'param "pam" p': ['param', 'pam', 'p'], + '"param" pam "p"': ['param', 'pam', 'p'], + + '"pa ram"': ['pa ram'], + '"pa ram" pam': ['pa ram', 'pam'], + 'param "p a m"': ['param', 'p a m'], + } + incorrect_data = [ + 'param"', + '"param', + 'param "pam', + 'param "pam" "p a', + ] + for host, values in correct_data.items(): + self.assertEquals( + conf._get_hosts(host), + values + ) + for host in incorrect_data: + self.assertRaises(Exception, conf._get_hosts, host) -- cgit v1.2.3 From 6d00c591cd30f1ae877ade16eaac14a05327cfe8 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 7 May 2014 16:09:55 -0700 Subject: Bump alabaster for things like travis_button --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 88a32964..e9052898 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -5,6 +5,6 @@ tox>=1.4,<1.5 invoke>=0.7.0 invocations>=0.5.0 sphinx>=1.1.3 -alabaster>=0.4.0 +alabaster>=0.6.0 releases>=0.5.2 wheel==0.23.0 -- cgit v1.2.3 From 951faed80b017e553a27c4cb98f210df44341f8f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 7 May 2014 16:13:33 -0700 Subject: Cut 1.14 --- paramiko/__init__.py | 2 +- setup.py | 2 +- sites/www/changelog.rst | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 22f1bc21..4c62ad4a 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -23,7 +23,7 @@ if sys.version_info < (2, 6): __author__ = "Jeff Forcier " -__version__ = "1.13.1" +__version__ = "1.14.0" __version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" diff --git a/setup.py b/setup.py index 05dc98d5..c0f1e579 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if sys.platform == 'darwin': setup( name = "paramiko", - version = "1.13.1", + version = "1.14.0", description = "SSH2 protocol library", long_description = longdesc, author = "Jeff Forcier", diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 52fe4ff3..f8a4d2c1 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.14.0 <2014-05-07>` * :release:`1.13.1 <2014-05-07>` * :release:`1.12.4 <2014-05-07>` * :release:`1.11.6 <2014-05-07>` -- cgit v1.2.3 From e811e715833373dd2f2ba898089695eee9c882ed Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 25 May 2014 14:57:58 -0700 Subject: We support 3.2 --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 9305bfb3..61c5c852 100644 --- a/README +++ b/README @@ -35,7 +35,7 @@ Requirements ------------ - Python 2.6 or better - this includes Python - 3.3 and higher as well. + 3.2 and higher as well. - pycrypto 2.1 or better - ecdsa 0.9 or better -- cgit v1.2.3 From 8455a8a2d2058d1daf55e9f8446a2b4a0676339e Mon Sep 17 00:00:00 2001 From: Yuri Turchenkov Date: Mon, 26 May 2014 16:32:54 +0400 Subject: fix ecdsa key generation --- paramiko/ecdsakey.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6736315f..8cf4b1e7 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -24,7 +24,6 @@ import binascii from hashlib import sha256 from ecdsa import SigningKey, VerifyingKey, der, curves -from ecdsa.test_pyecdsa import ECDSA from paramiko.common import four_byte, one_byte from paramiko.message import Message @@ -51,7 +50,7 @@ class ECDSAKey (PKey): if (msg is None) and (data is not None): msg = Message(data) if vals is not None: - self.verifying_key, self.signing_key = vals + self.signing_key, self.verifying_key = vals else: if msg is None: raise SSHException('Key object may not be empty') @@ -125,7 +124,7 @@ class ECDSAKey (PKey): key = self.signing_key or self.verifying_key self._write_private_key('EC', file_obj, key.to_der(), password) - def generate(bits, progress_func=None): + def generate(curve=curves.NIST256p, progress_func=None): """ Generate a new private RSA key. This factory function can be used to generate a new host key or authentication key. @@ -138,7 +137,7 @@ class ECDSAKey (PKey): @return: new private key @rtype: L{RSAKey} """ - signing_key = ECDSA.generate() + signing_key = SigningKey.generate(curve) key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) return key generate = staticmethod(generate) -- cgit v1.2.3 From 56417507ee0fbe28b938e03909945c127c8bbde1 Mon Sep 17 00:00:00 2001 From: Xavier Nunn Date: Wed, 18 Jun 2014 18:54:32 +0200 Subject: Corrected the length of the salt for private key encryption This would corrupt the private key file whenever the DES cipher would get selected. --- paramiko/pkey.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 373563f6..2daf3723 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -324,13 +324,12 @@ class PKey (object): def _write_private_key(self, tag, f, data, password=None): f.write('-----BEGIN %s PRIVATE KEY-----\n' % tag) if password is not None: - # since we only support one cipher here, use it cipher_name = list(self._CIPHER_TABLE.keys())[0] cipher = self._CIPHER_TABLE[cipher_name]['cipher'] keysize = self._CIPHER_TABLE[cipher_name]['keysize'] blocksize = self._CIPHER_TABLE[cipher_name]['blocksize'] mode = self._CIPHER_TABLE[cipher_name]['mode'] - salt = os.urandom(16) + salt = os.urandom(blocksize) key = util.generate_key_bytes(md5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize -- cgit v1.2.3 From 9ef02e1a97477a1476f64e690fb6bc21e78be037 Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Sat, 5 Jul 2014 23:51:38 +0200 Subject: Support passing in "buffer" objects again where bytestrings are expected. This fixes bzr's use of paramiko. Fixes issue #343/#285. --- paramiko/py3compat.py | 4 ++++ tests/test_file.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index 8842b988..57c096b2 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -39,6 +39,8 @@ if PY2: return s elif isinstance(s, unicode): return s.encode(encoding) + elif isinstance(s, buffer): + return s else: raise TypeError("Expected unicode or bytes, got %r" % s) @@ -49,6 +51,8 @@ if PY2: return s.decode(encoding) elif isinstance(s, unicode): return s + elif isinstance(s, buffer): + return s.decode(encoding) else: raise TypeError("Expected unicode or bytes, got %r" % s) diff --git a/tests/test_file.py b/tests/test_file.py index c6edd7af..22a34aca 100755 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -23,6 +23,7 @@ Some unit tests for the BufferedFile abstraction. import unittest from paramiko.file import BufferedFile from paramiko.common import linefeed_byte, crlf, cr_byte +import sys class LoopbackFile (BufferedFile): @@ -151,6 +152,15 @@ class BufferedFileTest (unittest.TestCase): b'need to close them again.\n') f.close() + def test_8_buffering(self): + """ + verify that buffered objects can be written + """ + if sys.version_info[0] == 2: + f = LoopbackFile('r+', 16) + f.write(buffer(b'Too small.')) + f.close() + if __name__ == '__main__': from unittest import main main() -- cgit v1.2.3 From f17c2afbe0a98d5c2b80e5f16dbf47b9eb7370d0 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Wed, 11 Jun 2014 12:19:55 +0200 Subject: Use python-gssapi 0.6.1 --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 94aa3a9c..ceb3598a 100644 --- a/README +++ b/README @@ -79,7 +79,7 @@ If you want paramiko to do kerberos authentication or key exchange using GSS-API need the following python packages: - pyasn1 0.1.7 or better -- python-gssapi 0.4.0 or better (Unix) +- python-gssapi 0.6.1 or better (Unix) - pywin32 2.1.8 or better (Windows) So you have to install pyasn1 and python-gssapi on Unix or pywin32 on Windows. -- cgit v1.2.3 From de0d52851a9dc5b3b53fd52298f29c3d79db6910 Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Wed, 11 Jun 2014 12:22:45 +0200 Subject: Don't check for the qop value at MIC verification --- paramiko/auth_handler.py | 12 ++++++------ paramiko/ssh_gss.py | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index cb06da2d..a77ace1b 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -515,9 +515,9 @@ class AuthHandler (object): break mic_token = m.get_string() try: - retval = sshgss.ssh_check_mic(mic_token, - self.transport.session_id, - username) + sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + username) except Exception: result = AUTH_FAILED self._send_auth_result(username, method, result) @@ -541,9 +541,9 @@ class AuthHandler (object): result = AUTH_FAILED self._send_auth_result(username, method, result) try: - retval = sshgss.ssh_check_mic(mic_token, - self.transport.session_id, - self.auth_username) + sshgss.ssh_check_mic(mic_token, + self.transport.session_id, + self.auth_username) except Exception: result = AUTH_FAILED self._send_auth_result(username, method, result) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 58a64a56..03c5dcc0 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -384,14 +384,16 @@ class _SSH_GSSAPI(_SSH_GSSAuth): self._username, self._service, self._auth_method) - mic_status = self._gss_srv_ctxt.verify_mic(mic_field, - mic_token) + try: + self._gss_srv_ctxt.verify_mic(mic_field, + mic_token) + except gssapi.BadSignature: + raise Exception("GSS-API MIC check failed.") else: # for key exchange with gssapi-keyex # client mode - mic_status = self._gss_ctxt.verify_mic(self._session_id, - mic_token) - return mic_status + self._gss_ctxt.verify_mic(self._session_id, + mic_token) @property def credentials_delegated(self): -- cgit v1.2.3 From bd21af3405a3591dcc0533993d587aa2e4db0c4f Mon Sep 17 00:00:00 2001 From: Matthias Witte Date: Tue, 15 Jul 2014 12:22:11 +0200 Subject: Add support for sha256 based hmac and kexgex --- paramiko/kex_gex.py | 13 +++++++++---- paramiko/transport.py | 26 +++++++++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 5ff8a287..1eadba03 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -23,7 +23,7 @@ client side, and a B{lot} more on the server side. """ import os -from hashlib import sha1 +from hashlib import sha1, sha256 from paramiko import util from paramiko.common import DEBUG @@ -44,6 +44,7 @@ class KexGex (object): min_bits = 1024 max_bits = 8192 preferred_bits = 2048 + hash_algo = sha1 def __init__(self, transport): self.transport = transport @@ -87,7 +88,7 @@ class KexGex (object): return self._parse_kexdh_gex_reply(m) elif ptype == _MSG_KEXDH_GEX_REQUEST_OLD: return self._parse_kexdh_gex_request_old(m) - raise SSHException('KexGex asked to handle packet type %d' % ptype) + raise SSHException('KexGex %s asked to handle packet type %d' % self.name, ptype) ### internals... @@ -204,7 +205,7 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = sha1(hm.asbytes()).digest() + H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(H) @@ -239,6 +240,10 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() + +class KexGexSHA256(KexGex): + name = 'diffie-hellman-group-exchange-sha256' + hash_algo = sha256 diff --git a/paramiko/transport.py b/paramiko/transport.py index 406626a7..48ab1729 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -26,7 +26,7 @@ import sys import threading import time import weakref -from hashlib import md5, sha1 +from hashlib import md5, sha1, sha256 import paramiko from paramiko import util @@ -45,7 +45,7 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey -from paramiko.kex_gex import KexGex +from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko.kex_group1 import KexGroup1 from paramiko.message import Message from paramiko.packet import Packetizer, NeedRekeyException @@ -90,9 +90,12 @@ class Transport (threading.Thread): _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_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96', + 'hmac-sha2-256') _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ('diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1') + _preferred_kex = ('diffie-hellman-group1-sha1', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group-exchange-sha256') _preferred_compression = ('none',) _cipher_info = { @@ -109,6 +112,7 @@ class Transport (threading.Thread): _mac_info = { 'hmac-sha1': {'class': sha1, 'size': 20}, 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-sha2-256': {'class': sha256, 'size': 32}, 'hmac-md5': {'class': md5, 'size': 16}, 'hmac-md5-96': {'class': md5, 'size': 12}, } @@ -122,6 +126,7 @@ class Transport (threading.Thread): _kex_info = { 'diffie-hellman-group1-sha1': KexGroup1, 'diffie-hellman-group-exchange-sha1': KexGex, + 'diffie-hellman-group-exchange-sha256': KexGexSHA256, } _compression_info = { @@ -1336,13 +1341,14 @@ class Transport (threading.Thread): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = sha1(m.asbytes()).digest() + hash_algo = self._mac_info[self.local_mac]['class'] + out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = sha1(m.asbytes()).digest() + digest = hash_algo(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1572,10 +1578,12 @@ class Transport (threading.Thread): self.clear_to_send_lock.release() self.in_kex = True if self.server_mode: - if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex): + mp_required_prefix = 'diffie-hellman-group-exchange-sha' + kex_mp = [k for k in self._preferred_kex if k.startswith(mp_required_prefix)] + if (self._modulus_pack is None) and (len(kex_mp) > 0): # can't do group-exchange if we don't have a pack of potential primes - pkex = list(self.get_security_options().kex) - pkex.remove('diffie-hellman-group-exchange-sha1') + pkex = [k for k in self.get_security_options().kex + if not k.startswith(mp_required_prefix)] self.get_security_options().kex = pkex available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, self._preferred_keys)) -- cgit v1.2.3 From f4cfd1967a1b9d15262ab859d392b3edef512ad7 Mon Sep 17 00:00:00 2001 From: Matthias Witte Date: Tue, 15 Jul 2014 12:22:11 +0200 Subject: Add support for sha256 based hmac and kexgex - fix tab damage --- paramiko/kex_gex.py | 13 +++++++++---- paramiko/transport.py | 26 +++++++++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 5ff8a287..1eadba03 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -23,7 +23,7 @@ client side, and a B{lot} more on the server side. """ import os -from hashlib import sha1 +from hashlib import sha1, sha256 from paramiko import util from paramiko.common import DEBUG @@ -44,6 +44,7 @@ class KexGex (object): min_bits = 1024 max_bits = 8192 preferred_bits = 2048 + hash_algo = sha1 def __init__(self, transport): self.transport = transport @@ -87,7 +88,7 @@ class KexGex (object): return self._parse_kexdh_gex_reply(m) elif ptype == _MSG_KEXDH_GEX_REQUEST_OLD: return self._parse_kexdh_gex_request_old(m) - raise SSHException('KexGex asked to handle packet type %d' % ptype) + raise SSHException('KexGex %s asked to handle packet type %d' % self.name, ptype) ### internals... @@ -204,7 +205,7 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = sha1(hm.asbytes()).digest() + H = self.hash_algo(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it sig = self.transport.get_server_key().sign_ssh_data(H) @@ -239,6 +240,10 @@ class KexGex (object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, sha1(hm.asbytes()).digest()) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() + +class KexGexSHA256(KexGex): + name = 'diffie-hellman-group-exchange-sha256' + hash_algo = sha256 diff --git a/paramiko/transport.py b/paramiko/transport.py index 406626a7..9d0889bb 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -26,7 +26,7 @@ import sys import threading import time import weakref -from hashlib import md5, sha1 +from hashlib import md5, sha1, sha256 import paramiko from paramiko import util @@ -45,7 +45,7 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey -from paramiko.kex_gex import KexGex +from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko.kex_group1 import KexGroup1 from paramiko.message import Message from paramiko.packet import Packetizer, NeedRekeyException @@ -90,9 +90,12 @@ class Transport (threading.Thread): _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_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96', + 'hmac-sha2-256') _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ('diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1') + _preferred_kex = ('diffie-hellman-group1-sha1', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group-exchange-sha256') _preferred_compression = ('none',) _cipher_info = { @@ -109,6 +112,7 @@ class Transport (threading.Thread): _mac_info = { 'hmac-sha1': {'class': sha1, 'size': 20}, 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-sha2-256': {'class': sha256, 'size': 32}, 'hmac-md5': {'class': md5, 'size': 16}, 'hmac-md5-96': {'class': md5, 'size': 12}, } @@ -122,6 +126,7 @@ class Transport (threading.Thread): _kex_info = { 'diffie-hellman-group1-sha1': KexGroup1, 'diffie-hellman-group-exchange-sha1': KexGex, + 'diffie-hellman-group-exchange-sha256': KexGexSHA256, } _compression_info = { @@ -1336,13 +1341,14 @@ class Transport (threading.Thread): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = sha1(m.asbytes()).digest() + hash_algo = self._mac_info[self.local_mac]['class'] + out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = sha1(m.asbytes()).digest() + digest = hash_algo(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1572,10 +1578,12 @@ class Transport (threading.Thread): self.clear_to_send_lock.release() self.in_kex = True if self.server_mode: - if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex): + mp_required_prefix = 'diffie-hellman-group-exchange-sha' + kex_mp = [k for k in self._preferred_kex if k.startswith(mp_required_prefix)] + if (self._modulus_pack is None) and (len(kex_mp) > 0): # can't do group-exchange if we don't have a pack of potential primes - pkex = list(self.get_security_options().kex) - pkex.remove('diffie-hellman-group-exchange-sha1') + pkex = [k for k in self.get_security_options().kex + if not k.startswith(mp_required_prefix)] self.get_security_options().kex = pkex available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, self._preferred_keys)) -- cgit v1.2.3 From 26b11d06ff2790cfa55c9557f10a2b48a3fc55b7 Mon Sep 17 00:00:00 2001 From: Matthias Witte Date: Tue, 15 Jul 2014 14:36:41 +0200 Subject: Fix transport test --- tests/test_transport.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index 485a18e8..3e794c12 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -26,6 +26,7 @@ import socket import time import threading import random +from hashlib import sha1 from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \ SSHException, ChannelException @@ -160,6 +161,7 @@ class TransportTest(ParamikoTest): self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929 self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3' self.tc.session_id = self.tc.H + self.tc.local_mac = 'hmac-sha1' key = self.tc._compute_key('C', 32) self.assertEqual(b'207E66594CA87C44ECCBA3B3CD39FDDB378E6FDB0F97C54B2AA0CFBF900CD995', hexlify(key).upper()) @@ -426,9 +428,11 @@ class TransportTest(ParamikoTest): bytes = self.tc.packetizer._Packetizer__sent_bytes chan.send('x' * 1024) bytes2 = self.tc.packetizer._Packetizer__sent_bytes + block_size = self.tc._cipher_info[self.tc.local_cipher]['block-size'] + mac_size = self.tc._mac_info[self.tc.local_mac]['size'] # tests show this is actually compressed to *52 bytes*! including packet overhead! nice!! :) self.assertTrue(bytes2 - bytes < 1024) - self.assertEqual(52, bytes2 - bytes) + self.assertEqual(16 + block_size + mac_size, bytes2 - bytes) chan.close() schan.close() -- cgit v1.2.3 From 47da1935dc84784bfee2e493474232bf2e0d8d37 Mon Sep 17 00:00:00 2001 From: Matthias Witte Date: Wed, 16 Jul 2014 11:17:40 +0200 Subject: Include sha2 changes in tests - let _compute_key default default to sha1 if local_mac is not set instead of setting local_mac explicitly in the unit test - add tests for KexGexSHA256 --- paramiko/transport.py | 2 +- tests/test_kex.py | 120 +++++++++++++++++++++++++++++++++++++++++++++++- tests/test_transport.py | 1 - 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 9d0889bb..35c20341 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1341,7 +1341,7 @@ class Transport (threading.Thread): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - hash_algo = self._mac_info[self.local_mac]['class'] + hash_algo = self._mac_info[self.local_mac]['class'] if self.local_mac else sha1 out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() diff --git a/tests/test_kex.py b/tests/test_kex.py index 56f1b7c7..19804fbf 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -26,7 +26,7 @@ import unittest import paramiko.util from paramiko.kex_group1 import KexGroup1 -from paramiko.kex_gex import KexGex +from paramiko.kex_gex import KexGex, KexGexSHA256 from paramiko import Message from paramiko.common import byte_chr @@ -252,3 +252,121 @@ class KexTest (unittest.TestCase): self.assertEqual(H, hexlify(transport._H).upper()) self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) self.assertTrue(transport._activated) + + def test_7_gex_sha256_client(self): + transport = FakeTransport() + transport.server_mode = False + kex = KexGexSHA256(transport) + kex.start_kex() + x = b'22000004000000080000002000' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) + + msg = Message() + msg.add_mpint(FakeModulusPack.P) + msg.add_mpint(FakeModulusPack.G) + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg) + x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) + + msg = Message() + msg.add_string('fake-host-key') + msg.add_mpint(69) + msg.add_string('fake-sig') + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg) + H = b'AD1A9365A67B4496F05594AD1BF656E3CDA0851289A4C1AFF549FEAE50896DF4' + self.assertEqual(self.K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) + self.assertTrue(transport._activated) + + def test_8_gex_sha256_old_client(self): + transport = FakeTransport() + transport.server_mode = False + kex = KexGexSHA256(transport) + kex.start_kex(_test_old_style=True) + x = b'1E00000800' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect) + + msg = Message() + msg.add_mpint(FakeModulusPack.P) + msg.add_mpint(FakeModulusPack.G) + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg) + x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect) + + msg = Message() + msg.add_string('fake-host-key') + msg.add_mpint(69) + msg.add_string('fake-sig') + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg) + H = b'518386608B15891AE5237DEE08DCADDE76A0BCEFCE7F6DB3AD66BC41D256DFE5' + self.assertEqual(self.K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify) + self.assertTrue(transport._activated) + + def test_9_gex_sha256_server(self): + transport = FakeTransport() + transport.server_mode = True + kex = KexGexSHA256(transport) + kex.start_kex() + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) + + msg = Message() + msg.add_int(1024) + msg.add_int(2048) + msg.add_int(4096) + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg) + x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) + + msg = Message() + msg.add_mpint(12345) + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg) + K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 + H = b'CCAC0497CF0ABA1DBF55E1A3995D17F4CC31824B0E8D95CDF8A06F169D050D80' + x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' + self.assertEqual(K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertTrue(transport._activated) + + def test_10_gex_sha256_server_with_old_client(self): + transport = FakeTransport() + transport.server_mode = True + kex = KexGexSHA256(transport) + kex.start_kex() + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect) + + msg = Message() + msg.add_int(2048) + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg) + x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102' + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect) + + msg = Message() + msg.add_mpint(12345) + msg.rewind() + kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg) + K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581 + H = b'3DDD2AD840AD095E397BA4D0573972DC60F6461FD38A187CACA6615A5BC8ADBB' + x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967' + self.assertEqual(K, transport._K) + self.assertEqual(H, hexlify(transport._H).upper()) + self.assertEqual(x, hexlify(transport._message.asbytes()).upper()) + self.assertTrue(transport._activated) + + diff --git a/tests/test_transport.py b/tests/test_transport.py index 3e794c12..0d66c0dc 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -161,7 +161,6 @@ class TransportTest(ParamikoTest): self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929 self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3' self.tc.session_id = self.tc.H - self.tc.local_mac = 'hmac-sha1' key = self.tc._compute_key('C', 32) self.assertEqual(b'207E66594CA87C44ECCBA3B3CD39FDDB378E6FDB0F97C54B2AA0CFBF900CD995', hexlify(key).upper()) -- cgit v1.2.3 From 2ee8ec8cde1b8f893cfbaf0b1482bd922f3b850f Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Thu, 24 Jul 2014 10:05:37 +0100 Subject: Allow setting of SSH banner timeouts on Clients --- paramiko/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index c1bf4735..67503dc3 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -171,7 +171,7 @@ class SSHClient (object): 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, - compress=False, sock=None): + compress=False, sock=None, banner_timeout=None): """ Connect to an SSH server and authenticate to it. The server's host key is checked against the system host keys (see `load_system_host_keys`) @@ -210,6 +210,8 @@ class SSHClient (object): :param socket sock: an open socket or socket-like object (such as a `.Channel`) to use for communication to the target host + :param float banner_timeout: an optional timeout (in seconds) to wait + for the SSH banner to be presented. :raises BadHostKeyException: if the server's host key could not be verified @@ -239,6 +241,8 @@ class SSHClient (object): t.use_compression(compress=compress) if self._log_channel is not None: t.set_log_channel(self._log_channel) + if banner_timeout is not None: + t.banner_timeout = banner_timeout t.start_client() ResourceManager.register(self, t) -- cgit v1.2.3 From 6d48018d11a2058213481fdfeed8887fccc31854 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 11 Aug 2014 10:31:02 -0700 Subject: Add FAQ about nonstandard SSH implementations --- sites/www/faq.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sites/www/faq.rst b/sites/www/faq.rst index a7e80014..bf450f63 100644 --- a/sites/www/faq.rst +++ b/sites/www/faq.rst @@ -7,3 +7,20 @@ Which version should I use? I see multiple active releases. Please see :ref:`the installation docs ` which have an explicit section about this topic. + +Paramiko doesn't work with my Cisco, Windows or other non-Unix system! +====================================================================== + +In an ideal world, the developers would love to support every possible target +system. Unfortunately, volunteer development time and access to non-mainstream +platforms are limited, meaning that we can only fully support standard OpenSSH +implementations such as those found on the average Linux distribution (as well +as on Mac OS X and \*BSD.) + +Because of this, **we typically close bug reports for nonstandard SSH +implementations**. + +However, **closed does not imply locked** - affected users can still post +comments on such tickets - and **we will always consider actual patch +submissions for these issues**, provided they can get +1s from similarly +affected users and are proven to not break existing functionality. -- cgit v1.2.3 From dac6952331109bdab441040cc8b0136697252297 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 11 Aug 2014 10:55:46 -0700 Subject: Make links to Github more explicit --- sites/www/contact.rst | 1 + sites/www/contributing.rst | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/sites/www/contact.rst b/sites/www/contact.rst index 2b6583f5..7e6c947e 100644 --- a/sites/www/contact.rst +++ b/sites/www/contact.rst @@ -9,3 +9,4 @@ following ways: * Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage `_ for usage details). * This website - a blog section is forthcoming. +* Submit contributions on Github - see the :doc:`contributing` page. diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst index 634c2b26..a44414e8 100644 --- a/sites/www/contributing.rst +++ b/sites/www/contributing.rst @@ -5,18 +5,22 @@ Contributing How to get the code =================== -Our primary Git repository is on Github at `paramiko/paramiko -`_; please follow their instructions for -cloning to your local system. (If you intend to submit patches/pull requests, -we recommend forking first, then cloning your fork. Github has excellent -documentation for all this.) +Our primary Git repository is on Github at `paramiko/paramiko`_; +please follow their instructions for cloning to your local system. (If you +intend to submit patches/pull requests, we recommend forking first, then +cloning your fork. Github has excellent documentation for all this.) How to submit bug reports or new code ===================================== Please see `this project-agnostic contribution guide -`_ - we follow it explicitly. +`_ - we follow it explicitly. Again, our code +repository and bug tracker is `on Github`_. Our current changelog is located in ``sites/www/changelog.rst`` - the top level files like ``ChangeLog.*`` and ``NEWS`` are historical only. + + +.. _paramiko/paramiko: +.. _on Github: https://github.com/paramiko/paramiko -- cgit v1.2.3 From 991d56bad32c1ea4eda6c86771a4a4b7bef00475 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 11 Aug 2014 11:40:20 -0700 Subject: Clarify FAQ --- sites/www/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/faq.rst b/sites/www/faq.rst index bf450f63..a5d9b383 100644 --- a/sites/www/faq.rst +++ b/sites/www/faq.rst @@ -18,7 +18,7 @@ implementations such as those found on the average Linux distribution (as well as on Mac OS X and \*BSD.) Because of this, **we typically close bug reports for nonstandard SSH -implementations**. +implementations or host systems**. However, **closed does not imply locked** - affected users can still post comments on such tickets - and **we will always consider actual patch -- cgit v1.2.3 From 80148a3039499fb52d7b5c7a583c14258ccf9cae Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Wed, 13 Aug 2014 16:56:14 +0200 Subject: Change window and packet size to match opensshs'. Update tests to match the new numbers. --- paramiko/transport.py | 4 ++-- tests/test_transport.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 406626a7..bda05443 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -231,8 +231,8 @@ class Transport (threading.Thread): self.channel_events = {} # (id -> Event) self.channels_seen = {} # (id -> True) self._channel_counter = 1 - self.window_size = 65536 - self.max_packet_size = 34816 + self.max_packet_size = 2 ** 15 + self.window_size = 64 * self.max_packet_size self._forward_agent_handler = None self._x11_handler = None self._tcp_handler = None diff --git a/tests/test_transport.py b/tests/test_transport.py index 485a18e8..d84747b6 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -585,12 +585,13 @@ class TransportTest(ParamikoTest): self.assertEqual(chan.send_ready(), True) total = 0 K = '*' * 1024 - while total < 1024 * 1024: + limit = 1+(64 * 2 ** 15) + while total < limit: chan.send(K) total += len(K) if not chan.send_ready(): break - self.assertTrue(total < 1024 * 1024) + self.assertTrue(total < limit) schan.close() chan.close() -- cgit v1.2.3 From 677285a3583b15c2bbd765816d5b62f4cb403965 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Wed, 13 Aug 2014 17:09:06 +0200 Subject: Rename max_packet_size and window_size. This is to indicate that they should be seen as defaults and can be overriden. --- paramiko/transport.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index bda05443..7776b9d7 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -231,8 +231,8 @@ class Transport (threading.Thread): self.channel_events = {} # (id -> Event) self.channels_seen = {} # (id -> True) self._channel_counter = 1 - self.max_packet_size = 2 ** 15 - self.window_size = 64 * self.max_packet_size + self.default_max_packet_size = 2 ** 15 + self.default_window_size = 64 * self.default_max_packet_size self._forward_agent_handler = None self._x11_handler = None self._tcp_handler = None @@ -610,8 +610,8 @@ class Transport (threading.Thread): m.add_byte(cMSG_CHANNEL_OPEN) m.add_string(kind) m.add_int(chanid) - m.add_int(self.window_size) - m.add_int(self.max_packet_size) + m.add_int(self.default_window_size) + m.add_int(self.default_max_packet_size) if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'): m.add_string(dest_addr[0]) m.add_int(dest_addr[1]) @@ -625,7 +625,7 @@ class Transport (threading.Thread): self.channel_events[chanid] = event = threading.Event() self.channels_seen[chanid] = True chan._set_transport(self) - chan._set_window(self.window_size, self.max_packet_size) + chan._set_window(self.default_window_size, self.default_max_packet_size) finally: self.lock.release() self._send_user_message(m) @@ -1953,7 +1953,7 @@ class Transport (threading.Thread): self._channels.put(my_chanid, chan) self.channels_seen[my_chanid] = True chan._set_transport(self) - chan._set_window(self.window_size, self.max_packet_size) + chan._set_window(self.default_window_size, self.default_max_packet_size) chan._set_remote_channel(chanid, initial_window_size, max_packet_size) finally: self.lock.release() @@ -1961,8 +1961,8 @@ class Transport (threading.Thread): m.add_byte(cMSG_CHANNEL_OPEN_SUCCESS) m.add_int(chanid) m.add_int(my_chanid) - m.add_int(self.window_size) - m.add_int(self.max_packet_size) + m.add_int(self.default_window_size) + m.add_int(self.default_max_packet_size) self._send_message(m) self._log(INFO, 'Secsh channel %d (%s) opened.', my_chanid, kind) if kind == 'auth-agent@openssh.com': -- cgit v1.2.3 From c0e60de5291b9f7dc660147a9f9beb0b87b24c36 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Wed, 13 Aug 2014 17:18:51 +0200 Subject: Add possibility to set default sizes. This change adds two new parameters to the constructor of the Transport class. Letting the user set the default windows size and default max packet size for the instance. --- paramiko/transport.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 7776b9d7..d9a48247 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -135,7 +135,10 @@ class Transport (threading.Thread): _modulus_pack = None - def __init__(self, sock): + def __init__(self, + sock, + default_window_size=64 * 2 ** 15, + default_max_packet_size=2 ** 15): """ Create a new SSH session over an existing socket, or socket-like object. This only creates the `.Transport` object; it doesn't begin the @@ -163,6 +166,12 @@ class Transport (threading.Thread): :param socket sock: a socket or socket-like object to create the session over. + :param int default_window_size: + sets the default window size on the transport. (defaults to + 2097152) + :param int default_max_packet_size: + sets the default max packet size on the transport. (defaults to + 32768) """ if isinstance(sock, string_types): # convert "host:port" into (host, port) @@ -231,8 +240,8 @@ class Transport (threading.Thread): self.channel_events = {} # (id -> Event) self.channels_seen = {} # (id -> True) self._channel_counter = 1 - self.default_max_packet_size = 2 ** 15 - self.default_window_size = 64 * self.default_max_packet_size + self.default_max_packet_size = default_max_packet_size + self.default_window_size = default_window_size self._forward_agent_handler = None self._x11_handler = None self._tcp_handler = None -- cgit v1.2.3 From a429e17e8e5cc3f5d442d374fb4843badfd4dd48 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 12:00:46 +0200 Subject: Join the threads with a low timeout. Instead of using private methods on the threading class, let the thread join, but with a low timeout. --- test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test.py b/test.py index 2b3d4ed4..92a3e6d0 100755 --- a/test.py +++ b/test.py @@ -149,10 +149,7 @@ def main(): # TODO: make that not a problem, jeez for thread in threading.enumerate(): if thread is not threading.currentThread(): - if PY2: - thread._Thread__stop() - else: - thread._stop() + thread.join(timeout=1) # Exit correctly if not result.wasSuccessful(): sys.exit(1) -- cgit v1.2.3 From 075f5c0338cf48e2b0280dd97b35aa6e40d94b3d Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 12:03:32 +0200 Subject: Support py3.4. --- .travis.yml | 1 + setup.py | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7042570f..c7abce9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" install: # Self-install for setup.py-driven deps - pip install -e . diff --git a/setup.py b/setup.py index c0f1e579..163c44be 100644 --- a/setup.py +++ b/setup.py @@ -79,6 +79,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', ], **kw ) diff --git a/tox.ini b/tox.ini index 43c391dd..83704dfc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33 +envlist = py26,py27,py32,py33,py34 [testenv] commands = pip install --use-mirrors -q -r tox-requirements.txt -- cgit v1.2.3 From f6e789298fd31914c257c7cb8cde0f7ba31793bc Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 11:57:59 +0200 Subject: Support specyfiying window/packet size per session Also update the documentation about values and the adverse effects that changing them might create. --- paramiko/transport.py | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index d9a48247..78d34414 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -164,6 +164,11 @@ class Transport (threading.Thread): address and used for communication. Exceptions from the ``socket`` call may be thrown in this case. + .. note:: Modifying the the window and packet sizes might have adverse + effects on your channels created from this transport. The + default values are the same as in the OpenSSH code base and have + been battle tested. + :param socket sock: a socket or socket-like object to create the session over. :param int default_window_size: @@ -536,18 +541,29 @@ class Transport (threading.Thread): """ return self.active - def open_session(self): + def open_session(self, window_size=None, max_packet_size=None): """ Request a new channel to the server, of type ``"session"``. This is just an alias for calling `open_channel` with an argument of ``"session"``. + .. note:: Modifying the the window and packet sizes might have adverse + effects on the session created. The default values are the same + as in the OpenSSH code base and have been battle tested. + + :param int window_size: + optional window size for this session. + :param int max_packet_size: + optional max packet size for this session. + :return: a new `.Channel` :raises SSHException: if the request is rejected or the session ends prematurely """ - return self.open_channel('session') + return self.open_channel('session', + window_size=window_size, + max_packet_size=max_packet_size) def open_x11_channel(self, src_addr=None): """ @@ -589,13 +605,22 @@ class Transport (threading.Thread): """ return self.open_channel('forwarded-tcpip', dest_addr, src_addr) - def open_channel(self, kind, dest_addr=None, src_addr=None): + def open_channel(self, + kind, + dest_addr=None, + src_addr=None, + window_size=None, + max_packet_size=None): """ Request a new channel to the server. `Channels <.Channel>` are socket-like objects used for the actual transfer of data across the session. You may only request a channel after negotiating encryption (using `connect` or `start_client`) and authenticating. + .. note:: Modifying the the window and packet sizes might have adverse + effects on the channel created. The default values are the same + as in the OpenSSH code base and have been battle tested. + :param str kind: the kind of channel requested (usually ``"session"``, ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``) @@ -605,6 +630,11 @@ class Transport (threading.Thread): ``"direct-tcpip"`` (ignored for other channel types) :param src_addr: the source address of this port forwarding, if ``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"`` + :param int window_size: + optional window size for this session. + :param int max_packet_size: + optional max packet size for this session. + :return: a new `.Channel` on success :raises SSHException: if the request is rejected or the session ends @@ -614,13 +644,17 @@ class Transport (threading.Thread): raise SSHException('SSH session not active') self.lock.acquire() try: + if window_size is None: + window_size = self.default_window_size + if max_packet_size is None: + max_packet_size = self.default_max_packet_size chanid = self._next_channel() m = Message() m.add_byte(cMSG_CHANNEL_OPEN) m.add_string(kind) m.add_int(chanid) - m.add_int(self.default_window_size) - m.add_int(self.default_max_packet_size) + m.add_int(window_size) + m.add_int(max_packet_size) if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'): m.add_string(dest_addr[0]) m.add_int(dest_addr[1]) @@ -634,7 +668,7 @@ class Transport (threading.Thread): self.channel_events[chanid] = event = threading.Event() self.channels_seen[chanid] = True chan._set_transport(self) - chan._set_window(self.default_window_size, self.default_max_packet_size) + chan._set_window(window_size, max_packet_size) finally: self.lock.release() self._send_user_message(m) @@ -678,6 +712,7 @@ class Transport (threading.Thread): :param callable handler: optional handler for incoming forwarded connections, of the form ``func(Channel, (str, int), (str, int))``. + :return: the port number (`int`) allocated by the server :raises SSHException: if the server refused the TCP forward request -- cgit v1.2.3 From 72c6b5ce3a49ec3c9cd68127091549c3fc4914cb Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 12:13:34 +0200 Subject: Expose the ability to set window/packet for sftp. --- paramiko/sftp_client.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 1caaf165..ac0071db 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -61,7 +61,7 @@ b_slash = b'/' class SFTPClient(BaseSFTP): """ SFTP client object. - + Used to open an SFTP session across an open SSH `.Transport` and perform remote file operations. """ @@ -98,16 +98,27 @@ class SFTPClient(BaseSFTP): raise SSHException('EOF during negotiation') self._log(INFO, 'Opened sftp connection (server version %d)' % server_version) - def from_transport(cls, t): + def from_transport(cls, t, window_size=None, max_packet_size=None): """ Create an SFTP client channel from an open `.Transport`. + Setting the window and packet sizes might affect the transfer speed. + The default settings in the `.Transport` class are the same as in + OpenSSH and should work adequately for both files transfers and + interactive sessions. + :param .Transport t: an open `.Transport` which is already authenticated + :param int window_size: + optional window size for the `.SFTPClient` session. + :param int max_packet_size: + optional max packet size for the `.SFTPClient` session.. + :return: a new `.SFTPClient` object, referring to an sftp session (channel) across the transport """ - chan = t.open_session() + chan = t.open_session(window_size=window_size, + max_packet_size=max_packet_size) if chan is None: return None chan.invoke_subsystem('sftp') @@ -578,7 +589,7 @@ class SFTPClient(BaseSFTP): .. versionadded:: 1.4 .. versionchanged:: 1.7.4 - ``callback`` and rich attribute return value added. + ``callback`` and rich attribute return value added. .. versionchanged:: 1.7.7 ``confirm`` param added. """ -- cgit v1.2.3 From e4e499fdf327a4f7db60bc9245bf9a8d6dc3da39 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 13:58:05 +0200 Subject: Whitespace fixes. --- paramiko/channel.py | 112 ++++++++++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 583809d5..164e9243 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -96,13 +96,13 @@ class Channel (object): self.combine_stderr = False self.exit_status = -1 self.origin_addr = None - + def __del__(self): try: self.close() except: pass - + def __repr__(self): """ Return a string representation of this object, for debugging. @@ -136,7 +136,7 @@ class Channel (object): :param int height: height (in characters) of the terminal screen :param int width_pixels: width (in pixels) of the terminal screen :param int height_pixels: height (in pixels) of the terminal screen - + :raises SSHException: if the request was rejected or the channel was closed """ @@ -162,14 +162,14 @@ 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, stdout, and stderr of the shell. - + Normally you would call `get_pty` before this, in which case the shell will operate through the pty, and the channel will be connected to the stdin and stdout of the pty. - + When the shell exits, the channel will be closed and can't be reused. You must open a new channel if you wish to open another shell. - + :raises SSHException: if the request was rejected or the channel was closed """ @@ -189,7 +189,7 @@ class Channel (object): Execute a command on the server. If the server allows it, the channel will then be directly connected to the stdin, stdout, and stderr of the command being executed. - + When the command finishes executing, the channel will be closed and can't be reused. You must open a new channel if you wish to execute another command. @@ -216,7 +216,7 @@ class Channel (object): Request a subsystem on the server (for example, ``sftp``). If the server allows it, the channel will then be directly connected to the requested subsystem. - + When the subsystem finishes, the channel will be closed and can't be reused. @@ -269,14 +269,14 @@ class Channel (object): status. You may use this to poll the process status if you don't want to block in `recv_exit_status`. Note that the server may not return an exit status in some cases (like bad servers). - + :return: ``True`` if `recv_exit_status` will return immediately, else ``False``. .. versionadded:: 1.7.3 """ return self.closed or self.status_event.isSet() - + def recv_exit_status(self): """ Return the exit status from the process on the server. This is @@ -284,9 +284,9 @@ class Channel (object): If the command hasn't finished yet, this method will wait until it does, or until the channel is closed. If no exit status is provided by the server, -1 is returned. - + :return: the exit code (as an `int`) of the process on the server. - + .. versionadded:: 1.2 """ self.status_event.wait() @@ -299,9 +299,9 @@ class Channel (object): really only makes sense in server mode.) Many clients expect to get some sort of status code back from an executed command after it completes. - + :param int status: the exit code of the process - + .. versionadded:: 1.2 """ # in many cases, the channel will not still be open here. @@ -313,32 +313,32 @@ class Channel (object): m.add_boolean(False) m.add_int(status) self.transport._send_user_message(m) - + def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None, single_connection=False, handler=None): """ Request an x11 session on this channel. If the server allows it, further x11 requests can be made from the server to the client, when an x11 application is run in a shell session. - + From RFC4254:: It is RECOMMENDED that the 'x11 authentication cookie' that is sent be a fake, random cookie, and that the cookie be checked and replaced by the real cookie when a connection request is received. - + If you omit the auth_cookie, a new secure random 128-bit value will be generated, used, and returned. You will need to use this value to verify incoming x11 requests and replace them with the actual local x11 cookie (which requires some knoweldge of the x11 protocol). - + If a handler is passed in, the handler is called from another thread whenever a new x11 connection arrives. The default handler queues up incoming x11 connections, which may be retrieved using `.Transport.accept`. The handler's calling signature is:: - + handler(channel: Channel, (address: str, port: int)) - + :param int screen_number: the x11 screen number (0, 10, etc) :param str auth_protocol: the name of the X11 authentication method used; if none is given, @@ -425,33 +425,33 @@ class Channel (object): def get_id(self): """ Return the `int` ID # for this channel. - + The channel ID is unique across a `.Transport` and usually a small number. It's also the number passed to `.ServerInterface.check_channel_request` when determining whether to accept a channel request in server mode. """ return self.chanid - + def set_combine_stderr(self, combine): """ Set whether stderr should be combined into stdout on this channel. The default is ``False``, but in some cases it may be convenient to have both streams combined. - + If this is ``False``, and `exec_command` is called (or ``invoke_shell`` with no pty), output to stderr will not show up through the `recv` and `recv_ready` calls. You will have to use `recv_stderr` and `recv_stderr_ready` to get stderr output. - + If this is ``True``, data will never show up via `recv_stderr` or `recv_stderr_ready`. - + :param bool combine: ``True`` if stderr output should be combined into stdout on this channel. :return: the previous setting (a `bool`). - + .. versionadded:: 1.1 """ data = bytes() @@ -560,7 +560,7 @@ class Channel (object): Returns true if data is buffered and ready to be read from this channel. A ``False`` result does not mean that the channel has closed; it means you may need to wait before more data arrives. - + :return: ``True`` if a `recv` call on this channel would immediately return at least one byte; ``False`` otherwise. @@ -576,7 +576,7 @@ class Channel (object): :param int nbytes: maximum number of bytes to read. :return: received data, as a `str` - + :raises socket.timeout: if no data is ready before the timeout set by `settimeout`. """ @@ -602,11 +602,11 @@ class Channel (object): channel's stderr stream. Only channels using `exec_command` or `invoke_shell` without a pty will ever have data on the stderr stream. - + :return: ``True`` if a `recv_stderr` call on this channel would immediately return at least one byte; ``False`` otherwise. - + .. versionadded:: 1.1 """ return self.in_stderr_buffer.read_ready() @@ -622,17 +622,17 @@ class Channel (object): :param int nbytes: maximum number of bytes to read. :return: received data as a `str` - + :raises socket.timeout: if no data is ready before the timeout set by `settimeout`. - + .. versionadded:: 1.1 """ try: out = self.in_stderr_buffer.read(nbytes, self.timeout) except PipeTimeout: raise socket.timeout() - + ack = self._check_add_window(len(out)) # no need to hold the channel lock when sending this if ack > 0: @@ -648,11 +648,11 @@ class Channel (object): """ Returns true if data can be written to this channel without blocking. This means the channel is either closed (so any write attempt would - return immediately) or there is at least one byte of space in the + return immediately) or there is at least one byte of space in the outbound buffer. If there is at least one byte of space in the outbound buffer, a `send` call will succeed immediately and return the number of bytes actually written. - + :return: ``True`` if a `send` call on this channel would immediately succeed or fail @@ -664,7 +664,7 @@ class Channel (object): return self.out_window_size > 0 finally: self.lock.release() - + def send(self, s): """ Send data to the channel. Returns the number of bytes sent, or 0 if @@ -705,13 +705,13 @@ class Channel (object): stream is closed. Applications are responsible for checking that all data has been sent: if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. - + :param str s: data to send. :return: number of bytes actually sent, as an `int`. - + :raises socket.timeout: if no data could be sent before the timeout set by `settimeout`. - + .. versionadded:: 1.1 """ size = len(s) @@ -745,7 +745,7 @@ class Channel (object): if sending stalled for longer than the timeout set by `settimeout`. :raises socket.error: if an error occured before the entire string was sent. - + .. note:: If the channel is closed while only part of the data hase been sent, there is no way to determine how much data (if any) was sent. @@ -765,14 +765,14 @@ class Channel (object): results. Unlike `send_stderr`, this method continues to send data from the given string until all data has been sent or an error occurs. Nothing is returned. - + :param str s: data to send to the client as "stderr" output. - + :raises socket.timeout: if sending stalled for longer than the timeout set by `settimeout`. :raises socket.error: if an error occured before the entire string was sent. - + .. versionadded:: 1.1 """ while s: @@ -797,18 +797,18 @@ class Channel (object): Return a file-like object associated with this channel's stderr stream. Only channels using `exec_command` or `invoke_shell` without a pty will ever have data on the stderr stream. - + The optional ``mode`` and ``bufsize`` arguments are interpreted the same way as by the built-in ``file()`` function in Python. For a client, it only makes sense to open this file for reading. For a server, it only makes sense to open this file for writing. - + :return: `.ChannelFile` object which can be used for Python file I/O. .. versionadded:: 1.1 """ return ChannelStderrFile(*([self] + list(params))) - + def fileno(self): """ Returns an OS-level file descriptor which can be used for polling, but @@ -822,7 +822,7 @@ class Channel (object): open at the same time.) :return: an OS-level file descriptor (`int`) - + .. warning:: This method causes channel reads to be slightly less efficient. """ @@ -861,7 +861,7 @@ class Channel (object): self.lock.release() if m is not None: self.transport._send_user_message(m) - + def shutdown_read(self): """ Shutdown the receiving side of this socket, closing the stream in @@ -869,11 +869,11 @@ class Channel (object): channel will fail instantly. This is a convenience method, equivalent to ``shutdown(0)``, for people who don't make it a habit to memorize unix constants from the 1970s. - + .. versionadded:: 1.2 """ self.shutdown(0) - + def shutdown_write(self): """ Shutdown the sending side of this socket, closing the stream in @@ -881,7 +881,7 @@ class Channel (object): channel will fail instantly. This is a convenience method, equivalent to ``shutdown(1)``, for people who don't make it a habit to memorize unix constants from the 1970s. - + .. versionadded:: 1.2 """ self.shutdown(1) @@ -899,14 +899,14 @@ class Channel (object): self.in_window_threshold = window_size // 10 self.in_window_sofar = 0 self._log(DEBUG, 'Max packet in: %d bytes' % max_packet_size) - + def _set_remote_channel(self, chanid, window_size, max_packet_size): self.remote_chanid = chanid self.out_window_size = window_size 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 @@ -941,7 +941,7 @@ class Channel (object): self._feed(s) else: self.in_stderr_buffer.feed(s) - + def _window_adjust(self, m): nbytes = m.get_int() self.lock.acquire() @@ -1183,7 +1183,7 @@ class Channel (object): if self.ultra_debug: self._log(DEBUG, 'window down to %d' % self.out_window_size) return size - + class ChannelFile (BufferedFile): """ @@ -1223,7 +1223,7 @@ class ChannelStderrFile (ChannelFile): def _read(self, size): return self.channel.recv_stderr(size) - + def _write(self, data): self.channel.sendall_stderr(data) return len(data) -- cgit v1.2.3 From 05d2b425c8082c2b5dfba90dd4f2db978d6d2774 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 13:58:34 +0200 Subject: Bump the MIN_PACKET_SIZE to what's in the RFC. --- paramiko/channel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 164e9243..df6b1cf6 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -40,7 +40,9 @@ from paramiko import pipe # lower bound on the max packet size we'll accept from the remote host -MIN_PACKET_SIZE = 1024 +# Minimum packet size is 32768 bytes according to +# http://www.ietf.org/rfc/rfc4254.txt +MIN_PACKET_SIZE = 2 ** 15 class Channel (object): -- cgit v1.2.3 From b5c537c4c88cc39323717531a46a3241eb7ca7f5 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 14:20:24 +0200 Subject: Add a MAX_WINDOW_SIZE constant. --- paramiko/channel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paramiko/channel.py b/paramiko/channel.py index df6b1cf6..90761d43 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -44,6 +44,8 @@ from paramiko import pipe # http://www.ietf.org/rfc/rfc4254.txt MIN_PACKET_SIZE = 2 ** 15 +# Max windows size according to http://www.ietf.org/rfc/rfc4254.txt +MAX_WINDOW_SIZE = 2**32 -1 class Channel (object): """ -- cgit v1.2.3 From 2dec0a7671d83f21a290e1a2b3ea9159da45757a Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 15:13:58 +0200 Subject: Add a utility method for value clamping. --- paramiko/util.py | 3 +++ tests/test_util.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/paramiko/util.py b/paramiko/util.py index f4ee3adc..d029f52e 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -320,3 +320,6 @@ def constant_time_bytes_eq(a, b): for i in (xrange if PY2 else range)(len(a)): res |= byte_ord(a[i]) ^ byte_ord(b[i]) return res == 0 + +def clamp_value(minimum, val, maximum): + return max(minimum, min(val, maximum)) diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..24f58076 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -333,3 +333,8 @@ IdentityFile something_%l_using_fqdn """ config = paramiko.util.parse_ssh_config(StringIO(test_config)) assert config.lookup('meh') # will die during lookup() if bug regresses + + def test_12_clamp_value(self): + self.assertEqual(32768, paramiko.util.clamp_value(32767, 32768, 32769)) + self.assertEqual(32767, paramiko.util.clamp_value(32767, 32765, 32769)) + self.assertEqual(32769, paramiko.util.clamp_value(32767, 32770, 32769)) -- cgit v1.2.3 From c0df755ead0eb54d798dd3f49b7dd86b8ad6baeb Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 14:22:59 +0200 Subject: Add sanitation methods for window and packet size. --- paramiko/transport.py | 21 +++++--- tests/test_transport.py | 124 ++++++++++++++++++++++++++++-------------------- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 78d34414..da67408f 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -31,7 +31,7 @@ from hashlib import md5, sha1 import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler -from paramiko.channel import Channel +from paramiko.channel import Channel, MIN_PACKET_SIZE, MAX_WINDOW_SIZE from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \ @@ -57,7 +57,7 @@ from paramiko.server import ServerInterface from paramiko.sftp_client import SFTPClient from paramiko.ssh_exception import (SSHException, BadAuthenticationType, ChannelException, ProxyCommandFailure) -from paramiko.util import retry_on_signal +from paramiko.util import retry_on_signal, clamp_value from Crypto.Cipher import Blowfish, AES, DES3, ARC4 try: @@ -644,10 +644,8 @@ class Transport (threading.Thread): raise SSHException('SSH session not active') self.lock.acquire() try: - if window_size is None: - window_size = self.default_window_size - if max_packet_size is None: - max_packet_size = self.default_max_packet_size + window_size = self._sanitize_window_size(window_size) + max_packet_size = self._sanitize_packet_size(max_packet_size) chanid = self._next_channel() m = Message() m.add_byte(cMSG_CHANNEL_OPEN) @@ -1434,6 +1432,17 @@ class Transport (threading.Thread): finally: self.lock.release() + def _sanitize_window_size(self, window_size): + if window_size is None: + window_size = self.default_window_size + return clamp_value(MIN_PACKET_SIZE, window_size, MAX_WINDOW_SIZE) + + def _sanitize_packet_size(self, max_packet_size): + if max_packet_size is None: + max_packet_size = self.default_max_packet_size + return clamp_value(MIN_PACKET_SIZE, max_packet_size, MAX_WINDOW_SIZE) + + 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 diff --git a/tests/test_transport.py b/tests/test_transport.py index d84747b6..f5795d0e 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -31,7 +31,9 @@ from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey SSHException, ChannelException from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED -from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST +from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST, \ + MIN_PACKET_SIZE, MAX_WINDOW_SIZE, \ + DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE from paramiko.py3compat import bytes from paramiko.message import Message from tests.loop import LoopSocket @@ -55,7 +57,7 @@ class NullServer (ServerInterface): paranoid_did_password = False paranoid_did_public_key = False paranoid_key = DSSKey.from_private_key_file(test_path('test_dss.key')) - + def get_allowed_auths(self, username): if username == 'slowdive': return 'publickey,password' @@ -78,24 +80,24 @@ class NullServer (ServerInterface): def check_channel_shell_request(self, channel): return True - + def check_global_request(self, kind, msg): self._global_request = kind return False - + def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number): self._x11_single_connection = single_connection self._x11_auth_protocol = auth_protocol self._x11_auth_cookie = auth_cookie self._x11_screen_number = screen_number return True - + def check_port_forward_request(self, addr, port): self._listen = socket.socket() self._listen.bind(('127.0.0.1', 0)) self._listen.listen(1) return self._listen.getsockname()[1] - + def cancel_port_forward_request(self, addr, port): self._listen.close() self._listen = None @@ -123,12 +125,12 @@ class TransportTest(ParamikoTest): host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = RSAKey(data=host_key.asbytes()) self.ts.add_server_key(host_key) - + if client_options is not None: client_options(self.tc.get_security_options()) if server_options is not None: server_options(self.ts.get_security_options()) - + event = threading.Event() self.server = NullServer() self.assertTrue(not event.isSet()) @@ -155,7 +157,7 @@ class TransportTest(ParamikoTest): self.assertTrue(False) except TypeError: pass - + def test_2_compute_key(self): self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929 self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3' @@ -208,7 +210,7 @@ class TransportTest(ParamikoTest): event.wait(1.0) self.assertTrue(event.isSet()) self.assertTrue(self.ts.is_active()) - + def test_4_special(self): """ verify that the client can demand odd handshake settings, and can @@ -222,7 +224,7 @@ class TransportTest(ParamikoTest): self.assertEqual('aes256-cbc', self.tc.remote_cipher) self.assertEqual(12, self.tc.packetizer.get_mac_size_out()) self.assertEqual(12, self.tc.packetizer.get_mac_size_in()) - + self.tc.send_ignore(1024) self.tc.renegotiate_keys() self.ts.send_ignore(1024) @@ -236,7 +238,7 @@ class TransportTest(ParamikoTest): self.tc.set_keepalive(1) time.sleep(2) self.assertEqual('keepalive@lag.net', self.server._global_request) - + def test_6_exec_command(self): """ verify that exec_command() does something reasonable. @@ -250,7 +252,7 @@ class TransportTest(ParamikoTest): self.assertTrue(False) except SSHException: pass - + chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) @@ -264,7 +266,7 @@ class TransportTest(ParamikoTest): f = chan.makefile_stderr() self.assertEqual('This is on stderr.\n', f.readline()) self.assertEqual('', f.readline()) - + # now try it with combined stdout/stderr chan = self.tc.open_session() chan.exec_command('yes') @@ -273,7 +275,7 @@ class TransportTest(ParamikoTest): schan.send_stderr('This is on stderr.\n') schan.close() - chan.set_combine_stderr(True) + chan.set_combine_stderr(True) f = chan.makefile() self.assertEqual('Hello there.\n', f.readline()) self.assertEqual('This is on stderr.\n', f.readline()) @@ -320,7 +322,7 @@ class TransportTest(ParamikoTest): schan.shutdown_write() schan.send_exit_status(23) schan.close() - + f = chan.makefile() self.assertEqual('Hello there.\n', f.readline()) self.assertEqual('', f.readline()) @@ -342,14 +344,14 @@ class TransportTest(ParamikoTest): chan.invoke_shell() schan = self.ts.accept(1.0) - # nothing should be ready + # nothing should be ready r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) self.assertEqual([], w) self.assertEqual([], e) - + schan.send('hello\n') - + # something should be ready now (give it 1 second to appear) for i in range(10): r, w, e = select.select([chan], [], [], 0.1) @@ -361,7 +363,7 @@ class TransportTest(ParamikoTest): self.assertEqual([], e) self.assertEqual(b'hello\n', chan.recv(6)) - + # and, should be dead again now r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) @@ -369,7 +371,7 @@ class TransportTest(ParamikoTest): self.assertEqual([], e) schan.close() - + # detect eof? for i in range(10): r, w, e = select.select([chan], [], [], 0.1) @@ -380,14 +382,14 @@ class TransportTest(ParamikoTest): self.assertEqual([], w) self.assertEqual([], e) self.assertEqual(bytes(), chan.recv(16)) - + # make sure the pipe is still open for now... p = chan._pipe self.assertEqual(False, p._closed) chan.close() # ...and now is closed. self.assertEqual(True, p._closed) - + def test_B_renegotiate(self): """ verify that a transport can correctly renegotiate mid-stream. @@ -402,7 +404,7 @@ class TransportTest(ParamikoTest): for i in range(20): chan.send('x' * 1024) chan.close() - + # allow a few seconds for the rekeying to complete for i in range(50): if self.tc.H != self.tc.session_id: @@ -441,28 +443,28 @@ class TransportTest(ParamikoTest): chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) - + requested = [] def handler(c, addr_port): addr, port = addr_port requested.append((addr, port)) self.tc._queue_incoming_channel(c) - + self.assertEqual(None, getattr(self.server, '_x11_screen_number', None)) cookie = chan.request_x11(0, single_connection=True, handler=handler) self.assertEqual(0, self.server._x11_screen_number) self.assertEqual('MIT-MAGIC-COOKIE-1', self.server._x11_auth_protocol) self.assertEqual(cookie, self.server._x11_auth_cookie) self.assertEqual(True, self.server._x11_single_connection) - + x11_server = self.ts.open_x11_channel(('localhost', 6093)) x11_client = self.tc.accept() self.assertEqual('localhost', requested[0][0]) self.assertEqual(6093, requested[0][1]) - + x11_server.send('hello') self.assertEqual(b'hello', x11_client.recv(5)) - + x11_server.close() x11_client.close() chan.close() @@ -477,13 +479,13 @@ class TransportTest(ParamikoTest): chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) - + requested = [] def handler(c, origin_addr_port, server_addr_port): requested.append(origin_addr_port) requested.append(server_addr_port) self.tc._queue_incoming_channel(c) - + port = self.tc.request_port_forward('127.0.0.1', 0, handler) self.assertEqual(port, self.server._listen.getsockname()[1]) @@ -492,14 +494,14 @@ class TransportTest(ParamikoTest): ss, _ = self.server._listen.accept() sch = self.ts.open_forwarded_tcpip_channel(ss.getsockname(), ss.getpeername()) cch = self.tc.accept() - + sch.send('hello') self.assertEqual(b'hello', cch.recv(5)) sch.close() cch.close() ss.close() cs.close() - + # now cancel it. self.tc.cancel_port_forward('127.0.0.1', port) self.assertTrue(self.server._listen is None) @@ -513,7 +515,7 @@ class TransportTest(ParamikoTest): chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) - + # open a port on the "server" that the client will ask to forward to. greeting_server = socket.socket() greeting_server.bind(('127.0.0.1', 0)) @@ -524,13 +526,13 @@ class TransportTest(ParamikoTest): sch = self.ts.accept(1.0) cch = socket.socket() cch.connect(self.server._tcpip_dest) - + ss, _ = greeting_server.accept() ss.send(b'Hello!\n') ss.close() sch.send(cch.recv(8192)) sch.close() - + self.assertEqual(b'Hello!\n', cs.recv(7)) cs.close() @@ -544,14 +546,14 @@ class TransportTest(ParamikoTest): chan.invoke_shell() schan = self.ts.accept(1.0) - # nothing should be ready + # nothing should be ready r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) self.assertEqual([], w) self.assertEqual([], e) - + schan.send_stderr('hello\n') - + # something should be ready now (give it 1 second to appear) for i in range(10): r, w, e = select.select([chan], [], [], 0.1) @@ -563,7 +565,7 @@ class TransportTest(ParamikoTest): self.assertEqual([], e) self.assertEqual(b'hello\n', chan.recv_stderr(6)) - + # and, should be dead again now r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) @@ -600,10 +602,10 @@ class TransportTest(ParamikoTest): def test_I_rekey_deadlock(self): """ Regression test for deadlock when in-transit messages are received after MSG_KEXINIT is sent - + Note: When this test fails, it may leak threads. """ - + # Test for an obscure deadlocking bug that can occur if we receive # certain messages while initiating a key exchange. # @@ -620,7 +622,7 @@ class TransportTest(ParamikoTest): # NeedRekeyException. # 4. In response to NeedRekeyException, the transport thread sends # MSG_KEXINIT to the remote host. - # + # # On the remote host (using any SSH implementation): # 5. The MSG_CHANNEL_DATA is received, and MSG_CHANNEL_WINDOW_ADJUST is sent. # 6. The MSG_KEXINIT is received, and a corresponding MSG_KEXINIT is sent. @@ -655,7 +657,7 @@ class TransportTest(ParamikoTest): self.done_event = done_event self.watchdog_event = threading.Event() self.last = None - + def run(self): try: for i in range(1, 1+self.iterations): @@ -667,7 +669,7 @@ class TransportTest(ParamikoTest): finally: self.done_event.set() self.watchdog_event.set() - + class ReceiveThread(threading.Thread): def __init__(self, chan, done_event): threading.Thread.__init__(self, None, None, self.__class__.__name__) @@ -675,7 +677,7 @@ class TransportTest(ParamikoTest): self.chan = chan self.done_event = done_event self.watchdog_event = threading.Event() - + def run(self): try: while not self.done_event.isSet(): @@ -688,10 +690,10 @@ class TransportTest(ParamikoTest): finally: self.done_event.set() self.watchdog_event.set() - + self.setup_test_server() self.ts.packetizer.REKEY_BYTES = 2048 - + chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) @@ -713,7 +715,7 @@ class TransportTest(ParamikoTest): self._send_message(m2) return _negotiate_keys(self, m) self.tc._handler_table[MSG_KEXINIT] = _negotiate_keys_wrapper - + # Parameters for the test iterations = 500 # The deadlock does not happen every time, but it # should after many iterations. @@ -725,12 +727,12 @@ class TransportTest(ParamikoTest): # Start the sending thread st = SendThread(schan, iterations, done_event) st.start() - + # Start the receiving thread rt = ReceiveThread(chan, done_event) rt.start() - # Act as a watchdog timer, checking + # Act as a watchdog timer, checking deadlocked = False while not deadlocked and not done_event.isSet(): for event in (st.watchdog_event, rt.watchdog_event): @@ -741,7 +743,7 @@ class TransportTest(ParamikoTest): deadlocked = True break event.clear() - + # Tell the threads to stop (if they haven't already stopped). Note # that if one or more threads are deadlocked, they might hang around # forever (until the process exits). @@ -753,3 +755,21 @@ class TransportTest(ParamikoTest): # Close the channels schan.close() chan.close() + + def test_J_sanitze_packet_size(self): + """ + verify that we conform to the rfc of packet and window sizes. + """ + for val, correct in [(32767, MIN_PACKET_SIZE), + (None, DEFAULT_MAX_PACKET_SIZE), + (2**32, MAX_WINDOW_SIZE)]: + self.assertEqual(self.tc._sanitize_packet_size(val), correct) + + def test_K_sanitze_window_size(self): + """ + verify that we conform to the rfc of packet and window sizes. + """ + for val, correct in [(32767, MIN_PACKET_SIZE), + (None, DEFAULT_WINDOW_SIZE), + (2**32, MAX_WINDOW_SIZE)]: + self.assertEqual(self.tc._sanitize_window_size(val), correct) -- cgit v1.2.3 From 83aa7335f7121f9259a41ae27cab5fb8dad09b96 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 14:28:34 +0200 Subject: Move window and packet constants. Centralise them to the common module to avoid import cycles. Also add constants for default values and use them in the transport class. --- paramiko/channel.py | 10 +--------- paramiko/common.py | 11 +++++++++++ paramiko/transport.py | 9 +++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 90761d43..d6ad1a52 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -30,7 +30,7 @@ from paramiko import util from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \ cMSG_CHANNEL_DATA, cMSG_CHANNEL_EXTENDED_DATA, DEBUG, ERROR, \ cMSG_CHANNEL_SUCCESS, cMSG_CHANNEL_FAILURE, cMSG_CHANNEL_EOF, \ - cMSG_CHANNEL_CLOSE + cMSG_CHANNEL_CLOSE, MIN_PACKET_SIZE from paramiko.message import Message from paramiko.py3compat import bytes_types from paramiko.ssh_exception import SSHException @@ -39,14 +39,6 @@ from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe -# lower bound on the max packet size we'll accept from the remote host -# Minimum packet size is 32768 bytes according to -# http://www.ietf.org/rfc/rfc4254.txt -MIN_PACKET_SIZE = 2 ** 15 - -# Max windows size according to http://www.ietf.org/rfc/rfc4254.txt -MAX_WINDOW_SIZE = 2**32 -1 - class Channel (object): """ A secure tunnel across an SSH `.Transport`. A Channel is meant to behave diff --git a/paramiko/common.py b/paramiko/common.py index 18298922..93f71df0 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -171,3 +171,14 @@ CRITICAL = logging.CRITICAL # Common IO/select/etc sleep period, in seconds io_sleep = 0.01 + +DEFAULT_WINDOW_SIZE = 64 * 2 ** 15 +DEFAULT_MAX_PACKET_SIZE = 2 ** 15 + +# lower bound on the max packet size we'll accept from the remote host +# Minimum packet size is 32768 bytes according to +# http://www.ietf.org/rfc/rfc4254.txt +MIN_PACKET_SIZE = 2 ** 15 + +# Max windows size according to http://www.ietf.org/rfc/rfc4254.txt +MAX_WINDOW_SIZE = 2**32 -1 diff --git a/paramiko/transport.py b/paramiko/transport.py index da67408f..fb7ab113 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -31,7 +31,7 @@ from hashlib import md5, sha1 import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler -from paramiko.channel import Channel, MIN_PACKET_SIZE, MAX_WINDOW_SIZE +from paramiko.channel import Channel from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \ @@ -42,7 +42,8 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, \ MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \ MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \ - MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE + MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_PACKET_SIZE, MAX_WINDOW_SIZE, \ + DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey from paramiko.kex_gex import KexGex @@ -137,8 +138,8 @@ class Transport (threading.Thread): def __init__(self, sock, - default_window_size=64 * 2 ** 15, - default_max_packet_size=2 ** 15): + default_window_size=DEFAULT_WINDOW_SIZE, + default_max_packet_size=DEFAULT_MAX_PACKET_SIZE): """ Create a new SSH session over an existing socket, or socket-like object. This only creates the `.Transport` object; it doesn't begin the -- cgit v1.2.3 From b0ffd7c9c8d0941ee95e6f133cb6697bfb6a9b99 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 14 Aug 2014 15:25:44 +0200 Subject: Use the new packet sanitize method in channel. --- paramiko/channel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index d6ad1a52..0042ab15 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -30,7 +30,7 @@ from paramiko import util from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \ cMSG_CHANNEL_DATA, cMSG_CHANNEL_EXTENDED_DATA, DEBUG, ERROR, \ cMSG_CHANNEL_SUCCESS, cMSG_CHANNEL_FAILURE, cMSG_CHANNEL_EOF, \ - cMSG_CHANNEL_CLOSE, MIN_PACKET_SIZE + cMSG_CHANNEL_CLOSE from paramiko.message import Message from paramiko.py3compat import bytes_types from paramiko.ssh_exception import SSHException @@ -899,7 +899,8 @@ class Channel (object): def _set_remote_channel(self, chanid, window_size, max_packet_size): self.remote_chanid = chanid self.out_window_size = window_size - self.out_max_packet_size = max(max_packet_size, MIN_PACKET_SIZE) + self.out_max_packet_size = self.transport. \ + _sanitize_packet_size(max_packet_size) self.active = 1 self._log(DEBUG, 'Max packet out: %d bytes' % max_packet_size) -- cgit v1.2.3 From 3b9eaefebb1b2e55b6392d5c55ac07594cc057e3 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Thu, 14 Aug 2014 20:43:55 +0100 Subject: Test banner timeout. --- tests/test_client.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 7e5c80b4..1867fb78 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -27,6 +27,7 @@ import unittest import weakref import warnings import os +import time from tests.util import test_path import paramiko from paramiko.common import PY2 @@ -72,12 +73,14 @@ class SSHClientTest (unittest.TestCase): if hasattr(self, attr): getattr(self, attr).close() - def _run(self): + def _run(self, delay=0): self.socks, addr = self.sockl.accept() self.ts = paramiko.Transport(self.socks) host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) self.ts.add_server_key(host_key) server = NullServer() + if delay: + time.sleep(delay) self.ts.start_server(self.event, server) def test_1_client(self): @@ -252,3 +255,25 @@ class SSHClientTest (unittest.TestCase): gc.collect() self.assertTrue(p() is None) + + def test_7_banner_timeout(self): + """ + verify that the SSHClient has a configurable banner timeout. + """ + # Start the thread with a 5 second wait. + threading.Thread(target=self._run, args=(5,)).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + + self.tc = paramiko.SSHClient() + self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) + # Connect with a three second banner timeout. + self.assertRaises( + paramiko.SSHException, + self.tc.connect, + self.addr, + self.port, + username='slowdive', + password='pygmalion', + banner_timeout=3 + ) -- cgit v1.2.3 From 021160d8db62b8873f7deda3f7d89750941d2704 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Thu, 14 Aug 2014 21:35:31 +0100 Subject: Use a shorter timeout in banner timeout test. --- tests/test_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1867fb78..48e2e9b0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -260,14 +260,14 @@ class SSHClientTest (unittest.TestCase): """ verify that the SSHClient has a configurable banner timeout. """ - # Start the thread with a 5 second wait. - threading.Thread(target=self._run, args=(5,)).start() + # Start the thread with a 1 second wait. + threading.Thread(target=self._run, args=(1,)).start() host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=host_key.asbytes()) self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - # Connect with a three second banner timeout. + # Connect with a half second banner timeout. self.assertRaises( paramiko.SSHException, self.tc.connect, @@ -275,5 +275,5 @@ class SSHClientTest (unittest.TestCase): self.port, username='slowdive', password='pygmalion', - banner_timeout=3 + banner_timeout=0.5 ) -- cgit v1.2.3 From 4dff7074d1f3f331047d67bff5a9780c0e8457d0 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 12:16:38 +0200 Subject: Don't try to join() if we are the current thread. This fixes #354. --- paramiko/transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 406626a7..3463cfcf 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1256,7 +1256,7 @@ class Transport (threading.Thread): def stop_thread(self): self.active = False self.packetizer.close() - while self.isAlive(): + while self.is_alive() and (self is not threading.current_thread()): self.join(10) ### internals... -- cgit v1.2.3 From f88189835eebab72a3792f67de4aacc7fca68df6 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 12:21:44 +0200 Subject: Document what is breaking in the client tests. --- tests/test_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 7e5c80b4..93574fc8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -221,6 +221,8 @@ class SSHClientTest (unittest.TestCase): """ # Unclear why this is borked on Py3, but it is, and does not seem worth # pursuing at the moment. + # XXX: It's the release of the references to e.g packetizer that fails + # in py3... if not PY2: return threading.Thread(target=self._run).start() -- cgit v1.2.3 From 6fe52ccac45f527ced2d4dc3f2d4911f9db7a396 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 12:22:31 +0200 Subject: Document potential brokeness. --- paramiko/channel.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/paramiko/channel.py b/paramiko/channel.py index 583809d5..a7d71212 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -683,6 +683,9 @@ class Channel (object): self.lock.acquire() try: size = self._wait_for_send_window(size) + # Instead of returning 0 here when the channel is closed, we might + # want to raise EOFError or similar. Should we just do as in the + # send_all method, raise a socket.error? if size == 0: # eof or similar return 0 @@ -718,6 +721,9 @@ class Channel (object): self.lock.acquire() try: size = self._wait_for_send_window(size) + # Instead of returning 0 here when the channel is closed, we might + # want to raise EOFError or similar. Should we just do as in the + # send_all method, raise a socket.error? if size == 0: # eof or similar return 0 -- cgit v1.2.3 From d23be11a7a8403cc18f89e4396bb8e4d2f8ae72e Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:30:37 +0200 Subject: Strip whitespace. --- tests/test_buffered_pipe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index a53081a9..1045a925 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -48,12 +48,12 @@ class BufferedPipeTest(ParamikoTest): self.assertTrue(p.read_ready()) data = p.read(6) self.assertEqual(b'hello.', data) - + p.feed('plus/minus') self.assertEqual(b'plu', p.read(3)) self.assertEqual(b's/m', p.read(3)) self.assertEqual(b'inus', p.read(4)) - + p.close() self.assertTrue(not p.read_ready()) self.assertEqual(b'', p.read(1)) -- cgit v1.2.3 From 888c17a9c4e8d8b1460aa42197803501a04091b4 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:32:35 +0200 Subject: Strip whitespace. --- tests/test_transport.py | 102 ++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index 485a18e8..ac744b26 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -55,7 +55,7 @@ class NullServer (ServerInterface): paranoid_did_password = False paranoid_did_public_key = False paranoid_key = DSSKey.from_private_key_file(test_path('test_dss.key')) - + def get_allowed_auths(self, username): if username == 'slowdive': return 'publickey,password' @@ -78,24 +78,24 @@ class NullServer (ServerInterface): def check_channel_shell_request(self, channel): return True - + def check_global_request(self, kind, msg): self._global_request = kind return False - + def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number): self._x11_single_connection = single_connection self._x11_auth_protocol = auth_protocol self._x11_auth_cookie = auth_cookie self._x11_screen_number = screen_number return True - + def check_port_forward_request(self, addr, port): self._listen = socket.socket() self._listen.bind(('127.0.0.1', 0)) self._listen.listen(1) return self._listen.getsockname()[1] - + def cancel_port_forward_request(self, addr, port): self._listen.close() self._listen = None @@ -123,12 +123,12 @@ class TransportTest(ParamikoTest): host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = RSAKey(data=host_key.asbytes()) self.ts.add_server_key(host_key) - + if client_options is not None: client_options(self.tc.get_security_options()) if server_options is not None: server_options(self.ts.get_security_options()) - + event = threading.Event() self.server = NullServer() self.assertTrue(not event.isSet()) @@ -155,7 +155,7 @@ class TransportTest(ParamikoTest): self.assertTrue(False) except TypeError: pass - + def test_2_compute_key(self): self.tc.K = 123281095979686581523377256114209720774539068973101330872763622971399429481072519713536292772709507296759612401802191955568143056534122385270077606457721553469730659233569339356140085284052436697480759510519672848743794433460113118986816826624865291116513647975790797391795651716378444844877749505443714557929 self.tc.H = b'\x0C\x83\x07\xCD\xE6\x85\x6F\xF3\x0B\xA9\x36\x84\xEB\x0F\x04\xC2\x52\x0E\x9E\xD3' @@ -208,7 +208,7 @@ class TransportTest(ParamikoTest): event.wait(1.0) self.assertTrue(event.isSet()) self.assertTrue(self.ts.is_active()) - + def test_4_special(self): """ verify that the client can demand odd handshake settings, and can @@ -222,7 +222,7 @@ class TransportTest(ParamikoTest): self.assertEqual('aes256-cbc', self.tc.remote_cipher) self.assertEqual(12, self.tc.packetizer.get_mac_size_out()) self.assertEqual(12, self.tc.packetizer.get_mac_size_in()) - + self.tc.send_ignore(1024) self.tc.renegotiate_keys() self.ts.send_ignore(1024) @@ -236,7 +236,7 @@ class TransportTest(ParamikoTest): self.tc.set_keepalive(1) time.sleep(2) self.assertEqual('keepalive@lag.net', self.server._global_request) - + def test_6_exec_command(self): """ verify that exec_command() does something reasonable. @@ -250,7 +250,7 @@ class TransportTest(ParamikoTest): self.assertTrue(False) except SSHException: pass - + chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) @@ -264,7 +264,7 @@ class TransportTest(ParamikoTest): f = chan.makefile_stderr() self.assertEqual('This is on stderr.\n', f.readline()) self.assertEqual('', f.readline()) - + # now try it with combined stdout/stderr chan = self.tc.open_session() chan.exec_command('yes') @@ -273,7 +273,7 @@ class TransportTest(ParamikoTest): schan.send_stderr('This is on stderr.\n') schan.close() - chan.set_combine_stderr(True) + chan.set_combine_stderr(True) f = chan.makefile() self.assertEqual('Hello there.\n', f.readline()) self.assertEqual('This is on stderr.\n', f.readline()) @@ -320,7 +320,7 @@ class TransportTest(ParamikoTest): schan.shutdown_write() schan.send_exit_status(23) schan.close() - + f = chan.makefile() self.assertEqual('Hello there.\n', f.readline()) self.assertEqual('', f.readline()) @@ -342,14 +342,14 @@ class TransportTest(ParamikoTest): chan.invoke_shell() schan = self.ts.accept(1.0) - # nothing should be ready + # nothing should be ready r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) self.assertEqual([], w) self.assertEqual([], e) - + schan.send('hello\n') - + # something should be ready now (give it 1 second to appear) for i in range(10): r, w, e = select.select([chan], [], [], 0.1) @@ -361,7 +361,7 @@ class TransportTest(ParamikoTest): self.assertEqual([], e) self.assertEqual(b'hello\n', chan.recv(6)) - + # and, should be dead again now r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) @@ -369,7 +369,7 @@ class TransportTest(ParamikoTest): self.assertEqual([], e) schan.close() - + # detect eof? for i in range(10): r, w, e = select.select([chan], [], [], 0.1) @@ -380,14 +380,14 @@ class TransportTest(ParamikoTest): self.assertEqual([], w) self.assertEqual([], e) self.assertEqual(bytes(), chan.recv(16)) - + # make sure the pipe is still open for now... p = chan._pipe self.assertEqual(False, p._closed) chan.close() # ...and now is closed. self.assertEqual(True, p._closed) - + def test_B_renegotiate(self): """ verify that a transport can correctly renegotiate mid-stream. @@ -402,7 +402,7 @@ class TransportTest(ParamikoTest): for i in range(20): chan.send('x' * 1024) chan.close() - + # allow a few seconds for the rekeying to complete for i in range(50): if self.tc.H != self.tc.session_id: @@ -441,28 +441,28 @@ class TransportTest(ParamikoTest): chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) - + requested = [] def handler(c, addr_port): addr, port = addr_port requested.append((addr, port)) self.tc._queue_incoming_channel(c) - + self.assertEqual(None, getattr(self.server, '_x11_screen_number', None)) cookie = chan.request_x11(0, single_connection=True, handler=handler) self.assertEqual(0, self.server._x11_screen_number) self.assertEqual('MIT-MAGIC-COOKIE-1', self.server._x11_auth_protocol) self.assertEqual(cookie, self.server._x11_auth_cookie) self.assertEqual(True, self.server._x11_single_connection) - + x11_server = self.ts.open_x11_channel(('localhost', 6093)) x11_client = self.tc.accept() self.assertEqual('localhost', requested[0][0]) self.assertEqual(6093, requested[0][1]) - + x11_server.send('hello') self.assertEqual(b'hello', x11_client.recv(5)) - + x11_server.close() x11_client.close() chan.close() @@ -477,13 +477,13 @@ class TransportTest(ParamikoTest): chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) - + requested = [] def handler(c, origin_addr_port, server_addr_port): requested.append(origin_addr_port) requested.append(server_addr_port) self.tc._queue_incoming_channel(c) - + port = self.tc.request_port_forward('127.0.0.1', 0, handler) self.assertEqual(port, self.server._listen.getsockname()[1]) @@ -492,14 +492,14 @@ class TransportTest(ParamikoTest): ss, _ = self.server._listen.accept() sch = self.ts.open_forwarded_tcpip_channel(ss.getsockname(), ss.getpeername()) cch = self.tc.accept() - + sch.send('hello') self.assertEqual(b'hello', cch.recv(5)) sch.close() cch.close() ss.close() cs.close() - + # now cancel it. self.tc.cancel_port_forward('127.0.0.1', port) self.assertTrue(self.server._listen is None) @@ -513,7 +513,7 @@ class TransportTest(ParamikoTest): chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) - + # open a port on the "server" that the client will ask to forward to. greeting_server = socket.socket() greeting_server.bind(('127.0.0.1', 0)) @@ -524,13 +524,13 @@ class TransportTest(ParamikoTest): sch = self.ts.accept(1.0) cch = socket.socket() cch.connect(self.server._tcpip_dest) - + ss, _ = greeting_server.accept() ss.send(b'Hello!\n') ss.close() sch.send(cch.recv(8192)) sch.close() - + self.assertEqual(b'Hello!\n', cs.recv(7)) cs.close() @@ -544,14 +544,14 @@ class TransportTest(ParamikoTest): chan.invoke_shell() schan = self.ts.accept(1.0) - # nothing should be ready + # nothing should be ready r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) self.assertEqual([], w) self.assertEqual([], e) - + schan.send_stderr('hello\n') - + # something should be ready now (give it 1 second to appear) for i in range(10): r, w, e = select.select([chan], [], [], 0.1) @@ -563,7 +563,7 @@ class TransportTest(ParamikoTest): self.assertEqual([], e) self.assertEqual(b'hello\n', chan.recv_stderr(6)) - + # and, should be dead again now r, w, e = select.select([chan], [], [], 0.1) self.assertEqual([], r) @@ -599,10 +599,10 @@ class TransportTest(ParamikoTest): def test_I_rekey_deadlock(self): """ Regression test for deadlock when in-transit messages are received after MSG_KEXINIT is sent - + Note: When this test fails, it may leak threads. """ - + # Test for an obscure deadlocking bug that can occur if we receive # certain messages while initiating a key exchange. # @@ -619,7 +619,7 @@ class TransportTest(ParamikoTest): # NeedRekeyException. # 4. In response to NeedRekeyException, the transport thread sends # MSG_KEXINIT to the remote host. - # + # # On the remote host (using any SSH implementation): # 5. The MSG_CHANNEL_DATA is received, and MSG_CHANNEL_WINDOW_ADJUST is sent. # 6. The MSG_KEXINIT is received, and a corresponding MSG_KEXINIT is sent. @@ -654,7 +654,7 @@ class TransportTest(ParamikoTest): self.done_event = done_event self.watchdog_event = threading.Event() self.last = None - + def run(self): try: for i in range(1, 1+self.iterations): @@ -666,7 +666,7 @@ class TransportTest(ParamikoTest): finally: self.done_event.set() self.watchdog_event.set() - + class ReceiveThread(threading.Thread): def __init__(self, chan, done_event): threading.Thread.__init__(self, None, None, self.__class__.__name__) @@ -674,7 +674,7 @@ class TransportTest(ParamikoTest): self.chan = chan self.done_event = done_event self.watchdog_event = threading.Event() - + def run(self): try: while not self.done_event.isSet(): @@ -687,10 +687,10 @@ class TransportTest(ParamikoTest): finally: self.done_event.set() self.watchdog_event.set() - + self.setup_test_server() self.ts.packetizer.REKEY_BYTES = 2048 - + chan = self.tc.open_session() chan.exec_command('yes') schan = self.ts.accept(1.0) @@ -712,7 +712,7 @@ class TransportTest(ParamikoTest): self._send_message(m2) return _negotiate_keys(self, m) self.tc._handler_table[MSG_KEXINIT] = _negotiate_keys_wrapper - + # Parameters for the test iterations = 500 # The deadlock does not happen every time, but it # should after many iterations. @@ -724,12 +724,12 @@ class TransportTest(ParamikoTest): # Start the sending thread st = SendThread(schan, iterations, done_event) st.start() - + # Start the receiving thread rt = ReceiveThread(chan, done_event) rt.start() - # Act as a watchdog timer, checking + # Act as a watchdog timer, checking deadlocked = False while not deadlocked and not done_event.isSet(): for event in (st.watchdog_event, rt.watchdog_event): @@ -740,7 +740,7 @@ class TransportTest(ParamikoTest): deadlocked = True break event.clear() - + # Tell the threads to stop (if they haven't already stopped). Note # that if one or more threads are deadlocked, they might hang around # forever (until the process exits). -- cgit v1.2.3 From 163caabf5e4a2a63216c2192dc476d4184ab81b3 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:33:25 +0200 Subject: Remove all occurences of ParamikoTest. Sorry paramiko, it's time to put on the big boy pants. You no longer support old as hell versions of python. --- tests/test_buffered_pipe.py | 5 ++--- tests/test_transport.py | 5 +++-- tests/test_util.py | 5 ++--- tests/util.py | 10 ---------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index 1045a925..3b8f97ad 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -22,11 +22,10 @@ Some unit tests for BufferedPipe. import threading import time +import unittest from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe -from tests.util import ParamikoTest - def delay_thread(p): p.feed('a') @@ -40,7 +39,7 @@ def close_thread(p): p.close() -class BufferedPipeTest(ParamikoTest): +class BufferedPipeTest(unittest.TestCase): def test_1_buffered_pipe(self): p = BufferedPipe() self.assertTrue(not p.read_ready()) diff --git a/tests/test_transport.py b/tests/test_transport.py index ac744b26..5c77a321 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -26,6 +26,7 @@ import socket import time import threading import random +import unittest from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \ SSHException, ChannelException @@ -35,7 +36,7 @@ from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST from paramiko.py3compat import bytes from paramiko.message import Message from tests.loop import LoopSocket -from tests.util import ParamikoTest, test_path +from tests.util import test_path LONG_BANNER = """\ @@ -105,7 +106,7 @@ class NullServer (ServerInterface): return OPEN_SUCCEEDED -class TransportTest(ParamikoTest): +class TransportTest(unittest.TestCase): def setUp(self): self.socks = LoopSocket() self.sockc = LoopSocket() diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..643d9171 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -24,13 +24,12 @@ from binascii import hexlify import errno import os from hashlib import sha1 +import unittest import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config from paramiko.py3compat import StringIO, byte_ord -from tests.util import ParamikoTest - test_config_file = """\ Host * User robey @@ -60,7 +59,7 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ from paramiko import * -class UtilTest(ParamikoTest): +class UtilTest(unittest.TestCase): def test_1_import(self): """ verify that all the classes can be imported from paramiko. diff --git a/tests/util.py b/tests/util.py index 66d2696c..b546a7e1 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,17 +1,7 @@ import os -import unittest root_path = os.path.dirname(os.path.realpath(__file__)) - -class ParamikoTest(unittest.TestCase): - # for Python 2.3 and below - if not hasattr(unittest.TestCase, 'assertTrue'): - assertTrue = unittest.TestCase.failUnless - if not hasattr(unittest.TestCase, 'assertFalse'): - assertFalse = unittest.TestCase.failIf - - def test_path(filename): return os.path.join(root_path, filename) -- cgit v1.2.3 From d05ef512bab1849e4fecafbc2a0c23465bf6b2c6 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:41:49 +0200 Subject: Don't end a line with whitespace. This might be stripped by editors at will, which will make some tests brake. --- tests/test_util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_util.py b/tests/test_util.py index 69c75518..28c162e5 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -41,7 +41,12 @@ Host *.example.com \tUser bjork Port=3333 Host * - \t \t Crazy something dumb +""" + +dont_strip_whitespace_please = "\t \t Crazy something dumb " + +test_config_file += dont_strip_whitespace_please +test_config_file += """ Host spoo.example.com Crazy something else """ -- cgit v1.2.3 From 74bd98fcf5cdd1fa63e5caf085e5b8fdafb0cb4e Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 12:23:02 +0200 Subject: Let packetizer handle 0-length sends from channel. If the channel is closed the send method returs a response length of 0. This is not handled correctly by the packetizer and puts it in an infinite loop. (Fixes #156 for real :-) We make sure we don't do more than 10 iteration on a 0 length respose, but raise an EOFError. --- paramiko/packet.py | 10 ++++++++++ tests/test_packetizer.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/paramiko/packet.py b/paramiko/packet.py index e97d92f0..f516ff9b 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -231,6 +231,7 @@ class Packetizer (object): def write_all(self, out): self.__keepalive_last = time.time() + iteration_with_zero_as_return_value = 0 while len(out) > 0: retry_write = False try: @@ -254,6 +255,15 @@ class Packetizer (object): n = 0 if self.__closed: n = -1 + else: + if n == 0 and iteration_with_zero_as_return_value > 10: + # We shouldn't retry the write, but we didn't + # manage to send anything over the socket. This might be an + # indication that we have lost contact with the remote side, + # but are yet to receive an EOFError or other socket errors. + # Let's give it some iteration to try and catch up. + n = -1 + iteration_with_zero_as_return_value += 1 if n < 0: raise EOFError() if n == len(out): diff --git a/tests/test_packetizer.py b/tests/test_packetizer.py index a8c0f973..8faec03c 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -74,3 +74,49 @@ class PacketizerTest (unittest.TestCase): self.assertEqual(100, m.get_int()) self.assertEqual(1, m.get_int()) self.assertEqual(900, m.get_int()) + + def test_3_closed(self): + rsock = LoopSocket() + wsock = LoopSocket() + rsock.link(wsock) + p = Packetizer(wsock) + p.set_log(util.get_logger('paramiko.transport')) + p.set_hexdump(True) + cipher = AES.new(zero_byte * 16, AES.MODE_CBC, x55 * 16) + p.set_outbound_cipher(cipher, 16, sha1, 12, x1f * 20) + + # message has to be at least 16 bytes long, so we'll have at least one + # block of data encrypted that contains zero random padding bytes + m = Message() + m.add_byte(byte_chr(100)) + m.add_int(100) + m.add_int(1) + m.add_int(900) + wsock.send = lambda x: 0 + from functools import wraps + import errno + import os + import signal + + class TimeoutError(Exception): + pass + + def timeout(seconds=1, error_message=os.strerror(errno.ETIME)): + def decorator(func): + def _handle_timeout(signum, frame): + raise TimeoutError(error_message) + + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.alarm(seconds) + try: + result = func(*args, **kwargs) + finally: + signal.alarm(0) + return result + + return wraps(func)(wrapper) + + return decorator + send = timeout()(p.send_message) + self.assertRaises(EOFError, send, m) -- cgit v1.2.3 From 12343df81cc07018a382bbe1ead102ca26c1d9d2 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 20:55:22 +0200 Subject: Whitespace fixes. --- paramiko/channel.py | 112 ++++++++++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index a7d71212..145fac14 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -96,13 +96,13 @@ class Channel (object): self.combine_stderr = False self.exit_status = -1 self.origin_addr = None - + def __del__(self): try: self.close() except: pass - + def __repr__(self): """ Return a string representation of this object, for debugging. @@ -136,7 +136,7 @@ class Channel (object): :param int height: height (in characters) of the terminal screen :param int width_pixels: width (in pixels) of the terminal screen :param int height_pixels: height (in pixels) of the terminal screen - + :raises SSHException: if the request was rejected or the channel was closed """ @@ -162,14 +162,14 @@ 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, stdout, and stderr of the shell. - + Normally you would call `get_pty` before this, in which case the shell will operate through the pty, and the channel will be connected to the stdin and stdout of the pty. - + When the shell exits, the channel will be closed and can't be reused. You must open a new channel if you wish to open another shell. - + :raises SSHException: if the request was rejected or the channel was closed """ @@ -189,7 +189,7 @@ class Channel (object): Execute a command on the server. If the server allows it, the channel will then be directly connected to the stdin, stdout, and stderr of the command being executed. - + When the command finishes executing, the channel will be closed and can't be reused. You must open a new channel if you wish to execute another command. @@ -216,7 +216,7 @@ class Channel (object): Request a subsystem on the server (for example, ``sftp``). If the server allows it, the channel will then be directly connected to the requested subsystem. - + When the subsystem finishes, the channel will be closed and can't be reused. @@ -269,14 +269,14 @@ class Channel (object): status. You may use this to poll the process status if you don't want to block in `recv_exit_status`. Note that the server may not return an exit status in some cases (like bad servers). - + :return: ``True`` if `recv_exit_status` will return immediately, else ``False``. .. versionadded:: 1.7.3 """ return self.closed or self.status_event.isSet() - + def recv_exit_status(self): """ Return the exit status from the process on the server. This is @@ -284,9 +284,9 @@ class Channel (object): If the command hasn't finished yet, this method will wait until it does, or until the channel is closed. If no exit status is provided by the server, -1 is returned. - + :return: the exit code (as an `int`) of the process on the server. - + .. versionadded:: 1.2 """ self.status_event.wait() @@ -299,9 +299,9 @@ class Channel (object): really only makes sense in server mode.) Many clients expect to get some sort of status code back from an executed command after it completes. - + :param int status: the exit code of the process - + .. versionadded:: 1.2 """ # in many cases, the channel will not still be open here. @@ -313,32 +313,32 @@ class Channel (object): m.add_boolean(False) m.add_int(status) self.transport._send_user_message(m) - + def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None, single_connection=False, handler=None): """ Request an x11 session on this channel. If the server allows it, further x11 requests can be made from the server to the client, when an x11 application is run in a shell session. - + From RFC4254:: It is RECOMMENDED that the 'x11 authentication cookie' that is sent be a fake, random cookie, and that the cookie be checked and replaced by the real cookie when a connection request is received. - + If you omit the auth_cookie, a new secure random 128-bit value will be generated, used, and returned. You will need to use this value to verify incoming x11 requests and replace them with the actual local x11 cookie (which requires some knoweldge of the x11 protocol). - + If a handler is passed in, the handler is called from another thread whenever a new x11 connection arrives. The default handler queues up incoming x11 connections, which may be retrieved using `.Transport.accept`. The handler's calling signature is:: - + handler(channel: Channel, (address: str, port: int)) - + :param int screen_number: the x11 screen number (0, 10, etc) :param str auth_protocol: the name of the X11 authentication method used; if none is given, @@ -425,33 +425,33 @@ class Channel (object): def get_id(self): """ Return the `int` ID # for this channel. - + The channel ID is unique across a `.Transport` and usually a small number. It's also the number passed to `.ServerInterface.check_channel_request` when determining whether to accept a channel request in server mode. """ return self.chanid - + def set_combine_stderr(self, combine): """ Set whether stderr should be combined into stdout on this channel. The default is ``False``, but in some cases it may be convenient to have both streams combined. - + If this is ``False``, and `exec_command` is called (or ``invoke_shell`` with no pty), output to stderr will not show up through the `recv` and `recv_ready` calls. You will have to use `recv_stderr` and `recv_stderr_ready` to get stderr output. - + If this is ``True``, data will never show up via `recv_stderr` or `recv_stderr_ready`. - + :param bool combine: ``True`` if stderr output should be combined into stdout on this channel. :return: the previous setting (a `bool`). - + .. versionadded:: 1.1 """ data = bytes() @@ -560,7 +560,7 @@ class Channel (object): Returns true if data is buffered and ready to be read from this channel. A ``False`` result does not mean that the channel has closed; it means you may need to wait before more data arrives. - + :return: ``True`` if a `recv` call on this channel would immediately return at least one byte; ``False`` otherwise. @@ -576,7 +576,7 @@ class Channel (object): :param int nbytes: maximum number of bytes to read. :return: received data, as a `str` - + :raises socket.timeout: if no data is ready before the timeout set by `settimeout`. """ @@ -602,11 +602,11 @@ class Channel (object): channel's stderr stream. Only channels using `exec_command` or `invoke_shell` without a pty will ever have data on the stderr stream. - + :return: ``True`` if a `recv_stderr` call on this channel would immediately return at least one byte; ``False`` otherwise. - + .. versionadded:: 1.1 """ return self.in_stderr_buffer.read_ready() @@ -622,17 +622,17 @@ class Channel (object): :param int nbytes: maximum number of bytes to read. :return: received data as a `str` - + :raises socket.timeout: if no data is ready before the timeout set by `settimeout`. - + .. versionadded:: 1.1 """ try: out = self.in_stderr_buffer.read(nbytes, self.timeout) except PipeTimeout: raise socket.timeout() - + ack = self._check_add_window(len(out)) # no need to hold the channel lock when sending this if ack > 0: @@ -648,11 +648,11 @@ class Channel (object): """ Returns true if data can be written to this channel without blocking. This means the channel is either closed (so any write attempt would - return immediately) or there is at least one byte of space in the + return immediately) or there is at least one byte of space in the outbound buffer. If there is at least one byte of space in the outbound buffer, a `send` call will succeed immediately and return the number of bytes actually written. - + :return: ``True`` if a `send` call on this channel would immediately succeed or fail @@ -664,7 +664,7 @@ class Channel (object): return self.out_window_size > 0 finally: self.lock.release() - + def send(self, s): """ Send data to the channel. Returns the number of bytes sent, or 0 if @@ -708,13 +708,13 @@ class Channel (object): stream is closed. Applications are responsible for checking that all data has been sent: if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. - + :param str s: data to send. :return: number of bytes actually sent, as an `int`. - + :raises socket.timeout: if no data could be sent before the timeout set by `settimeout`. - + .. versionadded:: 1.1 """ size = len(s) @@ -751,7 +751,7 @@ class Channel (object): if sending stalled for longer than the timeout set by `settimeout`. :raises socket.error: if an error occured before the entire string was sent. - + .. note:: If the channel is closed while only part of the data hase been sent, there is no way to determine how much data (if any) was sent. @@ -771,14 +771,14 @@ class Channel (object): results. Unlike `send_stderr`, this method continues to send data from the given string until all data has been sent or an error occurs. Nothing is returned. - + :param str s: data to send to the client as "stderr" output. - + :raises socket.timeout: if sending stalled for longer than the timeout set by `settimeout`. :raises socket.error: if an error occured before the entire string was sent. - + .. versionadded:: 1.1 """ while s: @@ -803,18 +803,18 @@ class Channel (object): Return a file-like object associated with this channel's stderr stream. Only channels using `exec_command` or `invoke_shell` without a pty will ever have data on the stderr stream. - + The optional ``mode`` and ``bufsize`` arguments are interpreted the same way as by the built-in ``file()`` function in Python. For a client, it only makes sense to open this file for reading. For a server, it only makes sense to open this file for writing. - + :return: `.ChannelFile` object which can be used for Python file I/O. .. versionadded:: 1.1 """ return ChannelStderrFile(*([self] + list(params))) - + def fileno(self): """ Returns an OS-level file descriptor which can be used for polling, but @@ -828,7 +828,7 @@ class Channel (object): open at the same time.) :return: an OS-level file descriptor (`int`) - + .. warning:: This method causes channel reads to be slightly less efficient. """ @@ -867,7 +867,7 @@ class Channel (object): self.lock.release() if m is not None: self.transport._send_user_message(m) - + def shutdown_read(self): """ Shutdown the receiving side of this socket, closing the stream in @@ -875,11 +875,11 @@ class Channel (object): channel will fail instantly. This is a convenience method, equivalent to ``shutdown(0)``, for people who don't make it a habit to memorize unix constants from the 1970s. - + .. versionadded:: 1.2 """ self.shutdown(0) - + def shutdown_write(self): """ Shutdown the sending side of this socket, closing the stream in @@ -887,7 +887,7 @@ class Channel (object): channel will fail instantly. This is a convenience method, equivalent to ``shutdown(1)``, for people who don't make it a habit to memorize unix constants from the 1970s. - + .. versionadded:: 1.2 """ self.shutdown(1) @@ -905,14 +905,14 @@ class Channel (object): self.in_window_threshold = window_size // 10 self.in_window_sofar = 0 self._log(DEBUG, 'Max packet in: %d bytes' % max_packet_size) - + def _set_remote_channel(self, chanid, window_size, max_packet_size): self.remote_chanid = chanid self.out_window_size = window_size 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 @@ -947,7 +947,7 @@ class Channel (object): self._feed(s) else: self.in_stderr_buffer.feed(s) - + def _window_adjust(self, m): nbytes = m.get_int() self.lock.acquire() @@ -1189,7 +1189,7 @@ class Channel (object): if self.ultra_debug: self._log(DEBUG, 'window down to %d' % self.out_window_size) return size - + class ChannelFile (BufferedFile): """ @@ -1229,7 +1229,7 @@ class ChannelStderrFile (ChannelFile): def _read(self, size): return self.channel.recv_stderr(size) - + def _write(self, data): self.channel.sendall_stderr(data) return len(data) -- cgit v1.2.3 From 3ee78214809f76181907808f9f27ed33a110be44 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 20:58:13 +0200 Subject: Add new _send method to reduce code duplication. This method is the lowest _send related method on Channel and is the place where we raise a socket is closed Excpetion. --- paramiko/channel.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/paramiko/channel.py b/paramiko/channel.py index 145fac14..f5f86d7a 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -1070,6 +1070,25 @@ class Channel (object): ### internals... + def _send(self, s, m): + size = len(s) + self.lock.acquire() + try: + if self.closed: + # this doesn't seem useful, but it is the documented behavior of Socket + raise socket.error('Socket is closed') + size = self._wait_for_send_window(size) + if size == 0: + # eof or similar + return 0 + m.add_string(s[:size]) + finally: + self.lock.release() + # Note: We release self.lock before calling _send_user_message. + # Otherwise, we can deadlock during re-keying. + self.transport._send_user_message(m) + return size + def _log(self, level, msg, *args): self.logger.log(level, "[chan " + self._name + "] " + msg, *args) -- cgit v1.2.3 From f8bc6dcc588bb48828b8b6b8bfdb0bd30b2c4fd4 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 21:00:25 +0200 Subject: Use new _send method. --- paramiko/channel.py | 53 ++++++++++++----------------------------------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index f5f86d7a..6ccdeea4 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -679,26 +679,11 @@ class Channel (object): :raises socket.timeout: if no data could be sent before the timeout set by `settimeout`. """ - size = len(s) - self.lock.acquire() - try: - size = self._wait_for_send_window(size) - # Instead of returning 0 here when the channel is closed, we might - # want to raise EOFError or similar. Should we just do as in the - # send_all method, raise a socket.error? - if size == 0: - # eof or similar - return 0 - m = Message() - m.add_byte(cMSG_CHANNEL_DATA) - m.add_int(self.remote_chanid) - m.add_string(s[:size]) - finally: - self.lock.release() - # Note: We release self.lock before calling _send_user_message. - # Otherwise, we can deadlock during re-keying. - self.transport._send_user_message(m) - return size + + m = Message() + m.add_byte(cMSG_CHANNEL_DATA) + m.add_int(self.remote_chanid) + return self._send(s, m) def send_stderr(self, s): """ @@ -717,27 +702,13 @@ class Channel (object): .. versionadded:: 1.1 """ - size = len(s) - self.lock.acquire() - try: - size = self._wait_for_send_window(size) - # Instead of returning 0 here when the channel is closed, we might - # want to raise EOFError or similar. Should we just do as in the - # send_all method, raise a socket.error? - if size == 0: - # eof or similar - return 0 - m = Message() - m.add_byte(cMSG_CHANNEL_EXTENDED_DATA) - m.add_int(self.remote_chanid) - m.add_int(1) - m.add_string(s[:size]) - finally: - self.lock.release() - # Note: We release self.lock before calling _send_user_message. - # Otherwise, we can deadlock during re-keying. - self.transport._send_user_message(m) - return size + + m = Message() + m.add_byte(cMSG_CHANNEL_EXTENDED_DATA) + m.add_int(self.remote_chanid) + m.add_int(1) + return self._send(s, m) + def sendall(self, s): """ -- cgit v1.2.3 From 49ae00822c910f62462fd881b1161bb8178b637d Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 21:00:56 +0200 Subject: Remove raising of socket excpetions from sendall*. --- paramiko/channel.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 6ccdeea4..4b83f81e 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -729,9 +729,6 @@ class Channel (object): This is irritating, but identically follows Python's API. """ while s: - if self.closed: - # this doesn't seem useful, but it is the documented behavior of Socket - raise socket.error('Socket is closed') sent = self.send(s) s = s[sent:] return None @@ -753,8 +750,6 @@ class Channel (object): .. versionadded:: 1.1 """ while s: - if self.closed: - raise socket.error('Socket is closed') sent = self.send_stderr(s) s = s[sent:] return None -- cgit v1.2.3 From d8a71fcdf0b57837632ccfa806386443fdb6dcc2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 18 Aug 2014 18:36:59 -0700 Subject: Sphinx conf cleanup --- sites/www/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sites/www/conf.py b/sites/www/conf.py index bdb5929a..0b0fb85c 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -12,13 +12,10 @@ extensions.append('releases') releases_release_uri = "https://github.com/paramiko/paramiko/tree/v%s" releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" -# Intersphinx for referencing API/usage docs -extensions.append('sphinx.ext.intersphinx') # Default is 'local' building, but reference the public docs site when building # under RTD. target = join(dirname(__file__), '..', 'docs', '_build') if os.environ.get('READTHEDOCS') == 'True': - # TODO: switch to docs.paramiko.org post go-live of sphinx API docs target = 'http://docs.paramiko.org/en/latest/' intersphinx_mapping['docs'] = (target, None) -- cgit v1.2.3 From 178e43060c52b9f65026a5ec57bbe5fae166ba4e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 21 Aug 2014 09:28:58 -0700 Subject: This isn't worth having and then always forgetting about --- sites/www/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/installing.rst b/sites/www/installing.rst index a28ce6cd..546aad9d 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -32,7 +32,7 @@ Release lines Users desiring stability may wish to pin themselves to a specific release line once they first start using Paramiko; to assist in this, we guarantee bugfixes -for at least the last 2-3 releases including the latest stable one. This currently means Paramiko **1.11** through **1.13**. +for at least the last 2-3 releases including the latest stable one. If you're unsure which version to install, we have suggestions: -- cgit v1.2.3 From e941d56e8a385f4f2ef5c0e9a77e66c844b5a729 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 21 Aug 2014 09:30:12 -0700 Subject: More tweaks - don't make it sound like we routinely support >3, we don't --- sites/www/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 546aad9d..729147c5 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -32,7 +32,7 @@ Release lines Users desiring stability may wish to pin themselves to a specific release line once they first start using Paramiko; to assist in this, we guarantee bugfixes -for at least the last 2-3 releases including the latest stable one. +for the last 2-3 releases including the latest stable one. If you're unsure which version to install, we have suggestions: -- cgit v1.2.3 From 84299d318bbea57a58d933ec79a053177f946500 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 21:53:33 +0200 Subject: Add a decorator for checking channel openness. This decorator will be used in future commits. --- paramiko/channel.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/paramiko/channel.py b/paramiko/channel.py index 583809d5..2215636b 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -25,6 +25,7 @@ import os import socket import time import threading +from functools import wraps from paramiko import util from paramiko.common import cMSG_CHANNEL_REQUEST, cMSG_CHANNEL_WINDOW_ADJUST, \ @@ -42,6 +43,15 @@ from paramiko import pipe # lower bound on the max packet size we'll accept from the remote host MIN_PACKET_SIZE = 1024 +def requires_open_channel(func): + """This decorator make sures that the channel is open else raises an + exception""" + @wraps(func) + def _check(self, *args, **kwds): + if self.closed or self.eof_received or self.eof_sent or not self.active: + raise SSHException('Channel is not open') + return func(self, *args, **kwds) + return _check class Channel (object): """ -- cgit v1.2.3 From dc63250293e742edc54ba4b08f372c265de3f5ca Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 21:55:46 +0200 Subject: Use the new decorator and remove repeated code. --- paramiko/channel.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 2215636b..bed182dd 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -44,8 +44,8 @@ from paramiko import pipe MIN_PACKET_SIZE = 1024 def requires_open_channel(func): - """This decorator make sures that the channel is open else raises an - exception""" + """This decorator makes sure that the channel is open, else it raises an + SSHException.""" @wraps(func) def _check(self, *args, **kwds): if self.closed or self.eof_received or self.eof_sent or not self.active: @@ -132,6 +132,7 @@ class Channel (object): out += '>' return out + @requires_open_channel def get_pty(self, term='vt100', width=80, height=24, width_pixels=0, height_pixels=0): """ @@ -150,8 +151,6 @@ class Channel (object): :raises SSHException: if the request was rejected or the channel was closed """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) @@ -167,6 +166,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() + @requires_open_channel def invoke_shell(self): """ Request an interactive shell session on this channel. If the server @@ -183,8 +183,6 @@ class Channel (object): :raises SSHException: if the request was rejected or the channel was closed """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) @@ -194,6 +192,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() + @requires_open_channel def exec_command(self, command): """ Execute a command on the server. If the server allows it, the channel @@ -209,8 +208,6 @@ class Channel (object): :raises SSHException: if the request was rejected or the channel was closed """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) @@ -221,6 +218,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() + @requires_open_channel def invoke_subsystem(self, subsystem): """ Request a subsystem on the server (for example, ``sftp``). If the @@ -235,8 +233,6 @@ class Channel (object): :raises SSHException: if the request was rejected or the channel was closed """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) @@ -247,6 +243,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() + @requires_open_channel def resize_pty(self, width=80, height=24, width_pixels=0, height_pixels=0): """ Resize the pseudo-terminal. This can be used to change the width and @@ -260,8 +257,6 @@ class Channel (object): :raises SSHException: if the request was rejected or the channel was closed """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) @@ -323,7 +318,8 @@ class Channel (object): m.add_boolean(False) m.add_int(status) self.transport._send_user_message(m) - + + @requires_open_channel def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None, single_connection=False, handler=None): """ @@ -364,8 +360,6 @@ class Channel (object): an optional handler to use for incoming X11 connections :return: the auth_cookie used """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') if auth_protocol is None: auth_protocol = 'MIT-MAGIC-COOKIE-1' if auth_cookie is None: @@ -386,6 +380,7 @@ class Channel (object): self.transport._set_x11_handler(handler) return auth_cookie + @requires_open_channel def request_forward_agent(self, handler): """ Request for a forward SSH Agent on this channel. @@ -398,9 +393,6 @@ class Channel (object): :raises: SSHException in case of channel problem. """ - if self.closed or self.eof_received or self.eof_sent or not self.active: - raise SSHException('Channel is not open') - m = Message() m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) -- cgit v1.2.3 From b076c10d00ee2cb4d465291a4ef0a4be9f048d9a Mon Sep 17 00:00:00 2001 From: Sebastian Deiss Date: Mon, 25 Aug 2014 16:52:06 +0200 Subject: reorder key exchange methods to increase security --- paramiko/transport.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 86c9130c..65c1af79 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -95,7 +95,7 @@ class Transport (threading.Thread): 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256') _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96') _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' ) + _preferred_kex = ( 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' , 'diffie-hellman-group1-sha1') _preferred_compression = ('none',) _cipher_info = { @@ -230,11 +230,11 @@ class Transport (threading.Thread): if self.use_gss_kex: self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds) self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==', - 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', 'diffie-hellman-group-exchange-sha1', - 'diffie-hellman-group1-sha1', - 'diffie-hellman-group14-sha1') + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group1-sha1') # state used during negotiation self.kex_engine = None -- cgit v1.2.3 From 2a88241ea7ed3facf730678ef28a8281903cf9a9 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 16:27:15 -0700 Subject: Add a plus to our 3.3 support to denote 3.4 and on --- sites/www/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 729147c5..052825c4 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -16,7 +16,7 @@ via `pip `_:: Users who want the bleeding edge can install the development version via ``pip install paramiko==dev``. -We currently support **Python 2.6, 2.7 and 3.3** (Python **3.2** should also +We currently support **Python 2.6, 2.7 and 3.3+** (Python **3.2** should also work but has a less-strong compatibility guarantee from us.) Users on Python 2.5 or older are urged to upgrade. -- cgit v1.2.3 From 5d010cd8c496e1ed7e13e7110f7fca9632c08e47 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 16:28:02 -0700 Subject: Changelog re #371 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f8a4d2c1..5ed0c961 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`371` Add Travis support & docs update for Python 3.4. Thanks to + Olle Lundberg. * :release:`1.14.0 <2014-05-07>` * :release:`1.13.1 <2014-05-07>` * :release:`1.12.4 <2014-05-07>` -- cgit v1.2.3 From fd1e162243898e34545ef5c1985bedee16174981 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 21:59:59 -0700 Subject: Changelog re #285, re #352 --- sites/www/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5ed0c961..2e2d2f63 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,12 @@ Changelog ========= +* :bug:`285` (also :issue:`352`) Update our Python 3 ``b()`` compatibility shim + to handle ``buffer`` objects correctly; this fixes a frequently reported + issue affecting many users, including users of the ``bzr`` software suite. + Thanks to ``@basictheprogram`` for the initial report, Jelmer Vernooij for + the fix and Andrew Starr-Bochicchio & Jeremy T. Bouse (among others) for + discussion & feedback. * :support:`371` Add Travis support & docs update for Python 3.4. Thanks to Olle Lundberg. * :release:`1.14.0 <2014-05-07>` -- cgit v1.2.3 From d7b93df7aaf9f409da7578a107829c138d042121 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 23:43:48 -0700 Subject: Cut 1.14.1 --- paramiko/__init__.py | 2 +- setup.py | 2 +- sites/www/changelog.rst | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 4c62ad4a..2ebc8a65 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -23,7 +23,7 @@ if sys.version_info < (2, 6): __author__ = "Jeff Forcier " -__version__ = "1.14.0" +__version__ = "1.14.1" __version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" diff --git a/setup.py b/setup.py index c0f1e579..38e444f5 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if sys.platform == 'darwin': setup( name = "paramiko", - version = "1.14.0", + version = "1.14.1", description = "SSH2 protocol library", long_description = longdesc, author = "Jeff Forcier", diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index c4c1e698..0fcde10f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.14.1 <2014-08-25>` * :release:`1.13.2 <2014-08-25>` * :bug:`376` Be less aggressive about expanding variables in ``ssh_config`` files, which results in a speedup of SSH config parsing. Credit to Olle -- cgit v1.2.3 From 28b7db145fed32840f8d2edea8fdce27dc00ca26 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 23:48:28 -0700 Subject: Consolidate version info. Closes #249 --- paramiko/__init__.py | 3 +-- paramiko/_version.py | 2 ++ setup.py | 9 ++++++++- sites/www/changelog.rst | 2 ++ 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 paramiko/_version.py diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 2ebc8a65..65f6f8a2 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -17,14 +17,13 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. import sys +from paramiko._version import __version__, __version_info__ if sys.version_info < (2, 6): raise RuntimeError('You need Python 2.6+ for this module.') __author__ = "Jeff Forcier " -__version__ = "1.14.1" -__version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" diff --git a/paramiko/_version.py b/paramiko/_version.py new file mode 100644 index 00000000..a7857b09 --- /dev/null +++ b/paramiko/_version.py @@ -0,0 +1,2 @@ +__version_info__ = (1, 15, 0) +__version__ = '.'.join(map(str, __version_info__)) diff --git a/setup.py b/setup.py index 3d8268d1..13386c8e 100644 --- a/setup.py +++ b/setup.py @@ -54,9 +54,16 @@ if sys.platform == 'darwin': setup_helper.install_custom_make_tarball() +# Version info -- read without importing +_locals = {} +with open('paramiko/_version.py') as fp: + exec(fp.read(), None, _locals) +version = _locals['__version__'] + + setup( name = "paramiko", - version = "1.14.1", + version = version, description = "SSH2 protocol library", long_description = longdesc, author = "Jeff Forcier", diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 089ec30e..b6fa7ccc 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`249` Consolidate version information into one spot. Thanks to Gabi + Davar for the reminder. * :release:`1.14.1 <2014-08-25>` * :release:`1.13.2 <2014-08-25>` * :bug:`376` Be less aggressive about expanding variables in ``ssh_config`` -- cgit v1.2.3 From 0b9280f5994c4bfc0504452276464d35b04370f1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 11:16:16 -0700 Subject: Update docstring for listdir_iter --- paramiko/sftp_client.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 252e7b96..39f08fbc 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -198,22 +198,17 @@ class SFTPClient(BaseSFTP): def listdir_iter(self, path='.', read_ahead_requests=50): """ - Generator yielding L{SFTPAttributes} objects corresponding to - files in the given C{path}. Files are yielded in arbitrary order. It does - not include the special entries C{'.'} and C{'..'} even if they are - present in the folder. + Generator version of `.listdir_attr`. - 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 - depend on the SFTP server implementation. + See the API docs for `.listdir_attr` for overall details. - @param path: path to list (defaults to C{'.'}) - @type path: str - @return: Yields L{SFTPAttributes} - @rtype: L{SFTPAttributes} + This function adds one more kwarg on top of `.listdir_attr`: + ``read_ahead_requests``, an integer controlling how many + ``SSH_FXP_READDIR`` requests are made to the server. The default of 50 + should suffice for most file listings as each request/response cycle + may contain multiple files (dependant on server implementation.) - @since: 1.9 + .. versionadded:: 1.15 """ path = self._adjust_cwd(path) self._log(DEBUG, 'listdir(%r)' % path) -- cgit v1.2.3 From f4de3307b1133c0b207f5af40227c64a8cb5389d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 11:16:52 -0700 Subject: 80-col tweaks --- paramiko/sftp_client.py | 14 +++++++++----- tests/test_sftp.py | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 39f08fbc..ee25aa6a 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -222,8 +222,9 @@ class SFTPClient(BaseSFTP): nums = list() while True: try: - # Send out a bunch of readdir requests so that we can read the responses later on - # Section 6.7 of the SSH file transfer RFC explains this + # Send out a bunch of readdir requests so that we can read the + # responses later on Section 6.7 of the SSH file transfer RFC + # explains this # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt for i in range(read_ahead_requests): num = self._async_request(type(None), CMD_READDIR, handle) @@ -232,8 +233,10 @@ class SFTPClient(BaseSFTP): # For each of our sent requests # Read and parse the corresponding packets - # If we're at the end of our queued requests, then fire off some more requests - # Exit the loop when we've reached the end of the directory handle + # If we're at the end of our queued requests, then fire off + # some more requests + # Exit the loop when we've reached the end of the directory + # handle for num in nums: t, pkt_data = self._read_packet() msg = Message(pkt_data) @@ -245,7 +248,8 @@ class SFTPClient(BaseSFTP): for i in range(count): filename = msg.get_string() longname = msg.get_string() - attr = SFTPAttributes._from_msg(msg, filename, longname) + attr = SFTPAttributes._from_msg( + msg, filename, longname) if (filename != '.') and (filename != '..'): yield attr diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 2b6aa3b6..26929bdb 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -279,8 +279,8 @@ class SFTPTest (unittest.TestCase): def test_7_listdir(self): """ - verify that a folder can be created, a bunch of files can be placed in it, - and those files show up in sftp.listdir. + verify that a folder can be created, a bunch of files can be placed in + it, and those files show up in sftp.listdir. """ try: sftp.open(FOLDER + '/duck.txt', 'w').close() -- cgit v1.2.3 From e5fc6a6ecc064f6b2b9862a9405b27f40385f821 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 11:38:27 -0700 Subject: Add quick test re #131 --- tests/test_sftp.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 26929bdb..1ae9781d 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -298,6 +298,26 @@ class SFTPTest (unittest.TestCase): sftp.remove(FOLDER + '/fish.txt') sftp.remove(FOLDER + '/tertiary.py') + def test_7_5_listdir_iter(self): + """ + listdir_iter version of above test + """ + try: + sftp.open(FOLDER + '/duck.txt', 'w').close() + sftp.open(FOLDER + '/fish.txt', 'w').close() + sftp.open(FOLDER + '/tertiary.py', 'w').close() + + x = [x.filename for x in sftp.listdir_iter(FOLDER)] + self.assertEqual(len(x), 3) + self.assertTrue('duck.txt' in x) + self.assertTrue('fish.txt' in x) + self.assertTrue('tertiary.py' in x) + self.assertTrue('random' not in x) + finally: + sftp.remove(FOLDER + '/duck.txt') + sftp.remove(FOLDER + '/fish.txt') + sftp.remove(FOLDER + '/tertiary.py') + def test_8_setstat(self): """ verify that the setstat functions (chown, chmod, utime, truncate) work. -- cgit v1.2.3 From b2323505b0a3e22d37a30ecdab9624bd086726e6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 11:42:27 -0700 Subject: Make read ahead kwarg a bit less verbose --- paramiko/sftp_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index ee25aa6a..d3b95933 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -196,14 +196,14 @@ class SFTPClient(BaseSFTP): self._request(CMD_CLOSE, handle) return filelist - def listdir_iter(self, path='.', read_ahead_requests=50): + def listdir_iter(self, path='.', read_aheads=50): """ Generator version of `.listdir_attr`. See the API docs for `.listdir_attr` for overall details. This function adds one more kwarg on top of `.listdir_attr`: - ``read_ahead_requests``, an integer controlling how many + ``read_aheads``, an integer controlling how many ``SSH_FXP_READDIR`` requests are made to the server. The default of 50 should suffice for most file listings as each request/response cycle may contain multiple files (dependant on server implementation.) @@ -226,7 +226,7 @@ class SFTPClient(BaseSFTP): # responses later on Section 6.7 of the SSH file transfer RFC # explains this # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt - for i in range(read_ahead_requests): + for i in range(read_aheads): num = self._async_request(type(None), CMD_READDIR, handle) nums.append(num) -- cgit v1.2.3 From 35b9d1540bd98af39e133960c61b06aba621f30d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 11:44:22 -0700 Subject: Changelog re #131 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 24679d5f..e18b5368 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :feature:`131` Add a `~paramiko.sftp_client.SFTPClient.listdir_iter` method + to `~paramiko.sftp_client.SFTPClient` allowing for more efficient, + async/generator based file listings. Thanks to John Begeman. * :support:`378 backported` Minor code cleanup in the SSH config module courtesy of Olle Lundberg. * :support:`249` Consolidate version information into one spot. Thanks to Gabi -- cgit v1.2.3 From 720806734a05d9de0f47588d816de05646a06f0b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 12:49:35 -0700 Subject: Fix an oversight re: py3 & strings in listdir_iter --- paramiko/sftp_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index d3b95933..3e85a8c9 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -246,8 +246,8 @@ class SFTPClient(BaseSFTP): self._convert_status(msg) count = msg.get_int() for i in range(count): - filename = msg.get_string() - longname = msg.get_string() + filename = msg.get_text() + longname = msg.get_text() attr = SFTPAttributes._from_msg( msg, filename, longname) if (filename != '.') and (filename != '..'): -- cgit v1.2.3 From b8022866fac62d1757aa730d5991030f223088fd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 13:27:22 -0700 Subject: Changelog re #184 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index e18b5368..57f00f12 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :feature:`184` Support quoted values in SSH config file parsing. Credit to + Yan Kalchevskiy. * :feature:`131` Add a `~paramiko.sftp_client.SFTPClient.listdir_iter` method to `~paramiko.sftp_client.SFTPClient` allowing for more efficient, async/generator based file listings. Thanks to John Begeman. -- cgit v1.2.3 From 76c2073e9e098bec89837ae980167ee9b0a6efcf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 14:22:11 -0700 Subject: Add a (failing :() test re: ECDSA private keys Re #218 --- tests/test_client.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 7e5c80b4..3576ef32 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -148,6 +148,40 @@ class SSHClientTest (unittest.TestCase): stdout.close() stderr.close() + def test_2_5_client_ecdsa(self): + """ + verify that SSHClient works with an ECDSA key. + """ + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + + self.tc = paramiko.SSHClient() + self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) + self.tc.connect(self.addr, self.port, username='slowdive', key_filename=test_path('test_ecdsa.key')) + + self.event.wait(1.0) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) + self.assertEqual('slowdive', self.ts.get_username()) + self.assertEqual(True, self.ts.is_authenticated()) + + stdin, stdout, stderr = self.tc.exec_command('yes') + schan = self.ts.accept(1.0) + + schan.send('Hello there.\n') + schan.send_stderr('This is on stderr.\n') + schan.close() + + self.assertEqual('Hello there.\n', stdout.readline()) + self.assertEqual('', stdout.readline()) + self.assertEqual('This is on stderr.\n', stderr.readline()) + self.assertEqual('', stderr.readline()) + + stdin.close() + stdout.close() + stderr.close() + def test_3_multiple_key_files(self): """ verify that SSHClient accepts and tries multiple key files. -- cgit v1.2.3 From 286b5fb7088b11efcbe6c1b452a1d1785890ac3f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 14:26:27 -0700 Subject: Changelog re #335 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 57f00f12..f1215f62 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :bug:`335 major` Fix ECDSA key generation (generation of brand new ECDSA keys + was broken previously). Thanks to ``@solarw`` for catch & patch. * :feature:`184` Support quoted values in SSH config file parsing. Credit to Yan Kalchevskiy. * :feature:`131` Add a `~paramiko.sftp_client.SFTPClient.listdir_iter` method -- cgit v1.2.3 From dc9de3f3c37752e5ee2b36bb4e9747e767baea14 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 15:40:25 -0700 Subject: Formatting tweaks --- paramiko/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 4d269d0d..4326abbd 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -185,7 +185,8 @@ class SSHClient (object): - The ``pkey`` or ``key_filename`` passed in (if any) - Any key we can find through an SSH agent - - Any "id_rsa" or "id_dsa" key discoverable in ``~/.ssh/`` + - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in + ``~/.ssh/`` - Plain username/password auth, if a password was given If a private key requires a password to unlock it, and a password is @@ -361,7 +362,8 @@ class SSHClient (object): - 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). + - Any "id_rsa", "id_dsa" or "id_ecdsa" 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, or for -- cgit v1.2.3 From 6c8a5eb8fbf023194634f3077479a70ac9f024a2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 16:05:30 -0700 Subject: Refactor horrible old copypasta --- tests/test_client.py | 95 ++++++++++++---------------------------------------- 1 file changed, 21 insertions(+), 74 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 3576ef32..0697d289 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -80,24 +80,30 @@ class SSHClientTest (unittest.TestCase): server = NullServer() self.ts.start_server(self.event, server) - def test_1_client(self): + def _test_connection(self, **kwargs): """ - verify that the SSHClient stuff works too. + kwargs get passed directly into SSHClient.connect(). """ + # Server setup threading.Thread(target=self._run).start() host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + # Client setup self.tc = paramiko.SSHClient() self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') + # Actual connection + self.tc.connect(self.addr, self.port, username='slowdive', **kwargs) + + # Authentication successful? self.event.wait(1.0) self.assertTrue(self.event.isSet()) self.assertTrue(self.ts.is_active()) self.assertEqual('slowdive', self.ts.get_username()) self.assertEqual(True, self.ts.is_authenticated()) + # Command execution functions? stdin, stdout, stderr = self.tc.exec_command('yes') schan = self.ts.accept(1.0) @@ -110,95 +116,36 @@ class SSHClientTest (unittest.TestCase): self.assertEqual('This is on stderr.\n', stderr.readline()) self.assertEqual('', stderr.readline()) + # Cleanup stdin.close() stdout.close() stderr.close() + def test_1_client(self): + """ + verify that the SSHClient stuff works too. + """ + self._test_connection(password='pygmalion') + def test_2_client_dsa(self): """ verify that SSHClient works with a DSA key. """ - threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) - - self.tc = paramiko.SSHClient() - self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename=test_path('test_dss.key')) - - self.event.wait(1.0) - self.assertTrue(self.event.isSet()) - self.assertTrue(self.ts.is_active()) - self.assertEqual('slowdive', self.ts.get_username()) - self.assertEqual(True, self.ts.is_authenticated()) - - stdin, stdout, stderr = self.tc.exec_command('yes') - schan = self.ts.accept(1.0) - - schan.send('Hello there.\n') - schan.send_stderr('This is on stderr.\n') - schan.close() - - self.assertEqual('Hello there.\n', stdout.readline()) - self.assertEqual('', stdout.readline()) - self.assertEqual('This is on stderr.\n', stderr.readline()) - self.assertEqual('', stderr.readline()) - - stdin.close() - stdout.close() - stderr.close() + self._test_connection(key_filename=test_path('test_dss.key')) def test_2_5_client_ecdsa(self): """ verify that SSHClient works with an ECDSA key. """ - threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) - - self.tc = paramiko.SSHClient() - self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename=test_path('test_ecdsa.key')) - - self.event.wait(1.0) - self.assertTrue(self.event.isSet()) - self.assertTrue(self.ts.is_active()) - self.assertEqual('slowdive', self.ts.get_username()) - self.assertEqual(True, self.ts.is_authenticated()) - - stdin, stdout, stderr = self.tc.exec_command('yes') - schan = self.ts.accept(1.0) - - schan.send('Hello there.\n') - schan.send_stderr('This is on stderr.\n') - schan.close() - - self.assertEqual('Hello there.\n', stdout.readline()) - self.assertEqual('', stdout.readline()) - self.assertEqual('This is on stderr.\n', stderr.readline()) - self.assertEqual('', stderr.readline()) - - stdin.close() - stdout.close() - stderr.close() + self._test_connection(key_filename=test_path('test_ecdsa.key')) def test_3_multiple_key_files(self): """ verify that SSHClient accepts and tries multiple key files. """ - threading.Thread(target=self._run).start() - host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) - public_host_key = paramiko.RSAKey(data=host_key.asbytes()) - - self.tc = paramiko.SSHClient() - self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key) - self.tc.connect(self.addr, self.port, username='slowdive', key_filename=[test_path('test_rsa.key'), test_path('test_dss.key')]) - - self.event.wait(1.0) - self.assertTrue(self.event.isSet()) - self.assertTrue(self.ts.is_active()) - self.assertEqual('slowdive', self.ts.get_username()) - self.assertEqual(True, self.ts.is_authenticated()) + self._test_connection(key_filename=[ + test_path('test_rsa.key'), test_path('test_dss.key') + ]) def test_4_auto_add_policy(self): """ -- cgit v1.2.3 From f7f3b4560f8a6a44d3738db798cba977bb5daf4f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 16:08:05 -0700 Subject: Add an explicit RSA test, which fails (!) --- tests/test_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 0697d289..1791363a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -133,6 +133,12 @@ class SSHClientTest (unittest.TestCase): """ self._test_connection(key_filename=test_path('test_dss.key')) + def test_client_rsa(self): + """ + verify that SSHClient works with an RSA key. + """ + self._test_connection(key_filename=test_path('test_rsa.key')) + def test_2_5_client_ecdsa(self): """ verify that SSHClient works with an ECDSA key. -- cgit v1.2.3 From a8fd0bd9b2811a989fb646842a2dc17f9d94fd01 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 17:54:33 -0700 Subject: Set things up for cleaner test key tomfoolery --- tests/test_client.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1791363a..7ccf92b0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,10 +29,25 @@ import warnings import os from tests.util import test_path import paramiko -from paramiko.common import PY2 +from paramiko.common import PY2, b + + +def _fingerprint_to_bytes(fingerprint): + """ + Takes ssh-keygen style fingerprint, returns hex-y bytestring. + """ + encoded = b(''.join([r'\x{0}'.format(x) for x in fingerprint.split(':')])) + return encoded.decode('string-escape') class NullServer (paramiko.ServerInterface): + def __init__(self, *args, **kwargs): + # Allow tests to enable/disable specific key types + self.__allowed_keys = kwargs.pop('allowed_keys', []) + self.__fingerprints = { + 'dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', + } + super(NullServer, self).__init__(*args, **kwargs) def get_allowed_auths(self, username): if username == 'slowdive': @@ -45,7 +60,16 @@ class NullServer (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - if (key.get_name() == 'ssh-dss') and key.get_fingerprint() == b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c': + type_ = key.get_name() + fingerprint = key.get_fingerprint() + if not type_.startswith('ssh-'): + return paramiko.AUTH_FAILED + # TODO: honor allowed_keys + try: + expected = self.__fingerprints[type_[4:]] + except KeyError: + return paramiko.AUTH_FAILED + if fingerprint == _fingerprint_to_bytes(expected): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED -- cgit v1.2.3 From 70e44d6386197ec77ca0f4fec8bba47ee22c3198 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 17:55:22 -0700 Subject: Add RSA key fingerprint, now that test passes --- tests/test_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_client.py b/tests/test_client.py index 7ccf92b0..cabc0c6f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -46,6 +46,7 @@ class NullServer (paramiko.ServerInterface): self.__allowed_keys = kwargs.pop('allowed_keys', []) self.__fingerprints = { 'dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', + 'rsa': '60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5', } super(NullServer, self).__init__(*args, **kwargs) -- cgit v1.2.3 From 7d72ce55b8b14a36fcd9e46253ed268eab816c5e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 18:18:36 -0700 Subject: More cleanup to support ECDSA key, and now it works! --- tests/test_client.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index cabc0c6f..1db2046d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -45,8 +45,9 @@ class NullServer (paramiko.ServerInterface): # Allow tests to enable/disable specific key types self.__allowed_keys = kwargs.pop('allowed_keys', []) self.__fingerprints = { - 'dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', - 'rsa': '60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5', + 'ssh-dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', + 'ssh-rsa': '60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5', + 'ecdsa-sha2-nistp256': '25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60', } super(NullServer, self).__init__(*args, **kwargs) @@ -61,16 +62,12 @@ class NullServer (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - type_ = key.get_name() - fingerprint = key.get_fingerprint() - if not type_.startswith('ssh-'): - return paramiko.AUTH_FAILED # TODO: honor allowed_keys try: - expected = self.__fingerprints[type_[4:]] + expected = self.__fingerprints[key.get_name()] except KeyError: return paramiko.AUTH_FAILED - if fingerprint == _fingerprint_to_bytes(expected): + if key.get_fingerprint() == _fingerprint_to_bytes(expected): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED -- cgit v1.2.3 From d8047a2e6d1aa06e113e0a9dc9e8f3e89ce2f70e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 19:23:15 -0700 Subject: Factor fingerprint data out of class --- tests/test_client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1db2046d..0293ec75 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -32,6 +32,12 @@ import paramiko from paramiko.common import PY2, b +FINGERPRINTS = { + 'ssh-dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', + 'ssh-rsa': '60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5', + 'ecdsa-sha2-nistp256': '25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60', +} + def _fingerprint_to_bytes(fingerprint): """ Takes ssh-keygen style fingerprint, returns hex-y bytestring. @@ -44,11 +50,6 @@ class NullServer (paramiko.ServerInterface): def __init__(self, *args, **kwargs): # Allow tests to enable/disable specific key types self.__allowed_keys = kwargs.pop('allowed_keys', []) - self.__fingerprints = { - 'ssh-dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', - 'ssh-rsa': '60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5', - 'ecdsa-sha2-nistp256': '25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60', - } super(NullServer, self).__init__(*args, **kwargs) def get_allowed_auths(self, username): @@ -64,7 +65,7 @@ class NullServer (paramiko.ServerInterface): def check_auth_publickey(self, username, key): # TODO: honor allowed_keys try: - expected = self.__fingerprints[key.get_name()] + expected = FINGERPRINTS[key.get_name()] except KeyError: return paramiko.AUTH_FAILED if key.get_fingerprint() == _fingerprint_to_bytes(expected): -- cgit v1.2.3 From dd0c618ea02024e06806ab99c79fcff8affeba03 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 19:32:50 -0700 Subject: Overhaul multi-key test to set up multiple scenarios --- tests/test_client.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0293ec75..ae10b814 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -95,20 +95,26 @@ class SSHClientTest (unittest.TestCase): if hasattr(self, attr): getattr(self, attr).close() - def _run(self): + def _run(self, allowed_keys=None): + if allowed_keys is None: + allowed_keys = FINGERPRINTS.keys() self.socks, addr = self.sockl.accept() self.ts = paramiko.Transport(self.socks) host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) self.ts.add_server_key(host_key) - server = NullServer() + server = NullServer(allowed_keys=allowed_keys) self.ts.start_server(self.event, server) def _test_connection(self, **kwargs): """ - kwargs get passed directly into SSHClient.connect(). + (Most) kwargs get passed directly into SSHClient.connect(). + + The exception is ``allowed_keys`` which is stripped and handed to the + ``NullServer`` used for testing. """ + run_kwargs = {'allowed_keys': kwargs.pop('allowed_keys', None)} # Server setup - threading.Thread(target=self._run).start() + threading.Thread(target=self._run, kwargs=run_kwargs).start() host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) public_host_key = paramiko.RSAKey(data=host_key.asbytes()) @@ -172,9 +178,23 @@ class SSHClientTest (unittest.TestCase): """ verify that SSHClient accepts and tries multiple key files. """ - self._test_connection(key_filename=[ - test_path('test_rsa.key'), test_path('test_dss.key') - ]) + # This is dumb :( + types_ = { + 'rsa': 'ssh-rsa', + 'dss': 'ssh-dss', + 'ecdsa': 'ecdsa-sha2-nistp256', + } + # Various combos of attempted & valid keys + for attempt, accept in ( + (['rsa', 'dss'], ['dss']), # Original test #3 + (['dss', 'rsa'], ['dss']), # Ordering matters sometimes, sadly + ): + self._test_connection( + key_filename=[ + test_path('test_{0}.key'.format(x)) for x in attempt + ], + allowed_keys=[types_[x] for x in accept], + ) def test_4_auto_add_policy(self): """ -- cgit v1.2.3 From 689ac4d9c6a30eccfba21d068239dc5b06c16bc4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 19:39:02 -0700 Subject: Add a couple more permutations --- tests/test_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index ae10b814..a1ef490c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -185,9 +185,12 @@ class SSHClientTest (unittest.TestCase): 'ecdsa': 'ecdsa-sha2-nistp256', } # Various combos of attempted & valid keys + # TODO: try every possible combo using itertools functions for attempt, accept in ( (['rsa', 'dss'], ['dss']), # Original test #3 (['dss', 'rsa'], ['dss']), # Ordering matters sometimes, sadly + (['dss', 'rsa', 'ecdsa'], ['dss']), # Try ECDSA but fail + (['rsa', 'ecdsa'], ['ecdsa']), # ECDSA success ): self._test_connection( key_filename=[ -- cgit v1.2.3 From 89a8ef55a662883239102c802cd637c56fcaebd5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 19:49:32 -0700 Subject: Changelog re #218 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f1215f62..f04e338b 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :feature:`218` Add support for ECDSA private keys on the client side. Thanks + to ``@aszlig`` for the patch. * :bug:`335 major` Fix ECDSA key generation (generation of brand new ECDSA keys was broken previously). Thanks to ``@solarw`` for catch & patch. * :feature:`184` Support quoted values in SSH config file parsing. Credit to -- cgit v1.2.3 From 5c81c60090b991f7fc4c87c850460a4adc48986e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 20:35:29 -0700 Subject: Forgot to actually implement allowed-keys/reverse testing --- tests/test_client.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index a1ef490c..4ba66828 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -30,6 +30,7 @@ import os from tests.util import test_path import paramiko from paramiko.common import PY2, b +from paramiko.ssh_exception import PasswordRequiredException FINGERPRINTS = { @@ -63,12 +64,14 @@ class NullServer (paramiko.ServerInterface): return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - # TODO: honor allowed_keys try: expected = FINGERPRINTS[key.get_name()] except KeyError: return paramiko.AUTH_FAILED - if key.get_fingerprint() == _fingerprint_to_bytes(expected): + if ( + key.get_name() in self.__allowed_keys and + key.get_fingerprint() == _fingerprint_to_bytes(expected) + ): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -199,6 +202,16 @@ class SSHClientTest (unittest.TestCase): allowed_keys=[types_[x] for x in accept], ) + def test_multiple_key_files_failure(self): + """ + Expect failure when multiple keys in play and none are accepted + """ + self.assertRaises(PasswordRequiredException, + self._test_connection, + key_filename=[test_path('test_rsa.key')], + allowed_keys=['ecdsa-sha2-nistp256'], + ) + def test_4_auto_add_policy(self): """ verify that SSHClient's AutoAddPolicy works. -- cgit v1.2.3 From ff419627b3676a598d10cd581282d21ec33136bb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 20:45:09 -0700 Subject: Fix a Python 3 encoding dealie --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 4ba66828..b0ea0dc7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -44,7 +44,7 @@ def _fingerprint_to_bytes(fingerprint): Takes ssh-keygen style fingerprint, returns hex-y bytestring. """ encoded = b(''.join([r'\x{0}'.format(x) for x in fingerprint.split(':')])) - return encoded.decode('string-escape') + return encoded.decode('string-escape' if PY2 else 'unicode_escape') class NullServer (paramiko.ServerInterface): -- cgit v1.2.3 From 774bc5e3fff41a53b986d1663e6e0d35bf695195 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 20:54:22 -0700 Subject: Yup, that was indeed too fucking clever. Bad bitprophet! --- tests/test_client.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index b0ea0dc7..76fda68d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -34,18 +34,11 @@ from paramiko.ssh_exception import PasswordRequiredException FINGERPRINTS = { - 'ssh-dss': '44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c', - 'ssh-rsa': '60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5', - 'ecdsa-sha2-nistp256': '25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60', + 'ssh-dss': b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c', + 'ssh-rsa': b'\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5', + 'ecdsa-sha2-nistp256': b'\x25\x19\xeb\x55\xe6\xa1\x47\xff\x4f\x38\xd2\x75\x6f\xa5\xd5\x60', } -def _fingerprint_to_bytes(fingerprint): - """ - Takes ssh-keygen style fingerprint, returns hex-y bytestring. - """ - encoded = b(''.join([r'\x{0}'.format(x) for x in fingerprint.split(':')])) - return encoded.decode('string-escape' if PY2 else 'unicode_escape') - class NullServer (paramiko.ServerInterface): def __init__(self, *args, **kwargs): @@ -70,7 +63,7 @@ class NullServer (paramiko.ServerInterface): return paramiko.AUTH_FAILED if ( key.get_name() in self.__allowed_keys and - key.get_fingerprint() == _fingerprint_to_bytes(expected) + key.get_fingerprint() == expected ): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED -- cgit v1.2.3 From 6a6ac4d78421667b810f5e3a017fb669853133f9 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 6 Sep 2014 15:59:49 -0700 Subject: Bah humbug --- tests/test_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 76fda68d..7c094628 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -30,7 +30,7 @@ import os from tests.util import test_path import paramiko from paramiko.common import PY2, b -from paramiko.ssh_exception import PasswordRequiredException +from paramiko.ssh_exception import SSHException FINGERPRINTS = { @@ -199,7 +199,9 @@ class SSHClientTest (unittest.TestCase): """ Expect failure when multiple keys in play and none are accepted """ - self.assertRaises(PasswordRequiredException, + # Until #387 is fixed we have to catch a high-up exception since + # various platforms trigger different errors here >_< + self.assertRaises(SSHException, self._test_connection, key_filename=[test_path('test_rsa.key')], allowed_keys=['ecdsa-sha2-nistp256'], -- cgit v1.2.3 From ec9f8a26d4ea77ce6e4a1afe1a9e3b29dbf002bf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 6 Sep 2014 16:09:16 -0700 Subject: Changelog re #234 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f04e338b..44bd61e9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`234 major` Lower logging levels for a few overly-noisy log messages + about secure channels. Thanks to David Pursehouse for noticing & contributing + the fix. * :feature:`218` Add support for ECDSA private keys on the client side. Thanks to ``@aszlig`` for the patch. * :bug:`335 major` Fix ECDSA key generation (generation of brand new ECDSA keys -- cgit v1.2.3 From 979838040f9ff2f52e369812be87f0c7c9c1a0db Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 00:49:16 +0100 Subject: Introduce ClosingContextManager for classes that already implement close() --- paramiko/sftp_file.py | 9 ++------- paramiko/util.py | 8 ++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 03d67b33..e0fb2297 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -34,9 +34,10 @@ from paramiko.py3compat import long from paramiko.sftp import CMD_CLOSE, CMD_READ, CMD_DATA, SFTPError, CMD_WRITE, \ CMD_STATUS, CMD_FSTAT, CMD_ATTRS, CMD_FSETSTAT, CMD_EXTENDED from paramiko.sftp_attr import SFTPAttributes +from paramiko.util import ClosingContextManager -class SFTPFile (BufferedFile): +class SFTPFile (BufferedFile, ClosingContextManager): """ Proxy object for a file on the remote server, in client mode SFTP. @@ -488,9 +489,3 @@ class SFTPFile (BufferedFile): x = self._saved_exception self._saved_exception = None raise x - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() diff --git a/paramiko/util.py b/paramiko/util.py index f4ee3adc..ee1a3abb 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -320,3 +320,11 @@ def constant_time_bytes_eq(a, b): for i in (xrange if PY2 else range)(len(a)): res |= byte_ord(a[i]) ^ byte_ord(b[i]) return res == 0 + + +class ClosingContextManager(object): + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() -- cgit v1.2.3 From 64d54e942c8839acdb615bb9cbe402d7c9bd0709 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:25:58 +0100 Subject: Convert SFTPClient to a context manager --- paramiko/sftp_client.py | 3 ++- tests/test_sftp.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 3e85a8c9..b45ad442 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -39,6 +39,7 @@ from paramiko.sftp import BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READ from paramiko.sftp_attr import SFTPAttributes from paramiko.ssh_exception import SSHException from paramiko.sftp_file import SFTPFile +from paramiko.util import ClosingContextManager def _to_unicode(s): @@ -58,7 +59,7 @@ def _to_unicode(s): b_slash = b'/' -class SFTPClient(BaseSFTP): +class SFTPClient(BaseSFTP, ClosingContextManager): """ SFTP client object. diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 1ae9781d..e3f7e59b 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -195,6 +195,18 @@ class SFTPTest (unittest.TestCase): pass sftp = paramiko.SFTP.from_transport(tc) + def test_2_sftp_can_be_used_as_context_manager(self): + """ + verify that the sftp session is closed when exiting the context manager + """ + global sftp + with sftp: + pass + try: + self._assert_opening_file_raises_error(sftp) + finally: + sftp = paramiko.SFTP.from_transport(tc) + def test_3_write(self): """ verify that a file can be created and written, and the size is correct. @@ -796,6 +808,15 @@ class SFTPTest (unittest.TestCase): sftp.remove('%s/nonutf8data' % FOLDER) + def _assert_opening_file_raises_error(self, sftp): + try: + sftp.open(FOLDER + '/test2', 'w') + self.fail('expected exception') + # TODO: extract failing command (the open) to share with a passing assertion + except EOFError as error: + pass + + if __name__ == '__main__': SFTPTest.init_loopback() # logging is required by test_N_file_with_percent -- cgit v1.2.3 From 3b7c8ee3bd87905d8ae6964aea3e2c1181672289 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:36:32 +0100 Subject: Convert Channel into a context manager --- paramiko/channel.py | 3 ++- tests/test_transport.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 49d8dd6e..144edefd 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -37,13 +37,14 @@ from paramiko.ssh_exception import SSHException from paramiko.file import BufferedFile from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe +from paramiko.util import ClosingContextManager # lower bound on the max packet size we'll accept from the remote host MIN_PACKET_SIZE = 1024 -class Channel (object): +class Channel (ClosingContextManager): """ A secure tunnel across an SSH `.Transport`. A Channel is meant to behave like a socket, and has an API that should be indistinguishable from the diff --git a/tests/test_transport.py b/tests/test_transport.py index 485a18e8..ae0f5e01 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -20,6 +20,8 @@ Some unit tests for the ssh2 protocol in Transport. """ +from __future__ import with_statement + from binascii import hexlify import select import socket @@ -278,6 +280,22 @@ class TransportTest(ParamikoTest): self.assertEqual('Hello there.\n', f.readline()) self.assertEqual('This is on stderr.\n', f.readline()) self.assertEqual('', f.readline()) + + def test_6a_channel_can_be_used_as_context_manager(self): + """ + verify that exec_command() does something reasonable. + """ + self.setup_test_server() + + with self.tc.open_session() as chan: + with self.ts.accept(1.0) as schan: + chan.exec_command('yes') + schan.send('Hello there.\n') + schan.close() + + f = chan.makefile() + self.assertEqual('Hello there.\n', f.readline()) + self.assertEqual('', f.readline()) def test_7_invoke_shell(self): """ -- cgit v1.2.3 From 4d1945c65932fcdcbba40c17f8fbc926b140f73b Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:40:25 +0100 Subject: Convert Transport to context manager --- paramiko/transport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index d3990e54..eb68211f 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -57,7 +57,7 @@ from paramiko.server import ServerInterface from paramiko.sftp_client import SFTPClient from paramiko.ssh_exception import (SSHException, BadAuthenticationType, ChannelException, ProxyCommandFailure) -from paramiko.util import retry_on_signal +from paramiko.util import retry_on_signal, ClosingContextManager from Crypto.Cipher import Blowfish, AES, DES3, ARC4 try: @@ -77,7 +77,7 @@ import atexit atexit.register(_join_lingering_threads) -class Transport (threading.Thread): +class Transport (threading.Thread, ClosingContextManager): """ An SSH Transport attaches to a stream (usually a socket), negotiates an encrypted session, authenticates, and then creates stream tunnels, called -- cgit v1.2.3 From a6c8b1118c5dd7e49d75cc87298709a545f9d74b Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:41:33 +0100 Subject: Fix error on Python 2.5 --- tests/test_sftp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index e3f7e59b..743a7831 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -813,7 +813,7 @@ class SFTPTest (unittest.TestCase): sftp.open(FOLDER + '/test2', 'w') self.fail('expected exception') # TODO: extract failing command (the open) to share with a passing assertion - except EOFError as error: + except EOFError: pass -- cgit v1.2.3 From cd07ddbfbb34a2482e90f3b95de97a1584ee8a9f Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:42:31 +0100 Subject: Remove TODO --- tests/test_sftp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 743a7831..58013cfd 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -812,7 +812,6 @@ class SFTPTest (unittest.TestCase): try: sftp.open(FOLDER + '/test2', 'w') self.fail('expected exception') - # TODO: extract failing command (the open) to share with a passing assertion except EOFError: pass -- cgit v1.2.3 From e92b82eb8a2a08259f21eb711e8943382e0020f1 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:45:12 +0100 Subject: Document that classes can be used as context managers --- paramiko/channel.py | 2 ++ paramiko/sftp_client.py | 2 ++ paramiko/transport.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/paramiko/channel.py b/paramiko/channel.py index 144edefd..95116fb7 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -57,6 +57,8 @@ class Channel (ClosingContextManager): flow-controlled independently.) Similarly, if the server isn't reading data you send, calls to `send` may block, unless you set a timeout. This is exactly like a normal network socket, so it shouldn't be too surprising. + + Instances of this class may be used as context managers. """ def __init__(self, chanid): diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index b45ad442..195d1ad5 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -65,6 +65,8 @@ class SFTPClient(BaseSFTP, ClosingContextManager): Used to open an SFTP session across an open SSH `.Transport` and perform remote file operations. + + Instances of this class may be used as context managers. """ def __init__(self, sock): """ diff --git a/paramiko/transport.py b/paramiko/transport.py index eb68211f..a8dae49b 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -84,6 +84,8 @@ class Transport (threading.Thread, ClosingContextManager): `channels <.Channel>`, across the session. Multiple channels can be multiplexed across a single session (and often are, in the case of port forwardings). + + Instances of this class may be used as context managers. """ _PROTO_ID = '2.0' _CLIENT_ID = 'paramiko_%s' % paramiko.__version__ -- cgit v1.2.3 From b14aedee595ba673d58e04014ff6be65963740da Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:50:22 +0100 Subject: Turn ProxyCommand into a context manager --- paramiko/proxy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/paramiko/proxy.py b/paramiko/proxy.py index 8959b244..0664ac6e 100644 --- a/paramiko/proxy.py +++ b/paramiko/proxy.py @@ -26,9 +26,10 @@ from select import select import socket from paramiko.ssh_exception import ProxyCommandFailure +from paramiko.util import ClosingContextManager -class ProxyCommand(object): +class ProxyCommand(ClosingContextManager): """ Wraps a subprocess running ProxyCommand-driven programs. @@ -36,6 +37,8 @@ class ProxyCommand(object): `.Transport` and `.Packetizer` classes. Using this class instead of a regular socket makes it possible to talk with a Popen'd command that will proxy traffic between the client and a server hosted in another machine. + + Instances of this class may be used as context managers. """ def __init__(self, command_line): """ -- cgit v1.2.3 From e0b323d7fb0d355bf7fcc469d3a24bd5a55f7ee4 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:52:59 +0100 Subject: Turn BufferedFile into a context manager --- paramiko/file.py | 4 +++- paramiko/sftp_file.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/paramiko/file.py b/paramiko/file.py index 2238f0bf..09998829 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -19,8 +19,10 @@ from paramiko.common import linefeed_byte_value, crlf, cr_byte, linefeed_byte, \ cr_byte_value from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types +from paramiko.util import ClosingContextManager -class BufferedFile (object): + +class BufferedFile (ClosingContextManager): """ Reusable base class to implement Python-style file buffering around a simpler stream. diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index e0fb2297..d0a37da3 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -34,10 +34,9 @@ from paramiko.py3compat import long from paramiko.sftp import CMD_CLOSE, CMD_READ, CMD_DATA, SFTPError, CMD_WRITE, \ CMD_STATUS, CMD_FSTAT, CMD_ATTRS, CMD_FSETSTAT, CMD_EXTENDED from paramiko.sftp_attr import SFTPAttributes -from paramiko.util import ClosingContextManager -class SFTPFile (BufferedFile, ClosingContextManager): +class SFTPFile (BufferedFile): """ Proxy object for a file on the remote server, in client mode SFTP. -- cgit v1.2.3 From 4c3b973c78b209170b2bbd7b50a0de1638a12e4a Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 17:56:41 +0100 Subject: Turn SFTPHandle into context managers --- paramiko/sftp_handle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py index 92dd9cfe..edceb5ad 100644 --- a/paramiko/sftp_handle.py +++ b/paramiko/sftp_handle.py @@ -22,9 +22,10 @@ Abstraction of an SFTP file handle (for server mode). import os from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK +from paramiko.util import ClosingContextManager -class SFTPHandle (object): +class SFTPHandle (ClosingContextManager): """ Abstract object representing a handle to an open file (or folder) in an SFTP server implementation. Each handle has a string representation used @@ -32,6 +33,8 @@ class SFTPHandle (object): Server implementations can (and should) subclass SFTPHandle to implement features of a file handle, like `stat` or `chattr`. + + Instances of this class may be used as context managers. """ def __init__(self, flags=0): """ -- cgit v1.2.3 From 0063e64046c732e8c50fc5f54234942feaa313d9 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Sun, 29 Sep 2013 18:10:12 +0100 Subject: Convert SSHClient into a context manager --- paramiko/client.py | 6 ++++-- tests/test_client.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 4326abbd..d718135e 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -37,10 +37,10 @@ from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey from paramiko.ssh_exception import SSHException, BadHostKeyException from paramiko.transport import Transport -from paramiko.util import retry_on_signal +from paramiko.util import retry_on_signal, ClosingContextManager -class SSHClient (object): +class SSHClient (ClosingContextManager): """ A high-level representation of a session with an SSH server. This class wraps `.Transport`, `.Channel`, and `.SFTPClient` to take care of most @@ -55,6 +55,8 @@ class SSHClient (object): checking. The default mechanism is to try to use local key files or an SSH agent (if one is running). + Instances of this class may be used as context managers. + .. versionadded:: 1.6 """ diff --git a/tests/test_client.py b/tests/test_client.py index 7c094628..b3635272 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -20,6 +20,8 @@ Some unit tests for SSHClient. """ +from __future__ import with_statement + import socket from tempfile import mkstemp import threading @@ -293,3 +295,25 @@ class SSHClientTest (unittest.TestCase): gc.collect() self.assertTrue(p() is None) + + def test_6_client_can_be_used_as_context_manager(self): + """ + verify that an SSHClient can be used a context manager + """ + threading.Thread(target=self._run).start() + host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = paramiko.RSAKey(data=host_key.asbytes()) + + with paramiko.SSHClient() as tc: + self.tc = tc + self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.assertEquals(0, len(self.tc.get_host_keys())) + self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') + + self.event.wait(1.0) + self.assertTrue(self.event.isSet()) + self.assertTrue(self.ts.is_active()) + + self.assertTrue(self.tc._transport is not None) + + self.assertTrue(self.tc._transport is None) -- cgit v1.2.3 From b36b87ceefd50fa691eb5b46865d0c28a8c511dd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 10:48:33 -0700 Subject: Changelog re #298 --- sites/www/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 44bd61e9..2f23d0fc 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,15 @@ Changelog ========= +* :bug:`298 major` Don't perform point validation on ECDSA keys in + ``known_hosts`` files, since a) this can cause significant slowdown when such + keys exist, and b) ``known_hosts`` files are implicitly trustworthy. Thanks + to Kieran Spear for catch & patch. + + .. note:: + This change bumps up the version requirement for the ``ecdsa`` library to + ``0.11``. + * :bug:`234 major` Lower logging levels for a few overly-noisy log messages about secure channels. Thanks to David Pursehouse for noticing & contributing the fix. -- cgit v1.2.3 From 69cb2248dc442ebebf7c143534b1865a23b10f61 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 10:58:43 -0700 Subject: Formatting and docstring tweaks re #377 --- paramiko/channel.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index ae1a06fd..38772283 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -43,16 +43,27 @@ from paramiko import pipe # lower bound on the max packet size we'll accept from the remote host MIN_PACKET_SIZE = 1024 + def requires_open_channel(func): - """This decorator makes sure that the channel is open, else it raises an - SSHException.""" + """ + Decorator for `.Channel` methods which performs an openness check. + + :raises SSHException: + If the wrapped method is called on an unopened `.Channel`. + """ @wraps(func) def _check(self, *args, **kwds): - if self.closed or self.eof_received or self.eof_sent or not self.active: + if ( + self.closed + or self.eof_received + or self.eof_sent + or not self.active + ): raise SSHException('Channel is not open') return func(self, *args, **kwds) return _check + class Channel (object): """ A secure tunnel across an SSH `.Transport`. A Channel is meant to behave -- cgit v1.2.3 From fb9c4cbb0c31fe22125e7e400f08fba4638dce33 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 11:01:43 -0700 Subject: Make new decorator name a bit slimmer re #377 --- paramiko/channel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 38772283..dadfe184 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -44,7 +44,7 @@ from paramiko import pipe MIN_PACKET_SIZE = 1024 -def requires_open_channel(func): +def open_only(func): """ Decorator for `.Channel` methods which performs an openness check. @@ -143,7 +143,7 @@ class Channel (object): out += '>' return out - @requires_open_channel + @open_only def get_pty(self, term='vt100', width=80, height=24, width_pixels=0, height_pixels=0): """ @@ -177,7 +177,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() - @requires_open_channel + @open_only def invoke_shell(self): """ Request an interactive shell session on this channel. If the server @@ -203,7 +203,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() - @requires_open_channel + @open_only def exec_command(self, command): """ Execute a command on the server. If the server allows it, the channel @@ -229,7 +229,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() - @requires_open_channel + @open_only def invoke_subsystem(self, subsystem): """ Request a subsystem on the server (for example, ``sftp``). If the @@ -254,7 +254,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() - @requires_open_channel + @open_only def resize_pty(self, width=80, height=24, width_pixels=0, height_pixels=0): """ Resize the pseudo-terminal. This can be used to change the width and @@ -330,7 +330,7 @@ class Channel (object): m.add_int(status) self.transport._send_user_message(m) - @requires_open_channel + @open_only def request_x11(self, screen_number=0, auth_protocol=None, auth_cookie=None, single_connection=False, handler=None): """ @@ -391,7 +391,7 @@ class Channel (object): self.transport._set_x11_handler(handler) return auth_cookie - @requires_open_channel + @open_only def request_forward_agent(self, handler): """ Request for a forward SSH Agent on this channel. -- cgit v1.2.3 From 76aba9dbda5d5cad6b3c092e943c538079fcc7f0 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 11:01:47 -0700 Subject: Changelog re #377 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 2f23d0fc..afe3b78d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`377` Factor `~paramiko.channel.Channel` openness sanity check into + a decorator. Thanks to Olle Lundberg for original patch. * :bug:`298 major` Don't perform point validation on ECDSA keys in ``known_hosts`` files, since a) this can cause significant slowdown when such keys exist, and b) ``known_hosts`` files are implicitly trustworthy. Thanks -- cgit v1.2.3 From a0f854d17fc59f0279d4d2b07d3fd810dfb1894e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 11:10:30 -0700 Subject: Changelog re #374, #375 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index afe3b78d..41e8310d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`374` (also :issue:`375`) Old code cleanup courtesy of Olle + Lundberg. * :support:`377` Factor `~paramiko.channel.Channel` openness sanity check into a decorator. Thanks to Olle Lundberg for original patch. * :bug:`298 major` Don't perform point validation on ECDSA keys in -- cgit v1.2.3 From 24e022bdf656f272b4dafb76df1a7739965be2f9 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 12:50:03 -0700 Subject: Changelog re #373 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 41e8310d..87f54e4e 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`373 major` Attempt to fix a handful of issues (such as :issue:`354`) + related to infinite loops and threading deadlocks. Thanks to Olle Lundberg as + well as a handful of community members who provided advice & feedback via + IRC. * :support:`374` (also :issue:`375`) Old code cleanup courtesy of Olle Lundberg. * :support:`377` Factor `~paramiko.channel.Channel` openness sanity check into -- cgit v1.2.3 From f564b0b155d9f30b32fe2b84f87eadc52d499afb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 14:54:20 -0700 Subject: Add versionchanged notices, and some minor formatting tweaks --- paramiko/sftp_client.py | 3 +++ paramiko/transport.py | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 84b70961..9c30426d 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -116,6 +116,9 @@ class SFTPClient(BaseSFTP): :return: a new `.SFTPClient` object, referring to an sftp session (channel) across the transport + + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. """ chan = t.open_session(window_size=window_size, max_packet_size=max_packet_size) diff --git a/paramiko/transport.py b/paramiko/transport.py index c43db6e2..c651429e 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -165,10 +165,11 @@ class Transport (threading.Thread): address and used for communication. Exceptions from the ``socket`` call may be thrown in this case. - .. note:: Modifying the the window and packet sizes might have adverse - effects on your channels created from this transport. The - default values are the same as in the OpenSSH code base and have - been battle tested. + .. note:: + Modifying the the window and packet sizes might have adverse + effects on your channels created from this transport. The default + values are the same as in the OpenSSH code base and have been + battle tested. :param socket sock: a socket or socket-like object to create the session over. @@ -178,6 +179,10 @@ class Transport (threading.Thread): :param int default_max_packet_size: sets the default max packet size on the transport. (defaults to 32768) + + .. versionchanged:: 1.15 + Added the ``default_window_size`` and ``default_max_packet_size`` + arguments. """ self.active = False @@ -334,9 +339,10 @@ class Transport (threading.Thread): .. note:: `connect` is a simpler method for connecting as a client. - .. note:: After calling this method (or `start_server` or `connect`), - you should no longer directly read from or write to the original - socket object. + .. note:: + After calling this method (or `start_server` or `connect`), you + should no longer directly read from or write to the original socket + object. :param .threading.Event event: an event to trigger when negotiation is complete (optional) @@ -562,6 +568,9 @@ class Transport (threading.Thread): :raises SSHException: if the request is rejected or the session ends prematurely + + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. """ return self.open_channel('session', window_size=window_size, @@ -641,6 +650,9 @@ class Transport (threading.Thread): :raises SSHException: if the request is rejected or the session ends prematurely + + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. """ if not self.active: raise SSHException('SSH session not active') -- cgit v1.2.3 From 88d932f2bbab907879639122969fb1f763258d29 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 14:58:45 -0700 Subject: Changelog re #372 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 87f54e4e..9dfb254f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :feature:`372` Update default window & packet sizes to more closely adhere to + the pertinent RFC; also expose these settings in the public API so they may + be overridden by client code. This should address some general speed issues + such as :issue:`175`. Big thanks to Olle Lundberg for the update. * :bug:`373 major` Attempt to fix a handful of issues (such as :issue:`354`) related to infinite loops and threading deadlocks. Thanks to Olle Lundberg as well as a handful of community members who provided advice & feedback via -- cgit v1.2.3 From 5c45cd9af604525d9aba87f1ce173e6b0a51130c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 15:03:20 -0700 Subject: Add versionchanged --- paramiko/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/paramiko/client.py b/paramiko/client.py index 4f75cefe..31d99e96 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -221,6 +221,9 @@ class SSHClient (object): :raises SSHException: if there was any other error connecting or establishing an SSH session :raises socket.error: if a socket error occurred while connecting + + .. versionchanged:: 1.15 + Added the ``banner_timeout`` argument. """ if not sock: for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): -- cgit v1.2.3 From 9c9dcaf4885653f4dcd100b2724199976a20c21e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 15:04:41 -0700 Subject: Changelog re #362 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9dfb254f..879483a4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :feature:`362` Allow users to control the SSH banner timeout. Thanks to Cory + Benfield. * :feature:`372` Update default window & packet sizes to more closely adhere to the pertinent RFC; also expose these settings in the public API so they may be overridden by client code. This should address some general speed issues -- cgit v1.2.3 From 35fa518d8b0b9c91dbd498aa20225825c428db60 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 15:37:23 -0700 Subject: Test proving #346 --- tests/test_pkey.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 1468ee27..3d5f0326 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -21,6 +21,7 @@ Some unit tests for public/private key objects. """ import unittest +import os from binascii import hexlify from hashlib import md5 @@ -253,3 +254,20 @@ class KeyTest (unittest.TestCase): msg.rewind() pub = ECDSAKey(data=key.asbytes()) self.assertTrue(pub.verify_ssh_sig(b'ice weasels', msg)) + + def test_salt_size(self): + # Read an existing encrypted private key + file_ = test_path('test_rsa_password.key') + password = 'television' + newfile = file_ + '.new' + newpassword = 'radio' + key = RSAKey(filename=file_, password=password) + # Write out a newly re-encrypted copy with a new password. + # When the bug under test exists, this will ValueError. + try: + key.write_private_key_file(newfile, password=newpassword) + # Verify the inner key data still matches (when no ValueError) + key2 = RSAKey(newfile, password=newpassword) + self.assertEqual(key, key2) + finally: + os.remove(newfile) -- cgit v1.2.3 From f99dfdeea21c63f8df8622ebd2fe1357bcf0ed46 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 15:40:15 -0700 Subject: Fix a braino in new test --- tests/test_pkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 3d5f0326..f673254f 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -267,7 +267,7 @@ class KeyTest (unittest.TestCase): try: key.write_private_key_file(newfile, password=newpassword) # Verify the inner key data still matches (when no ValueError) - key2 = RSAKey(newfile, password=newpassword) + key2 = RSAKey(filename=newfile, password=newpassword) self.assertEqual(key, key2) finally: os.remove(newfile) -- cgit v1.2.3 From de391e88e0a7e75cd977f162a883aa5ffdbdc591 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 15:41:46 -0700 Subject: Changelog re #346 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 879483a4..e111aab4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`346 major` Fix an issue in private key files' encryption salts that + could cause tracebacks and file corruption if keys were re-encrypted. Credit + to Xavier Nunn. * :feature:`362` Allow users to control the SSH banner timeout. Thanks to Cory Benfield. * :feature:`372` Update default window & packet sizes to more closely adhere to -- cgit v1.2.3 From d992118747e2f1dab247bd4e3d78b55d9b99c759 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 15:59:40 -0700 Subject: Update changelog re #267, #250 --- sites/www/changelog.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f97b4970..a42cfb3d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,8 +2,9 @@ Changelog ========= -* :feature:`250` GSS-API / SSPI authenticated Diffie-Hellman Key Exchange and - user authentication. +* :feature:`250` (also :issue:`267`) Add GSS-API / SSPI (e.g. Kerberos) key + exchange and authentication support. Mega thanks to Sebastian Deiß, with + assist by Torsten Landschoff. * :bug:`346 major` Fix an issue in private key files' encryption salts that could cause tracebacks and file corruption if keys were re-encrypted. Credit to Xavier Nunn. -- cgit v1.2.3 From 150b0797e935ebf2f62e86ae1c08a1a1ab94c459 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:36:33 -0700 Subject: Update README, docs, changelog re #267 --- README | 16 +++------------- sites/www/changelog.rst | 4 ++-- sites/www/installing.rst | 28 +++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/README b/README index ceb3598a..b5ccb697 100644 --- a/README +++ b/README @@ -75,19 +75,9 @@ Please file bug reports at https://github.com/paramiko/paramiko/. There is curre Kerberos Support ---------------- -If you want paramiko to do kerberos authentication or key exchange using GSS-API or SSPI, you -need the following python packages: - -- pyasn1 0.1.7 or better -- python-gssapi 0.6.1 or better (Unix) -- pywin32 2.1.8 or better (Windows) - -So you have to install pyasn1 and python-gssapi on Unix or pywin32 on Windows. -To enable GSS-API / SSPI authentication or key exchange see the demos or paramiko docs. -Note: If you use Microsoft SSPI for kerberos authentication and credential -delegation in paramiko, make sure that the target host is trusted for -delegation in the active directory configuration. For details see: -http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx +Paramiko ships with optional Kerberos/GSSAPI support; for info on the extra +dependencies for this, see the 'GSS-API' section on the 'Installation' page of +our main website, http://paramiko.org . Demo diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index a42cfb3d..3be56890 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -3,8 +3,8 @@ Changelog ========= * :feature:`250` (also :issue:`267`) Add GSS-API / SSPI (e.g. Kerberos) key - exchange and authentication support. Mega thanks to Sebastian Deiß, with - assist by Torsten Landschoff. + exchange and authentication support (:ref:`installation docs here `). + Mega thanks to Sebastian Deiß, with assist by Torsten Landschoff. * :bug:`346 major` Fix an issue in private key files' encryption salts that could cause tracebacks and file corruption if keys were re-encrypted. Credit to Xavier Nunn. diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 052825c4..5528b28a 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -20,11 +20,14 @@ We currently support **Python 2.6, 2.7 and 3.3+** (Python **3.2** should also work but has a less-strong compatibility guarantee from us.) Users on Python 2.5 or older are urged to upgrade. -Paramiko has two dependencies: the pure-Python ECDSA module ``ecdsa``, and the +Paramiko has two hard dependencies: the pure-Python ECDSA module ``ecdsa``, and the PyCrypto C extension. ``ecdsa`` is easily installable from wherever you obtained Paramiko's package; PyCrypto may require more work. Read on for details. +If you need GSS-API / SSPI support, see :ref:`the below subsection on it +` for details on additional dependencies. + .. _release-lines: Release lines @@ -99,3 +102,26 @@ installation of Paramiko via ``pypm``:: Installing paramiko-1.7.8 Installing pycrypto-2.4 C:\> + + +.. _gssapi: + +Optional dependencies for GSS-API / SSPI / Kerberos +=================================================== + +In order to use Kerberos & related functionality, a couple of additional +dependencies are required (these are not listed in our ``setup.py`` due to +their infrequent utility & non-platform-agnostic requirements): + +* **All platforms** need `pyasn1 `_ + ``0.1.7`` or better. +* **Unix** needs `python-gssapi `_ + ``0.6.1`` or better. +* **Windows** needs `pywin32 `_ ``2.1.8`` + or better. + +.. note:: + If you use Microsoft SSPI for kerberos authentication and credential + delegation, make sure that the target host is trusted for delegation in the + active directory configuration. For details see: + http://technet.microsoft.com/en-us/library/cc738491%28v=ws.10%29.aspx -- cgit v1.2.3 From e05f3bce49c38b2b861bda4a96b0b8f19a84863a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:37:57 -0700 Subject: Tweak changelog again for more tickets, use actually-merged PR as main issue number --- sites/www/changelog.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 3be56890..1dab5219 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,9 +2,10 @@ Changelog ========= -* :feature:`250` (also :issue:`267`) Add GSS-API / SSPI (e.g. Kerberos) key - exchange and authentication support (:ref:`installation docs here `). - Mega thanks to Sebastian Deiß, with assist by Torsten Landschoff. +* :feature:`267` (also :issue:`250`, :issue:`241`, :issue:`228`) Add GSS-API / + SSPI (e.g. Kerberos) key exchange and authentication support + (:ref:`installation docs here `). Mega thanks to Sebastian Deiß, with + assist by Torsten Landschoff. * :bug:`346 major` Fix an issue in private key files' encryption salts that could cause tracebacks and file corruption if keys were re-encrypted. Credit to Xavier Nunn. -- cgit v1.2.3 From 6b580b9feb54e1c73325e0c915021649ea8d479f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:45:59 -0700 Subject: Tighten up module docstrings of GSSAPI API files. * We don't use this style of header anywhere else * Links to the original author's website/links aren't going to help; users rarely observe such info in practice :( * The core info (credit, authorship, license) is retained elsewhere, either in this file, the changelog, or Git history --- paramiko/kex_gss.py | 23 +++-------------------- paramiko/ssh_gss.py | 25 ++++--------------------- sites/docs/api/kex_gss.rst | 4 ++-- 3 files changed, 9 insertions(+), 43 deletions(-) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 02f943ba..9669f86a 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -21,26 +21,9 @@ """ -This module provides GSS-API / SSPI Key Exchange for Paramiko as defined in -RFC 4462 with the following restrictions: -Credential delegation is not supported in server mode, -To Use this module, you need the following additional python packages: -`pyasn1 >= 0.1.7 `_, -`python-gssapi >= 0.4.0 (Unix) `_, -`pywin32 2.1.8 (Windows) `_. - -:summary: SSH2 GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange Module -:version: 0.1 -:author: Sebastian Deiss -:contact: https://github.com/SebastianDeiss/paramiko/issues -:organization: science + computing ag - `EMail `_ -:copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 `science + computing ag - `_ -:license: GNU Lesser General Public License (LGPL) -:see: `.ssh_gss` - -Created on 12.12.2013 +This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. + +.. note:: Credential delegation is not supported in server mode. """ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 03c5dcc0..1d179025 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -18,28 +18,11 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + """ -This module provides GSS-API / SSPI authentication for Paramiko as defined in -RFC 4462 with the following restrictions: -Credential delegation is not supported in server mode, -GSS-API key exchange is supported, but not implemented in Paramiko. -To Use this module, you need the following additional python packages: -`pyasn1 >= 0.1.7 `_, -`python-gssapi >= 0.4.0 (Unix) `_, -`pywin32 2.1.8 (Windows) `_. - -:summary: SSH2 GSS-API / SSPI authentication module -:version: 0.1 -:author: Sebastian Deiss -:contact: https://github.com/SebastianDeiss/paramiko/issues -:organization: science + computing ag - `EMail `_ -:copyright: (C) 2013-2014 `science + computing ag - `_ -:license: GNU Lesser General Public License (LGPL) -:see: `.kex_gss` - -Created on 07.11.2013 +This module provides GSS-API / SSPI authentication as defined in RFC 4462. + +.. note:: Credential delegation is not supported in server mode. """ import struct diff --git a/sites/docs/api/kex_gss.rst b/sites/docs/api/kex_gss.rst index a662be01..67c7a9a4 100644 --- a/sites/docs/api/kex_gss.rst +++ b/sites/docs/api/kex_gss.rst @@ -1,5 +1,5 @@ -GSS-API Key Exchange Module -=========================== +GSS-API Key Exchange +==================== .. automodule:: paramiko.kex_gss :member-order: bysource -- cgit v1.2.3 From 67810da14504bd4b4be60f8cb3067d4d6b46abdf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:48:47 -0700 Subject: Tweak API stub titles --- sites/docs/api/kex_gss.rst | 2 +- sites/docs/api/ssh_gss.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sites/docs/api/kex_gss.rst b/sites/docs/api/kex_gss.rst index 67c7a9a4..9fd09221 100644 --- a/sites/docs/api/kex_gss.rst +++ b/sites/docs/api/kex_gss.rst @@ -1,4 +1,4 @@ -GSS-API Key Exchange +GSS-API key exchange ==================== .. automodule:: paramiko.kex_gss diff --git a/sites/docs/api/ssh_gss.rst b/sites/docs/api/ssh_gss.rst index 1b08c7f8..1ce9daf7 100644 --- a/sites/docs/api/ssh_gss.rst +++ b/sites/docs/api/ssh_gss.rst @@ -1,5 +1,5 @@ -Paramiko GSS-API Interface -========================== +GSS-API interface +================= .. automodule:: paramiko.ssh_gss :member-order: bysource -- cgit v1.2.3 From 0010903c45cebd7b7d995d80de763147b5c7151f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:49:33 -0700 Subject: Match rest of API stubs --- sites/docs/api/agent.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/docs/api/agent.rst b/sites/docs/api/agent.rst index 3b614a82..f01ad972 100644 --- a/sites/docs/api/agent.rst +++ b/sites/docs/api/agent.rst @@ -1,4 +1,4 @@ -SSH Agents +SSH agents ========== .. automodule:: paramiko.agent -- cgit v1.2.3 From 6e91d103e8e6c618f1c514638bd2e7243e0e3767 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:50:47 -0700 Subject: Reinstate working seealso's --- paramiko/kex_gss.py | 2 ++ paramiko/ssh_gss.py | 2 ++ sites/docs/api/ssh_gss.rst | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 9669f86a..cdb18496 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -24,6 +24,8 @@ This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. .. note:: Credential delegation is not supported in server mode. + +.. seealso:: :doc:`/api/ssh_gss` """ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 1d179025..2fdde227 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -23,6 +23,8 @@ This module provides GSS-API / SSPI authentication as defined in RFC 4462. .. note:: Credential delegation is not supported in server mode. + +.. seealso:: :doc:`/api/kex_gss` """ import struct diff --git a/sites/docs/api/ssh_gss.rst b/sites/docs/api/ssh_gss.rst index 1ce9daf7..7a687e11 100644 --- a/sites/docs/api/ssh_gss.rst +++ b/sites/docs/api/ssh_gss.rst @@ -1,5 +1,5 @@ -GSS-API interface -================= +GSS-API authentication +====================== .. automodule:: paramiko.ssh_gss :member-order: bysource -- cgit v1.2.3 From 4a950ea474a783e6de870612f963d35c73486df1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 16:51:46 -0700 Subject: Nuke extraneous 'for Paramiko' now that it's inside us --- paramiko/ssh_gss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index 2fdde227..ec21a79a 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -76,7 +76,7 @@ except (ImportError, OSError): def GSSAuth(auth_method, gss_deleg_creds=True): """ - Provide SSH2 GSS-API / SSPI authentication for Paramiko. + Provide SSH2 GSS-API / SSPI authentication. :param str auth_method: The name of the SSH authentication mechanism (gssapi-with-mic or gss-keyex) -- cgit v1.2.3 From abd63a1334a1874ac3c85a00a9abeffb39eb049e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:03:15 -0700 Subject: Not sure why this variable was changed, but caps are Bad. --- demos/demo_simple.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demos/demo_simple.py b/demos/demo_simple.py index 7f179b90..3a17988c 100755 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -39,7 +39,7 @@ paramiko.util.log_to_file('demo_simple.log') # Paramiko client configuration UseGSSAPI = True # enable GSS-API / SSPI authentication DoGSSAPIKeyExchange = True -Port = 22 +port = 22 # get hostname username = '' @@ -55,7 +55,7 @@ if len(hostname) == 0: if hostname.find(':') >= 0: hostname, portstr = hostname.split(':') - Port = int(portstr) + port = int(portstr) # get username @@ -75,16 +75,16 @@ try: client.set_missing_host_key_policy(paramiko.WarningPolicy()) print('*** Connecting...') if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): - client.connect(hostname, Port, username, password) + client.connect(hostname, port, username, password) else: # SSPI works only with the FQDN of the target host hostname = socket.getfqdn(hostname) try: - client.connect(hostname, Port, username, gss_auth=UseGSSAPI, + client.connect(hostname, port, username, gss_auth=UseGSSAPI, gss_kex=DoGSSAPIKeyExchange) except Exception: password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) - client.connect(hostname, Port, username, password) + client.connect(hostname, port, username, password) chan = client.invoke_shell() print(repr(client.get_transport())) -- cgit v1.2.3 From ea9c1d2801440f8bc8859f1c3638033b09a86fa2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:05:24 -0700 Subject: Unclear why this was a docstring, it feels like a comment --- paramiko/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index e773c1b6..62fc90ce 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -270,11 +270,9 @@ class SSHClient (object): else: server_hostkey_name = "[%s]:%d" % (hostname, port) - """ - If GSS-API Key Exchange is performed we are not required to check the - host key, because the host is authenticated via GSS-API / SSPI as well - as out client. - """ + # If GSS-API Key Exchange is performed we are not required to check the + # host key, because the host is authenticated via GSS-API / SSPI as + # well as our client. if not self._transport.use_gss_kex: our_server_key = self._system_host_keys.get(server_hostkey_name, {}).get(keytype, None) -- cgit v1.2.3 From f8355eed688b1b2e0a0c23ab9a210fd8b8be4a96 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:10:56 -0700 Subject: Fix outdated reference syntax in a demo --- demos/demo_server.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/demos/demo_server.py b/demos/demo_server.py index 74e4677e..5b3d5164 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -71,15 +71,18 @@ class Server (paramiko.ServerInterface): gss_authenticated=paramiko.AUTH_FAILED, cc_file=None): """ - @note: We are just checking in L{AuthHandler} that the given user is - a valid krb5 principal! - We don't check if the krb5 principal is allowed to log in on - the server, because there is no way to do that in python. So - if you develop your own SSH server with paramiko for a certain - platform like Linux, you should call C{krb5_kuserok()} in your - local kerberos library to make sure that the krb5_principal has - an account on the server and is allowed to log in as a user. - @see: U{krb5_kuserok() man page } + .. note:: + We are just checking in `AuthHandler` that the given user is a + valid krb5 principal! We don't check if the krb5 principal is + allowed to log in on the server, because there is no way to do that + in python. So if you develop your own SSH server with paramiko for + a certain platform like Linux, you should call ``krb5_kuserok()`` in + your local kerberos library to make sure that the krb5_principal + has an account on the server and is allowed to log in as a user. + + .. seealso:: + `krb5_kuserok() man page + `_ """ if gss_authenticated == paramiko.AUTH_SUCCESSFUL: return paramiko.AUTH_SUCCESSFUL -- cgit v1.2.3 From ed09d7f461852a23b338b4773360a3c231ba65ad Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:11:22 -0700 Subject: Why were these all docstrings? --- paramiko/auth_handler.py | 54 ++++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index a77ace1b..29b440f2 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -253,11 +253,9 @@ class AuthHandler (object): self._parse_userauth_banner(m) ptype, m = self.transport.packetizer.read_message() if ptype == MSG_USERAUTH_GSSAPI_RESPONSE: - """ - Read the mechanism selected by the server. - We send just the Kerberos V5 OID, so the server can only - respond with this OID. - """ + # Read the mechanism selected by the server. We send just + # the Kerberos V5 OID, so the server can only respond with + # this OID. mech = m.get_string() m = Message() m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) @@ -273,11 +271,9 @@ class AuthHandler (object): mech, self.username, srv_token) - """ - After this step the GSSAPI should not return any - token. If it does, we keep sending the token to the - server until no more token is returned. - """ + # After this step the GSSAPI should not return any + # token. If it does, we keep sending the token to + # the server until no more token is returned. if next_token is None: break else: @@ -292,11 +288,10 @@ class AuthHandler (object): # send the MIC to the server m.add_string(sshgss.ssh_get_mic(self.transport.session_id)) elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK: - """ - RFC 4462 says we are not required to implement GSS-API error - messages. - :see: `RFC 4462 Section 3.8 `_ - """ + # RFC 4462 says we are not required to implement GSS-API + # error messages. + # See RFC 4462 Section 3.8 in + # http://www.ietf.org/rfc/rfc4462.txt raise SSHException("Server returned an error token") elif ptype == MSG_USERAUTH_GSSAPI_ERROR: maj_status = m.get_int() @@ -455,16 +450,12 @@ class AuthHandler (object): return elif method == "gssapi-with-mic" and gss_auth: sshgss = GSSAuth(method) - """ - OpenSSH sends just one OID. It's the Kerveros V5 OID and that's - the only OID we support. - """ - # read the number of OID mechanisms supported by the client + # Read the number of OID mechanisms supported by the client. + # OpenSSH sends just one OID. It's the Kerveros V5 OID and that's + # the only OID we support. mechs = m.get_int() - """ - We can't accept more than one OID, so if the SSH client send more - than one disconnect - """ + # We can't accept more than one OID, so if the SSH client sends + # more than one, disconnect. if mechs > 1: self.transport._log(INFO, 'Disconnect: Received more than one GSS-API OID mechanism') @@ -478,11 +469,8 @@ class AuthHandler (object): self._disconnect_no_more_auth() # send the Kerberos V5 GSSAPI OID to the client supported_mech = sshgss.ssh_gss_oids("server") - """ - RFC 4462 says we are not required to implement GSS-API error - messages. - :see: `RFC 4462 Section 3.8 `_ - """ + # RFC 4462 says we are not required to implement GSS-API error + # messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt while True: m = Message() m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE) @@ -523,11 +511,9 @@ class AuthHandler (object): self._send_auth_result(username, method, result) raise if retval == 0: - """ - :todo: Implement client credential saving - The OpenSSH server is able to create a TGT with the delegated - client credentials, but this is not supported by GSS-API. - """ + # TODO: Implement client credential saving. + # The OpenSSH server is able to create a TGT with the delegated + # client credentials, but this is not supported by GSS-API. result = AUTH_SUCCESSFUL self.transport.server_object.check_auth_gssapi_with_mic(username, result) -- cgit v1.2.3 From 063d0c4beee475b05f77467020873438d7f1b8be Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:11:28 -0700 Subject: Formatting --- paramiko/auth_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 29b440f2..b5fea654 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -515,8 +515,8 @@ class AuthHandler (object): # The OpenSSH server is able to create a TGT with the delegated # client credentials, but this is not supported by GSS-API. result = AUTH_SUCCESSFUL - self.transport.server_object.check_auth_gssapi_with_mic(username, - result) + self.transport.server_object.check_auth_gssapi_with_mic( + username, result) else: result = AUTH_FAILED elif method == "gssapi-keyex" and gss_auth: -- cgit v1.2.3 From 14644ff7fb296f9ed40e4be913a17720ebd9b72d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:11:40 -0700 Subject: Add GSSAPI kwargs to a `versionchanged` --- paramiko/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index 62fc90ce..481cbafc 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -228,7 +228,8 @@ class SSHClient (object): :raises socket.error: if a socket error occurred while connecting .. versionchanged:: 1.15 - Added the ``banner_timeout`` argument. + Added the ``banner_timeout``, ``gss_auth``, ``gss_kex``, + ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): -- cgit v1.2.3 From ae3760ecd982f26079adc78b71249cd2b094d9ef Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:12:50 -0700 Subject: Clean up more of the unnecessary-IMHO headers --- tests/test_gssapi.py | 10 ---------- tests/test_kex_gss.py | 10 ---------- tests/test_ssh_gss.py | 10 ---------- 3 files changed, 30 deletions(-) diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py index e9ef99a9..0d3df72c 100644 --- a/tests/test_gssapi.py +++ b/tests/test_gssapi.py @@ -20,16 +20,6 @@ """ Test the used APIs for GSS-API / SSPI authentication - -@author: Sebastian Deiss -@contact: U{https://github.com/SebastianDeiss/paramiko/issues} -@organization: science + computing ag - (U{EMail}) -@copyright: (C) 2013-2014 U{science + computing ag - } -@license: GNU Lesser General Public License (LGPL) - -Created on 04.12.2013 """ import unittest diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index e160eb35..b5e277b3 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -22,16 +22,6 @@ """ Unit Tests for the GSS-API / SSPI SSHv2 Diffie-Hellman Key Exchange and user authentication - -@author: Sebastian Deiss -@contact: U{https://github.com/SebastianDeiss/paramiko/issues} -@organization: science + computing ag - (U{EMail}) -@copyright: (C) 2003-2009 Robey Pointer, (C) 2013-2014 U{science + computing ag - } -@license: GNU Lesser General Public License (LGPL) - -Created on 08.01.2014 """ diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py index 98e280ec..595081b8 100644 --- a/tests/test_ssh_gss.py +++ b/tests/test_ssh_gss.py @@ -21,16 +21,6 @@ """ Unit Tests for the GSS-API / SSPI SSHv2 Authentication (gssapi-with-mic) - -@author: Sebastian Deiss -@contact: U{https://github.com/SebastianDeiss/paramiko/issues} -@organization: science + computing ag - (U{EMail}) -@copyright: (C) 2003-2007 Robey Pointer, (C) 2013-2014 U{science + computing ag - } -@license: GNU Lesser General Public License (LGPL) - -Created on 04.12.2013 """ import socket -- cgit v1.2.3 From f2e90f1446e80c0941ec3f7e0bd411d6e858bb04 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:26:20 -0700 Subject: More of these things --- paramiko/client.py | 16 ++++++---------- paramiko/kex_gss.py | 10 ++-------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 481cbafc..265389de 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -404,10 +404,8 @@ class SSHClient (object): two_factor = False allowed_types = [] - """ - If GSS-API support and GSS-PI Key Exchange was performed, we attempt - authentication with gssapi-keyex. - """ + # If GSS-API support and GSS-PI Key Exchange was performed, we attempt + # authentication with gssapi-keyex. if gss_kex and self._transport.gss_kex_used: try: self._transport.auth_gssapi_keyex(username) @@ -415,12 +413,10 @@ class SSHClient (object): except Exception as e: saved_exception = e - """ - Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key - Exchange is not performed, because if we use GSS-API for the key - exchange, there is already a fully established GSS-API context, so - why should we do that again? - """ + # Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key + # Exchange is not performed, because if we use GSS-API for the key + # exchange, there is already a fully established GSS-API context, so + # why should we do that again? if gss_auth: try: self._transport.auth_gssapi_with_mic(username, gss_host, diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index cdb18496..a4e60909 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -180,10 +180,7 @@ class KexGSSGroup1(object): if (self.f < 1) or (self.f > self.P - 1): raise SSHException('Server kex "f" is out of range') mic_token = m.get_string() - """ - This must be TRUE, if there is a GSS-API token in this - message. - """ + # This must be TRUE, if there is a GSS-API token in this message. bool = m.get_boolean() srv_token = None if bool: @@ -545,10 +542,7 @@ class KexGSSGex(object): self.transport.host_key = NullHostKey() self.f = m.get_mpint() mic_token = m.get_string() - """ - This must be TRUE, if there is a GSS-API token in this - message. - """ + # This must be TRUE, if there is a GSS-API token in this message. bool = m.get_boolean() srv_token = None if bool: -- cgit v1.2.3 From fd8a2a06ac35f25f49a5bfb40bcfadb504586963 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:26:38 -0700 Subject: Refactoring: a thing, even for docstrings. --- paramiko/kex_gss.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index a4e60909..ec7285c2 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -25,6 +25,12 @@ This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. .. note:: Credential delegation is not supported in server mode. +.. note:: + `RFC 4462 Section 2.2 `_ says we are + not required to implement GSS-API error messages. Thus, in many methods + within this module, if an error occurs an exception will be thrown and the + connection will be terminated. + .. seealso:: :doc:`/api/ssh_gss` """ @@ -49,12 +55,6 @@ class KexGSSGroup1(object): """ GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange as defined in `RFC 4462 Section 2 `_ - - :note: RFC 4462 says we are not required to implement GSS-API error - messages. - If an error occurs an exception will be thrown and the connection - will be terminated. - :see: `RFC 4462 Section 2.2 `_ """ # draft-ietf-secsh-transport-09.txt, page 17 P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF @@ -278,12 +278,6 @@ class KexGSSGroup14(KexGSSGroup1): """ GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange as defined in `RFC 4462 Section 2 `_ - - :note: RFC 4462 says we are not required to implement GSS-API error - messages. - If an error occurs an exception will be thrown and the connection - will be terminated. - :see: `RFC 4462 Section 2.2 `_ """ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF G = 2 @@ -294,12 +288,6 @@ class KexGSSGex(object): """ GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange as defined in `RFC 4462 Section 2 `_ - - :note: RFC 4462 says we are not required to implement GSS-API error - messages. - If an error occurs an exception will be thrown and the connection - will be terminated. - :see: `RFC 4462 Section 2.2 `_ """ NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" min_bits = 1024 -- cgit v1.2.3 From 27c1b4978890b403956756c1ca11e770a6be71da Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:31:43 -0700 Subject: Versionadded for the new modules --- paramiko/kex_gss.py | 2 ++ paramiko/ssh_gss.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index ec7285c2..a319b33b 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -32,6 +32,8 @@ This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. connection will be terminated. .. seealso:: :doc:`/api/ssh_gss` + +.. versionadded:: 1.15 """ diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index ec21a79a..d4be9bee 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -25,6 +25,8 @@ This module provides GSS-API / SSPI authentication as defined in RFC 4462. .. note:: Credential delegation is not supported in server mode. .. seealso:: :doc:`/api/kex_gss` + +.. versionadded:: 1.15 """ import struct -- cgit v1.2.3 From e71f4e59878a636268475f642ed4e98a1b3e375d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 8 Sep 2014 17:31:50 -0700 Subject: Consistency pls --- paramiko/ssh_gss.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index d4be9bee..ebf2cc80 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -33,10 +33,10 @@ import struct import os import sys -''' -:var bool GSS_AUTH_AVAILABLE: Constraint that indicates if GSS-API / SSPI is - Available. -''' +""" +:var bool GSS_AUTH_AVAILABLE: + Constraint that indicates if GSS-API / SSPI is available. +""" GSS_AUTH_AVAILABLE = True try: -- cgit v1.2.3 From 47f8639ef25c9ff79effbe566686339a9c3779d5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 10 Sep 2014 14:41:48 -0700 Subject: Force PTY in test runner so Python 3's buffering behavior isn't frustrating --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 94bf0aa2..cf43a5fd 100644 --- a/tasks.py +++ b/tasks.py @@ -28,7 +28,7 @@ www = Collection.from_module(_docs, name='www', config={ # Until we move to spec-based testing @task def test(ctx): - ctx.run("python test.py --verbose") + ctx.run("python test.py --verbose", pty=True) @task def coverage(ctx): -- cgit v1.2.3 From 4e66cf36c4c97ce539970553e721d9a81727fbb5 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 15 Sep 2014 12:47:29 -0700 Subject: Use hashlib in favor of PyCrypto's hashes --- paramiko/kex_gss.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index a319b33b..4e8380ef 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -36,8 +36,8 @@ This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. .. versionadded:: 1.15 """ +from hashlib import sha1 -from Crypto.Hash import SHA from paramiko.common import * from paramiko import util from paramiko.message import Message @@ -196,7 +196,7 @@ class KexGSSGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - self.transport._set_K_H(K, SHA.new(str(hm)).digest()) + self.transport._set_K_H(K, sha1(str(hm)).digest()) if srv_token is not None: self.kexgss.ssh_init_sec_context(target=self.gss_host, recv_token=srv_token) @@ -229,7 +229,7 @@ class KexGSSGroup1(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host, client_token) @@ -463,7 +463,7 @@ class KexGSSGex(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host, client_token) @@ -555,7 +555,7 @@ class KexGSSGex(object): hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(hm.asbytes()).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) if srv_token is not None: self.kexgss.ssh_init_sec_context(target=self.gss_host, -- cgit v1.2.3 From 4561623ff3aacb7e2f5fc5cc602c07f9709c86bf Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 17 Sep 2014 17:26:12 -0700 Subject: Don't tell pip to use mirrors anymore The mirrors don't add any value now that pypi has a really good CDN --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 83704dfc..7d4fcf8a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,5 +2,5 @@ envlist = py26,py27,py32,py33,py34 [testenv] -commands = pip install --use-mirrors -q -r tox-requirements.txt +commands = pip install -q -r tox-requirements.txt python test.py -- cgit v1.2.3 From fb3c81bf423b673526768fe4b506e3d74d295ac5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 13:32:24 -0700 Subject: Clean up & tweak failing sftp closure test --- tests/test_sftp.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 58013cfd..a35914e1 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -203,7 +203,10 @@ class SFTPTest (unittest.TestCase): with sftp: pass try: - self._assert_opening_file_raises_error(sftp) + sftp.open(FOLDER + '/test2', 'w') + self.fail('expected exception') + except EOFError, socket.error: + pass finally: sftp = paramiko.SFTP.from_transport(tc) @@ -808,14 +811,6 @@ class SFTPTest (unittest.TestCase): sftp.remove('%s/nonutf8data' % FOLDER) - def _assert_opening_file_raises_error(self, sftp): - try: - sftp.open(FOLDER + '/test2', 'w') - self.fail('expected exception') - except EOFError: - pass - - if __name__ == '__main__': SFTPTest.init_loopback() # logging is required by test_N_file_with_percent -- cgit v1.2.3 From a146374936f5eb8f4736af3f7297117cdf5d291b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 15:18:51 -0700 Subject: Add ability to run Kerberos tests to the test runner. Includes merging coverage task into test task --- .travis.yml | 3 ++- kerberos-requirements.txt | 2 ++ tasks.py | 16 +++++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 kerberos-requirements.txt diff --git a/.travis.yml b/.travis.yml index 3f6f7331..604b6fd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,10 @@ install: # Dev (doc/test running) requirements - pip install coveralls # For coveralls.io specifically - pip install -r dev-requirements.txt + - pip install -r kerberos-requirements.txt # for GSSAPI tests script: # Main tests, with coverage! - - invoke coverage + - inv test --coverage --kerberos # Ensure documentation & invoke pipeline run OK. # Run 'docs' first since its objects.inv is referred to by 'www'. # Also force warnings to be errors since most of them tend to be actual diff --git a/kerberos-requirements.txt b/kerberos-requirements.txt new file mode 100644 index 00000000..0e8c156a --- /dev/null +++ b/kerberos-requirements.txt @@ -0,0 +1,2 @@ +pyasn1>=0.1.7 +python-gssapi>=0.6.1 diff --git a/tasks.py b/tasks.py index cf43a5fd..68912ce0 100644 --- a/tasks.py +++ b/tasks.py @@ -27,12 +27,14 @@ www = Collection.from_module(_docs, name='www', config={ # Until we move to spec-based testing @task -def test(ctx): - ctx.run("python test.py --verbose", pty=True) - -@task -def coverage(ctx): - ctx.run("coverage run --source=paramiko test.py --verbose") +def test(ctx, kerberos=False, coverage=False): + runner = "python" + if coverage: + runner = "coverage run --source=paramiko" + flags = "--verbose" + if kerberos: + flags += " --gssapi-test --test-gssauth --test-gssapi-keyex" + ctx.run("{0} test.py {1}".format(runner, flags), pty=True) # Until we stop bundling docs w/ releases. Need to discover use cases first. @@ -48,4 +50,4 @@ def release(ctx): publish(ctx, wheel=True) -ns = Collection(test, coverage, release, docs=docs, www=www) +ns = Collection(test, release, docs=docs, www=www) -- cgit v1.2.3 From 8df6f80337208374304085dbc9408aa1bae5f1e9 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 15:19:01 -0700 Subject: Install docs tweak re: GSSAPI req --- sites/www/installing.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 5528b28a..486ed7e3 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -109,10 +109,12 @@ installation of Paramiko via ``pypm``:: Optional dependencies for GSS-API / SSPI / Kerberos =================================================== -In order to use Kerberos & related functionality, a couple of additional -dependencies are required (these are not listed in our ``setup.py`` due to -their infrequent utility & non-platform-agnostic requirements): +In order to use GSS-API/Kerberos & related functionality, a couple of +additional dependencies are required (these are not listed in our ``setup.py`` +due to their infrequent utility & non-platform-agnostic requirements): +* It hopefully goes without saying but **all platforms** need **a working + installation of GSS-API itself**, e.g. Heimdal. * **All platforms** need `pyasn1 `_ ``0.1.7`` or better. * **Unix** needs `python-gssapi `_ -- cgit v1.2.3 From 8bdd921d4c5c607b7c1f568df27f5460acd5a548 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 15:34:48 -0700 Subject: Note how python-gssapi only works on 2.7+ --- sites/www/changelog.rst | 6 ++++++ sites/www/installing.rst | 3 +++ 2 files changed, 9 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 1dab5219..a40338b0 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -6,6 +6,12 @@ Changelog SSPI (e.g. Kerberos) key exchange and authentication support (:ref:`installation docs here `). Mega thanks to Sebastian Deiß, with assist by Torsten Landschoff. + + .. note:: + Unix users should be aware that the ``python-gssapi`` library (a + requirement for using this functionality) only appears to support + Python 2.7 and up at this time. + * :bug:`346 major` Fix an issue in private key files' encryption salts that could cause tracebacks and file corruption if keys were re-encrypted. Credit to Xavier Nunn. diff --git a/sites/www/installing.rst b/sites/www/installing.rst index 486ed7e3..a657c3fc 100644 --- a/sites/www/installing.rst +++ b/sites/www/installing.rst @@ -119,6 +119,9 @@ due to their infrequent utility & non-platform-agnostic requirements): ``0.1.7`` or better. * **Unix** needs `python-gssapi `_ ``0.6.1`` or better. + + .. note:: This library appears to only function on Python 2.7 and up. + * **Windows** needs `pywin32 `_ ``2.1.8`` or better. -- cgit v1.2.3 From 7fb72ec3b4824399e2dce4b4faf59f519ad49013 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 16:03:38 -0700 Subject: Fix more dumb non-comment comments --- tests/test_gssapi.py | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py index 0d3df72c..a328dd65 100644 --- a/tests/test_gssapi.py +++ b/tests/test_gssapi.py @@ -72,9 +72,7 @@ class GSSAPITest(unittest.TestCase): gss_flags = (gssapi.C_PROT_READY_FLAG, gssapi.C_INTEG_FLAG, gssapi.C_DELEG_FLAG) - """ - Initialize a GSS-API context. - """ + # Initialize a GSS-API context. ctx = gssapi.Context() ctx.flags = gss_flags krb5_oid = gssapi.OID.mech_from_string(krb5_mech) @@ -87,41 +85,31 @@ class GSSAPITest(unittest.TestCase): c_token = gss_ctxt.step(c_token) gss_ctxt_status = gss_ctxt.established self.assertEquals(False, gss_ctxt_status) - """ - Accept a GSS-API context. - """ + # Accept a GSS-API context. gss_srv_ctxt = gssapi.AcceptContext() s_token = gss_srv_ctxt.step(c_token) gss_ctxt_status = gss_srv_ctxt.established self.assertNotEquals(None, s_token) self.assertEquals(True, gss_ctxt_status) - """ - Establish the client context - """ + # Establish the client context c_token = gss_ctxt.step(s_token) self.assertEquals(None, c_token) else: while not gss_ctxt.established: c_token = gss_ctxt.step(c_token) self.assertNotEquals(None, c_token) - """ - Build MIC - """ + # Build MIC mic_token = gss_ctxt.get_mic(mic_msg) if server_mode: - """ - Check MIC - """ + # Check MIC status = gss_srv_ctxt.verify_mic(mic_msg, mic_token) self.assertEquals(0, status) else: gss_flags = sspicon.ISC_REQ_INTEGRITY |\ sspicon.ISC_REQ_MUTUAL_AUTH |\ sspicon.ISC_REQ_DELEGATE - """ - Initialize a GSS-API context. - """ + # Initialize a GSS-API context. target_name = "host/" + socket.getfqdn(targ_name) gss_ctxt = sspi.ClientAuth("Kerberos", scflags=gss_flags, @@ -130,26 +118,18 @@ class GSSAPITest(unittest.TestCase): error, token = gss_ctxt.authorize(c_token) c_token = token[0].Buffer self.assertEquals(0, error) - """ - Accept a GSS-API context. - """ + # Accept a GSS-API context. gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=target_name) error, token = gss_srv_ctxt.authorize(c_token) s_token = token[0].Buffer - """ - Establish the context. - """ + # Establish the context. error, token = gss_ctxt.authorize(s_token) c_token = token[0].Buffer self.assertEquals(None, c_token) self.assertEquals(0, error) - """ - Build MIC - """ + # Build MIC mic_token = gss_ctxt.sign(mic_msg) - """ - Check MIC - """ + # Check MIC gss_srv_ctxt.verify(mic_msg, mic_token) else: error, token = gss_ctxt.authorize(c_token) -- cgit v1.2.3 From cbbf7106f8c6e2b0275fc65f17c8c419a2028d8a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 16:07:53 -0700 Subject: Revert attempts to run Kerberos tests :( --- .travis.yml | 3 +-- kerberos-requirements.txt | 2 -- tasks.py | 4 +--- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 kerberos-requirements.txt diff --git a/.travis.yml b/.travis.yml index 604b6fd2..a9a04c89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,9 @@ install: # Dev (doc/test running) requirements - pip install coveralls # For coveralls.io specifically - pip install -r dev-requirements.txt - - pip install -r kerberos-requirements.txt # for GSSAPI tests script: # Main tests, with coverage! - - inv test --coverage --kerberos + - inv test --coverage # Ensure documentation & invoke pipeline run OK. # Run 'docs' first since its objects.inv is referred to by 'www'. # Also force warnings to be errors since most of them tend to be actual diff --git a/kerberos-requirements.txt b/kerberos-requirements.txt deleted file mode 100644 index 0e8c156a..00000000 --- a/kerberos-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyasn1>=0.1.7 -python-gssapi>=0.6.1 diff --git a/tasks.py b/tasks.py index 68912ce0..b236ee42 100644 --- a/tasks.py +++ b/tasks.py @@ -27,13 +27,11 @@ www = Collection.from_module(_docs, name='www', config={ # Until we move to spec-based testing @task -def test(ctx, kerberos=False, coverage=False): +def test(ctx, coverage=False): runner = "python" if coverage: runner = "coverage run --source=paramiko" flags = "--verbose" - if kerberos: - flags += " --gssapi-test --test-gssauth --test-gssapi-keyex" ctx.run("{0} test.py {1}".format(runner, flags), pty=True) -- cgit v1.2.3 From 381e86171e28ebfaa64c3dabe0e394448eb03aa3 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 16:26:54 -0700 Subject: Changelog re #393 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index a40338b0..38a56101 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the + stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor. * :feature:`267` (also :issue:`250`, :issue:`241`, :issue:`228`) Add GSS-API / SSPI (e.g. Kerberos) key exchange and authentication support (:ref:`installation docs here `). Mega thanks to Sebastian Deiß, with -- cgit v1.2.3 From e27964254fbf5a55d5cc0c4db9268cc001827af1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 16:53:48 -0700 Subject: A herp and a derp (and an import shuffle) --- tests/test_sftp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index a35914e1..72c7ba03 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -23,12 +23,13 @@ a real actual sftp server is contacted, and a new folder is created there to do test file operations in (so no existing files will be harmed). """ -from binascii import hexlify import os +import socket import sys -import warnings import threading import unittest +import warnings +from binascii import hexlify from tempfile import mkstemp import paramiko @@ -205,7 +206,7 @@ class SFTPTest (unittest.TestCase): try: sftp.open(FOLDER + '/test2', 'w') self.fail('expected exception') - except EOFError, socket.error: + except (EOFError, socket.error): pass finally: sftp = paramiko.SFTP.from_transport(tc) -- cgit v1.2.3 From 35cb81b307ed44b5fe5f212a6f488f96364f954f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 16:56:37 -0700 Subject: Cut 1.15 --- sites/www/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 38a56101..d0bd481c 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.15.0 <2014-09-18>` * :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor. * :feature:`267` (also :issue:`250`, :issue:`241`, :issue:`228`) Add GSS-API / -- cgit v1.2.3 From 9cfb9d215a7903ec4eacde8704f7adf92b4afe6b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 17:50:35 -0700 Subject: Missed some old spots re: ECDSA docs still being epydoc --- paramiko/ecdsakey.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index e869ee61..a7f3c5ed 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -17,7 +17,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{ECDSAKey} +ECDSA keys """ import binascii @@ -131,13 +131,10 @@ class ECDSAKey (PKey): Generate a new private RSA key. This factory function can be used to generate a new host key or authentication key. - @param bits: number of bits the generated key should be. - @type bits: int - @param progress_func: an optional function to call at key points in - key generation (used by C{pyCrypto.PublicKey}). - @type progress_func: function - @return: new private key - @rtype: L{RSAKey} + :param function progress_func: + an optional function to call at key points in key generation (used + by ``pyCrypto.PublicKey``). + :returns: A new private key (`.RSAKey`) object """ signing_key = SigningKey.generate(curve) key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) -- cgit v1.2.3 From 8e2d4321509cf942c3d499b643b1978b1addaebf Mon Sep 17 00:00:00 2001 From: Søren Løvborg Date: Fri, 19 Sep 2014 15:00:18 +0200 Subject: SSHConfig.get_hostnames: List literal hostnames from SSH config --- paramiko/config.py | 10 ++++++++++ tests/test_util.py | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/paramiko/config.py b/paramiko/config.py index 20ca4aa7..4972c27b 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -126,6 +126,16 @@ class SSHConfig (object): ret = self._expand_variables(ret, hostname) return ret + def get_hostnames(self): + """ + Return the set of literal hostnames defined in the SSH config (both + explicit hostnames and wildcard entries). + """ + hosts = set() + for entry in self._config: + hosts.update(entry['host']) + return hosts + def _allowed(self, hosts, hostname): match = False for host in hosts: diff --git a/tests/test_util.py b/tests/test_util.py index 35e15765..f961fbbc 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -349,6 +349,11 @@ IdentityFile something_%l_using_fqdn config.parse(config_file) self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1") + def test_14_get_hostnames(self): + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEqual(config.get_hostnames(), set(['*', '*.example.com', 'spoo.example.com'])) + def test_quoted_host_names(self): test_config_file = """\ Host "param pam" param "pam" -- cgit v1.2.3 From 84995f99a9528b84bd1666060c832ca673641a53 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 19 Sep 2014 12:26:48 -0700 Subject: Changelog re #167 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index d0bd481c..49067855 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :feature:`167` Add `.SSHConfig.get_hostnames` for easier introspection of a + loaded SSH config file or object. Courtesy of Søren Løvborg. * :release:`1.15.0 <2014-09-18>` * :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor. -- cgit v1.2.3 From be220e3ee52ab842c84461011c36ffdea2cb01fb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 22 Sep 2014 09:48:30 -0700 Subject: 80-col tweaks --- paramiko/transport.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 2ffc8ca8..bf30c784 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -960,10 +960,14 @@ class Transport (threading.Thread, ClosingContextManager): :param .PKey pkey: a private key to use for authentication, if you want to use private key authentication; otherwise ``None``. - :param str gss_host: The targets name in the kerberos database. default: hostname - :param bool gss_auth: ``True`` if you want to use GSS-API authentication - :param bool gss_kex: Perform GSS-API Key Exchange and user authentication - :param bool gss_deleg_creds: Delegate GSS-API client credentials or not + :param str gss_host: + The target's name in the kerberos database. Default: hostname + :param bool gss_auth: + ``True`` if you want to use GSS-API authentication. + :param bool gss_kex: + Perform GSS-API Key Exchange and user authentication. + :param bool gss_deleg_creds: + Whether to delegate GSS-API client credentials. :raises SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. -- cgit v1.2.3 From 8bc2e827cffa7efce074404f71ad62ac028c5c84 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 22 Sep 2014 09:53:33 -0700 Subject: Changelog re #399 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index d0bd481c..43988826 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`399` SSH agent forwarding (potentially other functionality as + well) would hang due to incorrect values passed into the new window size + arguments for `.Transport` (thanks to a botched merge). This has been + corrected. Thanks to Dylan Thacker-Smith for the report & patch. * :release:`1.15.0 <2014-09-18>` * :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor. -- cgit v1.2.3 From 641a8cb62d43ce7ed1e07ddb61bca2744547f73a Mon Sep 17 00:00:00 2001 From: Dylan Thacker-Smith Date: Sun, 21 Sep 2014 23:13:42 -0400 Subject: Use keyword arguments for arguments when creating a Transport. Fixes #399 --- paramiko/client.py | 2 +- tests/test_kex_gss.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 05686d97..393e3e09 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -250,7 +250,7 @@ class SSHClient (ClosingContextManager): pass retry_on_signal(lambda: sock.connect(addr)) - t = self._transport = Transport(sock, gss_kex, gss_deleg_creds) + t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds) t.use_compression(compress=compress) if gss_kex and gss_host is None: t.set_gss_host(hostname) diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index b5e277b3..8769d09c 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -84,7 +84,7 @@ class GSSKexTest(unittest.TestCase): def _run(self): self.socks, addr = self.sockl.accept() - self.ts = paramiko.Transport(self.socks, True) + self.ts = paramiko.Transport(self.socks, gss_kex=True) host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') self.ts.add_server_key(host_key) self.ts.set_gss_host(targ_name) -- cgit v1.2.3 From af9f16f9a03bede1c5af84d00bc73097f6b45b54 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 22 Sep 2014 11:31:58 -0700 Subject: Cut 1.15.1 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index a7857b09..d9f78740 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 0) +__version_info__ = (1, 15, 1) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 43988826..3e654f69 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.15.1 <2014-09-22>` * :bug:`399` SSH agent forwarding (potentially other functionality as well) would hang due to incorrect values passed into the new window size arguments for `.Transport` (thanks to a botched merge). This has been -- cgit v1.2.3 From 5fbd4e3b6fcdc9edc04f84134d0ae76f349854a7 Mon Sep 17 00:00:00 2001 From: Jacob Beck Date: Tue, 14 Oct 2014 21:37:45 -0600 Subject: Converted all staticmethod/classmethod instances to decorators. --- paramiko/ber.py | 6 +++--- paramiko/dsskey.py | 2 +- paramiko/ecdsakey.py | 2 +- paramiko/hostkeys.py | 4 ++-- paramiko/pkey.py | 4 ++-- paramiko/rsakey.py | 2 +- paramiko/sftp_attr.py | 7 +++---- paramiko/sftp_client.py | 4 ++-- paramiko/sftp_server.py | 4 ++-- paramiko/transport.py | 4 ++-- paramiko/util.py | 2 +- tests/test_gssapi.py | 4 +--- tests/test_kex_gss.py | 4 +--- tests/test_sftp.py | 7 +++---- tests/test_ssh_gss.py | 4 +--- 15 files changed, 26 insertions(+), 34 deletions(-) diff --git a/paramiko/ber.py b/paramiko/ber.py index 05152303..a388df07 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -45,7 +45,7 @@ class BER(object): def decode(self): return self.decode_next() - + def decode_next(self): if self.idx >= len(self.content): return None @@ -89,6 +89,7 @@ class BER(object): # 1: boolean (00 false, otherwise true) raise BERException('Unknown ber encoding type %d (robey is lazy)' % ident) + @staticmethod def decode_sequence(data): out = [] ber = BER(data) @@ -98,7 +99,6 @@ class BER(object): break out.append(x) return out - decode_sequence = staticmethod(decode_sequence) def encode_tlv(self, ident, val): # no need to support ident > 31 here @@ -125,9 +125,9 @@ class BER(object): else: raise BERException('Unknown type for encoding: %s' % repr(type(x))) + @staticmethod def encode_sequence(data): ber = BER() for item in data: ber.encode(item) return ber.asbytes() - encode_sequence = staticmethod(encode_sequence) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 1901596d..d7dd6275 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -154,6 +154,7 @@ class DSSKey (PKey): def write_private_key(self, file_obj, password=None): self._write_private_key('DSA', file_obj, self._encode_key(), password) + @staticmethod def generate(bits=1024, progress_func=None): """ Generate a new private DSS key. This factory function can be used to @@ -169,7 +170,6 @@ class DSSKey (PKey): key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x return key - generate = staticmethod(generate) ### internals... diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index a7f3c5ed..6b047959 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -126,6 +126,7 @@ class ECDSAKey (PKey): key = self.signing_key or self.verifying_key self._write_private_key('EC', file_obj, key.to_der(), password) + @staticmethod def generate(curve=curves.NIST256p, progress_func=None): """ Generate a new private RSA key. This factory function can be used to @@ -139,7 +140,6 @@ class ECDSAKey (PKey): signing_key = SigningKey.generate(curve) key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) return key - generate = staticmethod(generate) ### internals... diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index b94ff0db..84868875 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -255,6 +255,7 @@ class HostKeys (MutableMapping): ret.append(self.lookup(k)) return ret + @staticmethod def hash_host(hostname, salt=None): """ Return a "hashed" form of the hostname, as used by OpenSSH when storing @@ -274,7 +275,6 @@ class HostKeys (MutableMapping): hmac = HMAC(salt, b(hostname), sha1).digest() hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) return hostkey.replace('\n', '') - hash_host = staticmethod(hash_host) class InvalidHostKey(Exception): @@ -294,6 +294,7 @@ class HostKeyEntry: self.hostnames = hostnames self.key = key + @classmethod def from_line(cls, line, lineno=None): """ Parses the given line of text to find the names for the host, @@ -336,7 +337,6 @@ class HostKeyEntry: raise InvalidHostKey(line, e) return cls(names, key) - from_line = classmethod(from_line) def to_line(self): """ diff --git a/paramiko/pkey.py b/paramiko/pkey.py index 2daf3723..1b4af010 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -160,6 +160,7 @@ class PKey (object): """ return False + @classmethod def from_private_key_file(cls, filename, password=None): """ Create a key object by reading a private key file. If the private @@ -181,8 +182,8 @@ class PKey (object): """ key = cls(filename=filename, password=password) return key - from_private_key_file = classmethod(from_private_key_file) + @classmethod def from_private_key(cls, file_obj, password=None): """ Create a key object by reading a private key from a file (or file-like) @@ -202,7 +203,6 @@ class PKey (object): """ key = cls(file_obj=file_obj, password=password) return key - from_private_key = classmethod(from_private_key) def write_private_key_file(self, filename, password=None): """ diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py index d1f3ecfe..4ebd8354 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -131,6 +131,7 @@ class RSAKey (PKey): def write_private_key(self, file_obj, password=None): self._write_private_key('RSA', file_obj, self._encode_key(), password) + @staticmethod def generate(bits, progress_func=None): """ Generate a new private RSA key. This factory function can be used to @@ -148,7 +149,6 @@ class RSAKey (PKey): key.p = rsa.p key.q = rsa.q return key - generate = staticmethod(generate) ### internals... diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index d12eff8d..cf48f654 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -60,6 +60,7 @@ class SFTPAttributes (object): self.st_mtime = None self.attr = {} + @classmethod def from_stat(cls, obj, filename=None): """ Create an `.SFTPAttributes` object from an existing ``stat`` object (an @@ -79,13 +80,12 @@ class SFTPAttributes (object): if filename is not None: attr.filename = filename return attr - from_stat = classmethod(from_stat) def __repr__(self): return '' % self._debug_str() ### internals... - + @classmethod def _from_msg(cls, msg, filename=None, longname=None): attr = cls() attr._unpack(msg) @@ -94,7 +94,6 @@ class SFTPAttributes (object): if longname is not None: attr.longname = longname return attr - _from_msg = classmethod(_from_msg) def _unpack(self, msg): self._flags = msg.get_int() @@ -159,6 +158,7 @@ class SFTPAttributes (object): out += ']' return out + @staticmethod def _rwx(n, suid, sticky=False): if suid: suid = 2 @@ -168,7 +168,6 @@ class SFTPAttributes (object): else: out += '-xSs'[suid + (n & 1)] return out - _rwx = staticmethod(_rwx) def __str__(self): """create a unix-style long description of the file (like ls -l)""" diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 62127cc2..89840eaa 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -65,7 +65,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): Used to open an SFTP session across an open SSH `.Transport` and perform remote file operations. - + Instances of this class may be used as context managers. """ def __init__(self, sock): @@ -101,6 +101,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): raise SSHException('EOF during negotiation') self._log(INFO, 'Opened sftp connection (server version %d)' % server_version) + @classmethod def from_transport(cls, t, window_size=None, max_packet_size=None): """ Create an SFTP client channel from an open `.Transport`. @@ -129,7 +130,6 @@ class SFTPClient(BaseSFTP, ClosingContextManager): return None chan.invoke_subsystem('sftp') return cls(chan) - from_transport = classmethod(from_transport) def _log(self, level, msg, *args): if isinstance(msg, list): diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index 2d8d1909..ce287e8f 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -129,6 +129,7 @@ class SFTPServer (BaseSFTP, SubsystemHandler): self.file_table = {} self.folder_table = {} + @staticmethod def convert_errno(e): """ Convert an errno value (as from an ``OSError`` or ``IOError``) into a @@ -146,8 +147,8 @@ class SFTPServer (BaseSFTP, SubsystemHandler): return SFTP_NO_SUCH_FILE else: return SFTP_FAILURE - convert_errno = staticmethod(convert_errno) + @staticmethod def set_file_attr(filename, attr): """ Change a file's attributes on the local filesystem. The contents of @@ -173,7 +174,6 @@ class SFTPServer (BaseSFTP, SubsystemHandler): if attr._flags & attr.FLAG_SIZE: with open(filename, 'w+') as f: f.truncate(attr.st_size) - set_file_attr = staticmethod(set_file_attr) ### internals... diff --git a/paramiko/transport.py b/paramiko/transport.py index bf30c784..6ce9225d 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -88,7 +88,7 @@ class Transport (threading.Thread, ClosingContextManager): `channels <.Channel>`, across the session. Multiple channels can be multiplexed across a single session (and often are, in the case of port forwardings). - + Instances of this class may be used as context managers. """ _PROTO_ID = '2.0' @@ -508,6 +508,7 @@ class Transport (threading.Thread, ClosingContextManager): pass return None + @staticmethod def load_server_moduli(filename=None): """ (optional) @@ -547,7 +548,6 @@ class Transport (threading.Thread, ClosingContextManager): # none succeeded Transport._modulus_pack = None return False - load_server_moduli = staticmethod(load_server_moduli) def close(self): """ diff --git a/paramiko/util.py b/paramiko/util.py index 88ca2bc4..2b3389a8 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -307,9 +307,9 @@ class Counter (object): self.value = array.array('c', zero_byte * (self.blocksize - len(x)) + x) return self.value.tostring() + @classmethod def new(cls, nbits, initial_value=long(1), overflow=long(0)): return cls(nbits, initial_value=initial_value, overflow=overflow) - new = classmethod(new) def constant_time_bytes_eq(a, b): diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py index a328dd65..96c268d9 100644 --- a/tests/test_gssapi.py +++ b/tests/test_gssapi.py @@ -27,15 +27,13 @@ import socket class GSSAPITest(unittest.TestCase): - + @staticmethod def init(hostname=None, srv_mode=False): global krb5_mech, targ_name, server_mode krb5_mech = "1.2.840.113554.1.2.2" targ_name = hostname server_mode = srv_mode - init = staticmethod(init) - def test_1_pyasn1(self): """ Test the used methods of pyasn1. diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index 8769d09c..91d4c9b2 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -58,14 +58,12 @@ class NullServer (paramiko.ServerInterface): class GSSKexTest(unittest.TestCase): - + @staticmethod def init(username, hostname): global krb5_principal, targ_name krb5_principal = username targ_name = hostname - init = staticmethod(init) - def setUp(self): self.username = krb5_principal self.hostname = socket.getfqdn(targ_name) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 72c7ba03..cb8f7f84 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -97,7 +97,7 @@ def get_sftp(): class SFTPTest (unittest.TestCase): - + @staticmethod def init(hostname, username, keyfile, passwd): global sftp, tc @@ -129,8 +129,8 @@ class SFTPTest (unittest.TestCase): sys.stderr.write('\n') sys.exit(1) sftp = paramiko.SFTP.from_transport(t) - init = staticmethod(init) + @staticmethod def init_loopback(): global sftp, tc @@ -150,12 +150,11 @@ class SFTPTest (unittest.TestCase): event.wait(1.0) sftp = paramiko.SFTP.from_transport(tc) - init_loopback = staticmethod(init_loopback) + @staticmethod def set_big_file_test(onoff): global g_big_file_test g_big_file_test = onoff - set_big_file_test = staticmethod(set_big_file_test) def setUp(self): global FOLDER diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py index 595081b8..99ccdc9c 100644 --- a/tests/test_ssh_gss.py +++ b/tests/test_ssh_gss.py @@ -57,14 +57,12 @@ class NullServer (paramiko.ServerInterface): class GSSAuthTest(unittest.TestCase): - + @staticmethod def init(username, hostname): global krb5_principal, targ_name krb5_principal = username targ_name = hostname - init = staticmethod(init) - def setUp(self): self.username = krb5_principal self.hostname = socket.getfqdn(targ_name) -- cgit v1.2.3 From c5836ad5524e52df87801ddd4f3a5187afae785f Mon Sep 17 00:00:00 2001 From: Jacob Beck Date: Tue, 14 Oct 2014 22:00:50 -0600 Subject: Property decorators --- paramiko/_winapi.py | 8 ++++--- paramiko/transport.py | 66 ++++++++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index 0d55d291..f48e1890 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -213,12 +213,14 @@ class SECURITY_ATTRIBUTES(ctypes.Structure): super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) - def _get_descriptor(self): + @property + def descriptor(self): return self._descriptor - def _set_descriptor(self, descriptor): + + @descriptor.setter + def descriptor(self, value): self._descriptor = descriptor self.lpSecurityDescriptor = ctypes.addressof(descriptor) - descriptor = property(_get_descriptor, _set_descriptor) def GetTokenInformation(token, information_class): """ diff --git a/paramiko/transport.py b/paramiko/transport.py index 6ce9225d..3b26f0b8 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -2207,21 +2207,6 @@ class SecurityOptions (object): """ return '' % repr(self._transport) - def _get_ciphers(self): - return self._transport._preferred_ciphers - - def _get_digests(self): - return self._transport._preferred_macs - - def _get_key_types(self): - return self._transport._preferred_keys - - def _get_kex(self): - return self._transport._preferred_kex - - def _get_compression(self): - return self._transport._preferred_compression - def _set(self, name, orig, x): if type(x) is list: x = tuple(x) @@ -2233,30 +2218,51 @@ class SecurityOptions (object): raise ValueError('unknown cipher') setattr(self._transport, name, x) - def _set_ciphers(self, x): + @property + def ciphers(self): + """Symmetric encryption ciphers""" + return self._transport._preferred_ciphers + + @ciphers.setter + def ciphers(self, x): self._set('_preferred_ciphers', '_cipher_info', x) - def _set_digests(self, x): + @property + def digests(self): + """Digest (one-way hash) algorithms""" + return self._transport._preferred_macs + + @digests.setter + def digests(self, x): self._set('_preferred_macs', '_mac_info', x) - def _set_key_types(self, x): + @property + def key_types(self): + """Public-key algorithms""" + return self._transport._preferred_keys + + @key_types.setter + def key_types(self, x): self._set('_preferred_keys', '_key_info', x) - def _set_kex(self, x): + + @property + def kex(self): + """Key exchange algorithms""" + return self._transport._preferred_kex + + @kex.setter + def kex(self, x): self._set('_preferred_kex', '_kex_info', x) - def _set_compression(self, x): - self._set('_preferred_compression', '_compression_info', x) + @property + def compression(self): + """Compression algorithms""" + return self._transport._preferred_compression - 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") - compression = property(_get_compression, _set_compression, None, - "Compression algorithms") + @compression.setter + def compression(self, x): + self._set('_preferred_compression', '_compression_info', x) class ChannelMap (object): -- cgit v1.2.3 From 14b517d3c131fd508e287fee1e09c632b6faa615 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 17 Dec 2014 14:45:02 -0800 Subject: Changelog re #419, closes #419 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 79cf318b..e8f103a9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :support:`419` Modernize a bunch of the codebase internals to leverage + decorators. Props to ``@beckjake`` for realizing we're no longer on Python + 2.2 :D * :bug:`266` Change numbering of `~paramiko.transport.Transport` channels to start at 0 instead of 1 for better compatibility with OpenSSH & certain server implementations which break on 1-indexed channels. Thanks to -- cgit v1.2.3 From 05030b2d5e63349c6abacd1e3a65c70faadbb35f Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Thu, 16 Oct 2014 17:20:20 +0200 Subject: Use modern api to check if event is set. Since we are a python2.6+ code base now, we want to be as forward compatible as possible. --- demos/demo_server.py | 2 +- paramiko/auth_handler.py | 2 +- paramiko/channel.py | 6 +++--- paramiko/transport.py | 12 ++++++------ tests/test_auth.py | 4 ++-- tests/test_client.py | 8 ++++---- tests/test_kex_gss.py | 2 +- tests/test_ssh_gss.py | 2 +- tests/test_transport.py | 22 +++++++++++----------- 9 files changed, 30 insertions(+), 30 deletions(-) diff --git a/demos/demo_server.py b/demos/demo_server.py index 5b3d5164..c4af9b10 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -159,7 +159,7 @@ try: print('Authenticated!') server.event.wait(10) - if not server.event.isSet(): + if not server.event.is_set(): print('*** Client never asked for a shell.') sys.exit(1) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index b5fea654..c001aeee 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -195,7 +195,7 @@ class AuthHandler (object): if (e is None) or issubclass(e.__class__, EOFError): e = AuthenticationException('Authentication failed.') raise e - if event.isSet(): + if event.is_set(): break if not self.is_authenticated(): e = self.transport.get_exception() diff --git a/paramiko/channel.py b/paramiko/channel.py index 9de278cb..f1b0483d 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -290,7 +290,7 @@ class Channel (ClosingContextManager): .. versionadded:: 1.7.3 """ - return self.closed or self.status_event.isSet() + return self.closed or self.status_event.is_set() def recv_exit_status(self): """ @@ -305,7 +305,7 @@ class Channel (ClosingContextManager): .. versionadded:: 1.2 """ self.status_event.wait() - assert self.status_event.isSet() + assert self.status_event.is_set() return self.exit_status def send_exit_status(self, status): @@ -1077,7 +1077,7 @@ class Channel (ClosingContextManager): def _wait_for_event(self): self.event.wait() - assert self.event.isSet() + assert self.event.is_set() if self.event_ready: return e = self.transport.get_exception() diff --git a/paramiko/transport.py b/paramiko/transport.py index cf2c49f5..2a700a88 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -405,7 +405,7 @@ class Transport (threading.Thread, ClosingContextManager): if e is not None: raise e raise SSHException('Negotiation failed.') - if event.isSet(): + if event.is_set(): break def start_server(self, event=None, server=None): @@ -470,7 +470,7 @@ class Transport (threading.Thread, ClosingContextManager): if e is not None: raise e raise SSHException('Negotiation failed.') - if event.isSet(): + if event.is_set(): break def add_server_key(self, key): @@ -729,7 +729,7 @@ class Transport (threading.Thread, ClosingContextManager): if e is None: e = SSHException('Unable to open channel.') raise e - if event.isSet(): + if event.is_set(): break chan = self._channels.get(chanid) if chan is not None: @@ -849,7 +849,7 @@ class Transport (threading.Thread, ClosingContextManager): if e is not None: raise e raise SSHException('Negotiation failed.') - if self.completion_event.isSet(): + if self.completion_event.is_set(): break return @@ -900,7 +900,7 @@ class Transport (threading.Thread, ClosingContextManager): self.completion_event.wait(0.1) if not self.active: return None - if self.completion_event.isSet(): + if self.completion_event.is_set(): break return self.global_response @@ -1461,7 +1461,7 @@ class Transport (threading.Thread, ClosingContextManager): self._log(DEBUG, 'Dropping user packet because connection is dead.') return self.clear_to_send_lock.acquire() - if self.clear_to_send.isSet(): + if self.clear_to_send.is_set(): break self.clear_to_send_lock.release() if time.time() > start + self.clear_to_send_timeout: diff --git a/tests/test_auth.py b/tests/test_auth.py index 1d972d53..ec78e3ce 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -118,12 +118,12 @@ class AuthTest (unittest.TestCase): self.ts.add_server_key(host_key) self.event = threading.Event() self.server = NullServer() - self.assertTrue(not self.event.isSet()) + self.assertTrue(not self.event.is_set()) self.ts.start_server(self.event, self.server) def verify_finished(self): self.event.wait(1.0) - self.assertTrue(self.event.isSet()) + self.assertTrue(self.event.is_set()) self.assertTrue(self.ts.is_active()) def test_1_bad_auth_type(self): diff --git a/tests/test_client.py b/tests/test_client.py index 28d1cb46..3d2e75c9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -128,7 +128,7 @@ class SSHClientTest (unittest.TestCase): # Authentication successful? self.event.wait(1.0) - self.assertTrue(self.event.isSet()) + self.assertTrue(self.event.is_set()) self.assertTrue(self.ts.is_active()) self.assertEqual('slowdive', self.ts.get_username()) self.assertEqual(True, self.ts.is_authenticated()) @@ -226,7 +226,7 @@ class SSHClientTest (unittest.TestCase): self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.event.wait(1.0) - self.assertTrue(self.event.isSet()) + self.assertTrue(self.event.is_set()) self.assertTrue(self.ts.is_active()) self.assertEqual('slowdive', self.ts.get_username()) self.assertEqual(True, self.ts.is_authenticated()) @@ -281,7 +281,7 @@ class SSHClientTest (unittest.TestCase): self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.event.wait(1.0) - self.assertTrue(self.event.isSet()) + self.assertTrue(self.event.is_set()) self.assertTrue(self.ts.is_active()) p = weakref.ref(self.tc._transport.packetizer) @@ -316,7 +316,7 @@ class SSHClientTest (unittest.TestCase): self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion') self.event.wait(1.0) - self.assertTrue(self.event.isSet()) + self.assertTrue(self.event.is_set()) self.assertTrue(self.ts.is_active()) self.assertTrue(self.tc._transport is not None) diff --git a/tests/test_kex_gss.py b/tests/test_kex_gss.py index 91d4c9b2..3bf788da 100644 --- a/tests/test_kex_gss.py +++ b/tests/test_kex_gss.py @@ -109,7 +109,7 @@ class GSSKexTest(unittest.TestCase): gss_auth=True, gss_kex=True) self.event.wait(1.0) - self.assert_(self.event.isSet()) + self.assert_(self.event.is_set()) self.assert_(self.ts.is_active()) self.assertEquals(self.username, self.ts.get_username()) self.assertEquals(True, self.ts.is_authenticated()) diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py index 99ccdc9c..e20d348f 100644 --- a/tests/test_ssh_gss.py +++ b/tests/test_ssh_gss.py @@ -102,7 +102,7 @@ class GSSAuthTest(unittest.TestCase): gss_auth=True) self.event.wait(1.0) - self.assert_(self.event.isSet()) + self.assert_(self.event.is_set()) self.assert_(self.ts.is_active()) self.assertEquals(self.username, self.ts.get_username()) self.assertEquals(True, self.ts.is_authenticated()) diff --git a/tests/test_transport.py b/tests/test_transport.py index 50b1d86b..dd522c4e 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -136,12 +136,12 @@ class TransportTest(unittest.TestCase): event = threading.Event() self.server = NullServer() - self.assertTrue(not event.isSet()) + self.assertTrue(not event.is_set()) self.ts.start_server(event, self.server) self.tc.connect(hostkey=public_host_key, username='slowdive', password='pygmalion') event.wait(1.0) - self.assertTrue(event.isSet()) + self.assertTrue(event.is_set()) self.assertTrue(self.ts.is_active()) def test_1_security_options(self): @@ -180,7 +180,7 @@ class TransportTest(unittest.TestCase): self.ts.add_server_key(host_key) event = threading.Event() server = NullServer() - self.assertTrue(not event.isSet()) + self.assertTrue(not event.is_set()) self.assertEqual(None, self.tc.get_username()) self.assertEqual(None, self.ts.get_username()) self.assertEqual(False, self.tc.is_authenticated()) @@ -189,7 +189,7 @@ class TransportTest(unittest.TestCase): self.tc.connect(hostkey=public_host_key, username='slowdive', password='pygmalion') event.wait(1.0) - self.assertTrue(event.isSet()) + self.assertTrue(event.is_set()) self.assertTrue(self.ts.is_active()) self.assertEqual('slowdive', self.tc.get_username()) self.assertEqual('slowdive', self.ts.get_username()) @@ -205,13 +205,13 @@ class TransportTest(unittest.TestCase): self.ts.add_server_key(host_key) event = threading.Event() server = NullServer() - self.assertTrue(not event.isSet()) + self.assertTrue(not event.is_set()) self.socks.send(LONG_BANNER) self.ts.start_server(event, server) self.tc.connect(hostkey=public_host_key, username='slowdive', password='pygmalion') event.wait(1.0) - self.assertTrue(event.isSet()) + self.assertTrue(event.is_set()) self.assertTrue(self.ts.is_active()) def test_4_special(self): @@ -680,7 +680,7 @@ class TransportTest(unittest.TestCase): def run(self): try: for i in range(1, 1+self.iterations): - if self.done_event.isSet(): + if self.done_event.is_set(): break self.watchdog_event.set() #print i, "SEND" @@ -699,7 +699,7 @@ class TransportTest(unittest.TestCase): def run(self): try: - while not self.done_event.isSet(): + while not self.done_event.is_set(): if self.chan.recv_ready(): chan.recv(65536) self.watchdog_event.set() @@ -753,12 +753,12 @@ class TransportTest(unittest.TestCase): # Act as a watchdog timer, checking deadlocked = False - while not deadlocked and not done_event.isSet(): + while not deadlocked and not done_event.is_set(): for event in (st.watchdog_event, rt.watchdog_event): event.wait(timeout) - if done_event.isSet(): + if done_event.is_set(): break - if not event.isSet(): + if not event.is_set(): deadlocked = True break event.clear() -- cgit v1.2.3 From e07dbc9cd7dcf6ebaa9315ad9d4a44eb5ed20e5b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 17 Dec 2014 14:59:43 -0800 Subject: Changelog re #421, closes #421 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index e8f103a9..de432870 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`421` Modernize threading calls to user newer API. Thanks to Olle + Lundberg. * :support:`419` Modernize a bunch of the codebase internals to leverage decorators. Props to ``@beckjake`` for realizing we're no longer on Python 2.2 :D -- cgit v1.2.3 From dc1e3d0f787fd042d5eeb1f7ac0dd60c0aabbd1f Mon Sep 17 00:00:00 2001 From: Yan Kalchevskiy Date: Tue, 11 Nov 2014 00:12:08 +0300 Subject: Improve parsing method of hosts in ssh_config. There is a module in standard library for such parsing -- shlex. --- paramiko/config.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 85fdddd3..91943ffb 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -24,6 +24,7 @@ Configuration file (aka ``ssh_config``) support. import fnmatch import os import re +import shlex import socket SSH_PORT = 22 @@ -54,7 +55,6 @@ class SSHConfig (object): :param file file_obj: a file-like object to read the config file from """ - host = {"host": ['*'], "config": {}} for line in file_obj: line = line.rstrip('\r\n').lstrip() @@ -222,25 +222,10 @@ class SSHConfig (object): """ Return a list of host_names from host value. """ - i, length = 0, len(host) - hosts = [] - while i < length: - if host[i] == '"': - end = host.find('"', i + 1) - if end < 0: - raise Exception("Unparsable host %s" % host) - hosts.append(host[i + 1:end]) - i = end + 1 - elif not host[i].isspace(): - end = i + 1 - while end < length and not host[end].isspace() and host[end] != '"': - end += 1 - hosts.append(host[i:end]) - i = end - else: - i += 1 - - return hosts + try: + return shlex.split(host) + except ValueError: + raise Exception("Unparsable host %s" % host) class LazyFqdn(object): -- cgit v1.2.3 From c0520adbe5905af2befc85064b25f3ba0a39b019 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 17 Dec 2014 15:10:12 -0800 Subject: Changelog closes #413 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 1c312ba2..68f5e910 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`413` Replace handrolled ``ssh_config`` parsing code with use of the + ``shlex`` module. Thanks to Yan Kalchevskiy. * :support:`422` Clean up some unused imports. Courtesy of Olle Lundberg. * :support:`421` Modernize threading calls to user newer API. Thanks to Olle Lundberg. -- cgit v1.2.3 From e5b105ca57b21b3142a80f29ee07e2a5e87ac547 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 17 Dec 2014 15:13:31 -0800 Subject: Dyslexia strikes again. Actually close #431, not #413 --- sites/www/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 68f5e910..d35ad788 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,7 +2,7 @@ Changelog ========= -* :support:`413` Replace handrolled ``ssh_config`` parsing code with use of the +* :support:`431` Replace handrolled ``ssh_config`` parsing code with use of the ``shlex`` module. Thanks to Yan Kalchevskiy. * :support:`422` Clean up some unused imports. Courtesy of Olle Lundberg. * :support:`421` Modernize threading calls to user newer API. Thanks to Olle -- cgit v1.2.3 From d120ce4f06da5866c76e5e61196742a89f3c54c3 Mon Sep 17 00:00:00 2001 From: Sean Johnson Date: Tue, 11 Nov 2014 13:15:08 +1100 Subject: Added check for proxycommand none and associated test as per Paramiko Issue 415 Conflicts: tests/test_util.py --- paramiko/config.py | 3 +++ tests/test_util.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/paramiko/config.py b/paramiko/config.py index 91943ffb..233a87d9 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -73,6 +73,9 @@ class SSHConfig (object): 'host': self._get_hosts(value), 'config': {} } + elif key == 'proxycommand' and value.lower() == 'none': + # Proxycommands of none should not be added as an actual value. (Issue #415) + continue else: if value.startswith('"') and value.endswith('"'): value = value[1:-1] diff --git a/tests/test_util.py b/tests/test_util.py index 7f68de21..bfdc525e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -464,3 +464,23 @@ Host param3 parara assert safe_vanilla == vanilla, err.format(safe_vanilla, vanilla) assert safe_has_bytes == expected_bytes, \ err.format(safe_has_bytes, expected_bytes) + + def test_proxycommand_none_issue_418(self): + test_config_file = """ +Host proxycommand-standard-none + ProxyCommand None + +Host proxycommand-with-equals-none + ProxyCommand=None + """ + for host, values in { + 'proxycommand-standard-none': {'hostname': 'proxycommand-standard-none'}, + 'proxycommand-with-equals-none': {'hostname': 'proxycommand-with-equals-none'} + }.items(): + + f = StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEqual( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) -- cgit v1.2.3 From 0a73a54c745c2102b74f0e40514692448e942fec Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 17 Dec 2014 15:35:09 -0800 Subject: Changelog re #415 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index d35ad788..9c2e2a0f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`415` Fix ``ssh_config`` parsing to correctly interpret ``ProxyCommand + none`` as the lack of a proxy command, instead of as a literal command string + of ``"none"``. Thanks to Richard Spiers for the catch & Sean Johnson for the + fix. * :support:`431` Replace handrolled ``ssh_config`` parsing code with use of the ``shlex`` module. Thanks to Yan Kalchevskiy. * :support:`422` Clean up some unused imports. Courtesy of Olle Lundberg. -- cgit v1.2.3 From 25e3c0c34e98b50f429a2e72ecc059d469dc1168 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 14 Dec 2014 00:03:42 -0800 Subject: Log "clamped" value of self.out_max_packet_size This log message misleads one to believe a maximum packet size, such as 16384 requested by OpenSSH has been honored, when in fact it is "clamped" to 32768. --- paramiko/channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index f1b0483d..8a97c974 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -890,7 +890,7 @@ class Channel (ClosingContextManager): self.out_max_packet_size = self.transport. \ _sanitize_packet_size(max_packet_size) self.active = 1 - self._log(DEBUG, 'Max packet out: %d bytes' % max_packet_size) + self._log(DEBUG, 'Max packet out: %d bytes' % self.out_max_packet_size) def _request_success(self, m): self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid) -- cgit v1.2.3 From 6b7d45f2c87c993b7944e7526b184290314d7f9b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sun, 14 Dec 2014 00:05:39 -0800 Subject: Suggest a MIN_WINDOW_SIZE and MIN_PACKET_SIZE Not fully confident with this change, though I will describe my findings fully in the pull request. The OpenSSH client requests a maximum packet size of 16384, but this MIN_PACKET_SIZE value of 32768 causes its request to be "clamped" up to 32768, later causing an error to stderr on the OpenSSH client. Suggest then, to delineate MIN_WINDOW_SIZE from MIN_PACKET_SIZE, as they are applied. I don't think there is any minimum value of MIN_PACKET_SIZE, however we can suggest a value of 4096 for now. --- paramiko/common.py | 6 +++++- paramiko/transport.py | 6 +++--- tests/test_transport.py | 6 +++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/paramiko/common.py b/paramiko/common.py index 97b2f958..0b0cc2a7 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -195,7 +195,11 @@ DEFAULT_MAX_PACKET_SIZE = 2 ** 15 # lower bound on the max packet size we'll accept from the remote host # Minimum packet size is 32768 bytes according to # http://www.ietf.org/rfc/rfc4254.txt -MIN_PACKET_SIZE = 2 ** 15 +MIN_WINDOW_SIZE = 2 ** 15 + +# However, according to http://www.ietf.org/rfc/rfc4253.txt it is perfectly +# legal to accept a size much smaller, as OpenSSH client does as size 16384. +MIN_PACKET_SIZE = 2 ** 12 # Max windows size according to http://www.ietf.org/rfc/rfc4254.txt MAX_WINDOW_SIZE = 2**32 -1 diff --git a/paramiko/transport.py b/paramiko/transport.py index 2a700a88..36da3043 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -43,8 +43,8 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, \ MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \ MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \ - MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_PACKET_SIZE, MAX_WINDOW_SIZE, \ - DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE + MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_WINDOW_SIZE, MIN_PACKET_SIZE, \ + MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey from paramiko.kex_gex import KexGex @@ -1554,7 +1554,7 @@ class Transport (threading.Thread, ClosingContextManager): def _sanitize_window_size(self, window_size): if window_size is None: window_size = self.default_window_size - return clamp_value(MIN_PACKET_SIZE, window_size, MAX_WINDOW_SIZE) + return clamp_value(MIN_WINDOW_SIZE, window_size, MAX_WINDOW_SIZE) def _sanitize_packet_size(self, max_packet_size): if max_packet_size is None: diff --git a/tests/test_transport.py b/tests/test_transport.py index dd522c4e..5cf9a867 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -35,7 +35,7 @@ from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST, \ - MIN_PACKET_SIZE, MAX_WINDOW_SIZE, \ + MIN_PACKET_SIZE, MIN_WINDOW_SIZE, MAX_WINDOW_SIZE, \ DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE from paramiko.py3compat import bytes from paramiko.message import Message @@ -779,7 +779,7 @@ class TransportTest(unittest.TestCase): """ verify that we conform to the rfc of packet and window sizes. """ - for val, correct in [(32767, MIN_PACKET_SIZE), + for val, correct in [(4095, MIN_PACKET_SIZE), (None, DEFAULT_MAX_PACKET_SIZE), (2**32, MAX_WINDOW_SIZE)]: self.assertEqual(self.tc._sanitize_packet_size(val), correct) @@ -788,7 +788,7 @@ class TransportTest(unittest.TestCase): """ verify that we conform to the rfc of packet and window sizes. """ - for val, correct in [(32767, MIN_PACKET_SIZE), + for val, correct in [(32767, MIN_WINDOW_SIZE), (None, DEFAULT_WINDOW_SIZE), (2**32, MAX_WINDOW_SIZE)]: self.assertEqual(self.tc._sanitize_window_size(val), correct) -- cgit v1.2.3 From 681f32583fe052c0516a2fda67e163169676ad11 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 17 Dec 2014 16:07:13 -0800 Subject: Changelog closes #455 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9603e6d5..4e56ad1f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`455` Tweak packet size handling to conform better to the OpenSSH RFCs; + this helps address issues with interactive program cursors. Courtesy of Jeff + Quast. * :bug:`428` Fix an issue in `~paramiko.file.BufferedFile` (primarily used in the SFTP modules) concerning incorrect behavior by `~paramiko.file.BufferedFile.readlines` on files whose size exceeds the -- cgit v1.2.3 From 5601bf0928e2e738917320d83f8302703a62091b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Dec 2014 14:02:28 -0800 Subject: Mark more backported support issues as such --- sites/www/changelog.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f6f2bb28..e5adbd22 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -18,15 +18,15 @@ Changelog none`` as the lack of a proxy command, instead of as a literal command string of ``"none"``. Thanks to Richard Spiers for the catch & Sean Johnson for the fix. -* :support:`431` Replace handrolled ``ssh_config`` parsing code with use of the - ``shlex`` module. Thanks to Yan Kalchevskiy. +* :support:`431 backported` Replace handrolled ``ssh_config`` parsing code with + use of the ``shlex`` module. Thanks to Yan Kalchevskiy. * :support:`422 backported` Clean up some unused imports. Courtesy of Olle Lundberg. -* :support:`421` Modernize threading calls to user newer API. Thanks to Olle - Lundberg. -* :support:`419` Modernize a bunch of the codebase internals to leverage - decorators. Props to ``@beckjake`` for realizing we're no longer on Python - 2.2 :D +* :support:`421 backported` Modernize threading calls to user newer API. Thanks + to Olle Lundberg. +* :support:`419 backported` Modernize a bunch of the codebase internals to + leverage decorators. Props to ``@beckjake`` for realizing we're no longer on + Python 2.2 :D * :bug:`266` Change numbering of `~paramiko.transport.Transport` channels to start at 0 instead of 1 for better compatibility with OpenSSH & certain server implementations which break on 1-indexed channels. Thanks to -- cgit v1.2.3 From ccdfd02c047d5588b6bebdc501a766271a009493 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 19 Dec 2014 14:55:15 -0800 Subject: Cut 1.14.2 --- sites/www/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 8ad82a71..695149de 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` * :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly smarter about polling & timing behavior when running proxy commands, to avoid -- cgit v1.2.3 From 424ba615c2a94d3b059e7f24db1a1093a92d8d22 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 19 Dec 2014 14:55:48 -0800 Subject: Cut 1.15.2 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index d9f78740..3bf9dac7 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 1) +__version_info__ = (1, 15, 2) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f5348e5b..bb93f885 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.15.2 <2014-12-19>` * :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` * :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly -- cgit v1.2.3 From 67ae41a03ef2c94443e809b65f5eb2c4e7cf8937 Mon Sep 17 00:00:00 2001 From: ThiefMaster Date: Thu, 1 Jan 2015 13:50:52 +0100 Subject: Fix typo --- paramiko/_winapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index f48e1890..d6aabf76 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -106,7 +106,7 @@ MapViewOfFile.restype = ctypes.wintypes.HANDLE class MemoryMap(object): """ - A memory map object which can have security attributes overrideden. + A memory map object which can have security attributes overridden. """ def __init__(self, name, length, security_attributes=None): self.name = name -- cgit v1.2.3 From 70924234bb70d15005e4ce18305fc610482acf1b Mon Sep 17 00:00:00 2001 From: Scott Maxwell Date: Mon, 26 Jan 2015 22:36:15 -0800 Subject: Revert add_int and get_int to strictly 32-bits and add adaptive versions --- paramiko/message.py | 45 +++++++++------------------------------------ tests/test_message.py | 8 ++++---- 2 files changed, 13 insertions(+), 40 deletions(-) diff --git a/paramiko/message.py b/paramiko/message.py index b893e76d..bf4c6b95 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -129,7 +129,7 @@ class Message (object): b = self.get_bytes(1) return b != zero_byte - def get_int(self): + def get_adaptive_int(self): """ Fetch an int from the stream. @@ -141,20 +141,7 @@ class Message (object): byte += self.get_bytes(3) return struct.unpack('>I', byte)[0] - def get_size(self): - """ - Fetch an int from the stream. - - @return: a 32-bit unsigned integer. - @rtype: int - """ - byte = self.get_bytes(1) - if byte == max_byte: - return util.inflate_long(self.get_binary()) - byte += self.get_bytes(3) - return struct.unpack('>I', byte)[0] - - def get_size(self): + def get_int(self): """ Fetch an int from the stream. @@ -185,7 +172,7 @@ class Message (object): contain unprintable characters. (It's not unheard of for a string to contain another byte-stream message.) """ - return self.get_bytes(self.get_size()) + return self.get_bytes(self.get_int()) def get_text(self): """ @@ -196,7 +183,7 @@ class Message (object): @return: a string. @rtype: string """ - return u(self.get_bytes(self.get_size())) + return u(self.get_bytes(self.get_int())) #return self.get_bytes(self.get_size()) def get_binary(self): @@ -208,7 +195,7 @@ class Message (object): @return: a string. @rtype: string """ - return self.get_bytes(self.get_size()) + return self.get_bytes(self.get_int()) def get_list(self): """ @@ -248,7 +235,7 @@ class Message (object): self.packet.write(zero_byte) return self - def add_size(self, n): + def add_int(self, n): """ Add an integer to the stream. @@ -257,7 +244,7 @@ class Message (object): self.packet.write(struct.pack('>I', n)) return self - def add_int(self, n): + def add_adaptive_int(self, n): """ Add an integer to the stream. @@ -270,20 +257,6 @@ class Message (object): self.packet.write(struct.pack('>I', n)) return self - def add_int(self, n): - """ - Add an integer to the stream. - - @param n: integer to add - @type n: int - """ - if n >= Message.big_int: - self.packet.write(max_byte) - self.add_string(util.deflate_long(n)) - else: - self.packet.write(struct.pack('>I', n)) - return self - def add_int64(self, n): """ Add a 64-bit int to the stream. @@ -310,7 +283,7 @@ class Message (object): :param str s: string to add """ s = asbytes(s) - self.add_size(len(s)) + self.add_int(len(s)) self.packet.write(s) return self @@ -329,7 +302,7 @@ class Message (object): if type(i) is bool: return self.add_boolean(i) elif isinstance(i, integer_types): - return self.add_int(i) + return self.add_adaptive_int(i) elif type(i) is list: return self.add_list(i) else: diff --git a/tests/test_message.py b/tests/test_message.py index f308c037..f18cae90 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -92,12 +92,12 @@ class MessageTest (unittest.TestCase): def test_4_misc(self): msg = Message(self.__d) - self.assertEqual(msg.get_int(), 5) - self.assertEqual(msg.get_int(), 0x1122334455) - self.assertEqual(msg.get_int(), 0xf00000000000000000) + self.assertEqual(msg.get_adaptive_int(), 5) + self.assertEqual(msg.get_adaptive_int(), 0x1122334455) + self.assertEqual(msg.get_adaptive_int(), 0xf00000000000000000) self.assertEqual(msg.get_so_far(), self.__d[:29]) self.assertEqual(msg.get_remainder(), self.__d[29:]) msg.rewind() - self.assertEqual(msg.get_int(), 5) + self.assertEqual(msg.get_adaptive_int(), 5) self.assertEqual(msg.get_so_far(), self.__d[:4]) self.assertEqual(msg.get_remainder(), self.__d[4:]) -- cgit v1.2.3 From 3700d6e151d7891a12b958cf8f2729576205d927 Mon Sep 17 00:00:00 2001 From: Ken Jordan Date: Thu, 22 Jan 2015 19:39:22 -0700 Subject: Updated agent.py to print a more appropriate exception when unable to connect to the SSH agent --- paramiko/agent.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index a75ac59e..185666e7 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -32,7 +32,7 @@ from select import select from paramiko.common import asbytes, io_sleep from paramiko.py3compat import byte_chr -from paramiko.ssh_exception import SSHException +from paramiko.ssh_exception import SSHException, AuthenticationException from paramiko.message import Message from paramiko.pkey import PKey from paramiko.util import retry_on_signal @@ -109,9 +109,15 @@ class AgentProxyThread(threading.Thread): def run(self): try: (r, addr) = self.get_connection() + # Found that r should be either a socket from the socket library or None self.__inr = r - self.__addr = addr + self.__addr = addr # This should be an IP address as a string? or None self._agent.connect() + print("Conn Value: ", self._agent._conn) + print("Has fileno method: ", hasattr(self._agent._conn, 'fileno')) + print("Verify isinstance of int: ", isinstance(self._agent, int)) + if self._agent._conn is None or not (hasattr(self._agent._conn, 'fileno') or isinstance(self._agent, int)): + raise AuthenticationException("Unable to connect to SSH agent") self._communicate() except: #XXX Not sure what to do here ... raise or pass ? -- cgit v1.2.3 From b26dbc3f719e9844721d38d6264c5cb5e0ed4c2f Mon Sep 17 00:00:00 2001 From: Ken Jordan Date: Thu, 22 Jan 2015 19:41:15 -0700 Subject: Removed debug print statements --- paramiko/agent.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 185666e7..01bd0252 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -113,9 +113,6 @@ class AgentProxyThread(threading.Thread): self.__inr = r self.__addr = addr # This should be an IP address as a string? or None self._agent.connect() - print("Conn Value: ", self._agent._conn) - print("Has fileno method: ", hasattr(self._agent._conn, 'fileno')) - print("Verify isinstance of int: ", isinstance(self._agent, int)) if self._agent._conn is None or not (hasattr(self._agent._conn, 'fileno') or isinstance(self._agent, int)): raise AuthenticationException("Unable to connect to SSH agent") self._communicate() -- cgit v1.2.3 From 394e269a081e6cf07d38fa5a375f0a03374c0d2b Mon Sep 17 00:00:00 2001 From: jordo1ken Date: Fri, 30 Jan 2015 17:57:40 -0700 Subject: Update agent.py Updated logic for error checking. --- paramiko/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/agent.py b/paramiko/agent.py index 01bd0252..f928881e 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -113,7 +113,7 @@ class AgentProxyThread(threading.Thread): self.__inr = r self.__addr = addr # This should be an IP address as a string? or None self._agent.connect() - if self._agent._conn is None or not (hasattr(self._agent._conn, 'fileno') or isinstance(self._agent, int)): + if not isinstance(self._agent, int) and (self._agent._conn is None or not hasattr(self._agent._conn, 'fileno')): raise AuthenticationException("Unable to connect to SSH agent") self._communicate() except: -- cgit v1.2.3 From c5d0d6a2919ca2158b3f6271f7449faeeb3c865f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 4 Feb 2015 16:00:50 -0800 Subject: Changelog fixes #402, closes #479 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index bb93f885..6520dde4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`402` Check to see if an SSH agent is actually present before trying to + forward it to the remote end. This replaces what was usually a useless + ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken + Jordan for the fix and Yvan Marques for original report. * :release:`1.15.2 <2014-12-19>` * :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` -- cgit v1.2.3 From d97c938db32c44a8253f6f872cc3b354492a21cc Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 15:33:28 -0800 Subject: Fix docstring for Sphinx --- paramiko/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 312f71a9..dbe7fcba 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -177,11 +177,9 @@ class SSHClient (ClosingContextManager): """ Yield pairs of address families and addresses to try for connecting. - @param hostname: the server to connect to - @type hostname: str - @param port: the server port to connect to - @type port: int - @rtype: generator + :param str hostname: the server to connect to + :param int port: the server port to connect to + :returns: Yields an iterable of ``(family, address)`` tuples """ guess = True addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) -- cgit v1.2.3 From b42b5338de868c3a5343dd03a8ee3f8a968d2f65 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 15:33:32 -0800 Subject: Comment --- paramiko/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paramiko/client.py b/paramiko/client.py index dbe7fcba..b907d3b0 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -256,6 +256,7 @@ class SSHClient (ClosingContextManager): ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: + # Try multiple possible address families (e.g. IPv4 vs IPv6) for af, addr in self._families_and_addresses(hostname, port): try: sock = socket.socket(af, socket.SOCK_STREAM) -- cgit v1.2.3 From c2e346372c06c5eb4982055c689e1eb89955bddf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 16:20:51 -0800 Subject: Bump dev version. (might end up 2.0 tho) --- paramiko/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 3bf9dac7..e82b8667 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 2) +__version_info__ = (1, 16, 0) __version__ = '.'.join(map(str, __version_info__)) -- cgit v1.2.3 From f99e1d8775be20c471c39f8d7a91126b8419381d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 6 Feb 2015 19:33:06 -0800 Subject: Raise usefully ambiguous error when every connect attempt fails. Re #22 --- paramiko/client.py | 24 ++++++++++++++++++++---- paramiko/ssh_exception.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index b907d3b0..57e9919d 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -36,7 +36,9 @@ from paramiko.hostkeys import HostKeys from paramiko.py3compat import string_types from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey -from paramiko.ssh_exception import SSHException, BadHostKeyException +from paramiko.ssh_exception import ( + SSHException, BadHostKeyException, ConnectionError +) from paramiko.transport import Transport from paramiko.util import retry_on_signal, ClosingContextManager @@ -256,8 +258,10 @@ class SSHClient (ClosingContextManager): ``gss_deleg_creds`` and ``gss_host`` arguments. """ if not sock: + errors = {} # Try multiple possible address families (e.g. IPv4 vs IPv6) - for af, addr in self._families_and_addresses(hostname, port): + to_try = list(self._families_and_addresses(hostname, port)) + for af, addr in to_try: try: sock = socket.socket(af, socket.SOCK_STREAM) if timeout is not None: @@ -269,10 +273,22 @@ class SSHClient (ClosingContextManager): # Break out of the loop on success break except socket.error, e: - # If the port is not open on IPv6 for example, we may still try IPv4. - # Likewise if the host is not reachable using that address family. + # Raise anything that isn't a straight up connection error + # (such as a resolution error) if e.errno not in (ECONNREFUSED, EHOSTUNREACH): raise + # Capture anything else so we know how the run looks once + # iteration is complete. Retain info about which attempt + # this was. + errors[addr] = e + + # Make sure we explode usefully if no address family attempts + # succeeded. We've no way of knowing which error is the "right" + # one, so we construct a hybrid exception containing all the real + # ones, of a subclass that client code should still be watching for + # (socket.error) + if len(errors) == len(to_try): + raise ConnectionError(errors) t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds) t.use_compression(compress=compress) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index b99e42b3..7e6f2568 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -16,6 +16,8 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +import socket + class SSHException (Exception): """ @@ -129,3 +131,31 @@ class ProxyCommandFailure (SSHException): self.error = error # for unpickling self.args = (command, error, ) + + +class ConnectionError(socket.error): + """ + High-level socket error wrapping 1+ actual socket.error objects. + + To see the wrapped exception objects, access the ``errors`` attribute. + ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', + 22)``) and whose values are the exception encountered trying to connect to + that address. + + It is implied/assumed that all the errors given to a single instance of + this class are from connecting to the same hostname + port (and thus that + the differences are in the resolution of the hostname - e.g. IPv4 vs v6). + """ + def __init__(self, errors): + """ + :param dict errors: + The errors dict to store, as described by class docstring. + """ + addrs = errors.keys() + body = ', '.join([x[0] for x in addrs[:-1]]) + tail = addrs[-1][0] + msg = "Unable to connect to port {0} at {1} or {2}" + super(ConnectionError, self).__init__( + msg.format(addrs[0][1], body, tail) + ) + self.errors = errors -- cgit v1.2.3 From b98ba1c5bb6ff1d968e9105ff093d360ac9091e9 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Tue, 24 Feb 2015 14:47:49 +0100 Subject: Add support for signaling a handshake process in packetizer. This makes it possible to raise an EOFError if the handshake process has started but takes too long time to finish. --- paramiko/packet.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/paramiko/packet.py b/paramiko/packet.py index f516ff9b..b922000c 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -99,6 +99,10 @@ class Packetizer (object): self.__keepalive_last = time.time() self.__keepalive_callback = None + self.__timer = None + self.__handshake_complete = False + self.__timer_expired = False + def set_log(self, log): """ Set the Python log object to use for logging. @@ -182,6 +186,45 @@ class Packetizer (object): self.__keepalive_callback = callback self.__keepalive_last = time.time() + def read_timer(self): + self.__timer_expired = True + + def start_handshake(self, timeout): + """ + Tells `Packetizer` that the handshake process started. + Starts a book keeping timer that can signal a timeout in the + handshake process. + + :param float timeout: amount of seconds to wait before timing out + """ + if not self.__timer: + self.__timer = threading.Timer(float(timeout), self.read_timer) + self.__timer.start() + + def handshake_timed_out(self): + """ + Checks if the handshake has timed out. + If `start_handshake` wasn't called before the call to this function + the return value will always be `False`. + If the handshake completed before a time out was reached the return value will be `False` + + :return: handshake time out status, as a `bool` + """ + if not self.__timer: + return False + if self.__handshake_complete: + return False + return self.__timer_expired + + def complete_handshake(self): + """ + Tells `Packetizer` that the handshake has completed. + """ + if self.__timer: + self.__timer.cancel() + self.__timer_expired = False + self.__handshake_complete = True + def read_all(self, n, check_rekey=False): """ Read as close to N bytes as possible, blocking as long as necessary. @@ -200,6 +243,8 @@ class Packetizer (object): n -= len(out) while n > 0: got_timeout = False + if self.handshake_timed_out(): + raise EOFError() try: x = self.__socket.recv(n) if len(x) == 0: -- cgit v1.2.3 From d1f72859c76beda46a072cdc75b2e19e4418275a Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Tue, 24 Feb 2015 14:49:36 +0100 Subject: Expose handshake timeout in the transport API. This is a reimplementation of #62. --- paramiko/transport.py | 9 +++++++++ sites/www/changelog.rst | 5 +++++ tests/test_transport.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/paramiko/transport.py b/paramiko/transport.py index 36da3043..6047fb99 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -295,6 +295,8 @@ class Transport (threading.Thread, ClosingContextManager): self.global_response = None # response Message from an arbitrary global request self.completion_event = None # user-defined event callbacks self.banner_timeout = 15 # how long (seconds) to wait for the SSH banner + self.handshake_timeout = 15 # how long (seconds) to wait for the handshake to finish after SSH banner sent. + # server mode: self.server_mode = False @@ -1582,6 +1584,12 @@ class Transport (threading.Thread, ClosingContextManager): try: self.packetizer.write_all(b(self.local_version + '\r\n')) self._check_banner() + # The above is actually very much part of the handshake, but sometimes the banner can be read + # but the machine is not responding, for example when the remote ssh daemon is loaded in to memory + # but we can not read from the disk/spawn a new shell. + # Make sure we can specify a timeout for the initial handshake. + # Re-use the banner timeout for now. + self.packetizer.start_handshake(self.handshake_timeout) self._send_kex_init() self._expect_packet(MSG_KEXINIT) @@ -1631,6 +1639,7 @@ class Transport (threading.Thread, ClosingContextManager): msg.add_byte(cMSG_UNIMPLEMENTED) msg.add_int(m.seqno) self._send_message(msg) + self.packetizer.complete_handshake() except SSHException as e: self._log(ERROR, 'Exception: ' + str(e)) self._log(ERROR, util.tb_strings()) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6520dde4..f9900327 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :bug:`62` Add timeout for handshake completion. + This adds a mechanism for timing out a connection if the ssh handshake + never completes. + Credit to ``@dacut`` for initial report and patch and to Olle Lundberg for + re-implementation. * :bug:`402` Check to see if an SSH agent is actually present before trying to forward it to the remote end. This replaces what was usually a useless ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken diff --git a/tests/test_transport.py b/tests/test_transport.py index 5cf9a867..3c8ad81e 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -792,3 +792,20 @@ class TransportTest(unittest.TestCase): (None, DEFAULT_WINDOW_SIZE), (2**32, MAX_WINDOW_SIZE)]: self.assertEqual(self.tc._sanitize_window_size(val), correct) + + def test_L_handshake_timeout(self): + """ + verify that we can get a hanshake timeout. + """ + host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = RSAKey(data=host_key.asbytes()) + self.ts.add_server_key(host_key) + event = threading.Event() + server = NullServer() + self.assertTrue(not event.is_set()) + self.tc.handshake_timeout = 0.000000000001 + self.ts.start_server(event, server) + self.assertRaises(EOFError, self.tc.connect, + hostkey=public_host_key, + username='slowdive', + password='pygmalion') -- cgit v1.2.3 From 6ba6ccda7bb34f16e92aa1acfb430055f264bd41 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Tue, 24 Feb 2015 15:14:51 +0100 Subject: Patch resolving the timeout issue on lost conection. (This rolls in patch in #439) --- paramiko/client.py | 2 +- paramiko/transport.py | 18 +++++++++++++----- sites/www/changelog.rst | 3 +++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 393e3e09..9ee30287 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -338,7 +338,7 @@ class SSHClient (ClosingContextManager): :raises SSHException: if the server fails to execute the command """ - chan = self._transport.open_session() + chan = self._transport.open_session(timeout=timeout) if get_pty: chan.get_pty() chan.settimeout(timeout) diff --git a/paramiko/transport.py b/paramiko/transport.py index 6047fb99..31c27a2f 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -589,7 +589,7 @@ class Transport (threading.Thread, ClosingContextManager): """ return self.active - def open_session(self, window_size=None, max_packet_size=None): + def open_session(self, window_size=None, max_packet_size=None, timeout=None): """ Request a new channel to the server, of type ``"session"``. This is just an alias for calling `open_channel` with an argument of @@ -614,7 +614,8 @@ class Transport (threading.Thread, ClosingContextManager): """ return self.open_channel('session', window_size=window_size, - max_packet_size=max_packet_size) + max_packet_size=max_packet_size, + timeout=timeout) def open_x11_channel(self, src_addr=None): """ @@ -661,7 +662,8 @@ class Transport (threading.Thread, ClosingContextManager): dest_addr=None, src_addr=None, window_size=None, - max_packet_size=None): + max_packet_size=None, + timeout=None): """ Request a new channel to the server. `Channels <.Channel>` are socket-like objects used for the actual transfer of data across the @@ -685,17 +687,20 @@ class Transport (threading.Thread, ClosingContextManager): optional window size for this session. :param int max_packet_size: optional max packet size for this session. + :param float timeout: + optional timeout opening a channel, default 3600s (1h) :return: a new `.Channel` on success - :raises SSHException: if the request is rejected or the session ends - prematurely + :raises SSHException: if the request is rejected, the session ends + prematurely or there is a timeout openning a channel .. versionchanged:: 1.15 Added the ``window_size`` and ``max_packet_size`` arguments. """ if not self.active: raise SSHException('SSH session not active') + timeout = 3600 if timeout is None else timeout self.lock.acquire() try: window_size = self._sanitize_window_size(window_size) @@ -724,6 +729,7 @@ class Transport (threading.Thread, ClosingContextManager): finally: self.lock.release() self._send_user_message(m) + start_ts = time.time() while True: event.wait(0.1) if not self.active: @@ -733,6 +739,8 @@ class Transport (threading.Thread, ClosingContextManager): raise e if event.is_set(): break + elif start_ts + timeout < time.time(): + raise SSHException('Timeout openning channel.') chan = self._channels.get(chanid) if chan is not None: return chan diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f9900327..16a60a68 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`439` Resolve the timeout issue on lost conection. + When the destination disappears on an established session paramiko will hang on trying to open a channel. + Credit to ``@vazir`` for patch. * :bug:`62` Add timeout for handshake completion. This adds a mechanism for timing out a connection if the ssh handshake never completes. -- cgit v1.2.3 From 4ca8d68c0443c4e5e17ae4fcee39dd6f2507c7cd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 13:19:35 -0800 Subject: Changelog closes #22 --- sites/www/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6520dde4..0e8f92c4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +* :bug:`22 major` Try harder to connect to multiple network families (e.g. IPv4 + vs IPv6) in case of connection issues; this helps with problems such as hosts + which resolve both IPv4 and IPv6 addresses but are only listening on IPv4. + Thanks to Dries Desmet for original report and Torsten Landschoff for the + foundational patchset. * :bug:`402` Check to see if an SSH agent is actually present before trying to forward it to the remote end. This replaces what was usually a useless ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken -- cgit v1.2.3 From 27e6177f4b7bae87c9fc7aa3556d20100ff9a319 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 13:29:32 -0800 Subject: Fix a Python 3 incompat bit from recent merge --- paramiko/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/client.py b/paramiko/client.py index 57e9919d..2e1a4dc4 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -272,7 +272,7 @@ class SSHClient (ClosingContextManager): retry_on_signal(lambda: sock.connect(addr)) # Break out of the loop on success break - except socket.error, e: + except socket.error as e: # Raise anything that isn't a straight up connection error # (such as a resolution error) if e.errno not in (ECONNREFUSED, EHOSTUNREACH): -- cgit v1.2.3 From 3ceab6a5f65db5be219b3dba17677e3101a0489f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 27 Feb 2015 14:52:26 -0800 Subject: Some 80-col fixes --- paramiko/client.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 2e1a4dc4..6c48b269 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -196,10 +196,25 @@ class SSHClient (ClosingContextManager): for family, _, _, _, sockaddr in addrinfos: yield family, sockaddr - 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, - compress=False, sock=None, gss_auth=False, gss_kex=False, - gss_deleg_creds=True, gss_host=None, banner_timeout=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, + compress=False, + sock=None, + gss_auth=False, + gss_kex=False, + gss_deleg_creds=True, + gss_host=None, + banner_timeout=None + ): """ Connect to an SSH server and authenticate to it. The server's host key is checked against the system host keys (see `load_system_host_keys`) @@ -230,8 +245,10 @@ class SSHClient (ClosingContextManager): :param str key_filename: the filename, or list of filenames, of optional private key(s) to try for authentication - :param float timeout: an optional timeout (in seconds) for the TCP connect - :param bool allow_agent: set to False to disable connecting to the SSH agent + :param float timeout: + an optional timeout (in seconds) for the TCP connect + :param bool allow_agent: + set to False to disable connecting to the SSH agent :param bool look_for_keys: set to False to disable searching for discoverable private key files in ``~/.ssh/`` @@ -240,9 +257,11 @@ class SSHClient (ClosingContextManager): an open socket or socket-like object (such as a `.Channel`) to use for communication to the target host :param bool gss_auth: ``True`` if you want to use GSS-API authentication - :param bool gss_kex: Perform GSS-API Key Exchange and user authentication + :param bool gss_kex: + Perform GSS-API Key Exchange and user authentication :param bool gss_deleg_creds: Delegate GSS-API client credentials or not - :param str gss_host: The targets name in the kerberos database. default: hostname + :param str gss_host: + The targets name in the kerberos database. default: hostname :param float banner_timeout: an optional timeout (in seconds) to wait for the SSH banner to be presented. -- cgit v1.2.3 From ca0fd1024ecf61b1758bdd38350fbd4c4ccaaefb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sat, 28 Feb 2015 19:54:52 -0800 Subject: Replace/add RFC links using ``:rfc:``, /ht @sigmavirus24 --- paramiko/channel.py | 2 +- paramiko/kex_gss.py | 27 +++++++++++++++------------ paramiko/ssh_gss.py | 2 +- sites/www/index.rst | 8 ++------ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index 8a97c974..7e39a15b 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -337,7 +337,7 @@ class Channel (ClosingContextManager): further x11 requests can be made from the server to the client, when an x11 application is run in a shell session. - From RFC4254:: + From :rfc:`4254`:: It is RECOMMENDED that the 'x11 authentication cookie' that is sent be a fake, random cookie, and that the cookie be checked and diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index 4e8380ef..d026807c 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -21,14 +21,15 @@ """ -This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462. +This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`. .. note:: Credential delegation is not supported in server mode. .. note:: - `RFC 4462 Section 2.2 `_ says we are - not required to implement GSS-API error messages. Thus, in many methods - within this module, if an error occurs an exception will be thrown and the + `RFC 4462 Section 2.2 + `_ says we are not + required to implement GSS-API error messages. Thus, in many methods within + this module, if an error occurs an exception will be thrown and the connection will be terminated. .. seealso:: :doc:`/api/ssh_gss` @@ -55,8 +56,8 @@ c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)] class KexGSSGroup1(object): """ - GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange - as defined in `RFC 4462 Section 2 `_ + GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange as defined in `RFC + 4462 Section 2 `_ """ # draft-ietf-secsh-transport-09.txt, page 17 P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF @@ -278,8 +279,9 @@ class KexGSSGroup1(object): class KexGSSGroup14(KexGSSGroup1): """ - GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange - as defined in `RFC 4462 Section 2 `_ + GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange as defined + in `RFC 4462 Section 2 + `_ """ P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF G = 2 @@ -288,8 +290,8 @@ class KexGSSGroup14(KexGSSGroup1): class KexGSSGex(object): """ - GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange - as defined in `RFC 4462 Section 2 `_ + GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange as defined in + `RFC 4462 Section 2 `_ """ NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" min_bits = 1024 @@ -590,8 +592,9 @@ class KexGSSGex(object): class NullHostKey(object): """ - This class represents the Null Host Key for GSS-API Key Exchange - as defined in `RFC 4462 Section 5 `_ + This class represents the Null Host Key for GSS-API Key Exchange as defined + in `RFC 4462 Section 5 + `_ """ def __init__(self): self.key = "" diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index ebf2cc80..aa28e2ec 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -20,7 +20,7 @@ """ -This module provides GSS-API / SSPI authentication as defined in RFC 4462. +This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`. .. note:: Credential delegation is not supported in server mode. diff --git a/sites/www/index.rst b/sites/www/index.rst index 1b609709..8e7562af 100644 --- a/sites/www/index.rst +++ b/sites/www/index.rst @@ -26,11 +26,7 @@ Please see the sidebar to the left to begin. .. rubric:: Footnotes .. [#] - SSH is defined in RFCs - `4251 `_, - `4252 `_, - `4253 `_, and - `4254 `_; - the primary working implementation of the protocol is the `OpenSSH project + SSH is defined in :rfc:`4251`, :rfc:`4252`, :rfc:`4253` and :rfc:`4254`. The + primary working implementation of the protocol is the `OpenSSH project `_. Paramiko implements a large portion of the SSH feature set, but there are occasional gaps. -- cgit v1.2.3 From 23dc9d1fdee82b94c40cc86970246e027c0f360e Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Tue, 3 Mar 2015 12:30:37 +0100 Subject: Add a missing import. --- paramiko/auth_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index c001aeee..9cf4e271 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -34,7 +34,7 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \ cMSG_USERAUTH_GSSAPI_ERRTOK, cMSG_USERAUTH_GSSAPI_MIC,\ MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, \ MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR, \ - MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC + MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES from paramiko.message import Message from paramiko.py3compat import bytestring -- cgit v1.2.3 From ee47257684d2b9999743317c1b1bc1a0b135875d Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Tue, 3 Mar 2015 12:59:01 +0100 Subject: Fix the documentation of both implementations of ssh_check_mic. The method raises an exception, if the check fails and has no return value. --- paramiko/ssh_gss.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py index aa28e2ec..e9b13a66 100644 --- a/paramiko/ssh_gss.py +++ b/paramiko/ssh_gss.py @@ -360,8 +360,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth): :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login - :return: 0 if the MIC check was successful and 1 if it fails - :rtype: int + :return: None if the MIC check was successful + :raises gssapi.GSSException: if the MIC check failed """ self._session_id = session_id self._username = username @@ -371,11 +371,7 @@ class _SSH_GSSAPI(_SSH_GSSAuth): self._username, self._service, self._auth_method) - try: - self._gss_srv_ctxt.verify_mic(mic_field, - mic_token) - except gssapi.BadSignature: - raise Exception("GSS-API MIC check failed.") + self._gss_srv_ctxt.verify_mic(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode @@ -534,31 +530,26 @@ class _SSH_SSPI(_SSH_GSSAuth): :param str mic_token: The MIC token received from the client :param str session_id: The SSH session ID :param str username: The name of the user who attempts to login - :return: 0 if the MIC check was successful - :rtype: int + :return: None if the MIC check was successful + :raises sspi.error: if the MIC check failed """ self._session_id = session_id self._username = username - mic_status = 1 if username is not None: # server mode mic_field = self._ssh_build_mic(self._session_id, self._username, self._service, self._auth_method) - mic_status = self._gss_srv_ctxt.verify(mic_field, - mic_token) + # Verifies data and its signature. If verification fails, an + # sspi.error will be raised. + self._gss_srv_ctxt.verify(mic_field, mic_token) else: # for key exchange with gssapi-keyex # client mode - mic_status = self._gss_ctxt.verify(self._session_id, - mic_token) - """ - The SSPI method C{verify} has no return value, so if no SSPI error - is returned, set C{mic_status} to 0. - """ - mic_status = 0 - return mic_status + # Verifies data and its signature. If verification fails, an + # sspi.error will be raised. + self._gss_ctxt.verify(self._session_id, mic_token) @property def credentials_delegated(self): -- cgit v1.2.3 From ceddde7498a1b88e9662668b037552b71010cf46 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Tue, 3 Mar 2015 13:00:47 +0100 Subject: Fix an uninitialised variable usage and simplify the code. --- paramiko/auth_handler.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 9cf4e271..ef4a8c7e 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -510,15 +510,11 @@ class AuthHandler (object): result = AUTH_FAILED self._send_auth_result(username, method, result) raise - if retval == 0: - # TODO: Implement client credential saving. - # The OpenSSH server is able to create a TGT with the delegated - # client credentials, but this is not supported by GSS-API. - result = AUTH_SUCCESSFUL - self.transport.server_object.check_auth_gssapi_with_mic( - username, result) - else: - result = AUTH_FAILED + # TODO: Implement client credential saving. + # The OpenSSH server is able to create a TGT with the delegated + # client credentials, but this is not supported by GSS-API. + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_with_mic(username, result) elif method == "gssapi-keyex" and gss_auth: mic_token = m.get_string() sshgss = self.transport.kexgss_ctxt @@ -534,12 +530,8 @@ class AuthHandler (object): result = AUTH_FAILED self._send_auth_result(username, method, result) raise - if retval == 0: - result = AUTH_SUCCESSFUL - self.transport.server_object.check_auth_gssapi_keyex(username, - result) - else: - result = AUTH_FAILED + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_keyex(username, result) else: result = self.transport.server_object.check_auth_none(username) # okay, send result -- cgit v1.2.3 From c158db9833c70b2f57db0a58ab6133595c740d6f Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Tue, 3 Mar 2015 13:02:02 +0100 Subject: Switch kex_gss from using PyCrypto's Random to using os.urandom. --- paramiko/kex_gss.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py index d026807c..69969f8a 100644 --- a/paramiko/kex_gss.py +++ b/paramiko/kex_gss.py @@ -37,6 +37,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`. .. versionadded:: 1.15 """ +import os from hashlib import sha1 from paramiko.common import * @@ -130,7 +131,7 @@ class KexGSSGroup1(object): larger than q (but this is a tiny tiny subset of potential x). """ while 1: - x_bytes = self.transport.rng.read(128) + x_bytes = os.urandom(128) x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:] if (x_bytes[:8] != self.b7fffffffffffffff) and \ (x_bytes[:8] != self.b0000000000000000): @@ -366,7 +367,7 @@ class KexGSSGex(object): qhbyte <<= 1 qmask >>= 1 while True: - x_bytes = self.transport.rng.read(byte_count) + x_bytes = os.urandom(byte_count) x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): -- cgit v1.2.3 From b0a5ca8e3747a34082895bbd170a617f76ebe7e5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 5 Mar 2015 09:54:41 -0800 Subject: Rename new exception class to be less generic Re #22 --- paramiko/client.py | 4 ++-- paramiko/ssh_exception.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/paramiko/client.py b/paramiko/client.py index 6c48b269..e10e9da7 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -37,7 +37,7 @@ from paramiko.py3compat import string_types from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey from paramiko.ssh_exception import ( - SSHException, BadHostKeyException, ConnectionError + SSHException, BadHostKeyException, NoValidConnectionsError ) from paramiko.transport import Transport from paramiko.util import retry_on_signal, ClosingContextManager @@ -307,7 +307,7 @@ class SSHClient (ClosingContextManager): # ones, of a subclass that client code should still be watching for # (socket.error) if len(errors) == len(to_try): - raise ConnectionError(errors) + raise NoValidConnectionsError(errors) t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds) t.use_compression(compress=compress) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 7e6f2568..169dad81 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -133,7 +133,7 @@ class ProxyCommandFailure (SSHException): self.args = (command, error, ) -class ConnectionError(socket.error): +class NoValidConnectionsError(socket.error): """ High-level socket error wrapping 1+ actual socket.error objects. @@ -155,7 +155,7 @@ class ConnectionError(socket.error): body = ', '.join([x[0] for x in addrs[:-1]]) tail = addrs[-1][0] msg = "Unable to connect to port {0} at {1} or {2}" - super(ConnectionError, self).__init__( + super(NoValidConnectionsError, self).__init__( msg.format(addrs[0][1], body, tail) ) self.errors = errors -- cgit v1.2.3 From 136e0deef9c949a1b223244b696e6d870cc12e34 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 5 Mar 2015 09:55:29 -0800 Subject: Add null errno to socket.error subclass. Makes downstream code less likely to break when they expect errno+msg style socket error objects. Re #22 --- paramiko/ssh_exception.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 169dad81..1fbebde8 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -135,7 +135,14 @@ class ProxyCommandFailure (SSHException): class NoValidConnectionsError(socket.error): """ - High-level socket error wrapping 1+ actual socket.error objects. + Multiple connection attempts were made and no families succeeded. + + This exception class wraps multiple "real" underlying connection errors, + all of which represent failed connection attempts. Because these errors are + not guaranteed to all be of the same error type (i.e. different errno, + class, message, etc) we expose a single unified error message and a + ``None`` errno so that instances of this class match most normal handling + of `socket.error` objects. To see the wrapped exception objects, access the ``errors`` attribute. ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', @@ -156,6 +163,7 @@ class NoValidConnectionsError(socket.error): tail = addrs[-1][0] msg = "Unable to connect to port {0} at {1} or {2}" super(NoValidConnectionsError, self).__init__( + None, # stand-in for errno msg.format(addrs[0][1], body, tail) ) self.errors = errors -- cgit v1.2.3 From cfeca480db0116c5c8d95ba1aaa9e5e5e02951ed Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 5 Mar 2015 09:55:36 -0800 Subject: Error message langauge tweak --- paramiko/ssh_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 1fbebde8..2e09c6d6 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -161,7 +161,7 @@ class NoValidConnectionsError(socket.error): addrs = errors.keys() body = ', '.join([x[0] for x in addrs[:-1]]) tail = addrs[-1][0] - msg = "Unable to connect to port {0} at {1} or {2}" + msg = "Unable to connect to port {0} on {1} or {2}" super(NoValidConnectionsError, self).__init__( None, # stand-in for errno msg.format(addrs[0][1], body, tail) -- cgit v1.2.3 From 2e4d604cdd3d65dd5a826794231ad03839c28d4a Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Wed, 18 Mar 2015 14:49:09 +0100 Subject: Add a missing import. --- paramiko/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/server.py b/paramiko/server.py index bf5039a2..f79a1748 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -22,7 +22,7 @@ import threading from paramiko import util -from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED +from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED, AUTH_SUCCESSFUL from paramiko.py3compat import string_types -- cgit v1.2.3 From 7400ce4fd80fc6c0cfc1b3d96900ee2fb87f9ebe Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 29 Apr 2015 19:25:25 -0700 Subject: Packaging updates --- dev-requirements.txt | 8 +++++--- tasks.py | 24 +++--------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 7a0ccbc5..059572cf 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,11 @@ # Older junk tox>=1.4,<1.5 # For newer tasks like building Sphinx docs. -invoke>=0.7.0,<0.8 -invocations>=0.5.0 +invoke>=0.10 +invocations>=0.9.2 sphinx>=1.1.3 alabaster>=0.6.1 releases>=0.5.2 -wheel==0.23.0 +semantic_version>=2.4,<2.5 +wheel==0.24 +twine==1.5 diff --git a/tasks.py b/tasks.py index 1afce514..20ded03d 100644 --- a/tasks.py +++ b/tasks.py @@ -3,28 +3,10 @@ from os.path import join from shutil import rmtree, copytree from invoke import Collection, ctask as task -from invocations import docs as _docs +from invocations.docs import docs, www from invocations.packaging import publish -d = 'sites' - -# Usage doc/API site (published as docs.paramiko.org) -docs_path = join(d, 'docs') -docs_build = join(docs_path, '_build') -docs = Collection.from_module(_docs, name='docs', config={ - 'sphinx.source': docs_path, - 'sphinx.target': docs_build, -}) - -# Main/about/changelog site ((www.)?paramiko.org) -www_path = join(d, 'www') -www = Collection.from_module(_docs, name='www', config={ - 'sphinx.source': www_path, - 'sphinx.target': join(www_path, '_build'), -}) - - # Until we move to spec-based testing @task def test(ctx): @@ -45,9 +27,9 @@ def release(ctx): rmtree(target, ignore_errors=True) copytree(docs_build, target) # Publish - publish(ctx, wheel=True) + publish(ctx) # Remind print("\n\nDon't forget to update RTD's versions page for new minor releases!") -ns = Collection(test, coverage, release, docs=docs, www=www) +ns = Collection(test, coverage, release, docs, www) -- cgit v1.2.3 From 9c77538747881bb8cb3f6c7b220515cfd6943b92 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 29 Apr 2015 19:39:04 -0700 Subject: Not sure how this got nuked --- tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tasks.py b/tasks.py index c7d28b16..3d575670 100644 --- a/tasks.py +++ b/tasks.py @@ -17,6 +17,11 @@ def test(ctx, coverage=False): ctx.run("{0} test.py {1}".format(runner, flags), pty=True) +@task +def coverage(ctx): + ctx.run("coverage run --source=paramiko test.py --verbose") + + # Until we stop bundling docs w/ releases. Need to discover use cases first. @task def release(ctx): -- cgit v1.2.3 From 2cea4c78317d0295d5d1a9d309e66c2b35bf0ca9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 8 May 2015 10:28:07 -0400 Subject: Refresh vendored _winapi module. --- paramiko/_winapi.py | 545 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 321 insertions(+), 224 deletions(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index f141b005..84ee07fd 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -1,269 +1,366 @@ """ Windows API functions implemented as ctypes functions and classes as found -in jaraco.windows (2.10). +in jaraco.windows (3.3). If you encounter issues with this module, please consider reporting the issues in jaraco.windows and asking the author to port the fixes back here. """ -import ctypes +import sys import ctypes.wintypes -import __builtin__ + +import six +from six.moves import builtins + ###################### # jaraco.windows.error def format_system_message(errno): - """ - Call FormatMessage with a system error number to retrieve - the descriptive error message. - """ - # first some flags used by FormatMessageW - ALLOCATE_BUFFER = 0x100 - ARGUMENT_ARRAY = 0x2000 - FROM_HMODULE = 0x800 - FROM_STRING = 0x400 - FROM_SYSTEM = 0x1000 - IGNORE_INSERTS = 0x200 - - # Let FormatMessageW allocate the buffer (we'll free it below) - # Also, let it know we want a system error message. - flags = ALLOCATE_BUFFER | FROM_SYSTEM - source = None - message_id = errno - language_id = 0 - result_buffer = ctypes.wintypes.LPWSTR() - buffer_size = 0 - arguments = None - bytes = ctypes.windll.kernel32.FormatMessageW( - flags, - source, - message_id, - language_id, - ctypes.byref(result_buffer), - buffer_size, - arguments, - ) - # note the following will cause an infinite loop if GetLastError - # repeatedly returns an error that cannot be formatted, although - # this should not happen. - handle_nonzero_success(bytes) - message = result_buffer.value - ctypes.windll.kernel32.LocalFree(result_buffer) - return message - - -class WindowsError(__builtin__.WindowsError): - "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" - - def __init__(self, value=None): - if value is None: - value = ctypes.windll.kernel32.GetLastError() - strerror = format_system_message(value) - super(WindowsError, self).__init__(value, strerror) - - @property - def message(self): - return self.strerror - - @property - def code(self): - return self.winerror - - def __str__(self): - return self.message - - def __repr__(self): - return '{self.__class__.__name__}({self.winerror})'.format(**vars()) + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + FROM_SYSTEM = 0x1000 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(builtins.WindowsError): + "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + if sys.version_info > (3,3): + args = 0, strerror, None, value + else: + args = value, strerror + super(WindowsError, self).__init__(*args) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return '{self.__class__.__name__}({self.winerror})'.format(**vars()) def handle_nonzero_success(result): - if result == 0: - raise WindowsError() + if result == 0: + raise WindowsError() -##################### -# jaraco.windows.mmap +########################### +# jaraco.windows.api.memory + +GMEM_MOVEABLE = 0x2 + +GlobalAlloc = ctypes.windll.kernel32.GlobalAlloc +GlobalAlloc.argtypes = ctypes.wintypes.UINT, ctypes.c_ssize_t +GlobalAlloc.restype = ctypes.wintypes.HANDLE + +GlobalLock = ctypes.windll.kernel32.GlobalLock +GlobalLock.argtypes = ctypes.wintypes.HGLOBAL, +GlobalLock.restype = ctypes.wintypes.LPVOID + +GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock +GlobalUnlock.argtypes = ctypes.wintypes.HGLOBAL, +GlobalUnlock.restype = ctypes.wintypes.BOOL + +GlobalSize = ctypes.windll.kernel32.GlobalSize +GlobalSize.argtypes = ctypes.wintypes.HGLOBAL, +GlobalSize.restype = ctypes.c_size_t CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW CreateFileMapping.argtypes = [ - ctypes.wintypes.HANDLE, - ctypes.c_void_p, - ctypes.wintypes.DWORD, - ctypes.wintypes.DWORD, - ctypes.wintypes.DWORD, - ctypes.wintypes.LPWSTR, + ctypes.wintypes.HANDLE, + ctypes.c_void_p, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.LPWSTR, ] CreateFileMapping.restype = ctypes.wintypes.HANDLE MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile MapViewOfFile.restype = ctypes.wintypes.HANDLE +##################### +# jaraco.windows.mmap + class MemoryMap(object): - """ - A memory map object which can have security attributes overrideden. - """ - def __init__(self, name, length, security_attributes=None): - self.name = name - self.length = length - self.security_attributes = security_attributes - self.pos = 0 - - def __enter__(self): - p_SA = ( - ctypes.byref(self.security_attributes) - if self.security_attributes else None - ) - INVALID_HANDLE_VALUE = -1 - PAGE_READWRITE = 0x4 - FILE_MAP_WRITE = 0x2 - filemap = ctypes.windll.kernel32.CreateFileMappingW( - INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, - unicode(self.name)) - handle_nonzero_success(filemap) - if filemap == INVALID_HANDLE_VALUE: - raise Exception("Failed to create file mapping") - self.filemap = filemap - self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) - return self - - def seek(self, pos): - self.pos = pos - - def write(self, msg): - ctypes.windll.msvcrt.memcpy(self.view + self.pos, msg, len(msg)) - self.pos += len(msg) - - def read(self, n): - """ - Read n bytes from mapped view. - """ - out = ctypes.create_string_buffer(n) - ctypes.windll.msvcrt.memcpy(out, self.view + self.pos, n) - self.pos += n - return out.raw - - def __exit__(self, exc_type, exc_val, tb): - ctypes.windll.kernel32.UnmapViewOfFile(self.view) - ctypes.windll.kernel32.CloseHandle(self.filemap) + """ + A memory map object which can have security attributes overrideden. + """ + def __init__(self, name, length, security_attributes=None): + self.name = name + self.length = length + self.security_attributes = security_attributes + self.pos = 0 + + def __enter__(self): + p_SA = ( + ctypes.byref(self.security_attributes) + if self.security_attributes else None + ) + INVALID_HANDLE_VALUE = -1 + PAGE_READWRITE = 0x4 + FILE_MAP_WRITE = 0x2 + filemap = ctypes.windll.kernel32.CreateFileMappingW( + INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, + six.text_type(self.name)) + handle_nonzero_success(filemap) + if filemap == INVALID_HANDLE_VALUE: + raise Exception("Failed to create file mapping") + self.filemap = filemap + self.view = memory.MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) + return self + + def seek(self, pos): + self.pos = pos + + def write(self, msg): + assert isinstance(msg, bytes) + n = len(msg) + if self.pos + n >= self.length: # A little safety. + raise ValueError("Refusing to write %d bytes" % n) + dest = self.view + self.pos + length = ctypes.wintypes.SIZE(n) + ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length) + self.pos += n + + def read(self, n): + """ + Read n bytes from mapped view. + """ + out = ctypes.create_string_buffer(n) + source = self.view + self.pos + length = ctypes.wintypes.SIZE(n) + ctypes.windll.kernel32.RtlMoveMemory(out, source, length) + self.pos += n + return out.raw + + def __exit__(self, exc_type, exc_val, tb): + ctypes.windll.kernel32.UnmapViewOfFile(self.view) + ctypes.windll.kernel32.CloseHandle(self.filemap) + +############################# +# jaraco.windows.api.security + +# from WinNT.h +READ_CONTROL = 0x00020000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 +STANDARD_RIGHTS_READ = READ_CONTROL +STANDARD_RIGHTS_WRITE = READ_CONTROL +STANDARD_RIGHTS_EXECUTE = READ_CONTROL +STANDARD_RIGHTS_ALL = 0x001F0000 + +# from NTSecAPI.h +POLICY_VIEW_LOCAL_INFORMATION = 0x00000001 +POLICY_VIEW_AUDIT_INFORMATION = 0x00000002 +POLICY_GET_PRIVATE_INFORMATION = 0x00000004 +POLICY_TRUST_ADMIN = 0x00000008 +POLICY_CREATE_ACCOUNT = 0x00000010 +POLICY_CREATE_SECRET = 0x00000020 +POLICY_CREATE_PRIVILEGE = 0x00000040 +POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080 +POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100 +POLICY_AUDIT_LOG_ADMIN = 0x00000200 +POLICY_SERVER_ADMIN = 0x00000400 +POLICY_LOOKUP_NAMES = 0x00000800 +POLICY_NOTIFICATION = 0x00001000 + +POLICY_ALL_ACCESS = ( + STANDARD_RIGHTS_REQUIRED | + POLICY_VIEW_LOCAL_INFORMATION | + POLICY_VIEW_AUDIT_INFORMATION | + POLICY_GET_PRIVATE_INFORMATION | + POLICY_TRUST_ADMIN | + POLICY_CREATE_ACCOUNT | + POLICY_CREATE_SECRET | + POLICY_CREATE_PRIVILEGE | + POLICY_SET_DEFAULT_QUOTA_LIMITS | + POLICY_SET_AUDIT_REQUIREMENTS | + POLICY_AUDIT_LOG_ADMIN | + POLICY_SERVER_ADMIN | + POLICY_LOOKUP_NAMES) + + +POLICY_READ = ( + STANDARD_RIGHTS_READ | + POLICY_VIEW_AUDIT_INFORMATION | + POLICY_GET_PRIVATE_INFORMATION) + +POLICY_WRITE = ( + STANDARD_RIGHTS_WRITE | + POLICY_TRUST_ADMIN | + POLICY_CREATE_ACCOUNT | + POLICY_CREATE_SECRET | + POLICY_CREATE_PRIVILEGE | + POLICY_SET_DEFAULT_QUOTA_LIMITS | + POLICY_SET_AUDIT_REQUIREMENTS | + POLICY_AUDIT_LOG_ADMIN | + POLICY_SERVER_ADMIN) + +POLICY_EXECUTE = ( + STANDARD_RIGHTS_EXECUTE | + POLICY_VIEW_LOCAL_INFORMATION | + POLICY_LOOKUP_NAMES) -######################### -# jaraco.windows.security +class TokenAccess: + TOKEN_QUERY = 0x8 class TokenInformationClass: - TokenUser = 1 + TokenUser = 1 class TOKEN_USER(ctypes.Structure): - num = 1 - _fields_ = [ - ('SID', ctypes.c_void_p), - ('ATTRIBUTES', ctypes.wintypes.DWORD), - ] + num = 1 + _fields_ = [ + ('SID', ctypes.c_void_p), + ('ATTRIBUTES', ctypes.wintypes.DWORD), + ] class SECURITY_DESCRIPTOR(ctypes.Structure): - """ - typedef struct _SECURITY_DESCRIPTOR - { - UCHAR Revision; - UCHAR Sbz1; - SECURITY_DESCRIPTOR_CONTROL Control; - PSID Owner; - PSID Group; - PACL Sacl; - PACL Dacl; - } SECURITY_DESCRIPTOR; - """ - SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT - REVISION = 1 - - _fields_ = [ - ('Revision', ctypes.c_ubyte), - ('Sbz1', ctypes.c_ubyte), - ('Control', SECURITY_DESCRIPTOR_CONTROL), - ('Owner', ctypes.c_void_p), - ('Group', ctypes.c_void_p), - ('Sacl', ctypes.c_void_p), - ('Dacl', ctypes.c_void_p), - ] + """ + typedef struct _SECURITY_DESCRIPTOR + { + UCHAR Revision; + UCHAR Sbz1; + SECURITY_DESCRIPTOR_CONTROL Control; + PSID Owner; + PSID Group; + PACL Sacl; + PACL Dacl; + } SECURITY_DESCRIPTOR; + """ + SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT + REVISION = 1 + + _fields_ = [ + ('Revision', ctypes.c_ubyte), + ('Sbz1', ctypes.c_ubyte), + ('Control', SECURITY_DESCRIPTOR_CONTROL), + ('Owner', ctypes.c_void_p), + ('Group', ctypes.c_void_p), + ('Sacl', ctypes.c_void_p), + ('Dacl', ctypes.c_void_p), + ] class SECURITY_ATTRIBUTES(ctypes.Structure): - """ - typedef struct _SECURITY_ATTRIBUTES { - DWORD nLength; - LPVOID lpSecurityDescriptor; - BOOL bInheritHandle; - } SECURITY_ATTRIBUTES; - """ - _fields_ = [ - ('nLength', ctypes.wintypes.DWORD), - ('lpSecurityDescriptor', ctypes.c_void_p), - ('bInheritHandle', ctypes.wintypes.BOOL), - ] - - def __init__(self, *args, **kwargs): - super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) - self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) - - def _get_descriptor(self): - return self._descriptor - def _set_descriptor(self, descriptor): - self._descriptor = descriptor - self.lpSecurityDescriptor = ctypes.addressof(descriptor) - descriptor = property(_get_descriptor, _set_descriptor) + """ + typedef struct _SECURITY_ATTRIBUTES { + DWORD nLength; + LPVOID lpSecurityDescriptor; + BOOL bInheritHandle; + } SECURITY_ATTRIBUTES; + """ + _fields_ = [ + ('nLength', ctypes.wintypes.DWORD), + ('lpSecurityDescriptor', ctypes.c_void_p), + ('bInheritHandle', ctypes.wintypes.BOOL), + ] + + def __init__(self, *args, **kwargs): + super(SECURITY_ATTRIBUTES, self).__init__(*args, **kwargs) + self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) + + @property + def descriptor(self): + return self._descriptor + + @descriptor.setter + def descriptor(self, value): + self._descriptor = value + self.lpSecurityDescriptor = ctypes.addressof(value) -def GetTokenInformation(token, information_class): - """ - Given a token, get the token information for it. - """ - data_size = ctypes.wintypes.DWORD() - ctypes.windll.advapi32.GetTokenInformation(token, information_class.num, - 0, 0, ctypes.byref(data_size)) - data = ctypes.create_string_buffer(data_size.value) - handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token, - information_class.num, - ctypes.byref(data), ctypes.sizeof(data), - ctypes.byref(data_size))) - return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents +######################### +# jaraco.windows.security -class TokenAccess: - TOKEN_QUERY = 0x8 +def GetTokenInformation(token, information_class): + """ + Given a token, get the token information for it. + """ + data_size = ctypes.wintypes.DWORD() + ctypes.windll.advapi32.GetTokenInformation(token, information_class.num, + 0, 0, ctypes.byref(data_size)) + data = ctypes.create_string_buffer(data_size.value) + handle_nonzero_success(ctypes.windll.advapi32.GetTokenInformation(token, + information_class.num, + ctypes.byref(data), ctypes.sizeof(data), + ctypes.byref(data_size))) + return ctypes.cast(data, ctypes.POINTER(security.TOKEN_USER)).contents def OpenProcessToken(proc_handle, access): - result = ctypes.wintypes.HANDLE() - proc_handle = ctypes.wintypes.HANDLE(proc_handle) - handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken( - proc_handle, access, ctypes.byref(result))) - return result + result = ctypes.wintypes.HANDLE() + proc_handle = ctypes.wintypes.HANDLE(proc_handle) + handle_nonzero_success(ctypes.windll.advapi32.OpenProcessToken( + proc_handle, access, ctypes.byref(result))) + return result def get_current_user(): - """ - Return a TOKEN_USER for the owner of this process. - """ - process = OpenProcessToken( - ctypes.windll.kernel32.GetCurrentProcess(), - TokenAccess.TOKEN_QUERY, - ) - return GetTokenInformation(process, TOKEN_USER) + """ + Return a TOKEN_USER for the owner of this process. + """ + process = OpenProcessToken( + ctypes.windll.kernel32.GetCurrentProcess(), + security.TokenAccess.TOKEN_QUERY, + ) + return GetTokenInformation(process, security.TOKEN_USER) def get_security_attributes_for_user(user=None): - """ - Return a SECURITY_ATTRIBUTES structure with the SID set to the - specified user (uses current user if none is specified). - """ - if user is None: - user = get_current_user() - - assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" - - SD = SECURITY_DESCRIPTOR() - SA = SECURITY_ATTRIBUTES() - # by attaching the actual security descriptor, it will be garbage- - # collected with the security attributes - SA.descriptor = SD - SA.bInheritHandle = 1 - - ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), - SECURITY_DESCRIPTOR.REVISION) - ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), - user.SID, 0) - return SA + """ + Return a SECURITY_ATTRIBUTES structure with the SID set to the + specified user (uses current user if none is specified). + """ + if user is None: + user = get_current_user() + + assert isinstance(user, security.TOKEN_USER), "user must be TOKEN_USER instance" + + SD = security.SECURITY_DESCRIPTOR() + SA = security.SECURITY_ATTRIBUTES() + # by attaching the actual security descriptor, it will be garbage- + # collected with the security attributes + SA.descriptor = SD + SA.bInheritHandle = 1 + + ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), + security.SECURITY_DESCRIPTOR.REVISION) + ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), + user.SID, 0) + return SA -- cgit v1.2.3 From 9375e451f48b99e0cfeeb28da3c6357732f2bd07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 8 May 2015 10:31:34 -0400 Subject: Replace use of six with imports from py3compat --- paramiko/_winapi.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index 84ee07fd..9ec4d44f 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -9,8 +9,7 @@ in jaraco.windows and asking the author to port the fixes back here. import sys import ctypes.wintypes -import six -from six.moves import builtins +from paramiko.py3compat import u, builtins ###################### @@ -142,7 +141,7 @@ class MemoryMap(object): FILE_MAP_WRITE = 0x2 filemap = ctypes.windll.kernel32.CreateFileMappingW( INVALID_HANDLE_VALUE, p_SA, PAGE_READWRITE, 0, self.length, - six.text_type(self.name)) + u(self.name)) handle_nonzero_success(filemap) if filemap == INVALID_HANDLE_VALUE: raise Exception("Failed to create file mapping") -- cgit v1.2.3 From f2213b8a40c652780c209071574e4c85a8480eb3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 8 May 2015 10:32:32 -0400 Subject: Remove references to other modules --- paramiko/_winapi.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index 9ec4d44f..c67c2421 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -146,7 +146,7 @@ class MemoryMap(object): if filemap == INVALID_HANDLE_VALUE: raise Exception("Failed to create file mapping") self.filemap = filemap - self.view = memory.MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) + self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) return self def seek(self, pos): @@ -322,7 +322,7 @@ def GetTokenInformation(token, information_class): information_class.num, ctypes.byref(data), ctypes.sizeof(data), ctypes.byref(data_size))) - return ctypes.cast(data, ctypes.POINTER(security.TOKEN_USER)).contents + return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents def OpenProcessToken(proc_handle, access): result = ctypes.wintypes.HANDLE() @@ -337,9 +337,9 @@ def get_current_user(): """ process = OpenProcessToken( ctypes.windll.kernel32.GetCurrentProcess(), - security.TokenAccess.TOKEN_QUERY, + TokenAccess.TOKEN_QUERY, ) - return GetTokenInformation(process, security.TOKEN_USER) + return GetTokenInformation(process, TOKEN_USER) def get_security_attributes_for_user(user=None): """ @@ -349,17 +349,17 @@ def get_security_attributes_for_user(user=None): if user is None: user = get_current_user() - assert isinstance(user, security.TOKEN_USER), "user must be TOKEN_USER instance" + assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" - SD = security.SECURITY_DESCRIPTOR() - SA = security.SECURITY_ATTRIBUTES() + SD = SECURITY_DESCRIPTOR() + SA = SECURITY_ATTRIBUTES() # by attaching the actual security descriptor, it will be garbage- # collected with the security attributes SA.descriptor = SD SA.bInheritHandle = 1 ctypes.windll.advapi32.InitializeSecurityDescriptor(ctypes.byref(SD), - security.SECURITY_DESCRIPTOR.REVISION) + SECURITY_DESCRIPTOR.REVISION) ctypes.windll.advapi32.SetSecurityDescriptorOwner(ctypes.byref(SD), user.SID, 0) return SA -- cgit v1.2.3 From e7808e11de87127451f25e5cd45f8f764cbcb590 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 8 May 2015 10:36:57 -0400 Subject: Add builtins to compatibility imports --- paramiko/py3compat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py index 57c096b2..6fafc31d 100644 --- a/paramiko/py3compat.py +++ b/paramiko/py3compat.py @@ -3,7 +3,7 @@ import base64 __all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input', 'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask', - 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next'] + 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next', 'builtins'] PY2 = sys.version_info[0] < 3 @@ -18,6 +18,8 @@ if PY2: decodebytes = base64.decodestring encodebytes = base64.encodestring + import __builtin__ as builtins + def bytestring(s): # NOQA if isinstance(s, unicode): @@ -102,6 +104,7 @@ if PY2: else: import collections import struct + import builtins string_types = str text_type = str bytes = bytes -- cgit v1.2.3 From dd9135b24174ba8b02e0982ce2f9026cd22bef21 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 11 Jun 2015 16:42:07 -0700 Subject: Tweak docstring --- paramiko/ssh_exception.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 2e09c6d6..d053974a 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -140,9 +140,9 @@ class NoValidConnectionsError(socket.error): This exception class wraps multiple "real" underlying connection errors, all of which represent failed connection attempts. Because these errors are not guaranteed to all be of the same error type (i.e. different errno, - class, message, etc) we expose a single unified error message and a - ``None`` errno so that instances of this class match most normal handling - of `socket.error` objects. + `socket.error` subclass, message, etc) we expose a single unified error + message and a ``None`` errno so that instances of this class match most + normal handling of `socket.error` objects. To see the wrapped exception objects, access the ``errors`` attribute. ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', -- cgit v1.2.3 From 7a6e89bcbd75c495205ddf7520c044000c2e6a65 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 17 Aug 2015 21:00:05 -0700 Subject: Add TODO found while poking API from fabric v2 --- paramiko/sftp_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 89840eaa..6d48e692 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -589,6 +589,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager): .. versionadded:: 1.4 """ + # TODO: make class initialize with self._cwd set to self.normalize('.') return self._cwd and u(self._cwd) def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): -- cgit v1.2.3 From 6c5df3650a90ca2567e6f865de237c5fb1fadf5d Mon Sep 17 00:00:00 2001 From: Peter Odding Date: Sat, 5 Sep 2015 17:57:45 +0200 Subject: Try to fix `python setup.py bdist_dumb' on Mac OS X (paylogic/pip-accel#2) Additions based on /usr/lib/python2.7/distutils/archive_util.py from the Ubuntu 12.04 package python2.7 (2.7.3-0ubuntu3.8). I have not looked into the compatibility of the software licenses of Paramiko vs distutils however the original setup_helper.py code in Paramiko was clearly also copied from distutils so to be honest it's not like I'm changing the status quo. --- setup_helper.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/setup_helper.py b/setup_helper.py index ff6b0e16..5d23137b 100644 --- a/setup_helper.py +++ b/setup_helper.py @@ -30,9 +30,42 @@ import distutils.archive_util from distutils.dir_util import mkpath from distutils.spawn import spawn - -def make_tarball(base_name, base_dir, compress='gzip', - verbose=False, dry_run=False): +try: + from pwd import getpwnam +except ImportError: + getpwnam = None + +try: + from grp import getgrnam +except ImportError: + getgrnam = None + +def _get_gid(name): + """Returns a gid, given a group name.""" + if getgrnam is None or name is None: + return None + try: + result = getgrnam(name) + except KeyError: + result = None + if result is not None: + return result[2] + return None + +def _get_uid(name): + """Returns an uid, given a user name.""" + if getpwnam is None or name is None: + return None + try: + result = getpwnam(name) + except KeyError: + result = None + if result is not None: + return result[2] + return None + +def make_tarball(base_name, base_dir, compress='gzip', verbose=0, dry_run=0, + owner=None, group=None): """Create a tar file from all the files under 'base_dir'. This file may be compressed. @@ -75,11 +108,25 @@ def make_tarball(base_name, base_dir, compress='gzip', mkpath(os.path.dirname(archive_name), dry_run=dry_run) log.info('Creating tar file %s with mode %s' % (archive_name, mode)) + uid = _get_uid(owner) + gid = _get_gid(group) + + def _set_uid_gid(tarinfo): + if gid is not None: + tarinfo.gid = gid + tarinfo.gname = group + if uid is not None: + tarinfo.uid = uid + tarinfo.uname = owner + return tarinfo + if not dry_run: tar = tarfile.open(archive_name, mode=mode) # This recursively adds everything underneath base_dir - tar.add(base_dir) - tar.close() + try: + tar.add(base_dir, filter=_set_uid_gid) + finally: + tar.close() if compress and compress not in tarfile_compress_flag: spawn([compress] + compress_flags[compress] + [archive_name], -- cgit v1.2.3 From a7c8266fe784dffa2a5fdd1526437c6ba7ba1aab Mon Sep 17 00:00:00 2001 From: Peter Odding Date: Wed, 9 Sep 2015 22:18:24 +0200 Subject: Restore Python 2.6 compatibility for `python setup.py {s,b}dist' --- setup_helper.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup_helper.py b/setup_helper.py index 5d23137b..9e3834b3 100644 --- a/setup_helper.py +++ b/setup_helper.py @@ -124,7 +124,12 @@ def make_tarball(base_name, base_dir, compress='gzip', verbose=0, dry_run=0, tar = tarfile.open(archive_name, mode=mode) # This recursively adds everything underneath base_dir try: - tar.add(base_dir, filter=_set_uid_gid) + try: + # Support for the `filter' parameter was added in Python 2.7, + # earlier versions will raise TypeError. + tar.add(base_dir, filter=_set_uid_gid) + except TypeError: + tar.add(base_dir) finally: tar.close() -- cgit v1.2.3 From 97e134aa43c9632f34be278ca1d08f56cc83993a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 10 Sep 2015 14:09:13 -0700 Subject: Changelog fixes #582 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 0e8f92c4..6379dba9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`582` Fix some old ``setup.py`` related helper code which was + breaking ``bdist_dumb`` on Mac OS X. Thanks to Peter Odding for the patch. * :bug:`22 major` Try harder to connect to multiple network families (e.g. IPv4 vs IPv6) in case of connection issues; this helps with problems such as hosts which resolve both IPv4 and IPv6 addresses but are only listening on IPv4. -- cgit v1.2.3 From 7b33770b4786af2508fab11cebe934584fe19ca6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 21 Sep 2015 17:19:40 -0700 Subject: gratipay no more :( --- sites/shared_conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sites/shared_conf.py b/sites/shared_conf.py index 4a6a5c4e..99fab315 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -12,7 +12,6 @@ html_theme_options = { 'description': "A Python implementation of SSHv2.", 'github_user': 'paramiko', 'github_repo': 'paramiko', - 'gratipay_user': 'bitprophet', 'analytics_id': 'UA-18486793-2', 'travis_button': True, } -- cgit v1.2.3 From aef405c9adc3ca087b21836d4a2ee56e05a2b3c4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:02:27 -0700 Subject: Changelog closes #353 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6520dde4..be3f5da7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`353` (via :issue:`482`) Fix a bug introduced in the Python 3 port + which caused ``OverFlowError`` (and other symptoms) in SFTP functionality. + Thanks to ``@dboreham`` for leading the troubleshooting charge, and to + Scott Maxwell for the final patch. * :bug:`402` Check to see if an SSH agent is actually present before trying to forward it to the remote end. This replaces what was usually a useless ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken -- cgit v1.2.3 From a436b9f1087752b60bc31d73306bdac1229d1118 Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Tue, 17 Feb 2015 13:47:03 -0500 Subject: Typo causes failure if Putty Pageant is running. --- paramiko/_winapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py index d6aabf76..cf4d68e5 100644 --- a/paramiko/_winapi.py +++ b/paramiko/_winapi.py @@ -219,8 +219,8 @@ class SECURITY_ATTRIBUTES(ctypes.Structure): @descriptor.setter def descriptor(self, value): - self._descriptor = descriptor - self.lpSecurityDescriptor = ctypes.addressof(descriptor) + self._descriptor = value + self.lpSecurityDescriptor = ctypes.addressof(value) def GetTokenInformation(token, information_class): """ -- cgit v1.2.3 From e9d65f4199bb6a8589c9a89f8a8d68edd66ac6d0 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:09:15 -0700 Subject: Changelog closes #488 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index be3f5da7..7e8c02fe 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`469` (also :issue:`488`, :issue:`461` and like a dozen others) Fix a + typo introduced in the 1.15 release which broke WinPageant support. Thanks to + everyone who submitted patches, and to Steve Cohen who was the lucky winner + of the cherry-pick lottery. * :bug:`353` (via :issue:`482`) Fix a bug introduced in the Python 3 port which caused ``OverFlowError`` (and other symptoms) in SFTP functionality. Thanks to ``@dboreham`` for leading the troubleshooting charge, and to -- cgit v1.2.3 From 3bc8fd1a02358a8647bcee11e652ee5aec0bdf97 Mon Sep 17 00:00:00 2001 From: Loic Dachary Date: Thu, 25 Sep 2014 21:32:19 +0200 Subject: Add informative BadHostKeyException Displaying the keys being compared makes it easy to diagnose the problem. Otherwise there is more guessing involved. Signed-off-by: Loic Dachary --- paramiko/ssh_exception.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index b99e42b3..e120a45e 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -105,7 +105,11 @@ class BadHostKeyException (SSHException): .. versionadded:: 1.6 """ def __init__(self, hostname, got_key, expected_key): - SSHException.__init__(self, 'Host key for server %s does not match!' % hostname) + SSHException.__init__(self, + 'Host key for server %s does not match : got %s expected %s' % ( + hostname, + got_key.get_base64(), + expected_key.get_base64())) self.hostname = hostname self.key = got_key self.expected_key = expected_key -- cgit v1.2.3 From 48dc72b87567152ac8d45b4bad2bdd0d4ad3ac8b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:14:27 -0700 Subject: Changelog closes #404 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 7e8c02fe..3c11ff87 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`404` Print details when displaying `BadHostKeyException` objects + (expected vs received data) instead of just "hey shit broke". Patch credit: + Loic Dachary. * :bug:`469` (also :issue:`488`, :issue:`461` and like a dozen others) Fix a typo introduced in the 1.15 release which broke WinPageant support. Thanks to everyone who submitted patches, and to Steve Cohen who was the lucky winner -- cgit v1.2.3 From 669ecbd66f1218e5c93f6a53a25ea062c7b0b947 Mon Sep 17 00:00:00 2001 From: Martin Topholm Date: Mon, 2 Mar 2015 07:17:50 +0100 Subject: Silently ignore invalid keys in HostKeys.load() When broken entries exists in known_hosts, paramiko raises SSHException with "Invalid key". This patch catches the exception during HostKeys.load() and continues to next line. This should fix #490. --- paramiko/hostkeys.py | 6 +++++- tests/test_hostkeys.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 84868875..7e2d22cf 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -19,6 +19,7 @@ import binascii import os +import ssh_exception from hashlib import sha1 from hmac import HMAC @@ -96,7 +97,10 @@ class HostKeys (MutableMapping): line = line.strip() if (len(line) == 0) or (line[0] == '#'): continue - e = HostKeyEntry.from_line(line, lineno) + try: + e = HostKeyEntry.from_line(line, lineno) + except ssh_exception.SSHException: + continue if e is not None: _hostnames = e.hostnames for h in _hostnames: diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py index 0ee1bbf0..2bdcad9c 100644 --- a/tests/test_hostkeys.py +++ b/tests/test_hostkeys.py @@ -31,6 +31,7 @@ test_hosts_file = """\ secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\ 9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\ D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc= +broken.example.com ssh-rsa AAAA happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ 5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M= -- cgit v1.2.3 From fb258f88b4b61627a51f30f9a21fcbc7ec35c1e6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:18:24 -0700 Subject: Changelog closes #490, closes #500 (cherry-pick) --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 3c11ff87..5f6a16f9 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`490` Skip invalid/unparseable lines in ``known_hosts`` files, instead + of raising `SSHException`. This brings Paramiko's behavior more in line with + OpenSSH, which silently ignores such input. Catch & patch courtesy of Martin + Topholm. * :bug:`404` Print details when displaying `BadHostKeyException` objects (expected vs received data) instead of just "hey shit broke". Patch credit: Loic Dachary. -- cgit v1.2.3 From 0e9b78539da392170dd09d629bcfaef9cf724bc1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:32:05 -0700 Subject: No idea why this only tanked tests on Python 3, ugh @ our old crusty test suite --- paramiko/hostkeys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 7e2d22cf..c7e1f72e 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -19,7 +19,6 @@ import binascii import os -import ssh_exception from hashlib import sha1 from hmac import HMAC @@ -36,6 +35,7 @@ from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey from paramiko.util import get_logger, constant_time_bytes_eq from paramiko.ecdsakey import ECDSAKey +from paramiko.ssh_exception import SSHException class HostKeys (MutableMapping): @@ -99,7 +99,7 @@ class HostKeys (MutableMapping): continue try: e = HostKeyEntry.from_line(line, lineno) - except ssh_exception.SSHException: + except SSHException: continue if e is not None: _hostnames = e.hostnames -- cgit v1.2.3 From e287d6b70c5de5470393210623a46741f73a526d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:41:26 -0700 Subject: Don't use --coverage for tests under 3.2 anymore --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a55dbb6..45fb1722 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,8 @@ install: - pip install coveralls # For coveralls.io specifically - pip install -r dev-requirements.txt script: - # Main tests, with coverage! - - inv test --coverage + # Main tests, w/ coverage! (but skip coverage on 3.2, coverage.py dropped it) + - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && inv test --coverage || inv test" # Ensure documentation & invoke pipeline run OK. # Run 'docs' first since its objects.inv is referred to by 'www'. # Also force warnings to be errors since most of them tend to be actual -- cgit v1.2.3 From 57106d04def84ca1d9dd23c4d85b2ba9242556ff Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 14:53:02 -0700 Subject: Rework changelog entries re #491 a bit Closes #491, closes #62, closes #439 --- sites/www/changelog.rst | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 97b6fe9c..764c8801 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,14 +2,10 @@ Changelog ========= -* :bug:`439` Resolve the timeout issue on lost conection. - When the destination disappears on an established session paramiko will hang on trying to open a channel. - Credit to ``@vazir`` for patch. -* :bug:`62` Add timeout for handshake completion. - This adds a mechanism for timing out a connection if the ssh handshake - never completes. - Credit to ``@dacut`` for initial report and patch and to Olle Lundberg for - re-implementation. +* :bug:`491` (combines :issue:`62` and :issue:`439`) Implement timeout + functionality to address hangs from dropped network connections and/or failed + handshakes. Credit to ``@vazir`` and ``@dacut`` for the original patches and + to Olle Lundberg for reimplementation. * :bug:`490` Skip invalid/unparseable lines in ``known_hosts`` files, instead of raising `SSHException`. This brings Paramiko's behavior more in line with OpenSSH, which silently ignores such input. Catch & patch courtesy of Martin -- cgit v1.2.3 From 8bf03014128b074bf6988100f18e48a94671cca2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 15:43:59 -0700 Subject: Changelog re #496 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 764c8801..5b900c61 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`496` Fix a handful of small but critical bugs in Paramiko's GSSAPI + support (note: this includes switching from PyCrypo's Random to + `os.urandom`). Thanks to Anselm Kruis for catch & patch. * :bug:`491` (combines :issue:`62` and :issue:`439`) Implement timeout functionality to address hangs from dropped network connections and/or failed handshakes. Credit to ``@vazir`` and ``@dacut`` for the original patches and -- cgit v1.2.3 From 93694bb9ed4e28673586fd18ffce0fc0e925df1f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 15:57:59 -0700 Subject: Add docstring to AgentRequestHandler so it shows up in the docs. --- paramiko/agent.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/paramiko/agent.py b/paramiko/agent.py index f928881e..6a8e7fb4 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -290,6 +290,26 @@ class AgentServerProxy(AgentSSH): class AgentRequestHandler(object): + """ + Primary/default implementation of SSH agent forwarding functionality. + + Simply instantiate this class, handing it a live command-executing session + object, and it will handle forwarding any local SSH agent processes it + finds. + + For example:: + + # Connect + client = SSHClient() + client.connect(host, port, username) + # Obtain session + session = client.get_transport().open_session() + # Forward local agent + AgentRequestHandler(session) + # Commands executed after this point will see the forwarded agent on + # the remote end. + session.exec_command("git clone https://my.git.repository/") + """ def __init__(self, chanClient): self._conn = None self.__chanC = chanClient -- cgit v1.2.3 From b67ee80ba6cbb985a537123a0ae099b81ddfc999 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 30 Sep 2015 15:59:04 -0700 Subject: Changelog closes #516 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5b900c61..b7f19d63 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`516 backported` Document `~paramiko.agent.AgentRequestHandler`. + Thanks to ``@toejough`` for report & suggestions. * :bug:`496` Fix a handful of small but critical bugs in Paramiko's GSSAPI support (note: this includes switching from PyCrypo's Random to `os.urandom`). Thanks to Anselm Kruis for catch & patch. -- cgit v1.2.3 From 150960800dd5cf260a9932df53847d2d029c3656 Mon Sep 17 00:00:00 2001 From: Jared Hance Date: Sun, 5 Jul 2015 10:18:34 -0700 Subject: Fix ECDSA generate documentation. Was a blatant copy of a comment from RSAKey. --- paramiko/ecdsakey.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py index 6b047959..8827a1db 100644 --- a/paramiko/ecdsakey.py +++ b/paramiko/ecdsakey.py @@ -129,13 +129,11 @@ class ECDSAKey (PKey): @staticmethod def generate(curve=curves.NIST256p, progress_func=None): """ - Generate a new private RSA key. This factory function can be used to + Generate a new private ECDSA key. This factory function can be used to generate a new host key or authentication key. - :param function progress_func: - an optional function to call at key points in key generation (used - by ``pyCrypto.PublicKey``). - :returns: A new private key (`.RSAKey`) object + :param function progress_func: Not used for this type of key. + :returns: A new private key (`.ECDSAKey`) object """ signing_key = SigningKey.generate(curve) key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) -- cgit v1.2.3 From a8ac9e6441030f2cc49de579c3d598e5f05ca331 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 2 Oct 2015 15:23:16 -0700 Subject: Changelog closes #554 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index b7f19d63..1d3debb7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`554 backported` Fix inaccuracies in the docstring for the ECDSA key + class. Thanks to Jared Hance for the patch. * :support:`516 backported` Document `~paramiko.agent.AgentRequestHandler`. Thanks to ``@toejough`` for report & suggestions. * :bug:`496` Fix a handful of small but critical bugs in Paramiko's GSSAPI -- cgit v1.2.3 From 9a5fbad601d7567cde59071f36ba6a34d6bcf696 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 2 Oct 2015 15:56:28 -0700 Subject: Fix some typos/bad doc references in changelog --- sites/www/changelog.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 1d3debb7..9a4e6c76 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -14,12 +14,12 @@ Changelog handshakes. Credit to ``@vazir`` and ``@dacut`` for the original patches and to Olle Lundberg for reimplementation. * :bug:`490` Skip invalid/unparseable lines in ``known_hosts`` files, instead - of raising `SSHException`. This brings Paramiko's behavior more in line with - OpenSSH, which silently ignores such input. Catch & patch courtesy of Martin - Topholm. -* :bug:`404` Print details when displaying `BadHostKeyException` objects - (expected vs received data) instead of just "hey shit broke". Patch credit: - Loic Dachary. + of raising `~paramiko.ssh_exception.SSHException`. This brings Paramiko's + behavior more in line with OpenSSH, which silently ignores such input. Catch + & patch courtesy of Martin Topholm. +* :bug:`404` Print details when displaying + `~paramiko.ssh_exception.BadHostKeyException` objects (expected vs received + data) instead of just "hey shit broke". Patch credit: Loic Dachary. * :bug:`469` (also :issue:`488`, :issue:`461` and like a dozen others) Fix a typo introduced in the 1.15 release which broke WinPageant support. Thanks to everyone who submitted patches, and to Steve Cohen who was the lucky winner @@ -30,8 +30,9 @@ Changelog Scott Maxwell for the final patch. * :bug:`402` Check to see if an SSH agent is actually present before trying to forward it to the remote end. This replaces what was usually a useless - ``TypeError`` with a human-readable ``AuthenticationError``. Credit to Ken - Jordan for the fix and Yvan Marques for original report. + ``TypeError`` with a human-readable + `~paramiko.ssh_exception.AuthenticationException`. Credit to Ken Jordan for + the fix and Yvan Marques for original report. * :release:`1.15.2 <2014-12-19>` * :release:`1.14.2 <2014-12-19>` * :release:`1.13.3 <2014-12-19>` -- cgit v1.2.3 From 5b1b13c2fb48ac55d64022212bf132b8c01ce0c7 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 2 Oct 2015 15:59:15 -0700 Subject: Cut 1.15.3 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 3bf9dac7..25aac14f 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 2) +__version_info__ = (1, 15, 3) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9a4e6c76..d94d5bc2 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.15.3 <2015-10-02>` * :support:`554 backported` Fix inaccuracies in the docstring for the ECDSA key class. Thanks to Jared Hance for the patch. * :support:`516 backported` Document `~paramiko.agent.AgentRequestHandler`. -- cgit v1.2.3 From 985df357ec039bcb28f6f260e64e9838204506f6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 2 Oct 2015 16:20:27 -0700 Subject: Fix dumb bug in release task --- tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 3d575670..7c920daf 100644 --- a/tasks.py +++ b/tasks.py @@ -30,7 +30,8 @@ def release(ctx): # Move the built docs into where Epydocs used to live target = 'docs' rmtree(target, ignore_errors=True) - copytree(docs_build, target) + # TODO: make it easier to yank out this config val from the docs coll + copytree('sites/docs/_build', target) # Publish publish(ctx) # Remind -- cgit v1.2.3 From 53e91cc449ff3070cd57af2ed317a17a47d378e1 Mon Sep 17 00:00:00 2001 From: gshaanan Date: Thu, 15 Oct 2015 13:51:12 +0300 Subject: This adds SHA-256 support based on a fork of 'zamiam69:add_sha2_support'. This commit fixes the problem with the fork where you can't connect using SHA1 anymore. The fix changes the '_preferred_macs' and '_preferred_kex' order. --- paramiko/transport.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 9009f9f5..5ab5ac95 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -96,13 +96,13 @@ class Transport (threading.Thread, ClosingContextManager): _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', - 'hmac-sha2-256') + _preferred_macs = ('hmac-sha2-256', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96', + 'hmac-sha1') _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ('diffie-hellman-group-exchange-sha256', + _preferred_kex = ('diffie-hellman-group1-sha1', 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1' , - 'diffie-hellman-group1-sha1') + 'diffie-hellman-group-exchange-sha1' , + 'diffie-hellman-group-exchange-sha256') _preferred_compression = ('none',) _cipher_info = { -- cgit v1.2.3 From ae1cb8fe2cf39fea0ee441dac037931c5e4bc23e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 30 Oct 2015 11:43:43 -0700 Subject: Whitespace trims --- paramiko/transport.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index ef57108a..e0b7c296 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -100,8 +100,8 @@ class Transport (threading.Thread, ClosingContextManager): 'hmac-sha1') _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') _preferred_kex = ('diffie-hellman-group1-sha1', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1' , + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group-exchange-sha1', 'diffie-hellman-group-exchange-sha256') _preferred_compression = ('none',) @@ -1772,7 +1772,7 @@ class Transport (threading.Thread, ClosingContextManager): kex_mp = [k for k in self._preferred_kex if k.startswith(mp_required_prefix)] if (self._modulus_pack is None) and (len(kex_mp) > 0): # can't do group-exchange if we don't have a pack of potential primes - pkex = [k for k in self.get_security_options().kex + pkex = [k for k in self.get_security_options().kex if not k.startswith(mp_required_prefix)] self.get_security_options().kex = pkex available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, -- cgit v1.2.3 From 439876b38cb8caf38ed957bdf0caeeeedfac740d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 30 Oct 2015 11:45:57 -0700 Subject: Explicitly log agreed-upon MAC --- paramiko/transport.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paramiko/transport.py b/paramiko/transport.py index e0b7c296..5d6ffa67 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1873,6 +1873,7 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable macs)') self.local_mac = agreed_local_macs[0] self.remote_mac = agreed_remote_macs[0] + self._log(DEBUG, 'MACs agreed: local=%s, remote=%s' % (self.local_mac, self.remote_mac)) if self.server_mode: agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list)) -- cgit v1.2.3 From c06927016f0b3e51ba725b57e6f20f0f5c74dbc7 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 30 Oct 2015 12:21:59 -0700 Subject: Formatting --- paramiko/packet.py | 3 ++- paramiko/transport.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/paramiko/packet.py b/paramiko/packet.py index b922000c..2be2bb2b 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -389,7 +389,8 @@ class Packetizer (object): if self.__dump_packets: self._log(DEBUG, util.format_binary(header, 'IN: ')) packet_size = struct.unpack('>I', header[:4])[0] - # leftover contains decrypted bytes from the first block (after the length field) + # leftover contains decrypted bytes from the first block (after the + # length field) leftover = header[4:] if (packet_size - len(leftover)) % self.__block_size_in != 0: raise SSHException('Invalid packet blocking') diff --git a/paramiko/transport.py b/paramiko/transport.py index 5d6ffa67..7d0857c8 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1599,9 +1599,11 @@ class Transport (threading.Thread, ClosingContextManager): try: self.packetizer.write_all(b(self.local_version + '\r\n')) self._check_banner() - # The above is actually very much part of the handshake, but sometimes the banner can be read - # but the machine is not responding, for example when the remote ssh daemon is loaded in to memory - # but we can not read from the disk/spawn a new shell. + # The above is actually very much part of the handshake, but + # sometimes the banner can be read but the machine is not + # responding, for example when the remote ssh daemon is loaded + # in to memory but we can not read from the disk/spawn a new + # shell. # Make sure we can specify a timeout for the initial handshake. # Re-use the banner timeout for now. self.packetizer.start_handshake(self.handshake_timeout) @@ -1909,8 +1911,8 @@ class Transport (threading.Thread, ClosingContextManager): engine = self._get_cipher(self.remote_cipher, key_in, IV_in) mac_size = self._mac_info[self.remote_mac]['size'] mac_engine = self._mac_info[self.remote_mac]['class'] - # initial mac keys are done in the hash's natural size (not the potentially truncated - # transmission size) + # initial mac keys are done in the hash's natural size (not the + # potentially truncated transmission size) if self.server_mode: mac_key = self._compute_key('E', mac_engine().digest_size) else: @@ -1936,8 +1938,8 @@ class Transport (threading.Thread, ClosingContextManager): engine = self._get_cipher(self.local_cipher, key_out, IV_out) mac_size = self._mac_info[self.local_mac]['size'] mac_engine = self._mac_info[self.local_mac]['class'] - # initial mac keys are done in the hash's natural size (not the potentially truncated - # transmission size) + # initial mac keys are done in the hash's natural size (not the + # potentially truncated transmission size) if self.server_mode: mac_key = self._compute_key('F', mac_engine().digest_size) else: -- cgit v1.2.3 From 013ec610643f56f737fe7d1d4ac0f33f09bb3f4a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 11:51:41 -0800 Subject: Log identificatin-string exchange at DEBUG --- paramiko/transport.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paramiko/transport.py b/paramiko/transport.py index 7d0857c8..2f90cdfa 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1598,6 +1598,7 @@ class Transport (threading.Thread, ClosingContextManager): try: try: self.packetizer.write_all(b(self.local_version + '\r\n')) + self._log(DEBUG, 'Local version/idstring: %s' % self.local_version) self._check_banner() # The above is actually very much part of the handshake, but # sometimes the banner can be read but the machine is not @@ -1742,6 +1743,7 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Indecipherable protocol version "' + buf + '"') # save this server version string for later self.remote_version = buf + self._log(DEBUG, 'Remote version/idstring: %s' % buf) # pull off any attached comment comment = '' i = buf.find(' ') -- cgit v1.2.3 From 380aaa3631a0b2a197987d0c5c6163be418e128d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 13:25:58 -0800 Subject: Reformat algorithm tuples for readability, & add ordering comment --- paramiko/transport.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 2f90cdfa..e2a0c0ff 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -94,15 +94,36 @@ class Transport (threading.Thread, ClosingContextManager): _PROTO_ID = '2.0' _CLIENT_ID = 'paramiko_%s' % paramiko.__version__ - _preferred_ciphers = ('aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', - 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256') - _preferred_macs = ('hmac-sha2-256', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96', - 'hmac-sha1') - _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') - _preferred_kex = ('diffie-hellman-group1-sha1', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', - 'diffie-hellman-group-exchange-sha256') + # These tuples of algorithm identifiers are in preference order; do not + # reorder without reason! + _preferred_ciphers = ( + 'aes128-ctr', + 'aes256-ctr', + 'aes128-cbc', + 'blowfish-cbc', + 'aes256-cbc', + '3des-cbc', + 'arcfour128', + 'arcfour256', + ) + _preferred_macs = ( + 'hmac-sha2-256', + 'hmac-md5', + 'hmac-sha1-96', + 'hmac-md5-96', + 'hmac-sha1', + ) + _preferred_keys = ( + 'ssh-rsa', + 'ssh-dss', + 'ecdsa-sha2-nistp256', + ) + _preferred_kex = ( + 'diffie-hellman-group1-sha1', + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group-exchange-sha256', + ) _preferred_compression = ('none',) _cipher_info = { -- cgit v1.2.3 From 347f948bcfc5db180a49c7d06c1b67f24b75ad21 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 14:37:29 -0800 Subject: Fix incorrect hash algorithm selection during kex key generation. Re #356 --- paramiko/kex_group1.py | 1 + paramiko/kex_group14.py | 2 ++ paramiko/transport.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index a88f00d2..9eee066c 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -45,6 +45,7 @@ class KexGroup1(object): G = 2 name = 'diffie-hellman-group1-sha1' + hash_algo = sha1 def __init__(self, transport): self.transport = transport diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py index a914aeaf..9f7dd216 100644 --- a/paramiko/kex_group14.py +++ b/paramiko/kex_group14.py @@ -22,6 +22,7 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of """ from paramiko.kex_group1 import KexGroup1 +from hashlib import sha1 class KexGroup14(KexGroup1): @@ -31,3 +32,4 @@ class KexGroup14(KexGroup1): G = 2 name = 'diffie-hellman-group14-sha1' + hash_algo = sha1 diff --git a/paramiko/transport.py b/paramiko/transport.py index e2a0c0ff..d2fccf07 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1534,7 +1534,7 @@ class Transport (threading.Thread, ClosingContextManager): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - hash_algo = self._mac_info[self.local_mac]['class'] if self.local_mac else sha1 + hash_algo = self.kex_engine.hash_algo out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() -- cgit v1.2.3 From 9b745412a9b36dffe3ba5ad7304e202a500e77fb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 15:09:12 -0800 Subject: Allow specifying test.py flags in 'inv test' --- tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index 7c920daf..05654d3b 100644 --- a/tasks.py +++ b/tasks.py @@ -9,11 +9,12 @@ from invocations.packaging import publish # Until we move to spec-based testing @task -def test(ctx, coverage=False): +def test(ctx, coverage=False, flags=""): + if "--verbose" not in flags.split(): + flags += " --verbose" runner = "python" if coverage: runner = "coverage run --source=paramiko" - flags = "--verbose" ctx.run("{0} test.py {1}".format(runner, flags), pty=True) -- cgit v1.2.3 From aeb28073e95557122d7325f7addac9eca7b07e7f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 15:48:44 -0800 Subject: Fallback to sha1 in _compute_key --- paramiko/transport.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index d2fccf07..98b810ee 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1534,7 +1534,9 @@ class Transport (threading.Thread, ClosingContextManager): m.add_bytes(self.H) m.add_byte(b(id)) m.add_bytes(self.session_id) - hash_algo = self.kex_engine.hash_algo + # Fallback to SHA1 for kex engines that fail to specify a hex + # algorithm, or for e.g. transport tests that don't run kexinit. + hash_algo = getattr(self.kex_engine, 'hex_algo', sha1) out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() -- cgit v1.2.3 From 66ff4deabbd1c14df3fd2d8729107d904c30c7d5 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 16:04:58 -0800 Subject: Changelog closes #356, closes #596. Will expand to include SHA512 stuff if I merge that prior to release. --- sites/www/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index ff05365c..833560af 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,15 @@ Changelog ========= +* :feature:`356` (also :issue:`596`, :issue:`365`, :issue:`341`, :issue:`164`, + and a bunch of other duplicates besides) Add support for 256-bit SHA-2 based + key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256`` and + (H)MAC algorithm ``hmac-sha2-256``. + + Thanks to the many people who submitted patches for this functionality and/or + assisted in testing those patches. That list includes but is not limited to, + and in no particular order: Matthias Witte, Dag Wieers, Ash Berlin, Etienne + Perot, Gert van Dijk, ``@GuyShaanan``, Aaron Bieber, and ``@cyphase``. * :release:`1.15.3 <2015-10-02>` * :support:`554 backported` Fix inaccuracies in the docstring for the ECDSA key class. Thanks to Jared Hance for the patch. -- cgit v1.2.3 From e937e37d2559064d3f746e6b969546f44b858e52 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 16:35:41 -0800 Subject: What a dumb-as-rocks typo --- paramiko/transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 98b810ee..773a8769 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1536,7 +1536,7 @@ class Transport (threading.Thread, ClosingContextManager): m.add_bytes(self.session_id) # Fallback to SHA1 for kex engines that fail to specify a hex # algorithm, or for e.g. transport tests that don't run kexinit. - hash_algo = getattr(self.kex_engine, 'hex_algo', sha1) + hash_algo = getattr(self.kex_engine, 'hash_algo', None) out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() -- cgit v1.2.3 From aeff78b28c50844cd15d4774e74e912bcae6f2ca Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 16:35:55 -0800 Subject: Log one DEBUG message per key-compute hash algo selection --- paramiko/transport.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/paramiko/transport.py b/paramiko/transport.py index 773a8769..bca4d37e 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1537,6 +1537,13 @@ class Transport (threading.Thread, ClosingContextManager): # Fallback to SHA1 for kex engines that fail to specify a hex # algorithm, or for e.g. transport tests that don't run kexinit. hash_algo = getattr(self.kex_engine, 'hash_algo', None) + hash_select_msg = "kex engine %s specified hash_algo %r" % (self.kex_engine.__class__.__name__, hash_algo) + if hash_algo is None: + hash_algo = sha1 + hash_select_msg += ", falling back to sha1" + if not hasattr(self, '_logged_hash_selection'): + self._log(DEBUG, hash_select_msg) + setattr(self, '_logged_hash_selection', True) out = sofar = hash_algo(m.asbytes()).digest() while len(out) < nbytes: m = Message() -- cgit v1.2.3 From 445d739757c927e0bde8cb4156122e90ac1d486a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 18:16:50 -0800 Subject: Formatting --- paramiko/transport.py | 72 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index bca4d37e..5f170ffd 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -127,14 +127,54 @@ class Transport (threading.Thread, ClosingContextManager): _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}, + '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 = { @@ -1859,12 +1899,20 @@ class Transport (threading.Thread, ClosingContextManager): ' server lang:' + str(server_lang_list) + ' kex follows?' + str(kex_follows)) - # 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. + # 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 = list(filter(self._preferred_kex.__contains__, kex_algo_list)) + agreed_kex = list(filter( + self._preferred_kex.__contains__, + kex_algo_list + )) else: - agreed_kex = list(filter(kex_algo_list.__contains__, self._preferred_kex)) + agreed_kex = list(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) -- cgit v1.2.3 From 375ab8b8475081dfdd8c92ae3a6953d8dbbe6297 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 18:17:20 -0800 Subject: Implement SHA-2 512bit HMAC support from #581 --- paramiko/transport.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 5f170ffd..b8ad8424 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -26,7 +26,7 @@ import sys import threading import time import weakref -from hashlib import md5, sha1, sha256 +from hashlib import md5, sha1, sha256, sha512 import paramiko from paramiko import util @@ -108,6 +108,7 @@ class Transport (threading.Thread, ClosingContextManager): ) _preferred_macs = ( 'hmac-sha2-256', + 'hmac-sha2-512', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96', @@ -181,6 +182,7 @@ class Transport (threading.Thread, ClosingContextManager): 'hmac-sha1': {'class': sha1, 'size': 20}, 'hmac-sha1-96': {'class': sha1, 'size': 12}, 'hmac-sha2-256': {'class': sha256, 'size': 32}, + 'hmac-sha2-512': {'class': sha512, 'size': 64}, 'hmac-md5': {'class': md5, 'size': 16}, 'hmac-md5-96': {'class': md5, 'size': 12}, } -- cgit v1.2.3 From 5a89ea28105ea7e6caad861e64b8aa4f2ffc7394 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 18:19:16 -0800 Subject: Update changelog closing #581 --- sites/www/changelog.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 833560af..f1e33bcf 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -3,14 +3,15 @@ Changelog ========= * :feature:`356` (also :issue:`596`, :issue:`365`, :issue:`341`, :issue:`164`, - and a bunch of other duplicates besides) Add support for 256-bit SHA-2 based - key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256`` and - (H)MAC algorithm ``hmac-sha2-256``. + :issue:`581`, and a bunch of other duplicates besides) Add support for SHA-2 + based key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256`` + and (H)MAC algorithms ``hmac-sha2-256`` and ``hmac-sha2-512``. Thanks to the many people who submitted patches for this functionality and/or assisted in testing those patches. That list includes but is not limited to, and in no particular order: Matthias Witte, Dag Wieers, Ash Berlin, Etienne - Perot, Gert van Dijk, ``@GuyShaanan``, Aaron Bieber, and ``@cyphase``. + Perot, Gert van Dijk, ``@GuyShaanan``, Aaron Bieber, ``@cyphase``, and Eric + Brown. * :release:`1.15.3 <2015-10-02>` * :support:`554 backported` Fix inaccuracies in the docstring for the ECDSA key class. Thanks to Jared Hance for the patch. -- cgit v1.2.3 From bd58eb6ab221a52e2a8c76aa1f24eac5a618f4d7 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 18:20:23 -0800 Subject: Explicitly log kex and compression algorithms --- paramiko/transport.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paramiko/transport.py b/paramiko/transport.py index b8ad8424..1d3b7c78 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1918,6 +1918,7 @@ class Transport (threading.Thread, ClosingContextManager): if len(agreed_kex) == 0: raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)') self.kex_engine = self._kex_info[agreed_kex[0]](self) + self._log(DEBUG, "Kex agreed: %s" % agreed_kex[0]) if self.server_mode: available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__, @@ -1969,6 +1970,7 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression)) self.local_compression = agreed_local_compression[0] self.remote_compression = agreed_remote_compression[0] + self._log(DEBUG, 'Compressions agreed: local=%s' % self.local_compression) self._log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' % (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac, -- cgit v1.2.3 From e2bce29615c7418b51995aa008bc8c79849971b0 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 18:21:34 -0800 Subject: Remove aggregated algorithms log line. All constituent parts now have their own log lines which are printed closer to when the handshakes occur. More accurate and more readable. --- paramiko/transport.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 1d3b7c78..324ed277 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1972,10 +1972,6 @@ class Transport (threading.Thread, ClosingContextManager): self.remote_compression = agreed_remote_compression[0] self._log(DEBUG, 'Compressions agreed: local=%s' % self.local_compression) - self._log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' % - (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac, - self.remote_mac, self.local_compression, self.remote_compression)) - # save for computing hash later... # now wait! openssh has a bug (and others might too) where there are # actually some extra bytes (one NUL byte in openssh's case) added to -- cgit v1.2.3 From 9c12f12a08f25a2135e0f17832d2acdc8bafbf1b Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 18:24:37 -0800 Subject: Add note re: logging tweaks to changelog. Better safe than sorry. --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index f1e33bcf..3aa2b84b 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -6,6 +6,10 @@ Changelog :issue:`581`, and a bunch of other duplicates besides) Add support for SHA-2 based key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256`` and (H)MAC algorithms ``hmac-sha2-256`` and ``hmac-sha2-512``. + + This change includes tweaks to debug-level logging regarding + algorithm-selection handshakes; the old all-in-one log line is now multiple + easier-to-read, printed-at-handshake-time log lines. Thanks to the many people who submitted patches for this functionality and/or assisted in testing those patches. That list includes but is not limited to, -- cgit v1.2.3 From 6f3ea41cbc559cbf1db92eeebcb4ec3432182299 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 10:16:35 -0800 Subject: Finish cleaning up algo-agreement log lines. Make them all consistent re: display of local-vs-remote, and less verbose when possible. --- paramiko/transport.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 324ed277..b60fe85f 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1778,6 +1778,18 @@ class Transport (threading.Thread, ClosingContextManager): if self.sys.modules is not None: raise + + def _log_agreement(self, which, local, remote): + # Log useful, non-duplicative line re: an agreed-upon algorithm. + # Old code implied algorithms could be asymmetrical (different for + # inbound vs outbound) so we preserve that possibility. + msg = "{0} agreed: ".format(which) + if local == remote: + msg += local + else: + msg += "local={0}, remote={1}".format(local, remote) + self._log(DEBUG, msg) + ### protocol stages def _negotiate_keys(self, m): @@ -1946,7 +1958,9 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable ciphers)') self.local_cipher = agreed_local_ciphers[0] self.remote_cipher = agreed_remote_ciphers[0] - self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher)) + self._log_agreement( + 'Cipher', local=self.local_cipher, remote=self.remote_cipher + ) if self.server_mode: agreed_remote_macs = list(filter(self._preferred_macs.__contains__, client_mac_algo_list)) @@ -1958,7 +1972,9 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable macs)') self.local_mac = agreed_local_macs[0] self.remote_mac = agreed_remote_macs[0] - self._log(DEBUG, 'MACs agreed: local=%s, remote=%s' % (self.local_mac, self.remote_mac)) + self._log_agreement( + 'MAC', local=self.local_mac, remote=self.remote_mac + ) if self.server_mode: agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list)) @@ -1970,7 +1986,11 @@ class Transport (threading.Thread, ClosingContextManager): raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression)) self.local_compression = agreed_local_compression[0] self.remote_compression = agreed_remote_compression[0] - self._log(DEBUG, 'Compressions agreed: local=%s' % self.local_compression) + self._log_agreement( + 'Compression', + local=self.local_compression, + remote=self.remote_compression + ) # save for computing hash later... # now wait! openssh has a bug (and others might too) where there are -- cgit v1.2.3 From 7030ad9f8d2497761bcbf7d1bc97d41a5041cb2a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 10:23:02 -0800 Subject: Port #604 to latest master --- paramiko/transport.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/paramiko/transport.py b/paramiko/transport.py index b60fe85f..c5054dea 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -98,9 +98,11 @@ class Transport (threading.Thread, ClosingContextManager): # reorder without reason! _preferred_ciphers = ( 'aes128-ctr', + 'aes192-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', + 'aes192-cbc', 'aes256-cbc', '3des-cbc', 'arcfour128', @@ -134,6 +136,12 @@ class Transport (threading.Thread, ClosingContextManager): 'block-size': 16, 'key-size': 16 }, + 'aes192-ctr': { + 'class': AES, + 'mode': AES.MODE_CTR, + 'block-size': 16, + 'key-size': 24 + }, 'aes256-ctr': { 'class': AES, 'mode': AES.MODE_CTR, @@ -152,6 +160,12 @@ class Transport (threading.Thread, ClosingContextManager): 'block-size': 16, 'key-size': 16 }, + 'aes192-cbc': { + 'class': AES, + 'mode': AES.MODE_CBC, + 'block-size': 16, + 'key-size': 24 + }, 'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, -- cgit v1.2.3 From 9a091e0494269ae5e6074877fb0b335181ad28ae Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 10:24:48 -0800 Subject: Changelog closes #604 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 3aa2b84b..9c94002d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :feature:`604` Add support for the ``aes192-ctr`` and ``aes192-cbc`` ciphers. + Thanks to Michiel Tiller for noticing it was as easy as tweaking some key + sizes :D * :feature:`356` (also :issue:`596`, :issue:`365`, :issue:`341`, :issue:`164`, :issue:`581`, and a bunch of other duplicates besides) Add support for SHA-2 based key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256`` -- cgit v1.2.3 From aed3d8b75a49ca85a322cf95d6b6dcc1131619fd Mon Sep 17 00:00:00 2001 From: Prasanna Santhanam Date: Tue, 21 Jul 2015 14:46:08 +0530 Subject: compare end of key file before reading it --- paramiko/pkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index c8f84e0a..db5b77ac 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -274,7 +274,7 @@ class PKey (object): start += 1 # find end end = start - while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): + while end < len(lines) and lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----': end += 1 # if we trudged to the end of the file, just try to cope. try: -- cgit v1.2.3 From 663e4ca4a0363670d6dd72a512e936d0c47457c0 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 2 Oct 2015 16:20:27 -0700 Subject: Fix dumb bug in release task --- tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 20ded03d..2aeea0da 100644 --- a/tasks.py +++ b/tasks.py @@ -25,7 +25,8 @@ def release(ctx): # Move the built docs into where Epydocs used to live target = 'docs' rmtree(target, ignore_errors=True) - copytree(docs_build, target) + # TODO: make it easier to yank out this config val from the docs coll + copytree('sites/docs/_build', target) # Publish publish(ctx) # Remind -- cgit v1.2.3 From ce2df91a790aedcd0ec08f3526141cd01c63560d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Sun, 1 Nov 2015 15:09:12 -0800 Subject: Allow specifying test.py flags in 'inv test' --- tasks.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index 2aeea0da..05654d3b 100644 --- a/tasks.py +++ b/tasks.py @@ -9,8 +9,14 @@ from invocations.packaging import publish # Until we move to spec-based testing @task -def test(ctx): - ctx.run("python test.py --verbose", pty=True) +def test(ctx, coverage=False, flags=""): + if "--verbose" not in flags.split(): + flags += " --verbose" + runner = "python" + if coverage: + runner = "coverage run --source=paramiko" + ctx.run("{0} test.py {1}".format(runner, flags), pty=True) + @task def coverage(ctx): -- cgit v1.2.3 From fcacbe4620a867acedf33da7a069b09e4a8d370d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 12:52:24 -0800 Subject: Changelog closes #565 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9ce2eded..7c6b74e4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`565` Don't explode with ``IndexError`` when reading private key files + lacking an ``-----END PRIVATE KEY-----`` footer. Patch courtesy of + Prasanna Santhanam. * :release:`1.13.3 <2014-12-19>` * :bug:`413` (also :issue:`414`, :issue:`420`, :issue:`454`) Be significantly smarter about polling & timing behavior when running proxy commands, to avoid -- cgit v1.2.3 From 0683452c622e95c2b3a775621d3ace7a5afcc152 Mon Sep 17 00:00:00 2001 From: Sergey Skripnick Date: Mon, 12 Oct 2015 12:32:59 +0200 Subject: Fix data types in some docstrings Some methods return `bytes` type, but documentation said `str`. Also method BufferedPipe.feed accept both types. --- paramiko/buffered_pipe.py | 4 ++-- paramiko/channel.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py index ac35b3e1..d5fe164e 100644 --- a/paramiko/buffered_pipe.py +++ b/paramiko/buffered_pipe.py @@ -81,7 +81,7 @@ class BufferedPipe (object): Feed new data into this pipe. This method is assumed to be called from a separate thread, so synchronization is done. - :param data: the data to add, as a `str` + :param data: the data to add, as a `str` or `bytes` """ self._lock.acquire() try: @@ -125,7 +125,7 @@ class BufferedPipe (object): :param int nbytes: maximum number of bytes to read :param float timeout: maximum seconds to wait (or ``None``, the default, to wait forever) - :return: the read data, as a `str` + :return: the read data, as a `bytes` :raises PipeTimeout: if a timeout was specified and no data was ready before that diff --git a/paramiko/channel.py b/paramiko/channel.py index 23323650..a36c70f4 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -574,8 +574,8 @@ class Channel (object): is returned, the channel stream has closed. :param int nbytes: maximum number of bytes to read. - :return: received data, as a `str` - + :return: received data, as a `bytes` + :raises socket.timeout: if no data is ready before the timeout set by `settimeout`. """ -- cgit v1.2.3 From 3e08a40e9aee4aa289e9704c115773e1596d7f5d Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 12:55:36 -0800 Subject: Changelog closes #594 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 7c6b74e4..e81327fc 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`594 backported` Correct some post-Python3-port docstrings to + specify ``bytes`` type instead of ``str``. Credit to ``@redixin``. * :bug:`565` Don't explode with ``IndexError`` when reading private key files lacking an ``-----END PRIVATE KEY-----`` footer. Patch courtesy of Prasanna Santhanam. -- cgit v1.2.3 From 81024992e8fe5ccde8303e08feb4b9bdc4bd0d24 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 18 Jul 2014 11:20:34 -0700 Subject: Fixed a typo in method name The method on Python3 is `bit_length`, not `bitlength`. --- paramiko/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/util.py b/paramiko/util.py index 46278a69..0d8ce32a 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -118,7 +118,7 @@ def safe_string(s): def bit_length(n): try: - return n.bitlength() + return n.bit_length() except AttributeError: norm = deflate_long(n, False) hbyte = byte_ord(norm[0]) -- cgit v1.2.3 From 7611c57910f49aadf8caafbc7970bc3d991382d8 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 13:07:02 -0800 Subject: Changelog closes #359 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index e81327fc..5dc877c4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`359` Use correct attribute name when trying to use Python 3's + ``int.bit_length`` method; prior to fix, the Python 2 custom fallback + implementation was always used, even on Python 3. Thanks to Alex Gaynor. * :support:`594 backported` Correct some post-Python3-port docstrings to specify ``bytes`` type instead of ``str``. Credit to ``@redixin``. * :bug:`565` Don't explode with ``IndexError`` when reading private key files -- cgit v1.2.3 From 815f9c7b7dd169060a69fb818bec04ed4db1f9da Mon Sep 17 00:00:00 2001 From: Ulrich Petri Date: Sat, 26 Jul 2014 17:40:58 +0200 Subject: Fixed str() of empty SFTPAttributes() --- paramiko/sftp_attr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index d12eff8d..708afc5a 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -210,12 +210,15 @@ class SFTPAttributes (object): # not all servers support uid/gid uid = self.st_uid gid = self.st_gid + size = self.st_size if uid is None: uid = 0 if gid is None: gid = 0 + if size is None: + size = 0 - return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename) + return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, size, datestr, filename) def asbytes(self): return b(str(self)) -- cgit v1.2.3 From f6135f43d49c3903f9538f57191c2c381e86f689 Mon Sep 17 00:00:00 2001 From: Ulrich Petri Date: Sat, 26 Jul 2014 17:36:35 +0200 Subject: Added test for str() of empty SFTPAttributes() --- tests/test_sftp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 2b6aa3b6..980e8367 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -776,6 +776,11 @@ class SFTPTest (unittest.TestCase): sftp.remove('%s/nonutf8data' % FOLDER) + def test_sftp_attributes_empty_str(self): + sftp_attributes = SFTPAttributes() + self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?") + + if __name__ == '__main__': SFTPTest.init_loopback() # logging is required by test_N_file_with_percent -- cgit v1.2.3 From f3649c0d7d9d6d46269c5ad05ef88383cf50180f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 13:12:39 -0800 Subject: Changelog closes #366 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5dc877c4..831d425b 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string + representation doesn't raise exceptions on empty/initialized instances. Patch + by Ulrich Petri. * :bug:`359` Use correct attribute name when trying to use Python 3's ``int.bit_length`` method; prior to fix, the Python 2 custom fallback implementation was always used, even on Python 3. Thanks to Alex Gaynor. -- cgit v1.2.3 From a1d0cd6c6c375e6b1c17710805f0a2e7503ee0a9 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 13:37:12 -0800 Subject: 80-col tweaks --- paramiko/pkey.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/paramiko/pkey.py b/paramiko/pkey.py index db5b77ac..d8018e70 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -171,8 +171,9 @@ class PKey (object): is useless on the abstract PKey class. :param str filename: name of the file to read - :param str password: an optional password to use to decrypt the key file, - if it's encrypted + :param str password: + an optional password to use to decrypt the key file, if it's + encrypted :return: a new `.PKey` based on the given private key :raises IOError: if there was an error reading the file @@ -187,8 +188,8 @@ class PKey (object): def from_private_key(cls, file_obj, password=None): """ Create a key object by reading a private key from a file (or file-like) - object. If the private key is encrypted and ``password`` is not ``None``, - the given password will be used to decrypt the key (otherwise + object. If the private key is encrypted and ``password`` is not + ``None``, the given password will be used to decrypt the key (otherwise `.PasswordRequiredException` is thrown). :param file file_obj: the file to read from @@ -197,8 +198,8 @@ class PKey (object): :return: a new `.PKey` based on the given private key :raises IOError: if there was an error reading the key - :raises PasswordRequiredException: if the private key file is encrypted, - and ``password`` is ``None`` + :raises PasswordRequiredException: + if the private key file is encrypted, and ``password`` is ``None`` :raises SSHException: if the key file is invalid """ key = cls(file_obj=file_obj, password=password) -- cgit v1.2.3 From 545dedba3c3074e8ceacf0a4b639e515c85df671 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 13:51:26 -0800 Subject: Add 'sites' task for rigorous doc error discovery --- tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index 05654d3b..3d55a778 100644 --- a/tasks.py +++ b/tasks.py @@ -3,7 +3,7 @@ from os.path import join from shutil import rmtree, copytree from invoke import Collection, ctask as task -from invocations.docs import docs, www +from invocations.docs import docs, www, sites from invocations.packaging import publish @@ -39,4 +39,4 @@ def release(ctx): print("\n\nDon't forget to update RTD's versions page for new minor releases!") -ns = Collection(test, coverage, release, docs, www) +ns = Collection(test, coverage, release, docs, www, sites) -- cgit v1.2.3 From e6593de3b5ebb88a9a32d8e83245726d394a2646 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 13:51:46 -0800 Subject: Fix somebody's smart quotes --- paramiko/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/config.py b/paramiko/config.py index c21de936..b58e66a2 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -99,7 +99,7 @@ class SSHConfig (object): The host-matching rules of OpenSSH's ``ssh_config`` man page are used: For each parameter, the first obtained value will be used. The - configuration files contain sections separated by ``Host'' + configuration files contain sections separated by ``Host`` specifications, and that section is only applied for hosts that match one of the patterns given in the specification. -- cgit v1.2.3 From e73dc7751bb2ad360308f43918f6bd62aefbb085 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:06:11 -0800 Subject: Fix a handful of non-strict warnings in the Sphinx docs. Turns out there's a reason we haven't run the strict checker (sphinx-build -nW) before - it tries to resolve literally every info-list field like ':raises IOError:' and sphinx is apparently not cool enough to mesh that with intersphinx. --- paramiko/config.py | 2 +- paramiko/pkey.py | 9 +++++---- paramiko/sftp_client.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index b58e66a2..5c2efdcc 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -51,7 +51,7 @@ class SSHConfig (object): """ Read an OpenSSH config from the given file object. - :param file file_obj: a file-like object to read the config file from + :param file_obj: a file-like object to read the config file from """ host = {"host": ['*'], "config": {}} for line in file_obj: diff --git a/paramiko/pkey.py b/paramiko/pkey.py index d8018e70..472c1287 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -192,7 +192,7 @@ class PKey (object): ``None``, the given password will be used to decrypt the key (otherwise `.PasswordRequiredException` is thrown). - :param file file_obj: the file to read from + :param file_obj: the file-like object to read from :param str password: an optional password to use to decrypt the key, if it's encrypted :return: a new `.PKey` based on the given private key @@ -225,7 +225,7 @@ class PKey (object): Write private key contents into a file (or file-like) object. If the password is not ``None``, the key is encrypted before writing. - :param file file_obj: the file object to write into + :param file_obj: the file-like object to write into :param str password: an optional password to use to encrypt the key :raises IOError: if there was an error writing to the file @@ -311,8 +311,9 @@ class PKey (object): a trivially-encoded format (base64) which is completely insecure. If a password is given, DES-EDE3-CBC is used. - :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. - :param file filename: name of the file to write. + :param str tag: + ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. + :param filename: name of the file to write. :param str data: data blob that makes up the private key. :param str password: an optional password to use to encrypt the file. diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 99a29e36..9a3e04b7 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -517,7 +517,7 @@ class SFTPClient(BaseSFTP): The SFTP operations use pipelining for speed. - :param file fl: opened file or file-like object to copy + :param fl: opened file or file-like object to copy :param str remotepath: the destination path on the SFTP server :param int file_size: optional size parameter passed to callback. If none is specified, -- cgit v1.2.3 From 98b504ce1d840cc543aa85bdfb23b217931eabbf Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:14:30 -0800 Subject: Bump dev-req for invoke/invocations updates --- dev-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 059572cf..90cfd477 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,8 +1,8 @@ # Older junk tox>=1.4,<1.5 # For newer tasks like building Sphinx docs. -invoke>=0.10 -invocations>=0.9.2 +invoke>=0.11.1 +invocations>=0.11.0 sphinx>=1.1.3 alabaster>=0.6.1 releases>=0.5.2 -- cgit v1.2.3 From d22597b558ca8a1465352497e5adfd5d79cf4f3a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:24:41 -0800 Subject: Really, really sick of Python 3.2 being shite to support. Step 1: make it stop tanking Travis builds randomly. --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 64f64e60..dc5bd561 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ sudo: false python: - "2.6" - "2.7" - - "3.2" - "3.3" install: # Self-install for setup.py-driven deps @@ -18,9 +17,7 @@ script: # Run 'docs' first since its objects.inv is referred to by 'www'. # Also force warnings to be errors since most of them tend to be actual # problems. - # Finally, skip them under Python 3.2 due to sphinx shenanigans - - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke docs -o -W || true" - - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke www -o -W || true" + - invoke docs -o -W www -o -W notifications: irc: channels: "irc.freenode.org#paramiko" -- cgit v1.2.3 From 583d6eaedfe2b004777b100ab8a8185bb9bec073 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:32:00 -0800 Subject: We ought to work on python 3.4/3.5... --- .travis.yml | 2 ++ setup.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index dc5bd561..f841a71e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: - "2.6" - "2.7" - "3.3" + - "3.4" + - "3.5" install: # Self-install for setup.py-driven deps - pip install -e . diff --git a/setup.py b/setup.py index 235fd0e3..9e08323d 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,8 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', ], **kw ) -- cgit v1.2.3 From 24e767792dab6e4cac6e5374032e772951d0392e Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:43:03 -0800 Subject: Backport a Python 3.4+ threading change to test.py --- test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test.py b/test.py index bd966d1e..6a2c9a8b 100755 --- a/test.py +++ b/test.py @@ -149,10 +149,7 @@ def main(): # TODO: make that not a problem, jeez for thread in threading.enumerate(): if thread is not threading.currentThread(): - if PY2: - thread._Thread__stop() - else: - thread._stop() + thread.join(timeout=1) # Exit correctly if not result.wasSuccessful(): sys.exit(1) -- cgit v1.2.3 From 0a57d0337778d99066688e310c81d449c64c9bb6 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:57:45 -0800 Subject: Cut 1.13.4 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 0402fcf2..63bba727 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 13, 3) +__version_info__ = (1, 13, 4) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 831d425b..e435c65e 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.13.4 <2015-11-02>` * :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string representation doesn't raise exceptions on empty/initialized instances. Patch by Ulrich Petri. -- cgit v1.2.3 From 79bdefe35610b651566bb7422518fb60b3f72bdd Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 17:59:40 -0800 Subject: Cut 1.14.3 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index f941ac22..871565d3 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 14, 2) +__version_info__ = (1, 14, 3) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 484e7be9..7a140e38 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.14.3 <2015-11-02>` * :release:`1.13.4 <2015-11-02>` * :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string representation doesn't raise exceptions on empty/initialized instances. Patch -- cgit v1.2.3 From d37c68673396b247c08d0d5122bb012e9c3c46c3 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 2 Nov 2015 18:01:15 -0800 Subject: Cut 1.15.4 --- paramiko/_version.py | 2 +- sites/www/changelog.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/_version.py b/paramiko/_version.py index 25aac14f..3b9c059e 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (1, 15, 3) +__version_info__ = (1, 15, 4) __version__ = '.'.join(map(str, __version_info__)) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index a7824175..fe4b2b2d 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :release:`1.15.4 <2015-11-02>` * :release:`1.14.3 <2015-11-02>` * :release:`1.13.4 <2015-11-02>` * :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string -- cgit v1.2.3 From 6f773cef69f2a70e51d44affd0e592edc099cc11 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 3 Nov 2015 12:57:14 -0800 Subject: Changelog re #525 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index e435c65e..cbecabea 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :support:`525 backported` Update the vendored Windows API addon to a more + recent edition. Also fixes :issue:`193`, :issue:`488`, :issue:`498`. Thanks + to Jason Coombs. * :release:`1.13.4 <2015-11-02>` * :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string representation doesn't raise exceptions on empty/initialized instances. Patch -- cgit v1.2.3 From 52ba29d62796215d417716802808f8702b673baf Mon Sep 17 00:00:00 2001 From: Dylan Thacker-Smith Date: Mon, 22 Sep 2014 00:18:07 -0400 Subject: Fix line number for known_hosts error messages. --- paramiko/hostkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 7c6a5aa5..1a5031d1 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -89,7 +89,7 @@ class HostKeys (MutableMapping): :raises IOError: if there was an error reading the file """ with open(filename, 'r') as f: - for lineno, line in enumerate(f): + for lineno, line in enumerate(f, 1): line = line.strip() if (len(line) == 0) or (line[0] == '#'): continue -- cgit v1.2.3 From 3a5227c477295c8e14e395d3ac66e9a58db0ebc8 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 3 Nov 2015 13:31:13 -0800 Subject: Changelog closes #401 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index cbecabea..278f7450 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`401` Fix line number reporting in log output regarding invalid + ``known_hosts`` line entries. Thanks to Dylan Thacker-Smith for catch & + patch. * :support:`525 backported` Update the vendored Windows API addon to a more recent edition. Also fixes :issue:`193`, :issue:`488`, :issue:`498`. Thanks to Jason Coombs. -- cgit v1.2.3