[Python-modules-commits] [python-asyncssh] 02/07: Import python-asyncssh_1.5.3.orig.tar.gz

Vincent Bernat bernat at moszumanska.debian.org
Sat May 21 14:17:21 UTC 2016


This is an automated email from the git hooks/post-receive script.

bernat pushed a commit to branch master
in repository python-asyncssh.

commit 79cb36f3893c01e2808ecb05f8a247f65b508100
Author: Vincent Bernat <bernat at debian.org>
Date:   Sat May 21 16:13:21 2016 +0200

    Import python-asyncssh_1.5.3.orig.tar.gz
---
 README.rst                            |   38 +-
 asyncssh/__init__.py                  |   22 +-
 asyncssh/agent.py                     |  215 +++
 asyncssh/auth.py                      |   54 +-
 asyncssh/auth_keys.py                 |    4 +-
 asyncssh/channel.py                   |  510 ++++---
 asyncssh/client.py                    |  298 ++++
 asyncssh/connection.py                | 2612 ++++++++++++++++-----------------
 asyncssh/crypto/chacha.py             |    1 +
 asyncssh/crypto/ec.py                 |   10 +-
 asyncssh/crypto/pyca/cipher.py        |    4 +-
 asyncssh/crypto/pyca/dsa.py           |    4 +-
 asyncssh/crypto/pyca/ec.py            |    4 +-
 asyncssh/dh.py                        |    8 +-
 asyncssh/ecdh.py                      |   11 +-
 asyncssh/ed25519.py                   |    1 +
 asyncssh/forward.py                   |  134 +-
 asyncssh/known_hosts.py               |  228 ++-
 asyncssh/listener.py                  |  215 ++-
 asyncssh/misc.py                      |   40 +-
 asyncssh/packet.py                    |    8 +-
 asyncssh/public_key.py                |  100 +-
 asyncssh/server.py                    |  656 +++++++++
 asyncssh/session.py                   |  113 +-
 asyncssh/sftp.py                      |  963 ++++++------
 asyncssh/stream.py                    |   63 +-
 asyncssh/version.py                   |    2 +-
 docs/api.rst                          |  255 +++-
 docs/changes.rst                      |  152 ++
 examples/check_exit_status.py         |    2 +
 examples/direct_client.py             |    2 +
 examples/listening_client.py          |    2 +
 examples/local_forwarding_client.py   |    2 +
 examples/local_forwarding_client2.py  |    2 +
 examples/math_client.py               |    2 +
 examples/remote_forwarding_client.py  |    2 +
 examples/remote_forwarding_client2.py |    2 +
 examples/sample_client.py             |    2 +
 examples/set_environment.py           |    2 +
 examples/set_terminal.py              |    2 +
 examples/sftp_client.py               |    2 +
 examples/simple_client.py             |    2 +
 examples/stderr_client.py             |    2 +
 examples/stream_direct_client.py      |    2 +
 examples/stream_listening_client.py   |    2 +
 examples/stream_math_client.py        |    2 +
 pylintrc                              |   46 +-
 tests/server.py                       |  148 ++
 tests/test_agent.py                   |  221 +++
 tests/test_auth.py                    |   54 +-
 tests/test_channel.py                 | 1355 +++++++++++++++++
 tests/test_connection.py              |  843 +++++++++++
 tests/test_connection_auth.py         |  832 +++++++++++
 tests/test_forward.py                 |  933 ++++++++++++
 tests/test_kex.py                     |   34 +-
 tests/test_known_hosts.py             |   18 +-
 tests/test_packet.py                  |  180 +++
 tests/test_public_key.py              |   68 +-
 tests/test_sftp.py                    |   58 +
 tests/test_stream.py                  |  296 ++++
 tests/util.py                         |  150 +-
 61 files changed, 9461 insertions(+), 2534 deletions(-)

diff --git a/README.rst b/README.rst
index 31e3eb7..b5696da 100644
--- a/README.rst
+++ b/README.rst
@@ -13,9 +13,12 @@ asyncio framework.
   def run_client():
       with (yield from asyncssh.connect('localhost')) as conn:
           stdin, stdout, stderr = yield from conn.open_session('echo "Hello!"')
+
           output = yield from stdout.read()
           print(output, end='')
 
+          yield from stdout.channel.wait_closed()
+
           status = stdout.channel.get_exit_status()
           if status:
               print('Program exited with status %d' % status, file=sys.stderr)
@@ -25,7 +28,8 @@ asyncio framework.
   asyncio.get_event_loop().run_until_complete(run_client())
 
 Check out the `examples`__ to get started!
