mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-16 17:41:29 +00:00
318 lines
13 KiB
C#
318 lines
13 KiB
C#
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using StreamParser.Common.Daybreak;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommandLine;
|
|
using System.Globalization;
|
|
using Org.BouncyCastle.Crypto.Engines;
|
|
using Org.BouncyCastle.Crypto.Modes;
|
|
using Org.BouncyCastle.Crypto;
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
|
|
namespace StreamParser
|
|
{
|
|
public class ConsoleHostedService : IHostedService
|
|
{
|
|
private class ParsedPacket {
|
|
public byte[] Data { get; set; }
|
|
public Direction Direction { get; set; }
|
|
public DateTime Time { get; set; }
|
|
}
|
|
|
|
private class ParsedConnection
|
|
{
|
|
public IPAddress ClientAddress { get; set; }
|
|
public int ClientPort { get; set; }
|
|
public IPAddress ServerAddress { get; set; }
|
|
public int ServerPort { get; set; }
|
|
public ConnectionType ConnectionType { get; set; }
|
|
public List<ParsedPacket> Packets { get; set; }
|
|
public DateTime ConnectedTime { get; set; }
|
|
public DateTime? DisconnectedTime { get; set; }
|
|
}
|
|
|
|
private readonly ILogger<ConsoleHostedService> _logger;
|
|
private readonly IHostApplicationLifetime _applicationLifetime;
|
|
private readonly IParser _parser;
|
|
private readonly Dictionary<Guid, ParsedConnection> _connections = new Dictionary<Guid, ParsedConnection>();
|
|
|
|
public ConsoleHostedService(ILogger<ConsoleHostedService> logger,
|
|
IHostApplicationLifetime applicationLifetime,
|
|
IParser parser)
|
|
{
|
|
_logger = logger;
|
|
_applicationLifetime = applicationLifetime;
|
|
_parser = parser;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_applicationLifetime.ApplicationStarted.Register(() =>
|
|
{
|
|
var args = Environment.GetCommandLineArgs();
|
|
|
|
CommandLine.Parser.Default.ParseArguments<ConsoleHostedServiceOptions>(args)
|
|
.WithParsed<ConsoleHostedServiceOptions>(o =>
|
|
{
|
|
_parser.OnNewConnection += OnNewConnection;
|
|
_parser.OnLostConnection += OnLostConnection;
|
|
|
|
foreach(var f in o.Input)
|
|
{
|
|
_logger.LogInformation("Parsing {0}...", f);
|
|
_parser.Parse(f);
|
|
}
|
|
|
|
foreach(var c in _connections)
|
|
{
|
|
if(o.Dump)
|
|
{
|
|
DumpConnectionToTextFile(c.Value, o.Output, o.Decrypt);
|
|
}
|
|
|
|
if(o.Csv)
|
|
{
|
|
DumpConnectionToCsvFile(c.Value, o.Output, o.Decrypt);
|
|
}
|
|
}
|
|
|
|
_applicationLifetime.StopApplication();
|
|
})
|
|
.WithNotParsed<ConsoleHostedServiceOptions>(e =>
|
|
{
|
|
bool stops_processing = false;
|
|
foreach(var err in e)
|
|
{
|
|
_logger.LogError("Error: {0}", err.Tag);
|
|
stops_processing = stops_processing || err.StopsProcessing;
|
|
}
|
|
|
|
if(stops_processing)
|
|
{
|
|
_applicationLifetime.StopApplication();
|
|
}
|
|
});
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void OnNewConnection(IConnection connection, DateTime connectionTime)
|
|
{
|
|
_logger.LogTrace("New connection {0}:{1} <-> {2}:{3} of type {4}",
|
|
connection.ClientAddress,
|
|
connection.ClientPort,
|
|
connection.ServerAddress,
|
|
connection.ServerPort,
|
|
connection.ConnectionType);
|
|
connection.OnPacketRecv += OnPacketRecv;
|
|
|
|
_connections.Add(connection.Id, new ParsedConnection
|
|
{
|
|
ClientAddress = connection.ClientAddress,
|
|
ClientPort = connection.ClientPort,
|
|
ServerAddress = connection.ServerAddress,
|
|
ServerPort = connection.ServerPort,
|
|
ConnectionType = connection.ConnectionType,
|
|
Packets = new List<ParsedPacket>(),
|
|
ConnectedTime = connectionTime,
|
|
});
|
|
}
|
|
|
|
private void OnLostConnection(IConnection connection, DateTime connectionTime)
|
|
{
|
|
_logger.LogTrace("Lost connection {0}:{1} <-> {2}:{3}",
|
|
connection.ClientAddress,
|
|
connection.ClientPort,
|
|
connection.ServerAddress,
|
|
connection.ServerPort);
|
|
connection.OnPacketRecv -= OnPacketRecv;
|
|
|
|
var parsedConnection = _connections.GetValueOrDefault(connection.Id);
|
|
parsedConnection.DisconnectedTime = connectionTime;
|
|
}
|
|
|
|
private void OnPacketRecv(IConnection connection, Direction direction, DateTime packetTime, ReadOnlySpan<byte> data)
|
|
{
|
|
var parsedConnection = _connections.GetValueOrDefault(connection.Id);
|
|
parsedConnection.Packets.Add(new ParsedPacket
|
|
{
|
|
Data = data.ToArray(),
|
|
Direction = direction,
|
|
Time = packetTime
|
|
});
|
|
}
|
|
|
|
private void DumpConnectionToTextFile(ParsedConnection c, string output, bool decrypt)
|
|
{
|
|
try
|
|
{
|
|
var path = output + string.Format("{0}-{1}.txt", c.ConnectionType.ToString().ToLower(), c.ConnectedTime.ToString("yyyyMMddHHmmssfff"));
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
|
|
File.AppendAllText(path, string.Format("### type: {0}\n", c.ConnectionType));
|
|
File.AppendAllText(path, string.Format("### started: {0}\n", c.ConnectedTime.ToString("s")));
|
|
File.AppendAllText(path, string.Format("### ended: {0}\n", c.DisconnectedTime.HasValue ? c.DisconnectedTime.Value.ToString("s") : "unknown"));
|
|
File.AppendAllText(path, string.Format("### client: {0}:{1}\n", c.ClientAddress.ToString(), c.ClientPort));
|
|
File.AppendAllText(path, string.Format("### server: {0}:{1}\n\n", c.ServerAddress.ToString(), c.ServerPort));
|
|
|
|
foreach(var p in c.Packets)
|
|
{
|
|
ReadOnlySpan<byte> data = p.Data;
|
|
string dir = p.Direction == Direction.ClientToServer ? "Client -> Server" : "Server -> Client";
|
|
|
|
switch (c.ConnectionType)
|
|
{
|
|
case ConnectionType.Login:
|
|
{
|
|
int opcode = BitConverter.ToUInt16(data.Slice(0, 2));
|
|
{
|
|
File.AppendAllText(path,
|
|
string.Format("{0} [Opcode: 0x{1}, Size: {2}] ({3})\n", dir, opcode.ToString("X4"), data.Length - 2, p.Time.ToString("s")));
|
|
|
|
var gp = new GamePacket(data.Slice(2));
|
|
File.AppendAllText(path, string.Format("{0}\n", gp.ToString()));
|
|
|
|
if(decrypt && opcode == 2 || opcode == 24)
|
|
{
|
|
var encrypted_block = data.Slice(12, data.Length - 12);
|
|
var dec = EQDecrypt(encrypted_block);
|
|
|
|
if(dec != null)
|
|
{
|
|
File.AppendAllText(path, string.Format("[Decrypted Data, Offset: {0}, Size: {1}]\n", 10, dec.Length));
|
|
gp = new GamePacket(dec);
|
|
File.AppendAllText(path, string.Format("{0}\n", gp.ToString()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case ConnectionType.Chat:
|
|
{
|
|
int opcode = data[0];
|
|
File.AppendAllText(path,
|
|
string.Format("{0} [Opcode: 0x{1}, Size: {2}] ({3})\n", dir, opcode.ToString("X2"), data.Length - 1, p.Time.ToString("s")));
|
|
|
|
var gp = new GamePacket(data.Slice(1));
|
|
File.AppendAllText(path, string.Format("{0}\n", gp.ToString()));
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
int opcode = BitConverter.ToUInt16(data.Slice(0, 2));
|
|
File.AppendAllText(path,
|
|
string.Format("{0} [Opcode: 0x{1}, Size: {2}] ({3})\n", dir, opcode.ToString("X4"), data.Length - 2, p.Time.ToString("s")));
|
|
|
|
var gp = new GamePacket(data.Slice(2));
|
|
File.AppendAllText(path, string.Format("{0}\n", gp.ToString()));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error dumping connection {0} to txt file", c.ConnectedTime.ToString("s"));
|
|
}
|
|
}
|
|
|
|
private class CsvRow
|
|
{
|
|
public int Index { get; set; }
|
|
public string Direction { get; set; }
|
|
public string Opcode { get; set; }
|
|
public int Size { get; set; }
|
|
public string Data { get; set; }
|
|
}
|
|
|
|
private void DumpConnectionToCsvFile(ParsedConnection c, string output, bool decrypt)
|
|
{
|
|
try
|
|
{
|
|
var path = output + string.Format("{0}-{1}.csv", c.ConnectionType.ToString().ToLower(), c.ConnectedTime.ToString("yyyyMMddHHmmssfff"));
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
|
|
var rows = new List<CsvRow>();
|
|
var i = 0;
|
|
foreach (var p in c.Packets)
|
|
{
|
|
var row = new CsvRow();
|
|
row.Index = i++;
|
|
ReadOnlySpan<byte> data = p.Data;
|
|
row.Direction = p.Direction == Direction.ClientToServer ? "0" : "1";
|
|
|
|
switch (c.ConnectionType)
|
|
{
|
|
case ConnectionType.Chat:
|
|
{
|
|
row.Opcode = data[0].ToString();
|
|
var gp = new GamePacket(data.Slice(1));
|
|
row.Size = data.Length - 1;
|
|
row.Data = gp.ToModelString(512, false);
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
|
|
row.Opcode = BitConverter.ToUInt16(data.Slice(0, 2)).ToString();
|
|
var gp = new GamePacket(data.Slice(2));
|
|
row.Size = data.Length - 2;
|
|
row.Data = gp.ToModelString(512, false);
|
|
}
|
|
break;
|
|
}
|
|
|
|
rows.Add(row);
|
|
}
|
|
|
|
using (var writer = new StreamWriter(path))
|
|
using (var csv = new CsvHelper.CsvWriter(writer, CultureInfo.InvariantCulture))
|
|
{
|
|
csv.WriteRecords(rows);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error dumping connection {0} to csv file", c.ConnectedTime.ToString("s"));
|
|
}
|
|
}
|
|
|
|
private byte[] EQDecrypt(ReadOnlySpan<byte> data)
|
|
{
|
|
try
|
|
{
|
|
var desEngine = new DesEngine();
|
|
var cbcBlockCipher = new CbcBlockCipher(desEngine);
|
|
var bufferedBlockCipher = new BufferedBlockCipher(cbcBlockCipher);
|
|
bufferedBlockCipher.Init(false, new ParametersWithIV(new KeyParameter(new byte[16]), new byte[8]));
|
|
var cipherData = new byte[bufferedBlockCipher.GetOutputSize(data.Length)];
|
|
var outputLength = bufferedBlockCipher.ProcessBytes(data.ToArray(), 0, data.Length, cipherData, 0);
|
|
bufferedBlockCipher.DoFinal(cipherData, outputLength);
|
|
return cipherData;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error decrypting EQ Datablock");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|