paramiko_vendor.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. # paramiko_vendor.py -- paramiko implementation of the SSHVendor interface
  2. # Copyright (C) 2013 Aaron O'Mullan <aaron.omullan@friendco.de>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as public by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Paramiko SSH support for Dulwich.
  22. To use this implementation as the SSH implementation in Dulwich, override
  23. the dulwich.client.get_ssh_vendor attribute:
  24. >>> from dulwich import client as _mod_client
  25. >>> from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor
  26. >>> _mod_client.get_ssh_vendor = ParamikoSSHVendor
  27. This implementation is experimental and does not have any tests.
  28. """
  29. from typing import Any, BinaryIO, Optional, cast
  30. import paramiko
  31. import paramiko.client
  32. class _ParamikoWrapper:
  33. def __init__(self, client: paramiko.SSHClient, channel: paramiko.Channel) -> None:
  34. self.client = client
  35. self.channel = channel
  36. # Channel must block
  37. self.channel.setblocking(True)
  38. @property
  39. def stderr(self) -> BinaryIO:
  40. return cast(BinaryIO, self.channel.makefile_stderr("rb"))
  41. def can_read(self) -> bool:
  42. return self.channel.recv_ready()
  43. def write(self, data: bytes) -> None:
  44. return self.channel.sendall(data)
  45. def read(self, n: Optional[int] = None) -> bytes:
  46. data = self.channel.recv(n or 4096)
  47. data_len = len(data)
  48. # Closed socket
  49. if not data:
  50. return b""
  51. # Read more if needed
  52. if n and data_len < n:
  53. diff_len = n - data_len
  54. return data + self.read(diff_len)
  55. return data
  56. def close(self) -> None:
  57. self.channel.close()
  58. class ParamikoSSHVendor:
  59. # http://docs.paramiko.org/en/2.4/api/client.html
  60. def __init__(self, **kwargs: object) -> None:
  61. self.kwargs = kwargs
  62. def run_command(
  63. self,
  64. host: str,
  65. command: str,
  66. username: Optional[str] = None,
  67. port: Optional[int] = None,
  68. password: Optional[str] = None,
  69. pkey: Optional[paramiko.PKey] = None,
  70. key_filename: Optional[str] = None,
  71. protocol_version: Optional[int] = None,
  72. **kwargs: object,
  73. ) -> _ParamikoWrapper:
  74. client = paramiko.SSHClient()
  75. connection_kwargs: dict[str, Any] = {"hostname": host}
  76. connection_kwargs.update(self.kwargs)
  77. if username:
  78. connection_kwargs["username"] = username
  79. if port:
  80. connection_kwargs["port"] = port
  81. if password:
  82. connection_kwargs["password"] = password
  83. if pkey:
  84. connection_kwargs["pkey"] = pkey
  85. if key_filename:
  86. connection_kwargs["key_filename"] = key_filename
  87. connection_kwargs.update(kwargs)
  88. policy = paramiko.client.MissingHostKeyPolicy()
  89. client.set_missing_host_key_policy(policy)
  90. client.connect(**connection_kwargs)
  91. # Open SSH session
  92. transport = client.get_transport()
  93. if transport is None:
  94. raise RuntimeError("Transport is None")
  95. channel = transport.open_session()
  96. if protocol_version is None or protocol_version == 2:
  97. channel.set_environment_variable(name="GIT_PROTOCOL", value="version=2")
  98. # Run commands
  99. channel.exec_command(command)
  100. return _ParamikoWrapper(client, channel)