asyncssh 2.22.0__tar.gz → 2.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. {asyncssh-2.22.0/asyncssh.egg-info → asyncssh-2.23.0}/PKG-INFO +11 -7
  2. {asyncssh-2.22.0 → asyncssh-2.23.0}/README.rst +8 -4
  3. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/agent.py +1 -3
  4. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/config.py +40 -4
  5. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/connection.py +53 -26
  6. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/pq.py +5 -3
  7. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/encryption.py +26 -2
  8. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/kex.py +1 -1
  9. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/misc.py +1 -1
  10. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/scp.py +2 -1
  11. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/server.py +4 -0
  12. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/sftp.py +22 -17
  13. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/sk.py +1 -1
  14. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/version.py +1 -1
  15. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/x11.py +11 -9
  16. {asyncssh-2.22.0 → asyncssh-2.23.0/asyncssh.egg-info}/PKG-INFO +11 -7
  17. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh.egg-info/requires.txt +2 -2
  18. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/api.rst +1 -15
  19. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/changes.rst +40 -0
  20. {asyncssh-2.22.0 → asyncssh-2.23.0}/pyproject.toml +1 -1
  21. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_config.py +21 -1
  22. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_connection.py +30 -2
  23. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_forward.py +24 -0
  24. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_kex.py +6 -0
  25. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_process.py +17 -22
  26. {asyncssh-2.22.0 → asyncssh-2.23.0}/tox.ini +1 -1
  27. {asyncssh-2.22.0 → asyncssh-2.23.0}/.coveragerc +0 -0
  28. {asyncssh-2.22.0 → asyncssh-2.23.0}/.github/workflows/run_tests.yml +0 -0
  29. {asyncssh-2.22.0 → asyncssh-2.23.0}/.gitignore +0 -0
  30. {asyncssh-2.22.0 → asyncssh-2.23.0}/.readthedocs.yaml +0 -0
  31. {asyncssh-2.22.0 → asyncssh-2.23.0}/CONTRIBUTING.rst +0 -0
  32. {asyncssh-2.22.0 → asyncssh-2.23.0}/COPYRIGHT +0 -0
  33. {asyncssh-2.22.0 → asyncssh-2.23.0}/LICENSE +0 -0
  34. {asyncssh-2.22.0 → asyncssh-2.23.0}/MANIFEST.in +0 -0
  35. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/__init__.py +0 -0
  36. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/agent_unix.py +0 -0
  37. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/agent_win32.py +0 -0
  38. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/asn1.py +0 -0
  39. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/auth.py +0 -0
  40. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/auth_keys.py +0 -0
  41. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/channel.py +0 -0
  42. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/client.py +0 -0
  43. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/compression.py +0 -0
  44. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/constants.py +0 -0
  45. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/__init__.py +0 -0
  46. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/chacha.py +0 -0
  47. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/cipher.py +0 -0
  48. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/dh.py +0 -0
  49. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/dsa.py +0 -0
  50. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/ec.py +0 -0
  51. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/ec_params.py +0 -0
  52. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/ed.py +0 -0
  53. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/kdf.py +0 -0
  54. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/misc.py +0 -0
  55. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/rsa.py +0 -0
  56. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/umac.py +0 -0
  57. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/crypto/x509.py +0 -0
  58. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/dsa.py +0 -0
  59. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/ecdsa.py +0 -0
  60. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/eddsa.py +0 -0
  61. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/editor.py +0 -0
  62. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/forward.py +0 -0
  63. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/gss.py +0 -0
  64. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/gss_unix.py +0 -0
  65. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/gss_win32.py +0 -0
  66. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/kex_dh.py +0 -0
  67. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/kex_rsa.py +0 -0
  68. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/keysign.py +0 -0
  69. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/known_hosts.py +0 -0
  70. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/listener.py +0 -0
  71. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/logging.py +0 -0
  72. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/mac.py +0 -0
  73. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/packet.py +0 -0
  74. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/pattern.py +0 -0
  75. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/pbe.py +0 -0
  76. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/pkcs11.py +0 -0
  77. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/process.py +0 -0
  78. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/public_key.py +0 -0
  79. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/py.typed +0 -0
  80. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/rsa.py +0 -0
  81. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/saslprep.py +0 -0
  82. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/session.py +0 -0
  83. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/sk_ecdsa.py +0 -0
  84. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/sk_eddsa.py +0 -0
  85. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/socks.py +0 -0
  86. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/stream.py +0 -0
  87. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/subprocess.py +0 -0
  88. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh/tuntap.py +0 -0
  89. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh.egg-info/SOURCES.txt +0 -0
  90. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh.egg-info/dependency_links.txt +0 -0
  91. {asyncssh-2.22.0 → asyncssh-2.23.0}/asyncssh.egg-info/top_level.txt +0 -0
  92. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/_templates/sidebarbottom.html +0 -0
  93. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/_templates/sidebartop.html +0 -0
  94. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/conf.py +0 -0
  95. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/contributing.rst +0 -0
  96. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/index.rst +0 -0
  97. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/requirements.txt +0 -0
  98. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/rftheme/layout.html +0 -0
  99. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/rftheme/static/rftheme.css_t +0 -0
  100. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/rftheme/theme.conf +0 -0
  101. {asyncssh-2.22.0 → asyncssh-2.23.0}/docs/rtd-req.txt +0 -0
  102. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/callback_client.py +0 -0
  103. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/callback_client2.py +0 -0
  104. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/callback_client3.py +0 -0
  105. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/callback_math_server.py +0 -0
  106. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/chat_server.py +0 -0
  107. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/check_exit_status.py +0 -0
  108. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/chroot_sftp_server.py +0 -0
  109. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/direct_client.py +0 -0
  110. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/direct_server.py +0 -0
  111. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/editor.py +0 -0
  112. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/gather_results.py +0 -0
  113. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/listening_client.py +0 -0
  114. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/local_forwarding_client.py +0 -0
  115. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/local_forwarding_client2.py +0 -0
  116. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/local_forwarding_server.py +0 -0
  117. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/math_client.py +0 -0
  118. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/math_server.py +0 -0
  119. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/redirect_input.py +0 -0
  120. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/redirect_local_pipe.py +0 -0
  121. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/redirect_remote_pipe.py +0 -0
  122. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/redirect_server.py +0 -0
  123. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/remote_forwarding_client.py +0 -0
  124. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/remote_forwarding_client2.py +0 -0
  125. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/remote_forwarding_server.py +0 -0
  126. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/reverse_client.py +0 -0
  127. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/reverse_server.py +0 -0
  128. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/scp_client.py +0 -0
  129. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/set_environment.py +0 -0
  130. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/set_terminal.py +0 -0
  131. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/sftp_client.py +0 -0
  132. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/show_environment.py +0 -0
  133. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/show_terminal.py +0 -0
  134. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/simple_cert_server.py +0 -0
  135. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/simple_client.py +0 -0
  136. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/simple_keyed_server.py +0 -0
  137. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/simple_scp_server.py +0 -0
  138. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/simple_server.py +0 -0
  139. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/simple_sftp_server.py +0 -0
  140. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/stream_direct_client.py +0 -0
  141. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/stream_direct_server.py +0 -0
  142. {asyncssh-2.22.0 → asyncssh-2.23.0}/examples/stream_listening_client.py +0 -0
  143. {asyncssh-2.22.0 → asyncssh-2.23.0}/mypy.ini +0 -0
  144. {asyncssh-2.22.0 → asyncssh-2.23.0}/pylintrc +0 -0
  145. {asyncssh-2.22.0 → asyncssh-2.23.0}/setup.cfg +0 -0
  146. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/__init__.py +0 -0
  147. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/gss_stub.py +0 -0
  148. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/gssapi_stub.py +0 -0
  149. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/keysign_stub.py +0 -0
  150. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/pkcs11_stub.py +0 -0
  151. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/server.py +0 -0
  152. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/sk_stub.py +0 -0
  153. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/sspi_stub.py +0 -0
  154. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_agent.py +0 -0
  155. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_asn1.py +0 -0
  156. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_auth.py +0 -0
  157. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_auth_keys.py +0 -0
  158. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_channel.py +0 -0
  159. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_compression.py +0 -0
  160. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_connection_auth.py +0 -0
  161. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_editor.py +0 -0
  162. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_encryption.py +0 -0
  163. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_known_hosts.py +0 -0
  164. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_logging.py +0 -0
  165. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_mac.py +0 -0
  166. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_packet.py +0 -0
  167. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_pkcs11.py +0 -0
  168. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_public_key.py +0 -0
  169. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_saslprep.py +0 -0
  170. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_sftp.py +0 -0
  171. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_sk.py +0 -0
  172. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_stream.py +0 -0
  173. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_subprocess.py +0 -0
  174. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_tuntap.py +0 -0
  175. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_x11.py +0 -0
  176. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/test_x509.py +0 -0
  177. {asyncssh-2.22.0 → asyncssh-2.23.0}/tests/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncssh
