summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2013-03-19 13:36:52 -0700
committerJeff Forcier <jeff@bitprophet.org>2013-03-19 13:36:52 -0700
commita7ee2509e48d7e2bb533ce5e50796b9a887c7a8a (patch)
tree69ac6ffb7193e8b64bd97fd03db70c6a21485480
parent5f5137414c91c931f04522b1bb8c2800294d6a90 (diff)
parentd5db60329701df2423909773af9cc9aa5df4d4f6 (diff)
downloadparamiko-a7ee2509e48d7e2bb533ce5e50796b9a887c7a8a.tar.gz
Merge branch 'master' into 112-int
Conflicts: paramiko/win_pageant.py
-rw-r--r--.gitignore1
-rw-r--r--NEWS33
-rw-r--r--README20
-rw-r--r--paramiko/__init__.py6
-rw-r--r--paramiko/channel.py24
-rw-r--r--paramiko/client.py13
-rw-r--r--paramiko/config.py180
-rw-r--r--paramiko/message.py3
-rw-r--r--paramiko/packet.py12
-rw-r--r--paramiko/sftp_client.py2
-rw-r--r--paramiko/sftp_file.py25
-rw-r--r--paramiko/transport.py3
-rw-r--r--requirements.txt2
-rwxr-xr-xtests/test_sftp.py13
-rw-r--r--tests/test_util.py117
-rw-r--r--tox.ini6
16 files changed, 344 insertions, 116 deletions
diff --git a/.gitignore b/.gitignore
index 5f9c3d74..4b578950 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.pyc
build/
dist/
+.tox/
paramiko.egg-info/
test.log
docs/
diff --git a/NEWS b/NEWS
index 96b1261f..e8fceab2 100644
--- a/NEWS
+++ b/NEWS
@@ -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.
diff --git a/README b/README
index 9e9cc3a7..310a7f02 100644
--- a/README
+++ b/README
@@ -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::
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 7d7dcbf4..e2b359fb 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -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>"
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 534f8d7c..0c603c62 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -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):
"""
diff --git a/paramiko/client.py b/paramiko/client.py
index a777b45b..5b719581 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -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
diff --git a/paramiko/config.py b/paramiko/config.py
index d1ce9490..e41bae43 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.
#
@@ -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
diff --git a/paramiko/message.py b/paramiko/message.py
index 366c43c9..47acc34b 100644
--- a/paramiko/message.py
+++ b/paramiko/message.py
@@ -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 5d918e2a..38a6d4b5 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -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):
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 8cb8ceaf..7df643f5 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -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,
diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py
index 8c5c7aca..e056d706 100644
--- a/paramiko/sftp_file.py
+++ b/paramiko/sftp_file.py
@@ -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()
diff --git a/paramiko/transport.py b/paramiko/transport.py
index c8010312..fd6dab76 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -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 ...')
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..75112a23
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pycrypto
+tox
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index 2eadabcd..f95da69c 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -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.
diff --git a/tests/test_util.py b/tests/test_util.py
index 093a2157..efda9b2f 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -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
+ )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..6cb80012
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,6 @@
+[tox]
+envlist = py25,py26,py27
+
+[testenv]
+commands = pip install --use-mirrors -q -r requirements.txt
+ python test.py