SshClient.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.IO;
  6. using System.Net;
  7. using System.Text;
  8. using Renci.SshNet.Common;
  9. namespace Renci.SshNet
  10. {
  11. /// <inheritdoc cref="ISshClient" />
  12. public class SshClient : BaseClient, ISshClient
  13. {
  14. /// <summary>
  15. /// Holds the list of forwarded ports.
  16. /// </summary>
  17. private readonly List<ForwardedPort> _forwardedPorts;
  18. /// <summary>
  19. /// Holds a value indicating whether the current instance is disposed.
  20. /// </summary>
  21. /// <value>
  22. /// <see langword="true"/> if the current instance is disposed; otherwise, <see langword="false"/>.
  23. /// </value>
  24. private bool _isDisposed;
  25. private MemoryStream? _inputStream;
  26. /// <inheritdoc />
  27. public IEnumerable<ForwardedPort> ForwardedPorts
  28. {
  29. get
  30. {
  31. return _forwardedPorts.AsReadOnly();
  32. }
  33. }
  34. /// <summary>
  35. /// Initializes a new instance of the <see cref="SshClient" /> class.
  36. /// </summary>
  37. /// <param name="connectionInfo">The connection info.</param>
  38. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  39. public SshClient(ConnectionInfo connectionInfo)
  40. : this(connectionInfo, ownsConnectionInfo: false)
  41. {
  42. }
  43. /// <summary>
  44. /// Initializes a new instance of the <see cref="SshClient"/> class.
  45. /// </summary>
  46. /// <param name="host">Connection host.</param>
  47. /// <param name="port">Connection port.</param>
  48. /// <param name="username">Authentication username.</param>
  49. /// <param name="password">Authentication password.</param>
  50. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
  51. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  52. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  53. public SshClient(string host, int port, string username, string password)
  54. #pragma warning disable CA2000 // Dispose objects before losing scope
  55. : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true)
  56. #pragma warning restore CA2000 // Dispose objects before losing scope
  57. {
  58. }
  59. /// <summary>
  60. /// Initializes a new instance of the <see cref="SshClient"/> class.
  61. /// </summary>
  62. /// <param name="host">Connection host.</param>
  63. /// <param name="username">Authentication username.</param>
  64. /// <param name="password">Authentication password.</param>
  65. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
  66. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, or <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  67. public SshClient(string host, string username, string password)
  68. : this(host, ConnectionInfo.DefaultPort, username, password)
  69. {
  70. }
  71. /// <summary>
  72. /// Initializes a new instance of the <see cref="SshClient"/> class.
  73. /// </summary>
  74. /// <param name="host">Connection host.</param>
  75. /// <param name="port">Connection port.</param>
  76. /// <param name="username">Authentication username.</param>
  77. /// <param name="keyFiles">Authentication private key file(s) .</param>
  78. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
  79. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  80. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  81. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  82. public SshClient(string host, int port, string username, params IPrivateKeySource[] keyFiles)
  83. : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true)
  84. {
  85. }
  86. /// <summary>
  87. /// Initializes a new instance of the <see cref="SshClient"/> class.
  88. /// </summary>
  89. /// <param name="host">Connection host.</param>
  90. /// <param name="username">Authentication username.</param>
  91. /// <param name="keyFiles">Authentication private key file(s) .</param>
  92. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
  93. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid, -or- <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  94. public SshClient(string host, string username, params IPrivateKeySource[] keyFiles)
  95. : this(host, ConnectionInfo.DefaultPort, username, keyFiles)
  96. {
  97. }
  98. /// <summary>
  99. /// Initializes a new instance of the <see cref="SshClient"/> class.
  100. /// </summary>
  101. /// <param name="connectionInfo">The connection info.</param>
  102. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  103. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  104. /// <remarks>
  105. /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
  106. /// connection info will be disposed when this instance is disposed.
  107. /// </remarks>
  108. private SshClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
  109. : this(connectionInfo, ownsConnectionInfo, new ServiceFactory())
  110. {
  111. }
  112. /// <summary>
  113. /// Initializes a new instance of the <see cref="SshClient"/> class.
  114. /// </summary>
  115. /// <param name="connectionInfo">The connection info.</param>
  116. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  117. /// <param name="serviceFactory">The factory to use for creating new services.</param>
  118. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  119. /// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <see langword="null"/>.</exception>
  120. /// <remarks>
  121. /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, then the
  122. /// connection info will be disposed when this instance is disposed.
  123. /// </remarks>
  124. internal SshClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
  125. : base(connectionInfo, ownsConnectionInfo, serviceFactory)
  126. {
  127. _forwardedPorts = new List<ForwardedPort>();
  128. }
  129. /// <summary>
  130. /// Called when client is disconnecting from the server.
  131. /// </summary>
  132. protected override void OnDisconnecting()
  133. {
  134. base.OnDisconnecting();
  135. foreach (var port in _forwardedPorts)
  136. {
  137. port.Stop();
  138. }
  139. }
  140. /// <inheritdoc />
  141. public void AddForwardedPort(ForwardedPort port)
  142. {
  143. ThrowHelper.ThrowIfNull(port);
  144. EnsureSessionIsOpen();
  145. AttachForwardedPort(port);
  146. _forwardedPorts.Add(port);
  147. }
  148. /// <inheritdoc />
  149. public void RemoveForwardedPort(ForwardedPort port)
  150. {
  151. ThrowHelper.ThrowIfNull(port);
  152. // Stop port forwarding before removing it
  153. port.Stop();
  154. DetachForwardedPort(port);
  155. _ = _forwardedPorts.Remove(port);
  156. }
  157. private void AttachForwardedPort(ForwardedPort port)
  158. {
  159. if (port.Session != null && port.Session != Session)
  160. {
  161. throw new InvalidOperationException("Forwarded port is already added to a different client.");
  162. }
  163. port.Session = Session;
  164. }
  165. private static void DetachForwardedPort(ForwardedPort port)
  166. {
  167. port.Session = null;
  168. }
  169. /// <inheritdoc />
  170. public SshCommand CreateCommand(string commandText)
  171. {
  172. return CreateCommand(commandText, ConnectionInfo.Encoding);
  173. }
  174. /// <inheritdoc />
  175. public SshCommand CreateCommand(string commandText, Encoding encoding)
  176. {
  177. EnsureSessionIsOpen();
  178. ConnectionInfo.Encoding = encoding;
  179. return new SshCommand(Session!, commandText, encoding);
  180. }
  181. /// <inheritdoc />
  182. public SshCommand RunCommand(string commandText)
  183. {
  184. var cmd = CreateCommand(commandText);
  185. _ = cmd.Execute();
  186. return cmd;
  187. }
  188. /// <inheritdoc />
  189. public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint>? terminalModes, int bufferSize)
  190. {
  191. EnsureSessionIsOpen();
  192. return new Shell(Session, input, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, bufferSize);
  193. }
  194. /// <inheritdoc />
  195. public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModes)
  196. {
  197. return CreateShell(input, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, 1024);
  198. }
  199. /// <inheritdoc />
  200. public Shell CreateShell(Stream input, Stream output, Stream extendedOutput)
  201. {
  202. return CreateShell(input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
  203. }
  204. /// <inheritdoc />
  205. public Shell CreateShell(Encoding encoding, string input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint>? terminalModes, int bufferSize)
  206. {
  207. /*
  208. * TODO Issue #1224: let shell dispose of input stream when we own the stream!
  209. */
  210. _inputStream = new MemoryStream();
  211. using (var writer = new StreamWriter(_inputStream, encoding, bufferSize: 1024, leaveOpen: true))
  212. {
  213. writer.Write(input);
  214. writer.Flush();
  215. }
  216. _ = _inputStream.Seek(0, SeekOrigin.Begin);
  217. return CreateShell(_inputStream, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, bufferSize);
  218. }
  219. /// <inheritdoc />
  220. public Shell CreateShell(Encoding encoding, string input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModes)
  221. {
  222. return CreateShell(encoding, input, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, 1024);
  223. }
  224. /// <inheritdoc />
  225. public Shell CreateShell(Encoding encoding, string input, Stream output, Stream extendedOutput)
  226. {
  227. return CreateShell(encoding, input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
  228. }
  229. /// <inheritdoc />
  230. public Shell CreateShellNoTerminal(Stream input, Stream output, Stream extendedOutput, int bufferSize = -1)
  231. {
  232. EnsureSessionIsOpen();
  233. return new Shell(Session, input, output, extendedOutput, bufferSize);
  234. }
  235. /// <inheritdoc />
  236. public ShellStream CreateShellStream(string terminalName, uint columns, uint rows, uint width, uint height, int bufferSize)
  237. {
  238. return CreateShellStream(terminalName, columns, rows, width, height, bufferSize, terminalModeValues: null);
  239. }
  240. /// <inheritdoc />
  241. public ShellStream CreateShellStream(string terminalName, uint columns, uint rows, uint width, uint height, int bufferSize, IDictionary<TerminalModes, uint>? terminalModeValues)
  242. {
  243. EnsureSessionIsOpen();
  244. return ServiceFactory.CreateShellStream(Session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
  245. }
  246. /// <inheritdoc />
  247. public ShellStream CreateShellStreamNoTerminal(int bufferSize = -1)
  248. {
  249. EnsureSessionIsOpen();
  250. return ServiceFactory.CreateShellStreamNoTerminal(Session, bufferSize);
  251. }
  252. /// <summary>
  253. /// Stops forwarded ports.
  254. /// </summary>
  255. protected override void OnDisconnected()
  256. {
  257. base.OnDisconnected();
  258. for (var i = _forwardedPorts.Count - 1; i >= 0; i--)
  259. {
  260. var port = _forwardedPorts[i];
  261. DetachForwardedPort(port);
  262. _forwardedPorts.RemoveAt(i);
  263. }
  264. }
  265. /// <summary>
  266. /// Releases unmanaged and - optionally - managed resources.
  267. /// </summary>
  268. /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
  269. protected override void Dispose(bool disposing)
  270. {
  271. base.Dispose(disposing);
  272. if (_isDisposed)
  273. {
  274. return;
  275. }
  276. if (disposing)
  277. {
  278. if (_inputStream != null)
  279. {
  280. _inputStream.Dispose();
  281. _inputStream = null;
  282. }
  283. _isDisposed = true;
  284. }
  285. }
  286. private void EnsureSessionIsOpen()
  287. {
  288. if (Session is null)
  289. {
  290. throw new SshConnectionException("Client not connected.");
  291. }
  292. }
  293. }
  294. }