~ruther/NosTale-PacketLogger

1ac87d8e49a6ebb1fa3b619a0538dc828ab7fd73 — Rutherther 2 years ago c6ddfec
feat: add pcap support
A src/PacketLogger/Models/Packets/ClientPacketProvider.cs => src/PacketLogger/Models/Packets/ClientPacketProvider.cs +135 -0
@@ 0,0 1,135 @@
//
//  ClientPacketProvider.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;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using DynamicData;
using DynamicData.Binding;
using NosSmooth.Comms.Local;
using NosSmooth.Core.Client;
using NosSmooth.Core.Packets;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using ReactiveUI;
using Remora.Results;

namespace PacketLogger.Models.Packets;

/// <summary>
/// A packet provider using <see cref="INostaleClient"/>.
/// </summary>
public abstract class ClientPacketProvider : ReactiveObject, IPacketProvider
{
    private readonly IDisposable _cleanUp;
    private readonly NostaleProcess _process;
    private readonly INostaleClient _client;
    private readonly CancellationTokenSource _ctSource;
    private long _currentIndex;
    private Task<Result>? _runTask;

    /// <summary>
    /// Initializes a new instance of the <see cref="ClientPacketProvider"/> class.
    /// </summary>
    /// <param name="process">The process.</param>
    /// <param name="client">The nostale client.</param>
    public ClientPacketProvider(NostaleProcess process, INostaleClient client)
    {
        _ctSource = new CancellationTokenSource();
        _process = process;
        _client = client;
        Packets = new SourceList<PacketInfo>();
        _cleanUp = process.WhenPropertyChanged(x => x.CharacterString)
            .Subscribe
            (
                _ => this.RaisePropertyChanged(nameof(Name))
            );
    }

    /// <inheritdoc />
    public event PropertyChangedEventHandler? PropertyChanged;

    /// <inheritdoc />
    public string Name => (_process.BrowserManager.IsInGame
        ? _process.BrowserManager.PlayerManager.Player.Name
        : null) ?? $"Not in game ({_process.Process.Id})";

    /// <inheritdoc />
    public abstract bool IsOpen { get; }

    /// <inheritdoc />
    public SourceList<PacketInfo> Packets { get; }

    /// <inheritdoc />
    public bool LogReceived { get; set; } = true;

    /// <inheritdoc />
    public bool LogSent { get; set; } = true;

