asyncssh 2.20.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.20.0/asyncssh.egg-info → asyncssh-2.21.0}/PKG-INFO +3 -2
  2. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/auth.py +12 -12
  3. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/channel.py +12 -5
  4. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/connection.py +112 -23
  5. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/kex_dh.py +17 -1
  6. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/misc.py +25 -1
  7. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/scp.py +61 -27
  8. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/server.py +54 -21
  9. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sftp.py +360 -117
  10. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/stream.py +3 -5
  11. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/version.py +1 -1
  12. {asyncssh-2.20.0 → asyncssh-2.21.0/asyncssh.egg-info}/PKG-INFO +3 -2
  13. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/changes.rst +38 -0
  14. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_auth.py +2 -2
  15. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_channel.py +16 -0
  16. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_connection.py +18 -0
  17. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_connection_auth.py +4 -1
  18. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_forward.py +63 -6
  19. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_kex.py +44 -11
  20. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_sftp.py +118 -7
  21. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_stream.py +33 -1
  22. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_tuntap.py +59 -1
  23. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/util.py +0 -2
  24. {asyncssh-2.20.0 → asyncssh-2.21.0}/.coveragerc +0 -0
  25. {asyncssh-2.20.0 → asyncssh-2.21.0}/.github/workflows/run_tests.yml +0 -0
  26. {asyncssh-2.20.0 → asyncssh-2.21.0}/.gitignore +0 -0
  27. {asyncssh-2.20.0 → asyncssh-2.21.0}/.readthedocs.yaml +0 -0
  28. {asyncssh-2.20.0 → asyncssh-2.21.0}/CONTRIBUTING.rst +0 -0
  29. {asyncssh-2.20.0 → asyncssh-2.21.0}/COPYRIGHT +0 -0
  30. {asyncssh-2.20.0 → asyncssh-2.21.0}/LICENSE +0 -0
  31. {asyncssh-2.20.0 → asyncssh-2.21.0}/MANIFEST.in +0 -0
  32. {asyncssh-2.20.0 → asyncssh-2.21.0}/README.rst +0 -0
  33. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/__init__.py +0 -0
  34. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/agent.py +0 -0
  35. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/agent_unix.py +0 -0
  36. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/agent_win32.py +0 -0
  37. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/asn1.py +0 -0
  38. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/auth_keys.py +0 -0
  39. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/client.py +0 -0
  40. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/compression.py +0 -0
  41. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/config.py +0 -0
  42. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/constants.py +0 -0
  43. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/__init__.py +0 -0
  44. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/chacha.py +0 -0
  45. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/cipher.py +0 -0
  46. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/dh.py +0 -0
  47. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/dsa.py +0 -0
  48. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/ec.py +0 -0
  49. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/ec_params.py +0 -0
  50. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/ed.py +0 -0
  51. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/kdf.py +0 -0
  52. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/misc.py +0 -0
  53. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/pq.py +0 -0
  54. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/rsa.py +0 -0
  55. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/umac.py +0 -0
  56. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/x509.py +0 -0
  57. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/dsa.py +0 -0
  58. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/ecdsa.py +0 -0
  59. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/eddsa.py +0 -0
  60. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/editor.py +0 -0
  61. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/encryption.py +0 -0
  62. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/forward.py +0 -0
  63. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/gss.py +0 -0
  64. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/gss_unix.py +0 -0
  65. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/gss_win32.py +0 -0
  66. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/kex.py +0 -0
  67. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/kex_rsa.py +0 -0
  68. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/keysign.py +0 -0
  69. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/known_hosts.py +0 -0
  70. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/listener.py +0 -0
  71. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/logging.py +0 -0
  72. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/mac.py +0 -0
  73. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/packet.py +0 -0
  74. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/pattern.py +0 -0
  75. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/pbe.py +0 -0
  76. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/pkcs11.py +0 -0
  77. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/process.py +0 -0
  78. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/public_key.py +0 -0
  79. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/py.typed +0 -0
  80. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/rsa.py +0 -0
  81. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/saslprep.py +0 -0
  82. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/session.py +0 -0
  83. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sk.py +0 -0
  84. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sk_ecdsa.py +0 -0
  85. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sk_eddsa.py +0 -0
  86. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/socks.py +0 -0
  87. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/subprocess.py +0 -0
  88. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/tuntap.py +0 -0
  89. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/x11.py +0 -0
  90. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/SOURCES.txt +0 -0
  91. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/dependency_links.txt +0 -0
  92. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/requires.txt +0 -0
  93. {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/top_level.txt +0 -0
  94. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/_templates/sidebarbottom.html +0 -0
  95. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/_templates/sidebartop.html +0 -0
  96. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/api.rst +0 -0
  97. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/conf.py +0 -0
  98. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/contributing.rst +0 -0
  99. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/index.rst +0 -0
  100. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/requirements.txt +0 -0
  101. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rftheme/layout.html +0 -0
  102. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rftheme/static/rftheme.css_t +0 -0
  103. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rftheme/theme.conf +0 -0
  104. {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rtd-req.txt +0 -0
  105. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_client.py +0 -0
  106. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_client2.py +0 -0
  107. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_client3.py +0 -0
  108. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_math_server.py +0 -0
  109. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/chat_server.py +0 -0
  110. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/check_exit_status.py +0 -0
  111. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/chroot_sftp_server.py +0 -0
  112. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/direct_client.py +0 -0
  113. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/direct_server.py +0 -0
  114. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/editor.py +0 -0
  115. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/gather_results.py +0 -0
  116. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/listening_client.py +0 -0
  117. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/local_forwarding_client.py +0 -0
  118. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/local_forwarding_client2.py +0 -0
  119. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/local_forwarding_server.py +0 -0
  120. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/math_client.py +0 -0
  121. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/math_server.py +0 -0
  122. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_input.py +0 -0
  123. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_local_pipe.py +0 -0
  124. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_remote_pipe.py +0 -0
  125. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_server.py +0 -0
  126. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/remote_forwarding_client.py +0 -0
  127. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/remote_forwarding_client2.py +0 -0
  128. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/remote_forwarding_server.py +0 -0
  129. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/reverse_client.py +0 -0
  130. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/reverse_server.py +0 -0
  131. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/scp_client.py +0 -0
  132. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/set_environment.py +0 -0
  133. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/set_terminal.py +0 -0
  134. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/sftp_client.py +0 -0
  135. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/show_environment.py +0 -0
  136. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/show_terminal.py +0 -0
  137. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_cert_server.py +0 -0
  138. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_client.py +0 -0
  139. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_keyed_server.py +0 -0
  140. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_scp_server.py +0 -0
  141. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_server.py +0 -0
  142. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_sftp_server.py +0 -0
  143. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/stream_direct_client.py +0 -0
  144. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/stream_direct_server.py +0 -0
  145. {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/stream_listening_client.py +0 -0
  146. {asyncssh-2.20.0 → asyncssh-2.21.0}/mypy.ini +0 -0
  147. {asyncssh-2.20.0 → asyncssh-2.21.0}/pylintrc +0 -0
  148. {asyncssh-2.20.0 → asyncssh-2.21.0}/pyproject.toml +0 -0
  149. {asyncssh-2.20.0 → asyncssh-2.21.0}/setup.cfg +0 -0
  150. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/__init__.py +0 -0
  151. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/gss_stub.py +0 -0
  152. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/gssapi_stub.py +0 -0
  153. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/keysign_stub.py +0 -0
  154. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/pkcs11_stub.py +0 -0
  155. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/server.py +0 -0
  156. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/sk_stub.py +0 -0
  157. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/sspi_stub.py +0 -0
  158. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_agent.py +0 -0
  159. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_asn1.py +0 -0
  160. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_auth_keys.py +0 -0
  161. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_compression.py +0 -0
  162. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_config.py +0 -0
  163. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_editor.py +0 -0
  164. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_encryption.py +0 -0
  165. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_known_hosts.py +0 -0
  166. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_logging.py +0 -0
  167. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_mac.py +0 -0
  168. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_packet.py +0 -0
  169. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_pkcs11.py +0 -0
  170. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_process.py +0 -0
  171. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_public_key.py +0 -0
  172. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_saslprep.py +0 -0
  173. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_sk.py +0 -0
  174. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_subprocess.py +0 -0
  175. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_x11.py +0 -0
  176. {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_x509.py +0 -0
  177. {asyncssh-2.20.0 → asyncssh-2.21.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: asyncssh
3
- Version: 2.20.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
@@ -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:
@@ -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
@@ -1075,7 +1075,13 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1075
1075
  self._wait = None
1076
1076
 
1077
1077
  if self._owner: # pragma: no branch
1078
- self._owner.connection_lost(exc)
1078
+ # pylint: disable=broad-except
1079
+ try:
1080
+ self._owner.connection_lost(exc)
1081
+ except Exception:
1082
+ self.logger.debug1('Uncaught exception in owner ignored',
1083
+ exc_info=sys.exc_info)
1084
+
1079
1085
  self._owner = None
1080
1086
 
1081
1087
  self._cancel_login_timer()
@@ -1196,7 +1202,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
1196
1202
 
1197
1203
  return self._server
1198
1204
 
1199
- def is_closed(self):
1205
+ def is_closed(self) -> bool:
1200
1206
  """Return whether the connection is closed"""
1201
1207
 
1202
1208
  return self._close_event.is_set()
@@ -2069,7 +2075,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2069
2075
  self.send_packet(MSG_USERAUTH_FAILURE, NameList(methods),
2070
2076
  Boolean(partial_success))
2071
2077
 
2072
- def send_userauth_success(self) -> None:
2078
+ async def send_userauth_success(self) -> None:
2073
2079
  """Send a user authentication success response"""
2074
2080
 
2075
2081
  self.logger.info('Auth for user %s succeeded', self._username)
@@ -2086,13 +2092,15 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2086
2092
  self._set_keepalive_timer()
2087
2093
 
2088
2094
  if self._owner: # pragma: no branch
2089
- self._owner.auth_completed()
2095
+ result = self._owner.auth_completed()
2096
+
2097
+ if inspect.isawaitable(result):
2098
+ await result
2090
2099
 
2091
2100
  if self._acceptor:
2092
2101
  result = self._acceptor(self)
2093
2102
 
2094
2103
  if inspect.isawaitable(result):
2095
- assert result is not None
2096
2104
  self.create_task(result)
2097
2105
 
2098
2106
  self._acceptor = None
@@ -2506,7 +2514,7 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2506
2514
  result = await cast(Awaitable[bool], result)
2507
2515
 
2508
2516
  if not result:
2509
- self.send_userauth_success()
2517
+ await self.send_userauth_success()
2510
2518
  return
2511
2519
 
2512
2520
  if not self._owner: # pragma: no cover
@@ -2603,7 +2611,6 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
2603
2611
  result = self._acceptor(self)
2604
2612
 
2605
2613
  if inspect.isawaitable(result):
2606
- assert result is not None
2607
2614
  self.create_task(result)
2608
2615
 
2609
2616
  self._acceptor = None
@@ -3229,9 +3236,8 @@ class SSHConnection(SSHPacketHandler, asyncio.Protocol):
3229
3236
  raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED,
3230
3237
  'Connection forwarding denied')
3231
3238
 
3232
- return (await self.create_connection(session_factory,
3233
- dest_host, dest_port,
3234
- orig_host, orig_port))
3239
+ return await self.create_connection(session_factory, dest_host,
3240
+ dest_port, orig_host, orig_port)
3235
3241
 
3236
3242
  if (listen_host, listen_port) == (dest_host, dest_port):
3237
3243
  self.logger.info('Creating local TCP forwarder on %s',
@@ -4130,7 +4136,6 @@ class SSHClientConnection(SSHConnection):
4130
4136
  retained, revoked)
4131
4137
 
4132
4138
  if inspect.isawaitable(result):
4133
- assert result is not None
4134
4139
  await result
4135
4140
 
4136
4141
  self._report_global_response(True)
@@ -5052,8 +5057,8 @@ class SSHClientConnection(SSHConnection):
5052
5057
 
5053
5058
  """
5054
5059
 
5055
- return (await create_connection(client_factory, host, port,
5056
- tunnel=self, **kwargs)) # type: ignore
5060
+ return await create_connection(client_factory, host, port,
5061
+ tunnel=self, **kwargs) # type: ignore
5057
5062
 
5058
5063
  @async_context_manager
5059
5064
  async def connect_ssh(self, host: str, port: DefTuple[int] = (),
@@ -5321,8 +5326,7 @@ class SSHClientConnection(SSHConnection):
5321
5326
  raise ChannelOpenError(OPEN_ADMINISTRATIVELY_PROHIBITED,
5322
5327
  'Connection forwarding denied')
5323
5328
 
5324
- return (await self.create_unix_connection(session_factory,
5325
- dest_path))
5329
+ return await self.create_unix_connection(session_factory, dest_path)
5326
5330
 
5327
5331
  self.logger.info('Creating local TCP forwarder from %s to %s',
5328
5332
  (listen_host, listen_port), dest_path)
@@ -6301,6 +6305,9 @@ class SSHServerConnection(SSHConnection):
6301
6305
  if not result:
6302
6306
  raise ChannelOpenError(OPEN_CONNECT_FAILED, 'Session refused')
6303
6307
 
6308
+ if isinstance(result, SSHClientConnection):
6309
+ result = self.forward_tunneled_session(result)
6310
+
6304
6311
  if isinstance(result, tuple):
6305
6312
  chan, result = result
6306
6313
  else:
@@ -6356,6 +6363,10 @@ class SSHServerConnection(SSHConnection):
6356
6363
  if result is True:
6357
6364
  result = cast(SSHTCPSession[bytes],
6358
6365
  self.forward_connection(dest_host, dest_port))
6366
+ elif isinstance(result, SSHClientConnection):
6367
+ result = cast(Awaitable[SSHTCPSession[bytes]],
6368
+ self.forward_tunneled_connection(
6369
+ result, dest_host, dest_port))
6359
6370
 
6360
6371
  if isinstance(result, tuple):
6361
6372
  chan, result = result
@@ -6502,6 +6513,10 @@ class SSHServerConnection(SSHConnection):
6502
6513
  if result is True:
6503
6514
  result = cast(SSHUNIXSession[bytes],
6504
6515
  self.forward_unix_connection(dest_path))
6516
+ elif isinstance(result, SSHClientConnection):
6517
+ result = cast(Awaitable[SSHUNIXSession[bytes]],
6518
+ self.forward_tunneled_unix_connection(
6519
+ result, dest_path))
6505
6520
 
6506
6521
  if isinstance(result, tuple):
6507
6522
  chan, result = result
@@ -6617,10 +6632,14 @@ class SSHServerConnection(SSHConnection):
6617
6632
  result = False
6618
6633
 
6619
6634
  if not result:
6620
- raise ChannelOpenError(OPEN_CONNECT_FAILED, 'Connection refused')
6635
+ raise ChannelOpenError(OPEN_CONNECT_FAILED,
6636
+ 'TUN/TAP request refused')
6621
6637
 
6622
6638
  if result is True:
6623
6639
  result = cast(SSHTunTapSession, self.forward_tuntap(mode, unit))
6640
+ elif isinstance(result, SSHClientConnection):
6641
+ result = cast(Awaitable[SSHTunTapSession],
6642
+ self.forward_tunneled_tuntap(result, mode, unit))
6624
6643
 
6625
6644
  if isinstance(result, tuple):
6626
6645
  chan, result = result
@@ -7175,6 +7194,76 @@ class SSHServerConnection(SSHConnection):
7175
7194
 
7176
7195
  return SSHReader[bytes](session, chan), SSHWriter[bytes](session, chan)
7177
7196
 
7197
+ async def forward_tunneled_session(
7198
+ self, conn: SSHClientConnection) -> SSHServerProcess:
7199
+ """Forward a tunneled session between SSH connections"""
7200
+
7201
+ async def process_factory(process: SSHServerProcess) -> None:
7202
+ """Return an upstream process used to forward the session"""
7203
+
7204
+ encoding, errors = process.channel.get_encoding()
7205
+
7206
+ upstream_process: SSHClientProcess = await conn.create_process(
7207
+ command=process.command, subsystem=process.subsystem,
7208
+ env=process.env, term_type=process.term_type,
7209
+ term_size=process.term_size, term_modes=process.term_modes,
7210
+ encoding=encoding, errors=errors, stdin=process.stdin,
7211
+ stdout=process.stdout, stderr=process.stderr)
7212
+
7213
+ await upstream_process.wait_closed()
7214
+
7215
+ self.logger.info(' Forwarding session via SSH tunnel')
7216
+
7217
+ return SSHServerProcess(process_factory, None, MIN_SFTP_VERSION, False)
7218
+
7219
+ async def forward_tunneled_connection(
7220
+ self, conn: SSHClientConnection,
7221
+ dest_host: str, dest_port: int) -> SSHForwarder:
7222
+ """Forward a tunneled TCP connection between SSH connections"""
7223
+
7224
+ _, peer = await conn.create_connection(
7225
+ cast(SSHTCPSessionFactory[bytes], SSHForwarder),
7226
+ dest_host, dest_port)
7227
+
7228
+ self.logger.info(' Forwarding TCP connection to %s via SSH tunnel',
7229
+ (dest_host, dest_port))
7230
+
7231
+ return SSHForwarder(cast(SSHForwarder, peer))
7232
+
7233
+ async def forward_tunneled_unix_connection(
7234
+ self, conn: SSHClientConnection,
7235
+ dest_path: str) -> SSHForwarder:
7236
+ """Forward a tunneled UNIX connection between SSH connections"""
7237
+
7238
+ _, peer = await conn.create_unix_connection(
7239
+ cast(SSHUNIXSessionFactory[bytes], SSHForwarder), dest_path)
7240
+
7241
+ self.logger.info(' Forwarding UNIX connection to %s via SSH tunnel',
7242
+ dest_path)
7243
+
7244
+ return SSHForwarder(cast(SSHForwarder, peer))
7245
+
7246
+ async def forward_tunneled_tuntap(
7247
+ self, conn: SSHClientConnection,
7248
+ mode: int, unit: Optional[int]) -> SSHForwarder:
7249
+ """Forward a TUN/TAP connection between SSH connections"""
7250
+
7251
+ if mode == SSH_TUN_MODE_POINTTOPOINT:
7252
+ create_func = conn.create_tun
7253
+ layer = 3
7254
+ else:
7255
+ create_func = conn.create_tap
7256
+ layer = 2
7257
+
7258
+ transport, peer = await create_func(
7259
+ cast(SSHTunTapSessionFactory, SSHForwarder), unit)
7260
+ interface = transport.get_extra_info('interface')
7261
+
7262
+ self.logger.info(' Forwarding layer %d traffic to %s via SSH tunnel',
7263
+ layer, interface)
7264
+
7265
+ return SSHForwarder(cast(SSHForwarder, peer))
7266
+
7178
7267
 
7179
7268
  class SSHConnectionOptions(Options, Generic[_Options]):
7180
7269
  """SSH connection options"""
@@ -8473,11 +8562,11 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
8473
8562
  errors of data exchanged on sessions on this server, defaulting
8474
8563
  to 'strict'.
8475
8564
  :param sftp_factory: (optional)
8476
- A `callable` which returns an :class:`SFTPServer` object that
8477
- will be created each time an SFTP session is requested by the
8478
- client, or `True` to use the base :class:`SFTPServer` class
8479
- to handle SFTP requests. If not specified, SFTP sessions are
8480
- rejected by default.
8565
+ A `callable` or coroutine which returns an :class:`SFTPServer`
8566
+ object that will be created each time an SFTP session is
8567
+ requested by the client, or `True` to use the base
8568
+ :class:`SFTPServer` class to handle SFTP requests. If not
8569
+ specified, SFTP sessions are rejected by default.
8481
8570
  :param sftp_version: (optional)
8482
8571
  The maximum version of the SFTP protocol to support, currently
8483
8572
  either 3 or 4, defaulting to 3.
@@ -8624,7 +8713,7 @@ class SSHServerConnectionOptions(SSHConnectionOptions):
8624
8713
  :type session_factory: `callable` or coroutine
8625
8714
  :type encoding: `str` or `None`
8626
8715
  :type errors: `str`
8627
- :type sftp_factory: `callable`
8716
+ :type sftp_factory: `callable` or coroutine
8628
8717
  :type sftp_version: `int`
8629
8718
  :type allow_scp: `bool`
8630
8719
  :type window: `int`
@@ -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
@@ -343,6 +343,9 @@ class _KexDHGex(_KexDHBase):
343
343
  if self._conn.is_client():
344
344
  raise ProtocolError('Unexpected kex request msg')
345
345
 
346
+ if self._p:
347
+ raise ProtocolError('Kex DH group already requested')
348
+
346
349
  self._gex_data = packet.get_remaining_payload()
347
350
 
348
351
  if pkttype == MSG_KEX_DH_GEX_REQUEST_OLD:
@@ -377,6 +380,9 @@ class _KexDHGex(_KexDHBase):
377
380
  if self._conn.is_server():
378
381
  raise ProtocolError('Unexpected kex group msg')
379
382
 
383
+ if self._p:
384
+ raise ProtocolError('Kex DH group already sent')
385
+
380
386
  p = packet.get_mpint()
381
387
  g = packet.get_mpint()
382
388
  packet.check_end()
@@ -529,6 +535,7 @@ class _KexGSSBase(_KexDHBase):
529
535
 
530
536
  self._gss = conn.get_gss_context()
531
537
  self._token: Optional[bytes] = None
538
+ self._host_key_msg_ok = False
532
539
  self._host_key_data = b''
533
540
 
534
541
  def _check_secure(self) -> None:
@@ -621,6 +628,8 @@ class _KexGSSBase(_KexDHBase):
621
628
  if self._conn.is_client() and self._gss.complete:
622
629
  raise ProtocolError('Unexpected kexgss continue msg')
623
630
 
631
+ self._host_key_msg_ok = False
632
+
624
633
  await self._process_token(token)
625
634
 
626
635
  if self._conn.is_server() and self._gss.complete:
@@ -636,6 +645,8 @@ class _KexGSSBase(_KexDHBase):
636
645
  if self._conn.is_server():
637
646
  raise ProtocolError('Unexpected kexgss complete msg')
638
647
 
648
+ self._host_key_msg_ok = False
649
+
639
650
  self._parse_server_key(packet)
640
651
  mic = packet.get_string()
641
652
  token_present = packet.get_boolean()
@@ -662,6 +673,10 @@ class _KexGSSBase(_KexDHBase):
662
673
  packet: SSHPacket) -> None:
663
674
  """Process a GSS hostkey message"""
664
675
 
676
+ if not self._host_key_msg_ok:
677
+ raise ProtocolError('Unexpected kexgss hostkey msg')
678
+
679
+ self._host_key_msg_ok = False
665
680
  self._host_key_data = packet.get_string()
666
681
  packet.check_end()
667
682
 
@@ -685,6 +700,7 @@ class _KexGSSBase(_KexDHBase):
685
700
  """Start GSS key exchange"""
686
701
 
687
702
  if self._conn.is_client():
703
+ self._host_key_msg_ok = True
688
704
  await self._process_token()
689
705
  await super().start()
690
706
 
@@ -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
@@ -46,6 +46,17 @@ from .constants import DISC_NO_MORE_AUTH_METHODS_AVAILABLE
46
46
  from .constants import DISC_PROTOCOL_ERROR, DISC_PROTOCOL_VERSION_NOT_SUPPORTED
47
47
  from .constants import DISC_SERVICE_NOT_AVAILABLE
48
48
 
49
+ _pywin32_available = False
50
+
51
+ if sys.platform == 'win32': # pragma: no cover
52
+ try:
53
+ import msvcrt
54
+ import win32file
55
+ import winioctlcon
56
+ _pywin32_available = True
57
+ except ImportError:
58
+ pass
59
+
49
60
  if sys.platform != 'win32': # pragma: no branch
50
61
  import fcntl
51
62
  import struct
@@ -305,6 +316,19 @@ def write_file(filename: FilePath, data: bytes, mode: str = 'wb') -> int:
305
316
  return f.write(data)
306
317
 
307
318
 
319
+ if sys.platform == 'win32' and _pywin32_available: # pragma: no cover
320
+ def make_sparse_file(file_obj: IO) -> None:
321
+ """Enable sparse file support on a file on Windows"""
322
+
323
+ handle = msvcrt.get_osfhandle(file_obj.fileno())
324
+
325
+ win32file.DeviceIoControl(handle, winioctlcon.FSCTL_SET_SPARSE,
326
+ b'', 0, None)
327
+ else:
328
+ def make_sparse_file(_file_obj: IO) -> None:
329
+ """Sparse files are automatically enabled on non-Windows systems"""
330
+
331
+
308
332
  def _parse_units(value: str, suffixes: Mapping[str, int], label: str) -> float:
309
333
  """Parse a series of integers followed by unit suffixes"""
310
334
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2017-2024 by Ron Frederick <ronf@timeheart.net> and others.
1
+ # Copyright (c) 2017-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
@@ -24,8 +24,9 @@
24
24
 
25
25
  import argparse
26
26
  import asyncio
27
- import posixpath
27
+ import inspect
28
28
  from pathlib import PurePath
29
+ import posixpath
29
30
  import shlex
30
31
  import string
31
32
  import sys
@@ -40,9 +41,8 @@ from .logging import SSHLogger
40
41
  from .misc import BytesOrStr, FilePath, HostPort, MaybeAwait
41
42
  from .misc import async_context_manager, plural
42
43
  from .sftp import SFTPAttrs, SFTPGlob, SFTPName, SFTPServer, SFTPServerFS
43
- from .sftp import SFTPFileProtocol, SFTPError, SFTPFailure, SFTPBadMessage
44
- from .sftp import SFTPConnectionLost, SFTPErrorHandler, SFTPProgressHandler
45
- from .sftp import local_fs
44
+ from .sftp import SFTPError, SFTPFailure, SFTPBadMessage, SFTPConnectionLost
45
+ from .sftp import SFTPErrorHandler, SFTPProgressHandler, local_fs
46
46
 
47
47
 
48
48
  if TYPE_CHECKING:
@@ -60,6 +60,27 @@ _SCPConnPath = Union[Tuple[_SCPConn, _SCPPath], _SCPConn, _SCPPath]
60
60
  _SCP_BLOCK_SIZE = 256*1024 # 256 KiB
61
61
 
62
62
 
63
+ class _SCPFileProtocol(Protocol):
64
+ """Protocol for accessing a file during an SCP copy"""
65
+
66
+ async def __aenter__(self) -> Self:
67
+ """Allow _SCPFileProtocol to be used as an async context manager"""
68
+
69
+ async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
70
+ _exc_value: Optional[BaseException],
71
+ _traceback: Optional[TracebackType]) -> bool:
72
+ """Wait for file close when used as an async context manager"""
73
+
74
+ async def read(self, size: int, offset: int) -> bytes:
75
+ """Read data from the local file"""
76
+
77
+ async def write(self, data: bytes, offset: int) -> int:
78
+ """Write data to the local file"""
79
+
80
+ async def close(self) -> None:
81
+ """Close the local file"""
82
+
83
+
63
84
  class _SCPFSProtocol(Protocol):
64
85
  """Protocol for accessing a filesystem during an SCP copy"""
65
86
 
@@ -86,7 +107,7 @@ class _SCPFSProtocol(Protocol):
86
107
  """Create a directory"""
87
108
 
88
109
  @async_context_manager
89
- async def open(self, path: bytes, mode: str) -> SFTPFileProtocol:
110
+ async def open(self, path: bytes, mode: str) -> _SCPFileProtocol:
90
111
  """Open a file"""
91
112
 
92
113
 
@@ -1066,22 +1087,46 @@ async def scp(srcpaths: Union[_SCPConnPath, Sequence[_SCPConnPath]],
1066
1087
  await dstconn.wait_closed()
1067
1088
 
1068
1089
 
1069
- def run_scp_server(sftp_server: SFTPServer, command: str,
1090
+ async def _scp_handler(sftp_server: MaybeAwait[SFTPServer],
1091
+ args: _SCPArgs, reader: 'SSHReader[bytes]',
1092
+ writer: 'SSHWriter[bytes]') -> None:
1093
+ """Run an SCP server to handle this request"""
1094
+
1095
+ if inspect.isawaitable(sftp_server):
1096
+ sftp_server = await sftp_server
1097
+
1098
+ fs = SFTPServerFS(sftp_server)
1099
+
1100
+ handler: Union[_SCPSource, _SCPSink]
1101
+
1102
+ if args.source:
1103
+ handler = _SCPSource(fs, reader, writer, args.preserve,
1104
+ args.recurse, error_handler=False, server=True)
1105
+ else:
1106
+ handler = _SCPSink(fs, reader, writer, args.must_be_dir,
1107
+ args.preserve, args.recurse,
1108
+ error_handler=False, server=True)
1109
+
1110
+ try:
1111
+ await handler.run(args.path)
1112
+ finally:
1113
+ result = sftp_server.exit()
1114
+
1115
+ if inspect.isawaitable(result):
1116
+ await result
1117
+
1118
+
1119
+ def run_scp_server(sftp_server: MaybeAwait[SFTPServer], command: str,
1070
1120
  stdin: 'SSHReader[bytes]', stdout: 'SSHWriter[bytes]',
1071
1121
  stderr: 'SSHWriter[bytes]') -> MaybeAwait[None]:
1072
1122
  """Return a handler for an SCP server session"""
1073
1123
 
1074
- async def _run_handler() -> None:
1075
- """Run an SCP server to handle this request"""
1076
-
1077
- try:
1078
- await handler.run(args.path)
1079
- finally:
1080
- sftp_server.exit()
1081
-
1082
1124
  try:
1083
1125
  args = _SCPArgParser().parse(command)
1084
1126
  except ValueError as exc:
1127
+ if inspect.iscoroutine(sftp_server):
1128
+ sftp_server.close()
1129
+
1085
1130
  stdin.logger.info('Error starting SCP server: %s', str(exc))
1086
1131
  stderr.write(b'scp: ' + str(exc).encode('utf-8') + b'\n')
1087
1132
  cast('SSHServerChannel', stderr.channel).exit(1)
@@ -1089,15 +1134,4 @@ def run_scp_server(sftp_server: SFTPServer, command: str,
1089
1134
 
1090
1135
  stdin.logger.info('Starting SCP server, args: %s', command[4:].strip())
1091
1136
 
1092
- fs = SFTPServerFS(sftp_server)
1093
-
1094
- handler: Union[_SCPSource, _SCPSink]
1095
-
1096
- if args.source:
1097
- handler = _SCPSource(fs, stdin, stdout, args.preserve, args.recurse,
1098
- error_handler=False, server=True)
1099
- else:
1100
- handler = _SCPSink(fs, stdin, stdout, args.must_be_dir, args.preserve,
1101
- args.recurse, error_handler=False, server=True)
1102
-
1103
- return _run_handler()
1137
+ return _scp_handler(sftp_server, args, stdin, stdout)