asyncssh 2.19.0__tar.gz → 2.21.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.19.0 → asyncssh-2.21.0}/.github/workflows/run_tests.yml +3 -18
  2. {asyncssh-2.19.0/asyncssh.egg-info → asyncssh-2.21.0}/PKG-INFO +4 -3
  3. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/auth.py +12 -12
  4. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/channel.py +12 -5
  5. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/client.py +9 -0
  6. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/config.py +50 -28
  7. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/connection.py +134 -31
  8. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/kex_dh.py +17 -1
  9. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/misc.py +32 -7
  10. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/process.py +2 -2
  11. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/scp.py +61 -27
  12. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/server.py +54 -21
  13. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/sftp.py +401 -125
  14. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/stream.py +3 -5
  15. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/version.py +1 -1
  16. {asyncssh-2.19.0 → asyncssh-2.21.0/asyncssh.egg-info}/PKG-INFO +4 -3
  17. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/changes.rst +68 -0
  18. {asyncssh-2.19.0 → asyncssh-2.21.0}/pyproject.toml +1 -1
  19. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/server.py +16 -12
  20. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_auth.py +2 -2
  21. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_channel.py +19 -3
  22. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_config.py +29 -4
  23. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_connection.py +18 -0
  24. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_connection_auth.py +6 -2
  25. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_forward.py +63 -6
  26. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_kex.py +44 -11
  27. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_sftp.py +156 -8
  28. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_stream.py +33 -1
  29. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_tuntap.py +59 -1
  30. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/util.py +0 -2
  31. {asyncssh-2.19.0 → asyncssh-2.21.0}/tox.ini +3 -3
  32. {asyncssh-2.19.0 → asyncssh-2.21.0}/.coveragerc +0 -0
  33. {asyncssh-2.19.0 → asyncssh-2.21.0}/.gitignore +0 -0
  34. {asyncssh-2.19.0 → asyncssh-2.21.0}/.readthedocs.yaml +0 -0
  35. {asyncssh-2.19.0 → asyncssh-2.21.0}/CONTRIBUTING.rst +0 -0
  36. {asyncssh-2.19.0 → asyncssh-2.21.0}/COPYRIGHT +0 -0
  37. {asyncssh-2.19.0 → asyncssh-2.21.0}/LICENSE +0 -0
  38. {asyncssh-2.19.0 → asyncssh-2.21.0}/MANIFEST.in +0 -0
  39. {asyncssh-2.19.0 → asyncssh-2.21.0}/README.rst +0 -0
  40. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/__init__.py +0 -0
  41. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/agent.py +0 -0
  42. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/agent_unix.py +0 -0
  43. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/agent_win32.py +0 -0
  44. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/asn1.py +0 -0
  45. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/auth_keys.py +0 -0
  46. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/compression.py +0 -0
  47. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/constants.py +0 -0
  48. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/__init__.py +0 -0
  49. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/chacha.py +0 -0
  50. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/cipher.py +0 -0
  51. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/dh.py +0 -0
  52. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/dsa.py +0 -0
  53. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/ec.py +0 -0
  54. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/ec_params.py +0 -0
  55. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/ed.py +0 -0
  56. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/kdf.py +0 -0
  57. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/misc.py +0 -0
  58. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/pq.py +0 -0
  59. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/rsa.py +0 -0
  60. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/umac.py +0 -0
  61. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/crypto/x509.py +0 -0
  62. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/dsa.py +0 -0
  63. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/ecdsa.py +0 -0
  64. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/eddsa.py +0 -0
  65. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/editor.py +0 -0
  66. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/encryption.py +0 -0
  67. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/forward.py +0 -0
  68. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/gss.py +0 -0
  69. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/gss_unix.py +0 -0
  70. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/gss_win32.py +0 -0
  71. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/kex.py +0 -0
  72. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/kex_rsa.py +0 -0
  73. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/keysign.py +0 -0
  74. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/known_hosts.py +0 -0
  75. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/listener.py +0 -0
  76. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/logging.py +0 -0
  77. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/mac.py +0 -0
  78. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/packet.py +0 -0
  79. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/pattern.py +0 -0
  80. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/pbe.py +0 -0
  81. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/pkcs11.py +0 -0
  82. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/public_key.py +0 -0
  83. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/py.typed +0 -0
  84. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/rsa.py +0 -0
  85. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/saslprep.py +0 -0
  86. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/session.py +0 -0
  87. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/sk.py +0 -0
  88. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/sk_ecdsa.py +0 -0
  89. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/sk_eddsa.py +0 -0
  90. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/socks.py +0 -0
  91. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/subprocess.py +0 -0
  92. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/tuntap.py +0 -0
  93. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh/x11.py +0 -0
  94. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh.egg-info/SOURCES.txt +0 -0
  95. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh.egg-info/dependency_links.txt +0 -0
  96. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh.egg-info/requires.txt +0 -0
  97. {asyncssh-2.19.0 → asyncssh-2.21.0}/asyncssh.egg-info/top_level.txt +0 -0
  98. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/_templates/sidebarbottom.html +0 -0
  99. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/_templates/sidebartop.html +0 -0
  100. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/api.rst +0 -0
  101. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/conf.py +0 -0
  102. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/contributing.rst +0 -0
  103. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/index.rst +0 -0
  104. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/requirements.txt +0 -0
  105. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/rftheme/layout.html +0 -0
  106. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/rftheme/static/rftheme.css_t +0 -0
  107. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/rftheme/theme.conf +0 -0
  108. {asyncssh-2.19.0 → asyncssh-2.21.0}/docs/rtd-req.txt +0 -0
  109. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/callback_client.py +0 -0
  110. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/callback_client2.py +0 -0
  111. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/callback_client3.py +0 -0
  112. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/callback_math_server.py +0 -0
  113. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/chat_server.py +0 -0
  114. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/check_exit_status.py +0 -0
  115. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/chroot_sftp_server.py +0 -0
  116. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/direct_client.py +0 -0
  117. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/direct_server.py +0 -0
  118. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/editor.py +0 -0
  119. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/gather_results.py +0 -0
  120. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/listening_client.py +0 -0
  121. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/local_forwarding_client.py +0 -0
  122. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/local_forwarding_client2.py +0 -0
  123. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/local_forwarding_server.py +0 -0
  124. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/math_client.py +0 -0
  125. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/math_server.py +0 -0
  126. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/redirect_input.py +0 -0
  127. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/redirect_local_pipe.py +0 -0
  128. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/redirect_remote_pipe.py +0 -0
  129. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/redirect_server.py +0 -0
  130. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/remote_forwarding_client.py +0 -0
  131. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/remote_forwarding_client2.py +0 -0
  132. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/remote_forwarding_server.py +0 -0
  133. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/reverse_client.py +0 -0
  134. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/reverse_server.py +0 -0
  135. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/scp_client.py +0 -0
  136. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/set_environment.py +0 -0
  137. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/set_terminal.py +0 -0
  138. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/sftp_client.py +0 -0
  139. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/show_environment.py +0 -0
  140. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/show_terminal.py +0 -0
  141. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/simple_cert_server.py +0 -0
  142. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/simple_client.py +0 -0
  143. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/simple_keyed_server.py +0 -0
  144. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/simple_scp_server.py +0 -0
  145. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/simple_server.py +0 -0
  146. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/simple_sftp_server.py +0 -0
  147. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/stream_direct_client.py +0 -0
  148. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/stream_direct_server.py +0 -0
  149. {asyncssh-2.19.0 → asyncssh-2.21.0}/examples/stream_listening_client.py +0 -0
  150. {asyncssh-2.19.0 → asyncssh-2.21.0}/mypy.ini +0 -0
  151. {asyncssh-2.19.0 → asyncssh-2.21.0}/pylintrc +0 -0
  152. {asyncssh-2.19.0 → asyncssh-2.21.0}/setup.cfg +0 -0
  153. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/__init__.py +0 -0
  154. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/gss_stub.py +0 -0
  155. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/gssapi_stub.py +0 -0
  156. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/keysign_stub.py +0 -0
  157. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/pkcs11_stub.py +0 -0
  158. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/sk_stub.py +0 -0
  159. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/sspi_stub.py +0 -0
  160. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_agent.py +0 -0
  161. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_asn1.py +0 -0
  162. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_auth_keys.py +0 -0
  163. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_compression.py +0 -0
  164. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_editor.py +0 -0
  165. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_encryption.py +0 -0
  166. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_known_hosts.py +0 -0
  167. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_logging.py +0 -0
  168. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_mac.py +0 -0
  169. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_packet.py +0 -0
  170. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_pkcs11.py +0 -0
  171. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_process.py +0 -0
  172. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_public_key.py +0 -0
  173. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_saslprep.py +0 -0
  174. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_sk.py +0 -0
  175. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_subprocess.py +0 -0
  176. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_x11.py +0 -0
  177. {asyncssh-2.19.0 → asyncssh-2.21.0}/tests/test_x509.py +0 -0
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest, macos-latest, windows-latest]
11
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
11
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
12
12
  include:
