eqemu-server/utils/stream_parser/stream_parser/ConsoleHostedService.cs

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;
}
}
}
}