asyncssh 2.22.0__tar.gz → 2.23.1__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.1}/PKG-INFO +11 -7
  2. {asyncssh-2.22.0 → asyncssh-2.23.1}/README.rst +8 -4
  3. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/agent.py +1 -3
  4. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/config.py +56 -6
  5. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/connection.py +64 -35
  6. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/pq.py +5 -3
  7. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/encryption.py +26 -2
  8. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/kex.py +1 -1
  9. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/misc.py +1 -1
  10. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/scp.py +6 -1
  11. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/server.py +4 -0
  12. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/sftp.py +30 -17
  13. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/sk.py +1 -1
  14. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/version.py +1 -1
  15. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/x11.py +11 -9
  16. {asyncssh-2.22.0 → asyncssh-2.23.1/asyncssh.egg-info}/PKG-INFO +11 -7
  17. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh.egg-info/requires.txt +2 -2
  18. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/api.rst +1 -15
  19. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/changes.rst +54 -0
  20. {asyncssh-2.22.0 → asyncssh-2.23.1}/pyproject.toml +1 -1
  21. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_config.py +27 -3
  22. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_connection.py +30 -2
  23. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_forward.py +24 -0
  24. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_kex.py +6 -0
  25. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_process.py +17 -22
  26. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_sftp.py +14 -0
  27. {asyncssh-2.22.0 → asyncssh-2.23.1}/tox.ini +1 -1
  28. {asyncssh-2.22.0 → asyncssh-2.23.1}/.coveragerc +0 -0
  29. {asyncssh-2.22.0 → asyncssh-2.23.1}/.github/workflows/run_tests.yml +0 -0
  30. {asyncssh-2.22.0 → asyncssh-2.23.1}/.gitignore +0 -0
  31. {asyncssh-2.22.0 → asyncssh-2.23.1}/.readthedocs.yaml +0 -0
  32. {asyncssh-2.22.0 → asyncssh-2.23.1}/CONTRIBUTING.rst +0 -0
  33. {asyncssh-2.22.0 → asyncssh-2.23.1}/COPYRIGHT +0 -0
  34. {asyncssh-2.22.0 → asyncssh-2.23.1}/LICENSE +0 -0
  35. {asyncssh-2.22.0 → asyncssh-2.23.1}/MANIFEST.in +0 -0
  36. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/__init__.py +0 -0
  37. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/agent_unix.py +0 -0
  38. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/agent_win32.py +0 -0
  39. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/asn1.py +0 -0
  40. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/auth.py +0 -0
  41. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/auth_keys.py +0 -0
  42. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/channel.py +0 -0
  43. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/client.py +0 -0
  44. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/compression.py +0 -0
  45. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/constants.py +0 -0
  46. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/__init__.py +0 -0
  47. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/chacha.py +0 -0
  48. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/cipher.py +0 -0
  49. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/dh.py +0 -0
  50. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/dsa.py +0 -0
  51. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/ec.py +0 -0
  52. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/ec_params.py +0 -0
  53. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/ed.py +0 -0
  54. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/kdf.py +0 -0
  55. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/misc.py +0 -0
  56. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/rsa.py +0 -0
  57. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/umac.py +0 -0
  58. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/crypto/x509.py +0 -0
  59. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/dsa.py +0 -0
  60. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/ecdsa.py +0 -0
  61. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/eddsa.py +0 -0
  62. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/editor.py +0 -0
  63. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/forward.py +0 -0
  64. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/gss.py +0 -0
  65. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/gss_unix.py +0 -0
  66. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/gss_win32.py +0 -0
  67. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/kex_dh.py +0 -0
  68. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/kex_rsa.py +0 -0
  69. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/keysign.py +0 -0
  70. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/known_hosts.py +0 -0
  71. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/listener.py +0 -0
  72. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/logging.py +0 -0
  73. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/mac.py +0 -0
  74. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/packet.py +0 -0
  75. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/pattern.py +0 -0
  76. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/pbe.py +0 -0
  77. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/pkcs11.py +0 -0
  78. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/process.py +0 -0
  79. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/public_key.py +0 -0
  80. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/py.typed +0 -0
  81. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/rsa.py +0 -0
  82. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/saslprep.py +0 -0
  83. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/session.py +0 -0
  84. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/sk_ecdsa.py +0 -0
  85. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/sk_eddsa.py +0 -0
  86. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/socks.py +0 -0
  87. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/stream.py +0 -0
  88. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/subprocess.py +0 -0
  89. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh/tuntap.py +0 -0
  90. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh.egg-info/SOURCES.txt +0 -0
  91. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh.egg-info/dependency_links.txt +0 -0
  92. {asyncssh-2.22.0 → asyncssh-2.23.1}/asyncssh.egg-info/top_level.txt +0 -0
  93. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/_templates/sidebarbottom.html +0 -0
  94. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/_templates/sidebartop.html +0 -0
  95. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/conf.py +0 -0
  96. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/contributing.rst +0 -0
  97. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/index.rst +0 -0
  98. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/requirements.txt +0 -0
  99. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/rftheme/layout.html +0 -0
  100. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/rftheme/static/rftheme.css_t +0 -0
  101. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/rftheme/theme.conf +0 -0
  102. {asyncssh-2.22.0 → asyncssh-2.23.1}/docs/rtd-req.txt +0 -0
  103. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/callback_client.py +0 -0
  104. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/callback_client2.py +0 -0
  105. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/callback_client3.py +0 -0
  106. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/callback_math_server.py +0 -0
  107. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/chat_server.py +0 -0
  108. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/check_exit_status.py +0 -0
  109. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/chroot_sftp_server.py +0 -0
  110. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/direct_client.py +0 -0
  111. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/direct_server.py +0 -0
  112. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/editor.py +0 -0
  113. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/gather_results.py +0 -0
  114. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/listening_client.py +0 -0
  115. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/local_forwarding_client.py +0 -0
  116. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/local_forwarding_client2.py +0 -0
  117. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/local_forwarding_server.py +0 -0
  118. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/math_client.py +0 -0
  119. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/math_server.py +0 -0
  120. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/redirect_input.py +0 -0
  121. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/redirect_local_pipe.py +0 -0
  122. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/redirect_remote_pipe.py +0 -0
  123. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/redirect_server.py +0 -0
  124. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/remote_forwarding_client.py +0 -0
  125. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/remote_forwarding_client2.py +0 -0
  126. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/remote_forwarding_server.py +0 -0
  127. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/reverse_client.py +0 -0
  128. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/reverse_server.py +0 -0
  129. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/scp_client.py +0 -0
  130. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/set_environment.py +0 -0
  131. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/set_terminal.py +0 -0
  132. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/sftp_client.py +0 -0
  133. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/show_environment.py +0 -0
  134. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/show_terminal.py +0 -0
  135. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/simple_cert_server.py +0 -0
  136. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/simple_client.py +0 -0
  137. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/simple_keyed_server.py +0 -0
  138. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/simple_scp_server.py +0 -0
  139. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/simple_server.py +0 -0
  140. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/simple_sftp_server.py +0 -0
  141. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/stream_direct_client.py +0 -0
  142. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/stream_direct_server.py +0 -0
  143. {asyncssh-2.22.0 → asyncssh-2.23.1}/examples/stream_listening_client.py +0 -0
  144. {asyncssh-2.22.0 → asyncssh-2.23.1}/mypy.ini +0 -0
  145. {asyncssh-2.22.0 → asyncssh-2.23.1}/pylintrc +0 -0
  146. {asyncssh-2.22.0 → asyncssh-2.23.1}/setup.cfg +0 -0
  147. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/__init__.py +0 -0
  148. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/gss_stub.py +0 -0
  149. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/gssapi_stub.py +0 -0
  150. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/keysign_stub.py +0 -0
  151. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/pkcs11_stub.py +0 -0
  152. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/server.py +0 -0
  153. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/sk_stub.py +0 -0
  154. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/sspi_stub.py +0 -0
  155. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_agent.py +0 -0
  156. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_asn1.py +0 -0
  157. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_auth.py +0 -0
  158. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_auth_keys.py +0 -0
  159. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_channel.py +0 -0
  160. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_compression.py +0 -0
  161. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_connection_auth.py +0 -0
  162. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_editor.py +0 -0
  163. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_encryption.py +0 -0
  164. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_known_hosts.py +0 -0
  165. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_logging.py +0 -0
  166. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_mac.py +0 -0
  167. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_packet.py +0 -0
  168. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_pkcs11.py +0 -0
  169. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_public_key.py +0 -0
  170. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_saslprep.py +0 -0
  171. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_sk.py +0 -0
  172. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_stream.py +0 -0
  173. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_subprocess.py +0 -0
  174. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_tuntap.py +0 -0
  175. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_x11.py +0 -0
  176. {asyncssh-2.22.0 → asyncssh-2.23.1}/tests/test_x509.py +0 -0
  177. {asyncssh-2.22.0 → asyncssh-2.23.1}/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.1
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,20 +29,27 @@ 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
 