13
13
  - os: macos-latest
14
14
  python-version: "3.10"
@@ -19,22 +19,9 @@ jobs:
19
19
  - os: macos-latest
20
20
  python-version: "3.12"
21
21
  openssl-version: "3"
22
- exclude:
23
- # having trouble with arch arm64 on macos-ltest on Python 3.7
24
22
  - os: macos-latest
25
- python-version: "3.7"
26
-
27
- # test hangs on these combination
28
- - os: windows-latest
29
- python-version: "3.8"
30
- - os: windows-latest
31
- python-version: "3.9"
32
- - os: windows-latest
33
- python-version: "3.10"
34
- - os: windows-latest
35
- python-version: "3.11"
36
- - os: windows-latest
37
- python-version: "3.12"
23
+ python-version: "3.13"
24
+ openssl-version: "3"
38
25
 
39
26
  runs-on: ${{ matrix.os }}
40
27
  env:
@@ -153,8 +140,6 @@ jobs:
153
140
  steps:
154
141
  - uses: actions/checkout@v4
155
142
  - uses: actions/setup-python@v5
156
- with:
157
- python-version: "3.7"
158
143
  - uses: actions/download-artifact@v4
159
144
  with:
160
145
  name: coverage
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: asyncssh
3
- Version: 2.19.0
3
+ Version: 2.21.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
@@ -14,12 +14,12 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved
15
15
  Classifier: Operating System :: MacOS :: MacOS X
