// // 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"); } } }