43
49
 
44
50
  _token_pattern = re.compile(r'%(.)')
45
- _env_pattern = re.compile(r'\${(.*)}')
51
+ _env_pattern = re.compile(r'\${(.*?)}')
52
+ _unsafe_user_pattern = re.compile(r'^\.\.$|^~|^[A-Za-z]:|[/\\]|\$\{.*?\}')
46
53
 
47
54
 
48
55
  def _exec(cmd: str) -> bool:
@@ -52,6 +59,18 @@ def _exec(cmd: str) -> bool:
52
59
  stdout=DEVNULL, stderr=DEVNULL).returncode == 0
53
60
 
54
61
 
62
+ def _get_local_ips() -> Iterator[str]:
63
+ """Return local IP addresses of the system"""
64
+
65
+ for adapter in ifaddr.get_adapters():
66
+ for ip in adapter.ips:
67
+ if isinstance(ip.ip, tuple):
68
+ addr, _, scope_id = ip.ip
69
+ yield f'{addr}%{scope_id}' if scope_id else addr
70
+ else:
71
+ yield ip.ip
72
+
73
+
55
74
  class ConfigParseError(ValueError):
56
75
  """Configuration parsing exception"""
57
76
 
@@ -183,6 +202,10 @@ class SSHConfig:
183
202
  elif match == 'final':
