302 lines
No EOL
13 KiB
C#
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 _);
|
|
}
|
|
}
|
|
} |