Files
AdamuSw/src/Network/Connection.cs
2026-07-14 19:00:35 +03:00

218 lines
8.3 KiB
C#

// <copyright file="Connection.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Network;
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.IO.Pipelines;
using System.Net;
using System.Threading;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Network.SimpleModulus;
using MUnique.OpenMU.PlugIns;
using Nito.AsyncEx;
using Nito.AsyncEx.Synchronous;
using Pipelines.Sockets.Unofficial;
/// <summary>
/// A connection which works on <see cref="IDuplexPipe"/>.
/// </summary>
/// <seealso cref="MUnique.OpenMU.Network.PacketPipeReaderBase" />
public sealed class Connection : PacketPipeReaderBase, IConnection
{
private static readonly ActivitySource ActivitySource = new(typeof(Connection).FullName ?? nameof(Connection));
private static readonly Meter ConnectionMeter = new(MeterName);
private static readonly Counter<long> IncomingBytesCounter = ConnectionMeter.CreateCounter<long>("IncomingBytes", "bytes");
private static readonly Counter<long> OutgoingBytesCounter = ConnectionMeter.CreateCounter<long>("OutgoingBytes", "bytes");
private static readonly Counter<long> InvalidBlocksCounter = ConnectionMeter.CreateCounter<long>("InvalidBlocks");
private static readonly Counter<long> ConnectionCounter = ConnectionMeter.CreateCounter<long>("ConnectionCount");
/// <summary>
/// The start of an RDP connection attempt.
/// </summary>
/// <remarks>
/// Some hackers try to break into servers.
/// The packet mostly contains the content "Cookie: mstshash=Administr".
/// </remarks>
private static readonly byte[] RdpConnectionAttemptHeader = { 0x03, 0x00, 0x00 };
private readonly IPipelinedEncryptor? _encryptionPipe;
private readonly ILogger<Connection> _logger;
private readonly EndPoint _remoteEndPoint;
private IDuplexPipe? _duplexPipe;
private bool _disconnected;
private PipeWriter? _outputWriter;
/// <summary>
/// Initializes a new instance of the <see cref="Connection" /> class.
/// </summary>
/// <param name="duplexPipe">The duplex pipe of the (socket) connection.</param>
/// <param name="decryptionPipe">The decryption pipe.</param>
/// <param name="encryptionPipe">The encryption pipe.</param>
/// <param name="logger">The logger.</param>
public Connection(IDuplexPipe duplexPipe, IPipelinedDecryptor? decryptionPipe, IPipelinedEncryptor? encryptionPipe, ILogger<Connection> logger)
{
this._duplexPipe = duplexPipe;
this._encryptionPipe = encryptionPipe;
this._logger = logger;
this.Source = decryptionPipe?.Reader ?? this._duplexPipe!.Input;
this._remoteEndPoint = this.SocketConnection?.Socket.RemoteEndPoint ?? new IPEndPoint(IPAddress.Any, 0);
this.LocalEndPoint = this.SocketConnection?.Socket.LocalEndPoint;
this.OutputLock = new();
}
/// <inheritdoc />
public event AsyncEventHandler<ReadOnlySequence<byte>>? PacketReceived;
/// <inheritdoc />
public event AsyncEventHandler? Disconnected;
/// <inheritdoc />
public bool Connected => this.SocketConnection != null ? this.SocketConnection.ShutdownKind == PipeShutdownKind.None && !this._disconnected : !this._disconnected;
/// <inheritdoc />
public EndPoint? EndPoint => this._remoteEndPoint;
/// <inheritdoc />
public EndPoint? LocalEndPoint { get; }
/// <inheritdoc />
public PipeWriter Output => this._outputWriter ??= new ExtendedPipeWriter(this._encryptionPipe?.Writer ?? this._duplexPipe!.Output, OutgoingBytesCounter);
/// <inheritdoc />
public AsyncLock OutputLock { get; }
/// <summary>
/// Gets the name of the meter.
/// </summary>
internal static string MeterName => typeof(Connection).FullName ?? nameof(Connection);
/// <summary>
/// Gets the socket connection, if the <see cref="_duplexPipe"/> is an instance of <see cref="SocketConnection"/>. Otherwise, it returns null.
/// </summary>
private SocketConnection? SocketConnection => this._duplexPipe as SocketConnection;
/// <inheritdoc/>
public override string ToString() => this._remoteEndPoint?.ToString() ?? $"{base.ToString()} {this.GetHashCode()}";
/// <inheritdoc/>
public async Task BeginReceiveAsync()
{
try
{
ConnectionCounter.Add(1);
await this.ReadSourceAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// not an error which we need to handle.
}
catch (Exception ex)
{
await this.OnCompleteAsync(ex).ConfigureAwait(false);
return;
}
await this.OnCompleteAsync(null).ConfigureAwait(false);
}
/// <inheritdoc />
public async ValueTask DisconnectAsync()
{
using var scope = this._logger.BeginScope(this._remoteEndPoint);
if (this._disconnected)
{
this._logger.LogDebug("Connection already disconnected.");
return;
}
ConnectionCounter.Add(-1);
this._logger.LogDebug("Disconnecting...");
if (this._duplexPipe is not null)
{
await this.Source.CompleteAsync().ConfigureAwait(false);
await this.Output.CompleteAsync().ConfigureAwait(false);
(this._duplexPipe as IDisposable)?.Dispose();
this._duplexPipe = null;
}
this._logger.LogDebug("Disconnected");
this._disconnected = true;
await this.Disconnected.SafeInvokeAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public void Dispose()
{
this.DisconnectAsync().AsTask().WaitAndUnwrapException();
this.PacketReceived = null;
this.Disconnected = null;
}
/// <inheritdoc />
protected override async ValueTask OnCompleteAsync(Exception? exception)
{
using var scope = this._logger.BeginScope(this._remoteEndPoint);
if (exception is InvalidBlockChecksumException)
{
InvalidBlocksCounter.Add(1, new KeyValuePair<string, object?>("RemoteEndPoint", this._remoteEndPoint));
}
if (exception != null)
{
if (exception is ConnectionResetException)
{
this._logger.LogInformation(exception, "Connection was closed.");
}
else if (exception is ConnectionAbortedException)
{
this._logger.LogInformation(exception, "Connection was aborted by the server.");
}
else if (exception is InvalidOperationException && exception.Message == "Reading is not allowed after reader was completed.")
{
this._logger.LogInformation(exception, "Reader was completed.");
}
else if (exception is InvalidPacketHeaderException packetHeaderException
&& packetHeaderException.Header.Take(3).SequenceEqual(RdpConnectionAttemptHeader))
{
this._logger.LogWarning("Connection will be closed, RDP connection attempt by {endPoint}", this.EndPoint);
}
else
{
this._logger.LogError(exception, "Connection will be disconnected, because of an exception");
}
}
await this.Output.CompleteAsync(exception).ConfigureAwait(false);
await this.DisconnectAsync().ConfigureAwait(false);
}
/// <summary>
/// Reads the mu online packet by raising <see cref="PacketReceived" />.
/// </summary>
/// <param name="packet">The mu online packet.</param>
/// <returns><see langword="true" />, if the flush was successful or not required.<see langword="false" />, if the pipe reader is completed and no longer reading data.</returns>
protected override async ValueTask<bool> ReadPacketAsync(ReadOnlySequence<byte> packet)
{
IncomingBytesCounter.Add(packet.Length);
using var activity = ActivitySource.CreateActivity("Read Packet", ActivityKind.Server);
activity?.SetTag("remoteEndPoint", this._remoteEndPoint)
.SetTag("rawPacket", packet)
.Start();
try
{
await this.PacketReceived.SafeInvokeAsync(packet).ConfigureAwait(false);
return true;
}
finally
{
activity?.Stop();
}
}
}