Skip to content

Commit a6da7dd

Browse files
author
cosullivan
committed
added MaxAuthenticationAttempts and support for IPv6 literals
1 parent 7364f68 commit a6da7dd

File tree

10 files changed

+137
-73
lines changed

10 files changed

+137
-73
lines changed

Src/SampleApp/Program.cs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,40 @@ namespace SampleApp
1414
{
1515
class Program
1616
{
17-
static Task Main(string[] args)
17+
static async Task Main(string[] args)
1818
{
19-
CustomEndpointListenerExample.Run();
19+
//CustomEndpointListenerExample.Run();
2020

21-
//ServicePointManager.ServerCertificateValidationCallback = SmtpServerTests.IgnoreCertificateValidationFailureForTestingOnly;
21+
ServicePointManager.ServerCertificateValidationCallback = SmtpServerTests.IgnoreCertificateValidationFailureForTestingOnly;
2222

2323
//var options = new SmtpServerOptionsBuilder()
2424
// .ServerName("SmtpServer SampleApp")
2525
// .Port(587, false)
26-
// .Certificate(SmtpServerTests.CreateCertificate())
26+
// //.Certificate(SmtpServerTests.CreateCertificate())
2727
// .Build();
2828

29-
////var options = new SmtpServerOptionsBuilder()
30-
//// .ServerName("SmtpServer SampleApp")
31-
//// .Endpoint(endpoint =>
32-
//// endpoint
33-
//// .Port(587, true)
34-
//// .AllowUnsecureAuthentication(false)
35-
//// .AuthenticationRequired(false))
36-
//// .Certificate(SmtpServerTests.CreateCertificate())
37-
//// .Build();
29+
var options = new SmtpServerOptionsBuilder()
30+
.ServerName("SmtpServer SampleApp")
31+
.Endpoint(endpoint =>
32+
endpoint
33+
.Port(587)
34+
.AllowUnsecureAuthentication(true)
35+
.AuthenticationRequired(false))
36+
.UserAuthenticator(new SampleUserAuthenticator())
37+
//.Certificate(SmtpServerTests.CreateCertificate())
38+
.Build();
3839

39-
//var server = new SmtpServer.SmtpServer(options);
40+
var server = new SmtpServer.SmtpServer(options);
4041

41-
//server.SessionCreated += OnSessionCreated;
42-
//server.SessionCompleted += OnSessionCompleted;
43-
//server.SessionFaulted += OnSessionFaulted;
42+
server.SessionCreated += OnSessionCreated;
43+
server.SessionCompleted += OnSessionCompleted;
44+
server.SessionFaulted += OnSessionFaulted;
4445

45-
//var serverTask = server.StartAsync(CancellationToken.None);
46+
var serverTask = server.StartAsync(CancellationToken.None);
4647

47-
//Console.ReadKey();
48+
Console.ReadKey();
4849

49-
//await serverTask.ConfigureAwait(false);
50-
51-
return Task.CompletedTask;
50+
await serverTask.ConfigureAwait(false);
5251
}
5352

5453
static void OnSessionFaulted(object sender, SessionFaultedEventArgs e)

Src/SmtpServer/ISmtpServerOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public interface ISmtpServerOptions
2020
/// </summary>
2121
int MaxRetryCount { get; }
2222

23+
/// <summary>
24+
/// The maximum number of authentication attempts.
25+
/// </summary>
26+
int MaxAuthenticationAttempts { get; }
27+
2328
/// <summary>
2429
/// Gets the SMTP server name.
2530
/// </summary>

Src/SmtpServer/Protocol/AuthCommand.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,16 @@ internal override async Task<bool> ExecuteAsync(SmtpSessionContext context, Canc
6262
{
6363
if (await container.Instance.AuthenticateAsync(context, _user, _password, cancellationToken).ConfigureAwait(false) == false)
6464
{
65-
await context.NetworkClient.ReplyAsync(SmtpResponse.AuthenticationFailed, cancellationToken).ConfigureAwait(false);
65+
var remaining = context.ServerOptions.MaxAuthenticationAttempts - ++context.AuthenticationAttempts;
66+
var response = new SmtpResponse(SmtpReplyCode.AuthenticationFailed, $"authentication failed, {remaining} attempt(s) remaining.");
67+
68+
await context.NetworkClient.ReplyAsync(response, cancellationToken).ConfigureAwait(false);
69+
70+
if (remaining <= 0)
71+
{
72+
throw new SmtpResponseException(SmtpResponse.ServiceClosingTransmissionChannel, true);
73+
}
74+
6675
return false;
6776
}
6877
}

Src/SmtpServer/Protocol/SmtpParser.cs

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -657,56 +657,77 @@ public bool TryMakeSnum(out int snum)
657657
/// Try to make Ip version from ip version tag which is a formatted text IPv[Version]:
658658
/// </summary>
659659
/// <param name="version">IP version. IPv6 is supported atm.</param>
660-
/// <returns>true if ip version tag can be extracted</returns>
660+
/// <returns>true if ip version tag can be extracted.</returns>
661661
public bool TryMakeIpVersion(out int version)
662662
{
663663
version = default;
664-
var tag = Enumerator.Peek();
665-
if (tag != Tokens.Text.IpVersionTag)
664+
665+
if (Enumerator.Take() != Tokens.Text.IpVersionTag)
666+
{
666667
return false;
667-
Enumerator.Take();
668-
var versionToken = Enumerator.Take();
669-
if (versionToken.Kind == TokenKind.Number && int.TryParse(versionToken.Text, out var v))
668+
}
669+
670+
var token = Enumerator.Take();
671+
672+
if (token.Kind == TokenKind.Number && int.TryParse(token.Text, out var v))
670673
{
671674
version = v;
672675
return Enumerator.Take() == Tokens.Colon;
673676
}
677+
674678
return false;
675679
}
676680

677681
/// <summary>
678-
/// Try to make 16 bits hex number
682+
/// Try to make 16 bits hex number.
679683
/// </summary>
680-
/// <param name="hexNumber">Extracted hex number</param>
681-
/// <returns>true if valid hex number can be extracted</returns>
684+
/// <param name="hexNumber">Extracted hex number.</param>
685+
/// <returns>true if valid hex number can be extracted.</returns>
682686
public bool TryMake16BitsHexNumber(out string hexNumber)
683687
{
684688
hexNumber = null;
689+
685690
var token = Enumerator.Peek();
686691
while (token.Kind == TokenKind.Number || token.Kind == TokenKind.Text)
687692
{
688693
if (hexNumber != null && (hexNumber.Length + token.Text.Length) > 4)
694+
{
689695
return false;
690-
// Validate hex chars
691-
if (token.Kind == TokenKind.Text && !token.Text.ToUpperInvariant().All(c => c >= 'A' && c <= 'F'))
696+
}
697+
698+
if (token.Kind == TokenKind.Text && IsHex(token.Text) == false)
699+
{
692700
return false;
701+
}
702+
693703
hexNumber = string.Concat(hexNumber ?? string.Empty, token.Text);
704+
694705
Enumerator.Take();
695706
token = Enumerator.Peek();
696707
}
708+
697709
return true;
710+
711+
bool IsHex(string text)
712+
{
713+
return text.ToUpperInvariant().All(c => c >= 'A' && c <= 'F');
714+
}
698715
}
699716

700717
/// <summary>
701718
/// Try to extract IPv6 address. https://tools.ietf.org/html/rfc4291 section 2.2 used for specification.
702719
/// </summary>
703-
/// <param name="address">Extracted Ipv6 address</param>
704-
/// <returns>true if a valid Ipv6 address can be extracted</returns>
720+
/// <param name="address">Extracted Ipv6 address.</param>
721+
/// <returns>true if a valid Ipv6 address can be extracted.</returns>
705722
public bool TryMakeIpv6AddressLiteral(out string address)
706723
{
707724
address = null;
708-
if ((TryMake(TryMakeIpVersion, out int ipVersion) == false) || ipVersion != 6)
725+
726+
if (TryMake(TryMakeIpVersion, out int ipVersion) == false || ipVersion != 6)
727+
{
709728
return false;
729+
}
730+
710731
var hasDoubleColumn = false;
711732
var hexPartCount = 0;
712733
var hasIpv4Part = false;
@@ -729,15 +750,11 @@ public bool TryMakeIpv6AddressLiteral(out string address)
729750
builder.Append(ipv4);
730751
break;
731752
}
732-
else
733-
{
734-
return false;
735-
}
736-
}
737-
else
738-
{
739-
cp.Rollback();
753+
754+
return false;
740755
}
756+
757+
cp.Rollback();
741758
}
742759

743760
if (token == Tokens.Colon)
@@ -746,7 +763,9 @@ public bool TryMakeIpv6AddressLiteral(out string address)
746763
{
747764
// Double column is allowed only once
748765
if (hasDoubleColumn)
766+
{
749767
return false;
768+
}
750769
hasDoubleColumn = true;
751770
}
752771
builder.Append(token.Text);
@@ -756,7 +775,10 @@ public bool TryMakeIpv6AddressLiteral(out string address)
756775
else
757776
{
758777
if (wasColon == false && builder.Length > 0)
778+
{
759779
return false;
780+
}
781+
760782
wasColon = false;
761783
if (TryMake(TryMake16BitsHexNumber, out string hexNumber))
762784
{
@@ -775,7 +797,9 @@ public bool TryMakeIpv6AddressLiteral(out string address)
775797

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

780804
return true;
781805
}

Src/SmtpServer/SmtpServer.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<LangVersion>7.2</LangVersion>
66
<AssemblyName>SmtpServer</AssemblyName>
77
<RootNamespace>SmtpServer</RootNamespace>
8-
<Version>6.4.1-RC1</Version>
8+
<Version>6.5.0</Version>
99
<Description>.NET SmtpServer</Description>
1010
<Authors>Cain O'Sullivan</Authors>
1111
<Company />
@@ -15,9 +15,9 @@
1515
<PackageTags>smtp smtpserver smtp server</PackageTags>
1616
<PackageLicenseUrl></PackageLicenseUrl>
1717
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
18-
<AssemblyVersion>6.4.1.0</AssemblyVersion>
19-
<FileVersion>6.4.1.0</FileVersion>
20-
<PackageReleaseNotes>Added the SessionFaulted event to the SmtpServer.</PackageReleaseNotes>
18+
<AssemblyVersion>6.5.0.0</AssemblyVersion>
19+
<FileVersion>6.5.0.0</FileVersion>
20+
<PackageReleaseNotes>Added MaxAuthenticationAttempts and support for IPv6 addresses.</PackageReleaseNotes>
2121
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
2222
<PackageLicenseFile>LICENSE</PackageLicenseFile>
2323
</PropertyGroup>

Src/SmtpServer/SmtpServerOptionsBuilder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public ISmtpServerOptions Build()
2626
MailboxFilterFactory = DoNothingMailboxFilter.Instance,
2727
UserAuthenticatorFactory = DoNothingUserAuthenticator.Instance,
2828
MaxRetryCount = 5,
29+
MaxAuthenticationAttempts = 3,
2930
SupportedSslProtocols = SslProtocols.Tls12,
3031
NetworkBufferSize = 128,
3132
NetworkBufferReadTimeout = TimeSpan.FromMinutes(2),
@@ -187,6 +188,18 @@ public SmtpServerOptionsBuilder MaxRetryCount(int value)
187188
return this;
188189
}
189190

191+
/// <summary>
192+
/// Sets the maximum number of authentication attempts.
193+
/// </summary>
194+
/// <param name="value">The maximum number of authentication attempts for a failed authentication.</param>
195+
/// <returns>A OptionsBuilder to continue building on.</returns>
196+
public SmtpServerOptionsBuilder MaxAuthenticationAttempts(int value)
197+
{
198+
_setters.Add(options => options.MaxAuthenticationAttempts = value);
199+
200+
return this;
201+
}
202+
190203
/// <summary>
191204
/// Sets the supported SSL protocols.
192205
/// </summary>
@@ -261,6 +274,11 @@ class SmtpServerOptions : ISmtpServerOptions
261274
/// </summary>
262275
public int MaxRetryCount { get; set; }
263276

277+
/// <summary>
278+
/// The maximum number of authentication attempts.
279+
/// </summary>
280+
public int MaxAuthenticationAttempts { get; set; }
281+
264282
/// <summary>
265283
/// Gets or sets the SMTP server name.
266284
/// </summary>

Src/SmtpServer/SmtpSession.cs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -84,33 +84,37 @@ async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellati
8484
return;
8585
}
8686

