Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-asyncssh for openSUSE:Factory 
checked in at 2023-11-13 22:18:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
 and      /work/SRC/openSUSE:Factory/.python-asyncssh.new.17445 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-asyncssh"

Mon Nov 13 22:18:05 2023 rev:25 rq:1124972 version:2.14.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes  
2023-10-05 20:06:09.181215623 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-asyncssh.new.17445/python-asyncssh.changes   
    2023-11-13 22:20:41.635111844 +0100
@@ -1,0 +2,16 @@
+Fri Nov 10 12:34:04 UTC 2023 - Dirk Müller <[email protected]>
+
+- update to 2.14.1 (bsc#1217028, CVE-2023-46445):
+  * Hardened AsyncSSH state machine against potential message
+    injection attacks, described in more detail in
+    `CVE-2023-46445 and CVE-2023-46446
+  * Added support for passing in a regex in readuntil in
+    SSHReader,
+  * Added support for get_addresses() and get_port() methods on
+  * SSHAcceptor.
+  * Fixed an issue with AsyncFileWriter potentially writing data
+  * out of order.
+  * Updated testing to include Python 3.12.
+  * Updated readthedocs integration to use YAML config file.
+
+-------------------------------------------------------------------

Old:
----
  asyncssh-2.14.0.tar.gz

New:
----
  asyncssh-2.14.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.QQbB0w/_old  2023-11-13 22:20:42.303136440 +0100
+++ /var/tmp/diff_new_pack.QQbB0w/_new  2023-11-13 22:20:42.303136440 +0100
@@ -19,7 +19,7 @@
 %define skip_python2 1
 %define skip_python36 1
 Name:           python-asyncssh
-Version:        2.14.0
+Version:        2.14.1
 Release:        0
 Summary:        Asynchronous SSHv2 client and server library
 License:        EPL-2.0 OR GPL-2.0-or-later

++++++ asyncssh-2.14.0.tar.gz -> asyncssh-2.14.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/.github/workflows/run_tests.yml 
new/asyncssh-2.14.1/.github/workflows/run_tests.yml
--- old/asyncssh-2.14.0/.github/workflows/run_tests.yml 2023-10-01 
02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/.github/workflows/run_tests.yml 2023-11-09 
03:28:14.000000000 +0100
@@ -8,7 +8,7 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
           - os: macos-latest
             python-version: "3.10"
@@ -16,6 +16,9 @@
           - os: macos-latest
             python-version: "3.11"
             openssl-version: "3"
+          - os: macos-latest
+            python-version: "3.12"
+            openssl-version: "3"
         exclude:
           # test hangs on these combination
           - os: windows-latest
@@ -26,6 +29,8 @@
             python-version: "3.10"
           - os: windows-latest
             python-version: "3.11"
+          - os: windows-latest
+            python-version: "3.12"
 
     runs-on: ${{ matrix.os }}
     env:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/.readthedocs.yaml 
new/asyncssh-2.14.1/.readthedocs.yaml
--- old/asyncssh-2.14.0/.readthedocs.yaml       1970-01-01 01:00:00.000000000 
+0100
+++ new/asyncssh-2.14.1/.readthedocs.yaml       2023-10-01 04:26:44.000000000 
+0200
@@ -0,0 +1,14 @@
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
+python:
+  install:
+    - requirements: docs/requirements.txt
+
+sphinx:
+  configuration: docs/conf.py
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/PKG-INFO new/asyncssh-2.14.1/PKG-INFO
--- old/asyncssh-2.14.0/PKG-INFO        2023-10-01 03:06:55.014456500 +0200
+++ new/asyncssh-2.14.1/PKG-INFO        2023-11-09 04:02:21.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: asyncssh
-Version: 2.14.0
+Version: 2.14.1
 Summary: AsyncSSH: Asynchronous SSHv2 client and server library
 Home-page: http://asyncssh.timeheart.net
 Author: Ron Frederick
@@ -16,11 +16,12 @@
 Classifier: License :: OSI Approved
 Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 Classifier: Topic :: Internet
 Classifier: Topic :: Security :: Cryptography
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -265,3 +266,5 @@
 __ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/connection.py 
new/asyncssh-2.14.1/asyncssh/connection.py
--- old/asyncssh-2.14.0/asyncssh/connection.py  2023-10-01 02:35:42.000000000 
+0200
+++ new/asyncssh-2.14.1/asyncssh/connection.py  2023-11-09 03:28:14.000000000 
+0100
@@ -693,13 +693,61 @@
     def __getattr__(self, name: str) -> Any:
         return getattr(self._server, name)
 
+    def get_addresses(self) -> List[Tuple]:
+        """Return socket addresses being listened on
+
+           This method returns the socket addresses being listened on.
+           It returns tuples of the form returned by
+           :meth:`socket.getsockname`.  If the listener was created
+           using a hostname, the host's resolved IPs will be returned.
+           If the requested listening port was `0`, the selected
+           listening ports will be returned.
+
+           :returns: A list of socket addresses being listened on
+
+        """
+
+        if hasattr(self._server, 'get_addresses'):
+            return self._server.get_addresses()
+        else:
+            return [sock.getsockname() for sock in self.sockets]
+
+    def get_port(self) -> int:
+        """Return the port number being listened on
+
+           This method returns the port number being listened on.
+           If it is listening on multiple sockets with different port
+           numbers, this function will return `0`. In that case,
+           :meth:`get_addresses` can be used to retrieve the full
+           list of listening addresses and ports.
+
+           :returns: The port number being listened on, if there's only one
+
+        """
+
+        if hasattr(self._server, 'get_port'):
+            return self._server.get_port()
+        else:
+            ports = set(addr[1] for addr in self.get_addresses())
+            return ports.pop() if len(ports) == 1 else 0
+
     def close(self) -> None:
-        """Close this SSH listener"""
+        """Stop listening for new connections
+
+           This method can be called to stop listening for new
+           SSH connections. Existing connections will remain open.
+
+        """
 
         self._server.close()
 
     async def wait_closed(self) -> None:
-        """Wait for this SSH listener to close"""
+        """Wait for this listener to close
+
+           This method is a coroutine which waits for this
+           listener to be closed.
+
+        """
 
         await self._server.wait_closed()
 
@@ -851,6 +899,8 @@
         self._can_send_ext_info = False
         self._extensions_to_send: 'OrderedDict[bytes, bytes]' = OrderedDict()
 
+        self._can_recv_ext_info = False
+
         self._server_sig_algs: Set[bytes] = set()
 
         self._next_service: Optional[bytes] = None
@@ -860,6 +910,7 @@
         self._auth: Optional[Auth] = None
         self._auth_in_progress = False
         self._auth_complete = False
+        self._auth_final = False
         self._auth_methods = [b'none']
         self._auth_was_trivial = True
         self._username = ''
@@ -1490,15 +1541,25 @@
         skip_reason = ''
         exc_reason = ''
 
-        if self._kex and MSG_KEX_FIRST <= pkttype <= MSG_KEX_LAST:
-            if self._ignore_first_kex: # pragma: no cover
-                skip_reason = 'ignored first kex'
-                self._ignore_first_kex = False
+        if MSG_KEX_FIRST <= pkttype <= MSG_KEX_LAST:
+            if self._kex:
+                if self._ignore_first_kex: # pragma: no cover
+                    skip_reason = 'ignored first kex'
+                    self._ignore_first_kex = False
+                else:
+                    handler = self._kex
+            else:
+                skip_reason = 'kex not in progress'
+                exc_reason = 'Key exchange not in progress'
+        elif MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST:
+            if self._auth:
+                handler = self._auth
             else:
-                handler = self._kex
-        elif (self._auth and
-              MSG_USERAUTH_FIRST <= pkttype <= MSG_USERAUTH_LAST):
-            handler = self._auth
+                skip_reason = 'auth not in progress'
+                exc_reason = 'Authentication not in progress'
+        elif pkttype > MSG_KEX_LAST and not self._recv_encryption:
+            skip_reason = 'invalid request before kex complete'
+            exc_reason = 'Invalid request before key exchange was complete'
         elif pkttype > MSG_USERAUTH_LAST and not self._auth_complete:
             skip_reason = 'invalid request before auth complete'
             exc_reason = 'Invalid request before authentication was complete'
@@ -1531,6 +1592,9 @@
         if exc_reason:
             raise ProtocolError(exc_reason)
 
+        if pkttype > MSG_USERAUTH_LAST:
+            self._auth_final = True
+
         if self._transport:
             self._recv_seq = (seq + 1) & 0xffffffff
             self._recv_handler = self._recv_pkthdr
@@ -1548,9 +1612,7 @@
             self._send_kexinit()
             self._kexinit_sent = True
 
-        if (((pkttype in {MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT} or
-              pkttype > MSG_KEX_LAST) and not self._kex_complete) or
-                (pkttype == MSG_USERAUTH_BANNER and
+        if ((pkttype == MSG_USERAUTH_BANNER and
                  not (self._auth_in_progress or self._auth_complete)) or
                 (pkttype > MSG_USERAUTH_LAST and not self._auth_complete)):
             self._deferred_packets.append((pkttype, args))
@@ -1762,9 +1824,11 @@
                         not self._waiter.cancelled():
                     self._waiter.set_result(None)
                     self._wait = None
-                else:
-                    self.send_service_request(_USERAUTH_SERVICE)
+                    return
         else:
+            self._extensions_to_send[b'server-sig-algs'] = \
+                b','.join(self._sig_algs)
+
             self._send_encryption = next_enc_sc
             self._send_enchdrlen = 1 if etm_sc else 5
             self._send_blocksize = max(8, enc_blocksize_sc)
@@ -1785,17 +1849,18 @@
                 recv_mac=self._mac_alg_cs.decode('ascii'),
                 recv_compression=self._cmp_alg_cs.decode('ascii'))
 
-            if first_kex:
-                self._next_service = _USERAUTH_SERVICE
-
-                self._extensions_to_send[b'server-sig-algs'] = \
-                    b','.join(self._sig_algs)
-
         if self._can_send_ext_info:
             self._send_ext_info()
             self._can_send_ext_info = False
 
         self._kex_complete = True
+
+        if first_kex:
+            if self.is_client():
+                self.send_service_request(_USERAUTH_SERVICE)
+            else:
+                self._next_service = _USERAUTH_SERVICE
+
         self._send_deferred_packets()
 
     def send_service_request(self, service: bytes) -> None:
@@ -2032,18 +2097,25 @@
         service = packet.get_string()
         packet.check_end()
 
-        if service == self._next_service:
-            self.logger.debug2('Accepting request for service %s', service)
+        if self.is_client():
+            raise ProtocolError('Unexpected service request received')
 
-            self.send_packet(MSG_SERVICE_ACCEPT, String(service))
+        if not self._recv_encryption:
+            raise ProtocolError('Service request received before kex complete')
 
-            if (self.is_server() and               # pragma: no branch
-                    not self._auth_in_progress and
-                    service == _USERAUTH_SERVICE):
-                self._auth_in_progress = True
-                self._send_deferred_packets()
-        else:
-            raise ServiceNotAvailable('Unexpected service request received')
+        if service != self._next_service:
+            raise ServiceNotAvailable('Unexpected service in service request')
+
+        self.logger.debug2('Accepting request for service %s', service)
+
+        self.send_packet(MSG_SERVICE_ACCEPT, String(service))
+
+        self._next_service = None
+
+        if service == _USERAUTH_SERVICE: # pragma: no branch
+            self._auth_in_progress = True
+            self._can_recv_ext_info = False
+            self._send_deferred_packets()
 
     def _process_service_accept(self, _pkttype: int, _pktid: int,
                                 packet: SSHPacket) -> None:
@@ -2052,27 +2124,35 @@
         service = packet.get_string()
         packet.check_end()
 
-        if service == self._next_service:
-            self.logger.debug2('Request for service %s accepted', service)
+        if self.is_server():
+            raise ProtocolError('Unexpected service accept received')
 
-            self._next_service = None
+        if not self._recv_encryption:
+            raise ProtocolError('Service accept received before kex complete')
 
-            if (self.is_client() and               # pragma: no branch
-                    service == _USERAUTH_SERVICE):
-                self.logger.info('Beginning auth for user %s', self._username)
+        if service != self._next_service:
+            raise ServiceNotAvailable('Unexpected service in service accept')
 
-                self._auth_in_progress = True
+        self.logger.debug2('Request for service %s accepted', service)
 
-                # This method is only in SSHClientConnection
-                # pylint: disable=no-member
-                cast('SSHClientConnection', self).try_next_auth()
-        else:
-            raise ServiceNotAvailable('Unexpected service accept received')
+        self._next_service = None
+
+        if service == _USERAUTH_SERVICE: # pragma: no branch
+            self.logger.info('Beginning auth for user %s', self._username)
+
+            self._auth_in_progress = True
+
+            # This method is only in SSHClientConnection
+            # pylint: disable=no-member
+            cast('SSHClientConnection', self).try_next_auth()
 
     def _process_ext_info(self, _pkttype: int, _pktid: int,
                           packet: SSHPacket) -> None:
         """Process extension information"""
 
+        if not self._can_recv_ext_info:
+            raise ProtocolError('Unexpected ext_info received')
+
         extensions: Dict[bytes, bytes] = {}
 
         self.logger.debug2('Received extension info')
@@ -2198,6 +2278,7 @@
             self._decompress_after_auth = self._next_decompress_after_auth
 
             self._next_recv_encryption = None
+            self._can_recv_ext_info = True
         else:
             raise ProtocolError('New keys not negotiated')
 
@@ -2225,8 +2306,10 @@
         if self.is_client():
             raise ProtocolError('Unexpected userauth request')
         elif self._auth_complete:
-            # Silently ignore requests if we're already authenticated
-            pass
+            # Silently ignore additional auth requests after auth succeeds,
+            # until the client sends a non-auth message
+            if self._auth_final:
+                raise ProtocolError('Unexpected userauth request')
         else:
             if username != self._username:
                 self.logger.info('Beginning auth for user %s', username)
@@ -2268,7 +2351,7 @@
         self._auth = lookup_server_auth(cast(SSHServerConnection, self),
                                              self._username, method, packet)
 
-    def _process_userauth_failure(self, _pkttype: int, pktid: int,
+    def _process_userauth_failure(self, _pkttype: int, _pktid: int,
                                   packet: SSHPacket) -> None:
         """Process a user authentication failure response"""
 
@@ -2308,10 +2391,9 @@
             # pylint: disable=no-member
             cast(SSHClientConnection, self).try_next_auth()
         else:
-            self.logger.debug2('Unexpected userauth failure response')
-            self.send_packet(MSG_UNIMPLEMENTED, UInt32(pktid))
+            raise ProtocolError('Unexpected userauth failure response')
 
-    def _process_userauth_success(self, _pkttype: int, pktid: int,
+    def _process_userauth_success(self, _pkttype: int, _pktid: int,
                                   packet: SSHPacket) -> None:
         """Process a user authentication success response"""
 
@@ -2337,6 +2419,7 @@
             self._auth = None
             self._auth_in_progress = False
             self._auth_complete = True
+            self._can_recv_ext_info = False
 
             if self._agent:
                 self._agent.close()
@@ -2364,8 +2447,7 @@
                 self._waiter.set_result(None)
                 self._wait = None
         else:
-            self.logger.debug2('Unexpected userauth success response')
-            self.send_packet(MSG_UNIMPLEMENTED, UInt32(pktid))
+            raise ProtocolError('Unexpected userauth success response')
 
     def _process_userauth_banner(self, _pkttype: int, _pktid: int,
                                  packet: SSHPacket) -> None:
@@ -4013,7 +4095,7 @@
                              stderr: ProcessTarget = PIPE,
                              bufsize: int = io.DEFAULT_BUFFER_SIZE,
                              send_eof: bool = True, recv_eof: bool = True,
-                             **kwargs: object) -> SSHClientProcess:
+                             **kwargs: object) -> SSHClientProcess[AnyStr]:
         """Create a process on the remote system
 
            This method is a coroutine wrapper around :meth:`create_session`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/listener.py 
new/asyncssh-2.14.1/asyncssh/listener.py
--- old/asyncssh-2.14.0/asyncssh/listener.py    2022-11-20 17:44:58.000000000 
+0100
+++ new/asyncssh-2.14.1/asyncssh/listener.py    2023-11-09 03:28:14.000000000 
+0100
@@ -184,6 +184,11 @@
 
         return chan, self._session_factory(orig_host, orig_port)
 
+    def get_addresses(self) -> List[Tuple]:
+        """Return the socket addresses being listened on"""
+
+        return [(self._listen_host, self._listen_port)]
+
     def get_port(self) -> int:
         """Return the port number being listened on"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/process.py 
new/asyncssh-2.14.1/asyncssh/process.py
--- old/asyncssh-2.14.0/asyncssh/process.py     2023-08-12 23:12:09.000000000 
+0200
+++ new/asyncssh-2.14.1/asyncssh/process.py     2023-10-06 07:36:18.000000000 
+0200
@@ -30,8 +30,8 @@
 import socket
 import stat
 from types import TracebackType
-from typing import Any, AnyStr, Callable, Dict, Generic, IO
-from typing import Iterable, Mapping, Optional, Set, TextIO
+from typing import Any, AnyStr, Awaitable, Callable, Dict, Generic, IO
+from typing import Iterable, List, Mapping, Optional, Set, TextIO
 from typing import Tuple, Type, TypeVar, Union, cast
 from typing_extensions import Protocol
 
@@ -308,14 +308,33 @@
                  encoding: Optional[str], errors: str):
         super().__init__(encoding, errors, hasattr(file, 'encoding'))
 
-        self._conn = process.channel.get_connection()
+        self._process: 'SSHProcess[AnyStr]' = process
         self._file = file
         self._needs_close = needs_close
+        self._queue: asyncio.Queue[Optional[AnyStr]] = asyncio.Queue()
+        self._write_task: Optional[asyncio.Task[None]] = \
+            process.channel.get_connection().create_task(self._writer())
+
+    async def _writer(self) -> None:
+        """Process writes to the file"""
+
+        while True:
+            data = await self._queue.get()
+
+            if data is None:
+                self._queue.task_done()
+                break
+
+            await self._file.write(self.encode(data))
+            self._queue.task_done()
+
+        if self._needs_close:
+            await self._file.close()
 
     def write(self, data: AnyStr) -> None:
         """Write data to the file"""
 
-        self._conn.create_task(self._file.write(self.encode(data)))
+        self._queue.put_nowait(data)
 
     def write_eof(self) -> None:
         """Close output file when end of file is received"""
@@ -325,8 +344,10 @@
     def close(self) -> None:
         """Stop forwarding data to the file"""
 
-        if self._needs_close:
-            self._conn.create_task(self._file.close())
+        if self._write_task:
+            self._write_task = None
+            self._queue.put_nowait(None)
+            self._process.add_cleanup_task(self._queue.join())
 
 
 class _PipeReader(_UnicodeReader[AnyStr], asyncio.BaseProtocol):
@@ -721,6 +742,8 @@
     def __init__(self, *args) -> None:
         super().__init__(*args)
 
+        self._cleanup_tasks: List[Awaitable[None]] = []
+
         self._readers: Dict[Optional[int], _ReaderProtocol] = {}
         self._send_eof: Dict[Optional[int], bool] = {}
 
@@ -729,6 +752,20 @@
 
         self._paused_write_streams: Set[Optional[int]] = set()
 
+    async def __aenter__(self) -> 'SSHProcess[AnyStr]':
+        """Allow SSHProcess to be used as an async context manager"""
+
+        return self
+
+    async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
+                        _exc_value: Optional[BaseException],
+                        _traceback: Optional[TracebackType]) -> bool:
+        """Wait for a full channel close when exiting the async context"""
+
+        self.close()
+        await self.wait_closed()
+        return False
+
     @property
     def channel(self) -> SSHChannel[AnyStr]:
         """The channel associated with the process"""
@@ -931,6 +968,11 @@
         return bool(self._paused_write_streams) or \
             super()._should_pause_reading()
 
+    def add_cleanup_task(self, task: Awaitable[None]) -> None:
+        """Add a task to run when the process exits"""
+
+        self._cleanup_tasks.append(task)
+
     def connection_lost(self, exc: Optional[Exception]) -> None:
         """Handle a close of the SSH channel"""
 
@@ -1091,6 +1133,9 @@
         assert self._chan is not None
         await self._chan.wait_closed()
 
+        for task in self._cleanup_tasks:
+            await task
+
 
 class SSHClientProcess(SSHProcess[AnyStr], SSHClientStreamSession[AnyStr]):
     """SSH client process handler"""
@@ -1105,20 +1150,6 @@
         self._stdout: Optional[SSHReader[AnyStr]] = None
         self._stderr: Optional[SSHReader[AnyStr]] = None
 
-    async def __aenter__(self) -> 'SSHClientProcess[AnyStr]':
-        """Allow SSHProcess to be used as an async context manager"""
-
-        return self
-
-    async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
-                        _exc_value: Optional[BaseException],
-                        _traceback: Optional[TracebackType]) -> bool:
-        """Wait for a full channel close when exiting the async context"""
-
-        self.close()
-        await self._chan.wait_closed()
-        return False
-
     def _collect_output(self, datatype: DataType = None) -> AnyStr:
         """Return output from the process"""
 
@@ -1333,7 +1364,7 @@
             self._chan.write(input)
             self._chan.write_eof()
 
-        await self._chan.wait_closed()
+        await self.wait_closed()
 
         return self.collect_output()
     # pylint: enable=redefined-builtin
@@ -1482,20 +1513,6 @@
         self._stdout: Optional[SSHWriter[AnyStr]] = None
         self._stderr: Optional[SSHWriter[AnyStr]] = None
 
-    async def __aenter__(self) -> 'SSHServerProcess[AnyStr]':
-        """Allow SSHProcess to be used as an async context manager"""
-
-        return self
-
-    async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
-                        _exc_value: Optional[BaseException],
-                        _traceback: Optional[TracebackType]) -> bool:
-        """Wait for a full channel close when exiting the async context"""
-
-        self.close()
-        await self._chan.wait_closed()
-        return False
-
     def _start_process(self, stdin: SSHReader[AnyStr],
                        stdout: SSHWriter[AnyStr],
                        stderr: SSHWriter[AnyStr]) -> MaybeAwait[None]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/stream.py 
new/asyncssh-2.14.1/asyncssh/stream.py
--- old/asyncssh-2.14.0/asyncssh/stream.py      2022-12-27 22:30:36.000000000 
+0100
+++ new/asyncssh-2.14.1/asyncssh/stream.py      2023-11-09 03:28:14.000000000 
+0100
@@ -24,8 +24,8 @@
 import inspect
 import re
 from typing import TYPE_CHECKING, Any, AnyStr, AsyncIterator
-from typing import Callable, Dict, Generic, Iterable
-from typing import List, Optional, Set, Tuple, Union, cast
+from typing import Callable, Dict, Generic, Iterable, List
+from typing import Optional, Pattern, Set, Tuple, Union, cast
 
 from .constants import EXTENDED_DATA_STDERR
 from .logging import SSHLogger
@@ -180,17 +180,35 @@
         except asyncio.IncompleteReadError as exc:
             return cast(AnyStr, exc.partial)
 
-    async def readuntil(self, separator: object) -> AnyStr:
+    async def readuntil(self, separator: object,
+                        max_separator_len = 0) -> AnyStr:
         """Read data from the stream until `separator` is seen
 
            This method is a coroutine which reads from the stream until
            the requested separator is seen. If a match is found, the
            returned data will include the separator at the end.
 
-           The separator argument can be either a single `bytes` or
-           `str` value or a sequence of multiple values to match
-           against, returning data as soon as any of the separators
-           are found in the stream.
+           The `separator` argument can be a single `bytes` or `str`
+           value, a sequence of multiple `bytes` or `str` values,
+           or a compiled regex (`re.Pattern`) to match against,
+           returning data as soon as a matching separator is found
+           in the stream.
+
+           When passing a regex pattern as the separator, the
+           `max_separator_len` argument should be set to the
+           maximum length of an expected separator match. This
+           can greatly improve performance, by minimizing how far
+           back into the stream must be searched for a match.
+           When passing literal separators to match against, the
+           max separator length will be set automatically.
+
+           .. note:: For best results, a separator regex should
+                     both begin and end with data which is as
+                     unique as possible, and should not start or
+                     end with optional or repeated elements.
+                     Otherwise, you run the risk of failing to
+                     match parts of a separator when it is split
+                     across multiple reads.
 
            If EOF or a signal is received before a match occurs, an
            :exc:`IncompleteReadError <asyncio.IncompleteReadError>`
@@ -202,7 +220,8 @@
 
         """
 
-        return await self._session.readuntil(separator, self._datatype)
+        return await self._session.readuntil(separator, self._datatype,
+                                             max_separator_len)
 
     async def readexactly(self, n: int) -> AnyStr:
         """Read an exact amount of data from the stream
@@ -558,7 +577,8 @@
 
         return result
 
-    async def readuntil(self, separator: object, datatype: DataType) -> AnyStr:
+    async def readuntil(self, separator: object, datatype: DataType,
+                        max_separator_len: int) -> AnyStr:
         """Read data from the channel until a separator is seen"""
 
         if not separator:
@@ -570,16 +590,20 @@
         if separator is _NEWLINE:
             seplen = 1
             separators = cast(AnyStr, '\n' if self._encoding else b'\n')
+            pat = re.compile(separators)
         elif isinstance(separator, (bytes, str)):
             seplen = len(separator)
-            separators = re.escape(cast(AnyStr, separator))
+            pat = re.compile(re.escape(cast(AnyStr, separator)))
+        elif isinstance(separator, Pattern):
+            seplen = max_separator_len
+            pat = cast(Pattern[AnyStr], separator)
         else:
             bar = cast(AnyStr, '|' if self._encoding else b'|')
             seplist = list(cast(Iterable[AnyStr], separator))
             seplen = max(len(sep) for sep in seplist)
             separators = bar.join(re.escape(sep) for sep in seplist)
+            pat = re.compile(separators)
 
-        pat = re.compile(separators)
         curbuf = 0
         buflen = 0
 
@@ -602,7 +626,7 @@
 
                     newbuf = cast(AnyStr, recv_buf[curbuf])
                     buf += newbuf
-                    start = max(buflen + 1 - seplen, 0)
+                    start = 0 if seplen == 0 else max(buflen + 1 - seplen, 0)
 
                     match = pat.search(buf, start)
                     if match:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh/version.py 
new/asyncssh-2.14.1/asyncssh/version.py
--- old/asyncssh-2.14.0/asyncssh/version.py     2023-10-01 03:04:52.000000000 
+0200
+++ new/asyncssh-2.14.1/asyncssh/version.py     2023-11-09 03:30:33.000000000 
+0100
@@ -26,4 +26,4 @@
 
 __url__ = 'http://asyncssh.timeheart.net'
 
-__version__ = '2.14.0'
+__version__ = '2.14.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh.egg-info/PKG-INFO 
new/asyncssh-2.14.1/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-2.14.0/asyncssh.egg-info/PKG-INFO      2023-10-01 
03:06:54.000000000 +0200
+++ new/asyncssh-2.14.1/asyncssh.egg-info/PKG-INFO      2023-11-09 
04:02:21.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: asyncssh
-Version: 2.14.0
+Version: 2.14.1
 Summary: AsyncSSH: Asynchronous SSHv2 client and server library
 Home-page: http://asyncssh.timeheart.net
 Author: Ron Frederick
@@ -16,11 +16,12 @@
 Classifier: License :: OSI Approved
 Classifier: Operating System :: MacOS :: MacOS X
 Classifier: Operating System :: POSIX
-Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 Classifier: Topic :: Internet
 Classifier: Topic :: Security :: Cryptography
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -265,3 +266,5 @@
 __ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/asyncssh.egg-info/SOURCES.txt 
new/asyncssh-2.14.1/asyncssh.egg-info/SOURCES.txt
--- old/asyncssh-2.14.0/asyncssh.egg-info/SOURCES.txt   2023-10-01 
03:06:54.000000000 +0200
+++ new/asyncssh-2.14.1/asyncssh.egg-info/SOURCES.txt   2023-11-09 
04:02:21.000000000 +0100
@@ -1,5 +1,6 @@
 .coveragerc
 .gitignore
+.readthedocs.yaml
 CONTRIBUTING.rst
 COPYRIGHT
 LICENSE
@@ -86,6 +87,7 @@
 docs/conf.py
 docs/contributing.rst
 docs/index.rst
+docs/requirements.txt
 docs/rtd-req.txt
 docs/_templates/sidebarbottom.html
 docs/_templates/sidebartop.html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/api.rst 
new/asyncssh-2.14.1/docs/api.rst
--- old/asyncssh-2.14.0/docs/api.rst    2023-10-01 02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/docs/api.rst    2023-11-09 03:28:14.000000000 +0100
@@ -1038,9 +1038,13 @@
 
 .. autoclass:: SSHAcceptor()
 
-   ====================== =
+   ============================= =
+   .. automethod:: get_addresses
+   .. automethod:: get_port
+   .. automethod:: close
+   .. automethod:: wait_closed
    .. automethod:: update
-   ====================== =
+   ============================= =
 
 
 SSHListener
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/changes.rst 
new/asyncssh-2.14.1/docs/changes.rst
--- old/asyncssh-2.14.0/docs/changes.rst        2023-10-01 03:04:52.000000000 
+0200
+++ new/asyncssh-2.14.1/docs/changes.rst        2023-11-09 03:50:21.000000000 
+0100
@@ -3,6 +3,32 @@
 Change Log
 ==========
 
+Release 2.14.1 (8 Nov 2023)
+---------------------------
+
+* Hardened AsyncSSH state machine against potential message
+  injection attacks, described in more detail in `CVE-2023-46445
+  <https://github.com/advisories/CVE-2023-46445>`_ and `CVE-2023-46446
+  <https://github.com/advisories/CVE-2023-46446>`_. Thanks go to
+  Fabian Bäumer, Marcus Brinkmann, and Jörg Schwenk for identifying
+  and reporting these vulnerabilities and providing detailed analysis
+  and suggestions about the proposed fixes.
+
+* Added support for passing in a regex in readuntil in SSHReader,
+  contributed by Oded Engel.
+
+* Added support for get_addresses() and get_port() methods on
+  SSHAcceptor. Thanks go to Allison Karlitskaya for suggesting
+  this feature.
+
+* Fixed an issue with AsyncFileWriter potentially writing data
+  out of order. Thanks go to Chan Chun Wai for reporting this
+  issue and providing code to reproduce it.
+
+* Updated testing to include Python 3.12.
+
+* Updated readthedocs integration to use YAML config file.
+
 Release 2.14.0 (30 Sep 2023)
 ----------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/conf.py 
new/asyncssh-2.14.1/docs/conf.py
--- old/asyncssh-2.14.0/docs/conf.py    2022-01-23 17:15:42.000000000 +0100
+++ new/asyncssh-2.14.1/docs/conf.py    2023-11-09 03:28:14.000000000 +0100
@@ -44,7 +44,7 @@
 
 # General information about the project.
 project = 'AsyncSSH'
-copyright = '2013-2017, ' + __author__
+copyright = '2013-2023, ' + __author__
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -103,7 +103,7 @@
 # further.  For a list of options available for each theme, see the
 # documentation.
 html_theme_options = {
-    "sidebarwidth": 305,
+    "sidebarwidth": 450,
     "stickysidebar": "true"
 }
 
@@ -129,7 +129,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+#html_static_path = ['_static']
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/docs/requirements.txt 
new/asyncssh-2.14.1/docs/requirements.txt
--- old/asyncssh-2.14.0/docs/requirements.txt   1970-01-01 01:00:00.000000000 
+0100
+++ new/asyncssh-2.14.1/docs/requirements.txt   2023-10-01 04:26:44.000000000 
+0200
@@ -0,0 +1,2 @@
+cryptography >= 39.0
+typing_extensions >= 3.6
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/setup.py new/asyncssh-2.14.1/setup.py
--- old/asyncssh-2.14.0/setup.py        2023-10-01 02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/setup.py        2023-11-09 03:28:14.000000000 +0100
@@ -78,11 +78,12 @@
           'License :: OSI Approved',
           'Operating System :: MacOS :: MacOS X',
           'Operating System :: POSIX',
-          'Programming Language :: Python :: 3.6',
           'Programming Language :: Python :: 3.7',
           'Programming Language :: Python :: 3.8',
           'Programming Language :: Python :: 3.9',
           'Programming Language :: Python :: 3.10',
+          'Programming Language :: Python :: 3.11',
+          'Programming Language :: Python :: 3.12',
           'Topic :: Internet',
           'Topic :: Security :: Cryptography',
           'Topic :: Software Development :: Libraries :: Python Modules',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/tests/test_connection.py 
new/asyncssh-2.14.1/tests/test_connection.py
--- old/asyncssh-2.14.0/tests/test_connection.py        2023-10-01 
02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/tests/test_connection.py        2023-11-09 
03:28:14.000000000 +0100
@@ -30,11 +30,12 @@
 from unittest.mock import patch
 
 import asyncssh
-from asyncssh.constants import MSG_UNIMPLEMENTED, MSG_DEBUG
+from asyncssh.constants import MSG_DEBUG
 from asyncssh.constants import MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT
-from asyncssh.constants import MSG_KEXINIT, MSG_NEWKEYS
+from asyncssh.constants import MSG_KEXINIT, MSG_NEWKEYS, MSG_KEX_FIRST
 from asyncssh.constants import MSG_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS
 from asyncssh.constants import MSG_USERAUTH_FAILURE, MSG_USERAUTH_BANNER
+from asyncssh.constants import MSG_USERAUTH_FIRST
 from asyncssh.constants import MSG_GLOBAL_REQUEST
 from asyncssh.constants import MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_CONFIRMATION
 from asyncssh.constants import MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_DATA
@@ -337,14 +338,6 @@
         return False
 
 
-def disconnect_on_unimplemented(self, pkttype, pktid, packet):
-    """Process an unimplemented message response"""
-
-    # pylint: disable=unused-argument
-
-    self.disconnect(asyncssh.DISC_BY_APPLICATION, 'Unexpected response')
-
-
 @patch_gss
 @patch('asyncssh.connection.SSHClientConnection', _CheckAlgsClientConnection)
 class _TestConnection(ServerTestCase):
@@ -974,8 +967,8 @@
 
         with patch('asyncssh.connection.SSHClientConnection.send_newkeys',
                    send_newkeys):
-            async with self.connect():
-                pass
+            with self.assertRaises(asyncssh.ProtocolError):
+                await self.connect()
 
     @asynctest
     async def test_encryption_algs(self):
@@ -1101,22 +1094,86 @@
         await conn.wait_closed()
 
     @asynctest
-    async def test_invalid_service_request(self):
-        """Test invalid service request"""
+    async def test_service_request_before_kex_complete(self):
+        """Test service request before kex is complete"""
+
+        def send_newkeys(self, k, h):
+            """Finish a key exchange and send a new keys message"""
+
+            self.send_packet(MSG_SERVICE_REQUEST, String('ssh-userauth'))
+
+            asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+        with patch('asyncssh.connection.SSHClientConnection.send_newkeys',
+                   send_newkeys):
+            with self.assertRaises(asyncssh.ProtocolError):
+                await self.connect()
+
+    @asynctest
+    async def test_service_accept_before_kex_complete(self):
+        """Test service accept before kex is complete"""
+
+        def send_newkeys(self, k, h):
+            """Finish a key exchange and send a new keys message"""
+
+            self.send_packet(MSG_SERVICE_ACCEPT, String('ssh-userauth'))
+
+            asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+        with patch('asyncssh.connection.SSHServerConnection.send_newkeys',
+                   send_newkeys):
+            with self.assertRaises(asyncssh.ProtocolError):
+                await self.connect()
+
+    @asynctest
+    async def test_unexpected_service_name_in_request(self):
+        """Test unexpected service name in service request"""
 
         conn = await self.connect()
         conn.send_packet(MSG_SERVICE_REQUEST, String('xxx'))
         await conn.wait_closed()
 
     @asynctest
-    async def test_invalid_service_accept(self):
-        """Test invalid service accept"""
+    async def test_unexpected_service_name_in_accept(self):
+        """Test unexpected service name in accept sent by server"""
+
+        def send_newkeys(self, k, h):
+            """Finish a key exchange and send a new keys message"""
+
+            asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+            self.send_packet(MSG_SERVICE_ACCEPT, String('xxx'))
+
+        with patch('asyncssh.connection.SSHServerConnection.send_newkeys',
+                   send_newkeys):
+            with self.assertRaises(asyncssh.ServiceNotAvailable):
+                await self.connect()
+
+    @asynctest
+    async def test_service_accept_from_client(self):
+        """Test service accept sent by client"""
 
         conn = await self.connect()
-        conn.send_packet(MSG_SERVICE_ACCEPT, String('xxx'))
+        conn.send_packet(MSG_SERVICE_ACCEPT, String('ssh-userauth'))
         await conn.wait_closed()
 
     @asynctest
+    async def test_service_request_from_server(self):
+        """Test service request sent by server"""
+
+        def send_newkeys(self, k, h):
+            """Finish a key exchange and send a new keys message"""
+
+            asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+            self.send_packet(MSG_SERVICE_REQUEST, String('ssh-userauth'))
+
+        with patch('asyncssh.connection.SSHServerConnection.send_newkeys',
+                   send_newkeys):
+            with self.assertRaises(asyncssh.ProtocolError):
+                await self.connect()
+
+    @asynctest
     async def test_packet_decode_error(self):
         """Test SSH packet decode error"""
 
@@ -1323,6 +1380,39 @@
         await conn.wait_closed()
 
     @asynctest
+    async def test_kex_after_kex_complete(self):
+        """Test kex request when kex not in progress"""
+
+        conn = await self.connect()
+        conn.send_packet(MSG_KEX_FIRST)
+        await conn.wait_closed()
+
+    @asynctest
+    async def test_userauth_after_auth_complete(self):
+        """Test userauth request when auth not in progress"""
+
+        conn = await self.connect()
+        conn.send_packet(MSG_USERAUTH_FIRST)
+        await conn.wait_closed()
+
+    @asynctest
+    async def test_userauth_before_kex_complete(self):
+        """Test receiving userauth before kex is complete"""
+
+        def send_newkeys(self, k, h):
+            """Finish a key exchange and send a new keys message"""
+
+            self.send_packet(MSG_USERAUTH_REQUEST, String('guest'),
+                             String('ssh-connection'), String('none'))
+
+            asyncssh.connection.SSHConnection.send_newkeys(self, k, h)
+
+        with patch('asyncssh.connection.SSHClientConnection.send_newkeys',
+                   send_newkeys):
+            with self.assertRaises(asyncssh.ProtocolError):
+                await self.connect()
+
+    @asynctest
     async def test_invalid_userauth_service(self):
         """Test invalid service in userauth request"""
 
@@ -1372,24 +1462,31 @@
             await asyncio.sleep(0.1)
 
     @asynctest
+    async def test_late_userauth_request(self):
+        """Test userauth request after auth is final"""
+
+        async with self.connect() as conn:
+            conn.send_packet(MSG_GLOBAL_REQUEST, String('xxx'),
+                             Boolean(False))
+            conn.send_packet(MSG_USERAUTH_REQUEST, String('guest'),
+                             String('ssh-connection'), String('none'))
+            await conn.wait_closed()
+
+    @asynctest
     async def test_unexpected_userauth_success(self):
         """Test unexpected userauth success response"""
 
-        with patch.dict('asyncssh.connection.SSHConnection._packet_handlers',
-                        {MSG_UNIMPLEMENTED: disconnect_on_unimplemented}):
-            conn = await self.connect()
-            conn.send_packet(MSG_USERAUTH_SUCCESS)
-            await conn.wait_closed()
+        conn = await self.connect()
+        conn.send_packet(MSG_USERAUTH_SUCCESS)
+        await conn.wait_closed()
 
     @asynctest
     async def test_unexpected_userauth_failure(self):
         """Test unexpected userauth failure response"""
 
-        with patch.dict('asyncssh.connection.SSHConnection._packet_handlers',
-                        {MSG_UNIMPLEMENTED: disconnect_on_unimplemented}):
-            conn = await self.connect()
-            conn.send_packet(MSG_USERAUTH_FAILURE, NameList([]), 
Boolean(False))
-            await conn.wait_closed()
+        conn = await self.connect()
+        conn.send_packet(MSG_USERAUTH_FAILURE, NameList([]), Boolean(False))
+        await conn.wait_closed()
 
     @asynctest
     async def test_unexpected_userauth_banner(self):
@@ -2276,8 +2373,7 @@
         """Test using an SSH listener as a context manager"""
 
         async with self.listen() as server:
-            listen_port = server.sockets[0].getsockname()[1]
-
+            listen_port = server.get_port()
 
             async with asyncssh.connect('127.0.0.1', listen_port,
                                         known_hosts=(['skey.pub'], [], [])):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/tests/test_forward.py 
new/asyncssh-2.14.1/tests/test_forward.py
--- old/asyncssh-2.14.0/tests/test_forward.py   2023-10-01 02:35:42.000000000 
+0200
+++ new/asyncssh-2.14.1/tests/test_forward.py   2023-11-09 03:28:14.000000000 
+0100
@@ -422,8 +422,10 @@
 
         async with self.connect() as conn:
             async with conn.listen_ssh(port=0, server_factory=Server,
-                                       server_host_keys=['skey']) as server2:
-                listen_port = server2.get_port()
+                                       server_host_keys=['skey']) as server:
+                listen_port = server.get_port()
+
+                self.assertEqual(server.get_addresses(), [('', listen_port)])
 
                 async with asyncssh.connect('127.0.0.1', listen_port,
                                             known_hosts=(['skey.pub'], [], 
[])):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/tests/test_stream.py 
new/asyncssh-2.14.1/tests/test_stream.py
--- old/asyncssh-2.14.0/tests/test_stream.py    2022-12-27 22:30:36.000000000 
+0100
+++ new/asyncssh-2.14.1/tests/test_stream.py    2023-11-09 03:28:14.000000000 
+0100
@@ -21,6 +21,7 @@
 """Unit tests for AsyncSSH stream API"""
 
 import asyncio
