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 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 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 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 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 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 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 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 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 23c03b812242736bbfa1bf2e7780911ff180aaae 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 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 c3ba66b90e9da0cce9c88fbc45894fde492edfd0 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 d05ca17db876055a5f584e720b1b0733116b4365 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 Conflicts: sites/www/changelog.rst --- sites/www/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index a652e565..5c8e02f4 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. * :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 ab8ea174501023fda3686d836057a6f8b475ab61 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 22:25:40 -0700 Subject: Minor refactor, re #169 --- 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 1caaf165..cbef8f71 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -584,7 +584,7 @@ class SFTPClient(BaseSFTP): """ file_size = os.stat(localpath).st_size with open(localpath, 'rb') as fl: - return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) + return self.putfo(fl, remotepath, file_size, callback, confirm) def getfo(self, remotepath, fl, callback=None): """ -- cgit v1.2.3 From 87f6f56196b0b384c906db96ef5593c5cb6a0c61 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 22:26:33 -0700 Subject: Changelog re #169 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 5c8e02f4..b9d0df0b 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`169 backported` Minor refactor of + `paramiko.sftp_client.SFTPClient.put` thanks to Abhinav Upadhyay. * :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. -- cgit v1.2.3 From 5fc6969e23ec5e013f432e4efbe12d771150c1e1 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 22:32:21 -0700 Subject: Fix docstrings re: addition of `getfo`/`putfo`, closes #229 --- paramiko/sftp_client.py | 8 ++------ sites/www/changelog.rst | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index cbef8f71..2ff2d51d 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -534,9 +534,7 @@ class SFTPClient(BaseSFTP): an `.SFTPAttributes` object containing attributes about the given file. - .. versionadded:: 1.4 - .. versionchanged:: 1.7.4 - Began returning rich attribute objects. + .. versionadded:: 1.10 """ with self.file(remotepath, 'wb') as fr: fr.set_pipelined(True) @@ -601,9 +599,7 @@ class SFTPClient(BaseSFTP): the bytes transferred so far and the total bytes to be transferred :return: the `number ` of bytes written to the opened file object - .. versionadded:: 1.4 - .. versionchanged:: 1.7.4 - Added the ``callable`` param. + .. versionadded:: 1.10 """ with self.open(remotepath, 'rb') as fr: file_size = self.stat(remotepath).st_size diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index b9d0df0b..cdd513f4 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`229` Fix a couple of incorrectly-copied docstrings' ``.. + versionadded::`` RST directives. Thanks to Aarni Koskela for the catch. * :support:`169 backported` Minor refactor of `paramiko.sftp_client.SFTPClient.put` thanks to Abhinav Upadhyay. * :bug:`285` (also :issue:`352`) Update our Python 3 ``b()`` compatibility shim -- cgit v1.2.3 From 12e752d11aa22ee6ba959115725e386ca779c1cb Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 22:44:54 -0700 Subject: Rework re #239 to work off post-1.13 codebase. Closes #239 --- paramiko/config.py | 2 +- sites/www/changelog.rst | 2 ++ tests/test_util.py | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/paramiko/config.py b/paramiko/config.py index 77fa13d7..f650ee24 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -55,7 +55,7 @@ class SSHConfig (object): """ host = {"host": ['*'], "config": {}} for line in file_obj: - line = line.rstrip('\n').lstrip() + line = line.rstrip('\r\n').lstrip() if (line == '') or (line[0] == '#'): continue if '=' in line: diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index cdd513f4..903e6378 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :bug:`239` Add Windows-style CRLF support to SSH config file parsing. Props + to Christopher Swenson. * :support:`229` Fix a couple of incorrectly-copied docstrings' ``.. versionadded::`` RST directives. Thanks to Aarni Koskela for the catch. * :support:`169 backported` Minor refactor of diff --git a/tests/test_util.py b/tests/test_util.py index 6bde4045..ecf8db72 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -338,3 +338,9 @@ 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_13_config_dos_crlf_succeeds(self): + config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n") + config = paramiko.SSHConfig() + config.parse(config_file) + self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1") -- cgit v1.2.3 From 16b820aede0d3050bc8c96ab59f0aa9a79bebcdb Mon Sep 17 00:00:00 2001 From: Sigmund Augdal Date: Sat, 22 Feb 2014 15:52:53 +0100 Subject: Don't hash the input hostname when checking a host key if the input hostname is already hashed This happens in the loop checking if a hostkey is already loaded when loading host key files. This reduces time to load host keys file with about two seconds for me --- paramiko/hostkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index f32fbeb6..7c6a5aa5 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -176,7 +176,7 @@ class HostKeys (MutableMapping): entries = [] for e in self._entries: for h in e.hostnames: - if h.startswith('|1|') and constant_time_bytes_eq(self.hash_host(hostname, h), h) or h == hostname: + if h.startswith('|1|') and not hostname.startswith('|1|') and constant_time_bytes_eq(self.hash_host(hostname, h), h) or h == hostname: entries.append(e) if len(entries) == 0: return None -- cgit v1.2.3 From 118d581f92b1aa49a482c68faa73df0c8967b3c4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 22:58:58 -0700 Subject: Changelog re #272 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 903e6378..99115727 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`272` Fix a bug where ``known_hosts`` parsing hashed the input hostname + as well as the hostnames from the ``known_hosts`` file, on every comparison. + Thanks to ``@sigmunau`` for final patch and ``@ostacey`` for the original + report. * :bug:`239` Add Windows-style CRLF support to SSH config file parsing. Props to Christopher Swenson. * :support:`229` Fix a couple of incorrectly-copied docstrings' ``.. -- cgit v1.2.3 From adaf8a2c36bf37aa98c347874316ef1809dc40a3 Mon Sep 17 00:00:00 2001 From: Simon Percivall Date: Tue, 15 Apr 2014 11:17:24 +0200 Subject: Set Transport.active to False early in Transport.__init__. --- paramiko/transport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 1471b543..d904da1d 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -164,6 +164,8 @@ class Transport (threading.Thread): :param socket sock: a socket or socket-like object to create the session over. """ + self.active = False + if isinstance(sock, string_types): # convert "host:port" into (host, port) hl = sock.split(':', 1) @@ -220,7 +222,6 @@ class Transport (threading.Thread): self.H = None self.K = None - self.active = False self.initial_kex_done = False self.in_kex = False self.authenticated = False -- cgit v1.2.3 From c7ffe1718a3bae232b7ecef959545dac9f729095 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 23:07:57 -0700 Subject: Changelog re #312, closes #312 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 99115727..b315b0d7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`312` `paramiko.transport.Transport` had a bug in its ``__repr__`` which + surfaces during errors encountered within its ``__init__``, causing + problematic tracebacks in such situations. Thanks to Simon Percivall for + catch & patch. * :bug:`272` Fix a bug where ``known_hosts`` parsing hashed the input hostname as well as the hostnames from the ``known_hosts`` file, on every comparison. Thanks to ``@sigmunau`` for final patch and ``@ostacey`` for the original -- cgit v1.2.3 From 488cd21f6bc6bdf9e91ac891c4247897acc3bcb5 Mon Sep 17 00:00:00 2001 From: Roy Wellington Ⅳ Date: Wed, 7 May 2014 16:21:49 -0700 Subject: Fix some minor typos. --- paramiko/channel.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/paramiko/channel.py b/paramiko/channel.py index e10ddbac..23323650 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -279,7 +279,7 @@ class Channel (object): def recv_exit_status(self): """ Return the exit status from the process on the server. This is - mostly useful for retrieving the reults of an `exec_command`. + mostly useful for retrieving the results of an `exec_command`. 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. @@ -329,7 +329,7 @@ class Channel (object): 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). + x11 cookie (which requires some knowledge 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 @@ -338,7 +338,7 @@ class Channel (object): handler(channel: Channel, (address: str, port: int)) - :param int screen_number: the x11 screen number (0, 10, etc) + :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, ``"MIT-MAGIC-COOKIE-1"`` is used @@ -743,10 +743,10 @@ class Channel (object): :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. + if an error occurred before the entire string was sent. .. note:: - If the channel is closed while only part of the data hase been + If the channel is closed while only part of the data has been sent, there is no way to determine how much data (if any) was sent. This is irritating, but identically follows Python's API. """ @@ -770,7 +770,7 @@ class Channel (object): :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. + if an error occurred before the entire string was sent. .. versionadded:: 1.1 """ @@ -811,7 +811,7 @@ class Channel (object): def fileno(self): """ Returns an OS-level file descriptor which can be used for polling, but - but not for reading or writing. This is primaily to allow Python's + but not for reading or writing. This is primarily to allow Python's ``select`` module to work. The first time ``fileno`` is called on a channel, a pipe is created to -- cgit v1.2.3 From 77919c6c25d9016c74537ea9b901cedc19050983 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 23:15:22 -0700 Subject: Changelog re #324, closes #324 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index b315b0d7..6979b97e 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`324 backported` A bevvy of documentation typo fixes, courtesy of Roy + Wellington. * :bug:`312` `paramiko.transport.Transport` had a bug in its ``__repr__`` which surfaces during errors encountered within its ``__init__``, causing problematic tracebacks in such situations. Thanks to Simon Percivall for -- cgit v1.2.3 From 1fa69c7003bac49c22721ac694fe399f369ece8a Mon Sep 17 00:00:00 2001 From: w3iBStime Date: Thu, 19 Jun 2014 15:26:53 -0500 Subject: Updating doco for sftp_client.put() Adding a note to indicate that the remotepath should include a filename. --- paramiko/sftp_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 2ff2d51d..99a29e36 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -564,7 +564,9 @@ class SFTPClient(BaseSFTP): The SFTP operations use pipelining for speed. :param str localpath: the local file to copy - :param str remotepath: the destination path on the SFTP server + :param str remotepath: the destination path on the SFTP server. Note + that the filename should be included. Only specifying a directory + may result in an error. :param callable callback: optional callback function (form: ``func(int, int)``) that accepts the bytes transferred so far and the total bytes to be transferred -- cgit v1.2.3 From c50d023c91210eb25416bf5f3a4e05353cb16ca4 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Fri, 15 Aug 2014 15:53:07 +0200 Subject: Don't make unnecessary calls to LazyFqdn.__str__. Before this patch we always tried to expand variables in the config even if they weren't present. This meant that we made an expensive call to LazyFqdn.__str__ the first iteration of the expand loop, stealing precious cpu and user time. We now check that the expansion actually exists in the config before expanding it, this will speed up the case where %l is not used. This fixes #338 --- paramiko/config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index f650ee24..96ec9ef7 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -83,7 +83,7 @@ class SSHConfig (object): #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) @@ -200,10 +200,12 @@ class SSHConfig (object): for find, replace in replacements[k]: if isinstance(config[k], list): for item in range(len(config[k])): - config[k][item] = config[k][item].\ - replace(find, str(replace)) + if find in config[k][item]: + config[k][item] = config[k][item].\ + replace(find, str(replace)) else: - config[k] = config[k].replace(find, str(replace)) + if find in config[k]: + config[k] = config[k].replace(find, str(replace)) return config -- cgit v1.2.3 From 3908baad6e1d6d4fc62da24111cd466ce5cb9a88 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 23:34:13 -0700 Subject: Changelog re #376, closes #376 --- sites/www/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 6979b97e..9258cfec 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,9 @@ Changelog ========= +* :bug:`376` Be less aggressive about expanding variables in ``ssh_config`` + files, which results in a speedup of SSH config parsing. Credit to Olle + Lundberg. * :support:`324 backported` A bevvy of documentation typo fixes, courtesy of Roy Wellington. * :bug:`312` `paramiko.transport.Transport` had a bug in its ``__repr__`` which -- cgit v1.2.3 From 1b69e0f4095883ef3f282552fde12dfb00ebdda4 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Mon, 25 Aug 2014 23:42:21 -0700 Subject: Cut 1.13.2 --- 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..d86b868b 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.13.2" __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..082a843e 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ if sys.platform == 'darwin': setup( name = "paramiko", - version = "1.13.1", + version = "1.13.2", description = "SSH2 protocol library", long_description = longdesc, author = "Jeff Forcier", diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9258cfec..9c519c03 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :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 Lundberg. -- 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 38332e0054eddbb0a0a9812894920e38a721eab2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 26 Aug 2014 00:04:22 -0700 Subject: Fix up release task --- dev-requirements.txt | 2 +- tasks.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 88a32964..19cf7503 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,7 @@ tox>=1.4,<1.5 # For newer tasks like building Sphinx docs. # NOTE: Requires Python >=2.6 -invoke>=0.7.0 +invoke>=0.7.0,<0.8 invocations>=0.5.0 sphinx>=1.1.3 alabaster>=0.4.0 diff --git a/tasks.py b/tasks.py index 38282b8e..94bf0aa2 100644 --- a/tasks.py +++ b/tasks.py @@ -36,8 +36,10 @@ def coverage(ctx): # Until we stop bundling docs w/ releases. Need to discover use cases first. -@task('docs') # Will invoke the API doc site build +@task def release(ctx): + # Build docs first. Use terribad workaround pending invoke #146 + ctx.run("inv docs") # Move the built docs into where Epydocs used to live target = 'docs' rmtree(target, ignore_errors=True) -- cgit v1.2.3 From 9014c735fc94d133c5d9aada93d43f1f5d1145f2 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 26 Aug 2014 00:14:02 -0700 Subject: Forgot to mark one support-bug as backported --- 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 9c519c03..1ca3dfef 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -18,7 +18,7 @@ Changelog report. * :bug:`239` Add Windows-style CRLF support to SSH config file parsing. Props to Christopher Swenson. -* :support:`229` Fix a couple of incorrectly-copied docstrings' ``.. +* :support:`229 backported` Fix a couple of incorrectly-copied docstrings' ``.. versionadded::`` RST directives. Thanks to Aarni Koskela for the catch. * :support:`169 backported` Minor refactor of `paramiko.sftp_client.SFTPClient.put` thanks to Abhinav Upadhyay. -- cgit v1.2.3 From b641c69a80e2bd4737c636e7edd0918d6753c3d0 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Tue, 26 Aug 2014 12:50:10 -0700 Subject: Backport #378 to 1.13, closes #378 --- paramiko/config.py | 12 +++++++----- sites/www/changelog.rst | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 96ec9ef7..30e000e7 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -56,7 +56,7 @@ class SSHConfig (object): host = {"host": ['*'], "config": {}} for line in file_obj: line = line.rstrip('\r\n').lstrip() - if (line == '') or (line[0] == '#'): + if not line or line.startswith('#'): continue if '=' in line: # Ensure ProxyCommand gets properly split @@ -90,7 +90,7 @@ class SSHConfig (object): else: host['config'][key] = [value] elif key not in host['config']: - host['config'].update({key: value}) + host['config'][key] = value self._config.append(host) def lookup(self, hostname): @@ -111,8 +111,10 @@ class SSHConfig (object): :param str hostname: the hostname to lookup """ - matches = [config for config in self._config if - self._allowed(hostname, config['host'])] + matches = [ + config for config in self._config + if self._allowed(config['host'], hostname) + ] ret = {} for match in matches: @@ -128,7 +130,7 @@ class SSHConfig (object): ret = self._expand_variables(ret, hostname) return ret - def _allowed(self, hostname, hosts): + def _allowed(self, hosts, hostname): match = False for host in hosts: if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]): diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 1ca3dfef..0cbc933f 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :support:`378 backported` Minor code cleanup in the SSH config module + courtesy of Olle Lundberg. * :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 162eebf704b5111d34f430488868d3185d51ba21 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 Conflicts: paramiko/__init__.py setup.py sites/www/changelog.rst --- 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 d86b868b..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.13.2" -__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..0402fcf2 --- /dev/null +++ b/paramiko/_version.py @@ -0,0 +1,2 @@ +__version_info__ = (1, 13, 3) +__version__ = '.'.join(map(str, __version_info__)) diff --git a/setup.py b/setup.py index 082a843e..235fd0e3 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.13.2", + 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 0cbc933f..0f79775c 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -4,6 +4,8 @@ Changelog * :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 + Davar for the reminder. * :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 23fc8a76da7cd376aef1f27bdd5fc4d7416ced04 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 10:10:15 -0700 Subject: No notices for travis irc notifications --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7042570f..801356e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,6 @@ notifications: - "%{repository}@%{branch}: %{message} (%{build_url})" on_success: change on_failure: change - use_notice: true email: false after_success: - coveralls -- cgit v1.2.3 From 5f73547aa5fa8f677f3d9064d2cd95b5979a3767 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 10:11:15 -0700 Subject: Nuke old comment --- dev-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 19cf7503..a21a48ed 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,6 @@ # Older junk tox>=1.4,<1.5 # For newer tasks like building Sphinx docs. -# NOTE: Requires Python >=2.6 invoke>=0.7.0,<0.8 invocations>=0.5.0 sphinx>=1.1.3 -- cgit v1.2.3 From 43c5cdaa3094c59718c8df69b1de834aee15485c Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 10:12:40 -0700 Subject: s/gittip/gratipay/ --- dev-requirements.txt | 2 +- sites/shared_conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index a21a48ed..7a0ccbc5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,6 +4,6 @@ tox>=1.4,<1.5 invoke>=0.7.0,<0.8 invocations>=0.5.0 sphinx>=1.1.3 -alabaster>=0.4.0 +alabaster>=0.6.1 releases>=0.5.2 wheel==0.23.0 diff --git a/sites/shared_conf.py b/sites/shared_conf.py index 69908388..4a6a5c4e 100644 --- a/sites/shared_conf.py +++ b/sites/shared_conf.py @@ -12,7 +12,7 @@ html_theme_options = { 'description': "A Python implementation of SSHv2.", 'github_user': 'paramiko', 'github_repo': 'paramiko', - 'gittip_user': 'bitprophet', + 'gratipay_user': 'bitprophet', 'analytics_id': 'UA-18486793-2', 'travis_button': True, } -- 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 4aa798ef21dc643a271e4ff5581ddf7cbde4c144 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Fri, 5 Sep 2014 11:40:12 -0700 Subject: Try skipping docs builds under Python 3.2 --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 801356e0..e17bdafd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,9 @@ 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. - - invoke docs -o -W - - invoke www -o -W + # 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" notifications: irc: channels: "irc.freenode.org#paramiko" -- 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 8254f0162b54aff7c0aa4877ceb592bf19e4822b 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 2cfe9f9a77833ae104019b7a7ee4ac73e340835f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 17:00:54 -0700 Subject: Remind user to update RTD after cutting a new release line --- tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks.py b/tasks.py index cf43a5fd..1afce514 100644 --- a/tasks.py +++ b/tasks.py @@ -46,6 +46,8 @@ def release(ctx): copytree(docs_build, target) # Publish publish(ctx, wheel=True) + # 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) -- 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 64fea9c8c7ae7af3720cf2922a400c94f736c43a 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 6ae2d277..7376b81f 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 @@ -130,13 +130,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 = ECDSA.generate() key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key())) -- cgit v1.2.3 From 17dbb6a48c0ccf645bc8a53d22ba502a71a99a26 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 17:50:54 -0700 Subject: Make keys doc file easier to follow --- sites/docs/api/keys.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sites/docs/api/keys.rst b/sites/docs/api/keys.rst index af7b58c4..c6412f77 100644 --- a/sites/docs/api/keys.rst +++ b/sites/docs/api/keys.rst @@ -1,6 +1,23 @@ +============ Key handling ============ +Parent key class +================ + .. automodule:: paramiko.pkey + +DSA (DSS) +========= + .. automodule:: paramiko.dsskey + +RSA +=== + .. automodule:: paramiko.rsakey + +ECDSA +===== + +.. automodule:: paramiko.ecdsakey -- cgit v1.2.3 From 233f0a422e3384584e20eff7330a536230d3cd31 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 17:54:20 -0700 Subject: Fix more missed epydoc syntax --- paramiko/file.py | 9 ++++----- paramiko/server.py | 15 ++++++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/paramiko/file.py b/paramiko/file.py index 2238f0bf..0a7fbcba 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -104,14 +104,13 @@ class BufferedFile (object): else: def __next__(self): """ - Returns the next line from the input, or raises L{StopIteration} when + Returns the next line from the input, or raises `.StopIteration` when EOF is hit. Unlike python file objects, it's okay to mix calls to - C{next} and L{readline}. + `.next` and `.readline`. - @raise StopIteration: when the end of the file is reached. + :raises StopIteration: when the end of the file is reached. - @return: a line read from the file. - @rtype: str + :returns: a line (`str`) read from the file. """ line = self.readline() if not line: diff --git a/paramiko/server.py b/paramiko/server.py index 496cd60c..2f630dfb 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -473,21 +473,18 @@ class ServerInterface (object): def check_channel_env_request(self, channel, name, value): """ Check whether a given environment variable can be specified for the - given channel. This method should return C{True} if the server + given channel. This method should return ``True`` if the server is willing to set the specified environment variable. Note that some environment variables (e.g., PATH) can be exceedingly dangerous, so blindly allowing the client to set the environment is almost certainly not a good idea. - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the env request arrived on - @type channel: L{Channel} - @param name: foo bar baz - @type name: str - @param value: flklj - @type value: str - @rtype: bool + :param channel: the `.Channel` the env request arrived on + :param str name: name + :param str value: Channel value + :returns: A boolean """ return False -- cgit v1.2.3 From 83f54e4f6cfebb0b90e8e2061ef5ccd3000a5bfe Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Thu, 18 Sep 2014 17:55:16 -0700 Subject: And moar --- paramiko/kex_gex.py | 2 +- paramiko/transport.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 02e507b7..a7cfb7c9 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -19,7 +19,7 @@ """ Variant on `KexGroup1 ` where the prime "p" and 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. +client side, and a **lot** more on the server side. """ from Crypto.Hash import SHA diff --git a/paramiko/transport.py b/paramiko/transport.py index d904da1d..4b91a434 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -963,10 +963,9 @@ class Transport (threading.Thread): def get_banner(self): """ Return the banner supplied by the server upon connect. If no banner is - supplied, this method returns C{None}. + supplied, this method returns ``None``. - @return: server supplied banner, or C{None}. - @rtype: string + :returns: server supplied banner (`str`), or ``None``. """ if not self.active or (self.auth_handler is None): return None -- 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 a381b08c1bacb4af4753e99d2a18e0a67e28a9c3 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Wed, 1 Oct 2014 13:34:37 +0200 Subject: Make sure we pass parameters as binary on windows. This fixes #320 (thanks @adamkerz). --- paramiko/win_pageant.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py index 20b1b0b9..4b482bee 100644 --- a/paramiko/win_pageant.py +++ b/paramiko/win_pageant.py @@ -26,6 +26,7 @@ import ctypes.wintypes import platform import struct from paramiko.util import * +from paramiko.py3compat import b try: import _thread as thread # Python 3.x @@ -43,7 +44,7 @@ win32con_WM_COPYDATA = 74 def _get_pageant_window_object(): - return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant') + return ctypes.windll.user32.FindWindowA(b('Pageant'), b('Pageant')) def can_talk_to_agent(): @@ -90,7 +91,7 @@ def _query_pageant(msg): with pymap: pymap.write(msg) # Create an array buffer containing the mapped filename - char_buffer = array.array("c", b(map_name) + zero_byte) + char_buffer = array.array("b", 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, -- cgit v1.2.3 From cf8921e310005832c82921ce67441cc82af18092 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 1 Oct 2014 16:54:03 -0700 Subject: Changelog re #320 Closes #409 --- sites/www/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 0f79775c..91d4daa7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,8 @@ Changelog ========= +* :bug:`320` Update our win_pageant module to be Python 3 compatible. Thanks to +``@sherbang`` and ``@adamkerz`` for the patches. * :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 789527166be2921b7987ca2e921109ad26907db6 Mon Sep 17 00:00:00 2001 From: Olle Lundberg Date: Wed, 1 Oct 2014 01:31:16 +0200 Subject: Update documentation of lookup method. The documentation reflected pre #93 behaviour. --- paramiko/config.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/paramiko/config.py b/paramiko/config.py index 30e000e7..c21de936 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -97,13 +97,15 @@ class SSHConfig (object): """ Return a dict of config options for a given hostname. - The host-matching rules of OpenSSH's ``ssh_config`` man page are used, - which means that all configuration options from matching host - specifications are merged, with more specific hostmasks taking - precedence. In other words, if ``"Port"`` is set under ``"Host *"`` - and also ``"Host *.example.com"``, and the lookup is for - ``"ssh.example.com"``, then the port entry for ``"Host *.example.com"`` - will win out. + 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'' + specifications, and that section is only applied for hosts that match + one of the patterns given in the specification. + + Since the first obtained value for each parameter is used, more host- + specific declarations should be given near the beginning of the file, + and general defaults at the end. The keys in the returned dict are all normalized to lowercase (look for ``"port"``, not ``"Port"``. The values are processed according to the -- cgit v1.2.3 From a08149d5f7aea1fcce55223a000ce9018df02961 Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 12 Nov 2014 13:49:03 -0800 Subject: Failing test proving #429 --- tests/test_util.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index ecf8db72..0e7d0b2b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -25,8 +25,8 @@ import errno import os 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 +from paramiko.util import lookup_ssh_host_config as host_config, safe_string +from paramiko.py3compat import StringIO, byte_ord, b from tests.util import ParamikoTest @@ -344,3 +344,14 @@ IdentityFile something_%l_using_fqdn config = paramiko.SSHConfig() config.parse(config_file) self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1") + + def test_safe_string(self): + vanilla = b("vanilla") + has_bytes = b("has \7\3 bytes") + safe_vanilla = safe_string(vanilla) + safe_has_bytes = safe_string(has_bytes) + expected_bytes = b("has %07%03 bytes") + err = "{0!r} != {1!r}" + assert safe_vanilla == vanilla, err.format(safe_vanilla, vanilla) + assert safe_has_bytes == expected_bytes, \ + err.format(safe_has_bytes, expected_bytes) -- cgit v1.2.3 From b3d703e984f2046f8eb208e3c82f8bf5c7c49d6a Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 12 Nov 2014 13:54:01 -0800 Subject: Improve byte/string interactions in debug logging. Fixes #429 --- paramiko/transport.py | 2 +- paramiko/util.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/paramiko/transport.py b/paramiko/transport.py index 4b91a434..296d6eb7 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1985,7 +1985,7 @@ class Transport (threading.Thread): always_display = m.get_boolean() msg = m.get_string() lang = m.get_string() - self._log(DEBUG, 'Debug msg: ' + util.safe_string(msg)) + self._log(DEBUG, 'Debug msg: {0}'.format(util.safe_string(msg))) def _get_subsystem_handler(self, name): try: diff --git a/paramiko/util.py b/paramiko/util.py index dbcbbae4..542bb218 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -115,12 +115,13 @@ def unhexify(s): def safe_string(s): - out = '' + out = b('') for c in s: - if (byte_ord(c) >= 32) and (byte_ord(c) <= 127): - out += c + i = byte_ord(c) + if 32 <= i <= 127: + out += byte_chr(i) else: - out += '%%%02X' % byte_ord(c) + out += b('%%%02X' % i) return out -- cgit v1.2.3 From ee06fc8f634b424b026afde3b853f8c321b1919f Mon Sep 17 00:00:00 2001 From: Jeff Forcier Date: Wed, 12 Nov 2014 13:55:56 -0800 Subject: Changelog re #429 --- sites/www/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 91d4daa7..3f05caf7 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +* :bug:`429` Server-level debug message logging was overlooked during the + Python 3 compatibility update; Python 3 clients attempting to log SSH debug + packets encountered type errors. This is now fixed. Thanks to ``@mjmaenpaa`` + for the catch. * :bug:`320` Update our win_pageant module to be Python 3 compatible. Thanks to ``@sherbang`` and ``@adamkerz`` for the patches. * :support:`378 backported` Minor code cleanup in the SSH config module -- cgit v1.2.3