14 Commits

Author SHA1 Message Date
f394aa6ee3 case 4754 - v7 licence email templates: forum lines removed
The forum is being retired. Removes "Technical support forum:
http://forum.ayanova.com" from the default V7NewKey and V7TemporaryTrial
templates (GlobalBizSettings.cs, used when a new database is created) and
from the unused copies in KeyFactory.cs. Production's stored templates
were edited the same way in Admin -> Global settings on 2026-10-30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:06:55 -07:00
4dad213752 devdocs - CLAUDE.md pointing to the AyaNova name map
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:39:26 -07:00
03ff2a2626 devdocs - keep devdocs scripts in LF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:39:26 -07:00
712d0c891f devdocs - AyaNova/Sockeye name map and compare script
NAME-MAP.md lists the names Sockeye changed from AyaNova (types, properties,
database columns, config keys, client files), the names left alone, and the
files that exist in only one codebase. compare-with-raven.sh maps the names
and diffs the shared server files so only real code differences show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:37:16 -07:00
e28dc48bd0 case 4682 - automatic backup runs at the chosen time on the server's clock all year
Same fix as AyaNova: the backup ran at the stored UTC hour, so with daylight
saving it moved an hour on the server's clock for part of the year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:10:00 -07:00
5a72164b68 8.1.1 pin System.Security.Cryptography.Pkcs 10.0.13 (CVE-2026-84008, CVE-2026-70162) 2026-10-25 08:45:23 -07:00
809f46fde1 Version bumps 2026-10-25 08:06:54 -07:00
6d6c74fd32 case 4726 - login lockout per login name and per address
Same as AyaNova: failed logins counted in memory, 10 per login name or 50
per client address within 15 minutes, then 429 for the rest of the window.
Sockeye's login is public (only SSH to the droplet is limited).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:12:31 -07:00
cd3ebed68b case 4681 - rate limits on the anonymous trial and licence routes, stricter email check, cryptographic codes
Anonymous trial requests could be sent without limit, each one emailing a
verification link to any address (bounces from made-up domains), and the
verification and v7 fetch codes came from System.Random (AyaNova security
review F40).
- ASP.NET Core rate limiter per client address: trial requests (rvr,
  subscription-server-trial-request) 5 per hour; verify, rvf and the legacy v7
  fetch 30 per 10 minutes; 429 over the limit. Runs after routing and after
  forwarded headers.
- rvr: the email must be a valid address (MailAddress) before anything is
  stored or sent.
- GenFetchCode: RandomNumberGenerator, same 10-letter format.
rvf is unchanged (still one-shot): repeatable fetch needs an AyaNova-side change
for older servers and is a separate later case.

Checked on the local server with invalid addresses only (no email sent, no
request stored): 6th trial request 429, 31st fetch 429, auth unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:54:59 -07:00
f7c1d7c856 case 4710 - 2FA step requires the temp token from the password step
Same fix as AyaNova (raven 141bdeb8): the 2FA step matched the user by temp
token alone and the token was left empty after a 2FA login, so an empty or
missing token skipped the password step. Now an empty or missing temp token is
refused, the token is cleared to null after success, and after 5 wrong codes
the password must be entered again.

