From 587a3ea90045ace1dbe5c9bd484fd71570239f1e Mon Sep 17 00:00:00 2001 From: Rutherther Date: Mon, 13 Feb 2023 17:44:28 +0100 Subject: [PATCH] feat(pcap): use new unknown decryption methods insice PcapNostaleClient Get rid of unnecessary allocations as well --- .../Extensions/StringExtensions.cs | 139 ++++++++++++++++++ Pcap/NosSmooth.Pcap/PcapNostaleClient.cs | 111 ++++++-------- 2 files changed, 187 insertions(+), 63 deletions(-) create mode 100644 Core/NosSmooth.Cryptography/Extensions/StringExtensions.cs diff --git a/Core/NosSmooth.Cryptography/Extensions/StringExtensions.cs b/Core/NosSmooth.Cryptography/Extensions/StringExtensions.cs new file mode 100644 index 0000000..fae6431 --- /dev/null +++ b/Core/NosSmooth.Cryptography/Extensions/StringExtensions.cs @@ -0,0 +1,139 @@ +// +// StringExtensions.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. + +namespace NosSmooth.Cryptography.Extensions; + +/// +/// Extension methods for string. +/// +public static class StringExtensions +{ + /// + /// Split a string into lines without allocations. + /// + /// The string to split. + /// An enumerator with lines. + public static LineSplitEnumerator SplitLines(this string str) + { + // LineSplitEnumerator is a struct so there is no allocation here + return new LineSplitEnumerator(str.AsSpan()); + } + + /// + /// An enumerator of a string lines. + /// + public ref struct LineSplitEnumerator + { + private ReadOnlySpan _str; + + /// + /// Initializes a new instance of the struct. + /// + /// The string. + public LineSplitEnumerator(ReadOnlySpan str) + { + _str = str; + Current = default; + } + + /// + /// Gets this enumerator. + /// + /// This. + public LineSplitEnumerator GetEnumerator() + => this; + + /// + /// Move to next line. + /// + /// Whether move was successful. + public bool MoveNext() + { + var span = _str; + if (span.Length == 0) + { + return false; + } + + var index = span.IndexOfAny('\r', '\n'); + if (index == -1) + { + _str = ReadOnlySpan.Empty; + Current = new LineSplitEntry(span, ReadOnlySpan.Empty); + return true; + } + + if (index < span.Length - 1 && span[index] == '\r') + { + var next = span[index + 1]; + if (next == '\n') + { + Current = new LineSplitEntry(span.Slice(0, index), span.Slice(index, 2)); + _str = span.Slice(index + 2); + return true; + } + } + + Current = new LineSplitEntry(span.Slice(0, index), span.Slice(index, 1)); + _str = span.Slice(index + 1); + return true; + } + + /// + /// Current line. + /// + public LineSplitEntry Current { get; private set; } + } + + /// + /// A line. + /// + public readonly ref struct LineSplitEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The line. + /// The line separator. + public LineSplitEntry(ReadOnlySpan line, ReadOnlySpan separator) + { + Line = line; + Separator = separator; + } + + /// + /// Gets the line. + /// + public ReadOnlySpan Line { get; } + + /// + /// Gets the separator of the line. + /// + public ReadOnlySpan Separator { get; } + + /// + /// This method allow to deconstruct the type, so you can write any of the following code + /// foreach (var entry in str.SplitLines()) { _ = entry.Line; } + /// foreach (var (line, endOfLine) in str.SplitLines()) { _ = line; } + /// https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/deconstruct?WT.mc_id=DT-MVP-5003978#deconstructing-user-defined-types. + /// + /// The line. + /// The line separator. + public void Deconstruct(out ReadOnlySpan line, out ReadOnlySpan separator) + { + line = Line; + separator = Separator; + } + + /// + /// An implicit cast to ReadOnySpan. + /// + /// The entry to cast. + /// The read only span of the entry. + public static implicit operator ReadOnlySpan(LineSplitEntry entry) + => entry.Line; + } +} \ No newline at end of file diff --git a/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs b/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs index e9c428f..ab53ddb 100644 --- a/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs +++ b/Pcap/NosSmooth.Pcap/PcapNostaleClient.cs @@ -5,18 +5,17 @@ // 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 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 PacketDotNet; using Remora.Results; -using SharpPcap; using SharpPcap.LibPcap; namespace NosSmooth.Pcap; @@ -36,6 +35,7 @@ public class PcapNostaleClient : BaseNostaleClient 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; @@ -48,23 +48,25 @@ public class PcapNostaleClient : BaseNostaleClient /// 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 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 encryptionKey, + int initialEncryptionKey, Encoding encoding, PcapNostaleManager pcapManager, ProcessTcpManager processTcpManager, IPacketHandler handler, CommandProcessor commandProcessor, - IOptions options + IOptions options, + ILogger logger ) : base(commandProcessor) { @@ -73,9 +75,10 @@ public class PcapNostaleClient : BaseNostaleClient _pcapManager = pcapManager; _processTcpManager = processTcpManager; _handler = handler; + _logger = logger; _options = options.Value; _crypto = new CryptographyManager(); - _crypto.EncryptionKey = encryptionKey; + _crypto.EncryptionKey = initialEncryptionKey; } /// @@ -103,8 +106,8 @@ public class PcapNostaleClient : BaseNostaleClient break; } - var connection = (await _processTcpManager.GetConnectionsAsync(_process.Id)).Cast() - .FirstOrDefault(); + var connections = await _processTcpManager.GetConnectionsAsync(_process.Id); + TcpConnection? connection = connections.Count > 0 ? connections[0] : null; if (lastConnection != connection) { @@ -190,83 +193,65 @@ public class PcapNostaleClient : BaseNostaleClient /// The raw payload data of the packet. internal void OnPacketArrival(LibPcapLiveDevice? device, TcpConnection connection, byte[] payloadData) { - // TODO: cleanup this method, split it into multiple methods - // TODO: make it more effective, currently it uses expensive operations such as Split on strings - _lastDevice = device; string data; PacketSource source; - bool containsPacketId = false; + bool mayContainPacketId = false; if (connection.LocalAddr == _connection.LocalAddr && connection.LocalPort == _connection.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; - } + mayContainPacketId = true; + data = _crypto.DecryptUnknownServerPacket(payloadData, _encoding); } else { // received packet source = PacketSource.Server; - if (_crypto.EncryptionKey == 0) - { // probably login - data = _crypto.ClientLogin.Decrypt(payloadData, _encoding); + data = _crypto.DecryptUnknownClientPacket(payloadData, _encoding); + } - var splitted = data.Split(' '); - var header = splitted.Length > 0 ? splitted[0] : string.Empty; - bool isPacket = true; - foreach (var c in header) + if (data.Length > 0) + { + foreach (ReadOnlySpan line in data.SplitLines()) + { + var linePacket = line; + if (mayContainPacketId) { - if (!char.IsAsciiLetterOrDigit(c) && c != '#') + var spaceIndex = linePacket.IndexOf(' '); + if (spaceIndex != -1) { - isPacket = false; - break; + var beginning = linePacket.Slice(0, spaceIndex); + + if (int.TryParse(beginning, out var packetIndex)) + { + _lastPacketIndex = packetIndex; + linePacket = linePacket.Slice(spaceIndex + 1); + } } } - if (!isPacket) - { // try world crypto? - data = _crypto.ClientWorld.Decrypt(payloadData, _encoding); - } - } - else - { - data = _crypto.ClientWorld.Decrypt(payloadData, _encoding); + var lineString = linePacket.ToString(); + Task.Run(() => ProcessPacketAsync(source, lineString)); } } + } - if (data.Length > 0) + private async Task ProcessPacketAsync(PacketSource type, string packetString) + { + try { - foreach (var line in data.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - var linePacket = line; - if (containsPacketId) - { - _lastPacketIndex = int.Parse(line.Substring(0, line.IndexOf(' '))); - linePacket = line.Substring(line.IndexOf(' ') + 1); - } + var result = await _handler.HandlePacketAsync(this, type, packetString); - _handler.HandlePacketAsync(this, source, linePacket.Trim(), _stoppingToken ?? default); + 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"); + } } } \ No newline at end of file -- 2.48.1