Merge branch '1.11' into 156-int

This commit is contained in:
Jeff Forcier 2013-09-27 17:30:34 -07:00
commit cba4c68365
17 changed files with 153 additions and 50 deletions

45
NEWS
View File

@ -12,8 +12,37 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
Releases Releases
======== ========
v1.11.0 (DD MM YYYY) 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)
-----------------------
* #98: On Windows, when interacting with the PuTTY PAgeant, Paramiko now * #98: On Windows, when interacting with the PuTTY PAgeant, Paramiko now
creates the shared memory map with explicit Security Attributes of the user, creates the shared memory map with explicit Security Attributes of the user,
@ -24,6 +53,18 @@ v1.11.0 (DD MM YYYY)
dependent on ctypes for constructing appropriate structures and had ctypes dependent on ctypes for constructing appropriate structures and had ctypes
implementations of all functionality. Thanks to Jason R. Coombs for the implementations of all functionality. Thanks to Jason R. Coombs for the
patch. 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) v1.10.1 (5th Apr 2013)
---------------------- ----------------------

1
README
View File

@ -8,6 +8,7 @@ paramiko
:Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org> :Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org>
:License: LGPL :License: LGPL
:Homepage: https://github.com/paramiko/paramiko/ :Homepage: https://github.com/paramiko/paramiko/
:API docs: http://docs.paramiko.org
What What

View File

@ -26,7 +26,6 @@ import os
import select import select
import socket import socket
import sys import sys
import threading
import time import time
import traceback import traceback

View File

@ -78,9 +78,11 @@ class Handler (SocketServer.BaseRequestHandler):
if len(data) == 0: if len(data) == 0:
break break
self.request.send(data) self.request.send(data)
peername = self.request.getpeername()
chan.close() chan.close()
self.request.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): def forward_tunnel(local_port, remote_host, remote_port, transport):

2
dev-requirements.txt Normal file
View File

@ -0,0 +1,2 @@
tox>=1.4,<1.5
epydoc>=3.0,<3.1

28
fabfile.py vendored
View File

@ -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.project import rsync_project
from fabric.contrib.console import confirm
@task @task
@hosts("paramiko.org")
def upload_docs(): def upload_docs():
target = "/var/www/paramiko.org" target = "/var/www/paramiko.org"
staging = "/tmp/paramiko_docs" staging = "/tmp/paramiko_docs"
@ -11,3 +13,27 @@ def upload_docs():
sudo("rm -rf %s/*" % target) sudo("rm -rf %s/*" % target)
rsync_project(local_dir='docs/', remote_dir=staging, delete=True) rsync_project(local_dir='docs/', remote_dir=staging, delete=True)
sudo("cp -R %s/* %s/" % (staging, target)) 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()

View File

@ -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). released under the GNU Lesser General Public License (LGPL).
Website: U{https://github.com/paramiko/paramiko/} Website: U{https://github.com/paramiko/paramiko/}
Mailing list: U{paramiko@librelist.com<mailto:paramiko@librelist.com>}
""" """
import sys import sys
@ -55,7 +57,7 @@ if sys.version_info < (2, 5):
__author__ = "Jeff Forcier <jeff@bitprophet.org>" __author__ = "Jeff Forcier <jeff@bitprophet.org>"
__version__ = "1.10.1" __version__ = "1.11.1"
__license__ = "GNU Lesser General Public License (LGPL)" __license__ = "GNU Lesser General Public License (LGPL)"

View File

@ -255,11 +255,11 @@ class AgentServerProxy(AgentSSH):
self.close() self.close()
def connect(self): def connect(self):
conn_sock = self.__t.open_forward_agent_channel() conn_sock = self.__t.open_forward_agent_channel()
if conn_sock is None: if conn_sock is None:
raise SSHException('lost ssh-agent') raise SSHException('lost ssh-agent')
conn_sock.set_name('auth-agent') conn_sock.set_name('auth-agent')
self._connect(conn_sock) self._connect(conn_sock)
def close(self): def close(self):
""" """

View File

@ -186,8 +186,13 @@ class SSHClient (object):
@raise IOError: if the file could not be written @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 = open(filename, 'w')
f.write('# SSH host keys collected by paramiko\n')
for hostname, keys in self._host_keys.iteritems(): for hostname, keys in self._host_keys.iteritems():
for keytype, key in keys.iteritems(): for keytype, key in keys.iteritems():
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64())) f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))

View File

