Removed internal ban handling and integrate with Misuzu.

This commit is contained in:
flash 2023-02-07 23:28:06 +01:00
parent 5e3eecda8c
commit 36f3ff6385
14 changed files with 455 additions and 443 deletions

View file

@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
LICENSE = LICENSE
Protocol.md = Protocol.md
README.md = README.md
start.sh = start.sh
EndProjectSection
EndProject
Global

View file

@ -1,192 +0,0 @@
using SharpChat.Flashii;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace SharpChat {
public interface IBan {
DateTimeOffset Expires { get; }
string ToString();
}
public class BannedUser : IBan {
public long UserId { get; set; }
public DateTimeOffset Expires { get; set; }
public string Username { get; set; }
public BannedUser() {
}
public BannedUser(FlashiiBan fb) {
UserId = fb.UserId;
Expires = fb.Expires;
Username = fb.Username;
}
public override string ToString() {
return Username;
}
}
public class BannedIPAddress : IBan {
public IPAddress Address { get; set; }
public DateTimeOffset Expires { get; set; }
public BannedIPAddress() {
}
public BannedIPAddress(FlashiiBan fb) {
Address = IPAddress.Parse(fb.UserIP);
Expires = fb.Expires;
}
public override string ToString() {
return Address.ToString();
}
}
public class BanManager : IDisposable {
private readonly List<IBan> BanList = new();
private readonly HttpClient HttpClient;
public readonly ChatContext Context;
public bool IsDisposed { get; private set; }
public BanManager(HttpClient httpClient, ChatContext context) {
HttpClient = httpClient;
Context = context;
RefreshFlashiiBans().Wait();
}
public void Add(ChatUser user, DateTimeOffset expires) {
if(expires <= DateTimeOffset.Now)
return;
lock(BanList) {
BannedUser ban = BanList.OfType<BannedUser>().FirstOrDefault(x => x.UserId == user.UserId);
if(ban == null)
Add(new BannedUser { UserId = user.UserId, Expires = expires, Username = user.Username });
else
ban.Expires = expires;
}
}
public void Add(IPAddress addr, DateTimeOffset expires) {
if(expires <= DateTimeOffset.Now)
return;
lock(BanList) {
BannedIPAddress ban = BanList.OfType<BannedIPAddress>().FirstOrDefault(x => x.Address.Equals(addr));
if(ban == null)
Add(new BannedIPAddress { Address = addr, Expires = expires });
else
ban.Expires = expires;
}
}
private void Add(IBan ban) {
if(ban == null)
return;
lock(BanList)
if(!BanList.Contains(ban))
BanList.Add(ban);
}
public void Remove(ChatUser user) {
lock(BanList)
BanList.RemoveAll(x => x is BannedUser ub && ub.UserId == user.UserId);
}
public void Remove(IPAddress addr) {
lock(BanList)
BanList.RemoveAll(x => x is BannedIPAddress ib && ib.Address.Equals(addr));
}
public void Remove(IBan ban) {
lock(BanList)
BanList.Remove(ban);
}
public DateTimeOffset Check(ChatUser user) {
if(user == null)
return DateTimeOffset.MinValue;
lock(BanList)
return BanList.OfType<BannedUser>().Where(x => x.UserId == user.UserId).FirstOrDefault()?.Expires ?? DateTimeOffset.MinValue;
}
public DateTimeOffset Check(IPAddress addr) {
if(addr == null)
return DateTimeOffset.MinValue;
lock(BanList)
return BanList.OfType<BannedIPAddress>().Where(x => x.Address.Equals(addr)).FirstOrDefault()?.Expires ?? DateTimeOffset.MinValue;
}
public BannedUser GetUser(string username) {
if(username == null)
return null;
if(!long.TryParse(username, out long userId))
userId = 0;
lock(BanList)
return BanList.OfType<BannedUser>().FirstOrDefault(x => x.Username.ToLowerInvariant() == username.ToLowerInvariant() || (userId > 0 && x.UserId == userId));
}
public BannedIPAddress GetIPAddress(IPAddress addr) {
lock(BanList)
return BanList.OfType<BannedIPAddress>().FirstOrDefault(x => x.Address.Equals(addr));
}
public void RemoveExpired() {
lock(BanList)
BanList.RemoveAll(x => x.Expires <= DateTimeOffset.Now);
}
public async Task RefreshFlashiiBans() {
IEnumerable<FlashiiBan> bans = await FlashiiBan.GetListAsync(HttpClient);
if(!bans.Any())
return;
lock(BanList)
foreach(FlashiiBan fb in bans) {
if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId))
Add(new BannedUser(fb));
if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP))
Add(new BannedIPAddress(fb));
}
}
public IEnumerable<IBan> All() {
lock(BanList)
return BanList.ToList();
}
~BanManager() {
DoDispose();
}
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
BanList.Clear();
}
}
}

