Merge branch 'master' into 112-int

Conflicts:
	paramiko/win_pageant.py
This commit is contained in:
Jeff Forcier 2013-03-19 13:36:52 -07:00
commit a7ee2509e4
16 changed files with 344 additions and 116 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
*.pyc
build/
dist/
.tox/
paramiko.egg-info/
test.log
docs/

33
NEWS
View File

@ -12,9 +12,40 @@ Issues noted as "Fabric #NN" can be found at https://github.com/fabric/fabric/.
Releases
========
v1.10.0 (DD MM YYYY)
v1.11.0 (DD MM YYYY)
--------------------
* #100: Remove use of PyWin32 in `win_pageant` module. Module was already
dependent on ctypes for constructing appropriate structures and had ctypes
implementations of all functionality. Thanks to Jason R. Coombs for the
patch.
v1.10.0 (1st Mar 2013)
--------------------
* #66: Batch SFTP writes to help speed up file transfers. Thanks to Olle
Lundberg for the patch.
* #133: Fix handling of window-change events to be on-spec and not
attempt to wait for a response from the remote sshd; this fixes problems with
less common targets such as some Cisco devices. Thanks to Phillip Heller for
catch & patch.
* #93: Overhaul SSH config parsing to be in line with `man ssh_config` (& the
behavior of `ssh` itself), including addition of parameter expansion within
config values. Thanks to Olle Lundberg for the patch.
* #110: Honor SSH config `AddressFamily` setting when looking up local
host's FQDN. Thanks to John Hensley for the patch.
* #128: Defer FQDN resolution until needed, when parsing SSH config files.
Thanks to Parantapa Bhattacharya for catch & patch.
* #102: Forego random padding for packets when running under `*-ctr` ciphers.
This corrects some slowdowns on platforms where random byte generation is
inefficient (e.g. Windows). Thanks to `@warthog618` for catch & patch, and
Michael van der Kolff for code/technique review.
* #127: Turn `SFTPFile` into a context manager. Thanks to Michael Williamson
for the patch.
* #116: Limit `Message.get_bytes` to an upper bound of 1MB to protect against
potential DoS vectors. Thanks to `@mvschaik` for catch & patch.
* #115: Add convenience `get_pty` kwarg to `Client.exec_command` so users not
manually controlling a channel object can still toggle PTY creation. Thanks
to Michael van der Kolff for the patch.
* #71: Add `SFTPClient.putfo` and `.getfo` methods to allow direct
uploading/downloading of file-like objects. Thanks to Eric Buehl for the
patch.

20
README
View File

@ -5,7 +5,7 @@ paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
:Copyright: Copyright (c) 2012 Jeff Forcier <jeff@bitprophet.org>
:Copyright: Copyright (c) 2013 Jeff Forcier <jeff@bitprophet.org>
:License: LGPL
:Homepage: https://github.com/paramiko/paramiko/
@ -20,7 +20,7 @@ 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
@ -39,8 +39,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
@ -58,19 +57,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::

View File

@ -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
@ -50,8 +50,8 @@ 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__ = "Jeff Forcier <jeff@bitprophet.org>"

View File

@ -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):
"""

View File

@ -349,7 +349,7 @@ class SSHClient (object):
self._agent.close()
self._agent = None
def exec_command(self, command, bufsize=-1, timeout=None):
def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False):
"""
Execute a command on the SSH server. A new L{Channel} is opened and
the requested command is executed. The command's input and output
@ -368,6 +368,8 @@ class SSHClient (object):
@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)
@ -375,7 +377,8 @@ class SSHClient (object):
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
@ -387,13 +390,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

View File

