summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--paramiko/channel.py5
-rw-r--r--paramiko/client.py6
-rw-r--r--paramiko/ecdsakey.py13
-rw-r--r--paramiko/file.py13
-rw-r--r--paramiko/kex_gex.py2
-rw-r--r--paramiko/kex_gss.py10
-rw-r--r--paramiko/proxy.py5
-rw-r--r--paramiko/server.py15
-rw-r--r--paramiko/sftp_client.py5
-rw-r--r--paramiko/sftp_file.py6
-rw-r--r--paramiko/sftp_handle.py5
-rw-r--r--paramiko/transport.py11
-rw-r--r--paramiko/util.py9
-rw-r--r--sites/docs/api/keys.rst17
-rw-r--r--sites/www/changelog.rst9
-rw-r--r--sites/www/installing.rst11
-rw-r--r--tasks.py16
-rw-r--r--tests/test_client.py24
-rw-r--r--tests/test_gssapi.py40
-rwxr-xr-xtests/test_sftp.py20
-rw-r--r--tests/test_transport.py18
22 files changed, 173 insertions, 89 deletions
diff --git a/.travis.yml b/.travis.yml
index 3f6f7331..a9a04c89 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,7 +13,7 @@ install:
- pip install -r dev-requirements.txt
script:
# Main tests, with coverage!
- - invoke coverage
+ - inv test --coverage
# 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
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 78a14795..9de278cb 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -38,6 +38,7 @@ from paramiko.ssh_exception import SSHException
from paramiko.file import BufferedFile
from paramiko.buffered_pipe import BufferedPipe, PipeTimeout
from paramiko import pipe
+from paramiko.util import ClosingContextManager
def open_only(func):
@@ -60,7 +61,7 @@ def open_only(func):
return _check
-class Channel (object):
+class Channel (ClosingContextManager):
"""
A secure tunnel across an SSH `.Transport`. A Channel is meant to behave
like a socket, and has an API that should be indistinguishable from the
@@ -73,6 +74,8 @@ class Channel (object):
flow-controlled independently.) Similarly, if the server isn't reading
data you send, calls to `send` may block, unless you set a timeout. This
is exactly like a normal network socket, so it shouldn't be too surprising.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, chanid):
diff --git a/paramiko/client.py b/paramiko/client.py
index 265389de..05686d97 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -37,10 +37,10 @@ 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
+from paramiko.util import retry_on_signal, ClosingContextManager
-class SSHClient (object):
+class SSHClient (ClosingContextManager):
"""
A high-level representation of a session with an SSH server. This class
wraps `.Transport`, `.Channel`, and `.SFTPClient` to take care of most
@@ -55,6 +55,8 @@ class SSHClient (object):
checking. The default mechanism is to try to use local key files or an
SSH agent (if one is running).
+ Instances of this class may be used as context managers.
+
.. versionadded:: 1.6
"""
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index e869ee61..a7f3c5ed 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -17,7 +17,7 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
"""
-L{ECDSAKey}
+ECDSA keys
"""
import binascii
@@ -131,13 +131,10 @@ class ECDSAKey (PKey):
Generate a new private RSA key. This factory function can be used to
generate a new host key or authentication key.
- @param bits: number of bits the generated key should be.
- @type bits: int
- @param progress_func: an optional function to call at key points in
- key generation (used by C{pyCrypto.PublicKey}).
- @type progress_func: function
- @return: new private key
- @rtype: L{RSAKey}
+ :param function progress_func:
+ an optional function to call at key points in key generation (used
+ by ``pyCrypto.PublicKey``).
+ :returns: A new private key (`.RSAKey`) object
"""
signing_key = SigningKey.generate(curve)
key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key()))
diff --git a/paramiko/file.py b/paramiko/file.py
index 2238f0bf..311e1982 100644
--- a/paramiko/file.py
+++ b/paramiko/file.py
@@ -19,8 +19,10 @@ from paramiko.common import linefeed_byte_value, crlf, cr_byte, linefeed_byte, \
cr_byte_value
from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types
+from paramiko.util import ClosingContextManager
-class BufferedFile (object):
+
+class BufferedFile (ClosingContextManager):
"""
Reusable base class to implement Python-style file buffering around a
simpler stream.
@@ -104,14 +106,13 @@ class BufferedFile (object):
else:
def __next__(self):
"""
- Returns the next line from the input, or raises L{StopIteration} when
+ Returns the next line from the input, or raises `.StopIteration` when
EOF is hit. Unlike python file objects, it's okay to mix calls to
- C{next} and L{readline}.
+ `.next` and `.readline`.
- @raise StopIteration: when the end of the file is reached.
+ :raises StopIteration: when the end of the file is reached.
- @return: a line read from the file.
- @rtype: str
+ :returns: a line (`str`) read from the file.
"""
line = self.readline()
if not line:
diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py
index 5ff8a287..cb548f33 100644
--- a/paramiko/kex_gex.py
+++ b/paramiko/kex_gex.py
@@ -19,7 +19,7 @@
"""
Variant on `KexGroup1 <paramiko.kex_group1.KexGroup1>` where the prime "p" and
generator "g" are provided by the server. A bit more work is required on the
-client side, and a B{lot} more on the server side.
+client side, and a **lot** more on the server side.
"""
import os
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index a319b33b..4e8380ef 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -36,8 +36,8 @@ This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462.
.. versionadded:: 1.15
"""
+from hashlib import sha1
-from Crypto.Hash import SHA
from paramiko.common import *
from paramiko import util
from paramiko.message import Message
@@ -196,7 +196,7 @@ class KexGSSGroup1(object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- self.transport._set_K_H(K, SHA.new(str(hm)).digest())
+ self.transport._set_K_H(K, sha1(str(hm)).digest())
if srv_token is not None:
self.kexgss.ssh_init_sec_context(target=self.gss_host,
recv_token=srv_token)
@@ -229,7 +229,7 @@ class KexGSSGroup1(object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = SHA.new(hm.asbytes()).digest()
+ H = sha1(hm.asbytes()).digest()
self.transport._set_K_H(K, H)
srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host,
client_token)
@@ -463,7 +463,7 @@ class KexGSSGex(object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = SHA.new(hm.asbytes()).digest()
+ H = sha1(hm.asbytes()).digest()
self.transport._set_K_H(K, H)
srv_token = self.kexgss.ssh_accept_sec_context(self.gss_host,
client_token)
@@ -555,7 +555,7 @@ class KexGSSGex(object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = SHA.new(hm.asbytes()).digest()
+ H = sha1(hm.asbytes()).digest()
self.transport._set_K_H(K, H)
if srv_token is not None:
self.kexgss.ssh_init_sec_context(target=self.gss_host,
diff --git a/paramiko/proxy.py b/paramiko/proxy.py
index 8959b244..0664ac6e 100644
--- a/paramiko/proxy.py
+++ b/paramiko/proxy.py
@@ -26,9 +26,10 @@ from select import select
import socket
from paramiko.ssh_exception import ProxyCommandFailure
+from paramiko.util import ClosingContextManager
-class ProxyCommand(object):
+class ProxyCommand(ClosingContextManager):
"""
Wraps a subprocess running ProxyCommand-driven programs.
@@ -36,6 +37,8 @@ class ProxyCommand(object):
`.Transport` and `.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.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, command_line):
"""
diff --git a/paramiko/server.py b/paramiko/server.py
index cf396b15..bf5039a2 100644
--- a/paramiko/server.py
+++ b/paramiko/server.py
@@ -547,21 +547,18 @@ class ServerInterface (object):
def check_channel_env_request(self, channel, name, value):
"""
Check whether a given environment variable can be specified for the
- given channel. This method should return C{True} if the server
+ given channel. This method should return ``True`` if the server
is willing to set the specified environment variable. Note that
some environment variables (e.g., PATH) can be exceedingly
dangerous, so blindly allowing the client to set the environment
is almost certainly not a good idea.
- The default implementation always returns C{False}.
+ The default implementation always returns ``False``.
- @param channel: the L{Channel} the env request arrived on
- @type channel: L{Channel}
- @param name: foo bar baz
- @type name: str
- @param value: flklj
- @type value: str
- @rtype: bool
+ :param channel: the `.Channel` the env request arrived on
+ :param str name: name
+ :param str value: Channel value
+ :returns: A boolean
"""
return False
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 9c30426d..62127cc2 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -39,6 +39,7 @@ from paramiko.sftp import BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READ
from paramiko.sftp_attr import SFTPAttributes
from paramiko.ssh_exception import SSHException
from paramiko.sftp_file import SFTPFile
+from paramiko.util import ClosingContextManager
def _to_unicode(s):
@@ -58,12 +59,14 @@ def _to_unicode(s):
b_slash = b'/'
-class SFTPClient(BaseSFTP):
+class SFTPClient(BaseSFTP, ClosingContextManager):
"""
SFTP client object.
Used to open an SFTP session across an open SSH `.Transport` and perform
remote file operations.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, sock):
"""
diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py
index 03d67b33..d0a37da3 100644
--- a/paramiko/sftp_file.py
+++ b/paramiko/sftp_file.py
@@ -488,9 +488,3 @@ 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 92dd9cfe..edceb5ad 100644
--- a/paramiko/sftp_handle.py
+++ b/paramiko/sftp_handle.py
@@ -22,9 +22,10 @@ Abstraction of an SFTP file handle (for server mode).
import os
from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK
+from paramiko.util import ClosingContextManager
-class SFTPHandle (object):
+class SFTPHandle (ClosingContextManager):
"""
Abstract object representing a handle to an open file (or folder) in an
SFTP server implementation. Each handle has a string representation used
@@ -32,6 +33,8 @@ class SFTPHandle (object):
Server implementations can (and should) subclass SFTPHandle to implement
features of a file handle, like `stat` or `chattr`.
+
+ Instances of this class may be used as context managers.
"""
def __init__(self, flags=0):
"""
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 0f747343..2ffc8ca8 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -61,7 +61,7 @@ from paramiko.server import ServerInterface
from paramiko.sftp_client import SFTPClient
from paramiko.ssh_exception import (SSHException, BadAuthenticationType,
ChannelException, ProxyCommandFailure)
-from paramiko.util import retry_on_signal, clamp_value
+from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
from Crypto.Cipher import Blowfish, AES, DES3, ARC4
try:
@@ -81,13 +81,15 @@ import atexit
atexit.register(_join_lingering_threads)
-class Transport (threading.Thread):
+class Transport (threading.Thread, ClosingContextManager):
"""
An SSH Transport attaches to a stream (usually a socket), negotiates an
encrypted session, authenticates, and then creates stream tunnels, called
`channels <.Channel>`, across the session. Multiple channels can be
multiplexed across a single session (and often are, in the case of port
forwardings).
+
+ Instances of this class may be used as context managers.
"""
_PROTO_ID = '2.0'
_CLIENT_ID = 'paramiko_%s' % paramiko.__version__
@@ -1065,10 +1067,9 @@ class Transport (threading.Thread):
def get_banner(self):
"""
Return the banner supplied by the server upon connect. If no banner is
- supplied, this method returns C{None}.
+ supplied, this method returns ``None``.
- @return: server supplied banner, or C{None}.
- @rtype: string
+ :returns: server supplied banner (`str`), or ``None``.
"""
if not self.active or (self.auth_handler is None):
return None
diff --git a/paramiko/util.py b/paramiko/util.py
index d029f52e..88ca2bc4 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -321,5 +321,14 @@ def constant_time_bytes_eq(a, b):
res |= byte_ord(a[i]) ^ byte_ord(b[i])
return res == 0
+
+class ClosingContextManager(object):
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.close()
+
+
def clamp_value(minimum, val, maximum):
return max(minimum, min(val, maximum))
diff --git a/sites/docs/api/keys.rst b/sites/docs/api/keys.rst
index af7b58c4..c6412f77 100644
--- a/sites/docs/api/keys.rst
+++ b/sites/docs/api/keys.rst
@@ -1,6 +1,23 @@
+============
Key handling
============
+Parent key class
+================
+
.. automodule:: paramiko.pkey
+
+DSA (DSS)
+=========
+
.. automodule:: paramiko.dsskey
+
+RSA
+===
+
.. automodule:: paramiko.rsakey
+
+ECDSA
+=====
+
+.. automodule:: paramiko.ecdsakey
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 1dab5219..d0bd481c 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,10 +2,19 @@
Changelog
=========
+* :release:`1.15.0 <2014-09-18>`
+* :support:`393` Replace internal use of PyCrypto's ``SHA.new`` with the
+ stdlib's ``hashlib.sha1``. Thanks to Alex Gaynor.
* :feature:`267` (also :issue:`250`, :issue:`241`, :issue:`228`) Add GSS-API /
SSPI (e.g. Kerberos) key exchange and authentication support
(:ref:`installation docs here <gssapi>`). Mega thanks to Sebastian Deiß, with
assist by Torsten Landschoff.
+
+ .. note::
+ Unix users should be aware that the ``python-gssapi`` library (a
+ requirement for using this functionality) only appears to support
+ Python 2.7 and up at this time.
+
* :bug:`346 major` Fix an issue in private key files' encryption salts that
could cause tracebacks and file corruption if keys were re-encrypted. Credit
to Xavier Nunn.
diff --git a/sites/www/installing.rst b/sites/www/installing.rst
index 5528b28a..a657c3fc 100644
--- a/sites/www/installing.rst
+++ b/sites/www/installing.rst
@@ -109,14 +109,19 @@ installation of Paramiko via ``pypm``::
Optional dependencies for GSS-API / SSPI / Kerberos
===================================================
-In order to use Kerberos & related functionality, a couple of additional
-dependencies are required (these are not listed in our ``setup.py`` due to
-their infrequent utility & non-platform-agnostic requirements):
+In order to use GSS-API/Kerberos & related functionality, a couple of
+additional dependencies are required (these are not listed in our ``setup.py``
+due to their infrequent utility & non-platform-agnostic requirements):
+* It hopefully goes without saying but **all platforms** need **a working
+ installation of GSS-API itself**, e.g. Heimdal.
* **All platforms** need `pyasn1 <https://pypi.python.org/pypi/pyasn1>`_
``0.1.7`` or better.
* **Unix** needs `python-gssapi <https://pypi.python.org/pypi/python-gssapi/>`_
``0.6.1`` or better.
+
+ .. note:: This library appears to only function on Python 2.7 and up.
+
* **Windows** needs `pywin32 <https://pypi.python.org/pypi/pywin32>`_ ``2.1.8``
or better.
diff --git a/tasks.py b/tasks.py
index cf43a5fd..3503d019 100644
--- a/tasks.py
+++ b/tasks.py
@@ -27,12 +27,12 @@ www = Collection.from_module(_docs, name='www', config={
# Until we move to spec-based testing
@task
-def test(ctx):
- ctx.run("python test.py --verbose", pty=True)
-
-@task
-def coverage(ctx):
- ctx.run("coverage run --source=paramiko test.py --verbose")
+def test(ctx, coverage=False):
+ runner = "python"
+ if coverage:
+ runner = "coverage run --source=paramiko"
+ flags = "--verbose"
+ ctx.run("{0} test.py {1}".format(runner, flags), pty=True)
# Until we stop bundling docs w/ releases. Need to discover use cases first.
@@ -46,6 +46,8 @@ def release(ctx):
copytree(docs_build, target)
# Publish
publish(ctx, wheel=True)
+ # Remind
+ print("\n\nDon't forget to update RTD's versions page for new minor releases!")
-ns = Collection(test, coverage, release, docs=docs, www=www)
+ns = Collection(test, release, docs=docs, www=www)
diff --git a/tests/test_client.py b/tests/test_client.py
index 6fda7f5e..28d1cb46 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -20,6 +20,8 @@
Some unit tests for SSHClient.
"""
+from __future__ import with_statement
+
import socket
from tempfile import mkstemp
import threading
@@ -299,6 +301,28 @@ class SSHClientTest (unittest.TestCase):
self.assertTrue(p() is None)
+ def test_client_can_be_used_as_context_manager(self):
+ """
+ verify that an SSHClient can be used a context manager
+ """
+ threading.Thread(target=self._run).start()
+ host_key = paramiko.RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+
+ with paramiko.SSHClient() as tc:
+ self.tc = tc
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.assertEquals(0, len(self.tc.get_host_keys()))
+ self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+
+ self.event.wait(1.0)
+ self.assertTrue(self.event.isSet())
+ self.assertTrue(self.ts.is_active())
+
+ self.assertTrue(self.tc._transport is not None)
+
+ self.assertTrue(self.tc._transport is None)
+
def test_7_banner_timeout(self):
"""
verify that the SSHClient has a configurable banner timeout.
diff --git a/tests/test_gssapi.py b/tests/test_gssapi.py
index 0d3df72c..a328dd65 100644
--- a/tests/test_gssapi.py
+++ b/tests/test_gssapi.py
@@ -72,9 +72,7 @@ class GSSAPITest(unittest.TestCase):
gss_flags = (gssapi.C_PROT_READY_FLAG,
gssapi.C_INTEG_FLAG,
gssapi.C_DELEG_FLAG)
- """
- Initialize a GSS-API context.
- """
+ # Initialize a GSS-API context.
ctx = gssapi.Context()
ctx.flags = gss_flags
krb5_oid = gssapi.OID.mech_from_string(krb5_mech)
@@ -87,41 +85,31 @@ class GSSAPITest(unittest.TestCase):
c_token = gss_ctxt.step(c_token)
gss_ctxt_status = gss_ctxt.established
self.assertEquals(False, gss_ctxt_status)
- """
- Accept a GSS-API context.
- """
+ # Accept a GSS-API context.
gss_srv_ctxt = gssapi.AcceptContext()
s_token = gss_srv_ctxt.step(c_token)
gss_ctxt_status = gss_srv_ctxt.established
self.assertNotEquals(None, s_token)
self.assertEquals(True, gss_ctxt_status)
- """
- Establish the client context
- """
+ # Establish the client context
c_token = gss_ctxt.step(s_token)
self.assertEquals(None, c_token)
else:
while not gss_ctxt.established:
c_token = gss_ctxt.step(c_token)
self.assertNotEquals(None, c_token)
- """
- Build MIC
- """
+ # Build MIC
mic_token = gss_ctxt.get_mic(mic_msg)
if server_mode:
- """
- Check MIC
- """
+ # Check MIC
status = gss_srv_ctxt.verify_mic(mic_msg, mic_token)
self.assertEquals(0, status)
else:
gss_flags = sspicon.ISC_REQ_INTEGRITY |\
sspicon.ISC_REQ_MUTUAL_AUTH |\
sspicon.ISC_REQ_DELEGATE
- """
- Initialize a GSS-API context.
- """
+ # Initialize a GSS-API context.
target_name = "host/" + socket.getfqdn(targ_name)
gss_ctxt = sspi.ClientAuth("Kerberos",
scflags=gss_flags,
@@ -130,26 +118,18 @@ class GSSAPITest(unittest.TestCase):
error, token = gss_ctxt.authorize(c_token)
c_token = token[0].Buffer
self.assertEquals(0, error)
- """
- Accept a GSS-API context.
- """
+ # Accept a GSS-API context.
gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=target_name)
error, token = gss_srv_ctxt.authorize(c_token)
s_token = token[0].Buffer
- """
- Establish the context.
- """
+ # Establish the context.
error, token = gss_ctxt.authorize(s_token)
c_token = token[0].Buffer
self.assertEquals(None, c_token)
self.assertEquals(0, error)
- """
- Build MIC
- """
+ # Build MIC
mic_token = gss_ctxt.sign(mic_msg)
- """
- Check MIC
- """
+ # Check MIC
gss_srv_ctxt.verify(mic_msg, mic_token)
else:
error, token = gss_ctxt.authorize(c_token)
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index 1ae9781d..72c7ba03 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -23,12 +23,13 @@ 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 binascii import hexlify
import os
+import socket
import sys
-import warnings
import threading
import unittest
+import warnings
+from binascii import hexlify
from tempfile import mkstemp
import paramiko
@@ -195,6 +196,21 @@ class SFTPTest (unittest.TestCase):
pass
sftp = paramiko.SFTP.from_transport(tc)
+ def test_2_sftp_can_be_used_as_context_manager(self):
+ """
+ verify that the sftp session is closed when exiting the context manager
+ """
+ global sftp
+ with sftp:
+ pass
+ try:
+ sftp.open(FOLDER + '/test2', 'w')
+ self.fail('expected exception')
+ except (EOFError, socket.error):
+ pass
+ finally:
+ sftp = paramiko.SFTP.from_transport(tc)
+
def test_3_write(self):
"""
verify that a file can be created and written, and the size is correct.
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 344d64b8..50b1d86b 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -20,6 +20,8 @@
Some unit tests for the ssh2 protocol in Transport.
"""
+from __future__ import with_statement
+
from binascii import hexlify
import select
import socket
@@ -281,6 +283,22 @@ class TransportTest(unittest.TestCase):
self.assertEqual('Hello there.\n', f.readline())
self.assertEqual('This is on stderr.\n', f.readline())
self.assertEqual('', f.readline())
+
+ def test_6a_channel_can_be_used_as_context_manager(self):
+ """
+ verify that exec_command() does something reasonable.
+ """
+ self.setup_test_server()
+
+ with self.tc.open_session() as chan:
+ with self.ts.accept(1.0) as schan:
+ chan.exec_command('yes')
+ schan.send('Hello there.\n')
+ schan.close()
+
+ f = chan.makefile()
+ self.assertEqual('Hello there.\n', f.readline())
+ self.assertEqual('', f.readline())
def test_7_invoke_shell(self):
"""