diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 77d719d1..3c77a21a 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -20,9 +20,9 @@ jobs: steps: - name: Clone Repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f230e330..18b9671f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,11 +18,11 @@ jobs: steps: - name: Clone Repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3d3f1ef..633d362d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,9 +22,9 @@ jobs: steps: - name: Clone Repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: ${{ matrix.os }} SSH diff --git a/docs/changes.rst b/docs/changes.rst index 4feeff51..ddcc6436 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,5 +1,13 @@ -1.2.1 (current, released 2026-2-11) +1.2.2 (current, released 2026-5-10) ----------------------------------- + * adding new test for _sftp_channel exception handling. + * adding curve25519-sha256@libssh.org to kex list. + * fix for UnboundLocalError on a certain exception in _sftp_channel. + * removing diffie-hellman-group-exchange-sha1 from kex list per paramiko. + * removing ssh-rsa from public key type list per paramiko. + +1.2.1 (released 2026-2-11) +-------------------------- * adding boolean to rename for switch between posix and standard behavior. * change in default path behavior where cwd is set when default path is not. * update drivedrop to better handle all non-UNC Windows path possibilities. diff --git a/docs/conf.py b/docs/conf.py index 459455df..3f918a10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = '1.2.1' +version = '1.2.2' # The full version, including alpha/beta/rc tags. -release = '1.2.1' +release = '1.2.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyproject.toml b/pyproject.toml index 5f917831..5e7202a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ keywords = [ name = 'sftpretty' readme = 'README.rst' requires-python = '>=3.6' -version = '1.2.1' +version = '1.2.2' [project.scripts] sftpretty = 'sftpretty:Connection' diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index c566b5c6..2e34ae25 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -74,13 +74,14 @@ def __init__(self, config=None, knownhosts=Path( 'hmac-sha1', 'hmac-md5') self.disabled_algorithms = {} self.hostkeys = hostkeys.HostKeys() - self.kex = ('ecdh-sha2-nistp521', 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp256', 'diffie-hellman-group16-sha512', + self.kex = ('curve25519-sha256@libssh.org', 'ecdh-sha2-nistp521', + 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp256', + 'diffie-hellman-group16-sha512', 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group-exchange-sha1') + 'diffie-hellman-group14-sha256') self.key_types = ('ssh-ed25519', 'ecdsa-sha2-nistp521', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp256', - 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa') + 'rsa-sha2-512', 'rsa-sha2-256') self.log = False self.log_level = 'info' self.ssh_config = SSHConfig() @@ -300,24 +301,26 @@ def _sftp_channel(self): channel = None fatal = False + self._cache.__dict__.setdefault('cwd', self._default_path) + try: channel_name, data = next( (key, value) for key, value in self._channels.items() - if not value['busy'] + if not value['busy'] and not value['meta'].closed ) + + channel = data['channel'] meta = data['meta'] - if not meta.closed: - channel = data['channel'] - self._channels[channel_name]['busy'] = True - log.debug(f'Cached Channel: [{channel_name}]') + self._channels[channel_name]['busy'] = True + log.debug(f'Cached Channel: [{channel_name}]') except StopIteration: pass try: if channel is None: - channel = SFTPClient.from_transport(self._transport) channel_name = uuid4().hex + channel = SFTPClient.from_transport(self._transport) meta = channel.get_channel() meta.set_name(channel_name) log.debug(f'Channel Name: [{channel_name}]') @@ -326,7 +329,6 @@ def _sftp_channel(self): } meta.settimeout(self._timeout) - self._cache.__dict__.setdefault('cwd', self._default_path) if self._cache.cwd is None: self._cache.cwd = drivedrop(channel.normalize('.')) @@ -344,6 +346,7 @@ def _sftp_channel(self): log.error(_message) raise TimeoutError(_message) except SFTPError as err: + code = err.args[0] if err.args else None _message_map = { SFTP_FAILURE: ( 'A generic failure occurred on the SFTP server for path: ' @@ -361,9 +364,9 @@ def _sftp_channel(self): ), } _message = _message_map.get( - err.errno, + code, ('Unhandled SFTP error on directory change to ' - f'[{self._cache.cwd}] (Code {err.errno}): {err}') + f'[{self._cache.cwd}] (Code {code}): {err}') ) log.error(_message) raise err diff --git a/tests/test_connection.py b/tests/test_connection.py index 93b04409..1c0feb64 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -2,6 +2,7 @@ import pytest +from paramiko import SFTPError from paramiko.ed25519key import Ed25519Key from common import conn, LOCAL, VFS @@ -10,6 +11,38 @@ HostKeysException, SSHException) +def test_channel_exception(sftpserver): + '''test except blocks in _sftp_channel don't raise secondary errors''' + with sftpserver.serve_content(VFS): + with Connection(**conn(sftpserver)) as sftp: + sftp.close() + with pytest.raises(AttributeError): + sftp.listdir() + + with sftpserver.serve_content(VFS): + with Connection(**conn(sftpserver)) as sftp: + with pytest.raises(OSError): + sftp.chdir('/does/not/exist') + + with sftpserver.serve_content(VFS): + with Connection(**conn(sftpserver)) as sftp: + with pytest.raises(SFTPError): + sftp.chdir('/home/test/read.me') + + with sftpserver.serve_content(VFS): + sftp = Connection(**conn(sftpserver)) + sftp._transport.close() + with pytest.raises(SSHException): + sftp.listdir() + + with sftpserver.serve_content(VFS): + with Connection(**conn(sftpserver)) as sftp: + sftp.timeout = 0.0001 + with pytest.raises(TimeoutError, + match='operation timed out after'): + sftp.listdir() + + def test_cnopts_bad_knownhosts(): '''test setting knownhosts to a not understood file''' with pytest.raises(HostKeysException):