diff -Nru paramiko-1.10.1/debian/changelog paramiko-1.15.1/debian/changelog --- paramiko-1.10.1/debian/changelog 2014-02-23 13:49:47.000000000 +0000 +++ paramiko-1.15.1/debian/changelog 2014-10-28 07:16:37.000000000 +0000 @@ -1,18 +1,62 @@ -paramiko (1.10.1-1git1build1) trusty; urgency=medium +paramiko (1.15.1-1~0x2go1~ubuntu14.04.1) trusty; urgency=medium - * Rebuild to drop files installed into /usr/share/pyshared. + * Rebuild for trusty and provide via Launchpad. + * Disable build of documentation files. Don't provide + python-paramiko-doc package. - -- Matthias Klose Sun, 23 Feb 2014 13:49:47 +0000 + -- Mike Gabriel Tue, 21 Oct 2014 10:23:31 +0200 -paramiko (1.10.1-1git1) trusty; urgency=low +paramiko (1.15.1-1) unstable; urgency=medium - Upload current Debian packaging git head. + * Imported Upstream version 1.15.1 + * Update gbp.conf section headers for newer version + * Move from epydoc to sphinx for doc build + * Add patch to localize generated documentation + * Implement pybuild buildsystem + * debian/gbp.conf: Update postbuild for git-buildpackage + * Imported Upstream version 1.15.0 + * Specify minimum required version of ecdsa - [ Jean-Baptiste Lallement ] - * Add autopkgtest that runs the upstream test suite against the installed - package. (LP: #1248706) + -- Jeremy T. Bouse Tue, 23 Sep 2014 14:07:59 -0400 - -- Martin Pitt Thu, 07 Nov 2013 14:17:13 +0100 +paramiko (1.14.1-1) unstable; urgency=medium + + [ Thomas Kluyver ] + * Add autopkgtest information + + [ Jeremy T. Bouse ] + * Include upstream GitHub Pull Request #352 to fix RC bug. + Thanks to Jelmer Vernooij (Closes: #750517, #755910) + * Imported Upstream version 1.14.1 + * Revert "Include upstream GitHub Pull Request #352 to fix RC bug" + as it was included in upstream bug fix release 1.14.1 to remove + the regression introduced into 1.14.0. + + -- Jeremy T. Bouse Thu, 28 Aug 2014 22:23:51 -0700 + +paramiko (1.14.0-2) unstable; urgency=low + + * Add extend-diff-ignore to debian/source/options. + Thanks to Thomas Goirand + * Add python-ecdsa to Build-Depends (Closes: #702571) + * Add Python 3 support to build. + Thanks to Thomas Goirand (Closes: #697600, #749512) + * Clean up build environment + + -- Jeremy T. Bouse Tue, 27 May 2014 21:30:02 -0400 + +paramiko (1.14.0-1) unstable; urgency=low + + * Imported Upstream version 1.14.0 (Closes: #742005) + * Update Standards-Version to 3.9.5 + * Fix paramiko-doc overwriting /usr/share/doc-base/python-paramiko. + Thanks to Felix Geyer (Closes: #718004) + * debian/patches/*: Removed patch applied upstream + * debian/control: Move VCS location + * debian/control: Fix type in paramiko-doc block + * debian/control: Removed unknown XS-Testsuite + + -- Jeremy T. Bouse Sun, 11 May 2014 23:01:05 -0400 paramiko (1.10.1-1) unstable; urgency=low diff -Nru paramiko-1.10.1/debian/compat paramiko-1.15.1/debian/compat --- paramiko-1.10.1/debian/compat 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/compat 2014-09-23 00:55:46.000000000 +0000 @@ -1 +1 @@ -8 +9 diff -Nru paramiko-1.10.1/debian/control paramiko-1.15.1/debian/control --- paramiko-1.10.1/debian/control 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/control 2014-10-21 08:29:44.000000000 +0000 @@ -2,40 +2,52 @@ Section: python Priority: optional Maintainer: Jeremy T. Bouse -Uploaders: Guido Guenther -Build-Depends: debhelper (>> 8), +Uploaders: Guido Guenther , + Mike Gabriel , +X-Python-Version: >= 2.6 +X-Python3-Version: >= 3.2 +Build-Depends: debhelper (>> 9), + dh-python, python-all (>= 2.6.6-3~), python-crypto (>= 2.1.0-2), + python-ecdsa (>= 0.11), python-setuptools, - python-epydoc -Standards-Version: 3.9.4 + python3-all, + python3-crypto, + python3-ecdsa (>= 0.11), + python3-setuptools +Standards-Version: 3.9.5 Homepage: https://github.com/paramiko/paramiko/ -Vcs-Git: git://git.debian.org/collab-maint/paramiko.git -Vcs-Browser: http://git.debian.org/?p=collab-maint/paramiko.git +Vcs-Git: https://github.com/jbouse-debian/paramiko.git +Vcs-Browser: https://github.com/jbouse-debian/paramiko XS-Testsuite: autopkgtest -Package: paramiko-doc -Section: doc +Package: python-paramiko +Package-Type: deb Architecture: all -Depends: ${misc:Depends} -Description: Make ssh v2 connections with Python (Documentation) +Depends: python-crypto (>= 2.1.0-2), + ${misc:Depends}, + ${python:Depends} +Provides: ${python:Provides} +Description: Make ssh v2 connections with Python (Python 2) This is a library for making SSH2 connections (client or server). Emphasis is on using SSH2 as an alternative to SSL for making secure connections between Python scripts. All major ciphers and hash methods are supported. SFTP client and server mode are both supported too. . - This is the documentation for the package. + This is the Python 2 version of the package. -Package: python-paramiko +Package: python3-paramiko +Package-Type: deb Architecture: all -Depends: ${misc:Depends}, - ${python:Depends}, - python-crypto (>= 2.1.0-2) -Provides: ${python:Provides} -Description: Make ssh v2 connections with Python (Python 2) +Depends: python3-crypto (>= 2.1.0-2), + ${misc:Depends}, + ${python3:Depends} +Provides: ${python3:Provides} +Description: Make ssh v2 connections with Python (Python 3) This is a library for making SSH2 connections (client or server). Emphasis is on using SSH2 as an alternative to SSL for making secure connections between Python scripts. All major ciphers and hash methods are supported. SFTP client and server mode are both supported too. . - This is the Python 2 version of the package. + This is the Python 3 version of the package. diff -Nru paramiko-1.10.1/debian/gbp.conf paramiko-1.15.1/debian/gbp.conf --- paramiko-1.10.1/debian/gbp.conf 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/gbp.conf 2014-09-23 16:52:36.000000000 +0000 @@ -1,10 +1,10 @@ [DEFAULT] pristine-tar = True -[git-buildpackage] +[buildpackage] sign-tags = True -postbuild = lintian $GBP_CHANGES_FILE +postbuild = lintian -I $GBP_CHANGES_FILE && echo "Lintian OK" -[git-dch] +[dch] meta = True git-author = True diff -Nru paramiko-1.10.1/debian/paramiko-doc.doc-base paramiko-1.15.1/debian/paramiko-doc.doc-base --- paramiko-1.10.1/debian/paramiko-doc.doc-base 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/paramiko-doc.doc-base 2014-09-23 00:55:46.000000000 +0000 @@ -1,9 +1,9 @@ Document: python-paramiko Title: Paramiko -Author: Robey Pointer +Author: Jeff Forcier Abstract: A Python interface to the paramiko SSH2 protocol library Section: Programming/Python Format: html -Index: /usr/share/doc/paramiko-doc/docs/index.html -Files: /usr/share/doc/paramiko-doc/docs/*.html +Index: /usr/share/doc/paramiko-doc/html/index.html +Files: /usr/share/doc/paramiko-doc/html/*.html diff -Nru paramiko-1.10.1/debian/paramiko-doc.docs paramiko-1.15.1/debian/paramiko-doc.docs --- paramiko-1.10.1/debian/paramiko-doc.docs 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/paramiko-doc.docs 2014-09-23 00:55:46.000000000 +0000 @@ -1,2 +1,2 @@ README -docs +html diff -Nru paramiko-1.10.1/debian/paramiko-doc.examples paramiko-1.15.1/debian/paramiko-doc.examples --- paramiko-1.10.1/debian/paramiko-doc.examples 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/debian/paramiko-doc.examples 2014-09-23 00:55:46.000000000 +0000 @@ -0,0 +1 @@ +demos/* diff -Nru paramiko-1.10.1/debian/paramiko-doc.links paramiko-1.15.1/debian/paramiko-doc.links --- paramiko-1.10.1/debian/paramiko-doc.links 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/debian/paramiko-doc.links 2014-09-23 17:57:19.000000000 +0000 @@ -0,0 +1,2 @@ +usr/share/javascript/jquery/jquery.js usr/share/doc/paramiko-doc/html/_static/jquery.js +usr/share/javascript/underscore/underscore.js usr/share/doc/paramiko-doc/html/_static/underscore.js diff -Nru paramiko-1.10.1/debian/patches/0001-Localize-generated-documentation.patch paramiko-1.15.1/debian/patches/0001-Localize-generated-documentation.patch --- paramiko-1.10.1/debian/patches/0001-Localize-generated-documentation.patch 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/debian/patches/0001-Localize-generated-documentation.patch 2014-09-23 18:01:09.000000000 +0000 @@ -0,0 +1,28 @@ +From: "Jeremy T. Bouse" +Date: Tue, 23 Sep 2014 12:54:17 -0400 +Subject: Localize generated documentation + +Remove external resource references from the Sphinx layout to +generate local documentation to package under /usr/share/doc for +the paramiko-doc. +--- + sites/shared_conf.py | 6 +----- + 1 file changed, 1 insertion(+), 5 deletions(-) + +diff --git a/sites/shared_conf.py b/sites/shared_conf.py +index 4a6a5c4..343b579 100644 +--- a/sites/shared_conf.py ++++ b/sites/shared_conf.py +@@ -10,11 +10,7 @@ extensions = ['alabaster', 'sphinx.ext.intersphinx'] + html_theme = 'alabaster' + html_theme_options = { + 'description': "A Python implementation of SSHv2.", +- 'github_user': 'paramiko', +- 'github_repo': 'paramiko', +- 'gratipay_user': 'bitprophet', +- 'analytics_id': 'UA-18486793-2', +- 'travis_button': True, ++ 'github_button': False, + } + html_sidebars = { + '**': [ diff -Nru paramiko-1.10.1/debian/patches/0001-Remove-upstream-Makefile.patch paramiko-1.15.1/debian/patches/0001-Remove-upstream-Makefile.patch --- paramiko-1.10.1/debian/patches/0001-Remove-upstream-Makefile.patch 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/patches/0001-Remove-upstream-Makefile.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -From: Jeremy T. Bouse -Date: Sat, 25 May 2013 01:05:44 -0400 -Subject: [PATCH] Remove upstream Makefile - -The upstream Makefile is non-functional for package building. It is -meant for upstream release management rather than package management. -Removing it to get it out of the way and allow debhelper to build using -python setuptools. ---- - Makefile | 15 --------------- - 1 files changed, 0 insertions(+), 15 deletions(-) - delete mode 100644 Makefile - -diff --git a/Makefile b/Makefile -deleted file mode 100644 -index 572f867..0000000 ---- a/Makefile -+++ /dev/null -@@ -1,15 +0,0 @@ --release: docs -- python setup.py sdist register upload -- --docs: paramiko/* -- epydoc --no-private -o docs/ paramiko -- --clean: -- rm -rf build dist docs -- rm -f MANIFEST *.log demos/*.log -- rm -f paramiko/*.pyc -- rm -f test.log -- rm -rf paramiko.egg-info -- --test: -- python ./test.py --- diff -Nru paramiko-1.10.1/debian/patches/series paramiko-1.15.1/debian/patches/series --- paramiko-1.10.1/debian/patches/series 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/patches/series 2014-09-23 18:01:09.000000000 +0000 @@ -1 +1 @@ -0001-Remove-upstream-Makefile.patch +0001-Localize-generated-documentation.patch diff -Nru paramiko-1.10.1/debian/python-paramiko.examples paramiko-1.15.1/debian/python-paramiko.examples --- paramiko-1.10.1/debian/python-paramiko.examples 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/python-paramiko.examples 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -demos/* diff -Nru paramiko-1.10.1/debian/python-paramiko.install paramiko-1.15.1/debian/python-paramiko.install --- paramiko-1.10.1/debian/python-paramiko.install 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/python-paramiko.install 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -usr/lib/python2* diff -Nru paramiko-1.10.1/debian/rules paramiko-1.15.1/debian/rules --- paramiko-1.10.1/debian/rules 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/rules 2014-10-21 08:28:56.000000000 +0000 @@ -1,37 +1,6 @@ #!/usr/bin/make -f -PYTHON2=$(shell pyversions -vr) +export PYBUILD_NAME=paramiko %: - dh $@ --with python2 - -ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) -test-python%: - python$* setup.py test -vv - -override_dh_auto_test: $(PYTHON2:%=test-python%) -endif - -build-python%: - python$* setup.py build - -override_dh_auto_build: $(PYTHON2:%=build-python%) - dh_auto_build - -install-python%: - python$* setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb - -override_dh_auto_install: $(PYTHON2:%=install-python%) - dh_auto_install - -override_dh_installdocs: - epydoc --no-private -o docs/ paramiko - dh_installdocs - -# Commands not to run -override_dh_installcatalogs override_dh_installcron: -override_dh_installdebconf override_dh_installemacsen override_dh_installifupdown: -override_dh_installinfo override_dh_installinit override_dh_installmenu override_dh_installmime: -override_dh_installmodules override_dh_installlogcheck override_dh_installlogrotate: -override_dh_installpam override_dh_installppp override_dh_installudev override_dh_installwm: -override_dh_installxfonts override_dh_gconf override_dh_icons override_dh_perl override_dh_usrlocal: + dh $@ --with python2,python3 --buildsystem=pybuild diff -Nru paramiko-1.10.1/debian/source/options paramiko-1.15.1/debian/source/options --- paramiko-1.10.1/debian/source/options 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/debian/source/options 2014-09-23 00:55:46.000000000 +0000 @@ -0,0 +1,2 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" +extend-diff-ignore = "^docs/index.html" diff -Nru paramiko-1.10.1/debian/tests/upstream paramiko-1.15.1/debian/tests/upstream --- paramiko-1.10.1/debian/tests/upstream 2013-11-07 13:17:38.000000000 +0000 +++ paramiko-1.15.1/debian/tests/upstream 2014-09-23 00:55:46.000000000 +0000 @@ -2,3 +2,4 @@ set -e python ./test.py --verbose +python3 ./test.py --verbose diff -Nru paramiko-1.10.1/demos/demo_keygen.py paramiko-1.15.1/demos/demo_keygen.py --- paramiko-1.10.1/demos/demo_keygen.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/demo_keygen.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -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 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 @@ 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 @@ 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 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 @@ 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 -Nru paramiko-1.10.1/demos/demo.py paramiko-1.15.1/demos/demo.py --- paramiko-1.10.1/demos/demo.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/demo.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -26,12 +26,15 @@ import select import socket import sys -import threading 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): @@ -46,24 +49,24 @@ 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: @@ -74,7 +77,7 @@ 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: @@ -97,9 +100,9 @@ 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: @@ -110,8 +113,8 @@ 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) @@ -120,7 +123,7 @@ try: t.start_client() except paramiko.SSHException: - print '*** SSH negotiation failed.' + print('*** SSH negotiation failed.') sys.exit(1) try: @@ -129,25 +132,25 @@ 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 @@ -155,21 +158,20 @@ 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 -Nru paramiko-1.10.1/demos/demo_server.py paramiko-1.15.1/demos/demo_server.py --- paramiko-1.10.1/demos/demo_server.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/demo_server.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -27,6 +27,7 @@ import traceback import paramiko +from paramiko.py3compat import b, u, decodebytes # setup logging @@ -35,17 +36,17 @@ 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,13 +62,46 @@ 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 + + def check_auth_gssapi_with_mic(self, username, + gss_authenticated=paramiko.AUTH_FAILED, + cc_file=None): + """ + .. 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 + 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() @@ -78,52 +112,55 @@ return True +DoGSSAPIKeyExchange = True + # now connect 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) + t = paramiko.Transport(client, gss_kex=DoGSSAPIKeyExchange) + t.set_gss_host(socket.getfqdn("")) 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 +172,8 @@ 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 -Nru paramiko-1.10.1/demos/demo_sftp.py paramiko-1.15.1/demos/demo_sftp.py --- paramiko-1.10.1/demos/demo_sftp.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/demo_sftp.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -28,11 +28,17 @@ import traceback import paramiko +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: @@ -40,23 +46,26 @@ 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: hostname, portstr = hostname.split(':') - port = int(portstr) + Port = int(portstr) # 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)) +if not UseGSSAPI: + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) +else: + password = None # get host key, if we know one @@ -69,39 +78,44 @@ # 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 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 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 +123,8 @@ 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 -Nru paramiko-1.10.1/demos/demo_simple.py paramiko-1.15.1/demos/demo_simple.py --- paramiko-1.10.1/demos/demo_simple.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/demo_simple.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -25,13 +25,21 @@ 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 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 = '' @@ -40,11 +48,11 @@ 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: hostname, portstr = hostname.split(':') port = int(portstr) @@ -53,29 +61,40 @@ # 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)) +if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange): + password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) # now, connect and use paramiko Client to negotiate SSH2 across the connection try: client = paramiko.SSHClient() client.load_system_host_keys() - client.set_missing_host_key_policy(paramiko.WarningPolicy) - print '*** Connecting...' - client.connect(hostname, port, username, password) + 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) + else: + # SSPI works only with the FQDN of the target host + hostname = socket.getfqdn(hostname) + 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!' - 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 -Nru paramiko-1.10.1/demos/forward.py paramiko-1.15.1/demos/forward.py --- paramiko-1.10.1/demos/forward.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/forward.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -30,7 +30,11 @@ 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 @@ 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))) @@ -78,9 +82,11 @@ if len(data) == 0: break self.request.send(data) + + peername = self.request.getpeername() chan.close() self.request.close() - verbose('Tunnel closed from %r' % (self.request.getpeername(),)) + verbose('Tunnel closed from %r' % (peername,)) def forward_tunnel(local_port, remote_host, remote_port, transport): @@ -96,7 +102,7 @@ def verbose(s): if g_verbose: - print s + print(s) HELP = """\ @@ -163,8 +169,8 @@ 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])) @@ -172,7 +178,7 @@ 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 -Nru paramiko-1.10.1/demos/interactive.py paramiko-1.15.1/demos/interactive.py --- paramiko-1.10.1/demos/interactive.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/interactive.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -19,6 +19,7 @@ import socket import sys +from paramiko.py3compat import u # windows does not have termios... try: @@ -49,9 +50,9 @@ 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 -Nru paramiko-1.10.1/demos/rforward.py paramiko-1.15.1/demos/rforward.py --- paramiko-1.10.1/demos/rforward.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/demos/rforward.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -46,7 +46,7 @@ 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 verbose(s): if g_verbose: - print s + print(s) HELP = """\ @@ -150,8 +150,8 @@ 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 @@ 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 -Nru paramiko-1.10.1/dev-requirements.txt paramiko-1.15.1/dev-requirements.txt --- paramiko-1.10.1/dev-requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/dev-requirements.txt 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,9 @@ +# Older junk +tox>=1.4,<1.5 +# For newer tasks like building Sphinx docs. +invoke>=0.7.0,<0.8 +invocations>=0.5.0 +sphinx>=1.1.3 +alabaster>=0.6.1 +releases>=0.5.2 +wheel==0.23.0 diff -Nru paramiko-1.10.1/fabfile.py paramiko-1.15.1/fabfile.py --- paramiko-1.10.1/fabfile.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/fabfile.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -from fabric.api import task, sudo, env -from fabric.contrib.project import rsync_project - - -@task -def upload_docs(): - target = "/var/www/paramiko.org" - staging = "/tmp/paramiko_docs" - sudo("mkdir -p %s" % staging) - sudo("chown -R %s %s" % (env.user, staging)) - sudo("rm -rf %s/*" % target) - rsync_project(local_dir='docs/', remote_dir=staging, delete=True) - sudo("cp -R %s/* %s/" % (staging, target)) diff -Nru paramiko-1.10.1/.gitignore paramiko-1.15.1/.gitignore --- paramiko-1.10.1/.gitignore 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/.gitignore 2014-09-22 18:31:58.000000000 +0000 @@ -5,3 +5,7 @@ paramiko.egg-info/ test.log docs/ +demos/*.log +!sites/docs +_build +.coverage diff -Nru paramiko-1.10.1/Makefile paramiko-1.15.1/Makefile --- paramiko-1.10.1/Makefile 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/Makefile 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -release: docs - python setup.py sdist register upload - -docs: paramiko/* - epydoc --no-private -o docs/ paramiko - -clean: - rm -rf build dist docs - rm -f MANIFEST *.log demos/*.log - rm -f paramiko/*.pyc - rm -f test.log - rm -rf paramiko.egg-info - -test: - python ./test.py diff -Nru paramiko-1.10.1/NEWS paramiko-1.15.1/NEWS --- paramiko-1.10.1/NEWS 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/NEWS 2014-09-22 18:31:58.000000000 +0000 @@ -9,57 +9,13 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/. -Releases -======== -v1.10.1 (5th Apr 2013) ----------------------- +**PLEASE NOTE:** For changes in 1.10.x and newer releases, please see +www.paramiko.org's changelog page, or the source file, sites/www/changelog.rst + -* #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. -* #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. - -v1.10.0 (1st Mar 2013) --------------------- - -* #66: Batch SFTP writes to help speed up file transfers. Thanks to Olle - Lundberg for the patch. -* #133: 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. -* #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. -* #110: Honor SSH config `AddressFamily` setting when looking up local - host's FQDN. Thanks to John Hensley for the patch. -* #128: Defer FQDN resolution until needed, when parsing SSH config files. - Thanks to Parantapa Bhattacharya for catch & patch. -* #102: 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. -* #127: Turn `SFTPFile` into a context manager. Thanks to Michael Williamson - for the patch. -* #116: Limit `Message.get_bytes` to an upper bound of 1MB to protect against - potential DoS vectors. Thanks to `@mvschaik` for catch & patch. -* #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. -* #71: Add `SFTPClient.putfo` and `.getfo` methods to allow direct - uploading/downloading of file-like objects. Thanks to Eric Buehl for the - patch. -* #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. -* #94: Remove duplication of SSH port constant. Thanks to Olle Lundberg for the - catch. -* #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. +Releases +======== v1.9.0 (6th Nov 2012) --------------------- diff -Nru paramiko-1.10.1/paramiko/agent.py paramiko-1.15.1/paramiko/agent.py --- paramiko-1.10.1/paramiko/agent.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/agent.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,7 +17,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -SSH Agent interface for Unix clients. +SSH Agent interface """ import os @@ -29,28 +29,22 @@ import tempfile import stat from select import select +from paramiko.common import asbytes, io_sleep +from paramiko.py3compat import byte_chr from paramiko.ssh_exception import SSHException from paramiko.message import Message from paramiko.pkey import PKey -from paramiko.channel import Channel -from paramiko.common import io_sleep 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): - """ - Client interface for using private keys from an SSH agent running on the - local machine. If an SSH agent is running, this class can be used to - connect to it and retreive L{PKey} objects which can be used when - attempting to authenticate to remote SSH servers. - Because the SSH agent protocol uses environment variables and unix-domain - sockets, this probably doesn't work on Windows. It does work on most - posix platforms though (Linux and MacOS X, for example). - """ + +class AgentSSH(object): def __init__(self): self._conn = None self._keys = () @@ -61,19 +55,20 @@ no SSH agent was running (or it couldn't be contacted), an empty list will be returned. - @return: a list of keys available on the SSH agent - @rtype: tuple of L{AgentKey} + :return: + a tuple of `.AgentKey` objects representing keys available on the + SSH agent """ return self._keys 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 +78,7 @@ 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])) @@ -100,8 +95,11 @@ result += extra return result + class AgentProxyThread(threading.Thread): - """ Class in charge of communication between two chan """ + """ + Class in charge of communication between two channels. + """ def __init__(self, agent): threading.Thread.__init__(self, target=self.run) self._agent = agent @@ -109,7 +107,7 @@ def run(self): try: - (r,addr) = self.get_connection() + (r, addr) = self.get_connection() self.__inr = r self.__addr = addr self._agent.connect() @@ -146,6 +144,7 @@ self.__inr.close() self._agent._conn.close() + class AgentLocalProxy(AgentProxyThread): """ Class to be used when wanting to ask a local SSH Agent being @@ -155,18 +154,20 @@ AgentProxyThread.__init__(self, agent) def get_connection(self): - """ Return a pair of socket object and string address - May Block ! + """ + Return a pair of socket object and string address. + + May block! """ conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: conn.bind(self._agent._get_filename()) conn.listen(1) - (r,addr) = conn.accept() - return (r, addr) + (r, addr) = conn.accept() + return r, addr except: raise - return None + class AgentRemoteProxy(AgentProxyThread): """ @@ -177,22 +178,20 @@ self.__chan = chan def get_connection(self): - """ - Class to be used when wanting to ask a local SSH Agent being - asked from a remote fake agent (so use a unix socket for ex.) - """ - return (self.__chan, None) + return self.__chan, None + class AgentClientProxy(object): """ Class proxying request as a client: - -> client ask for a request_forward_agent() - -> server creates a proxy and a fake SSH Agent - -> server ask for establishing a connection when needed, + + #. client ask for a request_forward_agent() + #. server creates a proxy and a fake SSH Agent + #. server ask for establishing a connection when needed, calling the forward_agent_handler at client side. - -> the forward_agent_handler launch a thread for connecting + #. the forward_agent_handler launch a thread for connecting the remote fake agent and the local agent - -> Communication occurs ... + #. Communication occurs ... """ def __init__(self, chanRemote): self._conn = None @@ -205,7 +204,7 @@ def connect(self): """ - Method automatically called by the run() method of the AgentProxyThread + Method automatically called by ``AgentProxyThread.run``. """ if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -215,7 +214,7 @@ # 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: @@ -236,11 +235,12 @@ if self._conn is not None: self._conn.close() + class AgentServerProxy(AgentSSH): """ - @param t : transport used for the Forward for SSH Agent communication + :param .Transport t: Transport used for SSH Agent communication forwarding - @raise SSHException: mostly if we lost the agent + :raises SSHException: mostly if we lost the agent """ def __init__(self, t): AgentSSH.__init__(self) @@ -255,11 +255,11 @@ self.close() def connect(self): - conn_sock = self.__t.open_forward_agent_channel() - if conn_sock is None: - raise SSHException('lost ssh-agent') - conn_sock.set_name('auth-agent') - self._connect(conn_sock) + conn_sock = self.__t.open_forward_agent_channel() + if conn_sock is None: + raise SSHException('lost ssh-agent') + conn_sock.set_name('auth-agent') + self._connect(conn_sock) def close(self): """ @@ -276,16 +276,15 @@ """ Helper for the environnement under unix - @return: the SSH_AUTH_SOCK Environnement variables - @rtype: dict + :return: + a dict containing the ``SSH_AUTH_SOCK`` environnement variables """ - env = {} - env['SSH_AUTH_SOCK'] = self._get_filename() - return env + return {'SSH_AUTH_SOCK': self._get_filename()} def _get_filename(self): return self._file + class AgentRequestHandler(object): def __init__(self, chanClient): self._conn = None @@ -303,27 +302,22 @@ for p in self.__clientProxys: p.close() + class Agent(AgentSSH): """ Client interface for using private keys from an SSH agent running on the local machine. If an SSH agent is running, this class can be used to - connect to it and retreive L{PKey} objects which can be used when + connect to it and retreive `.PKey` objects which can be used when attempting to authenticate to remote SSH servers. - Because the SSH agent protocol uses environment variables and unix-domain - sockets, this probably doesn't work on Windows. It does work on most - posix platforms though (Linux and MacOS X, for example). - """ + Upon initialization, a session with the local machine's SSH agent is + opened, if one is running. If no agent is running, initialization will + succeed, but `get_keys` will return an empty tuple. + :raises SSHException: + if an SSH agent is found, but speaks an incompatible protocol + """ def __init__(self): - """ - Open a session with the local machine's SSH agent, if one is running. - If no agent is running, initialization will succeed, but L{get_keys} - will return an empty tuple. - - @raise SSHException: if an SSH agent is found, but speaks an - incompatible protocol - """ AgentSSH.__init__(self) if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): @@ -334,7 +328,7 @@ # 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: @@ -350,31 +344,34 @@ """ self._close() + class AgentKey(PKey): """ Private key held in a local SSH agent. This type of key can be used for authenticating to a remote server (signing). Most other key operations work as expected. """ - 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): + def sign_ssh_data(self, 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 -Nru paramiko-1.10.1/paramiko/auth_handler.py paramiko-1.15.1/paramiko/auth_handler.py --- paramiko-1.10.1/paramiko/auth_handler.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/auth_handler.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,34 +17,45 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{AuthHandler} +`.AuthHandler` """ -import threading import weakref +from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \ + DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, \ + cMSG_USERAUTH_REQUEST, cMSG_SERVICE_ACCEPT, DEBUG, AUTH_SUCCESSFUL, INFO, \ + cMSG_USERAUTH_SUCCESS, cMSG_USERAUTH_FAILURE, AUTH_PARTIALLY_SUCCESSFUL, \ + 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, \ + 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 -# this helps freezing utils -import encodings.utf_8 - -from paramiko.common import * -from paramiko import util from paramiko.message import Message +from paramiko.py3compat import bytestring 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 self.authenticated = False self.auth_event = None self.auth_method = '' + self.banner = None self.password = None self.private_key = None self.interactive_handler = None @@ -52,7 +63,10 @@ # 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 @@ -93,7 +107,7 @@ self._request_auth() finally: self.transport.lock.release() - + def auth_interactive(self, username, handler, event, submethods=''): """ response_list = handler(title, instructions, prompt_list) @@ -108,24 +122,44 @@ self._request_auth() 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() - ### internals... - 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 +168,7 @@ 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 +178,14 @@ 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_boolean(True) 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: @@ -167,7 +201,7 @@ e = self.transport.get_exception() if e is None: e = AuthenticationException('Authentication failed.') - # this is horrible. python Exception isn't yet descended from + # this is horrible. Python Exception isn't yet descended from # object, so type(e) won't work. :( if issubclass(e.__class__, PartialAuthentication): return e.allowed_types @@ -175,11 +209,11 @@ 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,30 +221,99 @@ 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)) + sig = self.private_key.sign_ssh_data(blob) + m.add_string(sig) 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_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. 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 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() + 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) + 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\ + 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: @@ -224,16 +327,16 @@ 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) + m.add_boolean(True) else: - m.add_boolean(0) + m.add_boolean(False) self.auth_fail_count += 1 self.transport._send_message(m) if self.auth_fail_count >= 10: @@ -244,10 +347,10 @@ 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,17 +361,17 @@ 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) + m.add_boolean(False) self.transport._send_message(m) return 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() @@ -278,12 +381,14 @@ 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) 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 +399,7 @@ # 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 +409,11 @@ 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 +430,12 @@ # 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') @@ -343,6 +448,98 @@ # make interactive query instead of response self._interactive_query(result) return + elif method == "gssapi-with-mic" and gss_auth: + sshgss = GSSAuth(method) + # 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 sends + # 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 section 3.8 in http://www.ietf.org/rfc/rfc4462.txt + 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: + 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: + 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 @@ -352,7 +549,7 @@ self.transport._log(INFO, 'Authentication (%s) successful!' % self.auth_method) self.authenticated = True self.transport._auth_trigger() - if self.auth_event != None: + if self.auth_event is not None: self.auth_event.set() def _parse_userauth_failure(self, m): @@ -370,29 +567,30 @@ self.transport._log(INFO, 'Authentication (%s) failed.' % self.auth_method) self.authenticated = False self.username = None - if self.auth_event != None: + if self.auth_event is not None: self.auth_event.set() def _parse_userauth_banner(self, m): banner = m.get_string() + self.banner = banner 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 +602,13 @@ 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, @@ -423,4 +620,3 @@ MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request, MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response, } - diff -Nru paramiko-1.10.1/paramiko/ber.py paramiko-1.15.1/paramiko/ber.py --- paramiko-1.10.1/paramiko/ber.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/ber.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -15,9 +15,10 @@ # 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 paramiko.common import max_byte, zero_byte +from paramiko.py3compat import b, byte_ord, byte_chr, long - -import util +import paramiko.util as util class BERException (Exception): @@ -29,13 +30,16 @@ 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 @@ 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 @@ 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... @@ -67,12 +71,12 @@ t = size & 0x7f if self.idx + t > len(self.content): return None - size = util.inflate_long(self.content[self.idx : self.idx + t], True) + size = util.inflate_long(self.content[self.idx: self.idx + t], True) self.idx += t if self.idx + size > len(self.content): # can't fit return None - data = self.content[self.idx : self.idx + size] + data = self.content[self.idx: self.idx + size] self.idx += size # now switch on id if ident == 0x30: @@ -87,9 +91,9 @@ def decode_sequence(data): out = [] - b = BER(data) + ber = BER(data) while True: - x = b.decode_next() + x = ber.decode_next() if x is None: break out.append(x) @@ -98,20 +102,20 @@ 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: @@ -122,8 +126,8 @@ raise BERException('Unknown type for encoding: %s' % repr(type(x))) def encode_sequence(data): - b = BER() + ber = BER() for item in data: - b.encode(item) - return str(b) + ber.encode(item) + return ber.asbytes() encode_sequence = staticmethod(encode_sequence) diff -Nru paramiko-1.10.1/paramiko/buffered_pipe.py paramiko-1.15.1/paramiko/buffered_pipe.py --- paramiko-1.10.1/paramiko/buffered_pipe.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/buffered_pipe.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,7 +17,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -Attempt to generalize the "feeder" part of a Channel: an object which can be +Attempt to generalize the "feeder" part of a `.Channel`: an object which can be read from and closed, but is reading from a buffer fed by another thread. The read operations are blocking and can have a timeout set. """ @@ -25,11 +25,12 @@ import array import threading import time +from paramiko.py3compat import PY2, b class PipeTimeout (IOError): """ - Indicates that a timeout was reached on a read from a L{BufferedPipe}. + Indicates that a timeout was reached on a read from a `.BufferedPipe`. """ pass @@ -38,7 +39,7 @@ """ A buffer that obeys normal read (with timeout) & close semantics for a file or socket, but is fed data from another thread. This is used by - L{Channel}. + `.Channel`. """ def __init__(self): @@ -48,14 +49,26 @@ 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 buffer has been closed), the event will be set. When no data is ready, the event will be cleared. - @param event: the event to set/clear - @type event: Event + :param threading.Event event: the event to set/clear """ self._event = event if len(self._buffer) > 0: @@ -68,14 +81,13 @@ Feed new data into this pipe. This method is assumed to be called from a separate thread, so synchronization is done. - @param data: the data to add - @type data: str + :param data: the data to add, as a `str` """ self._lock.acquire() 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() @@ -83,12 +95,12 @@ def read_ready(self): """ Returns true if data is buffered and ready to be read from this - feeder. A C{False} result does not mean that the feeder has closed; + feeder. A ``False`` result does not mean that the feeder has closed; it means you may need to wait before more data arrives. - @return: C{True} if a L{read} call would immediately return at least - one byte; C{False} otherwise. - @rtype: bool + :return: + ``True`` if a `read` call would immediately return at least one + byte; ``False`` otherwise. """ self._lock.acquire() try: @@ -102,26 +114,24 @@ """ Read data from the pipe. The return value is a string representing the data received. The maximum amount of data to be received at once - is specified by C{nbytes}. If a string of length zero is returned, + is specified by ``nbytes``. If a string of length zero is returned, the pipe has been closed. - The optional C{timeout} argument can be a nonnegative float expressing - seconds, or C{None} for no timeout. If a float is given, a - C{PipeTimeout} will be raised if the timeout period value has - elapsed before any data arrives. - - @param nbytes: maximum number of bytes to read - @type nbytes: int - @param timeout: maximum seconds to wait (or C{None}, the default, to - wait forever) - @type timeout: float - @return: data - @rtype: str - - @raise PipeTimeout: if a timeout was specified and no data was ready - before that timeout + The optional ``timeout`` argument can be a nonnegative float expressing + seconds, or ``None`` for no timeout. If a float is given, a + `.PipeTimeout` will be raised if the timeout period value has elapsed + before any data arrives. + + :param int nbytes: maximum number of bytes to read + :param float timeout: + maximum seconds to wait (or ``None``, the default, to wait forever) + :return: the read data, as a `str` + + :raises 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 +152,12 @@ # 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() @@ -158,12 +168,13 @@ """ Clear out the buffer and return all data that was in it. - @return: any data that was in the buffer prior to clearing it out - @rtype: str + :return: + any data that was in the buffer prior to clearing it out, as a + `str` """ 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() @@ -173,7 +184,7 @@ def close(self): """ - Close this pipe object. Future calls to L{read} after the buffer + Close this pipe object. Future calls to `read` after the buffer has been emptied will return immediately with an empty string. """ self._lock.acquire() @@ -189,12 +200,10 @@ """ Return the number of bytes buffered. - @return: number of bytes bufferes - @rtype: int + :return: number (`int`) of bytes buffered """ self._lock.acquire() try: return len(self._buffer) finally: self._lock.release() - diff -Nru paramiko-1.10.1/paramiko/channel.py paramiko-1.15.1/paramiko/channel.py --- paramiko-1.10.1/paramiko/channel.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/channel.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,50 +21,72 @@ """ import binascii -import sys +import os +import socket import time import threading -import socket -import os +from functools import wraps -from paramiko.common import * 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 from paramiko.message import Message +from paramiko.py3compat import bytes_types 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 +def open_only(func): + """ + 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 + ): + raise SSHException('Channel is not open') + return func(self, *args, **kwds) + return _check -class Channel (object): + +class Channel (ClosingContextManager): """ - A secure tunnel across an SSH L{Transport}. A Channel is meant to behave + 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 - python socket API. + Python socket API. Because SSH2 has a windowing kind of flow control, if you stop reading data from a Channel and its buffer fills up, the server will be unable to send you any more data until you read some of it. (This won't affect other channels on the same transport -- all channels on a single transport are flow-controlled independently.) Similarly, if the server isn't reading - data you send, calls to L{send} may block, unless you set a timeout. This + 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): """ Create a new channel. The channel is not associated with any - particular session or L{Transport} until the Transport attaches it. + particular session or `.Transport` until the Transport attaches it. Normally you would only call this method from the constructor of a - subclass of L{Channel}. + subclass of `.Channel`. - @param chanid: the ID of this channel, as passed by an existing - L{Transport}. - @type chanid: int + :param int chanid: + the ID of this channel, as passed by an existing `.Transport`. """ self.chanid = chanid self.remote_chanid = 0 @@ -94,18 +116,16 @@ 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. - - @rtype: str """ out = ' 0: out += ' in-buffer=%d' % (len(self.in_buffer),) out += ' -> ' + repr(self.transport) out += '>' return out + @open_only def get_pty(self, term='vt100', width=80, height=24, width_pixels=0, height_pixels=0): """ Request a pseudo-terminal from the server. This is usually used right after creating a client channel, to ask the server to provide some - basic terminal semantics for a shell invoked with L{invoke_shell}. + basic terminal semantics for a shell invoked with `invoke_shell`. It isn't necessary (or desirable) to call this method if you're going - to exectue a single command with L{exec_command}. + to exectue a single command with `exec_command`. - @param term: the terminal type to emulate (for example, C{'vt100'}) - @type term: str - @param width: width (in characters) of the terminal screen - @type width: int - @param height: height (in characters) of the terminal screen - @type height: int - @param width_pixels: width (in pixels) of the terminal screen - @type width_pixels: int - @param height_pixels: height (in pixels) of the terminal screen - @type height_pixels: int - - @raise SSHException: if the request was rejected or the channel was - closed + :param str term: the terminal type to emulate (for example, ``'vt100'``) + :param int width: width (in characters) of the terminal screen + :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 """ - 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,58 +171,55 @@ 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() + @open_only def invoke_shell(self): """ 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 L{get_pty} before this, in which case the + + 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. - - @raise SSHException: if the request was rejected or the channel was + + :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(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('shell') - m.add_boolean(1) + m.add_boolean(True) self._event_pending() self.transport._send_user_message(m) self._wait_for_event() + @open_only def exec_command(self, command): """ 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. - @param command: a shell command to execute. - @type command: str + :param str command: a shell command to execute. - @raise SSHException: if the request was rejected or the channel was + :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(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('exec') m.add_boolean(True) @@ -217,25 +228,23 @@ self.transport._send_user_message(m) self._wait_for_event() + @open_only def invoke_subsystem(self, subsystem): """ - Request a subsystem on the server (for example, C{sftp}). If the + 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. - @param subsystem: name of the subsystem being requested. - @type subsystem: str + :param str subsystem: name of the subsystem being requested. - @raise SSHException: if the request was rejected or the channel was - closed + :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(chr(MSG_CHANNEL_REQUEST)) + m.add_byte(cMSG_CHANNEL_REQUEST) m.add_int(self.remote_chanid) m.add_string('subsystem') m.add_boolean(True) @@ -244,27 +253,22 @@ self.transport._send_user_message(m) self._wait_for_event() + @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 - height of the terminal emulation created in a previous L{get_pty} call. + height of the terminal emulation created in a previous `get_pty` call. - @param width: new width (in characters) of the terminal screen - @type width: int - @param height: new height (in characters) of the terminal screen - @type height: int - @param width_pixels: new width (in pixels) of the terminal screen - @type width_pixels: int - @param height_pixels: new height (in pixels) of the terminal screen - @type height_pixels: int + :param int width: new width (in characters) of the terminal screen + :param int height: new height (in characters) of the terminal screen + :param int width_pixels: new width (in pixels) of the terminal screen + :param int height_pixels: new height (in pixels) of the terminal screen - @raise SSHException: if the request was rejected or the channel was - closed + :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(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) @@ -278,27 +282,27 @@ """ Return true if the remote process has exited and returned an exit status. You may use this to poll the process status if you don't - want to block in L{recv_exit_status}. Note that the server may not + 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 L{recv_exit_status} will return immediately - @rtype: bool - @since: 1.7.3 + + :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 - mostly useful for retrieving the reults of an L{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. - - @return: the exit code of the process on the server. - @rtype: int - - @since: 1.2 + + :return: the exit code (as an `int`) of the process on the server. + + .. versionadded:: 1.2 """ self.status_event.wait() assert self.status_event.isSet() @@ -310,72 +314,69 @@ 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 status: the exit code of the process - @type status: int - - @since: 1.2 + + :param int status: the exit code of the process + + .. versionadded:: 1.2 """ # 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) m.add_int(status) self.transport._send_user_message(m) - + + @open_only 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). + 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 incoming x11 connections, which may be retrieved using - L{Transport.accept}. The handler's calling signature is:: - + `.Transport.accept`. The handler's calling signature is:: + handler(channel: Channel, (address: str, port: int)) - - @param screen_number: the x11 screen number (0, 10, etc) - @type screen_number: int - @param auth_protocol: the name of the X11 authentication method used; - if none is given, C{"MIT-MAGIC-COOKIE-1"} is used - @type auth_protocol: str - @param auth_cookie: hexadecimal string containing the x11 auth cookie; - if none is given, a secure random 128-bit value is generated - @type auth_cookie: str - @param single_connection: if True, only a single x11 connection will be - forwarded (by default, any number of x11 connections can arrive - over this session) - @type single_connection: bool - @param handler: an optional handler to use for incoming X11 connections - @type handler: function - @return: the auth_cookie used + + :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 + :param str auth_cookie: + hexadecimal string containing the x11 auth cookie; if none is + given, a secure random 128-bit value is generated + :param bool single_connection: + if True, only a single x11 connection will be forwarded (by + default, any number of x11 connections can arrive over this + session) + :param function handler: + 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: - auth_cookie = binascii.hexlify(self.transport.rng.read(16)) + auth_cookie = binascii.hexlify(os.urandom(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) @@ -389,24 +390,21 @@ self.transport._set_x11_handler(handler) return auth_cookie + @open_only def request_forward_agent(self, handler): """ Request for a forward SSH Agent on this channel. - This is only valid for an ssh-agent from openssh !!! + This is only valid for an ssh-agent from OpenSSH !!! - @param handler: a required handler to use for incoming SSH Agent connections - @type handler: function + :param function handler: + a required handler to use for incoming SSH Agent connections - @return: if we are ok or not (at that time we always return ok) - @rtype: boolean + :return: True if we are ok, else False (at that time we always return ok) - @raise: SSHException in case of channel problem. + :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(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) @@ -416,10 +414,7 @@ def get_transport(self): """ - Return the L{Transport} associated with this channel. - - @return: the L{Transport} that was used to create this channel. - @rtype: L{Transport} + Return the `.Transport` associated with this channel. """ return self.transport @@ -427,57 +422,51 @@ """ Set a name for this channel. Currently it's only used to set the name of the channel in logfile entries. The name can be fetched with the - L{get_name} method. + `get_name` method. - @param name: new channel name - @type name: str + :param str name: new channel name """ self._name = name def get_name(self): """ - Get the name of this channel that was previously set by L{set_name}. - - @return: the name of this channel. - @rtype: str + Get the name of this channel that was previously set by `set_name`. """ return self._name def get_id(self): """ - Return the ID # for this channel. The channel ID is unique across - a L{Transport} and usually a small number. It's also the number - passed to L{ServerInterface.check_channel_request} when determining - whether to accept a channel request in server mode. + Return the `int` ID # for this channel. - @return: the ID of this channel. - @rtype: int + 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 C{False}, but in some cases it may be convenient to + The default is ``False``, but in some cases it may be convenient to have both streams combined. - - If this is C{False}, and L{exec_command} is called (or C{invoke_shell} - with no pty), output to stderr will not show up through the L{recv} - and L{recv_ready} calls. You will have to use L{recv_stderr} and - L{recv_stderr_ready} to get stderr output. - - If this is C{True}, data will never show up via L{recv_stderr} or - L{recv_stderr_ready}. - - @param combine: C{True} if stderr output should be combined into - stdout on this channel. - @type combine: bool - @return: previous setting. - @rtype: bool - - @since: 1.1 + + 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 = '' + data = bytes() self.lock.acquire() try: old = self.combine_stderr @@ -491,57 +480,51 @@ self._feed(data) return old - ### socket API - def settimeout(self, timeout): """ - Set a timeout on blocking read/write operations. The C{timeout} - argument can be a nonnegative float expressing seconds, or C{None}. If + Set a timeout on blocking read/write operations. The ``timeout`` + argument can be a nonnegative float expressing seconds, or ``None``. If a float is given, subsequent channel read/write operations will raise a timeout exception if the timeout period value has elapsed before the - operation has completed. Setting a timeout of C{None} disables + operation has completed. Setting a timeout of ``None`` disables timeouts on socket operations. - C{chan.settimeout(0.0)} is equivalent to C{chan.setblocking(0)}; - C{chan.settimeout(None)} is equivalent to C{chan.setblocking(1)}. + ``chan.settimeout(0.0)`` is equivalent to ``chan.setblocking(0)``; + ``chan.settimeout(None)`` is equivalent to ``chan.setblocking(1)``. - @param timeout: seconds to wait for a pending read/write operation - before raising C{socket.timeout}, or C{None} for no timeout. - @type timeout: float + :param float timeout: + seconds to wait for a pending read/write operation before raising + ``socket.timeout``, or ``None`` for no timeout. """ self.timeout = timeout def gettimeout(self): """ Returns the timeout in seconds (as a float) associated with socket - operations, or C{None} if no timeout is set. This reflects the last - call to L{setblocking} or L{settimeout}. - - @return: timeout in seconds, or C{None}. - @rtype: float + operations, or ``None`` if no timeout is set. This reflects the last + call to `setblocking` or `settimeout`. """ return self.timeout def setblocking(self, blocking): """ - Set blocking or non-blocking mode of the channel: if C{blocking} is 0, + Set blocking or non-blocking mode of the channel: if ``blocking`` is 0, the channel is set to non-blocking mode; otherwise it's set to blocking mode. Initially all channels are in blocking mode. - In non-blocking mode, if a L{recv} call doesn't find any data, or if a - L{send} call can't immediately dispose of the data, an error exception + In non-blocking mode, if a `recv` call doesn't find any data, or if a + `send` call can't immediately dispose of the data, an error exception is raised. In blocking mode, the calls block until they can proceed. An - EOF condition is considered "immediate data" for L{recv}, so if the + EOF condition is considered "immediate data" for `recv`, so if the channel is closed in the read direction, it will never block. - C{chan.setblocking(0)} is equivalent to C{chan.settimeout(0)}; - C{chan.setblocking(1)} is equivalent to C{chan.settimeout(None)}. + ``chan.setblocking(0)`` is equivalent to ``chan.settimeout(0)``; + ``chan.setblocking(1)`` is equivalent to ``chan.settimeout(None)``. - @param blocking: 0 to set non-blocking mode; non-0 to set blocking - mode. - @type blocking: int + :param int blocking: + 0 to set non-blocking mode; non-0 to set blocking mode. """ if blocking: self.settimeout(None) @@ -551,12 +534,10 @@ def getpeername(self): """ Return the address of the remote side of this Channel, if possible. - This is just a wrapper around C{'getpeername'} on the Transport, used - to provide enough of a socket-like interface to allow asyncore to work. - (asyncore likes to call C{'getpeername'}.) - @return: the address if the remote host, if known - @rtype: tuple(str, int) + This simply wraps `.Transport.getpeername`, used to provide enough of a + socket-like interface to allow asyncore to work. (asyncore likes to + call ``'getpeername'``.) """ return self.transport.getpeername() @@ -564,7 +545,7 @@ """ Close the channel. All future read/write operations on the channel will fail. The remote end will receive no more data (after queued data - is flushed). Channels are automatically closed when their L{Transport} + is flushed). Channels are automatically closed when their `.Transport` is closed or when they are garbage collected. """ self.lock.acquire() @@ -589,12 +570,12 @@ def recv_ready(self): """ Returns true if data is buffered and ready to be read from this - channel. A C{False} result does not mean that the channel has closed; + channel. A ``False`` result does not mean that the channel has closed; it means you may need to wait before more data arrives. - - @return: C{True} if a L{recv} call on this channel would immediately - return at least one byte; C{False} otherwise. - @rtype: boolean + + :return: + ``True`` if a `recv` call on this channel would immediately return + at least one byte; ``False`` otherwise. """ return self.in_buffer.read_ready() @@ -602,27 +583,25 @@ """ Receive data from the channel. The return value is a string representing the data received. The maximum amount of data to be - received at once is specified by C{nbytes}. If a string of length zero + received at once is specified by ``nbytes``. If a string of length zero is returned, the channel stream has closed. - @param nbytes: maximum number of bytes to read. - @type nbytes: int - @return: data. - @rtype: str - - @raise socket.timeout: if no data is ready before the timeout set by - L{settimeout}. + :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`. """ 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) @@ -632,47 +611,45 @@ def recv_stderr_ready(self): """ Returns true if data is buffered and ready to be read from this - channel's stderr stream. Only channels using L{exec_command} or - L{invoke_shell} without a pty will ever have data on the stderr + channel's stderr stream. Only channels using `exec_command` or + `invoke_shell` without a pty will ever have data on the stderr stream. - - @return: C{True} if a L{recv_stderr} call on this channel would - immediately return at least one byte; C{False} otherwise. - @rtype: boolean - - @since: 1.1 + + :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() def recv_stderr(self, nbytes): """ Receive data from the channel's stderr stream. Only channels using - L{exec_command} or L{invoke_shell} without a pty will ever have data + `exec_command` or `invoke_shell` without a pty will ever have data on the stderr stream. The return value is a string representing the data received. The maximum amount of data to be received at once is - specified by C{nbytes}. If a string of length zero is returned, the + specified by ``nbytes``. If a string of length zero is returned, the channel stream has closed. - @param nbytes: maximum number of bytes to read. - @type nbytes: int - @return: data. - @rtype: str - - @raise socket.timeout: if no data is ready before the timeout set by - L{settimeout}. - - @since: 1.1 + :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, 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) @@ -683,14 +660,14 @@ """ 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 L{send} call will succeed immediately and return + outbound buffer, a `send` call will succeed immediately and return the number of bytes actually written. - - @return: C{True} if a L{send} call on this channel would immediately - succeed or fail - @rtype: boolean + + :return: + ``True`` if a `send` call on this channel would immediately succeed + or fail """ self.lock.acquire() try: @@ -699,7 +676,7 @@ 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,31 +685,17 @@ transmitted, the application needs to attempt delivery of the remaining data. - @param s: data to send - @type s: str - @return: number of bytes actually sent - @rtype: int + :param str s: data to send + :return: number of bytes actually sent, as an `int` - @raise socket.timeout: if no data could be sent before the timeout set - by L{settimeout}. + :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) - if size == 0: - # eof or similar - return 0 - m = Message() - m.add_byte(chr(MSG_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): """ @@ -742,58 +705,42 @@ 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 s: data to send. - @type s: str - @return: number of bytes actually sent. - @rtype: int - - @raise socket.timeout: if no data could be sent before the timeout set - by L{settimeout}. - - @since: 1.1 + + :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) - self.lock.acquire() - try: - size = self._wait_for_send_window(size) - if size == 0: - # eof or similar - return 0 - m = Message() - m.add_byte(chr(MSG_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): """ Send data to the channel, without allowing partial results. Unlike - L{send}, this method continues to send data from the given string until + `send`, this method continues to send data from the given string until either all data has been sent or an error occurs. Nothing is returned. - @param s: data to send. - @type s: str + :param str s: data to send. - @raise socket.timeout: if sending stalled for longer than the timeout - set by L{settimeout}. - @raise socket.error: if an error occured before the entire string was - sent. + :raises socket.timeout: + if sending stalled for longer than the timeout set by `settimeout`. + :raises socket.error: + if an error occurred before the entire string was sent. - @note: If the channel is closed while only part of the data hase been + .. note:: + 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. + 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 @@ -801,23 +748,20 @@ def sendall_stderr(self, s): """ Send data to the channel's "stderr" stream, without allowing partial - results. Unlike L{send_stderr}, this method continues to send data + 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 s: data to send to the client as "stderr" output. - @type s: str - - @raise socket.timeout: if sending stalled for longer than the timeout - set by L{settimeout}. - @raise socket.error: if an error occured before the entire string was - sent. + + :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 occurred before the entire string was sent. - @since: 1.1 + .. versionadded:: 1.1 """ while s: - if self.closed: - raise socket.error('Socket is closed') sent = self.send_stderr(s) s = s[sent:] return None @@ -825,49 +769,46 @@ def makefile(self, *params): """ Return a file-like object associated with this channel. The optional - C{mode} and C{bufsize} arguments are interpreted the same way as by - the built-in C{file()} function in python. + ``mode`` and ``bufsize`` arguments are interpreted the same way as by + the built-in ``file()`` function in Python. - @return: object which can be used for python file I/O. - @rtype: L{ChannelFile} + :return: `.ChannelFile` object which can be used for Python file I/O. """ return ChannelFile(*([self] + list(params))) def makefile_stderr(self, *params): """ Return a file-like object associated with this channel's stderr - stream. Only channels using L{exec_command} or L{invoke_shell} + stream. Only channels using `exec_command` or `invoke_shell` without a pty will ever have data on the stderr stream. - - The optional C{mode} and C{bufsize} arguments are interpreted the - same way as by the built-in C{file()} function in python. For a + + 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: object which can be used for python file I/O. - @rtype: L{ChannelFile} - @since: 1.1 + :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 - but I{not} for reading or writing. This is primaily to allow python's - C{select} module to work. + but not for reading or writing. This is primarily to allow Python's + ``select`` module to work. - The first time C{fileno} is called on a channel, a pipe is created to + The first time ``fileno`` is called on a channel, a pipe is created to simulate real OS-level file descriptor (FD) behavior. Because of this, two OS-level FDs are created, which will use up FDs faster than normal. (You won't notice this effect unless you have hundreds of channels open at the same time.) - @return: an OS-level file descriptor - @rtype: int - - @warning: This method causes channel reads to be slightly less - efficient. + :return: an OS-level file descriptor (`int`) + + .. warning:: + This method causes channel reads to be slightly less efficient. """ self.lock.acquire() try: @@ -884,14 +825,14 @@ def shutdown(self, how): """ - Shut down one or both halves of the connection. If C{how} is 0, - further receives are disallowed. If C{how} is 1, further sends - are disallowed. If C{how} is 2, further sends and receives are + Shut down one or both halves of the connection. If ``how`` is 0, + further receives are disallowed. If ``how`` is 1, further sends + are disallowed. If ``how`` is 2, further sends and receives are disallowed. This closes the stream in one or both directions. - @param how: 0 (stop receiving), 1 (stop sending), or 2 (stop - receiving and sending). - @type how: int + :param int how: + 0 (stop receiving), 1 (stop sending), or 2 (stop receiving and + sending). """ if (how == 0) or (how == 2): # feign "read" shutdown @@ -904,35 +845,33 @@ 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 the incoming direction. After this call, future reads on this channel will fail instantly. This is a convenience method, equivalent - to C{shutdown(0)}, for people who don't make it a habit to + to ``shutdown(0)``, for people who don't make it a habit to memorize unix constants from the 1970s. - - @since: 1.2 + + .. versionadded:: 1.2 """ self.shutdown(0) - + def shutdown_write(self): """ Shutdown the sending side of this socket, closing the stream in the outgoing direction. After this call, future writes on this channel will fail instantly. This is a convenience method, equivalent - to C{shutdown(1)}, for people who don't make it a habit to + to ``shutdown(1)``, for people who don't make it a habit to memorize unix constants from the 1970s. - - @since: 1.2 + + .. versionadded:: 1.2 """ self.shutdown(1) - ### calls from Transport - def _set_transport(self, transport): self.transport = transport self.logger = util.get_logger(self.transport.get_log_channel()) @@ -944,14 +883,15 @@ 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.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) - + def _request_success(self, m): self._log(DEBUG, 'Sesch channel %d request ok' % self.chanid) self.event_ready = True @@ -969,16 +909,16 @@ 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 @@ -986,7 +926,7 @@ self._feed(s) else: self.in_stderr_buffer.feed(s) - + def _window_adjust(self, m): nbytes = m.get_int() self.lock.acquire() @@ -999,7 +939,7 @@ 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 @@ -1027,14 +967,21 @@ ok = False else: ok = server.check_channel_shell_request(self) + elif key == 'env': + name = m.get_string() + value = m.get_string() + if server is None: + ok = False + 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: @@ -1051,8 +998,8 @@ 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 @@ -1070,9 +1017,9 @@ 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) @@ -1100,9 +1047,26 @@ if m is not None: self.transport._send_user_message(m) - ### 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) @@ -1138,7 +1102,7 @@ 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) @@ -1150,7 +1114,7 @@ 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 @@ -1189,7 +1153,7 @@ def _wait_for_send_window(self, size): """ (You are already holding the lock.) - Wait for the send window to open up, and allocate up to C{size} bytes + Wait for the send window to open up, and allocate up to ``size`` bytes for transmission. If no space opens up before the timeout, a timeout exception is raised. Returns the number of bytes available to send (may be less than requested). @@ -1208,7 +1172,7 @@ return 0 then = time.time() self.out_buffer_cv.wait(timeout) - if timeout != None: + if timeout is not None: timeout -= time.time() - then if timeout <= 0.0: raise socket.timeout() @@ -1223,20 +1187,22 @@ if self.ultra_debug: self._log(DEBUG, 'window down to %d' % self.out_window_size) return size - + class ChannelFile (BufferedFile): """ - A file-like wrapper around L{Channel}. A ChannelFile is created by calling - L{Channel.makefile}. + A file-like wrapper around `.Channel`. A ChannelFile is created by calling + `Channel.makefile`. - @bug: To correctly emulate the file object created from a socket's - C{makefile} method, a L{Channel} and its C{ChannelFile} should be able - to be closed or garbage-collected independently. Currently, closing - the C{ChannelFile} does nothing but flush the buffer. + .. warning:: + To correctly emulate the file object created from a socket's `makefile + ` method, a `.Channel` and its + `.ChannelFile` should be able to be closed or garbage-collected + independently. Currently, closing the `ChannelFile` does nothing but + flush the buffer. """ - def __init__(self, channel, mode = 'r', bufsize = -1): + def __init__(self, channel, mode='r', bufsize=-1): self.channel = channel BufferedFile.__init__(self) self._set_mode(mode, bufsize) @@ -1244,8 +1210,6 @@ def __repr__(self): """ Returns a string representation of this object, for debugging. - - @rtype: str """ return '' @@ -1258,12 +1222,12 @@ class ChannelStderrFile (ChannelFile): - def __init__(self, channel, mode = 'r', bufsize = -1): + def __init__(self, channel, mode='r', bufsize=-1): ChannelFile.__init__(self, channel, mode, bufsize) def _read(self, size): return self.channel.recv_stderr(size) - + def _write(self, data): self.channel.sendall_stderr(data) return len(data) diff -Nru paramiko-1.10.1/paramiko/client.py paramiko-1.15.1/paramiko/client.py --- paramiko-1.10.1/paramiko/client.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/client.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,7 +17,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{SSHClient}. +SSH client & key policies """ from binascii import hexlify @@ -27,78 +27,23 @@ import warnings from paramiko.agent import Agent -from paramiko.common import * +from paramiko.common import DEBUG from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey +from paramiko.ecdsakey import ECDSAKey from paramiko.hostkeys import HostKeys +from paramiko.py3compat import string_types from paramiko.resource import ResourceManager from paramiko.rsakey import RSAKey from paramiko.ssh_exception import SSHException, BadHostKeyException from paramiko.transport import Transport -from paramiko.util import retry_on_signal +from paramiko.util import retry_on_signal, ClosingContextManager -class MissingHostKeyPolicy (object): - """ - Interface for defining the policy that L{SSHClient} should use when the - SSH server's hostname is not in either the system host keys or the - application's keys. Pre-made classes implement policies for automatically - adding the key to the application's L{HostKeys} object (L{AutoAddPolicy}), - and for automatically rejecting the key (L{RejectPolicy}). - - This function may be used to ask the user to verify the key, for example. - """ - - def missing_host_key(self, client, hostname, key): - """ - Called when an L{SSHClient} receives a server key for a server that - isn't in either the system or local L{HostKeys} object. To accept - the key, simply return. To reject, raised an exception (which will - be passed to the calling application). - """ - pass - - -class AutoAddPolicy (MissingHostKeyPolicy): - """ - Policy for automatically adding the hostname and new host key to the - local L{HostKeys} object, and saving it. This is used by L{SSHClient}. - """ - - def missing_host_key(self, client, hostname, key): - client._host_keys.add(hostname, key.get_name(), key) - if client._host_keys_filename is not None: - client.save_host_keys(client._host_keys_filename) - client._log(DEBUG, 'Adding %s host key for %s: %s' % - (key.get_name(), hostname, hexlify(key.get_fingerprint()))) - - -class RejectPolicy (MissingHostKeyPolicy): - """ - Policy for automatically rejecting the unknown hostname & key. This is - used by L{SSHClient}. - """ - - def missing_host_key(self, client, hostname, key): - client._log(DEBUG, 'Rejecting %s host key for %s: %s' % - (key.get_name(), hostname, hexlify(key.get_fingerprint()))) - raise SSHException('Server %r not found in known_hosts' % hostname) - - -class WarningPolicy (MissingHostKeyPolicy): - """ - Policy for logging a python-style warning for an unknown host key, but - accepting it. This is used by L{SSHClient}. - """ - def missing_host_key(self, client, hostname, key): - warnings.warn('Unknown %s host key for %s: %s' % - (key.get_name(), hostname, hexlify(key.get_fingerprint()))) - - -class SSHClient (object): +class SSHClient (ClosingContextManager): """ A high-level representation of a session with an SSH server. This class - wraps L{Transport}, L{Channel}, and L{SFTPClient} to take care of most + wraps `.Transport`, `.Channel`, and `.SFTPClient` to take care of most aspects of authenticating and opening channels. A typical use case is:: client = SSHClient() @@ -110,7 +55,9 @@ checking. The default mechanism is to try to use local key files or an SSH agent (if one is running). - @since: 1.6 + Instances of this class may be used as context managers. + + .. versionadded:: 1.6 """ def __init__(self): @@ -128,22 +75,21 @@ def load_system_host_keys(self, filename=None): """ Load host keys from a system (read-only) file. Host keys read with - this method will not be saved back by L{save_host_keys}. + this method will not be saved back by `save_host_keys`. This method can be called multiple times. Each new set of host keys will be merged with the existing set (new replacing old if there are conflicts). - If C{filename} is left as C{None}, an attempt will be made to read + If ``filename`` is left as ``None``, an attempt will be made to read keys from the user's local "known hosts" file, as used by OpenSSH, and no exception will be raised if the file can't be read. This is probably only useful on posix. - @param filename: the filename to read, or C{None} - @type filename: str + :param str filename: the filename to read, or ``None`` - @raise IOError: if a filename was provided and the file could not be - read + :raises IOError: + if a filename was provided and the file could not be read """ if filename is None: # try the user's .ssh key file, and mask exceptions @@ -158,19 +104,18 @@ def load_host_keys(self, filename): """ Load host keys from a local host-key file. Host keys read with this - method will be checked I{after} keys loaded via L{load_system_host_keys}, - but will be saved back by L{save_host_keys} (so they can be modified). - The missing host key policy L{AutoAddPolicy} adds keys to this set and + method will be checked after keys loaded via `load_system_host_keys`, + but will be saved back by `save_host_keys` (so they can be modified). + The missing host key policy `.AutoAddPolicy` adds keys to this set and saves them, when connecting to a previously-unknown server. This method can be called multiple times. Each new set of host keys will be merged with the existing set (new replacing old if there are conflicts). When automatically saving, the last hostname is used. - @param filename: the filename to read - @type filename: str + :param str filename: the filename to read - @raise IOError: if the filename could not be read + :raises IOError: if the filename could not be read """ self._host_keys_filename = filename self._host_keys.load(filename) @@ -178,109 +123,115 @@ def save_host_keys(self, filename): """ Save the host keys back to a file. Only the host keys loaded with - L{load_host_keys} (plus any added directly) will be saved -- not any - host keys loaded with L{load_system_host_keys}. + `load_host_keys` (plus any added directly) will be saved -- not any + host keys loaded with `load_system_host_keys`. - @param filename: the filename to save to - @type filename: str + :param str filename: the filename to save to - @raise IOError: if the file could not be written + :raises IOError: if the file could not be written """ - f = open(filename, 'w') - f.write('# SSH host keys collected by paramiko\n') - 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() + + # update local host keys from file (in case other SSH clients + # have written to the known_hosts file meanwhile. + if self._host_keys_filename is not None: + self.load_host_keys(self._host_keys_filename) + + 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): """ - Get the local L{HostKeys} object. This can be used to examine the + Get the local `.HostKeys` object. This can be used to examine the local host keys or change them. - @return: the local host keys - @rtype: L{HostKeys} + :return: the local host keys as a `.HostKeys` object. """ return self._host_keys def set_log_channel(self, name): """ - Set the channel for logging. The default is C{"paramiko.transport"} + Set the channel for logging. The default is ``"paramiko.transport"`` but it can be set to anything you want. - @param name: new channel name for logging - @type name: str + :param str name: new channel name for logging """ self._log_channel = name def set_missing_host_key_policy(self, policy): """ Set the policy to use when connecting to a server that doesn't have a - host key in either the system or local L{HostKeys} objects. The - default policy is to reject all unknown servers (using L{RejectPolicy}). - You may substitute L{AutoAddPolicy} or write your own policy class. + host key in either the system or local `.HostKeys` objects. The + default policy is to reject all unknown servers (using `.RejectPolicy`). + You may substitute `.AutoAddPolicy` or write your own policy class. - @param policy: the policy to use when receiving a host key from a + :param .MissingHostKeyPolicy policy: + the policy to use when receiving a host key from a previously-unknown server - @type policy: L{MissingHostKeyPolicy} """ self._policy = policy def connect(self, hostname, port=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, 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 L{load_system_host_keys}) - and any local host keys (L{load_host_keys}). If the server's hostname + is checked against the system host keys (see `load_system_host_keys`) + and any local host keys (`load_host_keys`). If the server's hostname is not found in either set of host keys, the missing host key policy - is used (see L{set_missing_host_key_policy}). The default policy is - to reject the key and raise an L{SSHException}. + is used (see `set_missing_host_key_policy`). The default policy is + to reject the key and raise an `.SSHException`. Authentication is attempted in the following order of priority: - - The C{pkey} or C{key_filename} passed in (if any) + - 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 C{~/.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 passed in, that password will be used to attempt to unlock the key. - @param hostname: the server to connect to - @type hostname: str - @param port: the server port to connect to - @type port: int - @param username: the username to authenticate as (defaults to the - current local username) - @type username: str - @param password: a password to use for authentication or for unlocking - a private key - @type password: str - @param pkey: an optional private key to use for authentication - @type pkey: L{PKey} - @param key_filename: the filename, or list of filenames, of optional - private key(s) to try for authentication - @type key_filename: str or list(str) - @param timeout: an optional timeout (in seconds) for the TCP connect - @type timeout: float - @param allow_agent: set to False to disable connecting to the SSH agent - @type allow_agent: bool - @param look_for_keys: set to False to disable searching for discoverable - private key files in C{~/.ssh/} - @type look_for_keys: bool - @param compress: set to True to turn on compression - @type compress: bool - @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 str hostname: the server to connect to + :param int port: the server port to connect to + :param str username: + the username to authenticate as (defaults to the current local + username) + :param str password: + a password to use for authentication or for unlocking a private key + :param .PKey pkey: an optional private key to use for authentication + :param str key_filename: + the filename, or list of filenames, of optional private key(s) to + try for authentication + :param float timeout: an optional timeout (in seconds) for the TCP connect + :param bool allow_agent: set to False to disable connecting to the SSH agent + :param bool look_for_keys: + set to False to disable searching for discoverable private key + files in ``~/.ssh/`` + :param bool compress: set to True to turn on compression + :param socket sock: + an open socket or socket-like object (such as a `.Channel`) to use + for communication to the target host + :param bool gss_auth: ``True`` if you want to use GSS-API authentication + :param bool gss_kex: Perform GSS-API Key Exchange and user authentication + :param bool gss_deleg_creds: Delegate GSS-API client credentials or not + :param str gss_host: The targets name in the kerberos database. default: hostname + :param float banner_timeout: an optional timeout (in seconds) to wait + for the SSH banner to be presented. - @raise BadHostKeyException: if the server's host key could not be + :raises BadHostKeyException: if the server's host key could not be verified - @raise AuthenticationException: if authentication failed - @raise SSHException: if there was any other error connecting or + :raises AuthenticationException: if authentication failed + :raises SSHException: if there was any other error connecting or establishing an SSH session - @raise socket.error: if a socket error occurred while connecting + :raises socket.error: if a socket error occurred while connecting + + .. versionchanged:: 1.15 + 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): @@ -299,10 +250,18 @@ pass retry_on_signal(lambda: sock.connect(addr)) - t = self._transport = Transport(sock) + 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) + 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) + if banner_timeout is not None: + t.banner_timeout = banner_timeout t.start_client() ResourceManager.register(self, t) @@ -313,91 +272,98 @@ 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 our 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() if key_filename is None: key_filenames = [] - elif isinstance(key_filename, (str, unicode)): - key_filenames = [ key_filename ] + elif isinstance(key_filename, string_types): + 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): """ - Close this SSHClient and its underlying L{Transport}. + Close this SSHClient and its underlying `.Transport`. """ if self._transport is None: return self._transport.close() self._transport = None - if self._agent != None: + if self._agent is not None: self._agent.close() self._agent = None def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False): """ - Execute a command on the SSH server. A new L{Channel} is opened and + Execute a command on the SSH server. A new `.Channel` is opened and the requested command is executed. The command's input and output - streams are returned as python C{file}-like objects representing + streams are returned as Python ``file``-like objects representing stdin, stdout, and stderr. - @param command: the command to execute - @type command: str - @param bufsize: interpreted the same way as by the built-in C{file()} function in python - @type bufsize: int - @param timeout: set command's channel timeout. See L{Channel.settimeout}.settimeout - @type timeout: int - @return: the stdin, stdout, and stderr of the executing command - @rtype: tuple(L{ChannelFile}, L{ChannelFile}, L{ChannelFile}) + :param str command: the command to execute + :param int bufsize: + interpreted the same way as by the built-in ``file()`` function in + Python + :param int timeout: + set command's channel timeout. See `Channel.settimeout`.settimeout + :return: + the stdin, stdout, and stderr of the executing command, as a + 3-tuple - @raise SSHException: if the server fails to execute the command + :raises SSHException: if the server fails to execute the command """ chan = self._transport.open_session() - if(get_pty): + if get_pty: chan.get_pty() 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} + Start an interactive shell session on the SSH server. A new `.Channel` is opened and connected to a pseudo-terminal using the requested terminal type and size. - @param term: the terminal type to emulate (for example, C{"vt100"}) - @type term: str - @param width: the width (in characters) of the terminal window - @type width: int - @param height: the height (in characters) of the terminal window - @type height: int - @param width_pixels: the width (in pixels) of the terminal window - @type width_pixels: int - @param height_pixels: the height (in pixels) of the terminal window - @type height_pixels: int - @return: a new channel connected to the remote shell - @rtype: L{Channel} + :param str term: + the terminal type to emulate (for example, ``"vt100"``) + :param int width: the width (in characters) of the terminal window + :param int height: the height (in characters) of the terminal window + :param int width_pixels: the width (in pixels) of the terminal window + :param int height_pixels: the height (in pixels) of the terminal window + :return: a new `.Channel` connected to the remote shell - @raise SSHException: if the server fails to invoke a shell + :raises SSHException: if the server fails to invoke a shell """ chan = self._transport.open_session() chan.get_pty(term, width, height, width_pixels, height_pixels) @@ -408,29 +374,29 @@ """ Open an SFTP session on the SSH server. - @return: a new SFTP session object - @rtype: L{SFTPClient} + :return: a new `.SFTPClient` session object """ return self._transport.open_sftp_client() def get_transport(self): """ - Return the underlying L{Transport} object for this SSH connection. + Return the underlying `.Transport` object for this SSH connection. This can be used to perform lower-level tasks, like opening specific kinds of channels. - @return: the Transport for this connection - @rtype: L{Transport} + :return: the `.Transport` for this connection """ 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: - 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 @@ -440,6 +406,27 @@ 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())) @@ -447,12 +434,12 @@ two_factor = (allowed_types == ['password']) if not two_factor: return - except SSHException, e: + except SSHException as e: saved_exception = e 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)) @@ -461,11 +448,11 @@ if not two_factor: return break - except SSHException, e: + except SSHException as e: saved_exception = e if not two_factor and allow_agent: - if self._agent == None: + if self._agent is None: self._agent = Agent() for key in self._agent.get_keys(): @@ -477,24 +464,30 @@ if not two_factor: return break - except SSHException, e: + except SSHException as e: saved_exception = e if not two_factor: 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 = [] @@ -509,16 +502,14 @@ 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: + except SSHException as e: saved_exception = e elif two_factor: raise SSHException('Two-factor authentication requires a password') @@ -531,3 +522,59 @@ def _log(self, level, msg): self._transport._log(level, msg) + +class MissingHostKeyPolicy (object): + """ + Interface for defining the policy that `.SSHClient` should use when the + SSH server's hostname is not in either the system host keys or the + application's keys. Pre-made classes implement policies for automatically + adding the key to the application's `.HostKeys` object (`.AutoAddPolicy`), + and for automatically rejecting the key (`.RejectPolicy`). + + This function may be used to ask the user to verify the key, for example. + """ + + def missing_host_key(self, client, hostname, key): + """ + Called when an `.SSHClient` receives a server key for a server that + isn't in either the system or local `.HostKeys` object. To accept + the key, simply return. To reject, raised an exception (which will + be passed to the calling application). + """ + pass + + +class AutoAddPolicy (MissingHostKeyPolicy): + """ + Policy for automatically adding the hostname and new host key to the + local `.HostKeys` object, and saving it. This is used by `.SSHClient`. + """ + + def missing_host_key(self, client, hostname, key): + client._host_keys.add(hostname, key.get_name(), key) + if client._host_keys_filename is not None: + client.save_host_keys(client._host_keys_filename) + client._log(DEBUG, 'Adding %s host key for %s: %s' % + (key.get_name(), hostname, hexlify(key.get_fingerprint()))) + + +class RejectPolicy (MissingHostKeyPolicy): + """ + Policy for automatically rejecting the unknown hostname & key. This is + used by `.SSHClient`. + """ + + def missing_host_key(self, client, hostname, key): + client._log(DEBUG, 'Rejecting %s host key for %s: %s' % + (key.get_name(), hostname, hexlify(key.get_fingerprint()))) + raise SSHException('Server %r not found in known_hosts' % hostname) + + +class WarningPolicy (MissingHostKeyPolicy): + """ + Policy for logging a Python-style warning for an unknown host key, but + accepting it. This is used by `.SSHClient`. + """ + def missing_host_key(self, client, hostname, key): + warnings.warn('Unknown %s host key for %s: %s' % + (key.get_name(), hostname, hexlify(key.get_fingerprint()))) diff -Nru paramiko-1.10.1/paramiko/common.py paramiko-1.15.1/paramiko/common.py --- paramiko-1.10.1/paramiko/common.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/common.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -19,20 +19,60 @@ """ Common constants and global variables. """ +import logging +from paramiko.py3compat import byte_chr, PY2, bytes_types, string_types, b, long 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_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, \ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST, \ MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE = range(90, 101) +cMSG_DISCONNECT = byte_chr(MSG_DISCONNECT) +cMSG_IGNORE = byte_chr(MSG_IGNORE) +cMSG_UNIMPLEMENTED = byte_chr(MSG_UNIMPLEMENTED) +cMSG_DEBUG = byte_chr(MSG_DEBUG) +cMSG_SERVICE_REQUEST = byte_chr(MSG_SERVICE_REQUEST) +cMSG_SERVICE_ACCEPT = byte_chr(MSG_SERVICE_ACCEPT) +cMSG_KEXINIT = byte_chr(MSG_KEXINIT) +cMSG_NEWKEYS = byte_chr(MSG_NEWKEYS) +cMSG_USERAUTH_REQUEST = byte_chr(MSG_USERAUTH_REQUEST) +cMSG_USERAUTH_FAILURE = byte_chr(MSG_USERAUTH_FAILURE) +cMSG_USERAUTH_SUCCESS = byte_chr(MSG_USERAUTH_SUCCESS) +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) +cMSG_CHANNEL_OPEN = byte_chr(MSG_CHANNEL_OPEN) +cMSG_CHANNEL_OPEN_SUCCESS = byte_chr(MSG_CHANNEL_OPEN_SUCCESS) +cMSG_CHANNEL_OPEN_FAILURE = byte_chr(MSG_CHANNEL_OPEN_FAILURE) +cMSG_CHANNEL_WINDOW_ADJUST = byte_chr(MSG_CHANNEL_WINDOW_ADJUST) +cMSG_CHANNEL_DATA = byte_chr(MSG_CHANNEL_DATA) +cMSG_CHANNEL_EXTENDED_DATA = byte_chr(MSG_CHANNEL_EXTENDED_DATA) +cMSG_CHANNEL_EOF = byte_chr(MSG_CHANNEL_EOF) +cMSG_CHANNEL_CLOSE = byte_chr(MSG_CHANNEL_CLOSE) +cMSG_CHANNEL_REQUEST = byte_chr(MSG_CHANNEL_REQUEST) +cMSG_CHANNEL_SUCCESS = byte_chr(MSG_CHANNEL_SUCCESS) +cMSG_CHANNEL_FAILURE = byte_chr(MSG_CHANNEL_FAILURE) # for debugging: MSG_NAMES = { @@ -49,6 +89,8 @@ 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', @@ -68,8 +110,14 @@ 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' +} # authentication request return codes: @@ -95,29 +143,42 @@ DISCONNECT_SERVICE_NOT_AVAILABLE, DISCONNECT_AUTH_CANCELLED_BY_USER, \ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 7, 13, 14 -from Crypto import Random - -# keep a crypto-strong PRNG nearby -rng = Random.new() - -import sys -if sys.version_info < (2, 3): - try: - import logging - except: - import logging22 as logging - import select - PY22 = True - - import socket - if not hasattr(socket, 'timeout'): - class timeout(socket.error): pass - socket.timeout = timeout - del timeout +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: - import logging - PY22 = False + 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 @@ -127,3 +188,14 @@ # 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 -Nru paramiko-1.10.1/paramiko/compress.py paramiko-1.15.1/paramiko/compress.py --- paramiko-1.10.1/paramiko/compress.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/compress.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. diff -Nru paramiko-1.10.1/paramiko/config.py paramiko-1.15.1/paramiko/config.py --- paramiko-1.10.1/paramiko/config.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/config.py 2014-09-22 18:31:58.000000000 +0000 @@ -8,7 +8,7 @@ # 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 +# 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. @@ -18,7 +18,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{SSHConfig}. +Configuration file (aka ``ssh_config``) support. """ import fnmatch @@ -27,65 +27,21 @@ import socket SSH_PORT = 22 -proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) - - -class LazyFqdn(object): - """ - Returns the host's fqdn on request as string. - """ - - def __init__(self, config): - self.fqdn = None - self.config = config - - def __str__(self): - if self.fqdn is None: - # - # If the SSH config contains AddressFamily, use that when - # determining the local host's FQDN. Using socket.getfqdn() from - # the standard library is the most general solution, but can - # result in noticeable delays on some platforms when IPv6 is - # misconfigured or not available, as it calls getaddrinfo with no - # address family specified, so both IPv4 and IPv6 are checked. - # - - # Handle specific option - fqdn = None - address_family = self.config.get('addressfamily', 'any').lower() - if address_family != 'any': - family = socket.AF_INET if address_family == 'inet' \ - else socket.AF_INET6 - results = socket.getaddrinfo(host, - None, - family, - socket.SOCK_DGRAM, - socket.IPPROTO_IP, - socket.AI_CANONNAME) - for res in results: - af, socktype, proto, canonname, sa = res - if canonname and '.' in canonname: - fqdn = canonname - break - # Handle 'any' / unspecified - if fqdn is None: - fqdn = socket.getfqdn() - # Cache - self.fqdn = fqdn - return self.fqdn class SSHConfig (object): """ Representation of config information as stored in the format used by - OpenSSH. Queries can be made via L{lookup}. The format is described in - OpenSSH's C{ssh_config} man page. This class is provided primarily as a + OpenSSH. Queries can be made via `lookup`. The format is described in + OpenSSH's ``ssh_config`` man page. This class is provided primarily as a convenience to posix users (since the OpenSSH format is a de-facto standard on posix) but should work fine on Windows too. - @since: 1.6 + .. versionadded:: 1.6 """ + SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)') + def __init__(self): """ Create a new OpenSSH config object. @@ -96,74 +52,69 @@ """ Read an OpenSSH config from the given file object. - @param file_obj: a file-like object to read the config file from - @type file_obj: file + :param file file_obj: a file-like object to read the config file from """ + host = {"host": ['*'], "config": {}} for line in file_obj: - line = line.rstrip('\n').lstrip() - if (line == '') or (line[0] == '#'): + line = line.rstrip('\r\n').lstrip() + if not line or line.startswith('#'): 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) - value = value.split() - host = {key: value, 'config': {}} - #identityfile is a special case, since it is allowed to be - # specified multiple times and they should be tried in order - # of specification. - elif key == 'identityfile': - if key in host['config']: - host['config']['identityfile'].append(value) - else: - host['config']['identityfile'] = [value] - elif key not in host['config']: - host['config'].update({key: value}) + host = { + 'host': self._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): """ Return a dict of config options for a given hostname. - The host-matching rules of OpenSSH's C{ssh_config} man page are used, + 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 C{"Port"} is set under C{"Host *"} - and also C{"Host *.example.com"}, and the lookup is for - C{"ssh.example.com"}, then the port entry for C{"Host *.example.com"} + 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 keys in the returned dict are all normalized to lowercase (look for - C{"port"}, not C{"Port"}. The values are processed according to the - rules for substitution variable expansion in C{ssh_config}. + ``"port"``, not ``"Port"``. The values are processed according to the + rules for substitution variable expansion in ``ssh_config``. - @param hostname: the hostname to lookup - @type hostname: str + :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: - 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 @@ -175,7 +126,7 @@ 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:]): @@ -189,13 +140,11 @@ Return a dict of config options with expanded substitutions for a given hostname. - Please refer to man C{ssh_config} for the parameters that + Please refer to man ``ssh_config`` for the parameters that are replaced. - @param config: the config for the hostname - @type hostname: dict - @param hostname: the hostname that the config belongs to - @type hostname: str + :param dict config: the config for the hostname + :param str hostname: the hostname that the config belongs to """ if 'hostname' in config: @@ -215,7 +164,7 @@ remoteuser = user host = socket.gethostname().split('.')[0] - fqdn = LazyFqdn(config) + fqdn = LazyFqdn(config, host) homedir = os.path.expanduser('~') replacements = {'controlpath': [ @@ -249,8 +198,88 @@ 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: + if find in config[k]: 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): + """ + Returns the host's fqdn on request as string. + """ + + def __init__(self, config, host=None): + self.fqdn = None + self.config = config + self.host = host + + def __str__(self): + if self.fqdn is None: + # + # If the SSH config contains AddressFamily, use that when + # determining the local host's FQDN. Using socket.getfqdn() from + # the standard library is the most general solution, but can + # result in noticeable delays on some platforms when IPv6 is + # misconfigured or not available, as it calls getaddrinfo with no + # address family specified, so both IPv4 and IPv6 are checked. + # + + # Handle specific option + fqdn = None + address_family = self.config.get('addressfamily', 'any').lower() + if address_family != 'any': + try: + family = socket.AF_INET if address_family == 'inet' \ + else socket.AF_INET6 + results = socket.getaddrinfo( + self.host, + None, + family, + socket.SOCK_DGRAM, + socket.IPPROTO_IP, + socket.AI_CANONNAME + ) + for res in results: + af, socktype, proto, canonname, sa = res + if canonname and '.' in canonname: + fqdn = canonname + break + # giaerror -> socket.getaddrinfo() can't resolve self.host + # (which is from socket.gethostname()). Fall back to the + # getfqdn() call below. + except socket.gaierror: + pass + # Handle 'any' / unspecified + if fqdn is None: + fqdn = socket.getfqdn() + # Cache + self.fqdn = fqdn + return self.fqdn diff -Nru paramiko-1.10.1/paramiko/dsskey.py paramiko-1.15.1/paramiko/dsskey.py --- paramiko-1.10.1/paramiko/dsskey.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/dsskey.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,14 +17,17 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{DSSKey} +DSS keys. """ +import os +from hashlib import sha1 + from Crypto.PublicKey import DSA -from Crypto.Hash import SHA -from paramiko.common import * from paramiko import util +from paramiko.common import zero_byte +from paramiko.py3compat import long from paramiko.ssh_exception import SSHException from paramiko.message import Message from paramiko.ber import BER, BERException @@ -56,7 +59,7 @@ 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 +67,17 @@ 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()) @@ -87,17 +93,17 @@ def get_bits(self): return self.size - + def can_sign(self): return self.x is not None - def sign_ssh_data(self, rng, data): - digest = SHA.new(data).digest() + def sign_ssh_data(self, data): + digest = sha1(data).digest() dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q), long(self.x))) # generate a suitable k qsize = len(util.deflate_long(self.q, 0)) while True: - k = util.inflate_long(rng.read(qsize), 1) + k = util.inflate_long(os.urandom(qsize), 1) if (k > 2) and (k < self.q): break r, s = dss.sign(util.inflate_long(digest, 1), k) @@ -107,26 +113,26 @@ 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) sigS = util.inflate_long(sig[20:], 1) - sigM = util.inflate_long(SHA.new(data).digest(), 1) + sigM = util.inflate_long(sha1(data).digest(), 1) dss = DSA.construct((long(self.y), long(self.g), long(self.p), long(self.q))) return dss.verify(sigM, (sigR, sigS)) @@ -134,13 +140,13 @@ def _encode_key(self): if self.x is None: raise SSHException('Not enough key information') - keylist = [ 0, self.p, self.q, self.g, self.y, self.x ] + keylist = [0, self.p, self.q, self.g, self.y, self.x] try: b = BER() 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) @@ -153,39 +159,35 @@ Generate a new private DSS 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{DSSKey} + :param int bits: number of bits the generated key should be. + :param function progress_func: + an optional function to call at key points in key generation (used + by ``pyCrypto.PublicKey``). + :return: new `.DSSKey` private key """ - dsa = DSA.generate(bits, rng.read, progress_func) + dsa = DSA.generate(bits, os.urandom, progress_func) key = DSSKey(vals=(dsa.p, dsa.q, dsa.g, dsa.y)) key.x = dsa.x return key generate = staticmethod(generate) - ### internals... - def _from_private_key_file(self, filename, password): data = self._read_private_key_file('DSA', filename, password) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('DSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # DSAPrivateKey = { version = 0, p, q, g, y, x } 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 -Nru paramiko-1.10.1/paramiko/ecdsakey.py paramiko-1.15.1/paramiko/ecdsakey.py --- paramiko-1.10.1/paramiko/ecdsakey.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/ecdsakey.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,178 @@ +# Copyright (C) 2003-2007 Robey Pointer +# +# 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. + +""" +ECDSA keys +""" + +import binascii +from hashlib import sha256 + +from ecdsa import SigningKey, VerifyingKey, der, curves + +from paramiko.common import four_byte, one_byte +from paramiko.message import Message +from paramiko.pkey import PKey +from paramiko.py3compat import byte_chr, u +from paramiko.ssh_exception import SSHException + + +class ECDSAKey (PKey): + """ + Representation of an ECDSA key which can be used to sign and verify SSH2 + data. + """ + + 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: + self._from_private_key(file_obj, password) + return + if filename is not None: + self._from_private_key_file(filename, password) + return + if (msg is None) and (data is not None): + msg = Message(data) + if vals is not None: + self.signing_key, self.verifying_key = vals + else: + if msg is None: + raise SSHException('Key object may not be empty') + if msg.get_text() != 'ecdsa-sha2-nistp256': + raise SSHException('Invalid key') + curvename = msg.get_text() + if curvename != 'nistp256': + raise SSHException("Can't handle curve of type %s" % curvename) + + 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, + validate_point=validate_point) + self.size = 256 + + def asbytes(self): + key = self.verifying_key + m = Message() + m.add_string('ecdsa-sha2-nistp256') + m.add_string('nistp256') + + point_str = four_byte + key.to_string() + + m.add_string(point_str) + return m.asbytes() + + def __str__(self): + return self.asbytes() + + def __hash__(self): + h = hash(self.get_name()) + h = h * 37 + hash(self.verifying_key.pubkey.point.x()) + h = h * 37 + hash(self.verifying_key.pubkey.point.y()) + return hash(h) + + def get_name(self): + return 'ecdsa-sha2-nistp256' + + def get_bits(self): + return self.size + + def can_sign(self): + return self.signing_key is not None + + def sign_ssh_data(self, data): + sig = self.signing_key.sign_deterministic( + data, sigencode=self._sigencode, hashfunc=sha256) + m = Message() + m.add_string('ecdsa-sha2-nistp256') + m.add_string(sig) + return m + + def verify_ssh_sig(self, data, msg): + if msg.get_text() != 'ecdsa-sha2-nistp256': + return False + sig = msg.get_binary() + + # verify the signature by SHA'ing the data and encrypting it + # using the public key. + hash_obj = sha256(data).digest() + return self.verifying_key.verify_digest(sig, hash_obj, + sigdecode=self._sigdecode) + + def write_private_key_file(self, filename, password=None): + key = self.signing_key or self.verifying_key + self._write_private_key_file('EC', filename, key.to_der(), password) + + def write_private_key(self, file_obj, password=None): + key = self.signing_key or self.verifying_key + self._write_private_key('EC', file_obj, key.to_der(), password) + + 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. + + :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())) + return key + generate = staticmethod(generate) + + ### internals... + + def _from_private_key_file(self, filename, password): + data = self._read_private_key_file('EC', filename, password) + self._decode_key(data) + + def _from_private_key(self, file_obj, password): + data = self._read_private_key('EC', file_obj, password) + self._decode_key(data) + + 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" % u(binascii.hexlify(data))) + data = data[:-len(padding)] + key = SigningKey.from_der(data) + self.signing_key = key + self.verifying_key = key.get_verifying_key() + self.size = 256 + + def _sigencode(self, r, s, order): + msg = Message() + msg.add_mpint(r) + msg.add_mpint(s) + return msg.asbytes() + + def _sigdecode(self, sig, order): + msg = Message(sig) + r = msg.get_mpint() + s = msg.get_mpint() + return r, s diff -Nru paramiko-1.10.1/paramiko/file.py paramiko-1.15.1/paramiko/file.py --- paramiko-1.10.1/paramiko/file.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/file.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -15,17 +15,16 @@ # 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 paramiko.common import linefeed_byte_value, crlf, cr_byte, linefeed_byte, \ + cr_byte_value +from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types -""" -BufferedFile. -""" +from paramiko.util import ClosingContextManager -from cStringIO import StringIO - -class BufferedFile (object): +class BufferedFile (ClosingContextManager): """ - Reusable base class to implement python-style file buffering around a + Reusable base class to implement Python-style file buffering around a simpler stream. """ @@ -47,8 +46,8 @@ 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 @@ -67,10 +66,7 @@ file. This iterator happens to return the file itself, since a file is its own iterator. - @raise ValueError: if the file is closed. - - @return: an interator. - @rtype: iterator + :raises ValueError: if the file is closed. """ if self._closed: raise ValueError('I/O operation on closed file') @@ -89,36 +85,56 @@ 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 + `~exceptions.StopIteration` when EOF is hit. Unlike Python file + objects, it's okay to mix calls to `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 - """ - line = self.readline() - if not line: - raise StopIteration - return line + :return: a line (`str`) read from the file. + """ + line = self.readline() + if not line: + raise StopIteration + return line + else: + def __next__(self): + """ + 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 + `.next` and `.readline`. + + :raises StopIteration: when the end of the file is reached. + + :returns: a line (`str`) read from the file. + """ + line = self.readline() + if not line: + raise StopIteration + return line def read(self, size=None): """ - Read at most C{size} bytes from the file (less if we hit the end of the - file first). If the C{size} argument is negative or omitted, read all + Read at most ``size`` bytes from the file (less if we hit the end of the + file first). If the ``size`` argument is negative or omitted, read all the remaining data in the file. - @param size: maximum number of bytes to read - @type size: int - @return: data read from the file, or an empty string if EOF was + .. note:: + ``'b'`` mode flag is ignored (``self.FLAG_BINARY`` in + ``self._flags``), because SSH treats all files as binary, since we + have no idea what encoding the file is in, or even if the file is + text data. + + :param int size: maximum number of bytes to read + :return: + data read from the file (as bytes), or an empty string if EOF was encountered immediately - @rtype: str """ if self._closed: raise IOError('File is closed') @@ -127,7 +143,7 @@ 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 +155,12 @@ result += new_data self._realpos += len(new_data) self._pos += len(new_data) - return result + return result if size <= len(self._rbuffer): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result + return result while len(self._rbuffer) < size: read_size = size - len(self._rbuffer) if self._flags & self.FLAG_BUFFERED: @@ -160,7 +176,7 @@ result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result + return result def readline(self, size=None): """ @@ -171,14 +187,18 @@ incomplete line may be returned. An empty string is returned only when EOF is encountered immediately. - @note: Unlike stdio's C{fgets()}, the returned string contains null - characters (C{'\\0'}) if they occurred in the input. - - @param size: maximum length of returned string. - @type size: int - @return: next line of the file, or an empty string if the end of the + .. note:: + Unlike stdio's ``fgets``, the returned string contains null + characters (``'\\0'``) if they occurred in the input. + + :param int size: maximum length of returned string. + :return: + next line of the file, or an empty string if the end of the file has been reached. - @rtype: str + + If the file was opened in binary (``'b'``) mode: bytes are returned + Else: the encoding of the file is assumed to be UTF-8 and character + strings (`str`) are returned """ # it's almost silly how complex this function is. if self._closed: @@ -190,11 +210,11 @@ 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,84 +224,82 @@ 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') - if (rpos >= 0) and ((rpos < pos) or (pos < 0)): + 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): """ - Read all remaining lines using L{readline} and return them as a list. - If the optional C{sizehint} argument is present, instead of reading up + Read all remaining lines using `readline` and return them as a list. + If the optional ``sizehint`` argument is present, instead of reading up to EOF, whole lines totalling approximately sizehint bytes (possibly after rounding up to an internal buffer size) are read. - @param sizehint: desired maximum number of bytes to read. - @type sizehint: int - @return: list of lines read from the file. - @rtype: list + :param int sizehint: desired maximum number of bytes to read. + :return: `list` of lines read from the file. """ 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 def seek(self, offset, whence=0): """ - Set the file's current position, like stdio's C{fseek}. Not all file + Set the file's current position, like stdio's ``fseek``. Not all file objects support seeking. - @note: If a file is opened in append mode (C{'a'} or C{'a+'}), any seek + .. note:: + If a file is opened in append mode (``'a'`` or ``'a+'``), any seek operations will be undone at the next write (as the file position will move back to the end of the file). - @param offset: position to move to within the file, relative to - C{whence}. - @type offset: int - @param whence: type of movement: 0 = absolute; 1 = relative to the - current position; 2 = relative to the end of the file. - @type whence: int + :param int offset: + position to move to within the file, relative to ``whence``. + :param int whence: + type of movement: 0 = absolute; 1 = relative to the current + position; 2 = relative to the end of the file. - @raise IOError: if the file doesn't support random access. + :raises IOError: if the file doesn't support random access. """ raise IOError('File does not support seeking.') @@ -291,21 +309,20 @@ useful if the underlying file doesn't support random access, or was opened in append mode. - @return: file position (in bytes). - @rtype: int + :return: file position (`number ` of bytes). """ return self._pos def write(self, data): """ - Write data to the file. If write buffering is on (C{bufsize} was + Write data to the file. If write buffering is on (``bufsize`` was specified and non-zero), some or all of the data may not actually be - written yet. (Use L{flush} or L{close} to force buffered data to be + written yet. (Use `flush` or `close` to force buffered data to be written out.) - @param data: data to write. - @type data: str + :param str data: data to write """ + data = b(data) if self._closed: raise IOError('File is closed') if not (self._flags & self.FLAG_WRITE): @@ -316,12 +333,12 @@ 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 @@ -334,11 +351,10 @@ """ Write a sequence of strings to the file. The sequence can be any iterable object producing strings, typically a list of strings. (The - name is intended to match L{readlines}; C{writelines} does not add line + name is intended to match `readlines`; `writelines` does not add line separators.) - @param sequence: an iterable sequence of strings. - @type sequence: sequence + :param iterable sequence: an iterable sequence of strings. """ for line in sequence: self.write(line) @@ -346,11 +362,8 @@ def xreadlines(self): """ - Identical to C{iter(f)}. This is a deprecated file interface that - predates python iterator support. - - @return: an iterator. - @rtype: iterator + Identical to ``iter(f)``. This is a deprecated file interface that + predates Python iterator support. """ return self @@ -358,40 +371,36 @@ def closed(self): return self._closed - ### overrides... - def _read(self, size): """ - I{(subclass override)} - Read data from the stream. Return C{None} or raise C{EOFError} to + (subclass override) + Read data from the stream. Return ``None`` or raise ``EOFError`` to indicate EOF. """ raise EOFError() def _write(self, data): """ - I{(subclass override)} + (subclass override) Write data into the stream. """ raise IOError('write not implemented') def _get_size(self): """ - I{(subclass override)} - Return the size of the file. This is called from within L{_set_mode} + (subclass override) + Return the size of the file. This is called from within `_set_mode` if the file is opened in append mode, so the file position can be - tracked and L{seek} and L{tell} will work correctly. If the file is + tracked and `seek` and `tell` will work correctly. If the file is a stream that can't be randomly accessed, you don't need to override this method, """ return 0 - ### internals... - def _set_mode(self, mode='r', bufsize=-1): """ Subclasses call this method to initialize the BufferedFile. @@ -419,13 +428,13 @@ self._flags |= self.FLAG_READ if ('w' in mode) or ('+' in mode): self._flags |= self.FLAG_WRITE - if ('a' in mode): + if 'a' in mode: self._flags |= self.FLAG_WRITE | self.FLAG_APPEND self._size = self._get_size() self._pos = self._realpos = self._size - if ('b' in mode): + if 'b' in mode: self._flags |= self.FLAG_BINARY - if ('U' in mode): + if 'U' in mode: self._flags |= self.FLAG_UNIVERSAL_NEWLINE # built-in file objects have this attribute to store which kinds of # line terminations they've seen: @@ -454,7 +463,7 @@ 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 -Nru paramiko-1.10.1/paramiko/hostkeys.py paramiko-1.15.1/paramiko/hostkeys.py --- paramiko-1.10.1/paramiko/hostkeys.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/hostkeys.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -16,109 +16,45 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -""" -L{HostKeys} -""" -import base64 import binascii -from Crypto.Hash import SHA, HMAC -import UserDict +import os -from paramiko.common import * -from paramiko.dsskey import DSSKey -from paramiko.rsakey import RSAKey - - -class InvalidHostKey(Exception): - - def __init__(self, line, exc): - self.line = line - self.exc = exc - self.args = (line, exc) +from hashlib import sha1 +from hmac import HMAC +from paramiko.py3compat import b, u, encodebytes, decodebytes -class HostKeyEntry: - """ - Representation of a line in an OpenSSH-style "known hosts" file. - """ +try: + from collections import MutableMapping +except ImportError: + # noinspection PyUnresolvedReferences + from UserDict import DictMixin as MutableMapping - def __init__(self, hostnames=None, key=None): - self.valid = (hostnames is not None) and (key is not None) - self.hostnames = hostnames - self.key = key - - def from_line(cls, line): - """ - Parses the given line of text to find the names for the host, - the type of key, and the key data. The line is expected to be in the - format used by the openssh known_hosts file. - - Lines are expected to not have leading or trailing whitespace. - We don't bother to check for comments or empty lines. All of - that should be taken care of before sending the line to us. - - @param line: a line from an OpenSSH known_hosts file - @type line: str - """ - fields = line.split(' ') - if len(fields) < 3: - # Bad number of fields - return None - fields = fields[:3] - - names, keytype, key = fields - names = names.split(',') - - # Decide what kind of key we're looking at and create an object - # to hold it accordingly. - try: - if keytype == 'ssh-rsa': - key = RSAKey(data=base64.decodestring(key)) - elif keytype == 'ssh-dss': - key = DSSKey(data=base64.decodestring(key)) - else: - return None - except binascii.Error, e: - raise InvalidHostKey(line, e) - - return cls(names, key) - from_line = classmethod(from_line) - - def to_line(self): - """ - Returns a string in OpenSSH known_hosts file format, or None if - the object is not in a valid state. A trailing newline is - included. - """ - if self.valid: - return '%s %s %s\n' % (','.join(self.hostnames), self.key.get_name(), - self.key.get_base64()) - return None - - def __repr__(self): - return '' % (self.hostnames, self.key) +from paramiko.dsskey import DSSKey +from paramiko.rsakey import RSAKey +from paramiko.util import get_logger, constant_time_bytes_eq +from paramiko.ecdsakey import ECDSAKey -class HostKeys (UserDict.DictMixin): +class HostKeys (MutableMapping): """ - Representation of an openssh-style "known hosts" file. Host keys can be + Representation of an OpenSSH-style "known hosts" file. Host keys can be read from one or more files, and then individual hosts can be looked up to verify server keys during SSH negotiation. - A HostKeys object can be treated like a dict; any dict lookup is equivalent - to calling L{lookup}. + A `.HostKeys` object can be treated like a dict; any dict lookup is + equivalent to calling `lookup`. - @since: 1.5.3 + .. versionadded:: 1.5.3 """ def __init__(self, filename=None): """ - Create a new HostKeys object, optionally loading keys from an openssh + Create a new HostKeys object, optionally loading keys from an OpenSSH style host-key file. - @param filename: filename to load host keys from, or C{None} - @type filename: str + :param str filename: filename to load host keys from, or ``None`` """ # emulate a dict of { hostname: { keytype: PKey } } self._entries = [] @@ -128,14 +64,11 @@ def add(self, hostname, keytype, key): """ Add a host key entry to the table. Any existing entry for a - C{(hostname, keytype)} pair will be replaced. + ``(hostname, keytype)`` pair will be replaced. - @param hostname: the hostname (or IP) to add - @type hostname: str - @param keytype: key type (C{"ssh-rsa"} or C{"ssh-dss"}) - @type keytype: str - @param key: the key to add - @type key: L{PKey} + :param str hostname: the hostname (or IP) to add + :param str keytype: key type (``"ssh-rsa"`` or ``"ssh-dss"``) + :param .PKey key: the key to add """ for e in self._entries: if (hostname in e.hostnames) and (e.key.get_name() == keytype): @@ -145,68 +78,81 @@ def load(self, filename): """ - Read a file of known SSH host keys, in the format used by openssh. + Read a file of known SSH host keys, in the format used by OpenSSH. This type of file unfortunately doesn't exist on Windows, but on posix, it will usually be stored in - C{os.path.expanduser("~/.ssh/known_hosts")}. + ``os.path.expanduser("~/.ssh/known_hosts")``. If this method is called multiple times, the host keys are merged, - not cleared. So multiple calls to C{load} will just call L{add}, + not cleared. So multiple calls to `load` will just call `add`, replacing any existing entries and adding new ones. - @param filename: name of the file to read host keys from - @type filename: str + :param str filename: name of the file to read host keys from - @raise IOError: if there was an error reading the file + :raises IOError: if there was an error reading the file """ - f = open(filename, 'r') - for line in f: - line = line.strip() - if (len(line) == 0) or (line[0] == '#'): - continue - e = HostKeyEntry.from_line(line) - if e is not None: - 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): """ - Save host keys into a file, in the format used by openssh. The order of + Save host keys into a file, in the format used by OpenSSH. The order of keys in the file will be preserved when possible (if these keys were loaded from a file originally). The single exception is that combined lines will be split into individual key lines, which is arguably a bug. - @param filename: name of the file to write - @type filename: str + :param str filename: name of the file to write - @raise IOError: if there was an error writing the file + :raises IOError: if there was an error writing the file - @since: 1.6.1 + .. versionadded:: 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): """ Find a hostkey entry for a given hostname or IP. If no entry is found, - C{None} is returned. Otherwise a dictionary of keytype to key is - returned. The keytype will be either C{"ssh-rsa"} or C{"ssh-dss"}. + ``None`` is returned. Otherwise a dictionary of keytype to key is + returned. The keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. - @param hostname: the hostname (or IP) to lookup - @type hostname: str - @return: keys associated with this host (or C{None}) - @rtype: dict(str, L{PKey}) + :param str hostname: the hostname (or IP) to lookup + :return: dict of `str` -> `.PKey` keys associated with this host (or ``None``) """ - 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: @@ -233,7 +179,7 @@ entries = [] for e in self._entries: for h in e.hostnames: - if (h.startswith('|1|') and (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 @@ -244,13 +190,10 @@ Return True if the given key is associated with the given hostname in this dictionary. - @param hostname: hostname (or IP) of the SSH server - @type hostname: str - @param key: the key to check - @type key: L{PKey} - @return: C{True} if the key is associated with the hostname; C{False} - if not - @rtype: bool + :param str hostname: hostname (or IP) of the SSH server + :param .PKey key: the key to check + :return: + ``True`` if the key is associated with the hostname; else ``False`` """ k = self.lookup(hostname) if k is None: @@ -258,7 +201,7 @@ 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): """ @@ -266,6 +209,16 @@ """ 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] + def __getitem__(self, key): ret = self.lookup(key) if ret is None: @@ -288,7 +241,7 @@ self._entries.append(HostKeyEntry([hostname], entry[key_type])) def keys(self): - # python 2.4 sets would be nice here. + # Python 2.4 sets would be nice here. ret = [] for e in self._entries: for h in e.hostnames: @@ -304,25 +257,97 @@ def hash_host(hostname, salt=None): """ - Return a "hashed" form of the hostname, as used by openssh when storing + Return a "hashed" form of the hostname, as used by OpenSSH when storing hashed hostnames in the known_hosts file. - @param hostname: the hostname to hash - @type hostname: str - @param salt: optional salt to use when hashing (must be 20 bytes long) - @type salt: str - @return: the hashed hostname - @rtype: str + :param str hostname: the hostname to hash + :param str salt: optional salt to use when hashing (must be 20 bytes long) + :return: the hashed hostname as a `str` """ if salt is None: - salt = rng.read(SHA.digest_size) + salt = os.urandom(sha1().digest_size) else: if salt.startswith('|1|'): salt = salt.split('|')[2] - salt = base64.decodestring(salt) - assert len(salt) == SHA.digest_size - hmac = HMAC.HMAC(salt, hostname, SHA).digest() - hostkey = '|1|%s|%s' % (base64.encodestring(salt), base64.encodestring(hmac)) + salt = decodebytes(b(salt)) + assert len(salt) == sha1().digest_size + hmac = HMAC(salt, b(hostname), sha1).digest() + hostkey = '|1|%s|%s' % (u(encodebytes(salt)), u(encodebytes(hmac))) return hostkey.replace('\n', '') hash_host = staticmethod(hash_host) + +class InvalidHostKey(Exception): + def __init__(self, line, exc): + self.line = line + self.exc = exc + self.args = (line, exc) + + +class HostKeyEntry: + """ + Representation of a line in an OpenSSH-style "known hosts" file. + """ + + def __init__(self, hostnames=None, key=None): + self.valid = (hostnames is not None) and (key is not None) + self.hostnames = hostnames + self.key = key + + def from_line(cls, line, lineno=None): + """ + Parses the given line of text to find the names for the host, + the type of key, and the key data. The line is expected to be in the + format used by the OpenSSH known_hosts file. + + Lines are expected to not have leading or trailing whitespace. + We don't bother to check for comments or empty lines. All of + that should be taken care of before sending the line to us. + + :param str line: a line from an OpenSSH known_hosts file + """ + log = get_logger('paramiko.hostkeys') + fields = line.split(' ') + if len(fields) < 3: + # Bad number of fields + log.info("Not enough fields found in known_hosts in line %s (%r)" % + (lineno, line)) + return None + fields = fields[:3] + + names, keytype, key = fields + names = names.split(',') + + # 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=decodebytes(key)) + elif keytype == 'ssh-dss': + key = DSSKey(data=decodebytes(key)) + elif keytype == 'ecdsa-sha2-nistp256': + key = ECDSAKey(data=decodebytes(key), validate_point=False) + else: + log.info("Unable to handle key of type %s" % (keytype,)) + return None + + except binascii.Error as e: + raise InvalidHostKey(line, e) + + return cls(names, key) + from_line = classmethod(from_line) + + def to_line(self): + """ + Returns a string in OpenSSH known_hosts file format, or None if + the object is not in a valid state. A trailing newline is + included. + """ + if self.valid: + return '%s %s %s\n' % (','.join(self.hostnames), self.key.get_name(), + self.key.get_base64()) + return None + + def __repr__(self): + return '' % (self.hostnames, self.key) diff -Nru paramiko-1.10.1/paramiko/__init__.py paramiko-1.15.1/paramiko/__init__.py --- paramiko-1.10.1/paramiko/__init__.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/__init__.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -16,90 +16,53 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -""" -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 -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 -replaced C{telnet} and C{rsh} for secure access to remote shells, but the -protocol also includes the ability to open arbitrary channels to remote -services across an encrypted tunnel. (This is how C{sftp} works, for example.) - -The high-level client API starts with creation of an L{SSHClient} object. -For more direct control, pass a socket (or socket-like object) to a -L{Transport}, and use L{start_server } or -L{start_client } to negoatite -with the remote host as either a server or client. As a client, you are -responsible for authenticating using a password or private key, and checking -the server's host key. I{(Key signature and verification is done by paramiko, -but you will need to provide private keys and check that the content of a -public key matches what you expected to see.)} As a server, you are -responsible for deciding which users, passwords, and keys to allow, and what -kind of channels to allow. - -Once you have finished, either side may request flow-controlled L{Channel}s to -the other side, which are python objects that act like sockets, but send and -receive data over the encrypted session. - -Paramiko is written entirely in python (no C or platform-dependent code) and is -released under the GNU Lesser General Public License (LGPL). - -Website: U{https://github.com/paramiko/paramiko/} -""" - import sys +from paramiko._version import __version__, __version_info__ -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.10.1" __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.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE +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 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 - -# fix module names for epydoc -for c in 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.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 + +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 -Nru paramiko-1.10.1/paramiko/kex_gex.py paramiko-1.15.1/paramiko/kex_gex.py --- paramiko-1.10.1/paramiko/kex_gex.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/kex_gex.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,22 +17,25 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -Variant on L{KexGroup1 } where the prime "p" and +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 -from Crypto.Util import number +import os +from hashlib import sha1 -from paramiko.common import * from paramiko import util +from paramiko.common import DEBUG from paramiko.message import Message +from paramiko.py3compat import byte_chr, byte_ord, byte_mask 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 +65,11 @@ 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) @@ -86,23 +89,21 @@ return self._parse_kexdh_gex_request_old(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 = 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 = os.urandom(byte_count) + x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] x = util.inflate_long(x_bytes, 1) if (x > 1) and (x < q): break @@ -135,7 +136,7 @@ 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 +157,7 @@ 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 +176,7 @@ # 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 +188,7 @@ 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,19 +204,19 @@ hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(str(hm)).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(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() - + def _parse_kexdh_gex_reply(self, m): host_key = m.get_string() self.f = m.get_mpint() @@ -238,6 +239,6 @@ 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(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() diff -Nru paramiko-1.10.1/paramiko/kex_group14.py paramiko-1.15.1/paramiko/kex_group14.py --- paramiko-1.10.1/paramiko/kex_group14.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/kex_group14.py 2014-09-22 18:31:58.000000000 +0000 @@ -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 -Nru paramiko-1.10.1/paramiko/kex_group1.py paramiko-1.15.1/paramiko/kex_group1.py --- paramiko-1.10.1/paramiko/kex_group1.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/kex_group1.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,42 +21,48 @@ 1024 bit key halves, using a known "p" prime and "g" generator. """ -from Crypto.Hash import SHA +import os +from hashlib import sha1 -from paramiko.common import * from paramiko import util +from paramiko.common import max_byte, zero_byte from paramiko.message import Message +from paramiko.py3compat import byte_chr, long, byte_mask 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 -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 + name = 'diffie-hellman-group1-sha1' 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() 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(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,11 +73,9 @@ 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... - 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. @@ -79,10 +83,10 @@ # 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 = 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 = os.urandom(128) + 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) @@ -90,10 +94,10 @@ # 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_string() - K = pow(self.f, self.x, P) + sig = m.get_binary() + 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, @@ -102,17 +106,17 @@ 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(hm.asbytes()).digest()) self.transport._verify_key(host_key, sig) self.transport._activate_outbound() 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) - key = str(self.transport.get_server_key()) + 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() hm.add(self.transport.remote_version, self.transport.local_version, @@ -121,15 +125,15 @@ hm.add_mpint(self.e) hm.add_mpint(self.f) hm.add_mpint(K) - H = SHA.new(str(hm)).digest() + H = sha1(hm.asbytes()).digest() self.transport._set_K_H(K, H) # sign it - sig = self.transport.get_server_key().sign_ssh_data(self.transport.rng, H) + sig = self.transport.get_server_key().sign_ssh_data(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 -Nru paramiko-1.10.1/paramiko/kex_gss.py paramiko-1.15.1/paramiko/kex_gss.py --- paramiko-1.10.1/paramiko/kex_gss.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/kex_gss.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,603 @@ +# 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 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` + +.. versionadded:: 1.15 +""" + +from hashlib import sha1 + +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 + + +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 `RFC 4462 Section 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 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) + 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 `.Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY 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 `.Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE 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 `.Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE 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, sha1(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 `.Message` m: The content of the SSH2_MSG_KEXGSS_INIT 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 = sha1(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 `.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 + """ + 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 `RFC 4462 Section 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 `RFC 4462 Section 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 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) + 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 `.Message` m: The content of the SSH2_MSG_KEXGSS_GROUPREQ 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 `Message` m: The content of the SSH2_MSG_KEXGSS_GROUP 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 `Message` m: The content of the SSH2_MSG_KEXGSS_INIT 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 = sha1(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 `Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY 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 `Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE 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 `Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE 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 = 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, + 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 `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 + """ + 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 `RFC 4462 Section 5 `_ + """ + def __init__(self): + self.key = "" + + def __str__(self): + return self.key + + def get_name(self): + return self.key diff -Nru paramiko-1.10.1/paramiko/logging22.py paramiko-1.15.1/paramiko/logging22.py --- paramiko-1.10.1/paramiko/logging22.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/logging22.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,66 +0,0 @@ -# Copyright (C) 2003-2007 Robey Pointer -# -# 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. - -""" -Stub out logging on python < 2.3. -""" - - -DEBUG = 10 -INFO = 20 -WARNING = 30 -ERROR = 40 -CRITICAL = 50 - - -def getLogger(name): - return _logger - - -class logger (object): - def __init__(self): - self.handlers = [ ] - self.level = ERROR - - def setLevel(self, level): - self.level = level - - def addHandler(self, h): - self.handlers.append(h) - - def addFilter(self, filter): - pass - - def log(self, level, text): - if level >= self.level: - for h in self.handlers: - h.f.write(text + '\n') - h.f.flush() - -class StreamHandler (object): - def __init__(self, f): - self.f = f - - def setFormatter(self, f): - pass - -class Formatter (object): - def __init__(self, x, y): - pass - -_logger = logger() diff -Nru paramiko-1.10.1/paramiko/message.py paramiko-1.15.1/paramiko/message.py --- paramiko-1.10.1/paramiko/message.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/message.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,52 +21,56 @@ """ import struct -import cStringIO from paramiko import util +from paramiko.common import zero_byte, max_byte, one_byte, asbytes +from paramiko.py3compat import long, BytesIO, u, integer_types class Message (object): """ - An SSH2 I{Message} is a stream of bytes that encodes some combination of - strings, integers, bools, and infinite-precision integers (known in python - as I{long}s). This class builds or breaks down such a byte stream. + An SSH2 message is a stream of bytes that encodes some combination of + strings, integers, bools, and infinite-precision integers (known in Python + as longs). This class builds or breaks down such a byte stream. Normally you don't need to deal with anything this low-level, but it's exposed for people implementing custom extensions, or features that paramiko doesn't support yet. """ + big_int = long(0xff000000) + def __init__(self, content=None): """ - Create a new SSH2 Message. + Create a new SSH2 message. - @param content: the byte stream to use as the Message content (passed - in only when decomposing a Message). - @type content: string + :param str content: + the byte stream to use as the message content (passed in only when + decomposing a message). """ - if content != None: - self.packet = cStringIO.StringIO(content) + if content is not None: + 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 the byte stream content of this message, as a string/bytes obj. """ - return self.packet.getvalue() + return self.asbytes() def __repr__(self): """ Returns a string representation of this object, for debugging. - - @rtype: string """ return 'paramiko.Message(' + repr(self.packet.getvalue()) + ')' + def asbytes(self): + """ + Return the byte stream content of this Message, as bytes. + """ + return self.packet.getvalue() + def rewind(self): """ Rewind the message to the beginning as if no items had been parsed @@ -76,11 +80,8 @@ def get_remainder(self): """ - Return the bytes of this Message that haven't already been parsed and - returned. - - @return: a string of the bytes not parsed yet. - @rtype: string + Return the bytes (as a `str`) of this message that haven't already been + parsed and returned. """ position = self.packet.tell() remainder = self.packet.read() @@ -89,12 +90,9 @@ def get_so_far(self): """ - Returns the bytes of this Message that have been parsed and returned. - The string passed into a Message's constructor can be regenerated by - concatenating C{get_so_far} and L{get_remainder}. - - @return: a string of the bytes parsed so far. - @rtype: string + Returns the `str` bytes of this message that have been parsed and + returned. The string passed into a message's constructor can be + regenerated by concatenating ``get_so_far`` and `get_remainder`. """ position = self.packet.tell() self.rewind() @@ -102,44 +100,64 @@ def get_bytes(self, n): """ - Return the next C{n} bytes of the Message, without decomposing into - an int, string, etc. Just the raw bytes are returned. - - @return: a string of the next C{n} bytes of the Message, or a string - of C{n} zero bytes, if there aren't C{n} bytes remaining. - @rtype: string + Return the next ``n`` bytes of the message (as a `str`), without + decomposing into an int, decoded string, etc. Just the raw bytes are + returned. Returns a string of ``n`` zero bytes if there weren't ``n`` + bytes remaining in the message. """ 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)) + max_pad_size = 1 << 20 # Limit padding to 1 MB + if len(b) < n < max_pad_size: + return b + zero_byte * (n - len(b)) return b def get_byte(self): """ - Return the next byte of the Message, without decomposing it. This - is equivalent to L{get_bytes(1)}. + Return the next byte of the message, without decomposing it. This + is equivalent to `get_bytes(1) `. - @return: the next byte of the Message, or C{'\000'} if there aren't + :return: + the next (`str`) byte of the message, or ``'\000'`` if there aren't any bytes remaining. - @rtype: string """ return self.get_bytes(1) def get_boolean(self): """ Fetch a boolean from the stream. - - @return: C{True} or C{False} (from the Message). - @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 `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 + """ + 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 """ @@ -149,8 +167,7 @@ """ Fetch a 64-bit int from the stream. - @return: a 64-bit unsigned integer. - @rtype: long + :return: a 64-bit unsigned integer (`long`). """ return struct.unpack('>Q', self.get_bytes(8))[0] @@ -158,13 +175,20 @@ """ Fetch a long int (mpint) from the stream. - @return: an arbitrary-length integer. - @rtype: long + :return: an arbitrary-length integer (`long`). """ - return util.inflate_long(self.get_string()) + return util.inflate_long(self.get_binary()) def get_string(self): """ + Fetch a `str` 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 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.) @@ -172,24 +196,33 @@ @return: a string. @rtype: string """ - return self.get_bytes(self.get_int()) + return u(self.get_bytes(self.get_size())) + #return self.get_bytes(self.get_size()) - def get_list(self): + def get_binary(self): """ - Fetch a list of strings from the stream. These are trivially encoded - as comma-separated values in a string. + 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 list of strings. - @rtype: list of strings + @return: a string. + @rtype: string """ - return self.get_string().split(',') + return self.get_bytes(self.get_size()) + + def get_list(self): + """ + Fetch a `list` of `strings ` from the stream. + + These are trivially encoded as comma-separated values in a string. + """ + return self.get_text().split(',') def add_bytes(self, b): """ Write bytes to the stream, without any formatting. - @param b: bytes to add - @type b: str + :param str b: bytes to add """ self.packet.write(b) return self @@ -198,8 +231,7 @@ """ Write a single byte to the stream, without any formatting. - @param b: byte to add - @type b: str + :param str b: byte to add """ self.packet.write(b) return self @@ -208,31 +240,55 @@ """ Add a boolean value to the stream. - @param b: boolean value to add - @type b: bool + :param bool b: boolean value to add """ 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_size(self, n): + """ + Add an integer to the stream. + + :param int n: integer to add + """ + self.packet.write(struct.pack('>I', n)) return self def add_int(self, n): """ Add an integer to the stream. + :param int n: integer to add + """ + 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_int(self, n): + """ + Add an integer to the stream. + @param n: integer to add @type n: int """ - self.packet.write(struct.pack('>I', n)) + 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. - @param n: long int to add - @type n: long + :param long n: long int to add """ self.packet.write(struct.pack('>Q', n)) return self @@ -242,8 +298,7 @@ Add a long int to the stream, encoded as an infinite-precision integer. This method only works on positive numbers. - @param z: long int to add - @type z: long + :param long z: long int to add """ self.add_string(util.deflate_long(z)) return self @@ -252,10 +307,10 @@ """ Add a string to the stream. - @param s: string to add - @type s: str + :param str s: string to add """ - self.add_int(len(s)) + s = asbytes(s) + self.add_size(len(s)) self.packet.write(s) return self @@ -265,38 +320,30 @@ a single string of values separated by commas. (Yes, really, that's how SSH2 does it.) - @param l: list of strings to add - @type l: list(str) + :param list l: list of strings to add """ self.add_string(','.join(l)) 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): """ Add a sequence of items to the stream. The values are encoded based on their type: str, int, bool, list, or long. + + .. warning:: + Longs are encoded non-deterministically. Don't use this method. - @param seq: the sequence of items - @type seq: sequence - - @bug: longs are encoded non-deterministically. Don't use this method. + :param seq: the sequence of items """ for item in seq: self._add(item) diff -Nru paramiko-1.10.1/paramiko/packet.py paramiko-1.15.1/paramiko/packet.py --- paramiko-1.10.1/paramiko/packet.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/packet.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,33 +17,27 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -Packetizer. +Packet handling """ import errno -import select +import os import socket import struct import threading import time +from hmac import HMAC -from paramiko.common import * from paramiko import util +from paramiko.common import linefeed_byte, cr_byte_value, asbytes, MSG_NAMES, \ + DEBUG, xffffffff, zero_byte +from paramiko.py3compat import u, byte_ord from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message -got_r_hmac = False -try: - import r_hmac - got_r_hmac = True -except ImportError: - pass def compute_hmac(key, message, digest_class): - if got_r_hmac: - return r_hmac.HMAC(key, message, digest_class).digest() - from Crypto.Hash import HMAC - return HMAC.HMAC(key, message, digest_class).digest() + return HMAC(key, message, digest_class).digest() class NeedRekeyException (Exception): @@ -60,8 +54,8 @@ REKEY_PACKETS = pow(2, 29) REKEY_BYTES = pow(2, 29) - REKEY_PACKETS_OVERFLOW_MAX = pow(2,29) # Allow receiving this many packets after a re-key request before terminating - REKEY_BYTES_OVERFLOW_MAX = pow(2,29) # Allow receiving this many bytes after a re-key request before terminating + REKEY_PACKETS_OVERFLOW_MAX = pow(2, 29) # Allow receiving this many packets after a re-key request before terminating + REKEY_BYTES_OVERFLOW_MAX = pow(2, 29) # Allow receiving this many bytes after a re-key request before terminating def __init__(self, socket): self.__socket = socket @@ -70,7 +64,7 @@ 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 @@ -90,12 +84,12 @@ 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() @@ -107,7 +101,7 @@ def set_log(self, log): """ - Set the python log object to use for logging. + Set the Python log object to use for logging. """ self.__logger = log @@ -172,17 +166,15 @@ def need_rekey(self): """ - Returns C{True} if a new set of keys needs to be negotiated. This + Returns ``True`` if a new set of keys needs to be negotiated. This will be triggered during a packet read or write, so it should be checked after every read or write, or at least after every few. - - @return: C{True} if a new set of keys needs to be negotiated """ return self.__need_rekey def set_keepalive(self, interval, callback): """ - Turn on/off the callback keepalive. If C{interval} seconds pass with + Turn on/off the callback keepalive. If ``interval`` seconds pass with no data read from or written to the socket, the callback will be executed and the timer will be reset. """ @@ -194,21 +186,18 @@ """ Read as close to N bytes as possible, blocking as long as necessary. - @param n: number of bytes to read - @type n: int - @return: the data read - @rtype: str - @raise EOFError: if the socket was closed before all the bytes could - be read + :param int n: number of bytes to read + :return: the data read, as a `str` + + :raises 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] self.__remainder = self.__remainder[n:] n -= len(out) - if PY22: - return self._py22_read_all(n, out) while n > 0: got_timeout = False try: @@ -219,7 +208,7 @@ 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. @@ -242,13 +231,14 @@ 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: 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): @@ -257,7 +247,7 @@ else: n = -1 except ProxyCommandFailure: - raise # so it doesn't get swallowed by the below catchall + raise # so it doesn't get swallowed by the below catchall except Exception: # could be: (32, 'Broken pipe') n = -1 @@ -265,6 +255,15 @@ 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): @@ -278,22 +277,22 @@ 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') - self.__remainder = buf[n+1:] + 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: @@ -307,21 +306,21 @@ if self.__dump_packets: self._log(DEBUG, 'Write packet <%s>, length %d' % (cmd_name, orig_len)) self._log(DEBUG, util.format_binary(packet, 'OUT: ')) - if self.__block_engine_out != None: + if self.__block_engine_out is not None: out = self.__block_engine_out.encrypt(packet) else: out = packet # + mac - if self.__block_engine_out != None: + if self.__block_engine_out is not 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) self.__sent_packets += 1 - if ((self.__sent_packets >= self.REKEY_PACKETS) or (self.__sent_bytes >= self.REKEY_BYTES)) \ - and not self.__need_rekey: + if (self.__sent_packets >= self.REKEY_PACKETS or self.__sent_bytes >= self.REKEY_BYTES)\ + and not self.__need_rekey: # only ask once for rekeying self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes sent)' % (self.__sent_packets, self.__sent_bytes)) @@ -336,14 +335,14 @@ Only one thread should ever be in this function (no other locking is done). - @raise SSHException: if the packet is mangled - @raise NeedRekeyException: if the transport should rekey + :raises SSHException: if the packet is mangled + :raises NeedRekeyException: if the transport should rekey """ header = self.read_all(self.__block_size_in, check_rekey=True) - if self.__block_engine_in != None: + if self.__block_engine_in is not None: header = self.__block_engine_in.decrypt(header) if self.__dump_packets: - self._log(DEBUG, util.format_binary(header, 'IN: ')); + self._log(DEBUG, util.format_binary(header, 'IN: ')) packet_size = struct.unpack('>I', header[:4])[0] # leftover contains decrypted bytes from the first block (after the length field) leftover = header[4:] @@ -352,21 +351,21 @@ buf = self.read_all(packet_size + self.__mac_size_in - len(leftover)) packet = buf[:packet_size - len(leftover)] post_packet = buf[packet_size - len(leftover):] - if self.__block_engine_in != None: + if self.__block_engine_in is not None: packet = self.__block_engine_in.decrypt(packet) if self.__dump_packets: - self._log(DEBUG, util.format_binary(packet, 'IN: ')); + self._log(DEBUG, util.format_binary(packet, 'IN: ')) packet = leftover + packet if self.__mac_size_in > 0: mac = post_packet[:self.__mac_size_in] mac_payload = struct.pack('>II', self.__sequence_number_in, packet_size) + packet my_mac = compute_hmac(self.__mac_key_in, mac_payload, self.__mac_engine_in)[:self.__mac_size_in] - if my_mac != mac: + if not util.constant_time_bytes_eq(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: self._log(DEBUG, 'Got payload (%d bytes, %d padding)' % (packet_size, padding)) @@ -375,7 +374,7 @@ 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 @@ -398,7 +397,7 @@ 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: @@ -407,10 +406,8 @@ self._log(DEBUG, 'Read packet <%s>, length %d' % (cmd_name, len(payload))) return cmd, msg - ########## protected - def _log(self, level, msg): if self.__logger is None: return @@ -422,7 +419,7 @@ def _check_keepalive(self): if (not self.__keepalive_interval) or (not self.__block_engine_out) or \ - self.__need_rekey: + self.__need_rekey: # wait till we're encrypting, and not in the middle of rekeying return now = time.time() @@ -430,40 +427,7 @@ self.__keepalive_callback() self.__keepalive_last = now - def _py22_read_all(self, n, out): - while n > 0: - r, w, e = select.select([self.__socket], [], [], 0.1) - if self.__socket not in r: - if self.__closed: - raise EOFError() - self._check_keepalive() - else: - x = self.__socket.recv(n) - if len(x) == 0: - raise EOFError() - out += x - n -= len(x) - return out - - def _py22_read_timeout(self, timeout): - start = time.time() - while True: - r, w, e = select.select([self.__socket], [], [], 0.1) - if self.__socket in r: - x = self.__socket.recv(1) - if len(x) == 0: - raise EOFError() - break - if self.__closed: - raise EOFError() - now = time.time() - if now - start >= timeout: - raise socket.timeout() - return x - def _read_timeout(self, timeout): - if PY22: - return self._py22_read_timeout(timeout) start = time.time() while True: try: @@ -473,9 +437,9 @@ break except socket.timeout: pass - except EnvironmentError, e: - if ((type(e.args) is tuple) and (len(e.args) > 0) and - (e.args[0] == errno.EINTR)): + except EnvironmentError as e: + if (type(e.args) is tuple and len(e.args) > 0 and + e.args[0] == errno.EINTR): pass else: raise @@ -495,9 +459,9 @@ 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) + packet += os.urandom(padding) return packet def _trigger_rekey(self): diff -Nru paramiko-1.10.1/paramiko/pipe.py paramiko-1.15.1/paramiko/pipe.py --- paramiko-1.10.1/paramiko/pipe.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/pipe.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,19 +17,21 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -Abstraction of a one-way pipe where the read end can be used in select(). -Normally this is trivial, but Windows makes it nearly impossible. +Abstraction of a one-way pipe where the read end can be used in +`select.select`. Normally this is trivial, but Windows makes it nearly +impossible. The pipe acts like an Event, which can be set or cleared. When set, the pipe -will trigger as readable in select(). +will trigger as readable in `select `. """ import sys import os import socket +from paramiko.py3compat import b -def make_pipe (): +def make_pipe(): if sys.platform[:3] != 'win': p = PosixPipe() else: @@ -38,34 +40,34 @@ class PosixPipe (object): - def __init__ (self): + def __init__(self): self._rfd, self._wfd = os.pipe() self._set = False self._forever = False self._closed = False - def close (self): + def close(self): os.close(self._rfd) os.close(self._wfd) # used for unit tests: self._closed = True - def fileno (self): + def fileno(self): return self._rfd - def clear (self): + def clear(self): if not self._set or self._forever: return os.read(self._rfd, 1) self._set = False - def set (self): + def set(self): if self._set or self._closed: return self._set = True - os.write(self._wfd, '*') + os.write(self._wfd, b'*') - def set_forever (self): + def set_forever(self): self._forever = True self.set() @@ -75,7 +77,7 @@ On Windows, only an OS-level "WinSock" may be used in select(), but reads and writes must be to the actual socket object. """ - def __init__ (self): + def __init__(self): serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serv.bind(('127.0.0.1', 0)) serv.listen(1) @@ -90,13 +92,13 @@ self._forever = False self._closed = False - def close (self): + def close(self): self._rsock.close() self._wsock.close() # used for unit tests: self._closed = True - def fileno (self): + def fileno(self): return self._rsock.fileno() def clear (self): @@ -109,7 +111,7 @@ 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 -Nru paramiko-1.10.1/paramiko/pkey.py paramiko-1.15.1/paramiko/pkey.py --- paramiko-1.10.1/paramiko/pkey.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/pkey.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -23,13 +23,13 @@ import base64 from binascii import hexlify, unhexlify import os +from hashlib import md5 -from Crypto.Hash import MD5 from Crypto.Cipher import DES3, AES -from paramiko.common import * from paramiko import util -from paramiko.message import Message +from paramiko.common import o600, zero_byte +from paramiko.py3compat import u, encodebytes, decodebytes, b from paramiko.ssh_exception import SSHException, PasswordRequiredException @@ -40,40 +40,39 @@ # known encryption types for private key files: _CIPHER_TABLE = { - 'AES-128-CBC': { 'cipher': AES, 'keysize': 16, 'blocksize': 16, 'mode': AES.MODE_CBC }, - 'DES-EDE3-CBC': { 'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': DES3.MODE_CBC }, + 'AES-128-CBC': {'cipher': AES, 'keysize': 16, 'blocksize': 16, 'mode': AES.MODE_CBC}, + 'DES-EDE3-CBC': {'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': DES3.MODE_CBC}, } - def __init__(self, msg=None, data=None): """ - Create a new instance of this public key type. If C{msg} is given, + Create a new instance of this public key type. If ``msg`` is given, the key's public part(s) will be filled in from the message. If - C{data} is given, the key's public part(s) will be filled in from + ``data`` is given, the key's public part(s) will be filled in from the string. - @param msg: an optional SSH L{Message} containing a public key of this - type. - @type msg: L{Message} - @param data: an optional string containing a public key of this type - @type data: str - - @raise SSHException: if a key cannot be created from the C{data} or - C{msg} given, or no key was passed in. + :param .Message msg: + an optional SSH `.Message` containing a public key of this type. + :param str data: an optional string containing a public key of this type + + :raises SSHException: + if a key cannot be created from the ``data`` or ``msg`` given, or + no key was passed in. """ 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 + Return a string of an SSH `.Message` made up of the public part(s) of + this key. This string is suitable for passing to `__init__` to re-create the key object later. - - @return: string representation of an SSH key message. - @rtype: str """ - return '' + return bytes() + + def __str__(self): + return self.asbytes() + # noinspection PyUnresolvedReferences def __cmp__(self, other): """ Compare this key to another. Returns 0 if this key is equivalent to @@ -81,24 +80,24 @@ of the key are compared, so a public key will compare equal to its corresponding private key. - @param other: key to compare to. - @type other: L{PKey} - @return: 0 if the two keys are equivalent, non-0 otherwise. - @rtype: int + :param .Pkey other: key to compare to. """ hs = hash(self) 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): """ Return the name of this private key implementation. - @return: name of this private key type, in SSH terminology (for - example, C{"ssh-rsa"}). - @rtype: str + :return: + name of this private key type, in SSH terminology, as a `str` (for + example, ``"ssh-rsa"``). """ return '' @@ -107,18 +106,14 @@ Return the number of significant bits in this key. This is useful for judging the relative security of a key. - @return: bits in the key. - @rtype: int + :return: bits in the key (as an `int`) """ return 0 def can_sign(self): """ - Return C{True} if this key has the private part necessary for signing + Return ``True`` if this key has the private part necessary for signing data. - - @return: C{True} if this is a private key. - @rtype: bool """ return False @@ -127,11 +122,11 @@ Return an MD5 fingerprint of the public part of this key. Nothing secret is revealed. - @return: a 16-byte string (binary) of the MD5 fingerprint, in SSH + :return: + a 16-byte `string ` (binary) of the MD5 fingerprint, in SSH format. - @rtype: str """ - return MD5.new(str(self)).digest() + return md5(self.asbytes()).digest() def get_base64(self): """ @@ -139,61 +134,50 @@ secret is revealed. This format is compatible with that used to store public key files or recognized host keys. - @return: a base64 string containing the public part of the key. - @rtype: str + :return: a base64 `string ` containing the public part of the key. """ - return base64.encodestring(str(self)).replace('\n', '') + return u(encodebytes(self.asbytes())).replace('\n', '') - def sign_ssh_data(self, rng, data): + def sign_ssh_data(self, data): """ - Sign a blob of data with this private key, and return a L{Message} + Sign a blob of data with this private key, and return a `.Message` representing an SSH signature message. - @param rng: a secure random number generator. - @type rng: L{Crypto.Util.rng.RandomPool} - @param data: the data to sign. - @type data: str - @return: an SSH signature message. - @rtype: L{Message} + :param str data: the data to sign. + :return: an SSH signature `message <.Message>`. """ - return '' + return bytes() def verify_ssh_sig(self, data, msg): """ Given a blob of data, and an SSH message representing a signature of that data, verify that it was signed with this key. - @param data: the data that was signed. - @type data: str - @param msg: an SSH signature message - @type msg: L{Message} - @return: C{True} if the signature verifies correctly; C{False} - otherwise. - @rtype: boolean + :param str data: the data that was signed. + :param .Message msg: an SSH signature message + :return: + ``True`` if the signature verifies correctly; ``False`` otherwise. """ return False def from_private_key_file(cls, filename, password=None): """ Create a key object by reading a private key file. If the private - key is encrypted and C{password} is not C{None}, the given password - will be used to decrypt the key (otherwise L{PasswordRequiredException} - is thrown). Through the magic of python, this factory method will - exist in all subclasses of PKey (such as L{RSAKey} or L{DSSKey}), but + key is encrypted and ``password`` is not ``None``, the given password + will be used to decrypt the key (otherwise `.PasswordRequiredException` + is thrown). Through the magic of Python, this factory method will + exist in all subclasses of PKey (such as `.RSAKey` or `.DSSKey`), but is useless on the abstract PKey class. - @param filename: name of the file to read - @type filename: str - @param password: an optional password to use to decrypt the key file, + :param str filename: name of the file to read + :param str password: an optional password to use to decrypt the key file, if it's encrypted - @type password: str - @return: a new key object based on the given private key - @rtype: L{PKey} - - @raise IOError: if there was an error reading the file - @raise PasswordRequiredException: if the private key file is - encrypted, and C{password} is C{None} - @raise SSHException: if the key file is invalid + :return: a new `.PKey` based on the given private key + + :raises IOError: if there was an error reading the file + :raises PasswordRequiredException: if the private key file is + encrypted, and ``password`` is ``None`` + :raises SSHException: if the key file is invalid """ key = cls(filename=filename, password=password) return key @@ -202,22 +186,19 @@ def from_private_key(cls, file_obj, password=None): """ Create a key object by reading a private key from a file (or file-like) - object. If the private key is encrypted and C{password} is not C{None}, + object. If the private key is encrypted and ``password`` is not ``None``, the given password will be used to decrypt the key (otherwise - L{PasswordRequiredException} is thrown). + `.PasswordRequiredException` is thrown). - @param file_obj: the file to read from - @type file_obj: file - @param password: an optional password to use to decrypt the key, if it's - encrypted - @type password: str - @return: a new key object based on the given private key - @rtype: L{PKey} - - @raise IOError: if there was an error reading the key - @raise PasswordRequiredException: if the private key file is encrypted, - and C{password} is C{None} - @raise SSHException: if the key file is invalid + :param file file_obj: the file to read from + :param str password: + an optional password to use to decrypt the key, if it's encrypted + :return: a new `.PKey` based on the given private key + + :raises IOError: if there was an error reading the key + :raises PasswordRequiredException: if the private key file is encrypted, + and ``password`` is ``None`` + :raises SSHException: if the key file is invalid """ key = cls(file_obj=file_obj, password=password) return key @@ -226,59 +207,52 @@ def write_private_key_file(self, filename, password=None): """ Write private key contents into a file. If the password is not - C{None}, the key is encrypted before writing. + ``None``, the key is encrypted before writing. - @param filename: name of the file to write - @type filename: str - @param password: an optional password to use to encrypt the key file - @type password: str + :param str filename: name of the file to write + :param str password: + an optional password to use to encrypt the key file - @raise IOError: if there was an error writing the file - @raise SSHException: if the key is invalid + :raises IOError: if there was an error writing the file + :raises SSHException: if the key is invalid """ raise Exception('Not implemented in PKey') def write_private_key(self, file_obj, password=None): """ Write private key contents into a file (or file-like) object. If the - password is not C{None}, the key is encrypted before writing. + password is not ``None``, the key is encrypted before writing. - @param file_obj: the file object to write into - @type file_obj: file - @param password: an optional password to use to encrypt the key - @type password: str + :param file file_obj: the file object to write into + :param str password: an optional password to use to encrypt the key - @raise IOError: if there was an error writing to the file - @raise SSHException: if the key is invalid + :raises IOError: if there was an error writing to the file + :raises SSHException: if the key is invalid """ raise Exception('Not implemented in PKey') def _read_private_key_file(self, tag, filename, password=None): """ Read an SSH2-format private key file, looking for a string of the type - C{"BEGIN xxx PRIVATE KEY"} for some C{xxx}, base64-decode the text we + ``"BEGIN xxx PRIVATE KEY"`` for some ``xxx``, base64-decode the text we find, and return it as a string. If the private key is encrypted and - C{password} is not C{None}, the given password will be used to decrypt - the key (otherwise L{PasswordRequiredException} is thrown). + ``password`` is not ``None``, the given password will be used to decrypt + the key (otherwise `.PasswordRequiredException` is thrown). - @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. - @type tag: str - @param filename: name of the file to read. - @type filename: str - @param password: an optional password to use to decrypt the key file, - if it's encrypted. - @type password: str - @return: data blob that makes up the private key. - @rtype: str - - @raise IOError: if there was an error reading the file. - @raise PasswordRequiredException: if the private key file is - 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() + :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. + :param str filename: name of the file to read. + :param str password: + an optional password to use to decrypt the key file, if it's + encrypted. + :return: data blob (`str`) that makes up the private key. + + :raises IOError: if there was an error reading the file. + :raises PasswordRequiredException: if the private key file is + encrypted, and ``password`` is ``None``. + :raises SSHException: if the key file is invalid. + """ + 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 +277,8 @@ 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 +289,7 @@ 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,8 +298,8 @@ cipher = self._CIPHER_TABLE[encryption_type]['cipher'] keysize = self._CIPHER_TABLE[encryption_type]['keysize'] mode = self._CIPHER_TABLE[encryption_type]['mode'] - salt = unhexlify(saltstr) - key = util.generate_key_bytes(MD5, salt, password, keysize) + salt = unhexlify(b(saltstr)) + key = util.generate_key_bytes(md5, salt, password, keysize) return cipher.new(key, mode, salt).decrypt(data) def _write_private_key_file(self, tag, filename, data, password=None): @@ -335,47 +309,41 @@ a trivially-encoded format (base64) which is completely insecure. If a password is given, DES-EDE3-CBC is used. - @param tag: C{"RSA"} or C{"DSA"}, the tag used to mark the data block. - @type tag: str - @param filename: name of the file to write. - @type filename: str - @param data: data blob that makes up the private key. - @type data: str - @param password: an optional password to use to encrypt the file. - @type password: str - - @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() + :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. + :param file filename: name of the file to write. + :param str data: data blob that makes up the private key. + :param str password: an optional password to use to encrypt the file. + + :raises IOError: if there was an error writing the file. + """ + 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) - key = util.generate_key_bytes(MD5, salt, password, keysize) + salt = os.urandom(blocksize) + key = util.generate_key_bytes(md5, salt, password, keysize) if len(data) % blocksize != 0: n = blocksize - len(data) % blocksize - #data += rng.read(n) + #data += os.urandom(n) # that would make more sense ^, but it confuses openssh. - data += '\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)]) + s = '\n'.join([s[i: i + 64] for i in range(0, len(s), 64)]) f.write(s) f.write('\n') f.write('-----END %s PRIVATE KEY-----\n' % tag) diff -Nru paramiko-1.10.1/paramiko/primes.py paramiko-1.15.1/paramiko/primes.py --- paramiko-1.10.1/paramiko/primes.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/primes.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -20,33 +20,18 @@ Utility functions for dealing with primes. """ -from Crypto.Util import number +import os from paramiko import util +from paramiko.py3compat import byte_mask, long from paramiko.ssh_exception import SSHException +from paramiko.common import * -def _generate_prime(bits, rng): - "primtive attempt at prime generation" - hbyte_mask = pow(2, bits % 8) - 1 - while True: - # loop catches the case where we increment n into a higher bit-range - x = rng.read((bits+7) // 8) - if hbyte_mask > 0: - x = chr(ord(x[0]) & hbyte_mask) + x[1:] - n = util.inflate_long(x, 1) - n |= 1 - n |= (1 << (bits - 1)) - while not number.isPrime(n): - n += 2 - if util.bit_length(n) == bits: - break - return n - -def _roll_random(rng, n): - "returns a random # from 0 to N-1" - bits = util.bit_length(n-1) - bytes = (bits + 7) // 8 +def _roll_random(n): + """returns a random # from 0 to N-1""" + bits = util.bit_length(n - 1) + byte_count = (bits + 7) // 8 hbyte_mask = pow(2, bits % 8) - 1 # so here's the plan: @@ -56,9 +41,9 @@ # 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 = os.urandom(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 @@ -71,11 +56,10 @@ on systems that have such a file. """ - def __init__(self, rpool): + def __init__(self): # pack is a hash of: bits -> [ (generator, modulus) ... ] self.pack = {} self.discarded = [] - self.rng = rpool def _parse_modulus(self, line): timestamp, mod_type, tests, tries, size, generator, modulus = line.split() @@ -109,29 +93,27 @@ def read_file(self, filename): """ - @raise IOError: passed from any file operations that fail. + :raises 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 # find nearest bitsize >= preferred for b in bitsizes: - if (b >= prefer) and (b < max) and ((b < good) or (good == -1)): + if (b >= prefer) and (b < max) and (b < good or good == -1): good = b # if that failed, find greatest bitsize >= min if good == -1: @@ -147,5 +129,5 @@ if min > good: good = bitsizes[-1] # now pick a random modulus of this bitsize - n = _roll_random(self.rng, len(self.pack[good])) + n = _roll_random(len(self.pack[good])) return self.pack[good][n] diff -Nru paramiko-1.10.1/paramiko/proxy.py paramiko-1.15.1/paramiko/proxy.py --- paramiko-1.10.1/paramiko/proxy.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/proxy.py 2014-09-22 18:31:58.000000000 +0000 @@ -16,76 +16,93 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -""" -L{ProxyCommand}. -""" +from datetime import datetime import os from shlex import split as shlsplit import signal from subprocess import Popen, PIPE +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. This class implements a the socket-like interface needed by the - L{Transport} and L{Packetizer} classes. Using this class instead of a + `.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): """ Create a new CommandProxy instance. The instance created by this - class can be passed as an argument to the L{Transport} class. + class can be passed as an argument to the `.Transport` class. - @param command_line: the command that should be executed and - used as the proxy. - @type command_line: str + :param str command_line: + the command that should be executed and used as the proxy. """ self.cmd = shlsplit(command_line) self.process = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + self.timeout = None + self.buffer = [] def send(self, content): """ Write the content received from the SSH client to the standard input of the forked command. - @param content: string to be sent to the forked command - @type content: str + :param str content: string to be sent to the forked command """ 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): """ Read from the standard output of the forked program. - @param size: how many chars should be read - @type size: int + :param int size: how many chars should be read - @return: the length of the read content - @rtype: int + :return: the length of the read content, as an `int` """ try: - return os.read(self.process.stdout.fileno(), size) - except IOError, e: - raise BadProxyCommand(' '.join(self.cmd), e.strerror) + start = datetime.now() + while len(self.buffer) < size: + if self.timeout is not None: + elapsed = (datetime.now() - start).microseconds + timeout = self.timeout * 1000 * 1000 # to microseconds + if elapsed >= timeout: + raise socket.timeout() + r, w, x = select([self.process.stdout], [], [], 0.0) + if r and r[0] == self.process.stdout: + b = os.read(self.process.stdout.fileno(), 1) + # Store in class-level buffer for persistence across + # timeouts; this makes us act more like a real socket + # (where timeouts don't actually drop data.) + self.buffer.append(b) + result = ''.join(self.buffer) + self.buffer = [] + return result + except socket.timeout: + raise # socket.timeout is a subclass of IOError + except IOError as e: + raise ProxyCommandFailure(' '.join(self.cmd), e.strerror) def close(self): os.kill(self.process.pid, signal.SIGTERM) def settimeout(self, timeout): - # Timeouts are meaningless for this implementation, but are part of the - # spec, so must be present. - pass + self.timeout = timeout diff -Nru paramiko-1.10.1/paramiko/py3compat.py paramiko-1.15.1/paramiko/py3compat.py --- paramiko-1.10.1/paramiko/py3compat.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/py3compat.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,166 @@ +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) + elif isinstance(s, buffer): + return s + 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 + elif isinstance(s, buffer): + return s.decode(encoding) + 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): + # In case we're handed a string instead of an int. + if not isinstance(c, int): + c = ord(c) + 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 -Nru paramiko-1.10.1/paramiko/resource.py paramiko-1.15.1/paramiko/resource.py --- paramiko-1.10.1/paramiko/resource.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/resource.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -28,13 +28,13 @@ A registry of objects and resources that should be closed when those objects are deleted. - This is meant to be a safer alternative to python's C{__del__} method, + This is meant to be a safer alternative to Python's ``__del__`` method, which can cause reference cycles to never be collected. Objects registered with the ResourceManager can be collected but still free resources when they die. - Resources are registered using L{register}, and when an object is garbage - collected, each registered resource is closed by having its C{close()} + Resources are registered using `register`, and when an object is garbage + collected, each registered resource is closed by having its ``close()`` method called. Multiple resources may be registered per object, but a resource will only be closed once, even if multiple objects register it. (The last object to register it wins.) @@ -47,14 +47,13 @@ """ Register a resource to be closed with an object is collected. - When the given C{obj} is garbage-collected by the python interpreter, - the C{resource} will be closed by having its C{close()} method called. + When the given ``obj`` is garbage-collected by the Python interpreter, + the ``resource`` will be closed by having its ``close()`` method called. Any exceptions are ignored. - @param obj: the object to track - @type obj: object - @param resource: the resource to close when the object is collected - @type resource: object + :param object obj: the object to track + :param object resource: + the resource to close when the object is collected """ def callback(ref): try: diff -Nru paramiko-1.10.1/paramiko/rsakey.py paramiko-1.15.1/paramiko/rsakey.py --- paramiko-1.10.1/paramiko/rsakey.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/rsakey.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,20 +17,24 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{RSAKey} +RSA keys. """ +import os +from hashlib import sha1 + from Crypto.PublicKey import RSA -from Crypto.Hash import SHA, MD5 -from Crypto.Cipher import DES3 -from paramiko.common import * from paramiko import util +from paramiko.common import max_byte, zero_byte, one_byte from paramiko.message import Message from paramiko.ber import BER, BERException from paramiko.pkey import PKey +from paramiko.py3compat import long 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 +61,21 @@ 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()) @@ -85,42 +92,42 @@ def can_sign(self): return self.d is not None - def sign_ssh_data(self, rpool, data): - digest = SHA.new(data).digest() + def sign_ssh_data(self, data): + digest = sha1(data).digest() rsa = RSA.construct((long(self.n), long(self.e), long(self.d))) - sig = util.deflate_long(rsa.sign(self._pkcs1imify(digest), '')[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. - hash_obj = util.inflate_long(self._pkcs1imify(SHA.new(data).digest()), True) + hash_obj = util.inflate_long(self._pkcs1imify(sha1(data).digest()), True) rsa = RSA.construct((long(self.n), long(self.e))) return rsa.verify(hash_obj, (sig,)) def _encode_key(self): if (self.p is None) or (self.q is None): raise SSHException('Not enough key info to write private key file') - keylist = [ 0, self.n, self.e, self.d, self.p, self.q, - self.d % (self.p - 1), self.d % (self.q - 1), - util.mod_inverse(self.q, self.p) ] + keylist = [0, self.n, self.e, self.d, self.p, self.q, + self.d % (self.p - 1), self.d % (self.q - 1), + util.mod_inverse(self.q, self.p)] try: b = BER() 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) - + def write_private_key(self, file_obj, password=None): self._write_private_key('RSA', file_obj, self._encode_key(), password) @@ -129,15 +136,13 @@ 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 int bits: number of bits the generated key should be. + :param function progress_func: + an optional function to call at key points in key generation (used + by ``pyCrypto.PublicKey``). + :return: new `.RSAKey` private key """ - rsa = RSA.generate(bits, rng.read, progress_func) + rsa = RSA.generate(bits, os.urandom, progress_func) key = RSAKey(vals=(rsa.e, rsa.n)) key.d = rsa.d key.p = rsa.p @@ -145,28 +150,25 @@ return key generate = staticmethod(generate) - ### internals... - def _pkcs1imify(self, data): """ 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) self._decode_key(data) - + def _from_private_key(self, file_obj, password): data = self._read_private_key('RSA', file_obj, password) self._decode_key(data) - + def _decode_key(self, data): # private key file contains: # RSAPrivateKey = { version = 0, n, e, d, p, q, d mod p-1, d mod q-1, q**-1 mod p } diff -Nru paramiko-1.10.1/paramiko/server.py paramiko-1.15.1/paramiko/server.py --- paramiko-1.10.1/paramiko/server.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/server.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,62 +17,21 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{ServerInterface} is an interface to override for server support. +`.ServerInterface` is an interface to override for server support. """ import threading -from paramiko.common import * from paramiko import util - - -class InteractiveQuery (object): - """ - A query (set of prompts) for a user during interactive authentication. - """ - - def __init__(self, name='', instructions='', *prompts): - """ - Create a new interactive query to send to the client. The name and - instructions are optional, but are generally displayed to the end - user. A list of prompts may be included, or they may be added via - the L{add_prompt} method. - - @param name: name of this query - @type name: str - @param instructions: user instructions (usually short) about this query - @type instructions: str - @param prompts: one or more authentication prompts - @type prompts: str - """ - self.name = name - self.instructions = instructions - self.prompts = [] - for x in prompts: - if (type(x) is str) or (type(x) is unicode): - self.add_prompt(x) - else: - self.add_prompt(x[0], x[1]) - - def add_prompt(self, prompt, echo=True): - """ - Add a prompt to this query. The prompt should be a (reasonably short) - string. Multiple prompts can be added to the same query. - - @param prompt: the user prompt - @type prompt: str - @param echo: C{True} (default) if the user's response should be echoed; - C{False} if not (for a password or similar) - @type echo: bool - """ - self.prompts.append((prompt, echo)) +from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED +from paramiko.py3compat import string_types class ServerInterface (object): """ - This class defines an interface for controlling the behavior of paramiko + This class defines an interface for controlling the behavior of Paramiko in server mode. - Methods on this class are called from paramiko's primary thread, so you + Methods on this class are called from Paramiko's primary thread, so you shouldn't do too much work in them. (Certainly nothing that blocks or sleeps.) """ @@ -80,7 +39,7 @@ def check_channel_request(self, kind, chanid): """ Determine if a channel request of a given type will be granted, and - return C{OPEN_SUCCEEDED} or an error code. This method is + return ``OPEN_SUCCEEDED`` or an error code. This method is called in server mode when the client requests a channel, after authentication is complete. @@ -88,37 +47,37 @@ useless), you should also override some of the channel request methods below, which are used to determine which services will be allowed on a given channel: - - L{check_channel_pty_request} - - L{check_channel_shell_request} - - L{check_channel_subsystem_request} - - L{check_channel_window_change_request} - - L{check_channel_x11_request} - - L{check_channel_forward_agent_request} - - The C{chanid} parameter is a small number that uniquely identifies the - channel within a L{Transport}. A L{Channel} object is not created - unless this method returns C{OPEN_SUCCEEDED} -- once a - L{Channel} object is created, you can call L{Channel.get_id} to + + - `check_channel_pty_request` + - `check_channel_shell_request` + - `check_channel_subsystem_request` + - `check_channel_window_change_request` + - `check_channel_x11_request` + - `check_channel_forward_agent_request` + + The ``chanid`` parameter is a small number that uniquely identifies the + channel within a `.Transport`. A `.Channel` object is not created + unless this method returns ``OPEN_SUCCEEDED`` -- once a + `.Channel` object is created, you can call `.Channel.get_id` to retrieve the channel ID. - The return value should either be C{OPEN_SUCCEEDED} (or - C{0}) to allow the channel request, or one of the following error + The return value should either be ``OPEN_SUCCEEDED`` (or + ``0``) to allow the channel request, or one of the following error codes to reject it: - - C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED} - - C{OPEN_FAILED_CONNECT_FAILED} - - C{OPEN_FAILED_UNKNOWN_CHANNEL_TYPE} - - C{OPEN_FAILED_RESOURCE_SHORTAGE} + + - ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED`` + - ``OPEN_FAILED_CONNECT_FAILED`` + - ``OPEN_FAILED_UNKNOWN_CHANNEL_TYPE`` + - ``OPEN_FAILED_RESOURCE_SHORTAGE`` The default implementation always returns - C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}. + ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED``. - @param kind: the kind of channel the client would like to open - (usually C{"session"}). - @type kind: str - @param chanid: ID of the channel - @type chanid: int - @return: a success or failure code (listed above) - @rtype: int + :param str kind: + the kind of channel the client would like to open (usually + ``"session"``). + :param int chanid: ID of the channel + :return: an `int` success or failure code (listed above) """ return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED @@ -129,15 +88,13 @@ of authentication methods that might be successful. The "list" is actually a string of comma-separated names of types of - authentication. Possible values are C{"password"}, C{"publickey"}, - and C{"none"}. + authentication. Possible values are ``"password"``, ``"publickey"``, + and ``"none"``. - The default implementation always returns C{"password"}. + The default implementation always returns ``"password"``. - @param username: the username requesting authentication. - @type username: str - @return: a comma-separated list of authentication types - @rtype: str + :param str username: the username requesting authentication. + :return: a comma-separated `str` of authentication types """ return 'password' @@ -146,17 +103,17 @@ Determine if a client may open channels with no (further) authentication. - Return L{AUTH_FAILED} if the client must authenticate, or - L{AUTH_SUCCESSFUL} if it's okay for the client to not + Return `.AUTH_FAILED` if the client must authenticate, or + `.AUTH_SUCCESSFUL` if it's okay for the client to not authenticate. - The default implementation always returns L{AUTH_FAILED}. + The default implementation always returns `.AUTH_FAILED`. - @param username: the username of the client. - @type username: str - @return: L{AUTH_FAILED} if the authentication fails; - L{AUTH_SUCCESSFUL} if it succeeds. - @rtype: int + :param str username: the username of the client. + :return: + `.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if + it succeeds. + :rtype: int """ return AUTH_FAILED @@ -165,25 +122,23 @@ Determine if a given username and password supplied by the client is acceptable for use in authentication. - Return L{AUTH_FAILED} if the password is not accepted, - L{AUTH_SUCCESSFUL} if the password is accepted and completes - the authentication, or L{AUTH_PARTIALLY_SUCCESSFUL} if your + Return `.AUTH_FAILED` if the password is not accepted, + `.AUTH_SUCCESSFUL` if the password is accepted and completes + the authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your authentication is stateful, and this key is accepted for authentication, but more authentication is required. (In this latter - case, L{get_allowed_auths} will be called to report to the client what + case, `get_allowed_auths` will be called to report to the client what options it has for continuing the authentication.) - The default implementation always returns L{AUTH_FAILED}. + The default implementation always returns `.AUTH_FAILED`. - @param username: the username of the authenticating client. - @type username: str - @param password: the password given by the client. - @type password: str - @return: L{AUTH_FAILED} if the authentication fails; - L{AUTH_SUCCESSFUL} if it succeeds; - L{AUTH_PARTIALLY_SUCCESSFUL} if the password auth is + :param str username: the username of the authenticating client. + :param str password: the password given by the client. + :return: + `.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if + it succeeds; `.AUTH_PARTIALLY_SUCCESSFUL` if the password auth is successful, but authentication must continue. - @rtype: int + :rtype: int """ return AUTH_FAILED @@ -194,29 +149,28 @@ check the username and key and decide if you would accept a signature made using this key. - Return L{AUTH_FAILED} if the key is not accepted, - L{AUTH_SUCCESSFUL} if the key is accepted and completes the - authentication, or L{AUTH_PARTIALLY_SUCCESSFUL} if your + Return `.AUTH_FAILED` if the key is not accepted, + `.AUTH_SUCCESSFUL` if the key is accepted and completes the + authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your authentication is stateful, and this password is accepted for authentication, but more authentication is required. (In this latter - case, L{get_allowed_auths} will be called to report to the client what + case, `get_allowed_auths` will be called to report to the client what options it has for continuing the authentication.) Note that you don't have to actually verify any key signtature here. - If you're willing to accept the key, paramiko will do the work of + If you're willing to accept the key, Paramiko will do the work of verifying the client's signature. - The default implementation always returns L{AUTH_FAILED}. + The default implementation always returns `.AUTH_FAILED`. - @param username: the username of the authenticating client - @type username: str - @param key: the key object provided by the client - @type key: L{PKey } - @return: L{AUTH_FAILED} if the client can't authenticate - with this key; L{AUTH_SUCCESSFUL} if it can; - L{AUTH_PARTIALLY_SUCCESSFUL} if it can authenticate with - this key but must continue with authentication - @rtype: int + :param str username: the username of the authenticating client + :param .PKey key: the key object provided by the client + :return: + `.AUTH_FAILED` if the client can't authenticate with this key; + `.AUTH_SUCCESSFUL` if it can; `.AUTH_PARTIALLY_SUCCESSFUL` if it + can authenticate with this key but must continue with + authentication + :rtype: int """ return AUTH_FAILED @@ -224,24 +178,24 @@ """ Begin an interactive authentication challenge, if supported. You should override this method in server mode if you want to support the - C{"keyboard-interactive"} auth type, which requires you to send a + ``"keyboard-interactive"`` auth type, which requires you to send a series of questions for the client to answer. - Return L{AUTH_FAILED} if this auth method isn't supported. Otherwise, - you should return an L{InteractiveQuery} object containing the prompts + Return `.AUTH_FAILED` if this auth method isn't supported. Otherwise, + you should return an `.InteractiveQuery` object containing the prompts and instructions for the user. The response will be sent via a call - to L{check_auth_interactive_response}. + to `check_auth_interactive_response`. - The default implementation always returns L{AUTH_FAILED}. + The default implementation always returns `.AUTH_FAILED`. - @param username: the username of the authenticating client - @type username: str - @param submethods: a comma-separated list of methods preferred by the - client (usually empty) - @type submethods: str - @return: L{AUTH_FAILED} if this auth method isn't supported; otherwise - an object containing queries for the user - @rtype: int or L{InteractiveQuery} + :param str username: the username of the authenticating client + :param str submethods: + a comma-separated list of methods preferred by the client (usually + empty) + :return: + `.AUTH_FAILED` if this auth method isn't supported; otherwise an + object containing queries for the user + :rtype: int or `.InteractiveQuery` """ return AUTH_FAILED @@ -249,54 +203,125 @@ """ Continue or finish an interactive authentication challenge, if supported. You should override this method in server mode if you want - to support the C{"keyboard-interactive"} auth type. + to support the ``"keyboard-interactive"`` auth type. - Return L{AUTH_FAILED} if the responses are not accepted, - L{AUTH_SUCCESSFUL} if the responses are accepted and complete - the authentication, or L{AUTH_PARTIALLY_SUCCESSFUL} if your + Return `.AUTH_FAILED` if the responses are not accepted, + `.AUTH_SUCCESSFUL` if the responses are accepted and complete + the authentication, or `.AUTH_PARTIALLY_SUCCESSFUL` if your authentication is stateful, and this set of responses is accepted for authentication, but more authentication is required. (In this latter - case, L{get_allowed_auths} will be called to report to the client what + case, `get_allowed_auths` will be called to report to the client what options it has for continuing the authentication.) If you wish to continue interactive authentication with more questions, - you may return an L{InteractiveQuery} object, which should cause the + you may return an `.InteractiveQuery` object, which should cause the client to respond with more answers, calling this method again. This cycle can continue indefinitely. - The default implementation always returns L{AUTH_FAILED}. + The default implementation always returns `.AUTH_FAILED`. + + :param list responses: list of `str` responses from the client + :return: + `.AUTH_FAILED` if the authentication fails; `.AUTH_SUCCESSFUL` if + it succeeds; `.AUTH_PARTIALLY_SUCCESSFUL` if the interactive auth + is successful, but authentication must continue; otherwise an + object containing queries for the user + :rtype: int or `.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 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 + return AUTH_FAILED - @param responses: list of responses from the client - @type responses: list(str) - @return: L{AUTH_FAILED} if the authentication fails; - L{AUTH_SUCCESSFUL} if it succeeds; - L{AUTH_PARTIALLY_SUCCESSFUL} if the interactive auth is - successful, but authentication must continue; otherwise an object - containing queries for the user - @rtype: int or L{InteractiveQuery} + 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 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 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): """ Handle a request for port forwarding. The client is asking that connections to the given address and port be forwarded back across - this ssh connection. An address of C{"0.0.0.0"} indicates a global - address (any address associated with this server) and a port of C{0} + this ssh connection. An address of ``"0.0.0.0"`` indicates a global + address (any address associated with this server) and a port of ``0`` indicates that no specific port is requested (usually the OS will pick a port). - The default implementation always returns C{False}, rejecting the + The default implementation always returns ``False``, rejecting the port forwarding request. If the request is accepted, you should return the port opened for listening. - @param address: the requested address - @type address: str - @param port: the requested port - @type port: int - @return: the port number that was opened for listening, or C{False} to - reject - @rtype: int + :param str address: the requested address + :param int port: the requested port + :return: + the port number (`int`) that was opened for listening, or ``False`` + to reject """ return False @@ -306,19 +331,17 @@ If the given address and port is being forwarded across this ssh connection, the port should be closed. - @param address: the forwarded address - @type address: str - @param port: the forwarded port - @type port: int + :param str address: the forwarded address + :param int port: the forwarded port """ pass def check_global_request(self, kind, msg): """ - Handle a global request of the given C{kind}. This method is called + Handle a global request of the given ``kind``. This method is called in server mode and client mode, whenever the remote host makes a global request. If there are any arguments to the request, they will be in - C{msg}. + ``msg``. There aren't any useful global requests defined, aside from port forwarding, so usually this type of request is an extension to the @@ -329,115 +352,100 @@ sent back with the successful result. (Note that the items in the tuple can only be strings, ints, longs, or bools.) - The default implementation always returns C{False}, indicating that it + The default implementation always returns ``False``, indicating that it does not support any global requests. - @note: Port forwarding requests are handled separately, in - L{check_port_forward_request}. + .. note:: Port forwarding requests are handled separately, in + `check_port_forward_request`. - @param kind: the kind of global request being made. - @type kind: str - @param msg: any extra arguments to the request. - @type msg: L{Message} - @return: C{True} or a tuple of data if the request was granted; - C{False} otherwise. - @rtype: bool + :param str kind: the kind of global request being made. + :param .Message msg: any extra arguments to the request. + :return: + ``True`` or a `tuple` of data if the request was granted; ``False`` + otherwise. """ return False - ### Channel requests - def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): """ Determine if a pseudo-terminal of the given dimensions (usually requested for shell access) can be provided on the given channel. - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the pty request arrived on. - @type channel: L{Channel} - @param term: type of terminal requested (for example, C{"vt100"}). - @type term: str - @param width: width of screen in characters. - @type width: int - @param height: height of screen in characters. - @type height: int - @param pixelwidth: width of screen in pixels, if known (may be C{0} if - unknown). - @type pixelwidth: int - @param pixelheight: height of screen in pixels, if known (may be C{0} - if unknown). - @type pixelheight: int - @return: C{True} if the psuedo-terminal has been allocated; C{False} + :param .Channel channel: the `.Channel` the pty request arrived on. + :param str term: type of terminal requested (for example, ``"vt100"``). + :param int width: width of screen in characters. + :param int height: height of screen in characters. + :param int pixelwidth: + width of screen in pixels, if known (may be ``0`` if unknown). + :param int pixelheight: + height of screen in pixels, if known (may be ``0`` if unknown). + :return: + ``True`` if the psuedo-terminal has been allocated; ``False`` otherwise. - @rtype: bool """ return False def check_channel_shell_request(self, channel): """ Determine if a shell will be provided to the client on the given - channel. If this method returns C{True}, the channel should be + channel. If this method returns ``True``, the channel should be connected to the stdin/stdout of a shell (or something that acts like a shell). - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the request arrived on. - @type channel: L{Channel} - @return: C{True} if this channel is now hooked up to a shell; C{False} - if a shell can't or won't be provided. - @rtype: bool + :param .Channel channel: the `.Channel` the request arrived on. + :return: + ``True`` if this channel is now hooked up to a shell; ``False`` if + a shell can't or won't be provided. """ return False def check_channel_exec_request(self, channel, command): """ Determine if a shell command will be executed for the client. If this - method returns C{True}, the channel should be connected to the stdin, + method returns ``True``, the channel should be connected to the stdin, stdout, and stderr of the shell command. - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the request arrived on. - @type channel: L{Channel} - @param command: the command to execute. - @type command: str - @return: C{True} if this channel is now hooked up to the stdin, - stdout, and stderr of the executing command; C{False} if the - command will not be executed. - @rtype: bool + :param .Channel channel: the `.Channel` the request arrived on. + :param str command: the command to execute. + :return: + ``True`` if this channel is now hooked up to the stdin, stdout, and + stderr of the executing command; ``False`` if the command will not + be executed. - @since: 1.1 + .. versionadded:: 1.1 """ return False def check_channel_subsystem_request(self, channel, name): """ Determine if a requested subsystem will be provided to the client on - the given channel. If this method returns C{True}, all future I/O + the given channel. If this method returns ``True``, all future I/O through this channel will be assumed to be connected to the requested - subsystem. An example of a subsystem is C{sftp}. + subsystem. An example of a subsystem is ``sftp``. The default implementation checks for a subsystem handler assigned via - L{Transport.set_subsystem_handler}. + `.Transport.set_subsystem_handler`. If one has been set, the handler is invoked and this method returns - C{True}. Otherwise it returns C{False}. + ``True``. Otherwise it returns ``False``. - @note: Because the default implementation uses the L{Transport} to + .. note:: Because the default implementation uses the `.Transport` to identify valid subsystems, you probably won't need to override this method. - @param channel: the L{Channel} the pty request arrived on. - @type channel: L{Channel} - @param name: name of the requested subsystem. - @type name: str - @return: C{True} if this channel is now hooked up to the requested - subsystem; C{False} if that subsystem can't or won't be provided. - @rtype: bool + :param .Channel channel: the `.Channel` the pty request arrived on. + :param str name: name of the requested subsystem. + :return: + ``True`` if this channel is now hooked up to the requested + subsystem; ``False`` if that subsystem can't or won't be provided. """ handler_class, larg, kwarg = channel.get_transport()._get_subsystem_handler(name) if handler_class is None: @@ -451,137 +459,175 @@ Determine if the pseudo-terminal on the given channel can be resized. This only makes sense if a pty was previously allocated on it. - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the pty request arrived on. - @type channel: L{Channel} - @param width: width of screen in characters. - @type width: int - @param height: height of screen in characters. - @type height: int - @param pixelwidth: width of screen in pixels, if known (may be C{0} if - unknown). - @type pixelwidth: int - @param pixelheight: height of screen in pixels, if known (may be C{0} - if unknown). - @type pixelheight: int - @return: C{True} if the terminal was resized; C{False} if not. - @rtype: bool + :param .Channel channel: the `.Channel` the pty request arrived on. + :param int width: width of screen in characters. + :param int height: height of screen in characters. + :param int pixelwidth: + width of screen in pixels, if known (may be ``0`` if unknown). + :param int pixelheight: + height of screen in pixels, if known (may be ``0`` if unknown). + :return: ``True`` if the terminal was resized; ``False`` if not. """ return False def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number): """ Determine if the client will be provided with an X11 session. If this - method returns C{True}, X11 applications should be routed through new - SSH channels, using L{Transport.open_x11_channel}. + method returns ``True``, X11 applications should be routed through new + SSH channels, using `.Transport.open_x11_channel`. - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the X11 request arrived on - @type channel: L{Channel} - @param single_connection: C{True} if only a single X11 channel should - be opened - @type single_connection: bool - @param auth_protocol: the protocol used for X11 authentication - @type auth_protocol: str - @param auth_cookie: the cookie used to authenticate to X11 - @type auth_cookie: str - @param screen_number: the number of the X11 screen to connect to - @type screen_number: int - @return: C{True} if the X11 session was opened; C{False} if not - @rtype: bool + :param .Channel channel: the `.Channel` the X11 request arrived on + :param bool single_connection: + ``True`` if only a single X11 channel should be opened, else + ``False``. + :param str auth_protocol: the protocol used for X11 authentication + :param str auth_cookie: the cookie used to authenticate to X11 + :param int screen_number: the number of the X11 screen to connect to + :return: ``True`` if the X11 session was opened; ``False`` if not """ return False def check_channel_forward_agent_request(self, channel): """ Determine if the client will be provided with an forward agent session. - If this method returns C{True}, the server will allow SSH Agent + If this method returns ``True``, the server will allow SSH Agent forwarding. - The default implementation always returns C{False}. + The default implementation always returns ``False``. - @param channel: the L{Channel} the request arrived on - @type channel: L{Channel} - @return: C{True} if the AgentForward was loaded; C{False} if not - @rtype: bool + :param .Channel channel: the `.Channel` the request arrived on + :return: ``True`` if the AgentForward was loaded; ``False`` if not """ return False def check_channel_direct_tcpip_request(self, chanid, origin, destination): """ Determine if a local port forwarding channel will be granted, and - return C{OPEN_SUCCEEDED} or an error code. This method is + return ``OPEN_SUCCEEDED`` or an error code. This method is called in server mode when the client requests a channel, after authentication is complete. - The C{chanid} parameter is a small number that uniquely identifies the - channel within a L{Transport}. A L{Channel} object is not created - unless this method returns C{OPEN_SUCCEEDED} -- once a - L{Channel} object is created, you can call L{Channel.get_id} to + The ``chanid`` parameter is a small number that uniquely identifies the + channel within a `.Transport`. A `.Channel` object is not created + unless this method returns ``OPEN_SUCCEEDED`` -- once a + `.Channel` object is created, you can call `.Channel.get_id` to retrieve the channel ID. The origin and destination parameters are (ip_address, port) tuples that correspond to both ends of the TCP connection in the forwarding tunnel. - The return value should either be C{OPEN_SUCCEEDED} (or - C{0}) to allow the channel request, or one of the following error + The return value should either be ``OPEN_SUCCEEDED`` (or + ``0``) to allow the channel request, or one of the following error codes to reject it: - - C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED} - - C{OPEN_FAILED_CONNECT_FAILED} - - C{OPEN_FAILED_UNKNOWN_CHANNEL_TYPE} - - C{OPEN_FAILED_RESOURCE_SHORTAGE} + + - ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED`` + - ``OPEN_FAILED_CONNECT_FAILED`` + - ``OPEN_FAILED_UNKNOWN_CHANNEL_TYPE`` + - ``OPEN_FAILED_RESOURCE_SHORTAGE`` The default implementation always returns - C{OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED}. + ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED``. - @param chanid: ID of the channel - @type chanid: int - @param origin: 2-tuple containing the IP address and port of the - originator (client side) - @type origin: tuple - @param destination: 2-tuple containing the IP address and port of the - destination (server side) - @type destination: tuple - @return: a success or failure code (listed above) - @rtype: int + :param int chanid: ID of the channel + :param tuple origin: + 2-tuple containing the IP address and port of the originator + (client side) + :param tuple destination: + 2-tuple containing the IP address and port of the destination + (server side) + :return: an `int` success or failure code (listed above) """ return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + 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 ``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 ``False``. + + :param channel: the `.Channel` the env request arrived on + :param str name: name + :param str value: Channel value + :returns: A boolean + """ + return False + + +class InteractiveQuery (object): + """ + A query (set of prompts) for a user during interactive authentication. + """ + + def __init__(self, name='', instructions='', *prompts): + """ + Create a new interactive query to send to the client. The name and + instructions are optional, but are generally displayed to the end + user. A list of prompts may be included, or they may be added via + the `add_prompt` method. + + :param str name: name of this query + :param str instructions: + user instructions (usually short) about this query + :param str prompts: one or more authentication prompts + """ + self.name = name + self.instructions = instructions + self.prompts = [] + for x in prompts: + if isinstance(x, string_types): + self.add_prompt(x) + else: + self.add_prompt(x[0], x[1]) + + def add_prompt(self, prompt, echo=True): + """ + Add a prompt to this query. The prompt should be a (reasonably short) + string. Multiple prompts can be added to the same query. + + :param str prompt: the user prompt + :param bool echo: + ``True`` (default) if the user's response should be echoed; + ``False`` if not (for a password or similar) + """ + self.prompts.append((prompt, echo)) + class SubsystemHandler (threading.Thread): """ Handler for a subsytem in server mode. If you create a subclass of this - class and pass it to - L{Transport.set_subsystem_handler}, - an object of this + class and pass it to `.Transport.set_subsystem_handler`, an object of this class will be created for each request for this subsystem. Each new object - will be executed within its own new thread by calling L{start_subsystem}. + will be executed within its own new thread by calling `start_subsystem`. When that method completes, the channel is closed. - For example, if you made a subclass C{MP3Handler} and registered it as the - handler for subsystem C{"mp3"}, then whenever a client has successfully - authenticated and requests subsytem C{"mp3"}, an object of class - C{MP3Handler} will be created, and L{start_subsystem} will be called on + For example, if you made a subclass ``MP3Handler`` and registered it as the + handler for subsystem ``"mp3"``, then whenever a client has successfully + authenticated and requests subsytem ``"mp3"``, an object of class + ``MP3Handler`` will be created, and `start_subsystem` will be called on it from a new thread. """ def __init__(self, channel, name, server): """ - Create a new handler for a channel. This is used by L{ServerInterface} + Create a new handler for a channel. This is used by `.ServerInterface` to start up a new handler when a channel requests this subsystem. You don't need to override this method, but if you do, be sure to pass the - C{channel} and C{name} parameters through to the original C{__init__} + ``channel`` and ``name`` parameters through to the original ``__init__`` method here. - @param channel: the channel associated with this subsystem request. - @type channel: L{Channel} - @param name: name of the requested subsystem. - @type name: str - @param server: the server object for the session that started this - subsystem - @type server: L{ServerInterface} + :param .Channel channel: the channel associated with this subsystem request. + :param str name: name of the requested subsystem. + :param .ServerInterface server: + the server object for the session that started this subsystem """ threading.Thread.__init__(self, target=self._run) self.__channel = channel @@ -591,10 +637,8 @@ def get_server(self): """ - Return the L{ServerInterface} object associated with this channel and + Return the `.ServerInterface` object associated with this channel and subsystem. - - @rtype: L{ServerInterface} """ return self.__server @@ -602,7 +646,7 @@ 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()) @@ -619,22 +663,20 @@ subsystem is finished, this method will return. After this method returns, the channel is closed. - The combination of C{transport} and C{channel} are unique; this handler - corresponds to exactly one L{Channel} on one L{Transport}. + The combination of ``transport`` and ``channel`` are unique; this handler + corresponds to exactly one `.Channel` on one `.Transport`. - @note: It is the responsibility of this method to exit if the - underlying L{Transport} is closed. This can be done by checking - L{Transport.is_active} or noticing an EOF - on the L{Channel}. If this method loops forever without checking - for this case, your python interpreter may refuse to exit because - this thread will still be running. - - @param name: name of the requested subsystem. - @type name: str - @param transport: the server-mode L{Transport}. - @type transport: L{Transport} - @param channel: the channel associated with this subsystem request. - @type channel: L{Channel} + .. note:: + It is the responsibility of this method to exit if the underlying + `.Transport` is closed. This can be done by checking + `.Transport.is_active` or noticing an EOF on the `.Channel`. If + this method loops forever without checking for this case, your + Python interpreter may refuse to exit because this thread will + still be running. + + :param str name: name of the requested subsystem. + :param .Transport transport: the server-mode `.Transport`. + :param .Channel channel: the channel associated with this subsystem request. """ pass @@ -643,6 +685,6 @@ Perform any cleanup at the end of a subsystem. The default implementation just closes the channel. - @since: 1.1 + .. versionadded:: 1.1 """ self.__channel.close() diff -Nru paramiko-1.10.1/paramiko/sftp_attr.py paramiko-1.15.1/paramiko/sftp_attr.py --- paramiko-1.10.1/paramiko/sftp_attr.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp_attr.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -18,33 +18,34 @@ import stat import time -from paramiko.common import * -from paramiko.sftp import * +from paramiko.common import x80000000, o700, o70, xffffffff +from paramiko.py3compat import long, b class SFTPAttributes (object): """ Representation of the attributes of a file (or proxied file) for SFTP in client or server mode. It attemps to mirror the object returned by - C{os.stat} as closely as possible, so it may have the following fields, - with the same meanings as those returned by an C{os.stat} object: - - st_size - - st_uid - - st_gid - - st_mode - - st_atime - - st_mtime + `os.stat` as closely as possible, so it may have the following fields, + with the same meanings as those returned by an `os.stat` object: + + - ``st_size`` + - ``st_uid`` + - ``st_gid`` + - ``st_mode`` + - ``st_atime`` + - ``st_mtime`` Because SFTP allows flags to have other arbitrary named attributes, these - are stored in a dict named C{attr}. Occasionally, the filename is also - stored, in C{filename}. + are stored in a dict named ``attr``. Occasionally, the filename is also + stored, in ``filename``. """ FLAG_SIZE = 1 FLAG_UIDGID = 2 FLAG_PERMISSIONS = 4 FLAG_AMTIME = 8 - FLAG_EXTENDED = 0x80000000L + FLAG_EXTENDED = x80000000 def __init__(self): """ @@ -61,15 +62,12 @@ def from_stat(cls, obj, filename=None): """ - Create an SFTPAttributes object from an existing C{stat} object (an - object returned by C{os.stat}). + Create an `.SFTPAttributes` object from an existing ``stat`` object (an + object returned by `os.stat`). - @param obj: an object returned by C{os.stat} (or equivalent). - @type obj: object - @param filename: the filename associated with this file. - @type filename: str - @return: new L{SFTPAttributes} object with the same attribute fields. - @rtype: L{SFTPAttributes} + :param object obj: an object returned by `os.stat` (or equivalent). + :param str filename: the filename associated with this file. + :return: new `.SFTPAttributes` object with the same attribute fields. """ attr = cls() attr.st_size = obj.st_size @@ -86,10 +84,8 @@ def __repr__(self): return '' % self._debug_str() - ### internals... - def _from_msg(cls, msg, filename=None, longname=None): attr = cls() attr._unpack(msg) @@ -143,7 +139,7 @@ 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 +154,7 @@ 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 @@ -175,7 +171,7 @@ _rwx = staticmethod(_rwx) def __str__(self): - "create a unix-style long description of the file (like ls -l)" + """create a unix-style long description of the file (like ls -l)""" if self.st_mode is not None: kind = stat.S_IFMT(self.st_mode) if kind == stat.S_IFIFO: @@ -194,13 +190,13 @@ 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 +217,5 @@ return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename) + def asbytes(self): + return b(str(self)) diff -Nru paramiko-1.10.1/paramiko/sftp_client.py paramiko-1.15.1/paramiko/sftp_client.py --- paramiko-1.10.1/paramiko/sftp_client.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp_client.py 2014-09-22 18:31:58.000000000 +0000 @@ -1,13 +1,13 @@ # Copyright (C) 2003-2007 Robey Pointer # -# This file is part of paramiko. +# 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 +# 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. @@ -16,9 +16,6 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -""" -Client-mode SFTP support. -""" from binascii import hexlify import errno @@ -27,11 +24,22 @@ import threading import time import weakref +from paramiko import util +from paramiko.channel import Channel +from paramiko.message import Message +from paramiko.common import INFO, DEBUG, o777 +from paramiko.py3compat import bytestring, b, u, long, string_types, bytes_types +from paramiko.sftp import BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, \ + CMD_NAME, CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE, \ + SFTP_FLAG_TRUNC, SFTP_FLAG_APPEND, SFTP_FLAG_EXCL, CMD_OPEN, CMD_REMOVE, \ + CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_STAT, CMD_ATTRS, CMD_LSTAT, \ + CMD_SYMLINK, CMD_SETSTAT, CMD_READLINK, CMD_REALPATH, CMD_STATUS, SFTP_OK, \ + SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED -from paramiko.sftp import * 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): @@ -42,31 +50,35 @@ """ 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): - """ - SFTP client object. C{SFTPClient} is used to open an sftp session across - an open ssh L{Transport} and do remote file operations. + +class SFTPClient(BaseSFTP, ClosingContextManager): """ + SFTP client object. + 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): """ - Create an SFTP client from an existing L{Channel}. The channel - should already have requested the C{"sftp"} subsystem. + Create an SFTP client from an existing `.Channel`. The channel + should already have requested the ``"sftp"`` subsystem. An alternate way to create an SFTP client context is by using - L{from_transport}. + `from_transport`. - @param sock: an open L{Channel} using the C{"sftp"} subsystem - @type sock: L{Channel} + :param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem - @raise SSHException: if there's an exception while negotiating + :raises SSHException: if there's an exception while negotiating sftp """ BaseSFTP.__init__(self) @@ -85,21 +97,34 @@ 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) - 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 L{Transport}. + 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 - @param t: an open L{Transport} which is already authenticated - @type t: L{Transport} - @return: a new L{SFTPClient} object, referring to an sftp session - (channel) across the transport - @rtype: L{SFTPClient} + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. """ - 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') @@ -109,124 +134,181 @@ def _log(self, level, msg, *args): if isinstance(msg, list): for m in msg: - super(SFTPClient, self)._log(level, "[chan %s] " + m, *([ self.sock.get_name() ] + list(args))) + self._log(level, m, *args) else: - super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args))) + # escape '%' in msg (they could come from file or directory names) before logging + msg = msg.replace('%','%%') + super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([self.sock.get_name()] + list(args))) def close(self): """ Close the SFTP session and its underlying channel. - @since: 1.4 + .. versionadded:: 1.4 """ self._log(INFO, 'sftp session closed.') self.sock.close() def get_channel(self): """ - Return the underlying L{Channel} object for this SFTP session. This + Return the underlying `.Channel` object for this SFTP session. This might be useful for doing things like setting a timeout on the channel. - @return: the SSH channel - @rtype: L{Channel} - - @since: 1.7.1 + .. versionadded:: 1.7.1 """ return self.sock def listdir(self, path='.'): """ - Return a list containing the names of the entries in the given C{path}. + Return a list containing the names of the entries in the given ``path``. + The list is in arbitrary order. It does not include the special - entries C{'.'} and C{'..'} even if they are present in the folder. - This method is meant to mirror C{os.listdir} as closely as possible. - For a list of full L{SFTPAttributes} objects, see L{listdir_attr}. - - @param path: path to list (defaults to C{'.'}) - @type path: str - @return: list of filenames - @rtype: list of str + entries ``'.'`` and ``'..'`` even if they are present in the folder. + This method is meant to mirror ``os.listdir`` as closely as possible. + For a list of full `.SFTPAttributes` objects, see `listdir_attr`. + + :param str path: path to list (defaults to ``'.'``) """ return [f.filename for f in self.listdir_attr(path)] def listdir_attr(self, path='.'): """ - Return a list containing L{SFTPAttributes} objects corresponding to - files in the given C{path}. The list is in arbitrary order. It does - not include the special entries C{'.'} and C{'..'} even if they are + Return a list containing `.SFTPAttributes` objects corresponding to + files in the given ``path``. The list is in arbitrary order. It does + not include the special entries ``'.'`` and ``'..'`` 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 + The returned `.SFTPAttributes` objects will each have an additional + field: ``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: list of attributes - @rtype: list of L{SFTPAttributes} + :param str path: path to list (defaults to ``'.'``) + :return: list of `.SFTPAttributes` objects - @since: 1.2 + .. versionadded:: 1.2 """ 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() + 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) self._request(CMD_CLOSE, handle) return filelist + 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_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.) + + .. versionadded:: 1.15 + """ + 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: + # 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_aheads): + 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: + 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_text() + longname = msg.get_text() + attr = SFTPAttributes._from_msg( + msg, filename, longname) + if (filename != '.') and (filename != '..'): + yield attr + + # If we've hit the end of our queued requests, reset nums. + nums = list() + + except EOFError: + 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 - python's built-in C{file} (aka C{open}). A file-like object is - returned, which closely mimics the behavior of a normal python file - object, including the ability to be used as a context manager. - - The mode indicates how the file is to be opened: C{'r'} for reading, - C{'w'} for writing (truncating an existing file), C{'a'} for appending, - C{'r+'} for reading/writing, C{'w+'} for reading/writing (truncating an - existing file), C{'a+'} for reading/appending. The python C{'b'} flag - is ignored, since SSH treats all files as binary. The C{'U'} flag is - supported in a compatible way. + Python's built-in `python:file` (aka `python:open`). A file-like + object is returned, which closely mimics the behavior of a normal + Python file object, including the ability to be used as a context + manager. + + The mode indicates how the file is to be opened: ``'r'`` for reading, + ``'w'`` for writing (truncating an existing file), ``'a'`` for + appending, ``'r+'`` for reading/writing, ``'w+'`` for reading/writing + (truncating an existing file), ``'a+'`` for reading/appending. The + Python ``'b'`` flag is ignored, since SSH treats all files as binary. + The ``'U'`` flag is supported in a compatible way. - Since 1.5.2, an C{'x'} flag indicates that the operation should only + Since 1.5.2, an ``'x'`` flag indicates that the operation should only succeed if the file was created and did not previously exist. This has - no direct mapping to python's file flags, but is commonly known as the - C{O_EXCL} flag in posix. + no direct mapping to Python's file flags, but is commonly known as the + ``O_EXCL`` flag in posix. - The file will be buffered in standard python style by default, but - can be altered with the C{bufsize} parameter. C{0} turns off - buffering, C{1} uses line buffering, and any number greater than 1 - (C{>1}) uses that specific buffer size. - - @param filename: name of the file to open - @type filename: str - @param mode: mode (python-style) to open in - @type mode: str - @param bufsize: desired buffering (-1 = default buffer size) - @type bufsize: int - @return: a file object representing the open file - @rtype: SFTPFile + The file will be buffered in standard Python style by default, but + can be altered with the ``bufsize`` parameter. ``0`` turns off + buffering, ``1`` uses line buffering, and any number greater than 1 + (``>1``) uses that specific buffer size. + + :param str filename: name of the file to open + :param str mode: mode (Python-style) to open in + :param int bufsize: desired buffering (-1 = default buffer size) + :return: an `.SFTPFile` object representing the open file - @raise IOError: if the file could not be opened. + :raises IOError: if the file could not be opened. """ filename = self._adjust_cwd(filename) self._log(DEBUG, 'open(%r, %r)' % (filename, mode)) @@ -235,32 +317,31 @@ imode |= SFTP_FLAG_READ if ('w' in mode) or ('+' in mode) or ('a' in mode): imode |= SFTP_FLAG_WRITE - if ('w' in mode): + if 'w' in mode: imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC - if ('a' in mode): + if 'a' in mode: imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND - if ('x' in mode): + if 'x' in mode: imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL attrblock = SFTPAttributes() 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) - # python continues to vacillate about "open" vs "file"... + # Python continues to vacillate about "open" vs "file"... file = open def remove(self, path): """ Remove the file at the given path. This only works on files; for - removing folders (directories), use L{rmdir}. + removing folders (directories), use `rmdir`. - @param path: path (absolute or relative) of the file to remove - @type path: str + :param str path: path (absolute or relative) of the file to remove - @raise IOError: if the path refers to a folder (directory) + :raises IOError: if the path refers to a folder (directory) """ path = self._adjust_cwd(path) self._log(DEBUG, 'remove(%r)' % path) @@ -270,14 +351,12 @@ def rename(self, oldpath, newpath): """ - Rename a file or folder from C{oldpath} to C{newpath}. + Rename a file or folder from ``oldpath`` to ``newpath``. - @param oldpath: existing name of the file or folder - @type oldpath: str - @param newpath: new name for the file or folder - @type newpath: str + :param str oldpath: existing name of the file or folder + :param str newpath: new name for the file or folder - @raise IOError: if C{newpath} is a folder, or something else goes + :raises IOError: if ``newpath`` is a folder, or something else goes wrong """ oldpath = self._adjust_cwd(oldpath) @@ -285,16 +364,14 @@ 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}. + Create a folder (directory) named ``path`` with numeric mode ``mode``. The default mode is 0777 (octal). On some systems, mode is ignored. Where it is used, the current umask value is first masked out. - @param path: name of the folder to create - @type path: str - @param mode: permissions (posix-style) for the newly-created folder - @type mode: int + :param str path: name of the folder to create + :param int mode: permissions (posix-style) for the newly-created folder """ path = self._adjust_cwd(path) self._log(DEBUG, 'mkdir(%r, %r)' % (path, mode)) @@ -304,10 +381,9 @@ def rmdir(self, path): """ - Remove the folder named C{path}. + Remove the folder named ``path``. - @param path: name of the folder to remove - @type path: str + :param str path: name of the folder to remove """ path = self._adjust_cwd(path) self._log(DEBUG, 'rmdir(%r)' % path) @@ -317,20 +393,20 @@ """ Retrieve information about a file on the remote system. The return value is an object whose attributes correspond to the attributes of - python's C{stat} structure as returned by C{os.stat}, except that it + Python's ``stat`` structure as returned by ``os.stat``, except that it contains fewer fields. An SFTP server may return as much or as little info as it wants, so the results may vary from server to server. - Unlike a python C{stat} object, the result may not be accessed as a - tuple. This is mostly due to the author's slack factor. + Unlike a Python `python:stat` object, the result may not be accessed as + a tuple. This is mostly due to the author's slack factor. - The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid}, - C{st_atime}, and C{st_mtime}. + The fields supported are: ``st_mode``, ``st_size``, ``st_uid``, + ``st_gid``, ``st_atime``, and ``st_mtime``. - @param path: the filename to stat - @type path: str - @return: an object containing attributes about the given file - @rtype: SFTPAttributes + :param str path: the filename to stat + :return: + an `.SFTPAttributes` object containing attributes about the given + file """ path = self._adjust_cwd(path) self._log(DEBUG, 'stat(%r)' % path) @@ -343,12 +419,12 @@ """ Retrieve information about a file on the remote system, without following symbolic links (shortcuts). This otherwise behaves exactly - the same as L{stat}. + the same as `stat`. - @param path: the filename to stat - @type path: str - @return: an object containing attributes about the given file - @rtype: SFTPAttributes + :param str path: the filename to stat + :return: + an `.SFTPAttributes` object containing attributes about the given + file """ path = self._adjust_cwd(path) self._log(DEBUG, 'lstat(%r)' % path) @@ -359,30 +435,25 @@ def symlink(self, source, dest): """ - Create a symbolic link (shortcut) of the C{source} path at - C{destination}. + Create a symbolic link (shortcut) of the ``source`` path at + ``destination``. - @param source: path of the original file - @type source: str - @param dest: path of the newly created symlink - @type dest: str + :param str source: path of the original file + :param str dest: path of the newly created symlink """ 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): """ Change the mode (permissions) of a file. The permissions are - unix-style and identical to those used by python's C{os.chmod} + unix-style and identical to those used by Python's `os.chmod` function. - @param path: path of the file to change the permissions of - @type path: str - @param mode: new permissions - @type mode: int + :param str path: path of the file to change the permissions of + :param int mode: new permissions """ path = self._adjust_cwd(path) self._log(DEBUG, 'chmod(%r, %r)' % (path, mode)) @@ -392,17 +463,14 @@ def chown(self, path, uid, gid): """ - Change the owner (C{uid}) and group (C{gid}) of a file. As with - python's C{os.chown} function, you must pass both arguments, so if you - only want to change one, use L{stat} first to retrieve the current + Change the owner (``uid``) and group (``gid``) of a file. As with + Python's `os.chown` function, you must pass both arguments, so if you + only want to change one, use `stat` first to retrieve the current owner and group. - @param path: path of the file to change the owner and group of - @type path: str - @param uid: new owner's uid - @type uid: int - @param gid: new group id - @type gid: int + :param str path: path of the file to change the owner and group of + :param int uid: new owner's uid + :param int gid: new group id """ path = self._adjust_cwd(path) self._log(DEBUG, 'chown(%r, %r, %r)' % (path, uid, gid)) @@ -412,18 +480,17 @@ def utime(self, path, times): """ - Set the access and modified times of the file specified by C{path}. If - C{times} is C{None}, then the file's access and modified times are set - to the current time. Otherwise, C{times} must be a 2-tuple of numbers, - of the form C{(atime, mtime)}, which is used to set the access and - modified times, respectively. This bizarre API is mimicked from python + Set the access and modified times of the file specified by ``path``. If + ``times`` is ``None``, then the file's access and modified times are set + to the current time. Otherwise, ``times`` must be a 2-tuple of numbers, + of the form ``(atime, mtime)``, which is used to set the access and + modified times, respectively. This bizarre API is mimicked from Python for the sake of consistency -- I apologize. - @param path: path of the file to modify - @type path: str - @param times: C{None} or a tuple of (access time, modified time) in - standard internet epoch time (seconds since 01 January 1970 GMT) - @type times: tuple(int) + :param str path: path of the file to modify + :param tuple times: + ``None`` or a tuple of (access time, modified time) in standard + internet epoch time (seconds since 01 January 1970 GMT) """ path = self._adjust_cwd(path) if times is None: @@ -435,14 +502,13 @@ def truncate(self, path, size): """ - Change the size of the file specified by C{path}. This usually extends - or shrinks the size of the file, just like the C{truncate()} method on - python file objects. - - @param path: path of the file to modify - @type path: str - @param size: the new size of the file - @type size: int or long + Change the size of the file specified by ``path``. This usually + extends or shrinks the size of the file, just like the `~file.truncate` + method on Python file objects. + + :param str path: path of the file to modify + :param size: the new size of the file + :type size: int or long """ path = self._adjust_cwd(path) self._log(DEBUG, 'truncate(%r, %r)' % (path, size)) @@ -453,13 +519,11 @@ def readlink(self, path): """ Return the target of a symbolic link (shortcut). You can use - L{symlink} to create these. The result may be either an absolute or + `symlink` to create these. The result may be either an absolute or relative pathname. - @param path: path of the symbolic link file - @type path: str - @return: target path - @rtype: str + :param str path: path of the symbolic link file + :return: target path, as a `str` """ path = self._adjust_cwd(path) self._log(DEBUG, 'readlink(%r)' % path) @@ -477,15 +541,13 @@ """ Return the normalized path (on the server) of a given path. This can be used to quickly resolve symbolic links or determine what the - server is considering to be the "current folder" (by passing C{'.'} - as C{path}). + server is considering to be the "current folder" (by passing ``'.'`` + as ``path``). - @param path: path to be normalized - @type path: str - @return: normalized form of the given path - @rtype: str + :param str path: path to be normalized + :return: normalized form of the given path (as a `str`) - @raise IOError: if the path can't be resolved on the server + :raises IOError: if the path can't be resolved on the server """ path = self._adjust_cwd(path) self._log(DEBUG, 'normalize(%r)' % path) @@ -495,76 +557,70 @@ 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 - is emulated by paramiko. Once you use this method to set a working - directory, all operations on this SFTPClient object will be relative - to that path. You can pass in C{None} to stop using a current working + doesn't really have the concept of a current working directory, this is + emulated by Paramiko. Once you use this method to set a working + directory, all operations on this `.SFTPClient` object will be relative + to that path. You can pass in ``None`` to stop using a current working directory. - @param path: new current working directory - @type path: str + :param str path: new current working directory - @raise IOError: if the requested path doesn't exist on the server + :raises IOError: if the requested path doesn't exist on the server - @since: 1.4 + .. versionadded:: 1.4 """ if path is None: self._cwd = None return if not stat.S_ISDIR(self.stat(path).st_mode): raise SFTPError(errno.ENOTDIR, "%s: %s" % (os.strerror(errno.ENOTDIR), path)) - self._cwd = self.normalize(path).encode('utf-8') + self._cwd = b(self.normalize(path)) def getcwd(self): """ Return the "current working directory" for this SFTP session, as - emulated by paramiko. If no directory has been set with L{chdir}, - this method will return C{None}. + emulated by Paramiko. If no directory has been set with `chdir`, + this method will return ``None``. - @return: the current working directory on the server, or C{None} - @rtype: str - - @since: 1.4 + .. versionadded:: 1.4 """ - return self._cwd + return self._cwd and u(self._cwd) def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): """ - Copy the contents of an open file object (C{fl}) to the SFTP server as - C{remotepath}. Any exception raised by operations will be passed through. + Copy the contents of an open file object (``fl``) to the SFTP server as + ``remotepath``. Any exception raised by operations will be passed + through. The SFTP operations use pipelining for speed. - @param fl: opened file or file-like object to copy - @type localpath: object - @param remotepath: the destination path on the SFTP server - @type remotepath: str - @param file_size: optional size parameter passed to callback. If none is - specified, size defaults to 0 - @type file_size: int - @param callback: optional callback function that accepts the bytes - transferred so far and the total bytes to be transferred - (since 1.7.4) - @type callback: function(int, int) - @param confirm: whether to do a stat() on the file afterwards to - confirm the file size (since 1.7.7) - @type confirm: bool - - @return: an object containing attributes about the given file + :param file fl: opened file or file-like object to copy + :param str remotepath: the destination path on the SFTP server + :param int file_size: + optional size parameter passed to callback. If none is specified, + size defaults to 0 + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + the bytes transferred so far and the total bytes to be transferred (since 1.7.4) - @rtype: SFTPAttributes + :param bool confirm: + whether to do a stat() on the file afterwards to confirm the file + size (since 1.7.7) + + :return: + an `.SFTPAttributes` object containing attributes about the given + file. - @since: 1.4 + .. versionadded:: 1.10 """ - 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 +629,6 @@ callback(size, file_size) if len(data) == 0: break - finally: - fr.close() if confirm: s = self.stat(remotepath) if s.st_size != size: @@ -585,61 +639,55 @@ def put(self, localpath, remotepath, callback=None, confirm=True): """ - Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. + Copy a local file (``localpath``) to the SFTP server as ``remotepath``. Any exception raised by operations will be passed through. This method is primarily provided as a convenience. The SFTP operations use pipelining for speed. - @param localpath: the local file to copy - @type localpath: str - @param remotepath: the destination path on the SFTP server - @type remotepath: str - @param callback: optional callback function that accepts the bytes - transferred so far and the total bytes to be transferred - (since 1.7.4) - @type callback: function(int, int) - @param confirm: whether to do a stat() on the file afterwards to - confirm the file size (since 1.7.7) - @type confirm: bool - - @return: an object containing attributes about the given file - (since 1.7.4) - @rtype: SFTPAttributes - - @since: 1.4 + :param str localpath: the local file to copy + :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 + :param bool confirm: + whether to do a stat() on the file afterwards to confirm the file + size + + :return: an `.SFTPAttributes` object containing attributes about the given file + + .. versionadded:: 1.4 + .. versionchanged:: 1.7.4 + ``callback`` and rich attribute return value added. + .. versionchanged:: 1.7.7 + ``confirm`` param added. """ file_size = os.stat(localpath).st_size - fl = file(localpath, 'rb') - try: - return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) - finally: - fl.close() + with open(localpath, 'rb') as fl: + return self.putfo(fl, remotepath, file_size, callback, confirm) def getfo(self, remotepath, fl, callback=None): """ - Copy a remote file (C{remotepath}) from the SFTP server and write to - an open file or file-like object, C{fl}. Any exception raised by + Copy a remote file (``remotepath``) from the SFTP server and write to + an open file or file-like object, ``fl``. Any exception raised by operations will be passed through. This method is primarily provided as a convenience. - @param remotepath: opened file or file-like object to copy to - @type remotepath: object - @param fl: the destination path on the local host or open file - object - @type localpath: str - @param callback: optional callback function that accepts the bytes - transferred so far and the total bytes to be transferred - (since 1.7.4) - @type callback: function(int, int) - @return: the number of bytes written to the opened file object - - @since: 1.4 - """ - fr = self.file(remotepath, 'rb') - file_size = self.stat(remotepath).st_size - fr.prefetch() - try: + :param object remotepath: opened file or file-like object to copy to + :param str fl: + the destination path on the local host or open file object + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + 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.10 + """ + 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,41 +697,33 @@ callback(size, file_size) if len(data) == 0: break - finally: - fr.close() return size def get(self, remotepath, localpath, callback=None): """ - Copy a remote file (C{remotepath}) from the SFTP server to the local - host as C{localpath}. Any exception raised by operations will be + Copy a remote file (``remotepath``) from the SFTP server to the local + host as ``localpath``. Any exception raised by operations will be passed through. This method is primarily provided as a convenience. - @param remotepath: the remote file to copy - @type remotepath: str - @param localpath: the destination path on the local host - @type localpath: str - @param callback: optional callback function that accepts the bytes - transferred so far and the total bytes to be transferred - (since 1.7.4) - @type callback: function(int, int) - - @since: 1.4 + :param str remotepath: the remote file to copy + :param str localpath: the destination path on the local host + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + the bytes transferred so far and the total bytes to be transferred + + .. versionadded:: 1.4 + .. versionchanged:: 1.7.4 + Added the ``callback`` param """ 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)) - ### internals... - def _request(self, t, *arg): num = self._async_request(type(None), t, *arg) return self._read_response(num) @@ -695,11 +735,11 @@ 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 +747,7 @@ 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 +757,8 @@ 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: @@ -736,11 +776,11 @@ self._convert_status(msg) return t, msg if fileobj is not type(None): - fileobj._async_response(t, msg) + fileobj._async_response(t, msg, num) if waitfor is None: # just doing a single check break - return (None, None) + return None, None def _finish_responses(self, fileobj): while fileobj in self._expecting.values(): @@ -752,7 +792,7 @@ 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,18 +810,19 @@ 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): - "an alias for L{SFTPClient} for backwards compatability" +class SFTP(SFTPClient): + """ + An alias for `.SFTPClient` for backwards compatability. + """ pass diff -Nru paramiko-1.10.1/paramiko/sftp_file.py paramiko-1.15.1/paramiko/sftp_file.py --- paramiko-1.10.1/paramiko/sftp_file.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp_file.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,18 +17,22 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{SFTPFile} +SFTP file object """ +from __future__ import with_statement + from binascii import hexlify from collections import deque import socket import threading import time +from paramiko.common import DEBUG -from paramiko.common import * -from paramiko.sftp import * from paramiko.file import BufferedFile +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 @@ -53,7 +57,8 @@ self._prefetching = False self._prefetch_done = False self._prefetch_data = {} - self._prefetch_reads = [] + self._prefetch_extents = {} + self._prefetch_lock = threading.Lock() self._saved_exception = None self._reqs = deque() @@ -61,6 +66,9 @@ self._close(async=True) def close(self): + """ + Close the file. + """ self._close(async=False) def _close(self, async=False): @@ -91,10 +99,10 @@ pass def _data_in_prefetch_requests(self, offset, size): - k = [i for i in self._prefetch_reads if i[0] <= offset] + k = [x for x in list(self._prefetch_extents.values()) if x[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 +173,7 @@ 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() @@ -178,34 +186,34 @@ def settimeout(self, timeout): """ Set a timeout on read/write operations on the underlying socket or - ssh L{Channel}. + ssh `.Channel`. - @see: L{Channel.settimeout} - @param timeout: seconds to wait for a pending read/write operation - before raising C{socket.timeout}, or C{None} for no timeout - @type timeout: float + :param float timeout: + seconds to wait for a pending read/write operation before raising + ``socket.timeout``, or ``None`` for no timeout + + .. seealso:: `.Channel.settimeout` """ self.sftp.sock.settimeout(timeout) def gettimeout(self): """ - Returns the timeout in seconds (as a float) associated with the socket - or ssh L{Channel} used for this file. + Returns the timeout in seconds (as a `float`) associated with the + socket or ssh `.Channel` used for this file. - @see: L{Channel.gettimeout} - @rtype: float + .. seealso:: `.Channel.gettimeout` """ return self.sftp.sock.gettimeout() def setblocking(self, blocking): """ Set blocking or non-blocking mode on the underiying socket or ssh - L{Channel}. + `.Channel`. + + :param int blocking: + 0 to set non-blocking mode; non-0 to set blocking mode. - @see: L{Channel.setblocking} - @param blocking: 0 to set non-blocking mode; non-0 to set blocking - mode. - @type blocking: int + .. seealso:: `.Channel.setblocking` """ self.sftp.sock.setblocking(blocking) @@ -218,16 +226,15 @@ self._realpos = self._pos else: self._realpos = self._pos = self._get_size() + offset - self._rbuffer = '' + self._rbuffer = bytes() def stat(self): """ Retrieve information about this file from the remote system. This is - exactly like L{SFTP.stat}, except that it operates on an already-open - file. + exactly like `.SFTPClient.stat`, except that it operates on an + already-open file. - @return: an object containing attributes about this file. - @rtype: SFTPAttributes + :return: an `.SFTPAttributes` object containing attributes about this file. """ t, msg = self.sftp._request(CMD_FSTAT, self.handle) if t != CMD_ATTRS: @@ -237,11 +244,10 @@ def chmod(self, mode): """ Change the mode (permissions) of this file. The permissions are - unix-style and identical to those used by python's C{os.chmod} + unix-style and identical to those used by Python's `os.chmod` function. - @param mode: new permissions - @type mode: int + :param int mode: new permissions """ self.sftp._log(DEBUG, 'chmod(%s, %r)' % (hexlify(self.handle), mode)) attr = SFTPAttributes() @@ -250,15 +256,13 @@ def chown(self, uid, gid): """ - Change the owner (C{uid}) and group (C{gid}) of this file. As with - python's C{os.chown} function, you must pass both arguments, so if you - only want to change one, use L{stat} first to retrieve the current + Change the owner (``uid``) and group (``gid``) of this file. As with + Python's `os.chown` function, you must pass both arguments, so if you + only want to change one, use `stat` first to retrieve the current owner and group. - @param uid: new owner's uid - @type uid: int - @param gid: new group id - @type gid: int + :param int uid: new owner's uid + :param int gid: new group id """ self.sftp._log(DEBUG, 'chown(%s, %r, %r)' % (hexlify(self.handle), uid, gid)) attr = SFTPAttributes() @@ -268,15 +272,15 @@ def utime(self, times): """ Set the access and modified times of this file. If - C{times} is C{None}, then the file's access and modified times are set - to the current time. Otherwise, C{times} must be a 2-tuple of numbers, - of the form C{(atime, mtime)}, which is used to set the access and - modified times, respectively. This bizarre API is mimicked from python + ``times`` is ``None``, then the file's access and modified times are set + to the current time. Otherwise, ``times`` must be a 2-tuple of numbers, + of the form ``(atime, mtime)``, which is used to set the access and + modified times, respectively. This bizarre API is mimicked from Python for the sake of consistency -- I apologize. - @param times: C{None} or a tuple of (access time, modified time) in - standard internet epoch time (seconds since 01 January 1970 GMT) - @type times: tuple(int) + :param tuple times: + ``None`` or a tuple of (access time, modified time) in standard + internet epoch time (seconds since 01 January 1970 GMT) """ if times is None: times = (time.time(), time.time()) @@ -288,11 +292,11 @@ def truncate(self, size): """ Change the size of this file. This usually extends - or shrinks the size of the file, just like the C{truncate()} method on - python file objects. + or shrinks the size of the file, just like the ``truncate()`` method on + Python file objects. - @param size: the new size of the file - @type size: int or long + :param size: the new size of the file + :type size: int or long """ self.sftp._log(DEBUG, 'truncate(%s, %r)' % (hexlify(self.handle), size)) attr = SFTPAttributes() @@ -305,51 +309,53 @@ to verify a successful upload or download, or for various rsync-like operations. - The file is hashed from C{offset}, for C{length} bytes. If C{length} - is 0, the remainder of the file is hashed. Thus, if both C{offset} - and C{length} are zero, the entire file is hashed. + The file is hashed from ``offset``, for ``length`` bytes. If ``length`` + is 0, the remainder of the file is hashed. Thus, if both ``offset`` + and ``length`` are zero, the entire file is hashed. - Normally, C{block_size} will be 0 (the default), and this method will + Normally, ``block_size`` will be 0 (the default), and this method will return a byte string representing the requested hash (for example, a string of length 16 for MD5, or 20 for SHA-1). If a non-zero - C{block_size} is given, each chunk of the file (from C{offset} to - C{offset + length}) of C{block_size} bytes is computed as a separate + ``block_size`` is given, each chunk of the file (from ``offset`` to + ``offset + length``) of ``block_size`` bytes is computed as a separate hash. The hash results are all concatenated and returned as a single string. - For example, C{check('sha1', 0, 1024, 512)} will return a string of + For example, ``check('sha1', 0, 1024, 512)`` will return a string of length 40. The first 20 bytes will be the SHA-1 of the first 512 bytes of the file, and the last 20 bytes will be the SHA-1 of the next 512 bytes. - @param hash_algorithm: the name of the hash algorithm to use (normally - C{"sha1"} or C{"md5"}) - @type hash_algorithm: str - @param offset: offset into the file to begin hashing (0 means to start - from the beginning) - @type offset: int or long - @param length: number of bytes to hash (0 means continue to the end of - the file) - @type length: int or long - @param block_size: number of bytes to hash per result (must not be less - than 256; 0 means to compute only one hash of the entire segment) - @type block_size: int - @return: string of bytes representing the hash of each block, - concatenated together - @rtype: str - - @note: Many (most?) servers don't support this extension yet. + :param str hash_algorithm: + the name of the hash algorithm to use (normally ``"sha1"`` or + ``"md5"``) + :param offset: + offset into the file to begin hashing (0 means to start from the + beginning) + :type offset: int or long + :param length: + number of bytes to hash (0 means continue to the end of the file) + :type length: int or long + :param int block_size: + number of bytes to hash per result (must not be less than 256; 0 + means to compute only one hash of the entire segment) + :type block_size: int + :return: + `str` of bytes representing the hash of each block, concatenated + together - @raise IOError: if the server doesn't support the "check-file" + :raises IOError: if the server doesn't support the "check-file" extension, or possibly doesn't support the hash algorithm requested - @since: 1.4 + .. note:: Many (most?) servers don't support this extension yet. + + .. versionadded:: 1.4 """ 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 @@ -357,35 +363,35 @@ """ Turn on/off the pipelining of write operations to this file. When pipelining is on, paramiko won't wait for the server response after - each write operation. Instead, they're collected as they come in. - At the first non-write operation (including L{close}), all remaining + each write operation. Instead, they're collected as they come in. At + the first non-write operation (including `.close`), all remaining server responses are collected. This means that if there was an error - with one of your later writes, an exception might be thrown from - within L{close} instead of L{write}. + with one of your later writes, an exception might be thrown from within + `.close` instead of `.write`. - By default, files are I{not} pipelined. + By default, files are not pipelined. - @param pipelined: C{True} if pipelining should be turned on for this - file; C{False} otherwise - @type pipelined: bool + :param bool pipelined: + ``True`` if pipelining should be turned on for this file; ``False`` + otherwise - @since: 1.5 + .. versionadded:: 1.5 """ self.pipelined = pipelined def prefetch(self): """ - Pre-fetch the remaining contents of this file in anticipation of - future L{read} calls. If reading the entire file, pre-fetching can + Pre-fetch the remaining contents of this file in anticipation of future + `.read` calls. If reading the entire file, pre-fetching can dramatically improve the download speed by avoiding roundtrip latency. The file's contents are incrementally buffered in a background thread. - The prefetched data is stored in a buffer until read via the L{read} + The prefetched data is stored in a buffer until read via the `.read` method. Once data has been read, it's removed from the buffer. The - data may be read in a random order (using L{seek}); chunks of the + data may be read in a random order (using `.seek`); chunks of the buffer that haven't been read will continue to be buffered. - @since: 1.5.1 + .. versionadded:: 1.5.1 """ size = self.stat().st_size # queue up async reads for the rest of the file @@ -401,17 +407,17 @@ def readv(self, chunks): """ Read a set of blocks from the file by (offset, length). This is more - efficient than doing a series of L{seek} and L{read} calls, since the + efficient than doing a series of `.seek` and `.read` calls, since the prefetch machinery is used to retrieve all the requested blocks at once. - @param chunks: a list of (offset, length) tuples indicating which - sections of the file to read - @type chunks: list(tuple(long, int)) - @return: a list of blocks read, in the same order as in C{chunks} - @rtype: list(str) + :param chunks: + a list of (offset, length) tuples indicating which sections of the + file to read + :type chunks: list(tuple(long, int)) + :return: a list of blocks read, in the same order as in ``chunks`` - @since: 1.5.4 + .. versionadded:: 1.5.4 """ self.sftp._log(DEBUG, 'readv(%s, %r)' % (hexlify(self.handle), chunks)) @@ -433,11 +439,9 @@ for x in chunks: self.seek(x[0]) yield self.read(x[1]) - ### internals... - def _get_size(self): try: return self.stat().st_size @@ -447,7 +451,6 @@ def _start_prefetch(self, chunks): self._prefetching = True self._prefetch_done = False - self._prefetch_reads.extend(chunks) t = threading.Thread(target=self._prefetch_thread, args=(chunks,)) t.setDaemon(True) @@ -457,33 +460,31 @@ # do these read requests in a temporary thread because there may be # a lot of them, so it may block. for offset, length in chunks: - self.sftp._async_request(self, CMD_READ, self.handle, long(offset), int(length)) + with self._prefetch_lock: + num = self.sftp._async_request(self, CMD_READ, self.handle, long(offset), int(length)) + self._prefetch_extents[num] = (offset, length) - def _async_response(self, t, msg): + def _async_response(self, t, msg, num): if t == CMD_STATUS: # 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') data = msg.get_string() - offset, length = self._prefetch_reads.pop(0) - self._prefetch_data[offset] = data - if len(self._prefetch_reads) == 0: - self._prefetch_done = True + with self._prefetch_lock: + offset, length = self._prefetch_extents[num] + self._prefetch_data[offset] = data + del self._prefetch_extents[num] + if len(self._prefetch_extents) == 0: + self._prefetch_done = True def _check_exception(self): - "if there's a saved exception, raise & clear it" + """if there's a saved exception, raise & clear it""" if self._saved_exception is not None: x = self._saved_exception self._saved_exception = None raise x - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() diff -Nru paramiko-1.10.1/paramiko/sftp_handle.py paramiko-1.15.1/paramiko/sftp_handle.py --- paramiko-1.10.1/paramiko/sftp_handle.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp_handle.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,33 +21,33 @@ """ import os +from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK +from paramiko.util import ClosingContextManager -from paramiko.common import * -from paramiko.sftp import * - -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 by the client to refer to the underlying file. Server implementations can (and should) subclass SFTPHandle to implement - features of a file handle, like L{stat} or L{chattr}. + features of a file handle, like `stat` or `chattr`. + + Instances of this class may be used as context managers. """ def __init__(self, flags=0): """ Create a new file handle representing a local file being served over - SFTP. If C{flags} is passed in, it's used to determine if the file + SFTP. If ``flags`` is passed in, it's used to determine if the file is open in append mode. - @param flags: optional flags as passed to L{SFTPServerInterface.open} - @type flags: int + :param int flags: optional flags as passed to `.SFTPServerInterface.open` """ self.__flags = flags self.__name = None # only for handles to folders: - self.__files = { } + self.__files = {} self.__tell = None def close(self): @@ -56,10 +56,10 @@ Normally you would use this method to close the underlying OS level file object(s). - The default implementation checks for attributes on C{self} named - C{readfile} and/or C{writefile}, and if either or both are present, - their C{close()} methods are called. This means that if you are - using the default implementations of L{read} and L{write}, this + The default implementation checks for attributes on ``self`` named + ``readfile`` and/or ``writefile``, and if either or both are present, + their ``close()`` methods are called. This means that if you are + using the default implementations of `read` and `write`, this method's default implementation should be fine also. """ readfile = getattr(self, 'readfile', None) @@ -71,24 +71,22 @@ def read(self, offset, length): """ - Read up to C{length} bytes from this file, starting at position - C{offset}. The offset may be a python long, since SFTP allows it + Read up to ``length`` bytes from this file, starting at position + ``offset``. The offset may be a Python long, since SFTP allows it to be 64 bits. If the end of the file has been reached, this method may return an - empty string to signify EOF, or it may also return L{SFTP_EOF}. + empty string to signify EOF, or it may also return `.SFTP_EOF`. - The default implementation checks for an attribute on C{self} named - C{readfile}, and if present, performs the read operation on the python + The default implementation checks for an attribute on ``self`` named + ``readfile``, and if present, performs the read operation on the Python file-like object found there. (This is meant as a time saver for the - common case where you are wrapping a python file object.) + common case where you are wrapping a Python file object.) - @param offset: position in the file to start reading from. - @type offset: int or long - @param length: number of bytes to attempt to read. - @type length: int - @return: data read from the file, or an SFTP error code. - @rtype: str + :param offset: position in the file to start reading from. + :type offset: int or long + :param int length: number of bytes to attempt to read. + :return: data read from the file, or an SFTP error code, as a `str`. """ readfile = getattr(self, 'readfile', None) if readfile is None: @@ -100,7 +98,7 @@ 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) @@ -108,23 +106,22 @@ def write(self, offset, data): """ - Write C{data} into this file at position C{offset}. Extending the - file past its original end is expected. Unlike python's normal - C{write()} methods, this method cannot do a partial write: it must - write all of C{data} or else return an error. - - The default implementation checks for an attribute on C{self} named - C{writefile}, and if present, performs the write operation on the - python file-like object found there. The attribute is named - differently from C{readfile} to make it easy to implement read-only + Write ``data`` into this file at position ``offset``. Extending the + file past its original end is expected. Unlike Python's normal + ``write()`` methods, this method cannot do a partial write: it must + write all of ``data`` or else return an error. + + The default implementation checks for an attribute on ``self`` named + ``writefile``, and if present, performs the write operation on the + Python file-like object found there. The attribute is named + differently from ``readfile`` to make it easy to implement read-only (or write-only) files, but if both attributes are present, they should refer to the same file. - @param offset: position in the file to start reading from. - @type offset: int or long - @param data: data to write into the file. - @type data: str - @return: an SFTP error code like L{SFTP_OK}. + :param offset: position in the file to start reading from. + :type offset: int or long + :param str data: data to write into the file. + :return: an SFTP error code like `.SFTP_OK`. """ writefile = getattr(self, 'writefile', None) if writefile is None: @@ -139,7 +136,7 @@ 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: @@ -148,33 +145,30 @@ def stat(self): """ - Return an L{SFTPAttributes} object referring to this open file, or an - error code. This is equivalent to L{SFTPServerInterface.stat}, except + Return an `.SFTPAttributes` object referring to this open file, or an + error code. This is equivalent to `.SFTPServerInterface.stat`, except it's called on an open file instead of a path. - @return: an attributes object for the given file, or an SFTP error - code (like L{SFTP_PERMISSION_DENIED}). - @rtype: L{SFTPAttributes} I{or error code} + :return: + an attributes object for the given file, or an SFTP error code + (like `.SFTP_PERMISSION_DENIED`). + :rtype: `.SFTPAttributes` or error code """ return SFTP_OP_UNSUPPORTED def chattr(self, attr): """ - Change the attributes of this file. The C{attr} object will contain + Change the attributes of this file. The ``attr`` object will contain only those fields provided by the client in its request, so you should check for the presence of fields before using them. - @param attr: the attributes to change on this file. - @type attr: L{SFTPAttributes} - @return: an error code like L{SFTP_OK}. - @rtype: int + :param .SFTPAttributes attr: the attributes to change on this file. + :return: an `int` error code like `.SFTP_OK`. """ return SFTP_OP_UNSUPPORTED - ### internals... - def _set_files(self, files): """ Used by the SFTP server code to cache a directory listing. (In diff -Nru paramiko-1.10.1/paramiko/sftp.py paramiko-1.15.1/paramiko/sftp.py --- paramiko-1.10.1/paramiko/sftp.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -20,32 +20,31 @@ import socket import struct -from paramiko.common import * from paramiko import util -from paramiko.channel import Channel +from paramiko.common import asbytes, DEBUG from paramiko.message import Message +from paramiko.py3compat import byte_chr, byte_ord CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, CMD_FSTAT, \ - CMD_SETSTAT, CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, CMD_REMOVE, CMD_MKDIR, \ - CMD_RMDIR, CMD_REALPATH, CMD_STAT, CMD_RENAME, CMD_READLINK, CMD_SYMLINK \ - = range(1, 21) + CMD_SETSTAT, CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, CMD_REMOVE, CMD_MKDIR, \ + CMD_RMDIR, CMD_REALPATH, CMD_STAT, CMD_RENAME, CMD_READLINK, CMD_SYMLINK = range(1, 21) CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS = range(101, 106) CMD_EXTENDED, CMD_EXTENDED_REPLY = range(200, 202) SFTP_OK = 0 SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED, SFTP_FAILURE, SFTP_BAD_MESSAGE, \ - SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED = range(1, 9) + SFTP_NO_CONNECTION, SFTP_CONNECTION_LOST, SFTP_OP_UNSUPPORTED = range(1, 9) -SFTP_DESC = [ 'Success', - 'End of file', - 'No such file', - 'Permission denied', - 'Failure', - 'Bad message', - 'No connection', - 'Connection lost', - 'Operation unsupported' ] +SFTP_DESC = ['Success', + 'End of file', + 'No such file', + 'Permission denied', + 'Failure', + 'Bad message', + 'No connection', + 'Connection lost', + 'Operation unsupported'] SFTP_FLAG_READ = 0x1 SFTP_FLAG_WRITE = 0x2 @@ -86,7 +85,7 @@ CMD_ATTRS: 'attrs', CMD_EXTENDED: 'extended', CMD_EXTENDED_REPLY: 'extended_reply' - } +} class SFTPError (Exception): @@ -99,10 +98,8 @@ self.sock = None self.ultra_debug = False - ### internals... - def _send_version(self): self._send_packet(CMD_INIT, struct.pack('>I', _VERSION)) t, data = self._read_packet() @@ -121,11 +118,11 @@ raise SFTPError('Incompatible sftp protocol') version = struct.unpack('>I', data[:4])[0] # advertise that we support "check-file" - extension_pairs = [ 'check-file', 'md5,sha1' ] + extension_pairs = ['check-file', 'md5,sha1'] 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 +139,7 @@ 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 @@ -151,7 +148,7 @@ # return or raise an exception, but calling select on a closed # socket will.) while True: - read, write, err = select.select([ self.sock ], [], [], 0.1) + read, write, err = select.select([self.sock], [], [], 0.1) if len(read) > 0: x = self.sock.recv(n) break @@ -166,7 +163,8 @@ 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 +173,14 @@ 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: ')); + 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 -Nru paramiko-1.10.1/paramiko/sftp_server.py paramiko-1.15.1/paramiko/sftp_server.py --- paramiko-1.10.1/paramiko/sftp_server.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp_server.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -22,46 +22,55 @@ import os import errno +import sys +from hashlib import md5, sha1 -from Crypto.Hash import MD5, SHA -from paramiko.common import * +from paramiko import util +from paramiko.sftp import BaseSFTP, Message, SFTP_FAILURE, \ + SFTP_PERMISSION_DENIED, SFTP_NO_SUCH_FILE +from paramiko.sftp_si import SFTPServerInterface +from paramiko.sftp_attr import SFTPAttributes +from paramiko.common import DEBUG +from paramiko.py3compat import long, string_types, bytes_types, b from paramiko.server import SubsystemHandler -from paramiko.sftp import * -from paramiko.sftp_si import * -from paramiko.sftp_attr import * # known hash algorithms for the "check-file" extension +from paramiko.sftp import CMD_HANDLE, SFTP_DESC, CMD_STATUS, SFTP_EOF, CMD_NAME, \ + SFTP_BAD_MESSAGE, CMD_EXTENDED_REPLY, SFTP_FLAG_READ, SFTP_FLAG_WRITE, \ + SFTP_FLAG_APPEND, SFTP_FLAG_CREATE, SFTP_FLAG_TRUNC, SFTP_FLAG_EXCL, \ + CMD_NAMES, CMD_OPEN, CMD_CLOSE, SFTP_OK, CMD_READ, CMD_DATA, CMD_WRITE, \ + CMD_REMOVE, CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_OPENDIR, CMD_READDIR, \ + CMD_STAT, CMD_ATTRS, CMD_LSTAT, CMD_FSTAT, CMD_SETSTAT, CMD_FSETSTAT, \ + CMD_READLINK, CMD_SYMLINK, CMD_REALPATH, CMD_EXTENDED, SFTP_OP_UNSUPPORTED + _hash_class = { - 'sha1': SHA, - 'md5': MD5, + 'sha1': sha1, + 'md5': md5, } class SFTPServer (BaseSFTP, SubsystemHandler): """ - Server-side SFTP subsystem support. Since this is a L{SubsystemHandler}, - it can be (and is meant to be) set as the handler for C{"sftp"} requests. - Use L{Transport.set_subsystem_handler} to activate this class. + Server-side SFTP subsystem support. Since this is a `.SubsystemHandler`, + it can be (and is meant to be) set as the handler for ``"sftp"`` requests. + Use `.Transport.set_subsystem_handler` to activate this class. """ def __init__(self, channel, name, server, sftp_si=SFTPServerInterface, *largs, **kwargs): """ The constructor for SFTPServer is meant to be called from within the - L{Transport} as a subsystem handler. C{server} and any additional + `.Transport` as a subsystem handler. ``server`` and any additional parameters or keyword parameters are passed from the original call to - L{Transport.set_subsystem_handler}. + `.Transport.set_subsystem_handler`. - @param channel: channel passed from the L{Transport}. - @type channel: L{Channel} - @param name: name of the requested subsystem. - @type name: str - @param server: the server object associated with this channel and - subsystem - @type server: L{ServerInterface} - @param sftp_si: a subclass of L{SFTPServerInterface} to use for handling - individual requests. - @type sftp_si: class + :param .Channel channel: channel passed from the `.Transport`. + :param str name: name of the requested subsystem. + :param .ServerInterface server: + the server object associated with this channel and subsystem + :param class sftp_si: + a subclass of `.SFTPServerInterface` to use for handling individual + requests. """ BaseSFTP.__init__(self) SubsystemHandler.__init__(self, channel, name, server) @@ -70,17 +79,17 @@ self.ultra_debug = transport.get_hexdump() self.next_handle = 1 # map of handle-string to SFTPHandle for files & folders: - self.file_table = { } - self.folder_table = { } + self.file_table = {} + self.folder_table = {} self.server = sftp_si(server, *largs, **kwargs) - + def _log(self, level, msg): if issubclass(type(msg), list): for m in msg: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m) else: super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg) - + def start_subsystem(self, name, transport, channel): self.sock = channel self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel)) @@ -92,7 +101,7 @@ 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 +109,7 @@ 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,23 +122,21 @@ 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 = {} def convert_errno(e): """ - Convert an errno value (as from an C{OSError} or C{IOError}) into a + Convert an errno value (as from an ``OSError`` or ``IOError``) into a standard SFTP result code. This is a convenience function for trapping exceptions in server code and returning an appropriate result. - @param e: an errno code, as from C{OSError.errno}. - @type e: int - @return: an SFTP error code like L{SFTP_NO_SUCH_FILE}. - @rtype: int + :param int e: an errno code, as from ``OSError.errno``. + :return: an `int` SFTP error code like ``SFTP_NO_SUCH_FILE``. """ if e == errno.EACCES: # permission denied @@ -144,18 +151,16 @@ def set_file_attr(filename, attr): """ Change a file's attributes on the local filesystem. The contents of - C{attr} are used to change the permissions, owner, group ownership, + ``attr`` are used to change the permissions, owner, group ownership, and/or modification & access time of the file, depending on which - attributes are present in C{attr}. + attributes are present in ``attr``. This is meant to be a handy helper function for translating SFTP file requests into local file operations. - - @param filename: name of the file to alter (should usually be an - absolute path). - @type filename: str - @param attr: attributes to change. - @type attr: L{SFTPAttributes} + + :param str filename: + name of the file to alter (should usually be an absolute path). + :param .SFTPAttributes attr: attributes to change. """ if sys.platform != 'win32': # mode operations are meaningless on win32 @@ -166,35 +171,34 @@ 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) - ### internals... - def _response(self, request_number, t, *arg): 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 +236,16 @@ 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,17 +274,17 @@ 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) # don't try to read more than about 64KB at a time chunklen = min(blocklen, 65536) count = 0 - hash_obj = alg.new() + hash_obj = alg() while count < blocklen: data = f.read(offset, chunklen) - if not 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,10 +297,10 @@ 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" + """convert SFTP-style open() flags to Python's os.open() flags""" if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): flags = os.O_RDWR elif pflags & SFTP_FLAG_WRITE: @@ -316,12 +320,12 @@ 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 +337,14 @@ 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 +352,54 @@ 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 +409,34 @@ 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 -Nru paramiko-1.10.1/paramiko/sftp_si.py paramiko-1.15.1/paramiko/sftp_si.py --- paramiko-1.10.1/paramiko/sftp_si.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/sftp_si.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,19 +17,18 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{SFTPServerInterface} is an interface to override for SFTP server support. +An interface to override for SFTP server support. """ import os - -from paramiko.common import * -from paramiko.sftp import * +import sys +from paramiko.sftp import SFTP_OP_UNSUPPORTED class SFTPServerInterface (object): """ This class defines an interface for controlling the behavior of paramiko - when using the L{SFTPServer} subsystem to provide an SFTP server. + when using the `.SFTPServer` subsystem to provide an SFTP server. Methods on this class are called from the SFTP session's thread, so you can block as long as necessary without affecting other sessions (even other @@ -41,14 +40,13 @@ clients & servers obey the requirement that paths be encoded in UTF-8. """ - def __init__ (self, server, *largs, **kwargs): + def __init__(self, server, *largs, **kwargs): """ Create a new SFTPServerInterface object. This method does nothing by default and is meant to be overridden by subclasses. - @param server: the server object associated with this channel and - SFTP subsystem - @type server: L{ServerInterface} + :param .ServerInterface server: + the server object associated with this channel and SFTP subsystem """ super(SFTPServerInterface, self).__init__(*largs, **kwargs) @@ -64,7 +62,7 @@ """ The SFTP server session has just ended, either cleanly or via an exception. This method is meant to be overridden to perform any - necessary cleanup before this C{SFTPServerInterface} object is + necessary cleanup before this `.SFTPServerInterface` object is destroyed. """ pass @@ -72,103 +70,105 @@ def open(self, path, flags, attr): """ Open a file on the server and create a handle for future operations - on that file. On success, a new object subclassed from L{SFTPHandle} + on that file. On success, a new object subclassed from `.SFTPHandle` should be returned. This handle will be used for future operations on the file (read, write, etc). On failure, an error code such as - L{SFTP_PERMISSION_DENIED} should be returned. + `.SFTP_PERMISSION_DENIED` should be returned. + + ``flags`` contains the requested mode for opening (read-only, + write-append, etc) as a bitset of flags from the ``os`` module: + + - ``os.O_RDONLY`` + - ``os.O_WRONLY`` + - ``os.O_RDWR`` + - ``os.O_APPEND`` + - ``os.O_CREAT`` + - ``os.O_TRUNC`` + - ``os.O_EXCL`` - C{flags} contains the requested mode for opening (read-only, - write-append, etc) as a bitset of flags from the C{os} module: - - C{os.O_RDONLY} - - C{os.O_WRONLY} - - C{os.O_RDWR} - - C{os.O_APPEND} - - C{os.O_CREAT} - - C{os.O_TRUNC} - - C{os.O_EXCL} - (One of C{os.O_RDONLY}, C{os.O_WRONLY}, or C{os.O_RDWR} will always + (One of ``os.O_RDONLY``, ``os.O_WRONLY``, or ``os.O_RDWR`` will always be set.) - The C{attr} object contains requested attributes of the file if it + The ``attr`` object contains requested attributes of the file if it has to be created. Some or all attribute fields may be missing if the client didn't specify them. - @note: The SFTP protocol defines all files to be in "binary" mode. - There is no equivalent to python's "text" mode. + .. note:: The SFTP protocol defines all files to be in "binary" mode. + There is no equivalent to Python's "text" mode. - @param path: the requested path (relative or absolute) of the file - to be opened. - @type path: str - @param flags: flags or'd together from the C{os} module indicating the - requested mode for opening the file. - @type flags: int - @param attr: requested attributes of the file if it is newly created. - @type attr: L{SFTPAttributes} - @return: a new L{SFTPHandle} I{or error code}. - @rtype L{SFTPHandle} + :param str path: + the requested path (relative or absolute) of the file to be opened. + :param int flags: + flags or'd together from the ``os`` module indicating the requested + mode for opening the file. + :param .SFTPAttributes attr: + requested attributes of the file if it is newly created. + :return: a new `.SFTPHandle` or error code. """ return SFTP_OP_UNSUPPORTED def list_folder(self, path): """ - Return a list of files within a given folder. The C{path} will use - posix notation (C{"/"} separates folder names) and may be an absolute + Return a list of files within a given folder. The ``path`` will use + posix notation (``"/"`` separates folder names) and may be an absolute or relative path. - The list of files is expected to be a list of L{SFTPAttributes} + The list of files is expected to be a list of `.SFTPAttributes` objects, which are similar in structure to the objects returned by - C{os.stat}. In addition, each object should have its C{filename} + ``os.stat``. In addition, each object should have its ``filename`` field filled in, since this is important to a directory listing and - not normally present in C{os.stat} results. The method - L{SFTPAttributes.from_stat} will usually do what you want. + not normally present in ``os.stat`` results. The method + `.SFTPAttributes.from_stat` will usually do what you want. - In case of an error, you should return one of the C{SFTP_*} error - codes, such as L{SFTP_PERMISSION_DENIED}. + In case of an error, you should return one of the ``SFTP_*`` error + codes, such as `.SFTP_PERMISSION_DENIED`. - @param path: the requested path (relative or absolute) to be listed. - @type path: str - @return: a list of the files in the given folder, using - L{SFTPAttributes} objects. - @rtype: list of L{SFTPAttributes} I{or error code} + :param str path: the requested path (relative or absolute) to be listed. + :return: + a list of the files in the given folder, using `.SFTPAttributes` + objects. - @note: You should normalize the given C{path} first (see the - C{os.path} module) and check appropriate permissions before returning - the list of files. Be careful of malicious clients attempting to use - relative paths to escape restricted folders, if you're doing a direct - translation from the SFTP server path to your local filesystem. + .. note:: + You should normalize the given ``path`` first (see the `os.path` + module) and check appropriate permissions before returning the list + of files. Be careful of malicious clients attempting to use + relative paths to escape restricted folders, if you're doing a + direct translation from the SFTP server path to your local + filesystem. """ return SFTP_OP_UNSUPPORTED def stat(self, path): """ - Return an L{SFTPAttributes} object for a path on the server, or an + Return an `.SFTPAttributes` object for a path on the server, or an error code. If your server supports symbolic links (also known as - "aliases"), you should follow them. (L{lstat} is the corresponding + "aliases"), you should follow them. (`lstat` is the corresponding call that doesn't follow symlinks/aliases.) - @param path: the requested path (relative or absolute) to fetch - file statistics for. - @type path: str - @return: an attributes object for the given file, or an SFTP error - code (like L{SFTP_PERMISSION_DENIED}). - @rtype: L{SFTPAttributes} I{or error code} + :param str path: + the requested path (relative or absolute) to fetch file statistics + for. + :return: + an `.SFTPAttributes` object for the given file, or an SFTP error + code (like `.SFTP_PERMISSION_DENIED`). """ return SFTP_OP_UNSUPPORTED def lstat(self, path): """ - Return an L{SFTPAttributes} object for a path on the server, or an + Return an `.SFTPAttributes` object for a path on the server, or an error code. If your server supports symbolic links (also known as - "aliases"), you should I{not} follow them -- instead, you should - return data on the symlink or alias itself. (L{stat} is the + "aliases"), you should not follow them -- instead, you should + return data on the symlink or alias itself. (`stat` is the corresponding call that follows symlinks/aliases.) - @param path: the requested path (relative or absolute) to fetch - file statistics for. - @type path: str - @return: an attributes object for the given file, or an SFTP error - code (like L{SFTP_PERMISSION_DENIED}). - @rtype: L{SFTPAttributes} I{or error code} + :param str path: + the requested path (relative or absolute) to fetch file statistics + for. + :type path: str + :return: + an `.SFTPAttributes` object for the given file, or an SFTP error + code (like `.SFTP_PERMISSION_DENIED`). """ return SFTP_OP_UNSUPPORTED @@ -176,11 +176,9 @@ """ Delete a file, if possible. - @param path: the requested path (relative or absolute) of the file - to delete. - @type path: str - @return: an SFTP error code like L{SFTP_OK}. - @rtype: int + :param str path: + the requested path (relative or absolute) of the file to delete. + :return: an SFTP error code `int` like `.SFTP_OK`. """ return SFTP_OP_UNSUPPORTED @@ -192,83 +190,74 @@ probably a good idea to implement "move" in this method too, even for files that cross disk partition boundaries, if at all possible. - @note: You should return an error if a file with the same name as - C{newpath} already exists. (The rename operation should be + .. note:: You should return an error if a file with the same name as + ``newpath`` already exists. (The rename operation should be non-desctructive.) - @param oldpath: the requested path (relative or absolute) of the - existing file. - @type oldpath: str - @param newpath: the requested new path of the file. - @type newpath: str - @return: an SFTP error code like L{SFTP_OK}. - @rtype: int + :param str oldpath: + the requested path (relative or absolute) of the existing file. + :param str newpath: the requested new path of the file. + :return: an SFTP error code `int` like `.SFTP_OK`. """ return SFTP_OP_UNSUPPORTED def mkdir(self, path, attr): """ - Create a new directory with the given attributes. The C{attr} + Create a new directory with the given attributes. The ``attr`` object may be considered a "hint" and ignored. - The C{attr} object will contain only those fields provided by the - client in its request, so you should use C{hasattr} to check for - the presense of fields before using them. In some cases, the C{attr} + The ``attr`` object will contain only those fields provided by the + client in its request, so you should use ``hasattr`` to check for + the presense of fields before using them. In some cases, the ``attr`` object may be completely empty. - @param path: requested path (relative or absolute) of the new - folder. - @type path: str - @param attr: requested attributes of the new folder. - @type attr: L{SFTPAttributes} - @return: an SFTP error code like L{SFTP_OK}. - @rtype: int + :param str path: + requested path (relative or absolute) of the new folder. + :param .SFTPAttributes attr: requested attributes of the new folder. + :return: an SFTP error code `int` like `.SFTP_OK`. """ return SFTP_OP_UNSUPPORTED def rmdir(self, path): """ - Remove a directory if it exists. The C{path} should refer to an + Remove a directory if it exists. The ``path`` should refer to an existing, empty folder -- otherwise this method should return an error. - @param path: requested path (relative or absolute) of the folder - to remove. - @type path: str - @return: an SFTP error code like L{SFTP_OK}. - @rtype: int + :param str path: + requested path (relative or absolute) of the folder to remove. + :return: an SFTP error code `int` like `.SFTP_OK`. """ return SFTP_OP_UNSUPPORTED def chattr(self, path, attr): """ - Change the attributes of a file. The C{attr} object will contain + Change the attributes of a file. The ``attr`` object will contain only those fields provided by the client in its request, so you should check for the presence of fields before using them. - @param path: requested path (relative or absolute) of the file to - change. - @type path: str - @param attr: requested attributes to change on the file. - @type attr: L{SFTPAttributes} - @return: an error code like L{SFTP_OK}. - @rtype: int + :param str path: + requested path (relative or absolute) of the file to change. + :param attr: + requested attributes to change on the file (an `.SFTPAttributes` + object) + :return: an error code `int` like `.SFTP_OK`. """ return SFTP_OP_UNSUPPORTED def canonicalize(self, path): """ Return the canonical form of a path on the server. For example, - if the server's home folder is C{/home/foo}, the path - C{"../betty"} would be canonicalized to C{"/home/betty"}. Note + if the server's home folder is ``/home/foo``, the path + ``"../betty"`` would be canonicalized to ``"/home/betty"``. Note the obvious security issues: if you're serving files only from a specific folder, you probably don't want this method to reveal path names outside that folder. - You may find the python methods in C{os.path} useful, especially - C{os.path.normpath} and C{os.path.realpath}. + You may find the Python methods in ``os.path`` useful, especially + ``os.path.normpath`` and ``os.path.realpath``. - The default implementation returns C{os.path.normpath('/' + path)}. + The default implementation returns ``os.path.normpath('/' + path)``. """ if os.path.isabs(path): out = os.path.normpath(path) @@ -285,26 +274,23 @@ If the specified path doesn't refer to a symbolic link, an error should be returned. - @param path: path (relative or absolute) of the symbolic link. - @type path: str - @return: the target path of the symbolic link, or an error code like - L{SFTP_NO_SUCH_FILE}. - @rtype: str I{or error code} + :param str path: path (relative or absolute) of the symbolic link. + :return: + the target `str` path of the symbolic link, or an error code like + `.SFTP_NO_SUCH_FILE`. """ return SFTP_OP_UNSUPPORTED def symlink(self, target_path, path): """ - Create a symbolic link on the server, as new pathname C{path}, - with C{target_path} as the target of the link. + Create a symbolic link on the server, as new pathname ``path``, + with ``target_path`` as the target of the link. - @param target_path: path (relative or absolute) of the target for - this new symbolic link. - @type target_path: str - @param path: path (relative or absolute) of the symbolic link to - create. - @type path: str - @return: an error code like C{SFTP_OK}. - @rtype: int + :param str target_path: + path (relative or absolute) of the target for this new symbolic + link. + :param str path: + path (relative or absolute) of the symbolic link to create. + :return: an error code `int` like ``SFTP_OK``. """ return SFTP_OP_UNSUPPORTED diff -Nru paramiko-1.10.1/paramiko/ssh_exception.py paramiko-1.15.1/paramiko/ssh_exception.py --- paramiko-1.10.1/paramiko/ssh_exception.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/ssh_exception.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -16,10 +16,6 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. -""" -Exceptions defined by paramiko. -""" - class SSHException (Exception): """ @@ -34,7 +30,7 @@ possible to retry with different credentials. (Other classes specify more specific reasons.) - @since: 1.6 + .. versionadded:: 1.6 """ pass @@ -52,19 +48,20 @@ the server isn't allowing that type. (It may only allow public-key, for example.) - @ivar allowed_types: list of allowed authentication types provided by the - server (possible values are: C{"none"}, C{"password"}, and - C{"publickey"}). - @type allowed_types: list + :ivar list allowed_types: + list of allowed authentication types provided by the server (possible + values are: ``"none"``, ``"password"``, and ``"publickey"``). - @since: 1.1 + .. versionadded:: 1.1 """ allowed_types = [] def __init__(self, explanation, types): AuthenticationException.__init__(self, explanation) self.allowed_types = types - + # for unpickling + self.args = (explanation, types, ) + def __str__(self): return SSHException.__str__(self) + ' (allowed_types=%r)' % self.allowed_types @@ -78,50 +75,50 @@ def __init__(self, types): AuthenticationException.__init__(self, 'partial authentication') self.allowed_types = types + # for unpickling + self.args = (types, ) class ChannelException (SSHException): """ - Exception raised when an attempt to open a new L{Channel} fails. + Exception raised when an attempt to open a new `.Channel` fails. - @ivar code: the error code returned by the server - @type code: int + :ivar int code: the error code returned by the server - @since: 1.6 + .. versionadded:: 1.6 """ def __init__(self, code, text): SSHException.__init__(self, text) self.code = code + # for unpickling + self.args = (code, text, ) class BadHostKeyException (SSHException): """ The host key given by the SSH server did not match what we were expecting. - @ivar hostname: the hostname of the SSH server - @type hostname: str - @ivar key: the host key presented by the server - @type key: L{PKey} - @ivar expected_key: the host key expected - @type expected_key: L{PKey} + :ivar str hostname: the hostname of the SSH server + :ivar PKey got_key: the host key presented by the server + :ivar PKey expected_key: the host key expected - @since: 1.6 + .. versionadded:: 1.6 """ def __init__(self, hostname, got_key, expected_key): SSHException.__init__(self, 'Host key for server %s does not match!' % hostname) self.hostname = hostname self.key = got_key self.expected_key = expected_key + # for unpickling + self.args = (hostname, got_key, expected_key, ) class ProxyCommandFailure (SSHException): """ The "ProxyCommand" found in the .ssh/config file returned an error. - @ivar command: The command line that is generating this exception. - @type command: str - @ivar error: The error captured from the proxy command output. - @type error: str + :ivar str command: The command line that is generating this exception. + :ivar str error: The error captured from the proxy command output. """ def __init__(self, command, error): SSHException.__init__(self, @@ -130,3 +127,5 @@ ) ) self.error = error + # for unpickling + self.args = (command, error, ) diff -Nru paramiko-1.10.1/paramiko/ssh_gss.py paramiko-1.15.1/paramiko/ssh_gss.py --- paramiko-1.10.1/paramiko/ssh_gss.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/ssh_gss.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,587 @@ +# 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 as defined in RFC 4462. + +.. note:: Credential delegation is not supported in server mode. + +.. seealso:: :doc:`/api/kex_gss` + +.. versionadded:: 1.15 +""" + +import struct +import os +import sys + +""" +:var bool GSS_AUTH_AVAILABLE: + Constraint that indicates if GSS-API / SSPI is available. +""" +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") + + class decoder(object): + 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 + +""" +:var str _API: Constraint for the used API +""" +_API = "MIT" + +try: + import gssapi +except (ImportError, OSError): + try: + import sspicon + import sspi + _API = "SSPI" + except ImportError: + GSS_AUTH_AVAILABLE = False + _API = None + + +def GSSAuth(auth_method, gss_deleg_creds=True): + """ + Provide SSH2 GSS-API / SSPI authentication. + + :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 `._SSH_GSSAPI` object will be returned. + If there is no supported API available, + ``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 `._SSH_GSSAPI` and + `._SSH_SSPI`. + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + :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 + 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 str service: The desired SSH service + :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 str username: The name of the user who attempts to login + :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 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 + :rtype: Bytes + :note: In server mode we just return the OID length and the DER encoded + OID. + """ + 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 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: + return False + return True + + # Internals + #-------------------------------------------------------------------------- + def _make_uint32(self, integer): + """ + Create a 32 bit unsigned integer (The byte sequence of an integer). + + :param int integer: The integer value to convert + :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 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, + 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 += username.encode() + mic += self._make_uint32(len(service)) + mic += service.encode() + mic += self._make_uint32(len(auth_method)) + mic += auth_method.encode() + return mic + + +class _SSH_GSSAPI(_SSH_GSSAuth): + """ + Implementation of the GSS-API MIT Kerberos Authentication for SSH2. + + :see: `.GSSAuth` + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + :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) + + 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 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 ``String`` if the GSS-API has returned a token or ``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 + 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 + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + :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 ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from GSS-API with the SSH session ID as + message. + :rtype: String + :see: `._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 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 + 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 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 + if self._username is not None: + # server mode + mic_field = self._ssh_build_mic(self._session_id, + self._username, + self._service, + self._auth_method) + try: + self._gss_srv_ctxt.verify_mic(mic_field, + mic_token) + except gssapi.BadSignature: + raise Exception("GSS-API MIC check failed.") + else: + # for key exchange with gssapi-keyex + # client mode + self._gss_ctxt.verify_mic(self._session_id, + mic_token) + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + :return: ``True`` if credentials are delegated, otherwise ``False`` + :rtype: bool + """ + 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 str client_token: The GSS-API token received form the client + :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: `.GSSAuth` + """ + def __init__(self, auth_method, gss_deleg_creds): + """ + :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) + + 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 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 ``String`` if the SSPI has returned a token or ``None`` if + no token was returned + :rtype: String or None + """ + 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.") + 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 + 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 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 ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from SSPI with the SSH session ID as + message. + :rtype: String + :see: `._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 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 + """ + 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 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 + 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: ``True`` if credentials are delegated, otherwise ``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 str client_token: The SSPI token received form the client + :raise NotImplementedError: Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError diff -Nru paramiko-1.10.1/paramiko/transport.py paramiko-1.15.1/paramiko/transport.py --- paramiko-1.10.1/paramiko/transport.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/transport.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -17,40 +17,53 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{Transport} handles the core SSH2 protocol. +Core protocol implementation """ import os import socket -import string -import struct import sys import threading import time import weakref +from hashlib import md5, sha1 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.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ + cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \ + MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \ + cMSG_NEWKEYS, MSG_NEWKEYS, cMSG_REQUEST_SUCCESS, cMSG_REQUEST_FAILURE, \ + CONNECTION_FAILED_CODE, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, \ + OPEN_SUCCEEDED, cMSG_CHANNEL_OPEN_FAILURE, cMSG_CHANNEL_OPEN_SUCCESS, \ + MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE, \ + MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, \ + MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \ + MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \ + MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_PACKET_SIZE, MAX_WINDOW_SIZE, \ + DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE 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 +from paramiko.py3compat import string_types, long, byte_ord, b from paramiko.rsakey import RSAKey +from paramiko.ecdsakey import ECDSAKey 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 + ChannelException, ProxyCommandFailure) +from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value -from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 -from Crypto.Hash import SHA, MD5 try: from Crypto.Util import Counter except ImportError: @@ -59,222 +72,132 @@ # for thread cleanup _active_threads = [] + def _join_lingering_threads(): for thr in _active_threads: thr.stop_thread() + import atexit atexit.register(_join_lingering_threads) -class SecurityOptions (object): - """ - Simple object containing the security preferences of an ssh transport. - These are tuples of acceptable ciphers, digests, key types, and key - exchange algorithms, listed in order of preference. - - Changing the contents and/or order of these fields affects the underlying - L{Transport} (but only if you change them before starting the session). - If you try to add an algorithm that paramiko doesn't recognize, - C{ValueError} will be raised. If you try to assign something besides a - tuple to one of the fields, C{TypeError} will be raised. - """ - __slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', 'compression', '_transport' ] - - def __init__(self, transport): - self._transport = transport - - def __repr__(self): - """ - Returns a string representation of this object, for debugging. - - @rtype: str - """ - return '' % repr(self._transport) - - def _get_ciphers(self): - return self._transport._preferred_ciphers - - def _get_digests(self): - return self._transport._preferred_macs - - def _get_key_types(self): - return self._transport._preferred_keys - - def _get_kex(self): - return self._transport._preferred_kex - - def _get_compression(self): - return self._transport._preferred_compression - - def _set(self, name, orig, x): - if type(x) is list: - x = tuple(x) - 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) - if len(forbidden) > 0: - raise ValueError('unknown cipher') - setattr(self._transport, name, x) - - def _set_ciphers(self, x): - self._set('_preferred_ciphers', '_cipher_info', x) - - def _set_digests(self, x): - self._set('_preferred_macs', '_mac_info', x) - - def _set_key_types(self, x): - self._set('_preferred_keys', '_key_info', x) - - def _set_kex(self, x): - self._set('_preferred_kex', '_kex_info', x) - - def _set_compression(self, x): - self._set('_preferred_compression', '_compression_info', x) - - ciphers = property(_get_ciphers, _set_ciphers, None, - "Symmetric encryption ciphers") - digests = property(_get_digests, _set_digests, None, - "Digest (one-way hash) algorithms") - key_types = property(_get_key_types, _set_key_types, None, - "Public-key algorithms") - kex = property(_get_kex, _set_kex, None, "Key exchange algorithms") - compression = property(_get_compression, _set_compression, None, - "Compression algorithms") - - -class ChannelMap (object): - def __init__(self): - # (id -> Channel) - self._map = weakref.WeakValueDictionary() - self._lock = threading.Lock() - - def put(self, chanid, chan): - self._lock.acquire() - try: - self._map[chanid] = chan - finally: - self._lock.release() - - def get(self, chanid): - self._lock.acquire() - try: - return self._map.get(chanid, None) - finally: - self._lock.release() - - def delete(self, chanid): - self._lock.acquire() - try: - try: - del self._map[chanid] - except KeyError: - pass - finally: - self._lock.release() - - def values(self): - self._lock.acquire() - try: - return self._map.values() - finally: - self._lock.release() - - def __len__(self): - self._lock.acquire() - try: - return len(self._map) - finally: - self._lock.release() - - -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 - L{Channel}s, across the session. Multiple channels can be multiplexed - across a single session (and often are, in the case of port forwardings). + `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__) + _CLIENT_ID = 'paramiko_%s' % paramiko.__version__ - _preferred_ciphers = ( 'aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc', - 'arcfour128', 'arcfour256' ) - _preferred_macs = ( 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ) - _preferred_keys = ( 'ssh-rsa', 'ssh-dss' ) - _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ) - _preferred_compression = ( 'none', ) + _preferred_ciphers = ('aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', + 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256') + _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96') + _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256') + _preferred_kex = ( 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' , 'diffie-hellman-group1-sha1') + _preferred_compression = ('none',) _cipher_info = { - 'aes128-ctr': { 'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16 }, - 'aes256-ctr': { 'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32 }, - 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 }, - 'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 }, - 'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 }, - '3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 }, - 'arcfour128': { 'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16 }, - 'arcfour256': { 'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32 }, - } + 'aes128-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16}, + 'aes256-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32}, + 'blowfish-cbc': {'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16}, + 'aes128-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16}, + 'aes256-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32}, + '3des-cbc': {'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24}, + 'arcfour128': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16}, + 'arcfour256': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32}, + } _mac_info = { - 'hmac-sha1': { 'class': SHA, 'size': 20 }, - 'hmac-sha1-96': { 'class': SHA, 'size': 12 }, - 'hmac-md5': { 'class': MD5, 'size': 16 }, - 'hmac-md5-96': { 'class': MD5, 'size': 12 }, - } + 'hmac-sha1': {'class': sha1, 'size': 20}, + 'hmac-sha1-96': {'class': sha1, 'size': 12}, + 'hmac-md5': {'class': md5, 'size': 16}, + 'hmac-md5-96': {'class': md5, 'size': 12}, + } _key_info = { 'ssh-rsa': RSAKey, 'ssh-dss': DSSKey, - } + 'ecdsa-sha2-nistp256': ECDSAKey, + } _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 = { # zlib@openssh.com is just zlib, but only turned on after a successful # authentication. openssh servers may only offer this type because # they've had troubles with security holes in zlib in the past. - 'zlib@openssh.com': ( ZlibCompressor, ZlibDecompressor ), - 'zlib': ( ZlibCompressor, ZlibDecompressor ), - 'none': ( None, None ), + 'zlib@openssh.com': (ZlibCompressor, ZlibDecompressor), + 'zlib': (ZlibCompressor, ZlibDecompressor), + 'none': (None, None), } - _modulus_pack = None - def __init__(self, sock): + def __init__(self, + sock, + default_window_size=DEFAULT_WINDOW_SIZE, + default_max_packet_size=DEFAULT_MAX_PACKET_SIZE, + 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 - SSH session yet. Use L{connect} or L{start_client} to begin a client - session, or L{start_server} to begin a server session. + object. This only creates the `.Transport` object; it doesn't begin the + SSH session yet. Use `connect` or `start_client` to begin a client + session, or `start_server` to begin a server session. If the object is not actually a socket, it must have the following methods: - - C{send(str)}: Writes from 1 to C{len(str)} bytes, and - returns an int representing the number of bytes written. Returns - 0 or raises C{EOFError} if the stream has been closed. - - C{recv(int)}: Reads from 1 to C{int} bytes and returns them as a - string. Returns 0 or raises C{EOFError} if the stream has been - closed. - - C{close()}: Closes the socket. - - C{settimeout(n)}: Sets a (float) timeout on I/O operations. - For ease of use, you may also pass in an address (as a tuple) or a host - string as the C{sock} argument. (A host string is a hostname with an - optional port (separated by C{":"}) which will be converted into a - tuple of C{(hostname, port)}.) A socket will be connected to this - address and used for communication. Exceptions from the C{socket} call - may be thrown in this case. + - ``send(str)``: Writes from 1 to ``len(str)`` bytes, and returns an + int representing the number of bytes written. Returns + 0 or raises ``EOFError`` if the stream has been closed. + - ``recv(int)``: Reads from 1 to ``int`` bytes and returns them as a + string. Returns 0 or raises ``EOFError`` if the stream has been + closed. + - ``close()``: Closes the socket. + - ``settimeout(n)``: Sets a (float) timeout on I/O operations. - @param sock: a socket or socket-like object to create the session over. - @type sock: socket + For ease of use, you may also pass in an address (as a tuple) or a host + string as the ``sock`` argument. (A host string is a hostname with an + optional port (separated by ``":"``) which will be converted into a + tuple of ``(hostname, port)``.) A socket will be connected to this + 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: + 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) + + .. versionchanged:: 1.15 + Added the ``default_window_size`` and ``default_max_packet_size`` + arguments. """ - if isinstance(sock, (str, unicode)): + self.active = False + + if isinstance(sock, string_types): # convert "host:port" into (host, port) hl = sock.split(':', 1) if len(hl) == 1: @@ -292,7 +215,7 @@ 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 @@ -302,7 +225,6 @@ # okay, normal socket-ish flow here... threading.Thread.__init__(self) self.setDaemon(True) - self.rng = rng self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB try: @@ -325,12 +247,26 @@ 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 self.use_gss_kex: + self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds) + self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group1-sha1') + # state used during negotiation self.kex_engine = None self.H = None self.K = None - self.active = False self.initial_kex_done = False self.in_kex = False self.authenticated = False @@ -339,11 +275,11 @@ # tracking open channels self._channels = ChannelMap() - self.channel_events = { } # (id -> Event) - self.channels_seen = { } # (id -> True) + self.channel_events = {} # (id -> Event) + self.channels_seen = {} # (id -> True) self._channel_counter = 1 - self.window_size = 65536 - self.max_packet_size = 34816 + 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 @@ -363,18 +299,16 @@ # server mode: self.server_mode = False self.server_object = None - self.server_key_dict = { } - self.server_accepts = [ ] + self.server_key_dict = {} + self.server_accepts = [] self.server_accept_cv = threading.Condition(self.lock) - self.subsystem_table = { } + self.subsystem_table = {} def __repr__(self): """ Returns a string representation of this object, for debugging. - - @rtype: str """ - out = '} or - L{auth_publickey }. + calling `auth_password ` or + `auth_publickey `. - @note: L{connect} is a simpler method for connecting as a client. + .. note:: `connect` is a simpler method for connecting as a client. - @note: After calling this method (or L{start_server} or L{connect}), - you should no longer directly read from or write to the original - socket object. + .. 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 event: an event to trigger when negotiation is complete - (optional) - @type event: threading.Event + :param .threading.Event event: + an event to trigger when negotiation is complete (optional) - @raise SSHException: if negotiation fails (and no C{event} was passed + :raises SSHException: if negotiation fails (and no ``event`` was passed in) """ self.active = True @@ -456,7 +398,6 @@ # synchronous, wait for a result self.completion_event = event = threading.Event() self.start() - Random.atfork() while True: event.wait(0.1) if not self.active: @@ -470,41 +411,42 @@ def start_server(self, event=None, server=None): """ Negotiate a new SSH2 session as a server. This is the first step after - creating a new L{Transport} and setting up your server host key(s). A + creating a new `.Transport` and setting up your server host key(s). A separate thread is created for protocol negotiation. If an event is passed in, this method returns immediately. When - negotiation is done (successful or not), the given C{Event} will - be triggered. On failure, L{is_active} will return C{False}. + negotiation is done (successful or not), the given ``Event`` will + be triggered. On failure, `is_active` will return ``False``. - (Since 1.4) If C{event} is C{None}, this method will not return until + (Since 1.4) If ``event`` is ``None``, this method will not return until negotation is done. On success, the method returns normally. Otherwise an SSHException is raised. After a successful negotiation, the client will need to authenticate. - Override the methods - L{get_allowed_auths }, - L{check_auth_none }, - L{check_auth_password }, and - L{check_auth_publickey } in the - given C{server} object to control the authentication process. - - After a successful authentication, the client should request to open - a channel. Override - L{check_channel_request } in the - given C{server} object to allow channels to be opened. - - @note: After calling this method (or L{start_client} or L{connect}), - you should no longer directly read from or write to the original - socket object. - - @param event: an event to trigger when negotiation is complete. - @type event: threading.Event - @param server: an object used to perform authentication and create - L{Channel}s. - @type server: L{server.ServerInterface} + Override the methods `get_allowed_auths + <.ServerInterface.get_allowed_auths>`, `check_auth_none + <.ServerInterface.check_auth_none>`, `check_auth_password + <.ServerInterface.check_auth_password>`, and `check_auth_publickey + <.ServerInterface.check_auth_publickey>` in the given ``server`` object + to control the authentication process. + + After a successful authentication, the client should request to open a + channel. Override `check_channel_request + <.ServerInterface.check_channel_request>` in the given ``server`` + object to allow channels to be opened. + + .. note:: + After calling this method (or `start_client` 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. + :param .ServerInterface server: + an object used to perform authentication and create `channels + <.Channel>` - @raise SSHException: if negotiation fails (and no C{event} was passed + :raises SSHException: if negotiation fails (and no ``event`` was passed in) """ if server is None: @@ -540,9 +482,8 @@ key info, not just the public half. Only one key of each type (RSA or DSS) is kept. - @param key: the host key to add, usually an L{RSAKey } or - L{DSSKey }. - @type key: L{PKey } + :param .PKey key: + the host key to add, usually an `.RSAKey` or `.DSSKey`. """ self.server_key_dict[key.get_name()] = key @@ -550,15 +491,16 @@ """ Return the active host key, in server mode. After negotiating with the client, this method will return the negotiated host key. If only one - type of host key was set with L{add_server_key}, that's the only key + type of host key was set with `add_server_key`, that's the only key that will ever be returned. But in cases where you have set more than one type of host key (for example, an RSA key and a DSS key), the key type will be negotiated by the client, and this method will return the key of the type agreed on. If the host key has not been negotiated - yet, C{None} is returned. In client mode, the behavior is undefined. + yet, ``None`` is returned. In client mode, the behavior is undefined. - @return: host key of the type negotiated by the client, or C{None}. - @rtype: L{PKey } + :return: + host key (`.PKey`) of the type negotiated by the client, or + ``None``. """ try: return self.server_key_dict[self.host_key_type] @@ -568,7 +510,7 @@ def load_server_moduli(filename=None): """ - I{(optional)} + (optional) Load a file of prime moduli for use in doing group-exchange key negotiation in server mode. It's a rather obscure option and can be safely ignored. @@ -577,24 +519,23 @@ negotiation, which asks the server to send a random prime number that fits certain criteria. These primes are pretty difficult to compute, so they can't be generated on demand. But many systems contain a file - of suitable primes (usually named something like C{/etc/ssh/moduli}). - If you call C{load_server_moduli} and it returns C{True}, then this + of suitable primes (usually named something like ``/etc/ssh/moduli``). + If you call `load_server_moduli` and it returns ``True``, then this file of primes has been loaded and we will support "group-exchange" in server mode. Otherwise server mode will just claim that it doesn't support that method of key negotiation. - @param filename: optional path to the moduli file, if you happen to - know that it's not in a standard location. - @type filename: str - @return: True if a moduli file was successfully loaded; False - otherwise. - @rtype: bool + :param str filename: + optional path to the moduli file, if you happen to know that it's + not in a standard location. + :return: + True if a moduli file was successfully loaded; False otherwise. - @note: This has no effect when used in client mode. + .. note:: This has no effect when used in client mode. """ - Transport._modulus_pack = ModulusPack(rng) + Transport._modulus_pack = ModulusPack() # places to look for the openssh "moduli" file - file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ] + file_list = ['/etc/ssh/moduli', '/usr/local/etc/moduli'] if filename is not None: file_list.insert(0, filename) for fn in file_list: @@ -614,25 +555,23 @@ """ if not self.active: return - self.active = False - self.packetizer.close() - self.join() - for chan in self._channels.values(): + self.stop_thread() + for chan in list(self._channels.values()): chan._unlink() + self.sock.close() def get_remote_server_key(self): """ Return the host key of the server (in client mode). - @note: Previously this call returned a tuple of (key type, key string). - You can get the same effect by calling - L{PKey.get_name } for the key type, and - C{str(key)} for the key string. + .. note:: + Previously this call returned a tuple of ``(key type, key + string)``. You can get the same effect by calling `.PKey.get_name` + for the key type, and ``str(key)`` for the key string. - @raise SSHException: if no session is currently active. + :raises SSHException: if no session is currently active. - @return: public key of the remote server - @rtype: L{PKey } + :return: public key (`.PKey`) of the remote server """ if (not self.active) or (not self.initial_kex_done): raise SSHException('No existing session') @@ -642,37 +581,50 @@ """ Return true if this session is active (open). - @return: True if the session is still active (open); False if the - session is closed - @rtype: bool + :return: + True if the session is still active (open); False if the session is + closed """ 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 C{"session"}. This - is just an alias for C{open_channel('session')}. + 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 L{Channel} - @rtype: L{Channel} + :return: a new `.Channel` - @raise SSHException: if the request is rejected or the session ends + :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') + return self.open_channel('session', + window_size=window_size, + max_packet_size=max_packet_size) def open_x11_channel(self, src_addr=None): """ - Request a new channel to the client, of type C{"x11"}. This - is just an alias for C{open_channel('x11', src_addr=src_addr)}. + Request a new channel to the client, of type ``"x11"``. This + is just an alias for ``open_channel('x11', src_addr=src_addr)``. - @param src_addr: the source address of the x11 server (port is the + :param tuple src_addr: + the source address (``(str, int)``) of the x11 server (port is the x11 port, ie. 6010) - @type src_addr: (str, int) - @return: a new L{Channel} - @rtype: L{Channel} + :return: a new `.Channel` - @raise SSHException: if the request is rejected or the session ends + :raises SSHException: if the request is rejected or the session ends prematurely """ return self.open_channel('x11', src_addr=src_addr) @@ -680,64 +632,79 @@ def open_forward_agent_channel(self): """ Request a new channel to the client, of type - C{"auth-agent@openssh.com"}. + ``"auth-agent@openssh.com"``. - This is just an alias for C{open_channel('auth-agent@openssh.com')}. - @return: a new L{Channel} - @rtype: L{Channel} + This is just an alias for ``open_channel('auth-agent@openssh.com')``. - @raise SSHException: if the request is rejected or the session ends - prematurely + :return: a new `.Channel` + + :raises SSHException: + if the request is rejected or the session ends prematurely """ return self.open_channel('auth-agent@openssh.com') - def open_forwarded_tcpip_channel(self, (src_addr, src_port), (dest_addr, dest_port)): + def open_forwarded_tcpip_channel(self, src_addr, dest_addr): """ - Request a new channel back to the client, of type C{"forwarded-tcpip"}. + Request a new channel back to the client, of type ``"forwarded-tcpip"``. This is used after a client has requested port forwarding, for sending incoming connections back to the client. - @param src_addr: originator's address - @param src_port: originator's port - @param dest_addr: local (server) connected address - @param dest_port: local (server) connected port - """ - return self.open_channel('forwarded-tcpip', (dest_addr, dest_port), (src_addr, src_port)) - - def open_channel(self, kind, dest_addr=None, src_addr=None): - """ - Request a new channel to the server. L{Channel}s are socket-like - objects used for the actual transfer of data across the session. - You may only request a channel after negotiating encryption (using - L{connect} or L{start_client}) and authenticating. - - @param kind: the kind of channel requested (usually C{"session"}, - C{"forwarded-tcpip"}, C{"direct-tcpip"}, or C{"x11"}) - @type kind: str - @param dest_addr: the destination address of this port forwarding, - if C{kind} is C{"forwarded-tcpip"} or C{"direct-tcpip"} (ignored - for other channel types) - @type dest_addr: (str, int) - @param src_addr: the source address of this port forwarding, if - C{kind} is C{"forwarded-tcpip"}, C{"direct-tcpip"}, or C{"x11"} - @type src_addr: (str, int) - @return: a new L{Channel} on success - @rtype: L{Channel} + :param src_addr: originator's address + :param dest_addr: local (server) connected address + """ + return self.open_channel('forwarded-tcpip', dest_addr, src_addr) + + 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"``) + :param tuple dest_addr: + the destination address (address + port tuple) of this port + forwarding, if ``kind`` is ``"forwarded-tcpip"`` or + ``"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. - @raise SSHException: if the request is rejected or the session ends + :return: a new `.Channel` on success + + :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') self.lock.acquire() try: + 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(chr(MSG_CHANNEL_OPEN)) + 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(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]) @@ -751,12 +718,12 @@ 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(window_size, max_packet_size) finally: self.lock.release() self._send_user_message(m) while True: - event.wait(0.1); + event.wait(0.1) if not self.active: e = self.get_exception() if e is None: @@ -782,28 +749,26 @@ handler(channel, (origin_addr, origin_port), (server_addr, server_port)) - where C{server_addr} and C{server_port} are the address and port that + where ``server_addr`` and ``server_port`` are the address and port that the server was listening on. If no handler is set, the default behavior is to send new incoming forwarded connections into the accept queue, to be picked up via - L{accept}. + `accept`. - @param address: the address to bind when forwarding - @type address: str - @param port: the port to forward, or 0 to ask the server to allocate - any port - @type port: int - @param handler: optional handler for incoming forwarded connections - @type handler: function(Channel, (str, int), (str, int)) - @return: the port # allocated by the server - @rtype: int + :param str address: the address to bind when forwarding + :param int port: + the port to forward, or 0 to ask the server to allocate any port + :param callable handler: + optional handler for incoming forwarded connections, of the form + ``func(Channel, (str, int), (str, int))``. - @raise SSHException: if the server refused the TCP forward request + :return: the port number (`int`) allocated by the server + + :raises SSHException: if the server refused the TCP forward request """ if not self.active: raise SSHException('SSH session not active') - address = str(address) port = int(port) response = self.global_request('tcpip-forward', (address, port), wait=True) if response is None: @@ -811,7 +776,9 @@ if port == 0: port = response.get_int() if handler is None: - def default_handler(channel, (src_addr, src_port), (dest_addr, dest_port)): + def default_handler(channel, src_addr, dest_addr_port): + #src_addr, src_port = src_addr_port + #dest_addr, dest_port = dest_addr_port self._queue_incoming_channel(channel) handler = default_handler self._tcp_handler = handler @@ -823,10 +790,8 @@ connections to the given address & port will be forwarded across this ssh connection. - @param address: the address to stop forwarding - @type address: str - @param port: the port to stop forwarding - @type port: int + :param str address: the address to stop forwarding + :param int port: the port to stop forwarding """ if not self.active: return @@ -835,32 +800,32 @@ def open_sftp_client(self): """ - Create an SFTP client channel from an open transport. On success, - an SFTP session will be opened with the remote host, and a new - SFTPClient object will be returned. - - @return: a new L{SFTPClient} object, referring to an sftp session - (channel) across this transport - @rtype: L{SFTPClient} + Create an SFTP client channel from an open transport. On success, an + SFTP session will be opened with the remote host, and a new + `.SFTPClient` object will be returned. + + :return: + a new `.SFTPClient` referring to an sftp session (channel) across + this transport """ return SFTPClient.from_transport(self) - def send_ignore(self, bytes=None): + def send_ignore(self, byte_count=None): """ Send a junk packet across the encrypted link. This is sometimes used to add "noise" to a connection to confuse would-be attackers. It can also be used as a keep-alive for long lived connections traversing firewalls. - @param bytes: the number of random bytes to send in the payload of the - ignored packet -- defaults to a random number from 10 to 41. - @type bytes: int + :param int byte_count: + the number of random bytes to send in the payload of the ignored + packet -- defaults to a random number from 10 to 41. """ m = Message() - m.add_byte(chr(MSG_IGNORE)) - if bytes is None: - bytes = (ord(rng.read(1)) % 32) + 10 - m.add_bytes(rng.read(bytes)) + m.add_byte(cMSG_IGNORE) + if byte_count is None: + byte_count = (byte_ord(os.urandom(1)) % 32) + 10 + m.add_bytes(os.urandom(byte_count)) self._send_user_message(m) def renegotiate_keys(self): @@ -872,7 +837,7 @@ traffic both ways as the two sides swap keys and do computations. This method returns when the session has switched to new keys. - @raise SSHException: if the key renegotiation failed (which causes the + :raises SSHException: if the key renegotiation failed (which causes the session to end) """ self.completion_event = threading.Event() @@ -891,39 +856,38 @@ def set_keepalive(self, interval): """ Turn on/off keepalive packets (default is off). If this is set, after - C{interval} seconds without sending any data over the connection, a + ``interval`` seconds without sending any data over the connection, a "keepalive" packet will be sent (and ignored by the remote host). This can be useful to keep connections alive over a NAT, for example. - @param interval: seconds to wait before sending a keepalive packet (or + :param int interval: + seconds to wait before sending a keepalive packet (or 0 to disable keepalives). - @type interval: int """ self.packetizer.set_keepalive(interval, - lambda x=weakref.proxy(self): x.global_request('keepalive@lag.net', wait=False)) + lambda x=weakref.proxy(self): x.global_request('keepalive@lag.net', wait=False)) def global_request(self, kind, data=None, wait=True): """ Make a global request to the remote host. These are normally extensions to the SSH2 protocol. - @param kind: name of the request. - @type kind: str - @param data: an optional tuple containing additional data to attach - to the request. - @type data: tuple - @param wait: C{True} if this method should not return until a response - is received; C{False} otherwise. - @type wait: bool - @return: a L{Message} containing possible additional data if the - request was successful (or an empty L{Message} if C{wait} was - C{False}); C{None} if the request was denied. - @rtype: L{Message} + :param str kind: name of the request. + :param tuple data: + an optional tuple containing additional data to attach to the + request. + :param bool wait: + ``True`` if this method should not return until a response is + received; ``False`` otherwise. + :return: + a `.Message` containing possible additional data if the request was + successful (or an empty `.Message` if ``wait`` was ``False``); + ``None`` if the request was denied. """ if wait: self.completion_event = threading.Event() m = Message() - m.add_byte(chr(MSG_GLOBAL_REQUEST)) + m.add_byte(cMSG_GLOBAL_REQUEST) m.add_string(kind) m.add_boolean(wait) if data is not None: @@ -943,14 +907,12 @@ def accept(self, timeout=None): """ Return the next channel opened by the client over this transport, in - server mode. If no channel is opened before the given timeout, C{None} + server mode. If no channel is opened before the given timeout, ``None`` is returned. - @param timeout: seconds to wait for a channel, or C{None} to wait - forever - @type timeout: int - @return: a new Channel opened by the client - @rtype: L{Channel} + :param int timeout: + seconds to wait for a channel, or ``None`` to wait forever + :return: a new `.Channel` opened by the client """ self.lock.acquire() try: @@ -967,62 +929,79 @@ 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 - for L{start_client}, L{get_remote_server_key}, and - L{Transport.auth_password} or L{Transport.auth_publickey}. Use those + for `start_client`, `get_remote_server_key`, and + `Transport.auth_password` or `Transport.auth_publickey`. Use those methods if you want more control. You can use this method immediately after creating a Transport to negotiate encryption with a server. If it fails, an exception will be thrown. On success, the method will return cleanly, and an encrypted - session exists. You may immediately call L{open_channel} or - L{open_session} to get a L{Channel} object, which is used for data + session exists. You may immediately call `open_channel` or + `open_session` to get a `.Channel` object, which is used for data transfer. - @note: If you fail to supply a password or private key, this method may - succeed, but a subsequent L{open_channel} or L{open_session} call may - fail because you haven't authenticated yet. - - @param hostkey: the host key expected from the server, or C{None} if - you don't want to do host key verification. - @type hostkey: L{PKey} - @param username: the username to authenticate as. - @type username: str - @param password: a password to use for authentication, if you want to - use password authentication; otherwise C{None}. - @type password: str - @param pkey: a private key to use for authentication, if you want to - use private key authentication; otherwise C{None}. - @type pkey: L{PKey} + .. note:: + If you fail to supply a password or private key, this method may + succeed, but a subsequent `open_channel` or `open_session` call may + fail because you haven't authenticated yet. + + :param .PKey hostkey: + the host key expected from the server, or ``None`` if you don't + want to do host key verification. + :param str username: the username to authenticate as. + :param str password: + a password to use for authentication, if you want to use password + authentication; otherwise ``None``. + :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 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. - @raise SSHException: if the SSH2 negotiation fails, the host key + :raises SSHException: if the SSH2 negotiation fails, the host key supplied by the server is incorrect, or authentication fails. """ if hostkey is not None: - self._preferred_keys = [ hostkey.get_name() ] + self._preferred_keys = [hostkey.get_name()] 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 (str(key) != str(hostkey)): + if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()): self._log(DEBUG, 'Bad host key from server') - self._log(DEBUG, 'Expected: %s: %s' % (hostkey.get_name(), repr(str(hostkey)))) - self._log(DEBUG, 'Got : %s: %s' % (key.get_name(), repr(str(key)))) + self._log(DEBUG, 'Expected: %s: %s' % (hostkey.get_name(), repr(hostkey.asbytes()))) + self._log(DEBUG, 'Got : %s: %s' % (key.get_name(), repr(key.asbytes()))) 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 password is not None: - self._log(DEBUG, 'Attempting password auth...') - self.auth_password(username, password) - else: + if (pkey is not None) or (password is not None) or gss_auth or gss_kex: + if gss_auth: + self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)') + self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds) + 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 @@ -1030,13 +1009,13 @@ """ Return any exception that happened during the last server request. This can be used to fetch more specific error information after using - calls like L{start_client}. The exception (if any) is cleared after + calls like `start_client`. The exception (if any) is cleared after this call. - @return: an exception, or C{None} if there is no stored exception. - @rtype: Exception + :return: + an exception, or ``None`` if there is no stored exception. - @since: 1.1 + .. versionadded:: 1.1 """ self.lock.acquire() try: @@ -1050,17 +1029,15 @@ """ Set the handler class for a subsystem in server mode. If a request for this subsystem is made on an open ssh channel later, this handler - will be constructed and called -- see L{SubsystemHandler} for more + will be constructed and called -- see `.SubsystemHandler` for more detailed documentation. Any extra parameters (including keyword arguments) are saved and - passed to the L{SubsystemHandler} constructor later. + passed to the `.SubsystemHandler` constructor later. - @param name: name of the subsystem. - @type name: str - @param handler: subclass of L{SubsystemHandler} that handles this - subsystem. - @type handler: class + :param str name: name of the subsystem. + :param class handler: + subclass of `.SubsystemHandler` that handles this subsystem. """ try: self.lock.acquire() @@ -1072,10 +1049,10 @@ """ Return true if this session is active and authenticated. - @return: True if the session is still open and has been authenticated + :return: + True if the session is still open and has been authenticated successfully; False if authentication failed and/or the session is closed. - @rtype: bool """ return self.active and (self.auth_handler is not None) and self.auth_handler.is_authenticated() @@ -1083,34 +1060,43 @@ """ Return the username this connection is authenticated for. If the session is not authenticated (or authentication failed), this method - returns C{None}. + returns ``None``. - @return: username that was authenticated, or C{None}. - @rtype: string + :return: username that was authenticated (a `str`), or ``None``. """ if not self.active or (self.auth_handler is None): return None return self.auth_handler.get_username() + def get_banner(self): + """ + Return the banner supplied by the server upon connect. If no banner is + supplied, this method returns ``None``. + + :returns: server supplied banner (`str`), or ``None``. + """ + if not self.active or (self.auth_handler is None): + return None + return self.auth_handler.banner + def auth_none(self, username): """ Try to authenticate to the server using no authentication at all. This will almost always fail. It may be useful for determining the list of authentication types supported by the server, by catching the - L{BadAuthenticationType} exception raised. + `.BadAuthenticationType` exception raised. - @param username: the username to authenticate as - @type username: string - @return: list of auth types permissible for the next stage of + :param str username: the username to authenticate as + :return: + `list` of auth types permissible for the next stage of authentication (normally empty) - @rtype: list - @raise BadAuthenticationType: if "none" authentication isn't allowed + :raises BadAuthenticationType: if "none" authentication isn't allowed by the server for this user - @raise SSHException: if the authentication failed due to a network + :raises SSHException: if the authentication failed due to a network error - @since: 1.5 + .. versionadded:: 1.5 """ if (not self.active) or (not self.initial_kex_done): raise SSHException('No existing session') @@ -1124,16 +1110,16 @@ Authenticate to the server using a password. The username and password are sent over an encrypted link. - If an C{event} is passed in, this method will return immediately, and + If an ``event`` is passed in, this method will return immediately, and the event will be triggered once authentication succeeds or fails. On - success, L{is_authenticated} will return C{True}. On failure, you may - use L{get_exception} to get more detailed error information. + success, `is_authenticated` will return ``True``. On failure, you may + use `get_exception` to get more detailed error information. Since 1.1, if no event is passed, this method will block until the authentication succeeds or fails. On failure, an exception is raised. Otherwise, the method simply returns. - Since 1.5, if no event is passed and C{fallback} is C{True} (the + Since 1.5, if no event is passed and ``fallback`` is ``True`` (the default), if the server doesn't support plain password authentication but does support so-called "keyboard-interactive" mode, an attempt will be made to authenticate using this interactive mode. If it fails, @@ -1146,26 +1132,23 @@ this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. - @param username: the username to authenticate as - @type username: str - @param password: the password to authenticate with - @type password: str or unicode - @param event: an event to trigger when the authentication attempt is - complete (whether it was successful or not) - @type event: threading.Event - @param fallback: C{True} if an attempt at an automated "interactive" - password auth should be made if the server doesn't support normal - password auth - @type fallback: bool - @return: list of auth types permissible for the next stage of + :param str username: the username to authenticate as + :param basestring password: the password to authenticate with + :param .threading.Event event: + an event to trigger when the authentication attempt is complete + (whether it was successful or not) + :param bool fallback: + ``True`` if an attempt at an automated "interactive" password auth + should be made if the server doesn't support normal password auth + :return: + `list` of auth types permissible for the next stage of authentication (normally empty) - @rtype: list - @raise BadAuthenticationType: if password authentication isn't + :raises BadAuthenticationType: if password authentication isn't allowed by the server for this user (and no event was passed in) - @raise AuthenticationException: if the authentication failed (and no + :raises AuthenticationException: if the authentication failed (and no event was passed in) - @raise SSHException: if there was a network error + :raises SSHException: if there was a network error """ if (not self.active) or (not self.initial_kex_done): # we should never try to send the password unless we're on a secure link @@ -1181,9 +1164,9 @@ return [] try: return self.auth_handler.wait_for_response(my_event) - except BadAuthenticationType, x: + except BadAuthenticationType as e: # if password auth isn't allowed, but keyboard-interactive *is*, try to fudge it - if not fallback or ('keyboard-interactive' not in x.allowed_types): + if not fallback or ('keyboard-interactive' not in e.allowed_types): raise try: def handler(title, instructions, fields): @@ -1195,22 +1178,21 @@ # to try to fake out automated scripting of the exact # type we're doing here. *shrug* :) return [] - return [ password ] + return [password] return self.auth_interactive(username, handler) - except SSHException, ignored: + except SSHException: # attempt failed; just raise the original exception - raise x - return None + raise e def auth_publickey(self, username, key, event=None): """ Authenticate to the server using a private key. The key is used to sign data from the server, so it must include the private part. - If an C{event} is passed in, this method will return immediately, and + If an ``event`` is passed in, this method will return immediately, and the event will be triggered once authentication succeeds or fails. On - success, L{is_authenticated} will return C{True}. On failure, you may - use L{get_exception} to get more detailed error information. + success, `is_authenticated` will return ``True``. On failure, you may + use `get_exception` to get more detailed error information. Since 1.1, if no event is passed, this method will block until the authentication succeeds or fails. On failure, an exception is raised. @@ -1220,22 +1202,20 @@ this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. - @param username: the username to authenticate as - @type username: string - @param key: the private key to authenticate with - @type key: L{PKey } - @param event: an event to trigger when the authentication attempt is - complete (whether it was successful or not) - @type event: threading.Event - @return: list of auth types permissible for the next stage of + :param str username: the username to authenticate as + :param .PKey key: the private key to authenticate with + :param .threading.Event event: + an event to trigger when the authentication attempt is complete + (whether it was successful or not) + :return: + `list` of auth types permissible for the next stage of authentication (normally empty) - @rtype: list - @raise BadAuthenticationType: if public-key authentication isn't + :raises BadAuthenticationType: if public-key authentication isn't allowed by the server for this user (and no event was passed in) - @raise AuthenticationException: if the authentication failed (and no + :raises AuthenticationException: if the authentication failed (and no event was passed in) - @raise SSHException: if there was a network error + :raises 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 @@ -1263,15 +1243,15 @@ if the server continues to ask questions. The handler is expected to be a callable that will handle calls of the - form: C{handler(title, instructions, prompt_list)}. The C{title} is - meant to be a dialog-window title, and the C{instructions} are user - instructions (both are strings). C{prompt_list} will be a list of - prompts, each prompt being a tuple of C{(str, bool)}. The string is + form: ``handler(title, instructions, prompt_list)``. The ``title`` is + meant to be a dialog-window title, and the ``instructions`` are user + instructions (both are strings). ``prompt_list`` will be a list of + prompts, each prompt being a tuple of ``(str, bool)``. The string is the prompt and the boolean indicates whether the user text should be echoed. A sample call would thus be: - C{handler('title', 'instructions', [('Password:', False)])}. + ``handler('title', 'instructions', [('Password:', False)])``. The handler should return a list or tuple of answers to the server's questions. @@ -1280,22 +1260,19 @@ this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. - @param username: the username to authenticate as - @type username: string - @param handler: a handler for responding to server questions - @type handler: callable - @param submethods: a string list of desired submethods (optional) - @type submethods: str - @return: list of auth types permissible for the next stage of + :param str username: the username to authenticate as + :param callable handler: a handler for responding to server questions + :param str submethods: a string list of desired submethods (optional) + :return: + `list` of auth types permissible for the next stage of authentication (normally empty). - @rtype: list - @raise BadAuthenticationType: if public-key authentication isn't + :raises BadAuthenticationType: if public-key authentication isn't allowed by the server for this user - @raise AuthenticationException: if the authentication failed - @raise SSHException: if there was a network error + :raises AuthenticationException: if the authentication failed + :raises SSHException: if there was a network error - @since: 1.5 + .. versionadded:: 1.5 """ if (not self.active) or (not self.initial_kex_done): # we should never try to authenticate unless we're on a secure link @@ -1305,17 +1282,65 @@ 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 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 + 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 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 + 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 - C{"paramiko.transport"} but it can be set to anything you want. - (See the C{logging} module for more info.) SSH Channels will log - to a sub-channel of the one specified. + ``"paramiko.transport"`` but it can be set to anything you want. (See + the `.logging` module for more info.) SSH Channels will log to a + sub-channel of the one specified. - @param name: new channel name for logging - @type name: str + :param str name: new channel name for logging - @since: 1.1 + .. versionadded:: 1.1 """ self.log_name = name self.logger = util.get_logger(name) @@ -1325,10 +1350,9 @@ """ Return the channel name used for this transport's logging. - @return: channel name. - @rtype: str + :return: channel name as a `str` - @since: 1.2 + .. versionadded:: 1.2 """ return self.log_name @@ -1338,64 +1362,64 @@ the logs. Normally you would want this off (which is the default), but if you are debugging something, it may be useful. - @param hexdump: C{True} to log protocol traffix (in hex) to the log; - C{False} otherwise. - @type hexdump: bool + :param bool hexdump: + ``True`` to log protocol traffix (in hex) to the log; ``False`` + otherwise. """ self.packetizer.set_hexdump(hexdump) def get_hexdump(self): """ - Return C{True} if the transport is currently logging hex dumps of + Return ``True`` if the transport is currently logging hex dumps of protocol traffic. - @return: C{True} if hex dumps are being logged - @rtype: bool + :return: ``True`` if hex dumps are being logged, else ``False``. - @since: 1.4 + .. versionadded:: 1.4 """ return self.packetizer.get_hexdump() def use_compression(self, compress=True): """ Turn on/off compression. This will only have an affect before starting - the transport (ie before calling L{connect}, etc). By default, + the transport (ie before calling `connect`, etc). By default, compression is off since it negatively affects interactive sessions. - @param compress: C{True} to ask the remote client/server to compress - traffic; C{False} to refuse compression - @type compress: bool + :param bool compress: + ``True`` to ask the remote client/server to compress traffic; + ``False`` to refuse compression - @since: 1.5.2 + .. versionadded:: 1.5.2 """ if compress: - self._preferred_compression = ( 'zlib@openssh.com', 'zlib', 'none' ) + self._preferred_compression = ('zlib@openssh.com', 'zlib', 'none') else: - self._preferred_compression = ( 'none', ) + self._preferred_compression = ('none',) def getpeername(self): """ Return the address of the remote side of this Transport, if possible. - This is effectively a wrapper around C{'getpeername'} on the underlying - socket. If the socket-like object has no C{'getpeername'} method, - then C{("unknown", 0)} is returned. - - @return: the address if the remote host, if known - @rtype: tuple(str, int) + This is effectively a wrapper around ``'getpeername'`` on the underlying + socket. If the socket-like object has no ``'getpeername'`` method, + then ``("unknown", 0)`` is returned. + + :return: + the address of the remote host, if known, as a ``(str, int)`` + tuple. """ gp = getattr(self.sock, 'getpeername', None) if gp is None: - return ('unknown', 0) + return 'unknown', 0 return gp() def stop_thread(self): self.active = False self.packetizer.close() - + while self.is_alive() and (self is not threading.current_thread()): + self.join(10) ### internals... - def _log(self, level, msg, *args): if issubclass(type(msg), list): for m in msg: @@ -1404,11 +1428,11 @@ self.logger.log(level, msg, *args) def _get_modulus_pack(self): - "used by KexGex to find primes for group exchange" + """used by KexGex to find primes for group exchange""" return self._modulus_pack def _next_channel(self): - "you are holding the lock" + """you are holding the lock""" chanid = self._channel_counter while self._channels.get(chanid) is not None: self._channel_counter = (self._channel_counter + 1) & 0xffffff @@ -1417,7 +1441,7 @@ return chanid def _unlink_channel(self, chanid): - "used by a Channel to remove itself from the active channel list" + """used by a Channel to remove itself from the active channel list""" self._channels.delete(chanid) def _send_message(self, data): @@ -1439,21 +1463,21 @@ break self.clear_to_send_lock.release() if time.time() > start + self.clear_to_send_timeout: - raise SSHException('Key-exchange timed out waiting for key negotiation') + raise SSHException('Key-exchange timed out waiting for key negotiation') try: self._send_message(data) finally: self.clear_to_send_lock.release() def _set_K_H(self, k, h): - "used by a kex object to set the K (root key) and H (exchange hash)" + """used by a kex object to set the K (root key) and H (exchange hash)""" self.K = k self.H = h - if self.session_id == None: + if self.session_id is None: self.session_id = h def _expect_packet(self, *ptypes): - "used by a kex object to register the next packet type it expects to see" + """used by a kex object to register the next packet type it expects to see""" self._expected_packet = tuple(ptypes) def _verify_key(self, host_key, sig): @@ -1465,19 +1489,19 @@ self.host_key = key def _compute_key(self, id, nbytes): - "id is 'A' - 'F' for the various keys used by ssh" + """id is 'A' - 'F' for the various keys used by ssh""" m = Message() m.add_mpint(self.K) m.add_bytes(self.H) - m.add_byte(id) + m.add_byte(b(id)) m.add_bytes(self.session_id) - out = sofar = SHA.new(str(m)).digest() + out = sofar = sha1(m.asbytes()).digest() while len(out) < nbytes: m = Message() m.add_mpint(self.K) m.add_bytes(self.H) m.add_bytes(sofar) - digest = SHA.new(str(m)).digest() + digest = sha1(m.asbytes()).digest() out += digest sofar += digest return out[:nbytes] @@ -1511,7 +1535,7 @@ # only called if a channel has turned on x11 forwarding if handler is None: # by default, use the same mechanism as accept() - def default_handler(channel, (src_addr, src_port)): + def default_handler(channel, src_addr_port): self._queue_incoming_channel(channel) self._x11_handler = default_handler else: @@ -1525,6 +1549,17 @@ 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 @@ -1535,23 +1570,15 @@ # interpreter shutdown. self.sys = sys - # Required to prevent RNG errors when running inside many subprocess - # containers. - Random.atfork() - - # Hold reference to 'sys' so we can test sys.modules to detect - # interpreter shutdown. - self.sys = sys - # active=True occurs before the thread is launched, to avoid a race _active_threads.append(self) if self.server_mode: - self._log(DEBUG, 'starting thread (server mode): %s' % hex(long(id(self)) & 0xffffffffL)) + self._log(DEBUG, 'starting thread (server mode): %s' % hex(long(id(self)) & xffffffff)) else: - self._log(DEBUG, 'starting thread (client mode): %s' % hex(long(id(self)) & 0xffffffffL)) + self._log(DEBUG, 'starting thread (client mode): %s' % hex(long(id(self)) & xffffffff)) try: try: - self.packetizer.write_all(self.local_version + '\r\n') + self.packetizer.write_all(b(self.local_version + '\r\n')) self._check_banner() self._send_kex_init() self._expect_packet(MSG_KEXINIT) @@ -1577,7 +1604,7 @@ 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 @@ -1599,35 +1626,38 @@ else: self._log(WARNING, 'Oops, unhandled type %d' % ptype) msg = Message() - msg.add_byte(chr(MSG_UNIMPLEMENTED)) + msg.add_byte(cMSG_UNIMPLEMENTED) msg.add_int(m.seqno) self._send_message(msg) - except SSHException, e: + except SSHException as e: self._log(ERROR, 'Exception: ' + str(e)) self._log(ERROR, util.tb_strings()) self.saved_exception = e - except EOFError, e: + except EOFError as e: self._log(DEBUG, 'EOF in transport thread') #self._log(DEBUG, util.tb_strings()) self.saved_exception = e - except socket.error, e: + except socket.error as e: if type(e.args) is tuple: - emsg = '%s (%d)' % (e.args[1], e.args[0]) + if e.args: + emsg = '%s (%d)' % (e.args[1], e.args[0]) + else: # empty tuple, e.g. socket.timeout + emsg = str(e) or repr(e) else: emsg = e.args self._log(ERROR, 'Socket exception: ' + emsg) self.saved_exception = e - except Exception, e: + except Exception as e: self._log(ERROR, 'Unknown exception: ' + str(e)) self._log(ERROR, util.tb_strings()) self.saved_exception = e _active_threads.remove(self) - for chan in self._channels.values(): + for chan in list(self._channels.values()): chan._unlink() if self.active: self.active = False self.packetizer.close() - if self.completion_event != None: + if self.completion_event is not None: self.completion_event.set() if self.auth_handler is not None: self.auth_handler.abort() @@ -1647,10 +1677,8 @@ if self.sys.modules is not None: raise - ### protocol stages - def _negotiate_keys(self, m): # throws SSHException on anything unusual self.clear_to_send_lock.acquire() @@ -1658,7 +1686,7 @@ self.clear_to_send.clear() finally: self.clear_to_send_lock.release() - if self.local_kex_init == None: + if self.local_kex_init is None: # remote side wants to renegotiate self._send_kex_init() self._parse_kex_init(m) @@ -1677,8 +1705,8 @@ buf = self.packetizer.readline(timeout) except ProxyCommandFailure: raise - except Exception, x: - raise SSHException('Error reading SSH protocol banner' + str(x)) + except Exception as e: + raise SSHException('Error reading SSH protocol banner' + str(e)) if buf[:4] == 'SSH-': break self._log(DEBUG, 'Banner: ' + buf) @@ -1688,7 +1716,7 @@ self.remote_version = buf # pull off any attached comment comment = '' - i = string.find(buf, ' ') + i = buf.find(' ') if i >= 0: comment = buf[i+1:] buf = buf[:i] @@ -1719,14 +1747,14 @@ 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_bytes(rng.read(16)) + m.add_byte(cMSG_KEXINIT) + m.add_bytes(os.urandom(16)) m.add_list(self._preferred_kex) m.add_list(available_server_keys) m.add_list(self._preferred_ciphers) @@ -1735,12 +1763,12 @@ 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): @@ -1758,33 +1786,33 @@ kex_follows = m.get_boolean() unused = m.get_int() - self._log(DEBUG, 'kex algos:' + str(kex_algo_list) + ' server key:' + str(server_key_algo_list) + \ - ' client encrypt:' + str(client_encrypt_algo_list) + \ - ' server encrypt:' + str(server_encrypt_algo_list) + \ - ' client mac:' + str(client_mac_algo_list) + \ - ' server mac:' + str(server_mac_algo_list) + \ - ' client compress:' + str(client_compress_algo_list) + \ - ' server compress:' + str(server_compress_algo_list) + \ - ' client lang:' + str(client_lang_list) + \ - ' server lang:' + str(server_lang_list) + \ + self._log(DEBUG, 'kex algos:' + str(kex_algo_list) + ' server key:' + str(server_key_algo_list) + + ' client encrypt:' + str(client_encrypt_algo_list) + + ' server encrypt:' + str(server_encrypt_algo_list) + + ' client mac:' + str(client_mac_algo_list) + + ' server mac:' + str(server_mac_algo_list) + + ' client compress:' + str(client_compress_algo_list) + + ' server compress:' + str(server_compress_algo_list) + + ' client lang:' + str(client_lang_list) + + ' server lang:' + str(server_lang_list) + ' kex follows?' + str(kex_follows)) # as a server, we pick the first item in the client's list that we support. # as a client, we pick the first item in our list that the server supports. 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] @@ -1792,15 +1820,15 @@ 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) - 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(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 = 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] @@ -1808,22 +1836,22 @@ 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] @@ -1838,10 +1866,10 @@ # 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" + """switch on newly negotiated encryption parameters for inbound traffic""" block_size = self._cipher_info[self.remote_cipher]['block-size'] if self.server_mode: IV_in = self._compute_key('A', block_size) @@ -1855,9 +1883,9 @@ # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) else: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) self.packetizer.set_inbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) compress_in = self._compression_info[self.remote_compression][1] if (compress_in is not None) and ((self.remote_compression != 'zlib@openssh.com') or self.authenticated): @@ -1865,9 +1893,9 @@ self.packetizer.set_inbound_compressor(compress_in()) def _activate_outbound(self): - "switch on newly negotiated encryption parameters for outbound traffic" + """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: @@ -1882,9 +1910,9 @@ # initial mac keys are done in the hash's natural size (not the potentially truncated # transmission size) if self.server_mode: - mac_key = self._compute_key('F', mac_engine.digest_size) + mac_key = self._compute_key('F', mac_engine().digest_size) else: - mac_key = self._compute_key('E', mac_engine.digest_size) + mac_key = self._compute_key('E', mac_engine().digest_size) sdctr = self.local_cipher.endswith('-ctr') self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key, sdctr) compress_out = self._compression_info[self.local_compression][0] @@ -1922,7 +1950,7 @@ # this was the first key exchange self.initial_kex_done = True # send an event? - if self.completion_event != None: + if self.completion_event is not None: self.completion_event.set() # it's now okay to send data again (if this was a re-key) if not self.packetizer.need_rekey(): @@ -1936,24 +1964,24 @@ 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: + if ok: 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 @@ -1966,10 +1994,10 @@ 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): @@ -1996,7 +2024,7 @@ 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] @@ -2007,10 +2035,10 @@ 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._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) @@ -2024,7 +2052,7 @@ 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() @@ -2037,7 +2065,7 @@ 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() @@ -2046,9 +2074,9 @@ 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() @@ -2068,13 +2096,12 @@ 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), - (dest_addr, dest_port)) + my_chanid, (origin_addr, origin_port), (dest_addr, dest_port)) else: reason = self.server_object.check_channel_request(kind, my_chanid) if reason != OPEN_SUCCEEDED: @@ -2082,7 +2109,7 @@ 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('') @@ -2096,18 +2123,18 @@ 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() 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) - 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) + 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': @@ -2128,7 +2155,7 @@ try: self.lock.acquire() if name not in self.subsystem_table: - return (None, [], {}) + return None, [], {} return self.subsystem_table[name] finally: self.lock.release() @@ -2142,7 +2169,7 @@ MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure, MSG_CHANNEL_OPEN: _parse_channel_open, MSG_KEXINIT: _negotiate_keys, - } + } _channel_handler_table = { MSG_CHANNEL_SUCCESS: Channel._request_success, @@ -2153,4 +2180,125 @@ MSG_CHANNEL_REQUEST: Channel._handle_request, MSG_CHANNEL_EOF: Channel._handle_eof, MSG_CHANNEL_CLOSE: Channel._handle_close, - } + } + + +class SecurityOptions (object): + """ + Simple object containing the security preferences of an ssh transport. + These are tuples of acceptable ciphers, digests, key types, and key + exchange algorithms, listed in order of preference. + + Changing the contents and/or order of these fields affects the underlying + `.Transport` (but only if you change them before starting the session). + If you try to add an algorithm that paramiko doesn't recognize, + ``ValueError`` will be raised. If you try to assign something besides a + tuple to one of the fields, ``TypeError`` will be raised. + """ + #__slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', 'compression', '_transport' ] + __slots__ = '_transport' + + def __init__(self, transport): + self._transport = transport + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + """ + return '' % repr(self._transport) + + def _get_ciphers(self): + return self._transport._preferred_ciphers + + def _get_digests(self): + return self._transport._preferred_macs + + def _get_key_types(self): + return self._transport._preferred_keys + + def _get_kex(self): + return self._transport._preferred_kex + + def _get_compression(self): + return self._transport._preferred_compression + + def _set(self, name, orig, x): + if type(x) is list: + x = tuple(x) + if type(x) is not tuple: + raise TypeError('expected tuple or list') + 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) + + def _set_ciphers(self, x): + self._set('_preferred_ciphers', '_cipher_info', x) + + def _set_digests(self, x): + self._set('_preferred_macs', '_mac_info', x) + + def _set_key_types(self, x): + self._set('_preferred_keys', '_key_info', x) + + def _set_kex(self, x): + self._set('_preferred_kex', '_kex_info', x) + + def _set_compression(self, x): + self._set('_preferred_compression', '_compression_info', x) + + ciphers = property(_get_ciphers, _set_ciphers, None, + "Symmetric encryption ciphers") + digests = property(_get_digests, _set_digests, None, + "Digest (one-way hash) algorithms") + key_types = property(_get_key_types, _set_key_types, None, + "Public-key algorithms") + kex = property(_get_kex, _set_kex, None, "Key exchange algorithms") + compression = property(_get_compression, _set_compression, None, + "Compression algorithms") + + +class ChannelMap (object): + def __init__(self): + # (id -> Channel) + self._map = weakref.WeakValueDictionary() + self._lock = threading.Lock() + + def put(self, chanid, chan): + self._lock.acquire() + try: + self._map[chanid] = chan + finally: + self._lock.release() + + def get(self, chanid): + self._lock.acquire() + try: + return self._map.get(chanid, None) + finally: + self._lock.release() + + def delete(self, chanid): + self._lock.acquire() + try: + try: + del self._map[chanid] + except KeyError: + pass + finally: + self._lock.release() + + def values(self): + self._lock.acquire() + try: + return list(self._map.values()) + finally: + self._lock.release() + + def __len__(self): + self._lock.acquire() + try: + return len(self._map) + finally: + self._lock.release() diff -Nru paramiko-1.10.1/paramiko/util.py paramiko-1.15.1/paramiko/util.py --- paramiko-1.10.1/paramiko/util.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/util.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -29,78 +29,65 @@ import struct import traceback import threading +import logging -from paramiko.common import * +from paramiko.common import DEBUG, zero_byte, xffffffff, max_byte +from paramiko.py3compat import PY2, long, byte_ord, b, byte_chr from paramiko.config import SSHConfig -# Change by RogerB - python < 2.3 doesn't have enumerate so we implement it -if sys.version_info < (2,3): - class enumerate: - def __init__ (self, sequence): - self.sequence = sequence - def __iter__ (self): - count = 0 - for item in self.sequence: - yield (count, item) - count += 1 - - def inflate_long(s, always_positive=False): - "turns a normalized byte string into a long-int (adapted from Crypto.Util.number)" - out = 0L + """turns a normalized byte string into a long-int (adapted from Crypto.Util.number)""" + 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 + # never convert this to ``s +=`` because this is a string, not a number + # noinspection PyAugmentAssignment 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)" + """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 - n = n >> 32 + s = struct.pack('>I', n & xffffffff) + s + 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 @@ -112,69 +99,73 @@ out.append(format_binary_line(data[x:])) 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): return hexlify(s).upper() + def unhexify(s): return unhexlify(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, False) + 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') -def generate_key_bytes(hashclass, salt, key, nbytes): + +def generate_key_bytes(hash_alg, salt, key, nbytes): """ Given a password, passphrase, or other human-source key, scramble it through a secure hash into some keyworthy bytes. This specific algorithm is used for encrypting/decrypting private key files. - @param hashclass: class from L{Crypto.Hash} that can be used as a secure - hashing function (like C{MD5} or C{SHA}). - @type hashclass: L{Crypto.Hash} - @param salt: data to salt the hash with. - @type salt: string - @param key: human-entered password or passphrase. - @type key: string - @param nbytes: number of bytes to generate. - @type nbytes: int - @return: key data - @rtype: string + :param function hash_alg: A function which creates a new hash object, such + as ``hashlib.sha256``. + :param salt: data to salt the hash with. + :type salt: byte string + :param str key: human-entered password or passphrase. + :param int nbytes: number of bytes to generate. + :return: Key data `str` """ - keydata = '' - digest = '' + keydata = bytes() + digest = bytes() if len(salt) > 8: salt = salt[:8] while nbytes > 0: - hash_obj = hashclass.new() + hash_obj = hash_alg() if len(digest) > 0: hash_obj.update(digest) - hash_obj.update(key) + hash_obj.update(b(key)) hash_obj.update(salt) digest = hash_obj.digest() size = min(nbytes, len(digest)) @@ -182,42 +173,45 @@ nbytes -= size return keydata + def load_host_keys(filename): """ Read a file of known SSH host keys, in the format used by openssh, and - return a compound dict of C{hostname -> keytype ->} L{PKey }. - The hostname may be an IP address or DNS name. The keytype will be either - C{"ssh-rsa"} or C{"ssh-dss"}. + return a compound dict of ``hostname -> keytype ->`` `PKey + `. The hostname may be an IP address or DNS name. The + keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. This type of file unfortunately doesn't exist on Windows, but on posix, - it will usually be stored in C{os.path.expanduser("~/.ssh/known_hosts")}. + it will usually be stored in ``os.path.expanduser("~/.ssh/known_hosts")``. - Since 1.5.3, this is just a wrapper around L{HostKeys}. + Since 1.5.3, this is just a wrapper around `.HostKeys`. - @param filename: name of the file to read host keys from - @type filename: str - @return: dict of host keys, indexed by hostname and then keytype - @rtype: dict(hostname, dict(keytype, L{PKey })) + :param str filename: name of the file to read host keys from + :return: + nested dict of `.PKey` objects, indexed by hostname and then keytype """ from paramiko.hostkeys import HostKeys return HostKeys(filename) + def parse_ssh_config(file_obj): """ - Provided only as a backward-compatible wrapper around L{SSHConfig}. + Provided only as a backward-compatible wrapper around `.SSHConfig`. """ config = SSHConfig() config.parse(file_obj) return config + def lookup_ssh_host_config(hostname, config): """ - Provided only as a backward-compatible wrapper around L{SSHConfig}. + Provided only as a backward-compatible wrapper around `.SSHConfig`. """ return config.lookup(hostname) + def mod_inverse(x, m): - # it's crazy how small python can make this function. + # it's crazy how small Python can make this function. u1, u2, u3 = 1, 0, m v1, v2, v3 = 0, 1, x @@ -233,6 +227,8 @@ _g_thread_ids = {} _g_thread_counter = 0 _g_thread_lock = threading.Lock() + + def get_thread_id(): global _g_thread_ids, _g_thread_counter, _g_thread_lock tid = id(threading.currentThread()) @@ -247,8 +243,9 @@ _g_thread_lock.release() return ret + def log_to_file(filename, level=DEBUG): - "send paramiko logs to a logfile, if they're not already going somewhere" + """send paramiko logs to a logfile, if they're not already going somewhere""" l = logging.getLogger("paramiko") if len(l.handlers) > 0: return @@ -259,6 +256,7 @@ '%Y%m%d-%H:%M:%S')) l.addHandler(lh) + # make only one filter object, so it doesn't get applied more than once class PFilter (object): def filter(self, record): @@ -266,46 +264,71 @@ return True _pfilter = PFilter() + def get_logger(name): l = logging.getLogger(name) l.addFilter(_pfilter) return l + def retry_on_signal(function): """Retries function until it doesn't raise an EINTR error""" 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) + + +def constant_time_bytes_eq(a, b): + if len(a) != len(b): + return False + res = 0 + # noinspection PyUnresolvedReferences + 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() + + +def clamp_value(minimum, val, maximum): + return max(minimum, min(val, maximum)) diff -Nru paramiko-1.10.1/paramiko/_version.py paramiko-1.15.1/paramiko/_version.py --- paramiko-1.10.1/paramiko/_version.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/_version.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,2 @@ +__version_info__ = (1, 15, 1) +__version__ = '.'.join(map(str, __version_info__)) diff -Nru paramiko-1.10.1/paramiko/_winapi.py paramiko-1.15.1/paramiko/_winapi.py --- paramiko-1.10.1/paramiko/_winapi.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/paramiko/_winapi.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,278 @@ +""" +Windows API functions implemented as ctypes functions and classes as found +in jaraco.windows (2.10). + +If you encounter issues with this module, please consider reporting the issues +in jaraco.windows and asking the author to port the fixes back here. +""" + +import ctypes +import ctypes.wintypes +from paramiko.py3compat import u +try: + import builtins +except ImportError: + import __builtin__ as builtins + +try: + USHORT = ctypes.wintypes.USHORT +except AttributeError: + USHORT = ctypes.c_ushort + +###################### +# 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 + 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() + + +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, +] +CreateFileMapping.restype = ctypes.wintypes.HANDLE + +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, + 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): + n = len(msg) + if self.pos + n >= self.length: # A little safety. + raise ValueError("Refusing to write %d bytes" % n) + ctypes.windll.kernel32.RtlMoveMemory(self.view + self.pos, msg, n) + self.pos += n + + def read(self, n): + """ + Read n bytes from mapped view. + """ + out = ctypes.create_string_buffer(n) + ctypes.windll.kernel32.RtlMoveMemory(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 + +class TOKEN_USER(ctypes.Structure): + 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 = 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) + +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 + +class TokenAccess: + 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 + +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) + +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 diff -Nru paramiko-1.10.1/paramiko/win_pageant.py paramiko-1.15.1/paramiko/win_pageant.py --- paramiko-1.10.1/paramiko/win_pageant.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/paramiko/win_pageant.py 2014-09-22 18:31:58.000000000 +0000 @@ -8,7 +8,7 @@ # 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 +# 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. @@ -21,28 +21,19 @@ Functions for communicating with Pageant, the basic windows ssh agent program. """ -import os -import struct -import tempfile -import mmap import array -import platform import ctypes.wintypes +import platform +import struct +from paramiko.util import * -# if you're on windows, you should have one of these, i guess? -# ctypes is part of standard library since Python 2.5 -_has_win32all = False -_has_ctypes = False try: - # win32gui is preferred over win32ui to avoid MFC dependencies - import win32gui - _has_win32all = True + import _thread as thread # Python 3.x except ImportError: - try: - import ctypes - _has_ctypes = True - except ImportError: - pass + import thread # Python 2.5-2.7 + +from . import _winapi + _AGENT_COPYDATA_ID = 0x804e50ba _AGENT_MAX_MSGLEN = 8192 @@ -52,16 +43,7 @@ def _get_pageant_window_object(): - if _has_win32all: - try: - hwnd = win32gui.FindWindow('Pageant', 'Pageant') - return hwnd - except win32gui.error: - pass - elif _has_ctypes: - # Return 0 if there is no Pageant window. - return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant') - return None + return ctypes.windll.user32.FindWindowA('Pageant', 'Pageant') def can_talk_to_agent(): @@ -71,11 +53,12 @@ This checks both if we have the required libraries (win32all or ctypes) and if there is a Pageant currently running. """ - if (_has_win32all or _has_ctypes) and _get_pageant_window_object(): - return True - return False + return bool(_get_pageant_window_object()) + ULONG_PTR = ctypes.c_uint64 if platform.architecture()[0] == '64bit' else ctypes.c_uint32 + + class COPYDATASTRUCT(ctypes.Structure): """ ctypes implementation of @@ -85,53 +68,46 @@ ('num_data', ULONG_PTR), ('data_size', ctypes.wintypes.DWORD), ('data_loc', ctypes.c_void_p), - ] + ] + def _query_pageant(msg): + """ + Communication with the Pageant process is done through a shared + memory-mapped file. + """ hwnd = _get_pageant_window_object() if not hwnd: # Raise a failure to connect exception, pageant isn't running anymore! return None - # Write our pageant request string into the file (pageant will read this to determine what to do) - filename = tempfile.mktemp('.pag') - map_filename = os.path.basename(filename) - - f = open(filename, 'w+b') - f.write(msg ) - # Ensure the rest of the file is empty, otherwise pageant will read this - f.write('\0' * (_AGENT_MAX_MSGLEN - len(msg))) - # Create the shared file map that pageant will use to read from - pymap = mmap.mmap(f.fileno(), _AGENT_MAX_MSGLEN, tagname=map_filename, access=mmap.ACCESS_WRITE) - try: + # create a name for the mmap + map_name = 'PageantRequest%08x' % thread.get_ident() + + pymap = _winapi.MemoryMap(map_name, _AGENT_MAX_MSGLEN, + _winapi.get_security_attributes_for_user(), + ) + with pymap: + pymap.write(msg) # Create an array buffer containing the mapped filename - char_buffer = array.array("c", map_filename + '\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, char_buffer_address) + cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, + char_buffer_address) - if _has_win32all: - # win32gui.SendMessage should also allow the same pattern as - # ctypes, but let's keep it like this for now... - response = win32gui.SendMessage(hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.addressof(cds)) - elif _has_ctypes: - response = ctypes.windll.user32.SendMessageA(hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds)) - else: - response = 0 + response = ctypes.windll.user32.SendMessageA(hwnd, + win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds)) if response > 0: + pymap.seek(0) datalen = pymap.read(4) retlen = struct.unpack('>I', datalen)[0] return datalen + pymap.read(retlen) return None - finally: - pymap.close() - f.close() - # Remove the file, it was temporary only - os.unlink(filename) -class PageantConnection (object): +class PageantConnection(object): """ Mock "connection" to an agent which roughly approximates the behavior of a unix local-domain socket (as used by Agent). Requests are sent to the diff -Nru paramiko-1.10.1/README paramiko-1.15.1/README --- paramiko-1.10.1/README 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/README 2014-09-22 18:31:58.000000000 +0000 @@ -5,16 +5,17 @@ :Paramiko: Python SSH module :Copyright: Copyright (c) 2003-2009 Robey Pointer -:Copyright: Copyright (c) 2013 Jeff Forcier +:Copyright: Copyright (c) 2013-2014 Jeff Forcier :License: LGPL :Homepage: https://github.com/paramiko/paramiko/ +:API docs: http://docs.paramiko.org 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 @@ -33,8 +34,10 @@ Requirements ------------ - - python 2.5 or better + - Python 2.6 or better - this includes Python + 3.2 and higher as well. - pycrypto 2.1 or better + - ecdsa 0.9 or better If you have setuptools, you can build and install paramiko and all its dependencies with this command (as root):: @@ -69,6 +72,14 @@ 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 +---------------- + +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 ---- @@ -122,10 +133,7 @@ --- the demo scripts are probably the best example of how to use this package. -there is also a lot of documentation, generated with epydoc, in the doc/ -folder. point your browser there. seriously, do it. mad props to -epydoc, which actually motivated me to write more documentation than i -ever would have before. +there is also a lot of documentation, generated with Sphinx autodoc, in the doc/ folder. there are also unit tests here:: diff -Nru paramiko-1.10.1/requirements.txt paramiko-1.15.1/requirements.txt --- paramiko-1.10.1/requirements.txt 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -pycrypto -tox diff -Nru paramiko-1.10.1/setup.cfg paramiko-1.15.1/setup.cfg --- paramiko-1.10.1/setup.cfg 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/setup.cfg 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,2 @@ +[wheel] +universal = 1 diff -Nru paramiko-1.10.1/setup_helper.py paramiko-1.15.1/setup_helper.py --- paramiko-1.10.1/setup_helper.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/setup_helper.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. diff -Nru paramiko-1.10.1/setup.py paramiko-1.15.1/setup.py --- paramiko-1.10.1/setup.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/setup.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -40,7 +40,10 @@ try: from setuptools import setup kw = { - 'install_requires': 'pycrypto >= 2.1, != 2.4', + 'install_requires': [ + 'pycrypto >= 2.1, != 2.4', + 'ecdsa >= 0.11', + ], } except ImportError: from distutils.core import setup @@ -51,21 +54,39 @@ setup_helper.install_custom_make_tarball() -setup(name = "paramiko", - version = "1.10.1", - description = "SSH2 protocol library", - author = "Jeff Forcier", - author_email = "jeff@bitprophet.org", - url = "https://github.com/paramiko/paramiko/", - packages = [ 'paramiko' ], - license = 'LGPL', - platforms = 'Posix; MacOS X; Windows', - classifiers = [ 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', - 'Operating System :: OS Independent', - 'Topic :: Internet', - 'Topic :: Security :: Cryptography' ], - long_description = longdesc, - **kw - ) +# 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 = version, + description = "SSH2 protocol library", + long_description = longdesc, + author = "Jeff Forcier", + author_email = "jeff@bitprophet.org", + url = "https://github.com/paramiko/paramiko/", + packages = [ 'paramiko' ], + license = 'LGPL', + platforms = 'Posix; MacOS X; Windows', + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', + 'Operating System :: OS Independent', + 'Topic :: Internet', + 'Topic :: Security :: Cryptography', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + **kw +) diff -Nru paramiko-1.10.1/sites/docs/api/agent.rst paramiko-1.15.1/sites/docs/api/agent.rst --- paramiko-1.10.1/sites/docs/api/agent.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/agent.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,6 @@ +SSH agents +========== + +.. automodule:: paramiko.agent + :inherited-members: + :no-special-members: diff -Nru paramiko-1.10.1/sites/docs/api/buffered_pipe.rst paramiko-1.15.1/sites/docs/api/buffered_pipe.rst --- paramiko-1.10.1/sites/docs/api/buffered_pipe.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/buffered_pipe.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Buffered pipes +============== + +.. automodule:: paramiko.buffered_pipe diff -Nru paramiko-1.10.1/sites/docs/api/channel.rst paramiko-1.15.1/sites/docs/api/channel.rst --- paramiko-1.10.1/sites/docs/api/channel.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/channel.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Channel +======= + +.. automodule:: paramiko.channel diff -Nru paramiko-1.10.1/sites/docs/api/client.rst paramiko-1.15.1/sites/docs/api/client.rst --- paramiko-1.10.1/sites/docs/api/client.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/client.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +Client +====== + +.. automodule:: paramiko.client + :member-order: bysource diff -Nru paramiko-1.10.1/sites/docs/api/config.rst paramiko-1.15.1/sites/docs/api/config.rst --- paramiko-1.10.1/sites/docs/api/config.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/config.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +Configuration +============= + +.. automodule:: paramiko.config + :member-order: bysource diff -Nru paramiko-1.10.1/sites/docs/api/file.rst paramiko-1.15.1/sites/docs/api/file.rst --- paramiko-1.10.1/sites/docs/api/file.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/file.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Buffered files +============== + +.. automodule:: paramiko.file diff -Nru paramiko-1.10.1/sites/docs/api/hostkeys.rst paramiko-1.15.1/sites/docs/api/hostkeys.rst --- paramiko-1.10.1/sites/docs/api/hostkeys.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/hostkeys.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +Host keys / ``known_hosts`` files +================================= + +.. automodule:: paramiko.hostkeys + :member-order: bysource diff -Nru paramiko-1.10.1/sites/docs/api/kex_gss.rst paramiko-1.15.1/sites/docs/api/kex_gss.rst --- paramiko-1.10.1/sites/docs/api/kex_gss.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/kex_gss.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +GSS-API key exchange +==================== + +.. automodule:: paramiko.kex_gss + :member-order: bysource diff -Nru paramiko-1.10.1/sites/docs/api/keys.rst paramiko-1.15.1/sites/docs/api/keys.rst --- paramiko-1.10.1/sites/docs/api/keys.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/keys.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,23 @@ +============ +Key handling +============ + +Parent key class +================ + +.. automodule:: paramiko.pkey + +DSA (DSS) +========= + +.. automodule:: paramiko.dsskey + +RSA +=== + +.. automodule:: paramiko.rsakey + +ECDSA +===== + +.. automodule:: paramiko.ecdsakey diff -Nru paramiko-1.10.1/sites/docs/api/message.rst paramiko-1.15.1/sites/docs/api/message.rst --- paramiko-1.10.1/sites/docs/api/message.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/message.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Message +======= + +.. automodule:: paramiko.message diff -Nru paramiko-1.10.1/sites/docs/api/packet.rst paramiko-1.15.1/sites/docs/api/packet.rst --- paramiko-1.10.1/sites/docs/api/packet.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/packet.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Packetizer +========== + +.. automodule:: paramiko.packet diff -Nru paramiko-1.10.1/sites/docs/api/pipe.rst paramiko-1.15.1/sites/docs/api/pipe.rst --- paramiko-1.10.1/sites/docs/api/pipe.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/pipe.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Cross-platform pipe implementations +=================================== + +.. automodule:: paramiko.pipe diff -Nru paramiko-1.10.1/sites/docs/api/proxy.rst paramiko-1.15.1/sites/docs/api/proxy.rst --- paramiko-1.10.1/sites/docs/api/proxy.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/proxy.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +``ProxyCommand`` support +======================== + +.. automodule:: paramiko.proxy diff -Nru paramiko-1.10.1/sites/docs/api/server.rst paramiko-1.15.1/sites/docs/api/server.rst --- paramiko-1.10.1/sites/docs/api/server.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/server.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +Server implementation +===================== + +.. automodule:: paramiko.server + :member-order: bysource diff -Nru paramiko-1.10.1/sites/docs/api/sftp.rst paramiko-1.15.1/sites/docs/api/sftp.rst --- paramiko-1.10.1/sites/docs/api/sftp.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/sftp.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,13 @@ +SFTP +==== + +.. automodule:: paramiko.sftp +.. automodule:: paramiko.sftp_client +.. automodule:: paramiko.sftp_server +.. automodule:: paramiko.sftp_attr +.. automodule:: paramiko.sftp_file + :inherited-members: + :no-special-members: + :show-inheritance: +.. automodule:: paramiko.sftp_handle +.. automodule:: paramiko.sftp_si diff -Nru paramiko-1.10.1/sites/docs/api/ssh_exception.rst paramiko-1.15.1/sites/docs/api/ssh_exception.rst --- paramiko-1.10.1/sites/docs/api/ssh_exception.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/ssh_exception.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,4 @@ +Exceptions +========== + +.. automodule:: paramiko.ssh_exception diff -Nru paramiko-1.10.1/sites/docs/api/ssh_gss.rst paramiko-1.15.1/sites/docs/api/ssh_gss.rst --- paramiko-1.10.1/sites/docs/api/ssh_gss.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/ssh_gss.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,14 @@ +GSS-API authentication +====================== + +.. 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 -Nru paramiko-1.10.1/sites/docs/api/transport.rst paramiko-1.15.1/sites/docs/api/transport.rst --- paramiko-1.10.1/sites/docs/api/transport.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/api/transport.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +Transport +========= + +.. automodule:: paramiko.transport + :member-order: bysource diff -Nru paramiko-1.10.1/sites/docs/conf.py paramiko-1.15.1/sites/docs/conf.py --- paramiko-1.10.1/sites/docs/conf.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/conf.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,16 @@ +# Obtain shared config values +import os, sys +sys.path.append(os.path.abspath('..')) +sys.path.append(os.path.abspath('../..')) +from shared_conf import * + +# Enable autodoc, intersphinx +extensions.extend(['sphinx.ext.autodoc']) + +# Autodoc settings +autodoc_default_flags = ['members', 'special-members'] + +# Sister-site links to WWW +html_theme_options['extra_nav_links'] = { + "Main website": 'http://www.paramiko.org', +} diff -Nru paramiko-1.10.1/sites/docs/index.rst paramiko-1.15.1/sites/docs/index.rst --- paramiko-1.10.1/sites/docs/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/docs/index.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,74 @@ +==================================== +Welcome to Paramiko's documentation! +==================================== + +This site covers Paramiko's usage & API documentation. For basic info on what +Paramiko is, including its public changelog & how the project is maintained, +please see `the main project website `_. + + +API documentation +================= + +The high-level client API starts with creation of an `.SSHClient` object. For +more direct control, pass a socket (or socket-like object) to a `.Transport`, +and use `start_server <.Transport.start_server>` or `start_client +<.Transport.start_client>` to negotiate with the remote host as either a server +or client. + +As a client, you are responsible for authenticating using a password or private +key, and checking the server's host key. (Key signature and verification is +done by paramiko, but you will need to provide private keys and check that the +content of a public key matches what you expected to see.) + +As a server, you are responsible for deciding which users, passwords, and keys +to allow, and what kind of channels to allow. + +Once you have finished, either side may request flow-controlled `channels +<.Channel>` to the other side, which are Python objects that act like sockets, +but send and receive data over the encrypted session. + +For details, please see the following tables of contents (which are organized +by area of interest.) + + +Core SSH protocol classes +------------------------- + +.. toctree:: + api/channel + api/client + api/message + api/packet + api/transport + + +Authentication & keys +--------------------- + +.. toctree:: + api/agent + api/hostkeys + api/keys + api/ssh_gss + api/kex_gss + + +Other primary functions +----------------------- + +.. toctree:: + api/config + api/proxy + api/server + api/sftp + + +Miscellany +---------- + +.. toctree:: + api/buffered_pipe + api/file + api/pipe + api/ssh_exception diff -Nru paramiko-1.10.1/sites/shared_conf.py paramiko-1.15.1/sites/shared_conf.py --- paramiko-1.10.1/sites/shared_conf.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/shared_conf.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,41 @@ +from datetime import datetime + +import alabaster + + +# Alabaster theme + mini-extension +html_theme_path = [alabaster.get_path()] +extensions = ['alabaster', 'sphinx.ext.intersphinx'] +# Paths relative to invoking conf.py - not this shared file +html_theme = 'alabaster' +html_theme_options = { + 'description': "A Python implementation of SSHv2.", + 'github_user': 'paramiko', + 'github_repo': 'paramiko', + 'gratipay_user': 'bitprophet', + 'analytics_id': 'UA-18486793-2', + 'travis_button': True, +} +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'searchbox.html', + 'donate.html', + ] +} + +# Everything intersphinx's to Python +intersphinx_mapping = { + 'python': ('http://docs.python.org/2.6', None), +} + +# Regular settings +project = 'Paramiko' +year = datetime.now().year +copyright = '%d Jeff Forcier' % year +master_doc = 'index' +templates_path = ['_templates'] +exclude_trees = ['_build'] +source_suffix = '.rst' +default_role = 'obj' diff -Nru paramiko-1.10.1/sites/www/changelog.rst paramiko-1.15.1/sites/www/changelog.rst --- paramiko-1.10.1/sites/www/changelog.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/changelog.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,276 @@ +========= +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 + 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. +* :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. + + .. 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. +* :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 + 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 + IRC. +* :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 + ``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. +* :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 + 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. +* :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.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 + 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 + 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 + report. +* :bug:`239` Add Windows-style CRLF support to SSH config file parsing. Props + to Christopher Swenson. +* :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. +* :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>` +* :release:`1.13.1 <2014-05-07>` +* :release:`1.12.4 <2014-05-07>` +* :release:`1.11.6 <2014-05-07>` +* :bug:`-` `paramiko.file.BufferedFile.read` incorrectly returned text strings + after the Python 3 migration, despite bytes being more appropriate for file + contents (which may be binary or of an unknown encoding.) This has been + addressed. + + .. note:: + `paramiko.file.BufferedFile.readline` continues to return strings, not + bytes, as "lines" only make sense for textual data. It assumes UTF-8 by + default. + + This should fix `this issue raised on the Obnam mailing list + `_. Thanks + to Antoine Brenner for the patch. +* :bug:`-` Added self.args for exception classes. Used for unpickling. Related + to (`Fabric #986 `_, `Fabric + #714 `_). Thanks to Alex + Plugaru. +* :bug:`-` Fix logging error in sftp_client for filenames containing the '%' + character. Thanks to Antoine Brenner. +* :bug:`308` Fix regression in dsskey.py that caused sporadic signature + verification failures. Thanks to Chris Rose. +* :support:`299` Use deterministic signatures for ECDSA keys for improved + security. Thanks to Alex Gaynor. +* :support:`297` Replace PyCrypto's ``Random`` with `os.urandom` for improved + speed and security. Thanks again to Alex. +* :support:`295` Swap out a bunch of PyCrypto hash functions with use of + `hashlib`. Thanks to Alex Gaynor. +* :support:`290` (also :issue:`292`) Add support for building universal + (Python 2+3 compatible) wheel files during the release process. Courtesy of + Alex Gaynor. +* :support:`284` Add Python language trove identifiers to ``setup.py``. Thanks + to Alex Gaynor for catch & patch. +* :bug:`235` Improve string type testing in a handful of spots (e.g. ``s/if + type(x) is str/if isinstance(x, basestring)/g``.) Thanks to ``@ksamuel`` for + the report. +* :release:`1.13.0 <2014-03-13>` +* :release:`1.12.3 <2014-03-13>` +* :release:`1.11.5 <2014-03-13>` +* :release:`1.10.7 <2014-03-13>` +* :feature:`16` **Python 3 support!** Our test suite passes under Python 3, and + it (& Fabric's test suite) continues to pass under Python 2. **Python 2.5 is + no longer supported with this change!** + + The merged code was built on many contributors' efforts, both code & + feedback. In no particular order, we thank Daniel Goertzen, Ivan Kolodyazhny, + Tomi Pieviläinen, Jason R. Coombs, Jan N. Schulze, ``@Lazik``, Dorian Pula, + Scott Maxwell, Tshepang Lekhonkhobe, Aaron Meurer, and Dave Halter. +* :support:`256 backported` Convert API documentation to Sphinx, yielding a new + API docs website to replace the old Epydoc one. Thanks to Olle Lundberg for + the initial conversion work. +* :bug:`-` Use constant-time hash comparison operations where possible, to + protect against `timing-based attacks + `_. Thanks to Alex Gaynor + for the patch. +* :release:`1.12.2 <2014-02-14>` +* :release:`1.11.4 <2014-02-14>` +* :release:`1.10.6 <2014-02-14>` +* :feature:`58` Allow client code to access the stored SSH server banner via + `Transport.get_banner `. Thanks to + ``@Jhoanor`` for the patch. +* :bug:`252` (`Fabric #1020 `_) + Enhanced the implementation of ``ProxyCommand`` to avoid a deadlock/hang + condition that frequently occurs at ``Transport`` shutdown time. Thanks to + Mateusz Kobos, Matthijs van der Vleuten and Guillaume Zitta for the original + reports and to Marius Gedminas for helping test nontrivial use cases. +* :bug:`268` Fix some missed renames of ``ProxyCommand`` related error classes. + Thanks to Marius Gedminas for catch & patch. +* :bug:`34` (PR :issue:`35`) Fix SFTP prefetching incompatibility with some + SFTP servers regarding request/response ordering. Thanks to Richard + Kettlewell. +* :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>` +* :release:`1.10.5 <2014-01-08>` +* :bug:`225 (1.12+)` 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>` +* :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 `_ and + `ecdsa on PyPI `_. + + * 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 (1.11+)` 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 -Nru paramiko-1.10.1/sites/www/conf.py paramiko-1.15.1/sites/www/conf.py --- paramiko-1.10.1/sites/www/conf.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/conf.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,25 @@ +# 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 * + +# Releases changelog extension +extensions.append('releases') +# Paramiko 1.x tags start with 'v'. Meh. +releases_release_uri = "https://github.com/paramiko/paramiko/tree/v%s" +releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" + +# 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': + target = 'http://docs.paramiko.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 -Nru paramiko-1.10.1/sites/www/contact.rst paramiko-1.15.1/sites/www/contact.rst --- paramiko-1.10.1/sites/www/contact.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/contact.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,12 @@ +======= +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. +* Submit contributions on Github - see the :doc:`contributing` page. diff -Nru paramiko-1.10.1/sites/www/contributing.rst paramiko-1.15.1/sites/www/contributing.rst --- paramiko-1.10.1/sites/www/contributing.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/contributing.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,26 @@ +============ +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. 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 diff -Nru paramiko-1.10.1/sites/www/faq.rst paramiko-1.15.1/sites/www/faq.rst --- paramiko-1.10.1/sites/www/faq.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/faq.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,26 @@ +=================================== +Frequently Asked/Answered Questions +=================================== + +Which version should I use? I see multiple active releases. +=========================================================== + +Please see :ref:`the installation docs ` which have an explicit +section about this topic. + +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 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 +submissions for these issues**, provided they can get +1s from similarly +affected users and are proven to not break existing functionality. diff -Nru paramiko-1.10.1/sites/www/index.rst paramiko-1.15.1/sites/www/index.rst --- paramiko-1.10.1/sites/www/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/index.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,36 @@ +Welcome to Paramiko! +==================== + +Paramiko is a Python (2.6+, 3.3+) implementation of the SSHv2 protocol [#]_, +providing both client and server functionality. While it leverages a Python C +extension for low level cryptography (`PyCrypto `_), +Paramiko itself is a pure Python interface around SSH networking concepts. + +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 `_. + +Please see the sidebar to the left to begin. + +.. toctree:: + :hidden: + + changelog + FAQs + installing + contributing + contact + + +.. 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 -Nru paramiko-1.10.1/sites/www/installing.rst paramiko-1.15.1/sites/www/installing.rst --- paramiko-1.10.1/sites/www/installing.rst 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/installing.rst 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,132 @@ +========== +Installing +========== + +.. _paramiko-itself: + +Paramiko itself +=============== + +The recommended way to get Paramiko 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.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 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 +------------- + +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 the last 2-3 releases including the latest stable one. + +If you're unsure which version to install, we have suggestions: + +* **Completely new users** should always default to the **latest stable + release** (as above, whatever is newest / whatever shows up with ``pip + install paramiko``.) +* **Users upgrading from a much older version** (e.g. the 1.7.x line) should + probably get the **oldest actively supported line** (see the paragraph above + this list for what that currently is.) +* **Everybody else** is hopefully already "on" a given version and can + carefully upgrade to whichever version they care to, when their release line + stops being supported. + + +PyCrypto +======== + +`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. + +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:\> + + +.. _gssapi: + +Optional dependencies for GSS-API / SSPI / Kerberos +=================================================== + +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 `_ + ``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. + +.. 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 diff -Nru paramiko-1.10.1/sites/www/_templates/rss.xml paramiko-1.15.1/sites/www/_templates/rss.xml --- paramiko-1.10.1/sites/www/_templates/rss.xml 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/sites/www/_templates/rss.xml 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,19 @@ + + + + + {{ title }} + {{ link }} + {{ description }} + {{ date }} + {% for link, title, desc, date in posts %} + + {{ link }} + {{ link }} + <![CDATA[{{ title }}]]> + + {{ date }} + + {% endfor %} + + diff -Nru paramiko-1.10.1/tasks.py paramiko-1.15.1/tasks.py --- paramiko-1.10.1/tasks.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tasks.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,53 @@ +from os import mkdir +from os.path import join +from shutil import rmtree, copytree + +from invoke import Collection, ctask as task +from invocations import docs as _docs +from invocations.packaging import publish + + +d = 'sites' + +# Usage doc/API site (published as docs.paramiko.org) +docs_path = join(d, 'docs') +docs_build = join(docs_path, '_build') +docs = Collection.from_module(_docs, name='docs', config={ + 'sphinx.source': docs_path, + 'sphinx.target': docs_build, +}) + +# Main/about/changelog site ((www.)?paramiko.org) +www_path = join(d, 'www') +www = Collection.from_module(_docs, name='www', config={ + 'sphinx.source': www_path, + 'sphinx.target': join(www_path, '_build'), +}) + + +# Until we move to spec-based testing +@task +def test(ctx, coverage=False): + runner = "python" + if coverage: + runner = "coverage run --source=paramiko" + flags = "--verbose" + ctx.run("{0} test.py {1}".format(runner, flags), pty=True) + + +# Until we stop bundling docs w/ releases. Need to discover use cases first. +@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) + 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, release, docs=docs, www=www) diff -Nru paramiko-1.10.1/test.py paramiko-1.15.1/test.py --- paramiko-1.10.1/test.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/test.py 2014-09-22 18:31:58.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +# 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. @@ -29,22 +29,25 @@ 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 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 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') @@ -86,6 +89,17 @@ 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)') @@ -102,20 +116,33 @@ 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() - + # setup logging 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)) @@ -134,6 +161,15 @@ 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 @@ -147,7 +183,7 @@ # TODO: make that not a problem, jeez for thread in threading.enumerate(): if thread is not threading.currentThread(): - thread._Thread__stop() + thread.join(timeout=1) # Exit correctly if not result.wasSuccessful(): sys.exit(1) diff -Nru paramiko-1.10.1/tests/loop.py paramiko-1.15.1/tests/loop.py --- paramiko-1.10.1/tests/loop.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/loop.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,6 +21,7 @@ """ import threading, socket +from paramiko.common import asbytes class LoopSocket (object): @@ -31,7 +32,7 @@ """ 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 @@ 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 @@ 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 -Nru paramiko-1.10.1/tests/stub_sftp.py paramiko-1.15.1/tests/stub_sftp.py --- paramiko-1.10.1/tests/stub_sftp.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/stub_sftp.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,8 +21,10 @@ """ import os +import sys from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \ SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED +from paramiko.common import o666 class StubServer (ServerInterface): @@ -38,7 +40,7 @@ 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 @@ try: SFTPServer.set_file_attr(self.filename, attr) return SFTP_OK - except OSError, e: + except OSError as e: return SFTPServer.convert_errno(e.errno) @@ -62,34 +64,34 @@ def list_folder(self, path): path = self._realpath(path) try: - out = [ ] + out = [] flist = os.listdir(path) for fname in flist: attr = SFTPAttributes.from_stat(os.stat(os.path.join(path, fname))) 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): path = self._realpath(path) try: - binary_flag = getattr(os, 'O_BINARY', 0) + binary_flag = getattr(os, 'O_BINARY', 0) flags |= binary_flag mode = getattr(attr, 'st_mode', None) if mode is not None: @@ -97,8 +99,8 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -Nru paramiko-1.10.1/tests/test_auth.py paramiko-1.15.1/tests/test_auth.py --- paramiko-1.10.1/tests/test_auth.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_auth.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -25,18 +25,21 @@ import unittest from paramiko import Transport, ServerInterface, RSAKey, DSSKey, \ - SSHException, BadAuthenticationType, InteractiveQuery, ChannelException, \ + BadAuthenticationType, InteractiveQuery, \ 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 +67,7 @@ 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 +113,18 @@ 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 +135,11 @@ 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 +150,10 @@ 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 +164,10 @@ 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 +183,9 @@ 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 +196,7 @@ 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 +205,8 @@ """ 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 +217,7 @@ 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 +231,4 @@ 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 -Nru paramiko-1.10.1/tests/test_buffered_pipe.py paramiko-1.15.1/tests/test_buffered_pipe.py --- paramiko-1.10.1/tests/test_buffered_pipe.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_buffered_pipe.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -25,58 +25,57 @@ import unittest from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe +from paramiko.py3compat import b -from util import ParamikoTest - -def delay_thread(pipe): - pipe.feed('a') +def delay_thread(p): + p.feed('a') time.sleep(0.5) - pipe.feed('b') - pipe.close() + p.feed('b') + p.close() -def close_thread(pipe): +def close_thread(p): time.sleep(0.2) - pipe.close() + p.close() -class BufferedPipeTest(ParamikoTest): +class BufferedPipeTest(unittest.TestCase): 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() @@ -90,4 +89,3 @@ self.assertTrue(p._set) p2.clear() self.assertFalse(p._set) - diff -Nru paramiko-1.10.1/tests/test_client.py paramiko-1.15.1/tests/test_client.py --- paramiko-1.10.1/tests/test_client.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_client.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -20,17 +20,34 @@ Some unit tests for SSHClient. """ +from __future__ import with_statement + import socket +from tempfile import mkstemp import threading -import time import unittest import weakref -from binascii import hexlify - +import warnings +import os +import time +from tests.util import test_path import paramiko +from paramiko.common import PY2, b +from paramiko.ssh_exception import SSHException + + +FINGERPRINTS = { + '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', +} class NullServer (paramiko.ServerInterface): + def __init__(self, *args, **kwargs): + # Allow tests to enable/disable specific key types + self.__allowed_keys = kwargs.pop('allowed_keys', []) + super(NullServer, self).__init__(*args, **kwargs) def get_allowed_auths(self, username): if username == 'slowdive': @@ -43,7 +60,14 @@ return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - if (key.get_name() == 'ssh-dss') and (hexlify(key.get_fingerprint()) == '4478f0b9a23cc5182009ff755bc1d26c'): + try: + expected = FINGERPRINTS[key.get_name()] + except KeyError: + return paramiko.AUTH_FAILED + if ( + key.get_name() in self.__allowed_keys and + key.get_fingerprint() == expected + ): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -64,40 +88,52 @@ 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): + def _run(self, allowed_keys=None, delay=0): + 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('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() + server = NullServer(allowed_keys=allowed_keys) + if delay: + time.sleep(delay) self.ts.start_server(self.event, server) - - def test_1_client(self): + def _test_connection(self, **kwargs): """ - verify that the SSHClient stuff works too. + (Most) kwargs get passed directly into SSHClient.connect(). + + The exception is ``allowed_keys`` which is stripped and handed to the + ``NullServer`` used for testing. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + run_kwargs = {'allowed_keys': kwargs.pop('allowed_keys', None)} + # Server setup + 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()) + # 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.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()) + # Command execution functions? stdin, stdout, stderr = self.tc.exec_command('yes') schan = self.ts.accept(1.0) @@ -105,108 +141,206 @@ 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()) + # 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. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + self._test_connection(key_filename=test_path('test_dss.key')) - 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.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()) - - 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()) + def test_client_rsa(self): + """ + verify that SSHClient works with an RSA key. + """ + self._test_connection(key_filename=test_path('test_rsa.key')) - stdin.close() - stdout.close() - stderr.close() + def test_2_5_client_ecdsa(self): + """ + verify that SSHClient works with an ECDSA key. + """ + 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. """ - host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key') - public_host_key = paramiko.RSAKey(data=str(host_key)) + # This is dumb :( + types_ = { + 'rsa': 'ssh-rsa', + 'dss': 'ssh-dss', + '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=[ + test_path('test_{0}.key'.format(x)) for x in attempt + ], + allowed_keys=[types_[x] for x in accept], + ) - 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.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()) + def test_multiple_key_files_failure(self): + """ + Expect failure when multiple keys in play and none are accepted + """ + # 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'], + ) 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_cleanup(self): + def test_5_save_host_keys(self): + """ + verify that SSHClient correctly saves a known_hosts file. + """ + warnings.filterwarnings('ignore', '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())) + + host_id = '[%s]:%d' % (self.addr, self.port) + + client.get_host_keys().add(host_id, 'ssh-rsa', public_host_key) + self.assertEquals(1, len(client.get_host_keys())) + self.assertEquals(public_host_key, client.get_host_keys()[host_id]['ssh-rsa']) + + client.save_host_keys(localname) + + with open(localname) as fd: + assert host_id in fd.read() + + os.unlink(localname) + + def test_6_cleanup(self): """ 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)) + # 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() + 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) + + def test_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) + + def test_7_banner_timeout(self): + """ + verify that the SSHClient has a configurable banner timeout. + """ + # Start the thread with a 1 second wait. + threading.Thread(target=self._run, kwargs={'delay': 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 half second banner timeout. + self.assertRaises( + paramiko.SSHException, + self.tc.connect, + self.addr, + self.port, + username='slowdive', + password='pygmalion', + banner_timeout=0.5 + ) diff -Nru paramiko-1.10.1/tests/test_ecdsa.key paramiko-1.15.1/tests/test_ecdsa.key --- paramiko-1.10.1/tests/test_ecdsa.key 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tests/test_ecdsa.key 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49 +AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD +ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g== +-----END EC PRIVATE KEY----- diff -Nru paramiko-1.10.1/tests/test_ecdsa_password.key paramiko-1.15.1/tests/test_ecdsa_password.key --- paramiko-1.10.1/tests/test_ecdsa_password.key 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tests/test_ecdsa_password.key 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,EEB56BC745EDB2DE04FC3FE1F8DA387E + +wdt7QTCa6ahTJLaEPH7NhHyBcxhzrzf93d4UwQOuAhkM6//jKD4lF9fErHBW0f3B +ExberCU3UxfEF3xX2thXiLw47JgeOCeQUlqRFx92p36k6YmfNGX6W8CsZ3d+XodF +Z+pb6m285CiSX+W95NenFMexXFsIpntiCvTifTKJ8os= +-----END EC PRIVATE KEY----- diff -Nru paramiko-1.10.1/tests/test_file.py paramiko-1.15.1/tests/test_file.py --- paramiko-1.10.1/tests/test_file.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_file.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -22,6 +22,8 @@ import unittest from paramiko.file import BufferedFile +from paramiko.common import linefeed_byte, crlf, cr_byte +import sys class LoopbackFile (BufferedFile): @@ -31,7 +33,7 @@ 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: @@ -52,8 +54,8 @@ def test_1_simple(self): f = LoopbackFile('r') try: - f.write('hi') - self.assert_(False, 'no exception on write to read-only file') + f.write(b'hi') + self.assertTrue(False, 'no exception on write to read-only file') except: pass f.close() @@ -61,14 +63,14 @@ 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() def test_2_readline(self): f = LoopbackFile('r+U') - f.write('First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.') + f.write(b'First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.') self.assertEqual(f.readline(), 'First line.\n') # universal newline mode should convert this linefeed: self.assertEqual(f.readline(), 'Second line.\n') @@ -80,31 +82,31 @@ 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): """ try to trick the linefeed detector. """ f = LoopbackFile('r+U') - f.write('First line.\r') + f.write(b'First line.\r') self.assertEqual(f.readline(), 'First line.\n') - f.write('\nSecond.\r\n') + f.write(b'\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): """ verify that write buffering is on. """ f = LoopbackFile('r+', 1) - f.write('Complete line.\nIncomplete line.') + f.write(b'Complete line.\nIncomplete line.') self.assertEqual(f.readline(), 'Complete line.\n') self.assertEqual(f.readline(), '') f.write('..\n') @@ -117,12 +119,12 @@ """ f = LoopbackFile('r+', 512) f.write('Not\nquite\n512 bytes.\n') - self.assertEqual(f.read(1), '') + self.assertEqual(f.read(1), b'') f.flush() - self.assertEqual(f.read(5), 'Not\nq') - self.assertEqual(f.read(10), 'uite\n512 b') - self.assertEqual(f.read(9), 'ytes.\n') - self.assertEqual(f.read(3), '') + self.assertEqual(f.read(5), b'Not\nq') + self.assertEqual(f.read(10), b'uite\n512 b') + self.assertEqual(f.read(9), b'ytes.\n') + self.assertEqual(f.read(3), b'') f.close() def test_6_buffering(self): @@ -130,12 +132,12 @@ verify that flushing happens automatically on buffer crossing. """ f = LoopbackFile('r+', 16) - f.write('Too small.') - self.assertEqual(f.read(4), '') - f.write(' ') - self.assertEqual(f.read(4), '') - f.write('Enough.') - self.assertEqual(f.read(20), 'Too small. Enough.') + f.write(b'Too small.') + self.assertEqual(f.read(4), b'') + f.write(b' ') + self.assertEqual(f.read(4), b'') + f.write(b'Enough.') + self.assertEqual(f.read(20), b'Too small. Enough.') f.close() def test_7_read_all(self): @@ -143,9 +145,23 @@ verify that read(-1) returns everything left in the file. """ f = LoopbackFile('r+', 16) - f.write('The first thing you need to do is open your eyes. ') - f.write('Then, you need to close them again.\n') + f.write(b'The first thing you need to do is open your eyes. ') + f.write(b'Then, you need to close them again.\n') s = f.read(-1) - self.assertEqual(s, 'The first thing you need to do is open your eyes. Then, you ' + - 'need to close them again.\n') + self.assertEqual(s, b'The first thing you need to do is open your eyes. Then, you ' + + 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() + diff -Nru paramiko-1.10.1/tests/test_gssapi.py paramiko-1.15.1/tests/test_gssapi.py --- paramiko-1.10.1/tests/test_gssapi.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tests/test_gssapi.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,137 @@ +# 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 +""" + +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 -Nru paramiko-1.10.1/tests/test_hostkeys.py paramiko-1.15.1/tests/test_hostkeys.py --- paramiko-1.10.1/tests/test_hostkeys.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_hostkeys.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -20,11 +20,11 @@ Some unit tests for HostKeys. """ -import base64 from binascii import hexlify import os import unittest import paramiko +from paramiko.py3compat import decodebytes test_hosts_file = """\ @@ -36,12 +36,12 @@ 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 +55,50 @@ 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 +106,11 @@ 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 -Nru paramiko-1.10.1/tests/test_kex_gss.py paramiko-1.15.1/tests/test_kex_gss.py --- paramiko-1.10.1/tests/test_kex_gss.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tests/test_kex_gss.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,133 @@ +# 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 +""" + + +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, 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) + 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 -Nru paramiko-1.10.1/tests/test_kex.py paramiko-1.15.1/tests/test_kex.py --- paramiko-1.10.1/tests/test_kex.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_kex.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,34 +21,40 @@ """ from binascii import hexlify +import os import unittest + import paramiko.util from paramiko.kex_group1 import KexGroup1 from paramiko.kex_gex import KexGex from paramiko import Message +from paramiko.common import byte_chr -class FakeRng (object): - def read(self, n): - return chr(0xcc) * n +def dummy_urandom(n): + return byte_chr(0xcc) * n class FakeKey (object): def __str__(self): return 'fake-key' - def sign_ssh_data(self, rng, H): - return 'fake-sig' + + def asbytes(self): + return b'fake-key' + + def sign_ssh_data(self, H): + 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 -class FakeTransport (object): - rng = FakeRng() +class FakeTransport(object): local_version = 'SSH-2.0-paramiko_1.0' remote_version = 'SSH-2.0-lame' local_kex_init = 'local-kex-init' @@ -56,41 +62,49 @@ def _send_message(self, m): self._message = m + def _expect_packet(self, *t): self._expect = t + def _set_K_H(self, K, H): self._K = K self._H = H + def _verify_key(self, host_key, sig): self._verify = (host_key, sig) + def _activate_outbound(self): self._activated = True + def _log(self, level, s): pass + def get_server_key(self): return FakeKey() + def _get_modulus_pack(self): return FakeModulusPack() class KexTest (unittest.TestCase): - K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504L + K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504 def setUp(self): - pass + self._original_urandom = os.urandom + os.urandom = dummy_urandom def tearDown(self): - pass + os.urandom = self._original_urandom def test_1_group1_client(self): transport = FakeTransport() 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 +113,47 @@ 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 +161,29 @@ 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 +191,18 @@ 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 +210,45 @@ 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 -Nru paramiko-1.10.1/tests/test_message.py paramiko-1.15.1/tests/test_message.py --- paramiko-1.10.1/tests/test_message.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_message.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -22,14 +22,15 @@ import unittest from paramiko.message import Message +from paramiko.common import byte_chr, zero_byte 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 @@ 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 -Nru paramiko-1.10.1/tests/test_packetizer.py paramiko-1.15.1/tests/test_packetizer.py --- paramiko-1.10.1/tests/test_packetizer.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_packetizer.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,50 +21,102 @@ """ import unittest -from loop import LoopSocket +from hashlib import sha1 + +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 byte_chr, zero_byte + +x55 = byte_chr(0x55) +x1f = byte_chr(0x1f) + class PacketizerTest (unittest.TestCase): - def test_1_write (self): + def test_1_write(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('\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, 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(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]) - - def test_2_read (self): + 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() rsock.link(wsock) 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, sha1, 12, x1f * 20) + wsock.send(b'\x43\x91\x97\xbd\x5b\x50\xac\x25\x87\xc2\xc4\x6b\xc7\xe9\x38\xc0\x90\xd2\x16\x56\x0d\x71\x73\x61\x38\x7c\x4c\x3d\xfb\x97\x7d\xe2\x6e\x03\xb1\xa0\xc2\x1c\xd6\x41\x41\x4c\xb4\x59') cmd, m = p.read_message() - self.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()) + + 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) diff -Nru paramiko-1.10.1/tests/test_pkey.py paramiko-1.15.1/tests/test_pkey.py --- paramiko-1.10.1/tests/test_pkey.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_pkey.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -20,17 +20,24 @@ Some unit tests for public/private key objects. """ -from binascii import hexlify, unhexlify -import StringIO import unittest -from paramiko import RSAKey, DSSKey, Message, util -from paramiko.common import rng +import os +from binascii import hexlify +from hashlib import md5 + +from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util +from paramiko.py3compat import StringIO, byte_chr, b, bytes + +from tests.util import test_path # from openssh's ssh-keygen PUB_RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=' PUB_DSS = 'ssh-dss AAAAB3NzaC1kc3MAAACBAOeBpgNnfRzr/twmAQRu2XwWAp3CFtrVnug6s6fgwj/oLjYbVtjAy6pl/h0EKCWx2rf1IetyNsTxWrniA9I6HeDj65X1FyDkg6g8tvCnaNB8Xp/UUhuzHuGsMIipRxBxw9LF608EqZcj1E3ytktoW5B5OcjrkEoz3xG7C+rpIjYvAAAAFQDwz4UnmsGiSNu5iqjn3uTzwUpshwAAAIEAkxfFeY8P2wZpDjX0MimZl5wkoFQDL25cPzGBuB4OnB8NoUk/yjAHIIpEShw8V+LzouMK5CTJQo5+Ngw3qIch/WgRmMHy4kBq1SsXMjQCte1So6HBMvBPIW5SiMTmjCfZZiw4AYHK+B/JaOwaG9yRg2Ejg4Ok10+XFDxlqZo8Y+wAAACARmR7CCPjodxASvRbIyzaVpZoJ/Z6x7dAumV+ysrV1BVYd0lYukmnjO1kKBWApqpH1ve9XDQYN8zgxM4b16L21kpoWQnZtXrY3GZ4/it9kUgyB7+NwacIBlXa8cMDL7Q/69o0d54U0X/NeX5QxuYR6OMJlrkQB7oiW/P/1mwjQgE=' +PUB_ECDSA = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJSPZm3ZWkvk/Zx8WP+fZRZ5/NBBHnGQwR6uIC6XHGPDIHuWUzIjAwA0bzqkOUffEsbLe+uQgKl5kbc/L8KA/eo=' + FINGER_RSA = '1024 60:73:38:44:cb:51:86:65:7f:de:da:a2:2b:5a:57:d5' FINGER_DSS = '1024 44:78:f0:b9:a2:3c:c5:18:20:09:ff:75:5b:c1:d2:6c' +FINGER_ECDSA = '256 25:19:eb:55:e6:a1:47:ff:4f:38:d2:75:6f:a5:d5:60' SIGNED_RSA = '20:d7:8a:31:21:cb:f7:92:12:f2:a4:89:37:f5:78:af:e6:16:b6:25:b9:97:3d:a2:cd:5f:ca:20:21:73:4c:ad:34:73:8f:20:77:28:e2:94:15:08:d8:91:40:7a:85:83:bf:18:37:95:dc:54:1a:9b:88:29:6c:73:ca:38:b4:04:f1:56:b9:f2:42:9d:52:1b:29:29:b4:4f:fd:c9:2d:af:47:d2:40:76:30:f3:63:45:0c:d9:1d:43:86:0f:1c:70:e2:93:12:34:f3:ac:c5:0a:2f:14:50:66:59:f1:88:ee:c1:4a:e9:d1:9c:4e:46:f0:0e:47:6f:38:74:f1:44:a8' RSA_PRIVATE_OUT = """\ @@ -66,6 +73,16 @@ -----END DSA PRIVATE KEY----- """ +ECDSA_PRIVATE_OUT = """\ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKB6ty3yVyKEnfF/zprx0qwC76MsMlHY4HXCnqho2eKioAoGCCqGSM49 +AwEHoUQDQgAElI9mbdlaS+T9nHxY/59lFnn80EEecZDBHq4gLpccY8Mge5ZTMiMD +ADRvOqQ5R98Sxst765CAqXmRtz8vwoD96g== +-----END EC PRIVATE KEY----- +""" + +x1234 = b'\x01\x02\x03\x04' + class KeyTest (unittest.TestCase): @@ -76,115 +93,181 @@ pass 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) - 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()) + key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + msg = key.sign_ssh_data(b'ice weasels') + self.assertTrue(type(msg) is Message) msg.rewind() - pub = RSAKey(data=str(key)) - self.assert_(pub.verify_ssh_sig('ice weasels', msg)) + 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=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(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(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(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(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.assertEqual(exp_ecdsa, my_ecdsa) + self.assertEqual(PUB_ECDSA.split()[1], key.get_base64()) + self.assertEqual(256, key.get_bits()) + + s = StringIO() + key.write_private_key(s) + self.assertEqual(ECDSA_PRIVATE_OUT, s.getvalue()) + s.seek(0) + key2 = ECDSAKey.from_private_key(s) + self.assertEqual(key, key2) + + def test_11_load_ecdsa_password(self): + 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.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(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(test_path('test_ecdsa.key')) + msg = key.sign_ssh_data(b'ice weasels') + self.assertTrue(type(msg) is Message) + msg.rewind() + 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=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(filename=newfile, password=newpassword) + self.assertEqual(key, key2) + finally: + os.remove(newfile) diff -Nru paramiko-1.10.1/tests/test_sftp_big.py paramiko-1.15.1/tests/test_sftp_big.py --- paramiko-1.10.1/tests/test_sftp_big.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_sftp_big.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -23,19 +23,15 @@ do test file operations in (so no existing files will be harmed). """ -import logging import os import random import struct import sys -import threading 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 o660 +from tests.test_sftp import get_sftp FOLDER = os.environ.get('TEST_FOLDER', 'temp-testing000') @@ -45,7 +41,7 @@ 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 +61,17 @@ 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): @@ -91,15 +85,14 @@ write a 1MB file with no buffering. """ sftp = get_sftp() - kblob = (1024 * 'x') + kblob = (1024 * b'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 +100,10 @@ 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 +115,15 @@ 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 +131,21 @@ 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 +154,14 @@ 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 +169,20 @@ 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 +190,14 @@ 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 +205,21 @@ 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: @@ -245,30 +231,28 @@ without using it, to verify that paramiko doesn't get confused. """ sftp = get_sftp() - kblob = (1024 * 'x') + kblob = (1024 * b'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 +262,33 @@ 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 + 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 - f.close() sys.stderr.write(' ') finally: sftp.remove('%s/hongry.txt' % FOLDER) @@ -317,26 +299,24 @@ 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 +328,8 @@ 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 +344,26 @@ 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 -Nru paramiko-1.10.1/tests/test_sftp.py paramiko-1.15.1/tests/test_sftp.py --- paramiko-1.10.1/tests/test_sftp.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_sftp.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -23,19 +23,22 @@ 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 socket import sys import threading import unittest -import StringIO +import warnings +from binascii import hexlify +from tempfile import mkstemp import paramiko -from stub_sftp import StubServer, StubSFTPServer -from loop import LoopSocket +from paramiko.py3compat import PY2, b, u, StringIO +from paramiko.common import o777, o600, o666, o644 +from tests.stub_sftp import StubServer, StubSFTPServer +from tests.loop import LoopSocket +from tests.util import test_path +import paramiko.util from paramiko.sftp_attr import SFTPAttributes ARTICLE = ''' @@ -65,11 +68,27 @@ decreased compared with chicken. ''' + +# Here is how unicode characters are encoded over 1 to 6 bytes in utf-8 +# U-00000000 - U-0000007F: 0xxxxxxx +# U-00000080 - U-000007FF: 110xxxxx 10xxxxxx +# U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx +# U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx +# U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx +# U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx +# Note that: hex(int('11000011',2)) == '0xc3' +# Thus, the following 2-bytes sequence is not valid utf8: "invalid continuation byte" +NON_UTF8_DATA = b'\xC3\xC3' + 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 +140,7 @@ 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 +159,7 @@ 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 +168,7 @@ pass def tearDown(self): + #sftp.chdir() sftp.rmdir(FOLDER) def test_1_file(self): @@ -158,8 +178,8 @@ 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): @@ -176,14 +196,28 @@ 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: + sftp.open(FOLDER + '/test2', 'w') + self.fail('expected exception') + except (EOFError, socket.error): + pass + 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. """ - 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 +237,17 @@ """ 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 +255,18 @@ """ 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,38 +283,52 @@ 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 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: - f = sftp.open(FOLDER + '/duck.txt', 'w') - f.close() + sftp.open(FOLDER + '/duck.txt', 'w').close() + sftp.open(FOLDER + '/fish.txt', 'w').close() + sftp.open(FOLDER + '/tertiary.py', 'w').close() - f = sftp.open(FOLDER + '/fish.txt', 'w') - f.close() + x = sftp.listdir(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') - f = sftp.open(FOLDER + '/tertiary.py', 'w') - f.close() + 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 = sftp.listdir(FOLDER) + x = [x.filename for x in sftp.listdir_iter(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 +338,21 @@ """ 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 +376,38 @@ 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) + with sftp.open(FOLDER + '/special', 'w') as f: + f.write('x' * 1024) - # 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', '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 +419,23 @@ 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,18 +444,16 @@ 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() - self.assertEqual(data, 'hello kiddy.\n') + with sftp.open(FOLDER + '/testing.txt', 'r') as f: + data = f.read(20) + self.assertEqual(data, b'hello kiddy.\n') finally: sftp.remove(FOLDER + '/testing.txt') @@ -428,16 +465,14 @@ # 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 +485,7 @@ 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 +506,16 @@ """ 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(), u('full line?\n')) + self.assertEqual(f.read(7), b'partial') finally: try: sftp.remove(FOLDER + '/happy.txt') @@ -495,10 +528,10 @@ 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 +540,19 @@ 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 +567,16 @@ 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 +598,30 @@ """ 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 +632,18 @@ (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 +651,11 @@ """ 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 +666,39 @@ """ 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 +708,62 @@ """ 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)) 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 +772,49 @@ 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) + + + def test_N_file_with_percent(self): + """ + verify that we can create a file with a '%' in the filename. + ( it needs to be properly escaped by _log() ) + """ + self.assertTrue( paramiko.util.get_logger("paramiko").handlers, "This unit test requires logging to be enabled" ) + f = sftp.open(FOLDER + '/test%file', 'w') + try: + self.assertEqual(f.stat().st_size, 0) + finally: + f.close() + sftp.remove(FOLDER + '/test%file') + + + def test_O_non_utf8_data(self): + """Test write() and read() of non utf8 data""" + try: + with sftp.open('%s/nonutf8data' % FOLDER, 'w') as f: + f.write(NON_UTF8_DATA) + with sftp.open('%s/nonutf8data' % FOLDER, 'r') as f: + data = f.read() + self.assertEqual(data, NON_UTF8_DATA) + with sftp.open('%s/nonutf8data' % FOLDER, 'wb') as f: + f.write(NON_UTF8_DATA) + with sftp.open('%s/nonutf8data' % FOLDER, 'rb') as f: + data = f.read() + self.assertEqual(data, NON_UTF8_DATA) + finally: + sftp.remove('%s/nonutf8data' % FOLDER) + + +if __name__ == '__main__': + SFTPTest.init_loopback() + # logging is required by test_N_file_with_percent + paramiko.util.log_to_file('test_sftp.log') + from unittest import main + main() diff -Nru paramiko-1.10.1/tests/test_ssh_gss.py paramiko-1.15.1/tests/test_ssh_gss.py --- paramiko-1.10.1/tests/test_ssh_gss.py 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tests/test_ssh_gss.py 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,126 @@ +# 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) +""" + +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() diff -Nru paramiko-1.10.1/tests/test_transport.py paramiko-1.15.1/tests/test_transport.py --- paramiko-1.10.1/tests/test_transport.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_transport.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -20,23 +20,27 @@ Some unit tests for the ssh2 protocol in Transport. """ -from binascii import hexlify, unhexlify +from __future__ import with_statement + +from binascii import hexlify import select import socket -import sys import time import threading -import unittest import random +import unittest from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \ - SSHException, BadAuthenticationType, InteractiveQuery, ChannelException -from paramiko import AUTH_FAILED, AUTH_PARTIALLY_SUCCESSFUL, AUTH_SUCCESSFUL + SSHException, ChannelException +from paramiko import AUTH_FAILED, 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, 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 loop import LoopSocket -from util import ParamikoTest +from tests.loop import LoopSocket +from tests.util import test_path LONG_BANNER = """\ @@ -55,8 +59,8 @@ 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' @@ -79,24 +83,24 @@ 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 @@ -106,7 +110,7 @@ return OPEN_SUCCEEDED -class TransportTest(ParamikoTest): +class TransportTest(unittest.TestCase): def setUp(self): self.socks = LoopSocket() self.sockc = LoopSocket() @@ -121,48 +125,48 @@ 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: 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.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,45 +175,45 @@ 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): """ verify that the client can demand odd handshake settings, and can @@ -219,11 +223,11 @@ 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() self.ts.send_ignore(1024) @@ -233,11 +237,11 @@ 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): """ verify that exec_command() does something reasonable. @@ -248,10 +252,10 @@ 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() chan.exec_command('yes') schan = self.ts.accept(1.0) @@ -260,12 +264,12 @@ 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() chan.exec_command('yes') @@ -274,11 +278,27 @@ schan.send_stderr('This is on stderr.\n') schan.close() - chan.set_combine_stderr(True) + 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_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): """ @@ -290,9 +310,9 @@ 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 +322,8 @@ 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,23 +335,23 @@ 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() schan.send_exit_status(23) 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): @@ -343,52 +363,52 @@ 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.assertEquals([], r) - self.assertEquals([], w) - self.assertEquals([], e) - + 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) 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.assertEqual(b'hello\n', chan.recv(6)) - self.assertEquals('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() - + # detect eof? for i in range(10): r, w, e = select.select([chan], [], [], 0.1) 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): """ verify that a transport can correctly renegotiate mid-stream. @@ -399,17 +419,17 @@ 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 +448,8 @@ 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() @@ -442,27 +462,28 @@ chan = self.tc.open_session() chan.exec_command('yes') 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() chan.close() @@ -477,29 +498,29 @@ chan = self.tc.open_session() chan.exec_command('yes') 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)) ss, _ = self.server._listen.accept() sch = self.ts.open_forwarded_tcpip_channel(ss.getsockname(), ss.getpeername()) 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() cs.close() - + # now cancel it. self.tc.cancel_port_forward('127.0.0.1', port) self.assertTrue(self.server._listen is None) @@ -513,7 +534,7 @@ 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,14 +545,14 @@ sch = self.ts.accept(1.0) cch = socket.socket() 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): @@ -544,31 +565,31 @@ 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.assertEquals([], r) - self.assertEquals([], w) - self.assertEquals([], e) - + 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) 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.assertEqual(b'hello\n', chan.recv_stderr(6)) - self.assertEquals('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,27 +603,28 @@ 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: + limit = 1+(64 * 2 ** 15) + while total < limit: chan.send(K) total += len(K) if not chan.send_ready(): break - self.assert_(total < 1024 * 1024) + self.assertTrue(total < limit) schan.close() chan.close() - self.assertEquals(chan.send_ready(), True) + self.assertEqual(chan.send_ready(), True) 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 +641,7 @@ # 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,10 +676,10 @@ self.done_event = done_event self.watchdog_event = threading.Event() self.last = None - + 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() @@ -666,7 +688,7 @@ 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 +696,7 @@ 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 +709,10 @@ 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) @@ -706,13 +728,13 @@ # 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) 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 +746,12 @@ # 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 +762,7 @@ 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). @@ -752,3 +774,21 @@ # 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) diff -Nru paramiko-1.10.1/tests/test_util.py paramiko-1.15.1/tests/test_util.py --- paramiko-1.10.1/tests/test_util.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/test_util.py 2014-09-22 18:31:58.000000000 +0000 @@ -7,7 +7,7 @@ # 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 +# 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. @@ -21,15 +21,14 @@ """ from binascii import hexlify -import cStringIO import errno import os +from hashlib import sha1 import unittest -from Crypto.Hash import SHA + import paramiko.util from paramiko.util import lookup_ssh_host_config as host_config - -from util import ParamikoTest +from paramiko.py3compat import StringIO, byte_ord test_config_file = """\ Host * @@ -41,7 +40,12 @@ \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 """ @@ -60,12 +64,12 @@ 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. """ - symbols = globals().keys() + symbols = list(globals().keys()) self.assertTrue('Transport' in symbols) self.assertTrue('SSHClient' in symbols) self.assertTrue('MissingHostKeyPolicy' in symbols) @@ -101,9 +105,9 @@ 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 +115,7 @@ 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,36 +135,29 @@ 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(sha1, 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') - def test_6_random(self): - from paramiko.common import rng - # just verify that we can pull out 32 bytes and not get an exception. - x = rng.read(32) - self.assertEquals(len(x), 32) - def test_7_host_config_expose_issue_33(self): test_config_file = """ Host www13.* @@ -172,16 +169,16 @@ 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 +189,8 @@ 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 +213,10 @@ 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 +225,7 @@ """ 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 +242,7 @@ ('specific', "host specific port 37 lol"), ('portonly', "host portonly port 155"), ): - self.assertEquals( + self.assertEqual( host_config(host, config)['proxycommand'], val ) @@ -264,10 +261,10 @@ 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 +290,9 @@ '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 +320,131 @@ '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.assertEqual( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_12_config_addressfamily_and_lazy_fqdn(self): + """ + Ensure the code path honoring non-'all' AddressFamily doesn't asplode + """ + test_config = """ +AddressFamily inet +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_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)) + + 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") + + 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 ) + + 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) diff -Nru paramiko-1.10.1/tests/util.py paramiko-1.15.1/tests/util.py --- paramiko-1.10.1/tests/util.py 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tests/util.py 2014-09-22 18:31:58.000000000 +0000 @@ -1,10 +1,7 @@ -import unittest +import os +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) diff -Nru paramiko-1.10.1/tox.ini paramiko-1.15.1/tox.ini --- paramiko-1.10.1/tox.ini 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/tox.ini 2014-09-22 18:31:58.000000000 +0000 @@ -1,6 +1,6 @@ [tox] -envlist = py25,py26,py27 +envlist = py26,py27,py32,py33,py34 [testenv] -commands = pip install --use-mirrors -q -r requirements.txt +commands = pip install --use-mirrors -q -r tox-requirements.txt python test.py diff -Nru paramiko-1.10.1/tox-requirements.txt paramiko-1.15.1/tox-requirements.txt --- paramiko-1.10.1/tox-requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ paramiko-1.15.1/tox-requirements.txt 2014-09-22 18:31:58.000000000 +0000 @@ -0,0 +1,2 @@ +# Not sure why tox can't just read setup.py? +pycrypto diff -Nru paramiko-1.10.1/.travis.yml paramiko-1.15.1/.travis.yml --- paramiko-1.10.1/.travis.yml 2013-04-05 20:00:19.000000000 +0000 +++ paramiko-1.15.1/.travis.yml 2014-09-22 18:31:58.000000000 +0000 @@ -1,14 +1,33 @@ language: python python: - - "2.5" - "2.6" - "2.7" + - "3.2" + - "3.3" + - "3.4" install: # Self-install for setup.py-driven deps - pip install -e . -script: python test.py + # Dev (doc/test running) requirements + - pip install coveralls # For coveralls.io specifically + - pip install -r dev-requirements.txt +script: + # Main tests, with coverage! + - 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 + # problems. + # Finally, skip them under Python 3.2 due to sphinx shenanigans + - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke docs -o -W || true" + - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke www -o -W || true" notifications: irc: channels: "irc.freenode.org#paramiko" + template: + - "%{repository}@%{branch}: %{message} (%{build_url})" on_success: change on_failure: change + email: false +after_success: + - coveralls