View file

@ -16,9 +16,6 @@ namespace SharpChat {
public SockChatServer Server { get; }
public Timer BumpTimer { get; }
public BanManager Bans { get; }
public readonly object BansAccess = new();
public ChannelManager Channels { get; }
public UserManager Users { get; }
public ChatEventManager Events { get; }
@ -28,7 +25,6 @@ namespace SharpChat {
public ChatContext(HttpClient httpClient, SockChatServer server) {
HttpClient = httpClient;
Server = server;
Bans = new(httpClient, this);
Users = new(this);
Channels = new(this);
Events = new(this);
@ -37,26 +33,13 @@ namespace SharpChat {
}
public void Update() {
lock(BansAccess)
Bans.RemoveExpired();
CheckPings();
}
public void BanUser(ChatUser user, DateTimeOffset? until = null, bool banIPs = false, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(until.HasValue && until.Value <= DateTimeOffset.UtcNow)
until = null;
if(until.HasValue) {
user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, until.Value));
lock(BansAccess) {
Bans.Add(user, until.Value);
if(banIPs)
foreach(IPAddress ip in user.RemoteAddresses)
Bans.Add(ip, until.Value);
}
} else
public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(duration > TimeSpan.Zero)
user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration));
else
user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Kicked));
user.Close();
@ -194,7 +177,6 @@ namespace SharpChat {
Events?.Dispose();
Channels?.Dispose();
Users?.Dispose();
Bans?.Dispose();
}
}
}

View file