-  __ http://asyncssh.readthedocs.org/en/stable/#client-examples
+
+__ http://asyncssh.readthedocs.org/en/stable/#client-examples
 
 Features
 --------
@@ -35,7 +39,9 @@ Features
   * Shell, command, and subsystem channels
   * Environment variables, terminal type, and window size
   * Direct and forwarded TCP/IP channels
+  * OpenSSH-compatible direct and forwarded UNIX domain socket channels
   * Local and remote TCP/IP port forwarding
+  * Local and remote UNIX domain socket forwarding
   * SFTP protocol version 3 with OpenSSH extensions
 
 * Multiple simultaneous sessions on a single SSH connection
@@ -48,6 +54,8 @@ Features
 
 * Password, public key, and keyboard-interactive user authentication methods
 * Many types and formats of `public keys and certificates`__
+* Support for accessing keys managed by `ssh-agent`__
+* OpenSSH-style ssh-agent forwarding support
 * OpenSSH-style `known_hosts file`__ support
 * OpenSSH-style `authorized_keys file`__ support
 * Compatibility with OpenSSH "Encrypt then MAC" option for better security
@@ -55,12 +63,21 @@ Features
 * Designed to be easy to extend to support new forms of key exchange,
   authentication, encryption, and compression algorithms
 
+__ http://asyncssh.readthedocs.org/en/stable/api.html#key-exchange-algorithms
+__ http://asyncssh.readthedocs.org/en/stable/api.html#encryption-algorithms
+__ http://asyncssh.readthedocs.org/en/stable/api.html#mac-algorithms
+__ http://asyncssh.readthedocs.org/en/stable/api.html#compression-algorithms
+__ http://asyncssh.readthedocs.org/en/stable/api.html#public-key-support
+__ http://asyncssh.readthedocs.org/en/stable/api.html#ssh-agent-support
+__ http://asyncssh.readthedocs.org/en/stable/api.html#known-hosts
+__ http://asyncssh.readthedocs.org/en/stable/api.html#authorized-keys
+
 License
 -------
 
 This package is released under the following terms:
 
-  Copyright (c) 2013-2015 by Ron Frederick <ronf at timeheart.net>.
+  Copyright (c) 2013-2016 by Ron Frederick <ronf at timeheart.net>.
   All rights reserved.
 
   This program and the accompanying materials are made available under
@@ -120,6 +137,16 @@ listed above for libnacl to work correctly. Unfortunately, since
 libsodium is not a Python package, it cannot be directly installed using
 pip.
 
+Installing the development branch
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you would like to install the development branch of asyncssh directly
+from Github, you can use the following command to do this:
+
+  ::
+
+      pip install git+https://github.com/ronf/asyncssh@develop
+
 Mailing Lists
 -------------
 
@@ -129,13 +156,6 @@ Three mailing lists are available for AsyncSSH:
 * `asyncssh-dev at googlegroups.com`__: Development discussions
 * `asyncssh-users at googlegroups.com`__: End-user discussions
 
-__ http://asyncssh.readthedocs.org/en/stable/api.html#key-exchange-algorithms
-__ http://asyncssh.readthedocs.org/en/stable/api.html#encryption-algorithms
-__ http://asyncssh.readthedocs.org/en/stable/api.html#mac-algorithms
-__ http://asyncssh.readthedocs.org/en/stable/api.html#compression-algorithms
-__ http://asyncssh.readthedocs.org/en/stable/api.html#public-key-support
-__ http://asyncssh.readthedocs.org/en/stable/api.html#known-hosts
-__ http://asyncssh.readthedocs.org/en/stable/api.html#authorized-keys
 __ http://groups.google.com/d/forum/asyncssh-announce
 __ http://groups.google.com/d/forum/asyncssh-dev
 __ http://groups.google.com/d/forum/asyncssh-users
diff --git a/asyncssh/__init__.py b/asyncssh/__init__.py
index f0d8414..5018948 100644
--- a/asyncssh/__init__.py
+++ b/asyncssh/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2013-2015 by Ron Frederick <ronf at timeheart.net>.
+# Copyright (c) 2013-2016 by Ron Frederick <ronf at timeheart.net>.
 # All rights reserved.
 #
 # This program and the accompanying materials are made available under
@@ -20,15 +20,23 @@ from .constants import *
 
 # pylint: enable=wildcard-import
 
+from .agent import SSHAgentClient, connect_agent
+
 from .auth_keys import SSHAuthorizedKeys
 from .auth_keys import import_authorized_keys, read_authorized_keys
 
-from .channel import SSHClientChannel, SSHServerChannel, SSHTCPChannel
+from .channel import SSHClientChannel, SSHServerChannel
+from .channel import SSHTCPChannel, SSHUNIXChannel
+
+from .client import SSHClient
 
