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.
- {asyncssh-2.20.0/asyncssh.egg-info → asyncssh-2.21.0}/PKG-INFO +3 -2
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/auth.py +12 -12
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/channel.py +12 -5
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/connection.py +112 -23
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/kex_dh.py +17 -1
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/misc.py +25 -1
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/scp.py +61 -27
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/server.py +54 -21
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sftp.py +360 -117
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/stream.py +3 -5
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/version.py +1 -1
- {asyncssh-2.20.0 → asyncssh-2.21.0/asyncssh.egg-info}/PKG-INFO +3 -2
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/changes.rst +38 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_auth.py +2 -2
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_channel.py +16 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_connection.py +18 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_connection_auth.py +4 -1
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_forward.py +63 -6
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_kex.py +44 -11
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_sftp.py +118 -7
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_stream.py +33 -1
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_tuntap.py +59 -1
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/util.py +0 -2
- {asyncssh-2.20.0 → asyncssh-2.21.0}/.coveragerc +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/.github/workflows/run_tests.yml +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/.gitignore +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/.readthedocs.yaml +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/CONTRIBUTING.rst +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/COPYRIGHT +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/LICENSE +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/MANIFEST.in +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/README.rst +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/__init__.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/agent.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/agent_unix.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/agent_win32.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/asn1.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/auth_keys.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/compression.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/config.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/constants.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/__init__.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/chacha.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/cipher.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/dh.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/dsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/ec.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/ec_params.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/ed.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/kdf.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/misc.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/pq.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/rsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/umac.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/crypto/x509.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/dsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/ecdsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/eddsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/editor.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/encryption.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/forward.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/gss.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/gss_unix.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/gss_win32.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/kex.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/kex_rsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/keysign.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/known_hosts.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/listener.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/logging.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/mac.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/packet.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/pattern.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/pbe.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/pkcs11.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/process.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/public_key.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/py.typed +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/rsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/saslprep.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/session.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sk.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sk_ecdsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/sk_eddsa.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/socks.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/subprocess.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/tuntap.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh/x11.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/SOURCES.txt +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/dependency_links.txt +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/requires.txt +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/asyncssh.egg-info/top_level.txt +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/_templates/sidebarbottom.html +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/_templates/sidebartop.html +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/api.rst +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/conf.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/contributing.rst +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/index.rst +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/requirements.txt +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rftheme/layout.html +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rftheme/static/rftheme.css_t +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rftheme/theme.conf +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/docs/rtd-req.txt +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_client2.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_client3.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/callback_math_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/chat_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/check_exit_status.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/chroot_sftp_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/direct_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/direct_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/editor.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/gather_results.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/listening_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/local_forwarding_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/local_forwarding_client2.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/local_forwarding_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/math_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/math_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_input.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_local_pipe.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_remote_pipe.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/redirect_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/remote_forwarding_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/remote_forwarding_client2.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/remote_forwarding_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/reverse_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/reverse_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/scp_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/set_environment.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/set_terminal.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/sftp_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/show_environment.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/show_terminal.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_cert_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_keyed_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_scp_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/simple_sftp_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/stream_direct_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/stream_direct_server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/examples/stream_listening_client.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/mypy.ini +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/pylintrc +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/pyproject.toml +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/setup.cfg +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/__init__.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/gss_stub.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/gssapi_stub.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/keysign_stub.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/pkcs11_stub.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/server.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/sk_stub.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/sspi_stub.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_agent.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_asn1.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_auth_keys.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_compression.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_config.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_editor.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_encryption.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_known_hosts.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_logging.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_mac.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_packet.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_pkcs11.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_process.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_public_key.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_saslprep.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_sk.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_subprocess.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_x11.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tests/test_x509.py +0 -0
- {asyncssh-2.20.0 → asyncssh-2.21.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncssh
|
|
3
|
-
Version: 2.
|
|
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-
|
|
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
|
-
|
|
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
|
|
2062
|
-
|
|
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
|
|
2070
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
3233
|
-
|
|
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
|
|
5056
|
-
|
|
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
|
|
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,
|
|
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`
|
|
8477
|
-
will be created each time an SFTP session is
|
|
8478
|
-
client, or `True` to use the base
|
|
8479
|
-
to handle SFTP requests. If not
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
44
|
-
from .sftp import
|
|
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) ->
|
|
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
|
|
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
|
-
|
|
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)
|