mirror of
https://github.com/Stone-Red-Code/EchoHub.git
synced 2026-09-04 00:56:05 +02:00
- 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.
177 lines
5.6 KiB
C#
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();
|
|
}
|
|
}
|