-from .connection import SSHClient, SSHServer
 from .connection import SSHClientConnection, SSHServerConnection
 from .connection import create_connection, create_server, connect, listen
 
+from .known_hosts import SSHKnownHosts
+from .known_hosts import import_known_hosts, read_known_hosts
+from .known_hosts import match_known_hosts
+
 from .listener import SSHListener
 
 from .logging import logger
@@ -39,14 +47,18 @@ from .misc import BreakReceived, SignalReceived, TerminalSizeChanged
 
 from .pbe import KeyEncryptionError
 
-from .public_key import SSHKey, SSHCertificate, KeyImportError, KeyExportError
+from .public_key import SSHKey, SSHKeyPair, SSHCertificate
+from .public_key import KeyImportError, KeyExportError
 from .public_key import import_private_key, import_public_key
 from .public_key import import_certificate
 from .public_key import read_private_key, read_public_key, read_certificate
 from .public_key import read_private_key_list, read_public_key_list
 from .public_key import read_certificate_list
 
-from .session import SSHClientSession, SSHServerSession, SSHTCPSession
+from .session import SSHClientSession, SSHServerSession
+from .session import SSHTCPSession, SSHUNIXSession
+
+from .server import SSHServer
 
 from .sftp import SFTPClient, SFTPServer, SFTPFile, SFTPError
 from .sftp import SFTPAttrs, SFTPVFSAttrs, SFTPName
