|
|
|
@ -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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FlashiiAuth auth = authTask.Result;
|
|
|
|
|
if(!fai.Success) {
|
|
|
|
|
Logger.Debug($@"<{sess.Id}> Auth fail: {fai.Reason}");
|
|
|
|
|
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
|
|
|
|
sess.Dispose();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ChatUser aUser = Context.Users.Get(auth.UserId);
|
|
|
|
|
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(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));
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTimeOffset? banUntil = isBanning ? (DateTimeOffset?)DateTimeOffset.MaxValue : null;
|
|
|
|
|
duration = TimeSpan.FromSeconds(durationSeconds);
|
|
|
|
|
++banReasonIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(parts.Length > 2) {
|
|
|
|
|
if(!double.TryParse(parts[2], out double silenceSeconds)) {
|
|
|
|
|
user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
|
|
|
|
|
break;
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
banUntil = DateTimeOffset.UtcNow.AddSeconds(silenceSeconds);
|
|
|
|
|
}
|
|
|
|
|
await FlashiiBanInfo.CreateAsync(
|
|
|
|
|
HttpClient,
|
|
|
|
|
banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(),
|
|
|
|
|
user.UserId.ToString(), sess.RemoteAddress.ToString(),
|
|
|
|
|
duration, banReason
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Context.BanUser(banUser, banUntil, isBanning);
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUser));
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
Context.Bans.Remove(unbanIP);
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
|