2025-04-28 12:29:11 +00:00
using Microsoft.Extensions.Logging ;
2025-04-26 19:42:23 +00:00
using SharpChat.Auth ;
using SharpChat.Bans ;
using SharpChat.Configuration ;
using System.Net ;
using System.Security.Cryptography ;
using System.Text ;
using System.Text.Json ;
2025-04-28 12:29:11 +00:00
using ZLogger ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
namespace SharpChat.Flashii ;
2025-04-26 19:42:23 +00:00
2025-04-28 12:29:11 +00:00
public class FlashiiClient ( ILogger logger , HttpClient httpClient , Config config ) : AuthClient , BansClient {
2025-04-26 23:15:54 +00:00
private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat" ;
private readonly CachedValue < string > BaseURL = config . ReadCached ( "url" , DEFAULT_BASE_URL ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
private const string DEFAULT_SECRET_KEY = "woomy" ;
private readonly CachedValue < string > SecretKey = config . ReadCached ( "secret" , DEFAULT_SECRET_KEY ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
private string CreateStringSignature ( string str ) {
return CreateBufferSignature ( Encoding . UTF8 . GetBytes ( str ) ) ;
}
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
private string CreateBufferSignature ( byte [ ] bytes ) {
using HMACSHA256 algo = new ( Encoding . UTF8 . GetBytes ( SecretKey ! ) ) ;
return string . Concat ( algo . ComputeHash ( bytes ) . Select ( c = > c . ToString ( "x2" ) ) ) ;
}
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
private const string AUTH_VERIFY_URL = "{0}/verify" ;
private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}" ;
2025-04-26 19:42:23 +00:00
2025-04-27 00:33:59 +00:00
public async Task < AuthResult > AuthVerify ( IPAddress remoteAddr , string scheme , string token ) {
2025-04-28 12:29:11 +00:00
logger . ZLogInformation ( $"Verifying authentication data for {remoteAddr}..." ) ;
logger . ZLogTrace ( $"AuthVerify({remoteAddr}, {scheme}, {token})" ) ;
2025-04-26 23:15:54 +00:00
string remoteAddrStr = remoteAddr . ToString ( ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
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 ) ) } ,
} ,
} ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
using HttpResponseMessage response = await httpClient . SendAsync ( request ) ;
2025-04-28 12:29:11 +00:00
logger . ZLogTrace ( $"AuthVerify() -> HTTP {response.StatusCode}" ) ;
2025-04-26 23:15:54 +00:00
response . EnsureSuccessStatusCode ( ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
using Stream stream = await response . Content . ReadAsStreamAsync ( ) ;
FlashiiAuthResult ? authResult = await JsonSerializer . DeserializeAsync < FlashiiAuthResult > ( stream ) ;
if ( authResult ? . Success ! = true )
throw new AuthFailedException ( authResult ? . Reason ? ? "none" ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
return authResult ;
}
private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump" ;
2025-04-27 00:33:59 +00:00
public async Task AuthBumpUsersOnline ( IEnumerable < ( IPAddress remoteAddr , string userId ) > entries ) {
2025-04-26 23:15:54 +00:00
if ( ! entries . Any ( ) )
return ;
2025-04-28 13:03:38 +00:00
logger . ZLogInformation ( $"Bumping online users list..." ) ;
2025-04-26 23:15:54 +00:00
string now = DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) . ToString ( ) ;
StringBuilder sb = new ( ) ;
sb . AppendFormat ( "bump#{0}" , now ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
Dictionary < string , string > formData = new ( ) {
{ "t" , now } ,
} ;
foreach ( var ( remoteAddr , userId ) in entries ) {
2025-04-26 19:42:23 +00:00
string remoteAddrStr = remoteAddr . ToString ( ) ;
2025-04-26 23:15:54 +00:00
sb . AppendFormat ( "#{0}:{1}" , userId , remoteAddrStr ) ;
formData . Add ( string . Format ( "u[{0}]" , userId ) , remoteAddrStr ) ;
2025-04-26 19:42:23 +00:00
}
2025-04-26 23:15:54 +00:00
HttpRequestMessage request = new ( HttpMethod . Post , string . Format ( AUTH_BUMP_USERS_ONLINE_URL , BaseURL ) ) {
Headers = {
{ "X-SharpChat-Signature" , CreateStringSignature ( sb . ToString ( ) ) }
} ,
Content = new FormUrlEncodedContent ( formData ) ,
} ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
using HttpResponseMessage response = await httpClient . SendAsync ( request ) ;
2025-04-28 12:29:11 +00:00
logger . ZLogTrace ( $"AuthBumpUsersOnline() -> HTTP {response.StatusCode}" ) ;
2025-04-26 23:15:54 +00:00
response . EnsureSuccessStatusCode ( ) ;
}
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
private const string BANS_CREATE_URL = "{0}/bans/create" ;
private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}" ;
2025-04-27 00:33:59 +00:00
public async Task BanCreate (
2025-04-26 23:15:54 +00:00
BanKind kind ,
TimeSpan duration ,
IPAddress remoteAddr ,
string? userId = null ,
string? reason = null ,
IPAddress ? issuerRemoteAddr = null ,
string? issuerUserId = null
) {
2025-04-28 12:29:11 +00:00
logger . ZLogInformation ( $"Creating ban of kind {kind} with duration {duration} for {remoteAddr}/{userId} issued by {issuerRemoteAddr}/{issuerUserId}..." ) ;
2025-04-26 23:15:54 +00:00
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 ) ;
2025-04-28 12:29:11 +00:00
logger . ZLogTrace ( $"BanCreate() -> HTTP {response.StatusCode}" ) ;
2025-04-26 23:15:54 +00:00
response . EnsureSuccessStatusCode ( ) ;
}
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
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}" ;
2025-04-26 19:42:23 +00:00
2025-04-27 00:33:59 +00:00
public async Task < bool > BanRevoke ( BanInfo info ) {
2025-04-26 23:15:54 +00:00
string type ;
string target ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
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 ) ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
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 ) ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
type = "addr" ;
target = iabi . Address . ToString ( ) ;
} else throw new ArgumentException ( "info argument is set to unsupported implementation" , nameof ( info ) ) ;
2025-04-26 19:42:23 +00:00
2025-04-28 12:29:11 +00:00
logger . ZLogInformation ( $"Revoking ban of kind {info.Kind} issued on {target}..." ) ;
2025-04-26 23:15:54 +00:00
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 ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
HttpRequestMessage request = new ( HttpMethod . Delete , url ) {
Headers = {
{ "X-SharpChat-Signature" , CreateStringSignature ( sig ) } ,
} ,
} ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
using HttpResponseMessage response = await httpClient . SendAsync ( request ) ;
if ( response . StatusCode = = HttpStatusCode . NotFound )
return false ;
2025-04-26 19:42:23 +00:00
2025-04-28 12:29:11 +00:00
logger . ZLogTrace ( $"BanRevoke() -> HTTP {response.StatusCode}" ) ;
2025-04-26 23:15:54 +00:00
response . EnsureSuccessStatusCode ( ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
return response . StatusCode = = HttpStatusCode . NoContent ;
}
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
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}" ;
2025-04-27 00:33:59 +00:00
public async Task < BanInfo ? > BanGet ( string? userIdOrName = null , IPAddress ? remoteAddr = null ) {
2025-04-26 23:15:54 +00:00
userIdOrName ? ? = "0" ;
remoteAddr ? ? = IPAddress . None ;
2025-04-28 12:29:11 +00:00
logger . ZLogInformation ( $"Requesting ban info for {remoteAddr}/{userIdOrName}..." ) ;
2025-04-26 23:15:54 +00:00
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 ) ;
2025-04-28 12:29:11 +00:00
logger . ZLogTrace ( $"BanGet() -> HTTP {response.StatusCode}" ) ;
2025-04-26 23:15:54 +00:00
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 ) ;
}
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
private const string BANS_LIST_URL = "{0}/bans/list?x={1}" ;
private const string BANS_LIST_SIG = "list#{0}" ;
2025-04-26 19:42:23 +00:00
2025-04-27 00:33:59 +00:00
public async Task < BanInfo [ ] > BanGetList ( ) {
2025-04-28 12:29:11 +00:00
logger . ZLogInformation ( $"Requesting ban list..." ) ;
2025-04-26 23:15:54 +00:00
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 ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
HttpRequestMessage request = new ( HttpMethod . Get , url ) {
Headers = {
{ "X-SharpChat-Signature" , CreateStringSignature ( sig ) } ,
} ,
} ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
using HttpResponseMessage response = await httpClient . SendAsync ( request ) ;
2025-04-28 12:29:11 +00:00
logger . ZLogTrace ( $"BanGetList() -> HTTP {response.StatusCode}" ) ;
2025-04-26 23:15:54 +00:00
response . EnsureSuccessStatusCode ( ) ;
2025-04-26 19:42:23 +00:00
2025-04-26 23:15:54 +00:00
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 ) ) ;
} ) ] ;
2025-04-26 19:42:23 +00:00
}
}