sharp-chat/SharpChat.Flashii/FlashiiClient.cs

259 lines
11 KiB
C#

using Microsoft.Extensions.Logging;
using SharpChat.Auth;
using SharpChat.Bans;
using SharpChat.Configuration;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using ZLogger;
namespace SharpChat.Flashii;
public class FlashiiClient(ILogger logger, HttpClient httpClient, Config config) : AuthClient, BansClient {
private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
private const string DEFAULT_SECRET_KEY = "woomy";
private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
private string CreateStringSignature(string str) {
return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
}
private string CreateBufferSignature(byte[] bytes) {
using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!));
return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
}
private const string AUTH_VERIFY_URL = "{0}/verify";
private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}";
public async Task<AuthResult> AuthVerify(IPAddress remoteAddr, string scheme, string token) {
logger.ZLogInformation($"Verifying authentication data for {remoteAddr}...");
logger.ZLogTrace($"AuthVerify({remoteAddr}, {scheme}, {token})");
string remoteAddrStr = remoteAddr.ToString();
HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "method", scheme },
{ "token", token },
{ "ipaddr", remoteAddrStr },
}),
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
logger.ZLogTrace($"AuthVerify() -> HTTP {response.StatusCode}");
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream);
if(authResult?.Success != true)
throw new AuthFailedException(authResult?.Reason ?? "none");
return authResult;
}
private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump";
public async Task AuthBumpUsersOnline(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
if(!entries.Any())
return;
logger.ZLogInformation($"Bumping online users list...");
string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
StringBuilder sb = new();
sb.AppendFormat("bump#{0}", now);
Dictionary<string, string> formData = new() {
{ "t", now },
};
foreach(var (remoteAddr, userId) in entries) {
string remoteAddrStr = remoteAddr.ToString();
sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr);
formData.Add(string.Format("u[{0}]", userId), remoteAddrStr);
}
HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
},
Content = new FormUrlEncodedContent(formData),
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
logger.ZLogTrace($"AuthBumpUsersOnline() -> HTTP {response.StatusCode}");
response.EnsureSuccessStatusCode();
}
private const string BANS_CREATE_URL = "{0}/bans/create";
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
public async Task BanCreate(
BanKind kind,
TimeSpan duration,
IPAddress remoteAddr,
string? userId = null,
string? reason = null,
IPAddress? issuerRemoteAddr = null,
string? issuerUserId = null
) {
logger.ZLogInformation($"Creating ban of kind {kind} with duration {duration} for {remoteAddr}/{userId} issued by {issuerRemoteAddr}/{issuerUserId}...");
if(duration <= TimeSpan.Zero || kind != BanKind.User)
return;
issuerUserId ??= string.Empty;
userId ??= string.Empty;
reason ??= string.Empty;
issuerRemoteAddr ??= IPAddress.IPv6None;
string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
string remoteAddrStr = remoteAddr.ToString();
string issuerRemoteAddrStr = issuerRemoteAddr.ToString();
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
string sig = string.Format(
BANS_CREATE_SIG,
now, userId, remoteAddrStr,
issuerUserId, issuerRemoteAddrStr,
durationStr, isPerma, reason
);
HttpRequestMessage request = 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", userId },
{ "ua", remoteAddrStr },
{ "mi", issuerUserId },
{ "ma", issuerRemoteAddrStr },
{ "d", durationStr },
{ "p", isPerma },
{ "r", reason },
}),
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
logger.ZLogTrace($"BanCreate() -> HTTP {response.StatusCode}");
response.EnsureSuccessStatusCode();
}
private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
public async Task<bool> BanRevoke(BanInfo info) {
string type;
string target;
if(info is UserBanInfo ubi) {
if(info.Kind != BanKind.User)
throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info));
type = "user";
target = ubi.UserId;
} else if(info is IPAddressBanInfo iabi) {
if(info.Kind != BanKind.IPAddress)
throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info));
type = "addr";
target = iabi.Address.ToString();
} else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info));
logger.ZLogInformation($"Revoking ban of kind {info.Kind} issued on {target}...");
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 request = new(HttpMethod.Delete, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
if(response.StatusCode == HttpStatusCode.NotFound)
return false;
logger.ZLogTrace($"BanRevoke() -> HTTP {response.StatusCode}");
response.EnsureSuccessStatusCode();
return response.StatusCode == HttpStatusCode.NoContent;
}
private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
public async Task<BanInfo?> BanGet(string? userIdOrName = null, IPAddress? remoteAddr = null) {
userIdOrName ??= "0";
remoteAddr ??= IPAddress.None;
logger.ZLogInformation($"Requesting ban info for {remoteAddr}/{userIdOrName}...");
string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9');
string remoteAddrStr = remoteAddr.ToString();
string usingUserNameStr = usingUserName ? "1" : "0";
string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr));
string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr);
HttpRequestMessage request = new(HttpMethod.Get, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
logger.ZLogTrace($"BanGet() -> HTTP {response.StatusCode}");
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream);
if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired)
return null;
return rawBanInfo.RemoteAddress is null or "::"
? new FlashiiUserBanInfo(rawBanInfo)
: new FlashiiIPAddressBanInfo(rawBanInfo);
}
private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
private const string BANS_LIST_SIG = "list#{0}";
public async Task<BanInfo[]> BanGetList() {
logger.ZLogInformation($"Requesting ban list...");
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 request = new(HttpMethod.Get, url) {
Headers = {
{ "X-SharpChat-Signature", CreateStringSignature(sig) },
},
};
using HttpResponseMessage response = await httpClient.SendAsync(request);
logger.ZLogTrace($"BanGetList() -> HTTP {response.StatusCode}");
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream);
if(list is null || list.Length < 1)
return [];
return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => {
return (BanInfo)(b.RemoteAddress is null or "::"
? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b));
})];
}
}