87-
if (TryMake(context, text, out var command, out var response))
87+
if (TryMake(context, text, out var command, out var response) == false)
8888
{
89-
try
89+
await context.NetworkClient.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken).ConfigureAwait(false);
90+
continue;
91+
}
92+
93+
try
94+
{
95+
if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false))
9096
{
91-
if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false))
92-
{
93-
_stateMachine.Transition(context);
94-
}
95-
96-
retries = _context.ServerOptions.MaxRetryCount;
97-
98-
continue;
97+
_stateMachine.Transition(context);
9998
}
100-
catch (SmtpResponseException responseException)
101-
{
102-
context.IsQuitRequested = responseException.IsQuitRequested;
10399

104-
response = responseException.Response;
105-
}
106-
catch (OperationCanceledException)
107-
{
108-
await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken).ConfigureAwait(false);
109-
return;
110-
}
100+
retries = _context.ServerOptions.MaxRetryCount;
111101
}
102+
catch (SmtpResponseException responseException) when (responseException.IsQuitRequested)
103+
{
104+
await context.NetworkClient.ReplyAsync(responseException.Response, cancellationToken).ConfigureAwait(false);
105+
return;
106+
}
107+
catch (SmtpResponseException responseException)
108+
{
109+
response = CreateErrorResponse(responseException.Response, retries);
112110

113-
await context.NetworkClient.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken).ConfigureAwait(false);
111+
await context.NetworkClient.ReplyAsync(response, cancellationToken).ConfigureAwait(false);
112+
}
113+
catch (OperationCanceledException)
114+
{
115+
await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken).ConfigureAwait(false);
116+
return;
117+
}
114118
}
115119
}
116120

