LarpixServer/LarpixServer/Account/Utils.cs

401 lines
No EOL
13 KiB
C#

using System.Collections.Concurrent;
using System.Numerics;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using LarpixServer.Filesystem;
using LarpixServer.Utils.Jsons;
using static LarpixServer.Utils.Utils;
namespace LarpixServer.Account;
public class Utils
{
private static readonly Regex USERNAME_REGEX = new Regex(@"^[a-zA-Z0-9_]+$");
public static string LOGIN_SUCCESS = "success:login.successful";
private static SemaphoreSlim[]? _userLocksArray = null;
public static SemaphoreSlim GetUserLock(string id)
{
if (_userLocksArray == null)
{
int size = LOCK_SIZE > 0 ? LOCK_SIZE : 65536;
var newArray = Enumerable.Range(0, size).Select(_ => new SemaphoreSlim(1, 1)).ToArray();
Interlocked.CompareExchange(ref _userLocksArray, newArray, null);
}
int hash = id.GetHashCode();
int index = (hash & 0x7FFFFFFF) % _userLocksArray.Length;
return _userLocksArray[index];
}
public static string GetIdFromUsernameWD(string usernameWD)
{
int separatorIndex = usernameWD.IndexOf(':');
if (separatorIndex == -1) separatorIndex = usernameWD.IndexOf(';');
return separatorIndex == -1 ? usernameWD : usernameWD.Substring(0, separatorIndex);
}
public static string GetValidIdOrZero(string input)
{
string idPart = GetIdFromUsernameWD(input);
return ulong.TryParse(idPart, out _) ? idPart : "0";
}
public static string GetDmId(string id1, string domain1, string id2, string domain2)
{
string u1 = $"{id1};{domain1}";
string u2 = $"{id2};{domain2}";
return string.CompareOrdinal(u1, u2) < 0 ? $"{u1}_{u2}" : $"{u2}_{u1}";
}
public static bool IsUserLocal(string usernameWD, out string domain)
{
int separatorIndex = usernameWD.IndexOf(':');
if (separatorIndex == -1) separatorIndex = usernameWD.IndexOf(';');
if (separatorIndex == -1 || usernameWD.EndsWith(":" + DOMAIN) || usernameWD.EndsWith(";" + DOMAIN))
{
domain = DOMAIN;
return true;
}
domain = usernameWD.Substring(separatorIndex + 1);
return false;
}
public static bool IsValidUsername(string username, out string message)
{
message = "error:username.length:3-18";
if (string.IsNullOrWhiteSpace(username)) return false;
if (username.Length is < 3 or > 18) return false;
message = "error:username.conditions.allowed:letters;numbers;underscores";
return USERNAME_REGEX.IsMatch(username);
}
public static bool IsValidPassword(string password, out string message)
{
message = "error:password.not.hashed.properly";
if (password.Length != 128)
{
return false;
}
if (!IsValidUtf8(password))
{
return false;
}
return true;
}
public static bool IsValidUtf8(string password)
{
if (string.IsNullOrEmpty(password)) return false;
try
{
var encoding = Encoding.GetEncoding("utf-8",
new EncoderExceptionFallback(),
new DecoderExceptionFallback());
byte[] utf8Bytes = encoding.GetBytes(password);
return true;
}
catch (EncoderFallbackException)
{
return false;
}
}
public static async Task<string> IdFromName(string name)
{
if (name == null || !IsValidUsername(name, out _))
{
return "0";
}
string path = $"{ACCOUNTS_NAME_DIR}/{name.ToLowerInvariant()}";
if (!Fs.Exists(path))
{
return "0";
}
string id = Encoding.UTF8.GetString(await Fs.ReadFile(path));
return id;
}
public static async Task<string> NameFromId(string id)
{
string path = $"{ACCOUNTS_DATA_DIR}/{id}/username";
if (!Fs.Exists(path))
{
return "";
}
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
}
public static async Task<string> Auth(string id, string password, string password2)
{
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}"))
{
return "error:invalid.username.or.password";
}
if (password != password2)
{
return "error:invalid.password";
}
await UpdateLastLogin(id);
return LOGIN_SUCCESS;
}
public static async Task<string> NonceDecryptBody(string id, string password, string body, bool delEntry = true)
{
(string, DateTimeOffset) nonce;
if (delEntry)
{
if (!Requests.nonceHolder.TryRemove(id, out nonce))
{
return "error:invalid.nonce";
}
}
else
{
if (!Requests.nonceHolder.TryGetValue(id, out nonce))
{
return "error:invalid.nonce";
}
}
string decBody = Encryption.Encryption.PacketDecPass(body, password, nonce.Item1);
return decBody;
}
public static async Task UpdateLastLogin(string id)
{
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
{
return;
}
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin", Encoding.UTF8.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));
}
public static async Task<DateTimeOffset> GetLastLogin(string id)
{
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
{
return DateTimeOffset.MinValue;
}
return DateTimeOffset.FromUnixTimeSeconds(long.Parse(await Fs.ReadFile($"{ACCOUNTS_DATA_DIR}/{id}/lastlogin")));
}
public static async Task SetPassword(string id, string newPassword)
{
string path = $"{ACCOUNTS_DATA_DIR}/{id}/secret";
if (!Fs.Exists(path))
{
return;
}
await Fs.WriteFile(path, Encoding.UTF8.GetBytes(newPassword));
}
public static async Task<string> GetUserSentInvites(string id)
{
StringBuilder dmBuilder = new StringBuilder("\"dms\": {");
StringBuilder groupBuilder = new StringBuilder("\"groups\": {");
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dminvites/sent";
if (Fs.Exists(path))
{
string[] invites = Fs.ReadDirectory(path);
for (int i = 0; i < invites.Length; i++)
{
string invite = invites[i];
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (i > 0) dmBuilder.Append(',');
dmBuilder.Append($"\"{invite}\": {{ \"timestamp\": {content} }}");
}
}
dmBuilder.Append("}");
path = $"{ACCOUNTS_DATA_DIR}/{id}/groupinvites/sent";
if (Fs.Exists(path))
{
string[] invites = Fs.ReadDirectory(path);
for (int i = 0; i < invites.Length; i++)
{
string invite = invites[i];
string[] inviteArray = invite.Split('-');
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (i > 0) groupBuilder.Append(',');
groupBuilder.Append($"\"{inviteArray[0]}\": {{ \"timestamp\": {content}, \"receiver\": \"{inviteArray[1]}\" }}");
}
}
groupBuilder.Append("}");
return $"{{{dmBuilder.ToString()},{groupBuilder.ToString()}}}";
}
public static async Task<string> GetUserReceivedInvites(string id)
{
StringBuilder dmBuilder = new StringBuilder("\"dms\": {");
StringBuilder groupBuilder = new StringBuilder("\"groups\": {");
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dminvites/recv";
if (Fs.Exists(path))
{
string[] invites = Fs.ReadDirectory(path);
for (int i = 0; i < invites.Length; i++)
{
string invite = invites[i];
string content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}"));
if (i > 0) dmBuilder.Append(',');
dmBuilder.Append($"\"{invite}\": {{ \"timestamp\": {content} }}");
}
}
dmBuilder.Append("}");
path = $"{ACCOUNTS_DATA_DIR}/{id}/groupinvites/recv";
if (Fs.Exists(path))
{
string[] invites = Fs.ReadDirectory(path);
for (int i = 0; i < invites.Length; i++)
{
string invite = invites[i];
string[] content = Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{invite}")).Split(':');
if (i > 0) groupBuilder.Append(',');
groupBuilder.Append($"\"{invite}\": {{ \"timestamp\": {content[1]}, \"sender\": \"{content[0]}\" }}");
}
}
groupBuilder.Append("}");
return $"{{{dmBuilder.ToString()},{groupBuilder.ToString()}}}";
}
public static async Task UpdateUserKeys(string id, string body)
{
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
{
return;
}
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/keys", Encoding.UTF8.GetBytes(body));
}
public static async Task<string> GetUserKeys(string id)
{
string path = $"{ACCOUNTS_DATA_DIR}/{id}/keys";
if (!Fs.Exists(path))
{
return "";
}
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
}
public static async Task<byte[]> GetUserPublicStorageEntry(string id, string entry)
{
if (string.IsNullOrEmpty(entry) || entry.Contains("..") || entry.Contains("/") || entry.Contains("\\"))
{
return new byte[] {};
}
string path = $"{ACCOUNTS_DATA_DIR}/{id}/storage/public/{entry}";
if (!Fs.Exists(path))
{
return new byte[] {};
}
return await Fs.ReadFile(path);
}
public static async Task UpdateUserDm(string id, string dmId, string isRead = "false", string timestamp = "")
{
if (await Account.Utils.NameFromId(id) == "") //if user account just got deleted
{
return;
}
string dmPath = $"{ACCOUNTS_DATA_DIR}/{id}/dms/{dmId}";
Universal3String fileDm = new Universal3String();
if (Fs.Exists(dmPath))
{
fileDm = JsonSerializer.Deserialize(
Encoding.UTF8.GetString(await Fs.ReadFile(dmPath)),
AppJsonSerializerContext.Default.Universal3String
);
}
else
{
fileDm.string1 = "";
fileDm.string2 = "";
fileDm.string3 = "";
}
fileDm.string1 = dmId;
fileDm.string2 = timestamp;
fileDm.string3 = isRead;
await Fs.WriteFile(dmPath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fileDm, AppJsonSerializerContext.Default.Universal3String)));
}
public static async Task<string> GetUserDms(string id)
{
string path = $"{ACCOUNTS_DATA_DIR}/{id}/dms";
if (!Fs.Exists(path))
{
return "{\"dms\":{}}";
}
StringBuilder dmsBuilder = new StringBuilder("\"dms\":{");
string[] dmFiles = Fs.ReadDirectory(path);
for (int i = 0; i < dmFiles.Length; i++)
{
if (i > 0) dmsBuilder.Append(',');
dmsBuilder.Append($"\"{dmFiles[i]}\":");
dmsBuilder.Append(Encoding.UTF8.GetString(await Fs.ReadFile($"{path}/{dmFiles[i]}")));
}
dmsBuilder.Append("}");
return $"{{{dmsBuilder.ToString()}}}";
}
public static async Task RemoveOldestDmIndex(string id) //i wont implement this, client should just warn users that they have like 99999999 dms and should leave some
{
}
public static async Task<string> GetPassword(string id)
{
string path = $"{ACCOUNTS_DATA_DIR}/{id}/secret";
if (!Fs.Exists(path))
{
return "";
}
return Encoding.UTF8.GetString(await Fs.ReadFile(path));
}
public static async Task<string> SetUsername(string id, string newName)
{
if (!Fs.Exists($"{ACCOUNTS_DATA_DIR}/{id}/secret"))
{
return "error:account.not.exist";
}
if (!IsValidUsername(newName, out string message))
{
return message;
}
string oldName = await NameFromId(id);
string lowerNewName = newName.ToLowerInvariant();
string lowerOldName = oldName.ToLowerInvariant();
if (lowerNewName != lowerOldName)
{
if (Fs.Exists($"{ACCOUNTS_NAME_DIR}/{lowerNewName}"))
{
return "error:username.taken";
}
Fs.MoveFile($"{ACCOUNTS_NAME_DIR}/{lowerOldName}", $"{ACCOUNTS_NAME_DIR}/{lowerNewName}");
}
await Fs.WriteFile($"{ACCOUNTS_DATA_DIR}/{id}/username", Encoding.UTF8.GetBytes(newName));
return "success:username.changed";
}
}