3
- Version: 2.22.0
3
+ Version: 2.23.0
4
4
  Summary: AsyncSSH: Asynchronous SSHv2 client and server library
5
5
  Author-email: Ron Frederick <ronf@timeheart.net>
6
6
  License: EPL-2.0 OR GPL-2.0-or-later
@@ -32,10 +32,10 @@ Provides-Extra: bcrypt
32
32
  Requires-Dist: bcrypt>=3.1.3; extra == "bcrypt"
33
33
  Provides-Extra: fido2
34
34
  Requires-Dist: fido2>=2; extra == "fido2"
35
+ Provides-Extra: ifaddr
36
+ Requires-Dist: ifaddr>=0.2.0; extra == "ifaddr"
35
37
  Provides-Extra: gssapi
36
38
  Requires-Dist: gssapi>=1.2.0; extra == "gssapi"
37
- Provides-Extra: libnacl
38
- Requires-Dist: libnacl>=1.4.2; extra == "libnacl"
39
39
  Provides-Extra: pkcs11
40
40
  Requires-Dist: python-pkcs11>=0.7.0; extra == "pkcs11"
41
41
  Provides-Extra: pyopenssl
@@ -196,6 +196,9 @@ functionality:
196
196
  * Install fido2 from https://pypi.org/project/fido2 if you want support
197
197
  for key exchange and authentication with U2F/FIDO2 security keys.
198
198
 
199
+ * Install ifaddr from https://pypi.org/project/ifaddr/ if you want
200
+ support for matching on local network IP addresses.
201
+
199
202
  * Install python-pkcs11 from https://pypi.org/project/python-pkcs11 if
200
203
  you want support for accessing PIV keys on PKCS#11 security tokens.
