Add Authenticating event to allow capture of any banner text if exists that need to be presented to the user and to provide other authentication related information

Refactor userauthentication proccess and add support for "keyboard-interactive" authentication method
This commit is contained in:
olegkap_cp
2010-12-21 21:13:25 +00:00
parent 883de1a4d1
commit feafa2ef38
18 changed files with 397 additions and 122 deletions
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Renci.SshClient.Messages.Authentication;
namespace Renci.SshClient.Common
{
public class AuthenticationEventArgs : EventArgs
{
public string BannerMessage { get; private set; }
public string Language { get; private set; }
public string Instruction { get; private set; }
public IEnumerable<AuthenticationPrompt> Prompts { get; private set; }
public AuthenticationEventArgs(string message, string language)
{
this.BannerMessage = message;
this.Language = language;
}
public AuthenticationEventArgs(string instruction, string language, IEnumerable<AuthenticationPrompt> prompts)
{
this.Instruction = instruction;
this.Language = language;
this.Prompts = prompts;
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Renci.SshClient.Common
{
public class AuthenticationPrompt
{
public int Id { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the user input should be echoed as characters are typed.
/// </summary>
/// <value>
/// <c>true</c> if the user input should be echoed as characters are typed; otherwise, <c>false</c>.
/// </value>
public bool IsEchoed { get; private set; }
public string Request { get; private set; }
public string Response { get; set; }
public AuthenticationPrompt(int id, bool isEchoed, string request)
{
this.Id = id;
this.IsEchoed = isEchoed;
this.Request = request;
}
}
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Renci.SshClient.Common
{
[Serializable]
public class SshAuthenticationException : SshException
{
public SshAuthenticationException(string message)
: base(message)
{
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Renci.SshClient.Common;
namespace Renci.SshClient.Messages.Authentication
{
@@ -17,7 +18,7 @@ namespace Renci.SshClient.Messages.Authentication
public string Language { get; set; }
public IEnumerable<PromptEcho> Prompts { get; set; }
public IEnumerable<AuthenticationPrompt> Prompts { get; set; }
protected override void LoadData()
{
@@ -26,16 +27,14 @@ namespace Renci.SshClient.Messages.Authentication
this.Language = this.ReadString();
var numOfPrompts = this.ReadUInt32();
var prompts = new List<PromptEcho>();
var prompts = new List<AuthenticationPrompt>();
for (int i = 0; i < numOfPrompts; i++)
{
prompts.Add(new PromptEcho
{
Prompt = this.ReadString(),
Echo = this.ReadBoolean(),
});
}
{
var prompt = this.ReadString();
var echo = this.ReadBoolean();
prompts.Add(new AuthenticationPrompt(i, echo, prompt));
}
this.Prompts = prompts;
}
@@ -44,12 +43,5 @@ namespace Renci.SshClient.Messages.Authentication
{
throw new NotImplementedException();
}
public class PromptEcho
{
public string Prompt { get; set; }
public bool Echo { get; set; }
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Renci.SshClient.Messages.Authentication
{
@@ -9,6 +10,13 @@ namespace Renci.SshClient.Messages.Authentication
get { return MessageTypes.UserAuthenticationInformationResponse; }
}
public IList<string> Responses { get; private set; }
public InformationResponseMessage()
{
this.Responses = new List<string>();
}
protected override void LoadData()
{
throw new NotImplementedException();
@@ -16,7 +24,11 @@ namespace Renci.SshClient.Messages.Authentication
protected override void SaveData()
{
throw new NotImplementedException();
this.Write((UInt32)this.Responses.Count);
foreach (var response in this.Responses)
{
this.Write(response);
}
}
}
}
@@ -0,0 +1,34 @@
using System.Text;
namespace Renci.SshClient.Messages.Authentication
{
internal class RequestMessageKeyboardInteractive : RequestMessage
{
public override string MethodName
{
get
{
return "keyboard-interactive";
}
}
public string Language { get; set; }
public string SubMethods { get; set; }
public RequestMessageKeyboardInteractive()
{
this.Language = string.Empty;
this.SubMethods = string.Empty;
}
protected override void SaveData()
{
base.SaveData();
this.Write(this.Language);
this.Write(this.SubMethods, Encoding.UTF8);
}
}
}
@@ -2,7 +2,7 @@
namespace Renci.SshClient.Messages.Authentication
{
internal class PasswordRequestMessage : RequestMessage
internal class RequestMessagePassword : RequestMessage
{
public override string MethodName
{
@@ -2,7 +2,7 @@
namespace Renci.SshClient.Messages.Authentication
{
internal class PublicKeyRequestMessage : RequestMessage
internal class RequestMessagePublicKey : RequestMessage
{
public override string MethodName
{
@@ -60,13 +60,17 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Common\AuthenticationEventArgs.cs" />
<Compile Include="Common\AuthenticationPrompt.cs" />
<Compile Include="Common\ChannelDataEventArgs.cs" />
<Compile Include="Common\ChannelEventArgs.cs" />
<Compile Include="Common\ChannelOpenFailedEventArgs.cs" />
<Compile Include="Common\ChannelRequestEventArgs.cs" />
<Compile Include="Common\ConnectingEventArgs.cs" />
<Compile Include="Common\SshAuthenticationException.cs" />
<Compile Include="Common\SshConnectionException.cs" />
<Compile Include="Common\SshOperationTimeoutException.cs" />
<Compile Include="Messages\Authentication\RequestMessageKeyboardInteractive.cs" />
<Compile Include="Messages\Connection\ChannelOpen\ChannelOpenInfo.cs" />
<Compile Include="Messages\Connection\ChannelOpen\DirectTcpipChannelInfo.cs" />
<Compile Include="Messages\Connection\ChannelOpen\ForwardedTcpipChannelInfo.cs" />
@@ -99,6 +103,15 @@
</Compile>
<Compile Include="Security\KeyExchangeDiffieHellmanGroup14Sha1.cs" />
<Compile Include="Security\KeyExchangeDiffieHellmanGroupExchangeSha1.cs" />
<Compile Include="Security\UserAuthenticationKeyboardInteractive.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Security\UserAuthenticationPassword.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Security\UserAuthenticationPublicKey.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="SftpClient.cs" />
<Compile Include="Sftp\CreateDirectoryCommand.cs" />
<Compile Include="Sftp\DownloadFileCommand.cs" />
@@ -187,10 +200,7 @@
<Compile Include="PrivateKeyFile.cs" />
<Compile Include="Messages\Connection\ChannelMessage.cs" />
<Compile Include="Security\UserAuthentication.cs" />
<Compile Include="Security\UserAuthenticationHost.cs" />
<Compile Include="Security\UserAuthenticationNone.cs" />
<Compile Include="Security\UserAuthenticationPassword.cs" />
<Compile Include="Security\UserAuthenticationPublicKey.cs" />
<Compile Include="Channels\Channel.cs" />
<Compile Include="Channels\ChannelTypes.cs" />
<Compile Include="ConnectionInfo.cs" />
@@ -246,9 +256,9 @@
<Compile Include="Messages\Authentication\InformationResponseMessage.cs" />
<Compile Include="Messages\Authentication\Methods.cs" />
<Compile Include="Messages\Authentication\PasswordChangeRequiredMessage.cs" />
<Compile Include="Messages\Authentication\PasswordRequestMessage.cs" />
<Compile Include="Messages\Authentication\RequestMessagePassword.cs" />
<Compile Include="Messages\Authentication\PublicKeyMessage.cs" />
<Compile Include="Messages\Authentication\PublicKeyRequestMessage.cs" />
<Compile Include="Messages\Authentication\RequestMessagePublicKey.cs" />
<Compile Include="Messages\Authentication\RequestMessage.cs">
<SubType>Code</SubType>
</Compile>
@@ -259,6 +269,7 @@
<Compile Include="Common\SshData.cs" />
<Compile Include="Shell.cs" />
<Compile Include="SshClient.cs" />
<Compile Include="UserAuthentication.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
@@ -1,5 +1,7 @@
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
using System;
using Renci.SshClient.Common;
namespace Renci.SshClient.Security
{
@@ -11,32 +13,28 @@ namespace Renci.SshClient.Security
public string ErrorMessage { get; private set; }
public event EventHandler<AuthenticationEventArgs> Authenticating;
protected Session Session { get; private set; }
public void Init(Session session)
{
this.Session = session;
}
protected string Username { get; private set; }
/// <summary>
/// Executes this instance.
/// </summary>
/// <returns>true if method was execute; otherwise false.</returns>
public bool Execute()
public bool Authenticate(string username, Session session)
{
this.Username = username;
this.Session = session;
this.Session.RegisterMessageType<FailureMessage>(MessageTypes.UserAuthenticationFailure);
this.Session.RegisterMessageType<SuccessMessage>(MessageTypes.UserAuthenticationSuccess);
this.Session.RegisterMessageType<BannerMessage>(MessageTypes.UserAuthenticationBanner);
this.Session.UserAuthenticationRequestReceived += Session_UserAuthenticationRequestMessageReceived;
this.Session.UserAuthenticationFailureReceived += Session_UserAuthenticationFailureReceived;
this.Session.UserAuthenticationSuccessReceived += Session_UserAuthenticationSuccessMessageReceived;
this.Session.UserAuthenticationBannerReceived += Session_UserAuthenticationBannerMessageReceived;
this.Session.MessageReceived += Session_MessageReceived;
var result = this.Run();
this.OnAuthenticate();
this.Session.UserAuthenticationRequestReceived -= Session_UserAuthenticationRequestMessageReceived;
this.Session.UserAuthenticationFailureReceived -= Session_UserAuthenticationFailureReceived;
this.Session.UserAuthenticationSuccessReceived -= Session_UserAuthenticationSuccessMessageReceived;
this.Session.UserAuthenticationBannerReceived -= Session_UserAuthenticationBannerMessageReceived;
@@ -46,12 +44,10 @@ namespace Renci.SshClient.Security
this.Session.UnRegisterMessageType(MessageTypes.UserAuthenticationSuccess);
this.Session.UnRegisterMessageType(MessageTypes.UserAuthenticationBanner);
return result;
return this.IsAuthenticated;
}
protected virtual void Session_UserAuthenticationRequestMessageReceived(object sender, MessageEventArgs<RequestMessage> e)
{
}
protected abstract void OnAuthenticate();
protected virtual void Session_UserAuthenticationFailureReceived(object sender, MessageEventArgs<FailureMessage> e)
{
@@ -66,34 +62,20 @@ namespace Renci.SshClient.Security
protected virtual void Session_UserAuthenticationBannerMessageReceived(object sender, MessageEventArgs<BannerMessage> e)
{
RaiseAuthenticating(new AuthenticationEventArgs(e.Message.Message, e.Message.Language));
}
protected void RaiseAuthenticating(AuthenticationEventArgs args)
{
if (this.Authenticating != null)
{
this.Authenticating(this, args);
}
}
protected virtual void Session_MessageReceived(object sender, MessageEventArgs<Message> e)
{
//dynamic message = e.Message;
//this.HandleMessage(message);
}
protected abstract bool Run();
//protected abstract void HandleMessage<T>(T message) where T : Message;
//protected virtual void HandleMessage(SuccessMessage message)
//{
// this.IsAuthenticated = true;
//}
//protected virtual void HandleMessage(FailureMessage message)
//{
// this.ErrorMessage = message.Message;
// this.IsAuthenticated = false;
//}
//private void Session_MessageReceived(object sender, MessageEventArgs<Message> e)
//{
// dynamic message = e.Message;
// this.HandleMessage(message);
//}
}
}
@@ -1,18 +0,0 @@
namespace Renci.SshClient.Security
{
internal class UserAuthenticationHost : UserAuthentication
{
public override string Name
{
get
{
return "hostbased";
}
}
protected override bool Run()
{
throw new System.NotImplementedException();
}
}
}
@@ -0,0 +1,131 @@
using System;
using System.Linq;
using System.Threading;
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
using Renci.SshClient.Common;
using System.Threading.Tasks;
namespace Renci.SshClient.Security
{
internal class UserAuthenticationKeyboardInteractive : UserAuthentication, IDisposable
{
private EventWaitHandle _authenticationCompleted = new AutoResetEvent(false);
private Exception _exception;
public override string Name
{
get
{
return "keyboard-interactive";
}
}
protected override void OnAuthenticate()
{
this.Session.RegisterMessageType<InformationRequestMessage>(MessageTypes.UserAuthenticationInformationRequest);
this.Session.SendMessage(new RequestMessageKeyboardInteractive
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
});
this.Session.WaitHandle(this._authenticationCompleted);
this.Session.UnRegisterMessageType(MessageTypes.UserAuthenticationInformationRequest);
if (this._exception != null)
{
throw this._exception;
}
}
protected override void Session_UserAuthenticationSuccessMessageReceived(object sender, MessageEventArgs<SuccessMessage> e)
{
base.Session_UserAuthenticationSuccessMessageReceived(sender, e);
this._authenticationCompleted.Set();
}
protected override void Session_UserAuthenticationFailureReceived(object sender, MessageEventArgs<FailureMessage> e)
{
base.Session_UserAuthenticationFailureReceived(sender, e);
this._authenticationCompleted.Set();
}
protected override void Session_MessageReceived(object sender, MessageEventArgs<Message> e)
{
var informationRequestMessage = e.Message as InformationRequestMessage;
if (informationRequestMessage != null)
{
var eventArgs = new AuthenticationEventArgs(informationRequestMessage.Instruction, informationRequestMessage.Language, informationRequestMessage.Prompts);
var eventTask = Task.Factory.StartNew(() =>
{
try
{
this.RaiseAuthenticating(eventArgs);
var informationResponse = new InformationResponseMessage();
foreach (var response in from r in eventArgs.Prompts orderby r.Id ascending select r.Response)
{
informationResponse.Responses.Add(response);
}
// Send information response message
this.Session.SendMessage(informationResponse);
}
catch (Exception exp)
{
this._exception = exp;
this._authenticationCompleted.Set();
}
});
}
}
#region IDisposable Members
private bool isDisposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if (!this.isDisposed)
{
// If disposing equals true, dispose all managed
// and unmanaged resources.
if (disposing)
{
// Dispose managed resources.
if (this._authenticationCompleted != null)
{
this._authenticationCompleted.Dispose();
}
}
// Note disposing has been done.
isDisposed = true;
}
}
~UserAuthenticationKeyboardInteractive()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of
// readability and maintainability.
Dispose(false);
}
#endregion
}
}
@@ -2,6 +2,7 @@
using System.Threading;
using Renci.SshClient.Messages;
using Renci.SshClient.Messages.Authentication;
using System.Collections.Generic;
namespace Renci.SshClient.Security
{
@@ -14,17 +15,17 @@ namespace Renci.SshClient.Security
get { return "none"; }
}
protected override bool Run()
public IEnumerable<string> Methods { get; private set; }
protected override void OnAuthenticate()
{
this.Session.SendMessage(new RequestMessage
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
Username = this.Username,
});
this.Session.WaitHandle(this._authenticationCompleted);
return true;
}
protected override void Session_UserAuthenticationSuccessMessageReceived(object sender, MessageEventArgs<SuccessMessage> e)
@@ -36,6 +37,7 @@ namespace Renci.SshClient.Security
protected override void Session_UserAuthenticationFailureReceived(object sender, MessageEventArgs<FailureMessage> e)
{
base.Session_UserAuthenticationFailureReceived(sender, e);
this.Methods = e.Message.AllowedAuthentications;
this._authenticationCompleted.Set();
}
@@ -17,24 +17,19 @@ namespace Renci.SshClient.Security
}
}
protected override bool Run()
protected override void OnAuthenticate()
{
// TODO: Handle all user authentication messages
//Message.RegisterMessageType<PasswordChangeRequiredMessage>(MessageTypes.UserAuthenticationPasswordChangeRequired);
if (string.IsNullOrEmpty(this.Session.ConnectionInfo.Password))
return false;
this.Session.SendMessage(new PasswordRequestMessage
this.Session.SendMessage(new RequestMessagePassword
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
Password = this.Session.ConnectionInfo.Password,
Username = this.Username,
Password = this.Session.ConnectionInfo.Password ?? string.Empty,
});
this.Session.WaitHandle(this._authenticationCompleted);
return true;
}
protected override void Session_UserAuthenticationSuccessMessageReceived(object sender, MessageEventArgs<SuccessMessage> e)
@@ -20,10 +20,10 @@ namespace Renci.SshClient.Security
}
}
protected override bool Run()
protected override void OnAuthenticate()
{
if (this.Session.ConnectionInfo.KeyFiles == null)
return false;
return;
this.Session.RegisterMessageType<PublicKeyMessage>(MessageTypes.UserAuthenticationPublicKey);
@@ -32,7 +32,7 @@ namespace Renci.SshClient.Security
this._publicKeyRequestMessageResponseWaitHandle.Reset();
this._isSignatureRequired = false;
var message = new PublicKeyRequestMessage
var message = new RequestMessagePublicKey
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
@@ -57,7 +57,7 @@ namespace Renci.SshClient.Security
{
this._publicKeyRequestMessageResponseWaitHandle.Reset();
var signatureMessage = new PublicKeyRequestMessage
var signatureMessage = new RequestMessagePublicKey
{
ServiceName = ServiceNames.Connection,
Username = this.Session.ConnectionInfo.Username,
@@ -82,13 +82,12 @@ namespace Renci.SshClient.Security
}
this.Session.UnRegisterMessageType(MessageTypes.UserAuthenticationPublicKey);
return true;
}
protected override void Session_UserAuthenticationSuccessMessageReceived(object sender, MessageEventArgs<SuccessMessage> e)
{
base.Session_UserAuthenticationSuccessMessageReceived(sender, e);
this._publicKeyRequestMessageResponseWaitHandle.Set();
}
@@ -112,12 +111,11 @@ namespace Renci.SshClient.Security
private class SignatureData : SshData
{
private PublicKeyRequestMessage _message;
private RequestMessagePublicKey _message;
private string _sessionId;
public SignatureData(PublicKeyRequestMessage message, string sessionId)
public SignatureData(RequestMessagePublicKey message, string sessionId)
{
this._message = message;
this._sessionId = sessionId;
+43 -18
View File
@@ -228,6 +228,11 @@ namespace Renci.SshClient
public event EventHandler<EventArgs> Disconnected;
/// <summary>
/// Occurs when user is being authenticated and additional information available or required
/// </summary>
public event EventHandler<AuthenticationEventArgs> Authenticating;
#region Message events
/// <summary>
@@ -444,7 +449,10 @@ namespace Renci.SshClient
{"none", typeof(UserAuthenticationNone).AssemblyQualifiedName},
{"publickey", typeof(UserAuthenticationPublicKey).AssemblyQualifiedName},
{"password", typeof(UserAuthenticationPassword).AssemblyQualifiedName},
{"keyboard-interactive", typeof(UserAuthenticationKeyboardInteractive).AssemblyQualifiedName},
//{"hostbased", typeof(...).AssemblyQualifiedName},
//{"gssapi-keyex", typeof(...).AssemblyQualifiedName},
//{"gssapi-with-mic", typeof(...).AssemblyQualifiedName},
};
this.CompressionAlgorithms = new Dictionary<string, string>()
@@ -531,7 +539,7 @@ namespace Renci.SshClient
{
throw new InvalidOperationException("Server string is null or empty.");
}
versionMatch = _serverVersionRe.Match(this.ServerVersion);
if (versionMatch.Success)
@@ -595,33 +603,50 @@ namespace Renci.SshClient
{
throw new SshException("Username is not specified.");
}
// This implementation will ignore supported by server methods and will try to authenticated user using method supported by the client.
string errorMessage = null; // Hold last authentication error if any
foreach (var methodName in this.SupportedAuthenticationMethods.Keys)
// Query server supported authentication methods
var username = this.ConnectionInfo.Username;
IEnumerable<string> serverMethods = null;
using (var noneAuthentication = new UserAuthenticationNone())
{
var userAuthentication = this.SupportedAuthenticationMethods[methodName].CreateInstance<UserAuthentication>();
userAuthentication.Init(this);
if (userAuthentication.Execute())
if (noneAuthentication.Authenticate(username, this))
{
if (userAuthentication.IsAuthenticated)
throw new SshAuthenticationException("'none' authentication should not be allowed.");
}
serverMethods = noneAuthentication.Methods;
}
var methodNames = from serverMethod in serverMethods
from clientMethod in this.SupportedAuthenticationMethods.Keys
where
serverMethod == clientMethod
select serverMethod;
foreach (var methodName in methodNames)
{
var authentication = this.SupportedAuthenticationMethods[methodName].CreateInstance<UserAuthentication>();
authentication.Authenticating += delegate(object sender, AuthenticationEventArgs e)
{
if (this.Authenticating != null)
{
this._isAuthenticated = true;
break;
}
else
{
errorMessage = userAuthentication.ErrorMessage;
this.Authenticating(sender, e);
}
};
authentication.Authenticate(username, this);
if (authentication.IsAuthenticated)
{
this._isAuthenticated = true;
break;
}
}
if (!this._isAuthenticated)
{
throw new AuthenticationException(errorMessage ?? "User cannot be authenticated.");
throw new SshAuthenticationException("User cannot be authenticated.");
}
Monitor.Pulse(this);
@@ -43,6 +43,8 @@ namespace Renci.SshClient
/// </summary>
public event EventHandler<ConnectingEventArgs> Connecting;
public event EventHandler<AuthenticationEventArgs> Authenticating;
/// <summary>
/// Initializes a new instance of the <see cref="SshBaseClient"/> class.
/// </summary>
@@ -67,6 +69,7 @@ namespace Renci.SshClient
this.Session = new Session(this.ConnectionInfo);
this.Session.Connecting += Session_Connecting;
this.Session.Authenticating += Session_Authenticating;
this.Session.Connect();
this.OnConnected();
@@ -81,6 +84,7 @@ namespace Renci.SshClient
this.Session.Disconnect();
this.Session.Connecting -= Session_Connecting;
this.Session.Authenticating -= Session_Authenticating;
this.OnDisconnected();
}
@@ -133,6 +137,14 @@ namespace Renci.SshClient
}
}
private void Session_Authenticating(object sender, AuthenticationEventArgs e)
{
if (this.Authenticating != null)
{
this.Authenticating(this, e);
}
}
#region IDisposable Members
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Renci.SshClient
{
public class UserAuthentication2
{
private Session _session;
public string Username { get; set; }
internal UserAuthentication2(Session session)
{
this._session = session;
this.Username = session.ConnectionInfo.Username;
}
}
}