Hello community,
here is the log from the commit of package python-asyncssh for openSUSE:Factory checked in at 2019-08-09 16:53:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asyncssh (Old)
and /work/SRC/openSUSE:Factory/.python-asyncssh.new.9556 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asyncssh"
Fri Aug 9 16:53:50 2019 rev:8 rq:721769 version:1.17.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asyncssh/python-asyncssh.changes 2019-07-04 15:44:46.154255491 +0200
+++ /work/SRC/openSUSE:Factory/.python-asyncssh.new.9556/python-asyncssh.changes 2019-08-09 16:53:55.785460852 +0200
@@ -1,0 +2,24 @@
+Thu Aug 8 12:49:50 UTC 2019 - Ondřej Súkup
+
+- update to 1.17.1
+ * Improved construction of file paths in SFTP to better handle native Windows
+ source paths containing backslashes or drive letters.
+ * Improved SFTP parallel I/O for large reads and file copies to better handle
+ the case where a read returns less data than what was requested when not
+ at the end of the file, allowing AsyncSSH to get back the right result even
+ if the requested block size is larger than the SFTP server can handle.
+ * Fixed an issue where the requested SFTP block_size wasn’t used in the get,
+ copy, mget, and mcopy functions if it was larger than the default size of 16 KB.
+ * Fixed a problem where the list of client keys provided in
+ an SSHClientConnectionOptions object wasn’t always preserved properly across
+ the opening of multiple SSH connections.
+ * Made AsyncSSH tolerant of unexpected authentication success/failure messages
+ sent after authentication completes. AsyncSSH previously treated this as
+ a protocol error and dropped the connection, while most other SSH implementations
+ ignored these messages and allowed the connection to continue.
+ * Made AsyncSSH tolerant of SFTP status responses which are missing error message
+ and language tag fields, improving interoperability with servers that omit
+ these fields. When missing, AsyncSSH treats these fields as if they were
+ set to empty strings.
+
+-------------------------------------------------------------------
Old:
----
asyncssh-1.17.0.tar.gz
New:
----
asyncssh-1.17.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-asyncssh.spec ++++++
--- /var/tmp/diff_new_pack.FplzXE/_old 2019-08-09 16:53:56.221460747 +0200
+++ /var/tmp/diff_new_pack.FplzXE/_new 2019-08-09 16:53:56.221460747 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-asyncssh
-Version: 1.17.0
+Version: 1.17.1
Release: 0
Summary: Asynchronous SSHv2 client and server library
License: EPL-2.0 OR GPL-2.0-or-later
++++++ asyncssh-1.17.0.tar.gz -> asyncssh-1.17.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/PKG-INFO new/asyncssh-1.17.1/PKG-INFO
--- old/asyncssh-1.17.0/PKG-INFO 2019-06-01 06:59:51.000000000 +0200
+++ new/asyncssh-1.17.1/PKG-INFO 2019-07-24 08:26:22.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 1.17.0
+Version: 1.17.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
@@ -219,8 +219,8 @@
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
-Provides-Extra: gssapi
Provides-Extra: libnacl
+Provides-Extra: gssapi
Provides-Extra: bcrypt
Provides-Extra: pyOpenSSL
Provides-Extra: pypiwin32
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh/agent.py new/asyncssh-1.17.1/asyncssh/agent.py
--- old/asyncssh-1.17.0/asyncssh/agent.py 2019-06-01 06:34:50.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh/agent.py 2019-07-24 07:54:36.000000000 +0200
@@ -204,8 +204,16 @@
self._reader, self._writer = \
yield from self._agent_path.open_agent_connection()
else:
- self._reader, self._writer = \
- yield from open_agent(self._loop, self._agent_path)
+ agent_path = self._agent_path
+
+ try:
+ self._reader, self._writer = \
+ yield from open_agent(self._loop, agent_path)
+ except OSError as exc:
+ if agent_path:
+ logger.warning('Unable to contact agent: %s', exc)
+
+ raise
@asyncio.coroutine
def _make_request(self, msgtype, *args):
@@ -571,6 +579,9 @@
"""
+ if not agent_path:
+ agent_path = os.environ.get('SSH_AUTH_SOCK', None)
+
agent = SSHAgentClient(loop, agent_path)
try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh/agent_unix.py new/asyncssh-1.17.1/asyncssh/agent_unix.py
--- old/asyncssh-1.17.0/asyncssh/agent_unix.py 2019-03-31 04:50:15.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh/agent_unix.py 2019-07-24 07:54:36.000000000 +0200
@@ -22,7 +22,6 @@
import asyncio
import errno
-import os
@asyncio.coroutine
@@ -33,9 +32,6 @@
loop = asyncio.get_event_loop()
if not agent_path:
- agent_path = os.environ.get('SSH_AUTH_SOCK', None)
-
- if not agent_path:
- raise OSError(errno.ENOENT, 'Agent not found')
+ raise OSError(errno.ENOENT, 'Agent not found')
return (yield from asyncio.open_unix_connection(agent_path, loop=loop))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh/agent_win32.py new/asyncssh-1.17.1/asyncssh/agent_win32.py
--- old/asyncssh-1.17.0/asyncssh/agent_win32.py 2019-05-29 04:54:23.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh/agent_win32.py 2019-07-24 07:54:36.000000000 +0200
@@ -27,7 +27,6 @@
import ctypes
import ctypes.wintypes
import errno
-import os
try:
import mmapfile
@@ -162,14 +161,11 @@
transport = None
if not agent_path:
- agent_path = os.environ.get('SSH_AUTH_SOCK', None)
-
- if not agent_path:
- try:
- _find_agent_window()
- transport = _PageantTransport()
- except OSError:
- agent_path = _DEFAULT_OPENSSH_PATH
+ try:
+ _find_agent_window()
+ transport = _PageantTransport()
+ except OSError:
+ agent_path = _DEFAULT_OPENSSH_PATH
if not transport:
transport = _W10OpenSSHTransport(agent_path)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh/connection.py new/asyncssh-1.17.1/asyncssh/connection.py
--- old/asyncssh-1.17.0/asyncssh/connection.py 2019-06-01 06:34:50.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh/connection.py 2019-07-24 07:54:36.000000000 +0200
@@ -1698,7 +1698,8 @@
# pylint: disable=no-member
self.try_next_auth()
else:
- raise ProtocolError('Unexpected userauth response')
+ self.logger.debug2('Unexpected userauth failure response')
+ self.send_packet(MSG_UNIMPLEMENTED, UInt32(pktid))
def _process_userauth_success(self, pkttype, pktid, packet):
"""Process a user authentication success response"""
@@ -1738,7 +1739,8 @@
self._waiter.set_result(None)
self._wait = None
else:
- raise ProtocolError('Unexpected userauth response')
+ self.logger.debug2('Unexpected userauth success response')
+ self.send_packet(MSG_UNIMPLEMENTED, UInt32(pktid))
def _process_userauth_banner(self, pkttype, pktid, packet):
"""Process a user authentication banner message"""
@@ -2387,10 +2389,12 @@
self._password = options.password
self._client_host_keysign = options.client_host_keysign
- self._client_host_keys = options.client_host_keys
+ self._client_host_keys = None if options.client_host_keys is None else \
+ list(options.client_host_keys)
self._client_host = options.client_host
self._client_username = options.client_username
- self._client_keys = options.client_keys
+ self._client_keys = None if options.client_keys is None else \
+ list(options.client_keys)
if options.agent_path is not None:
self._agent = SSHAgentClient(self._loop, options.agent_path)
@@ -2602,8 +2606,8 @@
try:
agent_keys = yield from self._agent.get_keys()
self._client_keys = agent_keys + (self._client_keys or [])
- except ValueError as exc:
- logger.warning('Unable to read keys from agent: %s', exc)
+ except ValueError:
+ pass
self._get_agent_keys = False
@@ -5195,15 +5199,15 @@
:param client_keys: (optional)
A list of keys which will be used to authenticate this client
via public key authentication. If no client keys are specified,
- an attempt will be made to get them from an ssh-agent process.
- If that is not available, an attempt will be made to load them
- from the files :file:`.ssh/id_ed25519`, :file:`.ssh/id_ecdsa`,
- :file:`.ssh/id_rsa`, and :file:`.ssh/id_dsa` in the user's home
- directory, with optional certificates loaded from the files
- :file:`.ssh/id_ed25519-cert.pub`, :file:`.ssh/id_ecdsa-cert.pub`,
- :file:`.ssh/id_rsa-cert.pub`, and :file:`.ssh/id_dsa-cert.pub`.
- If this argument is explicitly set to `None`, client public
- key authentication will not be performed.
+ an attempt will be made to get them from an ssh-agent process
+ and/or load them from the files :file:`.ssh/id_ed25519`,
+ :file:`.ssh/id_ecdsa`, :file:`.ssh/id_rsa`, and :file:`.ssh/id_dsa`
+ in the user's home directory, with optional certificates loaded
+ from the files :file:`.ssh/id_ed25519-cert.pub`,
+ :file:`.ssh/id_ecdsa-cert.pub`, :file:`.ssh/id_rsa-cert.pub`,
+ and :file:`.ssh/id_dsa-cert.pub`. If this argument is explicitly
+ set to `None`, client public key authentication will not be
+ performed.
:param passphrase: (optional)
The passphrase to use to decrypt client keys when loading them,
if they are encrypted. If this is not specified, only unencrypted
@@ -5365,16 +5369,18 @@
self.gss_delegate_creds = gss_delegate_creds
if agent_path == ():
- agent_path = os.environ.get('SSH_AUTH_SOCK', ())
+ agent_path = os.environ.get('SSH_AUTH_SOCK', None)
self.agent_path = None
if client_keys:
client_keys = load_keypairs(client_keys, passphrase)
- elif client_keys == ():
- self.agent_path = agent_path
+ else:
+ if client_keys is not None:
+ self.agent_path = agent_path
- client_keys = load_default_keypairs(passphrase)
+ if client_keys == ():
+ client_keys = load_default_keypairs(passphrase)
self.agent_forward_path = agent_path if agent_forwarding else None
self.client_keys = client_keys
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh/sftp.py new/asyncssh-1.17.1/asyncssh/sftp.py
--- old/asyncssh-1.17.0/asyncssh/sftp.py 2019-06-01 06:34:50.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh/sftp.py 2019-07-24 07:54:36.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright (c) 2015-2018 by Ron Frederick and others.
+# Copyright (c) 2015-2019 by Ron Frederick and others.
#
# This program and the accompanying materials are made available under
# the terms of the Eclipse Public License v2.0 which accompanies this
@@ -254,6 +254,12 @@
self._file = f
@classmethod
+ def basename(cls, path):
+ """Return the final component of a local file path"""
+
+ return os.path.basename(path)
+
+ @classmethod
def encode(cls, path):
"""Encode path name using filesystem native encoding
@@ -288,9 +294,11 @@
@classmethod
@asyncio.coroutine
- def open(cls, path, *args):
+ def open(cls, path, *args, block_size=None):
"""Open a local file"""
+ # pylint: disable=unused-argument
+
return cls(open(_to_local_path(path), *args))
@classmethod
@@ -484,14 +492,20 @@
def run_task(self, offset, size):
"""Read a block of the file"""
- data = yield from self._handler.read(self._handle, offset, size)
- pos = offset - self._start
- pad = pos - len(self._data)
+ while size:
+ data = yield from self._handler.read(self._handle, offset, size)
- if pad > 0:
- self._data += pad * b'\0'
+ pos = offset - self._start
+ pad = pos - len(self._data)
- self._data[pos:pos+size] = data
+ if pad > 0:
+ self._data += pad * b'\0'
+
+ datalen = len(data)
+ self._data[pos:pos+datalen] = data
+
+ offset += datalen
+ size -= datalen
@asyncio.coroutine
def finish(self):
@@ -550,20 +564,38 @@
def start(self):
"""Start parallel copy"""
- self._src = yield from self._srcfs.open(self._srcpath, 'rb')
- self._dst = yield from self._dstfs.open(self._dstpath, 'wb')
+ self._src = yield from self._srcfs.open(self._srcpath, 'rb',
+ block_size=None)
+ self._dst = yield from self._dstfs.open(self._dstpath, 'wb',
+ block_size=None)
@asyncio.coroutine
def run_task(self, offset, size):
"""Copy the next block of the file"""
- data = yield from self._src.read(size, offset)
- yield from self._dst.write(data, offset)
+ while size:
+ data = yield from self._src.read(size, offset)
+
+ if not data:
+ exc = SFTPError(FX_FAILURE, 'Unexpected EOF during file copy')
+
+ # pylint: disable=attribute-defined-outside-init
+ exc.filename = self._srcpath
+ exc.offset = offset
+
+ raise exc
+
+ yield from self._dst.write(data, offset)
- if self._progress_handler:
- self._bytes_copied += size
- self._progress_handler(self._srcpath, self._dstpath,
- self._bytes_copied, self._total_bytes)
+ datalen = len(data)
+
+ if self._progress_handler:
+ self._bytes_copied += datalen
+ self._progress_handler(self._srcpath, self._dstpath,
+ self._bytes_copied, self._total_bytes)
+
+ offset += datalen
+ size -= datalen
@asyncio.coroutine
def cleanup(self):
@@ -1039,11 +1071,20 @@
code = packet.get_uint32()
- try:
- reason = packet.get_string().decode('utf-8')
- lang = packet.get_string().decode('ascii')
- except UnicodeDecodeError:
- raise SFTPError(FX_BAD_MESSAGE, 'Invalid status message') from None
+ if packet:
+ try:
+ reason = packet.get_string().decode('utf-8')
+ lang = packet.get_string().decode('ascii')
+ except UnicodeDecodeError:
+ raise SFTPError(FX_BAD_MESSAGE,
+ 'Invalid status message') from None
+ else:
+ # Some servers may not always send reason and lang (usually
+ # when responding with FX_OK). Tolerate this, automatically
+ # filling in empty strings for them if they're not present.
+
+ reason = ''
+ lang = ''
packet.check_end()
@@ -1887,6 +1928,13 @@
return self._handler.logger
+ def basename(self, path):
+ """Return the final component of a POSIX-style path"""
+
+ # pylint: disable=no-self-use
+
+ return posixpath.basename(path)
+
def encode(self, path):
"""Encode path name using configured path encoding
@@ -2070,7 +2118,7 @@
for srcfile in srcpaths:
srcfile = srcfs.encode(srcfile)
- filename = posixpath.basename(srcfile)
+ filename = srcfs.basename(srcfile)
if dstpath is None:
dstfile = filename
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh/version.py new/asyncssh-1.17.1/asyncssh/version.py
--- old/asyncssh-1.17.0/asyncssh/version.py 2019-06-01 06:57:05.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh/version.py 2019-07-24 07:55:53.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright (c) 2013-2018 by Ron Frederick and others.
+# Copyright (c) 2013-2019 by Ron Frederick and others.
#
# This program and the accompanying materials are made available under
# the terms of the Eclipse Public License v2.0 which accompanies this
@@ -26,4 +26,4 @@
__url__ = 'http://asyncssh.timeheart.net'
-__version__ = '1.17.0'
+__version__ = '1.17.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/asyncssh.egg-info/PKG-INFO new/asyncssh-1.17.1/asyncssh.egg-info/PKG-INFO
--- old/asyncssh-1.17.0/asyncssh.egg-info/PKG-INFO 2019-06-01 06:59:51.000000000 +0200
+++ new/asyncssh-1.17.1/asyncssh.egg-info/PKG-INFO 2019-07-24 08:26:22.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asyncssh
-Version: 1.17.0
+Version: 1.17.1
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
Home-page: http://asyncssh.timeheart.net
Author: Ron Frederick
@@ -219,8 +219,8 @@
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
-Provides-Extra: gssapi
Provides-Extra: libnacl
+Provides-Extra: gssapi
Provides-Extra: bcrypt
Provides-Extra: pyOpenSSL
Provides-Extra: pypiwin32
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/tests/test_agent.py new/asyncssh-1.17.1/tests/test_agent.py
--- old/asyncssh-1.17.0/tests/test_agent.py 2019-03-31 04:50:15.000000000 +0200
+++ new/asyncssh-1.17.1/tests/test_agent.py 2019-07-24 07:54:36.000000000 +0200
@@ -89,7 +89,7 @@
os.remove(self._path)
-class _TestAPI(AsyncTestCase):
+class _TestAgent(AsyncTestCase):
"""Unit tests for AsyncSSH API"""
_agent_pid = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/tests/test_connection.py new/asyncssh-1.17.1/tests/test_connection.py
--- old/asyncssh-1.17.0/tests/test_connection.py 2019-06-01 06:34:50.000000000 +0200
+++ new/asyncssh-1.17.1/tests/test_connection.py 2019-07-23 04:31:59.000000000 +0200
@@ -28,7 +28,7 @@
from unittest.mock import patch
import asyncssh
-from asyncssh.constants import MSG_DEBUG
+from asyncssh.constants import MSG_UNIMPLEMENTED, 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_USERAUTH_REQUEST, MSG_USERAUTH_SUCCESS
@@ -313,6 +313,14 @@
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
class _TestConnection(ServerTestCase):
"""Unit tests for AsyncSSH connection API"""
@@ -1098,21 +1106,25 @@
def test_unexpected_userauth_success(self):
"""Test unexpected userauth success response"""
- conn = yield from self.connect()
+ with patch.dict('asyncssh.connection.SSHConnection._packet_handlers',
+ {MSG_UNIMPLEMENTED: disconnect_on_unimplemented}):
+ conn = yield from self.connect()
- conn.send_packet(MSG_USERAUTH_SUCCESS)
+ conn.send_packet(MSG_USERAUTH_SUCCESS)
- yield from conn.wait_closed()
+ yield from conn.wait_closed()
@asynctest
def test_unexpected_userauth_failure(self):
"""Test unexpected userauth failure response"""
- conn = yield from self.connect()
+ with patch.dict('asyncssh.connection.SSHConnection._packet_handlers',
+ {MSG_UNIMPLEMENTED: disconnect_on_unimplemented}):
+ conn = yield from self.connect()
- conn.send_packet(MSG_USERAUTH_FAILURE, NameList([]), Boolean(False))
+ conn.send_packet(MSG_USERAUTH_FAILURE, NameList([]), Boolean(False))
- yield from conn.wait_closed()
+ yield from conn.wait_closed()
@asynctest
def test_unexpected_userauth_banner(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/tests/test_connection_auth.py new/asyncssh-1.17.1/tests/test_connection_auth.py
--- old/asyncssh-1.17.0/tests/test_connection_auth.py 2019-05-11 17:06:42.000000000 +0200
+++ new/asyncssh-1.17.1/tests/test_connection_auth.py 2019-07-23 04:31:59.000000000 +0200
@@ -1227,6 +1227,26 @@
yield from conn.wait_closed()
+ @asynctest
+ def test_auth_options_reuse(self):
+ """Test public key auth via SSHClientConnectionOptions"""
+
+ def connect():
+ """Connect to the server using options"""
+
+ with (yield from asyncssh.connect(self._server_addr,
+ self._server_port,
+ loop=self.loop,
+ options=options)) as conn:
+ pass
+
+ yield from conn.wait_closed()
+
+ options = asyncssh.SSHClientConnectionOptions(
+ username='ckey', client_keys=[('ckey', None)], gss_host=None)
+
+ yield from asyncio.gather(connect(), connect())
+
class _TestPublicKeyAsyncServerAuth(_TestPublicKeyAuth):
"""Unit tests for public key authentication with async server callbacks"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asyncssh-1.17.0/tests/test_sftp.py new/asyncssh-1.17.1/tests/test_sftp.py
--- old/asyncssh-1.17.0/tests/test_sftp.py 2019-06-01 06:34:50.000000000 +0200
+++ new/asyncssh-1.17.1/tests/test_sftp.py 2019-07-23 04:31:59.000000000 +0200
@@ -1,4 +1,4 @@
-# Copyright (c) 2015-2018 by Ron Frederick and others.
+# Copyright (c) 2015-2019 by Ron Frederick and others.
#
# This program and the accompanying materials are made available under
# the terms of the Eclipse Public License v2.0 which accompanies this
@@ -190,6 +190,28 @@
super().write(file_obj, offset, data)
+class _SmallBlockSizeSFTPServer(SFTPServer):
+ """Limit reads to a small block size"""
+
+ @asyncio.coroutine
+ def read(self, file_obj, offset, size):
+ """Limit reads to return no more than 4 KB at a time"""
+
+ return super().read(file_obj, offset, min(size, 4096))
+
+
+class _TruncateSFTPServer(SFTPServer):
+ """Truncate a file when it is accessed, simulating a simultaneous writer"""
+
+ @asyncio.coroutine
+ def read(self, file_obj, offset, size):
+ """Truncate a file to 32 KB when a read is done"""
+
+ os.truncate('src', 32768)
+
+ return super().read(file_obj, offset, size)
+
+
class _NotImplSFTPServer(SFTPServer):
"""Return an error that a request is not implemented"""
@@ -475,7 +497,9 @@
if data == ():
data = str(id(self))
- with open(name, 'w') as f:
+ binary = 'b' if isinstance(data, bytes) else ''
+
+ with open(name, 'w' + binary) as f:
f.write(data)
if mode is not None:
@@ -506,8 +530,8 @@
if preserve:
self._check_attr(name1, name2, follow_symlinks, check_atime)
- with open(name1) as file1:
- with open(name2) as file2:
+ with open(name1, 'rb') as file1:
+ with open(name2, 'rb') as file2:
self.assertEqual(file1.read(), file2.read())
def _check_stat(self, sftp_stat, local_stat):
@@ -2173,6 +2197,22 @@
yield from sftp.open('file')
@sftp_test
+ def test_short_ok_response(self, sftp):
+ """Test sending an FX_OK response without a reason and lang"""
+
+ @asyncio.coroutine
+ def _short_ok_response(self, pkttype, pktid, packet):
+ """Send an FX_OK response missing reason and lang"""
+
+ # pylint: disable=unused-argument
+
+ self.send_packet(FXP_STATUS, pktid, UInt32(pktid), UInt32(FX_OK))
+
+ with patch('asyncssh.sftp.SFTPServerHandler._process_packet',
+ _short_ok_response):
+ self.assertIsNone((yield from sftp.mkdir('dir')))
+
+ @sftp_test
def test_malformed_realpath_response(self, sftp):
"""Test receiving malformed realpath response"""
@@ -2490,6 +2530,68 @@
remove('file')
+class _TestSFTPSmallBlockSize(_CheckSFTP):
+ """Unit test for SFTP server returning file I/O error"""
+
+ @classmethod
+ @asyncio.coroutine
+ def start_server(cls):
+ """Start an SFTP server which returns file I/O errors"""
+
+ return (yield from cls.create_server(
+ sftp_factory=_SmallBlockSizeSFTPServer))
+
+ @sftp_test
+ def test_read(self, sftp):
+ """Test a large read on a server with a small block size"""
+
+ try:
+ data = os.urandom(65536)
+ self._create_file('file', data)
+
+ with (yield from sftp.open('file', 'rb')) as f:
+ result = yield from f.read(32768, 16384)
+
+ self.assertEqual(result, data[16384:49152])
+ finally:
+ remove('file')
+
+ @sftp_test
+ def test_get(self, sftp):
+ """Test getting a file from an SFTP server with a small block size"""
+
+ try:
+ data = os.urandom(65536)
+ self._create_file('src', data)
+ yield from sftp.get('src', 'dst')
+ self._check_file('src', 'dst')
+ finally:
+ remove('src dst')
+
+
+class _TestSFTPEOFDuringCopy(_CheckSFTP):
+ """Unit test for SFTP server returning EOF during a file copy"""
+
+ @classmethod
+ @asyncio.coroutine
+ def start_server(cls):
+ """Start an SFTP server which truncates files when accessed"""
+
+ return (yield from cls.create_server(sftp_factory=_TruncateSFTPServer))
+
+ @sftp_test
+ def test_get(self, sftp):
+ """Test getting a file from an SFTP server truncated during the copy"""
+
+ try:
+ self._create_file('src', 65536*'\0')
+
+ with self.assertRaises(SFTPError):
+ yield from sftp.get('src', 'dst')
+ finally:
+ remove('src dst')
+
+
class _TestSFTPNotImplemented(_CheckSFTP):
"""Unit test for SFTP server returning not-implemented error"""