@ -120,12 +120,12 @@ namespace SharpChat {
public ChatUser() {
}
public ChatUser(FlashiiAuth auth) {
public ChatUser(FlashiiAuthInfo auth) {
UserId = auth.UserId;
ApplyAuth(auth, true);
}
public void ApplyAuth(FlashiiAuth auth, bool invalidateRestrictions = false) {
public void ApplyAuth(FlashiiAuthInfo auth, bool invalidateRestrictions = false) {
Username = auth.Username;
if(Status == ChatUserStatus.Offline)

View file

@ -1,85 +0,0 @@
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace SharpChat.Flashii {
public class FlashiiAuthRequest {
[JsonPropertyName(@"user_id")]
public long UserId { get; set; }
[JsonPropertyName(@"token")]
public string Token { get; set; }
[JsonPropertyName(@"ip")]
public string IPAddress { get; set; }
[JsonIgnore]
public string Hash
=> string.Join(@"#", UserId, Token, IPAddress).GetSignedHash();
public byte[] GetJSON() {
return JsonSerializer.SerializeToUtf8Bytes(this);
}
}
public class FlashiiAuth {
[JsonPropertyName(@"success")]
public bool Success { get; set; }
[JsonPropertyName(@"reason")]
public string Reason { get; set; } = @"none";
[JsonPropertyName(@"user_id")]
public long UserId { get; set; }
[JsonPropertyName(@"username")]
public string Username { get; set; }
[JsonPropertyName(@"colour_raw")]
public int ColourRaw { get; set; }
[JsonPropertyName(@"hierarchy")]
public int Rank { get; set; }
[JsonPropertyName(@"is_silenced")]
public DateTimeOffset SilencedUntil { get; set; }
[JsonPropertyName(@"perms")]
public ChatUserPermissions Permissions { get; set; }
public static async Task<FlashiiAuth> AttemptAsync(HttpClient httpClient, FlashiiAuthRequest authRequest) {
if(httpClient == null)
throw new ArgumentNullException(nameof(httpClient));
if(authRequest == null)
throw new ArgumentNullException(nameof(authRequest));
#if DEBUG
if(authRequest.UserId >= 10000)
return new FlashiiAuth {
Success = true,
UserId = authRequest.UserId,
Username = @"Misaka-" + (authRequest.UserId - 10000),
ColourRaw = (RNG.Next(0, 255) << 16) | (RNG.Next(0, 255) << 8) | RNG.Next(0, 255),
Rank = 0,
SilencedUntil = DateTimeOffset.MinValue,
Permissions = ChatUserPermissions.SendMessage | ChatUserPermissions.EditOwnMessage | ChatUserPermissions.DeleteOwnMessage,
};
#endif
HttpRequestMessage request = new(HttpMethod.Post, FlashiiUrls.AuthURL) {
Content = new ByteArrayContent(authRequest.GetJSON()),
Headers = {
{ @"X-SharpChat-Signature", authRequest.Hash },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
return JsonSerializer.Deserialize<FlashiiAuth>(
await response.Content.ReadAsByteArrayAsync()
);
}
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace SharpChat.Flashii {
public class FlashiiAuthInfo {
[JsonPropertyName(@"success")]
public bool Success { get; set; }
[JsonPropertyName(@"reason")]
public string Reason { get; set; } = @"none";
[JsonPropertyName(@"user_id")]
public long UserId { get; set; }
[JsonPropertyName(@"username")]
public string Username { get; set; }
[JsonPropertyName(@"colour_raw")]
public int ColourRaw { get; set; }
[JsonPropertyName(@"hierarchy")]
public int Rank { get; set; }
[JsonPropertyName(@"is_silenced")]
public DateTimeOffset SilencedUntil { get; set; }
[JsonPropertyName(@"perms")]
public ChatUserPermissions Permissions { get; set; }
private const string SIG_FMT = "verify#{0}#{1}#{2}";
public static async Task<FlashiiAuthInfo> VerifyAsync(HttpClient client, string method, string token, string ipAddr) {
if(client == null)
throw new ArgumentNullException(nameof(client));
method ??= string.Empty;
token ??= string.Empty;
ipAddr ??= string.Empty;
string sig = string.Format(SIG_FMT, method, token, ipAddr);
HttpRequestMessage req = new(HttpMethod.Post, FlashiiUrls.VerifyURL) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "method", method },
{ "token", token },
{ "ipaddr", ipAddr },
}),
Headers = {
{ @"X-SharpChat-Signature", sig.GetSignedHash() },
},
};
using HttpResponseMessage res = await client.SendAsync(req);
return JsonSerializer.Deserialize<FlashiiAuthInfo>(
await res.Content.ReadAsByteArrayAsync()
);
}
}
}

View file

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace SharpChat.Flashii {
public class FlashiiBan {
private const string STRING = @"givemethebeans";
[JsonPropertyName(@"id")]
public int UserId { get; set; }
[JsonPropertyName(@"ip")]
public string UserIP { get; set; }
[JsonPropertyName(@"expires")]
public DateTimeOffset Expires { get; set; }
[JsonPropertyName(@"username")]
public string Username { get; set; }
public static async Task<IEnumerable<FlashiiBan>> GetListAsync(HttpClient httpClient) {
if(httpClient == null)
throw new ArgumentNullException(nameof(httpClient));
HttpRequestMessage request = new(HttpMethod.Get, FlashiiUrls.BansURL) {
Headers = {
{ @"X-SharpChat-Signature", STRING.GetSignedHash() },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
return JsonSerializer.Deserialize<IEnumerable<FlashiiBan>>(await response.Content.ReadAsByteArrayAsync());
}
}
}

View file

@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace SharpChat.Flashii {
public class FlashiiBanInfo {
[JsonPropertyName("is_ban")]
public bool IsBanned { get; set; }
[JsonPropertyName("user_id")]
public string UserId { get; set; }
[JsonPropertyName("ip_addr")]
public string RemoteAddress { get; set; }
[JsonPropertyName("is_perma")]
public bool IsPermanent { get; set; }
[JsonPropertyName("expires")]
public DateTimeOffset ExpiresAt { get; set; }
// only populated in list request
[JsonPropertyName("user_name")]
public string UserName { get; set; }
[JsonPropertyName(@"user_colour")]
public int UserColourRaw { get; set; }
public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
public ChatColour UserColour => ChatColour.FromMisuzu(UserColourRaw);
private const string CHECK_SIG_FMT = "check#{0}#{1}#{2}#{3}";
private const string REVOKE_SIG_FMT = "revoke#{0}#{1}#{2}";
private const string CREATE_SIG_FMT = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
private const string LIST_SIG_FMT = "list#{0}";
public static async Task<FlashiiBanInfo> CheckAsync(HttpClient client, string userId = null, string ipAddr = null, bool userIdIsName = false) {
if(client == null)
throw new ArgumentNullException(nameof(client));
userId ??= string.Empty;
ipAddr ??= string.Empty;
string userIdIsNameStr = userIdIsName ? "1" : "0";
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string url = string.Format(FlashiiUrls.BansCheckURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr));
string sig = string.Format(CHECK_SIG_FMT, now, userId, ipAddr, userIdIsNameStr);
HttpRequestMessage req = new(HttpMethod.Get, url) {
Headers = {
{ "X-SharpChat-Signature", sig.GetSignedHash() },
},
};
using HttpResponseMessage res = await client.SendAsync(req);
return JsonSerializer.Deserialize<FlashiiBanInfo>(
await res.Content.ReadAsByteArrayAsync()
);
}
public static async Task<FlashiiBanInfo[]> GetListAsync(HttpClient client) {
if(client == null)
throw new ArgumentNullException(nameof(client));
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string url = string.Format(FlashiiUrls.BansListURL, Uri.EscapeDataString(now));
string sig = string.Format(LIST_SIG_FMT, now);
HttpRequestMessage req = new(HttpMethod.Get, url) {
Headers = {
{ "X-SharpChat-Signature", sig.GetSignedHash() },
},
};
using HttpResponseMessage res = await client.SendAsync(req);
return JsonSerializer.Deserialize<FlashiiBanInfo[]>(
await res.Content.ReadAsByteArrayAsync()
);
}
public enum RevokeKind {
UserId,
RemoteAddress,
}
public async Task<bool> RevokeAsync(HttpClient client, RevokeKind kind) {
if(client == null)
throw new ArgumentNullException(nameof(client));
string type = kind switch {
RevokeKind.UserId => "user",
RevokeKind.RemoteAddress => "addr",
_ => throw new ArgumentException("Invalid kind specified.", nameof(kind)),
};
string target = kind switch {
RevokeKind.UserId => UserId,
RevokeKind.RemoteAddress => RemoteAddress,
_ => string.Empty,
};
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string url = string.Format(FlashiiUrls.BansRevokeURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
string sig = string.Format(REVOKE_SIG_FMT, now, type, target);
HttpRequestMessage req = new(HttpMethod.Delete, url) {
Headers = {
{ "X-SharpChat-Signature", sig.GetSignedHash() },
},
};
using HttpResponseMessage res = await client.SendAsync(req);
if(res.StatusCode == HttpStatusCode.NotFound)
return false;
res.EnsureSuccessStatusCode();
return res.StatusCode == HttpStatusCode.NoContent;
}
public static async Task CreateAsync(
HttpClient client,
string targetId,
string targetAddr,
string modId,
string modAddr,
TimeSpan duration,
string reason
) {
if(client == null)
throw new ArgumentNullException(nameof(client));
if(string.IsNullOrWhiteSpace(targetAddr))
throw new ArgumentNullException(nameof(targetAddr));
if(string.IsNullOrWhiteSpace(modAddr))
throw new ArgumentNullException(nameof(modAddr));
if(duration <= TimeSpan.Zero)
return;
modId ??= string.Empty;
targetId ??= string.Empty;
reason ??= string.Empty;
string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string sig = string.Format(
CREATE_SIG_FMT,
now, targetId, targetAddr,
modId, modAddr,
durationStr, isPerma, reason
);
HttpRequestMessage req = new(HttpMethod.Post, FlashiiUrls.BansCreateURL) {
Headers = {
{ "X-SharpChat-Signature", sig.GetSignedHash() },
},
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "t", now },
{ "ui", targetId },
{ "ua", targetAddr },
{ "mi", modId },
{ "ma", modAddr },
{ "d", durationStr },
{ "p", isPerma },
{ "r", reason },
}),
};
using HttpResponseMessage res = await client.SendAsync(req);
res.EnsureSuccessStatusCode();
}
}
}

View file

@ -2,21 +2,32 @@
namespace SharpChat.Flashii {
public static class FlashiiUrls {
private const string BASE_URL_FILE = @"msz_url.txt";
private const string BASE_URL_FALLBACK = @"https://flashii.net";
private const string BASE_URL_FILE = "msz_url.txt";
private const string BASE_URL_FALLBACK = "https://flashii.net";
private const string AUTH = @"/_sockchat/verify";
private const string BANS = @"/_sockchat/bans";
private const string BUMP = @"/_sockchat/bump";
private const string VERIFY = "/_sockchat/verify";
private const string BUMP = "/_sockchat/bump";
private const string BANS_CHECK = "/_sockchat/bans/check?u={0}&a={1}&x={2}&n={3}";
private const string BANS_CREATE = "/_sockchat/bans/create";
private const string BANS_REVOKE = "/_sockchat/bans/revoke?t={0}&s={1}&x={2}";
private const string BANS_LIST = "/_sockchat/bans/list?x={0}";
public static string AuthURL { get; }
public static string BansURL { get; }
public static string BumpURL { get; }
public static string VerifyURL { get; }
public static string BansCheckURL { get; }
public static string BansCreateURL { get; }
public static string BansRevokeURL { get; }
public static string BansListURL { get; }
static FlashiiUrls() {
AuthURL = GetURL(AUTH);
BansURL = GetURL(BANS);
BumpURL = GetURL(BUMP);
VerifyURL = GetURL(VERIFY);
BansCheckURL = GetURL(BANS_CHECK);
BansCreateURL = GetURL(BANS_CREATE);
BansRevokeURL = GetURL(BANS_REVOKE);
BansListURL = GetURL(BANS_LIST);
}
public static string GetBaseURL() {

View file

@ -1,4 +1,5 @@
using System;
using SharpChat.Flashii;
using System;
using System.Collections.Generic;
using System.Text;
@ -11,16 +12,13 @@ namespace SharpChat.Packet {
public class AuthFailPacket : ServerPacket {
public AuthFailReason Reason { get; private set; }
public DateTimeOffset Expires { get; private set; }
public FlashiiBanInfo BanInfo { get; private set; }
public AuthFailPacket(AuthFailReason reason, DateTimeOffset? expires = null) {
public AuthFailPacket(AuthFailReason reason, FlashiiBanInfo fbi = null) {
Reason = reason;
if(reason == AuthFailReason.Banned) {
if(!expires.HasValue)
throw new ArgumentNullException(nameof(expires));
Expires = expires.Value;
}
if(reason == AuthFailReason.Banned)
BanInfo = fbi ?? throw new ArgumentNullException(nameof(fbi));
}
public override IEnumerable<string> Pack() {
@ -45,10 +43,10 @@ namespace SharpChat.Packet {
if(Reason == AuthFailReason.Banned) {
sb.Append('\t');
if(Expires == DateTimeOffset.MaxValue)
sb.Append(@"-1");
if(BanInfo.IsPermanent)
sb.Append("-1");
else
sb.Append(Expires.ToUnixTimeSeconds());
sb.Append(BanInfo.ExpiresAt.ToUnixTimeSeconds());
}
yield return sb.ToString();

View file

@ -1,13 +1,14 @@
using System;
using SharpChat.Flashii;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpChat.Packet {
public class BanListPacket : ServerPacket {
public IEnumerable<IBan> Bans { get; private set; }
public IEnumerable<FlashiiBanInfo> Bans { get; private set; }
public BanListPacket(IEnumerable<IBan> bans) {
public BanListPacket(IEnumerable<FlashiiBanInfo> bans) {
Bans = bans ?? throw new ArgumentNullException(nameof(bans));
}
@ -19,8 +20,10 @@ namespace SharpChat.Packet {
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
sb.Append("\t-1\t0\fbanlist\f");
foreach(IBan ban in Bans)
sb.AppendFormat(@"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('/unban '+ this.innerHTML);"">{0}</a>, ", ban);
foreach(FlashiiBanInfo ban in Bans) {
string banStr = string.IsNullOrEmpty(ban.UserName) ? ban.RemoteAddress : ban.UserName;
sb.AppendFormat(@"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('/unban '+ this.innerHTML);"">{0}</a>, ", banStr);
}
if(Bans.Any())
sb.Length -= 2;

View file

@ -31,6 +31,9 @@ namespace SharpChat.Packet {
if(Reason == ForceDisconnectReason.Banned) {
sb.Append('\t');
if(Expires.Year >= 2100)
sb.Append("-1");
else
sb.Append(Expires.ToUnixTimeSeconds());
}

View file

@ -11,6 +11,7 @@ using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SharpChat {
public class SockChatServer : IDisposable {
@ -19,11 +20,9 @@ namespace SharpChat {
#if DEBUG
public const int MAX_CONNECTIONS = 9001;
public const int FLOOD_KICK_LENGTH = 5;
public const bool ENABLE_TYPING_EVENT = true;
#else
public const int MAX_CONNECTIONS = 5;
public const int FLOOD_KICK_LENGTH = 30;
public const bool ENABLE_TYPING_EVENT = false;
#endif
public bool IsDisposed { get; private set; }
@ -144,14 +143,25 @@ namespace SharpChat {
sess.User.RateLimiter.AddTimePoint();
if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) {
Context.BanUser(sess.User, DateTimeOffset.UtcNow.AddSeconds(FLOOD_KICK_LENGTH), false, UserDisconnectReason.Flood);
Task.Run(async () => {
TimeSpan duration = TimeSpan.FromSeconds(FLOOD_KICK_LENGTH);
await FlashiiBanInfo.CreateAsync(
HttpClient,
sess.User.UserId.ToString(), sess.RemoteAddress.ToString(),
string.Empty, "::1",
duration,
"Kicked from chat for flood protection."
);
Context.BanUser(sess.User, duration, UserDisconnectReason.Flood);
}).Wait();
return;
} else if(sess.User.RateLimiter.State == ChatRateLimitState.Warning)
sess.User.Send(new FloodWarningPacket()); // make it so this thing only sends once
sess.User.Send(new FloodWarningPacket());
}
string[] args = msg.Split('\t');
if(args.Length < 1)
return;
@ -168,58 +178,80 @@ namespace SharpChat {
if(sess.User != null)
break;
DateTimeOffset aBanned;
lock(Context.BansAccess)
aBanned = Context.Bans.Check(sess.RemoteAddress);
if(aBanned > DateTimeOffset.UtcNow) {
sess.Send(new AuthFailPacket(AuthFailReason.Banned, aBanned));
string authMethod = args.ElementAtOrDefault(1);
if(string.IsNullOrWhiteSpace(authMethod)) {
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
sess.Dispose();
break;
}
if(args.Length < 3 || !long.TryParse(args[1], out long aUserId))
string authToken = args.ElementAtOrDefault(2);
if(string.IsNullOrWhiteSpace(authToken)) {
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
sess.Dispose();
break;
}
FlashiiAuth.AttemptAsync(HttpClient, new FlashiiAuthRequest {
UserId = aUserId,
Token = args[2],
IPAddress = sess.RemoteAddress.ToString(),
}).ContinueWith(authTask => {
if(authTask.IsFaulted) {
Logger.Write($@"<{sess.Id}> Auth task fail: {authTask.Exception}");
if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) {
string[] tokenParts = authToken.Split(':', 2);
authMethod = tokenParts[0];
authToken = tokenParts[1];
}
Task.Run(async () => {
FlashiiAuthInfo fai;
string ipAddr = sess.RemoteAddress.ToString();
try {
fai = await FlashiiAuthInfo.VerifyAsync(HttpClient, authMethod, authToken, ipAddr);
} catch(Exception ex) {
Logger.Write($@"<{sess.Id}> Failed to authenticate: {ex}");
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
sess.Dispose();
#if DEBUG
throw;
#else
return;
#endif
}
if(!fai.Success) {
Logger.Debug($@"<{sess.Id}> Auth fail: {fai.Reason}");
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
sess.Dispose();
return;
}
FlashiiAuth auth = authTask.Result;
if(!auth.Success) {
Logger.Debug($@"<{sess.Id}> Auth fail: {auth.Reason}");
FlashiiBanInfo fbi;
try {
fbi = await FlashiiBanInfo.CheckAsync(HttpClient, fai.UserId.ToString(), ipAddr);
} catch(Exception ex) {
Logger.Write($@"<{sess.Id}> Failed auth ban check: {ex}");
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
sess.Dispose();
#if DEBUG
throw;
#else
return;
#endif
}
if(fbi.IsBanned && !fbi.HasExpired) {
Logger.Write($@"<{sess.Id}> User is banned.");
sess.Send(new AuthFailPacket(AuthFailReason.Banned, fbi));
sess.Dispose();
return;
}
ChatUser aUser = Context.Users.Get(auth.UserId);
ChatUser aUser = Context.Users.Get(fai.UserId);
if(aUser == null)
aUser = new ChatUser(auth);
aUser = new ChatUser(fai);
else {
aUser.ApplyAuth(auth);
aUser.ApplyAuth(fai);
aUser.Channel?.Send(new UserUpdatePacket(aUser));
}
lock(Context.BansAccess)
aBanned = Context.Bans.Check(aUser);
if(aBanned > DateTimeOffset.Now) {
sess.Send(new AuthFailPacket(AuthFailReason.Banned, aBanned));
sess.Dispose();
return;
}
// Enforce a maximum amount of connections per user
if(aUser.SessionCount >= MAX_CONNECTIONS) {
sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions));
@ -243,7 +275,7 @@ namespace SharpChat {
}
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess);
});
}).Wait();
break;
case "2":
@ -287,7 +319,7 @@ namespace SharpChat {
IChatMessage message = null;
if(messageText[0] == '/') {
message = HandleV1Command(messageText, mUser, mChannel);
message = HandleV1Command(messageText, mUser, mChannel, sess);
if(message == null)
break;
@ -307,7 +339,7 @@ namespace SharpChat {
}
}
public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel) {
public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel, ChatUserSession sess) {
string[] parts = message[1..].Split(' ');
string commandName = parts[0].Replace(@".", string.Empty).ToLowerInvariant();
@ -640,10 +672,13 @@ namespace SharpChat {
break;
}
ChatUser banUser;
string banUserTarget = parts.ElementAtOrDefault(1);
string banDurationStr = parts.ElementAtOrDefault(2);
int banReasonIndex = 2;
ChatUser banUser = null;
if(parts.Length < 2 || (banUser = Context.Users.Get(parts[1])) == null) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? @"User" : parts[1]));
if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? @"User" : banUserTarget));
break;
}
@ -652,24 +687,44 @@ namespace SharpChat {
break;
}
lock(Context.BansAccess)
if(Context.Bans.Check(banUser) > DateTimeOffset.Now) {
user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName));
break;
}
DateTimeOffset? banUntil = isBanning ? (DateTimeOffset?)DateTimeOffset.MaxValue : null;
if(parts.Length > 2) {
if(!double.TryParse(parts[2], out double silenceSeconds)) {
TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero;
if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) {
if(durationSeconds < 0) {
user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
break;
}
banUntil = DateTimeOffset.UtcNow.AddSeconds(silenceSeconds);
duration = TimeSpan.FromSeconds(durationSeconds);
++banReasonIndex;
}
Context.BanUser(banUser, banUntil, isBanning);
if(duration <= TimeSpan.Zero) {
Context.BanUser(banUser, duration);
break;
}
string banReason = string.Join(' ', parts.Skip(banReasonIndex));
Task.Run(async () => {
// obviously it makes no sense to only check for one ip address but that's current misuzu limitations
FlashiiBanInfo fbi = await FlashiiBanInfo.CheckAsync(
HttpClient, banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString()
);
if(fbi.IsBanned && !fbi.HasExpired) {
user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName));
return;
}
await FlashiiBanInfo.CreateAsync(
HttpClient,
banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(),
user.UserId.ToString(), sess.RemoteAddress.ToString(),
duration, banReason
);
Context.BanUser(banUser, duration);
}).Wait();
break;
case @"pardon":
case @"unban":
@ -678,24 +733,36 @@ namespace SharpChat {
break;
}
if(parts.Length < 2) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, string.Empty));
bool unbanUserTargetIsName = true;
string unbanUserTarget = parts.ElementAtOrDefault(1);
if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
break;
}
BannedUser unbanUser;
lock(Context.BansAccess)
unbanUser = Context.Bans.GetUser(parts[1]);
if(unbanUser == null || unbanUser.Expires <= DateTimeOffset.Now) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUser?.Username ?? parts[1]));
break;
ChatUser unbanUser = Context.Users.Get(unbanUserTarget);
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
unbanUserTargetIsName = false;
unbanUser = Context.Users.Get(unbanUserId);
}
lock(Context.BansAccess)
Context.Bans.Remove(unbanUser);
if(unbanUser != null)
unbanUserTarget = unbanUser.UserId.ToString();
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUser));
Task.Run(async () => {
FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, unbanUserTarget, userIdIsName: unbanUserTargetIsName);
if(!banInfo.IsBanned || banInfo.HasExpired) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget));
return;
}
bool wasBanned = await banInfo.RevokeAsync(HttpClient, FlashiiBanInfo.RevokeKind.UserId);
if(wasBanned)
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUserTarget));
else
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget));
}).Wait();
break;
case @"pardonip":
case @"unbanip":
@ -704,21 +771,28 @@ namespace SharpChat {
break;
}
if(parts.Length < 2 || !IPAddress.TryParse(parts[1], out IPAddress unbanIP)) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, string.Empty));
string unbanAddrTarget = parts.ElementAtOrDefault(1);
if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress unbanAddr)) {
user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
break;
}
lock(Context.BansAccess) {
if(Context.Bans.Check(unbanIP) <= DateTimeOffset.Now) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanIP));
break;
unbanAddrTarget = unbanAddr.ToString();
Task.Run(async () => {
FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, ipAddr: unbanAddrTarget);
if(!banInfo.IsBanned || banInfo.HasExpired) {
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget));
return;
}
Context.Bans.Remove(unbanIP);
}
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanIP));
bool wasBanned = await banInfo.RevokeAsync(HttpClient, FlashiiBanInfo.RevokeKind.RemoteAddress);
if(wasBanned)
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanAddrTarget));
else
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget));
}).Wait();
break;
case @"bans": // gets a list of bans
case @"banned":
@ -727,8 +801,11 @@ namespace SharpChat {
break;
}
lock(Context.BansAccess)
user.Send(new BanListPacket(Context.Bans.All()));
Task.Run(async () => {
user.Send(new BanListPacket(
await FlashiiBanInfo.GetListAsync(HttpClient)
));
}).Wait();
break;
case @"silence": // silence a user
if(!user.Can(ChatUserPermissions.SilenceUser)) {

6
start.sh Normal file
View file

@ -0,0 +1,6 @@
#!/bin/sh
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export LANG=en_US.UTF-8
dotnet run --project SharpChat -c Release