975 lines
No EOL
37 KiB
C#
975 lines
No EOL
37 KiB
C#
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;
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
|
|
namespace LarpixServer.Account;
|
|
|
|
public class Requests
|
|
{
|
|
public static ConcurrentDictionary<string, CreateHolder> createHolder = new();
|
|
public static ConcurrentDictionary<string, (string, DateTimeOffset)> nonceHolder = new();
|
|
public static ConcurrentDictionary<string, (int count, DateTime resetTime)> actionRateLimits = new();
|
|
|
|
public static bool IsActionRateLimited(string id)
|
|
{
|
|
if (ACTION_RATE_LIMIT <= 0) return false;
|
|
var now = DateTime.UtcNow;
|
|
|
|
actionRateLimits.AddOrUpdate(id,
|
|
_ => (1, now.AddMinutes(1)),
|
|
(_, old) => {
|
|
if (now > old.resetTime) return (1, now.AddMinutes(1));
|
|
return (old.count + 1, old.resetTime);
|
|
}
|
|
);
|
|
|
|
if (actionRateLimits.TryGetValue(id, out var record) && record.count > ACTION_RATE_LIMIT)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string password = await Utils.GetPassword(id);
|
|
secret = await Utils.NonceDecryptBody(id, 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
|
|
{
|
|
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin",
|
|
Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.AddMonths(-11).ToUnixTimeSeconds().ToString()));
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
|
|
await context.Response.WriteAsync("success:account.deleted.scheduled");
|
|
}
|
|
|
|
public static async Task Create(HttpContext context, Func<Task> next, IQueryCollection query,
|
|
StreamReader bodyReader)
|
|
{
|
|
if (!query.TryGetValue("step", out var step))
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
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"];
|
|
|
|
var serverInfo = Encryption.Encryption.InitHybridKEM();
|
|
KeyExchangePayload payload = new KeyExchangePayload();
|
|
payload.pubX25519 = Convert.ToBase64String(serverInfo.pubX25519);
|
|
payload.pubMlKem = Convert.ToBase64String(serverInfo.pubMlKem);
|
|
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.privX25519Server = serverInfo.privX25519;
|
|
dataHolder.privMlKemServer = serverInfo.privMlKem;
|
|
|
|
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("error:account.creation.request.expired");
|
|
return;
|
|
}
|
|
|
|
entry.date = DateTimeOffset.UtcNow;
|
|
|
|
byte[] pubX25519Client = Convert.FromBase64String(serializedBody.pubX25519);
|
|
byte[] ciphertextMlKem = Convert.FromBase64String(serializedBody.ciphertextMlKem);
|
|
|
|
byte[] sharedKey = Encryption.Encryption.CalcHybridSharedKey(
|
|
pubX25519Client, entry.privX25519Server, ciphertextMlKem, entry.privMlKemServer);
|
|
|
|
|
|
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(message);
|
|
return;
|
|
}
|
|
|
|
if (!Utils.IsValidPassword(entry.pass, out message))
|
|
{
|
|
await context.Response.WriteAsync(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":
|
|
{
|
|
string body = await LoadBody(bodyReader);
|
|
|
|
CaptchaPayloadClient serialized = JsonSerializer.Deserialize(
|
|
body,
|
|
AppJsonSerializerContext.Default.CaptchaPayloadClient
|
|
);
|
|
if (!createHolder.TryGetValue(serialized.idKey, out var entry))
|
|
{
|
|
await context.Response.WriteAsync("error:account.creation.request.expired");
|
|
return;
|
|
}
|
|
|
|
if (entry.captcha.ToLower() != serialized.captcha.ToLower())
|
|
{
|
|
createHolder.TryRemove(serialized.idKey, out _);
|
|
await context.Response.WriteAsync("error:incorrect.captcha");
|
|
return;
|
|
}
|
|
|
|
string lowerName = entry.name.ToLowerInvariant();
|
|
|
|
await createLock.WaitAsync();
|
|
try
|
|
{
|
|
if (Fs.Exists($"{ACCOUNTS_NAME_DIR}/{lowerName}"))
|
|
{
|
|
await context.Response.WriteAsync("error:username.taken");
|
|
return;
|
|
}
|
|
|
|
string registrationString =
|
|
Encoding.UTF8.GetString(await Fs.ReadFile($"{ACCOUNTS_DIR}/registration"));
|
|
if (registrationString.StartsWith("0;"))
|
|
{
|
|
await context.Response.WriteAsync("error:registration.disabled");
|
|
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(
|
|
"error:registration.disabled");
|
|
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++;
|
|
if (id == ulong.MaxValue)
|
|
{
|
|
await context.Response.WriteAsync("error:accounts.slots.full");
|
|
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()));
|
|
|
|
try
|
|
{
|
|
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));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
Fs.DeleteFile($"{ACCOUNTS_NAME_DIR}/{lowerName}");
|
|
throw;
|
|
}
|
|
|
|
createHolder.TryRemove(serialized.idKey, out _);
|
|
await Utils.UpdateLastLogin(id.ToString());
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
|
|
await context.Response.WriteAsync("success:account.created");
|
|
return;
|
|
}
|
|
finally
|
|
{
|
|
createLock.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
await next();
|
|
}
|
|
|
|
|
|
|
|
public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string password = await Utils.GetPassword(id);
|
|
secret = await Utils.NonceDecryptBody(id, 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 = Utils.GetIdFromUsernameWD(username.ToString());
|
|
|
|
await context.Response.WriteAsync(await Utils.NameFromId(await Utils.IdFromName(username)));
|
|
return;
|
|
|
|
}
|
|
|
|
public static async Task NameToId(HttpContext context, Func<Task> next, IQueryCollection query)
|
|
{
|
|
if (!query.TryGetValue("u", out var username))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Utils.IsUserLocal(username, out string domain)) //federation
|
|
{
|
|
username = Utils.GetIdFromUsernameWD(username.ToString());
|
|
string remoteId = await Federation.Sender.GetUserId(username, domain);
|
|
if (remoteId != "0" && !string.IsNullOrEmpty(remoteId))
|
|
{
|
|
await context.Response.WriteAsync(remoteId + $":{domain}");
|
|
}
|
|
return;
|
|
}
|
|
username = Utils.GetIdFromUsernameWD(username.ToString());
|
|
|
|
await context.Response.WriteAsync(await Utils.IdFromName(username) + $":{DOMAIN}");
|
|
return;
|
|
}
|
|
|
|
public static async Task IdToName(HttpContext context, Func<Task> next, IQueryCollection query)
|
|
{
|
|
if (!query.TryGetValue("id", out var id))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Utils.IsUserLocal(id, out string domain)) //federation :(
|
|
{
|
|
return;
|
|
}
|
|
id = Utils.GetValidIdOrZero(id.ToString());
|
|
|
|
await context.Response.WriteAsync(await Utils.NameFromId(id) + $":{DOMAIN}");
|
|
return;
|
|
|
|
}
|
|
|
|
public static async Task NextNonce(HttpContext context, Func<Task> next, IQueryCollection query)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string plainPass = await Utils.GetPassword(id);
|
|
foreach (var kvp in nonceHolder) //clearowanie nieuzytych nonce
|
|
{
|
|
if (kvp.Value.Item2 < DateTimeOffset.UtcNow.AddMinutes(-2))
|
|
{
|
|
nonceHolder.TryRemove(kvp.Key, out _);
|
|
}
|
|
else if (kvp.Key == id)
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(kvp.Value.Item1,
|
|
plainPass));
|
|
return;
|
|
}
|
|
}
|
|
|
|
string nonce = Encryption.Encryption.GetRandomString(64);
|
|
nonceHolder.TryAdd(id, (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("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string body = await LoadBody(bodyReader);
|
|
string password = await Utils.GetPassword(id);
|
|
string bodyDecrypted = await Utils.NonceDecryptBody(id, password, body, false);
|
|
secret = await Utils.NonceDecryptBody(id, password, secret);
|
|
|
|
string auth = await Utils.Auth(id, password, secret);
|
|
|
|
if (auth != Utils.LOGIN_SUCCESS)
|
|
{
|
|
await context.Response.WriteAsync(auth);
|
|
return;
|
|
}
|
|
|
|
string newPass = bodyDecrypted;
|
|
string newKeysBody = null;
|
|
|
|
try
|
|
{
|
|
Universal2String parsedBody = JsonSerializer.Deserialize(bodyDecrypted, AppJsonSerializerContext.Default.Universal2String);
|
|
if (parsedBody != null && !string.IsNullOrWhiteSpace(parsedBody.string1) && !string.IsNullOrWhiteSpace(parsedBody.string2))
|
|
{
|
|
newPass = parsedBody.string1;
|
|
newKeysBody = parsedBody.string2;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
|
await userLock.WaitAsync();
|
|
try
|
|
{
|
|
if (!Utils.IsValidPassword(newPass, out string message))
|
|
{
|
|
await context.Response.WriteAsync(message);
|
|
return;
|
|
}
|
|
|
|
string oldKeys = null;
|
|
if (newKeysBody != null)
|
|
{
|
|
oldKeys = await Utils.GetUserKeys(id);
|
|
string keyUpdateResult = await UpdateUserKeys(id, newKeysBody);
|
|
if (keyUpdateResult.StartsWith("error:"))
|
|
{
|
|
await context.Response.WriteAsync(keyUpdateResult);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
await Utils.SetPassword(id, newPass);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
if (newKeysBody != null && oldKeys != null)
|
|
{
|
|
await Utils.UpdateUserKeys(id, oldKeys);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
|
|
await context.Response.WriteAsync("success:password.changed");
|
|
return;
|
|
}
|
|
|
|
public static async Task ChangeUsername(HttpContext context, Func<Task> next, IQueryCollection query,
|
|
StreamReader bodyReader)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string body = await LoadBody(bodyReader);
|
|
string password = await Utils.GetPassword(id);
|
|
|
|
|
|
body = await Utils.NonceDecryptBody(id, password, body, false);
|
|
secret = await Utils.NonceDecryptBody(id, 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 existingRaw = await Utils.GetUserKeys(id);
|
|
if (!string.IsNullOrWhiteSpace(existingRaw))
|
|
{
|
|
Universal2String existing = JsonSerializer.Deserialize(
|
|
existingRaw,
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
string existingPub = existing.string2?.Trim() ?? "";
|
|
string newPub = serializedBody.string2?.Trim() ?? "";
|
|
if (!string.IsNullOrEmpty(existingPub) && !string.IsNullOrEmpty(newPub) && existingPub != newPub)
|
|
{
|
|
return "error:keys.public.mismatch";
|
|
}
|
|
}
|
|
await Utils.UpdateUserKeys(id, body);
|
|
return "success:keys.updated";
|
|
}
|
|
|
|
public static async Task GetUserPublicKey(HttpContext context, Func<Task> next, IQueryCollection query, StreamReader bodyReader)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Utils.IsUserLocal(idQuery, out string domain)) //federation :(
|
|
{
|
|
return;
|
|
}
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string keysRaw = await Utils.GetUserKeys(id);
|
|
if (string.IsNullOrEmpty(keysRaw))
|
|
{
|
|
await context.Response.WriteAsync("");
|
|
return;
|
|
}
|
|
Universal2String keys = JsonSerializer.Deserialize(
|
|
keysRaw,
|
|
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("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
if (!query.TryGetValue("e", out var entry))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Utils.IsUserLocal(idQuery, out string domain)) //federation :(
|
|
{
|
|
return;
|
|
}
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
|
|
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("id", out var idQuery))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string body = await LoadBody(bodyReader);
|
|
string password = await Utils.GetPassword(id);
|
|
body = await Utils.NonceDecryptBody(id, password, body, false);
|
|
secret = await Utils.NonceDecryptBody(id, 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;
|
|
}
|
|
|
|
string[] actionRequests = {
|
|
"user/dm/invite", "user/dm/invite/revoke", "user/dm/invite/decline", "user/dm/create",
|
|
"dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update",
|
|
"user/storage/public/update", "user/storage/private/update", "dm/read"
|
|
};
|
|
if (System.Array.IndexOf(actionRequests, serializedBody.string1) >= 0)
|
|
{
|
|
if (IsActionRateLimited(id))
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password));
|
|
return;
|
|
}
|
|
}
|
|
|
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
|
await userLock.WaitAsync();
|
|
try
|
|
{
|
|
switch (serializedBody.string1)
|
|
{
|
|
case "user/invites/sent":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Utils.GetUserSentInvites(id)
|
|
, password));
|
|
break;
|
|
case "user/invites/received":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Utils.GetUserReceivedInvites(id)
|
|
, password));
|
|
break;
|
|
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/storage/public/update":
|
|
{
|
|
Universal2String storageUpdate = JsonSerializer.Deserialize(
|
|
serializedBody.string2,
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Utils.UpdateUserPublicStorageEntry(id, storageUpdate.string1, storageUpdate.string2)
|
|
, password));
|
|
break;
|
|
}
|
|
case "user/storage/private/update":
|
|
{
|
|
Universal2String storageUpdate = JsonSerializer.Deserialize(
|
|
serializedBody.string2,
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Utils.UpdateUserPrivateStorageEntry(id, storageUpdate.string1, storageUpdate.string2)
|
|
, 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/invite/revoke":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmInviteRevoke(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "user/dm/invite/decline":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmInviteDecline(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;
|
|
case "dm/message/send":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmMessageSend(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/message/delete":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmMessageDelete(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/message/pin/toggle":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmMessagePinToggle(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/message/react/add":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmMessageReactAdd(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/message/react/remove":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmMessageReactRemove(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/messages/get":
|
|
{
|
|
Universal3String msgGet = JsonSerializer.Deserialize(
|
|
serializedBody.string2,
|
|
AppJsonSerializerContext.Default.Universal3String
|
|
);
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Requests.GetDmMessages(id, msgGet.string1, msgGet.string3, msgGet.string2), password));
|
|
break;
|
|
}
|
|
case "dm/messages/pinned":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.DmMessageGetPinned(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/read":
|
|
{
|
|
Universal2String readGet = JsonSerializer.Deserialize(
|
|
serializedBody.string2,
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
await Account.Utils.UpdateUserDm(id, readGet.string1, "true", "");
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("success:dm.read", password));
|
|
break;
|
|
}
|
|
case "dm/key/get":
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.GetDmKey(id, serializedBody.string2)
|
|
, password));
|
|
break;
|
|
case "dm/key/update":
|
|
{
|
|
Universal2String dmKeyUpdate = JsonSerializer.Deserialize(
|
|
serializedBody.string2,
|
|
AppJsonSerializerContext.Default.Universal2String
|
|
);
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Room.Requests.UpdateDmKey(id, dmKeyUpdate.string1, dmKeyUpdate.string2)
|
|
, password));
|
|
break;
|
|
}
|
|
default:
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(
|
|
$"error:unknown.request:{serializedBody.string1}"
|
|
, password));
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
return;
|
|
}
|
|
public static async Task Upload(HttpContext context, Func<Task> next, IQueryCollection query)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery))
|
|
return;
|
|
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret))
|
|
return;
|
|
|
|
if (!context.Request.Headers.TryGetValue("target", out var targetHeader))
|
|
return;
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string password = await Utils.GetPassword(id);
|
|
|
|
string target = await Utils.NonceDecryptBody(id, password, targetHeader, false);
|
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
|
|
|
string auth = await Utils.Auth(id, secretStr, password);
|
|
|
|
if (auth != Utils.LOGIN_SUCCESS)
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secretStr));
|
|
return;
|
|
}
|
|
|
|
if (IsActionRateLimited(id))
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password));
|
|
return;
|
|
}
|
|
|
|
byte[] bodyBytes = await LarpixServer.Utils.Utils.LoadBodyBytes(context.Request.Body, LarpixServer.Utils.Utils.PUBLIC_STORAGE_LIMIT);
|
|
byte[] fileBytes = await Utils.NonceDecryptBytes(id, password, bodyBytes, true);
|
|
|
|
if (fileBytes == null)
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:invalid.nonce", password));
|
|
return;
|
|
}
|
|
|
|
SemaphoreSlim userLock = Utils.GetUserLock(id);
|
|
await userLock.WaitAsync();
|
|
try
|
|
{
|
|
await context.Response.WriteAsync(
|
|
Encryption.Encryption.EncryptString(
|
|
await Utils.UpdateUserPublicStorageEntryRaw(id, target, fileBytes)
|
|
, password));
|
|
}
|
|
finally
|
|
{
|
|
userLock.Release();
|
|
}
|
|
}
|
|
public static async Task UploadDmAttachmentStaging(HttpContext context, Func<Task> next, IQueryCollection query)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery)) return;
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret)) return;
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string password = await Utils.GetPassword(id);
|
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
|
string auth = await Utils.Auth(id, secretStr, password);
|
|
|
|
if (auth != Utils.LOGIN_SUCCESS)
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(auth, secretStr));
|
|
return;
|
|
}
|
|
|
|
if (IsActionRateLimited(id))
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:rate.limit", password));
|
|
return;
|
|
}
|
|
|
|
byte[] fileBytes = await LoadBodyBytes(context.Request.Body, ATTACHMENT_SIZE_LIMIT); // attachment size limit
|
|
if (fileBytes == null || fileBytes.Length == 0)
|
|
{
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString("error:empty.body", password));
|
|
return;
|
|
}
|
|
|
|
string stagingId = Guid.NewGuid().ToString();
|
|
string stagingDir = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms_staging";
|
|
string filePath = $"{stagingDir}/{stagingId}";
|
|
|
|
await Fs.WriteFile(filePath, fileBytes);
|
|
|
|
// Fire and forget 10 min timer to delete
|
|
_ = Task.Run(async () =>
|
|
{
|
|
await Task.Delay(TimeSpan.FromMinutes(10));
|
|
if (Fs.Exists(filePath))
|
|
{
|
|
Fs.DeleteFile(filePath);
|
|
}
|
|
});
|
|
|
|
await context.Response.WriteAsync(Encryption.Encryption.EncryptString($"success:{stagingId}", password));
|
|
}
|
|
|
|
public static async Task GetDmAttachment(HttpContext context, Func<Task> next, IQueryCollection query)
|
|
{
|
|
if (!query.TryGetValue("id", out var idQuery)) return;
|
|
if (!query.TryGetValue("dm", out var dmQuery)) return;
|
|
if (!query.TryGetValue("msg", out var msgQuery)) return;
|
|
if (!query.TryGetValue("att", out var attQuery)) return;
|
|
if (!context.Request.Headers.TryGetValue("secret", out var secret)) return;
|
|
|
|
string id = Utils.GetValidIdOrZero(idQuery.ToString());
|
|
string password = await Utils.GetPassword(id);
|
|
string secretStr = await Utils.NonceDecryptBody(id, password, secret, false);
|
|
string auth = await Utils.Auth(id, secretStr, password);
|
|
|
|
if (auth != Utils.LOGIN_SUCCESS) return; // Don't leak info
|
|
|
|
string dmId = dmQuery.ToString();
|
|
string msgId = msgQuery.ToString();
|
|
string attId = attQuery.ToString();
|
|
|
|
// Check if user is in DM
|
|
if (!await Room.Requests.IsMemberOfDm(id, dmId)) return;
|
|
|
|
string filePath = $"{LarpixServer.Utils.Utils.ROOMS_DIR}/dms/{LarpixServer.Utils.Utils.DOMAIN}/{dmId}/attachments/{msgId}/{attId}";
|
|
if (!Fs.Exists(filePath))
|
|
{
|
|
context.Response.StatusCode = 404;
|
|
return;
|
|
}
|
|
|
|
byte[] fileBytes = await Fs.ReadFile(filePath);
|
|
context.Response.ContentType = "application/octet-stream";
|
|
await context.Response.Body.WriteAsync(fileBytes, 0, fileBytes.Length);
|
|
}
|
|
}
|
|
|
|
public class CreateHolder
|
|
{
|
|
public DateTimeOffset date;
|
|
public string name;
|
|
public string pass;
|
|
public string captcha;
|
|
public X25519PrivateKeyParameters privX25519Server;
|
|
public MLKemPrivateKeyParameters privMlKemServer;
|
|
} |