Files
EchoHub/src/EchoHub.Server.Irc/IrcGatewayService.cs
T
HueByte c0d38015ff Add comprehensive unit tests for IRC command handling and message formatting
- Implemented IrcCommandHandlerTests to cover various IRC commands including PING, JOIN, PART, and authentication scenarios.
- Added IrcMessageFormatterTests to validate message formatting for different message types such as text, images, files, and audio.
- Created IrcMessageTests to ensure correct parsing of IRC messages and handling of various command formats.
- Introduced TestHelpers to facilitate testing with mock connections and streams, including a FakeChatService and FakeEncryptionService for simulating chat behavior.
2026-02-21 11:25:34 +01:00

177 lines
5.6 KiB
C#

using System.Collections.Concurrent;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using EchoHub.Core.Contracts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace EchoHub.Server.Irc;
public sealed class IrcGatewayService : BackgroundService
{
private readonly IrcOptions _options;
private readonly IServiceProvider _services;
private readonly ILogger<IrcGatewayService> _logger;
private readonly ConcurrentDictionary<string, IrcClientConnection> _connections = new();
public IrcOptions Options => _options;
public IReadOnlyDictionary<string, IrcClientConnection> Connections => _connections;
public IrcGatewayService(
IOptions<IrcOptions> options,
IServiceProvider services,
ILogger<IrcGatewayService> logger)
{
_options = options.Value;
_services = services;
_logger = logger;
}
public IEnumerable<IrcClientConnection> GetConnectionsInChannel(string channelName)
{
return _connections.Values
.Where(c => c.IsAuthenticated && c.IsInChannel(channelName));
}
public IEnumerable<IrcClientConnection> GetAllConnections()
{
return _connections.Values.Where(c => c.IsAuthenticated);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
if (!_options.Enabled)
{
_logger.LogInformation("IRC gateway is disabled");
return;
}
var listeners = new List<Task>();
listeners.Add(RunListenerAsync(_options.Port, useTls: false, stoppingToken));
if (_options.TlsEnabled && !string.IsNullOrWhiteSpace(_options.TlsCertPath))
{
listeners.Add(RunListenerAsync(_options.TlsPort, useTls: true, stoppingToken));
}
await Task.WhenAll(listeners);
}
private async Task RunListenerAsync(int port, bool useTls, CancellationToken ct)
{
var listener = new TcpListener(IPAddress.Any, port);
listener.Start();
_logger.LogInformation("IRC gateway listening on port {Port} ({Mode})",
port, useTls ? "TLS" : "plain");
ct.Register(() => listener.Stop());
try
{
while (!ct.IsCancellationRequested)
{
var tcpClient = await listener.AcceptTcpClientAsync(ct);
_ = HandleClientAsync(tcpClient, useTls, ct);
}
}
catch (OperationCanceledException) { }
catch (ObjectDisposedException) { }
finally
{
listener.Stop();
}
}
private async Task HandleClientAsync(TcpClient tcpClient, bool useTls, CancellationToken ct)
{
Stream stream = tcpClient.GetStream();
if (useTls)
{
try
{
var cert = X509CertificateLoader.LoadPkcs12FromFile(_options.TlsCertPath!, _options.TlsCertPassword);
var sslStream = new SslStream(stream, leaveInnerStreamOpen: false);
await sslStream.AuthenticateAsServerAsync(cert);
stream = sslStream;
}
catch (Exception ex)
{
_logger.LogError(ex, "TLS handshake failed");
tcpClient.Close();
return;
}
}
var connection = new IrcClientConnection(tcpClient, stream);
_connections[connection.ConnectionId] = connection;
_logger.LogInformation("IRC client connected: {Id}", connection.ConnectionId);
IChatService? chatService = null;
try
{
chatService = _services.GetRequiredService<IChatService>();
var encryption = _services.GetRequiredService<IMessageEncryptionService>();
var handler = new IrcCommandHandler(
connection, _options, chatService, encryption, _logger);
await handler.RunAsync(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "IRC client {Id} error", connection.ConnectionId);
}
finally
{
if (connection.IsAuthenticated)
{
foreach (var ch in connection.GetJoinedChannels())
{
if (chatService is null) break;
await chatService.LeaveChannelAsync(
connection.ConnectionId, connection.Nickname!, ch);
}
if (chatService is not null)
await chatService.UserDisconnectedAsync(connection.ConnectionId);
}
_connections.TryRemove(connection.ConnectionId, out _);
await connection.DisposeAsync();
_logger.LogInformation("IRC client {Id} ({Nick}) disconnected",
connection.ConnectionId, connection.Nickname ?? "unregistered");
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
// Cancel ExecuteAsync first so listeners stop accepting
await base.StopAsync(cancellationToken);
// Force-close any remaining client connections
foreach (var (_, conn) in _connections)
{
try
{
await conn.SendAsync("ERROR :Server shutting down")
.WaitAsync(TimeSpan.FromSeconds(2));
}
catch { }
finally
{
try { await conn.DisposeAsync(); }
catch { }
}
}
_connections.Clear();
}
}