184
203
  result = cast(bool, self._final)
185
204
  else:
205
+ if (match == 'localnetwork' and
206
+ not _ifaddr_available): # pragma: no cover
207
+ self._error('Local network match requires ifaddr module')
208
+
186
209
  match_val = self._match_val(match)
187
210
 
188
211
  if match != 'exec' and match_val is None:
@@ -201,6 +224,15 @@ class SSHConfig:
201
224
  ip = ip_address(cast(str, match_val)) \
202
225
  if match_val else None
203
226
  result = host_pat.matches(None, match_val, ip)
227
+ elif match == 'localnetwork':
228
+ host_pat = HostPatternList(arg)
229
+
230
+ for addr in cast(Iterator[str], match_val):
231
+ if host_pat.matches(None, addr, ip_address(addr)):
232
+ result = True
233
+ break
234
+ else:
235
+ result = False
204
236
  else:
205
237
  wild_pat = WildcardPatternList(arg)
206
238
  result = wild_pat.matches(match_val)
@@ -514,6 +546,8 @@ class SSHClientConfig(SSHConfig):
514
546
  return self._options.get('Hostname', self._orig_host)
515
547
  elif match == 'originalhost':
516
548
  return self._orig_host
549
+ elif match == 'localnetwork':
550
+ return _get_local_ips()
517
551
  elif match == 'localuser':
518
552
  return self._local_user
519
553
  elif match == 'user':
@@ -677,7 +711,23 @@ class SSHServerConfig(SSHConfig):
677
711
  return None
678
712
 
679
713
  def _set_tokens(self) -> None:
680
- """Set the tokens available for percent expansion"""
714
+ """Set the tokens available for percent expansion
715
+
716
+ Only allow "safe" username substitutions. Unsafe usernames are:
717
+
718
+ - a username of exactly ".."
719
+ - a username beginning with a "~"
720
+ - a username beginning with a Windows drive letter and a ":"
721
+ - a username containing forward or backward slashes
722
+ - a username containing an env substitution like "${...}"
723
+
724
+ Note: this code assumes that saslprep has already been performed
725
+ on the username before it is accessed here.
726
+
727
+ """
728
+
729
+ if _unsafe_user_pattern.search(self._user):
730
+ raise IllegalUserName('Unsafe username substitution')
681
731
 
682
732
  self._tokens.update({'u': self._user})
683
733
 
@@ -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
 