diff --git a/asyncssh/agent.py b/asyncssh/agent.py
new file mode 100644
index 0000000..4a4aea3
--- /dev/null
+++ b/asyncssh/agent.py
@@ -0,0 +1,215 @@
+# Copyright (c) 2016 by Ron Frederick <ronf at timeheart.net>.
+# All rights reserved.
+#
+# This program and the accompanying materials are made available under
+# the terms of the Eclipse Public License v1.0 which accompanies this
+# distribution and is available at:
+#
+#     http://www.eclipse.org/legal/epl-v10.html
+#
+# Contributors:
+#     Ron Frederick - initial implementation, API, and documentation
+
+"""SSH agent client"""
+
+import asyncio
+import os
+
+from .misc import ChannelOpenError
+from .packet import Byte, String, UInt32, PacketDecodeError, SSHPacket
+from .public_key import SSHKeyPair
+
+
+# pylint: disable=bad-whitespace
+
+# Generic agent replies
+SSH_AGENT_FAILURE              = 5
+
+# Protocol 2 key operations
+SSH2_AGENTC_REQUEST_IDENTITIES = 11
+SSH2_AGENT_IDENTITIES_ANSWER   = 12
+SSH2_AGENTC_SIGN_REQUEST       = 13
+SSH2_AGENT_SIGN_RESPONSE       = 14
+
+# pylint: enable=bad-whitespace
+
+
+class _SSHAgentKeyPair(SSHKeyPair):
+    """Surrogate for a key managed by the SSH agent"""
+
+    def __init__(self, agent, public_data, comment):
+        self._agent = agent
+
+        packet = SSHPacket(public_data)
+        self.algorithm = packet.get_string()
+
+        self.public_data = public_data
+        self.comment = comment
+
+    @asyncio.coroutine
+    def sign(self, data):
+        """Sign a block of data with this private key"""
+
+        return (yield from self._agent.sign(self.public_data, data))
+
+
+class SSHAgentClient:
+    """SSH agent client"""
+
+    def __init__(self, loop, agent_path):
+        self._loop = loop
+        self._agent_path = agent_path
+        self._reader = None
+        self._writer = None
+        self._lock = asyncio.Lock()
+
+    def _cleanup(self):
+        """Clean up this SSH agent client"""
+
+        if self._writer:
+            self._writer.close()
+            self._reader = None
+            self._writer = None
+
+    @asyncio.coroutine
+    def connect(self):
+        """Connect to the SSH agent"""
+
+        if isinstance(self._agent_path, str):
+            # pylint doesn't think open_unix_connection exists
+            # pylint: disable=no-member
+            self._reader, self._writer = \
+                yield from asyncio.open_unix_connection(self._agent_path,
+                                                        loop=self._loop)
+        else:
+            self._reader, self._writer = \
+                yield from self._agent_path.open_agent_connection()
+
+    @asyncio.coroutine
+    def _make_request(self, msgtype, *args):
+        """Send an SSH agent request"""
+
+        with (yield from self._lock):
+            try:
+                if not self._writer:
+                    yield from self.connect()
+
+                payload = Byte(msgtype) + b''.join(args)
+                self._writer.write(UInt32(len(payload)) + payload)
+
+                resplen = yield from self._reader.readexactly(4)
+                resplen = int.from_bytes(resplen, 'big')
+
+                resp = yield from self._reader.readexactly(resplen)
+                resp = SSHPacket(resp)
+
+                resptype = resp.get_byte()
+
+                return resptype, resp
+            except (OSError, EOFError, PacketDecodeError) as exc:
+                self._cleanup()
+                raise ValueError(str(exc)) from None
+
+    @asyncio.coroutine
+    def get_keys(self):
+        """Request the available client keys
+
+           This method is a coroutine which returns a list of client keys
+           available in the ssh-agent.
+
+           :returns: A list of :class:`SSHKeyPair` objects
+
+        """
+
+        resptype, resp = \
+            yield from self._make_request(SSH2_AGENTC_REQUEST_IDENTITIES)
+
+        if resptype == SSH2_AGENT_IDENTITIES_ANSWER:
+            result = []
+
+            num_keys = resp.get_uint32()
+            for _ in range(num_keys):
+                key_blob = resp.get_string()
+                comment = resp.get_string()
+
+                result.append(_SSHAgentKeyPair(self, key_blob, comment))
+
+            resp.check_end()
+            return result
+        else:
+            raise ValueError('Unknown SSH agent response: %d' % resptype)
+
+    @asyncio.coroutine
+    def sign(self, key_blob, data):
+        """Sign a block of data with this private key"""
+
+        resptype, resp = \
+            yield from self._make_request(SSH2_AGENTC_SIGN_REQUEST,
+                                          String(key_blob), String(data),
+                                          UInt32(0))
+
+        if resptype == SSH2_AGENT_SIGN_RESPONSE:
+            sig = resp.get_string()
+            resp.check_end()
+            return sig
+        elif resptype == SSH_AGENT_FAILURE:
+            raise ValueError('Unknown key passed to SSH agent')
+        else:
+            raise ValueError('Unknown SSH agent response: %d' % resptype)
+
+    def close(self):
+        """Close the SSH agent connection
+
+           This method closes the connection to the ssh-agent. Any
+           attempts to use this :class:``SSHAgentClient`` or the key
+           pairs it previously returned will result in an error.
+
+        """
+
+        self._cleanup()
+
+
+ at asyncio.coroutine
+def connect_agent(agent_path=None, *, loop=None):
+    """Make a connection to the SSH agent
+
+       This function attempts to connect to an ssh-agent process
+       listening on a UNIX domain socket at ``agent_path``. If not
+       provided, it will attempt to get the path from the ``SSH_AUTH_SOCK``
+       environment variable.
+
+       If the connection is successful, an ``SSHAgentClient`` object
+       is returned that has methods on it you can use to query the
+       ssh-agent. If no path is specified and the environment variable
+       is not set or the connection to the agent fails, this function
+       returns ``None``.
+
+       :param agent_path: (optional)
+           The path to use to contact the ssh-agent process, or the
+           :class:`SSHServerConnection` to forward the agent request
+           over.
+       :param loop: (optional)
+           The event loop to use when creating the connection. If not
+           specified, the default event loop is used.
+       :type agent_path: str or :class:`SSHServerConnection`
+
+       :returns: An :class:`SSHAgentClient` or ``None``
+
+    """
+
+    if not loop:
+        loop = asyncio.get_event_loop()
+
+    if not agent_path:
+        agent_path = os.environ.get('SSH_AUTH_SOCK', None)
+
+        if not agent_path:
+            return None
+
+    agent = SSHAgentClient(loop, agent_path)
+
+    try:
+        yield from agent.connect()
+        return agent
+    except (OSError, ChannelOpenError):
+        return None
diff --git a/asyncssh/auth.py b/asyncssh/auth.py
index cb32362..603ebad 100644
--- a/asyncssh/auth.py
+++ b/asyncssh/auth.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2013-2015 by Ron Frederick <ronf at timeheart.net>.
+# Copyright (c) 2013-2016 by Ron Frederick <ronf at timeheart.net>.
 # All rights reserved.
 #
 # This program and the accompanying materials are made available under
@@ -52,16 +52,7 @@ class _Auth(SSHPacketHandler):
         """Create an asynchronous auth task"""
 
         self.cancel()
-        self._coro = asyncio.async(self.run_task(coro))
-
-    @asyncio.coroutine
-    def run_task(self, coro):
-        """Run an async auth task, catching disconnect errors"""
-
-        try:
-            yield from coro
-        except DisconnectError as exc:
-            self._conn.connection_lost(exc)
+        self._coro = self._conn.create_task(coro)
 
     def cancel(self):
         """Cancel any authentication in progress"""
