asyncssh 2.19.0__tar.gz → 2.20.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.20.0}/.github/workflows/run_tests.yml +3 -18
  2. {asyncssh-2.19.0/asyncssh.egg-info → asyncssh-2.20.0}/PKG-INFO +3 -3
  3. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/client.py +9 -0
  4. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/config.py +50 -28
  5. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/connection.py +22 -8
  6. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/misc.py +7 -6
  7. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/process.py +2 -2
  8. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/sftp.py +47 -14
  9. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/version.py +1 -1
  10. {asyncssh-2.19.0 → asyncssh-2.20.0/asyncssh.egg-info}/PKG-INFO +3 -3
  11. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/changes.rst +30 -0
  12. {asyncssh-2.19.0 → asyncssh-2.20.0}/pyproject.toml +1 -1
  13. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/server.py +16 -12
  14. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_channel.py +3 -3
  15. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_config.py +29 -4
  16. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_connection_auth.py +2 -1
  17. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_sftp.py +38 -1
  18. {asyncssh-2.19.0 → asyncssh-2.20.0}/tox.ini +3 -3
  19. {asyncssh-2.19.0 → asyncssh-2.20.0}/.coveragerc +0 -0
  20. {asyncssh-2.19.0 → asyncssh-2.20.0}/.gitignore +0 -0
  21. {asyncssh-2.19.0 → asyncssh-2.20.0}/.readthedocs.yaml +0 -0
  22. {asyncssh-2.19.0 → asyncssh-2.20.0}/CONTRIBUTING.rst +0 -0
  23. {asyncssh-2.19.0 → asyncssh-2.20.0}/COPYRIGHT +0 -0
  24. {asyncssh-2.19.0 → asyncssh-2.20.0}/LICENSE +0 -0
  25. {asyncssh-2.19.0 → asyncssh-2.20.0}/MANIFEST.in +0 -0
  26. {asyncssh-2.19.0 → asyncssh-2.20.0}/README.rst +0 -0
  27. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/__init__.py +0 -0
  28. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/agent.py +0 -0
  29. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/agent_unix.py +0 -0
  30. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/agent_win32.py +0 -0
  31. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/asn1.py +0 -0
  32. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/auth.py +0 -0
  33. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/auth_keys.py +0 -0
  34. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/channel.py +0 -0
  35. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/compression.py +0 -0
  36. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/constants.py +0 -0
  37. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/__init__.py +0 -0
  38. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/chacha.py +0 -0
  39. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/cipher.py +0 -0
  40. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/dh.py +0 -0
  41. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/dsa.py +0 -0
  42. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/ec.py +0 -0
  43. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/ec_params.py +0 -0
  44. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/ed.py +0 -0
  45. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/kdf.py +0 -0
  46. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/misc.py +0 -0
  47. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/pq.py +0 -0
  48. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/rsa.py +0 -0
  49. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/umac.py +0 -0
  50. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/crypto/x509.py +0 -0
  51. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/dsa.py +0 -0
  52. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/ecdsa.py +0 -0
  53. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/eddsa.py +0 -0
  54. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/editor.py +0 -0
  55. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/encryption.py +0 -0
  56. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/forward.py +0 -0
  57. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/gss.py +0 -0
  58. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/gss_unix.py +0 -0
  59. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/gss_win32.py +0 -0
  60. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/kex.py +0 -0
  61. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/kex_dh.py +0 -0
  62. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/kex_rsa.py +0 -0
  63. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/keysign.py +0 -0
  64. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/known_hosts.py +0 -0
  65. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/listener.py +0 -0
  66. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/logging.py +0 -0
  67. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/mac.py +0 -0
  68. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/packet.py +0 -0
  69. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/pattern.py +0 -0
  70. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/pbe.py +0 -0
  71. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/pkcs11.py +0 -0
  72. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/public_key.py +0 -0
  73. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/py.typed +0 -0
  74. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/rsa.py +0 -0
  75. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/saslprep.py +0 -0
  76. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/scp.py +0 -0
  77. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/server.py +0 -0
  78. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/session.py +0 -0
  79. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/sk.py +0 -0
  80. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/sk_ecdsa.py +0 -0
  81. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/sk_eddsa.py +0 -0
  82. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/socks.py +0 -0
  83. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/stream.py +0 -0
  84. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/subprocess.py +0 -0
  85. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/tuntap.py +0 -0
  86. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh/x11.py +0 -0
  87. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh.egg-info/SOURCES.txt +0 -0
  88. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh.egg-info/dependency_links.txt +0 -0
  89. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh.egg-info/requires.txt +0 -0
  90. {asyncssh-2.19.0 → asyncssh-2.20.0}/asyncssh.egg-info/top_level.txt +0 -0
  91. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/_templates/sidebarbottom.html +0 -0
  92. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/_templates/sidebartop.html +0 -0
  93. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/api.rst +0 -0
  94. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/conf.py +0 -0
  95. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/contributing.rst +0 -0
  96. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/index.rst +0 -0
  97. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/requirements.txt +0 -0
  98. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/rftheme/layout.html +0 -0
  99. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/rftheme/static/rftheme.css_t +0 -0
  100. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/rftheme/theme.conf +0 -0
  101. {asyncssh-2.19.0 → asyncssh-2.20.0}/docs/rtd-req.txt +0 -0
  102. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/callback_client.py +0 -0
  103. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/callback_client2.py +0 -0
  104. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/callback_client3.py +0 -0
  105. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/callback_math_server.py +0 -0
  106. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/chat_server.py +0 -0
  107. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/check_exit_status.py +0 -0
  108. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/chroot_sftp_server.py +0 -0
  109. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/direct_client.py +0 -0
  110. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/direct_server.py +0 -0
  111. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/editor.py +0 -0
  112. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/gather_results.py +0 -0
  113. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/listening_client.py +0 -0
  114. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/local_forwarding_client.py +0 -0
  115. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/local_forwarding_client2.py +0 -0
  116. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/local_forwarding_server.py +0 -0
  117. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/math_client.py +0 -0
  118. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/math_server.py +0 -0
  119. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/redirect_input.py +0 -0
  120. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/redirect_local_pipe.py +0 -0
  121. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/redirect_remote_pipe.py +0 -0
  122. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/redirect_server.py +0 -0
  123. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/remote_forwarding_client.py +0 -0
  124. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/remote_forwarding_client2.py +0 -0
  125. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/remote_forwarding_server.py +0 -0
  126. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/reverse_client.py +0 -0
  127. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/reverse_server.py +0 -0
  128. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/scp_client.py +0 -0
  129. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/set_environment.py +0 -0
  130. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/set_terminal.py +0 -0
  131. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/sftp_client.py +0 -0
  132. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/show_environment.py +0 -0
  133. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/show_terminal.py +0 -0
  134. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/simple_cert_server.py +0 -0
  135. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/simple_client.py +0 -0
  136. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/simple_keyed_server.py +0 -0
  137. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/simple_scp_server.py +0 -0
  138. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/simple_server.py +0 -0
  139. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/simple_sftp_server.py +0 -0
  140. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/stream_direct_client.py +0 -0
  141. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/stream_direct_server.py +0 -0
  142. {asyncssh-2.19.0 → asyncssh-2.20.0}/examples/stream_listening_client.py +0 -0
  143. {asyncssh-2.19.0 → asyncssh-2.20.0}/mypy.ini +0 -0
  144. {asyncssh-2.19.0 → asyncssh-2.20.0}/pylintrc +0 -0
  145. {asyncssh-2.19.0 → asyncssh-2.20.0}/setup.cfg +0 -0
  146. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/__init__.py +0 -0
  147. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/gss_stub.py +0 -0
  148. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/gssapi_stub.py +0 -0
  149. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/keysign_stub.py +0 -0
  150. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/pkcs11_stub.py +0 -0
  151. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/sk_stub.py +0 -0
  152. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/sspi_stub.py +0 -0
  153. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_agent.py +0 -0
  154. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_asn1.py +0 -0
  155. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_auth.py +0 -0
  156. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_auth_keys.py +0 -0
  157. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_compression.py +0 -0
  158. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_connection.py +0 -0
  159. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_editor.py +0 -0
  160. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_encryption.py +0 -0
  161. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_forward.py +0 -0
  162. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_kex.py +0 -0
  163. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_known_hosts.py +0 -0
  164. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_logging.py +0 -0
  165. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_mac.py +0 -0
  166. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_packet.py +0 -0
  167. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_pkcs11.py +0 -0
  168. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_process.py +0 -0
  169. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_public_key.py +0 -0
  170. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_saslprep.py +0 -0
  171. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_sk.py +0 -0
  172. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_stream.py +0 -0
  173. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_subprocess.py +0 -0
  174. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_tuntap.py +0 -0
  175. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_x11.py +0 -0
  176. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/test_x509.py +0 -0
  177. {asyncssh-2.19.0 → asyncssh-2.20.0}/tests/util.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.2
