Ported the config system from old master.
This commit is contained in:
parent
56a818254e
commit
e1e3def62c
23 changed files with 814 additions and 438 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,6 +7,8 @@ login_key.txt
|
|||
http-motd.txt
|
||||
_webdb.txt
|
||||
msz_url.txt
|
||||
sharpchat.cfg
|
||||
SharpChat/version.txt
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
|
|
|
@ -31,13 +31,13 @@ namespace SharpChat {
|
|||
UserLeave(user.Channel, user, reason);
|
||||
}
|
||||
|
||||
public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess) {
|
||||
public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) {
|
||||
if(!chan.HasUser(user)) {
|
||||
chan.Send(new UserConnectPacket(DateTimeOffset.Now, user));
|
||||
Events.Add(new UserConnectEvent(DateTimeOffset.Now, user, chan));
|
||||
}
|
||||
|
||||
sess.Send(new AuthSuccessPacket(user, chan, sess));
|
||||
sess.Send(new AuthSuccessPacket(user, chan, sess, maxMsgLength));
|
||||
sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user })));
|
||||
|
||||
IEnumerable<IChatEvent> msgs = Events.GetTargetLog(chan);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using SharpChat.Flashii;
|
||||
using SharpChat.Misuzu;
|
||||
using SharpChat.Packet;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -120,12 +120,12 @@ namespace SharpChat {
|
|||
public ChatUser() {
|
||||
}
|
||||
|
||||
public ChatUser(FlashiiAuthInfo auth) {
|
||||
public ChatUser(MisuzuAuthInfo auth) {
|
||||
UserId = auth.UserId;
|
||||
ApplyAuth(auth, true);
|
||||
}
|
||||
|
||||
public void ApplyAuth(FlashiiAuthInfo auth, bool invalidateRestrictions = false) {
|
||||
public void ApplyAuth(MisuzuAuthInfo auth, bool invalidateRestrictions = false) {
|
||||
Username = auth.Username;
|
||||
|
||||
if(Status == ChatUserStatus.Offline)
|
||||
|
|
49
SharpChat/Config/CachedValue.cs
Normal file
49
SharpChat/Config/CachedValue.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
public class CachedValue<T> {
|
||||
private IConfig Config { get; }
|
||||
private string Name { get; }
|
||||
private TimeSpan Lifetime { get; }
|
||||
private T Fallback { get; }
|
||||
private object Sync { get; } = new();
|
||||
|
||||
private object CurrentValue { get; set; }
|
||||
private DateTimeOffset LastRead { get; set; }
|
||||
|
||||
public T Value {
|
||||
get {
|
||||
lock(Sync) {
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
if((now - LastRead) >= Lifetime) {
|
||||
LastRead = now;
|
||||
CurrentValue = Config.ReadValue(Name, Fallback);
|
||||
Logger.Debug($"Read {Name} ({CurrentValue})");
|
||||
}
|
||||
}
|
||||
return (T)CurrentValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator T(CachedValue<T> val) => val.Value;
|
||||
|
||||
public CachedValue(IConfig config, string name, TimeSpan lifetime, T fallback) {
|
||||
Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Lifetime = lifetime;
|
||||
Fallback = fallback;
|
||||
if(string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Name cannot be empty.", nameof(name));
|
||||
}
|
||||
|
||||
public void Refresh() {
|
||||
lock(Sync) {
|
||||
LastRead = DateTimeOffset.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
16
SharpChat/Config/ConfigExceptions.cs
Normal file
16
SharpChat/Config/ConfigExceptions.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
public abstract class ConfigException : Exception {
|
||||
public ConfigException(string message) : base(message) { }
|
||||
public ConfigException(string message, Exception ex) : base(message, ex) { }
|
||||
}
|
||||
|
||||
public class ConfigLockException : ConfigException {
|
||||
public ConfigLockException() : base("Unable to acquire lock for reading configuration.") { }
|
||||
}
|
||||
|
||||
public class ConfigTypeException : ConfigException {
|
||||
public ConfigTypeException(Exception ex) : base("Given type does not match the value in the configuration.", ex) { }
|
||||
}
|
||||
}
|
35
SharpChat/Config/IConfig.cs
Normal file
35
SharpChat/Config/IConfig.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
public interface IConfig : IDisposable {
|
||||
/// <summary>
|
||||
/// Creates a proxy object that forces all names to start with the given prefix.
|
||||
/// </summary>
|
||||
IConfig ScopeTo(string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a raw (string) value from the config.
|
||||
/// </summary>
|
||||
string ReadValue(string name, string fallback = null);
|
||||
|
||||
/// <summary>
|
||||
/// Reads and casts value from the config.
|
||||
/// </summary>
|
||||
/// <exception cref="ConfigTypeException">Type conversion failed.</exception>
|
||||
T ReadValue<T>(string name, T fallback = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads and casts a value from the config. Returns fallback when type conversion fails.
|
||||
/// </summary>
|
||||
T SafeReadValue<T>(string name, T fallback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values.
|
||||
/// </summary>
|
||||
CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null);
|
||||
}
|
||||
}
|
45
SharpChat/Config/ScopedConfig.cs
Normal file
45
SharpChat/Config/ScopedConfig.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
public class ScopedConfig : IConfig {
|
||||
private IConfig Config { get; }
|
||||
private string Prefix { get; }
|
||||
|
||||
public ScopedConfig(IConfig config, string prefix) {
|
||||
Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix));
|
||||
if(string.IsNullOrWhiteSpace(prefix))
|
||||
throw new ArgumentException("Prefix must exist.", nameof(prefix));
|
||||
if(Prefix[^1] != ':')
|
||||
Prefix += ':';
|
||||
}
|
||||
|
||||
private string GetName(string name) {
|
||||
return Prefix + name;
|
||||
}
|
||||
|
||||
public string ReadValue(string name, string fallback = null) {
|
||||
return Config.ReadValue(GetName(name), fallback);
|
||||
}
|
||||
|
||||
public T ReadValue<T>(string name, T fallback = default) {
|
||||
return Config.ReadValue(GetName(name), fallback);
|
||||
}
|
||||
|
||||
public T SafeReadValue<T>(string name, T fallback) {
|
||||
return Config.SafeReadValue(GetName(name), fallback);
|
||||
}
|
||||
|
||||
public IConfig ScopeTo(string prefix) {
|
||||
return Config.ScopeTo(GetName(prefix));
|
||||
}
|
||||
|
||||
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
|
||||
return Config.ReadCached(GetName(name), fallback, lifetime);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
112
SharpChat/Config/StreamConfig.cs
Normal file
112
SharpChat/Config/StreamConfig.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpChat.Config {
|
||||
public class StreamConfig : IConfig {
|
||||
private Stream Stream { get; }
|
||||
private StreamReader StreamReader { get; }
|
||||
private Mutex Lock { get; }
|
||||
|
||||
private const int LOCK_TIMEOUT = 10000;
|
||||
|
||||
private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15);
|
||||
|
||||
public StreamConfig(string fileName)
|
||||
: this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) { }
|
||||
|
||||
public StreamConfig(Stream stream) {
|
||||
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
if(!Stream.CanRead)
|
||||
throw new ArgumentException("Provided stream must be readable.", nameof(stream));
|
||||
if(!Stream.CanSeek)
|
||||
throw new ArgumentException("Provided stream must be seekable.", nameof(stream));
|
||||
StreamReader = new StreamReader(stream, new UTF8Encoding(false), false);
|
||||
Lock = new Mutex();
|
||||
}
|
||||
|
||||
public string ReadValue(string name, string fallback = null) {
|
||||
if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong
|
||||
throw new ConfigLockException();
|
||||
|
||||
try {
|
||||
Stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
string line;
|
||||
while((line = StreamReader.ReadLine()) != null) {
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
line = line.TrimStart();
|
||||
if(line.StartsWith(";") || line.StartsWith("#"))
|
||||
continue;
|
||||
|
||||
string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
if(parts.Length < 2 || !string.Equals(parts[0], name))
|
||||
continue;
|
||||
|
||||
return parts[1];
|
||||
}
|
||||
} finally {
|
||||
Lock.ReleaseMutex();
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public T ReadValue<T>(string name, T fallback = default) {
|
||||
object value = ReadValue(name);
|
||||
if(value == null)
|
||||
return fallback;
|
||||
|
||||
Type type = typeof(T);
|
||||
if(value is string strVal) {
|
||||
if(type == typeof(bool))
|
||||
value = !string.Equals(strVal, "0", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& !string.Equals(strVal, "false", StringComparison.InvariantCultureIgnoreCase);
|
||||
else if(type == typeof(string[]))
|
||||
value = strVal.Split(' ');
|
||||
}
|
||||
|
||||
try {
|
||||
return (T)Convert.ChangeType(value, type);
|
||||
} catch(InvalidCastException ex) {
|
||||
throw new ConfigTypeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public T SafeReadValue<T>(string name, T fallback) {
|
||||
try {
|
||||
return ReadValue(name, fallback);
|
||||
} catch(ConfigTypeException) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
public IConfig ScopeTo(string prefix) {
|
||||
return new ScopedConfig(this, prefix);
|
||||
}
|
||||
|
||||
public CachedValue<T> ReadCached<T>(string name, T fallback = default, TimeSpan? lifetime = null) {
|
||||
return new CachedValue<T>(this, name, lifetime ?? CACHE_LIFETIME, fallback);
|
||||
}
|
||||
|
||||
private bool IsDisposed;
|
||||
~StreamConfig()
|
||||
=> DoDispose();
|
||||
public void Dispose() {
|
||||
DoDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
private void DoDispose() {
|
||||
if(IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
StreamReader.Dispose();
|
||||
Stream.Dispose();
|
||||
Lock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
using MySqlConnector;
|
||||
using SharpChat.Config;
|
||||
using SharpChat.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
|
@ -13,13 +13,13 @@ namespace SharpChat {
|
|||
public static bool HasDatabase
|
||||
=> !string.IsNullOrWhiteSpace(ConnectionString);
|
||||
|
||||
public static void ReadConfig() {
|
||||
if(!File.Exists("mariadb.txt"))
|
||||
return;
|
||||
string[] config = File.ReadAllLines("mariadb.txt");
|
||||
if(config.Length < 4)
|
||||
return;
|
||||
Init(config[0], config[1], config[2], config[3]);
|
||||
public static void Init(IConfig config) {
|
||||
Init(
|
||||
config.ReadValue("host", "localhost"),
|
||||
config.ReadValue("user", string.Empty),
|
||||
config.ReadValue("pass", string.Empty),
|
||||
config.ReadValue("db", "sharpchat")
|
||||
);
|
||||
}
|
||||
|
||||
public static void Init(string host, string username, string password, string database) {
|
||||
|
|
|
@ -1,28 +1,7 @@
|
|||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpChat {
|
||||
public static class Extensions {
|
||||
public static string GetSignedHash(this string str, string key = null) {
|
||||
return Encoding.UTF8.GetBytes(str).GetSignedHash(key);
|
||||
}
|
||||
|
||||
public static string GetSignedHash(this byte[] bytes, string key = null) {
|
||||
key ??= File.Exists("login_key.txt") ? File.ReadAllText("login_key.txt") : "woomy";
|
||||
|
||||
StringBuilder sb = new();
|
||||
|
||||
using(HMACSHA256 algo = new(Encoding.UTF8.GetBytes(key))) {
|
||||
byte[] hash = algo.ComputeHash(bytes);
|
||||
|
||||
foreach(byte b in hash)
|
||||
sb.AppendFormat("{0:x2}", b);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string GetIdString(this byte[] buffer) {
|
||||
const string id_chars = "abcdefghijklmnopqrstuvwxyz0123456789-_ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
StringBuilder sb = new();
|
||||
|
|
|
@ -1,64 +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 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 BUMP = "/_sockchat/bump";
|
||||
private const string VERIFY = "/_sockchat/verify";
|
||||
|
||||
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 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() {
|
||||
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() {
|
||||
if(!File.Exists(BASE_URL_FILE))
|
||||
return BASE_URL_FALLBACK;
|
||||
string url = File.ReadAllText(BASE_URL_FILE).Trim().Trim('/');
|
||||
if(string.IsNullOrEmpty(url))
|
||||
return BASE_URL_FALLBACK;
|
||||
return url;
|
||||
}
|
||||
|
||||
public static string GetURL(string path) {
|
||||
return GetBaseURL() + path;
|
||||
}
|
||||
|
||||
public static async Task BumpUsersOnlineAsync(HttpClient client, IEnumerable<(string userId, string ipAddr)> list) {
|
||||
if(client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
if(list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
if(!list.Any())
|
||||
return;
|
||||
|
||||
string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||
StringBuilder sb = new();
|
||||
sb.AppendFormat("bump#{0}", now);
|
||||
|
||||
Dictionary<string, string> formData = new() {
|
||||
{ "t", now }
|
||||
};
|
||||
|
||||
foreach(var (userId, ipAddr) in list) {
|
||||
sb.AppendFormat("#{0}:{1}", userId, ipAddr);
|
||||
formData.Add(string.Format("u[{0}]", userId), ipAddr);
|
||||
}
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, BumpURL) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", sb.ToString().GetSignedHash() }
|
||||
},
|
||||
Content = new FormUrlEncodedContent(formData),
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await client.SendAsync(req);
|
||||
|
||||
try {
|
||||
res.EnsureSuccessStatusCode();
|
||||
} catch(HttpRequestException) {
|
||||
Logger.Debug(await res.Content.ReadAsStringAsync());
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
SharpChat/Misuzu/MisuzuAuthInfo.cs
Normal file
30
SharpChat/Misuzu/MisuzuAuthInfo.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuAuthInfo {
|
||||
[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; }
|
||||
}
|
||||
}
|
32
SharpChat/Misuzu/MisuzuBanInfo.cs
Normal file
32
SharpChat/Misuzu/MisuzuBanInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuBanInfo {
|
||||
[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);
|
||||
}
|
||||
}
|
249
SharpChat/Misuzu/MisuzuClient.cs
Normal file
249
SharpChat/Misuzu/MisuzuClient.cs
Normal file
|
@ -0,0 +1,249 @@
|
|||
using SharpChat.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpChat.Misuzu {
|
||||
public class MisuzuClient {
|
||||
private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
|
||||
private const string DEFAULT_SECRET_KEY = "woomy";
|
||||
|
||||
private const string BUMP_ONLINE_URL = "{0}/bump";
|
||||
private const string AUTH_VERIFY_URL = "{0}/verify";
|
||||
|
||||
private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
|
||||
private const string BANS_CREATE_URL = "{0}/bans/create";
|
||||
private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
|
||||
private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
|
||||
|
||||
private const string VERIFY_SIG = "verify#{0}#{1}#{2}";
|
||||
private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
|
||||
private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
|
||||
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
|
||||
private const string BANS_LIST_SIG = "list#{0}";
|
||||
|
||||
private readonly HttpClient HttpClient;
|
||||
|
||||
private CachedValue<string> BaseURL { get; }
|
||||
private CachedValue<string> SecretKey { get; }
|
||||
|
||||
public MisuzuClient(HttpClient httpClient, IConfig config) {
|
||||
if(config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
|
||||
BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
|
||||
SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
|
||||
}
|
||||
|
||||
public string CreateStringSignature(string str) {
|
||||
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
public string CreateBufferSignature(byte[] bytes) {
|
||||
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey));
|
||||
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
|
||||
}
|
||||
|
||||
public async Task<MisuzuAuthInfo> AuthVerifyAsync(string method, string token, string ipAddr) {
|
||||
method ??= string.Empty;
|
||||
token ??= string.Empty;
|
||||
ipAddr ??= string.Empty;
|
||||
|
||||
string sig = string.Format(VERIFY_SIG, method, token, ipAddr);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "method", method },
|
||||
{ "token", token },
|
||||
{ "ipaddr", ipAddr },
|
||||
}),
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
return JsonSerializer.Deserialize<MisuzuAuthInfo>(
|
||||
await res.Content.ReadAsByteArrayAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
|
||||
if(list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
if(!list.Any())
|
||||
return;
|
||||
|
||||
string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||
StringBuilder sb = new();
|
||||
sb.AppendFormat("bump#{0}", now);
|
||||
|
||||
Dictionary<string, string> formData = new() {
|
||||
{ "t", now }
|
||||
};
|
||||
|
||||
foreach(var (userId, ipAddr) in list) {
|
||||
sb.AppendFormat("#{0}:{1}", userId, ipAddr);
|
||||
formData.Add(string.Format("u[{0}]", userId), ipAddr);
|
||||
}
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, string.Format(BUMP_ONLINE_URL, BaseURL)) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
|
||||
},
|
||||
Content = new FormUrlEncodedContent(formData),
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
try {
|
||||
res.EnsureSuccessStatusCode();
|
||||
} catch(HttpRequestException) {
|
||||
Logger.Debug(await res.Content.ReadAsStringAsync());
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MisuzuBanInfo> CheckBanAsync(string userId = null, string ipAddr = null, bool userIdIsName = false) {
|
||||
userId ??= string.Empty;
|
||||
ipAddr ??= string.Empty;
|
||||
|
||||
string userIdIsNameStr = userIdIsName ? "1" : "0";
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr));
|
||||
string sig = string.Format(BANS_CHECK_SIG, now, userId, ipAddr, userIdIsNameStr);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Get, url) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
return JsonSerializer.Deserialize<MisuzuBanInfo>(
|
||||
await res.Content.ReadAsByteArrayAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<MisuzuBanInfo[]> GetBanListAsync() {
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
|
||||
string sig = string.Format(BANS_LIST_SIG, now);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Get, url) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
return JsonSerializer.Deserialize<MisuzuBanInfo[]>(
|
||||
await res.Content.ReadAsByteArrayAsync()
|
||||
);
|
||||
}
|
||||
|
||||
public enum BanRevokeKind {
|
||||
UserId,
|
||||
RemoteAddress,
|
||||
}
|
||||
|
||||
public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
|
||||
if(banInfo == null)
|
||||
throw new ArgumentNullException(nameof(banInfo));
|
||||
|
||||
string type = kind switch {
|
||||
BanRevokeKind.UserId => "user",
|
||||
BanRevokeKind.RemoteAddress => "addr",
|
||||
_ => throw new ArgumentException("Invalid kind specified.", nameof(kind)),
|
||||
};
|
||||
|
||||
string target = kind switch {
|
||||
BanRevokeKind.UserId => banInfo.UserId,
|
||||
BanRevokeKind.RemoteAddress => banInfo.RemoteAddress,
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
|
||||
string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
|
||||
string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Delete, url) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
};
|
||||
|
||||
using HttpResponseMessage res = await HttpClient.SendAsync(req);
|
||||
|
||||
if(res.StatusCode == HttpStatusCode.NotFound)
|
||||
return false;
|
||||
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
return res.StatusCode == HttpStatusCode.NoContent;
|
||||
}
|
||||
|
||||
public async Task CreateBanAsync(
|
||||
string targetId,
|
||||
string targetAddr,
|
||||
string modId,
|
||||
string modAddr,
|
||||
TimeSpan duration,
|
||||
string reason
|
||||
) {
|
||||
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(
|
||||
BANS_CREATE_SIG,
|
||||
now, targetId, targetAddr,
|
||||
modId, modAddr,
|
||||
durationStr, isPerma, reason
|
||||
);
|
||||
|
||||
HttpRequestMessage req = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
|
||||
Headers = {
|
||||
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
|
||||
},
|
||||
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 HttpClient.SendAsync(req);
|
||||
|
||||
res.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using SharpChat.Flashii;
|
||||
using SharpChat.Misuzu;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
@ -12,9 +12,9 @@ namespace SharpChat.Packet {
|
|||
|
||||
public class AuthFailPacket : ServerPacket {
|
||||
public AuthFailReason Reason { get; private set; }
|
||||
public FlashiiBanInfo BanInfo { get; private set; }
|
||||
public MisuzuBanInfo BanInfo { get; private set; }
|
||||
|
||||
public AuthFailPacket(AuthFailReason reason, FlashiiBanInfo fbi = null) {
|
||||
public AuthFailPacket(AuthFailReason reason, MisuzuBanInfo fbi = null) {
|
||||
Reason = reason;
|
||||
|
||||
if(reason == AuthFailReason.Banned)
|
||||
|
|
|
@ -7,11 +7,18 @@ namespace SharpChat.Packet {
|
|||
public ChatUser User { get; private set; }
|
||||
public ChatChannel Channel { get; private set; }
|
||||
public ChatUserSession Session { get; private set; }
|
||||
public int MaxMessageLength { get; private set; }
|
||||
|
||||
public AuthSuccessPacket(ChatUser user, ChatChannel channel, ChatUserSession sess) {
|
||||
public AuthSuccessPacket(
|
||||
ChatUser user,
|
||||
ChatChannel channel,
|
||||
ChatUserSession sess,
|
||||
int maxMsgLength
|
||||
) {
|
||||
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
|
||||
Session = sess ?? throw new ArgumentNullException(nameof(channel));
|
||||
MaxMessageLength = maxMsgLength;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> Pack() {
|
||||
|
@ -23,7 +30,7 @@ namespace SharpChat.Packet {
|
|||
sb.Append('\t');
|
||||
sb.Append(Channel.Name);
|
||||
sb.Append('\t');
|
||||
sb.Append(SockChatServer.MSG_LENGTH_MAX);
|
||||
sb.Append(MaxMessageLength);
|
||||
|
||||
return new[] { sb.ToString() };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using SharpChat.Flashii;
|
||||
using SharpChat.Misuzu;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -6,9 +6,9 @@ using System.Text;
|
|||
|
||||
namespace SharpChat.Packet {
|
||||
public class BanListPacket : ServerPacket {
|
||||
public IEnumerable<FlashiiBanInfo> Bans { get; private set; }
|
||||
public IEnumerable<MisuzuBanInfo> Bans { get; private set; }
|
||||
|
||||
public BanListPacket(IEnumerable<FlashiiBanInfo> bans) {
|
||||
public BanListPacket(IEnumerable<MisuzuBanInfo> bans) {
|
||||
Bans = bans ?? throw new ArgumentNullException(nameof(bans));
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ namespace SharpChat.Packet {
|
|||
sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds());
|
||||
sb.Append("\t-1\t0\fbanlist\f");
|
||||
|
||||
foreach(FlashiiBanInfo ban in Bans) {
|
||||
foreach(MisuzuBanInfo 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);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
using System;
|
||||
using SharpChat.Config;
|
||||
using SharpChat.Misuzu;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpChat {
|
||||
public class Program {
|
||||
public const ushort PORT = 6770;
|
||||
public const string CONFIG = "sharpchat.cfg";
|
||||
|
||||
public static void Main(string[] args) {
|
||||
Console.WriteLine(@" _____ __ ________ __ ");
|
||||
|
@ -12,12 +16,14 @@ namespace SharpChat {
|
|||
Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/");
|
||||
Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ ");
|
||||
Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ ");
|
||||
Console.WriteLine(@" / _/ Sock Chat Server");
|
||||
#if DEBUG
|
||||
Console.WriteLine(@"============================================ DEBUG ==");
|
||||
#endif
|
||||
|
||||
Database.ReadConfig();
|
||||
/**/Console.Write(@" /__/");
|
||||
if(SharpInfo.IsDebugBuild) {
|
||||
Console.WriteLine();
|
||||
Console.Write(@"== ");
|
||||
Console.Write(SharpInfo.VersionString);
|
||||
Console.WriteLine(@" == DBG ==");
|
||||
} else
|
||||
Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' '));
|
||||
|
||||
using ManualResetEvent mre = new(false);
|
||||
bool hasCancelled = false;
|
||||
|
@ -32,17 +38,105 @@ namespace SharpChat {
|
|||
|
||||
if(hasCancelled) return;
|
||||
|
||||
using HttpClient httpClient = new(new HttpClientHandler() {
|
||||
UseProxy = false, // we will never and the initial resolving takes forever on linux
|
||||
});
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", "SharpChat/20230206");
|
||||
string configFile = CONFIG;
|
||||
|
||||
// If the config file doesn't exist and we're using the default path, run the converter
|
||||
if(!File.Exists(configFile) && configFile == CONFIG)
|
||||
ConvertConfiguration();
|
||||
|
||||
using IConfig config = new StreamConfig(configFile);
|
||||
|
||||
Database.Init(config.ScopeTo("mariadb"));
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
using SockChatServer scs = new(httpClient, PORT);
|
||||
using HttpClient httpClient = new(new HttpClientHandler() {
|
||||
UseProxy = false,
|
||||
});
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName);
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
MisuzuClient msz = new(httpClient, config.ScopeTo("msz"));
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
using SockChatServer scs = new(httpClient, msz, config.ScopeTo("chat"));
|
||||
scs.Listen(mre);
|
||||
|
||||
mre.WaitOne();
|
||||
}
|
||||
|
||||
private static void ConvertConfiguration() {
|
||||
using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||
s.SetLength(0);
|
||||
s.Flush();
|
||||
|
||||
using StreamWriter sw = new(s, new UTF8Encoding(false));
|
||||
sw.WriteLine("# and ; can be used at the start of a line for comments.");
|
||||
sw.WriteLine();
|
||||
|
||||
sw.WriteLine("# General Configuration");
|
||||
sw.WriteLine($"#chat:port {SockChatServer.DEFAULT_PORT}");
|
||||
sw.WriteLine($"#chat:msgMaxLength {SockChatServer.DEFAULT_MSG_LENGTH_MAX}");
|
||||
sw.WriteLine($"#chat:connMaxCount {SockChatServer.DEFAULT_MAX_CONNECTIONS}");
|
||||
sw.WriteLine($"#chat:floodKickLength {SockChatServer.DEFAULT_FLOOD_KICK_LENGTH}");
|
||||
|
||||
|
||||
sw.WriteLine();
|
||||
sw.WriteLine("# Channels");
|
||||
sw.WriteLine("chat:channels lounge staff");
|
||||
sw.WriteLine();
|
||||
|
||||
sw.WriteLine("# Lounge channel settings");
|
||||
sw.WriteLine("chat:channels:lounge:name Lounge");
|
||||
sw.WriteLine("chat:channels:lounge:autoJoin true");
|
||||
sw.WriteLine();
|
||||
|
||||
sw.WriteLine("# Staff channel settings");
|
||||
sw.WriteLine("chat:channels:staff:name Staff");
|
||||
sw.WriteLine("chat:channels:staff:minRank 5");
|
||||
|
||||
|
||||
const string msz_secret = "login_key.txt";
|
||||
const string msz_url = "msz_url.txt";
|
||||
|
||||
sw.WriteLine();
|
||||
sw.WriteLine("# Misuzu integration settings");
|
||||
if(File.Exists(msz_secret))
|
||||
sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim()));
|
||||
else
|
||||
sw.WriteLine("#msz:secret woomy");
|
||||
if(File.Exists(msz_url))
|
||||
sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim()));
|
||||
else
|
||||
sw.WriteLine("#msz:url https://flashii.net/_sockchat");
|
||||
|
||||
|
||||
const string mdb_config = @"mariadb.txt";
|
||||
bool hasMDB = File.Exists(mdb_config);
|
||||
string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : Array.Empty<string>();
|
||||
|
||||
sw.WriteLine();
|
||||
sw.WriteLine("# MariaDB configuration");
|
||||
if(!string.IsNullOrWhiteSpace(mdbCfg[0]))
|
||||
sw.WriteLine($"mariadb:host {mdbCfg[0]}");
|
||||
else
|
||||
sw.WriteLine($"#mariadb:host <username>");
|
||||
if(mdbCfg.Length > 1)
|
||||
sw.WriteLine($"mariadb:user {mdbCfg[1]}");
|
||||
else
|
||||
sw.WriteLine($"#mariadb:user <username>");
|
||||
if(mdbCfg.Length > 2)
|
||||
sw.WriteLine($"mariadb:pass {mdbCfg[2]}");
|
||||
else
|
||||
sw.WriteLine($"#mariadb:pass <password>");
|
||||
if(mdbCfg.Length > 3)
|
||||
sw.WriteLine($"mariadb:db {mdbCfg[3]}");
|
||||
else
|
||||
sw.WriteLine($"#mariadb:db <database>");
|
||||
|
||||
sw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,4 +10,16 @@
|
|||
<PackageReference Include="MySqlConnector" Version="2.2.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="git describe --tags --abbrev=0 --always > version.txt" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="version.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="version.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
37
SharpChat/SharpInfo.cs
Normal file
37
SharpChat/SharpInfo.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpChat {
|
||||
public static class SharpInfo {
|
||||
private const string NAME = @"SharpChat";
|
||||
private const string UNKNOWN = @"XXXXXXX";
|
||||
|
||||
public static string VersionString { get; }
|
||||
public static string VersionStringShort { get; }
|
||||
public static bool IsDebugBuild { get; }
|
||||
|
||||
public static string ProgramName { get; }
|
||||
|
||||
static SharpInfo() {
|
||||
#if DEBUG
|
||||
IsDebugBuild = true;
|
||||
#endif
|
||||
|
||||
try {
|
||||
using Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(@"SharpChat.version.txt");
|
||||
using StreamReader sr = new(s);
|
||||
VersionString = sr.ReadLine().Trim();
|
||||
VersionStringShort = VersionString.Length > 10 ? VersionString[..10] : VersionString;
|
||||
} catch {
|
||||
VersionStringShort = VersionString = UNKNOWN;
|
||||
}
|
||||
|
||||
StringBuilder sb = new();
|
||||
sb.Append(NAME);
|
||||
sb.Append('/');
|
||||
sb.Append(VersionStringShort);
|
||||
ProgramName = sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
using Fleck;
|
||||
using SharpChat.Commands;
|
||||
using SharpChat.Config;
|
||||
using SharpChat.Events;
|
||||
using SharpChat.Flashii;
|
||||
using SharpChat.Misuzu;
|
||||
using SharpChat.Packet;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -15,15 +16,10 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace SharpChat {
|
||||
public class SockChatServer : IDisposable {
|
||||
public const int MSG_LENGTH_MAX = 5000;
|
||||
|
||||
#if DEBUG
|
||||
public const int MAX_CONNECTIONS = 9001;
|
||||
public const int FLOOD_KICK_LENGTH = 5;
|
||||
#else
|
||||
public const int MAX_CONNECTIONS = 5;
|
||||
public const int FLOOD_KICK_LENGTH = 30;
|
||||
#endif
|
||||
public const ushort DEFAULT_PORT = 6770;
|
||||
public const int DEFAULT_MSG_LENGTH_MAX = 5000;
|
||||
public const int DEFAULT_MAX_CONNECTIONS = 5;
|
||||
public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
|
@ -38,6 +34,11 @@ namespace SharpChat {
|
|||
public ChatContext Context { get; }
|
||||
|
||||
private readonly HttpClient HttpClient;
|
||||
private readonly MisuzuClient Misuzu;
|
||||
|
||||
private readonly CachedValue<int> MaxMessageLength;
|
||||
private readonly CachedValue<int> MaxConnections;
|
||||
private readonly CachedValue<int> FloodKickLength;
|
||||
|
||||
private IReadOnlyCollection<IChatCommand> Commands { get; } = new IChatCommand[] {
|
||||
new AFKCommand(),
|
||||
|
@ -54,22 +55,36 @@ namespace SharpChat {
|
|||
private ManualResetEvent Shutdown { get; set; }
|
||||
private bool IsShuttingDown = false;
|
||||
|
||||
public SockChatServer(HttpClient httpClient, ushort port) {
|
||||
Logger.Write("Starting Sock Chat server...");
|
||||
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IConfig config) {
|
||||
Logger.Write("Initialising Sock Chat server...");
|
||||
|
||||
HttpClient = httpClient;
|
||||
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
|
||||
|
||||
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
|
||||
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
|
||||
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
|
||||
|
||||
Context = new ChatContext();
|
||||
|
||||
Context.Channels.Add(new ChatChannel("Lounge"));
|
||||
#if DEBUG
|
||||
Context.Channels.Add(new ChatChannel("Programming"));
|
||||
Context.Channels.Add(new ChatChannel("Games"));
|
||||
Context.Channels.Add(new ChatChannel("Splatoon"));
|
||||
Context.Channels.Add(new ChatChannel("Password") { Password = "meow", });
|
||||
#endif
|
||||
Context.Channels.Add(new ChatChannel("Staff") { Rank = 5 });
|
||||
string[] channelNames = config.ReadValue("channels", new[] { "lounge" });
|
||||
|
||||
foreach(string channelName in channelNames) {
|
||||
ChatChannel channelInfo = new(channelName);
|
||||
IConfig channelCfg = config.ScopeTo($"channels:{channelName}");
|
||||
|
||||
string tmp;
|
||||
tmp = channelCfg.SafeReadValue("name", string.Empty);
|
||||
if(!string.IsNullOrWhiteSpace(tmp))
|
||||
channelInfo.Name = tmp;
|
||||
|
||||
channelInfo.Password = channelCfg.SafeReadValue("password", string.Empty);
|
||||
channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0);
|
||||
|
||||
Context.Channels.Add(channelInfo);
|
||||
}
|
||||
|
||||
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
|
||||
Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}");
|
||||
}
|
||||
|
||||
|
@ -87,6 +102,8 @@ namespace SharpChat {
|
|||
sock.OnError = err => OnError(sock, err);
|
||||
sock.OnMessage = msg => OnMessage(sock, msg);
|
||||
});
|
||||
|
||||
Logger.Write("Listening...");
|
||||
}
|
||||
|
||||
private void OnOpen(IWebSocketConnection conn) {
|
||||
|
@ -148,10 +165,9 @@ namespace SharpChat {
|
|||
|
||||
if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) {
|
||||
Task.Run(async () => {
|
||||
TimeSpan duration = TimeSpan.FromSeconds(FLOOD_KICK_LENGTH);
|
||||
TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength);
|
||||
|
||||
await FlashiiBanInfo.CreateAsync(
|
||||
HttpClient,
|
||||
await Misuzu.CreateBanAsync(
|
||||
sess.User.UserId.ToString(), sess.RemoteAddress.ToString(),
|
||||
string.Empty, "::1",
|
||||
duration,
|
||||
|
@ -186,7 +202,7 @@ namespace SharpChat {
|
|||
|
||||
if(bumpList.Any())
|
||||
Task.Run(async () => {
|
||||
await FlashiiUrls.BumpUsersOnlineAsync(HttpClient, bumpList);
|
||||
await Misuzu.BumpUsersOnlineAsync(bumpList);
|
||||
}).Wait();
|
||||
|
||||
LastBump = DateTimeOffset.UtcNow;
|
||||
|
@ -219,11 +235,11 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
Task.Run(async () => {
|
||||
FlashiiAuthInfo fai;
|
||||
MisuzuAuthInfo fai;
|
||||
string ipAddr = sess.RemoteAddress.ToString();
|
||||
|
||||
try {
|
||||
fai = await FlashiiAuthInfo.VerifyAsync(HttpClient, authMethod, authToken, ipAddr);
|
||||
fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr);
|
||||
} catch(Exception ex) {
|
||||
Logger.Write($"<{sess.Id}> Failed to authenticate: {ex}");
|
||||
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
||||
|
@ -242,9 +258,9 @@ namespace SharpChat {
|
|||
return;
|
||||
}
|
||||
|
||||
FlashiiBanInfo fbi;
|
||||
MisuzuBanInfo fbi;
|
||||
try {
|
||||
fbi = await FlashiiBanInfo.CheckAsync(HttpClient, fai.UserId.ToString(), ipAddr);
|
||||
fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr);
|
||||
} catch(Exception ex) {
|
||||
Logger.Write($"<{sess.Id}> Failed auth ban check: {ex}");
|
||||
sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid));
|
||||
|
@ -273,7 +289,7 @@ namespace SharpChat {
|
|||
}
|
||||
|
||||
// Enforce a maximum amount of connections per user
|
||||
if(aUser.SessionCount >= MAX_CONNECTIONS) {
|
||||
if(aUser.SessionCount >= MaxConnections) {
|
||||
sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions));
|
||||
sess.Dispose();
|
||||
return;
|
||||
|
@ -294,7 +310,7 @@ namespace SharpChat {
|
|||
sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line));
|
||||
}
|
||||
|
||||
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess);
|
||||
Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess, MaxMessageLength);
|
||||
}).Wait();
|
||||
break;
|
||||
|
||||
|
@ -327,8 +343,9 @@ namespace SharpChat {
|
|||
mChannel.Send(new UserUpdatePacket(mUser));
|
||||
}
|
||||
|
||||
if(messageText.Length > MSG_LENGTH_MAX)
|
||||
messageText = messageText[..MSG_LENGTH_MAX];
|
||||
int maxMsgLength = MaxMessageLength;
|
||||
if(messageText.Length > maxMsgLength)
|
||||
messageText = messageText[..maxMsgLength];
|
||||
|
||||
messageText = messageText.Trim();
|
||||
|
||||
|
@ -727,8 +744,8 @@ namespace SharpChat {
|
|||
|
||||
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()
|
||||
MisuzuBanInfo fbi = await Misuzu.CheckBanAsync(
|
||||
banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString()
|
||||
);
|
||||
|
||||
if(fbi.IsBanned && !fbi.HasExpired) {
|
||||
|
@ -736,8 +753,7 @@ namespace SharpChat {
|
|||
return;
|
||||
}
|
||||
|
||||
await FlashiiBanInfo.CreateAsync(
|
||||
HttpClient,
|
||||
await Misuzu.CreateBanAsync(
|
||||
banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(),
|
||||
user.UserId.ToString(), sess.RemoteAddress.ToString(),
|
||||
duration, banReason
|
||||
|
@ -770,14 +786,14 @@ namespace SharpChat {
|
|||
unbanUserTarget = unbanUser.UserId.ToString();
|
||||
|
||||
Task.Run(async () => {
|
||||
FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, unbanUserTarget, userIdIsName: unbanUserTargetIsName);
|
||||
MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(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);
|
||||
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId);
|
||||
if(wasBanned)
|
||||
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUserTarget));
|
||||
else
|
||||
|
@ -800,14 +816,14 @@ namespace SharpChat {
|
|||
unbanAddrTarget = unbanAddr.ToString();
|
||||
|
||||
Task.Run(async () => {
|
||||
FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, ipAddr: unbanAddrTarget);
|
||||
MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget);
|
||||
|
||||
if(!banInfo.IsBanned || banInfo.HasExpired) {
|
||||
user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget));
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasBanned = await banInfo.RevokeAsync(HttpClient, FlashiiBanInfo.RevokeKind.RemoteAddress);
|
||||
bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress);
|
||||
if(wasBanned)
|
||||
user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanAddrTarget));
|
||||
else
|
||||
|
@ -823,7 +839,7 @@ namespace SharpChat {
|
|||
|
||||
Task.Run(async () => {
|
||||
user.Send(new BanListPacket(
|
||||
await FlashiiBanInfo.GetListAsync(HttpClient)
|
||||
await Misuzu.GetBanListAsync()
|
||||
));
|
||||
}).Wait();
|
||||
break;
|
||||
|
|
Loading…
Reference in a new issue