@@ -92,10 +83,12 @@ class _ClientAuth(_Auth):
     def auth_failed(self):
         """Callback when auth fails"""
 
+    @asyncio.coroutine
     def send_request(self, *args, key=None):
         """Send a user authentication request"""
 
-        self._conn.send_userauth_request(self._method, *args, key=key)
+        yield from self._conn.send_userauth_request(self._method,
+                                                    *args, key=key)
 
 
 class _ClientNullAuth(_ClientAuth):
@@ -105,7 +98,7 @@ class _ClientNullAuth(_ClientAuth):
     def _start(self):
         """Start client null authentication"""
 
-        self.send_request()
+        yield from self.send_request()
 
 
 class _ClientPublicKeyAuth(_ClientAuth):
@@ -115,15 +108,24 @@ class _ClientPublicKeyAuth(_ClientAuth):
     def _start(self):
         """Start client public key authentication"""
 
-        self._alg, self._key, self._key_data = \
-            yield from self._conn.public_key_auth_requested()
+        self._keypair = yield from self._conn.public_key_auth_requested()
 
-        if self._alg is None:
+        if self._keypair is None:
             self._conn.try_next_auth()
             return
 
-        self.send_request(Boolean(False), String(self._alg),
-                          String(self._key_data))
+        yield from self.send_request(Boolean(False),
+                                     String(self._keypair.algorithm),
+                                     String(self._keypair.public_data))
+
+    @asyncio.coroutine
+    def _send_signed_request(self):
+        """Send signed public key request"""
+
+        yield from self.send_request(Boolean(True),
+                                     String(self._keypair.algorithm),
+                                     String(self._keypair.public_data),
+                                     key=self._keypair)
 
     def _process_public_key_ok(self, pkttype, packet):
         """Process a public key ok response"""
@@ -134,11 +136,11 @@ class _ClientPublicKeyAuth(_ClientAuth):
         key_data = packet.get_string()
         packet.check_end()
 
-        if algorithm != self._alg or key_data != self._key_data:
+        if (algorithm != self._keypair.algorithm or
+                key_data != self._keypair.public_data):
             raise DisconnectError(DISC_PROTOCOL_ERROR, 'Key mismatch')
 
