diff --git a/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs index b4cb64a8118..18e8a14df6e 100644 --- a/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs +++ b/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs @@ -88,6 +88,7 @@ public IrcConnectionStringBuilder(string connectionString) case IrcPasswordType.NickServ: case IrcPasswordType.Sasl: case IrcPasswordType.Server: + case IrcPasswordType.Oper: PasswordType = passwordType; break; default: diff --git a/src/Tgstation.Server.Api/Models/IrcPasswordType.cs b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs index add54b1565f..9a0fca38c46 100644 --- a/src/Tgstation.Server.Api/Models/IrcPasswordType.cs +++ b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs @@ -19,5 +19,10 @@ public enum IrcPasswordType /// Use NickServ authentication. /// NickServ, + + /// + /// Use OPER authentication. + /// + Oper, } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index ad83a80b18b..3a532f6b425 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -42,11 +42,6 @@ sealed class IrcProvider : Provider /// public override string BotMention => client.Nickname; - /// - /// The client. - /// - readonly IrcFeatures client; - /// /// Address of the server to connect to. /// @@ -57,6 +52,11 @@ sealed class IrcProvider : Provider /// readonly ushort port; + /// + /// Wether or not this IRC client is to use ssl. + /// + readonly bool ssl; + /// /// IRC nickname. /// @@ -82,6 +82,16 @@ sealed class IrcProvider : Provider /// readonly Dictionary queryChannelIdMap; + /// + /// The version string obtained from . + /// + readonly string versionString; + + /// + /// The client. + /// + IrcFeatures client; + /// /// The used for . /// @@ -92,11 +102,6 @@ sealed class IrcProvider : Provider /// ulong channelIdCounter; - /// - /// If we are disconnecting. - /// - bool disconnecting; - /// /// Initializes a new instance of the class. /// @@ -121,33 +126,15 @@ public IrcProvider( address = ircBuilder.Address!; port = ircBuilder.Port!.Value; + ssl = ircBuilder.UseSsl!.Value; nickname = ircBuilder.Nickname!; password = ircBuilder.Password!; passwordType = ircBuilder.PasswordType; - client = new IrcFeatures - { - SupportNonRfc = true, - CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...", - AutoRejoin = true, - AutoRejoinOnKick = true, - AutoRelogin = true, - AutoRetry = false, - AutoReconnect = false, - ActiveChannelSyncing = true, - AutoNickHandling = true, - CtcpVersion = assemblyInformationProvider.VersionString, - UseSsl = ircBuilder.UseSsl!.Value, - }; - if (ircBuilder.UseSsl.Value) - client.ValidateServerCertificate = true; // dunno if it defaults to that or what - - client.OnChannelMessage += Client_OnChannelMessage; - client.OnQueryMessage += Client_OnQueryMessage; + versionString = assemblyInformationProvider.VersionString; - /*client.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line); - client.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);*/ + client = InstantiateClient(); channelIdMap = new Dictionary(); queryChannelIdMap = new Dictionary(); @@ -369,12 +356,15 @@ await SendMessage( /// protected override async ValueTask Connect(CancellationToken cancellationToken) { - disconnecting = false; cancellationToken.ThrowIfCancellationRequested(); try { await Task.Factory.StartNew( - () => client.Connect(address, port), + () => + { + client = InstantiateClient(); + client.Connect(address, port); + }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current) @@ -382,70 +372,50 @@ await Task.Factory.StartNew( cancellationToken.ThrowIfCancellationRequested(); + listenTask = Task.Factory.StartNew( + () => + { + Logger.LogTrace("Starting blocking listen..."); + try + { + client.Listen(); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "IRC Main Listen Exception!"); + } + + Logger.LogTrace("Exiting listening task..."); + }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); + Logger.LogTrace("Authenticating ({passwordType})...", passwordType); switch (passwordType) { case IrcPasswordType.Server: - client.Login(nickname, nickname, 0, nickname, password); + client.RfcPass(password); + await Login(client, nickname); break; case IrcPasswordType.NickServ: - client.Login(nickname, nickname, 0, nickname); + await Login(client, nickname); cancellationToken.ThrowIfCancellationRequested(); client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password)); break; case IrcPasswordType.Sasl: await SaslAuthenticate(cancellationToken); break; + case IrcPasswordType.Oper: + await Login(client, nickname); + cancellationToken.ThrowIfCancellationRequested(); + client.RfcOper(nickname, password, Priority.Critical); + break; case null: - client.Login(nickname, nickname, 0, nickname); + await Login(client, nickname); break; default: throw new InvalidOperationException($"Invalid IrcPasswordType: {passwordType.Value}"); } cancellationToken.ThrowIfCancellationRequested(); - Logger.LogTrace("Processing initial messages..."); - await NonBlockingListen(cancellationToken); - - var nickCheckCompleteTcs = new TaskCompletionSource(); - using (cancellationToken.Register(() => nickCheckCompleteTcs.TrySetCanceled(cancellationToken))) - { - listenTask = Task.Factory.StartNew( - async () => - { - Logger.LogTrace("Entering nick check loop"); - while (!disconnecting && client.IsConnected && client.Nickname != nickname) - { - client.ListenOnce(true); - if (disconnecting || !client.IsConnected) - break; - await NonBlockingListen(cancellationToken); - - // ensure we have the correct nick - if (client.GetIrcUser(nickname) == null) - client.RfcNick(nickname); - } - - nickCheckCompleteTcs.TrySetResult(); - - Logger.LogTrace("Starting blocking listen..."); - try - { - client.Listen(); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "IRC Main Listen Exception!"); - } - - Logger.LogTrace("Exiting listening task..."); - }, - cancellationToken, - DefaultIOManager.BlockingTaskCreationOptions, - TaskScheduler.Current); - - await nickCheckCompleteTcs.Task; - } Logger.LogTrace("Connection established!"); } @@ -487,6 +457,37 @@ await Task.Factory.StartNew( } } + /// + /// Register the client on the network. + /// + /// IRC client. + /// Nickname. + /// that resolves when registration has been completed. + /// If the IRC server fails to respond. + private async Task Login(IrcFeatures client, string nickname) + { + var promise = new TaskCompletionSource(); + + void Callback(object? sender, EventArgs e) + { + Logger.LogTrace("IRC Registered."); + promise.TrySetResult(e); + } + + client.OnRegistered += Callback; + + client.Login(nickname, nickname, 0, nickname); + + var completed = await Task.WhenAny(promise.Task, Task.Delay(30 * 1000)); + if (completed == promise.Task) + { + client.OnRegistered -= Callback; + return; + } + + throw new TimeoutException("Timed out waiting for IRC registration."); + } + /// /// Handle an IRC message. /// @@ -667,8 +668,6 @@ async ValueTask HardDisconnect(CancellationToken cancellationToken) Logger.LogTrace("Hard disconnect"); - disconnecting = true; - // This call blocks permanently randomly sometimes // Frankly I don't give a shit var disconnectTask = Task.Factory.StartNew( @@ -693,5 +692,44 @@ await Task.WhenAny( listenTask ?? Task.CompletedTask), AsyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken)); } + + /// + /// Creates a new instance of the IRC client. + /// Reusing the same client after a disconnection seems to cause issues. + /// + /// The client to use. + private IrcFeatures InstantiateClient() + { + IrcFeatures newClient = new IrcFeatures + { + SupportNonRfc = true, + CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...", + AutoRejoin = true, + AutoRejoinOnKick = true, + AutoRelogin = false, + AutoRetry = false, + AutoReconnect = false, + ActiveChannelSyncing = true, + AutoNickHandling = true, + CtcpVersion = versionString, + UseSsl = ssl, + EnableUTF8Recode = true, + }; + if (ssl) + newClient.ValidateServerCertificate = true; // dunno if it defaults to that or what + + newClient.OnChannelMessage += Client_OnChannelMessage; + newClient.OnQueryMessage += Client_OnQueryMessage; + + newClient.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line); + newClient.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line); + newClient.OnError += (sender, e) => + { + Logger.LogError("IRC ERROR: {error}", e.ErrorMessage); + newClient.Disconnect(); + }; + + return newClient; + } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs index 3f1e0502191..6a343b40a04 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs @@ -88,6 +88,17 @@ public async Task TestConnectAndDisconnect() await InvokeConnect(provider); Assert.IsTrue(provider.Connected); + await Task.Delay(2000); // IRC servers do not like it when you connect and disconnect in rapid succession + + await provider.Disconnect(default); + Assert.IsFalse(provider.Connected); + + await Task.Delay(2000); // same as above + + await InvokeConnect(provider); + await Task.Delay(2000); // make sure it stays connected after a reconnect attempt + Assert.IsTrue(provider.Connected); + await provider.Disconnect(default); Assert.IsFalse(provider.Connected); }