@ -35,9 +35,10 @@ class LazyFqdn(object):
Returns the host's fqdn on request as string. Returns the host's fqdn on request as string.
""" """
def __init__(self, config): def __init__(self, config, host=None):
self.fqdn = None self.fqdn = None
self.config = config self.config = config
self.host = host
def __str__(self): def __str__(self):
if self.fqdn is None: if self.fqdn is None:
@ -54,19 +55,27 @@ class LazyFqdn(object):
fqdn = None fqdn = None
address_family = self.config.get('addressfamily', 'any').lower() address_family = self.config.get('addressfamily', 'any').lower()
if address_family != 'any': if address_family != 'any':
family = socket.AF_INET if address_family == 'inet' \ try:
else socket.AF_INET6 family = socket.AF_INET if address_family == 'inet' \
results = socket.getaddrinfo(host, else socket.AF_INET6
None, results = socket.getaddrinfo(
family, self.host,
socket.SOCK_DGRAM, None,
socket.IPPROTO_IP, family,
socket.AI_CANONNAME) socket.SOCK_DGRAM,
for res in results: socket.IPPROTO_IP,
af, socktype, proto, canonname, sa = res socket.AI_CANONNAME
if canonname and '.' in canonname: )
fqdn = canonname for res in results:
break 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 # Handle 'any' / unspecified
if fqdn is None: if fqdn is None:
fqdn = socket.getfqdn() fqdn = socket.getfqdn()
@ -126,16 +135,17 @@ class SSHConfig (object):
self._config.append(host) self._config.append(host)
value = value.split() value = value.split()
host = {key: value, 'config': {}} 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 # specified multiple times and they should be tried in order
# of specification. # of specification.
elif key == 'identityfile':
elif key in ['identityfile', 'localforward', 'remoteforward']:
if key in host['config']: if key in host['config']:
host['config']['identityfile'].append(value) host['config'][key].append(value)
else: else:
host['config']['identityfile'] = [value] host['config'][key] = [value]
elif key not in host['config']: elif key not in host['config']:
host['config'].update({key: value}) host['config'].update({key: value})
self._config.append(host) self._config.append(host)
def lookup(self, hostname): def lookup(self, hostname):
@ -215,7 +225,7 @@ class SSHConfig (object):
remoteuser = user remoteuser = user
host = socket.gethostname().split('.')[0] host = socket.gethostname().split('.')[0]
fqdn = LazyFqdn(config) fqdn = LazyFqdn(config, host)
homedir = os.path.expanduser('~') homedir = os.path.expanduser('~')
replacements = {'controlpath': replacements = {'controlpath':
[ [
@ -252,5 +262,5 @@ class SSHConfig (object):
config[k][item] = config[k][item].\ config[k][item] = config[k][item].\
replace(find, str(replace)) replace(find, str(replace))
else: else:
config[k] = config[k].replace(find, str(replace)) config[k] = config[k].replace(find, str(replace))
return config return config

View File

@ -28,6 +28,7 @@ import UserDict
from paramiko.common import * from paramiko.common import *
from paramiko.dsskey import DSSKey from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey from paramiko.rsakey import RSAKey
from paramiko.util import get_logger
class InvalidHostKey(Exception): class InvalidHostKey(Exception):
@ -48,7 +49,7 @@ class HostKeyEntry:
self.hostnames = hostnames self.hostnames = hostnames
self.key = key 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, 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 the type of key, and the key data. The line is expected to be in the
@ -61,9 +62,12 @@ class HostKeyEntry:
@param line: a line from an OpenSSH known_hosts file @param line: a line from an OpenSSH known_hosts file
@type line: str @type line: str
""" """
log = get_logger('paramiko.hostkeys')
fields = line.split(' ') fields = line.split(' ')
if len(fields) < 3: if len(fields) < 3:
# Bad number of fields # Bad number of fields
log.info("Not enough fields found in known_hosts in line %s (%r)" %
(lineno, line))
return None return None
fields = fields[:3] fields = fields[:3]
@ -78,6 +82,7 @@ class HostKeyEntry:
elif keytype == 'ssh-dss': elif keytype == 'ssh-dss':
key = DSSKey(data=base64.decodestring(key)) key = DSSKey(data=base64.decodestring(key))
else: else:
log.info("Unable to handle key of type %s" % (keytype,))
return None return None
except binascii.Error, e: except binascii.Error, e:
raise InvalidHostKey(line, e) raise InvalidHostKey(line, e)
@ -160,13 +165,18 @@ class HostKeys (UserDict.DictMixin):
@raise IOError: if there was an error reading the file @raise IOError: if there was an error reading the file
""" """
f = open(filename, 'r') f = open(filename, 'r')
for line in f: for lineno, line in enumerate(f):
line = line.strip() line = line.strip()
if (len(line) == 0) or (line[0] == '#'): if (len(line) == 0) or (line[0] == '#'):
continue continue
e = HostKeyEntry.from_line(line) e = HostKeyEntry.from_line(line, lineno)
if e is not None: 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() f.close()
def save(self, filename): def save(self, filename):

View File

@ -33,17 +33,13 @@ from paramiko.ssh_exception import SSHException, ProxyCommandFailure
from paramiko.message import Message from paramiko.message import Message
got_r_hmac = False
try: try:
import r_hmac from r_hmac import HMAC
got_r_hmac = True
except ImportError: except ImportError:
pass from Crypto.Hash.HMAC import HMAC
def compute_hmac(key, message, digest_class): def compute_hmac(key, message, digest_class):
if got_r_hmac: return HMAC(key, message, digest_class).digest()
return r_hmac.HMAC(key, message, digest_class).digest()
from Crypto.Hash import HMAC
return HMAC.HMAC(key, message, digest_class).digest()
class NeedRekeyException (Exception): class NeedRekeyException (Exception):

View File

@ -1439,7 +1439,7 @@ class Transport (threading.Thread):
break break
self.clear_to_send_lock.release() self.clear_to_send_lock.release()
if time.time() > start + self.clear_to_send_timeout: 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: try:
self._send_message(data) self._send_message(data)
finally: finally:

View File

@ -1,2 +0,0 @@
pycrypto
tox

View File

@ -52,7 +52,7 @@ if sys.platform == 'darwin':
setup(name = "paramiko", setup(name = "paramiko",
version = "1.10.1", version = "1.11.1",
description = "SSH2 protocol library", description = "SSH2 protocol library",
author = "Jeff Forcier", author = "Jeff Forcier",
author_email = "jeff@bitprophet.org", author_email = "jeff@bitprophet.org",

View File

@ -329,3 +329,14 @@ IdentityFile id_dsa22
paramiko.util.lookup_ssh_host_config(host, config), paramiko.util.lookup_ssh_host_config(host, config),
values 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

View File

@ -2,5 +2,5 @@
envlist = py25,py26,py27 envlist = py25,py26,py27
[testenv] [testenv]
commands = pip install --use-mirrors -q -r requirements.txt commands = pip install --use-mirrors -q -r dev-requirements.txt
python test.py python test.py