401 lines
No EOL
13 KiB
C#
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";
|
|
}
|
|
} |