2
2
  Name: asyncssh
3
- Version: 2.19.0
3
+ Version: 2.20.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
@@ -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),
@@ -1741,7 +1741,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1741
1741
  self._send_kexinit()
1742
1742
  self._kexinit_sent = True
1743
1743
 
1744
- if (((pkttype in {MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT} or
1744
+ if (((pkttype in {MSG_DEBUG, MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT} or
1745
1745
  pkttype > MSG_KEX_LAST) and not self._kex_complete) or
1746
1746
  (pkttype == MSG_USERAUTH_BANNER and
1747
1747
  not (self._auth_in_progress or self._auth_complete)) or
@@ -1751,7 +1751,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1751
1751
 
1752
1752
  # If we're encrypting and we have no data outstanding, insert an
1753
1753
  # ignore packet into the stream
1754
- if self._send_encryption and pkttype not in (MSG_IGNORE, MSG_EXT_INFO):
1754
+ if self._send_encryption and pkttype > MSG_KEX_LAST:
1755
1755
  self.send_packet(MSG_IGNORE, String(b''))
1756
1756
 
1757
1757
  orig_payload = Byte(pkttype) + b''.join(args)
@@ -2292,6 +2292,9 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2292
2292
 
2293
2293
  self._auth_in_progress = True
2294
2294
 
2295
+ if self._owner: # pragma: no branch
2296
+ self._owner.begin_auth(self._username)
2297
+
2295
2298
  # This method is only in SSHClientConnection
2296
2299
  # pylint: disable=no-member
2297
2300
  cast('SSHClientConnection', self).try_next_auth()
@@ -7640,8 +7643,11 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
7640
7643
  made available for use. This is the default.
7641
7644
  :param agent_forwarding: (optional)
7642
7645
  Whether or not to allow forwarding of ssh-agent requests from
7643
- processes running on the server. By default, ssh-agent forwarding
7644
- requests from the server are not allowed.
7646
+ processes running on the server. This argument can also be set
7647
+ to the path of a UNIX domain socket in cases where forwarded
7648
+ agent requests should be sent to a different path than client
7649
+ agent requests. By default, forwarding ssh-agent requests from
7650
+ the server is not allowed.
7645
7651
  :param pkcs11_provider: (optional)
7646
7652
  The path of a shared library which should be used as a PKCS#11
7647
7653
  provider for accessing keys on PIV security tokens. By default,
@@ -7874,7 +7880,7 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
7874
7880
  :type agent_path: `str`
7875
7881
  :type agent_identities:
7876
7882
  *see* :ref:`SpecifyingPublicKeys` and :ref:`SpecifyingCertificates`
7877
- :type agent_forwarding: `bool`
7883
+ :type agent_forwarding: `bool` or `str`
7878
7884
  :type pkcs11_provider: `str` or `None`
7879
7885
  :type pkcs11_pin: `str`
7880
7886
  :type client_version: `str`
@@ -8016,7 +8022,7 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
8016
8022
  disable_trivial_auth: bool = False,
8017
8023
  agent_path: DefTuple[Optional[str]] = (),
8018
8024
  agent_identities: DefTuple[Optional[IdentityListArg]] = (),
8019
- agent_forwarding: DefTuple[bool] = (),
8025
+ agent_forwarding: DefTuple[Union[bool, str]] = (),
8020
8026
  pkcs11_provider: DefTuple[Optional[str]] = (),
8021
8027
  pkcs11_pin: Optional[str] = None,
8022
8028
  command: DefTuple[Optional[str]] = (),
@@ -8242,9 +8248,17 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
8242
8248
  self.pkcs11_pin = None
8243
8249
 
8244
8250
  if agent_forwarding == ():
8245
- agent_forwarding = cast(bool, config.get('ForwardAgent', False))
8251
+ agent_forwarding = cast(Union[bool, str],
8252
+ config.get('ForwardAgent', False))
8253
+
8254
+ agent_forwarding: Union[bool, str]
8246
8255
 
8247
- self.agent_forward_path = agent_path if agent_forwarding else None
8256
+ if not agent_forwarding:
8257
+ self.agent_forward_path = None
8258
+ elif agent_forwarding is True:
8259
+ self.agent_forward_path = agent_path
8260
+ else:
8261
+ self.agent_forward_path = agent_forwarding
8248
8262
 
8249
8263
  self.command = cast(Optional[str], command if command != () else
8250
8264
  config.get('RemoteCommand'))
@@ -466,17 +466,18 @@ class Options:
466
466
  class _RecordMeta(type):
467
467
  """Metaclass for general-purpose record type"""
468
468
 
469
+ __slots__: Dict[str, object] = {}
470
+
469
471
  def __new__(mcs: Type['_RecordMeta'], name: str, bases: Tuple[type, ...],
470
472
  ns: Dict[str, object]) -> '_RecordMeta':
473
+ cls = cast(_RecordMeta, super().__new__(mcs, name, bases, ns))
474
+
471
475
  if name != 'Record':
472
- fields = cast(Mapping[str, str],
473
- ns.get('__annotations__', {})).keys()
476
+ fields = cast(Mapping[str, str], cls.__annotations__.keys())
474
477
  defaults = {k: ns.get(k) for k in fields}
478
+ cls.__slots__ = defaults
475
479
 
476
- ns = {k: v for k, v in ns.items() if k not in fields}
477
- ns['__slots__'] = defaults
478
-
479
- return cast(_RecordMeta, super().__new__(mcs, name, bases, ns))
480
+ return cls
480
481
 
481
482
 
482
483
  class Record(metaclass=_RecordMeta):
@@ -928,7 +928,7 @@ class SSHProcess(SSHStreamSession, Generic[AnyStr]):
928
928
  file = source
929
929
 
930
930
  if hasattr(file, 'read') and \
931
- (asyncio.iscoroutinefunction(file.read) or
931
+ (inspect.iscoroutinefunction(file.read) or
932
932
  inspect.isgeneratorfunction(file.read)):
933
933
  reader = _AsyncFileReader(self, cast(_AsyncFileProtocol, file),
934
934
  bufsize, datatype, self._encoding,
@@ -997,7 +997,7 @@ class SSHProcess(SSHStreamSession, Generic[AnyStr]):
997
997
  needs_close = recv_eof
998
998
 
999
999
  if hasattr(file, 'write') and \
1000
- (asyncio.iscoroutinefunction(file.write) or
1000
+ (inspect.iscoroutinefunction(file.write) or
1001
1001
  inspect.isgeneratorfunction(file.write)):
1002
1002
  writer = _AsyncFileWriter(
1003
1003
  self, cast(_AsyncFileProtocol, file), needs_close,
@@ -3165,7 +3165,6 @@ class SFTPClientFile:
3165
3165
  self._appending = appending
3166
3166
  self._encoding = encoding
3167
3167
  self._errors = errors
3168
- self._max_requests = max_requests
3169
3168
  self._offset = None if appending else 0
3170
3169
 
3171
3170
  self.read_len = \
@@ -3173,6 +3172,15 @@ class SFTPClientFile:
3173
3172
  self.write_len = \
3174
3173
  handler.limits.max_write_len if block_size == -1 else block_size
3175
3174
 
3175
+ if max_requests <= 0:
3176
+ if self.read_len:
3177
+ max_requests = max(16, min(MAX_SFTP_READ_LEN //
3178
+ self.read_len, 128))
3179
+ else:
3180
+ max_requests = 1
3181
+
3182
+ self._max_requests = max_requests
3183
+
3176
3184
  async def __aenter__(self) -> Self:
3177
3185
  """Allow SFTPClientFile to be used as an async context manager"""
3178
3186
 
@@ -3859,6 +3867,9 @@ class SFTPClient:
3859
3867
  block_size = min(srcfs.limits.max_read_len,
3860
3868
  dstfs.limits.max_write_len)
3861
3869
 
3870
+ if max_requests <= 0:
3871
+ max_requests = max(16, min(MAX_SFTP_READ_LEN // block_size, 128))
3872
+
3862
3873
  if isinstance(srcpaths, (bytes, str, PurePath)):
3863
3874
  srcpaths = [srcpaths]
3864
3875
  elif not isinstance(srcpaths, list):
@@ -3916,7 +3927,7 @@ class SFTPClient:
3916
3927
  localpath: Optional[_SFTPPath] = None, *,
3917
3928
  preserve: bool = False, recurse: bool = False,
3918
3929
  follow_symlinks: bool = False, block_size: int = -1,
3919
- max_requests: int = _MAX_SFTP_REQUESTS,
3930
+ max_requests: int = -1,
3920
3931
  progress_handler: SFTPProgressHandler = None,
3921
3932
  error_handler: SFTPErrorHandler = None) -> None:
3922
3933
  """Download remote files
@@ -3957,7 +3968,9 @@ class SFTPClient:
3957
3968
  doesn't advertise limits.
3958
3969
 
3959
3970
  The max_requests argument specifies the maximum number of
3960
- parallel read or write requests issued, defaulting to 128.
3971
+ parallel read or write requests issued, defaulting to a
3972
+ value between 16 and 128 depending on the selected block
3973
+ size to avoid excessive memory usage.
3961
3974
 
3962
3975
  If progress_handler is specified, it will be called after
3963
3976
  each block of a file is successfully downloaded. The arguments
@@ -4022,7 +4035,7 @@ class SFTPClient:
4022
4035
  remotepath: Optional[_SFTPPath] = None, *,
4023
4036
  preserve: bool = False, recurse: bool = False,
4024
4037
  follow_symlinks: bool = False, block_size: int = -1,
4025
- max_requests: int = _MAX_SFTP_REQUESTS,
4038
+ max_requests: int = -1,
4026
4039
  progress_handler: SFTPProgressHandler = None,
4027
4040
  error_handler: SFTPErrorHandler = None) -> None:
4028
4041
  """Upload local files
@@ -4063,7 +4076,9 @@ class SFTPClient:
4063
4076
  doesn't advertise limits.
4064
4077
 
4065
4078
  The max_requests argument specifies the maximum number of
4066
- parallel read or write requests issued, defaulting to 128.
4079
+ parallel read or write requests issued, defaulting to a
4080
+ value between 16 and 128 depending on the selected block
4081
+ size to avoid excessive memory usage.
4067
4082
 
4068
4083
  If progress_handler is specified, it will be called after
4069
4084
  each block of a file is successfully uploaded. The arguments
@@ -4128,7 +4143,7 @@ class SFTPClient:
4128
4143
  dstpath: Optional[_SFTPPath] = None, *,
4129
4144
  preserve: bool = False, recurse: bool = False,
4130
4145
  follow_symlinks: bool = False, block_size: int = -1,
4131
- max_requests: int = _MAX_SFTP_REQUESTS,
4146
+ max_requests: int = -1,
4132
4147
  progress_handler: SFTPProgressHandler = None,
4133
4148
  error_handler: SFTPErrorHandler = None,
4134
4149
  remote_only: bool = False) -> None:
@@ -4170,7 +4185,9 @@ class SFTPClient:
4170
4185
  doesn't advertise limits.
4171
4186
 
4172
4187
  The max_requests argument specifies the maximum number of
4173
- parallel read or write requests issued, defaulting to 128.
4188
+ parallel read or write requests issued, defaulting to a
4189
+ value between 16 and 128 depending on the selected block
4190
+ size to avoid excessive memory usage.
4174
4191
 
4175
4192
  If progress_handler is specified, it will be called after
4176
4193
  each block of a file is successfully copied. The arguments
@@ -4238,7 +4255,7 @@ class SFTPClient:
4238
4255
  localpath: Optional[_SFTPPath] = None, *,
4239
4256
  preserve: bool = False, recurse: bool = False,
4240
4257
  follow_symlinks: bool = False, block_size: int = -1,
4241
- max_requests: int = _MAX_SFTP_REQUESTS,
4258
+ max_requests: int = -1,
4242
4259
  progress_handler: SFTPProgressHandler = None,
4243
4260
  error_handler: SFTPErrorHandler = None) -> None:
4244
4261
  """Download remote files with glob pattern match
@@ -4261,7 +4278,7 @@ class SFTPClient:
4261
4278
  remotepath: Optional[_SFTPPath] = None, *,
4262
4279
  preserve: bool = False, recurse: bool = False,
4263
4280
  follow_symlinks: bool = False, block_size: int = -1,
4264
- max_requests: int = _MAX_SFTP_REQUESTS,
4281
+ max_requests: int = -1,
4265
4282
  progress_handler: SFTPProgressHandler = None,
4266
4283
  error_handler: SFTPErrorHandler = None) -> None:
4267
4284
  """Upload local files with glob pattern match
@@ -4284,7 +4301,7 @@ class SFTPClient:
4284
4301
  dstpath: Optional[_SFTPPath] = None, *,
4285
4302
  preserve: bool = False, recurse: bool = False,
4286
4303
  follow_symlinks: bool = False, block_size: int = -1,
4287
- max_requests: int = _MAX_SFTP_REQUESTS,
4304
+ max_requests: int = -1,
4288
4305
  progress_handler: SFTPProgressHandler = None,
4289
4306
  error_handler: SFTPErrorHandler = None,
4290
4307
  remote_only: bool = False) -> None:
@@ -4586,7 +4603,7 @@ class SFTPClient:
4586
4603
  attrs: SFTPAttrs = SFTPAttrs(),
4587
4604
  encoding: Optional[str] = 'utf-8', errors: str = 'strict',
4588
4605
  block_size: int = -1,
4589
- max_requests: int = _MAX_SFTP_REQUESTS) -> SFTPClientFile:
4606
+ max_requests: int = -1) -> SFTPClientFile:
4590
4607
  """Open a remote file
4591
4608
 
4592
4609
  This method opens a remote file and returns an
@@ -4662,7 +4679,9 @@ class SFTPClient:
4662
4679
  default of using the server-advertised limits.
4663
4680
 
4664
4681
  The max_requests argument specifies the maximum number of
4665
- parallel read or write requests issued, defaulting to 128.
4682
+ parallel read or write requests issued, defaulting to a
4683
+ value between 16 and 128 depending on the selected block
4684
+ size to avoid excessive memory usage.
4666
4685
 
4667
4686
  :param path:
4668
4687
  The name of the remote file to open
@@ -4718,7 +4737,7 @@ class SFTPClient:
4718
4737
  attrs: SFTPAttrs = SFTPAttrs(),
4719
4738
  encoding: Optional[str] = 'utf-8', errors: str = 'strict',
4720
4739
  block_size: int = -1,
4721
- max_requests: int = _MAX_SFTP_REQUESTS) -> SFTPClientFile:
4740
+ max_requests: int = -1) -> SFTPClientFile:
4722
4741
  """Open a remote file using SFTP v5/v6 flags
4723
4742
 
4724
4743
  This method is very similar to :meth:`open`, but the pflags_or_mode
@@ -7515,6 +7534,14 @@ class SFTPServer:
7515
7534
  """
7516
7535
 
7517
7536
  path = os.readlink(_to_local_path(self.map_path(path)))
7537
+
7538
+ if sys.platform == 'win32' and \
7539
+ path.startswith('\\\\?\\'): # pragma: no cover
7540
+ path = path[4:]
7541
+
7542
+ if self._chroot:
7543
+ path = os.path.realpath(path)
7544
+
7518
7545
  return self.reverse_map_path(_from_local_path(path))
7519
7546
 
7520
7547
  def symlink(self, oldpath: bytes, newpath: bytes) -> MaybeAwait[None]:
@@ -7780,7 +7807,13 @@ class LocalFS:
7780
7807
  async def readlink(self, path: bytes) -> bytes:
7781
7808
  """Return the target of a local symbolic link"""
7782
7809
 
7783
- return _from_local_path(os.readlink(_to_local_path(path)))
7810
+ path = os.readlink(_to_local_path(path))
7811
+
7812
+ if sys.platform == 'win32' and \
7813
+ path.startswith('\\\\?\\'): # pragma: no cover
7814
+ path = path[4:]
7815
+
7816
+ return _from_local_path(path)
7784
7817
 
7785
7818
  async def symlink(self, oldpath: bytes, newpath: bytes) -> None:
7786
7819
  """Create a local symbolic link"""
@@ -26,4 +26,4 @@ __author_email__ = 'ronf@timeheart.net'
26
26
 
27
27
  __url__ = 'http://asyncssh.timeheart.net'
28
28
 
29
- __version__ = '2.19.0'
29
+ __version__ = '2.20.0'
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: asyncssh
3
- Version: 2.19.0
3
+ Version: 2.20.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
@@ -3,6 +3,36 @@
3
3
  Change Log
4
4
  ==========
5
5
 
6
+ Release 2.20.0 (17 Feb 2025)
7
+ ----------------------------
8
+
9
+ * Added support for specifying an explicit path when configuring
10
+ agent forwarding. Thanks go to Aleksandr Ilin for pointing out
11
+ that this options supports more than just a boolean value.
12
+
13
+ * Added support for environment variable expansion in SSH config,
14
+ for options which support percent expansion.
15
+
16
+ * Added a new begin_auth callback in SSHClient, reporting the
17
+ username being sent during SSH client authentication. This can be
18
+ useful when the user is conditionally set via an SSH config file.
19
+
20
+ * Improved strict-kex interoperability during re-keying. Thanks go
21
+ to GitHub user emeryalden for reporting this issue and helping
22
+ to track down the source of the problem.
23
+
24
+ * Updated SFTP max_requests default to reduce memory usage when
25
+ using large block sizes.
26
+
27
+ * Updated testing to add Python 3.13 and drop Python 3.7, avoiding
28
+ deprecation warnings from the cryptography package.
29
+
30
+ * Fixed unit test issues under Windows, allowing unit tests to run
31
+ on Windows on all supported versions of Python.
32
+
33
+ * Fixed a couple of issues with Python 3.14. Thanks go to Georg
34
+ Sauthoff for initially reporting this.
35
+
6
36
  Release 2.19.0 (12 Dec 2024)
7
37
  ----------------------------
8
38
 
@@ -15,12 +15,12 @@ classifiers = [
15
15
  'License :: OSI Approved',
16
16
  'Operating System :: MacOS :: MacOS X',
17
17
  'Operating System :: POSIX',
18
- 'Programming Language :: Python :: 3.7',
19
18
  'Programming Language :: Python :: 3.8',
20
19
  'Programming Language :: Python :: 3.9',
21
20
  'Programming Language :: Python :: 3.10',
22
21
  'Programming Language :: Python :: 3.11',
23
22
  'Programming Language :: Python :: 3.12',
23
+ 'Programming Language :: Python :: 3.13',
24
24
  'Topic :: Internet',
25
25
  'Topic :: Security :: Cryptography',
26
26
  'Topic :: Software Development :: Libraries :: Python Modules',
@@ -26,6 +26,7 @@ import shutil
26
26
  import signal
27
27
  import socket
28
28
  import subprocess
29
+ import sys
29
30
 
30
31
  import asyncssh
31
32
  from asyncssh.misc import async_context_manager
@@ -269,17 +270,20 @@ class ServerTestCase(AsyncTestCase):
269
270
  if 'XAUTHORITY' in os.environ: # pragma: no cover
270
271
  del os.environ['XAUTHORITY']
271
272
 
272
- try:
273
- output = run('ssh-agent -a agent 2>/dev/null')
274
- except subprocess.CalledProcessError: # pragma: no cover
275
- cls._agent_pid = None
276
- else:
277
- cls._agent_pid = int(output.splitlines()[2].split()[3][:-1])
273
+ if sys.platform != 'win32':
274
+ try:
275
+ output = run('ssh-agent -a agent 2>/dev/null')
276
+ except subprocess.CalledProcessError: # pragma: no cover
277
+ cls._agent_pid = None
278
+ else:
279
+ cls._agent_pid = int(output.splitlines()[2].split()[3][:-1])
278
280
 
279
- os.environ['SSH_AUTH_SOCK'] = 'agent'
281
+ os.environ['SSH_AUTH_SOCK'] = 'agent'
280
282
 
281
- async with asyncssh.connect_agent() as agent:
282
- await agent.add_keys([ckey_ecdsa, (ckey, ckey_cert)])
283
+ async with asyncssh.connect_agent() as agent:
284
+ await agent.add_keys([ckey_ecdsa, (ckey, ckey_cert)])
285
+ else: # pragma: no cover
286
+ cls._agent_pid = None
283
287
 
284
288
  with open('ssh-keysign', 'wb'):
285
289
  pass
@@ -288,14 +292,14 @@ class ServerTestCase(AsyncTestCase):
288
292
  async def asyncTearDownClass(cls):
289
293
  """Shut down test server and agent"""
290
294
 
295
+ cls._server.close()
296
+ await cls._server.wait_closed()
297
+
291
298
  tasks = all_tasks()
292
299
  tasks.remove(current_task())
293
300
 
294
301
  await asyncio.gather(*tasks, return_exceptions=True)
295
302
 
296
- cls._server.close()
297
- await cls._server.wait_closed()
298
-
299
303
  if cls._agent_pid: # pragma: no branch
300
304
  os.kill(cls._agent_pid, signal.SIGTERM)
301
305
 
@@ -862,14 +862,14 @@ class _TestChannel(ServerTestCase):
862
862
  chan.close()
863
863
 
864
864
  @asynctest
865
- async def test_agent_forwarding(self):
866
- """Test SSH agent forwarding"""
865
+ async def test_agent_forwarding_explicit(self):
866
+ """Test SSH agent forwarding with explicit path"""
867
867
 
868
868
  if not self.agent_available(): # pragma: no cover
869
869
  self.skipTest('ssh-agent not available')
870
870
 
871
871
  async with self.connect(username='ckey',
872
- agent_forwarding=True) as conn:
872
+ agent_forwarding='agent') as conn:
873
873
  chan, session = await _create_session(conn, 'agent')
874
874
 
875
875
  await chan.wait_closed()