16
16
  Classifier: Operating System :: POSIX
17
- Classifier: Programming Language :: Python :: 3.7
18
17
  Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Topic :: Internet
24
24
  Classifier: Topic :: Security :: Cryptography
25
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -43,6 +43,7 @@ Provides-Extra: pyopenssl
43
43
  Requires-Dist: pyOpenSSL>=23.0.0; extra == "pyopenssl"
44
44
  Provides-Extra: pywin32
45
45
  Requires-Dist: pywin32>=227; extra == "pywin32"
46
+ Dynamic: license-file
46
47
 
47
48
  .. image:: https://readthedocs.org/projects/asyncssh/badge/?version=latest
48
49
  :target: https://asyncssh.readthedocs.io/en/latest/?badge=latest
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2024 by Ron Frederick <ronf@timeheart.net> and others.
1
+ # Copyright (c) 2013-2025 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
@@ -548,10 +548,10 @@ class ServerAuth(Auth):
548
548
 
549
549
  self._conn.send_userauth_failure(partial_success)
550
550
 
551
- def send_success(self) -> None:
551
+ async def send_success(self) -> None:
552
552
  """Send a user authentication success response"""
553
553
 
554
- self._conn.send_userauth_success()
554
+ await self._conn.send_userauth_success()
555
555
 
556
556
 
557
557
  class _ServerNullAuth(ServerAuth):
@@ -596,7 +596,7 @@ class _ServerGSSKexAuth(ServerAuth):
596
596
  (await self._conn.validate_gss_principal(self._username,
597
597
  self._gss.user,
598
598
  self._gss.host))):
599
- self.send_success()
599
+ await self.send_success()
600
600
  else:
601
601
  self.send_failure()
602
602
 
@@ -650,7 +650,7 @@ class _ServerGSSMICAuth(ServerAuth):
650
650
  if (await self._conn.validate_gss_principal(self._username,
651
651
  self._gss.user,
652
652
  self._gss.host)):
653
- self.send_success()
653
+ await self.send_success()
654
654
  else:
655
655
  self.send_failure()
656
656
 
@@ -757,7 +757,7 @@ class _ServerHostBasedAuth(ServerAuth):
757
757
  key_data, client_host,
758
758
  client_username,
759
759
  msg, signature)):
760
- self.send_success()
760
+ await self.send_success()
761
761
  else:
762
762
  self.send_failure()
763
763
 
@@ -795,7 +795,7 @@ class _ServerPublicKeyAuth(ServerAuth):
795
795
  if (await self._conn.validate_public_key(self._username, key_data,
796
796
  msg, signature)):
797
797
  if sig_present:
798
- self.send_success()
798
+ await self.send_success()
799
799
  else:
800
800
  self.send_packet(MSG_USERAUTH_PK_OK, String(algorithm),
801
801
  String(key_data))
@@ -832,9 +832,9 @@ class _ServerKbdIntAuth(ServerAuth):
832
832
 
833
833
  challenge = await self._conn.get_kbdint_challenge(self._username,
834
834
  lang, submethods)
