summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--NEWS62
-rw-r--r--README7
-rwxr-xr-xdemos/demo.py1
-rw-r--r--demos/forward.py4
-rw-r--r--dev-requirements.txt2
-rw-r--r--fabfile.py28
-rw-r--r--paramiko/__init__.py4
-rw-r--r--paramiko/agent.py17
-rw-r--r--paramiko/client.py7
-rw-r--r--paramiko/config.py52
-rw-r--r--paramiko/hostkeys.py18
-rw-r--r--paramiko/packet.py13
-rw-r--r--paramiko/sftp_client.py2
-rw-r--r--paramiko/transport.py10
-rw-r--r--requirements.txt3
-rw-r--r--setup.py2
-rwxr-xr-xtests/test_sftp.py32
-rw-r--r--tests/test_util.py11
-rw-r--r--tox.ini2
19 files changed, 202 insertions, 75 deletions
diff --git a/NEWS b/NEWS
index 6bf371cd..4123b8f2 100644
--- a/NEWS
+++ b/NEWS
@@ -12,8 +12,44 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
Releases
========
-v1.11.0 (DD MM YYYY)
---------------------
+v1.11.2 (27th Sep 2013)
+-----------------------
+
+* #156: Fix potential deadlock condition when using Channel objects as sockets
+ (e.g. when using SSH gatewaying). Thanks to Steven Noonan and Frank Arnold
+ for catch & patch.
+
+v1.10.4 (27th Sep 2013)
+-----------------------
+
+* #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.
+
+v1.11.1 (20th Sep 2013)
+-----------------------
+
+* #162: Clean up HMAC module import to avoid deadlocks in certain uses of
+ SSHClient. Thanks to Gernot Hillier for the catch & suggested
+ fix.
+* #36: Fix the port-forwarding demo to avoid file descriptor errors. Thanks to
+ Jonathan Halcrow for catch & patch.
+* #168: Update config handling to properly handle multiple 'localforward' and
+ 'remoteforward' keys. Thanks to Emre Yılmaz for the patch.
+
+v1.10.3 (20th Sep 2013)
+-----------------------
+
+* #162: Clean up HMAC module import to avoid deadlocks in certain uses of
+ SSHClient. Thanks to Gernot Hillier for the catch & suggested
+ fix.
+* #36: Fix the port-forwarding demo to avoid file descriptor errors. Thanks to
+ Jonathan Halcrow for catch & patch.
+* #168: Update config handling to properly handle multiple 'localforward' and
+ 'remoteforward' keys. Thanks to Emre Yılmaz for the patch.
+
+v1.11.0 (26th Jul 2013)
+-----------------------
* #152: Add tentative support for ECDSA keys. *This adds the ecdsa
module as a dependency of Paramiko.* The module is available at
@@ -32,6 +68,28 @@ v1.11.0 (DD MM YYYY)
dependent on ctypes for constructing appropriate structures and had ctypes
implementations of all functionality. Thanks to Jason R. Coombs for the
patch.
+* #87: Ensure updates to `known_hosts` files account for any updates to said
+ files after Paramiko initially read them. (Includes related fix to guard
+ against duplicate entries during subsequent `known_hosts` loads.) Thanks to
+ `@sunweaver` for the contribution.
+
+v1.10.2 (26th Jul 2013)
+-----------------------
+
+* #153, #67: Warn on parse failure when reading known_hosts file. Thanks to
+ `@glasserc` for patch.
+* #146: Indentation fixes for readability. Thanks to Abhinav Upadhyay for catch
+ & patch.
+
+v1.10.1 (5th Apr 2013)
+----------------------
+
+* #142: (Fabric #811) SFTP put of empty file will still return the attributes
+ of the put file. Thanks to Jason R. Coombs for the patch.
+* #154: (Fabric #876) Forwarded SSH agent connections left stale local pipes
+ lying around, which could cause local (and sometimes remote or network)
+ resource starvation when running many agent-using remote commands. Thanks to
+ Kevin Tegtmeier for catch & patch.
v1.10.0 (1st Mar 2013)
--------------------
diff --git a/README b/README
index 310a7f02..1899819a 100644
--- a/README
+++ b/README
@@ -8,12 +8,7 @@ paramiko
:Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org>
:License: LGPL
:Homepage: https://github.com/paramiko/paramiko/
-
-
-paramiko 1.8.0
-==============
-
-Release of MM.YY.DD
+:API docs: http://docs.paramiko.org
What
diff --git a/demos/demo.py b/demos/demo.py
index 05524d3c..c21a926e 100755
--- a/demos/demo.py
+++ b/demos/demo.py
@@ -26,7 +26,6 @@ import os
import select
import socket
import sys
-import threading
import time
import traceback
diff --git a/demos/forward.py b/demos/forward.py
index 4e107855..2a4c4248 100644
--- a/demos/forward.py
+++ b/demos/forward.py
@@ -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/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 00000000..f706c46f
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,2 @@
+tox>=1.4,<1.5
+epydoc>=3.0,<3.1
diff --git a/fabfile.py b/fabfile.py
index 29394f94..7883daba 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -1,8 +1,10 @@
-from fabric.api import task, sudo, env
+from fabric.api import task, sudo, env, local, hosts
from fabric.contrib.project import rsync_project
+from fabric.contrib.console import confirm
@task
+@hosts("paramiko.org")
def upload_docs():
target = "/var/www/paramiko.org"
staging = "/tmp/paramiko_docs"
@@ -11,3 +13,27 @@ def upload_docs():
sudo("rm -rf %s/*" % target)
rsync_project(local_dir='docs/', remote_dir=staging, delete=True)
sudo("cp -R %s/* %s/" % (staging, target))
+
+@task
+def build_docs():
+ local("epydoc --no-private -o docs/ paramiko")
+
+@task
+def clean():
+ local("rm -rf build dist docs")
+ local("rm -f MANIFEST *.log demos/*.log")
+ local("rm -f paramiko/*.pyc")
+ local("rm -f test.log")
+ local("rm -rf paramiko.egg-info")
+
+@task
+def test():
+ local("python ./test.py")
+
+@task
+def release():
+ confirm("Only hit Enter if you remembered to update the version!")
+ confirm("Also, did you remember to tag your release?")
+ build_docs()
+ local("python setup.py sdist register upload")
+ upload_docs()
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 08eaf9b1..56a7a414 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -46,6 +46,8 @@ Paramiko is written entirely in python (no C or platform-dependent code) and is
released under the GNU Lesser General Public License (LGPL).
Website: U{https://github.com/paramiko/paramiko/}
+
+Mailing list: U{paramiko@librelist.com<mailto:paramiko@librelist.com>}
"""
import sys
@@ -55,7 +57,7 @@ if sys.version_info < (2, 5):
__author__ = "Jeff Forcier <jeff@bitprophet.org>"
-__version__ = "1.10.0"
+__version__ = "1.12.0"
__license__ = "GNU Lesser General Public License (LGPL)"
diff --git a/paramiko/agent.py b/paramiko/agent.py
index 5d04dce8..d4ff7036 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -130,15 +130,22 @@ class AgentProxyThread(threading.Thread):
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
@@ -248,11 +255,11 @@ class AgentServerProxy(AgentSSH):
self.close()
def connect(self):
- conn_sock = self.__t.open_forward_agent_channel()
- if conn_sock is None:
- raise SSHException('lost ssh-agent')
- conn_sock.set_name('auth-agent')
- self._connect(conn_sock)
+ conn_sock = self.__t.open_forward_agent_channel()
+ if conn_sock is None:
+ raise SSHException('lost ssh-agent')
+ conn_sock.set_name('auth-agent')
+ self._connect(conn_sock)
def close(self):
"""
diff --git a/paramiko/client.py b/paramiko/client.py
index 5b719581..493d5481 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -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.known_hosts is not None:
+ self.load_host_keys(self.known_hosts)
+
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()))
diff --git a/paramiko/config.py b/paramiko/config.py
index e41bae43..520da356 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -35,9 +35,10 @@ class LazyFqdn(object):
Returns the host's fqdn on request as string.
"""
- def __init__(self, config):
+ def __init__(self, config, host=None):
self.fqdn = None
self.config = config
+ self.host = host
def __str__(self):
if self.fqdn is None:
@@ -54,19 +55,27 @@ class LazyFqdn(object):
fqdn = None
address_family = self.config.get('addressfamily', 'any').lower()
if address_family != 'any':
- family = socket.AF_INET if address_family == 'inet' \
- else socket.AF_INET6
- results = socket.getaddrinfo(host,
- None,
- family,
- socket.SOCK_DGRAM,
- socket.IPPROTO_IP,
- socket.AI_CANONNAME)
- for res in results:
- af, socktype, proto, canonname, sa = res
- if canonname and '.' in canonname:
- fqdn = canonname
- break
+ 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()
@@ -126,16 +135,17 @@ class SSHConfig (object):
self._config.append(host)
value = value.split()
host = {key: value, 'config': {}}
- #identityfile is a special case, since it is allowed to be
+ #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 == 'identityfile':
+
+ elif key in ['identityfile', 'localforward', 'remoteforward']:
if key in host['config']:
- host['config']['identityfile'].append(value)
+ host['config'][key].append(value)
else:
- host['config']['identityfile'] = [value]
+ host['config'][key] = [value]
elif key not in host['config']:
- host['config'].update({key: value})
+ host['config'].update({key: value})
self._config.append(host)
def lookup(self, hostname):
@@ -215,7 +225,7 @@ class SSHConfig (object):
remoteuser = user
host = socket.gethostname().split('.')[0]
- fqdn = LazyFqdn(config)
+ fqdn = LazyFqdn(config, host)
homedir = os.path.expanduser('~')
replacements = {'controlpath':
[
@@ -252,5 +262,5 @@ class SSHConfig (object):
config[k][item] = config[k][item].\
replace(find, str(replace))
else:
- config[k] = config[k].replace(find, str(replace))
+ config[k] = config[k].replace(find, str(replace))
return config
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index edc9300f..03cfefd7 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -28,6 +28,7 @@ import UserDict
from paramiko.common import *
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
+from paramiko.util import get_logger
from paramiko.ecdsakey import ECDSAKey
@@ -49,7 +50,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
@@ -62,9 +63,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]
@@ -81,6 +85,7 @@ class HostKeyEntry:
elif keytype == 'ecdsa-sha2-nistp256':
key = ECDSAKey(data=base64.decodestring(key))
else:
+ log.info("Unable to handle key of type %s" % (keytype,))
return None
except binascii.Error, e:
@@ -164,13 +169,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/packet.py b/paramiko/packet.py
index 38a6d4b5..99138edc 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -33,17 +33,13 @@ 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):
@@ -156,7 +152,6 @@ class Packetizer (object):
def close(self):
self.__closed = True
- self.__socket.close()
def set_hexdump(self, hexdump):
self.__dump_packets = hexdump
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 7df643f5..17ea493c 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -575,7 +575,7 @@ class SFTPClient (BaseSFTP):
break
finally:
fr.close()
- if confirm and file_size:
+ if confirm:
s = self.stat(remotepath)
if s.st_size != size:
raise IOError('size mismatch in put! %d != %d' % (s.st_size, size))
diff --git a/paramiko/transport.py b/paramiko/transport.py
index aca51a94..89cf1d55 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -402,7 +402,6 @@ class Transport (threading.Thread):
@since: 1.5.3
"""
- self.sock.close()
self.close()
def get_security_options(self):
@@ -616,11 +615,10 @@ class Transport (threading.Thread):
"""
if not self.active:
return
- self.active = False
- self.packetizer.close()
- self.join()
+ self.stop_thread()
for chan in self._channels.values():
chan._unlink()
+ self.sock.close()
def get_remote_server_key(self):
"""
@@ -1393,6 +1391,8 @@ class Transport (threading.Thread):
def stop_thread(self):
self.active = False
self.packetizer.close()
+ while self.isAlive():
+ self.join(10)
### internals...
@@ -1441,7 +1441,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:
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index bc21503b..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-pycrypto
-tox
-ecdsa
diff --git a/setup.py b/setup.py
index 4b1dc3b0..b0adbc7c 100644
--- a/setup.py
+++ b/setup.py
@@ -54,7 +54,7 @@ if sys.platform == 'darwin':
setup(name = "paramiko",
- version = "1.10.0",
+ version = "1.12.0",
description = "SSH2 protocol library",
author = "Jeff Forcier",
author_email = "jeff@bitprophet.org",
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index f95da69c..b1697ea6 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -26,14 +26,12 @@ 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
@@ -227,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:
@@ -438,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('.')
@@ -566,7 +564,6 @@ class SFTPTest (unittest.TestCase):
"""
verify that get/put work.
"""
- import os, warnings
warnings.filterwarnings('ignore', 'tempnam.*')
localname = os.tempnam()
@@ -631,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')
@@ -671,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')
@@ -685,7 +682,6 @@ class SFTPTest (unittest.TestCase):
"""
verify that get/put work without confirmation.
"""
- import os, warnings
warnings.filterwarnings('ignore', 'tempnam.*')
localname = os.tempnam()
@@ -697,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')
@@ -730,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_util.py b/tests/test_util.py
index efda9b2f..a528224e 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -329,3 +329,14 @@ IdentityFile id_dsa22
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/tox.ini b/tox.ini
index 6cb80012..e2a8dcf4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,5 +2,5 @@
envlist = py25,py26,py27
[testenv]
-commands = pip install --use-mirrors -q -r requirements.txt
+commands = pip install --use-mirrors -q -r dev-requirements.txt
python test.py