Compare commits

..

No commits in common. "4267e0d864098cd2722713815cc6bdd05a24ac63" and "f7be24e61511caedc69cc5a1ec49b8504e77aaf4" have entirely different histories.

8 changed files with 126 additions and 548 deletions

View file

@ -666,7 +666,7 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
"dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update", "dm/message/pin/toggle", "dm/message/send", "dm/message/delete", "dm/message/react", "dm/key/update", "dm/message/pin/toggle",
"dm/message/react/add", "dm/message/react/remove", "dm/message/react/add", "dm/message/react/remove",
"user/storage/public/update", "user/storage/private/update", "dm/read", "user/storage/public/update", "user/storage/private/update", "dm/read",
"group/create", "user/group/list", "group/info", "group/settings/update", "group/create", "user/group/list", "group/info",
"user/invite/accept", "user/invite/decline", "user/invite/revoke", "user/dm/list", "user/group/list", "user/invite/accept", "user/invite/decline", "user/invite/revoke", "user/dm/list", "user/group/list",
"user/storage/public/update", "user/storage/private/update", "group/storage/public/update", "group/storage/private/update", "group/storage/private/getentry", "dm/read", "group/read", "user/storage/public/update", "user/storage/private/update", "group/storage/public/update", "group/storage/private/update", "group/storage/private/getentry", "dm/read", "group/read",
"dm/message/send", "dm/message/delete", "dm/message/react/add", "dm/message/react/remove", "dm/message/pin/toggle", "dm/message/send", "dm/message/delete", "dm/message/react/add", "dm/message/react/remove", "dm/message/pin/toggle",
@ -806,32 +806,23 @@ public static async Task Auth(HttpContext context, Func<Task> next, IQueryCollec
break; break;
case "group/create": case "group/create":
{ {
Universal3String groupCreate = JsonSerializer.Deserialize( Universal2String groupCreate = JsonSerializer.Deserialize(
serializedBody.string2, serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String AppJsonSerializerContext.Default.Universal2String
); );
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2, groupCreate.string3), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupCreate(id, groupCreate.string1, groupCreate.string2), password));
break; break;
} }
case "group/info": case "group/info":
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInfo(serializedBody.string2), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInfo(serializedBody.string2), password));
break; break;
case "group/settings/update":
{
Universal3String req = JsonSerializer.Deserialize(
serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String
);
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupSettingsUpdate(id, req.string1, req.string2, req.string3), password));
break;
}
case "group/invite": case "group/invite":
{ {
Universal3String payload = JsonSerializer.Deserialize( Universal2String payload = JsonSerializer.Deserialize(
serializedBody.string2, serializedBody.string2,
AppJsonSerializerContext.Default.Universal3String AppJsonSerializerContext.Default.Universal2String
); );
await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInvite(id, payload.string1, payload.string2, payload.string3), password)); await context.Response.WriteAsync(Encryption.Encryption.EncryptString(await Room.Groups.GroupInvite(id, payload.string1, payload.string2), password));
break; break;
} }
case "group/invite/revoke": case "group/invite/revoke":

View file

