// // 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.Net; using System.Net.Sockets; using System.Text; using NosSmooth.Core.Client; using NosSmooth.Core.Commands; using NosSmooth.Core.Packets; using NosSmooth.Cryptography; using NosSmooth.PacketSerializer.Abstractions.Attributes; using PacketDotNet; using Remora.Results; using SharpPcap; 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 CryptographyManager _crypto; private int _localPort; private long _localAddr; private CancellationToken? _stoppingToken; private bool _running; /// /// 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. public PcapNostaleClient ( Process process, int encryptionKey, Encoding encoding, PcapNostaleManager pcapManager, ProcessTcpManager processTcpManager, IPacketHandler handler, CommandProcessor commandProcessor ) : base(commandProcessor) { _process = process; _encoding = encoding; _pcapManager = pcapManager; _processTcpManager = processTcpManager; _handler = handler; _crypto = new CryptographyManager(); _crypto.EncryptionKey = encryptionKey; } /// 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 connection = (await _processTcpManager.GetConnectionsAsync(_process.Id)).Cast() .FirstOrDefault(); if (lastConnection != connection) { if (lastConnection is not null) { _pcapManager.UnregisterConnection(lastConnection.Value); _crypto.EncryptionKey = 0; } if (reverseLastConnection is not null) { _pcapManager.UnregisterConnection(reverseLastConnection.Value); } if (connection is not null) { var conn = connection.Value; var reverseConn = new TcpConnection (conn.RemoteAddr, conn.RemotePort, conn.LocalAddr, conn.LocalPort); _localAddr = conn.LocalAddr; _localPort = conn.LocalPort; _pcapManager.RegisterConnection(conn, this); _pcapManager.RegisterConnection(reverseConn, this); lastConnection = conn; reverseLastConnection = reverseConn; } else { lastConnection = null; reverseLastConnection = null; } } await Task.Delay(TimeSpan.FromMilliseconds(10), 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); } if (reverseLastConnection is not null) { _pcapManager.UnregisterConnection(reverseLastConnection.Value); } _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 connection that obtained the packet. /// The raw payload data of the packet. internal void OnPacketArrival(TcpConnection connection, byte[] payloadData) { string data; PacketSource source; bool containsPacketId = false; if (connection.LocalAddr == _localAddr && connection.LocalPort == _localPort) { // sent packet source = PacketSource.Client; if (_crypto.EncryptionKey == 0) { var worldDecrypted = _crypto.ServerWorld.Decrypt(payloadData, _encoding).Trim(); var splitted = worldDecrypted.Split(' '); if (splitted.Length == 2 && int.TryParse(splitted[1], out var encryptionKey)) { // possibly first packet from world _crypto.EncryptionKey = encryptionKey; data = worldDecrypted; containsPacketId = true; } else { // doesn't look like first packet from world, so assume login. data = _crypto.ServerLogin.Decrypt(payloadData, _encoding); } } else { data = _crypto.ServerWorld.Decrypt(payloadData, _encoding); containsPacketId = true; } } else { // received packet source = PacketSource.Server; if (_crypto.EncryptionKey == 0) { // probably login data = _crypto.ClientLogin.Decrypt(payloadData, _encoding); } else { data = _crypto.ClientWorld.Decrypt(payloadData, _encoding); } } if (data.Length > 0) { foreach (var line in data.Split('\n')) { var linePacket = line; if (containsPacketId) { linePacket = line.Substring(line.IndexOf(' ') + 1); } _handler.HandlePacketAsync(this, source, linePacket.Trim(), _stoppingToken ?? default); } } } }