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.
Files changed (178) hide show
  1. {asyncssh-2.18.0 → asyncssh-2.20.0}/.github/workflows/run_tests.yml +3 -18
  2. {asyncssh-2.18.0/asyncssh.egg-info → asyncssh-2.20.0}/PKG-INFO +8 -9
  3. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/client.py +9 -0
  4. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/config.py +137 -61
  5. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/connection.py +277 -45
  6. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/misc.py +8 -7
  7. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/process.py +2 -2
  8. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/public_key.py +7 -4
  9. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sftp.py +246 -46
  10. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sk.py +90 -19
  11. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sk_ecdsa.py +45 -22
  12. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/sk_eddsa.py +11 -12
  13. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/version.py +1 -1
  14. {asyncssh-2.18.0 → asyncssh-2.20.0/asyncssh.egg-info}/PKG-INFO +8 -9
  15. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/SOURCES.txt +1 -1
  16. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/api.rst +30 -6
  17. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/changes.rst +63 -0
  18. asyncssh-2.20.0/pyproject.toml +59 -0
  19. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/server.py +16 -12
  20. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/sk_stub.py +100 -18
  21. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_channel.py +3 -3
  22. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_config.py +70 -9
  23. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_connection.py +123 -1
  24. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_connection_auth.py +2 -1
  25. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_public_key.py +10 -0
  26. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_sftp.py +152 -13
  27. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_sk.py +18 -2
  28. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/util.py +25 -0
  29. {asyncssh-2.18.0 → asyncssh-2.20.0}/tox.ini +3 -3
  30. asyncssh-2.18.0/setup.py +0 -92
  31. {asyncssh-2.18.0 → asyncssh-2.20.0}/.coveragerc +0 -0
  32. {asyncssh-2.18.0 → asyncssh-2.20.0}/.gitignore +0 -0
  33. {asyncssh-2.18.0 → asyncssh-2.20.0}/.readthedocs.yaml +0 -0
  34. {asyncssh-2.18.0 → asyncssh-2.20.0}/CONTRIBUTING.rst +0 -0
  35. {asyncssh-2.18.0 → asyncssh-2.20.0}/COPYRIGHT +0 -0
  36. {asyncssh-2.18.0 → asyncssh-2.20.0}/LICENSE +0 -0
  37. {asyncssh-2.18.0 → asyncssh-2.20.0}/MANIFEST.in +0 -0
  38. {asyncssh-2.18.0 → asyncssh-2.20.0}/README.rst +0 -0
  39. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/__init__.py +0 -0
  40. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/agent.py +0 -0
  41. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/agent_unix.py +0 -0
  42. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/agent_win32.py +0 -0
  43. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/asn1.py +0 -0
  44. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/auth.py +0 -0
  45. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/auth_keys.py +0 -0
  46. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/channel.py +0 -0
  47. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/compression.py +0 -0
  48. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/constants.py +0 -0
  49. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/__init__.py +0 -0
  50. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/chacha.py +0 -0
  51. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/cipher.py +0 -0
  52. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/dh.py +0 -0
  53. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/dsa.py +0 -0
  54. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/ec.py +0 -0
  55. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/ec_params.py +0 -0
  56. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/ed.py +0 -0
  57. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/kdf.py +0 -0
  58. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/misc.py +0 -0
  59. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/pq.py +0 -0
  60. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/rsa.py +0 -0
  61. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/umac.py +0 -0
  62. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/crypto/x509.py +0 -0
  63. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/dsa.py +0 -0
  64. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/ecdsa.py +0 -0
  65. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/eddsa.py +0 -0
  66. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/editor.py +0 -0
  67. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/encryption.py +0 -0
  68. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/forward.py +0 -0
  69. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/gss.py +0 -0
  70. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/gss_unix.py +0 -0
  71. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/gss_win32.py +0 -0
  72. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/kex.py +0 -0
  73. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/kex_dh.py +0 -0
  74. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/kex_rsa.py +0 -0
  75. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/keysign.py +0 -0
  76. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/known_hosts.py +0 -0
  77. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/listener.py +0 -0
  78. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/logging.py +0 -0
  79. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/mac.py +0 -0
  80. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/packet.py +0 -0
  81. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/pattern.py +0 -0
  82. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/pbe.py +0 -0
  83. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/pkcs11.py +0 -0
  84. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/py.typed +0 -0
  85. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/rsa.py +0 -0
  86. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/saslprep.py +0 -0
  87. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/scp.py +0 -0
  88. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/server.py +0 -0
  89. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/session.py +0 -0
  90. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/socks.py +0 -0
  91. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/stream.py +0 -0
  92. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/subprocess.py +0 -0
  93. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/tuntap.py +0 -0
  94. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh/x11.py +0 -0
  95. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/dependency_links.txt +0 -0
  96. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/requires.txt +0 -0
  97. {asyncssh-2.18.0 → asyncssh-2.20.0}/asyncssh.egg-info/top_level.txt +0 -0
  98. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/_templates/sidebarbottom.html +0 -0
  99. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/_templates/sidebartop.html +0 -0
  100. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/conf.py +0 -0
  101. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/contributing.rst +0 -0
  102. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/index.rst +0 -0
  103. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/requirements.txt +0 -0
  104. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rftheme/layout.html +0 -0
  105. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rftheme/static/rftheme.css_t +0 -0
  106. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rftheme/theme.conf +0 -0
  107. {asyncssh-2.18.0 → asyncssh-2.20.0}/docs/rtd-req.txt +0 -0
  108. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_client.py +0 -0
  109. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_client2.py +0 -0
  110. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_client3.py +0 -0
  111. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/callback_math_server.py +0 -0
  112. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/chat_server.py +0 -0
  113. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/check_exit_status.py +0 -0
  114. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/chroot_sftp_server.py +0 -0
  115. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/direct_client.py +0 -0
  116. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/direct_server.py +0 -0
  117. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/editor.py +0 -0
  118. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/gather_results.py +0 -0
  119. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/listening_client.py +0 -0
  120. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/local_forwarding_client.py +0 -0
  121. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/local_forwarding_client2.py +0 -0
  122. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/local_forwarding_server.py +0 -0
  123. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/math_client.py +0 -0
  124. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/math_server.py +0 -0
  125. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_input.py +0 -0
  126. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_local_pipe.py +0 -0
  127. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_remote_pipe.py +0 -0
  128. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/redirect_server.py +0 -0
  129. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/remote_forwarding_client.py +0 -0
  130. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/remote_forwarding_client2.py +0 -0
  131. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/remote_forwarding_server.py +0 -0
  132. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/reverse_client.py +0 -0
  133. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/reverse_server.py +0 -0
  134. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/scp_client.py +0 -0
  135. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/set_environment.py +0 -0
  136. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/set_terminal.py +0 -0
  137. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/sftp_client.py +0 -0
  138. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/show_environment.py +0 -0
  139. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/show_terminal.py +0 -0
  140. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_cert_server.py +0 -0
  141. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_client.py +0 -0
  142. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_keyed_server.py +0 -0
  143. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_scp_server.py +0 -0
  144. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_server.py +0 -0
  145. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/simple_sftp_server.py +0 -0
  146. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/stream_direct_client.py +0 -0
  147. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/stream_direct_server.py +0 -0
  148. {asyncssh-2.18.0 → asyncssh-2.20.0}/examples/stream_listening_client.py +0 -0
  149. {asyncssh-2.18.0 → asyncssh-2.20.0}/mypy.ini +0 -0
  150. {asyncssh-2.18.0 → asyncssh-2.20.0}/pylintrc +0 -0
  151. {asyncssh-2.18.0 → asyncssh-2.20.0}/setup.cfg +0 -0
  152. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/__init__.py +0 -0
  153. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/gss_stub.py +0 -0
  154. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/gssapi_stub.py +0 -0
  155. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/keysign_stub.py +0 -0
  156. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/pkcs11_stub.py +0 -0
  157. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/sspi_stub.py +0 -0
  158. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_agent.py +0 -0
  159. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_asn1.py +0 -0
  160. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_auth.py +0 -0
  161. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_auth_keys.py +0 -0
  162. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_compression.py +0 -0
  163. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_editor.py +0 -0
  164. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_encryption.py +0 -0
  165. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_forward.py +0 -0
  166. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_kex.py +0 -0
  167. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_known_hosts.py +0 -0
  168. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_logging.py +0 -0
  169. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_mac.py +0 -0
  170. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_packet.py +0 -0
  171. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_pkcs11.py +0 -0
  172. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_process.py +0 -0
  173. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_saslprep.py +0 -0
  174. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_stream.py +0 -0
  175. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_subprocess.py +0 -0
  176. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_tuntap.py +0 -0
  177. {asyncssh-2.18.0 → asyncssh-2.20.0}/tests/test_x11.py +0 -0
  178. {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.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
11
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
12
12
  include:
13
13
  - os: macos-latest
14
14
  python-version: "3.10"
@@ -19,22 +19,9 @@ jobs:
19
19
  - os: macos-latest
20
20
  python-version: "3.12"
21
21
  openssl-version: "3"
22
- exclude:
23
- # having trouble with arch arm64 on macos-ltest on Python 3.7
24
22
  - os: macos-latest
25
- python-version: "3.7"
26
-
27
- # test hangs on these combination
28
- - os: windows-latest
29
- python-version: "3.8"
30
- - os: windows-latest
31
- python-version: "3.9"
32
- - os: windows-latest
33
- python-version: "3.10"
34
- - os: windows-latest
35
- python-version: "3.11"
36
- - os: windows-latest
37
- python-version: "3.12"
23
+ python-version: "3.13"
24
+ openssl-version: "3"
38
25
 
39
26
  runs-on: ${{ matrix.os }}
40
27
  env:
@@ -153,8 +140,6 @@ jobs:
153
140
  steps:
154
141
  - uses: actions/checkout@v4
155
142
  - uses: actions/setup-python@v5
156
- with:
157
- python-version: "3.7"
158
143
  - uses: actions/download-artifact@v4
159
144
  with:
160
145
  name: coverage
@@ -1,32 +1,31 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: asyncssh
3
- Version: 2.18.0
3
+ Version: 2.20.0
4
4
  Summary: AsyncSSH: Asynchronous SSHv2 client and server library
5
- Home-page: http://asyncssh.timeheart.net
6
- Author: Ron Frederick
7
- Author-email: ronf@timeheart.net
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: >= 3.6
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 _expand_val(self, value: str) -> str:
94
- """Perform percent token expansion on a string"""
100
+ def _expand_token(self, match):
101
+ """Expand a percent token reference"""
95
102
 
96
- last_idx = 0
97
- result: List[str] = []
103
+ try:
104
+ token = match.group(1)
105
+ return self._tokens[token]
106
+ except KeyError:
107
+ if token == 'd':
108
+ raise ConfigParseError('Home directory is '
109
+ 'not available') from None
110
+ elif token == 'i':
111
+ raise ConfigParseError('User id not available') from None
112
+ else:
113
+ raise ConfigParseError('Invalid token expansion: ' +
114
+ token) from None
98
115
 
99
- for match in re.finditer(r'%', value):
100
- idx = match.start()
116
+ @staticmethod
117
+ def _expand_env(match):
118
+ """Expand an environment variable reference"""
101
119
 
102
- if idx < last_idx:
103
- continue
120
+ try:
121
+ var = match.group(1)
122
+ return os.environ[var]
123
+ except KeyError:
124
+ raise ConfigParseError('Invalid environment expansion: ' +
125
+ var) from None
104
126
 
105
- try:
106
- token = value[idx+1]
107
- result.extend([value[last_idx:idx], self._tokens[token]])
108
- last_idx = idx + 2
109
- except IndexError:
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
- self._matching = True
161
- continue
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
- match_val = self._match_val(match)
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 match != 'exec' and match_val is None:
166
- self._error('Invalid match condition')
208
+ if matching and result == negated:
209
+ matching = False
167
210
 
168
- try:
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
- local_user: str, user: str, host: str, port: int) -> None:
433
- super().__init__(last_config, reload)
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 not in ('force', 'auto'):
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._set_bool),
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
- local_addr: str, local_port: int, user: str,
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),