835
- self._send_challenge(challenge)
835
+ await self._send_challenge(challenge)
836
836
 
837
- def _send_challenge(self, challenge: KbdIntChallenge) -> None:
837
+ async def _send_challenge(self, challenge: KbdIntChallenge) -> None:
838
838
  """Send a keyboard interactive authentication request"""
839
839
 
840
840
  if isinstance(challenge, (tuple, list)):
@@ -848,7 +848,7 @@ class _ServerKbdIntAuth(ServerAuth):
848
848
  String(instruction), String(lang),
849
849
  UInt32(num_prompts), *prompts_bytes)
850
850
  elif challenge:
851
- self.send_success()
851
+ await self.send_success()
852
852
  else:
853
853
  self.send_failure()
854
854
 
@@ -857,7 +857,7 @@ class _ServerKbdIntAuth(ServerAuth):
857
857
 
858
858
  next_challenge = \
859
859
  await self._conn.validate_kbdint_response(self._username, responses)
860
- self._send_challenge(next_challenge)
860
+ await self._send_challenge(next_challenge)
861
861
 
862
862
  def _process_info_response(self, _pkttype: int, _pktid: int,
863
863
  packet: SSHPacket) -> None:
@@ -922,7 +922,7 @@ class _ServerPasswordAuth(ServerAuth):
922
922
  await self._conn.validate_password(self._username, password)
923
923
 
924
924
  if result:
925
- self.send_success()
925
+ await self.send_success()
926
926
  else:
927
927
  self.send_failure()
928
928
  except PasswordChangeRequired as exc:
@@ -26,6 +26,7 @@ import codecs
26
26
  import inspect
27
27
  import re
28
28
  import signal as _signal
29
+ import sys
29
30
  from types import MappingProxyType
30
31
  from typing import TYPE_CHECKING, Any, AnyStr, Awaitable, Callable
31
32
  from typing import Dict, Generic, Iterable, List, Mapping, Optional
@@ -225,7 +226,13 @@ class SSHChannel(Generic[AnyStr], SSHPacketHandler):
225
226
  self._request_waiters = []
226
227
 
227
228
  if self._session is not None:
228
- self._session.connection_lost(exc)
229
+ # pylint: disable=broad-except
230
+ try:
231
+ self._session.connection_lost(exc)
232
+ except Exception:
233
+ self.logger.debug1('Uncaught exception in session ignored',
234
+ exc_info=sys.exc_info)
235
+
229
236
  self._session = None
230
237
 
231
238
  self._close_event.set()
@@ -2058,16 +2065,16 @@ class SSHTCPChannel(SSHForwardChannel, Generic[AnyStr]):
2058
2065
  SSHTCPSession[AnyStr]:
2059
2066
  """Create a new outbound TCP session"""
2060
2067
 
2061
- return (await self._open_tcp(session_factory, b'direct-tcpip',
2062
- host, port, orig_host, orig_port))
2068
+ return await self._open_tcp(session_factory, b'direct-tcpip',
2069
+ host, port, orig_host, orig_port)
2063
2070
 
2064
2071
  async def accept(self, session_factory: SSHTCPSessionFactory[AnyStr],
2065
2072
  host: str, port: int, orig_host: str,
2066
2073
  orig_port: int) -> SSHTCPSession[AnyStr]:
2067
2074
  """Create a new forwarded TCP session"""
2068
2075
 
2069
- return (await self._open_tcp(session_factory, b'forwarded-tcpip',
2070
- host, port, orig_host, orig_port))
2076
+ return await self._open_tcp(session_factory, b'forwarded-tcpip',
2077
+ host, port, orig_host, orig_port)
2071
2078
 
2072
2079
  def set_inbound_peer_names(self, dest_host: str, dest_port: int,
2073
2080
  orig_host: str, orig_port: int) -> None:
@@ -220,6 +220,15 @@ class SSHClient:
220
220
 
221
221
  """
222
222
 
223
+ def begin_auth(self, username: str) -> None:
224
+ """Begin client authentication
225
+
226
+ This method is called when client authentication is about to
227
+ begin, Applications may store the username passed here to
228
+ be used in future authentication callbacks.
229
+
230
+ """
231
+
223
232
  def auth_completed(self) -> None:
224
233
  """Authentication was completed successfully
225
234
 
@@ -41,6 +41,10 @@ from .pattern import HostPatternList, WildcardPatternList
41
41
  ConfigPaths = Union[None, FilePath, Sequence[FilePath]]
42
42
 
43
43
 