Checked on the local server with a temporary user (removed afterwards): no or
empty temp token 401; normal sign in 200; 5 wrong codes then the right code 401;
new password entry 200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:36:45 -07:00
b49bcaecff case 4670 .net logging set to trace by default caught by claude also random todo stuff re puppeteersharp 2026-10-19 14:25:55 -07:00
8f87f385de Added support for Atlanta DataCenter on subscription server request form to match contact form project which I just added that to as well, not technically part of the .net10 migration but might as well while I'm in there 2026-10-09 14:43:36 -07:00
d00ec199eb case 4670 update version properly 2026-10-09 13:19:24 -07:00
daebf202e9 Merge net10-port: .NET 10 port 2026-10-09 12:47:35 -07:00
19 changed files with 319 additions and 44 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
devdocs/*.sh text eol=lf

8
CLAUDE.md Normal file
View File

@@ -0,0 +1,8 @@
# Sockeye — instructions for Claude Code
Sockeye is the internal licensing server for AyaNova. Its server began as a copy of AyaNova's
(`..\raven`), with some names changed: `devdocs/NAME-MAP.md` lists them, and
`devdocs/compare-with-raven.sh` compares the shared files with the names mapped. For a low-level
server fix in either codebase, check whether the other needs it too, and record the answer.
The working rules in `..\raven\CLAUDE.md` apply here as well.

81
devdocs/NAME-MAP.md Normal file
View File

@@ -0,0 +1,81 @@
# AyaNova ↔ Sockeye name map
Sockeye's server began as a copy of AyaNova's (`raven/server/AyaNova`). Some names were
changed for Sockeye; most code is otherwise the same. Use this map to carry a fix from one
codebase to the other. Mapped 2026-10-27, AyaNova `main` (8.3.0) against Sockeye `main` (8.1.x).
`compare-with-raven.sh` (beside this file) applies the map and compares the shared files, so
only real code differences show:
```bash
bash compare-with-raven.sh # every shared file: differing lines, AyaNova lines, path
bash compare-with-raven.sh util/DbUtil.cs # the diff for one file (< AyaNova mapped, > Sockeye)
```
## Renamed
| AyaNova | Sockeye | Where |
|---|---|---|
| namespace `AyaNova.*`, `AyaNova` in text | `Sockeye.*`, `Sockeye` | everywhere |
| `AyaType` (enum) | `SockType` | `biz/AyaType.cs` → `biz/SockType.cs` |
| `AyaTypeId` | `SockTypeId` | `biz/` |
| `AyaEvent` (enum) | `SockEvent` | `biz/` |
| `AyaDaysOfWeek` (enum) | `SockDaysOfWeek` | `biz/` |
| `AyaNovaVersion` | `SockeyeVersion` | `util/` |
| `AyaNovaConnectionString`, `AyaNovaServerURL`, `logRuleAyaNovaItems` | `SockeyeConnectionString`, `SockeyeServerURL`, `logRuleSockeyeItems` | `util/` |
| `AyaTypeTranslated`, `ayaTypeColumnName` | `SockTypeTranslated`, `sockTypeColumnName` | DataList, PickList |
| stored type property `AType` (JSON `aType`) | `SockType` (JSON `sockType`) | `models/`: Attachment, Event, SearchKey, OpsJob, Report, Review, IntegrationItem, DataListSelectedRequest… |
| computed `public AyaType AyaType { get => AyaType.X; }` (JSON `ayaType`) | `public SockType SType { get => SockType.X; }` (JSON `sType`) | business models: Customer, User, Memo, Reminder, Review… |
| locals and parameters `ayaType`, `atype`, `ayId`, `ayaEvent` | `sockType`, `sockId`, `sockEvent` (a few `stype`, `sock`) | everywhere |
| database columns `atype`, `ayatype`, `ayid`, `ayevent` | `socktype`, `sockid`, `sockevent`, and `stype` on business tables | `util/AySchema.cs`, raw SQL in DataList/PickList/biz |
| config keys `AYANOVA_*` | `SOCKEYE_*`, same suffix | `util/ServerBootConfig.cs`, `config.json`, environment |
| client `src/api/ayatype.js`, `ayanova-version.js` | `src/api/socktype.js`, `sockeye-version.js` | `raven-client` ↔ `sockeye-client` |
The shared `AyaType`/`SockType` members have the same numbers (Customer 8, User 3, Report 57…),
and `AyaEvent`/`SockEvent` are identical, so stored values mean the same thing in both.
## Not renamed (same in both)
`AyContext`, `AySchema`, `AyaPickListFieldDefinition`, `AyaPickList`, `AyaField`, `AyImportData`,
`AyFileInfo`, `AyaNovaLicenseKey`; table names (`aevent`, `asearchkey`, `auser`…); routes
(`api/v{version}/…`); the folder layout (`biz`, `ControllerHelpers`, `Controllers`, `DataList`,
`generator`, `kpi`, `models`, `PickList`, `resource`, `util`).
## Only in one codebase
- **AyaNova only:** the service objects (work orders, quotes, PM, parts, inventory, units,
contracts, projects, rates, CSR, loan units, purchase orders, task groups, tax codes), their
KPIs, `License.cs` (licence check, client side), `Seeder.cs`, `ReportApiRelay.cs`,
`CoreJobLicense`, `CoreJobVersionCheck`, `CoreJobPM*`, `SuperUserDefaultPasswordRoutes`,
`CustomerContactRoutes`, and newer config keys (`AYANOVA_TRUSTED_PROXIES`,
`AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE`, `AYANOVA_REMOVE_LICENSE_FROM_DB`,
`AYANOVA_REPORT_RENDER_API_URL_OVERRIDE`). Also `AyaDayOfWeek` and `AyaUnitsOfTime` (PM).
- **Sockeye only:** licensing and sales (`KeyFactory`, `RavenKeyFactory`, `LicenseBiz`,
products, purchases, subscriptions, subscription servers and their states, trial licence
requests, vendor notifications, `GZCase`), `SockBot*` jobs, `FetchController`,
`RvfController`, `RvrController`, `AutoOrderProcessingUtil`.
## How close the shared files are (2026-10-27)
288 server files exist in both. After mapping, 156 are identical, about 55 differ by 1–10
lines, and the rest differ more, mostly because one codebase has objects the other lacks.
Low-level files that differ by more than the names, worth checking when a fix touches them:
| File | Differing lines | Main reason |
|---|---|---|
| `util/RunProgram.cs` | most | AyaNova has `RunNoShell` (backup without a shell, 8.3.0); Sockeye still has the old `Run` |
| `generator/CoreJobBackup.cs` | ~50 | same: Sockeye still passes the password in the `pg_dump` URI through a shell |
| `util/DbUtil.cs`, `util/AySchema.cs` | many | different tables; check `AdminConnectionString` and drop-and-recreate by hand |
| `DataList/DataListSqlFilterCriteriaBuilder.cs` | ~155 | filter fixes went into each separately; compare before changing either |
| `util/ServerBootConfig.cs`, `Program.cs`, `Startup.cs` | many | config keys and service wiring differ |
| `util/FileUtil.cs`, `util/StringUtil.cs`, `util/DateUtil.cs`, `generator/CoreJobNotify.cs`, `generator/CoreJobSweeper.cs`, `util/Mailer.cs` | 15–90 | partly real drift; check with the script |
Rerun the script for current numbers; this table is a snapshot.
## Carrying a fix across
1. Make and test the fix in one codebase.
2. Run `bash compare-with-raven.sh <file>` on each file touched: if the file matches apart from
names, the same change applies with the names above.
3. Record in both repos whether the fix applies (done, n/a, or deferred). In AyaNova that is
`raven/devdocs/modernization/NET10-PORT.md`.

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Compare Sockeye server source with AyaNova's (raven) after mapping AyaNova's names to
# Sockeye's (see NAME-MAP.md), so only real code differences show.
#
# bash compare-with-raven.sh one line per shared file: differing lines, raven lines, path
# bash compare-with-raven.sh util/DbUtil.cs the diff for one file (raven mapped < > sockeye)
#
# Read-only. Expects the repos side by side under one folder (C:\data\code).
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
R="$here/../../raven/server/AyaNova"
S="$here/../server"
# AyaNova name -> Sockeye name; order matters (longer names first)
norm() {
sed -E \
-e 's/AyaNovaVersion/SockeyeVersion/g' \
-e 's/AyaNova/Sockeye/g; s/ayanova/sockeye/g; s/AYANOVA_/SOCKEYE_/g' \
-e 's/public AyaType AyaType/public SockType SType/g' \
-e 's/AyaTypeId/SockTypeId/g; s/AyaType/SockType/g; s/ayaType/sockType/g; s/ayatype/socktype/g' \
-e 's/\bAType\b/SockType/g; s/\batype\b/socktype/g' \
-e 's/AyaEvent/SockEvent/g; s/ayaEvent/sockEvent/g; s/ayevent/sockevent/g' \
-e 's/AyaDaysOfWeek/SockDaysOfWeek/g' \
-e 's/\bAyId\b/SockId/g; s/\bayId\b/sockId/g; s/\bayid\b/sockid/g' \
"$1" | tr -d '\r'
}
if [ $# -gt 0 ]; then
diff <(norm "$R/$1") <(tr -d '\r' < "$S/$1") || true
exit 0
fi
(cd "$R" && find . -name '*.cs' -not -path './obj/*' -not -path './bin/*') | sort | while read -r f; do
[ -f "$S/$f" ] || continue
d=$(diff <(norm "$R/$f") <(tr -d '\r' < "$S/$f") | grep -c '^[<>]' || true)
echo "$d $(wc -l < "$R/$f") ${f#./}"
done

View File

@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Asp.Versioning;
@@ -35,6 +36,10 @@ namespace Sockeye.Api.Controllers
private readonly ApiServerState serverState;
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>
/// ctor
/// </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))
{
RecordLoginFailure(creds.Login, address);
//Make a failed pw wait
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
@@ -91,7 +102,7 @@ namespace Sockeye.Api.Controllers
string hashed = Hasher.hash(u.Salt, creds.Password);
if (hashed == u.Password)
{
LoginThrottle.RecordSuccess(creds.Login);
//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
@@ -100,6 +111,7 @@ namespace Sockeye.Api.Controllers
//Generate a temporary token to identify and verify this is the same user
u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", "");
await ct.SaveChangesAsync();
TfaFailures.TryRemove(u.Id, out _);
var UOpt = await ct.UserOptions.AsNoTracking().FirstAsync(z => z.UserId == u.Id);
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
RecordLoginFailure(creds.Login, address);
//Make a failed pw wait
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
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,7 +185,14 @@ namespace Sockeye.Api.Controllers
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
//Match to temp token that would have been set by initial credentialed login for 2fa User
//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
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");
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);
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
//User is valid and authenticated
//clear temp token
user.TempToken = string.Empty;
//clear temp token (case 4710: null, not empty, so no value is left to match)
user.TempToken = null;
await ct.SaveChangesAsync();
TfaFailures.TryRemove(user.Id, out _);
return await ReturnUserCredsOnSuccessfulAuthentication(user);
}

View File

@@ -41,6 +41,7 @@ namespace Sockeye.Api.Controllers
//////////////////////////////////////////////////////////////////////////
// GET: fetch/somecode/bob@bob.com
[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)
{

View File

@@ -46,6 +46,7 @@ namespace Sockeye.Api.Controllers
//### RAVEN FETCH LICENSE REQUEST HERE ####
//This route is used by Raven to check if there is an unfetched license key and return it
[HttpPost]
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
public async Task<IActionResult> PostLicenseFetchRequest([FromBody] dtoFetchRequest fetchRequest)
{
if (!serverState.IsOpen)

View File

@@ -60,6 +60,7 @@ namespace Sockeye.Api.Controllers
//REQUEST TRIAL ROUTE CALLED FROM RAVEN
//#########################################################
[HttpPost]
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_TRIAL_REQUESTS)]//case 4681
public async Task<IActionResult> Post([FromBody] dtoRequest r)
{
if (!serverState.IsOpen)
@@ -70,6 +71,12 @@ namespace Sockeye.Api.Controllers
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 (string.IsNullOrWhiteSpace(r.DbId))
{
@@ -114,6 +121,7 @@ namespace Sockeye.Api.Controllers
//### CUSTOMER ROUTE CALLED FROM CUSTOMER BROWSER / EMAIL ####
[HttpGet("verify/{code}")]
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
public async Task<IActionResult> GetVerify([FromRoute] string code)
{
if (!serverState.IsOpen)

View File

@@ -57,6 +57,7 @@ namespace Sockeye.Api.Controllers
//REQUEST ROUTE CALLED FROM CONTACT FORMS APP
//#########################################################
[HttpPost]
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_TRIAL_REQUESTS)]//case 4681
public async Task<IActionResult> Post([FromBody] dtoSubscriptionServerTrialRequest r)
{
if (!serverState.IsOpen)
@@ -109,6 +110,9 @@ namespace Sockeye.Api.Controllers
case "NY":
newObject.TimeZone = "America/New_York";
break;
case "ATL":
newObject.TimeZone = "America/New_York";//this is the correct time zone for Atlanta officially
break;
case "TOR":
newObject.TimeZone = "America/Toronto";
break;

View File

@@ -28,6 +28,10 @@ namespace Sockeye
{
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)
@@ -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 }));
});
@@ -375,7 +392,10 @@ namespace Sockeye
#endregion
_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");
app.UseCors("CorsPolicy");

View File

@@ -1,17 +1,8 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Trace"
}
},
"Console": {
"LogLevel": {
"Default": "Trace"
}
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
}

View File

@@ -535,9 +535,6 @@ http://www.ayanova.com
Answers to frequently asked questions:
http://www.ayanova.com/faqanswers.htm
Technical support forum:
http://forum.ayanova.com
Latest AyaNova version update information:
Downloads and help documentation:
http://www.ayanova.com/download.htm
@@ -617,9 +614,6 @@ http://www.ayanova.com
Answers to frequently asked questions:
http://www.ayanova.com/faqanswers.htm
Technical support forum:
http://forum.ayanova.com
Latest AyaNova version update information:
Downloads and help documentation:
http://www.ayanova.com/download.htm

View File

@@ -107,9 +107,6 @@ http://www.ayanova.com
Answers to frequently asked questions:
http://www.ayanova.com/faqanswers.htm
Technical support forum:
http://forum.ayanova.com
Latest AyaNova version update information:
Downloads and help documentation:
http://www.ayanova.com/download.htm
@@ -174,9 +171,6 @@ http://www.ayanova.com
Answers to frequently asked questions:
http://www.ayanova.com/faqanswers.htm
Technical support forum:
http://forum.ayanova.com
Latest AyaNova version update information:
Downloads and help documentation:
http://www.ayanova.com/download.htm

View File

@@ -4,8 +4,8 @@
</PropertyGroup>
<PropertyGroup>
<GenerateFullPaths>true</GenerateFullPaths>
<Version>8.1.0</Version>
<FileVersion>8.1.0.0</FileVersion>
<Version>8.1.1</Version>
<FileVersion>8.1.1.0</FileVersion>
<ApplicationIcon>sockeye.ico</ApplicationIcon>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<noWarn>1591</noWarn>
@@ -24,6 +24,7 @@
<PackageReference Include="Enums.NET" Version="5.0.0" />
<PackageReference Include="jose-jwt" Version="5.3.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.Mvc.NewtonsoftJson" Version="10.0.12" />
<PackageReference Include="Asp.Versioning.Mvc" Version="10.2.1" />

View 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 _);
}
}
}

View File

@@ -51,22 +51,36 @@ namespace Sockeye.Util
internal static void SetNextBackup()
{
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?
if (NextBackup < utcNow.AddHours(-24))
{
//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
if (NextBackup < utcNow)
NextBackup = NextBackup.AddDays(1);
NextBackup = BackupTimeOnLocalDay(localToday.AddDays(1));
}
else
{
//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 = NextBackup.AddDays(1);
NextBackup = BackupTimeOnLocalDay(localToday.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
}//eons

View File

@@ -5,7 +5,7 @@ namespace Sockeye.Util
/// </summary>
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 CurrentApiVersion="v8";
}//eoc

View File

@@ -178,11 +178,11 @@ namespace Sockeye.Util
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
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++)
{
stringChars[i] = chars[random.Next(chars.Length)];
stringChars[i] = chars[System.Security.Cryptography.RandomNumberGenerator.GetInt32(chars.Length)];
}
var finalString = new String(stringChars);

View File

@@ -1,11 +1,16 @@
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
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