baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
17
src/ChatServer/ExDbConnector/ChatServer.cfg
Normal file
17
src/ChatServer/ExDbConnector/ChatServer.cfg
Normal file
@@ -0,0 +1,17 @@
|
||||
##############################
|
||||
# ChatServer Configuration #
|
||||
##############################
|
||||
|
||||
# The following values are defaults and are even applied if this file or single configuration value-pairs are missing or are in the wrong format:
|
||||
# ChatServerListenerPort=55980
|
||||
# ExDbHost=127.0.0.1
|
||||
# ExDbPort=55906
|
||||
# Xor32Key=AB 11 CD FE 18 23 C5 A3 CA 33 C1 CC 66 67 21 F3 32 12 15 35 29 FF FE 1D 44 EF CD 41 26 3C 4E 4D
|
||||
|
||||
|
||||
ChatServerListenerPort=55980
|
||||
|
||||
ExDbHost=127.0.0.1
|
||||
ExDbPort=55906
|
||||
|
||||
Xor32Key=AB 11 CD FE 18 23 C5 A3 CA 33 C1 CC 66 67 21 F3 32 12 15 35 29 FF FE 1D 44 EF CD 41 26 3C 4E 4D
|
||||
@@ -0,0 +1,55 @@
|
||||
// <copyright file="ConfigurableNetworkEncryptionPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.Network.Xor;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A configurable network encryption factory plugin which reads the Xor32 key from the ChatServer.cfg file. Only used by the ExDbConnector project.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Display(Name = "Configurable encryption plugin", Description = "A configurable network encryption factory plugin which reads the Xor32 key from the ChatServer.cfg file. Only used by the ExDbConnector project.")]
|
||||
[Guid("890997B2-9334-4E9E-8C82-4492A831BCE3")]
|
||||
public class ConfigurableNetworkEncryptionPlugIn : INetworkEncryptionFactoryPlugIn
|
||||
{
|
||||
private readonly byte[] _xor32Key;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurableNetworkEncryptionPlugIn"/> class.
|
||||
/// </summary>
|
||||
public ConfigurableNetworkEncryptionPlugIn()
|
||||
{
|
||||
var settings = new Settings("ChatServer.cfg");
|
||||
this._xor32Key = settings.Xor32Key ?? new byte[32];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version for which this plugin is available.
|
||||
/// </summary>
|
||||
public static ClientVersion Version { get; } = new(byte.MaxValue, byte.MaxValue, ClientLanguage.Invariant);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ClientVersion Key => Version;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPipelinedDecryptor? CreateDecryptor(PipeReader source, DataDirection direction)
|
||||
{
|
||||
return new PipelinedXor32Decryptor(source, this._xor32Key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPipelinedEncryptor? CreateEncryptor(PipeWriter target, DataDirection direction)
|
||||
{
|
||||
// At least until season 6, there is no encryption from server to client.
|
||||
// ex700 may require packet twister here.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
275
src/ChatServer/ExDbConnector/ExDbClient.cs
Normal file
275
src/ChatServer/ExDbConnector/ExDbClient.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
// <copyright file="ExDbClient.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.Buffers;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.Packets;
|
||||
using Pipelines.Sockets.Unofficial;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
/// <summary>
|
||||
/// The connected exDB server. This class includes the communication implementation between chat server and exDB server.
|
||||
/// It registers clients for the chat server and hands back their authentication details.
|
||||
/// </summary>
|
||||
public class ExDbClient
|
||||
{
|
||||
private readonly ILogger<ExDbClient> _logger;
|
||||
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
private readonly IChatServer _chatServer;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ushort _chatServerPort;
|
||||
private readonly byte[] _packetBuffer = new byte[0xFF];
|
||||
|
||||
private IConnection? _connection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExDbClient" /> class.
|
||||
/// </summary>
|
||||
/// <param name="host">The host address of the exDB server.</param>
|
||||
/// <param name="port">The host port of the exDB server.</param>
|
||||
/// <param name="chatServer">The chat server.</param>
|
||||
/// <param name="chatServerPort">The chat server port.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public ExDbClient(string host, int port, IChatServer chatServer, int chatServerPort, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this._host = host;
|
||||
this._port = port;
|
||||
this._chatServer = chatServer;
|
||||
this._loggerFactory = loggerFactory;
|
||||
this._chatServerPort = (ushort)chatServerPort;
|
||||
this._logger = this._loggerFactory.CreateLogger<ExDbClient>();
|
||||
_ = Task.Run(this.ConnectAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the exDB server.
|
||||
/// </summary>
|
||||
public async ValueTask DisconnectAsync()
|
||||
{
|
||||
if (this._connection is { } connection)
|
||||
{
|
||||
await connection.DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask ConnectAsync()
|
||||
{
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
|
||||
while (!socket.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(this._host, this._port).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
this._logger.LogWarning($"Connection to ExDB-Server ({this._host}:{this._port}) failed, trying again in 10 Seconds...");
|
||||
await Task.Delay(10000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Connection to ExDB-Server established");
|
||||
|
||||
this._connection = new Connection(SocketConnection.Create(socket), null, null, this._loggerFactory.CreateLogger<Connection>());
|
||||
this._connection.PacketReceived += this.ExDbPacketReceivedAsync;
|
||||
this._connection.Disconnected += this.ConnectAsync;
|
||||
await this.SendHelloAsync().ConfigureAwait(false);
|
||||
await this._connection!.BeginReceiveAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask SendHelloAsync()
|
||||
{
|
||||
// C1 3A 00 02 AC DA 43 68 61 74 53 65 72 76 65 72 00 ...
|
||||
int Write()
|
||||
{
|
||||
var length = 0x3A;
|
||||
var span = this._connection!.Output.GetSpan(length)[..length];
|
||||
var packet = span;
|
||||
packet[0] = 0xC1;
|
||||
packet[1] = 0x3A;
|
||||
packet[3] = 0x02;
|
||||
packet[4] = this._chatServerPort.GetLowByte();
|
||||
packet[5] = this._chatServerPort.GetHighByte();
|
||||
packet.Slice(6).WriteString("ChatServer", Encoding.UTF8);
|
||||
return length;
|
||||
}
|
||||
|
||||
await this._connection!.SendAsync(Write).ConfigureAwait(false);
|
||||
this._logger.LogInformation("Sent registration packet to ExDB-Server");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is called when a packet is received from the exDB-Server.
|
||||
/// </summary>
|
||||
/// <param name="sequence">The packet.</param>
|
||||
private async ValueTask ExDbPacketReceivedAsync(ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
try
|
||||
{
|
||||
sequence.CopyTo(this._packetBuffer);
|
||||
var packet = this._packetBuffer.AsMemory(0, (int)sequence.Length);
|
||||
var type = packet.Span[0];
|
||||
if (type != 0xC1)
|
||||
{
|
||||
this._logger.LogWarning($"Unknown packet received from ExDB-Server, type: {type}");
|
||||
return;
|
||||
}
|
||||
|
||||
var code = packet.Span[2];
|
||||
switch (code)
|
||||
{
|
||||
case 0xA0:
|
||||
await this.ReadChatRoomCreationAsync(packet).ConfigureAwait(false);
|
||||
break;
|
||||
case 0xA1:
|
||||
await this.ReadChatRoomInvitationAsync(packet).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
this._logger.LogWarning($"Unknown packet received from ExDB-Server, code: {code}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this._logger.LogError(exception, $"An error occurred while processing an incoming packet from ExDB: {this._packetBuffer.AsString()}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the invitation to an existing chat room and registers the invited client.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet.</param>
|
||||
/// <remarks>
|
||||
/// Example: C1 15 A1 00 00 00 61 62 63 64 65 66 67 68 69 6F 20 01 00 01 57
|
||||
/// Index 4 and 5 is the room id, the next 10 bytes is the client name, after that the player id, game server id and a "type".
|
||||
/// The chat server answers this with the same packets as above(ticket 96862210):
|
||||
/// C1 2C A0 01 00 00 61 62 63 64 65 66 67 68 69 6F CC CC CC CC CC CC CC CC CC CC 53 54 55 56 CC CC 02 00 C6 05 CC CC CC CC 57 CC CC CC.
|
||||
/// </remarks>
|
||||
private async ValueTask ReadChatRoomInvitationAsync(Memory<byte> packet)
|
||||
{
|
||||
ushort roomId = 0;
|
||||
string clientName = string.Empty;
|
||||
ushort clientPlayerId = 0;
|
||||
ushort clientServerId = 0;
|
||||
byte type = 0;
|
||||
|
||||
void Extract(Span<byte> packet)
|
||||
{
|
||||
roomId = NumberConversionExtensions.MakeWord(packet[4], packet[5]);
|
||||
clientName = packet.ExtractString(6, 10, Encoding.UTF8);
|
||||
clientPlayerId = packet.TryMakeWordBigEndian(16);
|
||||
clientServerId = packet.TryMakeWordBigEndian(18);
|
||||
type = packet.Length > 20 ? packet[20] : (byte)0x57;
|
||||
}
|
||||
|
||||
Extract(packet.Span);
|
||||
this._logger.LogDebug($"Received request to invite {clientName} to chat room {roomId}, Client-ID: {clientPlayerId}, Server-ID: {clientServerId}");
|
||||
if (await this._chatServer.RegisterClientAsync(roomId, clientName).ConfigureAwait(false) is { } authentication)
|
||||
{
|
||||
await this.SendAuthenticationAsync(authentication, null, clientPlayerId, clientServerId, type).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the chat room creation message, creates a new chat room and registers the clients.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet.</param>
|
||||
/// <remarks>
|
||||
/// For example, we get here the following packet in:
|
||||
/// C1 20 A0 41 42 43 44 45 46 47 48 49 4A 50 51 52 53 54 55 56 57 58 59 00 E0 2E 01 00 E1 2E 01 00
|
||||
/// This packet includes the header and both names of the creator and the invited chat partner (each 10 bytes long).
|
||||
/// The server should then send the following data back to the exDB-Server:
|
||||
/// s | rid ||-----client name-----------||---------other client name-||plid| |svid||---| |-ticket--| |--------???----------|
|
||||
/// C1 2C A0 01 00 00 41 42 43 44 45 46 47 48 49 4A 50 51 52 53 54 55 56 57 58 59 00 00 00 00 CC CC 00 00 11 04 CC CC CC CC 00 CC CC CC
|
||||
/// C1 2C A0 01 00 00 50 51 52 53 54 55 56 57 58 59 41 42 43 44 45 46 47 48 49 4A 00 00 00 00 CC CC 01 00 BB 05 CC CC CC CC 01 CC CC CC.
|
||||
/// </remarks>
|
||||
private async ValueTask ReadChatRoomCreationAsync(Memory<byte> packet)
|
||||
{
|
||||
string clientName = string.Empty;
|
||||
string friendName = string.Empty;
|
||||
ushort clientPlayerId = 0;
|
||||
ushort clientServerId = 0;
|
||||
ushort friendPlayerId = 0;
|
||||
ushort friendServerId = 0;
|
||||
|
||||
void Extract(Span<byte> packet)
|
||||
{
|
||||
clientName = packet.ExtractString(3, 10, Encoding.UTF8);
|
||||
friendName = packet.ExtractString(13, 10, Encoding.UTF8);
|
||||
clientPlayerId = packet.TryMakeWordBigEndian(24);
|
||||
clientServerId = packet.TryMakeWordBigEndian(26);
|
||||
friendPlayerId = packet.TryMakeWordBigEndian(28);
|
||||
friendServerId = packet.TryMakeWordBigEndian(30);
|
||||
}
|
||||
|
||||
Extract(packet.Span);
|
||||
var roomId = await this._chatServer.CreateChatRoomAsync().ConfigureAwait(false);
|
||||
this._logger.LogDebug($"Received request to create chat room for {clientName} and {friendName}; Room-ID: {roomId}; Client-ID: {clientPlayerId}; Server-ID: {clientServerId}; Friend-ID: {friendPlayerId}; Friend-Server: {friendServerId}");
|
||||
var requesterAuthentication = await this._chatServer.RegisterClientAsync(roomId, clientName).ConfigureAwait(false);
|
||||
var friendAuthentication = await this._chatServer.RegisterClientAsync(roomId, friendName).ConfigureAwait(false);
|
||||
if (requesterAuthentication is not null)
|
||||
{
|
||||
await this.SendAuthenticationAsync(requesterAuthentication, friendAuthentication, clientPlayerId, clientServerId, requesterAuthentication.Index).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (friendAuthentication is not null)
|
||||
{
|
||||
await this.SendAuthenticationAsync(friendAuthentication, requesterAuthentication, friendPlayerId, friendServerId, friendAuthentication.Index).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the authentication information back to the ExDB-Server.
|
||||
/// </summary>
|
||||
/// <param name="authenticationInfo">The authentication information.</param>
|
||||
/// <param name="friendAuthenticationInfo">The friend authentication information.</param>
|
||||
/// <param name="clientId">The client identifier on the server where the client plays on.</param>
|
||||
/// <param name="serverId">The server identifier where the client plays on.</param>
|
||||
/// <param name="type">The type. Usually 0 for the player who requested the chat and 1 for the other player.</param>
|
||||
private async ValueTask SendAuthenticationAsync(ChatServerAuthenticationInfo authenticationInfo, ChatServerAuthenticationInfo? friendAuthenticationInfo, ushort clientId, ushort serverId, byte type)
|
||||
{
|
||||
this._logger.LogDebug($"Registered client {authenticationInfo.ClientName} with index {authenticationInfo.Index} and token {authenticationInfo.AuthenticationToken}");
|
||||
var token = uint.Parse(authenticationInfo.AuthenticationToken);
|
||||
uint friendToken = 0;
|
||||
if (friendAuthenticationInfo != null)
|
||||
{
|
||||
friendToken = uint.Parse(friendAuthenticationInfo.AuthenticationToken);
|
||||
}
|
||||
|
||||
var roomId = authenticationInfo.RoomId;
|
||||
|
||||
int Write()
|
||||
{
|
||||
var length = 0x2C;
|
||||
var packet = this._connection!.Output.GetSpan(length);
|
||||
packet[0] = 0xC1;
|
||||
packet[1] = 0x2C;
|
||||
packet[2] = 0xA0;
|
||||
packet[3] = 0x01;
|
||||
WriteUInt16LittleEndian(packet.Slice(4), roomId);
|
||||
packet.Slice(6).WriteString(authenticationInfo.ClientName, Encoding.UTF8);
|
||||
if (friendAuthenticationInfo != null)
|
||||
{
|
||||
packet.Slice(16).WriteString(friendAuthenticationInfo.ClientName, Encoding.UTF8);
|
||||
}
|
||||
|
||||
WriteUInt16LittleEndian(packet.Slice(26), clientId);
|
||||
WriteUInt16LittleEndian(packet.Slice(28), serverId);
|
||||
WriteUInt32LittleEndian(packet.Slice(32), token);
|
||||
WriteUInt32LittleEndian(packet.Slice(36), friendToken);
|
||||
packet[40] = type;
|
||||
return length;
|
||||
}
|
||||
|
||||
await this._connection!.SendAsync(Write).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AssemblyName>ChatServer</AssemblyName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
<ProjectReference Include="..\MUnique.OpenMU.ChatServer.csproj" />
|
||||
<ProjectReference Include="..\..\Network\MUnique.OpenMU.Network.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ChatServer.cfg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AssemblyName>ChatServer</AssemblyName>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DocumentationFile>bin\Debug\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DocumentationFile>bin\Release\MUnique.OpenMU.ChatServer.ExDbConnector.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\PlugIns\MUnique.OpenMU.PlugIns.csproj" />
|
||||
<ProjectReference Include="..\MUnique.OpenMU.ChatServer.csproj" />
|
||||
<ProjectReference Include="..\..\Network\MUnique.OpenMU.Network.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ChatServer.cfg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
79
src/ChatServer/ExDbConnector/Program.cs
Normal file
79
src/ChatServer/ExDbConnector/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
// <copyright file="Program.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.ComponentModel.Design;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MUnique.OpenMU.ChatServer;
|
||||
using MUnique.OpenMU.Network;
|
||||
using MUnique.OpenMU.Network.PlugIns;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
using Serilog;
|
||||
using Serilog.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry class of the application.
|
||||
/// </summary>
|
||||
internal class Program
|
||||
{
|
||||
private static ILogger<Program> _logger = NullLogger<Program>.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments. </param>
|
||||
internal static async Task Main(string[] args)
|
||||
{
|
||||
SelfLog.Enable(Console.Error);
|
||||
var logConfiguration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", false, true)
|
||||
.Build();
|
||||
|
||||
var logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(logConfiguration)
|
||||
.CreateLogger();
|
||||
|
||||
var loggerFactory = new LoggerFactory().AddSerilog(logger);
|
||||
_logger = loggerFactory.CreateLogger<Program>();
|
||||
|
||||
var addressResolver = IpAddressResolverFactory.CreateIpResolver(args, null, loggerFactory);
|
||||
var settings = new Settings("ChatServer.cfg");
|
||||
var serviceContainer = new ServiceContainer();
|
||||
serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
|
||||
|
||||
int chatServerListenerPort = settings.ChatServerListenerPort ?? 55980;
|
||||
int exDbPort = settings.ExDbPort ?? 55906;
|
||||
string exDbHost = settings.ExDbHost ?? "127.0.0.1";
|
||||
|
||||
try
|
||||
{
|
||||
// To make the chat server use our configured encryption key, we need to trick a bit. We add an endpoint with a special client version which is defined in the plugin.
|
||||
var configuration = new ChatServerSettings();
|
||||
configuration.Endpoints.Add(new ChatServerEndpoint { ClientVersion = ConfigurableNetworkEncryptionPlugIn.Version, NetworkPort = chatServerListenerPort });
|
||||
var pluginManager = new PlugInManager(null, loggerFactory, serviceContainer, null);
|
||||
pluginManager.DiscoverAndRegisterPlugInsOf<INetworkEncryptionFactoryPlugIn>();
|
||||
var chatServer = new ChatServer(addressResolver, loggerFactory, pluginManager);
|
||||
chatServer.Initialize(configuration);
|
||||
await chatServer.StartAsync().ConfigureAwait(false);
|
||||
var exDbClient = new ExDbClient(exDbHost, exDbPort, chatServer, chatServerListenerPort, loggerFactory);
|
||||
_logger.LogInformation("ChatServer started and ready");
|
||||
while (Console.ReadLine() != "exit")
|
||||
{
|
||||
// keep application running
|
||||
}
|
||||
|
||||
await exDbClient.DisconnectAsync().ConfigureAwait(false);
|
||||
await chatServer.ShutdownAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "Unexpected error occured");
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/ChatServer/ExDbConnector/Properties/AssemblyInfo.cs
Normal file
12
src/ChatServer/ExDbConnector/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="AssemblyInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MUnique.OpenMU.ChatServer.ExDbConnector")]
|
||||
[assembly: InternalsVisibleTo("MUnique.OpenMU.ChatServer.ExDbConnector.Tests")]
|
||||
156
src/ChatServer/ExDbConnector/Readme.md
Normal file
156
src/ChatServer/ExDbConnector/Readme.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# ChatServer ExDB Connector
|
||||
|
||||
This isn't directly a part of the OpenMU project. It's more like a side product
|
||||
to make the ChatServer available to users of the 'classical' private MU Servers.
|
||||
They have - or maybe had :) - the problem that they are bound to use the
|
||||
original closed source ChatServer of Webzen, if they get it working at all.
|
||||
|
||||
So to offer an open source alternative to the original ChatServer of Webzen,
|
||||
you can use this project to connect the OpenMU-ChatServer with your 'classic'
|
||||
ExDB server.
|
||||
|
||||
## Configuration
|
||||
|
||||
To make this work correctly with an existing ExDB-Server, some might do some
|
||||
minor adjustments in the configuration.
|
||||
|
||||
It's all configured in the ChatServer.cfg an should be self-explanatory.
|
||||
|
||||
### ChatServerListenerPort
|
||||
|
||||
It's the port to which the game clients should connect. Default is 55980,
|
||||
but I'm not sure if it can be changed without modifying the client.
|
||||
|
||||
### ExDbHost and Port
|
||||
|
||||
The host and tcp port of the ExDB server. Usually it's on the same server, so
|
||||
127.0.0.1 on port 55906.
|
||||
|
||||
### Xor32Key
|
||||
|
||||
This one is actually very important to get right. Otherwise, the game clients
|
||||
will not be able to connect.
|
||||
It's the same XOR32 key which is used for the 0xC1 packet encryption from game
|
||||
client to game server.
|
||||
|
||||
You can't edit this key at the original ChatServer of Webzen, that's the reason
|
||||
why it's pretty hard to get the ChatServer working on a private server.
|
||||
|
||||
## Communication between ExDB-Server and ChatServer
|
||||
|
||||
The ExDB server usually leaves the tcp port 55906 open, so that the ChatServer
|
||||
(and maybe other kind of subservers?) can connect to it.
|
||||
|
||||
### Registration
|
||||
|
||||
When the ChatServer connects to the ExDB server, it sends a data packet to
|
||||
register itself. It has the following struture:
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x3A | Packet header - length of the packet |
|
||||
| 1 | byte | 0x00 | Packet Type "server registration" |
|
||||
| 1 | byte | 0x02 | Id for "ChatServer" |
|
||||
| 2 | ushort | 0xDAAC | ChatServer client port (default: 55980) |
|
||||
| 11 | string | "ChatServer" | ChatServer name |
|
||||
|
||||
Example: C1 3A 00 02 AC DA 43 68 61 74 53 65 72 76 65 72 00
|
||||
|
||||
From now, the ChatServer will receive chat room creation and invitation
|
||||
requests from the ExDB Server, which were previously requested by the players.
|
||||
|
||||
### Chat Room Creation Request
|
||||
|
||||
When a client requests to create a new chat room, the following data packet is
|
||||
sent from the ExDB Server to the ChatServer.
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x25 | Packet header - length of the packet |
|
||||
| 1 | byte | 0xA0 | Packet Type 'chat room creation' |
|
||||
| 10 | string | | Name of the character who wants to create the room |
|
||||
| 10 | string | | Name of the character who should be invited to the room |
|
||||
| 1 | byte | 0x01 | "Type", not relevant? |
|
||||
| 2 | ushort | | Player id of the character who wants to create the room, big endian |
|
||||
| 2 | ushort | | Server id of the character who wants to create the room, big endian |
|
||||
| 2 | ushort | | Player id of the character who should be invited, big endian |
|
||||
| 2 | ushort | | Server id of the character who should be invited, big endian |
|
||||
|
||||
Example:
|
||||
C1 25 A0 41 42 43 44 45
|
||||
46 47 48 49 4A 50 51 52
|
||||
53 54 55 56 57 58 59 01
|
||||
20 01 00 01 20 02 00 01
|
||||
|
||||
### Chat Room Creation Responses
|
||||
|
||||
For each of both players, there is one data packet sent back to the ExDB Server:
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x2C | Packet header - length of the packet |
|
||||
| 1 | byte | 0xA0 | Packet Type 'chat room creation' |
|
||||
| 1 | byte | 0x01 | Success flag |
|
||||
| 2 | ushort | | Chat room id, big endian |
|
||||
| 10 | string | | Name of the character to which a chat room invitation should be sent |
|
||||
| 10 | string | | Name of the chat partner character |
|
||||
| 2 | ushort | | Player id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 2 | ushort | | Server id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 2 | byte | | Padding bytes for the alignment of the following authentication token |
|
||||
| 4 | uint | | Authentication token of the character to which a chat room invitation should be sent, big endian |
|
||||
| 4 | uint | | Authentication token of the chat partner, big endian |
|
||||
| 1 | byte | | 'Type' |
|
||||
| 3 | byte | | Don't know - padding?|
|
||||
|
||||
Example First Player:
|
||||
C1 2C A0 01 00 00 41 42
|
||||
43 44 45 46 47 48 49 4A
|
||||
50 51 52 53 54 55 56 57
|
||||
58 59 00 00 00 00 CC CC
|
||||
00 00 11 04 01 00 BB 05
|
||||
00 CC CC CC
|
||||
|
||||
Example Second Player:
|
||||
C1 2C A0 01 00 00 50 51
|
||||
52 53 54 55 56 57 58 59
|
||||
41 42 43 44 45 46 47 48
|
||||
49 4A 00 00 00 00 CC CC
|
||||
01 00 BB 05 00 00 11 04
|
||||
01 CC CC CC
|
||||
|
||||
### Chat Room Invitation Request
|
||||
|
||||
When a client requests to invite another friend to an existing chat room, the
|
||||
following data packet is sent from the ExDB Server to the ChatServer.
|
||||
|
||||
| Length | Data type | Value | Description |
|
||||
|----------|---------|-------------|---------|
|
||||
| 1 | byte | 0xC1 | Packet header - type |
|
||||
| 1 | byte | 0x16 | Packet header - length of the packet |
|
||||
| 1 | byte | 0xA1 | Packet Type 'chat room invitation' |
|
||||
| 1 | byte | 0x00 | Padding |
|
||||
| 2 | ushort | | Chat room id, big endian |
|
||||
| 10 | string | | Name of the character who should be invited to the room |
|
||||
| 2 | ushort | | Player id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 2 | ushort | | Server id of the character to which a chat room invitation should be sent, big endian |
|
||||
| 1 | byte | | 'Type' |
|
||||
|
||||
Example:
|
||||
C1 15 A1 00 00 00 61 62
|
||||
63 64 65 66 67 68 69 6F
|
||||
01 20 01 00 57
|
||||
|
||||
The ChatServer answers this with the same packet as above, but without filling
|
||||
the second character name - no wonder, there is more than one player in the
|
||||
room already.
|
||||
|
||||
Example:
|
||||
C1 2C A0 01 00 00 61 62
|
||||
63 64 65 66 67 68 69 6F
|
||||
CC CC CC CC CC CC CC CC
|
||||
CC CC 01 20 01 00 CC CC
|
||||
02 00 C6 05 CC CC CC CC
|
||||
57 CC CC CC
|
||||
121
src/ChatServer/ExDbConnector/Settings.cs
Normal file
121
src/ChatServer/ExDbConnector/Settings.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
// <copyright file="Settings.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.ChatServer.ExDbConnector;
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// A class which reads settings from a file.
|
||||
/// Line Format:
|
||||
/// [Key]=[Value]
|
||||
/// Line comments can be added by starting with "#".
|
||||
/// </summary>
|
||||
internal class Settings
|
||||
{
|
||||
private readonly IDictionary<string, string> _settingsDictionary = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Settings"/> class.
|
||||
/// Reads the file contents in, if the file is available.
|
||||
/// </summary>
|
||||
/// <param name="file">The file.</param>
|
||||
public Settings(string file)
|
||||
{
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var line in File.ReadAllLines(file))
|
||||
{
|
||||
var elements = line.Split('=');
|
||||
if (elements.Length > 1
|
||||
&& !elements[0].StartsWith("#", StringComparison.InvariantCulture)
|
||||
&& !this._settingsDictionary.ContainsKey(elements[0]))
|
||||
{
|
||||
this._settingsDictionary.Add(elements[0], elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured chat server listener port.
|
||||
/// </summary>
|
||||
public int? ChatServerListenerPort
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this["ChatServerListenerPort"] != null && int.TryParse(this["ChatServerListenerPort"], out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured exDb server port.
|
||||
/// </summary>
|
||||
public int? ExDbPort
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this["ExDbPort"] != null)
|
||||
{
|
||||
if (int.TryParse(this["ExDbPort"], out int result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured exDb server host.
|
||||
/// </summary>
|
||||
public string? ExDbHost => this["ExDbHost"];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured xor32 key.
|
||||
/// </summary>
|
||||
public byte[]? Xor32Key
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this["Xor32Key"] != null)
|
||||
{
|
||||
var customXor32KeyList = new List<byte>();
|
||||
var keyAsString = this["Xor32Key"];
|
||||
if (keyAsString is not null)
|
||||
{
|
||||
var bytesAsString = keyAsString.Split(' ');
|
||||
foreach (var byteString in bytesAsString)
|
||||
{
|
||||
customXor32KeyList.Add(byte.Parse(byteString, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
return customXor32KeyList.ToArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
this._settingsDictionary.TryGetValue(key, out var value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/ChatServer/ExDbConnector/appsettings.json
Normal file
38
src/ChatServer/ExDbConnector/appsettings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Fatal",
|
||||
"System": "Fatal",
|
||||
"Npgsql": "Information",
|
||||
"MUnique.OpenMU.Network.Connection": "Error",
|
||||
"MUnique": "Information"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "logs/log.txt",
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] [{SourceContext}] [{EventId}] {Message}{NewLine}{Exception}",
|
||||
"rollOnFileSizeLimit": true,
|
||||
"fileSizeLimitBytes": 4194304,
|
||||
"retainedFileCountLimit": 48,
|
||||
"rollingInterval": "Hour"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [ "FromLogContext" ],
|
||||
"Properties": {
|
||||
"Application": "MUnique.OpenMU.ChatServer.ExDbConnector"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user