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 _logger; private readonly ConcurrentDictionary _connections = new(); public IrcOptions Options => _options; public IReadOnlyDictionary Connections => _connections; public IrcGatewayService( IOptions options, IServiceProvider services, ILogger logger) { _options = options.Value; _services = services; _logger = logger; } public IEnumerable GetConnectionsInChannel(string channelName) { return _connections.Values .Where(c => c.IsAuthenticated && c.IsInChannel(channelName)); } public IEnumerable 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(); 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(); var channelService = _services.GetRequiredService(); var encryption = _services.GetRequiredService(); var handler = new IrcCommandHandler( connection, _options, chatService, channelService, 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(); } }