Implement POST request rate limiting (#490)

* Initial work for rate limiting

* Refactor GameServerStartup and change default rate limit config

* Adjust config naming and add Enabled option to global and override rate limits

* Fix LBP3 republish bug

* Fix bugs in rate limiting and allow for multiple matched overrides

* Add this qualifier for private variable

* Changes from self review
This commit is contained in:
Josh 2022-09-24 17:18:28 -05:00 committed by GitHub
commit 3ad211e5c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 451 additions and 206 deletions

View file

@ -13,7 +13,8 @@
}
else
{
<p>Failed to send email, please try again later</p>
<p>Failed to send email, please try again later.</p>
<p>If this issue persists please contact an Administrator</p>
}
<a href="/login/sendVerificationEmail">

View file

@ -1,5 +1,7 @@
#nullable enable
using System.Collections.Concurrent;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email;
@ -14,6 +16,9 @@ public class SendVerificationEmailPage : BaseLayout
public SendVerificationEmailPage(Database database) : base(database)
{}
// (User id, timestamp of last request + 30 seconds)
private static readonly ConcurrentDictionary<int, long> recentlySentEmail = new();
public bool Success { get; set; }
public async Task<IActionResult> OnGet()
@ -35,22 +40,34 @@ public class SendVerificationEmailPage : BaseLayout
}
#endif
EmailVerificationToken? verifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(v => v.UserId == user.UserId);
// If user doesn't have a token or it is expired then regenerate
if (verifyToken == null || DateTime.Now > verifyToken.ExpiresAt)
// Remove expired entries
for (int i = recentlySentEmail.Count - 1; i >= 0; i--)
{
verifyToken = new EmailVerificationToken
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now.AddHours(6),
};
this.Database.EmailVerificationTokens.Add(verifyToken);
await this.Database.SaveChangesAsync();
KeyValuePair<int, long> entry = recentlySentEmail.ElementAt(i);
if (TimeHelper.TimestampMillis > recentlySentEmail[user.UserId]) recentlySentEmail.TryRemove(entry.Key, out _);
}
if (recentlySentEmail.ContainsKey(user.UserId) && recentlySentEmail[user.UserId] > TimeHelper.TimestampMillis)
{
this.Success = true;
return this.Page();
}
string? existingToken = await this.Database.EmailVerificationTokens.Where(v => v.UserId == user.UserId).Select(v => v.EmailToken).FirstOrDefaultAsync();
if(existingToken != null)
this.Database.EmailVerificationTokens.RemoveWhere(t => t.EmailToken == existingToken);
EmailVerificationToken verifyToken = new()
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now.AddHours(6),
};
this.Database.EmailVerificationTokens.Add(verifyToken);
await this.Database.SaveChangesAsync();
string body = "Hello,\n\n" +
$"This email is a request to verify this email for your (likely new!) Project Lighthouse account ({user.Username}).\n\n" +
$"To verify your account, click the following link: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" +
@ -58,6 +75,9 @@ public class SendVerificationEmailPage : BaseLayout
this.Success = SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body);
// Don't send another email for 30 seconds
recentlySentEmail.TryAdd(user.UserId, TimeHelper.TimestampMillis + 30 * 1000);
return this.Page();
}
}

View file

@ -81,6 +81,7 @@ public class WebsiteStartup
app.UseMiddleware<HandlePageErrorMiddleware>();
app.UseMiddleware<RequestLogMiddleware>();
app.UseMiddleware<UserRequiredRedirectMiddleware>();
app.UseMiddleware<RateLimitMiddleware>();
app.UseRouting();