test_paramiko_vendor.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # test_paramiko_vendor.py
  2. #
  3. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Tests for paramiko_vendor."""
  21. import os
  22. import socket
  23. import tempfile
  24. import threading
  25. from io import StringIO
  26. from typing import Optional
  27. from unittest import skipIf
  28. from unittest.mock import patch
  29. from .. import TestCase
  30. try:
  31. import paramiko
  32. except ImportError:
  33. has_paramiko = False
  34. else:
  35. has_paramiko = True
  36. from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor
  37. class Server(paramiko.ServerInterface):
  38. """http://docs.paramiko.org/en/2.4/api/server.html."""
  39. def __init__(self, commands, *args, **kwargs) -> None:
  40. super().__init__(*args, **kwargs)
  41. self.commands = commands
  42. def check_channel_exec_request(self, channel, command) -> bool:
  43. self.commands.append(command)
  44. return True
  45. def check_auth_password(self, username, password):
  46. if username == USER and password == PASSWORD:
  47. return paramiko.AUTH_SUCCESSFUL
  48. return paramiko.AUTH_FAILED
  49. def check_auth_publickey(self, username, key):
  50. pubkey = paramiko.RSAKey.from_private_key(StringIO(CLIENT_KEY))
  51. if username == USER and key == pubkey:
  52. return paramiko.AUTH_SUCCESSFUL
  53. return paramiko.AUTH_FAILED
  54. def check_channel_request(self, kind, chanid):
  55. if kind == "session":
  56. return paramiko.OPEN_SUCCEEDED
  57. return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
  58. def get_allowed_auths(self, username) -> str:
  59. return "password,publickey"
  60. USER = "testuser"
  61. PASSWORD = "test"
  62. SERVER_KEY = """\
  63. -----BEGIN RSA PRIVATE KEY-----
  64. MIIEpAIBAAKCAQEAy/L1sSYAzxsMprtNXW4u/1jGXXkQmQ2xtmKVlR+RlIL3a1BH
  65. bzTpPlZyjltAAwzIP8XRh0iJFKz5y3zSQChhX47ZGN0NvQsVct8R+YwsUonwfAJ+
  66. JN0KBKKvC8fPHlzqBr3gX+ZxqsFH934tQ6wdQPH5eQWtdM8L826lMsH1737uyTGk
  67. +mCSDjL3c6EzY83g7qhkJU2R4qbi6ne01FaWADzG8sOzXnHT+xpxtk8TTT8yCVUY
  68. MmBNsSoA/ka3iWz70ghB+6Xb0WpFJZXWq1oYovviPAfZGZSrxBZMxsWMye70SdLl
  69. TqsBEt0+miIcm9s0fvjWvQuhaHX6mZs5VO4r5QIDAQABAoIBAGYqeYWaYgFdrYLA
  70. hUrubUCg+g3NHdFuGL4iuIgRXl4lFUh+2KoOuWDu8Uf60iA1AQNhV0sLvQ/Mbv3O
  71. s4xMLisuZfaclctDiCUZNenqnDFkxEF7BjH1QJV94W5nU4wEQ3/JEmM4D2zYkfKb
  72. FJW33JeyH6TOgUvohDYYEU1R+J9V8qA243p+ui1uVtNI6Pb0TXJnG5y9Ny4vkSWH
  73. Fi0QoMPR1r9xJ4SEearGzA/crb4SmmDTKhGSoMsT3d5ATieLmwcS66xWz8w4oFGJ
  74. yzDq24s4Fp9ccNjMf/xR8XRiekJv835gjEqwF9IXyvgOaq6XJ1iCqGPFDKa25nui
  75. JnEstOkCgYEA/ZXk7aIanvdeJlTqpX578sJfCnrXLydzE8emk1b7+5mrzGxQ4/pM
  76. PBQs2f8glT3t0O0mRX9NoRqnwrid88/b+cY4NCOICFZeasX336/gYQxyVeRLJS6Z
  77. hnGEQqry8qS7PdKAyeHMNmZFrUh4EiHiObymEfQS+mkRUObn0cGBTw8CgYEAzeQU
  78. D2baec1DawjppKaRynAvWjp+9ry1lZx9unryKVRwjRjkEpw+b3/+hdaF1IvsVSce
  79. cNj+6W2guZ2tyHuPhZ64/4SJVyE2hKDSKD4xTb2nVjsMeN0bLD2UWXC9mwbx8nWa
  80. 2tmtUZ7a/okQb2cSdosJinRewLNqXIsBXamT1csCgYEA0cXb2RCOQQ6U3dTFPx4A
  81. 3vMXuA2iUKmrsqMoEx6T2LBow/Sefdkik1iFOdipVYwjXP+w9zC2QR1Rxez/DR/X
  82. 8ymceNUjxPHdrSoTQQG29dFcC92MpDeGXQcuyA+uZjcLhbrLOzYEvsOfxBb87NMG
  83. 14hNQPDNekTMREafYo9WrtUCgYAREK54+FVzcwf7fymedA/xb4r9N4v+d3W1iNsC
  84. 8d3Qfyc1CrMct8aVB07ZWQaOr2pPRIbJY7L9NhD0UZVt4I/sy1MaGqonhqE2LP4+
  85. R6legDG2e/50ph7yc8gwAaA1kUXMiuLi8Nfkw/3yyvmJwklNegi4aRzRbA2Mzhi2
  86. 4q9WMQKBgQCb0JNyxHG4pvLWCF/j0Sm1FfvrpnqSv5678n1j4GX7Ka/TubOK1Y4K
  87. U+Oib7dKa/zQMWehVFNTayrsq6bKVZ6q7zG+IHiRLw4wjeAxREFH6WUjDrn9vl2l
  88. D48DKbBuBwuVOJWyq3qbfgJXojscgNQklrsPdXVhDwOF0dYxP89HnA==
  89. -----END RSA PRIVATE KEY-----"""
  90. CLIENT_KEY = """\
  91. -----BEGIN RSA PRIVATE KEY-----
  92. MIIEpAIBAAKCAQEAxvREKSElPOm/0z/nPO+j5rk2tjdgGcGc7We1QZ6TRXYLu7nN
  93. GeEFIL4p8N1i6dmB+Eydt7xqCU79MWD6Yy4prFe1+/K1wCDUxIbFMxqQcX5zjJzd
  94. i8j8PbcaUlVhP/OkjtkSxrXaGDO1BzfdV4iEBtTV/2l3zmLKJlt3jnOHLczP24CB
  95. DTQKp3rKshbRefzot9Y+wnaK692RsYgsyo9YEP0GyWKG9topCHk13r46J6vGLeuj
  96. ryUKqmbLJkzbJbIcEqwTDo5iHaCVqaMr5Hrb8BdMucSseqZQJsXSd+9tdRcIblUQ
  97. 38kZjmFMm4SFbruJcpZCNM2wNSZPIRX+3eiwNwIDAQABAoIBAHSacOBSJsr+jIi5
  98. KUOTh9IPtzswVUiDKwARCjB9Sf8p4lKR4N1L/n9kNJyQhApeikgGT2GCMftmqgoo
  99. tlculQoHFgemBlOmak0MV8NNzF5YKEy/GzF0CDH7gJfEpoyetVFrdA+2QS5yD6U9
  100. XqKQxiBi2VEqdScmyyeT8AwzNYTnPeH/DOEcnbdRjqiy/CD79F49CQ1lX1Fuqm0K
  101. I7BivBH1xo/rVnUP4F+IzocDqoga+Pjdj0LTXIgJlHQDSbhsQqWujWQDDuKb+MAw
  102. sNK4Zf8ErV3j1PyA7f/M5LLq6zgstkW4qikDHo4SpZX8kFOO8tjqb7kujj7XqeaB
  103. CxqrOTECgYEA73uWkrohcmDJ4KqbuL3tbExSCOUiaIV+sT1eGPNi7GCmXD4eW5Z4
  104. 75v2IHymW83lORSu/DrQ6sKr1nkuRpqr2iBzRmQpl/H+wahIhBXlnJ25uUjDsuPO
  105. 1Pq2LcmyD+jTxVnmbSe/q7O09gZQw3I6H4+BMHmpbf8tC97lqimzpJ0CgYEA1K0W
  106. ZL70Xtn9quyHvbtae/BW07NZnxvUg4UaVIAL9Zu34JyplJzyzbIjrmlDbv6aRogH
  107. /KtuG9tfbf55K/jjqNORiuRtzt1hUN1ye4dyW7tHx2/7lXdlqtyK40rQl8P0kqf8
  108. zaS6BqjnobgSdSpg32rWoL/pcBHPdJCJEgQ8zeMCgYEA0/PK8TOhNIzrP1dgGSKn
  109. hkkJ9etuB5nW5mEM7gJDFDf6JPupfJ/xiwe6z0fjKK9S57EhqgUYMB55XYnE5iIw
  110. ZQ6BV9SAZ4V7VsRs4dJLdNC3tn/rDGHJBgCaym2PlbsX6rvFT+h1IC8dwv0V79Ui
  111. Ehq9WTzkMoE8yhvNokvkPZUCgYEAgBAFxv5xGdh79ftdtXLmhnDvZ6S8l6Fjcxqo
  112. Ay/jg66Tp43OU226iv/0mmZKM8Dd1xC8dnon4GBVc19jSYYiWBulrRPlx0Xo/o+K
  113. CzZBN1lrXH1i6dqufpc0jq8TMf/N+q1q/c1uMupsKCY1/xVYpc+ok71b7J7c49zQ
  114. nOeuUW8CgYA9Infooy65FTgbzca0c9kbCUBmcAPQ2ItH3JcPKWPQTDuV62HcT00o
  115. fZdIV47Nez1W5Clk191RMy8TXuqI54kocciUWpThc6j44hz49oUueb8U4bLcEHzA
  116. WxtWBWHwxfSmqgTXilEA3ALJp0kNolLnEttnhENwJpZHlqtes0ZA4w==
  117. -----END RSA PRIVATE KEY-----"""
  118. @skipIf(not has_paramiko, "paramiko is not installed")
  119. class ParamikoSSHVendorTests(TestCase):
  120. def setUp(self) -> None:
  121. import paramiko.transport
  122. # re-enable server functionality for tests
  123. if hasattr(paramiko.transport, "SERVER_DISABLED_BY_GENTOO"):
  124. paramiko.transport.SERVER_DISABLED_BY_GENTOO = False
  125. self.commands = []
  126. socket.setdefaulttimeout(10)
  127. self.addCleanup(socket.setdefaulttimeout, None)
  128. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  129. self.socket.bind(("127.0.0.1", 0))
  130. self.socket.listen(5)
  131. self.addCleanup(self.socket.close)
  132. self.port = self.socket.getsockname()[1]
  133. self.thread = threading.Thread(target=self._run)
  134. self.thread.start()
  135. def tearDown(self) -> None:
  136. self.thread.join()
  137. def _run(self) -> Optional[bool]:
  138. try:
  139. conn, addr = self.socket.accept()
  140. except OSError:
  141. return False
  142. self.transport = paramiko.Transport(conn)
  143. self.addCleanup(self.transport.close)
  144. host_key = paramiko.RSAKey.from_private_key(StringIO(SERVER_KEY))
  145. self.transport.add_server_key(host_key)
  146. server = Server(self.commands)
  147. self.transport.start_server(server=server)
  148. def test_run_command_password(self) -> None:
  149. vendor = ParamikoSSHVendor(
  150. allow_agent=False,
  151. look_for_keys=False,
  152. )
  153. vendor.run_command(
  154. "127.0.0.1",
  155. "test_run_command_password",
  156. username=USER,
  157. port=self.port,
  158. password=PASSWORD,
  159. )
  160. self.assertIn(b"test_run_command_password", self.commands)
  161. def test_run_command_with_privkey(self) -> None:
  162. key = paramiko.RSAKey.from_private_key(StringIO(CLIENT_KEY))
  163. vendor = ParamikoSSHVendor(
  164. allow_agent=False,
  165. look_for_keys=False,
  166. )
  167. vendor.run_command(
  168. "127.0.0.1",
  169. "test_run_command_with_privkey",
  170. username=USER,
  171. port=self.port,
  172. pkey=key,
  173. )
  174. self.assertIn(b"test_run_command_with_privkey", self.commands)
  175. def test_run_command_data_transfer(self) -> None:
  176. vendor = ParamikoSSHVendor(
  177. allow_agent=False,
  178. look_for_keys=False,
  179. )
  180. con = vendor.run_command(
  181. "127.0.0.1",
  182. "test_run_command_data_transfer",
  183. username=USER,
  184. port=self.port,
  185. password=PASSWORD,
  186. )
  187. self.assertIn(b"test_run_command_data_transfer", self.commands)
  188. channel = self.transport.accept(5)
  189. channel.send(b"stdout\n")
  190. channel.send_stderr(b"stderr\n")
  191. channel.close()
  192. # Fixme: it's return false
  193. # self.assertTrue(con.can_read())
  194. self.assertEqual(b"stdout\n", con.read(4096))
  195. # Fixme: it's return empty string
  196. # self.assertEqual(b'stderr\n', con.read_stderr(4096))
  197. def test_ssh_config_parsing(self) -> None:
  198. """Test that SSH config is properly parsed and used by ParamikoSSHVendor."""
  199. # Create a temporary SSH config file
  200. with tempfile.NamedTemporaryFile(mode="w", suffix=".config", delete=False) as f:
  201. f.write(
  202. f"""
  203. Host testserver
  204. HostName 127.0.0.1
  205. User testuser
  206. Port {self.port}
  207. IdentityFile /path/to/key
  208. """
  209. )
  210. config_path = f.name
  211. try:
  212. # Mock the config path
  213. with patch(
  214. "dulwich.contrib.paramiko_vendor.os.path.expanduser"
  215. ) as mock_expanduser:
  216. def side_effect(path):
  217. if path == "~/.ssh/config":
  218. return config_path
  219. return path
  220. mock_expanduser.side_effect = side_effect
  221. vendor = ParamikoSSHVendor(
  222. allow_agent=False,
  223. look_for_keys=False,
  224. )
  225. # Test that SSH config values are loaded
  226. host_config = vendor.ssh_config.lookup("testserver")
  227. self.assertEqual(host_config["hostname"], "127.0.0.1")
  228. self.assertEqual(host_config["user"], "testuser")
  229. self.assertEqual(host_config["port"], str(self.port))
  230. self.assertIn("/path/to/key", host_config["identityfile"])
  231. finally:
  232. os.unlink(config_path)