//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
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;
///
/// A connection which works on .
///
///
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 IncomingBytesCounter = ConnectionMeter.CreateCounter("IncomingBytes", "bytes");
private static readonly Counter OutgoingBytesCounter = ConnectionMeter.CreateCounter("OutgoingBytes", "bytes");
private static readonly Counter InvalidBlocksCounter = ConnectionMeter.CreateCounter("InvalidBlocks");
private static readonly Counter ConnectionCounter = ConnectionMeter.CreateCounter("ConnectionCount");
///
/// The start of an RDP connection attempt.
///
///
/// Some hackers try to break into servers.
/// The packet mostly contains the content "Cookie: mstshash=Administr".
///
private static readonly byte[] RdpConnectionAttemptHeader = { 0x03, 0x00, 0x00 };
private readonly IPipelinedEncryptor? _encryptionPipe;
private readonly ILogger _logger;
private readonly EndPoint _remoteEndPoint;
private IDuplexPipe? _duplexPipe;
private bool _disconnected;
private PipeWriter? _outputWriter;
///
/// Initializes a new instance of the class.
///
/// The duplex pipe of the (socket) connection.
/// The decryption pipe.
/// The encryption pipe.
/// The logger.
public Connection(IDuplexPipe duplexPipe, IPipelinedDecryptor? decryptionPipe, IPipelinedEncryptor? encryptionPipe, ILogger 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();
}
///
public event AsyncEventHandler>? PacketReceived;
///
public event AsyncEventHandler? Disconnected;
///
public bool Connected => this.SocketConnection != null ? this.SocketConnection.ShutdownKind == PipeShutdownKind.None && !this._disconnected : !this._disconnected;
///
public EndPoint? EndPoint => this._remoteEndPoint;
///
public EndPoint? LocalEndPoint { get; }
///
public PipeWriter Output => this._outputWriter ??= new ExtendedPipeWriter(this._encryptionPipe?.Writer ?? this._duplexPipe!.Output, OutgoingBytesCounter);
///
public AsyncLock OutputLock { get; }
///
/// Gets the name of the meter.
///
internal static string MeterName => typeof(Connection).FullName ?? nameof(Connection);
///
/// Gets the socket connection, if the is an instance of . Otherwise, it returns null.
///
private SocketConnection? SocketConnection => this._duplexPipe as SocketConnection;
///
public override string ToString() => this._remoteEndPoint?.ToString() ?? $"{base.ToString()} {this.GetHashCode()}";
///
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);
}
///
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);
}
///
public void Dispose()
{
this.DisconnectAsync().AsTask().WaitAndUnwrapException();
this.PacketReceived = null;
this.Disconnected = null;
}
///
protected override async ValueTask OnCompleteAsync(Exception? exception)
{
using var scope = this._logger.BeginScope(this._remoteEndPoint);
if (exception is InvalidBlockChecksumException)
{
InvalidBlocksCounter.Add(1, new KeyValuePair("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);
}
///
/// Reads the mu online packet by raising .
///
/// The mu online packet.
/// , if the flush was successful or not required., if the pipe reader is completed and no longer reading data.
protected override async ValueTask ReadPacketAsync(ReadOnlySequence 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();
}
}
}