@@ -537,7 +553,7 @@ async def _connect(options: _Options, config: DefTuple[ConfigPaths],
537
553
  async def _listen(options: _Options, config: DefTuple[ConfigPaths],
538
554
  loop: asyncio.AbstractEventLoop, flags: int,
539
555
  backlog: int, sock: Optional[socket.socket],
540
- reuse_address: bool, reuse_port: bool,
556
+ reuse_address: Optional[bool], reuse_port: Optional[bool],
541
557
  conn_factory: Callable[[], _Conn],
542
558
  msg: str) -> 'SSHAcceptor':
543
559
  """Make inbound TCP or SSH tunneled listener"""
@@ -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))
@@ -9326,7 +9353,8 @@ async def listen(host = '', port: DefTuple[int] = (), *,
9326
9353
  tunnel: DefTuple[_TunnelListener] = (),
9327
9354
  family: DefTuple[int] = (), flags:int = socket.AI_PASSIVE,
9328
9355
  backlog: int = 100, sock: Optional[socket.socket] = None,
9329
- reuse_address: bool = False, reuse_port: bool = False,
9356
+ reuse_address: Optional[bool] = None,
9357
+ reuse_port: Optional[bool] = None,
9330
9358
  acceptor: _AcceptHandler = None,
9331
9359
  error_handler: _ErrorHandler = None,
9332
9360
  config: DefTuple[ConfigPaths] = (),
@@ -9384,7 +9412,7 @@ async def listen(host = '', port: DefTuple[int] = (), *,
9384
9412
  port other existing sockets are bound to, so long as they all
9385
9413
  set this flag when being created. If not specified, the
9386
9414
  default is to not allow this. This option is not supported
9387
- on Windows or Python versions prior to 3.4.4.
9415
+ on Windows.
9388
9416
  :param acceptor: (optional)
9389
9417
  A `callable` or coroutine which will be called when the
9390
9418
  SSH handshake completes on an accepted connection, taking
@@ -9414,8 +9442,8 @@ async def listen(host = '', port: DefTuple[int] = (), *,
9414
9442
  :type flags: flags to pass to :meth:`getaddrinfo() <socket.getaddrinfo>`
9415
9443
  :type backlog: `int`
9416
9444
  :type sock: :class:`socket.socket` or `None`
9417
- :type reuse_address: `bool`
9418
- :type reuse_port: `bool`
9445
+ :type reuse_address: `bool` or `None`
9446
+ :type reuse_port: `bool` or `None`
9419
9447
  :type acceptor: `callable` or coroutine
9420
9448
  :type error_handler: `callable`
9421
9449
  :type config: `list` of `str`
@@ -9451,7 +9479,8 @@ async def listen_reverse(host = '', port: DefTuple[int] = (), *,
9451
9479
  family: DefTuple[int] = (),
9452
9480
  flags: int = socket.AI_PASSIVE, backlog: int = 100,
9453
9481
  sock: Optional[socket.socket] = None,
9454
- reuse_address: bool = False, reuse_port: bool = False,
9482
+ reuse_address: Optional[bool] = None,
9483
+ reuse_port: Optional[bool] = None,
9455
9484
  acceptor: _AcceptHandler = None,
9456
9485
  error_handler: _ErrorHandler = None,
9457
9486
  config: DefTuple[ConfigPaths] = (),
@@ -9518,7 +9547,7 @@ async def listen_reverse(host = '', port: DefTuple[int] = (), *,
9518
9547
  port other existing sockets are bound to, so long as they all
9519
9548
  set this flag when being created. If not specified, the
9520
9549
  default is to not allow this. This option is not supported
9521
- on Windows or Python versions prior to 3.4.4.
9550
+ on Windows.
9522
9551
  :param acceptor: (optional)
9523
9552
  A `callable` or coroutine which will be called when the
9524
9553
  SSH handshake completes on an accepted connection, taking
@@ -9553,8 +9582,8 @@ async def listen_reverse(host = '', port: DefTuple[int] = (), *,
9553
9582
  :type flags: flags to pass to :meth:`getaddrinfo() <socket.getaddrinfo>`
9554
9583
  :type backlog: `int`
9555
9584
  :type sock: :class:`socket.socket` or `None`
9556
- :type reuse_address: `bool`
9557
- :type reuse_port: `bool`
9585
+ :type reuse_address: `bool` or `None`
9586
+ :type reuse_port: `bool` or `None`
9558
9587
  :type acceptor: `callable` or coroutine
9559
9588
  :type error_handler: `callable`
9560
9589
  :type config: `list` of `str`
@@ -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
@@ -136,6 +136,10 @@ def _parse_cd_args(args: bytes) -> Tuple[int, int, bytes]:
136
136
 
137
137
  try:
138
138
  permissions, size, name = args.split(None, 2)
139
+
140
+ if b'/' in name or b'\\' in name or name == b'..':
141
+ raise _scp_error(SFTPBadMessage, 'Invalid filename')
142
+
139
143
  return int(permissions, 8), int(size), name
140
144
  except ValueError:
141
145
  raise _scp_error(SFTPBadMessage,
@@ -346,7 +350,8 @@ class _SCPHandler:
346
350
 
347
351
  if isinstance(exc, SFTPError):
348
352
  reason = exc.reason.encode('utf-8')
349
- elif isinstance(exc, OSError): # pragma: no branch (win32)
353
+ elif isinstance(exc, OSError) and \
354
+ exc.strerror: # pragma: no branch (win32)
350
355
  reason = exc.strerror.encode('utf-8')
351
356
 
352
357
  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
  """