@ -256,23 +256,7 @@ public class Utils
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")); string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (!first) groupBuilder.Append(','); if (!first) groupBuilder.Append(',');
first = false; first = false;
groupBuilder.Append($"\"{groupId};{userId}\": {{ \"timestamp\": {content}, \"receiver\": \"{userId}\", \"groupId\": \"{groupId}\" }}");
string timestampStr;
string keysPayload = "";
if (content.StartsWith("{"))
{
var data = JsonSerializer.Deserialize<LarpixServer.Utils.Jsons.Universal2String>(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("}"); groupBuilder.Append("}");
@ -313,23 +297,7 @@ public class Utils
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")); string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (!first) groupBuilder.Append(','); if (!first) groupBuilder.Append(',');
first = false; first = false;
groupBuilder.Append($"\"{groupId};{userId}\": {{ \"timestamp\": {content}, \"sender\": \"{userId}\", \"groupId\": \"{groupId}\" }}");
string timestampStr;
string keysPayload = "";
if (content.StartsWith("{"))
{
var data = JsonSerializer.Deserialize<LarpixServer.Utils.Jsons.Universal2String>(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("}"); groupBuilder.Append("}");
@ -379,24 +347,15 @@ public class Utils
string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public"; string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public";
string path = $"{dirPath}/{entry}"; string path = $"{dirPath}/{entry}";
SemaphoreSlim userLock = GetUserLock(id); long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath);
await userLock.WaitAsync(); long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0;
try
{
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) if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT)
{
return "error:storage.limit.exceeded";
}
await Fs.WriteFile(path, contentBytes);
}
finally
{ {
userLock.Release(); return "error:storage.limit.exceeded";
} }
await Fs.WriteFile(path, contentBytes);
return "success:storage.entry.updated"; return "success:storage.entry.updated";
} }
@ -418,24 +377,15 @@ public class Utils
string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public"; string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public";
string path = $"{dirPath}/{entry}"; string path = $"{dirPath}/{entry}";
SemaphoreSlim userLock = GetUserLock(id); long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath);
await userLock.WaitAsync(); long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0;
try
{
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) if (currentTotalSize - fileExistingSize + contentBytes.Length > PUBLIC_STORAGE_LIMIT)
{
return "error:storage.limit.exceeded";
}
await Fs.WriteFile(path, contentBytes);
}
finally
{ {
userLock.Release(); return "error:storage.limit.exceeded";
} }
await Fs.WriteFile(path, contentBytes);
return "success:storage.entry.updated"; return "success:storage.entry.updated";
} }
@ -471,24 +421,15 @@ public class Utils
string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/private"; string dirPath = $"{ACCOUNTS_DATA_DIR}/{id}/storage/private";
string path = $"{dirPath}/{entry}"; string path = $"{dirPath}/{entry}";
SemaphoreSlim userLock = GetUserLock(id); long currentTotalSize = LarpixServer.Utils.Utils.GetDirectorySize(dirPath);
await userLock.WaitAsync(); long fileExistingSize = File.Exists(path) ? new FileInfo(path).Length : 0;
try
{
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) if (currentTotalSize - fileExistingSize + contentBytes.Length > PRIVATE_STORAGE_LIMIT)
{
return "error:storage.limit.exceeded";
}
await Fs.WriteFile(path, contentBytes);
}
finally
{ {
userLock.Release(); return "error:storage.limit.exceeded";
} }
await Fs.WriteFile(path, contentBytes);
return "success:storage.entry.updated"; return "success:storage.entry.updated";
} }

View file

