LarpixServer/LarpixServer/Program.cs
olcxja 6dc145a2c8
All checks were successful
Server Build / publish (push) Successful in 29s
Voice Build / publish (push) Successful in 25s
massive security update
2026-08-24 19:22:38 +02:00

302 lines
No EOL
13 KiB
C#

using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using LarpixServer.Account;
using LarpixServer.Filesystem;
using LarpixServer.Utils.Jsons;
namespace LarpixServer;
using static LarpixServer.Utils.Utils;
public class Program
{
public static async Task Main(string[] args)
{
await LoadEnv();
// Voice ticket sweeper: issued tickets live for 60s; remove expired
// ones a minute later so nothing lingers in memory. Also prunes stale
// per-IP auth rate-limit counters (they are written pre-HMAC-check,
// so without pruning any scanner could grow the map forever).
_ = Task.Run(async () =>
{
while (true)
{
await Task.Delay(30000);
long cutoff = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - 120;
foreach (var entry in VoiceTickets)
{
if (entry.Value.Exp < cutoff) VoiceTickets.TryRemove(entry.Key, out _);
}
long nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
foreach (var entry in VoiceAuthLimits)
{
if (entry.Value.resetAt <= nowSec) VoiceAuthLimits.TryRemove(entry.Key, out _);
}
//prune pozostałych rate limitów (okna 60s)
PruneRateLimits(AuthRateLimits, nowSec);
PruneRateLimits(NonceRateLimits, nowSec);
PruneRateLimits(KdfSaltRateLimits, nowSec);
PruneRateLimits(AccountInitRateLimits, nowSec);
LarpixServer.Account.Requests.PruneActionRateLimits(nowSec);
}
});
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<BackgroundDelete>();
builder.Logging.ClearProviders();
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.AddServerHeader = false;
serverOptions.ListenAnyIP(PORT);
serverOptions.Limits.MaxRequestBodySize = MAX_BODY_SIZE;
});
var app = builder.Build();
string stagingDir = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms_staging";
if (Fs.Exists(stagingDir))
{
foreach (var fileName in Fs.ReadDirectory(stagingDir))
{
var file = Path.Combine(stagingDir, fileName);
var fileInfo = new FileInfo(file);
var age = DateTime.UtcNow - fileInfo.LastWriteTimeUtc;
var remaining = TimeSpan.FromMinutes(10) - age;
if (remaining <= TimeSpan.Zero)
{
Fs.DeleteFile(file);
}
else
{
_ = Task.Run(async () =>
{
await Task.Delay(remaining);
if (Fs.Exists(file)) Fs.DeleteFile(file);
});
}
}
}
app.UseWebSockets();
app.Use(async (HttpContext context, Func<Task> next) =>
{
try
{
context.Response.ContentType = mimeTypes["txt"];
context.Response.StatusCode = 200;
var path = context.Request.Path.Value;
var url = $"{context.Request.Scheme}://{context.Request.Host}";
if (path == "" || path.Length > 512)
{
context.Response.Redirect(url + "/");
return;
}
int index = path.LastIndexOf('.');
string fileExtension = index >= 0 ? path[(index + 1)..] : "";
IQueryCollection query = context.Request.Query;
using StreamReader reader = new StreamReader(context.Request.Body);
//custom header moment
context.Response.Headers["Access-Control-Allow-Origin"] = "*";
context.Response.Headers["Access-Control-Allow-Headers"] = "*";
context.Response.Headers["Access-Control-Allow-Methods"] = "*";
if (context.Request.Method.ToUpper() == "OPTIONS")
{
context.Response.StatusCode = 200;
await context.Response.CompleteAsync();
return;
}
switch (path)
{
case "/_larpix/ws":
if (context.WebSockets.IsWebSocketRequest)
{
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await WsHandler(context, webSocket, query);
}
else
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
}
return;
case "/_larpix/voice/auth":
await LarpixServer.Room.Shared.InternalVoiceAuth(context);
return;
case "/_larpix/serverinfo":
ServerInfo serverInfo = new ServerInfo();
serverInfo.domain = DOMAIN;
serverInfo.owner_contact = OWNER_CONTACT;
string registrationString = Encoding.UTF8.GetString(await Fs.ReadFile($"{ACCOUNTS_DIR}/registration"));
if (registrationString.StartsWith("first;"))
{
serverInfo.registration = "enabled";
}
if (registrationString.StartsWith("1;"))
{
serverInfo.registration = "enabled";
}
if (registrationString.StartsWith("0;"))
{
serverInfo.registration = "disabled";
}
if (registrationString.StartsWith("code;"))
{
serverInfo.registration = "code";
}
var serializedPayload = JsonSerializer.Serialize(
serverInfo,
AppJsonSerializerContext.Default.ServerInfo
);
context.Response.ContentType = mimeTypes["json"];
await context.Response.WriteAsync(serializedPayload);
return;
}
//if (!context.Request.Headers.TryGetValue("username", out var username)) how to header
switch (path)
{
case "/_larpix/restart":
if (!query.TryGetValue("skey", out var skey))
{
context.Response.ContentType = mimeTypes["html"];
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
if (skey != SERVER_KEY)
{
context.Response.ContentType = mimeTypes["html"];
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
if (!query.TryGetValue("f", out var force))
{
force = "false";
}
if (force == "true")
{
await context.Response.WriteAsync("[FORCE] Shutting down...");
Console.WriteLine("[FORCE] Shutting down...");
Environment.Exit(0);
return;
}
await context.Response.WriteAsync("[SAFE] Shutting down...");
Console.WriteLine("[SAFE] Shutting down...");
var lifetime = context.RequestServices.GetRequiredService<IHostApplicationLifetime>();
lifetime.StopApplication();
return;
case "/_larpix/clearcache": //kiedys bedzie endpoint cache
if (!query.TryGetValue("skey", out skey))
{
context.Response.ContentType = mimeTypes["html"];
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
if (skey != SERVER_KEY)
{
context.Response.ContentType = mimeTypes["html"];
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
if (!query.TryGetValue("p", out var p))
{
p = "";
}
ulong removed = Fs.ClearCache(p);
await context.Response.WriteAsync($"Cache cleared with pattern: {p}. {removed} elements removed");
return;
case "/_larpix/createaccount":
await Account.Requests.Create(context, next, query, reader);
return;
case "/_larpix/deleteaccount":
await Account.Requests.Delete(context, next, query, reader);
return;
case "/_larpix/auth":
await Account.Requests.Auth(context, next, query, reader);
return;
case "/_larpix/chpass":
await Account.Requests.ChangePassword(context, next, query, reader);
return;
case "/_larpix/chname":
await Account.Requests.ChangeUsername(context, next, query, reader);
return;
case "/_larpix/correctedname":
await Account.Requests.CorrectedName(context, next, query);
return;
case "/_larpix/nametoid":
await Account.Requests.NameToId(context, next, query);
return;
case "/_larpix/idtoname":
await Account.Requests.IdToName(context, next, query);
return;
case "/_larpix/nextnonce":
await Account.Requests.NextNonce(context, next, query);
return;
case "/_larpix/encryptedrequest":
await Account.Requests.EncryptedRequest(context, next, query, reader);
return;
case "/_larpix/upload":
await Account.Requests.Upload(context, next, query);
return;
case "/_larpix/dm/attachment/staging":
await Account.Requests.UploadDmAttachmentStaging(context, next, query);
return;
case "/_larpix/dm/attachment/get":
await Account.Requests.GetDmAttachment(context, next, query);
return;
case "/_larpix/group/attachment/get":
await Account.Requests.GetGroupAttachment(context, next, query);
return;
//case "/_larpix/federation": //federation wylaczona (brak uwierzytelnienia miedzyserwerowego)
// await Federation.Receiver.FederationRequest(context, next, query, reader);
// return;
case "/_larpix/user/key/getpublic":
await Account.Requests.GetUserPublicKey(context, next, query, reader);
return;
case "/_larpix/user/storage/public/getentry":
await Account.Requests.GetUserPublicStorageEntry(context, next, query, reader);
return;
case "/_larpix/group/storage/public/getentry":
await Account.Requests.GetGroupPublicStorageEntry(context, next, query, reader);
return;
case "/_larpix/kdfsalt":
await Account.Requests.GetKdfSalt(context, query);
return;
}
}
catch (Exception ex)
{
if (ex.Message.StartsWith("error:"))
await context.Response.WriteAsync(ex.Message);
else
await context.Response.WriteAsync($"error:unknown.error");
Console.WriteLine(ex);
}
});
Console.WriteLine("Starting server at port: " + PORT);
app.Run();
}
private static void PruneRateLimits(ConcurrentDictionary<string, (int count, long resetAt)> store, long nowSec)
{
foreach (var entry in store)
{
if (entry.Value.resetAt <= nowSec) store.TryRemove(entry.Key, out _);
}
}
}