+import re
 
 import asyncssh
 
@@ -392,6 +393,27 @@
             stdin.close()
 
     @asynctest
+    async def test_readuntil_regex(self):
+        """Test readuntil with a regex pattern"""
+
+        async with self.connect() as conn:
+            stdin, stdout, _ = await conn.open_session()
+            stdin.write("hello world\nhello world")
+            output = await stdout.readuntil(
+                re.compile('hello world'), len('hello world')
+            )
+            self.assertEqual(output, "hello world")
+
+            output = await stdout.readuntil(
+                re.compile('hello world'), len('hello world')
+            )
+            self.assertEqual(output, "\nhello world")
+
+            stdin.close()
+
+        await conn.wait_closed()
+
+    @asynctest
     async def test_abort(self):
         """Test abort on a channel"""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asyncssh-2.14.0/tox.ini new/asyncssh-2.14.1/tox.ini
--- old/asyncssh-2.14.0/tox.ini 2023-10-01 02:35:42.000000000 +0200
+++ new/asyncssh-2.14.1/tox.ini 2023-11-09 03:28:14.000000000 +0100
@@ -4,7 +4,7 @@
 envlist =
     clean
     report
-    py3{7,8,9,10,11}-{linux,darwin,windows}
+    py3{7,8,9,10,11,12}-{linux,darwin,windows}
 
 [testenv]
 deps =
@@ -53,7 +53,7 @@
     coverage html
     coverage xml
 depends =
-    py3{7,8,9,10,11}-{linux,darwin,windows}
+    py3{7,8,9,10,11,12}-{linux,darwin,windows}
 
 [pytest]
 testpaths = tests

Reply via email to