44
+ _token_pattern = re.compile(r'%(.)')
45
+ _env_pattern = re.compile(r'\${(.*)}')
46
+
47
+
44
48
  def _exec(cmd: str) -> bool:
45
49
  """Execute a command and return if exit status is 0"""
46
50
 
@@ -93,36 +97,38 @@ class SSHConfig:
93
97
 
94
98
  raise NotImplementedError
95
99
 
96
- def _expand_val(self, value: str) -> str:
97
- """Perform percent token expansion on a string"""
100
+ def _expand_token(self, match):
101
+ """Expand a percent token reference"""
98
102
 
99
- last_idx = 0
100
- result: List[str] = []
103
+ try:
104
+ token = match.group(1)
105
+ return self._tokens[token]
106
+ except KeyError:
107
+ if token == 'd':
108
+ raise ConfigParseError('Home directory is '
109
+ 'not available') from None
110
+ elif token == 'i':
111
+ raise ConfigParseError('User id not available') from None
112
+ else:
113
+ raise ConfigParseError('Invalid token expansion: ' +
114
+ token) from None
101
115
 
102
- for match in re.finditer(r'%', value):
103
- idx = match.start()
116
+ @staticmethod
117
+ def _expand_env(match):
118
+ """Expand an environment variable reference"""
104
119
 
105
- if idx < last_idx:
106
- continue
120
+ try:
121
+ var = match.group(1)
122
+ return os.environ[var]
123
+ except KeyError:
124
+ raise ConfigParseError('Invalid environment expansion: ' +
125
+ var) from None
107
126
 
108
- try:
109
- token = value[idx+1]
110
- result.extend([value[last_idx:idx], self._tokens[token]])
111
- last_idx = idx + 2
112
- except IndexError:
113
- raise ConfigParseError('Invalid token substitution') from None
114
- except KeyError:
115
- if token == 'd':
116
- raise ConfigParseError('Home directory is '
117
- 'not available') from None
118
- elif token == 'i':
119
- raise ConfigParseError('User id not available') from None
120
- else:
121
- raise ConfigParseError('Invalid token substitution: ' +
122
- value[idx+1]) from None
123
-
124
- result.append(value[last_idx:])
125
- return ''.join(result)
127
+ def _expand_val(self, value: str) -> str:
128
+ """Perform percent token and environment expansion on a string"""
129
+
130
+ return _env_pattern.sub(self._expand_env,
131
+ _token_pattern.sub(self._expand_token, value))
126
132
 
127
133
  def _include(self, option: str, args: List[str]) -> None:
128
134
  """Read config from a list of other config files"""
@@ -219,6 +225,22 @@ class SSHConfig:
219
225
  if option not in self._options:
220
226
  self._options[option] = value
221
227
 
228
+ def _set_bool_or_str(self, option: str, args: List[str]) -> None:
229
+ """Set a boolean or string config option"""
230
+
231
+ value_str = args.pop(0)
232
+ value_lower = value_str.lower()
233
+
234
+ if value_lower in ('yes', 'true'):
235
+ value: Union[bool, str] = True
236
+ elif value_lower in ('no', 'false'):
237
+ value = False
238
+ else:
239
+ value = value_str
240
+
241
+ if option not in self._options:
242
+ self._options[option] = value
243
+
222
244
  def _set_int(self, option: str, args: List[str]) -> None:
223
245
  """Set an integer config option"""
224
246
 
@@ -468,7 +490,7 @@ class SSHClientConfig(SSHConfig):
468
490
 
469
491
  _conditionals = {'host', 'match'}
470
492
  _no_split = {'proxycommand', 'remotecommand'}
471
- _percent_expand = {'CertificateFile', 'IdentityAgent',
493
+ _percent_expand = {'CertificateFile', 'ForwardAgent', 'IdentityAgent',
472
494
  'IdentityFile', 'ProxyCommand', 'RemoteCommand'}
473
495
 
474
496
  def __init__(self, last_config: 'SSHConfig', reload: bool,
@@ -587,7 +609,7 @@ class SSHClientConfig(SSHConfig):
587
609
  ('Compression', SSHConfig._set_bool),
588
610
  ('ConnectTimeout', SSHConfig._set_int),
589
611
  ('EnableSSHKeySign', SSHConfig._set_bool),
590
- ('ForwardAgent', SSHConfig._set_bool),
612
+ ('ForwardAgent', SSHConfig._set_bool_or_str),
591
613
  ('ForwardX11Trusted', SSHConfig._set_bool),
592
614
  ('GlobalKnownHostsFile', SSHConfig._set_string_list),
593
615
  ('GSSAPIAuthentication', SSHConfig._set_bool),