Files
Ananta-ps/internal/logingate/server.go
T

304 lines
8.3 KiB
Go

package logingate
import (
"context"
"fmt"
"io"
"log"
"net"
"sync"
"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
}
func New(cfg *config.Config) *Server {
return &Server{
cfg: cfg,
}
}
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.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
}
}
go s.handleConnection(ctx, conn, port)
}
}
type Session struct {
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 protocol.WriteFrame(sess.conn, 0x09, payload, sess.sendCipher)
}
func (sess *Session) ReturnEmptyOk(invokeID int32, methodID uint32) error {
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 {
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 {
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 {
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,
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
_ = protocol.WriteFrame(conn, 0x03, frame.Payload, sess.sendCipher)
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 {
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.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
}
// Send SyncRoleList (35167428)
w := protocol.NewUxWriter()
w.U64(s.cfg.Player.Pid)
return sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes())
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 sess.NotifyData(protocol.MethodSyncRoleList, w.Bytes())
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, returning empty OK", pkt.MethodID)
return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID)
}
}
return nil
}