package logingate import ( "context" "fmt" "io" "log" "net" "sync" "time" "ananta-ps/internal/config" "ananta-ps/pkg/crypto/chacha8" "ananta-ps/pkg/protocol" ) type Server struct { cfg *config.Config listeners []net.Listener cancelFunc context.CancelFunc wg sync.WaitGroup mu sync.Mutex connections map[net.Conn]struct{} } func New(cfg *config.Config) *Server { return &Server{ cfg: cfg, connections: make(map[net.Conn]struct{}), } } func (s *Server) Start() error { ctx, cancel := context.WithCancel(context.Background()) s.cancelFunc = cancel for _, port := range s.cfg.Network.LoginPorts { addr := fmt.Sprintf("%s:%d", s.cfg.Network.BindHost, port) ln, err := net.Listen("tcp", addr) if err != nil { s.Stop() return fmt.Errorf("failed to listen on login gate %s: %w", addr, err) } s.listeners = append(s.listeners, ln) log.Printf("[LOGIN-GATE] Listening on %s:%d", s.cfg.Network.AdvertisedHost, port) s.wg.Add(1) go s.acceptLoop(ctx, ln, port) } return nil } func (s *Server) Stop() error { if s.cancelFunc != nil { s.cancelFunc() } for _, ln := range s.listeners { _ = ln.Close() } s.mu.Lock() for conn := range s.connections { _ = conn.Close() } s.mu.Unlock() s.wg.Wait() return nil } func (s *Server) acceptLoop(ctx context.Context, ln net.Listener, port int) { defer s.wg.Done() for { conn, err := ln.Accept() if err != nil { select { case <-ctx.Done(): return default: log.Printf("[LOGIN-GATE] Accept error on port %d: %v", port, err) return } } s.mu.Lock() if ctx.Err() != nil { s.mu.Unlock() conn.Close() return } s.connections[conn] = struct{}{} s.wg.Add(1) s.mu.Unlock() go func() { defer s.wg.Done() defer func() { s.mu.Lock(); delete(s.connections, conn); s.mu.Unlock() }() s.handleConnection(ctx, conn, port) }() } } type Session struct { replyKind protocol.RpcPacketKind writeTimeout time.Duration conn net.Conn readCipher *chacha8.UxChaCha8 sendCipher *chacha8.UxChaCha8 mu sync.Mutex } func (sess *Session) SendPacket(packet *protocol.RpcPacket) error { sess.mu.Lock() defer sess.mu.Unlock() var payload []byte switch packet.Kind { case protocol.PacketKindInvoke: payload = protocol.BuildInvoke(packet.MethodID, packet.InvokeID, packet.Body) case protocol.PacketKindReturn: payload = protocol.BuildReturn(packet.MethodID, packet.InvokeID, packet.Error, packet.Body) case protocol.PacketKindNotify: payload = protocol.BuildNotify(packet.MethodID, packet.Body) } return sess.writeFrameLocked(0x09, payload) } func (sess *Session) writeFrameLocked(mode byte, payload []byte) error { timeout := sess.writeTimeout if timeout <= 0 { timeout = 15 * time.Second } if err := sess.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { return err } if err := protocol.WriteFrame(sess.conn, mode, payload, sess.sendCipher); err != nil { sess.conn.Close() return err } return nil } func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error { if sess.replyKind != protocol.PacketKindInvoke { return nil } return sess.SendPacket(&protocol.RpcPacket{ Kind: protocol.PacketKindReturn, InvokeID: invokeID, MethodID: methodID, Error: 0, Body: nil, }) } func (sess *Session) ReturnEmpty(invokeID int32, methodID uint32, errCode int32) error { if sess.replyKind != protocol.PacketKindInvoke { return nil } return sess.SendPacket(&protocol.RpcPacket{ Kind: protocol.PacketKindReturn, InvokeID: invokeID, MethodID: methodID, Error: errCode, Body: nil, }) } func (sess *Session) ReturnData(invokeID int32, methodID uint32, body []byte) error { if sess.replyKind != protocol.PacketKindInvoke { return nil } return sess.SendPacket(&protocol.RpcPacket{ Kind: protocol.PacketKindReturn, InvokeID: invokeID, MethodID: methodID, Error: 0, Body: body, }) } func (sess *Session) NotifyData(methodID uint32, body []byte) error { if methodID == 0 { return fmt.Errorf("unverified notification method") } return sess.SendPacket(&protocol.RpcPacket{ Kind: protocol.PacketKindNotify, MethodID: methodID, Body: body, }) } func (s *Server) handleConnection(ctx context.Context, conn net.Conn, port int) { defer conn.Close() log.Printf("[LOGIN-GATE] Client connected from %s on port %d", conn.RemoteAddr(), port) readCipher, writeCipher, err := protocol.HandleHandshake(conn) if err != nil { log.Printf("[LOGIN-GATE] Handshake error from %s: %v", conn.RemoteAddr(), err) return } sess := &Session{ conn: conn, writeTimeout: time.Duration(s.cfg.Network.WriteTimeoutMs) * time.Millisecond, readCipher: readCipher, sendCipher: writeCipher, } for { select { case <-ctx.Done(): return default: } frame, err := protocol.ReadFrame(conn, sess.readCipher) if err != nil { if err != io.EOF { log.Printf("[LOGIN-GATE] Connection %s read error: %v", conn.RemoteAddr(), err) } return } // Frame mode 0x03 is heartbeat if frame.Mode == 0x03 { // Echo heartbeat back sess.mu.Lock() err = sess.writeFrameLocked(0x03, frame.Payload) sess.mu.Unlock() if err != nil { return } continue } if frame.Mode != 0x09 { continue } packet := protocol.ParseRpcPacket(frame.Payload) if err := s.dispatch(sess, &packet); err != nil { log.Printf("[LOGIN-GATE] Dispatch error for method %d: %v", packet.MethodID, err) } } } func (s *Server) dispatch(sess *Session, pkt *protocol.RpcPacket) error { if pkt.Kind != protocol.PacketKindInvoke && pkt.Kind != protocol.PacketKindNotify { return nil } sess.replyKind = pkt.Kind defer func() { sess.replyKind = protocol.PacketKindUnknown }() switch pkt.MethodID { case protocol.MethodCheckVersion, protocol.MethodHasOnlinePlayer: return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) case protocol.MethodAskNewHotFixPatchLogin: return sess.ReturnEmpty(pkt.InvokeID, pkt.MethodID, int32(s.cfg.Client.NoMoreHotfixPatchError)) case protocol.MethodRequestPatchesCheckDataFromLogin: return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodePatchCheck()) case protocol.MethodRequestPatchesFromLogin: return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodeEmptyList()) case protocol.MethodAskUniSdkShareToken_Login, protocol.MethodAskUniSdkShareToken_Gate: w := protocol.NewUxWriter() w.UxString(s.cfg.Player.ShareToken) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodRequestFpPassToken: w := protocol.NewUxWriter() w.UxString(s.cfg.Player.FpPassToken) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodCheckAccount, protocol.MethodCheckAccountPassBy, protocol.MethodCheckAccountOpenId: body := protocol.EncodeCheckAccount( s.cfg.Player.AccountId, s.cfg.Player.UserName, s.cfg.Player.LoginToken, s.cfg.Player.DisplayName, s.cfg.Client.Aid, s.cfg.Client.ServerID, s.cfg.Player.Pid, ) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body) case protocol.MethodTryLogin: if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } return s.syncRoleList(sess) case protocol.MethodRequestCreateRoleEx: w := protocol.NewUxWriter() w.U64(s.cfg.Player.Pid) if err := sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()); err != nil { return err } return s.syncRoleList(sess) case protocol.MethodRequestEnterGame, protocol.MethodDebugRequestEnterGame: body := protocol.EncodeEnterGame( s.cfg.Client.Aid, s.cfg.Player.Pid, s.cfg.Network.AdvertisedHost, s.cfg.Network.LoginPorts[0], s.cfg.Player.LoginToken, s.cfg.Client.ServerID, s.cfg.Player.AccountId, ) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body) case protocol.MethodAskCloseConnectionToGate: w := protocol.NewUxWriter() w.U32(0) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodGetServerTime: if pkt.Kind == protocol.PacketKindInvoke { if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } } return sess.NotifyData(protocol.MethodSendServerTime, protocol.EncodeServerTime(0)) case protocol.MethodQuerySkey: w := protocol.NewUxWriter() w.UxString(s.cfg.Player.Skey) return sess.ReturnData(pkt.InvokeID, pkt.MethodID, w.Bytes()) case protocol.MethodGateLogin: log.Printf("[GATE] Authenticated Gate session registered for IMasterToClient S2C") if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } // 1. SendServerTime _ = sess.NotifyData(protocol.MethodSendServerTime, protocol.EncodeServerTime(0)) // 2. SyncGameSwitchToClient_3 (53940944) - Crucial for vehicles, NPCs, gadgets gameSwitches := protocol.BuildGameSwitchPayload() log.Printf("[GATE] SyncGameSwitchToClient_3 entries=%d body=%dB", len(protocol.GameSwitchCatalog), len(gameSwitches)) _ = sess.NotifyData(protocol.MethodSyncGameSwitchToClient_3, gameSwitches) // 3. SyncPlayerGameServerInfo (52737039) - Handoff to Game Server gameInfo := protocol.EncodeGameServerInfo(s.cfg.Network.AdvertisedHost, s.cfg.Network.GamePort, s.cfg.Player.GameToken) return sess.NotifyData(protocol.MethodSyncPlayerGameServerInfo, gameInfo) case protocol.MethodGetFriendApplicationListToMe, protocol.MethodGetAllChatGroupLatestMessage, protocol.MethodGetSimplePlayerInfoByPidList: return sess.ReturnData(pkt.InvokeID, pkt.MethodID, protocol.EncodeEmptyList()) case protocol.MethodReportNetworkQuality, protocol.MethodDavinciCode: // S2C Notify ignore return nil default: if pkt.Kind == protocol.PacketKindInvoke { log.Printf("[LOGIN-GATE] Unhandled Invoke MethodID %d, using contract fallback", pkt.MethodID) if body, ok := protocol.DefaultReturnBody(pkt.MethodID); ok { return sess.ReturnData(pkt.InvokeID, pkt.MethodID, body) } return sess.SendPacket(&protocol.RpcPacket{Kind: protocol.PacketKindReturn, InvokeID: pkt.InvokeID, MethodID: pkt.MethodID, Error: -1}) } } return nil } // Match the original server: unsolicited role lists are opt-in because the legacy pid // payload is not a list and can stall a cold login. func (s *Server) syncRoleList(sess *Session) error { switch s.cfg.Login.RoleListMode { case "pid": return sess.NotifyData(protocol.MethodSyncRoleList, protocol.NewUxWriter().U64(s.cfg.Player.Pid).Bytes()) case "list": return sess.NotifyData(protocol.MethodSyncRoleList, protocol.EncodeEmptyList()) default: return nil } }