From 09a863d03689b0e95506df50798dafa6173f0390 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 23 Mar 2024 19:57:28 -0400 Subject: [PATCH] A lot of changes to the AniDB Rate Limiter and Internal Logic for Pings and Logout --- .../API/v3/Controllers/DebugController.cs | 6 +- .../Providers/AniDB/AniDBRateLimiter.cs | 54 +++-- .../Providers/AniDB/ConnectionHandler.cs | 40 ++-- .../AniDB/HTTP/AniDBHttpConnectionHandler.cs | 55 ++--- .../Providers/AniDB/HTTP/HttpRateLimiter.cs | 6 + .../AniDB/Interfaces/IUDPConnectionHandler.cs | 6 +- .../AniDB/UDP/AniDBUDPConnectionHandler.cs | 205 +++++++++--------- .../AniDB/UDP/Connection/RequestLogin.cs | 2 +- .../AniDB/UDP/Connection/RequestLogout.cs | 19 ++ .../AniDB/UDP/Connection/RequestPing.cs | 2 +- .../Providers/AniDB/UDP/Generic/UDPRequest.cs | 4 +- .../Providers/AniDB/UDP/UDPRateLimiter.cs | 6 + Shoko.Server/Server/Constants.cs | 106 +-------- 13 files changed, 222 insertions(+), 289 deletions(-) diff --git a/Shoko.Server/API/v3/Controllers/DebugController.cs b/Shoko.Server/API/v3/Controllers/DebugController.cs index 0bccef062..2318b8b91 100644 --- a/Shoko.Server/API/v3/Controllers/DebugController.cs +++ b/Shoko.Server/API/v3/Controllers/DebugController.cs @@ -108,8 +108,8 @@ public async Task CallAniDB([FromBody] AnidbUdpRequest request } var fullResponse = request.Unsafe ? - await _udpHandler.CallAniDBUDPDirectly(request.Command, isPing: request.IsPing) : - await _udpHandler.CallAniDBUDP(request.Command, isPing: request.IsPing); + await _udpHandler.SendDirectly(request.Command, resetTimers: request.IsPing) : + await _udpHandler.Send(request.Command, isPing: request.IsPing); var decodedParts = fullResponse.Split('\n'); var decodedResponse = string.Join('\n', fullResponse.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) @@ -144,7 +144,7 @@ await _udpHandler.CallAniDBUDPDirectly(request.Command, isPing: request.IsPing) { var errorMessage = $"{(int)returnCode} {returnCode}"; _logger.LogTrace("Waiting. AniDB returned {StatusCode} {Status}", (int)returnCode, returnCode); - _udpHandler.ExtendBanTimer(300, errorMessage); + _udpHandler.StartBackoffTimer(300, errorMessage); break; } case UDPReturnCode.UNKNOWN_COMMAND: diff --git a/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs b/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs index 8d69b18c3..216688402 100644 --- a/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs +++ b/Shoko.Server/Providers/AniDB/AniDBRateLimiter.cs @@ -1,14 +1,16 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Threading; -using NLog; +using Microsoft.Extensions.Logging; namespace Shoko.Server.Providers.AniDB; public abstract class AniDBRateLimiter { - protected static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - + private readonly ILogger _logger; private readonly object _lock = new(); + private readonly Stopwatch _requestWatch = new(); + private readonly Stopwatch _activeTimeWatch = new(); // Short Term: // A Client MUST NOT send more than 0.5 packets per second(that's one packet every two seconds, not two packets a second!) @@ -26,56 +28,50 @@ public abstract class AniDBRateLimiter // Switch to shorter delay after 30 minutes of inactivity protected abstract long resetPeriod { get; init; } - private readonly Stopwatch _requestWatch = new(); - - private readonly Stopwatch _activeTimeWatch = new(); - - public AniDBRateLimiter() + protected AniDBRateLimiter(ILogger logger) { + _logger = logger; _requestWatch.Start(); _activeTimeWatch.Start(); } - public void ResetRate() + private void ResetRate() { var elapsedTime = _activeTimeWatch.ElapsedMilliseconds; _activeTimeWatch.Restart(); - Logger.Trace($"Rate is reset. Active time was {elapsedTime} ms."); + _logger.LogTrace("Rate is reset. Active time was {Time} ms", elapsedTime); } - public void Reset() + public T EnsureRate(Func action) { - _requestWatch.Restart(); - } - - public void EnsureRate() - { - lock (_lock) + try { - var delay = _requestWatch.ElapsedMilliseconds; - - if (delay > resetPeriod) - { - ResetRate(); - } + Monitor.Enter(_lock); + var delay = _requestWatch.ElapsedMilliseconds; + if (delay > resetPeriod) ResetRate(); var currentDelay = _activeTimeWatch.ElapsedMilliseconds > shortPeriod ? LongDelay : ShortDelay; if (delay > currentDelay) { - Logger.Trace($"Time since last request is {delay} ms, not throttling."); - _requestWatch.Restart(); - return; + _logger.LogTrace("Time since last request is {Delay} ms, not throttling", delay); + _logger.LogTrace("Sending AniDB command"); + return action(); } // add 50ms for good measure var waitTime = currentDelay - (int)delay + 50; - Logger.Trace($"Time since last request is {delay} ms, throttling for {waitTime}."); + _logger.LogTrace("Time since last request is {Delay} ms, throttling for {Time}", delay, waitTime); Thread.Sleep(waitTime); - Logger.Trace("Sending AniDB command."); + _logger.LogTrace("Sending AniDB command"); + return action(); + } + finally + { _requestWatch.Restart(); + Monitor.Exit(_lock); } } } diff --git a/Shoko.Server/Providers/AniDB/ConnectionHandler.cs b/Shoko.Server/Providers/AniDB/ConnectionHandler.cs index b40332e4d..ad28a36e0 100644 --- a/Shoko.Server/Providers/AniDB/ConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/ConnectionHandler.cs @@ -11,10 +11,10 @@ public abstract class ConnectionHandler protected AniDBRateLimiter RateLimiter { get; set; } public abstract double BanTimerResetLength { get; } public abstract string Type { get; } - public abstract UpdateType BanEnum { get; } + protected abstract UpdateType BanEnum { get; } public event EventHandler AniDBStateUpdate; - protected AniDBStateUpdate _currentState; + private AniDBStateUpdate _currentState; public AniDBStateUpdate State { @@ -29,8 +29,9 @@ public AniDBStateUpdate State } } - protected int? ExtendPauseSecs { get; set; } - private Timer BanResetTimer; + protected int? BackoffSecs { get; set; } + private readonly Timer _backoffTimer; + private readonly Timer _banResetTimer; public DateTime? BanTime { get; set; } private bool _isBanned; @@ -44,13 +45,13 @@ public bool IsBanned { BanTime = DateTime.Now; Logger.LogWarning("AniDB {Type} Banned!", Type); - if (BanResetTimer.Enabled) + if (_banResetTimer.Enabled) { Logger.LogWarning("AniDB {Type} ban timer was already running, ban time extending", Type); - BanResetTimer.Stop(); //re-start implies stop + _banResetTimer.Stop(); //re-start implies stop } - BanResetTimer.Start(); + _banResetTimer.Start(); State = new AniDBStateUpdate { Value = true, @@ -61,9 +62,9 @@ public bool IsBanned } else { - if (BanResetTimer.Enabled) + if (_banResetTimer.Enabled) { - BanResetTimer.Stop(); + _banResetTimer.Stop(); Logger.LogInformation("AniDB {Type} ban timer stopped. Resuming queue if not paused", Type); } @@ -72,21 +73,24 @@ public bool IsBanned } } - public ConnectionHandler(ILoggerFactory loggerFactory, AniDBRateLimiter rateLimiter) + protected ConnectionHandler(ILoggerFactory loggerFactory, AniDBRateLimiter rateLimiter) { _loggerFactory = loggerFactory; Logger = loggerFactory.CreateLogger(GetType()); RateLimiter = rateLimiter; - BanResetTimer = new Timer + _banResetTimer = new Timer { AutoReset = false, Interval = TimeSpan.FromHours(BanTimerResetLength).TotalMilliseconds }; - BanResetTimer.Elapsed += BanResetTimerElapsed; + _banResetTimer.Elapsed += BanResetTimerElapsed; + _backoffTimer = new Timer { AutoReset = false }; + _backoffTimer.Elapsed += ResetBackoffTimer; } ~ConnectionHandler() { - BanResetTimer.Elapsed -= BanResetTimerElapsed; + _banResetTimer.Elapsed -= BanResetTimerElapsed; + _backoffTimer.Elapsed -= ResetBackoffTimer; } private void BanResetTimerElapsed(object sender, ElapsedEventArgs e) @@ -95,10 +99,12 @@ private void BanResetTimerElapsed(object sender, ElapsedEventArgs e) IsBanned = false; } - protected void ExtendBanTimer(int secsToPause, string pauseReason) + protected void StartBackoffTimer(int secsToPause, string pauseReason) { // This Handles the Waiting Period For When AniDB is under heavy load. Not likely to be used - ExtendPauseSecs = secsToPause; + BackoffSecs = secsToPause; + _backoffTimer.Interval = secsToPause * 1000; + _backoffTimer.Start(); AniDBStateUpdate?.Invoke(this, new AniDBStateUpdate { @@ -110,10 +116,10 @@ protected void ExtendBanTimer(int secsToPause, string pauseReason) }); } - protected void ResetBanTimer() + protected void ResetBackoffTimer(object sender, ElapsedEventArgs args) { // This Handles the Waiting Period For When AniDB is under heavy load. Not likely to be used - ExtendPauseSecs = null; + BackoffSecs = null; AniDBStateUpdate?.Invoke(this, new AniDBStateUpdate { UpdateType = UpdateType.OverloadBackoff, Value = false, UpdateTime = DateTime.Now }); } diff --git a/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs b/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs index f3b3cba76..c2a854b71 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs @@ -14,7 +14,7 @@ public class AniDBHttpConnectionHandler : ConnectionHandler, IHttpConnectionHand public override double BanTimerResetLength => 12; public override string Type => "HTTP"; - public override UpdateType BanEnum => UpdateType.HTTPBan; + protected override UpdateType BanEnum => UpdateType.HTTPBan; public bool IsAlive => true; public AniDBHttpConnectionHandler(ILoggerFactory loggerFactory, HttpRateLimiter rateLimiter) : base(loggerFactory, rateLimiter) @@ -46,37 +46,40 @@ public async Task> GetHttpDirectly(string url) }; } - RateLimiter.EnsureRate(); - - using var response = await _httpClient.GetAsync(url); - response.EnsureSuccessStatusCode(); - - var responseStream = await response.Content.ReadAsStreamAsync(); - if (responseStream == null) + var response = await RateLimiter.EnsureRate(async () => { - throw new EndOfStreamException("Response Body was expected, but none returned"); - } + using var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); - var charset = response.Content.Headers.ContentType?.CharSet; - Encoding encoding = null; - if (!string.IsNullOrEmpty(charset)) - { - encoding = Encoding.GetEncoding(charset); - } + var responseStream = await response.Content.ReadAsStreamAsync(); + if (responseStream == null) + { + throw new EndOfStreamException("Response Body was expected, but none returned"); + } - encoding ??= Encoding.UTF8; - using var reader = new StreamReader(responseStream, encoding); - var output = await reader.ReadToEndAsync(); + var charset = response.Content.Headers.ContentType?.CharSet; + Encoding encoding = null; + if (!string.IsNullOrEmpty(charset)) + { + encoding = Encoding.GetEncoding(charset); + } - if (CheckForBan(output)) - { - throw new AniDBBannedException + encoding ??= Encoding.UTF8; + using var reader = new StreamReader(responseStream, encoding); + var output = await reader.ReadToEndAsync(); + + if (CheckForBan(output)) { - BanType = UpdateType.HTTPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength) - }; - } + throw new AniDBBannedException + { + BanType = UpdateType.HTTPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength) + }; + } + + return new HttpResponse { Response = output, Code = response.StatusCode }; + }); - return new HttpResponse { Response = output, Code = response.StatusCode }; + return response; } private bool CheckForBan(string xmlResult) diff --git a/Shoko.Server/Providers/AniDB/HTTP/HttpRateLimiter.cs b/Shoko.Server/Providers/AniDB/HTTP/HttpRateLimiter.cs index ac5d075ac..7c3c41398 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/HttpRateLimiter.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/HttpRateLimiter.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; + namespace Shoko.Server.Providers.AniDB.HTTP; public class HttpRateLimiter : AniDBRateLimiter @@ -6,4 +8,8 @@ public class HttpRateLimiter : AniDBRateLimiter protected override int LongDelay { get; init; } = 4000; protected override long shortPeriod { get; init; } = 1000000; protected override long resetPeriod { get; init; } = 1800000; + + public HttpRateLimiter(ILogger logger) : base(logger) + { + } } diff --git a/Shoko.Server/Providers/AniDB/Interfaces/IUDPConnectionHandler.cs b/Shoko.Server/Providers/AniDB/Interfaces/IUDPConnectionHandler.cs index 853026de3..740167949 100644 --- a/Shoko.Server/Providers/AniDB/Interfaces/IUDPConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/Interfaces/IUDPConnectionHandler.cs @@ -16,12 +16,12 @@ public interface IUDPConnectionHandler : IConnectionHandler void ForceLogout(); Task CloseConnections(); Task ForceReconnection(); - void ExtendBanTimer(int time, string message); + void StartBackoffTimer(int time, string message); Task Init(); Task Init(string username, string password, string serverName, ushort serverPort, ushort clientPort); Task TestLogin(string username, string password); - Task CallAniDBUDPDirectly(string command, bool needsUnicode = true, bool isPing = false); + Task SendDirectly(string command, bool needsUnicode = true, bool resetTimers = true); - Task CallAniDBUDP(string command, bool needsUnicode = true, bool isPing = false); + Task Send(string command, bool needsUnicode = true, bool isPing = false); } diff --git a/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs b/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs index 0f3295b44..65f44b56d 100644 --- a/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/UDP/AniDBUDPConnectionHandler.cs @@ -18,6 +18,10 @@ namespace Shoko.Server.Providers.AniDB.UDP; public class AniDBUDPConnectionHandler : ConnectionHandler, IUDPConnectionHandler { + // 10 minutes + private const int LogoutPeriod = 10 * 60 * 1000; + // 45 seconds + private const int PingFrequency = 45 * 1000; private readonly IRequestFactory _requestFactory; private IAniDBSocketHandler _socketHandler; private static readonly Regex s_logMask = new("(?<=(\\bpass=|&pass=\\bs=|&s=))[^&]+", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -26,7 +30,7 @@ public class AniDBUDPConnectionHandler : ConnectionHandler, IUDPConnectionHandle public override double BanTimerResetLength => 1.5D; public override string Type => "UDP"; - public override UpdateType BanEnum => UpdateType.UDPBan; + protected override UpdateType BanEnum => UpdateType.UDPBan; public string SessionID { get; private set; } public bool IsAlive { get; private set; } @@ -37,8 +41,10 @@ public class AniDBUDPConnectionHandler : ConnectionHandler, IUDPConnectionHandle private ISettingsProvider SettingsProvider { get; set; } - private Timer _pulseTimer; + private Timer _pingTimer; + private Timer _logoutTimer; + private bool _isLoggedOn; private bool _isInvalidSession; public bool IsInvalidSession @@ -55,25 +61,10 @@ public bool IsInvalidSession } } - private bool _isLoggedOn; - - public bool IsLoggedOn - { - get => _isLoggedOn; - set => _isLoggedOn = value; - } - public bool IsNetworkAvailable { private set; get; } - private DateTime LastAniDBPing { get; set; } = DateTime.MinValue; - - private DateTime LastAniDBMessageNonPing { get; set; } = DateTime.MinValue; - - private DateTime LastMessage => - LastAniDBMessageNonPing < LastAniDBPing ? LastAniDBPing : LastAniDBMessageNonPing; - - public AniDBUDPConnectionHandler(IRequestFactory requestFactory, ILoggerFactory loggerFactory, - ISettingsProvider settings, UDPRateLimiter rateLimiter) : base(loggerFactory, rateLimiter) + public AniDBUDPConnectionHandler(IRequestFactory requestFactory, ILoggerFactory loggerFactory, ISettingsProvider settings, UDPRateLimiter rateLimiter) : + base(loggerFactory, rateLimiter) { _requestFactory = requestFactory; SettingsProvider = settings; @@ -85,9 +76,9 @@ public AniDBUDPConnectionHandler(IRequestFactory requestFactory, ILoggerFactory CloseConnections().GetAwaiter().GetResult(); } - public new void ExtendBanTimer(int time, string message) + void IUDPConnectionHandler.StartBackoffTimer(int time, string message) { - base.ExtendBanTimer(time, message); + base.StartBackoffTimer(time, message); } public async Task Init() @@ -129,48 +120,40 @@ private async Task InitInternal() _isLoggedOn = false; IsNetworkAvailable = await _socketHandler.TryConnection(); + _pingTimer = new Timer { Interval = PingFrequency, Enabled = true, AutoReset = true }; + _pingTimer.Elapsed += PingTimerElapsed; + _logoutTimer = new Timer { Interval = LogoutPeriod, Enabled = true, AutoReset = false }; + _logoutTimer.Elapsed += LogoutTimerElapsed; - _pulseTimer = new Timer { Interval = 5000, Enabled = true, AutoReset = true }; - _pulseTimer.Elapsed += PulseTimerElapsed; - - Logger.LogInformation("starting ping timer..."); - _pulseTimer.Start(); IsAlive = true; } - private void PulseTimerElapsed(object sender, ElapsedEventArgs e) + private void PingTimerElapsed(object sender, ElapsedEventArgs e) { try { - var tempTimestamp = DateTime.Now - LastMessage; - if (ExtendPauseSecs.HasValue && tempTimestamp.TotalSeconds >= ExtendPauseSecs.Value) - { - ResetBanTimer(); - } - - if (!_isLoggedOn) - { - return; - } - - // don't ping when AniDB is taking a long time to respond + if (!_isLoggedOn) return; if (_socketHandler.IsLocked) return; + if (IsBanned || BackoffSecs.HasValue) return; - var nonPingTimestamp = DateTime.Now - LastAniDBMessageNonPing; - var pingTimestamp = DateTime.Now - LastAniDBPing; - tempTimestamp = DateTime.Now - LastMessage; + var ping = _requestFactory.Create(); + ping.Send(); + } + catch (Exception exception) + { + Logger.LogError(exception, "{Message}", exception); + } + } - // if we haven't sent a command for 45 seconds, send a ping just to keep the connection alive - if (tempTimestamp.TotalSeconds >= Constants.PingFrequency && - pingTimestamp.TotalSeconds >= Constants.PingFrequency && - !IsBanned && !ExtendPauseSecs.HasValue) - { - var ping = _requestFactory.Create(); - ping.Send(); - } + private void LogoutTimerElapsed(object sender, ElapsedEventArgs e) + { + try + { + if (!_isLoggedOn) return; + if (_socketHandler.IsLocked) return; + if (IsBanned || BackoffSecs.HasValue) return; - if (nonPingTimestamp.TotalSeconds > Constants.ForceLogoutPeriod) // after 10 minutes - ForceLogout(); + ForceLogout(); } catch (Exception exception) { @@ -185,14 +168,13 @@ private void PulseTimerElapsed(object sender, ElapsedEventArgs e) /// Only for Login, specify whether to ask for UTF16 /// is it a ping command /// - public async Task CallAniDBUDP(string command, bool needsUnicode = true, bool isPing = false) + public async Task Send(string command, bool needsUnicode = true, bool isPing = false) { // Steps: // 1. Check Ban state and throw if Banned // 2. Check Login State and Login if needed // 3. Actually Call AniDB - if (_socketHandler == null) throw new ObjectDisposedException("The connection was closed by shoko before this request was made"); // Check Ban State // Ideally, this will never happen, as we stop the queue and attempt a graceful rollback of the command if (IsBanned) @@ -205,7 +187,7 @@ public async Task CallAniDBUDP(string command, bool needsUnicode = true, // TODO Low Priority: We need to handle Login Attempt Decay, so that we can try again if it's not just a bad user/pass // It wasn't handled before, and it's not caused serious problems - // if we got here and it's invalid session, then it already failed to re-log + // if we got here, and it's invalid session, then it already failed to re-log if (IsInvalidSession) { throw new NotLoggedInException(); @@ -218,10 +200,32 @@ public async Task CallAniDBUDP(string command, bool needsUnicode = true, } // Actually Call AniDB - return await CallAniDBUDPDirectly(command, needsUnicode, isPing); + return await SendDirectly(command, needsUnicode, isPing); + } + + public Task SendDirectly(string command, bool needsUnicode = true, bool resetTimers = true) + { + try + { + if (resetTimers) + { + _pingTimer.Stop(); + _logoutTimer.Stop(); + } + + return SendInternal(command, needsUnicode); + } + finally + { + if (resetTimers) + { + _pingTimer.Start(); + _logoutTimer.Start(); + } + } } - public async Task CallAniDBUDPDirectly(string command, bool needsUnicode = true, bool isPing = false) + private async Task SendInternal(string command, bool needsUnicode = true) { // 1. Call AniDB // 2. Decode the response, converting Unicode and decompressing, as needed @@ -229,40 +233,44 @@ public async Task CallAniDBUDPDirectly(string command, bool needsUnicode // 4. Return a pretty response object, with a parsed return code and trimmed string var encoding = needsUnicode ? new UnicodeEncoding(true, false) : Encoding.ASCII; - RateLimiter.EnsureRate(); if (_socketHandler == null) throw new ObjectDisposedException("The connection was closed by shoko"); - var start = DateTime.Now; - - Logger.LogTrace("AniDB UDP Call: (Using {Unicode}) {Command}", needsUnicode ? "Unicode" : "ASCII", MaskLog(command)); var sendByteAdd = encoding.GetBytes(command); - StampLastMessage(isPing); - var byReceivedAdd = await _socketHandler.Send(sendByteAdd); - StampLastMessage(isPing); - - if (byReceivedAdd.All(a => a == 0)) + var decodedString = await RateLimiter.EnsureRate(async () => { - // we are probably banned or have lost connection. We can't tell the difference, so we're assuming ban - IsBanned = true; - throw new AniDBBannedException + var start = DateTime.Now; + + Logger.LogTrace("AniDB UDP Call: (Using {Unicode}) {Command}", needsUnicode ? "Unicode" : "ASCII", MaskLog(command)); + var byReceivedAdd = await _socketHandler.Send(sendByteAdd); + + if (byReceivedAdd.All(a => a == 0)) { - BanType = UpdateType.UDPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength) - }; - } + // we are probably banned or have lost connection. We can't tell the difference, so we're assuming ban + IsBanned = true; + throw new AniDBBannedException + { + BanType = UpdateType.UDPBan, BanExpires = BanTime?.AddHours(BanTimerResetLength) + }; + } - // decode - var decodedString = Utils.GetEncoding(byReceivedAdd).GetString(byReceivedAdd, 0, byReceivedAdd.Length); - if (decodedString[0] == 0xFEFF) // remove BOM - { - decodedString = decodedString[1..]; - } + // decode + var decodedString = Utils.GetEncoding(byReceivedAdd).GetString(byReceivedAdd, 0, byReceivedAdd.Length); + // remove BOM + if (decodedString[0] == 0xFEFF) decodedString = decodedString[1..]; - var ts = DateTime.Now - start; - Logger.LogTrace("AniDB Response: Received in {Time:ss'.'ffff}s\n{DecodedString}", ts, MaskLog(decodedString)); + var ts = DateTime.Now - start; + Logger.LogTrace("AniDB Response: Received in {Time:ss'.'ffff}s\n{DecodedString}", ts, MaskLog(decodedString)); + return decodedString; + }); return decodedString; } + private void StopPinging() + { + _pingTimer?.Stop(); + } + public async Task ForceReconnection() { try @@ -293,24 +301,10 @@ public async Task ForceReconnection() } } - private void StampLastMessage(bool isPing) - { - if (isPing) - { - LastAniDBPing = DateTime.Now; - } - else - { - LastAniDBMessageNonPing = DateTime.Now; - } - } - public void ForceLogout() { - if (!_isLoggedOn) - { - return; - } + StopPinging(); + if (!_isLoggedOn) return; if (IsBanned) { @@ -337,8 +331,15 @@ public async Task CloseConnections() { IsNetworkAvailable = false; IsAlive = false; - _pulseTimer?.Stop(); - _pulseTimer = null; + + _pingTimer?.Stop(); + _pingTimer?.Dispose(); + _pingTimer = null; + + _logoutTimer?.Stop(); + _logoutTimer?.Dispose(); + _logoutTimer = null; + if (_socketHandler == null) return; Logger.LogInformation("AniDB UDP Socket Disposing..."); @@ -368,7 +369,7 @@ public async Task Login() private async Task Login(string username, string password) { // check if we are already logged in - if (IsLoggedOn) return true; + if (_isLoggedOn) return true; if (!ValidAniDBCredentials(username, password)) { @@ -393,19 +394,19 @@ private async Task Login(string username, string password) case UDPReturnCode.LOGIN_FAILED: SessionID = null; IsInvalidSession = true; - IsLoggedOn = false; + _isLoggedOn = false; Logger.LogError("AniDB Login Failed: invalid credentials"); LoginFailed?.Invoke(this, null!); break; case UDPReturnCode.LOGIN_ACCEPTED: SessionID = response.Response.SessionID; _cdnDomain = response.Response.ImageServer; - IsLoggedOn = true; + _isLoggedOn = true; IsInvalidSession = false; return true; default: SessionID = null; - IsLoggedOn = false; + _isLoggedOn = false; IsInvalidSession = true; break; } diff --git a/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogin.cs b/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogin.cs index e0cbcf4a3..dd1705139 100644 --- a/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogin.cs +++ b/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogin.cs @@ -50,7 +50,7 @@ public override UDPResponse Send() { Command = BaseCommand; // LOGIN commands have special needs, so we want to handle this differently - var rawResponse = Handler.CallAniDBUDPDirectly(Command, UseUnicode).Result; + var rawResponse = Handler.SendDirectly(Command, UseUnicode).Result; var response = ParseResponse(rawResponse, true); var parsedResponse = ParseResponse(response); return parsedResponse; diff --git a/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogout.cs b/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogout.cs index 1fc8b17cd..4a68c59a0 100644 --- a/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogout.cs +++ b/Shoko.Server/Providers/AniDB/UDP/Connection/RequestLogout.cs @@ -9,6 +9,25 @@ public class RequestLogout : UDPRequest // Normally we would override Execute, but we are always logged in here, and Login() just returns if we are protected override string BaseCommand => "LOGOUT"; + public override UDPResponse Send() + { + Command = BaseCommand.Trim(); + if (string.IsNullOrEmpty(Handler.SessionID) || Handler.IsBanned || Handler.IsInvalidSession) + { + return new UDPResponse + { + Code = UDPReturnCode.LOGGED_OUT + }; + } + + PreExecute(Handler.SessionID); + var rawResponse = Handler.SendDirectly(Command, resetTimers: false).Result; + var response = ParseResponse(rawResponse); + var parsedResponse = ParseResponse(response); + PostExecute(Handler.SessionID, parsedResponse); + return parsedResponse; + } + protected override UDPResponse ParseResponse(UDPResponse response) { var code = response.Code; diff --git a/Shoko.Server/Providers/AniDB/UDP/Connection/RequestPing.cs b/Shoko.Server/Providers/AniDB/UDP/Connection/RequestPing.cs index a4787c1ce..78ca126ac 100644 --- a/Shoko.Server/Providers/AniDB/UDP/Connection/RequestPing.cs +++ b/Shoko.Server/Providers/AniDB/UDP/Connection/RequestPing.cs @@ -28,7 +28,7 @@ protected override void PreExecute(string sessionID) public override UDPResponse Send() { - var rawResponse = Handler.CallAniDBUDPDirectly(BaseCommand, true, true).Result; + var rawResponse = Handler.SendDirectly(BaseCommand, true, false).Result; var response = ParseResponse(rawResponse); var parsedResponse = ParseResponse(response); return parsedResponse; diff --git a/Shoko.Server/Providers/AniDB/UDP/Generic/UDPRequest.cs b/Shoko.Server/Providers/AniDB/UDP/Generic/UDPRequest.cs index 1e341b998..f2a9b24cd 100644 --- a/Shoko.Server/Providers/AniDB/UDP/Generic/UDPRequest.cs +++ b/Shoko.Server/Providers/AniDB/UDP/Generic/UDPRequest.cs @@ -40,7 +40,7 @@ public virtual UDPResponse Send() } PreExecute(Handler.SessionID); - var rawResponse = Handler.CallAniDBUDP(Command).Result; + var rawResponse = Handler.Send(Command).Result; var response = ParseResponse(rawResponse); var parsedResponse = ParseResponse(response); PostExecute(Handler.SessionID, parsedResponse); @@ -134,7 +134,7 @@ protected virtual UDPResponse ParseResponse(string response, bool return { var errorMessage = $"{(int)status} {status}"; Logger.LogTrace("Waiting. AniDB returned {StatusCode} {Status}", (int)status, status); - Handler.ExtendBanTimer(300, errorMessage); + Handler.StartBackoffTimer(300, errorMessage); break; } case UDPReturnCode.UNKNOWN_COMMAND: diff --git a/Shoko.Server/Providers/AniDB/UDP/UDPRateLimiter.cs b/Shoko.Server/Providers/AniDB/UDP/UDPRateLimiter.cs index 723b32a60..e4993d760 100644 --- a/Shoko.Server/Providers/AniDB/UDP/UDPRateLimiter.cs +++ b/Shoko.Server/Providers/AniDB/UDP/UDPRateLimiter.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; + namespace Shoko.Server.Providers.AniDB.UDP; public class UDPRateLimiter : AniDBRateLimiter @@ -6,4 +8,8 @@ public class UDPRateLimiter : AniDBRateLimiter protected override int LongDelay { get; init; } = 4000; protected override long shortPeriod { get; init; } = 3600000; protected override long resetPeriod { get; init; } = 1800000; + + public UDPRateLimiter(ILogger logger) : base(logger) + { + } } diff --git a/Shoko.Server/Server/Constants.cs b/Shoko.Server/Server/Constants.cs index a595e48fa..0638e331c 100644 --- a/Shoko.Server/Server/Constants.cs +++ b/Shoko.Server/Server/Constants.cs @@ -1,26 +1,13 @@ -using System; -using Shoko.Commons.Properties; - -namespace Shoko.Server.Server; +namespace Shoko.Server.Server; public static class Constants { public const string SentryDsn = "SENTRY_DSN_KEY_GOES_HERE"; - - public static readonly string WebCacheError = @"No Results"; public static readonly string AniDBTitlesURL = @"http://anidb.net/api/anime-titles.xml.gz"; - public static readonly string AnonWebCacheUsername = @"AnonymousWebCacheUser"; public const string DatabaseTypeKey = "Database"; - public static readonly int ForceLogoutPeriod = 300; - public static readonly int PingFrequency = 45; - - public static readonly TimeSpan ContractLifespan = TimeSpan.FromHours(2); - - public static readonly string NO_GROUP_INFO = "NO GROUP INFO"; - public static readonly string NO_SOURCE_INFO = "NO SOURCE INFO"; public struct GroupFilterName { @@ -45,15 +32,6 @@ public struct DatabaseType public const string MySQL = "MySQL"; } - public struct DBLogType - { - public static readonly string APIAniDBHTTP = "AniDB HTTP"; - public static readonly string APIAniDBUDP = "AniDB UDP"; - public static readonly string APIAzureHTTP = "Cache HTTP"; - } - - #region Labels - // http://wiki.anidb.net/w/WebAOM#Move.2Frename_system public struct FileRenameTag { @@ -110,66 +88,14 @@ public struct FileRenameReserved public static readonly string Unknown = "unknown"; // used for videos with no audio or no subitle languages } - public struct Labels - { - public static readonly string LASTWATCHED = "Last Watched"; - public static readonly string NEWEPISODES = "New Episodes"; - public static readonly string FAVES = "Favorites"; - public static readonly string FAVESNEW = "New in Favorites"; - public static readonly string MISSING = "Missing Episodes"; - public static readonly string MAINVIEW = "[ Main View ]"; - public static readonly string PREVIOUSFOLDER = ".."; - } - - public struct SeriesDisplayString - { - public static readonly string SeriesName = ""; - public static readonly string AniDBNameRomaji = ""; - public static readonly string AniDBNameEnglish = ""; - public static readonly string TvDBSeason = ""; - public static readonly string AnimeYear = ""; - } - - public struct GroupDisplayString - { - public static readonly string GroupName = ""; - public static readonly string AniDBNameRomaji = ""; - public static readonly string AniDBNameEnglish = ""; - public static readonly string AnimeYear = ""; - } - - public struct FileSelectionDisplayString - { - public static readonly string Group = ""; - public static readonly string GroupShort = ""; - public static readonly string FileSource = ""; - public static readonly string FileRes = ""; - public static readonly string FileCodec = ""; - public static readonly string AudioCodec = ""; - } - - public struct EpisodeDisplayString - { - public static readonly string EpisodeNumber = ""; - public static readonly string EpisodeName = ""; - } - public struct URLS { - public static readonly string MAL_Series_Prefix = @"https://myanimelist.net/anime/"; public static readonly string MAL_Series = @"https://myanimelist.net/anime/{0}"; - public static readonly string MAL_SeriesDiscussion = @"https://myanimelist.net/anime/{0}/{1}/forum"; - - public static readonly string AniDB_File = @"https://anidb.net/perl-bin/animedb.pl?show=file&fid={0}"; - public static readonly string AniDB_Episode = @"https://anidb.net/perl-bin/animedb.pl?show=ep&eid={0}"; public static readonly string AniDB_Series = @"https://anidb.net/perl-bin/animedb.pl?show=anime&aid={0}"; public static readonly string AniDB_SeriesDiscussion = @"https://anidb.net/perl-bin/animedb.pl?show=threads&do=anime&id={0}"; - public static readonly string AniDB_ReleaseGroup = - @"https://anidb.net/perl-bin/animedb.pl?show=group&gid={0}"; - public static readonly string AniDB_Images = @"https://{0}/images/main/{{0}}"; // This is the fallback if the API response does not work. @@ -181,41 +107,11 @@ public struct URLS public static readonly string TvDB_Images = @"https://artworks.thetvdb.com/banners/{0}"; public static readonly string TvDB_Episode_Images = @"https://thetvdb.com/banners/{0}"; - public static readonly string MovieDB_Series = @"https://www.themoviedb.org/movie/{0}"; public static readonly string Trakt_Series = @"https://trakt.tv/show/{0}"; public static readonly string MovieDB_Images = @"https://image.tmdb.org/t/p/original{0}"; } - public struct GroupLabelStyle - { - public static readonly string EpCount = "Total Episode Count"; - public static readonly string Unwatched = "Only Unwatched Episode Count"; - public static readonly string WatchedUnwatched = "Watched and Unwatched Episode Counts"; - } - - public struct EpisodeLabelStyle - { - public static readonly string IconsDate = "Icons and Date"; - public static readonly string IconsOnly = "Icons Only"; - } - - #endregion - - public struct WebURLStrings - { - } - - public struct EpisodeTypeStrings - { - public static readonly string Normal = Resources.EpisodeType_Episodes; - public static readonly string Credits = Resources.EpisodeType_Credits; - public static readonly string Specials = Resources.EpisodeType_Specials; - public static readonly string Trailer = Resources.EpisodeType_Trailer; - public static readonly string Parody = Resources.EpisodeType_Parody; - public static readonly string Other = Resources.EpisodeType_Other; - } - public struct TvDB { public static readonly string apiKey = "B178B8940CAF4A2C";