Compare commits
10 Commits
net10-port
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e28dc48bd0 | |||
| 5a72164b68 | |||
| 809f46fde1 | |||
| 6d6c74fd32 | |||
| cd3ebed68b | |||
| f7c1d7c856 | |||
| b49bcaecff | |||
| 8f87f385de | |||
| d00ec199eb | |||
| daebf202e9 |
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
@@ -35,6 +36,10 @@ namespace Sockeye.Api.Controllers
|
|||||||
private readonly ApiServerState serverState;
|
private readonly ApiServerState serverState;
|
||||||
private const int JWT_LIFETIME_DAYS = 5;
|
private const int JWT_LIFETIME_DAYS = 5;
|
||||||
|
|
||||||
|
//case 4710: wrong 2fa codes allowed per password entry, counted per user (in memory, a restart only resets the count)
|
||||||
|
private const int MAX_TFA_FAILURES = 5;
|
||||||
|
private static readonly ConcurrentDictionary<long, int> TfaFailures = new ConcurrentDictionary<long, int>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -74,8 +79,14 @@ namespace Sockeye.Api.Controllers
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//case 4726: too many failed logins for this login name or from this address: refused at once, the password is not checked
|
||||||
|
var address = HttpContext.Connection.RemoteIpAddress;
|
||||||
|
if (LoginThrottle.IsRefused(creds.Login, address))
|
||||||
|
return StatusCode(429, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED, null, "Too many failed login attempts, please try again later"));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
|
if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
|
||||||
{
|
{
|
||||||
|
RecordLoginFailure(creds.Login, address);
|
||||||
//Make a failed pw wait
|
//Make a failed pw wait
|
||||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
@@ -91,7 +102,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
string hashed = Hasher.hash(u.Salt, creds.Password);
|
string hashed = Hasher.hash(u.Salt, creds.Password);
|
||||||
if (hashed == u.Password)
|
if (hashed == u.Password)
|
||||||
{
|
{
|
||||||
|
LoginThrottle.RecordSuccess(creds.Login);
|
||||||
|
|
||||||
//TWO FACTOR ENABLED??
|
//TWO FACTOR ENABLED??
|
||||||
//if 2fa enabled then need to validate it before sending token, so we're halfway there and need to send a 2fa prompt
|
//if 2fa enabled then need to validate it before sending token, so we're halfway there and need to send a 2fa prompt
|
||||||
@@ -100,6 +111,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
//Generate a temporary token to identify and verify this is the same user
|
//Generate a temporary token to identify and verify this is the same user
|
||||||
u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", "");
|
u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", "");
|
||||||
await ct.SaveChangesAsync();
|
await ct.SaveChangesAsync();
|
||||||
|
TfaFailures.TryRemove(u.Id, out _);
|
||||||
var UOpt = await ct.UserOptions.AsNoTracking().FirstAsync(z => z.UserId == u.Id);
|
var UOpt = await ct.UserOptions.AsNoTracking().FirstAsync(z => z.UserId == u.Id);
|
||||||
|
|
||||||
List<string> TranslationKeysToFetch = new List<string> { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel", "AuthPinInvalid" };
|
List<string> TranslationKeysToFetch = new List<string> { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel", "AuthPinInvalid" };
|
||||||
@@ -125,11 +137,20 @@ namespace Sockeye.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
//No users matched, it's a failed login
|
//No users matched, it's a failed login
|
||||||
|
RecordLoginFailure(creds.Login, address);
|
||||||
//Make a failed pw wait
|
//Make a failed pw wait
|
||||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//case 4726
|
||||||
|
private void RecordLoginFailure(string login, System.Net.IPAddress address)
|
||||||
|
{
|
||||||
|
var reached = LoginThrottle.RecordFailure(login, address);
|
||||||
|
if (reached != null)
|
||||||
|
log.LogWarning($"Too many failed login attempts for {reached}: further attempts refused for up to 15 minutes");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -164,6 +185,13 @@ namespace Sockeye.Api.Controllers
|
|||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//case 4710: the temp token is the only proof the password step was passed, so it must be present
|
||||||
|
if (string.IsNullOrWhiteSpace(pin.TempToken))
|
||||||
|
{
|
||||||
|
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
|
}
|
||||||
|
|
||||||
//Match to temp token that would have been set by initial credentialed login for 2fa User
|
//Match to temp token that would have been set by initial credentialed login for 2fa User
|
||||||
var user = await ct.User.Where(z => z.TempToken == pin.TempToken && z.Active == true && z.AllowLogin==true && z.TwoFactorEnabled == true).FirstOrDefaultAsync();
|
var user = await ct.User.Where(z => z.TempToken == pin.TempToken && z.Active == true && z.AllowLogin==true && z.TwoFactorEnabled == true).FirstOrDefaultAsync();
|
||||||
|
|
||||||
@@ -181,14 +209,22 @@ namespace Sockeye.Api.Controllers
|
|||||||
var tfa = new TwoFactorAuth("Sockeye");
|
var tfa = new TwoFactorAuth("Sockeye");
|
||||||
if (!tfa.VerifyCode(user.TotpSecret, pin.Pin.Replace(" ", "").Trim()))
|
if (!tfa.VerifyCode(user.TotpSecret, pin.Pin.Replace(" ", "").Trim()))
|
||||||
{
|
{
|
||||||
|
//case 4710: after too many wrong codes the password must be entered again
|
||||||
|
if (TfaFailures.AddOrUpdate(user.Id, 1, (k, n) => n + 1) >= MAX_TFA_FAILURES)
|
||||||
|
{
|
||||||
|
TfaFailures.TryRemove(user.Id, out _);
|
||||||
|
user.TempToken = null;
|
||||||
|
await ct.SaveChangesAsync();
|
||||||
|
}
|
||||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||||
}
|
}
|
||||||
|
|
||||||
//User is valid and authenticated
|
//User is valid and authenticated
|
||||||
//clear temp token
|
//clear temp token (case 4710: null, not empty, so no value is left to match)
|
||||||
user.TempToken = string.Empty;
|
user.TempToken = null;
|
||||||
await ct.SaveChangesAsync();
|
await ct.SaveChangesAsync();
|
||||||
|
TfaFailures.TryRemove(user.Id, out _);
|
||||||
|
|
||||||
return await ReturnUserCredsOnSuccessfulAuthentication(user);
|
return await ReturnUserCredsOnSuccessfulAuthentication(user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// GET: fetch/somecode/bob@bob.com
|
// GET: fetch/somecode/bob@bob.com
|
||||||
[HttpGet("{code}/{email}")]
|
[HttpGet("{code}/{email}")]
|
||||||
|
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
|
||||||
public async Task<IActionResult> Get([FromRoute] string code, [FromRoute] string email)
|
public async Task<IActionResult> Get([FromRoute] string code, [FromRoute] string email)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
//### RAVEN FETCH LICENSE REQUEST HERE ####
|
//### RAVEN FETCH LICENSE REQUEST HERE ####
|
||||||
//This route is used by Raven to check if there is an unfetched license key and return it
|
//This route is used by Raven to check if there is an unfetched license key and return it
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
|
||||||
public async Task<IActionResult> PostLicenseFetchRequest([FromBody] dtoFetchRequest fetchRequest)
|
public async Task<IActionResult> PostLicenseFetchRequest([FromBody] dtoFetchRequest fetchRequest)
|
||||||
{
|
{
|
||||||
if (!serverState.IsOpen)
|
if (!serverState.IsOpen)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
//REQUEST TRIAL ROUTE CALLED FROM RAVEN
|
//REQUEST TRIAL ROUTE CALLED FROM RAVEN
|
||||||
//#########################################################
|
//#########################################################
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_TRIAL_REQUESTS)]//case 4681
|
||||||
public async Task<IActionResult> Post([FromBody] dtoRequest r)
|
public async Task<IActionResult> Post([FromBody] dtoRequest r)
|
||||||
{
|
{
|
||||||
if (!serverState.IsOpen)
|
if (!serverState.IsOpen)
|
||||||
@@ -70,6 +71,12 @@ namespace Sockeye.Api.Controllers
|
|||||||
return BadRequest(ModelState);
|
return BadRequest(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//case 4681: a verification email is sent to this address, so it must at least be a valid address
|
||||||
|
if (string.IsNullOrWhiteSpace(r.Email) || !System.Net.Mail.MailAddress.TryCreate(r.Email.Trim(), out var parsedEmail) || parsedEmail.Address != r.Email.Trim())
|
||||||
|
{
|
||||||
|
return BadRequest("E1000 - Email address invalid");
|
||||||
|
}
|
||||||
|
|
||||||
// if (r.DbId == Guid.Empty)
|
// if (r.DbId == Guid.Empty)
|
||||||
if (string.IsNullOrWhiteSpace(r.DbId))
|
if (string.IsNullOrWhiteSpace(r.DbId))
|
||||||
{
|
{
|
||||||
@@ -114,6 +121,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
|
|
||||||
//### CUSTOMER ROUTE CALLED FROM CUSTOMER BROWSER / EMAIL ####
|
//### CUSTOMER ROUTE CALLED FROM CUSTOMER BROWSER / EMAIL ####
|
||||||
[HttpGet("verify/{code}")]
|
[HttpGet("verify/{code}")]
|
||||||
|
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
|
||||||
public async Task<IActionResult> GetVerify([FromRoute] string code)
|
public async Task<IActionResult> GetVerify([FromRoute] string code)
|
||||||
{
|
{
|
||||||
if (!serverState.IsOpen)
|
if (!serverState.IsOpen)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ namespace Sockeye.Api.Controllers
|
|||||||
//REQUEST ROUTE CALLED FROM CONTACT FORMS APP
|
//REQUEST ROUTE CALLED FROM CONTACT FORMS APP
|
||||||
//#########################################################
|
//#########################################################
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_TRIAL_REQUESTS)]//case 4681
|
||||||
public async Task<IActionResult> Post([FromBody] dtoSubscriptionServerTrialRequest r)
|
public async Task<IActionResult> Post([FromBody] dtoSubscriptionServerTrialRequest r)
|
||||||
{
|
{
|
||||||
if (!serverState.IsOpen)
|
if (!serverState.IsOpen)
|
||||||
@@ -109,6 +110,9 @@ namespace Sockeye.Api.Controllers
|
|||||||
case "NY":
|
case "NY":
|
||||||
newObject.TimeZone = "America/New_York";
|
newObject.TimeZone = "America/New_York";
|
||||||
break;
|
break;
|
||||||
|
case "ATL":
|
||||||
|
newObject.TimeZone = "America/New_York";//this is the correct time zone for Atlanta officially
|
||||||
|
break;
|
||||||
case "TOR":
|
case "TOR":
|
||||||
newObject.TimeZone = "America/Toronto";
|
newObject.TimeZone = "America/Toronto";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace Sockeye
|
|||||||
{
|
{
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
|
//case 4681: rate limit policy names used on the anonymous trial and licence routes
|
||||||
|
internal const string RATE_LIMIT_TRIAL_REQUESTS = "trial-requests";
|
||||||
|
internal const string RATE_LIMIT_LICENSE_FETCH = "license-fetch";
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
public Startup(Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment)
|
public Startup(Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment)
|
||||||
@@ -174,6 +178,19 @@ namespace Sockeye
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//case 4681: per client address limits on the anonymous trial request and licence fetch routes
|
||||||
|
_newLog.LogDebug("Rate limiting");
|
||||||
|
services.AddRateLimiter(options =>
|
||||||
|
{
|
||||||
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||||
|
options.AddPolicy(RATE_LIMIT_TRIAL_REQUESTS, context => System.Threading.RateLimiting.RateLimitPartition.GetFixedWindowLimiter(
|
||||||
|
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||||
|
_ => new System.Threading.RateLimiting.FixedWindowRateLimiterOptions { PermitLimit = 5, Window = TimeSpan.FromHours(1), QueueLimit = 0 }));
|
||||||
|
options.AddPolicy(RATE_LIMIT_LICENSE_FETCH, context => System.Threading.RateLimiting.RateLimitPartition.GetFixedWindowLimiter(
|
||||||
|
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||||
|
_ => new System.Threading.RateLimiting.FixedWindowRateLimiterOptions { PermitLimit = 30, Window = TimeSpan.FromMinutes(10), QueueLimit = 0 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -377,6 +394,9 @@ namespace Sockeye
|
|||||||
_newLog.LogDebug("Routing pipeline");
|
_newLog.LogDebug("Routing pipeline");
|
||||||
app.UseRouting();//this wasn't here for 2.2 but added for 3.0, needs to come before the stuff after
|
app.UseRouting();//this wasn't here for 2.2 but added for 3.0, needs to come before the stuff after
|
||||||
|
|
||||||
|
//case 4681: after routing so the per-route policies apply (and after forwarded headers so the client address is the real one)
|
||||||
|
app.UseRateLimiter();
|
||||||
|
|
||||||
_newLog.LogDebug("CORS pipeline");
|
_newLog.LogDebug("CORS pipeline");
|
||||||
app.UseCors("CorsPolicy");
|
app.UseCors("CorsPolicy");
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"IncludeScopes": false,
|
|
||||||
"Debug": {
|
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Trace"
|
"Default": "Information",
|
||||||
}
|
"Microsoft.AspNetCore": "Warning"
|
||||||
},
|
|
||||||
"Console": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Trace"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateFullPaths>true</GenerateFullPaths>
|
<GenerateFullPaths>true</GenerateFullPaths>
|
||||||
<Version>8.1.0</Version>
|
<Version>8.1.1</Version>
|
||||||
<FileVersion>8.1.0.0</FileVersion>
|
<FileVersion>8.1.1.0</FileVersion>
|
||||||
<ApplicationIcon>sockeye.ico</ApplicationIcon>
|
<ApplicationIcon>sockeye.ico</ApplicationIcon>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<noWarn>1591</noWarn>
|
<noWarn>1591</noWarn>
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<PackageReference Include="Enums.NET" Version="5.0.0" />
|
<PackageReference Include="Enums.NET" Version="5.0.0" />
|
||||||
<PackageReference Include="jose-jwt" Version="5.3.0" />
|
<PackageReference Include="jose-jwt" Version="5.3.0" />
|
||||||
<PackageReference Include="MailKit" Version="4.18.0" />
|
<PackageReference Include="MailKit" Version="4.18.0" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.12" />
|
||||||
<PackageReference Include="Asp.Versioning.Mvc" Version="10.2.1" />
|
<PackageReference Include="Asp.Versioning.Mvc" Version="10.2.1" />
|
||||||
|
|||||||
78
server/util/LoginThrottle.cs
Normal file
78
server/util/LoginThrottle.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Sockeye.Util
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// case 4726: failed logins counted per login name and per client address (in memory, a restart resets the counts).
|
||||||
|
/// Too many failures within the window and further attempts are refused until the window ends.
|
||||||
|
/// Login names that don't exist are counted the same way, so the refusal says nothing about which logins exist.
|
||||||
|
/// Requests from this machine (loopback) are not counted per address: behind nginx the real client address is seen,
|
||||||
|
/// so loopback is only the machine itself
|
||||||
|
/// </summary>
|
||||||
|
internal static class LoginThrottle
|
||||||
|
{
|
||||||
|
internal const int MAX_LOGIN_FAILURES = 10;
|
||||||
|
internal const int MAX_ADDRESS_FAILURES = 50;
|
||||||
|
private static readonly TimeSpan WINDOW = TimeSpan.FromMinutes(15);
|
||||||
|
private const int PRUNE_ABOVE = 10000;
|
||||||
|
|
||||||
|
private record Window(DateTime Start, int Count);
|
||||||
|
private static readonly ConcurrentDictionary<string, Window> Failures = new ConcurrentDictionary<string, Window>();
|
||||||
|
|
||||||
|
private static string LoginKey(string login) => "L:" + (login ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
private static string AddressKey(IPAddress address) => address == null || IPAddress.IsLoopback(address) ? null : "A:" + address.ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if this login or this address has reached its limit in the current window
|
||||||
|
/// </summary>
|
||||||
|
internal static bool IsRefused(string login, IPAddress address)
|
||||||
|
{
|
||||||
|
return Reached(LoginKey(login), MAX_LOGIN_FAILURES) || Reached(AddressKey(address), MAX_ADDRESS_FAILURES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts a failed login; returns a description when this failure reached a limit (for the log), otherwise null
|
||||||
|
/// </summary>
|
||||||
|
internal static string RecordFailure(string login, IPAddress address)
|
||||||
|
{
|
||||||
|
if (Failures.Count > PRUNE_ABOVE)
|
||||||
|
Prune();
|
||||||
|
string reached = null;
|
||||||
|
if (Count(LoginKey(login)) == MAX_LOGIN_FAILURES)
|
||||||
|
reached = $"login \"{login}\"";
|
||||||
|
var addressKey = AddressKey(address);
|
||||||
|
if (addressKey != null && Count(addressKey) == MAX_ADDRESS_FAILURES)
|
||||||
|
reached = (reached == null ? "" : reached + " and ") + $"address {address}";
|
||||||
|
return reached;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A correct password clears the count for that login
|
||||||
|
/// </summary>
|
||||||
|
internal static void RecordSuccess(string login)
|
||||||
|
{
|
||||||
|
Failures.TryRemove(LoginKey(login), out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Reached(string key, int max)
|
||||||
|
{
|
||||||
|
return key != null && Failures.TryGetValue(key, out var w) && DateTime.UtcNow - w.Start < WINDOW && w.Count >= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Count(string key)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
return Failures.AddOrUpdate(key, new Window(now, 1), (_, w) => now - w.Start < WINDOW ? w with { Count = w.Count + 1 } : new Window(now, 1)).Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Prune()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var kv in Failures)
|
||||||
|
if (now - kv.Value.Start >= WINDOW)
|
||||||
|
Failures.TryRemove(kv.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,22 +51,36 @@ namespace Sockeye.Util
|
|||||||
internal static void SetNextBackup()
|
internal static void SetNextBackup()
|
||||||
{
|
{
|
||||||
DateTime utcNow = DateTime.UtcNow;
|
DateTime utcNow = DateTime.UtcNow;
|
||||||
|
//case 4682: the backup time is a time on the server's clock; days are the server's local days
|
||||||
|
DateTime localToday = TimeZoneInfo.ConvertTimeFromUtc(utcNow, TimeZoneInfo.Local).Date;
|
||||||
//Has last backup run more than 24 hours ago?
|
//Has last backup run more than 24 hours ago?
|
||||||
if (NextBackup < utcNow.AddHours(-24))
|
if (NextBackup < utcNow.AddHours(-24))
|
||||||
{
|
{
|
||||||
//set it to today then
|
//set it to today then
|
||||||
NextBackup = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, Backup.BackupTime.Hour, Backup.BackupTime.Minute, 0, DateTimeKind.Utc);
|
NextBackup = BackupTimeOnLocalDay(localToday);
|
||||||
//Make sure next backup is in the future
|
//Make sure next backup is in the future
|
||||||
if (NextBackup < utcNow)
|
if (NextBackup < utcNow)
|
||||||
NextBackup = NextBackup.AddDays(1);
|
NextBackup = BackupTimeOnLocalDay(localToday.AddDays(1));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//less than 24 hours, set it to tomorrow
|
//less than 24 hours, set it to tomorrow
|
||||||
NextBackup = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, Backup.BackupTime.Hour, Backup.BackupTime.Minute, 0, DateTimeKind.Utc);
|
NextBackup = BackupTimeOnLocalDay(localToday.AddDays(1));
|
||||||
NextBackup = NextBackup.AddDays(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//case 4682: the backup time is stored as the UTC moment it meant on the day it was set, and was run at that UTC hour
|
||||||
|
//every day, so it moved an hour on the server's clock with each daylight saving change. The server's clock time is
|
||||||
|
//recovered with that day's offset and the backup is run at that clock time on the given local day (UTC returned)
|
||||||
|
private static DateTime BackupTimeOnLocalDay(DateTime localDay)
|
||||||
|
{
|
||||||
|
var chosen = TimeZoneInfo.ConvertTimeFromUtc(DateTime.SpecifyKind(Backup.BackupTime, DateTimeKind.Utc), TimeZoneInfo.Local);
|
||||||
|
var local = DateTime.SpecifyKind(localDay.Date + new TimeSpan(chosen.Hour, chosen.Minute, 0), DateTimeKind.Unspecified);
|
||||||
|
//a time skipped when the clocks go forward (e.g. 2:30 AM that night) runs an hour later
|
||||||
|
if (TimeZoneInfo.Local.IsInvalidTime(local))
|
||||||
|
local = local.AddHours(1);
|
||||||
|
return TimeZoneInfo.ConvertTimeToUtc(local, TimeZoneInfo.Local);
|
||||||
|
}
|
||||||
|
|
||||||
}//eoc
|
}//eoc
|
||||||
}//eons
|
}//eons
|
||||||
@@ -5,7 +5,7 @@ namespace Sockeye.Util
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class SockeyeVersion
|
internal static class SockeyeVersion
|
||||||
{
|
{
|
||||||
public const string VersionString = "8.0.17";
|
public const string VersionString = "8.1.1";
|
||||||
public const string FullNameAndVersion = "Sockeye server " + VersionString;
|
public const string FullNameAndVersion = "Sockeye server " + VersionString;
|
||||||
public const string CurrentApiVersion="v8";
|
public const string CurrentApiVersion="v8";
|
||||||
}//eoc
|
}//eoc
|
||||||
|
|||||||
@@ -178,11 +178,11 @@ namespace Sockeye.Util
|
|||||||
|
|
||||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
var stringChars = new char[10];
|
var stringChars = new char[10];
|
||||||
var random = new Random();
|
|
||||||
|
|
||||||
|
//case 4681: cryptographic random, same format as before
|
||||||
for (int i = 0; i < stringChars.Length; i++)
|
for (int i = 0; i < stringChars.Length; i++)
|
||||||
{
|
{
|
||||||
stringChars[i] = chars[random.Next(chars.Length)];
|
stringChars[i] = chars[System.Security.Cryptography.RandomNumberGenerator.GetInt32(chars.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalString = new String(stringChars);
|
var finalString = new String(stringChars);
|
||||||
|
|||||||
13
todo.txt
13
todo.txt
@@ -1,12 +1,17 @@
|
|||||||
TODO:
|
TODO:
|
||||||
|
|
||||||
|
Chromium / chrome change is in the changelog of ayanova, no big mystery just some kind of default:
|
||||||
|
AyaNova 8.2.0
|
||||||
|
Released 2024-06-03
|
||||||
|
Server: switched the report rendering system from Chromium to Chrome browser to align with the default browser now supported by the Puppeteer headless browser controller library that we use for report generation. There will be a longer than normal delay the first time a report is rendered after upgrading while AyaNova automatically downloads the new report rendering browser.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- Subscriptions
|
- Subscriptions
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
todo: Two reports, one revenue report for list as filterd and one showing who subscribes to what and count of subscription items for each type etc
|
todo: Two reports, one revenue report for list as filterd and one showing who subscribes to what and count of subscription items for each type etc
|
||||||
- Need to make it reportable so the subscription biz object needs to be able to support reporting fully with renewal and date and price viz etc
|
- Need to make it reportable so the subscription biz object needs to be able to support reporting fully with renewal and date and price viz etc
|
||||||
- Need to update the subscription item edit form to show renewal checkbox
|
- Need to update the subscription item edit form to show renewal checkbox
|
||||||
|
|||||||
Reference in New Issue
Block a user