201
204
 
@@ -221,24 +224,25 @@ easy to install any or all of these dependencies:
221
224
 
222
225
  | bcrypt
223
226
  | fido2
227
+ | ifaddr
224
228
  | gssapi
225
229
  | pkcs11
226
230
  | pyOpenSSL
227
231
  | pywin32
228
232
 
229
- For example, to install bcrypt, fido2, gssapi, pkcs11, and pyOpenSSL
233
+ For example, to install bcrypt, fido2, gssapi, ifaddr, pkcs11, and pyOpenSSL
230
234
  on UNIX, you can run:
231
235
 
232
236
  ::
233
237
 
234
- pip install 'asyncssh[bcrypt,fido2,gssapi,pkcs11,pyOpenSSL]'
238
+ pip install 'asyncssh[bcrypt,fido2,gssapi,ifaddr,pkcs11,pyOpenSSL]'
235
239
 
236
- To install bcrypt, fido2, pkcs11, pyOpenSSL, and pywin32 on Windows,
240
+ To install bcrypt, fido2, ifaddr, pkcs11, pyOpenSSL, and pywin32 on Windows,
237
241
  you can run:
238
242
 
239
243
  ::
240
244
 
241
- pip install 'asyncssh[bcrypt,fido2,pkcs11,pyOpenSSL,pywin32]'
245
+ pip install 'asyncssh[bcrypt,fido2,ifaddr,pkcs11,pyOpenSSL,pywin32]'
242
246
 
243
247
  Note that you will still need to manually install the libnettle library
244
248
  for UMAC support. Unfortunately, since liboqs and libnettle are not
@@ -150,6 +150,9 @@ functionality:
150
150
  * Install fido2 from https://pypi.org/project/fido2 if you want support
151
151
  for key exchange and authentication with U2F/FIDO2 security keys.
152
152
 
153
+ * Install ifaddr from https://pypi.org/project/ifaddr/ if you want
154
+ support for matching on local network IP addresses.
155
+
153
156
  * Install python-pkcs11 from https://pypi.org/project/python-pkcs11 if
154
157
  you want support for accessing PIV keys on PKCS#11 security tokens.
155
158
 
@@ -175,24 +178,25 @@ easy to install any or all of these dependencies:
175
178
 
176
179
  | bcrypt
177
180
  | fido2
181
+ | ifaddr
178
182
  | gssapi
179
183
  | pkcs11
180
184
  | pyOpenSSL
181
185
  | pywin32
182
186
 
183
- For example, to install bcrypt, fido2, gssapi, pkcs11, and pyOpenSSL
187
+ For example, to install bcrypt, fido2, gssapi, ifaddr, pkcs11, and pyOpenSSL
184
188
  on UNIX, you can run:
185
189
 
186
190
  ::
187
191
 
188
- pip install 'asyncssh[bcrypt,fido2,gssapi,pkcs11,pyOpenSSL]'
192
+ pip install 'asyncssh[bcrypt,fido2,gssapi,ifaddr,pkcs11,pyOpenSSL]'
189
193
 
190
- To install bcrypt, fido2, pkcs11, pyOpenSSL, and pywin32 on Windows,
194
+ To install bcrypt, fido2, ifaddr, pkcs11, pyOpenSSL, and pywin32 on Windows,
191
195
  you can run:
192
196
 
193
197
  ::
194
198
 
195
- pip install 'asyncssh[bcrypt,fido2,pkcs11,pyOpenSSL,pywin32]'
199
+ pip install 'asyncssh[bcrypt,fido2,ifaddr,pkcs11,pyOpenSSL,pywin32]'
196
200
 
197
201
  Note that you will still need to manually install the libnettle library
198
202
  for UMAC support. Unfortunately, since liboqs and libnettle are not
@@ -121,9 +121,7 @@ class SSHAgentKeyPair(SSHKeyPair):
121
121
  else:
122
122
  sig_algorithm = algorithm
123
123
 
124
- # Neither Pageant nor the Win10 OpenSSH agent seems to support the
125
- # ssh-agent protocol flags used to request RSA SHA2 signatures yet
126
- if sig_algorithm == b'ssh-rsa' and sys.platform != 'win32':
124
+ if sig_algorithm == b'ssh-rsa':
127
125
  sig_algorithms: Sequence[bytes] = \
128
126
  (b'rsa-sha2-256', b'rsa-sha2-512', b'ssh-rsa')
129
127
  else:
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2020-2024 by Ron Frederick <ronf@timeheart.net> and others.
1
+ # Copyright (c) 2020-2026 by Ron Frederick <ronf@timeheart.net> and others.
2
2
  #
3
3
  # This program and the accompanying materials are made available under
4
4
  # the terms of the Eclipse Public License v2.0 which accompanies this
@@ -29,14 +29,20 @@ import subprocess
29
29
  from hashlib import sha1
30
30
  from pathlib import Path, PurePath
31
31
  from subprocess import DEVNULL
32
- from typing import Callable, Dict, List, NoReturn, Optional, Sequence
33
- from typing import Set, Tuple, Union, cast
32
+ from typing import Callable, Dict, Iterator, List, NoReturn, Optional
33
+ from typing import Sequence, Set, Tuple, Union, cast
34
34
 
35
35
  from .constants import DEFAULT_PORT
36
36
  from .logging import logger
