diff options
91 files changed, 2655 insertions, 657 deletions
@@ -1,4 +1,9 @@ *.pyc build/ dist/ +.tox/ paramiko.egg-info/ +test.log +docs/ +!sites/docs +_build diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..df7c225a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +language: python +python: + - "2.6" + - "2.7" +install: + # Self-install for setup.py-driven deps + - pip install -e . + # Dev (doc/test running) requirements + - pip install coveralls # For coveralls.io specifically + - pip install -r dev-requirements.txt +script: + # Main tests, with coverage! + - coverage run --source=paramiko test.py --verbose + # 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. + - invoke docs -o -W + - invoke www -o -W +notifications: + irc: + channels: "irc.freenode.org#paramiko" + template: + - "%{repository}@%{branch}: %{message} (%{build_url})" + on_success: change + on_failure: change + use_notice: true + email: false +after_success: + - coveralls @@ -2,7 +2,7 @@ Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -485,7 +485,7 @@ convey the exclusion of warranty; and each file should have at least the You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA Also add information on how to contact you by electronic and paper mail. @@ -1,59 +1,8 @@ -# releases: -# aerodactyl (13sep03) -# bulbasaur (18sep03) -# charmander (10nov03) -# doduo (04jan04) - 0.9 -# eevee (08mar04) -# fearow (23apr04) -# gyarados (31may04) -# horsea (27jun04) -# ivysaur (22oct04) -# jigglypuff (6nov04) - 1.0 -# kabuto (12dec04) - 1.1 -# lapras (28feb05) - 1.2 -# marowak (9apr05) - 1.3 -# nidoran (28jun05) - 1.3.1 -# oddish (17jul05) - 1.4 -# paras (2oct05) - 1.5 -# quilava (31oct05) - 1.5.1 -# rhydon (04dec05) - 1.5.2 -# squirtle (19feb06) - 1.5.3 -# tentacool (11mar06) - 1.5.4 -# umbreon (10may06) - 1.6 -# vulpix (10jul06) - 1.6.1 -# weedle (16aug06) - 1.6.2 -# xatu (14oct06) - 1.6.3 -# yanma (19nov06) - 1.6.4 -# zubat (18feb07) - 1.7 -# amy (10jun07) - 1.7.1 -# basil (21jan08) - 1.7.2 -# clara (23mar08) - 1.7.3 -# desmond (06jul08) - 1.7.4 -# ernest (19jul09) - 1.7.5 -# fanny (1nov09) - 1.7.6 -# george (21may11) - 1.7.7.1 - - -ifeq ($(wildcard /sbin/md5),/sbin/md5) -# os x -MD5SUM := /sbin/md5 -else -MD5SUM := md5sum -endif - release: docs - python ./setup.py sdist --formats=zip - python ./setup.py sdist --formats=gztar - python ./setup.py bdist_egg - zip -r dist/docs.zip docs && rm -rf docs - cd dist && $(MD5SUM) paramiko*.zip *.gz > md5-sums - cd dist && gpg -ba paramiko*.zip - cd dist && gpg -ba paramiko*.gz + python setup.py sdist register upload - -docs: always +docs: paramiko/* epydoc --no-private -o docs/ paramiko -always: clean: rm -rf build dist docs @@ -64,16 +13,3 @@ clean: test: python ./test.py - -# places where the version number is stored: -# -# setup.py -# __init__.py -# README -# transport.py -# -# TRY on windows vm -# POST gpg sig on website! -# POST md5sum on website! -# RUN setup.py -# DO the push to pypy @@ -3,11 +3,98 @@ NEWS ==== -Highlights of what's new in each release: +Highlights of what's new in each release. + +Issues noted as "'ssh' #NN" can be found at https://github.com/bitprophet/ssh/. + +Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/. + + +**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 + Releases ======== +v1.9.0 (6th Nov 2012) +--------------------- + +* #97 (with a little #93): Improve config parsing of `ProxyCommand` directives + and provide a wrapper class to allow subprocess-driven proxy commands to be + used as `sock=` arguments for `SSHClient.connect`. +* #77: Allow `SSHClient.connect()` to take an explicit `sock` parameter + overriding creation of an internal, implicit socket object. +* Thanks in no particular order to Erwin Bolwidt, Oskari Saarenmaa, Steven + Noonan, Vladimir Lazarenko, Lincoln de Sousa, Valentino Volonghi, Olle + Lundberg, and Github user `@acrish` for the various and sundry patches + leading to the above changes. + +v1.8.1 (6th Nov 2012) +--------------------- + +* #90: Ensure that callbacks handed to `SFTPClient.get()` always fire at least + once, even for zero-length files downloaded. Thanks to Github user `@enB` for + the catch. +* #85: Paramiko's test suite overrides + `unittest.TestCase.assertTrue/assertFalse` to provide these modern assertions + to Python 2.2/2.3, which lacked them. However on newer Pythons such as 2.7, + this now causes deprecation warnings. The overrides have been patched to only + execute when necessary. Thanks to `@Arfrever` for catch & patch. + + +v1.8.0 (3rd Oct 2012) +--------------------- + +* #17 ('ssh' 28): Fix spurious `NoneType has no attribute 'error'` and similar + exceptions that crop up on interpreter exit. +* 'ssh' 32: Raise a more useful error explaining which `known_hosts` key line was + problematic, when encountering `binascii` issues decoding known host keys. + Thanks to `@thomasvs` for catch & patch. +* 'ssh' 33: Bring `ssh_config` parsing more in line with OpenSSH spec, re: order of + setting overrides by `Host` specifiers. Specifically, the overrides now go by + file order instead of automatically sorting by `Host` value length. In + addition, the first value found per config key (e.g. `Port`, `User` etc) + wins, instead of the last. Thanks to Jan Brauer for the contribution. +* 'ssh' 36: Support new server two-factor authentication option + (`RequiredAuthentications2`), at least re: combining key-based & password + auth. Thanks to Github user `bninja`. +* 'ssh' 11: When raising an exception for hosts not listed in + `known_hosts` (when `RejectPolicy` is in effect) the exception message was + confusing/vague. This has been improved somewhat. Thanks to Cal Leeming for + highlighting the issue. +* 'ssh' 40: Fixed up & expanded EINTR signal handling. Thanks to Douglas Turk. +* 'ssh' 15: Implemented parameter substitution in SSHConfig, matching the + implementation of `ssh_config(5)`. Thanks to Olle Lundberg for the patch. +* 'ssh' 24: Switch some internal type checking to use `isinstance` to help prevent + problems with client libraries using subclasses of builtin types. Thanks to + Alex Morega for the patch. +* Fabric #562: Agent forwarding would error out (with `Authentication response + too long`) or freeze, when more than one remote connection to the local agent + was active at the same time. This has been fixed. Thanks to Steven McDonald + for assisting in troubleshooting/patching, and to GitHub user `@lynxis` for + providing the final version of the patch. +* 'ssh' 5: Moved a `fcntl` import closer to where it's used to help avoid + `ImportError` problems on Windows platforms. Thanks to Jason Coombs for the + catch + suggested fix. +* 'ssh' 4: Updated implementation of WinPageant integration to work on 64-bit + Windows. Thanks again to Jason Coombs for the patch. +* Added an IO loop sleep() call to avoid needless CPU usage when agent + forwarding is in use. +* Handful of internal tweaks to version number storage. +* Updated `setup.py` with `==dev` install URL for `pip` users. +* Updated `setup.py` to account for packaging problems in PyCrypto 2.4.0 +* Added an extra `atfork()` call to help prevent spurious RNG errors when + running under high parallel (multiprocess) load. +* Merge PR #28: https://github.com/paramiko/paramiko/pull/28 which adds a + ssh-keygen like demo module. (Sofian Brabez) + +v1.7.7.2 16may12 +---------------- + * Merge pull request #63: https://github.com/paramiko/paramiko/pull/63 which + fixes exceptions that occur when re-keying over fast connections. (Dwayne + Litzenberger) + v1.7.7.1 (George) 21may11 ------------------------- * Make the verification phase of SFTP.put optional (Larry Wright) @@ -204,7 +291,7 @@ v1.5 (paras) 02oct05 separation * demo scripts fixed to have a better chance of loading the host keys correctly on windows/cygwin - + v1.4 (oddish) 17jul05 --------------------- * added SSH-agent support (for posix) from john rochester @@ -5,21 +5,17 @@ paramiko :Paramiko: Python SSH module :Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com> +:Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org> :License: LGPL -:Homepage: http://www.lag.net/paramiko/ - - -paramiko 1.7.7.1 -================ - -"George" release, 21 may 2011 +: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.2+ that implements the SSH2 protocol +"friend". it's a module for python 2.5+ 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 @@ -38,8 +34,7 @@ that should have come with this archive. Requirements ------------ - - python 2.3 or better <http://www.python.org/> - (python 2.2 is also supported, but not recommended) + - python 2.5 or better <http://www.python.org/> - pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/> If you have setuptools, you can build and install paramiko and all its @@ -57,19 +52,6 @@ should also work on Windows, though i don't test it as frequently there. if you run into Windows problems, send me a patch: portability is important to me. -python 2.2 may work, thanks to some patches from Roger Binns. things to -watch out for: - - * sockets in 2.2 don't support timeouts, so the 'select' module is - imported to do polling. - * logging is mostly stubbed out. it works just enough to let paramiko - create log files for debugging, if you want them. to get real logging, - you can backport python 2.3's logging package. Roger has done that - already: - http://sourceforge.net/project/showfiles.php?group_id=75211&package_id=113804 - -you really should upgrade to python 2.3. laziness is no excuse! :) - some python distributions don't include the utf-8 string encodings, for reasons of space (misdirected as that is). if your distribution is missing encodings, you'll see an error like this:: @@ -85,15 +67,7 @@ installs.) Valeriy Pogrebitskiy says the best place to look is Bugs & Support -------------- -there's a launchpage page for paramiko, with a bug tracker: - - https://launchpad.net/paramiko/ - -this is the primary place to file and browse bug reports. - -there's also a low-traffic mailing list for support and discussions: - - http://www.lag.net/mailman/listinfo/paramiko +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. Demo @@ -141,6 +115,9 @@ the following example scripts (in demos/) get progressively more detailed: 'robey' (password 'foo'), and pretends to be a BBS. meant to be a very simple demo of writing an ssh server. +:demo_keygen.py: + an key generator similar to openssh ssh-keygen(1) program with + paramiko keys generation and progress functions. Use --- @@ -1,22 +1,3 @@ - -TO-DO -===== - -* FIXME: use the locking scheme from java for channel.py - -* allow setting chmod bits on SFTPClient.open() for create -* host-based auth (yuck!) -* ctr forms of ciphers are missing (blowfish-ctr, aes128-ctr, aes256-ctr) -* sftp protocol 6 support (ugh....) -- once it settles down more -* make a simple example demonstrating use of SocketServer (besides forward.py?) -* should SSHClient try to use openssh config files? -* figure out how to parse ssh.com encrypted key files? -* is it possible to poll on a set of events at once? -* potentially create only one thread shared by all Transports -* SSHClient: flag to turn off agent detection? flag to turn off keyfile discovery? - -* vault: sftp server + bazaar - - single person can open file for writing at one time - - atomic rename @ file close - - bzr commit after close - +* Change license to BSD for v1.8 (obtain permission from Robey) +* Pending that, remove preamble from all files, ensure LICENSE is still correct +* Update version stuff: use an execfile'd paramiko/_version.py diff --git a/demos/demo.py b/demos/demo.py index 05524d3c..aa4bdaa5 100755 --- a/demos/demo.py +++ b/demos/demo.py @@ -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,7 +26,6 @@ import os import select import socket import sys -import threading import time import traceback diff --git a/demos/demo_keygen.py b/demos/demo_keygen.py new file mode 100755 index 00000000..bdd7388d --- /dev/null +++ b/demos/demo_keygen.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +# Copyright (C) 2010 Sofian Brabez <sbz@6dev.net> +# +# 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. +from __future__ import with_statement + +import string +import sys + +from binascii import hexlify +from optparse import OptionParser + +from paramiko import DSSKey +from paramiko import RSAKey +from paramiko.ssh_exception import SSHException + +usage=""" +%prog [-v] [-b bits] -t type [-N new_passphrase] [-f output_keyfile]""" + +default_values = { + "ktype": "dsa", + "bits": 1024, + "filename": "output", + "comment": "" +} + +key_dispatch_table = { + 'dsa': DSSKey, + 'rsa': RSAKey, +} + +def progress(arg=None): + + if not arg: + print '0%\x08\x08\x08', + sys.stdout.flush() + elif arg[0] == 'p': + print '25%\x08\x08\x08\x08', + sys.stdout.flush() + elif arg[0] == 'h': + print '50%\x08\x08\x08\x08', + sys.stdout.flush() + elif arg[0] == 'x': + print '75%\x08\x08\x08\x08', + sys.stdout.flush() + +if __name__ == '__main__': + + phrase=None + pfunc=None + + parser = OptionParser(usage=usage) + parser.add_option("-t", "--type", type="string", dest="ktype", + help="Specify type of key to create (dsa or rsa)", + metavar="ktype", default=default_values["ktype"]) + parser.add_option("-b", "--bits", type="int", dest="bits", + help="Number of bits in the key to create", metavar="bits", + default=default_values["bits"]) + parser.add_option("-N", "--new-passphrase", dest="newphrase", + help="Provide new passphrase", metavar="phrase") + parser.add_option("-P", "--old-passphrase", dest="oldphrase", + help="Provide old passphrase", metavar="phrase") + parser.add_option("-f", "--filename", type="string", dest="filename", + help="Filename of the key file", metavar="filename", + default=default_values["filename"]) + parser.add_option("-q", "--quiet", default=False, action="store_false", + help="Quiet") + parser.add_option("-v", "--verbose", default=False, action="store_true", + help="Verbose") + parser.add_option("-C", "--comment", type="string", dest="comment", + help="Provide a new comment", metavar="comment", + default=default_values["comment"]) + + (options, args) = parser.parse_args() + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(0) + + for o in default_values.keys(): + globals()[o] = getattr(options, o, default_values[string.lower(o)]) + + if options.newphrase: + phrase = getattr(options, 'newphrase') + + if options.verbose: + pfunc = progress + sys.stdout.write("Generating priv/pub %s %d bits key pair (%s/%s.pub)..." % (ktype, bits, filename, filename)) + sys.stdout.flush() + + if ktype == 'dsa' and bits > 1024: + raise SSHException("DSA Keys must be 1024 bits") + + if not key_dispatch_table.has_key(ktype): + raise SSHException("Unknown %s algorithm to generate keys pair" % ktype) + + # generating private key + prv = key_dispatch_table[ktype].generate(bits=bits, progress_func=pfunc) + prv.write_private_key_file(filename, password=phrase) + + # generating public key + pub = key_dispatch_table[ktype](filename=filename, password=phrase) + with open("%s.pub" % filename, 'w') as f: + f.write("%s %s" % (pub.get_name(), pub.get_base64())) + if options.comment: + f.write(" %s" % comment) + + if options.verbose: + 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)) diff --git a/demos/demo_server.py b/demos/demo_server.py index 4972928d..915b0c67 100644 --- a/demos/demo_server.py +++ b/demos/demo_server.py @@ -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. diff --git a/demos/demo_sftp.py b/demos/demo_sftp.py index 992615f5..7c4aaba0 100755 --- a/demos/demo_sftp.py +++ b/demos/demo_sftp.py @@ -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. diff --git a/demos/demo_simple.py b/demos/demo_simple.py index 231da8df..50f344a7 100755 --- a/demos/demo_simple.py +++ b/demos/demo_simple.py @@ -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. @@ -63,7 +63,7 @@ password = getpass.getpass('Password for %s@%s: ' % (username, hostname)) try: client = paramiko.SSHClient() client.load_system_host_keys() - client.set_missing_host_key_policy(paramiko.WarningPolicy) + client.set_missing_host_key_policy(paramiko.WarningPolicy()) print '*** Connecting...' client.connect(hostname, port, username, password) chan = client.invoke_shell() diff --git a/demos/forward.py b/demos/forward.py index 4e107855..5048c775 100644 --- a/demos/forward.py +++ b/demos/forward.py @@ -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. @@ -78,9 +78,11 @@ class Handler (SocketServer.BaseRequestHandler): 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): diff --git a/demos/interactive.py b/demos/interactive.py index 4cbc6171..f3be74d2 100644 --- a/demos/interactive.py +++ b/demos/interactive.py @@ -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 --git a/demos/rforward.py b/demos/rforward.py index ef4c5329..4a5d2e43 100755 --- a/demos/rforward.py +++ b/demos/rforward.py @@ -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. diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..e9770f4f --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,7 @@ +# For newer tasks like building Sphinx docs. +# NOTE: Requires Python >=2.6 +invoke>=0.7.0 +invocations>=0.4.4 +sphinx>=1.1.3 +alabaster>=0.3.0 +releases>=0.2.4 diff --git a/fabfile.py b/fabfile.py new file mode 100644 index 00000000..29394f94 --- /dev/null +++ b/fabfile.py @@ -0,0 +1,13 @@ +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 --git a/paramiko/__init__.py b/paramiko/__init__.py index 96b5943f..924e8bb5 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -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,7 +18,7 @@ """ I{Paramiko} (a combination of the esperanto words for "paranoid" and "friend") -is a module for python 2.3 or greater that implements the SSH2 protocol for +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 @@ -45,24 +45,18 @@ 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{http://www.lag.net/paramiko/} - -@version: 1.7.7.1 (George) -@author: Robey Pointer -@contact: robeypointer@gmail.com -@license: GNU Lesser General Public License (LGPL) +Website: U{https://github.com/paramiko/paramiko/} """ import sys -if sys.version_info < (2, 2): - raise RuntimeError('You need python 2.2 for this module.') +if sys.version_info < (2, 5): + raise RuntimeError('You need python 2.5+ for this module.') -__author__ = "Robey Pointer <robeypointer@gmail.com>" -__date__ = "21 May 2011" -__version__ = "1.7.7.1 (George)" -__version_info__ = (1, 7, 7, 1) +__author__ = "Jeff Forcier <jeff@bitprophet.org>" +__version__ = "1.10.5" +__version_info__ = tuple([ int(d) for d in __version__.split(".") ]) __license__ = "GNU Lesser General Public License (LGPL)" @@ -72,7 +66,7 @@ from auth_handler import AuthHandler from channel import Channel, ChannelFile from ssh_exception import SSHException, PasswordRequiredException, \ BadAuthenticationType, ChannelException, BadHostKeyException, \ - AuthenticationException + AuthenticationException, ProxyCommandFailure from server import ServerInterface, SubsystemHandler, InteractiveQuery from rsakey import RSAKey from dsskey import DSSKey @@ -90,6 +84,7 @@ 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(): @@ -105,6 +100,8 @@ from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ 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 common import io_sleep + __all__ = [ 'Transport', 'SSHClient', 'MissingHostKeyPolicy', @@ -124,6 +121,8 @@ __all__ = [ 'Transport', 'BadAuthenticationType', 'ChannelException', 'BadHostKeyException', + 'ProxyCommand', + 'ProxyCommandFailure', 'SFTP', 'SFTPFile', 'SFTPHandle', @@ -138,4 +137,5 @@ __all__ = [ 'Transport', 'AgentKey', 'HostKeys', 'SSHConfig', - 'util' ] + 'util', + 'io_sleep' ] diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py new file mode 100644 index 00000000..b8759245 --- /dev/null +++ b/paramiko/_winapi.py @@ -0,0 +1,274 @@ +""" +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 +import __builtin__ + +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 + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(__builtin__.WindowsError): + "more info about errors at http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx" + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + super(WindowsError, self).__init__(value, strerror) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return '{self.__class__.__name__}({self.winerror})'.format(**vars()) + +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, + unicode(self.name)) + handle_nonzero_success(filemap) + if filemap == INVALID_HANDLE_VALUE: + raise Exception("Failed to create file mapping") + self.filemap = filemap + self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) + return self + + def seek(self, pos): + self.pos = pos + + def write(self, msg): + 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 --git a/paramiko/agent.py b/paramiko/agent.py index 3bb94261..23a5a2e4 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -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. @@ -24,39 +24,308 @@ import os import socket import struct import sys +import threading +import time +import tempfile +import stat +from select import select 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) +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). + """ + def __init__(self): + self._conn = None + self._keys = () + + def get_keys(self): + """ + Return the list of keys available through the SSH agent, if any. If + 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 self._keys + + def _connect(self, conn): + self._conn = conn + ptype, result = self._send_message(chr(SSH2_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())) + result.get_string() + self._keys = tuple(keys) + + def _close(self): + #self._conn.close() + self._conn = None + self._keys = () + + def _send_message(self, msg): + msg = str(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])) + return ord(msg.get_byte()), msg + + def _read_all(self, wanted): + result = self._conn.recv(wanted) + while len(result) < wanted: + if len(result) == 0: + raise SSHException('lost ssh-agent') + extra = self._conn.recv(wanted - len(result)) + if len(extra) == 0: + raise SSHException('lost ssh-agent') + result += extra + return result + +class AgentProxyThread(threading.Thread): + """ Class in charge of communication between two chan """ + def __init__(self, agent): + threading.Thread.__init__(self, target=self.run) + self._agent = agent + self._exit = False + + def run(self): + try: + (r,addr) = self.get_connection() + self.__inr = r + self.__addr = addr + self._agent.connect() + self._communicate() + except: + #XXX Not sure what to do here ... raise or pass ? + raise + + def _communicate(self): + import fcntl + oldflags = fcntl.fcntl(self.__inr, fcntl.F_GETFL) + fcntl.fcntl(self.__inr, fcntl.F_SETFL, oldflags | os.O_NONBLOCK) + while not self._exit: + events = select([self._agent._conn, self.__inr], [], [], 0.5) + for fd in events[0]: + if self._agent._conn == fd: + data = self._agent._conn.recv(512) + if len(data) != 0: + self.__inr.send(data) + else: + self._close() + break + elif self.__inr == fd: + data = self.__inr.recv(512) + if len(data) != 0: + self._agent._conn.send(data) + else: + self._close() + break + time.sleep(io_sleep) + + def _close(self): + self._exit = True + self.__inr.close() + self._agent._conn.close() + +class AgentLocalProxy(AgentProxyThread): + """ + 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.) + """ + def __init__(self, agent): + AgentProxyThread.__init__(self, agent) + + def get_connection(self): + """ 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) + except: + raise + return None -class Agent: +class AgentRemoteProxy(AgentProxyThread): + """ + Class to be used when wanting to ask a remote SSH Agent + """ + def __init__(self, agent, chan): + AgentProxyThread.__init__(self, agent) + 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) + +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, + calling the forward_agent_handler at client side. + -> the forward_agent_handler launch a thread for connecting + the remote fake agent and the local agent + -> Communication occurs ... + """ + def __init__(self, chanRemote): + self._conn = None + self.__chanR = chanRemote + self.thread = AgentRemoteProxy(self, chanRemote) + self.thread.start() + + def __del__(self): + self.close() + + def connect(self): + """ + Method automatically called by the run() method of the AgentProxyThread + """ + if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + retry_on_signal(lambda: conn.connect(os.environ['SSH_AUTH_SOCK'])) + except: + # probably a dangling env var: the ssh agent is gone + return + elif sys.platform == 'win32': + import win_pageant + if win_pageant.can_talk_to_agent(): + conn = win_pageant.PageantConnection() + else: + return + else: + # no agent support + return + self._conn = conn + + def close(self): + """ + Close the current connection and terminate the agent + Should be called manually + """ + if hasattr(self, "thread"): + self.thread._exit = True + self.thread.join(1000) + if self._conn is not None: + self._conn.close() + +class AgentServerProxy(AgentSSH): + """ + @param t : transport used for the Forward for SSH Agent communication + + @raise SSHException: mostly if we lost the agent + """ + def __init__(self, t): + AgentSSH.__init__(self) + self.__t = t + self._dir = tempfile.mkdtemp('sshproxy') + os.chmod(self._dir, stat.S_IRWXU) + self._file = self._dir + '/sshproxy.ssh' + self.thread = AgentLocalProxy(self) + self.thread.start() + + def __del__(self): + 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) + + def close(self): + """ + Terminate the agent, clean the files, close connections + Should be called manually + """ + os.remove(self._file) + os.rmdir(self._dir) + self.thread._exit = True + self.thread.join(1000) + self._close() + + def get_env(self): + """ + Helper for the environnement under unix + + @return: the SSH_AUTH_SOCK Environnement variables + @rtype: dict + """ + env = {} + env['SSH_AUTH_SOCK'] = self._get_filename() + return env + + def _get_filename(self): + return self._file + +class AgentRequestHandler(object): + def __init__(self, chanClient): + self._conn = None + self.__chanC = chanClient + chanClient.request_forward_agent(self._forward_agent_handler) + self.__clientProxys = [] + + def _forward_agent_handler(self, chanRemote): + self.__clientProxys.append(AgentClientProxy(chanRemote)) + + def __del__(self): + self.close() + + def close(self): + 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 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). """ - + 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 """ - self.conn = None - self.keys = () + AgentSSH.__init__(self) + if ('SSH_AUTH_SOCK' in os.environ) and (sys.platform != 'win32'): conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: @@ -64,64 +333,22 @@ class Agent: except: # probably a dangling env var: the ssh agent is gone return - self.conn = conn elif sys.platform == 'win32': import win_pageant if win_pageant.can_talk_to_agent(): - self.conn = win_pageant.PageantConnection() + conn = win_pageant.PageantConnection() else: return else: # no agent support return - - ptype, result = self._send_message(chr(SSH2_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())) - result.get_string() - self.keys = tuple(keys) + self._connect(conn) def close(self): """ Close the SSH agent connection. """ - if self.conn is not None: - self.conn.close() - self.conn = None - self.keys = () - - def get_keys(self): - """ - Return the list of keys available through the SSH agent, if any. If - 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 self.keys - - def _send_message(self, msg): - msg = str(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])) - return ord(msg.get_byte()), msg - - def _read_all(self, wanted): - result = self.conn.recv(wanted) - while len(result) < wanted: - if len(result) == 0: - raise SSHException('lost ssh-agent') - extra = self.conn.recv(wanted - len(result)) - if len(extra) == 0: - raise SSHException('lost ssh-agent') - result += extra - return result - + self._close() class AgentKey(PKey): """ @@ -129,7 +356,7 @@ class AgentKey(PKey): authenticating to a remote server (signing). Most other key operations work as expected. """ - + def __init__(self, agent, blob): self.agent = agent self.blob = blob diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index e3bd82d4..acb7c8b8 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -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 --git a/paramiko/ber.py b/paramiko/ber.py index 19568dd5..3941581c 100644 --- a/paramiko/ber.py +++ b/paramiko/ber.py @@ -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 --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py index b19d74b2..4ef5cf74 100644 --- a/paramiko/buffered_pipe.py +++ b/paramiko/buffered_pipe.py @@ -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 --git a/paramiko/channel.py b/paramiko/channel.py index 6d895fe4..c680e44b 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -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. @@ -122,7 +122,8 @@ class Channel (object): out += '>' return out - def get_pty(self, term='vt100', width=80, height=24): + 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 @@ -136,6 +137,10 @@ class Channel (object): @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 @@ -150,8 +155,8 @@ class Channel (object): m.add_string(term) m.add_int(width) m.add_int(height) - # pixel height, width (usually useless) - m.add_int(0).add_int(0) + m.add_int(width_pixels) + m.add_int(height_pixels) m.add_string('') self._event_pending() self.transport._send_user_message(m) @@ -239,7 +244,7 @@ class Channel (object): self.transport._send_user_message(m) self._wait_for_event() - def resize_pty(self, width=80, height=24): + 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. @@ -248,6 +253,10 @@ class Channel (object): @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 @raise SSHException: if the request was rejected or the channel was closed @@ -258,13 +267,12 @@ class Channel (object): m.add_byte(chr(MSG_CHANNEL_REQUEST)) m.add_int(self.remote_chanid) m.add_string('window-change') - m.add_boolean(True) + m.add_boolean(False) m.add_int(width) m.add_int(height) - m.add_int(0).add_int(0) - self._event_pending() + m.add_int(width_pixels) + m.add_int(height_pixels) self.transport._send_user_message(m) - self._wait_for_event() def exit_status_ready(self): """ @@ -381,6 +389,31 @@ class Channel (object): self.transport._set_x11_handler(handler) return auth_cookie + 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 !!! + + @param handler: a required handler to use for incoming SSH Agent connections + @type handler: function + + @return: if we are ok or not (at that time we always return ok) + @rtype: boolean + + @raise: 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_int(self.remote_chanid) + m.add_string('auth-agent-req@openssh.com') + m.add_boolean(False) + self.transport._send_user_message(m) + self.transport._set_forward_agent_handler(handler) + return True + def get_transport(self): """ Return the L{Transport} associated with this channel. @@ -1026,6 +1059,11 @@ class Channel (object): else: ok = server.check_channel_x11_request(self, single_connection, auth_proto, auth_cookie, screen_number) + elif key == 'auth-agent-req@openssh.com': + if server is None: + ok = False + else: + ok = server.check_channel_forward_agent_request(self) else: self._log(DEBUG, 'Unhandled channel request "%s"' % key) ok = False diff --git a/paramiko/client.py b/paramiko/client.py index 4a65477d..be896091 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -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,16 +28,16 @@ import warnings from paramiko.agent import Agent from paramiko.common import * +from paramiko.config import SSH_PORT from paramiko.dsskey import DSSKey from paramiko.hostkeys import HostKeys 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 -SSH_PORT = 22 - class MissingHostKeyPolicy (object): """ Interface for defining the policy that L{SSHClient} should use when the @@ -82,7 +82,7 @@ class RejectPolicy (MissingHostKeyPolicy): 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('Unknown server %s' % hostname) + raise SSHException('Server %r not found in known_hosts' % hostname) class WarningPolicy (MissingHostKeyPolicy): @@ -186,8 +186,13 @@ class SSHClient (object): @raise IOError: if the file could not be written """ + + # 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) + 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())) @@ -228,7 +233,7 @@ class SSHClient (object): def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None, key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, - compress=False): + compress=False, sock=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}) @@ -271,6 +276,9 @@ class SSHClient (object): @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 @raise BadHostKeyException: if the server's host key could not be verified @@ -279,21 +287,23 @@ class SSHClient (object): establishing an SSH session @raise socket.error: if a socket error occurred while connecting """ - for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): - if socktype == socket.SOCK_STREAM: - af = family - addr = sockaddr - break - else: - # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( - af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) - sock = socket.socket(af, socket.SOCK_STREAM) - if timeout is not None: - try: - sock.settimeout(timeout) - except: - pass - sock.connect(addr) + if not sock: + for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): + if socktype == socket.SOCK_STREAM: + af = family + addr = sockaddr + break + else: + # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :( + af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM) + sock = socket.socket(af, socket.SOCK_STREAM) + if timeout is not None: + try: + sock.settimeout(timeout) + except: + pass + retry_on_signal(lambda: sock.connect(addr)) + t = self._transport = Transport(sock) t.use_compression(compress=compress) if self._log_channel is not None: @@ -344,7 +354,7 @@ class SSHClient (object): self._agent.close() self._agent = None - def exec_command(self, command, bufsize=-1): + 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 the requested command is executed. The command's input and output @@ -355,19 +365,25 @@ class SSHClient (object): @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}) @raise SSHException: if the server fails to execute the command """ chan = self._transport.open_session() + 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) return stdin, stdout, stderr - def invoke_shell(self, term='vt100', width=80, height=24): + def invoke_shell(self, term='vt100', width=80, height=24, width_pixels=0, + height_pixels=0): """ Start an interactive shell session on the SSH server. A new L{Channel} is opened and connected to a pseudo-terminal using the requested @@ -379,13 +395,17 @@ class SSHClient (object): @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} @raise SSHException: if the server fails to invoke a shell """ chan = self._transport.open_session() - chan.get_pty(term, width, height) + chan.get_pty(term, width, height, width_pixels, height_pixels) chan.invoke_shell() return chan @@ -418,68 +438,86 @@ class SSHClient (object): - Any "id_rsa" or "id_dsa" key discoverable in ~/.ssh/ (if allowed). - Plain username/password auth, if a password was given. - (The password might be needed to unlock a private key.) + (The password might be needed to unlock a private key, or for + two-factor authentication [for which it is required].) """ saved_exception = None + two_factor = False + allowed_types = [] if pkey is not None: try: self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint())) - self._transport.auth_publickey(username, pkey) - return + allowed_types = self._transport.auth_publickey(username, pkey) + two_factor = (allowed_types == ['password']) + if not two_factor: + return except SSHException, e: saved_exception = e - for key_filename in key_filenames: - for pkey_class in (RSAKey, DSSKey): - 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)) - self._transport.auth_publickey(username, key) - return - except SSHException, e: - saved_exception = e - - if allow_agent: + if not two_factor: + for key_filename in key_filenames: + for pkey_class in (RSAKey, DSSKey): + 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)) + self._transport.auth_publickey(username, key) + two_factor = (allowed_types == ['password']) + if not two_factor: + return + break + except SSHException, e: + saved_exception = e + + if not two_factor and allow_agent: if self._agent == None: self._agent = Agent() for key in self._agent.get_keys(): try: self._log(DEBUG, 'Trying SSH agent key %s' % hexlify(key.get_fingerprint())) - self._transport.auth_publickey(username, key) - return + # for 2-factor auth a successfully auth'd key will result in ['password'] + allowed_types = self._transport.auth_publickey(username, key) + two_factor = (allowed_types == ['password']) + if not two_factor: + return + break except SSHException, e: saved_exception = e - keyfiles = [] - rsa_key = os.path.expanduser('~/.ssh/id_rsa') - dsa_key = os.path.expanduser('~/.ssh/id_dsa') - if os.path.isfile(rsa_key): - keyfiles.append((RSAKey, rsa_key)) - if os.path.isfile(dsa_key): - keyfiles.append((DSSKey, dsa_key)) - # look in ~/ssh/ for windows users: - rsa_key = os.path.expanduser('~/ssh/id_rsa') - dsa_key = os.path.expanduser('~/ssh/id_dsa') - if os.path.isfile(rsa_key): - keyfiles.append((RSAKey, rsa_key)) - if os.path.isfile(dsa_key): - keyfiles.append((DSSKey, dsa_key)) - - if not look_for_keys: + if not two_factor: keyfiles = [] - - for pkey_class, filename in keyfiles: - try: - key = pkey_class.from_private_key_file(filename, password) - self._log(DEBUG, 'Trying discovered key %s in %s' % (hexlify(key.get_fingerprint()), filename)) - self._transport.auth_publickey(username, key) - return - except SSHException, e: - saved_exception = e - except IOError, e: - saved_exception = e + rsa_key = os.path.expanduser('~/.ssh/id_rsa') + dsa_key = os.path.expanduser('~/.ssh/id_dsa') + if os.path.isfile(rsa_key): + keyfiles.append((RSAKey, rsa_key)) + if os.path.isfile(dsa_key): + keyfiles.append((DSSKey, dsa_key)) + # look in ~/ssh/ for windows users: + rsa_key = os.path.expanduser('~/ssh/id_rsa') + dsa_key = os.path.expanduser('~/ssh/id_dsa') + if os.path.isfile(rsa_key): + keyfiles.append((RSAKey, rsa_key)) + if os.path.isfile(dsa_key): + keyfiles.append((DSSKey, dsa_key)) + + if not look_for_keys: + keyfiles = [] + + for pkey_class, filename in keyfiles: + try: + key = pkey_class.from_private_key_file(filename, password) + self._log(DEBUG, 'Trying discovered key %s in %s' % (hexlify(key.get_fingerprint()), filename)) + # for 2-factor auth a successfully auth'd key will result in ['password'] + allowed_types = self._transport.auth_publickey(username, key) + two_factor = (allowed_types == ['password']) + if not two_factor: + return + break + except SSHException, e: + saved_exception = e + except IOError, e: + saved_exception = e if password is not None: try: @@ -487,6 +525,8 @@ class SSHClient (object): return except SSHException, e: saved_exception = e + elif two_factor: + raise SSHException('Two-factor authentication requires a password') # if we got an auth-failed exception earlier, re-raise it if saved_exception is not None: diff --git a/paramiko/common.py b/paramiko/common.py index 3323f0a4..3d7ca588 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -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. @@ -124,3 +124,6 @@ INFO = logging.INFO WARNING = logging.WARNING ERROR = logging.ERROR CRITICAL = logging.CRITICAL + +# Common IO/select/etc sleep period, in seconds +io_sleep = 0.01 diff --git a/paramiko/compress.py b/paramiko/compress.py index 40b430f9..b55f0b1d 100644 --- a/paramiko/compress.py +++ b/paramiko/compress.py @@ -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 --git a/paramiko/config.py b/paramiko/config.py index 2a2cbff3..1705de76 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh> # # This file is part of paramiko. # @@ -7,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,6 +22,66 @@ L{SSHConfig}. """ import fnmatch +import os +import re +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, 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 class SSHConfig (object): @@ -38,7 +99,7 @@ class SSHConfig (object): """ Create a new OpenSSH config object. """ - self._config = [ { 'host': '*' } ] + self._config = [] def parse(self, file_obj): """ @@ -47,14 +108,19 @@ class SSHConfig (object): @param file_obj: a file-like object to read the config file from @type file_obj: file """ - configs = [self._config[0]] + host = {"host": ['*'], "config": {}} for line in file_obj: line = line.rstrip('\n').lstrip() if (line == '') or (line[0] == '#'): continue if '=' in line: - key, value = line.split('=', 1) - key = key.strip().lower() + # 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 @@ -66,20 +132,21 @@ class SSHConfig (object): value = line[i:].lstrip() if key == 'host': - del configs[:] - # the value may be multiple hosts, space-delimited - for host in value.split(): - # do we have a pre-existing host config to append to? - matches = [c for c in self._config if c['host'] == host] - if len(matches) > 0: - configs.append(matches[0]) - else: - config = { 'host': host } - self._config.append(config) - configs.append(config) - else: - for config in configs: - config[key] = value + self._config.append(host) + value = value.split() + host = {key: value, 'config': {}} + #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be + # specified multiple times and they should be tried in order + # of specification. + + elif key in ['identityfile', 'localforward', 'remoteforward']: + if key in host['config']: + host['config'][key].append(value) + else: + host['config'][key] = [value] + elif key not in host['config']: + host['config'].update({key: value}) + self._config.append(host) def lookup(self, hostname): """ @@ -94,17 +161,106 @@ class SSHConfig (object): will win out. The keys in the returned dict are all normalized to lowercase (look for - C{"port"}, not C{"Port"}. No other processing is done to the keys or - values. + C{"port"}, not C{"Port"}. The values are processed according to the + rules for substitution variable expansion in C{ssh_config}. @param hostname: the hostname to lookup @type hostname: str """ - matches = [x for x in self._config if fnmatch.fnmatch(hostname, x['host'])] - # sort in order of shortest match (usually '*') to longest - matches.sort(lambda x,y: cmp(len(x['host']), len(y['host']))) + + matches = [config for config in self._config if + self._allowed(hostname, config['host'])] + ret = {} - for m in matches: - ret.update(m) - del ret['host'] + for match in matches: + for key, value in match['config'].iteritems(): + if key not in ret: + # Create a copy of the original value, + # else it will reference the original list + # in self._config and update that value too + # when the extend() is being called. + ret[key] = value[:] + elif key == 'identityfile': + ret[key].extend(value) + ret = self._expand_variables(ret, hostname) return ret + + def _allowed(self, hostname, hosts): + match = False + for host in hosts: + if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]): + return False + elif fnmatch.fnmatch(hostname, host): + match = True + return match + + def _expand_variables(self, config, hostname): + """ + Return a dict of config options with expanded substitutions + for a given hostname. + + Please refer to man C{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 + """ + + if 'hostname' in config: + config['hostname'] = config['hostname'].replace('%h', hostname) + else: + config['hostname'] = hostname + + if 'port' in config: + port = config['port'] + else: + port = SSH_PORT + + user = os.getenv('USER') + if 'user' in config: + remoteuser = config['user'] + else: + remoteuser = user + + host = socket.gethostname().split('.')[0] + fqdn = LazyFqdn(config, host) + homedir = os.path.expanduser('~') + replacements = {'controlpath': + [ + ('%h', config['hostname']), + ('%l', fqdn), + ('%L', host), + ('%n', hostname), + ('%p', port), + ('%r', remoteuser), + ('%u', user) + ], + 'identityfile': + [ + ('~', homedir), + ('%d', homedir), + ('%h', config['hostname']), + ('%l', fqdn), + ('%u', user), + ('%r', remoteuser) + ], + 'proxycommand': + [ + ('%h', config['hostname']), + ('%p', port), + ('%r', remoteuser) + ] + } + + for k in config: + if k in replacements: + 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)) + else: + config[k] = config[k].replace(find, str(replace)) + return config diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py index 53ca92b9..f6ecb2a7 100644 --- a/paramiko/dsskey.py +++ b/paramiko/dsskey.py @@ -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 --git a/paramiko/file.py b/paramiko/file.py index d4aec8e3..5fd81cfe 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -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. @@ -354,6 +354,10 @@ class BufferedFile (object): """ return self + @property + def closed(self): + return self._closed + ### overrides... diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index 70ccf43d..f967a3da 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -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,12 +21,22 @@ L{HostKeys} """ import base64 +import binascii from Crypto.Hash import SHA, HMAC import UserDict from paramiko.common import * from paramiko.dsskey import DSSKey from paramiko.rsakey import RSAKey +from paramiko.util import get_logger + + +class InvalidHostKey(Exception): + + def __init__(self, line, exc): + self.line = line + self.exc = exc + self.args = (line, exc) class HostKeyEntry: @@ -39,7 +49,7 @@ class HostKeyEntry: self.hostnames = hostnames self.key = key - def from_line(cls, line): + 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 @@ -52,9 +62,12 @@ class HostKeyEntry: @param line: a line from an OpenSSH known_hosts file @type line: str """ + 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] @@ -63,12 +76,16 @@ class HostKeyEntry: # Decide what kind of key we're looking at and create an object # to hold it accordingly. - if keytype == 'ssh-rsa': - key = RSAKey(data=base64.decodestring(key)) - elif keytype == 'ssh-dss': - key = DSSKey(data=base64.decodestring(key)) - else: - return None + try: + if keytype == 'ssh-rsa': + key = RSAKey(data=base64.decodestring(key)) + elif keytype == 'ssh-dss': + key = DSSKey(data=base64.decodestring(key)) + else: + log.info("Unable to handle key of type %s" % (keytype,)) + return None + except binascii.Error, e: + raise InvalidHostKey(line, e) return cls(names, key) from_line = classmethod(from_line) @@ -148,13 +165,18 @@ class HostKeys (UserDict.DictMixin): @raise IOError: if there was an error reading the file """ f = open(filename, 'r') - for line in f: + for lineno, line in enumerate(f): line = line.strip() if (len(line) == 0) or (line[0] == '#'): continue - e = HostKeyEntry.from_line(line) + e = HostKeyEntry.from_line(line, lineno) if e is not None: - self._entries.append(e) + _hostnames = e.hostnames + for h in _hostnames: + if self.check(h, e.key): + e.hostnames.remove(h) + if len(e.hostnames): + self._entries.append(e) f.close() def save(self, filename): diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py index 9c983397..c0455a1f 100644 --- a/paramiko/kex_gex.py +++ b/paramiko/kex_gex.py @@ -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 --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py index 1386cf3e..6e89b6dc 100644 --- a/paramiko/kex_group1.py +++ b/paramiko/kex_group1.py @@ -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 --git a/paramiko/logging22.py b/paramiko/logging22.py index ed1d8919..e68c52cb 100644 --- a/paramiko/logging22.py +++ b/paramiko/logging22.py @@ -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 --git a/paramiko/message.py b/paramiko/message.py index 366c43c9..c0e8692b 100644 --- a/paramiko/message.py +++ b/paramiko/message.py @@ -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. @@ -110,7 +110,8 @@ class Message (object): @rtype: string """ b = self.packet.read(n) - if len(b) < n: + max_pad_size = 1<<20 # Limit padding to 1 MB + if len(b) < n and n < max_pad_size: return b + '\x00' * (n - len(b)) return b diff --git a/paramiko/packet.py b/paramiko/packet.py index 391c5d56..3f85d668 100644 --- a/paramiko/packet.py +++ b/paramiko/packet.py @@ -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,21 +29,17 @@ import time from paramiko.common import * from paramiko import util -from paramiko.ssh_exception import SSHException +from paramiko.ssh_exception import SSHException, ProxyCommandFailure from paramiko.message import Message -got_r_hmac = False try: - import r_hmac - got_r_hmac = True + from r_hmac import HMAC except ImportError: - pass + from Crypto.Hash.HMAC import HMAC + 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): @@ -57,8 +53,11 @@ class Packetizer (object): # READ the secsh RFC's before raising these values. if anything, # they should probably be lower. - REKEY_PACKETS = pow(2, 30) - REKEY_BYTES = pow(2, 30) + 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 def __init__(self, socket): self.__socket = socket @@ -74,6 +73,7 @@ class Packetizer (object): self.__sent_packets = 0 self.__received_bytes = 0 self.__received_packets = 0 + self.__received_bytes_overflow = 0 self.__received_packets_overflow = 0 # current inbound/outbound ciphering: @@ -83,6 +83,7 @@ class Packetizer (object): self.__mac_size_in = 0 self.__block_engine_out = None self.__block_engine_in = None + self.__sdctr_out = False self.__mac_engine_out = None self.__mac_engine_in = None self.__mac_key_out = '' @@ -106,11 +107,12 @@ class Packetizer (object): """ self.__logger = log - def set_outbound_cipher(self, block_engine, block_size, mac_engine, mac_size, mac_key): + def set_outbound_cipher(self, block_engine, block_size, mac_engine, mac_size, mac_key, sdctr=False): """ Switch outbound data cipher. """ self.__block_engine_out = block_engine + self.__sdctr_out = sdctr self.__block_size_out = block_size self.__mac_engine_out = mac_engine self.__mac_size_out = mac_size @@ -134,6 +136,7 @@ class Packetizer (object): self.__mac_key_in = mac_key self.__received_bytes = 0 self.__received_packets = 0 + self.__received_bytes_overflow = 0 self.__received_packets_overflow = 0 # wait until the reset happens in both directions before clearing rekey flag self.__init_count |= 2 @@ -236,23 +239,25 @@ class Packetizer (object): def write_all(self, out): self.__keepalive_last = time.time() while len(out) > 0: - got_timeout = False + retry_write = False try: n = self.__socket.send(out) except socket.timeout: - got_timeout = True + retry_write = True except socket.error, e: if (type(e.args) is tuple) and (len(e.args) > 0) and (e.args[0] == errno.EAGAIN): - got_timeout = True + retry_write = True elif (type(e.args) is tuple) and (len(e.args) > 0) and (e.args[0] == errno.EINTR): # syscall interrupted; try again - pass + retry_write = True else: n = -1 + except ProxyCommandFailure: + raise # so it doesn't get swallowed by the below catchall except Exception: # could be: (32, 'Broken pipe') n = -1 - if got_timeout: + if retry_write: n = 0 if self.__closed: n = -1 @@ -316,6 +321,7 @@ class Packetizer (object): # only ask once for rekeying self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes sent)' % (self.__sent_packets, self.__sent_bytes)) + self.__received_bytes_overflow = 0 self.__received_packets_overflow = 0 self._trigger_rekey() finally: @@ -368,19 +374,23 @@ class Packetizer (object): self.__sequence_number_in = (self.__sequence_number_in + 1) & 0xffffffffL # check for rekey - self.__received_bytes += packet_size + self.__mac_size_in + 4 + raw_packet_size = packet_size + self.__mac_size_in + 4 + self.__received_bytes += raw_packet_size self.__received_packets += 1 if self.__need_rekey: - # we've asked to rekey -- give them 20 packets to comply before + # we've asked to rekey -- give them some packets to comply before # dropping the connection + self.__received_bytes_overflow += raw_packet_size self.__received_packets_overflow += 1 - if self.__received_packets_overflow >= 20: + if (self.__received_packets_overflow >= self.REKEY_PACKETS_OVERFLOW_MAX) or \ + (self.__received_bytes_overflow >= self.REKEY_BYTES_OVERFLOW_MAX): raise SSHException('Remote transport is ignoring rekey requests') elif (self.__received_packets >= self.REKEY_PACKETS) or \ (self.__received_bytes >= self.REKEY_BYTES): # only ask once for rekeying self._log(DEBUG, 'Rekeying (hit %d packets, %d bytes received)' % (self.__received_packets, self.__received_bytes)) + self.__received_bytes_overflow = 0 self.__received_packets_overflow = 0 self._trigger_rekey() @@ -459,6 +469,12 @@ class Packetizer (object): 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)): + pass + else: + raise if self.__closed: raise EOFError() now = time.time() @@ -472,12 +488,12 @@ class Packetizer (object): padding = 3 + bsize - ((len(payload) + 8) % bsize) packet = struct.pack('>IB', len(payload) + padding + 1, padding) packet += payload - if self.__block_engine_out is not None: - packet += rng.read(padding) - else: - # cute trick i caught openssh doing: if we're not encrypting, + 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) + else: + packet += rng.read(padding) return packet def _trigger_rekey(self): diff --git a/paramiko/pipe.py b/paramiko/pipe.py index 37191ef9..db43d549 100644 --- a/paramiko/pipe.py +++ b/paramiko/pipe.py @@ -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 --git a/paramiko/pkey.py b/paramiko/pkey.py index 3e712222..b1199df8 100644 --- a/paramiko/pkey.py +++ b/paramiko/pkey.py @@ -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 --git a/paramiko/primes.py b/paramiko/primes.py index 9ebfec13..9419cd6b 100644 --- a/paramiko/primes.py +++ b/paramiko/primes.py @@ -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 --git a/paramiko/proxy.py b/paramiko/proxy.py new file mode 100644 index 00000000..218b76e2 --- /dev/null +++ b/paramiko/proxy.py @@ -0,0 +1,91 @@ +# Copyright (C) 2012 Yipit, Inc <coders@yipit.com> +# +# 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. + +""" +L{ProxyCommand}. +""" + +import os +from shlex import split as shlsplit +import signal +from subprocess import Popen, PIPE + +from paramiko.ssh_exception import ProxyCommandFailure + + +class ProxyCommand(object): + """ + 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 + 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. + """ + 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. + + @param command_line: the command that should be executed and + used as the proxy. + @type command_line: str + """ + self.cmd = shlsplit(command_line) + self.process = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + + 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 + """ + try: + self.process.stdin.write(content) + except IOError, 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) + 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 + + @return: the length of the read content + @rtype: int + """ + try: + return os.read(self.process.stdout.fileno(), size) + except IOError, e: + raise BadProxyCommand(' '.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 diff --git a/paramiko/resource.py b/paramiko/resource.py index 0d5c82fa..6ef86d8c 100644 --- a/paramiko/resource.py +++ b/paramiko/resource.py @@ -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 --git a/paramiko/rsakey.py b/paramiko/rsakey.py index 1e2d8f98..c7500f85 100644 --- a/paramiko/rsakey.py +++ b/paramiko/rsakey.py @@ -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 --git a/paramiko/server.py b/paramiko/server.py index 6424b63a..d737e056 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -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. @@ -93,6 +93,7 @@ class ServerInterface (object): - 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 @@ -492,7 +493,22 @@ class ServerInterface (object): @rtype: bool """ 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 + forwarding. + + The default implementation always returns C{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 + """ + return False + def check_channel_direct_tcpip_request(self, chanid, origin, destination): """ Determine if a local port forwarding channel will be granted, and diff --git a/paramiko/sftp.py b/paramiko/sftp.py index a0b08e02..a97c300f 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -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 --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py index 1f094219..b459b04b 100644 --- a/paramiko/sftp_attr.py +++ b/paramiko/sftp_attr.py @@ -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 --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 189caa47..cf94582c 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -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. @@ -198,7 +198,7 @@ class SFTPClient (BaseSFTP): 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. + 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, @@ -533,6 +533,56 @@ class SFTPClient (BaseSFTP): """ return 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. + + 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 + (since 1.7.4) + @rtype: SFTPAttributes + + @since: 1.4 + """ + fr = self.file(remotepath, 'wb') + fr.set_pipelined(True) + size = 0 + try: + while True: + data = fl.read(32768) + fr.write(data) + size += len(data) + if callback is not None: + callback(size, file_size) + if len(data) == 0: + break + finally: + fr.close() + if confirm: + s = self.stat(remotepath) + if s.st_size != size: + raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) + else: + s = SFTPAttributes() + return s + def put(self, localpath, remotepath, callback=None, confirm=True): """ Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. @@ -562,29 +612,46 @@ class SFTPClient (BaseSFTP): file_size = os.stat(localpath).st_size fl = file(localpath, 'rb') try: - fr = self.file(remotepath, 'wb') - fr.set_pipelined(True) - size = 0 - try: - while True: - data = fl.read(32768) - if len(data) == 0: - break - fr.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - finally: - fr.close() + return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm) finally: fl.close() - if confirm: - s = self.stat(remotepath) - if s.st_size != size: - raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) - else: - s = SFTPAttributes() - return s + + 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 + 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: + size = 0 + while True: + data = fr.read(32768) + fl.write(data) + size += len(data) + if callback is not None: + callback(size, file_size) + if len(data) == 0: + break + finally: + fr.close() + return size def get(self, remotepath, localpath, callback=None): """ @@ -603,25 +670,12 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ - fr = self.file(remotepath, 'rb') file_size = self.stat(remotepath).st_size - fr.prefetch() + fl = file(localpath, 'wb') try: - fl = file(localpath, 'wb') - try: - size = 0 - while True: - data = fr.read(32768) - if len(data) == 0: - break - fl.write(data) - size += len(data) - if callback is not None: - callback(size, file_size) - finally: - fl.close() + size = self.getfo(remotepath, fl, callback) finally: - fr.close() + fl.close() s = os.stat(localpath) if s.st_size != size: raise IOError('size mismatch in get! %d != %d' % (s.st_size, size)) @@ -641,13 +695,13 @@ class SFTPClient (BaseSFTP): msg = Message() msg.add_int(self.request_number) for item in arg: - if type(item) is int: + if isinstance(item, int): msg.add_int(item) - elif type(item) is long: + elif isinstance(item, long): msg.add_int64(item) - elif type(item) is str: + elif isinstance(item, str): msg.add_string(item) - elif type(item) is SFTPAttributes: + elif isinstance(item, SFTPAttributes): item._pack(msg) else: raise Exception('unknown type for %r type %r' % (item, type(item))) diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 2d4d4317..b7cf8501 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -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 @@ L{SFTPFile} """ from binascii import hexlify +from collections import deque import socket import threading import time @@ -34,6 +35,9 @@ from paramiko.sftp_attr import SFTPAttributes class SFTPFile (BufferedFile): """ Proxy object for a file on the remote server, in client mode SFTP. + + Instances of this class may be used as context managers in the same way + that built-in Python file objects are. """ # Some sftp servers will choke if you send read/write requests larger than @@ -52,6 +56,7 @@ class SFTPFile (BufferedFile): self._prefetch_extents = {} self._prefetch_lock = threading.Lock() self._saved_exception = None + self._reqs = deque() def __del__(self): self._close(async=True) @@ -161,12 +166,14 @@ class SFTPFile (BufferedFile): def _write(self, data): # may write less than requested if it would exceed max packet size chunk = min(len(data), self.MAX_REQUEST_SIZE) - req = self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), str(data[:chunk])) - if not self.pipelined or self.sftp.sock.recv_ready(): - t, msg = self.sftp._read_response(req) - if t != CMD_STATUS: - raise SFTPError('Expected status') - # convert_status already called + self._reqs.append(self.sftp._async_request(type(None), CMD_WRITE, self.handle, long(self._realpos), str(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() + t, msg = self.sftp._read_response(req) + if t != CMD_STATUS: + raise SFTPError('Expected status') + # convert_status already called return chunk def settimeout(self, timeout): @@ -478,3 +485,9 @@ class SFTPFile (BufferedFile): x = self._saved_exception self._saved_exception = None raise x + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py index a6cd44a8..29d3d0d8 100644 --- a/paramiko/sftp_handle.py +++ b/paramiko/sftp_handle.py @@ -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 --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py index 7cc6c0c3..1833c2e8 100644 --- a/paramiko/sftp_server.py +++ b/paramiko/sftp_server.py @@ -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 --git a/paramiko/sftp_si.py b/paramiko/sftp_si.py index 401a4e99..b0ee3c42 100644 --- a/paramiko/sftp_si.py +++ b/paramiko/sftp_si.py @@ -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 --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py index 68924d0f..b502b563 100644 --- a/paramiko/ssh_exception.py +++ b/paramiko/ssh_exception.py @@ -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. @@ -113,3 +113,20 @@ class BadHostKeyException (SSHException): self.key = got_key self.expected_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 + """ + def __init__(self, command, error): + SSHException.__init__(self, + '"ProxyCommand (%s)" returned non-zero exit status: %s' % ( + command, error + ) + ) + self.error = error diff --git a/paramiko/transport.py b/paramiko/transport.py index 30de295b..6c42cc27 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -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,6 +29,7 @@ import threading import time import weakref +import paramiko from paramiko import util from paramiko.auth_handler import AuthHandler from paramiko.channel import Channel @@ -43,7 +44,9 @@ from paramiko.primes import ModulusPack from paramiko.rsakey import RSAKey from paramiko.server import ServerInterface from paramiko.sftp_client import SFTPClient -from paramiko.ssh_exception import SSHException, BadAuthenticationType, ChannelException +from paramiko.ssh_exception import (SSHException, BadAuthenticationType, + ChannelException, ProxyCommandFailure) +from paramiko.util import retry_on_signal from Crypto import Random from Crypto.Cipher import Blowfish, AES, DES3, ARC4 @@ -194,7 +197,7 @@ class Transport (threading.Thread): """ _PROTO_ID = '2.0' - _CLIENT_ID = 'paramiko_1.7.7.1' + _CLIENT_ID = 'paramiko_%s' % (paramiko.__version__) _preferred_ciphers = ( 'aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256' ) @@ -288,7 +291,7 @@ class Transport (threading.Thread): addr = sockaddr sock = socket.socket(af, socket.SOCK_STREAM) try: - sock.connect((hostname, port)) + retry_on_signal(lambda: sock.connect((hostname, port))) except socket.error, e: reason = str(e) else: @@ -341,6 +344,7 @@ class Transport (threading.Thread): self._channel_counter = 1 self.window_size = 65536 self.max_packet_size = 34816 + self._forward_agent_handler = None self._x11_handler = None self._tcp_handler = None @@ -673,6 +677,20 @@ class Transport (threading.Thread): """ return self.open_channel('x11', src_addr=src_addr) + def open_forward_agent_channel(self): + """ + Request a new channel to the client, of type + C{"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} + + @raise 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)): """ Request a new channel back to the client, of type C{"forwarded-tcpip"}. @@ -1421,7 +1439,7 @@ class Transport (threading.Thread): 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: @@ -1481,6 +1499,14 @@ class Transport (threading.Thread): else: return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv) + def _set_forward_agent_handler(self, handler): + if handler is None: + def default_handler(channel): + self._queue_incoming_channel(channel) + self._forward_agent_handler = default_handler + else: + self._forward_agent_handler = handler + def _set_x11_handler(self, handler): # only called if a channel has turned on x11 forwarding if handler is None: @@ -1505,6 +1531,18 @@ class Transport (threading.Thread): # indefinitely, creating a GC cycle and not letting Transport ever be # GC'd. it's a bug in Thread.) + # Hold reference to 'sys' so we can test sys.modules to detect + # 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: @@ -1512,94 +1550,102 @@ class Transport (threading.Thread): else: self._log(DEBUG, 'starting thread (client mode): %s' % hex(long(id(self)) & 0xffffffffL)) try: - self.packetizer.write_all(self.local_version + '\r\n') - self._check_banner() - self._send_kex_init() - self._expect_packet(MSG_KEXINIT) - - while self.active: - if self.packetizer.need_rekey() and not self.in_kex: - self._send_kex_init() - try: - ptype, m = self.packetizer.read_message() - except NeedRekeyException: - continue - if ptype == MSG_IGNORE: - continue - elif ptype == MSG_DISCONNECT: - self._parse_disconnect(m) - self.active = False - self.packetizer.close() - break - elif ptype == MSG_DEBUG: - self._parse_debug(m) - continue - if len(self._expected_packet) > 0: - 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): - self.kex_engine.parse_next(ptype, m) + try: + self.packetizer.write_all(self.local_version + '\r\n') + self._check_banner() + self._send_kex_init() + self._expect_packet(MSG_KEXINIT) + + while self.active: + if self.packetizer.need_rekey() and not self.in_kex: + self._send_kex_init() + try: + ptype, m = self.packetizer.read_message() + except NeedRekeyException: continue - - if ptype in self._handler_table: - self._handler_table[ptype](self, m) - elif ptype in self._channel_handler_table: - chanid = m.get_int() - chan = self._channels.get(chanid) - if chan is not None: - self._channel_handler_table[ptype](chan, m) - elif chanid in self.channels_seen: - self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) - else: - self._log(ERROR, 'Channel request for unknown channel %d' % chanid) + if ptype == MSG_IGNORE: + continue + elif ptype == MSG_DISCONNECT: + self._parse_disconnect(m) self.active = False self.packetizer.close() - elif (self.auth_handler is not None) and (ptype in self.auth_handler._handler_table): - self.auth_handler._handler_table[ptype](self.auth_handler, m) + break + elif ptype == MSG_DEBUG: + self._parse_debug(m) + continue + if len(self._expected_packet) > 0: + 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): + self.kex_engine.parse_next(ptype, m) + continue + + if ptype in self._handler_table: + self._handler_table[ptype](self, m) + elif ptype in self._channel_handler_table: + chanid = m.get_int() + chan = self._channels.get(chanid) + if chan is not None: + self._channel_handler_table[ptype](chan, m) + elif chanid in self.channels_seen: + self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) + else: + self._log(ERROR, 'Channel request for unknown channel %d' % chanid) + self.active = False + self.packetizer.close() + elif (self.auth_handler is not None) and (ptype in self.auth_handler._handler_table): + self.auth_handler._handler_table[ptype](self.auth_handler, m) + else: + self._log(WARNING, 'Oops, unhandled type %d' % ptype) + msg = Message() + msg.add_byte(chr(MSG_UNIMPLEMENTED)) + msg.add_int(m.seqno) + self._send_message(msg) + except SSHException, e: + self._log(ERROR, 'Exception: ' + str(e)) + self._log(ERROR, util.tb_strings()) + self.saved_exception = e + except EOFError, e: + self._log(DEBUG, 'EOF in transport thread') + #self._log(DEBUG, util.tb_strings()) + self.saved_exception = e + except socket.error, e: + if type(e.args) is tuple: + emsg = '%s (%d)' % (e.args[1], e.args[0]) else: - self._log(WARNING, 'Oops, unhandled type %d' % ptype) - msg = Message() - msg.add_byte(chr(MSG_UNIMPLEMENTED)) - msg.add_int(m.seqno) - self._send_message(msg) - except SSHException, e: - self._log(ERROR, 'Exception: ' + str(e)) - self._log(ERROR, util.tb_strings()) - self.saved_exception = e - except EOFError, e: - self._log(DEBUG, 'EOF in transport thread') - #self._log(DEBUG, util.tb_strings()) - self.saved_exception = e - except socket.error, e: - if type(e.args) is tuple: - emsg = '%s (%d)' % (e.args[1], e.args[0]) - else: - emsg = e.args - self._log(ERROR, 'Socket exception: ' + emsg) - self.saved_exception = e - except Exception, 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(): - chan._unlink() - if self.active: - self.active = False - self.packetizer.close() - if self.completion_event != None: - self.completion_event.set() - if self.auth_handler is not None: - self.auth_handler.abort() - for event in self.channel_events.values(): - event.set() - try: - self.lock.acquire() - self.server_accept_cv.notify() - finally: - self.lock.release() - self.sock.close() + emsg = e.args + self._log(ERROR, 'Socket exception: ' + emsg) + self.saved_exception = e + except Exception, 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(): + chan._unlink() + if self.active: + self.active = False + self.packetizer.close() + if self.completion_event != None: + self.completion_event.set() + if self.auth_handler is not None: + self.auth_handler.abort() + for event in self.channel_events.values(): + event.set() + try: + self.lock.acquire() + self.server_accept_cv.notify() + finally: + self.lock.release() + self.sock.close() + except: + # Don't raise spurious 'NoneType has no attribute X' errors when we + # wake up during interpreter shutdown. Or rather -- raise + # everything *if* sys.modules (used as a convenient sentinel) + # appears to still exist. + if self.sys.modules is not None: + raise ### protocol stages @@ -1629,6 +1675,8 @@ class Transport (threading.Thread): timeout = 2 try: buf = self.packetizer.readline(timeout) + except ProxyCommandFailure: + raise except Exception, x: raise SSHException('Error reading SSH protocol banner' + str(x)) if buf[:4] == 'SSH-': @@ -1837,7 +1885,8 @@ class Transport (threading.Thread): mac_key = self._compute_key('F', mac_engine.digest_size) else: mac_key = self._compute_key('E', mac_engine.digest_size) - self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) + 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] if (compress_out is not None) and ((self.local_compression != 'zlib@openssh.com') or self.authenticated): self._log(DEBUG, 'Switching on outbound compression ...') @@ -1980,7 +2029,14 @@ class Transport (threading.Thread): initial_window_size = m.get_int() max_packet_size = m.get_int() reject = False - if (kind == 'x11') and (self._x11_handler is not None): + if (kind == 'auth-agent@openssh.com') and (self._forward_agent_handler is not None): + self._log(DEBUG, 'Incoming forward agent connection') + self.lock.acquire() + try: + my_chanid = self._next_channel() + finally: + self.lock.release() + elif (kind == 'x11') and (self._x11_handler is not None): origin_addr = m.get_string() origin_port = m.get_int() self._log(DEBUG, 'Incoming x11 connection from %s:%d' % (origin_addr, origin_port)) @@ -2052,7 +2108,9 @@ class Transport (threading.Thread): m.add_int(self.max_packet_size) self._send_message(m) self._log(INFO, 'Secsh channel %d (%s) opened.', my_chanid, kind) - if kind == 'x11': + if kind == 'auth-agent@openssh.com': + self._forward_agent_handler(chan) + elif kind == 'x11': self._x11_handler(chan, (origin_addr, origin_port)) elif kind == 'forwarded-tcpip': chan.origin_addr = (origin_addr, origin_port) diff --git a/paramiko/util.py b/paramiko/util.py index 0d6a5348..85ee6b06 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -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. @@ -24,6 +24,7 @@ from __future__ import generators import array from binascii import hexlify, unhexlify +import errno import sys import struct import traceback @@ -270,6 +271,14 @@ def get_logger(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: + if e.errno != errno.EINTR: + raise class Counter (object): """Stateful counter for CTR mode crypto""" diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py index 787032b8..d588e81d 100644 --- a/paramiko/win_pageant.py +++ b/paramiko/win_pageant.py @@ -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,26 +21,19 @@ Functions for communicating with Pageant, the basic windows ssh agent program. """ -import os -import struct -import tempfile -import mmap +from __future__ import with_statement + import array +import ctypes.wintypes +import platform +import struct -# 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 @@ -51,16 +44,7 @@ win32con_WM_COPYDATA = 74 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(): @@ -70,58 +54,61 @@ def can_talk_to_agent(): 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 + http://msdn.microsoft.com/en-us/library/windows/desktop/ms649010%28v=vs.85%29.aspx + """ + _fields_ = [ + ('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", map_name + '\0') char_buffer_address, char_buffer_size = char_buffer.buffer_info() # Create a string to use for the SendMessage function call - cds = struct.pack("LLP", _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, len(cds), cds) - elif _has_ctypes: - _buf = array.array('B', cds) - _addr, _size = _buf.buffer_info() - response = ctypes.windll.user32.SendMessageA(hwnd, win32con_WM_COPYDATA, _size, _addr) - else: - response = 0 - + cds = COPYDATASTRUCT(_AGENT_COPYDATA_ID, char_buffer_size, + char_buffer_address) + + 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 @@ -131,10 +118,10 @@ class PageantConnection (object): def __init__(self): self._response = None - + def send(self, data): self._response = _query_pageant(data) - + def recv(self, n): if self._response is None: return '' @@ -7,14 +7,14 @@ # 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. # # 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. +# 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA. longdesc = ''' @@ -25,6 +25,10 @@ are supported. SFTP client and server mode are both supported too. Required packages: pyCrypto + +To install the `in-development version +<https://github.com/paramiko/paramiko/tarball/master#egg=paramiko-dev>`_, use +`pip install paramiko==dev`. ''' # if someday we want to *require* setuptools, uncomment this: @@ -36,23 +40,23 @@ import sys try: from setuptools import setup kw = { - 'install_requires': 'pycrypto >= 2.1', + 'install_requires': 'pycrypto >= 2.1, != 2.4', } except ImportError: from distutils.core import setup kw = {} if sys.platform == 'darwin': - import setup_helper - setup_helper.install_custom_make_tarball() + import setup_helper + setup_helper.install_custom_make_tarball() setup(name = "paramiko", - version = "1.7.7.1", + version = "1.10.5", description = "SSH2 protocol library", - author = "Robey Pointer", - author_email = "robeypointer@gmail.com", - url = "http://www.lag.net/paramiko/", + author = "Jeff Forcier", + author_email = "jeff@bitprophet.org", + url = "https://github.com/paramiko/paramiko/", packages = [ 'paramiko' ], license = 'LGPL', platforms = 'Posix; MacOS X; Windows', diff --git a/setup_helper.py b/setup_helper.py index e8f3f2f3..ff6b0e16 100644 --- a/setup_helper.py +++ b/setup_helper.py @@ -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 --git a/sites/_shared_static/logo.png b/sites/_shared_static/logo.png Binary files differnew file mode 100644 index 00000000..bc76697e --- /dev/null +++ b/sites/_shared_static/logo.png diff --git a/sites/docs/conf.py b/sites/docs/conf.py new file mode 100644 index 00000000..0c7ffe55 --- /dev/null +++ b/sites/docs/conf.py @@ -0,0 +1,4 @@ +# Obtain shared config values +import os, sys +sys.path.append(os.path.abspath('..')) +from shared_conf import * diff --git a/sites/docs/index.rst b/sites/docs/index.rst new file mode 100644 index 00000000..08b34320 --- /dev/null +++ b/sites/docs/index.rst @@ -0,0 +1,6 @@ +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 website <http://paramiko.org>`_. diff --git a/sites/shared_conf.py b/sites/shared_conf.py new file mode 100644 index 00000000..86ecdfe8 --- /dev/null +++ b/sites/shared_conf.py @@ -0,0 +1,41 @@ +from datetime import datetime +import os +import sys + +import alabaster + + +# Alabaster theme + mini-extension +html_theme_path = [alabaster.get_path()] +extensions = ['alabaster'] +# Paths relative to invoking conf.py - not this shared file +html_static_path = ['../_shared_static'] +html_theme = 'alabaster' +html_theme_options = { + 'description': "A Python implementation of SSHv2.", + 'github_user': 'paramiko', + 'github_repo': 'paramiko', + 'gittip_user': 'bitprophet', + 'analytics_id': 'UA-18486793-2', + + 'link': '#3782BE', + 'link_hover': '#3782BE', +} +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'searchbox.html', + 'donate.html', + ] +} + +# Regular settings +project = u'Paramiko' +year = datetime.now().year +copyright = u'%d Jeff Forcier' % year +master_doc = 'index' +templates_path = ['_templates'] +exclude_trees = ['_build'] +source_suffix = '.rst' +default_role = 'obj' diff --git a/sites/www/_templates/rss.xml b/sites/www/_templates/rss.xml new file mode 100644 index 00000000..f6f9cbd1 --- /dev/null +++ b/sites/www/_templates/rss.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> + <channel> + <atom:link href="{{ atom }}" rel="self" type="application/rss+xml" /> + <title>{{ title }}</title> + <link>{{ link }}</link> + <description>{{ description }}</description> + <pubDate>{{ date }}</pubDate> + {% for link, title, desc, date in posts %} + <item> + <link>{{ link }}</link> + <guid>{{ link }}</guid> + <title><![CDATA[{{ title }}]]></title> + <description><![CDATA[{{ desc }}]]></description> + <pubDate>{{ date }}</pubDate> + </item> + {% endfor %} + </channel> +</rss> diff --git a/sites/www/blog.py b/sites/www/blog.py new file mode 100644 index 00000000..3b129ebf --- /dev/null +++ b/sites/www/blog.py @@ -0,0 +1,140 @@ +from collections import namedtuple +from datetime import datetime +import time +import email.utils + +from sphinx.util.compat import Directive +from docutils import nodes + + +class BlogDateDirective(Directive): + """ + Used to parse/attach date info to blog post documents. + + No nodes generated, since none are needed. + """ + has_content = True + + def run(self): + # Tag parent document with parsed date value. + self.state.document.blog_date = datetime.strptime( + self.content[0], "%Y-%m-%d" + ) + # Don't actually insert any nodes, we're already done. + return [] + +class blog_post_list(nodes.General, nodes.Element): + pass + +class BlogPostListDirective(Directive): + """ + Simply spits out a 'blog_post_list' temporary node for replacement. + + Gets replaced at doctree-resolved time - only then will all blog post + documents be written out (& their date directives executed). + """ + def run(self): + return [blog_post_list('')] + + +Post = namedtuple('Post', 'name doc title date opener') + +def get_posts(app): + # Obtain blog posts + post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) + posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) + # Obtain common data used for list page & RSS + data = [] + for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True): + # Welp. No "nice" way to get post title. Thanks Sphinx. + title = doc[0][0][0] + # Date. This may or may not end up reflecting the required + # *input* format, but doing it here gives us flexibility. + date = doc.blog_date + # 1st paragraph as opener. TODO: allow a role or something marking + # where to actually pull from? + opener = doc.traverse(nodes.paragraph)[0] + data.append(Post(post, doc, title, date, opener)) + return data + +def replace_blog_post_lists(app, doctree, fromdocname): + """ + Replace blog_post_list nodes with ordered list-o-links to posts. + """ + # Obtain blog posts + post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs) + posts = map(lambda x: (x, app.env.get_doctree(x)), post_names) + # Build "list" of links/etc + post_links = [] + for post, doc, title, date, opener in get_posts(app): + # Link itself + uri = app.builder.get_relative_uri(fromdocname, post) + link = nodes.reference('', '', refdocname=post, refuri=uri) + # Title, bolded. TODO: use 'topic' or something maybe? + link.append(nodes.strong('', title)) + date = date.strftime("%Y-%m-%d") + # Meh @ not having great docutils nodes which map to this. + html = '<div class="timestamp"><span>%s</span></div>' % date + timestamp = nodes.raw(text=html, format='html') + # NOTE: may group these within another element later if styling + # necessitates it + group = [timestamp, nodes.paragraph('', '', link), opener] + post_links.extend(group) + + # Replace temp node(s) w/ expanded list-o-links + for node in doctree.traverse(blog_post_list): + node.replace_self(post_links) + +def rss_timestamp(timestamp): + # Use horribly inappropriate module for its magical daylight-savings-aware + # timezone madness. Props to Tinkerer for the idea. + return email.utils.formatdate( + time.mktime(timestamp.timetuple()), + localtime=True + ) + +def generate_rss(app): + # Meh at having to run this subroutine like 3x per build. Not worth trying + # to be clever for now tho. + posts_ = get_posts(app) + # LOL URLs + root = app.config.rss_link + if not root.endswith('/'): + root += '/' + # Oh boy + posts = [ + ( + root + app.builder.get_target_uri(x.name), + x.title, + str(x.opener[0]), # Grab inner text element from paragraph + rss_timestamp(x.date), + ) + for x in posts_ + ] + location = 'blog/rss.xml' + context = { + 'title': app.config.project, + 'link': root, + 'atom': root + location, + 'description': app.config.rss_description, + # 'posts' is sorted by date already + 'date': rss_timestamp(posts_[0].date), + 'posts': posts, + } + yield (location, context, 'rss.xml') + +def setup(app): + # Link in RSS feed back to main website, e.g. 'http://paramiko.org' + app.add_config_value('rss_link', None, '') + # Ditto for RSS description field + app.add_config_value('rss_description', None, '') + # Interprets date metadata in blog post documents + app.add_directive('date', BlogDateDirective) + # Inserts blog post list node (in e.g. a listing page) for replacement + # below + app.add_node(blog_post_list) + app.add_directive('blog-posts', BlogPostListDirective) + # Performs abovementioned replacement + app.connect('doctree-resolved', replace_blog_post_lists) + # Generates RSS page from whole cloth at page generation step + app.connect('html-collect-pages', generate_rss) diff --git a/sites/www/blog.rst b/sites/www/blog.rst new file mode 100644 index 00000000..af9651e4 --- /dev/null +++ b/sites/www/blog.rst @@ -0,0 +1,16 @@ +==== +Blog +==== + +.. blog-posts directive gets replaced with an ordered list of blog posts. + +.. blog-posts:: + + +.. The following toctree ensures blog posts get processed. + +.. toctree:: + :hidden: + :glob: + + blog/* diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst new file mode 100644 index 00000000..7b075073 --- /dev/null +++ b/sites/www/blog/first-post.rst @@ -0,0 +1,7 @@ +=========== +First post! +=========== + +A blog post. + +.. date:: 2013-12-04 diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst new file mode 100644 index 00000000..c4463f33 --- /dev/null +++ b/sites/www/blog/second-post.rst @@ -0,0 +1,7 @@ +=========== +Another one +=========== + +.. date:: 2013-12-05 + +Indeed! diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst new file mode 100644 index 00000000..1a827587 --- /dev/null +++ b/sites/www/changelog.rst @@ -0,0 +1,79 @@ +========= +Changelog +========= + +* :bug:`34` (PR :issue:`35`) Fix SFTP prefetching incompatibility with some + SFTP servers regarding request/response ordering. Thanks to Richard + Kettlewell for catch & patch. +* :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.10.5 <2014-01-08>` +* :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.10.4 <2013-09-27>` 199, 200, 179 +* :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.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.10.2 <2013-07-26>` +* :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 <https://github.com/fabric/fabric/issues/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 <https://github.com/fabric/fabric/issues/876>`_) + Forwarded SSH agent connections left stale local pipes lying around, which + could cause local (and sometimes remote or network) resource starvation when + running many agent-using remote commands. Thanks to Kevin Tegtmeier for catch + & patch. +* :release:`1.10.0 <2013-03-01>` +* :feature:`66` Batch SFTP writes to help speed up file transfers. Thanks to + Olle Lundberg for the patch. +* :bug:`133 major` Fix handling of window-change events to be on-spec and not + attempt to wait for a response from the remote sshd; this fixes problems with + less common targets such as some Cisco devices. Thanks to Phillip Heller for + catch & patch. +* :feature:`93` Overhaul SSH config parsing to be in line with ``man + ssh_config`` (& the behavior of ``ssh`` itself), including addition of parameter + expansion within config values. Thanks to Olle Lundberg for the patch. +* :feature:`110` Honor SSH config ``AddressFamily`` setting when looking up + local host's FQDN. Thanks to John Hensley for the patch. +* :feature:`128` Defer FQDN resolution until needed, when parsing SSH config + files. Thanks to Parantapa Bhattacharya for catch & patch. +* :bug:`102 major` Forego random padding for packets when running under + ``*-ctr`` ciphers. This corrects some slowdowns on platforms where random + byte generation is inefficient (e.g. Windows). Thanks to ``@warthog618`` for + catch & patch, and Michael van der Kolff for code/technique review. +* :feature:`127` Turn ``SFTPFile`` into a context manager. Thanks to Michael + Williamson for the patch. +* :feature:`116` Limit ``Message.get_bytes`` to an upper bound of 1MB to protect + against potential DoS vectors. Thanks to ``@mvschaik`` for catch & patch. +* :feature:`115` Add convenience ``get_pty`` kwarg to ``Client.exec_command`` so + users not manually controlling a channel object can still toggle PTY + creation. Thanks to Michael van der Kolff for the patch. +* :feature:`71` Add ``SFTPClient.putfo`` and ``.getfo`` methods to allow direct + uploading/downloading of file-like objects. Thanks to Eric Buehl for the + patch. +* :feature:`113` Add ``timeout`` parameter to ``SSHClient.exec_command`` for + easier setting of the command's internal channel object's timeout. Thanks to + Cernov Vladimir for the patch. +* :support:`94` Remove duplication of SSH port constant. Thanks to Olle + Lundberg for the catch. +* :feature:`80` Expose the internal "is closed" property of the file transfer + class ``BufferedFile`` as ``.closed``, better conforming to Python's file + interface. Thanks to ``@smunaut`` and James Hiscock for catch & patch. diff --git a/sites/www/conf.py b/sites/www/conf.py new file mode 100644 index 00000000..481acdff --- /dev/null +++ b/sites/www/conf.py @@ -0,0 +1,35 @@ +# Obtain shared config values +import sys +import os +from os.path import abspath, join, dirname + +sys.path.append(abspath(join(dirname(__file__), '..'))) +from shared_conf import * + +# Local blog extension +sys.path.append(abspath('.')) +extensions.append('blog') +rss_link = 'http://paramiko.org' +rss_description = 'Paramiko project news' + +# Releases changelog extension +extensions.append('releases') +releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s" +releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" + +# Intersphinx for referencing API/usage docs +extensions.append('sphinx.ext.intersphinx') +# Default is 'local' building, but reference the public docs site when building +# under RTD. +target = join(dirname(__file__), '..', 'docs', '_build') +if os.environ.get('READTHEDOCS') == 'True': + # TODO: switch to docs.paramiko.org post go-live of sphinx API docs + target = 'http://paramiko-docs.readthedocs.org/en/latest/' +#intersphinx_mapping = { +# 'docs': (target, None), +#} + +# Sister-site links to API docs +html_theme_options['extra_nav_links'] = { + "API Docs": 'http://docs.paramiko.org', +} diff --git a/sites/www/contact.rst b/sites/www/contact.rst new file mode 100644 index 00000000..2b6583f5 --- /dev/null +++ b/sites/www/contact.rst @@ -0,0 +1,11 @@ +======= +Contact +======= + +You can get in touch with the developer & user community in any of the +following ways: + +* IRC: ``#paramiko`` on Freenode +* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage + <http://librelist.com>`_ for usage details). +* This website - a blog section is forthcoming. diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst new file mode 100644 index 00000000..2b752cc5 --- /dev/null +++ b/sites/www/contributing.rst @@ -0,0 +1,19 @@ +============ +Contributing +============ + +How to get the code +=================== + +Our primary Git repository is on Github at `paramiko/paramiko +<https://github.com/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 +<http://contribution-guide.org>`_ - we follow it explicitly. diff --git a/sites/www/index.rst b/sites/www/index.rst new file mode 100644 index 00000000..7fefedd2 --- /dev/null +++ b/sites/www/index.rst @@ -0,0 +1,38 @@ +Welcome to Paramiko! +==================== + +Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_, +providing both client and server functionality. While it leverages a Python C +extension for low level cryptography (`PyCrypto <http://pycrypto.org>`_), +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 <http://docs.paramiko.org>`_. + +.. toctree:: + changelog + installing + contributing + contact + +.. Hide blog in hidden toctree for now (to avoid warnings.) + +.. toctree:: + :hidden: + + blog + + +.. rubric:: Footnotes + +.. [#] + SSH is defined in RFCs + `4251 <http://www.rfc-editor.org/rfc/rfc4251.txt>`_, + `4252 <http://www.rfc-editor.org/rfc/rfc4252.txt>`_, + `4253 <http://www.rfc-editor.org/rfc/rfc4253.txt>`_, and + `4254 <http://www.rfc-editor.org/rfc/rfc4254.txt>`_; + the primary working implementation of the protocol is the `OpenSSH project + <http://openssh.org>`_. Paramiko implements a large portion of the SSH + feature set, but there are occasional gaps. diff --git a/sites/www/installing.rst b/sites/www/installing.rst new file mode 100644 index 00000000..0d4dc1ac --- /dev/null +++ b/sites/www/installing.rst @@ -0,0 +1,105 @@ +========== +Installing +========== + +Paramiko itself +=============== + +The recommended way to get Invoke is to **install the latest stable release** +via `pip <http://pip-installer.org>`_:: + + $ pip install paramiko + +.. note:: + Users who want the bleeding edge can install the development version via + ``pip install paramiko==dev``. + +We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming +soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on +Python 2.4 still, but there is no longer any support guarantee. + +Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the +PyCrypto C extension. `ecdsa` is easily installable from wherever you +obtained Paramiko's package; PyCrypto may require more work. Read on for +details. + +PyCrypto +======== + +`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level +(C-based) encryption algorithms we need to implement the SSH protocol. There +are a couple gotchas associated with installing PyCrypto: its compatibility +with Python's package tools, and the fact that it is a C-based extension. + +.. _pycrypto-and-pip: + +Possible gotcha on older Python and/or pip versions +--------------------------------------------------- + +We strongly recommend using ``pip`` to as it is newer and generally better than +``easy_install``. However, a combination of bugs in specific (now rather old) +versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto. +Specifically: + +* Python = 2.5.x +* PyCrypto >= 2.1 (required for most modern versions of Paramiko) +* ``pip`` < 0.8.1 + +When all three criteria are met, you may encounter ``No such file or +directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install +PyCrypto``. + +The fix is to make sure at least one of the above criteria is not met, by doing +the following (in order of preference): + +* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``. +* Upgrade to Python 2.6 or above. +* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1, + and install PyCrypto 2.0.1 (the oldest version on PyPI which works with + Paramiko 1.7.6/1.7.7) + + +C extension +----------- + +Unless you are installing from a precompiled source such as a Debian apt +repository or RedHat RPM, or using :ref:`pypm <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 <http://cygwin.com>`_ or obtaining a +precompiled Win32 PyCrypto package from `voidspace's Python modules page +<http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_. + +.. 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 + <https://github.com/fabric/fabric/issues/194>`_ for info. + + +.. _pypm: + +ActivePython and PyPM +===================== + +Windows users who already have ActiveState's `ActivePython +<http://www.activestate.com/activepython/downloads>`_ distribution installed +may find Paramiko is best installed with `its package manager, PyPM +<http://code.activestate.com/pypm/>`_. Below is example output from an +installation of Paramiko via ``pypm``:: + + C:\> pypm install paramiko + The following packages will be installed into "%APPDATA%\Python" (2.7): + paramiko-1.7.8 pycrypto-2.4 + Get: [pypm-free.activestate.com] paramiko 1.7.8 + Get: [pypm-free.activestate.com] pycrypto 2.4 + Installing paramiko-1.7.8 + Installing pycrypto-2.4 + C:\> diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..c7164158 --- /dev/null +++ b/tasks.py @@ -0,0 +1,23 @@ +from os.path import join + +from invoke import Collection +from invocations import docs as _docs, testing + + +d = 'sites' + +# Usage doc/API site (published as docs.paramiko.org) +path = join(d, 'docs') +docs = Collection.from_module(_docs, name='docs', config={ + 'sphinx.source': path, + 'sphinx.target': join(path, '_build'), +}) + +# Main/about/changelog site ((www.)?paramiko.org) +path = join(d, 'www') +www = Collection.from_module(_docs, name='www', config={ + 'sphinx.source': path, + 'sphinx.target': join(path, '_build'), +}) + +ns = Collection(testing.test, docs=docs, www=www) @@ -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,6 +28,7 @@ import sys import unittest from optparse import OptionParser import paramiko +import threading sys.path.append('tests') @@ -141,7 +142,15 @@ def main(): if len(args) > 0: filter = '|'.join(args) suite = filter_suite_by_re(suite, filter) - runner.run(suite) + result = runner.run(suite) + # Clean up stale threads from poorly cleaned-up tests. + # TODO: make that not a problem, jeez + for thread in threading.enumerate(): + if thread is not threading.currentThread(): + thread._Thread__stop() + # Exit correctly + if not result.wasSuccessful(): + sys.exit(1) if __name__ == '__main__': diff --git a/tests/loop.py b/tests/loop.py index bdc2f2da..91c216d2 100644 --- a/tests/loop.py +++ b/tests/loop.py @@ -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. @@ -62,12 +62,8 @@ class LoopSocket (object): self.__cv.wait(self.__timeout) if len(self.__in_buffer) == 0: raise socket.timeout - if n < self.__in_buffer: - out = self.__in_buffer[:n] - self.__in_buffer = self.__in_buffer[n:] - else: - out = self.__in_buffer - self.__in_buffer = '' + out = self.__in_buffer[:n] + self.__in_buffer = self.__in_buffer[n:] return out finally: self.__lock.release() diff --git a/tests/stub_sftp.py b/tests/stub_sftp.py index 7f1ecc7e..3021d816 100644 --- a/tests/stub_sftp.py +++ b/tests/stub_sftp.py @@ -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 --git a/tests/test_auth.py b/tests/test_auth.py index 816e978b..61fe63f4 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -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 --git a/tests/test_buffered_pipe.py b/tests/test_buffered_pipe.py index f285d05b..47ece936 100644 --- a/tests/test_buffered_pipe.py +++ b/tests/test_buffered_pipe.py @@ -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. @@ -26,6 +26,8 @@ import unittest from paramiko.buffered_pipe import BufferedPipe, PipeTimeout from paramiko import pipe +from util import ParamikoTest + def delay_thread(pipe): pipe.feed('a') @@ -39,11 +41,7 @@ def close_thread(pipe): pipe.close() -class BufferedPipeTest (unittest.TestCase): - - assertTrue = unittest.TestCase.failUnless # for Python 2.3 and below - assertFalse = unittest.TestCase.failIf # for Python 2.3 and below - +class BufferedPipeTest(ParamikoTest): def test_1_buffered_pipe(self): p = BufferedPipe() self.assert_(not p.read_ready()) diff --git a/tests/test_client.py b/tests/test_client.py index 2f9b9a76..fae1d329 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -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,14 @@ Some unit tests for SSHClient. """ +from __future__ import with_statement # Python 2.5 support import socket import threading import time import unittest import weakref +import warnings +import os from binascii import hexlify import paramiko @@ -68,11 +71,9 @@ class SSHClientTest (unittest.TestCase): thread.start() def tearDown(self): - if hasattr(self, 'tc'): - self.tc.close() - self.ts.close() - self.socks.close() - self.sockl.close() + 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() @@ -186,7 +187,33 @@ class SSHClientTest (unittest.TestCase): 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']) - 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('tests/test_rsa.key') + public_host_key = paramiko.RSAKey(data=str(host_key)) + localname = os.tempnam() + + 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. diff --git a/tests/test_file.py b/tests/test_file.py index c539b221..6cb35070 100755 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -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 --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py index e28a41d3..44070cbe 100644 --- a/tests/test_hostkeys.py +++ b/tests/test_hostkeys.py @@ -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 --git a/tests/test_kex.py b/tests/test_kex.py index f6e69960..39d2e17e 100644 --- a/tests/test_kex.py +++ b/tests/test_kex.py @@ -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 --git a/tests/test_message.py b/tests/test_message.py index 7bfd44df..ad622a27 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -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 --git a/tests/test_packetizer.py b/tests/test_packetizer.py index d1eb584a..1f5bec05 100644 --- a/tests/test_packetizer.py +++ b/tests/test_packetizer.py @@ -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 --git a/tests/test_pkey.py b/tests/test_pkey.py index 89d55803..5e143373 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -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 --git a/tests/test_sftp.py b/tests/test_sftp.py index 2eadabcd..cc512c18 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -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,15 +23,15 @@ a real actual sftp server is contacted, and a new folder is created there to do test file operations in (so no existing files will be harmed). """ +from __future__ import with_statement + from binascii import hexlify -import logging import os -import random -import struct +import warnings import sys import threading -import time import unittest +import StringIO import paramiko from stub_sftp import StubServer, StubSFTPServer @@ -188,6 +188,17 @@ class SFTPTest (unittest.TestCase): finally: sftp.remove(FOLDER + '/duck.txt') + def test_3_sftp_file_can_be_used_as_context_manager(self): + """ + verify that an opened file can be used as a context manager + """ + try: + 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') + def test_4_append(self): """ verify that a file can be opened for append, and tell() still works. @@ -214,7 +225,7 @@ class SFTPTest (unittest.TestCase): """ f = sftp.open(FOLDER + '/first.txt', 'w') try: - f.write('content!\n'); + f.write('content!\n') f.close() sftp.rename(FOLDER + '/first.txt', FOLDER + '/second.txt') try: @@ -425,7 +436,7 @@ class SFTPTest (unittest.TestCase): self.assertEqual(sftp.readlink(FOLDER + '/link.txt'), 'original.txt') f = sftp.open(FOLDER + '/link.txt', 'r') - self.assertEqual(f.readlines(), [ 'original\n' ]) + self.assertEqual(f.readlines(), ['original\n']) f.close() cwd = sftp.normalize('.') @@ -553,7 +564,6 @@ class SFTPTest (unittest.TestCase): """ verify that get/put work. """ - import os, warnings warnings.filterwarnings('ignore', 'tempnam.*') localname = os.tempnam() @@ -618,7 +628,7 @@ class SFTPTest (unittest.TestCase): try: f = sftp.open(FOLDER + '/unusual.txt', 'wx') self.fail('expected exception') - except IOError, x: + except IOError: pass finally: sftp.unlink(FOLDER + '/unusual.txt') @@ -658,12 +668,12 @@ class SFTPTest (unittest.TestCase): f.close() try: f = sftp.open(FOLDER + '/zero', 'r') - data = f.readv([(0, 12)]) + f.readv([(0, 12)]) f.close() f = sftp.open(FOLDER + '/zero', 'r') f.prefetch() - data = f.read(100) + f.read(100) f.close() finally: sftp.unlink(FOLDER + '/zero') @@ -672,7 +682,6 @@ class SFTPTest (unittest.TestCase): """ verify that get/put work without confirmation. """ - import os, warnings warnings.filterwarnings('ignore', 'tempnam.*') localname = os.tempnam() @@ -684,7 +693,7 @@ class SFTPTest (unittest.TestCase): 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) f = sftp.open(FOLDER + '/bunny.txt', 'r') @@ -717,3 +726,15 @@ class SFTPTest (unittest.TestCase): finally: sftp.remove(FOLDER + '/append.txt') + def test_putfo_empty_file(self): + """ + Send an empty file and confirm it is sent. + """ + target = FOLDER + '/empty file.txt' + stream = StringIO.StringIO() + try: + attrs = sftp.putfo(stream, target) + # the returned attributes should not be null + self.assertNotEqual(attrs, None) + finally: + sftp.remove(target) diff --git a/tests/test_sftp_big.py b/tests/test_sftp_big.py index a32a7000..04b15b0d 100644 --- a/tests/test_sftp_big.py +++ b/tests/test_sftp_big.py @@ -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 --git a/tests/test_transport.py b/tests/test_transport.py index cea4a1dd..e8f7f366 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -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. @@ -36,6 +36,7 @@ from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED from paramiko.common import MSG_KEXINIT, MSG_CHANNEL_WINDOW_ADJUST from paramiko.message import Message from loop import LoopSocket +from util import ParamikoTest LONG_BANNER = """\ @@ -105,11 +106,7 @@ class NullServer (ServerInterface): return OPEN_SUCCEEDED -class TransportTest (unittest.TestCase): - - assertTrue = unittest.TestCase.failUnless # for Python 2.3 and below - assertFalse = unittest.TestCase.failIf # for Python 2.3 and below - +class TransportTest(ParamikoTest): def setUp(self): self.socks = LoopSocket() self.sockc = LoopSocket() diff --git a/tests/test_util.py b/tests/test_util.py index 256c3d7c..12677a9b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -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,11 +22,14 @@ Some unit tests for utility functions. from binascii import hexlify import cStringIO +import errno import os import unittest from Crypto.Hash import SHA import paramiko.util +from paramiko.util import lookup_ssh_host_config as host_config +from util import ParamikoTest test_config_file = """\ Host * @@ -57,17 +60,7 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ from paramiko import * -class UtilTest (unittest.TestCase): - - assertTrue = unittest.TestCase.failUnless # for Python 2.3 and below - assertFalse = unittest.TestCase.failIf # for Python 2.3 and below - - def setUp(self): - pass - - def tearDown(self): - pass - +class UtilTest(ParamikoTest): def test_1_import(self): """ verify that all the classes can be imported from paramiko. @@ -111,21 +104,37 @@ class UtilTest (unittest.TestCase): f = cStringIO.StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) self.assertEquals(config._config, - [ {'identityfile': '~/.ssh/id_rsa', 'host': '*', 'user': 'robey', - 'crazy': 'something dumb '}, - {'host': '*.example.com', 'user': 'bjork', 'port': '3333'}, - {'host': 'spoo.example.com', 'crazy': 'something else'}]) + [{'host': ['*'], 'config': {}}, {'host': ['*'], 'config': {'identityfile': ['~/.ssh/id_rsa'], 'user': 'robey'}}, + {'host': ['*.example.com'], 'config': {'user': 'bjork', 'port': '3333'}}, + {'host': ['*'], 'config': {'crazy': 'something dumb '}}, + {'host': ['spoo.example.com'], 'config': {'crazy': 'something else'}}]) def test_3_host_config(self): global test_config_file f = cStringIO.StringIO(test_config_file) config = paramiko.util.parse_ssh_config(f) - c = paramiko.util.lookup_ssh_host_config('irc.danger.com', config) - self.assertEquals(c, {'identityfile': '~/.ssh/id_rsa', 'user': 'robey', 'crazy': 'something dumb '}) - c = paramiko.util.lookup_ssh_host_config('irc.example.com', config) - self.assertEquals(c, {'identityfile': '~/.ssh/id_rsa', 'user': 'bjork', 'crazy': 'something dumb ', 'port': '3333'}) - c = paramiko.util.lookup_ssh_host_config('spoo.example.com', config) - self.assertEquals(c, {'identityfile': '~/.ssh/id_rsa', 'user': 'bjork', 'crazy': 'something else', 'port': '3333'}) + + for host, values in { + 'irc.danger.com': {'crazy': 'something dumb ', + 'hostname': 'irc.danger.com', + 'user': 'robey'}, + 'irc.example.com': {'crazy': 'something dumb ', + 'hostname': 'irc.example.com', + 'user': 'robey', + 'port': '3333'}, + 'spoo.example.com': {'crazy': 'something dumb ', + 'hostname': 'spoo.example.com', + 'user': 'robey', + 'port': '3333'} + }.items(): + values = dict(values, + hostname=host, + identityfile=[os.path.expanduser("~/.ssh/id_rsa")] + ) + self.assertEquals( + 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) @@ -151,4 +160,183 @@ class UtilTest (unittest.TestCase): # 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.* + Port 22 + +Host *.example.com + Port 2222 + +Host * + Port 3333 + """ + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + host = 'www13.example.com' + self.assertEquals( + 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')) + + # Variables that are set by raises_intr + intr_errors_remaining = [3] + call_count = [0] + def raises_intr(): + call_count[0] += 1 + if intr_errors_remaining[0] > 0: + 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]) + + def raises_ioerror_not_eintr(): + raise IOError(errno.ENOENT, 'file', 'file not found') + self.assertRaises(IOError, + lambda: paramiko.util.retry_on_signal(raises_ioerror_not_eintr)) + + def raises_other_exception(): + raise AssertionError('foo') + self.assertRaises(AssertionError, + lambda: paramiko.util.retry_on_signal(raises_other_exception)) + + def test_9_proxycommand_config_equals_parsing(self): + """ + ProxyCommand should not split on equals signs within the value. + """ + conf = """ +Host space-delimited + ProxyCommand foo bar=biz baz + +Host equals-delimited + ProxyCommand=foo bar=biz baz +""" + f = cStringIO.StringIO(conf) + config = paramiko.util.parse_ssh_config(f) + for host in ('space-delimited', 'equals-delimited'): + self.assertEquals( + host_config(host, config)['proxycommand'], + 'foo bar=biz baz' + ) + + def test_10_proxycommand_interpolation(self): + """ + ProxyCommand should perform interpolation on the value + """ + config = paramiko.util.parse_ssh_config(cStringIO.StringIO(""" +Host specific + Port 37 + ProxyCommand host %h port %p lol + +Host portonly + Port 155 + +Host * + Port 25 + ProxyCommand host %h port %p +""")) + for host, val in ( + ('foo.com', "host foo.com port 25"), + ('specific', "host specific port 37 lol"), + ('portonly', "host portonly port 155"), + ): + self.assertEquals( + host_config(host, config)['proxycommand'], + val + ) + + def test_11_host_config_test_negation(self): + test_config_file = """ +Host www13.* !*.example.com + Port 22 + +Host *.example.com !www13.* + Port 2222 + +Host www13.* + Port 8080 + +Host * + Port 3333 + """ + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + host = 'www13.example.com' + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + {'hostname': host, 'port': '8080'} + ) + + def test_12_host_config_test_proxycommand(self): + test_config_file = """ +Host proxy-with-equal-divisor-and-space +ProxyCommand = foo=bar + +Host proxy-with-equal-divisor-and-no-space +ProxyCommand=foo=bar + +Host proxy-without-equal-divisor +ProxyCommand foo=bar:%h-%p + """ + for host, values in { + 'proxy-with-equal-divisor-and-space' :{'hostname': 'proxy-with-equal-divisor-and-space', + 'proxycommand': 'foo=bar'}, + 'proxy-with-equal-divisor-and-no-space':{'hostname': 'proxy-with-equal-divisor-and-no-space', + 'proxycommand': 'foo=bar'}, + 'proxy-without-equal-divisor' :{'hostname': 'proxy-without-equal-divisor', + 'proxycommand': + 'foo=bar:proxy-without-equal-divisor-22'} + }.items(): + + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEquals( + paramiko.util.lookup_ssh_host_config(host, config), + values + ) + + def test_11_host_config_test_identityfile(self): + test_config_file = """ + +IdentityFile id_dsa0 + +Host * +IdentityFile id_dsa1 + +Host dsa2 +IdentityFile id_dsa2 + +Host dsa2* +IdentityFile id_dsa22 + """ + for host, values in { + 'foo' :{'hostname': 'foo', + 'identityfile': ['id_dsa0', 'id_dsa1']}, + 'dsa2' :{'hostname': 'dsa2', + 'identityfile': ['id_dsa0', 'id_dsa1', 'id_dsa2', 'id_dsa22']}, + 'dsa22' :{'hostname': 'dsa22', + 'identityfile': ['id_dsa0', 'id_dsa1', 'id_dsa22']} + }.items(): + + f = cStringIO.StringIO(test_config_file) + config = paramiko.util.parse_ssh_config(f) + self.assertEquals( + 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(cStringIO.StringIO(test_config)) + assert config.lookup('meh') # will die during lookup() if bug regresses diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 00000000..2e0be087 --- /dev/null +++ b/tests/util.py @@ -0,0 +1,10 @@ +import unittest + + +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 + diff --git a/tox-requirements.txt b/tox-requirements.txt new file mode 100644 index 00000000..26224ce6 --- /dev/null +++ b/tox-requirements.txt @@ -0,0 +1,2 @@ +# Not sure why tox can't just read setup.py? +pycrypto diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..af4fbf20 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py25,py26,py27 + +[testenv] +commands = pip install --use-mirrors -q -r tox-requirements.txt + python test.py |