2
0

BaseClient.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. using System;
  2. using System.Net.Sockets;
  3. using System.Threading;
  4. using Renci.SshNet.Common;
  5. namespace Renci.SshNet
  6. {
  7. /// <summary>
  8. /// Serves as base class for client implementations, provides common client functionality.
  9. /// </summary>
  10. public abstract class BaseClient : IDisposable
  11. {
  12. private static readonly TimeSpan Infinite = new TimeSpan(0, 0, 0, 0, -1);
  13. /// <summary>
  14. /// Holds value indicating whether the connection info is owned by this client.
  15. /// </summary>
  16. private readonly bool _ownsConnectionInfo;
  17. private TimeSpan _keepAliveInterval;
  18. private Timer _keepAliveTimer;
  19. private ConnectionInfo _connectionInfo;
  20. /// <summary>
  21. /// Gets current session.
  22. /// </summary>
  23. protected Session Session { get; private set; }
  24. /// <summary>
  25. /// Gets the connection info.
  26. /// </summary>
  27. /// <value>
  28. /// The connection info.
  29. /// </value>
  30. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  31. public ConnectionInfo ConnectionInfo
  32. {
  33. get
  34. {
  35. CheckDisposed();
  36. return _connectionInfo;
  37. }
  38. private set
  39. {
  40. _connectionInfo = value;
  41. }
  42. }
  43. /// <summary>
  44. /// Gets a value indicating whether this client is connected to the server.
  45. /// </summary>
  46. /// <value>
  47. /// <c>true</c> if this client is connected; otherwise, <c>false</c>.
  48. /// </value>
  49. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  50. public bool IsConnected
  51. {
  52. get
  53. {
  54. CheckDisposed();
  55. return this.Session != null && this.Session.IsConnected;
  56. }
  57. }
  58. /// <summary>
  59. /// Gets or sets the keep-alive interval.
  60. /// </summary>
  61. /// <value>
  62. /// The keep-alive interval. Specify negative one (-1) milliseconds to disable the
  63. /// keep-alive. This is the default value.
  64. /// </value>
  65. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  66. public TimeSpan KeepAliveInterval
  67. {
  68. get
  69. {
  70. CheckDisposed();
  71. return this._keepAliveInterval;
  72. }
  73. set
  74. {
  75. CheckDisposed();
  76. if (value == _keepAliveInterval)
  77. return;
  78. if (value == Infinite)
  79. {
  80. // stop the timer when the value is -1 milliseconds
  81. StopKeepAliveTimer();
  82. }
  83. else
  84. {
  85. // change the due time and interval of the timer if has already
  86. // been created (which means the client is connected)
  87. //
  88. // if the client is not yet connected, then the timer will be
  89. // created with the new interval when Connect() is invoked
  90. if (_keepAliveTimer != null)
  91. _keepAliveTimer.Change(value, value);
  92. }
  93. this._keepAliveInterval = value;
  94. }
  95. }
  96. /// <summary>
  97. /// Occurs when an error occurred.
  98. /// </summary>
  99. /// <example>
  100. /// <code source="..\..\Renci.SshNet.Tests\Classes\SshClientTest.cs" region="Example SshClient Connect ErrorOccurred" language="C#" title="Handle ErrorOccurred event" />
  101. /// </example>
  102. public event EventHandler<ExceptionEventArgs> ErrorOccurred;
  103. /// <summary>
  104. /// Occurs when host key received.
  105. /// </summary>
  106. /// <example>
  107. /// <code source="..\..\Renci.SshNet.Tests\Classes\SshClientTest.cs" region="Example SshClient Connect HostKeyReceived" language="C#" title="Handle HostKeyReceived event" />
  108. /// </example>
  109. public event EventHandler<HostKeyEventArgs> HostKeyReceived;
  110. /// <summary>
  111. /// Initializes a new instance of the <see cref="BaseClient"/> class.
  112. /// </summary>
  113. /// <param name="connectionInfo">The connection info.</param>
  114. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  115. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is null.</exception>
  116. /// <remarks>
  117. /// If <paramref name="ownsConnectionInfo"/> is <c>true</c>, then the
  118. /// connection info will be disposed when this instance is disposed.
  119. /// </remarks>
  120. protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
  121. {
  122. if (connectionInfo == null)
  123. throw new ArgumentNullException("connectionInfo");
  124. ConnectionInfo = connectionInfo;
  125. _ownsConnectionInfo = ownsConnectionInfo;
  126. _keepAliveInterval = Infinite;
  127. }
  128. /// <summary>
  129. /// Connects client to the server.
  130. /// </summary>
  131. /// <exception cref="InvalidOperationException">The client is already connected.</exception>
  132. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  133. /// <exception cref="SocketException">Socket connection to the SSH server or proxy server could not be established, or an error occurred while resolving the hostname.</exception>
  134. /// <exception cref="SshConnectionException">SSH session could not be established.</exception>
  135. /// <exception cref="SshAuthenticationException">Authentication of SSH session failed.</exception>
  136. /// <exception cref="ProxyException">Failed to establish proxy connection.</exception>
  137. public void Connect()
  138. {
  139. CheckDisposed();
  140. // TODO (see issue #1758):
  141. // we're not stopping the keep-alive timer and disposing the session here
  142. //
  143. // we could do this but there would still be side effects as concrete
  144. // implementations may still hang on to the original session
  145. //
  146. // therefore it would be better to actually invoke the Disconnect method
  147. // (and then the Dispose on the session) but even that would have side effects
  148. // eg. it would remove all forwarded ports from SshClient
  149. //
  150. // I think we should modify our concrete clients to better deal with a
  151. // disconnect. In case of SshClient this would mean not removing the
  152. // forwarded ports on disconnect (but only on dispose ?) and link a
  153. // forwarded port with a client instead of with a session
  154. //
  155. // To be discussed with Oleg (or whoever is interested)
  156. if (Session != null && Session.IsConnected)
  157. throw new InvalidOperationException("The client is already connected.");
  158. OnConnecting();
  159. Session = new Session(ConnectionInfo);
  160. Session.HostKeyReceived += Session_HostKeyReceived;
  161. Session.ErrorOccured += Session_ErrorOccured;
  162. Session.Connect();
  163. StartKeepAliveTimer();
  164. OnConnected();
  165. }
  166. /// <summary>
  167. /// Disconnects client from the server.
  168. /// </summary>
  169. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  170. public void Disconnect()
  171. {
  172. CheckDisposed();
  173. OnDisconnecting();
  174. StopKeepAliveTimer();
  175. if (Session != null)
  176. Session.Disconnect();
  177. OnDisconnected();
  178. }
  179. /// <summary>
  180. /// Sends keep-alive message to the server.
  181. /// </summary>
  182. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  183. public void SendKeepAlive()
  184. {
  185. CheckDisposed();
  186. if (Session != null && Session.IsConnected)
  187. Session.SendKeepAlive();
  188. }
  189. /// <summary>
  190. /// Called when client is connecting to the server.
  191. /// </summary>
  192. protected virtual void OnConnecting()
  193. {
  194. }
  195. /// <summary>
  196. /// Called when client is connected to the server.
  197. /// </summary>
  198. protected virtual void OnConnected()
  199. {
  200. }
  201. /// <summary>
  202. /// Called when client is disconnecting from the server.
  203. /// </summary>
  204. protected virtual void OnDisconnecting()
  205. {
  206. if (Session != null)
  207. Session.OnDisconnecting();
  208. }
  209. /// <summary>
  210. /// Called when client is disconnected from the server.
  211. /// </summary>
  212. protected virtual void OnDisconnected()
  213. {
  214. }
  215. private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
  216. {
  217. var handler = this.ErrorOccurred;
  218. if (handler != null)
  219. {
  220. handler(this, e);
  221. }
  222. }
  223. private void Session_HostKeyReceived(object sender, HostKeyEventArgs e)
  224. {
  225. var handler = this.HostKeyReceived;
  226. if (handler != null)
  227. {
  228. handler(this, e);
  229. }
  230. }
  231. #region IDisposable Members
  232. private bool _isDisposed;
  233. /// <summary>
  234. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged ResourceMessages.
  235. /// </summary>
  236. public void Dispose()
  237. {
  238. Dispose(true);
  239. GC.SuppressFinalize(this);
  240. }
  241. /// <summary>
  242. /// Releases unmanaged and - optionally - managed resources
  243. /// </summary>
  244. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged ResourceMessages.</param>
  245. protected virtual void Dispose(bool disposing)
  246. {
  247. // Check to see if Dispose has already been called.
  248. if (!this._isDisposed)
  249. {
  250. // If disposing equals true, dispose all managed
  251. // and unmanaged ResourceMessages.
  252. if (disposing)
  253. {
  254. // stop sending keep-alive messages before we close the
  255. // session
  256. StopKeepAliveTimer();
  257. if (this.Session != null)
  258. {
  259. this.Session.ErrorOccured -= Session_ErrorOccured;
  260. this.Session.HostKeyReceived -= Session_HostKeyReceived;
  261. this.Session.Dispose();
  262. this.Session = null;
  263. }
  264. if (_ownsConnectionInfo && _connectionInfo != null)
  265. {
  266. var connectionInfoDisposable = _connectionInfo as IDisposable;
  267. if (connectionInfoDisposable != null)
  268. connectionInfoDisposable.Dispose();
  269. _connectionInfo = null;
  270. }
  271. }
  272. // Note disposing has been done.
  273. _isDisposed = true;
  274. }
  275. }
  276. /// <summary>
  277. /// Check if the current instance is disposed.
  278. /// </summary>
  279. /// <exception cref="ObjectDisposedException">THe current instance is disposed.</exception>
  280. protected void CheckDisposed()
  281. {
  282. if (_isDisposed)
  283. throw new ObjectDisposedException(GetType().FullName);
  284. }
  285. /// <summary>
  286. /// Releases unmanaged resources and performs other cleanup operations before the
  287. /// <see cref="BaseClient"/> is reclaimed by garbage collection.
  288. /// </summary>
  289. ~BaseClient()
  290. {
  291. // Do not re-create Dispose clean-up code here.
  292. // Calling Dispose(false) is optimal in terms of
  293. // readability and maintainability.
  294. Dispose(false);
  295. }
  296. #endregion
  297. /// <summary>
  298. /// Stops the keep-alive timer, and waits until all timer callbacks have been
  299. /// executed.
  300. /// </summary>
  301. private void StopKeepAliveTimer()
  302. {
  303. if (_keepAliveTimer == null)
  304. return;
  305. var timerDisposed = new ManualResetEvent(false);
  306. _keepAliveTimer.Dispose(timerDisposed);
  307. timerDisposed.WaitOne();
  308. timerDisposed.Dispose();
  309. _keepAliveTimer = null;
  310. }
  311. /// <summary>
  312. /// Starts the keep-alive timer.
  313. /// </summary>
  314. /// <remarks>
  315. /// When <see cref="KeepAliveInterval"/> is negative one (-1) milliseconds, then
  316. /// the timer will not be started.
  317. /// </remarks>
  318. private void StartKeepAliveTimer()
  319. {
  320. if (_keepAliveInterval == Infinite)
  321. return;
  322. if (_keepAliveTimer == null)
  323. _keepAliveTimer = new Timer(state => this.SendKeepAlive());
  324. _keepAliveTimer.Change(_keepAliveInterval, _keepAliveInterval);
  325. }
  326. }
  327. }