Src/SmtpServer/SmtpSessionContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ internal void RaiseSessionAuthenticated()
7878
/// </summary>
7979
public AuthenticationContext Authentication { get; internal set; } = AuthenticationContext.Unauthenticated;
8080

81+
/// <summary>
82+
/// Returns the number of athentication attempts.
83+
/// </summary>
84+
public int AuthenticationAttempts { get; internal set; }
85+
8186
/// <summary>
8287
/// Gets a value indicating whether a quit has been requested.
8388
/// </summary>

Src/SmtpServer/Text/ITokenEnumerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public interface ITokenEnumerator
1212
Token Peek();
1313

1414
/// <summary>
15-
/// Take the given number of tokens.
15+
/// Take the next token.
1616
/// </summary>
1717
/// <returns>The last token that was consumed.</returns>
1818
Token Take();

Src/SmtpServer/Text/TokenEnumerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public Token Peek()
3131
}
3232

3333
/// <summary>
34-
/// Take the given number of tokens.
34+
/// Take the next token.
3535
/// </summary>
3636
/// <returns>The last token that was consumed.</returns>
3737
public Token Take()
@@ -40,7 +40,7 @@ public Token Take()
4040
}
4141

4242
/// <summary>
43-
/// Take the given number of tokens.
43+
/// Returns the token at the given index.
4444
/// </summary>
4545
/// <returns>The last token that was consumed.</returns>
4646
Token At(int index)

0 commit comments

Comments
 (0)