383 lines
10 KiB
Go
383 lines
10 KiB
Go
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
|
|
}
|
|
}
|