@ -37,33 +37,25 @@ public class Fs
public static void ProcessCacheSpace() public static void ProcessCacheSpace()
{ {
if (fileCache.Count >= FILE_CACHE_SIZE) while (fileCache.Count >= FILE_CACHE_SIZE)
{ {
foreach (var kvp in fileCache) var firstKey = fileCache.Keys.FirstOrDefault();
{ if (firstKey != null) fileCache.TryRemove(firstKey, out _);
fileCache.TryRemove(kvp.Key, out _);
if (fileCache.Count < FILE_CACHE_SIZE) break;
}
} }
if (existCache.Count >= EXIST_CACHE_SIZE) while (existCache.Count >= EXIST_CACHE_SIZE)
{ {
foreach (var kvp in existCache) var firstKey = existCache.Keys.FirstOrDefault();
{ if (firstKey != null) existCache.TryRemove(firstKey, out _);
existCache.TryRemove(kvp.Key, out _);
if (existCache.Count < EXIST_CACHE_SIZE) break;
}
} }
if (dirCache.Count >= DIR_CACHE_SIZE) while (dirCache.Count >= DIR_CACHE_SIZE)
{ {
foreach (var kvp in dirCache) var firstKey = dirCache.Keys.FirstOrDefault();
{ if (firstKey != null) dirCache.TryRemove(firstKey, out _);
dirCache.TryRemove(kvp.Key, out _); }
if (dirCache.Count < DIR_CACHE_SIZE) break;
}
} }
}
public static ulong ClearCache(string pattern) public static ulong ClearCache(string pattern)
{ {

View file

@ -53,10 +53,9 @@ public class Groups
await Fs.WriteFile(groupPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileGroup, AppJsonSerializerContext.Default.Universal3String))); await Fs.WriteFile(groupPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileGroup, AppJsonSerializerContext.Default.Universal3String)));
} }
public static async Task<string> GroupCreate(string id, string name, string body, string historyVisible = "false") public static async Task<string> GroupCreate(string id, string name, string body)
{ {
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(id); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try try
{ {
string groupsDir = $"{ROOMS_DIR}/groups"; string groupsDir = $"{ROOMS_DIR}/groups";
@ -85,7 +84,6 @@ public class Groups
await Fs.WriteFile($"{groupPath}/keys/last", Encoding.UTF8.GetBytes("0")); await Fs.WriteFile($"{groupPath}/keys/last", Encoding.UTF8.GetBytes("0"));
await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("false")); await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("false"));
await Fs.WriteFile($"{groupPath}/name", Encoding.UTF8.GetBytes(name)); await Fs.WriteFile($"{groupPath}/name", Encoding.UTF8.GetBytes(name));
await Fs.WriteFile($"{groupPath}/history_visible", Encoding.UTF8.GetBytes(historyVisible == "true" ? "true" : "false"));
// body is the key encrypted for the creator // body is the key encrypted for the creator
await Fs.WriteFile($"{groupPath}/keys/0/{id};{DOMAIN}", Encoding.UTF8.GetBytes(body)); await Fs.WriteFile($"{groupPath}/keys/0/{id};{DOMAIN}", Encoding.UTF8.GetBytes(body));
@ -113,7 +111,7 @@ public class Groups
} }
finally finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
} }
} }
@ -151,103 +149,6 @@ public class Groups
return $"{ROOMS_DIR}/groups/{domain}/{id}"; return $"{ROOMS_DIR}/groups/{domain}/{id}";
} }
public static async Task<string> GroupSettingsUpdate(string userId, string groupId, string name, string historyVisible)
{
string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found";
string memberPath = $"{groupPath}/members/{userId};{Utils.Utils.DOMAIN}";
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";
await Fs.WriteFile($"{groupPath}/name", Encoding.UTF8.GetBytes(name));
}
if (!string.IsNullOrEmpty(historyVisible))
{
bool currentState = false;
if (Fs.Exists($"{groupPath}/history_visible"))
{
currentState = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible")) == "true";
}
bool newState = historyVisible == "true";
if (currentState != newState)
{
await Fs.WriteFile($"{groupPath}/history_visible", Encoding.UTF8.GetBytes(newState ? "true" : "false"));
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
Message infoMsg = new();
infoMsg.attachment = "";
infoMsg.author = userId;
infoMsg.timestamp = timestamp;
infoMsg.type = "larp.info.history";
infoMsg.content = newState ? "{blah(history.visible)}" : "{blah(history.hidden)}";
infoMsg.key = "";
ulong lastMessageId = 0;
if (Fs.Exists($"{groupPath}/messages/last"))
{
lastMessageId = ulong.Parse(Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/messages/last")));
}
ulong newMessageId = lastMessageId + 1;
infoMsg.pervious = lastMessageId.ToString();
await Fs.WriteFile($"{groupPath}/messages/{newMessageId}", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(infoMsg, AppJsonSerializerContext.Default.Message)));
await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(newMessageId.ToString()));
string[] members = Fs.ReadDirectory($"{groupPath}/members");
foreach (string memberWD in members)
{
string memberStr = Path.GetFileName(memberWD);
bool isLocal = Account.Utils.IsUserLocal(memberStr, out string d);
string memberId = Account.Utils.GetValidIdOrZero(memberStr);
if (isLocal)
{
if (memberId == userId)
{
await UpdateUserGroup(memberId, groupId, "true", timestamp);
await Utils.Utils.SendWsMessage(memberId, $"group_message:{groupId}:read");
}
else
{
_ = Task.Run(async () =>
{
SemaphoreSlim userLock = Account.Utils.GetUserLock(memberId);
await userLock.WaitAsync();
try
{
await UpdateUserGroup(memberId, groupId, "false", timestamp);
}
finally
{
userLock.Release();
}
await Utils.Utils.SendWsMessage(memberId, $"group_message:{groupId}:unread");
});
}
}
}
}
}
}
finally
{
roomLock.Release();
}
return "success";
}
public static async Task<string> GroupInfo(string groupId) public static async Task<string> GroupInfo(string groupId)
{ {
string groupPath = GetGroupPath(groupId); string groupPath = GetGroupPath(groupId);
@ -280,26 +181,19 @@ public class Groups
newRequired = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/keys/newrequired")); newRequired = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/keys/newrequired"));
} }
string historyVisible = "false"; var info = new Universal5String
if (Fs.Exists($"{groupPath}/history_visible"))
{
historyVisible = Encoding.UTF8.GetString(await Fs.ReadFile($"{groupPath}/history_visible"));
}
var info = new Universal6String
{ {
string1 = groupId, string1 = groupId,
string2 = name, string2 = name,
string3 = memberCount, string3 = memberCount,
string4 = lastKeyIndex, string4 = lastKeyIndex,
string5 = newRequired, string5 = newRequired
string6 = historyVisible
}; };
return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal6String); return JsonSerializer.Serialize(info, AppJsonSerializerContext.Default.Universal5String);
} }
public static async Task<string> GroupInvite(string id, string groupId, string targetId, string keysPayload = "") public static async Task<string> GroupInvite(string id, string groupId, string targetId)
{ {
bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain); bool isLocal = Account.Utils.IsUserLocal(targetId, out string domain);
string tId = Account.Utils.GetValidIdOrZero(targetId); string tId = Account.Utils.GetValidIdOrZero(targetId);
@ -307,54 +201,6 @@ public class Groups
string groupPath = GetGroupPath(groupId); string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found"; 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<string, string> keysDict = null;
try
{
keysDict = JsonSerializer.Deserialize<System.Collections.Generic.Dictionary<string, string>>(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 // Check if inviter is in group
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group"; if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
@ -373,15 +219,9 @@ public class Groups
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv/{groupId};{id}"; string inviteFile = ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv/{groupId};{id}";
if (Fs.Exists(inviteFile)) return "info:user.already.invited"; if (Fs.Exists(inviteFile)) return "info:user.already.invited";
LarpixServer.Utils.Jsons.Universal2String inviteData = new LarpixServer.Utils.Jsons.Universal2String byte[] timestamp = Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
{
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"); Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent");
await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent/{groupId};{id2}", inviteBytes); await Fs.WriteFile(ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/sent/{groupId};{id2}", timestamp);
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
@ -390,7 +230,7 @@ public class Groups
try try
{ {
Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv"); Fs.CreateDirectory(ACCOUNTS_DATA_DIR + $"/{id2}/groupinvites/recv");
await Fs.WriteFile(inviteFile, inviteBytes); await Fs.WriteFile(inviteFile, timestamp);
} }
finally finally
{ {
@ -461,17 +301,14 @@ public class Groups
if (iId == "0") return "error:invalid.user"; if (iId == "0") return "error:invalid.user";
string inviteFile = ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/recv/{groupId};{iId}"; string inviteFile = ACCOUNTS_DATA_DIR + $"/{id}/groupinvites/recv/{groupId};{iId}";
if (!Fs.Exists(inviteFile)) return "error:no.invite";
string groupPath = GetGroupPath(groupId); string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found"; if (!Fs.Exists(groupPath)) return "error:group.not.found";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try try
{ {
if (!Fs.Exists(inviteFile)) return "error:no.invite";
string inviteContent = Encoding.UTF8.GetString(await Fs.ReadFile(inviteFile));
// Delete the invites // Delete the invites
Fs.DeleteFile(inviteFile); Fs.DeleteFile(inviteFile);
if (Account.Utils.IsUserLocal(inviterId, out string domain)) if (Account.Utils.IsUserLocal(inviterId, out string domain))
@ -482,57 +319,9 @@ public class Groups
// Add to members // Add to members
await Fs.WriteFile($"{groupPath}/members/{id};{DOMAIN}", Array.Empty<byte>()); await Fs.WriteFile($"{groupPath}/members/{id};{DOMAIN}", Array.Empty<byte>());
// Read invite data // Require new key
string keysPayload = ""; await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true"));
if (inviteContent.StartsWith("{"))
{
try
{
var data = JsonSerializer.Deserialize<LarpixServer.Utils.Jsons.Universal2String>(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 && !string.IsNullOrEmpty(keysPayload))
{
System.Collections.Generic.Dictionary<string, string> keysDict = null;
try
{
keysDict = JsonSerializer.Deserialize<System.Collections.Generic.Dictionary<string, string>>(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"));
}
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); string timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
Message joinMsg = new(); Message joinMsg = new();
@ -554,26 +343,11 @@ public class Groups
await UpdateUserGroup(id, groupId, "true", timestamp); 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"; return "success:joined";
} }
finally finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
} }
} }
@ -607,13 +381,11 @@ public class Groups
string groupPath = GetGroupPath(groupId); string groupPath = GetGroupPath(groupId);
if (!Fs.Exists(groupPath)) return "error:group.not.found"; if (!Fs.Exists(groupPath)) return "error:group.not.found";
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try try
{ {
if (!Fs.Exists($"{groupPath}/members/{id};{DOMAIN}")) return "error:not.in.group";
// 1. Check if newrequired is true // 1. Check if newrequired is true
string newRequiredPath = $"{groupPath}/keys/newrequired"; string newRequiredPath = $"{groupPath}/keys/newrequired";
if (!Fs.Exists(newRequiredPath) || Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath)) != "true") if (!Fs.Exists(newRequiredPath) || Encoding.UTF8.GetString(await Fs.ReadFile(newRequiredPath)) != "true")
@ -666,7 +438,7 @@ public class Groups
} }
finally finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
} }
} }
@ -708,20 +480,11 @@ public class Groups
catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); } catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); }
string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/public"; string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/public";
string path = $"{dirPath}/{entry}"; string path = $"{dirPath}/{entry}";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); Fs.CreateDirectory(dirPath);
await roomLock.WaitAsync(); long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath);
try 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";
Fs.CreateDirectory(dirPath); await Fs.WriteFile(path, contentBytes);
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"; return "success:storage.entry.updated";
} }
public static async Task<string> GetGroupPrivateStorageEntry(string userId, string groupId, string entry) public static async Task<string> GetGroupPrivateStorageEntry(string userId, string groupId, string entry)
@ -749,60 +512,14 @@ public class Groups
catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); } catch { contentBytes = Encoding.UTF8.GetBytes(contentBase64); }
string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/private"; string dirPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}/storage/private";
string path = $"{dirPath}/{entry}"; string path = $"{dirPath}/{entry}";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); Fs.CreateDirectory(dirPath);
await roomLock.WaitAsync(); long currentTotalSize = Utils.Utils.GetDirectorySize(dirPath);
try 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";
Fs.CreateDirectory(dirPath); await Fs.WriteFile(path, contentBytes);
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"; return "success:storage.entry.updated";
} }
public static async Task<string> GroupDelete(string groupId)
{
string[] parts = groupId.Split(';');
string id = parts[0];
string domain = parts.Length > 1 ? parts[1] : Utils.Utils.DOMAIN;
string groupPath = $"{Utils.Utils.ROOMS_DIR}/groups/{domain}/{id}";
if (!Fs.Exists(groupPath)) return "error:group.not.found";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId);
await roomLock.WaitAsync();
try
{
string membersDir = $"{groupPath}/members";
if (Fs.Exists(membersDir))
{
string[] memberFiles = Fs.ReadDirectory(membersDir);
foreach (string memberFile in memberFiles)
{
string memberId = memberFile.Split(';')[0];
string userGroupPath = $"{Utils.Utils.ACCOUNTS_DATA_DIR}/{memberId}/groups/{id};{domain}";
if (Fs.Exists(userGroupPath))
{
Fs.DeleteFile(userGroupPath);
}
}
}
Fs.DeleteDirectory(groupPath);
}
finally
{
roomLock.Release();
}
return "success";
}
public static async Task<string> GroupLeave(string userId, string groupId) public static async Task<string> GroupLeave(string userId, string groupId)
{ {
string[] parts = groupId.Split(';'); string[] parts = groupId.Split(';');
@ -814,14 +531,12 @@ public class Groups
return "error:group.not.found"; return "error:group.not.found";
string memberPath = $"{groupPath}/members/{userId};{Utils.Utils.DOMAIN}"; string memberPath = $"{groupPath}/members/{userId};{Utils.Utils.DOMAIN}";
if (!Fs.Exists(memberPath))
return "error:not.a.member";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(groupId); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try try
{ {
if (!Fs.Exists(memberPath))
return "error:not.a.member";
// Remove from members // Remove from members
Fs.DeleteFile(memberPath); Fs.DeleteFile(memberPath);
@ -832,7 +547,7 @@ public class Groups
Fs.DeleteFile(userGroupPath); Fs.DeleteFile(userGroupPath);
} }
// Always require new key when someone leaves // Require new key
await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true")); await Fs.WriteFile($"{groupPath}/keys/newrequired", Encoding.UTF8.GetBytes("true"));
// Add info message // Add info message
@ -854,32 +569,10 @@ public class Groups
} }
await Fs.WriteFile($"{groupPath}/messages/last", Encoding.UTF8.GetBytes(msgId.ToString())); 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))); 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 finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
}
// If members count == 0, delete group folder and we are done.
// Doing it outside the lock since GroupDelete takes the lock again.
if (Fs.ReadDirectory($"{groupPath}/members").Length == 0)
{
await GroupDelete(groupId);
} }
return "success"; return "success";