37
- from .misc import DefTuple, FilePath, ip_address
37
+ from .misc import DefTuple, FilePath, IllegalUserName, ip_address
38
38
  from .pattern import HostPatternList, WildcardPatternList
39
39
 
40
+ try:
41
+ import ifaddr
42
+ _ifaddr_available = True
43
+ except ImportError: # pragma: no cover
44
+ _ifaddr_available = False
45
+
40
46
 
41
47
  ConfigPaths = Union[None, FilePath, Sequence[FilePath]]
42
48
 
@@ -52,6 +58,18 @@ def _exec(cmd: str) -> bool:
52
58
  stdout=DEVNULL, stderr=DEVNULL).returncode == 0
53
59
 
54
60
 
61
+ def _get_local_ips() -> Iterator[str]:
62
+ """Return local IP addresses of the system"""
63
+
64
+ for adapter in ifaddr.get_adapters():
65
+ for ip in adapter.ips:
66
+ if isinstance(ip.ip, tuple):
67
+ addr, _, scope_id = ip.ip
68
+ yield f'{addr}%{scope_id}' if scope_id else addr
69
+ else:
70
+ yield ip.ip
71
+
72
+
55
73
  class ConfigParseError(ValueError):
56
74
  """Configuration parsing exception"""
57
75
 
@@ -183,6 +201,10 @@ class SSHConfig:
183
201
  elif match == 'final':
184
202
  result = cast(bool, self._final)
185
203
  else:
204
+ if (match == 'localnetwork' and
205
+ not _ifaddr_available): # pragma: no cover
206
+ self._error('Local network match requires ifaddr module')
207
+
186
208
  match_val = self._match_val(match)
187
209
 
188
210
  if match != 'exec' and match_val is None:
@@ -201,6 +223,15 @@ class SSHConfig:
201
223
  ip = ip_address(cast(str, match_val)) \
202
224
  if match_val else None
203
225
  result = host_pat.matches(None, match_val, ip)
226
+ elif match == 'localnetwork':
227
+ host_pat = HostPatternList(arg)
228
+
229
+ for addr in cast(Iterator[str], match_val):
230
+ if host_pat.matches(None, addr, ip_address(addr)):
231
+ result = True
232
+ break
233
+ else:
234
+ result = False
204
235
  else:
205
236
  wild_pat = WildcardPatternList(arg)
206
237
  result = wild_pat.matches(match_val)
@@ -514,6 +545,8 @@ class SSHClientConfig(SSHConfig):
514
545
  return self._options.get('Hostname', self._orig_host)
515
546
  elif match == 'originalhost':
516
547
  return self._orig_host
548
+ elif match == 'localnetwork':
549
+ return _get_local_ips()
517
550
  elif match == 'localuser':
518
551
  return self._local_user
519
552
  elif match == 'user':
@@ -679,6 +712,9 @@ class SSHServerConfig(SSHConfig):
679
712
  def _set_tokens(self) -> None:
680
713
  """Set the tokens available for percent expansion"""
681
714
 
715
+ if self._user == '..' or '/' in self._user or '\\' in self._user:
716
+ raise IllegalUserName('Unsafe username substitution')
717
+
682
718
  self._tokens.update({'u': self._user})
683
719
 