-        self.send_request(Boolean(True), String(algorithm),
-                          String(key_data), key=self._key)
+        self.create_task(self._send_signed_request())
         return True
 
     packet_handlers = {
@@ -159,7 +161,7 @@ class _ClientKbdIntAuth(_ClientAuth):
             self._conn.try_next_auth()
             return
 
-        self.send_request(String(''), String(submethods))
+        yield from self.send_request(String(''), String(submethods))
 
     @asyncio.coroutine
     def _receive_challenge(self, name, instruction, lang, prompts):
@@ -236,7 +238,7 @@ class _ClientPasswordAuth(_ClientAuth):
             self._conn.try_next_auth()
             return
 
-        self.send_request(Boolean(False), String(password))
+        yield from self.send_request(Boolean(False), String(password))
 
     @asyncio.coroutine
     def _change_password(self, prompt, lang):
@@ -253,9 +255,9 @@ class _ClientPasswordAuth(_ClientAuth):
 
         self._password_change = True
 
-        self.send_request(Boolean(True),
-                          String(old_password.encode('utf-8')),
-                          String(new_password.encode('utf-8')))
+        yield from self.send_request(Boolean(True),
+                                     String(old_password.encode('utf-8')),
+                                     String(new_password.encode('utf-8')))
 
     def auth_succeeded(self):
         if self._password_change:
diff --git a/asyncssh/auth_keys.py b/asyncssh/auth_keys.py
index d0502c5..14e4438 100644
--- a/asyncssh/auth_keys.py
+++ b/asyncssh/auth_keys.py
@@ -195,7 +195,7 @@ def import_authorized_keys(data):
        This function imports public keys and associated options in
        OpenSSH authorized keys format.
 
-       :param string data:
+       :param str data:
            The key data to import.
 
        :returns: An :class:`SSHAuthorizedKeys` object
@@ -211,7 +211,7 @@ def read_authorized_keys(filename):
        This function reads public keys and associated options in
        OpenSSH authorized_keys format from a file.
 
-       :param string filename:
+       :param str filename:
            The file to read the keys from.
 
        :returns: An :class:`SSHAuthorizedKeys` object
diff --git a/asyncssh/channel.py b/asyncssh/channel.py
index cb96c75..1d918cb 100644
--- a/asyncssh/channel.py
+++ b/asyncssh/channel.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2013-2015 by Ron Frederick <ronf at timeheart.net>.
+# Copyright (c) 2013-2016 by Ron Frederick <ronf at timeheart.net>.
 # All rights reserved.
 #
 # This program and the accompanying materials are made available under
@@ -21,12 +21,8 @@ from .constants import MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MSG_CHANNEL_REQUEST
 from .constants import MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE
 from .constants import OPEN_CONNECT_FAILED, PTY_OP_RESERVED, PTY_OP_END
 from .constants import OPEN_REQUEST_PTY_FAILED, OPEN_REQUEST_SESSION_FAILED
-from .misc import ChannelOpenError, DisconnectError
+from .misc import ChannelOpenError, DisconnectError, map_handler_name
 from .packet import Boolean, Byte, String, UInt32, SSHPacketHandler
-from .sftp import SFTPServerSession
-
-
-_EOF = object()
 
 
 class SSHChannel(SSHPacketHandler):
@@ -73,12 +69,18 @@ class SSHChannel(SSHPacketHandler):
 
         self._open_waiter = None
         self._request_waiters = []
-        self._close_waiters = []
+
+        self._close_event = asyncio.Event()
 
         self.set_write_buffer_limits()
 
         self._recv_chan = conn.add_channel(self)
 
+    def get_connection(self):
+        """Return the connection used by this channel"""
+
+        return self._conn
+
     def get_loop(self):
         """Return the event loop used by this channel"""
 
@@ -89,6 +91,11 @@ class SSHChannel(SSHPacketHandler):
 
         return self._encoding
 
+    def set_encoding(self, encoding):
+        """Set the encoding on this channel"""
+
+        self._encoding = encoding
+
     def get_recv_window(self):
         """Return the configured receive window for this channel"""
 
@@ -103,7 +110,7 @@ class SSHChannel(SSHPacketHandler):
         """Clean up this channel"""
 
         if self._open_waiter:
-            if not self._open_waiter.cancelled():
+            if not self._open_waiter.cancelled(): # pragma: no branch
                 self._open_waiter.set_exception(
                     ChannelOpenError(OPEN_CONNECT_FAILED,
                                      'SSH connection closed'))
@@ -112,31 +119,45 @@ class SSHChannel(SSHPacketHandler):
 
         if self._request_waiters:
             for waiter in self._request_waiters:
-                if not waiter.cancelled():
+                if not waiter.cancelled(): # pragma: no branch
                     waiter.set_exception(exc)
 
             self._request_waiters = []
 
-        if self._close_waiters:
-            for waiter in self._close_waiters:
-                if not waiter.cancelled():
-                    waiter.set_result(None)
-
-            self._close_waiters = []
-
         if self._session:
             self._session.connection_lost(exc)
             self._session = None
 
-        if self._conn:
-            if self._recv_chan:
-                self._conn.remove_channel(self._recv_chan)
-                self._recv_chan = None
+        self._close_event.set()
 
+        if self._conn: # pragma: no branch
+            self._conn.remove_channel(self._recv_chan)
+            self._recv_chan = None
             self._conn = None
 
-        self._send_state = 'closed'
-        self._recv_state = 'closed'
+    def _close_send(self):
+        """Discard unsent data and close the channel for sending"""
+
+        # Discard unsent data
+        self._send_buf = []
+        self._send_buf_len = 0
+
+        if self._send_state != 'closed':
+            self._send_packet(MSG_CHANNEL_CLOSE)
+            self._send_chan = None
+            self._send_state = 'closed'
+
+    def _discard_recv(self):
+        """Discard unreceived data and clean up if close received"""
+
+        # Discard unreceived data
+        self._recv_buf = []
+        self._recv_paused = False
+
+        # If recv is close_pending, we know send is already closed
+        if self._recv_state == 'close_pending':
+            self._recv_state = 'closed'
+            self._loop.call_soon(self._cleanup)
 
     def _pause_resume_writing(self):
         """Pause or resume writing based on send buffer low/high water marks"""
@@ -178,59 +199,72 @@ class SSHChannel(SSHPacketHandler):
         if not self._send_buf:
             if self._send_state == 'eof_pending':
                 self._send_packet(MSG_CHANNEL_EOF)
-                self._send_state = 'eof_sent'
+                self._send_state = 'eof'
             elif self._send_state == 'close_pending':
-                self._send_packet(MSG_CHANNEL_CLOSE)
-                self._send_state = 'close_sent'
+                self._close_send()
+
+    def _flush_recv_buf(self, exc=None):
+        """Flush as much data in the recv buffer as the application allows"""
+
+        while self._recv_buf and not self._recv_paused:
+            self._deliver_data(*self._recv_buf.pop(0))
+
+        if not self._recv_buf:
+            if self._recv_state == 'eof_pending':
+                self._recv_state = 'eof'
+
+                if (not self._session.eof_received() and
+                        self._send_state == 'open'):
+                    self.write_eof()
+            elif self._recv_state == 'close_pending':
+                self._recv_state = 'closed'
+
+                if self._recv_partial and not exc:
+                    exc = DisconnectError(DISC_PROTOCOL_ERROR,
+                                          'Unicode decode error')
+
+                self._loop.call_soon(self._cleanup, exc)
 
     def _deliver_data(self, data, datatype):
         """Deliver incoming data to the session"""
 
-        if data == _EOF:
-            if datatype in self._recv_partial:
-                raise DisconnectError(DISC_PROTOCOL_ERROR,
-                                      'Unicode decode error')
+        self._recv_window -= len(data)
 
-            if not self._session.eof_received() and self._send_state == 'open':
-                self.write_eof()
-        else:
-            self._recv_window -= len(data)
-
-            if self._recv_window < self._init_recv_window / 2:
-                self._send_packet(MSG_CHANNEL_WINDOW_ADJUST,
-                                  UInt32(self._init_recv_window -
-                                         self._recv_window))
-                self._recv_window = self._init_recv_window
-
-            if self._encoding:
-                if datatype in self._recv_partial:
-                    encdata = self._recv_partial.pop(datatype) + data
-                else:
-                    encdata = data
-
-                while encdata:
-                    try:
-                        data = encdata.decode(self._encoding)
-                        encdata = b''
-                    except UnicodeDecodeError as exc:
-                        if exc.start > 0:
-                            # Avoid pylint false positive
-                            # pylint: disable=invalid-slice-index
-                            data = encdata[:exc.start].decode()
-                            encdata = encdata[exc.start:]
-                        elif exc.reason == 'unexpected end of data':
-                            break
-                        else:
-                            raise DisconnectError(DISC_PROTOCOL_ERROR,
-                                                  'Unicode decode error')
-
-                    self._session.data_received(data, datatype)
-
-                if encdata:
-                    self._recv_partial[datatype] = encdata
+        if self._recv_window < self._init_recv_window / 2:
+            self._send_packet(MSG_CHANNEL_WINDOW_ADJUST,
+                              UInt32(self._init_recv_window -
+                                     self._recv_window))
+            self._recv_window = self._init_recv_window
+
+        if self._encoding:
+            if datatype in self._recv_partial:
+                encdata = self._recv_partial.pop(datatype) + data
             else:
+                encdata = data
+
+            while encdata:
+                try:
+                    data = encdata.decode(self._encoding)
+                    encdata = b''
+                except UnicodeDecodeError as exc:
+                    if exc.start > 0:
+                        # Avoid pylint false positive
+                        # pylint: disable=invalid-slice-index
+                        data = encdata[:exc.start].decode()
+                        encdata = encdata[exc.start:]
+                    elif exc.reason == 'unexpected end of data':
+                        break
+                    else:
+                        raise DisconnectError(DISC_PROTOCOL_ERROR,
+                                              'Unicode decode error')
+
                 self._session.data_received(data, datatype)
 
+            if encdata:
+                self._recv_partial[datatype] = encdata
+        else:
+            self._session.data_received(data, datatype)
+
     def _accept_data(self, data, datatype=None):
         """Accept new data on the channel
 
@@ -246,10 +280,10 @@ class SSHChannel(SSHPacketHandler):
         if not data:
             return
 
-        if self._send_state in {'close_pending', 'close_sent', 'closed'}:
+        if self._send_state in {'close_pending', 'closed'}:
             return
 
-        if data != _EOF and len(data) > self._recv_window:
+        if len(data) > self._recv_window:
             raise DisconnectError(DISC_PROTOCOL_ERROR, 'Window exceeded')
 
         if self._recv_paused:
@@ -260,25 +294,29 @@ class SSHChannel(SSHPacketHandler):
     def process_connection_close(self, exc):
         """Process the SSH connection closing"""
 
-        self._cleanup(exc)
+        if self._send_state != 'closed':
+            self._send_state = 'closed'
+
+        if self._recv_state not in {'close_pending', 'closed'}:
+            self._recv_state = 'close_pending'
+            self._flush_recv_buf(exc)
+        elif self._recv_state == 'closed':
+            self._loop.call_soon(self._cleanup, exc)
 
     def process_open(self, send_chan, send_window, send_pktsize, session):
         """Process a channel open request"""
 
-        if self._recv_state != 'closed':
-            raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel already open')
-
-        self._send_state = 'open_received'
         self._send_chan = send_chan
         self._send_window = send_window
         self._send_pktsize = send_pktsize
 
-        asyncio.async(self._finish_open_request(session), loop=self._loop)
+        self._conn.create_task(self._finish_open_request(session))
 
     @asyncio.coroutine
     def _finish_open_request(self, session):
         """Finish processing a channel open request"""
 
+        # pylint: disable=broad-except
         try:
             if asyncio.iscoroutine(session):
                 session = yield from session
@@ -314,7 +352,7 @@ class SSHChannel(SSHPacketHandler):
         self._send_state = 'open'
         self._recv_state = 'open'
 
-        if not self._open_waiter.cancelled():
+        if not self._open_waiter.cancelled(): # pragma: no branch
             self._open_waiter.set_result(packet)
 
         self._open_waiter = None
@@ -326,7 +364,7 @@ class SSHChannel(SSHPacketHandler):
             raise DisconnectError(DISC_PROTOCOL_ERROR,
                                   'Channel not being opened')
 
-        if not self._open_waiter.cancelled():
+        if not self._open_waiter.cancelled(): # pragma: no branch
             self._open_waiter.set_exception(
                 ChannelOpenError(code, reason, lang))
 
@@ -338,7 +376,7 @@ class SSHChannel(SSHPacketHandler):
 
         # pylint: disable=unused-argument
 
-        if self._recv_state not in {'open', 'eof_received'}:
+        if self._recv_state not in {'open', 'eof_pending', 'eof'}:
             raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open')
 
         adjust = packet.get_uint32()
@@ -391,41 +429,32 @@ class SSHChannel(SSHPacketHandler):
 
         packet.check_end()
 
-        self._recv_state = 'eof_received'
-        self._accept_data(_EOF)
+        self._recv_state = 'eof_pending'
+        self._flush_recv_buf()
 
     def _process_close(self, pkttype, packet):
         """Process an incoming channel close"""
 
         # pylint: disable=unused-argument
 
-        if self._recv_state not in {'open', 'eof_received'}:
+        if self._recv_state not in {'open', 'eof_pending', 'eof'}:
             raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open')
 
         packet.check_end()
 
-        # Flush any unsent data
-        self._send_buf = []
-        self._send_buf_len = 0
-
-        # If we haven't yet sent a close, send one now
-        if self._send_state not in {'close_sent', 'closed'}:
-            self._send_packet(MSG_CHANNEL_CLOSE)
-            self._send_state = 'close_sent'
+        self._close_send()
 
-        self._loop.call_soon(self._cleanup)
+        self._recv_state = 'close_pending'
+        self._flush_recv_buf()
 
     def _process_request(self, pkttype, packet):
         """Process an incoming channel request"""
 
         # pylint: disable=unused-argument
 
-        if self._recv_state not in {'open', 'eof_received'}:
+        if self._recv_state not in {'open', 'eof_pending', 'eof'}:
             raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open')
 
-        if self._send_state in {'close_pending', 'close_sent', 'closed'}:
-            return
-
         request = packet.get_string()
         want_reply = packet.get_boolean()
 
@@ -435,34 +464,28 @@ class SSHChannel(SSHPacketHandler):
             raise DisconnectError(DISC_PROTOCOL_ERROR,
                                   'Invalid channel request') from None
 
-        name = '_process_' + request.replace('-', '_') + '_request'
+        name = '_process_' + map_handler_name(request) + '_request'
         handler = getattr(self, name, None)
         result = handler(packet) if callable(handler) else False
 
-        if want_reply:
+        if want_reply and self._send_state not in {'close_pending', 'closed'}:
             if result:
                 self._send_packet(MSG_CHANNEL_SUCCESS)
             else:
                 self._send_packet(MSG_CHANNEL_FAILURE)
 
-        if result and request in ('shell', 'exec', 'subsystem'):
+        if result and request in {'shell', 'exec', 'subsystem'}:
             self._session.session_started()
             self.resume_reading()
 
     def _process_response(self, pkttype, packet):
         """Process a success or failure response"""
 
-        # pylint: disable=unused-argument
-
-        if self._send_state not in {'open', 'eof_pending', 'eof_sent',
-                                    'close_pending', 'close_sent'}:
-            raise DisconnectError(DISC_PROTOCOL_ERROR, 'Channel not open')
-
         packet.check_end()
 
         if self._request_waiters:
             waiter = self._request_waiters.pop(0)
-            if not waiter.cancelled():
+            if not waiter.cancelled(): # pragma: no branch
                 waiter.set_result(pkttype == MSG_CHANNEL_SUCCESS)
... 15377 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-asyncssh.git



More information about the Python-modules-commits mailing list