@ -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.
#
@ -29,6 +30,51 @@ SSH_PORT = 22
proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
class LazyFqdn(object):
"""
Returns the host's fqdn on request as string.
"""
def __init__(self, config):
self.fqdn = None
self.config = config
def __str__(self):
if self.fqdn is None:
#
# If the SSH config contains AddressFamily, use that when
# determining the local host's FQDN. Using socket.getfqdn() from
# the standard library is the most general solution, but can
# result in noticeable delays on some platforms when IPv6 is
# misconfigured or not available, as it calls getaddrinfo with no
# address family specified, so both IPv4 and IPv6 are checked.
#
# Handle specific option
fqdn = None
address_family = self.config.get('addressfamily', 'any').lower()
if address_family != 'any':
family = socket.AF_INET if address_family == 'inet' \
else socket.AF_INET6
results = socket.getaddrinfo(host,
None,
family,
socket.SOCK_DGRAM,
socket.IPPROTO_IP,
socket.AI_CANONNAME)
for res in results:
af, socktype, proto, canonname, sa = res
if canonname and '.' in canonname:
fqdn = canonname
break
# Handle 'any' / unspecified
if fqdn is None:
fqdn = socket.getfqdn()
# Cache
self.fqdn = fqdn
return self.fqdn
class SSHConfig (object):
"""
Representation of config information as stored in the format used by
@ -44,7 +90,7 @@ class SSHConfig (object):
"""
Create a new OpenSSH config object.
"""
self._config = [ { 'host': '*' } ]
self._config = []
def parse(self, file_obj):
"""
@ -53,7 +99,7 @@ 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] == '#'):
@ -77,20 +123,20 @@ 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 is a special case, since it is allowed to be
# specified multiple times and they should be tried in order
# of specification.
elif key == 'identityfile':
if key in host['config']:
host['config']['identityfile'].append(value)
else:
host['config']['identityfile'] = [value]
elif key not in host['config']:
host['config'].update({key: value})
self._config.append(host)
def lookup(self, hostname):
"""
@ -105,31 +151,45 @@ 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'])]
# Move * to the end
_star = matches.pop(0)
matches.append(_star)
matches = [config for config in self._config if
self._allowed(hostname, config['host'])]
ret = {}
for m in matches:
for k,v in m.iteritems():
if not k in ret:
ret[k] = v
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)
del ret['host']
return ret
def _expand_variables(self, config, hostname ):
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 ssh_config(5) for the parameters that
Please refer to man C{ssh_config} for the parameters that
are replaced.
@param config: the config for the hostname
@ -139,7 +199,7 @@ class SSHConfig (object):
"""
if 'hostname' in config:
config['hostname'] = config['hostname'].replace('%h',hostname)
config['hostname'] = config['hostname'].replace('%h', hostname)
else:
config['hostname'] = hostname
@ -155,34 +215,42 @@ class SSHConfig (object):
remoteuser = user
host = socket.gethostname().split('.')[0]
fqdn = socket.getfqdn()
fqdn = LazyFqdn(config)
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),
],
}
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]:
config[k] = config[k].replace(find, str(replace))
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

View File

@ -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

View File

@ -87,6 +87,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 = ''
@ -110,11 +111,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
@ -490,12 +492,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):

View File

@ -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,

View File

@ -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
@ -51,6 +55,7 @@ class SFTPFile (BufferedFile):
self._prefetch_data = {}
self._prefetch_reads = []
self._saved_exception = None
self._reqs = deque()
def __del__(self):
self._close(async=True)
@ -160,12 +165,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):
@ -474,3 +481,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()

View File

@ -1885,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 ...')

2
requirements.txt Normal file
View File

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

View File

@ -23,6 +23,8 @@ 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
@ -188,6 +190,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.

View File

@ -104,23 +104,32 @@ class UtilTest(ParamikoTest):
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)
for host, values in {
'irc.danger.com': {'user': 'robey', 'crazy': 'something dumb '},
'irc.example.com': {'user': 'bjork', 'crazy': 'something dumb ', 'port': '3333'},
'spoo.example.com': {'user': 'bjork', 'crazy': 'something else', 'port': '3333'}
'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")
identityfile=[os.path.expanduser("~/.ssh/id_rsa")]
)
self.assertEquals(
paramiko.util.lookup_ssh_host_config(host, config),
@ -151,8 +160,8 @@ class UtilTest(ParamikoTest):
# 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_ssh_issue_33(self):
def test_7_host_config_expose_issue_33(self):
test_config_file = """
Host www13.*
Port 22
@ -220,16 +229,16 @@ Host equals-delimited
ProxyCommand should perform interpolation on the value
"""
config = paramiko.util.parse_ssh_config(cStringIO.StringIO("""
Host *
Port 25
ProxyCommand host %h port %p
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"),
@ -240,3 +249,83 @@ Host portonly
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
)

6
tox.ini Normal file
View File

@ -0,0 +1,6 @@
[tox]
envlist = py25,py26,py27
[testenv]
commands = pip install --use-mirrors -q -r requirements.txt
python test.py