//
// PcapNostaleClient.cs
//
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Diagnostics;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.Cryptography;
using NosSmooth.Cryptography.Extensions;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using Remora.Results;
using SharpPcap.LibPcap;
namespace NosSmooth.Pcap;
///
/// A NosTale client that works by capturing packets.
///
///
/// Sending packets means the same number of packet will appear twice.
/// That may be detected by the server and the server may suspect
/// something malicious is going on.
///
public class PcapNostaleClient : BaseNostaleClient
{
private readonly Process _process;
private readonly Encoding _encoding;
private readonly PcapNostaleManager _pcapManager;
private readonly ProcessTcpManager _processTcpManager;
private readonly IPacketHandler _handler;
private readonly ILogger _logger;
private readonly PcapNostaleOptions _options;
private CryptographyManager _crypto;
private CancellationToken? _stoppingToken;
private bool _running;
private LibPcapLiveDevice? _lastDevice;
private TcpConnection _connection;
private long _lastPacketIndex;
///
/// Initializes a new instance of the class.
///
/// The process to look for.
/// The current encryption key of the world connection, if known. Zero if unknown.
/// The encoding.
/// The pcap manager.
/// The process manager.
/// The packet handler.
/// The command processor.
/// The options.
/// The logger.
public PcapNostaleClient
(
Process process,
int initialEncryptionKey,
Encoding encoding,
PcapNostaleManager pcapManager,
ProcessTcpManager processTcpManager,
IPacketHandler handler,
CommandProcessor commandProcessor,
IOptions options,
ILogger logger
)
: base(commandProcessor)
{
_process = process;
_encoding = encoding;
_pcapManager = pcapManager;
_processTcpManager = processTcpManager;
_handler = handler;
_logger = logger;
_options = options.Value;
_crypto = new CryptographyManager();
_crypto.EncryptionKey = initialEncryptionKey;
}
///
public override async Task RunAsync(CancellationToken stopRequested = default)
{
if (_running)
{
return Result.FromSuccess();
}
_running = true;
_stoppingToken = stopRequested;
TcpConnection? lastConnection = null;
TcpConnection? reverseLastConnection = null;
try
{
await _processTcpManager.RegisterProcess(_process.Id);
_pcapManager.AddClient();
while (!stopRequested.IsCancellationRequested)
{
if (_process.HasExited)
{
break;
}
var connections = await _processTcpManager.GetConnectionsAsync(_process.Id);
TcpConnection? connection = connections.Count > 0 ? connections[0] : null;
if (lastConnection != connection)
{
if (lastConnection is not null)
{
_pcapManager.UnregisterConnection(lastConnection.Value, this);
_crypto.EncryptionKey = 0;
}
if (reverseLastConnection is not null)
{
_pcapManager.UnregisterConnection(reverseLastConnection.Value, this);
}
if (connection is not null)
{
var conn = connection.Value;
var reverseConn = new TcpConnection
(conn.RemoteAddr, conn.RemotePort, conn.LocalAddr, conn.LocalPort);
_connection = conn;
_pcapManager.RegisterConnection(conn, this);
_pcapManager.RegisterConnection(reverseConn, this);
lastConnection = conn;
reverseLastConnection = reverseConn;
}
else
{
lastConnection = null;
reverseLastConnection = null;
}
}
await Task.Delay(TimeSpan.FromMilliseconds(_options.ProcessRefreshInterval), stopRequested);
}
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception e)
{
return e;
}
finally
{
await _processTcpManager.UnregisterProcess(_process.Id);
_pcapManager.RemoveClient();
if (lastConnection is not null)
{
_pcapManager.UnregisterConnection(lastConnection.Value, this);
}
if (reverseLastConnection is not null)
{
_pcapManager.UnregisterConnection(reverseLastConnection.Value, this);
}
_running = false;
}
return Result.FromSuccess();
}
///
public override Task SendPacketAsync(string packetString, CancellationToken ct = default)
{
throw new NotImplementedException();
}
///
public override Task ReceivePacketAsync(string packetString, CancellationToken ct = default)
{
throw new NotImplementedException();
}
///
/// Called when an associated packet has been obtained.
///
/// The device the packet was received at.
/// The connection that obtained the packet.
/// The raw payload data of the packet.
internal void OnPacketArrival(LibPcapLiveDevice? device, TcpConnection connection, byte[] payloadData)
{
_lastDevice = device;
string data;
PacketSource source;
bool mayContainPacketId = false;
if (connection.LocalAddr == _connection.LocalAddr && connection.LocalPort == _connection.LocalPort)
{ // sent packet
source = PacketSource.Client;
mayContainPacketId = true;
data = _crypto.DecryptUnknownServerPacket(payloadData, _encoding);
}
else
{ // received packet
source = PacketSource.Server;
data = _crypto.DecryptUnknownClientPacket(payloadData, _encoding);
}
if (data.Length > 0)
{
foreach (ReadOnlySpan line in data.SplitLines())
{
var linePacket = line;
if (mayContainPacketId)
{
var spaceIndex = linePacket.IndexOf(' ');
if (spaceIndex != -1)
{
var beginning = linePacket.Slice(0, spaceIndex);
if (int.TryParse(beginning, out var packetIndex))
{
_lastPacketIndex = packetIndex;
linePacket = linePacket.Slice(spaceIndex + 1);
}
}
}
var lineString = linePacket.ToString();
Task.Run(() => ProcessPacketAsync(source, lineString));
}
}
}
private async Task ProcessPacketAsync(PacketSource type, string packetString)
{
try
{
var result = await _handler.HandlePacketAsync(this, type, packetString);
if (!result.IsSuccess)
{
_logger.LogError("There was an error whilst handling packet {packetString}", packetString);
_logger.LogResultError(result);
}
}
catch (Exception e)
{
_logger.LogError(e, "The process packet threw an exception");
}
}
}