asyncssh 2.18.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.
- {asyncssh-2.18.0 → asyncssh-2.20.0}/.github/workflows/run_tests.yml +3 -18
- {asyncssh-2.18.0/asyncssh.egg-info → asyncssh-2.20.0}/PKG-INFO +8 -9
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/client.py +9 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/config.py +137 -61
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/connection.py +277 -45
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/misc.py +8 -7
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/process.py +2 -2
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/public_key.py +7 -4
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sftp.py +246 -46
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sk.py +90 -19
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sk_ecdsa.py +45 -22
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sk_eddsa.py +11 -12
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/version.py +1 -1
- {asyncssh-2.18.0 → asyncssh-2.20.0/asyncssh.egg-info}/PKG-INFO +8 -9
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/SOURCES.txt +1 -1
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/api.rst +30 -6
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/changes.rst +63 -0
- asyncssh-2.20.0/pyproject.toml +59 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/server.py +16 -12
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/sk_stub.py +100 -18
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_channel.py +3 -3
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_config.py +70 -9
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_connection.py +123 -1
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_connection_auth.py +2 -1
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_public_key.py +10 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_sftp.py +152 -13
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_sk.py +18 -2
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/util.py +25 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tox.ini +3 -3
- asyncssh-2.18.0/setup.py +0 -92
- {asyncssh-2.18.0 → asyncssh-2.20.0}/.coveragerc +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/.gitignore +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/.readthedocs.yaml +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/CONTRIBUTING.rst +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/COPYRIGHT +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/LICENSE +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/MANIFEST.in +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/README.rst +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/__init__.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/agent.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/agent_unix.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/agent_win32.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/asn1.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/auth.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/auth_keys.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/channel.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/compression.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/constants.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/__init__.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/chacha.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/cipher.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/dh.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/dsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/ec.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/ec_params.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/ed.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/kdf.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/misc.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/pq.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/rsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/umac.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/x509.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/dsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/ecdsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/eddsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/editor.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/encryption.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/forward.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/gss.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/gss_unix.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/gss_win32.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/kex.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/kex_dh.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/kex_rsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/keysign.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/known_hosts.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/listener.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/logging.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/mac.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/packet.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/pattern.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/pbe.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/pkcs11.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/py.typed +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/rsa.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/saslprep.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/scp.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/session.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/socks.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/stream.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/subprocess.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/tuntap.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/x11.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/dependency_links.txt +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/requires.txt +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/top_level.txt +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/_templates/sidebarbottom.html +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/_templates/sidebartop.html +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/conf.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/contributing.rst +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/index.rst +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/requirements.txt +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rftheme/layout.html +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rftheme/static/rftheme.css_t +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rftheme/theme.conf +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rtd-req.txt +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_client2.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_client3.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_math_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/chat_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/check_exit_status.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/chroot_sftp_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/direct_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/direct_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/editor.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/gather_results.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/listening_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/local_forwarding_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/local_forwarding_client2.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/local_forwarding_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/math_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/math_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_input.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_local_pipe.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_remote_pipe.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/remote_forwarding_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/remote_forwarding_client2.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/remote_forwarding_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/reverse_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/reverse_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/scp_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/set_environment.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/set_terminal.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/sftp_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/show_environment.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/show_terminal.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_cert_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_keyed_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_scp_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_sftp_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/stream_direct_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/stream_direct_server.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/stream_listening_client.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/mypy.ini +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/pylintrc +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/setup.cfg +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/__init__.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/gss_stub.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/gssapi_stub.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/keysign_stub.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/pkcs11_stub.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/sspi_stub.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_agent.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_asn1.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_auth.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_auth_keys.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_compression.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_editor.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_encryption.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_forward.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_kex.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_known_hosts.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_logging.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_mac.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_packet.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_pkcs11.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_process.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_saslprep.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_stream.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_subprocess.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_tuntap.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_x11.py +0 -0
- {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_x509.py +0 -0
|
@@ -8,7 +8,7 @@ jobs:
|
|
|
8
8
|
fail-fast: false
|
|
9
9
|
matrix:
|
|
10
10
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
11
|
-
python-version: ["3.
|
|
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.
|
|
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,32 +1,31 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: asyncssh
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.20.0
|
|
4
4
|
Summary: AsyncSSH: Asynchronous SSHv2 client and server library
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
License: Eclipse Public License v2.0
|
|
5
|
+
Author-email: Ron Frederick <ronf@timeheart.net>
|
|
6
|
+
License: EPL-2.0 OR GPL-2.0-or-later
|
|
7
|
+
Project-URL: Homepage, http://asyncssh.timeheart.net
|
|
9
8
|
Project-URL: Documentation, https://asyncssh.readthedocs.io
|
|
10
9
|
Project-URL: Source, https://github.com/ronf/asyncssh
|
|
11
10
|
Project-URL: Tracker, https://github.com/ronf/asyncssh/issues
|
|
12
|
-
Platform: Any
|
|
13
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
14
12
|
Classifier: Environment :: Console
|
|
15
13
|
Classifier: Intended Audience :: Developers
|
|
16
14
|
Classifier: License :: OSI Approved
|
|
17
15
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
16
|
Classifier: Operating System :: POSIX
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
20
17
|
Classifier: Programming Language :: Python :: 3.8
|
|
21
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
23
|
Classifier: Topic :: Internet
|
|
26
24
|
Classifier: Topic :: Security :: Cryptography
|
|
27
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
26
|
Classifier: Topic :: System :: Networking
|
|
29
|
-
Requires-Python: >=
|
|
27
|
+
Requires-Python: >=3.6
|
|
28
|
+
Description-Content-Type: text/x-rst
|
|
30
29
|
License-File: LICENSE
|
|
31
30
|
Requires-Dist: cryptography>=39.0
|
|
32
31
|
Requires-Dist: typing_extensions>=4.0.0
|
|
@@ -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
|
|
|
@@ -60,12 +64,15 @@ class SSHConfig:
|
|
|
60
64
|
_percent_expand = {'AuthorizedKeysFile'}
|
|
61
65
|
_handlers: Dict[str, Tuple[str, Callable]] = {}
|
|
62
66
|
|
|
63
|
-
def __init__(self, last_config: Optional['SSHConfig'], reload: bool
|
|
67
|
+
def __init__(self, last_config: Optional['SSHConfig'], reload: bool,
|
|
68
|
+
canonical: bool, final: bool):
|
|
64
69
|
if last_config:
|
|
65
70
|
self._last_options = last_config.get_options(reload)
|
|
66
71
|
else:
|
|
67
72
|
self._last_options = {}
|
|
68
73
|
|
|
74
|
+
self._canonical = canonical
|
|
75
|
+
self._final = True if final else None
|
|
69
76
|
self._default_path = Path('~', '.ssh').expanduser()
|
|
70
77
|
self._path = Path()
|
|
71
78
|
self._line_no = 0
|
|
@@ -90,36 +97,38 @@ class SSHConfig:
|
|
|
90
97
|
|
|
91
98
|
raise NotImplementedError
|
|
92
99
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
100
|
+
def _expand_token(self, match):
|
|
101
|
+
"""Expand a percent token reference"""
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _expand_env(match):
|
|
118
|
+
"""Expand an environment variable reference"""
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
104
126
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
raise ConfigParseError('Invalid token substitution') from None
|
|
111
|
-
except KeyError:
|
|
112
|
-
if token == 'd':
|
|
113
|
-
raise ConfigParseError('Home directory is '
|
|
114
|
-
'not available') from None
|
|
115
|
-
elif token == 'i':
|
|
116
|
-
raise ConfigParseError('User id not available') from None
|
|
117
|
-
else:
|
|
118
|
-
raise ConfigParseError('Invalid token substitution: ' +
|
|
119
|
-
value[idx+1]) from None
|
|
120
|
-
|
|
121
|
-
result.append(value[last_idx:])
|
|
122
|
-
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))
|
|
123
132
|
|
|
124
133
|
def _include(self, option: str, args: List[str]) -> None:
|
|
125
134
|
"""Read config from a list of other config files"""
|
|
@@ -153,35 +162,53 @@ class SSHConfig:
|
|
|
153
162
|
|
|
154
163
|
# pylint: disable=unused-argument
|
|
155
164
|
|
|
165
|
+
matching = True
|
|
166
|
+
|
|
156
167
|
while args:
|
|
157
168
|
match = args.pop(0).lower()
|
|
158
169
|
|
|
170
|
+
if match[0] == '!':
|
|
171
|
+
match = match[1:]
|
|
172
|
+
negated = True
|
|
173
|
+
else:
|
|
174
|
+
negated = False
|
|
175
|
+
|
|
176
|
+
if match == 'final' and self._final is None:
|
|
177
|
+
self._final = False
|
|
178
|
+
|
|
159
179
|
if match == 'all':
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
result = True
|
|
181
|
+
elif match == 'canonical':
|
|
182
|
+
result = self._canonical
|
|
183
|
+
elif match == 'final':
|
|
184
|
+
result = cast(bool, self._final)
|
|
185
|
+
else:
|
|
186
|
+
match_val = self._match_val(match)
|
|
187
|
+
|
|
188
|
+
if match != 'exec' and match_val is None:
|
|
189
|
+
self._error(f'Invalid match condition {match}')
|
|
162
190
|
|
|
163
|
-
|
|
191
|
+
try:
|
|
192
|
+
arg = args.pop(0)
|
|
193
|
+
except IndexError:
|
|
194
|
+
self._error(f'Missing {match} match pattern')
|
|
195
|
+
|
|
196
|
+
if matching:
|
|
197
|
+
if match == 'exec':
|
|
198
|
+
result = _exec(arg)
|
|
199
|
+
elif match in ('address', 'localaddress'):
|
|
200
|
+
host_pat = HostPatternList(arg)
|
|
201
|
+
ip = ip_address(cast(str, match_val)) \
|
|
202
|
+
if match_val else None
|
|
203
|
+
result = host_pat.matches(None, match_val, ip)
|
|
204
|
+
else:
|
|
205
|
+
wild_pat = WildcardPatternList(arg)
|
|
206
|
+
result = wild_pat.matches(match_val)
|
|
164
207
|
|
|
165
|
-
if
|
|
166
|
-
|
|
208
|
+
if matching and result == negated:
|
|
209
|
+
matching = False
|
|
167
210
|
|
|
168
|
-
|
|
169
|
-
if match == 'exec':
|
|
170
|
-
self._matching = _exec(args.pop(0))
|
|
171
|
-
elif match in ('address', 'localaddress'):
|
|
172
|
-
host_pat = HostPatternList(args.pop(0))
|
|
173
|
-
ip = ip_address(cast(str, match_val)) \
|
|
174
|
-
if match_val else None
|
|
175
|
-
self._matching = host_pat.matches(None, match_val, ip)
|
|
176
|
-
else:
|
|
177
|
-
wild_pat = WildcardPatternList(args.pop(0))
|
|
178
|
-
self._matching = wild_pat.matches(match_val)
|
|
179
|
-
except IndexError:
|
|
180
|
-
self._error(f'Missing {match} match pattern')
|
|
181
|
-
|
|
182
|
-
if not self._matching:
|
|
183
|
-
args.clear()
|
|
184
|
-
break
|
|
211
|
+
self._matching = matching
|
|
185
212
|
|
|
186
213
|
def _set_bool(self, option: str, args: List[str]) -> None:
|
|
187
214
|
"""Set a boolean config option"""
|
|
@@ -198,6 +225,22 @@ class SSHConfig:
|
|
|
198
225
|
if option not in self._options:
|
|
199
226
|
self._options[option] = value
|
|
200
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
|
+
|
|
201
244
|
def _set_int(self, option: str, args: List[str]) -> None:
|
|
202
245
|
"""Set an integer config option"""
|
|
203
246
|
|
|
@@ -276,6 +319,23 @@ class SSHConfig:
|
|
|
276
319
|
if option not in self._options:
|
|
277
320
|
self._options[option] = value
|
|
278
321
|
|
|
322
|
+
def _set_canonicalize_host(self, option: str, args: List[str]) -> None:
|
|
323
|
+
"""Set a canonicalize host config option"""
|
|
324
|
+
|
|
325
|
+
value_str = args.pop(0).lower()
|
|
326
|
+
|
|
327
|
+
if value_str in ('yes', 'true'):
|
|
328
|
+
value: Union[bool, str] = True
|
|
329
|
+
elif value_str in ('no', 'false'):
|
|
330
|
+
value = False
|
|
331
|
+
elif value_str == 'always':
|
|
332
|
+
value = value_str
|
|
333
|
+
else:
|
|
334
|
+
self._error(f'Invalid {option} value: {value_str}')
|
|
335
|
+
|
|
336
|
+
if option not in self._options:
|
|
337
|
+
self._options[option] = value
|
|
338
|
+
|
|
279
339
|
def _set_rekey_limits(self, option: str, args: List[str]) -> None:
|
|
280
340
|
"""Set rekey limits config option"""
|
|
281
341
|
|
|
@@ -295,6 +355,11 @@ class SSHConfig:
|
|
|
295
355
|
if option not in self._options:
|
|
296
356
|
self._options[option] = byte_limit, time_limit
|
|
297
357
|
|
|
358
|
+
def has_match_final(self) -> bool:
|
|
359
|
+
"""Return whether this config includes a 'Match final' block"""
|
|
360
|
+
|
|
361
|
+
return self._final is not None
|
|
362
|
+
|
|
298
363
|
def parse(self, path: Path) -> None:
|
|
299
364
|
"""Parse an OpenSSH config file and return matching declarations"""
|
|
300
365
|
|
|
@@ -384,10 +449,10 @@ class SSHConfig:
|
|
|
384
449
|
@classmethod
|
|
385
450
|
def load(cls, last_config: Optional['SSHConfig'],
|
|
386
451
|
config_paths: ConfigPaths, reload: bool,
|
|
387
|
-
*args: object) -> 'SSHConfig':
|
|
452
|
+
canonical: bool, final: bool, *args: object) -> 'SSHConfig':
|
|
388
453
|
"""Load a list of OpenSSH config files into a config object"""
|
|
389
454
|
|
|
390
|
-
config = cls(last_config, reload, *args)
|
|
455
|
+
config = cls(last_config, reload, canonical, final, *args)
|
|
391
456
|
|
|
392
457
|
if config_paths:
|
|
393
458
|
if isinstance(config_paths, (str, PurePath)):
|
|
@@ -425,12 +490,13 @@ class SSHClientConfig(SSHConfig):
|
|
|
425
490
|
|
|
426
491
|
_conditionals = {'host', 'match'}
|
|
427
492
|
_no_split = {'proxycommand', 'remotecommand'}
|
|
428
|
-
_percent_expand = {'CertificateFile', 'IdentityAgent',
|
|
493
|
+
_percent_expand = {'CertificateFile', 'ForwardAgent', 'IdentityAgent',
|
|
429
494
|
'IdentityFile', 'ProxyCommand', 'RemoteCommand'}
|
|
430
495
|
|
|
431
496
|
def __init__(self, last_config: 'SSHConfig', reload: bool,
|
|
432
|
-
|
|
433
|
-
|
|
497
|
+
canonical: bool, final: bool, local_user: str,
|
|
498
|
+
user: str, host: str, port: int) -> None:
|
|
499
|
+
super().__init__(last_config, reload, canonical, final)
|
|
434
500
|
|
|
435
501
|
self._local_user = local_user
|
|
436
502
|
self._orig_host = host
|
|
@@ -485,10 +551,10 @@ class SSHClientConfig(SSHConfig):
|
|
|
485
551
|
value: Union[bool, str] = True
|
|
486
552
|
elif value_str in ('no', 'false'):
|
|
487
553
|
value = False
|
|
488
|
-
elif value_str
|
|
489
|
-
self._error(f'Invalid {option} value: {value_str}')
|
|
490
|
-
else:
|
|
554
|
+
elif value_str in ('force', 'auto'):
|
|
491
555
|
value = value_str
|
|
556
|
+
else:
|
|
557
|
+
self._error(f'Invalid {option} value: {value_str}')
|
|
492
558
|
|
|
493
559
|
if option not in self._options:
|
|
494
560
|
self._options[option] = value
|
|
@@ -531,6 +597,11 @@ class SSHClientConfig(SSHConfig):
|
|
|
531
597
|
|
|
532
598
|
('AddressFamily', SSHConfig._set_address_family),
|
|
533
599
|
('BindAddress', SSHConfig._set_string),
|
|
600
|
+
('CanonicalDomains', SSHConfig._set_string_list),
|
|
601
|
+
('CanonicalizeFallbackLocal', SSHConfig._set_bool),
|
|
602
|
+
('CanonicalizeHostname', SSHConfig._set_canonicalize_host),
|
|
603
|
+
('CanonicalizeMaxDots', SSHConfig._set_int),
|
|
604
|
+
('CanonicalizePermittedCNAMEs', SSHConfig._set_string_list),
|
|
534
605
|
('CASignatureAlgorithms', SSHConfig._set_string),
|
|
535
606
|
('CertificateFile', SSHConfig._append_string),
|
|
536
607
|
('ChallengeResponseAuthentication', SSHConfig._set_bool),
|
|
@@ -538,7 +609,7 @@ class SSHClientConfig(SSHConfig):
|
|
|
538
609
|
('Compression', SSHConfig._set_bool),
|
|
539
610
|
('ConnectTimeout', SSHConfig._set_int),
|
|
540
611
|
('EnableSSHKeySign', SSHConfig._set_bool),
|
|
541
|
-
('ForwardAgent', SSHConfig.
|
|
612
|
+
('ForwardAgent', SSHConfig._set_bool_or_str),
|
|
542
613
|
('ForwardX11Trusted', SSHConfig._set_bool),
|
|
543
614
|
('GlobalKnownHostsFile', SSHConfig._set_string_list),
|
|
544
615
|
('GSSAPIAuthentication', SSHConfig._set_bool),
|
|
@@ -579,9 +650,9 @@ class SSHServerConfig(SSHConfig):
|
|
|
579
650
|
"""Settings from an OpenSSH server config file"""
|
|
580
651
|
|
|
581
652
|
def __init__(self, last_config: 'SSHConfig', reload: bool,
|
|
582
|
-
|
|
583
|
-
host: str, addr: str) -> None:
|
|
584
|
-
super().__init__(last_config, reload)
|
|
653
|
+
canonical: bool, final: bool, local_addr: str,
|
|
654
|
+
local_port: int, user: str, host: str, addr: str) -> None:
|
|
655
|
+
super().__init__(last_config, reload, canonical, final)
|
|
585
656
|
|
|
586
657
|
self._local_addr = local_addr
|
|
587
658
|
self._local_port = local_port
|
|
@@ -618,6 +689,11 @@ class SSHServerConfig(SSHConfig):
|
|
|
618
689
|
('AuthorizedKeysFile', SSHConfig._set_string_list),
|
|
619
690
|
('AllowAgentForwarding', SSHConfig._set_bool),
|
|
620
691
|
('BindAddress', SSHConfig._set_string),
|
|
692
|
+
('CanonicalDomains', SSHConfig._set_string_list),
|
|
693
|
+
('CanonicalizeFallbackLocal', SSHConfig._set_bool),
|
|
694
|
+
('CanonicalizeHostname', SSHConfig._set_canonicalize_host),
|
|
695
|
+
('CanonicalizeMaxDots', SSHConfig._set_int),
|
|
696
|
+
('CanonicalizePermittedCNAMEs', SSHConfig._set_string_list),
|
|
621
697
|
('CASignatureAlgorithms', SSHConfig._set_string),
|
|
622
698
|
('ChallengeResponseAuthentication', SSHConfig._set_bool),
|
|
623
699
|
('Ciphers', SSHConfig._set_string),
|