Skip to content

Commit

Permalink
added MaxAuthenticationAttempts and support for IPv6 literals
Browse files Browse the repository at this point in the history
  • Loading branch information
cosullivan committed Apr 1, 2020
1 parent 7364f68 commit a6da7dd
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 73 deletions.
43 changes: 21 additions & 22 deletions Src/SampleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,40 @@ namespace SampleApp
{
class Program
{
static Task Main(string[] args)
static async Task Main(string[] args)
{
CustomEndpointListenerExample.Run();
//CustomEndpointListenerExample.Run();

//ServicePointManager.ServerCertificateValidationCallback = SmtpServerTests.IgnoreCertificateValidationFailureForTestingOnly;
ServicePointManager.ServerCertificateValidationCallback = SmtpServerTests.IgnoreCertificateValidationFailureForTestingOnly;

//var options = new SmtpServerOptionsBuilder()
// .ServerName("SmtpServer SampleApp")
// .Port(587, false)
// .Certificate(SmtpServerTests.CreateCertificate())
// //.Certificate(SmtpServerTests.CreateCertificate())
// .Build();

////var options = new SmtpServerOptionsBuilder()
//// .ServerName("SmtpServer SampleApp")
//// .Endpoint(endpoint =>
//// endpoint
//// .Port(587, true)
//// .AllowUnsecureAuthentication(false)
//// .AuthenticationRequired(false))
//// .Certificate(SmtpServerTests.CreateCertificate())
//// .Build();
var options = new SmtpServerOptionsBuilder()
.ServerName("SmtpServer SampleApp")
.Endpoint(endpoint =>
endpoint
.Port(587)
.AllowUnsecureAuthentication(true)
.AuthenticationRequired(false))
.UserAuthenticator(new SampleUserAuthenticator())
//.Certificate(SmtpServerTests.CreateCertificate())
.Build();

//var server = new SmtpServer.SmtpServer(options);
var server = new SmtpServer.SmtpServer(options);

//server.SessionCreated += OnSessionCreated;
//server.SessionCompleted += OnSessionCompleted;
//server.SessionFaulted += OnSessionFaulted;
server.SessionCreated += OnSessionCreated;
server.SessionCompleted += OnSessionCompleted;
server.SessionFaulted += OnSessionFaulted;

//var serverTask = server.StartAsync(CancellationToken.None);
var serverTask = server.StartAsync(CancellationToken.None);

//Console.ReadKey();
Console.ReadKey();

//await serverTask.ConfigureAwait(false);

return Task.CompletedTask;
await serverTask.ConfigureAwait(false);
}

static void OnSessionFaulted(object sender, SessionFaultedEventArgs e)
Expand Down
5 changes: 5 additions & 0 deletions Src/SmtpServer/ISmtpServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public interface ISmtpServerOptions
/// </summary>
int MaxRetryCount { get; }

/// <summary>
/// The maximum number of authentication attempts.
/// </summary>
int MaxAuthenticationAttempts { get; }

/// <summary>
/// Gets the SMTP server name.
/// </summary>
Expand Down
11 changes: 10 additions & 1 deletion Src/SmtpServer/Protocol/AuthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,16 @@ internal override async Task<bool> ExecuteAsync(SmtpSessionContext context, Canc
{
if (await container.Instance.AuthenticateAsync(context, _user, _password, cancellationToken).ConfigureAwait(false) == false)
{
await context.NetworkClient.ReplyAsync(SmtpResponse.AuthenticationFailed, cancellationToken).ConfigureAwait(false);
var remaining = context.ServerOptions.MaxAuthenticationAttempts - ++context.AuthenticationAttempts;
var response = new SmtpResponse(SmtpReplyCode.AuthenticationFailed, $"authentication failed, {remaining} attempt(s) remaining.");

await context.NetworkClient.ReplyAsync(response, cancellationToken).ConfigureAwait(false);

if (remaining <= 0)
{
throw new SmtpResponseException(SmtpResponse.ServiceClosingTransmissionChannel, true);
}

return false;
}
}
Expand Down
68 changes: 46 additions & 22 deletions Src/SmtpServer/Protocol/SmtpParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -657,56 +657,77 @@ public bool TryMakeSnum(out int snum)
/// Try to make Ip version from ip version tag which is a formatted text IPv[Version]:
/// </summary>
/// <param name="version">IP version. IPv6 is supported atm.</param>
/// <returns>true if ip version tag can be extracted</returns>
/// <returns>true if ip version tag can be extracted.</returns>
public bool TryMakeIpVersion(out int version)
{
version = default;
var tag = Enumerator.Peek();
if (tag != Tokens.Text.IpVersionTag)

if (Enumerator.Take() != Tokens.Text.IpVersionTag)
{
return false;
Enumerator.Take();
var versionToken = Enumerator.Take();
if (versionToken.Kind == TokenKind.Number && int.TryParse(versionToken.Text, out var v))
}

var token = Enumerator.Take();

if (token.Kind == TokenKind.Number && int.TryParse(token.Text, out var v))
{
version = v;
return Enumerator.Take() == Tokens.Colon;
}

return false;
}

/// <summary>
/// Try to make 16 bits hex number
/// Try to make 16 bits hex number.
/// </summary>
/// <param name="hexNumber">Extracted hex number</param>
/// <returns>true if valid hex number can be extracted</returns>
/// <param name="hexNumber">Extracted hex number.</param>
/// <returns>true if valid hex number can be extracted.</returns>
public bool TryMake16BitsHexNumber(out string hexNumber)
{
hexNumber = null;

var token = Enumerator.Peek();
while (token.Kind == TokenKind.Number || token.Kind == TokenKind.Text)
{
if (hexNumber != null && (hexNumber.Length + token.Text.Length) > 4)
{
return false;
// Validate hex chars
if (token.Kind == TokenKind.Text && !token.Text.ToUpperInvariant().All(c => c >= 'A' && c <= 'F'))
}

if (token.Kind == TokenKind.Text && IsHex(token.Text) == false)
{
return false;
}

hexNumber = string.Concat(hexNumber ?? string.Empty, token.Text);

Enumerator.Take();
token = Enumerator.Peek();
}

return true;

bool IsHex(string text)
{
return text.ToUpperInvariant().All(c => c >= 'A' && c <= 'F');
}
}

/// <summary>
/// Try to extract IPv6 address. https://tools.ietf.org/html/rfc4291 section 2.2 used for specification.
/// </summary>
/// <param name="address">Extracted Ipv6 address</param>
/// <returns>true if a valid Ipv6 address can be extracted</returns>
/// <param name="address">Extracted Ipv6 address.</param>
/// <returns>true if a valid Ipv6 address can be extracted.</returns>
public bool TryMakeIpv6AddressLiteral(out string address)
{
address = null;
if ((TryMake(TryMakeIpVersion, out int ipVersion) == false) || ipVersion != 6)

if (TryMake(TryMakeIpVersion, out int ipVersion) == false || ipVersion != 6)
{
return false;
}

var hasDoubleColumn = false;
var hexPartCount = 0;
var hasIpv4Part = false;
Expand All @@ -729,15 +750,11 @@ public bool TryMakeIpv6AddressLiteral(out string address)
builder.Append(ipv4);
break;
}
else
{
return false;
}
}
else
{
cp.Rollback();

return false;
}

cp.Rollback();
}

if (token == Tokens.Colon)
Expand All @@ -746,7 +763,9 @@ public bool TryMakeIpv6AddressLiteral(out string address)
{
// Double column is allowed only once
if (hasDoubleColumn)
{
return false;
}
hasDoubleColumn = true;
}
builder.Append(token.Text);
Expand All @@ -756,7 +775,10 @@ public bool TryMakeIpv6AddressLiteral(out string address)
else
{
if (wasColon == false && builder.Length > 0)
{
return false;
}

wasColon = false;
if (TryMake(TryMake16BitsHexNumber, out string hexNumber))
{
Expand All @@ -775,7 +797,9 @@ public bool TryMakeIpv6AddressLiteral(out string address)

var maxAllowedParts = (hasIpv4Part ? 6 : 8) - Math.Sign(hasDoubleColumn ? 1 : 0);
if ((hasDoubleColumn && hexPartCount > maxAllowedParts) || (!hasDoubleColumn && hexPartCount != maxAllowedParts))
{
return false;
}

return true;
}
Expand Down
8 changes: 4 additions & 4 deletions Src/SmtpServer/SmtpServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<LangVersion>7.2</LangVersion>
<AssemblyName>SmtpServer</AssemblyName>
<RootNamespace>SmtpServer</RootNamespace>
<Version>6.4.1-RC1</Version>
<Version>6.5.0</Version>
<Description>.NET SmtpServer</Description>
<Authors>Cain O'Sullivan</Authors>
<Company />
Expand All @@ -15,9 +15,9 @@
<PackageTags>smtp smtpserver smtp server</PackageTags>
<PackageLicenseUrl></PackageLicenseUrl>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<AssemblyVersion>6.4.1.0</AssemblyVersion>
<FileVersion>6.4.1.0</FileVersion>
<PackageReleaseNotes>Added the SessionFaulted event to the SmtpServer.</PackageReleaseNotes>
<AssemblyVersion>6.5.0.0</AssemblyVersion>
<FileVersion>6.5.0.0</FileVersion>
<PackageReleaseNotes>Added MaxAuthenticationAttempts and support for IPv6 addresses.</PackageReleaseNotes>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
Expand Down
18 changes: 18 additions & 0 deletions Src/SmtpServer/SmtpServerOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public ISmtpServerOptions Build()
MailboxFilterFactory = DoNothingMailboxFilter.Instance,
UserAuthenticatorFactory = DoNothingUserAuthenticator.Instance,
MaxRetryCount = 5,
MaxAuthenticationAttempts = 3,
SupportedSslProtocols = SslProtocols.Tls12,
NetworkBufferSize = 128,
NetworkBufferReadTimeout = TimeSpan.FromMinutes(2),
Expand Down Expand Up @@ -187,6 +188,18 @@ public SmtpServerOptionsBuilder MaxRetryCount(int value)
return this;
}

/// <summary>
/// Sets the maximum number of authentication attempts.
/// </summary>
/// <param name="value">The maximum number of authentication attempts for a failed authentication.</param>
/// <returns>A OptionsBuilder to continue building on.</returns>
public SmtpServerOptionsBuilder MaxAuthenticationAttempts(int value)
{
_setters.Add(options => options.MaxAuthenticationAttempts = value);

return this;
}

/// <summary>
/// Sets the supported SSL protocols.
/// </summary>
Expand Down Expand Up @@ -261,6 +274,11 @@ class SmtpServerOptions : ISmtpServerOptions
/// </summary>
public int MaxRetryCount { get; set; }

/// <summary>
/// The maximum number of authentication attempts.
/// </summary>
public int MaxAuthenticationAttempts { get; set; }

/// <summary>
/// Gets or sets the SMTP server name.
/// </summary>
Expand Down
46 changes: 25 additions & 21 deletions Src/SmtpServer/SmtpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,33 +84,37 @@ async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellati
return;
}

