First commit
This commit is contained in:
commit
0ac6ff9196
26 changed files with 2836 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.idea/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
Build/
|
||||||
|
projectSettingsUpdater.xml
|
||||||
|
indexLayout.xml
|
||||||
|
discord.xml
|
||||||
|
encodings.xml
|
||||||
|
*.DotSettings.user
|
||||||
22
Larpix.sln
Normal file
22
Larpix.sln
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LarpixServer", "LarpixServer\LarpixServer.csproj", "{EB27B476-5689-4CF5-9ED2-5A006C4D3CFB}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LarpixVoice", "LarpixVoice\LarpixVoice.csproj", "{CC8E6434-17CB-4743-8D59-E85531114C1E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{EB27B476-5689-4CF5-9ED2-5A006C4D3CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EB27B476-5689-4CF5-9ED2-5A006C4D3CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EB27B476-5689-4CF5-9ED2-5A006C4D3CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EB27B476-5689-4CF5-9ED2-5A006C4D3CFB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CC8E6434-17CB-4743-8D59-E85531114C1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CC8E6434-17CB-4743-8D59-E85531114C1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CC8E6434-17CB-4743-8D59-E85531114C1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CC8E6434-17CB-4743-8D59-E85531114C1E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
71
LarpixServer/Account/BackgroundDelete.cs
Normal file
71
LarpixServer/Account/BackgroundDelete.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System.Text;
|
||||||
|
using LarpixServer.Filesystem;
|
||||||
|
|
||||||
|
namespace LarpixServer.Account;
|
||||||
|
|
||||||
|
public class BackgroundDelete : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Running Background Delete");
|
||||||
|
await ScanAndDelete(stoppingToken);
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromDays(0.5), stoppingToken);
|
||||||
|
}
|
||||||
|
Console.WriteLine("Stopped Background Delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScanAndDelete(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var account in Directory.EnumerateDirectories(LarpixServer.Utils.Utils.ACCOUNTS_DATA_DIR, "*",
|
||||||
|
SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
if (stoppingToken.IsCancellationRequested) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string lpath = Path.Combine(account, "lastlogin");
|
||||||
|
if (!Fs.Exists(lpath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DateTimeOffset lastLogin =
|
||||||
|
DateTimeOffset.FromUnixTimeSeconds(
|
||||||
|
long.Parse(Encoding.UTF8.GetString(await Fs.ReadFile(lpath))));
|
||||||
|
if (lastLogin < DateTimeOffset.UtcNow.AddMonths(-12))
|
||||||
|
{
|
||||||
|
string id = Path.GetFileName(account);
|
||||||
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
await Requests.createLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string username = await Account.Utils.NameFromId(id);
|
||||||
|
Fs.DeleteFile($"{LarpixServer.Utils.Utils.ACCOUNTS_NAME_DIR}/{username.ToLower()}");
|
||||||
|
Fs.DeleteDirectory(account);
|
||||||
|
Fs.WriteFile($"{LarpixServer.Utils.Utils.ACCOUNTS_FREEID_DIR}/{id}", []);
|
||||||
|
Console.WriteLine($"Deleted {id} ({username})");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Requests.createLock.Release();
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
await Task.Delay(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
LarpixServer/Account/Captcha.cs
Normal file
167
LarpixServer/Account/Captcha.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
using SkiaSharp;
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace LarpixServer.Account;
|
||||||
|
|
||||||
|
public class Captcha
|
||||||
|
{
|
||||||
|
private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
private static readonly SKTypeface CaptchaTypeface = SKTypeface.FromFamilyName("Google Sans Code", SKFontStyle.Bold);
|
||||||
|
|
||||||
|
public static (byte[] ImageBytes, string CaptchaText) GenerateCaptcha()
|
||||||
|
{
|
||||||
|
int width = 1120;
|
||||||
|
int height = 320;
|
||||||
|
int charCount = 8;
|
||||||
|
|
||||||
|
Span<char> captchaSpan = stackalloc char[charCount];
|
||||||
|
Span<SKColor> charColors = stackalloc SKColor[charCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < charCount; i++)
|
||||||
|
{
|
||||||
|
captchaSpan[i] = Chars[Random.Shared.Next(Chars.Length)];
|
||||||
|
charColors[i] = new SKColor(
|
||||||
|
(byte)Random.Shared.Next(100, 256),
|
||||||
|
(byte)Random.Shared.Next(100, 256),
|
||||||
|
(byte)Random.Shared.Next(100, 256)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
string captchaText = new string(captchaSpan);
|
||||||
|
|
||||||
|
var imageInfo = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
|
||||||
|
using var sourceBitmap = new SKBitmap(imageInfo);
|
||||||
|
using var canvas = new SKCanvas(sourceBitmap);
|
||||||
|
canvas.Clear(SKColors.Transparent);
|
||||||
|
|
||||||
|
using var dotPaint = new SKPaint { IsAntialias = true };
|
||||||
|
float spacing = 12f;
|
||||||
|
|
||||||
|
Span<float> randOffsets = stackalloc float[256];
|
||||||
|
Span<float> randRadii = stackalloc float[256];
|
||||||
|
Span<SKColor> randColors = stackalloc SKColor[256];
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++)
|
||||||
|
{
|
||||||
|
randOffsets[i] = (float)(Random.Shared.NextDouble() - 0.5) * 8f;
|
||||||
|
randRadii[i] = (float)Random.Shared.NextDouble() * 3.5f + 1f;
|
||||||
|
randColors[i] = new SKColor(
|
||||||
|
(byte)Random.Shared.Next(120, 200),
|
||||||
|
(byte)Random.Shared.Next(120, 200),
|
||||||
|
(byte)Random.Shared.Next(120, 200)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int noiseIdx = 0;
|
||||||
|
for (float y = 0; y < height; y += spacing)
|
||||||
|
{
|
||||||
|
for (float x = 0; x < width; x += spacing)
|
||||||
|
{
|
||||||
|
noiseIdx = (noiseIdx + 1) & 255;
|
||||||
|
dotPaint.Color = randColors[noiseIdx];
|
||||||
|
canvas.DrawCircle(x + randOffsets[noiseIdx], y + randOffsets[255 - noiseIdx], randRadii[noiseIdx], dotPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var textPaint = new SKPaint
|
||||||
|
{
|
||||||
|
IsAntialias = true,
|
||||||
|
TextSize = 158,
|
||||||
|
Typeface = CaptchaTypeface
|
||||||
|
};
|
||||||
|
|
||||||
|
float cellWidth = width / (float)charCount;
|
||||||
|
|
||||||
|
for (int i = 0; i < charCount; i++)
|
||||||
|
{
|
||||||
|
string charStr = captchaText[i].ToString();
|
||||||
|
SKRect bounds = new SKRect();
|
||||||
|
textPaint.MeasureText(charStr, ref bounds);
|
||||||
|
textPaint.Color = charColors[i];
|
||||||
|
|
||||||
|
canvas.Save();
|
||||||
|
|
||||||
|
float maxRadius = (float)Math.Sqrt((bounds.Width * bounds.Width) + (bounds.Height * bounds.Height)) / 2f;
|
||||||
|
float margin = 5f;
|
||||||
|
|
||||||
|
float minX = (cellWidth * i) + maxRadius + margin;
|
||||||
|
float maxX = (cellWidth * (i + 1)) - maxRadius - margin;
|
||||||
|
float xCenter = (minX >= maxX) ? (cellWidth * i) + (cellWidth / 2f) : minX + (float)Random.Shared.NextDouble() * (maxX - minX);
|
||||||
|
|
||||||
|
float minY = maxRadius + margin;
|
||||||
|
float maxY = height - maxRadius - margin;
|
||||||
|
float yCenter = (minY >= maxY) ? height / 2f : minY + (float)Random.Shared.NextDouble() * (maxY - minY);
|
||||||
|
|
||||||
|
canvas.Translate(xCenter, yCenter);
|
||||||
|
canvas.RotateDegrees(Random.Shared.Next(-45, 46));
|
||||||
|
canvas.DrawText(charStr, -bounds.MidX, -bounds.MidY, textPaint);
|
||||||
|
canvas.Restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var linePaint = new SKPaint { StrokeWidth = 8, IsAntialias = true };
|
||||||
|
|
||||||
|
for (int i = 0; i < charCount; i++)
|
||||||
|
{
|
||||||
|
linePaint.Color = charColors[i];
|
||||||
|
float cellStartX = cellWidth * i;
|
||||||
|
float x1 = cellStartX + (float)Random.Shared.NextDouble() * cellWidth;
|
||||||
|
float x2 = cellStartX + (float)Random.Shared.NextDouble() * cellWidth;
|
||||||
|
canvas.DrawLine(x1, 0, x2, height, linePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
float bandHeight = height / 4f;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
linePaint.Color = charColors[Random.Shared.Next(charColors.Length)];
|
||||||
|
float bandStartY = bandHeight * i;
|
||||||
|
float y1 = bandStartY + (float)Random.Shared.NextDouble() * bandHeight;
|
||||||
|
float y2 = bandStartY + (float)Random.Shared.NextDouble() * bandHeight;
|
||||||
|
canvas.DrawLine(0, y1, width, y2, linePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.Flush();
|
||||||
|
|
||||||
|
float amplitude = 10.5f;
|
||||||
|
float frequency = 0.03f;
|
||||||
|
|
||||||
|
Span<int> sinY = stackalloc int[height];
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
sinY[y] = (int)(Math.Sin(y * frequency) * amplitude);
|
||||||
|
|
||||||
|
Span<int> cosX = stackalloc int[width];
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
cosX[x] = (int)(Math.Cos(x * frequency) * amplitude);
|
||||||
|
|
||||||
|
using var warpedBitmap = new SKBitmap(imageInfo);
|
||||||
|
|
||||||
|
Span<uint> srcPixels = MemoryMarshal.Cast<byte, uint>(sourceBitmap.GetPixelSpan());
|
||||||
|
Span<uint> dstPixels = MemoryMarshal.Cast<byte, uint>(warpedBitmap.GetPixelSpan());
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
int offsetX = sinY[y];
|
||||||
|
int yOffsetBase = y * width;
|
||||||
|
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
int srcX = x + offsetX;
|
||||||
|
int srcY = y + cosX[x];
|
||||||
|
|
||||||
|
if ((uint)srcX < (uint)width && (uint)srcY < (uint)height)
|
||||||
|
{
|
||||||
|
dstPixels[yOffsetBase + x] = srcPixels[srcY * width + srcX];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dstPixels[yOffsetBase + x] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var warpedImage = SKImage.FromBitmap(warpedBitmap);
|
||||||
|
|
||||||
|
using var data = warpedImage.Encode(SKEncodedImageFormat.Webp, 90);
|
||||||
|
|
||||||
|
return (data.ToArray(), captchaText);
|
||||||
|
}
|
||||||
|
}
|
||||||
591
LarpixServer/Account/Requests.cs
Normal file
591
LarpixServer/Account/Requests.cs
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using LarpixServer.Filesystem;
|
||||||
|
using LarpixServer.Utils;
|
||||||
|
using LarpixServer.Utils.Jsons;
|
||||||
|
using static LarpixServer.Utils.Utils;
|
||||||
|
|
||||||
|
namespace LarpixServer.Account;
|
||||||
|
|
||||||
|
public class Requests
|
||||||
|
{
|
||||||
|
public static ConcurrentDictionary<string, CreateHolder> createHolder = new();
|
||||||
|
public static ConcurrentDictionary<string, (string, DateTimeOffset)> nonceHolder = new();
|
||||||
|
|
||||||
|
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task Delete(HttpContext context, Func<Task> next, IQueryCollection query,
|
||||||
|
StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string body = await LoadBody(bodyReader);
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
string password = await Utils.GetPassword(id);
|
||||||
|
body = await Utils.NonceDecryptBody(username, password, body);
|
||||||
|
string auth = await Utils.Auth(id, password, body);
|
||||||
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(auth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin",
|
||||||
|
Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.AddMonths(-11).ToUnixTimeSeconds().ToString()));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
"ok|Your account will be deleted in one month|Please make sure you are logged out on all devices. Logging back into your account during this period will stop the deletion process");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task Create(HttpContext context, Func<Task> next, IQueryCollection query,
|
||||||
|
StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("step", out var step))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (step)
|
||||||
|
{
|
||||||
|
case "init":
|
||||||
|
foreach (var kvp in createHolder) // czyszczenie nieaktywnych od 2 minut requestow
|
||||||
|
{
|
||||||
|
if (kvp.Value.date < DateTimeOffset.UtcNow.AddMinutes(-2))
|
||||||
|
{
|
||||||
|
createHolder.TryRemove(kvp.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.ContentType = mimeTypes["json"];
|
||||||
|
|
||||||
|
(BigInteger p, BigInteger g, BigInteger pubServer, BigInteger secretServer) serverInfo =
|
||||||
|
Encryption.Encryption.Init();
|
||||||
|
KeyExchangePayload payload = new KeyExchangePayload();
|
||||||
|
payload.p = serverInfo.p.ToString();
|
||||||
|
payload.g = serverInfo.g.ToString();
|
||||||
|
payload.pubServer = serverInfo.pubServer.ToString();
|
||||||
|
payload.idKey = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + "\n";
|
||||||
|
|
||||||
|
while (createHolder.ContainsKey(payload.idKey))
|
||||||
|
{
|
||||||
|
payload.idKey += Random.Shared.Next(0, 10).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateHolder dataHolder = new();
|
||||||
|
dataHolder.date = DateTimeOffset.UtcNow;
|
||||||
|
dataHolder.serverInfo = serverInfo;
|
||||||
|
|
||||||
|
createHolder.TryAdd(payload.idKey, dataHolder);
|
||||||
|
|
||||||
|
var serializedPayload = JsonSerializer.Serialize(
|
||||||
|
payload,
|
||||||
|
AppJsonSerializerContext.Default.KeyExchangePayload
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(serializedPayload);
|
||||||
|
return;
|
||||||
|
case "register":
|
||||||
|
string body = await LoadBody(bodyReader);
|
||||||
|
|
||||||
|
KeyExchangePayloadClient serializedBody = JsonSerializer.Deserialize(
|
||||||
|
body,
|
||||||
|
AppJsonSerializerContext.Default.KeyExchangePayloadClient
|
||||||
|
);
|
||||||
|
if (!createHolder.TryGetValue(serializedBody.idKey, out CreateHolder entry))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Account request expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.date = DateTimeOffset.UtcNow;
|
||||||
|
entry.pubClient = BigInteger.Parse(serializedBody.pubClient);
|
||||||
|
byte[] sharedKey = Encryption.Encryption.CalcCommunicationKey(entry.pubClient,
|
||||||
|
entry.serverInfo.secretServer, entry.serverInfo.p);
|
||||||
|
|
||||||
|
|
||||||
|
entry.name = Encryption.Encryption.Decrypt(serializedBody.username, sharedKey);
|
||||||
|
entry.pass = Encryption.Encryption.Decrypt(serializedBody.password, sharedKey);
|
||||||
|
|
||||||
|
if (!Utils.IsValidUsername(entry.name, out string message))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Username: " + message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils.IsValidPassword(entry.pass, out message))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Password: " + message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(byte[] ImageBytes, string CaptchaText) captchaResult = Captcha.GenerateCaptcha();
|
||||||
|
entry.captcha = captchaResult.CaptchaText;
|
||||||
|
context.Response.ContentType = mimeTypes["webp"];
|
||||||
|
context.Response.ContentLength = captchaResult.ImageBytes.Length;
|
||||||
|
|
||||||
|
await context.Response.Body.WriteAsync(captchaResult.ImageBytes, 0,
|
||||||
|
captchaResult.ImageBytes.Length);
|
||||||
|
return;
|
||||||
|
case "finish":
|
||||||
|
body = await LoadBody(bodyReader);
|
||||||
|
|
||||||
|
CaptchaPayloadClient serialized = JsonSerializer.Deserialize(
|
||||||
|
body,
|
||||||
|
AppJsonSerializerContext.Default.CaptchaPayloadClient
|
||||||
|
);
|
||||||
|
if (!createHolder.TryGetValue(serialized.idKey, out entry))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Account request expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.captcha.ToLower() != serialized.captcha.ToLower())
|
||||||
|
{
|
||||||
|
createHolder.TryRemove(serialized.idKey, out _);
|
||||||
|
await context.Response.WriteAsync("Incorrect captcha. Please try again");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string lowerName = entry.name.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (Fs.Exists($"{ACCOUNTS_NAME_DIR}/{lowerName}"))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("This username is already taken");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string registrationString =
|
||||||
|
Encoding.UTF8.GetString(await Fs.ReadFile($"{ACCOUNTS_DIR}/registration"));
|
||||||
|
if (registrationString.StartsWith("0;"))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Account creation is currently disabled. Try again later");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registrationString.StartsWith("code;"))
|
||||||
|
{
|
||||||
|
bool valid = false;
|
||||||
|
string[] regArray = registrationString.Split(';');
|
||||||
|
foreach (string regEntryString in regArray)
|
||||||
|
{
|
||||||
|
string[] regEntry = regEntryString.Split(":");
|
||||||
|
if (regEntry[0] == "|" + serialized.regKey)
|
||||||
|
{
|
||||||
|
if (regEntry[1] != "infinite")
|
||||||
|
{
|
||||||
|
int left = int.Parse(regEntry[1]) - 1;
|
||||||
|
if (left < 1)
|
||||||
|
{
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DIR}/registration",
|
||||||
|
Encoding.UTF8.GetBytes(
|
||||||
|
registrationString.Replace(regEntryString + ";", "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
"Account creation is currently disabled. Try again later");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (registrationString.StartsWith("first;"))
|
||||||
|
{
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DIR}/registration", Encoding.UTF8.GetBytes("0;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ulong id = ulong.Parse(await Fs.ReadFile($"{ACCOUNTS_DIR}/last"));
|
||||||
|
id++;
|
||||||
|
var freeid = Path.GetFileName(Directory.EnumerateFiles(ACCOUNTS_FREEID_DIR).FirstOrDefault());
|
||||||
|
if (freeid != null)
|
||||||
|
{
|
||||||
|
id = ulong.Parse(freeid);
|
||||||
|
Fs.DeleteFile($"{ACCOUNTS_FREEID_DIR}/{freeid}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (id == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
"Server is full, cannot create new accounts. Try again later.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DIR}/last", Encoding.UTF8.GetBytes(id.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreSlim userLock = Utils.GetUserLock(id.ToString());
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_NAME_DIR}/{lowerName}", Encoding.UTF8.GetBytes(id.ToString()));
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id.ToString()}/username",
|
||||||
|
Encoding.UTF8.GetBytes(entry.name));
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id.ToString()}/secret",
|
||||||
|
Encoding.UTF8.GetBytes(entry.pass));
|
||||||
|
createHolder.TryRemove(serialized.idKey, out _);
|
||||||
|
await Utils.UpdateLastLogin(id.ToString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("Account created");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
createLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
string password = await Utils.GetPassword(id);
|
||||||
|
secret = await Utils.NonceDecryptBody(username, password, secret);
|
||||||
|
string auth = await Utils.Auth(id, password, secret);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(auth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CorrectedName(HttpContext context, Func<Task> next, IQueryCollection query)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils.IsUserLocal(username, out string domain)) //federation :(
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
username = username.ToString().Split(":")[0];
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(await Utils.NameFromId(await Utils.IdFromName(username)));
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task NextNonce(HttpContext context, Func<Task> next, IQueryCollection query)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string plainPass = await Utils.GetPassword(await Utils.IdFromName(username));
|
||||||
|
foreach (var kvp in nonceHolder) //clearowanie nieuzytych nonce
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
if (kvp.Key == username)
|
||||||
|
{
|
||||||
|
nonceHolder.TryRemove(kvp.Key, out _);
|
||||||
|
}
|
||||||
|
*/ //tak teraz sobie mysle moze jednak nie usuwac nonce?? bo co jak jakis cep beedzie ciagle komus spamic i
|
||||||
|
//bedzie mial nizszy ping i gosc nie bedzie mogl zadnego req wyslac, a nawet jak ktos bez secret to odczyta
|
||||||
|
//to nic z tym nie zrobi
|
||||||
|
if (kvp.Value.Item2 < DateTimeOffset.UtcNow.AddMinutes(-2))
|
||||||
|
{
|
||||||
|
nonceHolder.TryRemove(kvp.Key, out _);
|
||||||
|
}
|
||||||
|
else if (kvp.Key == username)
|
||||||
|
{
|
||||||
|
if (nonceHolder.TryGetValue(kvp.Key, out (string, DateTimeOffset) cachedNonce))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(cachedNonce.Item1,
|
||||||
|
plainPass));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string nonce = Encryption.Encryption.GetRandomString(64);
|
||||||
|
nonceHolder.TryAdd(username, (nonce, DateTimeOffset.UtcNow));
|
||||||
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(nonce,
|
||||||
|
plainPass));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task ChangePassword(HttpContext context, Func<Task> next, IQueryCollection query,
|
||||||
|
StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
string body = await LoadBody(bodyReader);
|
||||||
|
string password = await Utils.GetPassword(id);
|
||||||
|
string newPass = await Utils.NonceDecryptBody(username, password, body);
|
||||||
|
secret = await Utils.NonceDecryptBody(username, password, secret);
|
||||||
|
|
||||||
|
string auth = await Utils.Auth(id, password, secret);
|
||||||
|
|
||||||
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(auth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Utils.IsValidPassword(newPass, out string message))
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Utils.SetPassword(id, newPass);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("Password changed successfully");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task ChangeUsername(HttpContext context, Func<Task> next, IQueryCollection query,
|
||||||
|
StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
string body = await LoadBody(bodyReader);
|
||||||
|
string password = await Utils.GetPassword(id);
|
||||||
|
body = await Utils.NonceDecryptBody(username, password, body);
|
||||||
|
secret = await Utils.NonceDecryptBody(username, password, secret);
|
||||||
|
|
||||||
|
string auth = await Utils.Auth(id, secret, password);
|
||||||
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(auth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreSlim userLock = Utils.GetUserLock(id.ToString());
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(await Utils.SetUsername(id, body));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> UpdateUserKeys(string id, string body)
|
||||||
|
{
|
||||||
|
Universal2String serializedBody = JsonSerializer.Deserialize(
|
||||||
|
body,
|
||||||
|
AppJsonSerializerContext.Default.Universal2String
|
||||||
|
);
|
||||||
|
//string publicKey = serializedBody.string1;
|
||||||
|
//string privateEncryptedKey = serializedBody.string2;
|
||||||
|
await Utils.UpdateUserKeys(id, body);
|
||||||
|
return "Keys updated successfully";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task GetUserPublicKey(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils.IsUserLocal(username, out string domain)) //federation :(
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
username = username.ToString().Split(":")[0];
|
||||||
|
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
|
||||||
|
Universal2String keys = JsonSerializer.Deserialize(
|
||||||
|
await Utils.GetUserKeys(id),
|
||||||
|
AppJsonSerializerContext.Default.Universal2String
|
||||||
|
);
|
||||||
|
await context.Response.WriteAsync(keys.string2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task GetUserPublicStorageEntry(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!query.TryGetValue("entry", out var entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Utils.IsUserLocal(username, out string domain)) //federation :(
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
username = username.ToString().Split(":")[0];
|
||||||
|
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
|
||||||
|
byte[] entryByte = await Utils.GetUserPublicStorageEntry(id, entry);
|
||||||
|
|
||||||
|
context.Response.ContentType = mimeTypes["unknown"];
|
||||||
|
context.Response.ContentLength = entryByte.Length;
|
||||||
|
|
||||||
|
await context.Response.Body.WriteAsync(entryByte, 0,
|
||||||
|
entryByte.Length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task EncryptedRequest(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
|
||||||
|
{
|
||||||
|
if (!query.TryGetValue("u", out var username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = await Utils.IdFromName(username);
|
||||||
|
string body = await LoadBody(bodyReader);
|
||||||
|
string password = await Utils.GetPassword(id);
|
||||||
|
body = await Utils.NonceDecryptBody(username, password, body);
|
||||||
|
secret = await Utils.NonceDecryptBody(username, password, secret);
|
||||||
|
|
||||||
|
Universal2String serializedBody = JsonSerializer.Deserialize(
|
||||||
|
body,
|
||||||
|
AppJsonSerializerContext.Default.Universal2String
|
||||||
|
);
|
||||||
|
|
||||||
|
string auth = await Utils.Auth(id, secret, password);
|
||||||
|
|
||||||
|
if (auth != Utils.LOGIN_SUCCESS)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secret));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (serializedBody.string1)
|
||||||
|
{
|
||||||
|
case "user/key/update":
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
Encryption.Encryption.EncryptString(
|
||||||
|
await UpdateUserKeys(id, serializedBody.string2)
|
||||||
|
, password));
|
||||||
|
break;
|
||||||
|
case "user/key/get":
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
Encryption.Encryption.EncryptString(
|
||||||
|
await Utils.GetUserKeys(id)
|
||||||
|
, password));
|
||||||
|
break;
|
||||||
|
case "user/dm/list":
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
Encryption.Encryption.EncryptString(
|
||||||
|
await Utils.GetUserDms(id)
|
||||||
|
, password));
|
||||||
|
break;
|
||||||
|
case "user/dm/invite":
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
Encryption.Encryption.EncryptString(
|
||||||
|
await Room.Requests.DmInvite(id, serializedBody.string2)
|
||||||
|
, password));
|
||||||
|
break;
|
||||||
|
case "user/dm/create":
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
Encryption.Encryption.EncryptString(
|
||||||
|
await Room.Requests.DmCreate(id, serializedBody.string2)
|
||||||
|
, password));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateHolder
|
||||||
|
{
|
||||||
|
public DateTimeOffset date;
|
||||||
|
public string name;
|
||||||
|
public string pass;
|
||||||
|
public string captcha;
|
||||||
|
public (BigInteger p, BigInteger g, BigInteger pubServer, BigInteger secretServer) serverInfo;
|
||||||
|
public BigInteger pubClient;
|
||||||
|
}
|
||||||
260
LarpixServer/Account/Utils.cs
Normal file
260
LarpixServer/Account/Utils.cs
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using LarpixServer.Filesystem;
|
||||||
|
|
||||||
|
using static LarpixServer.Utils.Utils;
|
||||||
|
|
||||||
|
namespace LarpixServer.Account;
|
||||||
|
|
||||||
|
public class Utils
|
||||||
|
{
|
||||||
|
private static readonly Regex USERNAME_REGEX = new Regex(@"^[a-zA-Z0-9_]+$");
|
||||||
|
|
||||||
|
public static string LOGIN_SUCCESS = "Login successful";
|
||||||
|
|
||||||
|
public static ConcurrentDictionary<string, SemaphoreSlim> userLocks = new();
|
||||||
|
public static ConcurrentQueue<string> keyQueue = new();
|
||||||
|
|
||||||
|
public static SemaphoreSlim GetUserLock(string id)
|
||||||
|
{
|
||||||
|
while (userLocks.Count >= LOCK_SIZE)
|
||||||
|
{
|
||||||
|
if (!keyQueue.TryPeek(out var firstKey)) break;
|
||||||
|
|
||||||
|
var sem = userLocks[firstKey];
|
||||||
|
if (sem.Wait(0))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (userLocks.TryRemove(firstKey, out _))
|
||||||
|
keyQueue.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
finally { sem.Release(); }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var semLock = userLocks.GetOrAdd(id, _ => new SemaphoreSlim(1, 1));
|
||||||
|
keyQueue.Enqueue(id);
|
||||||
|
return semLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsUserLocal(string usernameWD, out string domain)
|
||||||
|
{
|
||||||
|
if (usernameWD.EndsWith(":" + DOMAIN))
|
||||||
|
{
|
||||||
|
domain = DOMAIN;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
domain = usernameWD.Split(':', 2)[1];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidUsername(string username, out string message)
|
||||||
|
{
|
||||||
|
message = "Username must be 3-18 characters long";
|
||||||
|
if (string.IsNullOrWhiteSpace(username)) return false;
|
||||||
|
if (username.Length is < 3 or > 18) return false;
|
||||||
|
message = "Only letters, numbers, and underscores allowed";
|
||||||
|
return USERNAME_REGEX.IsMatch(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidPassword(string password, out string message)
|
||||||
|
{
|
||||||
|
message = "Password is not hashed properly";
|
||||||
|
if (password.Length != 128)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!IsValidUtf8(password))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidUtf8(string password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(password)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var encoding = Encoding.GetEncoding("utf-8",
|
||||||
|
new EncoderExceptionFallback(),
|
||||||
|
new DecoderExceptionFallback());
|
||||||
|
|
||||||
|
byte[] utf8Bytes = encoding.GetBytes(password);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (EncoderFallbackException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> IdFromName(string name)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_NAME_DIR}/{name.ToLowerInvariant()}";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
string id = Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> NameFromId(string id)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/username";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> Auth(string id, string password, string password2)
|
||||||
|
{
|
||||||
|
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}"))
|
||||||
|
{
|
||||||
|
return "Invalid username or password";
|
||||||
|
}
|
||||||
|
if (password != password2)
|
||||||
|
{
|
||||||
|
return "Invalid password";
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpdateLastLogin(id);
|
||||||
|
return LOGIN_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> NonceDecryptBody(string username, string password, string body)
|
||||||
|
{
|
||||||
|
if (!Requests.nonceHolder.TryGetValue(username, out (string, DateTimeOffset) nonce))
|
||||||
|
{
|
||||||
|
return "Invalid nonce";
|
||||||
|
}
|
||||||
|
string decBody = Encryption.Encryption.PacketDecPass(body, password, nonce.Item1);
|
||||||
|
Requests.nonceHolder.TryRemove(username, out _);
|
||||||
|
return decBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task UpdateLastLogin(string id)
|
||||||
|
{
|
||||||
|
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin", Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DateTimeOffset> GetLastLogin(string id)
|
||||||
|
{
|
||||||
|
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
|
||||||
|
{
|
||||||
|
return DateTimeOffset.MinValue;
|
||||||
|
}
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(await Fs.ReadFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SetPassword(string id, string newPassword)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/secret";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Fs.WriteFile(path, Encoding.UTF8.GetBytes(newPassword));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task UpdateUserKeys(string id, string body)
|
||||||
|
{
|
||||||
|
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/keys", Encoding.UTF8.GetBytes(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetUserKeys(string id)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/keys";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]> GetUserPublicStorageEntry(string id, string entry)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public/{entry}";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return new byte[] {};
|
||||||
|
}
|
||||||
|
return await Fs.ReadFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetUserDms(string id)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dms";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RemoveOldestDmIndex(string id) //i wont implement this, client should just warn users that they have like 99999999 dms and should leave some
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetPassword(string id)
|
||||||
|
{
|
||||||
|
string path = $"{ACCOUNTS_DATA_DIR}/{id}/secret";
|
||||||
|
if (!Fs.Exists(path))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> SetUsername(string id, string newName)
|
||||||
|
{
|
||||||
|
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
|
||||||
|
{
|
||||||
|
return "Account does not exist";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValidUsername(newName, out string message))
|
||||||
|
{
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
string oldName = await NameFromId(id);
|
||||||
|
string lowerNewName = newName.ToLowerInvariant();
|
||||||
|
string lowerOldName = oldName.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (lowerNewName != lowerOldName)
|
||||||
|
{
|
||||||
|
if (Fs.Exists($"{ACCOUNTS_NAME_DIR}/{lowerNewName}"))
|
||||||
|
{
|
||||||
|
return "This username is already taken";
|
||||||
|
}
|
||||||
|
|
||||||
|
Fs.MoveFile($"{ACCOUNTS_NAME_DIR}/{lowerOldName}", $"{ACCOUNTS_NAME_DIR}/{lowerNewName}");
|
||||||
|
}
|
||||||
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/username", Encoding.UTF8.GetBytes(newName));
|
||||||
|
|
||||||
|
return "Username changed successfully";
|
||||||
|
}
|
||||||
|
}
|
||||||
234
LarpixServer/Encryption/Encryption.cs
Normal file
234
LarpixServer/Encryption/Encryption.cs
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace LarpixServer.Encryption;
|
||||||
|
|
||||||
|
public class Encryption
|
||||||
|
{
|
||||||
|
private static readonly string PrimeHex =
|
||||||
|
"0" +
|
||||||
|
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
|
||||||
|
"29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
|
||||||
|
"EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" +
|
||||||
|
"E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" +
|
||||||
|
"EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" +
|
||||||
|
"C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" +
|
||||||
|
"83655D23DCA3AD961C62F356208552BB9ED529077096966D" +
|
||||||
|
"670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" +
|
||||||
|
"E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" +
|
||||||
|
"DE2BCBF6955817183995497CEA956AE515D2261898FA0510" +
|
||||||
|
"15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" +
|
||||||
|
"ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" +
|
||||||
|
"ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" +
|
||||||
|
"F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" +
|
||||||
|
"BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" +
|
||||||
|
"43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" +
|
||||||
|
"88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" +
|
||||||
|
"2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" +
|
||||||
|
"287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" +
|
||||||
|
"1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" +
|
||||||
|
"93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" +
|
||||||
|
"FFFFFFFFFFFFFFFF";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFC 3526 4096-bit MODP Group (ID: 16) Prime
|
||||||
|
/// </summary>
|
||||||
|
public static BigInteger Prime { get; } = BigInteger.Parse(PrimeHex, NumberStyles.AllowHexSpecifier);
|
||||||
|
|
||||||
|
public static void Chuj()
|
||||||
|
{
|
||||||
|
(BigInteger p, BigInteger g, BigInteger pubServer, BigInteger secretServer) serverInfo = Init();
|
||||||
|
(BigInteger pubClient, byte[] aesKey) clientInfo = CalcCommunicationKeyClient(serverInfo.p, serverInfo.g, serverInfo.pubServer);
|
||||||
|
byte[] sharedKey = CalcCommunicationKey(clientInfo.pubClient, serverInfo.secretServer, serverInfo.p);
|
||||||
|
|
||||||
|
if (sharedKey == clientInfo.aesKey)
|
||||||
|
{
|
||||||
|
Console.WriteLine("key ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (BigInteger p, BigInteger g, BigInteger pubServer, BigInteger secretServer) Init()
|
||||||
|
{
|
||||||
|
BigInteger p = Prime;//GenerateRandomBigInt(4096);
|
||||||
|
BigInteger g = 2;
|
||||||
|
BigInteger secretServer = GenerateRandomBigInt(4096);
|
||||||
|
|
||||||
|
BigInteger pubServer = BigInteger.ModPow(g, secretServer, p);
|
||||||
|
|
||||||
|
return (p, g, pubServer, secretServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (BigInteger pubClient, byte[] aesKey) CalcCommunicationKeyClient(BigInteger p, BigInteger g, BigInteger pubServer)
|
||||||
|
{
|
||||||
|
BigInteger secretClient = GenerateRandomBigInt(4096);
|
||||||
|
|
||||||
|
BigInteger pubClient = BigInteger.ModPow(g, secretClient, p);
|
||||||
|
|
||||||
|
BigInteger sharedSecret = BigInteger.ModPow(pubServer, secretClient, p);
|
||||||
|
|
||||||
|
byte[] aesKey;
|
||||||
|
using (SHA256 sha256 = SHA256.Create())
|
||||||
|
{
|
||||||
|
aesKey = sha256.ComputeHash(sharedSecret.ToByteArray(isUnsigned: true, isBigEndian: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (pubClient, aesKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] CalcCommunicationKey(BigInteger pubClient, BigInteger secretServer, BigInteger p)
|
||||||
|
{
|
||||||
|
BigInteger sharedSecret = BigInteger.ModPow(pubClient, secretServer, p);
|
||||||
|
using (SHA256 sha256 = SHA256.Create())
|
||||||
|
{
|
||||||
|
return sha256.ComputeHash(sharedSecret.ToByteArray(isUnsigned: true, isBigEndian: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Encrypt(string plainText, byte[] key)
|
||||||
|
{
|
||||||
|
using (Aes aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = key;
|
||||||
|
aes.GenerateIV();
|
||||||
|
|
||||||
|
ICryptoTransform encryptor = aes.CreateEncryptor();
|
||||||
|
byte[] inputBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
byte[] encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
|
||||||
|
|
||||||
|
byte[] result = new byte[aes.IV.Length + encryptedBytes.Length];
|
||||||
|
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
|
||||||
|
Buffer.BlockCopy(encryptedBytes, 0, result, aes.IV.Length, encryptedBytes.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Decrypt(string cipherText, byte[] key)
|
||||||
|
{
|
||||||
|
byte[] fullCipher = Convert.FromBase64String(cipherText);
|
||||||
|
using (Aes aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Key = key;
|
||||||
|
byte[] iv = new byte[16];
|
||||||
|
byte[] cipher = new byte[fullCipher.Length - 16];
|
||||||
|
Buffer.BlockCopy(fullCipher, 0, iv, 0, 16);
|
||||||
|
Buffer.BlockCopy(fullCipher, 16, cipher, 0, cipher.Length);
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
ICryptoTransform decryptor = aes.CreateDecryptor();
|
||||||
|
byte[] decryptedBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||||
|
return Encoding.UTF8.GetString(decryptedBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigInteger GenerateRandomBigInt(int bits)
|
||||||
|
{
|
||||||
|
byte[] data = new byte[bits / 8];
|
||||||
|
RandomNumberGenerator.Fill(data);
|
||||||
|
return BigInteger.Abs(new BigInteger(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static string EncryptString(string plainText, string passphrase)
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(passphrase));
|
||||||
|
aes.GenerateIV();
|
||||||
|
var iv = aes.IV;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor(aes.Key, iv);
|
||||||
|
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
byte[] result = new byte[iv.Length + cipherBytes.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(cipherBytes, 0, result, iv.Length, cipherBytes.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DecryptString(string cipherTextCombined, string passphrase)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] combined = Convert.FromBase64String(cipherTextCombined);
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(passphrase));
|
||||||
|
|
||||||
|
byte[] iv = new byte[16];
|
||||||
|
byte[] cipherBytes = new byte[combined.Length - 16];
|
||||||
|
Buffer.BlockCopy(combined, 0, iv, 0, 16);
|
||||||
|
Buffer.BlockCopy(combined, 16, cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor(aes.Key, iv);
|
||||||
|
byte[] decryptedBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(decryptedBytes);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] EncryptByte(byte[] plainBytes, string passphrase)
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(passphrase));
|
||||||
|
aes.GenerateIV();
|
||||||
|
var iv = aes.IV;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor(aes.Key, iv);
|
||||||
|
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
byte[] result = new byte[iv.Length + cipherBytes.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(cipherBytes, 0, result, iv.Length, cipherBytes.Length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] DecryptByte(byte[] combinedBytes, string passphrase)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(passphrase));
|
||||||
|
|
||||||
|
byte[] iv = new byte[16];
|
||||||
|
byte[] cipherBytes = new byte[combinedBytes.Length - 16];
|
||||||
|
Buffer.BlockCopy(combinedBytes, 0, iv, 0, 16);
|
||||||
|
Buffer.BlockCopy(combinedBytes, 16, cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor(aes.Key, iv);
|
||||||
|
return decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return new byte[] { };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string PacketDecPass(string encResult, string key, string nonce)
|
||||||
|
{
|
||||||
|
return DecryptString(encResult, nonce + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetRandomString(int length)
|
||||||
|
{
|
||||||
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+=-";
|
||||||
|
return new string(Enumerable.Repeat(chars, length)
|
||||||
|
.Select(s => s[Random.Shared.Next(s.Length)]).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ComputeSHA3_512(string input)
|
||||||
|
{
|
||||||
|
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
|
||||||
|
byte[] hashBytes = SHA3_512.HashData(inputBytes);
|
||||||
|
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
LarpixServer/Filesystem/FileData.cs
Normal file
18
LarpixServer/Filesystem/FileData.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace LarpixServer.Filesystem;
|
||||||
|
|
||||||
|
public class FileData
|
||||||
|
{
|
||||||
|
public byte[] content { get; set; }
|
||||||
|
public bool isDir { get; set; }
|
||||||
|
public long size { get; set; }
|
||||||
|
|
||||||
|
public FileData(byte[]? content, bool isDir = false, long size = 0)
|
||||||
|
{
|
||||||
|
if (content != null)
|
||||||
|
{
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
this.isDir = isDir;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
410
LarpixServer/Filesystem/Fs.cs
Normal file
410
LarpixServer/Filesystem/Fs.cs
Normal file
|
|
@ -0,0 +1,410 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using static LarpixServer.Utils.Utils;
|
||||||
|
|
||||||
|
namespace LarpixServer.Filesystem;
|
||||||
|
|
||||||
|
public class Fs
|
||||||
|
{
|
||||||
|
public static ConcurrentDictionary<string, FileData> fileCache = new ConcurrentDictionary<string, FileData>();
|
||||||
|
public static ConcurrentDictionary<string, bool> existCache = new ConcurrentDictionary<string, bool>();
|
||||||
|
public static ConcurrentDictionary<string, string[]> dirCache = new ConcurrentDictionary<string, string[]>();
|
||||||
|
|
||||||
|
private static ConcurrentDictionary<string, SemaphoreSlim> fileLocks = new();
|
||||||
|
|
||||||
|
|
||||||
|
private static void InvalidateDirCacheFor(string path)
|
||||||
|
{
|
||||||
|
var parentDir = Path.GetDirectoryName(path)?.Replace("\\", "/").TrimEnd('/');
|
||||||
|
if (!string.IsNullOrEmpty(parentDir))
|
||||||
|
{
|
||||||
|
dirCache.TryRemove(parentDir, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SemaphoreSlim GetFileLock(string path)
|
||||||
|
{
|
||||||
|
return fileLocks.GetOrAdd(path, _ => new SemaphoreSlim(1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ProcessCacheSpace()
|
||||||
|
{
|
||||||
|
while (fileCache.Count >= FILE_CACHE_SIZE)
|
||||||
|
{
|
||||||
|
var firstKey = fileCache.Keys.FirstOrDefault();
|
||||||
|
if (firstKey != null) fileCache.TryRemove(firstKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (existCache.Count >= EXIST_CACHE_SIZE)
|
||||||
|
{
|
||||||
|
var firstKey = existCache.Keys.FirstOrDefault();
|
||||||
|
if (firstKey != null) existCache.TryRemove(firstKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (dirCache.Count >= DIR_CACHE_SIZE)
|
||||||
|
{
|
||||||
|
var firstKey = dirCache.Keys.FirstOrDefault();
|
||||||
|
if (firstKey != null) dirCache.TryRemove(firstKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fileLocks.Count >= LOCK_SIZE)
|
||||||
|
{
|
||||||
|
var firstKey = fileLocks.Keys.FirstOrDefault();
|
||||||
|
if (firstKey != null) fileLocks.TryRemove(firstKey, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ulong ClearCache(string pattern)
|
||||||
|
{
|
||||||
|
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||||
|
|
||||||
|
ulong removed = 0;
|
||||||
|
removed += ClearDict(fileCache, pattern, comparison);
|
||||||
|
removed += ClearDict(existCache, pattern, comparison);
|
||||||
|
removed += ClearDict(dirCache, pattern, comparison);
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ulong ClearDict<TValue>(ConcurrentDictionary<string, TValue> dict, string pattern,
|
||||||
|
StringComparison comparison)
|
||||||
|
{
|
||||||
|
ulong count = 0;
|
||||||
|
|
||||||
|
Parallel.ForEach(
|
||||||
|
dict.Where(x => x.Key.Contains(pattern, comparison))
|
||||||
|
.Select(x => x.Key)
|
||||||
|
.ToList(),
|
||||||
|
key =>
|
||||||
|
{
|
||||||
|
if (dict.TryRemove(key, out _))
|
||||||
|
Interlocked.Increment(ref count);
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Stream> ReadFileStream(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
await fileLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fileCache.TryGetValue(path, out var cachedData))
|
||||||
|
{
|
||||||
|
return new MemoryStream(cachedData.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
if (!fileInfo.Exists) throw new FileNotFoundException();
|
||||||
|
|
||||||
|
if (fileInfo.Length < CACHE_ENTRY_SIZE)
|
||||||
|
{
|
||||||
|
var bytes = await File.ReadAllBytesAsync(path);
|
||||||
|
fileCache[path] = new FileData(bytes, false, fileInfo.Length);
|
||||||
|
return new MemoryStream(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]> ReadFile(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
await fileLock.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fileCache.TryGetValue(path, out var fileData))
|
||||||
|
{
|
||||||
|
return fileData.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
if (fileInfo.Length < CACHE_ENTRY_SIZE)
|
||||||
|
{
|
||||||
|
byte[] content = await File.ReadAllBytesAsync(path);
|
||||||
|
fileCache[path] = new FileData(content, false, fileInfo.Length);
|
||||||
|
existCache[path] = true;
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
byte[] bytes = await File.ReadAllBytesAsync(path);
|
||||||
|
existCache[path] = true;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task WriteFile(string path, byte[] content)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
await fileLock.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(path, content);
|
||||||
|
existCache[path] = true;
|
||||||
|
if (content.Length < CACHE_ENTRY_SIZE)
|
||||||
|
{
|
||||||
|
fileCache[path] = new FileData(content, false, content.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileCache.TryRemove(path, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateDirCacheFor(path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MoveFile(string path, string newPath)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
newPath = newPath.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var lock1Path = string.Compare(path, newPath, StringComparison.OrdinalIgnoreCase) < 0 ? path : newPath;
|
||||||
|
var lock2Path = string.Compare(path, newPath, StringComparison.OrdinalIgnoreCase) < 0 ? newPath : path;
|
||||||
|
|
||||||
|
var sem1 = GetFileLock(lock1Path);
|
||||||
|
var sem2 = GetFileLock(lock2Path);
|
||||||
|
|
||||||
|
sem1.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sem2.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(path, newPath);
|
||||||
|
existCache[path] = false;
|
||||||
|
existCache[newPath] = true;
|
||||||
|
fileCache.TryRemove(path, out _);
|
||||||
|
InvalidateDirCacheFor(path);
|
||||||
|
InvalidateDirCacheFor(newPath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sem2.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sem1.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DeleteFile(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
fileLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
existCache[path] = false;
|
||||||
|
if (fileCache.ContainsKey(path))
|
||||||
|
{
|
||||||
|
fileCache.TryRemove(path, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateDirCacheFor(path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DeleteDirectory(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
fileLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
Directory.Delete(path, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysToRemove = existCache.Keys.Where(k => k.StartsWith(path + "/")).ToList();
|
||||||
|
foreach (var key in keysToRemove)
|
||||||
|
{
|
||||||
|
existCache[key] = false;
|
||||||
|
fileCache.TryRemove(key, out _);
|
||||||
|
dirCache.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
existCache[path] = false;
|
||||||
|
dirCache.TryRemove(path, out _);
|
||||||
|
InvalidateDirCacheFor(path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateDirectory(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
fileLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
existCache[path] = true;
|
||||||
|
InvalidateDirCacheFor(path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Exists(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
fileLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (existCache.TryGetValue(path, out bool exists))
|
||||||
|
{
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(path) || Directory.Exists(path))
|
||||||
|
{
|
||||||
|
FileAttributes attr = File.GetAttributes(path);
|
||||||
|
bool isDir = attr.HasFlag(FileAttributes.Directory);
|
||||||
|
if (isDir)
|
||||||
|
{
|
||||||
|
fileCache[path] = new FileData(null, true, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
existCache[path] = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existCache[path] = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] ReadDirectory(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
fileLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (dirCache.TryGetValue(path, out var directoryData))
|
||||||
|
{
|
||||||
|
return directoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryData = Directory.GetFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly);
|
||||||
|
dirCache[path] = directoryData;
|
||||||
|
return directoryData;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async static Task<bool> isDirectory(string path)
|
||||||
|
{
|
||||||
|
ProcessCacheSpace();
|
||||||
|
path = path.Replace("\\", "/").TrimEnd('/');
|
||||||
|
|
||||||
|
var fileLock = GetFileLock(path);
|
||||||
|
await fileLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fileCache.TryGetValue(path, out var fileData))
|
||||||
|
{
|
||||||
|
return fileData.isDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(path) && !Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
|
||||||
|
if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
|
||||||
|
{
|
||||||
|
fileCache[path] = new FileData(null, true, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.Length < CACHE_ENTRY_SIZE)
|
||||||
|
{
|
||||||
|
byte[] content = await File.ReadAllBytesAsync(path);
|
||||||
|
fileCache[path] = new FileData(content, false, fileInfo.Length);
|
||||||
|
existCache[path] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
LarpixServer/LarpixServer.csproj
Normal file
15
LarpixServer/LarpixServer.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SkiaSharp" Version="3.119.2" />
|
||||||
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
180
LarpixServer/Program.cs
Normal file
180
LarpixServer/Program.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
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();
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Services.AddHostedService<BackgroundDelete>();
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
|
||||||
|
builder.WebHost.ConfigureKestrel(serverOptions =>
|
||||||
|
{
|
||||||
|
serverOptions.AddServerHeader = false;
|
||||||
|
serverOptions.ListenAnyIP(PORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
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);
|
||||||
|
|
||||||
|
switch (path)
|
||||||
|
{
|
||||||
|
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 "/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 "/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 "/createaccount":
|
||||||
|
await Account.Requests.Create(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/deleteaccount":
|
||||||
|
await Account.Requests.Delete(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/auth":
|
||||||
|
await Account.Requests.Auth(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/chpass":
|
||||||
|
await Account.Requests.ChangePassword(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/chname":
|
||||||
|
await Account.Requests.ChangeUsername(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/correctedname":
|
||||||
|
await Account.Requests.CorrectedName(context, next, query);
|
||||||
|
return;
|
||||||
|
case "/nextnonce":
|
||||||
|
await Account.Requests.NextNonce(context, next, query);
|
||||||
|
return;
|
||||||
|
case "/encryptedrequest":
|
||||||
|
await Account.Requests.EncryptedRequest(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/user/key/getpublic":
|
||||||
|
await Account.Requests.GetUserPublicKey(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
case "/user/storage/public/getentry":
|
||||||
|
await Account.Requests.GetUserPublicStorageEntry(context, next, query, reader);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine("Starting server at port: " + PORT);
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
198
LarpixServer/Room/Requests.cs
Normal file
198
LarpixServer/Room/Requests.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using LarpixServer.Filesystem;
|
||||||
|
using LarpixServer.Utils.Jsons;
|
||||||
|
using static LarpixServer.Utils.Utils;
|
||||||
|
|
||||||
|
namespace LarpixServer.Room;
|
||||||
|
|
||||||
|
public class Requests
|
||||||
|
{
|
||||||
|
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1);
|
||||||
|
public static async Task<string> DmInvite(string id, string username2)
|
||||||
|
{
|
||||||
|
if (!Account.Utils.IsUserLocal(username2, out string domain)) //federation
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
username2 = username2.Split(":")[0];
|
||||||
|
string id2 = await Account.Utils.IdFromName(username2);
|
||||||
|
|
||||||
|
SemaphoreSlim userLock = Account.Utils.GetUserLock(id2);
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/dminvites/recv/{id};{DOMAIN}";
|
||||||
|
if (Fs.Exists(inviteFile))
|
||||||
|
{
|
||||||
|
string inviteState = Encoding.UTF8.GetString(await Fs.ReadFile(inviteFile));
|
||||||
|
switch (inviteState)
|
||||||
|
{
|
||||||
|
case "0": //invited
|
||||||
|
return "User already invited";
|
||||||
|
case "1": //accepted
|
||||||
|
return "You already have a DM with this user";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await Fs.WriteFile(inviteFile, Encoding.UTF8.GetBytes("0"));
|
||||||
|
await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/dminvites/sent/{id2};{domain}", []); //we should also save that this user invited someone, because this will act as dm accept verification while federating
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "User invited";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> DmCreate(string id, string body)
|
||||||
|
{
|
||||||
|
await createLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Universal3String serializedBody = JsonSerializer.Deserialize( //1 is ID2 nameWD, 2 is creators dm key, 3 is key for ID2
|
||||||
|
body,
|
||||||
|
AppJsonSerializerContext.Default.Universal3String
|
||||||
|
);
|
||||||
|
|
||||||
|
string username2 = serializedBody.string1;
|
||||||
|
bool isUserLocal = Account.Utils.IsUserLocal(username2, out string domain);
|
||||||
|
string id2;
|
||||||
|
username2 = username2.Split(":")[0];
|
||||||
|
if (isUserLocal)
|
||||||
|
{
|
||||||
|
id2 = await Account.Utils.IdFromName(username2);
|
||||||
|
}
|
||||||
|
else //id z federacji
|
||||||
|
{
|
||||||
|
id2 = "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id}/dminvites/recv/{id2};{domain}";
|
||||||
|
if (Fs.Exists(inviteFile))
|
||||||
|
{
|
||||||
|
string inviteState = Encoding.UTF8.GetString(await Fs.ReadFile(inviteFile));
|
||||||
|
switch (inviteState)
|
||||||
|
{
|
||||||
|
case "0": //invited
|
||||||
|
await Fs.WriteFile(inviteFile, Encoding.UTF8.GetBytes("1")); //we are setting this to 1,
|
||||||
|
//so now we should have an option to delete id from dmslist and still search it
|
||||||
|
|
||||||
|
|
||||||
|
BigInteger dmId =
|
||||||
|
BigInteger.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{ROOMS_DIR}/lastdm")));
|
||||||
|
dmId++;
|
||||||
|
var freeid =
|
||||||
|
Path.GetFileName(Directory.EnumerateFiles(ROOMS_DIR + "/freedms").FirstOrDefault());
|
||||||
|
if (!isUserLocal) //federacja
|
||||||
|
{
|
||||||
|
//jezeli freeid tez jest wolne na innym serwerze (tez jako freeid) to wszystko zostawiamy tak jak jest
|
||||||
|
//w przeciwnym razie porownojemy lastid z 2 serwerow i bierzemy to większe + 1 jako docelowe id dma
|
||||||
|
|
||||||
|
//no ale moze to byc problematyczne
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeid != null)
|
||||||
|
{
|
||||||
|
dmId = BigInteger.Parse(freeid);
|
||||||
|
Fs.DeleteFile($"{ROOMS_DIR}/freedms/{freeid}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Fs.WriteFile($"{ROOMS_DIR}/lastdm", Encoding.UTF8.GetBytes(dmId.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{dmId}/members",
|
||||||
|
Encoding.UTF8.GetBytes($"{id}:{DOMAIN};{id2}:{domain}"));
|
||||||
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{dmId}/messages/last", Encoding.UTF8.GetBytes($"0"));
|
||||||
|
|
||||||
|
Message startingMessage = new();
|
||||||
|
startingMessage.attachment = "";
|
||||||
|
startingMessage.author = "0";
|
||||||
|
startingMessage.timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
|
||||||
|
startingMessage.type = "larp.info";
|
||||||
|
startingMessage.content =
|
||||||
|
"At the beginning of a DM, you should always verify that the person you're talking to is the intended recipient";
|
||||||
|
startingMessage.key = ""; //no key == NOT encrypted
|
||||||
|
startingMessage.pervious = "";
|
||||||
|
|
||||||
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{dmId}/keys/0/{id};{DOMAIN}", //klucz dla uzytkownika tworzącego jest juz gotowy i ma zwykly zapis
|
||||||
|
Encoding.UTF8.GetBytes(serializedBody.string2));
|
||||||
|
|
||||||
|
|
||||||
|
Universal2String keys = JsonSerializer.Deserialize(
|
||||||
|
await Account.Utils.GetUserKeys(id),
|
||||||
|
AppJsonSerializerContext.Default.Universal2String
|
||||||
|
);
|
||||||
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{dmId}/keys/0/{id2};{domain}",
|
||||||
|
Encoding.UTF8.GetBytes($"SETUP:{keys.string2};{serializedBody.string3}")); //jezeli mamy setup to [1] to jest publiczny klucz drugiej osoby,
|
||||||
|
//a string 3 to zaszyfrowany klucz pokoju ktory musi odszyfrowac za pomoca swoich kluczy i tego publicznego
|
||||||
|
|
||||||
|
|
||||||
|
await Fs.WriteFile($"{ROOMS_DIR}/dms/{dmId}/messages/0", Encoding.UTF8.GetBytes(
|
||||||
|
JsonSerializer.Serialize(startingMessage, AppJsonSerializerContext.Default.Message)
|
||||||
|
));
|
||||||
|
|
||||||
|
string dmspath = $"{ACCOUNTS_DATA_DIR}/{id}/dms";
|
||||||
|
string dms;
|
||||||
|
if (!Fs.Exists(dmspath))
|
||||||
|
{
|
||||||
|
dms = "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dms = Encoding.UTF8.GetString(await Fs.ReadFile(dmspath));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Fs.WriteFile(dmspath, Encoding.UTF8.GetBytes($"{dmId};{dms}"));
|
||||||
|
|
||||||
|
if (isUserLocal)
|
||||||
|
{
|
||||||
|
SemaphoreSlim userLock = Account.Utils.GetUserLock(id2);
|
||||||
|
await userLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string dms2path = $"{ACCOUNTS_DATA_DIR}/{id2}/dms";
|
||||||
|
string dms2;
|
||||||
|
if (await Account.Utils.NameFromId(id2) == "") //if user2 account just got deleted
|
||||||
|
{
|
||||||
|
return "DM accepted";
|
||||||
|
}
|
||||||
|
if (!Fs.Exists(dms2path))
|
||||||
|
{
|
||||||
|
dms2 = "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dms2 = Encoding.UTF8.GetString(await Fs.ReadFile(dms2path));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Fs.WriteFile(dms2path, Encoding.UTF8.GetBytes($"{dmId};{dms2}"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
userLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else //federacja
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return "DM accepted";
|
||||||
|
case "1": //accepted
|
||||||
|
return "You already have a DM with this user";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
createLock.Release();
|
||||||
|
}
|
||||||
|
return "You can't create a DM without an invitation";
|
||||||
|
}
|
||||||
|
}
|
||||||
14
LarpixServer/Utils/AppJsonSerializerContext.cs
Normal file
14
LarpixServer/Utils/AppJsonSerializerContext.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LarpixServer.Utils.Jsons;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(KeyExchangePayload))]
|
||||||
|
[JsonSerializable(typeof(KeyExchangePayloadClient))]
|
||||||
|
[JsonSerializable(typeof(CaptchaPayloadClient))]
|
||||||
|
[JsonSerializable(typeof(Universal2String))]
|
||||||
|
[JsonSerializable(typeof(Universal3String))]
|
||||||
|
[JsonSerializable(typeof(ServerInfo))]
|
||||||
|
[JsonSerializable(typeof(Message))]
|
||||||
|
public partial class AppJsonSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
54
LarpixServer/Utils/Jsons.cs
Normal file
54
LarpixServer/Utils/Jsons.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
namespace LarpixServer.Utils.Jsons;
|
||||||
|
|
||||||
|
public class ServerInfo
|
||||||
|
{
|
||||||
|
public string domain { get; set; }
|
||||||
|
public string owner_contact { get; set; }
|
||||||
|
public string registration { get; set; }
|
||||||
|
}
|
||||||
|
public class Universal2String
|
||||||
|
{
|
||||||
|
public string string1 { get; set; }
|
||||||
|
public string string2 { get; set; }
|
||||||
|
}
|
||||||
|
public class Universal3String
|
||||||
|
{
|
||||||
|
public string string1 { get; set; }
|
||||||
|
public string string2 { get; set; }
|
||||||
|
public string string3 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class KeyExchangePayload
|
||||||
|
{
|
||||||
|
public string p { get; set; }
|
||||||
|
public string g { get; set; }
|
||||||
|
public string pubServer { get; set; }
|
||||||
|
public string idKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KeyExchangePayloadClient
|
||||||
|
{
|
||||||
|
public string pubClient { get; set; }
|
||||||
|
public string idKey { get; set; }
|
||||||
|
public string username { get; set; }
|
||||||
|
public string password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CaptchaPayloadClient
|
||||||
|
{
|
||||||
|
public string captcha { get; set; }
|
||||||
|
public string idKey { get; set; }
|
||||||
|
public string regKey { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Message
|
||||||
|
{
|
||||||
|
public string author { get; set; }
|
||||||
|
public string pervious { get; set; }
|
||||||
|
public string type { get; set; }
|
||||||
|
public string content { get; set; }
|
||||||
|
public string attachment { get; set; }
|
||||||
|
public string timestamp { get; set; }
|
||||||
|
public string key { get; set; }
|
||||||
|
}
|
||||||
258
LarpixServer/Utils/Utils.cs
Normal file
258
LarpixServer/Utils/Utils.cs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace LarpixServer.Utils;
|
||||||
|
|
||||||
|
public class Utils
|
||||||
|
{
|
||||||
|
public static string SERVER_KEY = "SecretThingOMGSoHardToGuess";
|
||||||
|
|
||||||
|
public static string DOMAIN = "example.com";
|
||||||
|
|
||||||
|
public static string OWNER_CONTACT = "larpix://@admin:example.com";
|
||||||
|
|
||||||
|
public static int PORT = 8090;
|
||||||
|
public static ConcurrentBag<string> VOICE_IPS = new();
|
||||||
|
|
||||||
|
public static string DATA_DIR = "./data";
|
||||||
|
public static string ROOMS_DIR = "./data/rooms";
|
||||||
|
public static string ACCOUNTS_DIR = "./data/accounts";
|
||||||
|
|
||||||
|
public static string ACCOUNTS_DATA_DIR = "./data/accounts/data";
|
||||||
|
public static string ACCOUNTS_NAME_DIR = "./data/accounts/name";
|
||||||
|
public static string ACCOUNTS_FREEID_DIR = "./data/accounts/freeid";
|
||||||
|
|
||||||
|
public static int CACHE_ENTRY_SIZE = 10240;
|
||||||
|
public static int FILE_CACHE_SIZE = 800000;
|
||||||
|
public static int EXIST_CACHE_SIZE = 1000000;
|
||||||
|
public static int DIR_CACHE_SIZE = 800000;
|
||||||
|
public static int LOCK_SIZE = 1000000;
|
||||||
|
|
||||||
|
public static int MAX_BODY_SIZE = 65536;
|
||||||
|
|
||||||
|
public static int PRIVATE_STORAGE_LIMIT = 5242880;
|
||||||
|
public static int PUBLIC_STORAGE_LIMIT = 10485760;
|
||||||
|
|
||||||
|
public static Dictionary<string, string> mimeTypes = new()
|
||||||
|
{
|
||||||
|
["unknown"] = "application/octet-stream",
|
||||||
|
["html"] = "text/html",
|
||||||
|
["htm"] = "text/html",
|
||||||
|
["css"] = "text/css",
|
||||||
|
["js"] = "application/javascript",
|
||||||
|
["mjs"] = "application/javascript",
|
||||||
|
["json"] = "application/json",
|
||||||
|
["xml"] = "application/xml",
|
||||||
|
["txt"] = "text/plain",
|
||||||
|
["csv"] = "text/csv",
|
||||||
|
["md"] = "text/markdown",
|
||||||
|
|
||||||
|
["png"] = "image/png",
|
||||||
|
["jpg"] = "image/jpeg",
|
||||||
|
["jpeg"] = "image/jpeg",
|
||||||
|
["gif"] = "image/gif",
|
||||||
|
["webp"] = "image/webp",
|
||||||
|
["svg"] = "image/svg+xml",
|
||||||
|
["ico"] = "image/x-icon",
|
||||||
|
["bmp"] = "image/bmp",
|
||||||
|
["tiff"] = "image/tiff",
|
||||||
|
["avif"] = "image/avif",
|
||||||
|
|
||||||
|
["mp3"] = "audio/mpeg",
|
||||||
|
["wav"] = "audio/wav",
|
||||||
|
["ogg"] = "audio/ogg",
|
||||||
|
["oga"] = "audio/ogg",
|
||||||
|
["weba"] = "audio/webm",
|
||||||
|
["aac"] = "audio/aac",
|
||||||
|
["flac"] = "audio/flac",
|
||||||
|
|
||||||
|
["mp4"] = "video/mp4",
|
||||||
|
["webm"] = "video/webm",
|
||||||
|
["mkv"] = "video/x-matroska",
|
||||||
|
["mov"] = "video/quicktime",
|
||||||
|
["avi"] = "video/x-msvideo",
|
||||||
|
["wmv"] = "video/x-ms-wmv",
|
||||||
|
|
||||||
|
["pdf"] = "application/pdf",
|
||||||
|
["zip"] = "application/zip",
|
||||||
|
["gz"] = "application/gzip",
|
||||||
|
["tar"] = "application/x-tar",
|
||||||
|
["rar"] = "application/vnd.rar",
|
||||||
|
["7z"] = "application/x-7z-compressed",
|
||||||
|
|
||||||
|
["woff"] = "font/woff",
|
||||||
|
["woff2"] = "font/woff2",
|
||||||
|
["ttf"] = "font/ttf",
|
||||||
|
["otf"] = "font/otf",
|
||||||
|
["eot"] = "application/vnd.ms-fontobject",
|
||||||
|
|
||||||
|
["wasm"] = "application/wasm",
|
||||||
|
|
||||||
|
["doc"] = "application/msword",
|
||||||
|
["docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
["xls"] = "application/vnd.ms-excel",
|
||||||
|
["xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
["ppt"] = "application/vnd.ms-powerpoint",
|
||||||
|
["pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<string> LoadBody(StreamReader reader)
|
||||||
|
{
|
||||||
|
StringBuilder bodyBuilder = new();
|
||||||
|
char[] buffer = new char[8192];
|
||||||
|
|
||||||
|
int read;
|
||||||
|
int totalRead = 0;
|
||||||
|
|
||||||
|
while ((read = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
totalRead += read;
|
||||||
|
|
||||||
|
if (totalRead > MAX_BODY_SIZE)
|
||||||
|
throw new Exception("Body too large");
|
||||||
|
|
||||||
|
bodyBuilder.Append(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bodyBuilder.ToString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task LoadEnv()
|
||||||
|
{
|
||||||
|
if (File.Exists(".env"))
|
||||||
|
{
|
||||||
|
|
||||||
|
foreach (var line in File.ReadAllLines(".env"))
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#")) continue;
|
||||||
|
|
||||||
|
var parts = trimmed.Split('=', 2, StringSplitOptions.None);
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
var key = parts[0].Trim();
|
||||||
|
var val = parts[1].Trim();
|
||||||
|
|
||||||
|
Environment.SetEnvironmentVariable(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string? port = Environment.GetEnvironmentVariable("PORT");
|
||||||
|
if (port != null)
|
||||||
|
{
|
||||||
|
PORT = int.Parse(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
VOICE_IPS.Clear();
|
||||||
|
|
||||||
|
port = Environment.GetEnvironmentVariable("VOICE_IPS");
|
||||||
|
if (port != null)
|
||||||
|
{
|
||||||
|
foreach (var address in port.Split(","))
|
||||||
|
{
|
||||||
|
VOICE_IPS.Add(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
port = Environment.GetEnvironmentVariable("PORT_VOICE");
|
||||||
|
if (port != null)
|
||||||
|
{
|
||||||
|
VOICE_IPS.Add($"127.0.0.1:{port}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DOMAIN = Environment.GetEnvironmentVariable("DOMAIN") ?? DOMAIN;
|
||||||
|
SERVER_KEY = Environment.GetEnvironmentVariable("SERVER_KEY") ?? SERVER_KEY;
|
||||||
|
DATA_DIR = Environment.GetEnvironmentVariable("DATA_DIR") ?? DATA_DIR;
|
||||||
|
OWNER_CONTACT = Environment.GetEnvironmentVariable("OWNER_CONTACT") ?? OWNER_CONTACT;
|
||||||
|
|
||||||
|
CACHE_ENTRY_SIZE = int.TryParse(Environment.GetEnvironmentVariable("CACHE_ENTRY_SIZE"), out var res) ? res : CACHE_ENTRY_SIZE;
|
||||||
|
FILE_CACHE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("FILE_CACHE_SIZE"), out res) ? res : FILE_CACHE_SIZE;
|
||||||
|
EXIST_CACHE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("EXIST_CACHE_SIZE"), out res) ? res : EXIST_CACHE_SIZE;
|
||||||
|
DIR_CACHE_SIZE = int.TryParse(Environment.GetEnvironmentVariable("DIR_CACHE_SIZE"), out res) ? res : DIR_CACHE_SIZE;
|
||||||
|
LOCK_SIZE = int.TryParse(Environment.GetEnvironmentVariable("LOCK_SIZE"), out res) ? res : LOCK_SIZE;
|
||||||
|
|
||||||
|
PRIVATE_STORAGE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("PRIVATE_STORAGE_LIMIT"), out res) ? res : PRIVATE_STORAGE_LIMIT;
|
||||||
|
PUBLIC_STORAGE_LIMIT = int.TryParse(Environment.GetEnvironmentVariable("PUBLIC_STORAGE_LIMIT"), out res) ? res : PUBLIC_STORAGE_LIMIT;
|
||||||
|
|
||||||
|
ROOMS_DIR = DATA_DIR + "/rooms";
|
||||||
|
ACCOUNTS_DIR = DATA_DIR + "/accounts";
|
||||||
|
ACCOUNTS_NAME_DIR = ACCOUNTS_DIR + "/name";
|
||||||
|
ACCOUNTS_DATA_DIR = ACCOUNTS_DIR + "/data";
|
||||||
|
ACCOUNTS_FREEID_DIR = ACCOUNTS_DIR + "/freeid";
|
||||||
|
|
||||||
|
if (!Directory.Exists(DATA_DIR))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(DATA_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(ROOMS_DIR))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR);
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ROOMS_DIR + "/dms"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR + "/dms");
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ROOMS_DIR + "/groups"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR + "/groups");
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ROOMS_DIR + "/spaces"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR + "/spaces");
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ROOMS_DIR + "/freedms"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR + "/freedms");
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ROOMS_DIR + "/freegroups"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR + "/freegroups");
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ROOMS_DIR + "/freespaces"))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ROOMS_DIR + "/freespaces");
|
||||||
|
}
|
||||||
|
if (!File.Exists(ROOMS_DIR + "/lastdm"))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(ROOMS_DIR + "/lastdm", "0");
|
||||||
|
}
|
||||||
|
if (!File.Exists(ROOMS_DIR + "/lastgroup"))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(ROOMS_DIR + "/lastgroup", "0");
|
||||||
|
}
|
||||||
|
if (!File.Exists(ROOMS_DIR + "/lastspace"))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(ROOMS_DIR + "/lastspace", "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(ACCOUNTS_DIR))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ACCOUNTS_DIR);
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ACCOUNTS_NAME_DIR))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ACCOUNTS_NAME_DIR);
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ACCOUNTS_DATA_DIR))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ACCOUNTS_DATA_DIR);
|
||||||
|
}
|
||||||
|
if (!Directory.Exists(ACCOUNTS_FREEID_DIR))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ACCOUNTS_FREEID_DIR);
|
||||||
|
}
|
||||||
|
if (!File.Exists(ACCOUNTS_DIR + "/last"))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(ACCOUNTS_DIR + "/last", "0");
|
||||||
|
}
|
||||||
|
if (!File.Exists($"{ACCOUNTS_DIR}/registration"))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync($"{ACCOUNTS_DIR}/registration", "first;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
LarpixServer/appsettings.Development.json
Normal file
9
LarpixServer/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
9
LarpixServer/appsettings.json
Normal file
9
LarpixServer/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
8
LarpixVoice/JsonAppSerializerContext.cs
Normal file
8
LarpixVoice/JsonAppSerializerContext.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LarpixVoice;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(string[]))]
|
||||||
|
internal partial class JsonAppSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
10
LarpixVoice/LarpixVoice.csproj
Normal file
10
LarpixVoice/LarpixVoice.csproj
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
95
LarpixVoice/Program.cs
Normal file
95
LarpixVoice/Program.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace LarpixVoice;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static ConcurrentDictionary<string, ConcurrentDictionary<ulong, Session>> Rooms = new ();
|
||||||
|
|
||||||
|
public static ulong TotalBytesSent = 0;
|
||||||
|
public static ulong TotalBytesReceived = 0;
|
||||||
|
private static ulong CurrentBandwidthKps = 0;
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Utils.LoadEnv();
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
ulong sentDelta = Interlocked.Read(ref TotalBytesSent);
|
||||||
|
ulong receivedDelta = Interlocked.Read(ref TotalBytesReceived);
|
||||||
|
Interlocked.Exchange(ref TotalBytesReceived, 0);
|
||||||
|
Interlocked.Exchange(ref TotalBytesSent, 0);
|
||||||
|
CurrentBandwidthKps = (sentDelta + receivedDelta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
|
||||||
|
builder.WebHost.ConfigureKestrel(serverOptions =>
|
||||||
|
{
|
||||||
|
serverOptions.AddServerHeader = false;
|
||||||
|
serverOptions.ListenAnyIP(Utils.PORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseWebSockets();
|
||||||
|
|
||||||
|
app.Use(async (HttpContext context, Func<Task> next) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.Headers["Access-Control-Allow-Origin"] = "*";//(context.Request.Headers["Origin"]).ToString().Replace("http://", "https://");
|
||||||
|
context.Response.Headers["Access-Control-Allow-Methods"]= "GET, POST, PUT, DELETE, OPTIONS";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Headers"]= "Content-Type, Authorization";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Credentials"]= "true";
|
||||||
|
switch (path)
|
||||||
|
{
|
||||||
|
case "/ws":
|
||||||
|
await Requests.Websocket(context);
|
||||||
|
return;
|
||||||
|
case "/load":
|
||||||
|
await context.Response.WriteAsync(Interlocked.Read(ref CurrentBandwidthKps).ToString());
|
||||||
|
return;
|
||||||
|
case "/room/users":
|
||||||
|
string roomId = context.Request.Query["room"].ToString();
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
if (Rooms.TryGetValue(roomId, out var room))
|
||||||
|
{
|
||||||
|
string[] users = room.Keys.Select(k => k.ToString()).ToArray();
|
||||||
|
await context.Response.WriteAsJsonAsync(users, JsonAppSerializerContext.Default.StringArray);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsJsonAsync(Array.Empty<string>(), JsonAppSerializerContext.Default.StringArray);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Console.WriteLine("Starting server at port: " + Utils.PORT);
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
124
LarpixVoice/Requests.cs
Normal file
124
LarpixVoice/Requests.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
using static LarpixVoice.Program;
|
||||||
|
|
||||||
|
namespace LarpixVoice;
|
||||||
|
|
||||||
|
public class Requests
|
||||||
|
{
|
||||||
|
public static async Task Websocket(HttpContext context)
|
||||||
|
{
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string roomId = context.Request.Query["room"].ToString();
|
||||||
|
string userIdStr = context.Request.Query["userId"].ToString();
|
||||||
|
string secret = context.Request.Query["secret"].ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(roomId) || !ulong.TryParse(userIdStr, out ulong clientId))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
var roomClients = Rooms.GetOrAdd(roomId, _ => new ConcurrentDictionary<ulong, Session>());
|
||||||
|
|
||||||
|
if (roomClients.TryGetValue(clientId, out var oldSession) && oldSession.Socket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await oldSession.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Kicked", CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSession = new Session(webSocket);
|
||||||
|
roomClients[clientId] = currentSession;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await foreach (var packet in currentSession.SendQueue.Reader.ReadAllAsync())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (currentSession.Socket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await currentSession.Socket.SendAsync(new ArraySegment<byte>(packet.Buffer, 0, packet.Length),
|
||||||
|
WebSocketMessageType.Binary, true, CancellationToken.None);
|
||||||
|
|
||||||
|
Interlocked.Add(ref TotalBytesSent, (ulong)packet.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(packet.Buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var receiveBuffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
int totalBytes = 0;
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
result = await webSocket.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(receiveBuffer, totalBytes, receiveBuffer.Length - totalBytes),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
totalBytes += result.Count;
|
||||||
|
Interlocked.Add(ref TotalBytesReceived, (ulong)result.Count);
|
||||||
|
|
||||||
|
} while (!result.EndOfMessage);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close) break;
|
||||||
|
if (result.MessageType == WebSocketMessageType.Binary)
|
||||||
|
{
|
||||||
|
int payloadLength = totalBytes + 8;
|
||||||
|
|
||||||
|
foreach (var (id, client) in roomClients)
|
||||||
|
{
|
||||||
|
if (id != clientId && client.Socket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
byte[] payload = ArrayPool<byte>.Shared.Rent(payloadLength);
|
||||||
|
BitConverter.TryWriteBytes(payload, clientId);
|
||||||
|
Array.Copy(receiveBuffer, 0, payload, 8, totalBytes);
|
||||||
|
|
||||||
|
if (!client.SendQueue.Writer.TryWrite((payload, payloadLength)))
|
||||||
|
ArrayPool<byte>.Shared.Return(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WebSocketException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(receiveBuffer);
|
||||||
|
if (roomClients.TryGetValue(clientId, out var session) && session == currentSession)
|
||||||
|
{
|
||||||
|
roomClients.TryRemove(clientId, out _);
|
||||||
|
session.SendQueue.Writer.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomClients.IsEmpty) Rooms.TryRemove(roomId, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
LarpixVoice/Session.cs
Normal file
19
LarpixVoice/Session.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace LarpixVoice;
|
||||||
|
|
||||||
|
public class Session
|
||||||
|
{
|
||||||
|
public WebSocket Socket { get; }
|
||||||
|
public Channel<(byte[] Buffer, int Length)> SendQueue { get; }
|
||||||
|
|
||||||
|
public Session(WebSocket socket)
|
||||||
|
{
|
||||||
|
Socket = socket;
|
||||||
|
SendQueue = Channel.CreateBounded<(byte[] Buffer, int Length)>(new BoundedChannelOptions(256)
|
||||||
|
{
|
||||||
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
42
LarpixVoice/Utils.cs
Normal file
42
LarpixVoice/Utils.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
namespace LarpixVoice;
|
||||||
|
|
||||||
|
public class Utils
|
||||||
|
{
|
||||||
|
public static int PORT = 8091;
|
||||||
|
public static int MAIN_PORT = 8090;
|
||||||
|
public static string SERVER_KEY = "SecretThingOMGSoHardToGuess";
|
||||||
|
|
||||||
|
public static void LoadEnv()
|
||||||
|
{
|
||||||
|
if (File.Exists(".env"))
|
||||||
|
{
|
||||||
|
|
||||||
|
foreach (var line in File.ReadAllLines(".env"))
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#")) continue;
|
||||||
|
|
||||||
|
var parts = trimmed.Split('=', 2, StringSplitOptions.None);
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
var key = parts[0].Trim();
|
||||||
|
var val = parts[1].Trim();
|
||||||
|
|
||||||
|
Environment.SetEnvironmentVariable(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string? port = Environment.GetEnvironmentVariable("PORT_VOICE");
|
||||||
|
if (port != null)
|
||||||
|
{
|
||||||
|
PORT = int.Parse(port);
|
||||||
|
}
|
||||||
|
port = Environment.GetEnvironmentVariable("PORT");
|
||||||
|
if (port != null)
|
||||||
|
{
|
||||||
|
MAIN_PORT = int.Parse(port);
|
||||||
|
}
|
||||||
|
SERVER_KEY = Environment.GetEnvironmentVariable("SERVER_KEY") ?? SERVER_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
LarpixVoice/appsettings.Development.json
Normal file
9
LarpixVoice/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
9
LarpixVoice/appsettings.json
Normal file
9
LarpixVoice/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
1
README.MD
Normal file
1
README.MD
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Larpix Server
|
||||||
Loading…
Add table
Add a link
Reference in a new issue