diff --git a/Renci.SshClient/Renci.SshClient.Tests/SshClientTests/TestSshCommand.cs b/Renci.SshClient/Renci.SshClient.Tests/SshClientTests/TestSshCommand.cs index 9130d778..e10eba69 100644 --- a/Renci.SshClient/Renci.SshClient.Tests/SshClientTests/TestSshCommand.cs +++ b/Renci.SshClient/Renci.SshClient.Tests/SshClientTests/TestSshCommand.cs @@ -53,6 +53,19 @@ namespace Renci.SshClient.Tests.SshClientTests } } + [TestMethod] + public void Test_Execute_Infinite_Timeout() + { + using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + { + client.Connect(); + var cmd = client.CreateCommand("sleep 10s"); + cmd.Execute(); + client.Disconnect(); + } + } + + [TestMethod] public void Test_MultipleThread_10000_MultipleSessions() { diff --git a/Renci.SshClient/Renci.SshClient/ConnectionInfo.cs b/Renci.SshClient/Renci.SshClient/ConnectionInfo.cs index 9dfae35c..9cec06d3 100644 --- a/Renci.SshClient/Renci.SshClient/ConnectionInfo.cs +++ b/Renci.SshClient/Renci.SshClient/ConnectionInfo.cs @@ -61,7 +61,6 @@ namespace Renci.SshClient /// Gets supported authentication methods for this connection. /// public IDictionary AuthenticationMethods { get; private set; } - // TODO: Restore AuthenticationMethods property functionality and allow connection only using supported method /// /// Gets supported compression algorithms for this connection. @@ -173,16 +172,16 @@ namespace Renci.SshClient {"ssh-dss", typeof(CryptoPublicKeyDss)}, }; - //this.SupportedAuthenticationMethods = new Dictionary() - //{ - // {"none", typeof(UserAuthenticationNone)}, - // {"publickey", typeof(UserAuthenticationPublicKey)}, - // {"password", typeof(UserAuthenticationPassword)}, - // {"keyboard-interactive", typeof(UserAuthenticationKeyboardInteractive)}, - // //{"hostbased", typeof(...)}, - // //{"gssapi-keyex", typeof(...)}, - // //{"gssapi-with-mic", typeof(...)}, - //}; + this.AuthenticationMethods = new Dictionary() + { + {"none", typeof(ConnectionInfo)}, + {"publickey", typeof(PrivateKeyConnectionInfo)}, + {"password", typeof(PasswordConnectionInfo)}, + {"keyboard-interactive", typeof(KeyboardInteractiveConnectionInfo)}, + //{"hostbased", typeof(...)}, + //{"gssapi-keyex", typeof(...)}, + //{"gssapi-with-mic", typeof(...)}, + }; this.CompressionAlgorithms = new Dictionary() { @@ -216,9 +215,9 @@ namespace Renci.SshClient { this.Session = session; - this.Session.RegisterMessage(); - this.Session.RegisterMessage(); - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"); + this.Session.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"); + this.Session.RegisterMessage("SSH_MSG_USERAUTH_BANNER"); this.Session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived; this.Session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessMessageReceived; @@ -232,9 +231,9 @@ namespace Renci.SshClient this.Session.UserAuthenticationBannerReceived -= Session_UserAuthenticationBannerMessageReceived; this.Session.MessageReceived -= Session_MessageReceived; - this.Session.UnRegisterMessage(); - this.Session.UnRegisterMessage(); - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"); + this.Session.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"); + this.Session.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"); return this.IsAuthenticated; } diff --git a/Renci.SshClient/Renci.SshClient/ForwardedPortLocal.cs b/Renci.SshClient/Renci.SshClient/ForwardedPortLocal.cs index 2ec4c8ad..a18caf29 100644 --- a/Renci.SshClient/Renci.SshClient/ForwardedPortLocal.cs +++ b/Renci.SshClient/Renci.SshClient/ForwardedPortLocal.cs @@ -75,7 +75,6 @@ namespace Renci.SshClient /// public override void Stop() { - // TODO: This check should be moved to base class // If port not started you cant stop it if (!this.IsStarted) return; diff --git a/Renci.SshClient/Renci.SshClient/ForwardedPortRemote.cs b/Renci.SshClient/Renci.SshClient/ForwardedPortRemote.cs index 09e7de5f..b2fcc030 100644 --- a/Renci.SshClient/Renci.SshClient/ForwardedPortRemote.cs +++ b/Renci.SshClient/Renci.SshClient/ForwardedPortRemote.cs @@ -26,9 +26,9 @@ namespace Renci.SshClient if (this.IsStarted) return; - this.Session.RegisterMessage(); - this.Session.RegisterMessage(); - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_REQUEST_FAILURE"); + this.Session.RegisterMessage("SSH_MSG_REQUEST_SUCCESS"); + this.Session.RegisterMessage("SSH_MSG_CHANNEL_OPEN"); this.Session.RequestSuccessReceived += Session_RequestSuccess; this.Session.RequestFailureReceived += Session_RequestFailure; diff --git a/Renci.SshClient/Renci.SshClient/KeyboardInteractiveConnectionInfo.cs b/Renci.SshClient/Renci.SshClient/KeyboardInteractiveConnectionInfo.cs index d1422758..dc9747d2 100644 --- a/Renci.SshClient/Renci.SshClient/KeyboardInteractiveConnectionInfo.cs +++ b/Renci.SshClient/Renci.SshClient/KeyboardInteractiveConnectionInfo.cs @@ -62,13 +62,13 @@ namespace Renci.SshClient /// protected override void OnAuthenticate() { - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_USERAUTH_INFO_REQUEST"); this.Session.SendMessage(new RequestMessageKeyboardInteractive(ServiceNames.Connection, this.Username)); this.WaitHandle(this._authenticationCompleted); - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_USERAUTH_INFO_REQUEST"); if (this._exception != null) { diff --git a/Renci.SshClient/Renci.SshClient/Messages/MessageAttribute.cs b/Renci.SshClient/Renci.SshClient/Messages/MessageAttribute.cs index 680469e9..7007723d 100644 --- a/Renci.SshClient/Renci.SshClient/Messages/MessageAttribute.cs +++ b/Renci.SshClient/Renci.SshClient/Messages/MessageAttribute.cs @@ -2,13 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Renci.SshClient.Common; namespace Renci.SshClient.Messages { + /// /// Indicates that a class represents SSH message. This class cannot be inherited. /// - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] public sealed class MessageAttribute : Attribute { /// diff --git a/Renci.SshClient/Renci.SshClient/PasswordConnectionInfo.cs b/Renci.SshClient/Renci.SshClient/PasswordConnectionInfo.cs index 19a50ad3..9fed3088 100644 --- a/Renci.SshClient/Renci.SshClient/PasswordConnectionInfo.cs +++ b/Renci.SshClient/Renci.SshClient/PasswordConnectionInfo.cs @@ -70,7 +70,7 @@ namespace Renci.SshClient /// protected override void OnAuthenticate() { - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_USERAUTH_PASSWD_CHANGEREQ"); this.SendMessage(new RequestMessagePassword(ServiceNames.Connection, this.Username, this.Password)); @@ -115,7 +115,7 @@ namespace Renci.SshClient if (e.Message is PasswordChangeRequiredMessage) { - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_USERAUTH_PASSWD_CHANGEREQ"); var eventTask = Task.Factory.StartNew(() => { diff --git a/Renci.SshClient/Renci.SshClient/PrivateKeyConnectionInfo.cs b/Renci.SshClient/Renci.SshClient/PrivateKeyConnectionInfo.cs index ba1a34a6..c992e736 100644 --- a/Renci.SshClient/Renci.SshClient/PrivateKeyConnectionInfo.cs +++ b/Renci.SshClient/Renci.SshClient/PrivateKeyConnectionInfo.cs @@ -68,7 +68,7 @@ namespace Renci.SshClient if (this.KeyFiles == null) return; - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_USERAUTH_PK_OK"); foreach (var keyFile in this.KeyFiles) { @@ -112,7 +112,7 @@ namespace Renci.SshClient } } - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_USERAUTH_PK_OK"); } /// diff --git a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs index 31700a4d..4f4e5303 100644 --- a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs +++ b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup14Sha1.cs @@ -55,7 +55,7 @@ namespace Renci.SshClient.Security { base.Start(session, message); - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_KEXDH_REPLY"); this.Session.MessageReceived += Session_MessageReceived; @@ -88,7 +88,7 @@ namespace Renci.SshClient.Security if (message != null) { // Unregister message once received - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_KEXDH_REPLY"); this.HandleServerDhReply(message.HostKey, message.F, message.Signature); } diff --git a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs index 70e64cc6..dcdd3bb3 100644 --- a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs +++ b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroup1Sha1.cs @@ -55,7 +55,7 @@ namespace Renci.SshClient.Security { base.Start(session, message); - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_KEXDH_REPLY"); this.Session.MessageReceived += Session_MessageReceived; @@ -89,7 +89,7 @@ namespace Renci.SshClient.Security if (message != null) { // Unregister message once received - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_KEXDH_REPLY"); this.HandleServerDhReply(message.HostKey, message.F, message.Signature); } diff --git a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs index 33dc94cb..498564f7 100644 --- a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs +++ b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha1.cs @@ -61,8 +61,8 @@ namespace Renci.SshClient.Security { base.Start(session, message); - this.Session.RegisterMessage(); - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_KEX_DH_GEX_GROUP"); + this.Session.RegisterMessage("SSH_MSG_KEX_DH_GEX_REPLY"); this.Session.MessageReceived += Session_MessageReceived; @@ -87,7 +87,7 @@ namespace Renci.SshClient.Security if (groupMessage != null) { // Unregister message once received - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_KEX_DH_GEX_GROUP"); // 2. Receive SSH_MSG_KEX_DH_GEX_GROUP this._prime = groupMessage.SafePrime; @@ -104,7 +104,7 @@ namespace Renci.SshClient.Security if (replyMessage != null) { // Unregister message once received - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_KEX_DH_GEX_REPLY"); this.HandleServerDhReply(replyMessage.HostKey, replyMessage.F, replyMessage.Signature); } diff --git a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs index 9d772c9e..7574065b 100644 --- a/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs +++ b/Renci.SshClient/Renci.SshClient/Security/KeyExchangeDiffieHellmanGroupExchangeSha256.cs @@ -33,8 +33,8 @@ namespace Renci.SshClient.Security { base.Start(session, message); - this.Session.RegisterMessage(); - this.Session.RegisterMessage(); + this.Session.RegisterMessage("SSH_MSG_KEX_DH_GEX_GROUP"); + this.Session.RegisterMessage("SSH_MSG_KEX_DH_GEX_REPLY"); this.Session.MessageReceived += Session_MessageReceived; @@ -108,7 +108,7 @@ namespace Renci.SshClient.Security if (groupMessage != null) { // Unregister message once received - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_KEX_DH_GEX_GROUP"); // 2. Receive SSH_MSG_KEX_DH_GEX_GROUP this._prime = groupMessage.SafePrime; @@ -125,7 +125,7 @@ namespace Renci.SshClient.Security if (replyMessage != null) { // Unregister message once received - this.Session.UnRegisterMessage(); + this.Session.UnRegisterMessage("SSH_MSG_KEX_DH_GEX_REPLY"); this.HandleServerDhReply(replyMessage.HostKey, replyMessage.F, replyMessage.Signature); } diff --git a/Renci.SshClient/Renci.SshClient/Session.cs b/Renci.SshClient/Renci.SshClient/Session.cs index acf737c9..501f30e3 100644 --- a/Renci.SshClient/Renci.SshClient/Session.cs +++ b/Renci.SshClient/Renci.SshClient/Session.cs @@ -18,6 +18,7 @@ using Renci.SshClient.Messages.Authentication; using Renci.SshClient.Messages.Connection; using Renci.SshClient.Messages.Transport; using Renci.SshClient.Security; +using System.Diagnostics; namespace Renci.SshClient { @@ -40,6 +41,11 @@ namespace Renci.SshClient private static Regex _serverVersionRe = new Regex("^SSH-(?[^-]+)-(?.+)( SP.+)?$", RegexOptions.Compiled); + /// + /// Holds metada about session messages + /// + private IEnumerable _messagesMetadata; + /// /// Controls how many authentication attempts can take place at the same time. /// @@ -425,6 +431,17 @@ namespace Renci.SshClient connectResult.AsyncWaitHandle.WaitOne(this.ConnectionInfo.Timeout); + // Build list of available messages while connecting + this._messagesMetadata = (from type in this.GetType().Assembly.GetTypes() + from messageAttribute in type.GetCustomAttributes(false).OfType() + select new MessageMetadata + { + Name = messageAttribute.Name, + Number = messageAttribute.Number, + Enabled = false, + Type = type, + }).ToList(); + this._socket.EndConnect(connectResult); this._socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.NoDelay, 1); @@ -475,13 +492,13 @@ namespace Renci.SshClient this.Write(Encoding.ASCII.GetBytes(string.Format("{0}\x0D\x0A", this.ClientVersion))); // Register Transport response messages - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); + this.RegisterMessage("SSH_MSG_DISCONNECT"); + this.RegisterMessage("SSH_MSG_IGNORE"); + this.RegisterMessage("SSH_MSG_UNIMPLEMENTED"); + this.RegisterMessage("SSH_MSG_DEBUG"); + this.RegisterMessage("SSH_MSG_SERVICE_ACCEPT"); + this.RegisterMessage("SSH_MSG_KEXINIT"); + this.RegisterMessage("SSH_MSG_NEWKEYS"); // Start incoming request listener @@ -508,8 +525,8 @@ namespace Renci.SshClient throw new SshException("Username is not specified."); } + // In future, if more then one authentication methods are supported perform the check here. // Authenticate using provided connection info object - this.ConnectionInfo.Authenticate(this); this._isAuthenticated = this.ConnectionInfo.IsAuthenticated; @@ -1018,19 +1035,21 @@ namespace Renci.SshClient this._keyExchangeCompletedWaitHandle.Reset(); // Connection type messages are not allowed during key exchange phase - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); - this.UnRegisterMessage(); + this.UnRegisterMessage("SSH_MSG_GLOBAL_REQUEST"); + this.UnRegisterMessage("SSH_MSG_REQUEST_SUCCESS"); + this.UnRegisterMessage("SSH_MSG_REQUEST_FAILURE"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_REQUEST"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_SUCCESS"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_FAILURE"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_DATA"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_EOF"); + this.UnRegisterMessage("SSH_MSG_CHANNEL_CLOSE"); + // TODO: Replace it with algorithm which disables all messages but relevant for key exchange + var keyExchangeAlgorithmName = (from c in this.ConnectionInfo.KeyExchangeAlgorithms.Keys from s in message.KeyExchangeAlgorithms @@ -1084,19 +1103,19 @@ namespace Renci.SshClient this._serverDecompression = this._keyExchange.Decompressor; // Register Connection messages - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); - this.RegisterMessage(); + this.RegisterMessage("SSH_MSG_GLOBAL_REQUEST"); + this.RegisterMessage("SSH_MSG_REQUEST_SUCCESS"); + this.RegisterMessage("SSH_MSG_REQUEST_FAILURE"); + this.RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION"); + this.RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE"); + this.RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST"); + this.RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA"); + this.RegisterMessage("SSH_MSG_CHANNEL_REQUEST"); + this.RegisterMessage("SSH_MSG_CHANNEL_SUCCESS"); + this.RegisterMessage("SSH_MSG_CHANNEL_FAILURE"); + this.RegisterMessage("SSH_MSG_CHANNEL_DATA"); + this.RegisterMessage("SSH_MSG_CHANNEL_EOF"); + this.RegisterMessage("SSH_MSG_CHANNEL_CLOSE"); if (this.NewKeysReceived != null) { @@ -1417,45 +1436,32 @@ namespace Renci.SshClient #region Message loading functions - private delegate T LoadFunc(IEnumerable data); - - private IDictionary> _registeredMessageTypes = new Dictionary>(); - /// - /// Registers the message type. This will allow message type to be recognized by and handled by the system. + /// Registers SSH Message with the session. /// - /// Some message types are not allowed during cirtain times or same code can be used for different type of message - /// Message type - public void RegisterMessage() where T : Message, new() + /// Name of the message. + public void RegisterMessage(string messageName) { - var messageAttribute = typeof(T).GetCustomAttributes(typeof(MessageAttribute), true).SingleOrDefault() as MessageAttribute; - - if (messageAttribute == null) - throw new SshException(string.Format("Type '{0}' is not a valid message type.", typeof(T).AssemblyQualifiedName)); - - lock (this._registeredMessageTypes) + lock (this._messagesMetadata) { - if (this._registeredMessageTypes.ContainsKey(messageAttribute.Number)) - { - this.UnRegisterMessage(); - } - - this._registeredMessageTypes.Add(messageAttribute.Number, new LoadFunc(Message.Load)); + Parallel.ForEach( + from m in this._messagesMetadata where m.Name == messageName select m, + (item) => { item.Enabled = true; }); } } /// - /// Registers the message type. Message that is not registered will not be allowed to be handled by the system. + /// Removes SSH message from the session /// - /// - public void UnRegisterMessage() + /// Name of the message. + public void UnRegisterMessage(string messageName) { - var messageAttribute = typeof(T).GetCustomAttributes(typeof(MessageAttribute), true).SingleOrDefault() as MessageAttribute; - - if (messageAttribute == null) - throw new SshException(string.Format("Type '{0}' is not a valid message type.", typeof(T).AssemblyQualifiedName)); - - this._registeredMessageTypes.Remove(messageAttribute.Number); + lock (this._messagesMetadata) + { + Parallel.ForEach( + from m in this._messagesMetadata where m.Name == messageName select m, + (item) => { item.Enabled = false; }); + } } /// @@ -1467,23 +1473,22 @@ namespace Renci.SshClient { var messageType = data.FirstOrDefault(); - lock (this._registeredMessageTypes) - { - if (this._registeredMessageTypes.ContainsKey(messageType)) - { - return this._registeredMessageTypes[messageType](data); - } - else - { - throw new NotSupportedException(string.Format("Message type '{0}' is not registered.", messageType)); - } - } + var messageMetadata = (from m in this._messagesMetadata where m.Number == messageType && m.Enabled == true select m).SingleOrDefault(); + + if (messageMetadata == null) + throw new SshException(string.Format("Message type {0} is not valid.", messageType)); + + var message = messageMetadata.Type.CreateInstance(); + + message.Load(data); + + return message; } #endregion /// - /// Listnets for incoming message from the server and handles them. This method run as a task on seperate thread. + /// Listens for incoming message from the server and handles them. This method run as a task on separate thread. /// private void MessageListener() { @@ -1626,5 +1631,17 @@ namespace Renci.SshClient } #endregion + + private class MessageMetadata + { + public string Name { get; set; } + + public byte Number { get; set; } + + public bool Enabled { get; set; } + + public Type Type { get; set; } + } + } }