if (TryMake(context, text, out var command, out var response))
if (TryMake(context, text, out var command, out var response) == false)
{
try
await context.NetworkClient.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken).ConfigureAwait(false);
continue;
}

try
{
if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false))
{
if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false))
{
_stateMachine.Transition(context);
}

retries = _context.ServerOptions.MaxRetryCount;

continue;
_stateMachine.Transition(context);
}
catch (SmtpResponseException responseException)
{
context.IsQuitRequested = responseException.IsQuitRequested;

response = responseException.Response;
}
catch (OperationCanceledException)
{
await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken).ConfigureAwait(false);
return;
}
retries = _context.ServerOptions.MaxRetryCount;
}
catch (SmtpResponseException responseException) when (responseException.IsQuitRequested)
{
await context.NetworkClient.ReplyAsync(responseException.Response, cancellationToken).ConfigureAwait(false);
return;
}
catch (SmtpResponseException responseException)
{
response = CreateErrorResponse(responseException.Response, retries);

await context.NetworkClient.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken).ConfigureAwait(false);
await context.NetworkClient.ReplyAsync(response, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken).ConfigureAwait(false);
return;
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions Src/SmtpServer/SmtpSessionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ internal void RaiseSessionAuthenticated()
/// </summary>
public AuthenticationContext Authentication { get; internal set; } = AuthenticationContext.Unauthenticated;

/// <summary>
/// Returns the number of athentication attempts.
/// </summary>
public int AuthenticationAttempts { get; internal set; }

/// <summary>
/// Gets a value indicating whether a quit has been requested.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Src/SmtpServer/Text/ITokenEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public interface ITokenEnumerator
Token Peek();

/// <summary>
/// Take the given number of tokens.
/// Take the next token.
/// </summary>
/// <returns>The last token that was consumed.</returns>
Token Take();
Expand Down
4 changes: 2 additions & 2 deletions Src/SmtpServer/Text/TokenEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public Token Peek()
}

/// <summary>
/// Take the given number of tokens.
/// Take the next token.
/// </summary>
/// <returns>The last token that was consumed.</returns>
public Token Take()
Expand All @@ -40,7 +40,7 @@ public Token Take()
}

/// <summary>
/// Take the given number of tokens.
/// Returns the token at the given index.
/// </summary>
/// <returns>The last token that was consumed.</returns>
Token At(int index)
Expand Down

0 comments on commit a6da7dd

Please sign in to comment.