~ruther/NosSmooth

587a3ea90045ace1dbe5c9bd484fd71570239f1e — Rutherther 2 years ago b39d402
feat(pcap): use new unknown decryption methods insice PcapNostaleClient

Get rid of unnecessary allocations as well
A Core/NosSmooth.Cryptography/Extensions/StringExtensions.cs => Core/NosSmooth.Cryptography/Extensions/StringExtensions.cs +139 -0
@@ 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;

/// <summary>
/// Extension methods for string.
/// </summary>
public static class StringExtensions
{
    /// <summary>
    /// Split a string into lines without allocations.
    /// </summary>
    /// <param name="str">The string to split.</param>
    /// <returns>An enumerator with lines.</returns>
    public static LineSplitEnumerator SplitLines(this string str)
    {
        // LineSplitEnumerator is a struct so there is no allocation here
        return new LineSplitEnumerator(str.AsSpan());
    }

    /// <summary>
    /// An enumerator of a string lines.
    /// </summary>
    public ref struct LineSplitEnumerator
    {
        private ReadOnlySpan<char> _str;

        /// <summary>
        /// Initializes a new instance of the <see cref="LineSplitEnumerator"/> struct.
        /// </summary>
        /// <param name="str">The string.</param>
        public LineSplitEnumerator(ReadOnlySpan<char> str)
        {
            _str = str;
            Current = default;
        }

        /// <summary>
        /// Gets this enumerator.
        /// </summary>
        /// <returns>This.</returns>
        public LineSplitEnumerator GetEnumerator()
            => this;

        /// <summary>
        /// Move to next line.
        /// </summary>
        /// <returns>Whether move was successful.</returns>
        public bool MoveNext()
        {
            var span = _str;
            if (span.Length == 0)
            {
                return false;
            }

            var index = span.IndexOfAny('\r', '\n');
            if (index == -1)
            {
                _str = ReadOnlySpan<char>.Empty;
                Current = new LineSplitEntry(span, ReadOnlySpan<char>.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;
        }

        /// <summary>
        /// Current line.
        /// </summary>
        public LineSplitEntry Current { get; private set; }
    }

    /// <summary>
    /// A line.
    /// </summary>
    public readonly ref struct LineSplitEntry
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="LineSplitEntry"/> struct.
        /// </summary>
        /// <param name="line">The line.</param>
        /// <param name="separator">The line separator.</param>
        public LineSplitEntry(ReadOnlySpan<char> line, ReadOnlySpan<char> separator)
        {
            Line = line;
            Separator = separator;
        }

        /// <summary>
        /// Gets the line.
        /// </summary>
        public ReadOnlySpan<char> Line { get; }

        /// <summary>
        /// Gets the separator of the line.
        /// </summary>
        public ReadOnlySpan<char> Separator { get; }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="line">The line.</param>
        /// <param name="separator">The line separator.</param>
        public void Deconstruct(out ReadOnlySpan<char> line, out ReadOnlySpan<char> separator)
        {
            line = Line;
            separator = Separator;
        }

        /// <summary>
        /// An implicit cast to ReadOnySpan.
        /// </summary>
        /// <param name="entry">The entry to cast.</param>
        /// <returns>The read only span of the entry.</returns>
        public static implicit operator ReadOnlySpan<char>(LineSplitEntry entry)
            => entry.Line;
    }
}
\ No newline at end of file

M Pcap/NosSmooth.Pcap/PcapNostaleClient.cs => Pcap/NosSmooth.Pcap/PcapNostaleClient.cs +48 -63
@@ 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<PcapNostaleClient> _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 <see cref="PcapNostaleClient"/> class.
    /// </summary>
    /// <param name="process">The process to look for.</param>
    /// <param name="encryptionKey">The current encryption key of the world connection, if known. Zero if unknown.</param>
    /// <param name="initialEncryptionKey">The current encryption key of the world connection, if known. Zero if unknown.</param>
    /// <param name="encoding">The encoding.</param>
    /// <param name="pcapManager">The pcap manager.</param>
    /// <param name="processTcpManager">The process manager.</param>
    /// <param name="handler">The packet handler.</param>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="options">The options.</param>
    /// <param name="logger">The logger.</param>
    public PcapNostaleClient
    (
        Process process,
        int encryptionKey,
        int initialEncryptionKey,
        Encoding encoding,
        PcapNostaleManager pcapManager,
        ProcessTcpManager processTcpManager,
        IPacketHandler handler,
        CommandProcessor commandProcessor,
        IOptions<PcapNostaleOptions> options
        IOptions<PcapNostaleOptions> options,
        ILogger<PcapNostaleClient> 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;
    }

    /// <inheritdoc />


@@ 103,8 106,8 @@ public class PcapNostaleClient : BaseNostaleClient
                    break;
                }

                var connection = (await _processTcpManager.GetConnectionsAsync(_process.Id)).Cast<TcpConnection?>()
                    .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
    /// <param name="payloadData">The raw payload data of the packet.</param>
    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<char> 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

Do not follow this link