274 lines
No EOL
9.7 KiB
C#
274 lines
No EOL
9.7 KiB
C#
// WingsEmu
|
|
//
|
|
// Developed by NosWings Team
|
|
|
|
using System;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using LoginServer.Handlers;
|
|
using NetCoreServer;
|
|
using PhoenixLib.Logging;
|
|
using WingsEmu.Packets;
|
|
|
|
namespace LoginServer.Network
|
|
{
|
|
public class LoginClientSession : TcpSession
|
|
{
|
|
private readonly IPacketDeserializer _deserializer;
|
|
private readonly IGlobalPacketProcessor _loginHandlers;
|
|
|
|
|
|
public LoginClientSession(TcpServer server, IGlobalPacketProcessor globalPacketProcessor, IPacketDeserializer deserializer) : base(server)
|
|
{
|
|
_loginHandlers = globalPacketProcessor;
|
|
_deserializer = deserializer;
|
|
}
|
|
|
|
public string IpAddress { get; private set; }
|
|
|
|
public void SendPacket(string packet) => Send(NostaleLoginEncrypter.Encode(packet, Encoding.Default).ToArray());
|
|
|
|
protected override void OnConnected()
|
|
{
|
|
try
|
|
{
|
|
if (IsDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IsSocketDisposed)
|
|
{
|
|
Disconnect();
|
|
return;
|
|
}
|
|
|
|
if (Socket == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Socket?.RemoteEndPoint is IPEndPoint ip)
|
|
{
|
|
IpAddress = ip.Address.ToString();
|
|
Log.Info($"[LOGIN_SERVER_SESSION] CONNECT from {ip.Address}:{ip.Port}");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error("[LOGIN_SERVER_SESSION] OnConnected", e);
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
protected override void OnReceived(byte[] buffer, long offset, long size)
|
|
{
|
|
try
|
|
{
|
|
ReadOnlySpan<byte> payload = buffer.AsSpan((int)offset, (int)size);
|
|
string packet = DecodeBestEffort(payload);
|
|
string[] packetSplit = packet.Replace('^', ' ').Split(' ');
|
|
string packetHeader = packetSplit.FirstOrDefault() ?? string.Empty;
|
|
|
|
bool tracePackets = string.Equals(Environment.GetEnvironmentVariable("LOGIN_PACKET_TRACE"), "true", StringComparison.OrdinalIgnoreCase);
|
|
if (tracePackets)
|
|
{
|
|
string hexPrefix = BitConverter.ToString(payload.Slice(0, Math.Min(64, payload.Length)).ToArray());
|
|
string[] debugParts = packetSplit.Where(s => !string.IsNullOrWhiteSpace(s)).Take(7).ToArray();
|
|
string indexedParts = string.Join(" | ", debugParts.Select((v, i) => $"[{i}]='{v}'"));
|
|
Log.Info($"[LOGIN_PACKET_TRACE] header='{packetHeader}' size={payload.Length} raw_hex_prefix={hexPrefix} decoded='{packet}' parts={indexedParts}");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(packetHeader))
|
|
{
|
|
Disconnect();
|
|
return;
|
|
}
|
|
|
|
TriggerHandler(packetHeader.Replace("#", ""), packet, payload);
|
|
}
|
|
catch
|
|
{
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
|
|
private void TriggerHandler(string packetHeader, string packetString, ReadOnlySpan<byte> rawPayload)
|
|
{
|
|
if (IsDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
(IClientPacket typedPacket, Type packetType) = _deserializer.Deserialize(packetString, false);
|
|
|
|
if (packetType == typeof(UnresolvedPacket) && typedPacket != null)
|
|
{
|
|
// Fallback: normalize/force known login packet headers and retry deserialization.
|
|
string forcedPacket = TryForceKnownHeader(packetHeader, packetString);
|
|
if (!string.IsNullOrWhiteSpace(forcedPacket) && !string.Equals(forcedPacket, packetString, StringComparison.Ordinal))
|
|
{
|
|
(IClientPacket forcedTypedPacket, Type forcedPacketType) = _deserializer.Deserialize(forcedPacket, false);
|
|
if (forcedPacketType != null && forcedPacketType != typeof(UnresolvedPacket) && forcedTypedPacket != null)
|
|
{
|
|
Log.Warn($"[HEADER_FORCE] '{packetHeader}' -> '{forcedPacket.Split(' ')[0]}'");
|
|
_loginHandlers.Execute(this, forcedTypedPacket, forcedPacketType);
|
|
return;
|
|
}
|
|
}
|
|
|
|
string rawBase64 = Convert.ToBase64String(rawPayload.ToArray());
|
|
string rawHexPrefix = BitConverter.ToString(rawPayload.Slice(0, Math.Min(32, rawPayload.Length)).ToArray());
|
|
Log.Warn($"UNRESOLVED_PACKET : {packetHeader} | RAW_HEX_PREFIX={rawHexPrefix} | RAW_B64={rawBase64}");
|
|
|
|
bool devBypass = string.Equals(Environment.GetEnvironmentVariable("DEV_LOGIN_BYPASS"), "true", StringComparison.OrdinalIgnoreCase);
|
|
if (devBypass)
|
|
{
|
|
try
|
|
{
|
|
const string synthetic = "NoS0577 1 test test bypass bypass";
|
|
(IClientPacket fallbackPacket, Type fallbackType) = _deserializer.Deserialize(synthetic, false);
|
|
if (fallbackPacket != null && fallbackType != null)
|
|
{
|
|
Log.Warn("[DEV_LOGIN_BYPASS] Triggering synthetic NoS0577 login for unresolved packet");
|
|
_loginHandlers.Execute(this, fallbackPacket, fallbackType);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error("[DEV_LOGIN_BYPASS] Failed synthetic login", ex);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (packetType == null && typedPacket == null)
|
|
{
|
|
Log.Debug($"DESERIALIZATION_ERROR : {packetString}");
|
|
return;
|
|
}
|
|
|
|
_loginHandlers.Execute(this, typedPacket, packetType);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// disconnect if something unexpected happens
|
|
Log.Error("Handler Error SessionId: " + Id, ex);
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
private static string TryForceKnownHeader(string packetHeader, string packetString)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(packetHeader) || string.IsNullOrWhiteSpace(packetString))
|
|
{
|
|
return packetString;
|
|
}
|
|
|
|
string normalized = packetHeader.Replace("#", string.Empty).Trim();
|
|
string[] parts = packetString.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length == 0)
|
|
{
|
|
return packetString;
|
|
}
|
|
|
|
string forcedHeader = normalized;
|
|
if (normalized.Equals("nos0577", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
forcedHeader = "NoS0577";
|
|
}
|
|
else if (normalized.Equals("nos0575", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
forcedHeader = "NoS0575";
|
|
}
|
|
else if (normalized.Equals("nos0574", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
forcedHeader = "NoS0574";
|
|
}
|
|
|
|
parts[0] = forcedHeader;
|
|
return string.Join(' ', parts);
|
|
}
|
|
|
|
private static string DecodeBestEffort(ReadOnlySpan<byte> payload)
|
|
{
|
|
string decodedDefault = NostaleLoginDecrypter.Decode(payload);
|
|
if (LooksLikePacket(decodedDefault))
|
|
{
|
|
return decodedDefault;
|
|
}
|
|
|
|
// Fallback 1: raw text (some newer launchers may pre-handle login encoding)
|
|
string rawText = Encoding.Default.GetString(payload);
|
|
if (LooksLikePacket(rawText))
|
|
{
|
|
return rawText;
|
|
}
|
|
|
|
// Fallback 2: +15 shift only
|
|
string shiftedOnly = DecodeShiftOnly(payload);
|
|
if (LooksLikePacket(shiftedOnly))
|
|
{
|
|
return shiftedOnly;
|
|
}
|
|
|
|
// Fallback 3: xor only
|
|
string xorOnly = DecodeXorOnly(payload);
|
|
if (LooksLikePacket(xorOnly))
|
|
{
|
|
return xorOnly;
|
|
}
|
|
|
|
return decodedDefault;
|
|
}
|
|
|
|
private static bool LooksLikePacket(string text)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string header = text.Replace('^', ' ').Split(' ').FirstOrDefault() ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(header))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
header = header.Replace("#", string.Empty);
|
|
return header.All(c => char.IsLetterOrDigit(c));
|
|
}
|
|
|
|
private static string DecodeShiftOnly(ReadOnlySpan<byte> payload)
|
|
{
|
|
var sb = new StringBuilder(payload.Length);
|
|
foreach (byte b in payload)
|
|
{
|
|
sb.Append(Convert.ToChar(b > 14 ? b - 0xF : 0x100 - (0xF - b)));
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string DecodeXorOnly(ReadOnlySpan<byte> payload)
|
|
{
|
|
var sb = new StringBuilder(payload.Length);
|
|
foreach (byte b in payload)
|
|
{
|
|
sb.Append(Convert.ToChar(b ^ 0xC3));
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
protected override void OnError(SocketError error)
|
|
{
|
|
Disconnect();
|
|
}
|
|
}
|
|
} |