View file

@ -10,23 +10,6 @@ namespace LarpixServer.Room;
public class Requests public class Requests
{ {
public static SemaphoreSlim createLock = new SemaphoreSlim(1, 1); 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; public const int MAX_DM_KEY_SIZE = 8192;
private static bool IsSafeDmId(string dmId) private static bool IsSafeDmId(string dmId)

View file

@ -122,46 +122,45 @@ public class Shared
public static async Task<string> MessageSend(string id, string roomType, string roomId, string content, string attachment, string responded, string keyIndex = "0") public static async Task<string> 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"; string msgDir = $"{GetRoomPath(roomType, roomId)}/messages";
long nextId = 0; long nextId = 0;
Message msg = new Message(); Message msg = new Message();
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try 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; long last = 0;
if (Fs.Exists($"{msgDir}/last")) if (Fs.Exists($"{msgDir}/last"))
{ {
@ -219,7 +218,7 @@ public class Shared
} }
finally finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
} }
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
@ -267,41 +266,32 @@ public class Shared
string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}"; string msgPath = $"{GetRoomPath(roomType, roomId)}/messages/{msgId}";
if (!Fs.Exists(msgPath)) return "error:message.not.found"; if (!Fs.Exists(msgPath)) return "error:message.not.found";
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); Message msg = JsonSerializer.Deserialize(
await roomLock.WaitAsync(); Encoding.UTF8.GetString(await Fs.ReadFile(msgPath)),
try 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))
{ {
Message msg = JsonSerializer.Deserialize( Fs.DeleteDirectory(reactDir);
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);
}
} }
finally
string attDir = $"{GetRoomPath(roomType, roomId)}/attachments/{msgId}";
if (Fs.Exists(attDir))
{ {
roomLock.Release(); Fs.DeleteDirectory(attDir);
} }
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
@ -341,8 +331,7 @@ public class Shared
string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}"; string reactDir = $"{GetRoomPath(roomType, roomId)}/reactions/{msgId}";
if (!Fs.Exists(reactDir)) Fs.CreateDirectory(reactDir); if (!Fs.Exists(reactDir)) Fs.CreateDirectory(reactDir);
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try try
{ {
long last = 0; long last = 0;
@ -357,7 +346,7 @@ public class Shared
} }
finally finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
} }
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");
@ -422,8 +411,7 @@ public class Shared
string pinnedFilePath = $"{pinnedDir}/{msgId}"; string pinnedFilePath = $"{pinnedDir}/{msgId}";
bool isNowPinned = false; bool isNowPinned = false;
SemaphoreSlim roomLock = Room.Requests.GetRoomLock(roomId); await Room.Requests.createLock.WaitAsync();
await roomLock.WaitAsync();
try try
{ {
if (Fs.Exists(pinnedFilePath)) if (Fs.Exists(pinnedFilePath))
@ -462,7 +450,7 @@ public class Shared
} }
finally finally
{ {
roomLock.Release(); Room.Requests.createLock.Release();
} }
string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members"); string[] members = Fs.ReadDirectory($"{GetRoomPath(roomType, roomId)}/members");

View file

@ -11,7 +11,6 @@ using System.Collections.Generic;
[JsonSerializable(typeof(Universal3String))] [JsonSerializable(typeof(Universal3String))]
[JsonSerializable(typeof(Universal4String))] [JsonSerializable(typeof(Universal4String))]
[JsonSerializable(typeof(Universal5String))] [JsonSerializable(typeof(Universal5String))]
[JsonSerializable(typeof(Universal6String))]
[JsonSerializable(typeof(ServerInfo))] [JsonSerializable(typeof(ServerInfo))]
[JsonSerializable(typeof(Message))] [JsonSerializable(typeof(Message))]
[JsonSerializable(typeof(Dictionary<string, string>))] [JsonSerializable(typeof(Dictionary<string, string>))]

View file

@ -34,15 +34,6 @@ public class Universal5String
public string string5 { get; set; } public string string5 { get; set; }
} }
public class Universal6String
{
public string string1 { get; set; }
public string string2 { get; set; }
public string string3 { get; set; }
public string string4 { get; set; }
public string string5 { get; set; }
public string string6 { get; set; }
}
public class KeyExchangePayload public class KeyExchangePayload
{ {