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 LICENSE = LICENSE
Protocol.md = Protocol.md Protocol.md = Protocol.md
README.md = README.md README.md = README.md
start.sh = start.sh
EndProjectSection EndProjectSection
EndProject EndProject
Global 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 SockChatServer Server { get; }
public Timer BumpTimer { get; } public Timer BumpTimer { get; }
public BanManager Bans { get; }
public readonly object BansAccess = new();
public ChannelManager Channels { get; } public ChannelManager Channels { get; }
public UserManager Users { get; } public UserManager Users { get; }
public ChatEventManager Events { get; } public ChatEventManager Events { get; }
@ -28,7 +25,6 @@ namespace SharpChat {
public ChatContext(HttpClient httpClient, SockChatServer server) { public ChatContext(HttpClient httpClient, SockChatServer server) {
HttpClient = httpClient; HttpClient = httpClient;
Server = server; Server = server;
Bans = new(httpClient, this);
Users = new(this); Users = new(this);
Channels = new(this); Channels = new(this);
Events = new(this); Events = new(this);
@ -37,26 +33,13 @@ namespace SharpChat {
} }
public void Update() { public void Update() {
lock(BansAccess)
Bans.RemoveExpired();
CheckPings(); CheckPings();
} }
public void BanUser(ChatUser user, DateTimeOffset? until = null, bool banIPs = false, UserDisconnectReason reason = UserDisconnectReason.Kicked) { public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
if(until.HasValue && until.Value <= DateTimeOffset.UtcNow) if(duration > TimeSpan.Zero)
until = null; user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration));
else
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
user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Kicked)); user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Kicked));
user.Close(); user.Close();
@ -194,7 +177,6 @@ namespace SharpChat {
Events?.Dispose(); Events?.Dispose();
Channels?.Dispose(); Channels?.Dispose();
Users?.Dispose(); Users?.Dispose();
Bans?.Dispose();
} }
} }
} }

View file

@ -120,12 +120,12 @@ namespace SharpChat {
public ChatUser() { public ChatUser() {
} }
public ChatUser(FlashiiAuth auth) { public ChatUser(FlashiiAuthInfo auth) {
UserId = auth.UserId; UserId = auth.UserId;
ApplyAuth(auth, true); ApplyAuth(auth, true);
} }
public void ApplyAuth(FlashiiAuth auth, bool invalidateRestrictions = false) { public void ApplyAuth(FlashiiAuthInfo auth, bool invalidateRestrictions = false) {
Username = auth.Username; Username = auth.Username;
if(Status == ChatUserStatus.Offline) 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 { namespace SharpChat.Flashii {
public static class FlashiiUrls { public static class FlashiiUrls {
private const string BASE_URL_FILE = @"msz_url.txt"; private const string BASE_URL_FILE = "msz_url.txt";
private const string BASE_URL_FALLBACK = @"https://flashii.net"; private const string BASE_URL_FALLBACK = "https://flashii.net";
private const string AUTH = @"/_sockchat/verify"; private const string VERIFY = "/_sockchat/verify";
private const string BANS = @"/_sockchat/bans"; private const string BUMP = "/_sockchat/bump";
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 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() { static FlashiiUrls() {
AuthURL = GetURL(AUTH);
BansURL = GetURL(BANS);
BumpURL = GetURL(BUMP); 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() { public static string GetBaseURL() {

View file

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

View file

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

View file

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

View file

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