@@ 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
@@ 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