mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-04-18 23:37:24 -05:00
347 lines
8.4 KiB
Go
347 lines
8.4 KiB
Go
package gpcm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"wwfc/common"
|
|
"wwfc/database"
|
|
"wwfc/logging"
|
|
"wwfc/qr2"
|
|
|
|
"github.com/jackc/pgx/v4/pgxpool"
|
|
"github.com/logrusorgru/aurora/v3"
|
|
"github.com/sasha-s/go-deadlock"
|
|
)
|
|
|
|
var ServerName = "gpcm"
|
|
|
|
type GameSpySession struct {
|
|
ConnIndex uint64
|
|
RemoteAddr string
|
|
User database.User
|
|
ModuleName string
|
|
LoggedIn bool
|
|
DeviceAuthenticated bool
|
|
Challenge string
|
|
AuthToken string
|
|
LoginTicket string
|
|
SessionKey int32
|
|
|
|
LoginInfoSet bool
|
|
GameName string
|
|
GameCode string
|
|
Region byte
|
|
Language byte
|
|
InGameName string
|
|
ConsoleFriendCode uint64
|
|
DeviceId uint32
|
|
HostPlatform string
|
|
UnitCode byte
|
|
|
|
StatusSet bool
|
|
Status string
|
|
LocString string
|
|
FriendList []uint32
|
|
AuthFriendList []uint32
|
|
// For syncing with local GS SDK buddy list
|
|
RecvStatusFromList []uint32
|
|
|
|
QR2IP uint64
|
|
Reservation common.MatchCommandData
|
|
ReservationPID uint32
|
|
|
|
NeedsExploit bool
|
|
|
|
ReadBuffer []byte
|
|
WriteBuffer string
|
|
}
|
|
|
|
var (
|
|
ctx = context.Background()
|
|
pool *pgxpool.Pool
|
|
// I would use a sync.Map instead of the map mutex combo, but this performs better.
|
|
sessions = map[uint32]*GameSpySession{}
|
|
sessionsByConnIndex = map[uint64]*GameSpySession{}
|
|
mutex = deadlock.Mutex{}
|
|
|
|
allowDefaultDolphinKeys bool
|
|
)
|
|
|
|
func StartServer(reload bool) {
|
|
qr2.SetGPErrorCallback(KickPlayer)
|
|
|
|
// Get config
|
|
config := common.GetConfig()
|
|
|
|
// Start SQL
|
|
dbString := fmt.Sprintf("postgres://%s:%s@%s/%s", config.Username, config.Password, config.DatabaseAddress, config.DatabaseName)
|
|
dbConf, err := pgxpool.ParseConfig(dbString)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
pool, err = pgxpool.ConnectConfig(ctx, dbConf)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
database.UpdateTables(pool, ctx)
|
|
|
|
allowDefaultDolphinKeys = config.AllowDefaultDolphinKeys
|
|
|
|
if reload {
|
|
err := loadState()
|
|
if err != nil {
|
|
logging.Error("GPCM", "Failed to load state:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
logging.Notice("GPCM", "Loaded", aurora.Cyan(len(sessions)), "sessions")
|
|
}
|
|
}
|
|
|
|
func Shutdown() {
|
|
err := saveState()
|
|
if err != nil {
|
|
logging.Error("GPCM", "Failed to save state:", err)
|
|
}
|
|
logging.Notice("GPCM", "Saved", aurora.Cyan(len(sessions)), "sessions")
|
|
}
|
|
|
|
func CloseConnection(index uint64) {
|
|
mutex.Lock()
|
|
session := sessionsByConnIndex[index]
|
|
mutex.Unlock()
|
|
|
|
if session == nil {
|
|
logging.Error("GPCM", "Cannot find session for this connection index:", aurora.Cyan(index))
|
|
return
|
|
}
|
|
|
|
logging.Notice(session.ModuleName, "Connection closed")
|
|
|
|
if session.LoggedIn {
|
|
qr2.Logout(session.User.ProfileId)
|
|
if session.QR2IP != 0 {
|
|
qr2.ProcessGPStatusUpdate(session.User.ProfileId, session.QR2IP, "0")
|
|
}
|
|
session.sendLogoutStatus()
|
|
}
|
|
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
if session.LoggedIn {
|
|
session.LoggedIn = false
|
|
delete(sessions, session.User.ProfileId)
|
|
}
|
|
}
|
|
|
|
func NewConnection(index uint64, address string) {
|
|
session := &GameSpySession{
|
|
ConnIndex: index,
|
|
RemoteAddr: address,
|
|
User: database.User{},
|
|
ModuleName: "GPCM:" + address,
|
|
LoggedIn: false,
|
|
Challenge: common.RandomString(10),
|
|
StatusSet: false,
|
|
Status: "",
|
|
LocString: "",
|
|
FriendList: []uint32{},
|
|
AuthFriendList: []uint32{},
|
|
}
|
|
|
|
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
|
|
Command: "lc",
|
|
CommandValue: "1",
|
|
OtherValues: map[string]string{
|
|
"challenge": session.Challenge,
|
|
"id": "1",
|
|
},
|
|
})
|
|
common.SendPacket(ServerName, index, []byte(payload))
|
|
|
|
logging.Notice(session.ModuleName, "Connection established from", address)
|
|
|
|
mutex.Lock()
|
|
sessionsByConnIndex[index] = session
|
|
mutex.Unlock()
|
|
}
|
|
|
|
func HandlePacket(index uint64, data []byte) {
|
|
mutex.Lock()
|
|
session := sessionsByConnIndex[index]
|
|
mutex.Unlock()
|
|
|
|
if session == nil {
|
|
logging.Error("GPCM", "Cannot find session for this connection index:", aurora.Cyan(index))
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logging.Error(session.ModuleName, "Panic:", r)
|
|
}
|
|
}()
|
|
|
|
// Enforce maximum buffer size
|
|
length := len(session.ReadBuffer) + len(data)
|
|
if length > 0x4000 {
|
|
logging.Error(session.ModuleName, "Buffer overflow")
|
|
return
|
|
}
|
|
|
|
session.ReadBuffer = append(session.ReadBuffer, data...)
|
|
|
|
// Packets can be received in fragments, so make sure we're at the end of a packet
|
|
if string(session.ReadBuffer[max(0, length-7):length]) != `\final\` {
|
|
return
|
|
}
|
|
|
|
builder := strings.Builder{}
|
|
builder.Grow(length)
|
|
|
|
// Copy one rune at a time to enforce ASCII (rather than UTF-8)
|
|
for i := 0; i < length; i++ {
|
|
if session.ReadBuffer[i] == 0 {
|
|
logging.Error(session.ModuleName, "Null byte in packet")
|
|
logging.Error(session.ModuleName, "Raw data:", string(data))
|
|
session.replyError(ErrParse)
|
|
session.ReadBuffer = []byte{}
|
|
return
|
|
}
|
|
|
|
builder.WriteRune(rune(session.ReadBuffer[i]))
|
|
}
|
|
|
|
message := builder.String()
|
|
session.ReadBuffer = []byte{}
|
|
|
|
commands, err := common.ParseGameSpyMessage(message)
|
|
if err != nil {
|
|
logging.Error(session.ModuleName, "Error parsing message:", err.Error())
|
|
logging.Error(session.ModuleName, "Raw data:", message)
|
|
session.replyError(ErrParse)
|
|
return
|
|
}
|
|
|
|
// Commands must be handled in a certain order, not in the order supplied by the client
|
|
|
|
commands = session.handleCommand("ka", commands, func(command common.GameSpyCommand) {
|
|
common.SendPacket(ServerName, session.ConnIndex, []byte(`\ka\\final\`))
|
|
})
|
|
commands = session.handleCommand("login", commands, session.login)
|
|
commands = session.handleCommand("wl:exlogin", commands, session.exLogin)
|
|
commands = session.ignoreCommand("logout", commands)
|
|
|
|
if len(commands) != 0 && !session.LoggedIn {
|
|
logging.Error(session.ModuleName, "Attempt to run command before login:", aurora.Cyan(commands[0]))
|
|
session.replyError(ErrNotLoggedIn)
|
|
return
|
|
}
|
|
|
|
commands = session.handleCommand("wl:report", commands, session.handleWWFCReport)
|
|
commands = session.handleCommand("updatepro", commands, session.updateProfile)
|
|
commands = session.handleCommand("status", commands, session.setStatus)
|
|
commands = session.handleCommand("addbuddy", commands, session.addFriend)
|
|
commands = session.handleCommand("delbuddy", commands, session.removeFriend)
|
|
commands = session.handleCommand("authadd", commands, session.authAddFriend)
|
|
commands = session.handleCommand("bm", commands, session.bestieMessage)
|
|
commands = session.handleCommand("getprofile", commands, session.getProfile)
|
|
|
|
for _, command := range commands {
|
|
logging.Error(session.ModuleName, "Unknown command:", aurora.Cyan(command))
|
|
}
|
|
|
|
if session.WriteBuffer != "" {
|
|
data := []byte{}
|
|
logged := false
|
|
for c := 0; c < len(session.WriteBuffer); c++ {
|
|
if session.WriteBuffer[c] == 0x00 {
|
|
if !logged {
|
|
logging.Warn(session.ModuleName, "Non-char or null byte in response packet:", session.WriteBuffer)
|
|
logged = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
data = append(data, session.WriteBuffer[c])
|
|
}
|
|
|
|
common.SendPacket(ServerName, session.ConnIndex, data)
|
|
session.WriteBuffer = ""
|
|
}
|
|
}
|
|
|
|
func (g *GameSpySession) handleCommand(name string, commands []common.GameSpyCommand, handler func(command common.GameSpyCommand)) []common.GameSpyCommand {
|
|
var unhandled []common.GameSpyCommand
|
|
|
|
for _, command := range commands {
|
|
if command.Command != name {
|
|
unhandled = append(unhandled, command)
|
|
continue
|
|
}
|
|
|
|
logging.Info(g.ModuleName, "Command:", aurora.Yellow(command.Command))
|
|
handler(command)
|
|
}
|
|
|
|
return unhandled
|
|
}
|
|
|
|
func (g *GameSpySession) ignoreCommand(name string, commands []common.GameSpyCommand) []common.GameSpyCommand {
|
|
var unhandled []common.GameSpyCommand
|
|
|
|
for _, command := range commands {
|
|
if command.Command != name {
|
|
unhandled = append(unhandled, command)
|
|
}
|
|
}
|
|
|
|
return unhandled
|
|
}
|
|
|
|
func saveState() error {
|
|
file, err := os.OpenFile("state/gpcm_sessions.gob", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
encoder := gob.NewEncoder(file)
|
|
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
err = encoder.Encode(sessions)
|
|
file.Close()
|
|
return err
|
|
}
|
|
|
|
func loadState() error {
|
|
file, err := os.Open("state/gpcm_sessions.gob")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
decoder := gob.NewDecoder(file)
|
|
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
err = decoder.Decode(&sessions)
|
|
file.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, session := range sessions {
|
|
sessionsByConnIndex[session.ConnIndex] = session
|
|
}
|
|
|
|
return nil
|
|
}
|