    /// <inheritdoc />
    public Task<Result> Open()
    {
        _runTask = Task.Run(() => _client.RunAsync(_ctSource.Token));
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public virtual Task<Result> Close()
    {
        _ctSource.Cancel();
        if (_runTask is not null)
        {
            return _runTask;
        }

        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public void Clear()
    {
        Packets.Clear();
    }

    /// <inheritdoc />
    public Task<Result> SendPacket(string packetString, CancellationToken ct = default)
        => _client.SendPacketAsync(packetString, ct);

    /// <inheritdoc />
    public Task<Result> ReceivePacket(string packetString, CancellationToken ct = default)
        => _client.ReceivePacketAsync(packetString, ct);

    /// <summary>
    /// Add the given packets from an event.
    /// </summary>
    /// <param name="packetArgs">The packet event args.</param>
    /// <typeparam name="TPacket">The type of the deserialized packet.</typeparam>
    internal void AddPacket(PacketEventArgs packetArgs)
    {
        var index = Interlocked.Increment(ref _currentIndex);
        if ((packetArgs.Source == PacketSource.Server && LogReceived)
            || (packetArgs.Source == PacketSource.Client && LogSent))
        {
            Packets.Add(new PacketInfo(index, DateTime.Now, packetArgs.Source, packetArgs.PacketString));
        }
    }

    /// <inheritdoc />
    public void Dispose()
    {
    }

    /// <summary>
    /// A dispose used instead of <see cref="Dispose"/>
    /// to prevent the service provider disposing.
    /// </summary>
    public void CustomDispose()
    {
        _ctSource.Dispose();
        _cleanUp.Dispose();
        Packets.Dispose();
    }
}
\ No newline at end of file

M src/PacketLogger/Models/Packets/CommsPacketProvider.cs => src/PacketLogger/Models/Packets/CommsPacketProvider.cs +4 -77
@@ 22,10 22,8 @@ namespace PacketLogger.Models.Packets;
/// <summary>
/// A packet provider using a connection to a nostale client.
/// </summary>
public class CommsPacketProvider : ReactiveObject, IPacketProvider
public class CommsPacketProvider : ClientPacketProvider
{
    private readonly IDisposable _cleanUp;
    private readonly NostaleProcess _process;
    private readonly Comms _comms;
    private long _currentIndex;



@@ 35,89 33,18 @@ public class CommsPacketProvider : ReactiveObject, IPacketProvider
    /// <param name="process">The process.</param>
    /// <param name="comms">The comms.</param>
    public CommsPacketProvider(NostaleProcess process, Comms comms)
        : base(process, comms.Client)
    {
        _process = process;
        _comms = comms;
        Packets = new SourceList<PacketInfo>();
        _cleanUp = process.WhenPropertyChanged(x => x.CharacterString)
            .Subscribe
            (
                _ => this.RaisePropertyChanged(nameof(Name))
            );
    }

    /// <inheritdoc />
    public event PropertyChangedEventHandler? PropertyChanged;
    public override bool IsOpen => _comms.Connection.Connection.State == ConnectionState.Open;

    /// <inheritdoc />
    public string Name => (_process.BrowserManager.IsInGame
        ? _process.BrowserManager.PlayerManager.Player.Name
        : null) ?? $"Not in game ({_process.Process.Id})";

    /// <inheritdoc />
    public bool IsOpen => _comms.Connection.Connection.State == ConnectionState.Open;

    /// <inheritdoc />
    public SourceList<PacketInfo> Packets { get; }

    /// <inheritdoc />
    public bool LogReceived { get; set; } = true;

    /// <inheritdoc />
    public bool LogSent { get; set; } = true;

    /// <inheritdoc />
    public Task<Result> Open()
        => Task.FromResult(Result.FromSuccess());

    /// <inheritdoc />
    public Task<Result> Close()
    public override Task<Result> Close()
    {
        _comms.Connection.Connection.Disconnect();
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public void Clear()
    {
        Packets.Clear();
    }

    /// <inheritdoc />
    public Task<Result> SendPacket(string packetString, CancellationToken ct = default)
        => _comms.Client.SendPacketAsync(packetString, ct);

    /// <inheritdoc />
    public Task<Result> ReceivePacket(string packetString, CancellationToken ct = default)
        => _comms.Client.ReceivePacketAsync(packetString, ct);

    /// <summary>
    /// Add the given packets from an event.
    /// </summary>
    /// <param name="packetArgs">The packet event args.</param>
    /// <typeparam name="TPacket">The type of the deserialized packet.</typeparam>
    internal void AddPacket(PacketEventArgs packetArgs)
    {
        var index = Interlocked.Increment(ref _currentIndex);
        if ((packetArgs.Source == PacketSource.Server && LogReceived)
            || (packetArgs.Source == PacketSource.Client && LogSent))
        {
            Packets.Add(new PacketInfo(index, DateTime.Now, packetArgs.Source, packetArgs.PacketString));
        }
    }

    /// <inheritdoc />
    public void Dispose()
    {
    }

    /// <summary>
    /// A dispose used instead of <see cref="Dispose"/>
    /// to prevent the service provider disposing.
    /// </summary>
    public void CustomDispose()
    {
        _cleanUp.Dispose();
        Packets.Dispose();
    }
}
\ No newline at end of file

M src/PacketLogger/Models/Packets/PacketResponder.cs => src/PacketLogger/Models/Packets/PacketResponder.cs +2 -2
@@ 19,13 19,13 @@ namespace PacketLogger.Models.Packets;
/// <inheritdoc />
public class PacketResponder : IRawPacketResponder
{
    private readonly CommsPacketProvider _provider;
    private readonly ClientPacketProvider _provider;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketResponder"/> class.
    /// </summary>
    /// <param name="provider">The provider.</param>
    public PacketResponder(CommsPacketProvider provider)
    public PacketResponder(ClientPacketProvider provider)
    {
        _provider = provider;
    }

A src/PacketLogger/Models/Packets/PcapPacketProvider.cs => src/PacketLogger/Models/Packets/PcapPacketProvider.cs +35 -0
@@ 0,0 1,35 @@
//
//  PcapPacketProvider.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.Threading.Tasks;
using NosSmooth.Core.Client;
using ReactiveUI;
using Remora.Results;

namespace PacketLogger.Models.Packets;

/// <summary>
/// A packet provider using pcap.
/// </summary>
public class PcapPacketProvider : ClientPacketProvider
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PcapPacketProvider"/> class.
    /// </summary>
    /// <param name="process">The nostale process.</param>
    /// <param name="client">The pcap client.</param>
    public PcapPacketProvider(NostaleProcess process, INostaleClient client)
        : base(process, client)
    {
    }

    /// <inheritdoc />
    public override bool IsOpen => true;

    /// <inheritdoc />
    public override Task<Result> Close()
        => Task.FromResult(Result.FromSuccess());
}
\ No newline at end of file

M src/PacketLogger/PacketLogger.csproj => src/PacketLogger/PacketLogger.csproj +4 -2
@@ 1,6 1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>


@@ 32,8 32,10 @@
      <PrivateAssets>None</PrivateAssets>
    </PackageReference>
    <PackageReference Include="NosSmooth.Core" Version="4.0.2" />
    <PackageReference Include="NosSmooth.LocalBinding" Version="1.0.0" />
    <PackageReference Include="NosSmooth.Cryptography" Version="1.0.0-main4152899734" />
    <PackageReference Include="NosSmooth.LocalBinding" Version="1.1.0-main4152737026" />
    <PackageReference Include="NosSmooth.PacketSerializer.Abstractions" Version="1.3.1" />
    <PackageReference Include="NosSmooth.Pcap" Version="1.0.0-main4152899734" />
    <PackageReference Include="Projektanker.Icons.Avalonia" Version="5.8.0" />
    <PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="5.8.0" />
    <PackageReference Include="PropertyChanged.Fody" Version="4.1.0">

M src/PacketLogger/ViewModels/DockFactory.cs => src/PacketLogger/ViewModels/DockFactory.cs +7 -0
@@ 30,6 30,7 @@ namespace PacketLogger.ViewModels;
public class DockFactory : Factory, IDisposable
{
    private readonly StatefulRepository _repository;
    private readonly IServiceProvider _services;
    private readonly FilterProfiles _filterProfiles;
    private readonly ObservableCollection<IPacketProvider> _providers;
    private readonly NostaleProcesses _processes;


@@ 41,6 42,7 @@ public class DockFactory : Factory, IDisposable
    /// <summary>
    /// Initializes a new instance of the <see cref="DockFactory"/> class.
    /// </summary>
    /// <param name="services">The services.</param>
    /// <param name="filterProfiles">The filter profiles.</param>
    /// <param name="providers">The providers.</param>
    /// <param name="processes">The nostale processes.</param>


@@ 48,6 50,7 @@ public class DockFactory : Factory, IDisposable
    /// <param name="repository">The repository.</param>
    public DockFactory
    (
        IServiceProvider services,
        FilterProfiles filterProfiles,
        ObservableCollection<IPacketProvider> providers,
        NostaleProcesses processes,


@@ 55,6 58,7 @@ public class DockFactory : Factory, IDisposable
        StatefulRepository repository
    )
    {
        _services = services;
        _filterProfiles = filterProfiles;
        _providers = providers;
        _processes = processes;


@@ 101,6 105,7 @@ public class DockFactory : Factory, IDisposable

        var document = new DocumentViewModel
            (
                _services,
                _filterProfiles,
                _injector,
                _repository,


@@ 144,6 149,7 @@ public class DockFactory : Factory, IDisposable
                var index = documentDock.VisibleDockables?.Count + 1;
                var document = new DocumentViewModel
                    (
                        _services,
                        _filterProfiles,
                        _injector,
                        _repository,


@@ 167,6 173,7 @@ public class DockFactory : Factory, IDisposable
    {
        var initialTab = new DocumentViewModel
            (
                _services,
                _filterProfiles,
                _injector,
                _repository,

M src/PacketLogger/ViewModels/DocumentViewModel.cs => src/PacketLogger/ViewModels/DocumentViewModel.cs +51 -3
@@ 13,17 13,20 @@ using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Dock.Model.Mvvm.Controls;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Local;
using NosSmooth.Core.Contracts;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Stateful;
using NosSmooth.Pcap;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;


@@ 48,6 51,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// <summary>
    /// Initializes a new instance of the <see cref="DocumentViewModel"/> class.
    /// </summary>
    /// <param name="services">The services.</param>
    /// <param name="filterProfiles">The filter profiles.</param>
    /// <param name="injector">The injector.</param>
    /// <param name="repository">The repository.</param>


@@ 57,6 61,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    /// <param name="onDocumentUnloaded">The action to call on document unloaded/closed.</param>
    public DocumentViewModel
    (
        IServiceProvider services,
        FilterProfiles filterProfiles,
        CommsInjector injector,
        StatefulRepository repository,


@@ 140,7 145,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable

                var provider = new CommsPacketProvider(process, connection);
                _packetProvider = provider;
                repository.SetEntity<CommsPacketProvider>(connection.Client, provider);
                repository.SetEntity<ClientPacketProvider>(connection.Client, provider);

                var contractResult = await connection.Connection.ContractHanshake
                        (new HandshakeRequest("PacketLogger", true, false))


@@ 184,6 189,42 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
            }
        );

        OpenPcap = ReactiveCommand.CreateFromTask<NostaleProcess>
        (
            async process =>
            {
                Loading = true;
                var encryptionKey = process.BrowserManager.IsInGame ? process.BrowserManager.NtClient.EncryptionKey : 0;
                var client = ActivatorUtilities.CreateInstance<PcapNostaleClient>
                    (services, process.Process, encryptionKey, Encoding.Default);

                var provider = new PcapPacketProvider(process, client);
                _packetProvider = provider;
                repository.SetEntity<ClientPacketProvider>(client, provider);

                _cleanUp = process.WhenPropertyChanged(x => x.CharacterString)
                    .ObserveOn(RxApp.MainThreadScheduler)
                    .Do
                    (
                        _ =>
                        {
                            Title = (process.BrowserManager.IsInGame
                                ? process.BrowserManager.PlayerManager.Player.Name
                                : null) ?? $"Not in game ({process.Process.Id})";
                        }
                    )
                    .Subscribe();

                await provider.Open();

                NestedViewModel = new PacketLogViewModel(provider, filterProfiles);
                Title = provider.Name;
                Loading = false;
                Loaded = true;
                onDocumentLoaded(this);
            }
        );

        OpenSender = ReactiveCommand.Create<IPacketProvider>
        (
            provider =>


@@ 198,13 239,15 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable

        ClearError = ReactiveCommand.Create(() => Error = null);

        OpenSettings = ReactiveCommand.Create(
        OpenSettings = ReactiveCommand.Create
        (
            () =>
            {
                Title = "Settings";
                NestedViewModel = new SettingsViewModel(filterProfiles);
                Loaded = true;
            });
            }
        );
    }

    /// <summary>


@@ 278,6 321,11 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
    public ReactiveCommand<NostaleProcess, Unit> OpenProcess { get; }

    /// <summary>
    /// Gets the command for opening a process / connecting to a process.
    /// </summary>
    public ReactiveCommand<NostaleProcess, Unit> OpenPcap { get; }

    /// <summary>
    /// Get open settings command.
    /// </summary>
    public ReactiveCommand<Unit, Unit> OpenSettings { get; }

M src/PacketLogger/ViewModels/MainWindowViewModel.cs => src/PacketLogger/ViewModels/MainWindowViewModel.cs +4 -1
@@ 25,6 25,7 @@ using NosSmooth.Core.Extensions;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Extensions;
using NosSmooth.PacketSerializer.Packets;
using NosSmooth.Pcap;
using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;


@@ 50,9 51,11 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
            .AddSingleton<DockFactory>()
            .AddSingleton<NostaleProcesses>()
            .AddSingleton<ObservableCollection<IPacketProvider>>(_ => Providers)
            .AddSingleton<ProcessTcpManager>()
            .AddSingleton<PcapNostaleManager>()
            .AddNostaleCore()
            .AddStatefulInjector()
            .AddStatefulEntity<CommsPacketProvider>()
            .AddStatefulEntity<ClientPacketProvider>()
            .AddLocalComms()
            .AddPacketResponder(typeof(PacketResponder))
            .BuildServiceProvider();

M src/PacketLogger/Views/DocumentView.axaml => src/PacketLogger/Views/DocumentView.axaml +12 -1
@@ 88,7 88,18 @@
                                            <Button Content="Connect"
                                                    Command="{Binding $parent[UserControl].DataContext.OpenProcess}"
                                                    IsEnabled="{Binding !$parent[UserControl].DataContext.Loading}"
                                                    CommandParameter="{Binding }" />
                                                    CommandParameter="{Binding}" />
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                                
                                <DataGridTemplateColumn Header="Pcap">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <Button Content="Sniff"
                                                    Command="{Binding $parent[UserControl].DataContext.OpenPcap}"
                                                    IsEnabled="{Binding !$parent[UserControl].DataContext.Loading}"
                                                    CommandParameter="{Binding}" />
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>

Do not follow this link