From 4267e0d864098cd2722713815cc6bdd05a24ac63 Mon Sep 17 00:00:00 2001 From: olcxja Date: Mon, 10 Aug 2026 02:17:27 +0200 Subject: [PATCH] bug fixes --- LarpixServer/Account/Requests.cs | 6 +- LarpixServer/Account/Utils.cs | 105 +++++++++++--- LarpixServer/Filesystem/Fs.cs | 30 ++-- LarpixServer/Room/Groups.cs | 231 +++++++++++++++++++++++++------ LarpixServer/Room/Requests.cs | 17 +++ LarpixServer/Room/Shared.cs | 134 ++++++++++-------- 6 files changed, 382 insertions(+), 141 deletions(-) diff --git a/LarpixServer/Account/Requests.cs b/LarpixServer/Account/Requests.cs index 8b7e840..7080d5b 100644 --- a/LarpixServer/Account/Requests.cs +++ b/LarpixServer/Account/Requests.cs @@ -827,11 +827,11 @@ public static async Task Auth(HttpContext context, Func next, IQueryCollec } case "group/invite": { - Universal2String payload = JsonSerializer.Deserialize( + Universal3String payload = JsonSerializer.Deserialize( serializedBody.string2, - AppJsonSerializerContext.Default.Universal2String + AppJsonSerializerContext.Default.Universal3String ); - await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInvite(id, payload.string1, payload.string2), password)); + await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInvite(id, payload.string1, payload.string2, payload.string3), password)); break; } case "group/invite/revoke": diff --git a/LarpixServer/Account/Utils.cs b/LarpixServer/Account/Utils.cs index 2411e59..f0da0f6 100644 --- a/LarpixServer/Account/Utils.cs +++ b/LarpixServer/Account/Utils.cs @@ -256,7 +256,23 @@ public class Utils string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")); if (!first) groupBuilder.Append(','); first = false; - groupBuilder.Append($"\"{groupId};{userId}\": {{ \"timestamp\": {content}, \"receiver\": \"{userId}\", \"groupId\": \"{groupId}\" }}"); + + string timestampStr; + string keysPayload = ""; + if (content.StartsWith("{")) + { + var data = JsonSerializer.Deserialize(content, LarpixServer.Utils.Jsons.AppJsonSerializerContext.Default.Universal2String); + timestampStr = data.string1; + if (!string.IsNullOrEmpty(data.string2)) { + keysPayload = $", \"keysPayload\": {JsonSerializer.Serialize(data.string2, LarpixServer.Utils.Jsons.AppJsonSerializerContext.Default.String)}"; + } + } + else + { + timestampStr = content; + } + + groupBuilder.Append($"\"{groupId};{userId}\": {{ \"timestamp\": {timestampStr}, \"receiver\": \"{userId}\", \"groupId\": \"{groupId}\"{keysPayload} }}"); } } groupBuilder.Append("}"); @@ -297,7 +313,23 @@ public class Utils string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")); if (!first) groupBuilder.Append(','); first = false; - groupBuilder.Append($"\"{groupId};{userId}\": {{ \"timestamp\": {content}, \"sender\": \"{userId}\", \"groupId\": \"{groupId}\" }}"); + + string timestampStr; + string keysPayload = ""; + if (content.StartsWith("{")) + { + var data = JsonSerializer.Deserialize(content, LarpixServer.Utils.Jsons.AppJsonSerializerContext.Default.Universal2String); + timestampStr = data.string1; + if (!string.IsNullOrEmpty(data.string2)) { + keysPayload = $", \"keysPayload\": {JsonSerializer.Serialize(data.string2, LarpixServer.Utils.Jsons.AppJsonSerializerContext.Default.String)}"; + } + } + else + { + timestampStr = content; + } + + groupBuilder.Append($"\"{groupId};{userId}\": {{ \"timestamp\": {timestampStr}, \"sender\": \"{userId}\", \"groupId\": \"{groupId}\"{keysPayload} }}"); } } groupBuilder.Append("}"); @@ -347,15 +379,24 @@ public class Utils string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public"; string path = $"{dirPath}/{entry}"; - long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - - if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) + SemaphoreSlim userLock = GetUserLock(id); + await userLock.WaitAsync(); + try { - return "error:storage.limit.exceeded"; - } + long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - await Fs.WriteFile(path, contentBytes); + if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) + { + return "error:storage.limit.exceeded"; + } + + await Fs.WriteFile(path, contentBytes); + } + finally + { + userLock.Release(); + } return "success:storage.entry.updated"; } @@ -377,15 +418,24 @@ public class Utils string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public"; string path = $"{dirPath}/{entry}"; - long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - - if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) + SemaphoreSlim userLock = GetUserLock(id); + await userLock.WaitAsync(); + try { - return "error:storage.limit.exceeded"; - } + long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - await Fs.WriteFile(path, contentBytes); + if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT) + { + return "error:storage.limit.exceeded"; + } + + await Fs.WriteFile(path, contentBytes); + } + finally + { + userLock.Release(); + } return "success:storage.entry.updated"; } @@ -421,15 +471,24 @@ public class Utils string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/private"; string path = $"{dirPath}/{entry}"; - long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - - if (currentTotalSize - fileExistingSize + contentBytes.Length > PRIVATE_STORAGE_LIMIT) + SemaphoreSlim userLock = GetUserLock(id); + await userLock.WaitAsync(); + try { - return "error:storage.limit.exceeded"; - } + long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0; - await Fs.WriteFile(path, contentBytes); + if (currentTotalSize - fileExistingSize + contentBytes.Length > PRIVATE_STORAGE_LIMIT) + { + return "error:storage.limit.exceeded"; + } + + await Fs.WriteFile(path, contentBytes); + } + finally + { + userLock.Release(); + } return "success:storage.entry.updated"; } diff --git a/LarpixServer/Filesystem/Fs.cs b/LarpixServer/Filesystem/Fs.cs index 2d69a7d..ebacd4e 100644 --- a/LarpixServer/Filesystem/Fs.cs +++ b/LarpixServer/Filesystem/Fs.cs @@ -37,25 +37,33 @@ public class Fs public static void ProcessCacheSpace() { - while (fileCache.Count >= FILE_CACHE_SIZE) + if (fileCache.Count >= FILE_CACHE_SIZE) { - var firstKey = fileCache.Keys.FirstOrDefault(); - if (firstKey != null) fileCache.TryRemove(firstKey, out _); + foreach (var kvp in fileCache) + { + fileCache.TryRemove(kvp.Key, out _); + if (fileCache.Count < FILE_CACHE_SIZE) break; + } } - while (existCache.Count >= EXIST_CACHE_SIZE) + if (existCache.Count >= EXIST_CACHE_SIZE) { - var firstKey = existCache.Keys.FirstOrDefault(); - if (firstKey != null) existCache.TryRemove(firstKey, out _); + foreach (var kvp in existCache) + { + existCache.TryRemove(kvp.Key, out _); + if (existCache.Count < EXIST_CACHE_SIZE) break; + } } - while (dirCache.Count >= DIR_CACHE_SIZE) + if (dirCache.Count >= DIR_CACHE_SIZE) { - var firstKey = dirCache.Keys.FirstOrDefault(); - if (firstKey != null) dirCache.TryRemove(firstKey, out _); - } - + foreach (var kvp in dirCache) + { + dirCache.TryRemove(kvp.Key, out _); + if (dirCache.Count < DIR_CACHE_SIZE) break; + } } + } public static ulong ClearCache(string pattern) { diff --git a/LarpixServer/Room/Groups.cs b/LarpixServer/Room/Groups.cs index 5967d88..bad84bc 100644 --- a/LarpixServer/Room/Groups.cs +++ b/LarpixServer/Room/Groups.cs @@ -55,7 +55,8 @@ public class Groups public static async Task GroupCreate(string id, string name, string body, string historyVisible = "false") { - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(id); + await roomLock.WaitAsync(); try { string groupsDir = $"{ROOMS_DIR}/groups"; @@ -112,7 +113,7 @@ public class Groups } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } } @@ -156,11 +157,12 @@ public class Groups if (!Fs.Exists(groupPath)) return "error:group.not.found"; string memberPath = $"{groupPath}/members/{userId};{Utils.Utils.DOMAIN}"; - if (!Fs.Exists(memberPath)) return "error:not.a.member"; - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); try { + if (!Fs.Exists(memberPath)) return "error:not.a.member"; if (!string.IsNullOrEmpty(name)) { if (string.IsNullOrWhiteSpace(name)) return "error:invalid.name"; @@ -240,7 +242,7 @@ public class Groups } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } return "success"; @@ -297,7 +299,7 @@ public class Groups return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal6String); } - public static async Task GroupInvite(string id, string groupId, string targetId) + public static async Task GroupInvite(string id, string groupId, string targetId, string keysPayload = "") { bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain); string tId = Account.Utils.GetValidIdOrZero(targetId); @@ -305,6 +307,54 @@ public class Groups string groupPath = GetGroupPath(groupId); if (!Fs.Exists(groupPath)) return "error:group.not.found"; + // Validate keys payload based on historyVisible + bool historyVisible = false; + string historyFile = $"{groupPath}/history_visible"; + if (Fs.Exists(historyFile)) + { + string histContent = Encoding.UTF8.GetString(await Fs.ReadFile(historyFile)); + historyVisible = (histContent == "true"); + } + + if (!historyVisible) + { + if (!string.IsNullOrEmpty(keysPayload)) + { + return "error:history.hidden.keys.provided"; + } + } + else + { + if (string.IsNullOrEmpty(keysPayload)) return "error:missing.keys"; + + System.Collections.Generic.Dictionary keysDict = null; + try + { + keysDict = JsonSerializer.Deserialize>(keysPayload, AppJsonSerializerContext.Default.DictionaryStringString); + } + catch { } + + if (keysDict == null) return "error:missing.keys"; + + string lastKeyFile = $"{groupPath}/keys/last"; + if (Fs.Exists(lastKeyFile)) + { + string lastKeyIndexStr = Encoding.UTF8.GetString(await Fs.ReadFile(lastKeyFile)); + if (int.TryParse(lastKeyIndexStr, out int lastKeyIndex)) + { + for (int i = 0; i <= lastKeyIndex; i++) + { + if (!keysDict.ContainsKey(i.ToString())) + { + return "error:missing.keys"; + } + } + } + else return "error:missing.keys"; + } + else return "error:missing.keys"; + } + // Check if inviter is in group if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group"; @@ -323,9 +373,15 @@ public class Groups string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv/{groupId};{id}"; if (Fs.Exists(inviteFile)) return "info:user.already.invited"; - byte[] timestamp = Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); + LarpixServer.Utils.Jsons.Universal2String inviteData = new LarpixServer.Utils.Jsons.Universal2String + { + string1 = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(), + string2 = keysPayload ?? "" + }; + byte[] inviteBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(inviteData, AppJsonSerializerContext.Default.Universal2String)); + Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent"); - await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent/{groupId};{id2}", timestamp); + await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent/{groupId};{id2}", inviteBytes); _ = Task.Run(async () => { @@ -334,7 +390,7 @@ public class Groups try { Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv"); - await Fs.WriteFile(inviteFile, timestamp); + await Fs.WriteFile(inviteFile, inviteBytes); } finally { @@ -405,14 +461,17 @@ public class Groups if (iId == "0") return "error:invalid.user"; string inviteFile = ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/recv/{groupId};{iId}"; - if (!Fs.Exists(inviteFile)) return "error:no.invite"; string groupPath = GetGroupPath(groupId); if (!Fs.Exists(groupPath)) return "error:group.not.found"; - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); try { + if (!Fs.Exists(inviteFile)) return "error:no.invite"; + string inviteContent = Encoding.UTF8.GetString(await Fs.ReadFile(inviteFile)); + // Delete the invites Fs.DeleteFile(inviteFile); if (Account.Utils.IsUserLocal(inviterId, out string domain)) @@ -423,13 +482,54 @@ public class Groups // Add to members await Fs.WriteFile($"{groupPath}/members/{id};{DOMAIN}", Array.Empty()); - // Require new key if history is not visible + // Read invite data + string keysPayload = ""; + if (inviteContent.StartsWith("{")) + { + try + { + var data = JsonSerializer.Deserialize(inviteContent, LarpixServer.Utils.Jsons.AppJsonSerializerContext.Default.Universal2String); + if (data != null) + { + keysPayload = data.string2 ?? ""; + } + } + catch { } + } + + // Require new key if history is not visible or no keys were provided bool historyVisible = false; if (Fs.Exists($"{groupPath}/history_visible")) { historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")) == "true"; } - if (!historyVisible) + + if (historyVisible && !string.IsNullOrEmpty(keysPayload)) + { + System.Collections.Generic.Dictionary keysDict = null; + try + { + keysDict = JsonSerializer.Deserialize>(keysPayload, AppJsonSerializerContext.Default.DictionaryStringString); + } + catch { } + + if (keysDict != null) + { + foreach (var kvp in keysDict) + { + string keyIndex = kvp.Key; + string keyData = kvp.Value; + string newKeyDir = $"{groupPath}/keys/{keyIndex}"; + Fs.CreateDirectory(newKeyDir); + await Fs.WriteFile($"{newKeyDir}/{id};{DOMAIN}", Encoding.UTF8.GetBytes(keyData)); + } + } + else + { + await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); + } + } + else { await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); } @@ -454,11 +554,26 @@ public class Groups await UpdateUserGroup(id, groupId, "true", timestamp); + // Notify all members (including the new one) via WebSocket + string[] currentMembers = Fs.ReadDirectory($"{groupPath}/members"); + foreach (string memberWD in currentMembers) + { + string memberStr = Path.GetFileName(memberWD); + string memberIdStr = Account.Utils.GetValidIdOrZero(memberStr); + if (Account.Utils.IsUserLocal(memberStr, out string d)) + { + _ = Task.Run(async () => + { + await Utils.Utils.SendWsMessage(memberIdStr, $"group_message:{groupId}:unread"); + }); + } + } + return "success:joined"; } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } } @@ -492,11 +607,13 @@ public class Groups string groupPath = GetGroupPath(groupId); if (!Fs.Exists(groupPath)) return "error:group.not.found"; - if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group"; - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); try { + if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group"; + // 1. Check if newrequired is true string newRequiredPath = $"{groupPath}/keys/newrequired"; if (!Fs.Exists(newRequiredPath) || Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath)) != "true") @@ -549,7 +666,7 @@ public class Groups } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } } @@ -591,11 +708,20 @@ public class Groups catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); } string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/public"; string path = $"{dirPath}/{entry}"; - Fs.CreateDirectory(dirPath); - long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = Fs.Exists(path) ? new System.IO.FileInfo(path).Length : 0; - if (currentTotalSize - fileExistingSize + contentBytes.Length > Utils.Utils.PUBLIC_STORAGE_LIMIT) return "error:storage.limit.exceeded"; - await Fs.WriteFile(path, contentBytes); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); + try + { + Fs.CreateDirectory(dirPath); + long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = Fs.Exists(path) ? new System.IO.FileInfo(path).Length : 0; + if (currentTotalSize - fileExistingSize + contentBytes.Length > Utils.Utils.PUBLIC_STORAGE_LIMIT) return "error:storage.limit.exceeded"; + await Fs.WriteFile(path, contentBytes); + } + finally + { + roomLock.Release(); + } return "success:storage.entry.updated"; } public static async Task GetGroupPrivateStorageEntry(string userId, string groupId, string entry) @@ -623,11 +749,20 @@ public class Groups catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); } string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/private"; string path = $"{dirPath}/{entry}"; - Fs.CreateDirectory(dirPath); - long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath); - long fileExistingSize = Fs.Exists(path) ? new System.IO.FileInfo(path).Length : 0; - if (currentTotalSize - fileExistingSize + contentBytes.Length > Utils.Utils.PRIVATE_STORAGE_LIMIT) return "error:storage.limit.exceeded"; - await Fs.WriteFile(path, contentBytes); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); + try + { + Fs.CreateDirectory(dirPath); + long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath); + long fileExistingSize = Fs.Exists(path) ? new System.IO.FileInfo(path).Length : 0; + if (currentTotalSize - fileExistingSize + contentBytes.Length > Utils.Utils.PRIVATE_STORAGE_LIMIT) return "error:storage.limit.exceeded"; + await Fs.WriteFile(path, contentBytes); + } + finally + { + roomLock.Release(); + } return "success:storage.entry.updated"; } @@ -640,7 +775,8 @@ public class Groups if (!Fs.Exists(groupPath)) return "error:group.not.found"; - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); try { string membersDir = $"{groupPath}/members"; @@ -661,7 +797,7 @@ public class Groups } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } return "success"; @@ -678,12 +814,14 @@ public class Groups return "error:group.not.found"; string memberPath = $"{groupPath}/members/{userId};{Utils.Utils.DOMAIN}"; - if (!Fs.Exists(memberPath)) - return "error:not.a.member"; - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); + await roomLock.WaitAsync(); try { + if (!Fs.Exists(memberPath)) + return "error:not.a.member"; + // Remove from members Fs.DeleteFile(memberPath); @@ -694,16 +832,8 @@ public class Groups Fs.DeleteFile(userGroupPath); } - // Require new key if history is not visible - bool historyVisible = false; - if (Fs.Exists($"{groupPath}/history_visible")) - { - historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")) == "true"; - } - if (!historyVisible) - { - await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); - } + // Always require new key when someone leaves + await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); // Add info message Message msg = new(); @@ -724,10 +854,25 @@ public class Groups } await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(msgId.ToString())); await Fs.WriteFile($"{groupPath}/messages/{msgId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); + + // Notify remaining members via WebSocket + string[] currentMembers = Fs.ReadDirectory($"{groupPath}/members"); + foreach (string memberWD in currentMembers) + { + string memberStr = Path.GetFileName(memberWD); + string memberIdStr = Account.Utils.GetValidIdOrZero(memberStr); + if (Account.Utils.IsUserLocal(memberStr, out string d)) + { + _ = Task.Run(async () => + { + await Utils.Utils.SendWsMessage(memberIdStr, $"group_message:{groupId}:unread"); + }); + } + } } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } // If members count == 0, delete group folder and we are done. diff --git a/LarpixServer/Room/Requests.cs b/LarpixServer/Room/Requests.cs index 5fbafe1..57d8d57 100644 --- a/LarpixServer/Room/Requests.cs +++ b/LarpixServer/Room/Requests.cs @@ -10,6 +10,23 @@ namespace LarpixServer.Room; public class Requests { public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1); + + private static SemaphoreSlim[]? _roomLocksArray = null; + + public static SemaphoreSlim GetRoomLock(string roomId) + { + if (_roomLocksArray == null) + { + int size = LOCK_SIZE > 0 ? LOCK_SIZE : 65536; + var newArray = Enumerable.Range(0, size).Select(_ => new SemaphoreSlim(1, 1)).ToArray(); + Interlocked.CompareExchange(ref _roomLocksArray, newArray, null); + } + + int hash = roomId.GetHashCode(); + int index = (hash & 0x7FFFFFFF) % _roomLocksArray.Length; + return _roomLocksArray[index]; + } + public const int MAX_DM_KEY_SIZE = 8192; private static bool IsSafeDmId(string dmId) diff --git a/LarpixServer/Room/Shared.cs b/LarpixServer/Room/Shared.cs index 0a6e4be..339af72 100644 --- a/LarpixServer/Room/Shared.cs +++ b/LarpixServer/Room/Shared.cs @@ -122,45 +122,46 @@ public class Shared public static async Task MessageSend(string id, string roomType, string roomId, string content, string attachment, string responded, string keyIndex = "0") { - if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; - - if (roomType == "groups") - { - string newRequiredPath = $"{Groups.GetGroupPath(roomId)}/keys/newrequired"; - if (Fs.Exists(newRequiredPath)) - { - string isRequired = Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath)); - if (isRequired == "true") - { - return "error:key.rotation.required"; - } - } - - // Check if keyIndex matches last key - string lastKeyPath = $"{Groups.GetGroupPath(roomId)}/keys/last"; - if (Fs.Exists(lastKeyPath)) - { - string lastKey = Encoding.UTF8.GetString(await Fs.ReadFile(lastKeyPath)); - if (keyIndex != lastKey && keyIndex != "") // empty key means larp.info messages - { - return "error:invalid.key.index"; - } - } - } - - if (content != null && content.Length > LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE) - { - string sizeStr = LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE >= 1024 ? $"{LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE / 1024.0:0.##} KB" : $"{LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE} B"; - return $"error:message.too.long:{sizeStr}"; - } - string msgDir = $"{GetRoomPath(roomType, roomId)}/messages"; long nextId = 0; Message msg = new Message(); - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); + await roomLock.WaitAsync(); try { + if (!await IsMemberOfRoom(id, roomType, roomId)) return "error:forbidden"; + + if (roomType == "groups") + { + string newRequiredPath = $"{Groups.GetGroupPath(roomId)}/keys/newrequired"; + if (Fs.Exists(newRequiredPath)) + { + string isRequired = Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath)); + if (isRequired == "true") + { + return "error:key.rotation.required"; + } + } + + // Check if keyIndex matches last key + string lastKeyPath = $"{Groups.GetGroupPath(roomId)}/keys/last"; + if (Fs.Exists(lastKeyPath)) + { + string lastKey = Encoding.UTF8.GetString(await Fs.ReadFile(lastKeyPath)); + if (keyIndex != lastKey && keyIndex != "") // empty key means larp.info messages + { + return "error:invalid.key.index"; + } + } + } + + if (content != null && content.Length > LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE) + { + string sizeStr = LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE >= 1024 ? $"{LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE / 1024.0:0.##} KB" : $"{LarpixServer.Utils.Utils.MAX_MESSAGE_SIZE} B"; + return $"error:message.too.long:{sizeStr}"; + } + long last = 0; if (Fs.Exists($"{msgDir}/last")) { @@ -218,7 +219,7 @@ public class Shared } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); @@ -266,32 +267,41 @@ public class Shared string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}"; if (!Fs.Exists(msgPath)) return "error:message.not.found"; - Message msg = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(await Fs.ReadFile(msgPath)), - AppJsonSerializerContext.Default.Message - ); - - if (msg.author != id) return "error:forbidden"; - if (msg.type == "larp.redacted") return "error:message.already.deleted"; - - msg.type = "larp.redacted"; - msg.reactions = ""; - msg.content = "{blah(larp.redacted)}"; - msg.attachment = ""; - msg.key = ""; - - await Fs.WriteFile(msgPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); - - string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}"; - if (Fs.Exists(reactDir)) + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); + await roomLock.WaitAsync(); + try { - Fs.DeleteDirectory(reactDir); + Message msg = JsonSerializer.Deserialize( + Encoding.UTF8.GetString(await Fs.ReadFile(msgPath)), + AppJsonSerializerContext.Default.Message + ); + + if (msg.author != id) return "error:forbidden"; + if (msg.type == "larp.redacted") return "error:message.already.deleted"; + + msg.type = "larp.redacted"; + msg.reactions = ""; + msg.content = "{blah(larp.redacted)}"; + msg.attachment = ""; + msg.key = ""; + + await Fs.WriteFile(msgPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg, AppJsonSerializerContext.Default.Message))); + + string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}"; + if (Fs.Exists(reactDir)) + { + Fs.DeleteDirectory(reactDir); + } + + string attDir = $"{GetRoomPath(roomType, roomId)}/attachments/{msgId}"; + if (Fs.Exists(attDir)) + { + Fs.DeleteDirectory(attDir); + } } - - string attDir = $"{GetRoomPath(roomType, roomId)}/attachments/{msgId}"; - if (Fs.Exists(attDir)) + finally { - Fs.DeleteDirectory(attDir); + roomLock.Release(); } string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); @@ -331,7 +341,8 @@ public class Shared string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}"; if (!Fs.Exists(reactDir)) Fs.CreateDirectory(reactDir); - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); + await roomLock.WaitAsync(); try { long last = 0; @@ -346,7 +357,7 @@ public class Shared } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); @@ -411,7 +422,8 @@ public class Shared string pinnedFilePath = $"{pinnedDir}/{msgId}"; bool isNowPinned = false; - await Room.Requests.createLock.WaitAsync(); + SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); + await roomLock.WaitAsync(); try { if (Fs.Exists(pinnedFilePath)) @@ -450,7 +462,7 @@ public class Shared } finally { - Room.Requests.createLock.Release(); + roomLock.Release(); } string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");