// 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 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 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 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 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 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(); } } }