684
720
  _handlers = {option.lower(): (option, handler) for option, handler in (
@@ -82,6 +82,7 @@ from .constants import OPEN_UNKNOWN_CHANNEL_TYPE
82
82
 
83
83
  from .encryption import Encryption, get_encryption_algs
84
84
  from .encryption import get_default_encryption_algs
85
+ from .encryption import encryption_needs_mac
85
86
  from .encryption import get_encryption_params, get_encryption
86
87
 
87
88
  from .forward import SSHForwarder
@@ -196,6 +197,9 @@ class _TunnelProtocol(Protocol):
196
197
  def close(self) -> None:
197
198
  """Close this tunnel"""
198
199
 
200
+ async def wait_closed(self):
201
+ """Wait for this tunnel to close"""
202
+
199
203
  class _TunnelConnectorProtocol(_TunnelProtocol, Protocol):
200
204
  """Protocol to open a connection to tunnel an SSH connection over"""
201
205
 
@@ -279,7 +283,7 @@ async def _canonicalize_host(loop: asyncio.AbstractEventLoop,
279
283
  options: 'SSHConnectionOptions') -> Optional[str]:
280
284
  """Canonicalize a host name"""
281
285
 
282
- host = options.host
286
+ host = options.orig_host
283
287
 
284
288
  if not options.canonicalize_hostname or not options.canonical_domains:
285
289
  logger.info('Host canonicalization disabled')
@@ -387,6 +391,11 @@ async def _open_proxy(
387
391
 
388
392
  self._conn.connection_lost(exc)
389
393
 
394
+ def process_exited(self):
395
+ """Called when the child process has exited"""
396
+
397
+ self._close_event.set()
398
+
390
399
  def write(self, data: bytes) -> None:
391
400
  """Write data to this tunnel"""
392
401
 
@@ -403,13 +412,20 @@ async def _open_proxy(
403
412
 
404
413
  if self._transport: # pragma: no cover
405
414
  self._transport.close()
415
+ self._transport = None
406
416
 
407
- self._close_event.set()
417
+ async def wait_closed(self):
418
+ """Wait for this subprocess to exit"""
419
+
420
+ await self._close_event.wait()
408
421
 
422
+ _, tunnel = await loop.subprocess_exec(_ProxyCommandTunnel, *command,
423
+ start_new_session=True)
409
424
 
410
- _, tunnel = await loop.subprocess_exec(_ProxyCommandTunnel, *command)
425
+ conn = cast(_Conn, cast(_ProxyCommandTunnel, tunnel).get_conn())
426
+ conn.set_tunnel(tunnel)
411
427
 
412
- return cast(_Conn, cast(_ProxyCommandTunnel, tunnel).get_conn())
428
+ return conn
413
429
 
414
430
 
415
431
  async def _open_tunnel(tunnels: object, options: _Options,
@@ -437,8 +453,8 @@ async def _open_tunnel(tunnels: object, options: _Options,
437
453
 
438
454
  last_conn = conn
439
455
  conn = await connect(host, port, username=username,
440
- passphrase=options.passphrase, tunnel=conn,
441
- config=config)
456
+ passphrase=options.passphrase,
457
+ tunnel=conn or (), config=config)
442
458
  conn.set_tunnel(last_conn)
443
459
 
444
460
  if options.canonicalize_hostname != 'always':
@@ -459,7 +475,7 @@ async def _connect(options: _Options, config: DefTuple[ConfigPaths],
459
475
 
460
476
  canon_host = await _canonicalize_host(loop, options)
461
477
 
462
- host = canon_host if canon_host else options.host
478
+ host = canon_host if canon_host else options.orig_host
463
479
  canonical = bool(canon_host)
464
480
  final = options.config.has_match_final()
465
481
 
@@ -651,7 +667,8 @@ def _expand_algs(alg_type: str, algs: str,
651
667
 
652
668
  def _select_algs(alg_type: str, algs: _AlgsArg, config_algs: _AlgsArg,
653
669
  possible_algs: List[bytes], default_algs: List[bytes],
654
- none_value: Optional[bytes] = None) -> Sequence[bytes]:
670
+ none_value: Optional[bytes] = None,
671
+ allow_empty: bool = False) -> Sequence[bytes]:
655
672
  """Select a set of allowed algorithms"""
656
673
 
657
674
  if algs == ():
@@ -682,6 +699,8 @@ def _select_algs(alg_type: str, algs: _AlgsArg, config_algs: _AlgsArg,
682
699
  return result
683
700
  elif none_value:
684
701
  return [none_value]
702
+ elif allow_empty:
703
+ return []
685
704
  else:
686
705
  raise ValueError(f'No {alg_type} algorithms selected')
687
706
 
@@ -714,7 +733,7 @@ def _validate_algs(config: SSHConfig, kex_algs_arg: _AlgsArg,
714
733
  get_default_encryption_algs())
715
734
  mac_algs = _select_algs('MAC', mac_algs_arg,
716
735
  cast(_AlgsArg, config.get('MACs', ())),
717
- get_mac_algs(), get_default_mac_algs())
736
+ get_mac_algs(), get_default_mac_algs(), None, True)
718
737
  cmp_algs = _select_algs('compression', cmp_algs_arg,
719
738
  cast(_AlgsArg, config.get_compression_algs()),
720
739
  get_compression_algs(),
@@ -1090,15 +1109,15 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1090
1109
 
1091
1110
  self._owner = None
1092
1111
 
1112
+ if self._tunnel:
1113
+ self._tunnel.close()
1114
+ self._tunnel = None
1115
+
1093
1116
  self._cancel_login_timer()
1094
1117
  self._close_event.set()
1095
1118
 
1096
1119
  self._inpbuf = b''
1097
1120
 
1098
- if self._tunnel:
1099
- self._tunnel.close()
1100
- self._tunnel = None
1101
-
1102
1121
  def _cancel_login_timer(self) -> None:
1103
1122
  """Cancel the login timer"""
1104
1123
 
@@ -1489,8 +1508,8 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1489
1508
 
1490
1509
  raise KeyExchangeFailed(
1491
1510
  f'No matching {alg_type} algorithm found, sent '
1492
- f'{b",".join(local_algs).decode("ascii")} and received '
1493
- f'{b",".join(remote_algs).decode("ascii")}')
1511
+ f'{b",".join(local_algs).decode("ascii") or "<None>"} and received '
1512
+ f'{b",".join(remote_algs).decode("ascii") or "<None>"}')
1494
1513
 
1495
1514
  def _get_extra_kex_algs(self) -> List[bytes]:
1496
1515
  """Return the extra kex algs to add"""
@@ -1852,7 +1871,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1852
1871
  self.logger.debug2(' Key exchange algs: %s', kex_algs)
1853
1872
  self.logger.debug2(' Host key algs: %s', host_key_algs)
1854
1873
  self.logger.debug2(' Encryption algs: %s', self._enc_algs)
1855
- self.logger.debug2(' MAC algs: %s', self._mac_algs)
1874
+ self.logger.debug2(' MAC algs: %s', self._mac_algs or '<None>')
1856
1875
  self.logger.debug2(' Compression algs: %s', self._cmp_algs)
1857
1876
 
1858
1877
  cookie = os.urandom(16)
@@ -1905,12 +1924,6 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1905
1924
  mac_keysize_sc, mac_hashsize_sc, etm_sc = \
1906
1925
  get_encryption_params(self._enc_alg_sc, self._mac_alg_sc)
1907
1926
 
1908
- if mac_keysize_cs == 0:
1909
- self._mac_alg_cs = self._enc_alg_cs
1910
-
1911
- if mac_keysize_sc == 0:
1912
- self._mac_alg_sc = self._enc_alg_sc
1913
-
1914
1927
  cmp_after_auth_cs = get_compression_params(self._cmp_alg_cs)
1915
1928
  cmp_after_auth_sc = get_compression_params(self._cmp_alg_sc)
1916
1929
 
@@ -2406,11 +2419,11 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2406
2419
  self.logger.debug2(' Host key algs: %s', peer_host_key_algs)
2407
2420
  self.logger.debug2(' Client to server:')
2408
2421
  self.logger.debug2(' Encryption algs: %s', enc_algs_cs)
2409
- self.logger.debug2(' MAC algs: %s', mac_algs_cs)
2422
+ self.logger.debug2(' MAC algs: %s', mac_algs_cs or '<None>')
2410
2423
  self.logger.debug2(' Compression algs: %s', cmp_algs_cs)
2411
2424
  self.logger.debug2(' Server to client:')
2412
2425
  self.logger.debug2(' Encryption algs: %s', enc_algs_sc)
2413
- self.logger.debug2(' MAC algs: %s', mac_algs_sc)
2426
+ self.logger.debug2(' MAC algs: %s', mac_algs_sc or '<None>')
2414
2427
  self.logger.debug2(' Compression algs: %s', cmp_algs_sc)
2415
2428
 
2416
2429
  kex_alg = self._choose_alg('key exchange', kex_algs, peer_kex_algs)
@@ -2431,8 +2444,17 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2431
2444
  self._enc_alg_sc = self._choose_alg('encryption', self._enc_algs,
2432
2445
  enc_algs_sc)
2433
2446
 
2434
- self._mac_alg_cs = self._choose_alg('MAC', self._mac_algs, mac_algs_cs)
2435
- self._mac_alg_sc = self._choose_alg('MAC', self._mac_algs, mac_algs_sc)
2447
+ if encryption_needs_mac(self._enc_alg_cs):
2448
+ self._mac_alg_cs = self._choose_alg('MAC', self._mac_algs,
2449
+ mac_algs_cs)
2450
+ else:
2451
+ self._mac_alg_cs = self._enc_alg_cs
2452
+
2453
+ if encryption_needs_mac(self._enc_alg_sc):
2454
+ self._mac_alg_sc = self._choose_alg('MAC', self._mac_algs,
2455
+ mac_algs_sc)
2456
+ else:
2457
+ self._mac_alg_sc = self._enc_alg_sc
2436
2458
 
2437
2459
  self._cmp_alg_cs = self._choose_alg('compression', self._cmp_algs,
2438
2460
  cmp_algs_cs)
@@ -2851,6 +2873,9 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2851
2873
  if self._agent:
2852
2874
  await self._agent.wait_closed()
2853
2875
 
2876
+ if self._tunnel:
2877
+ await self._tunnel.wait_closed()
2878
+
2854
2879
  await self._close_event.wait()
2855
2880
 
2856
2881
  def disconnect(self, code: int, reason: str,
@@ -7277,6 +7302,7 @@ class SSHConnectionOptions(Options, Generic[_Options]):
7277
7302
  waiter: Optional[asyncio.Future]
7278
7303
  protocol_factory: _ProtocolFactory
7279
7304
  version: bytes
7305
+ orig_host: str
7280
7306
  host: str
7281
7307
  port: int
7282
7308
  tunnel: object
@@ -7369,6 +7395,7 @@ class SSHConnectionOptions(Options, Generic[_Options]):
7369
7395
  self.protocol_factory = protocol_factory
7370
7396
  self.version = _validate_version(version)
7371
7397
 
7398
+ self.orig_host = host
7372
7399
  self.host = cast(str, config.get('Hostname', host))
7373
7400
  self.port = cast(int, port if port != () else
7374
7401
  config.get('Port', DEFAULT_PORT))
@@ -58,10 +58,12 @@ class PQDH:
58
58
  self.pubkey_bytes, self.privkey_bytes, \
59
59
  self.ciphertext_bytes, self.secret_bytes, \
60
60
  oqs_name = _pq_algs[alg_name]
61
- except KeyError: # pragma: no cover, other algs not registered
62
- raise ValueError(f'Unknown PQ algorithm {oqs_name}') from None
61
+ except KeyError:
62
+ raise ValueError('Unknown PQ algorithm ' +
63
+ alg_name.decode()) from None
63
64
 
64
- if not hasattr(_oqs, 'OQS_' + oqs_name + '_keypair'): # pragma: no cover
65
+ if not hasattr(_oqs, 'OQS_' + oqs_name + # pragma: no cover
66
+ '_keypair'):
65
67
  oqs_name += '_ipd'
66
68
 
67
69
  self._keypair = getattr(_oqs, 'OQS_' + oqs_name + '_keypair')
@@ -46,6 +46,12 @@ class Encryption:
46
46
 
47
47
  raise NotImplementedError
48
48
 
49
+ @classmethod
50
+ def needs_mac(cls) -> bool:
51
+ """Return whether a MAC algorithm is needed for this encryption"""
52
+
53
+ return True
54
+
49
55
  @classmethod
50
56
  def get_mac_params(cls, mac_alg: bytes) -> Tuple[int, int, bool]:
51
57
  """Get parameters of the MAC algorithm used with this encryption"""
@@ -161,6 +167,12 @@ class GCMEncryption(Encryption):
161
167
 
162
168
  return cls(GCMCipher(cipher_name, key, iv))
163
169
 
170
+ @classmethod
171
+ def needs_mac(cls) -> bool:
172
+ """GCM encryption doesn't need an external MAC algorithm"""
173
+
174
+ return False
175
+
164
176
  @classmethod
165
177
  def get_mac_params(cls, mac_alg: bytes) -> Tuple[int, int, bool]:
166
178
  """Get parameters of the MAC algorithm used with this encryption"""
@@ -200,6 +212,12 @@ class ChachaEncryption(Encryption):
200
212
 
201
213
  return cls(ChachaCipher(key))
202
214
 
215
+ @classmethod
216
+ def needs_mac(cls) -> bool:
217
+ """Chacha20 encryption doesn't need an external MAC algorithm"""
218
+
219
+ return False
220
+
203
221
  @classmethod
204
222
  def get_mac_params(cls, mac_alg: bytes) -> Tuple[int, int, bool]:
205
223
  """Get parameters of the MAC algorithm used with this encryption"""
@@ -258,8 +276,14 @@ def get_default_encryption_algs() -> List[bytes]:
258
276
  return _default_enc_algs
259
277
 
260
278
 
261
- def get_encryption_params(enc_alg: bytes,
262
- mac_alg: bytes = b'') -> _EncParams:
279
+ def encryption_needs_mac(enc_alg: bytes) -> bool:
280
+ """Return whether an encryption algorithm needs a MAC algorithm"""
281
+
282
+ encryption, _ = _enc_params[enc_alg]
283
+ return encryption.needs_mac()
284
+
285
+
286
+ def get_encryption_params(enc_alg: bytes, mac_alg: bytes = b'') -> _EncParams:
263
287
  """Get parameters of an encryption and MAC algorithm"""
264
288
 
265
289
  encryption, cipher_name = _enc_params[enc_alg]
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
35
35
 
36
36
 
37
37
  _KexAlgList = List[bytes]
38
- _KexAlgMap = Dict[bytes, Tuple[Type['Kex'], HashType, object]]
38
+ _KexAlgMap = Dict[bytes, Tuple[Type['Kex'], HashType, Tuple]]
39
39
 
40
40
 
41
41
  _kex_algs: _KexAlgList = []
@@ -251,7 +251,7 @@ def _normalize_scoped_ip(addr: str) -> str:
251
251
 
252
252
  if addrinfo[0] == socket.AF_INET6:
253
253
  sa = addrinfo[4]
254
- addr = sa[0]
254
+ addr = cast(str, sa[0])
255
255
 
256
256
  idx = addr.find('%')
257
257
  if idx >= 0: # pragma: no cover
@@ -346,7 +346,8 @@ class _SCPHandler:
346
346
 
347
347
  if isinstance(exc, SFTPError):
348
348
  reason = exc.reason.encode('utf-8')
349
- elif isinstance(exc, OSError): # pragma: no branch (win32)
349
+ elif isinstance(exc, OSError) and \
350
+ exc.strerror: # pragma: no branch (win32)
350
351
  reason = exc.strerror.encode('utf-8')
351
352
 
352
353
  filename = cast(BytesOrStr, exc.filename)
@@ -854,6 +854,10 @@ class SSHServer:
854
854
  * An :class:`SSHListener` object
855
855
  * `True` to set up standard port forwarding
856
856
  * `False` to reject the request
857
+ * A callable to use as an accept handler, taking
858
+ arguments of the original host and port of the
859
+ client and returning a boolean to indicate
860
+ whether or not to allow connection forwarding.
857
861
  * A coroutine object which returns one of the above
858
862
 
859
863
  """
@@ -145,6 +145,7 @@ if sys.platform == 'win32': # pragma: no cover
145
145
  else:
146
146
  _LocalPath = bytes
147
147
 
148
+ _SFTPExtensions = Sequence[Tuple[bytes, bytes]]
148
149
  _SFTPFileObj = IO[bytes]
149
150
  _SFTPPath = Union[bytes, FilePath]
150
151
  _SFTPPaths = Union[_SFTPPath, Sequence[_SFTPPath]]
@@ -1718,7 +1719,7 @@ class SFTPAttrs(Record):
1718
1719
  untrans_name: Optional[bytes]
1719
1720
  extended: Sequence[Tuple[bytes, bytes]] = ()
1720
1721
 
1721
- def _format_ns(self, k: str):
1722
+ def _format_ns(self, k: str) -> str:
1722
1723
  """Convert epoch seconds & nanoseconds to a string date & time"""
1723
1724
 
1724
1725
  result = time.ctime(getattr(self, k))
@@ -2241,7 +2242,7 @@ class SFTPRanges(Record):
2241
2242
  class SFTPGlob:
2242
2243
  """SFTP glob matcher"""
2243
2244
 
2244
- def __init__(self, fs: _SFTPGlobProtocol, multiple=False):
2245
+ def __init__(self, fs: _SFTPGlobProtocol, multiple: bool = False):
2245
2246
  self._fs = fs
2246
2247
  self._multiple = multiple
2247
2248
  self._prev_matches: Set[bytes] = set()
@@ -2280,7 +2281,7 @@ class SFTPGlob:
2280
2281
 
2281
2282
  return path, patlist
2282
2283
 
2283
- def _report_match(self, path, attrs):
2284
+ def _report_match(self, path: bytes, attrs: SFTPAttrs) -> None:
2284
2285
  """Report a matching name"""
2285
2286
 
2286
2287
  self._matched = True
@@ -2293,7 +2294,7 @@ class SFTPGlob:
2293
2294
 
2294
2295
  self._new_matches.append(SFTPName(path, attrs=attrs))
2295
2296
 
2296
- async def _stat(self, path) -> Optional[SFTPAttrs]:
2297
+ async def _stat(self, path: bytes) -> Optional[SFTPAttrs]:
2297
2298
  """Cache results of calls to stat"""
2298
2299
 
2299
2300
  try:
@@ -2309,7 +2310,7 @@ class SFTPGlob:
2309
2310
  self._stat_cache[path] = attrs
2310
2311
  return attrs
2311
2312
 
2312
- async def _scandir(self, path) -> AsyncIterator[SFTPName]:
2313
+ async def _scandir(self, path: bytes) -> AsyncIterator[SFTPName]:
2313
2314
  """Cache results of calls to scandir"""
2314
2315
 
2315
2316
  try:
@@ -2392,7 +2393,7 @@ class SFTPGlob:
2392
2393
 
2393
2394
  async def match(self, pattern: bytes,
2394
2395
  error_handler: SFTPErrorHandler = None,
2395
- sftp_version = MIN_SFTP_VERSION) -> Sequence[SFTPName]:
2396
+ sftp_version: int = MIN_SFTP_VERSION) -> Sequence[SFTPName]:
2396
2397
  """Match against a glob pattern"""
2397
2398
 
2398
2399
  self._new_matches = []
@@ -2476,7 +2477,7 @@ class SFTPHandler(SSHPacketLogger):
2476
2477
  self._reader = None
2477
2478
  self._writer = None
2478
2479
 
2479
- def _log_extensions(self, extensions: Sequence[Tuple[bytes, bytes]]):
2480
+ def _log_extensions(self, extensions: _SFTPExtensions) -> None:
2480
2481
  """Dump a formatted list of extensions to the debug log"""
2481
2482
 
2482
2483
  for name, data in extensions:
@@ -3628,7 +3629,7 @@ class SFTPClientFile:
3628
3629
 
3629
3630
  return self._offset
3630
3631
 
3631
- async def stat(self, flags = FILEXFER_ATTR_DEFINED_V4) -> SFTPAttrs:
3632
+ async def stat(self, flags: int = FILEXFER_ATTR_DEFINED_V4) -> SFTPAttrs:
3632
3633
  """Return file attributes of the remote file
3633
3634
 
3634
3635
  This method queries file attributes of the currently open file.
@@ -5737,6 +5738,14 @@ class SFTPClient:
5737
5738
  else:
5738
5739
  raise
5739
5740
 
5741
+ names[0].filename = self.decode(cast(bytes, names[0].filename),
5742
+ isinstance(path, (str, PurePath)))
5743
+
5744
+ if names[0].longname is not None:
5745
+ names[0].longname = \
5746
+ self.decode(cast(bytes, names[0].longname),
5747
+ isinstance(path, (str, PurePath)))
5748
+
5740
5749
  return names[0]
5741
5750
  else:
5742
5751
  return self.decode(cast(bytes, names[0].filename),
@@ -7289,10 +7298,8 @@ class SFTPServer:
7289
7298
  if pflags & FXF_EXCL:
7290
7299
  flags |= os.O_EXCL
7291
7300
 
7292
- try:
7293
- flags |= os.O_BINARY
7294
- except AttributeError: # pragma: no cover
7295
- pass
7301
+ if sys.platform == 'win32': # pragma: no cover
7302
+ flags |= os.O_BINARY # pylint: disable=no-member
7296
7303
 
7297
7304
  perms = 0o666 if attrs.permissions is None else attrs.permissions
7298
7305
 
@@ -7392,10 +7399,8 @@ class SFTPServer:
7392
7399
  desired_access & ACE4_WRITE_DATA:
7393
7400
  mode += '+'
7394
7401
 
7395
- try:
7396
- open_flags |= os.O_BINARY
7397
- except AttributeError: # pragma: no cover
7398
- pass
7402
+ if sys.platform == 'win32': # pragma: no cover
7403
+ open_flags |= os.O_BINARY # pylint: disable=no-member
7399
7404
 
7400
7405
  perms = 0o666 if attrs.permissions is None else attrs.permissions
7401
7406
 
@@ -7604,7 +7609,7 @@ class SFTPServer:
7604
7609
  # information.
7605
7610
 
7606
7611
  # pylint: disable=no-member
7607
- listdir_result = self.listdir(path) # type: ignore
7612
+ listdir_result = self.listdir(path)
7608
7613
 
7609
7614
  if inspect.isawaitable(listdir_result):
7610
7615
  listdir_result = await cast(
@@ -359,6 +359,6 @@ try: # pragma: no cover
359
359
  sk_use_webauthn = WindowsClient.is_available() and \
360
360
  hasattr(ctypes, 'windll') and \
361
361
  not ctypes.windll.shell32.IsUserAnAdmin()
362
- except ImportError:
362
+ except (AttributeError, ImportError):
363
363
  WindowsClient = None # type: ignore
364
364
  sk_use_webauthn = False