More server list and general changes

- Server browser (Matchmaking) now returns a server list (although hardcoded)
- GPCM now keeps track of an actual thread-safe session
- Some int types were changed to reflect the actual size
- Improvements made to logging in many places
This commit is contained in:
mkwcat
2023-10-22 21:22:44 -04:00
parent ecf8fcf32c
commit bdce41eaa4
11 changed files with 321 additions and 215 deletions

View File

@@ -23,10 +23,10 @@ func CalcFriendCode(pid uint32, gameId string) uint64 {
}
func CalcFriendCodeString(pid uint32, gameId string) string {
return GetFriendCodeString(CalcFriendCode(pid, gameId))
return GetRawFriendCodeString(CalcFriendCode(pid, gameId))
}
func GetFriendCodeString(fc uint64) string {
func GetRawFriendCodeString(fc uint64) string {
s := fmt.Sprintf("%012d", fc)
return s[len(s)-12:len(s)-8] + "-" + s[len(s)-8:len(s)-4] + "-" + s[len(s)-4:]
}

View File

@@ -107,14 +107,14 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string)
// Create the GPCM account
user.CreateUser(pool, ctx)
logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)).String(), aurora.Cyan(user.GsbrCode).String(), "-", aurora.Cyan(strconv.FormatInt(user.ProfileId, 10)).String())
logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)).String(), aurora.Cyan(user.GsbrCode).String(), "-", aurora.Cyan(strconv.FormatInt(int64(user.ProfileId), 10)).String())
} else {
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId)
if err != nil {
panic(err)
}
logging.Notice("DATABASE", "Log in GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)).String(), aurora.Cyan(user.GsbrCode).String(), "-", aurora.Cyan(strconv.FormatInt(user.ProfileId, 10)).String())
logging.Notice("DATABASE", "Log in GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)).String(), aurora.Cyan(user.GsbrCode).String(), "-", aurora.Cyan(strconv.FormatInt(int64(user.ProfileId), 10)).String())
}
return user, true

View File

@@ -21,7 +21,7 @@ const (
)
type User struct {
ProfileId int64
ProfileId uint32
UserId int64
GsbrCode string
Password string
@@ -53,10 +53,7 @@ func UpdateUser(pool *pgxpool.Pool, ctx context.Context, firstName string, lastN
return user
}
func CreateSession(pool *pgxpool.Pool, ctx context.Context, profileId int64, loginTicket string) string {
// Delete session first.
deleteSession(pool, ctx, profileId)
func CreateSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32, loginTicket string) string {
sessionKey := common.RandomString(8)
_, err := pool.Exec(ctx, CreateUserSession, sessionKey, profileId, loginTicket)
if err != nil {
@@ -66,14 +63,14 @@ func CreateSession(pool *pgxpool.Pool, ctx context.Context, profileId int64, log
return sessionKey
}
func deleteSession(pool *pgxpool.Pool, ctx context.Context, profileId int64) {
func deleteSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32) {
_, err := pool.Exec(ctx, DeleteUserSession, profileId)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
panic(err)
}
}
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId int64) User {
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) User {
user := User{}
row := pool.QueryRow(ctx, GetUser, profileId)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName)

View File

@@ -1,68 +0,0 @@
package gcsp
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"log"
"strconv"
"strings"
"wwfc/common"
"wwfc/database"
"github.com/jackc/pgx/v4/pgxpool"
)
func generateResponse(gpcmChallenge, nasChallenge, authToken, clientChallenge string) string {
hasher := md5.New()
hasher.Write([]byte(nasChallenge))
str := hex.EncodeToString(hasher.Sum(nil))
str += " "
str += authToken
str += clientChallenge
str += gpcmChallenge
str += hex.EncodeToString(hasher.Sum(nil))
_hasher := md5.New()
_hasher.Write([]byte(str))
return hex.EncodeToString(_hasher.Sum(nil))
}
func generateProof(gpcmChallenge, nasChallenge, authToken, clientChallenge string) string {
return generateResponse(clientChallenge, nasChallenge, authToken, gpcmChallenge)
}
func login(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand, challenge string) string {
// TODO: Validate login token with one in database
authToken := command.OtherValues["authtoken"]
response := generateResponse(challenge, "0qUekMb4", authToken, command.OtherValues["challenge"])
if response != command.OtherValues["response"] {
log.Fatalf("i hate my life")
}
proof := generateProof(challenge, "0qUekMb4", command.OtherValues["authtoken"], command.OtherValues["challenge"])
// Perform the login with the database.
// TODO: Check valid result
user, _ := database.LoginUserToGPCM(pool, ctx, authToken)
loginTicket := strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1)
// TODO: Remove in favour of proper thread safe holding
userId = user.UserId
// Now initiate the session
_ = database.CreateSession(pool, ctx, user.ProfileId, loginTicket)
return common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
CommandValue: "2",
OtherValues: map[string]string{
"sesskey": "199714190",
"proof": proof,
"userid": strconv.FormatInt(user.UserId, 10),
"profileid": strconv.FormatInt(user.ProfileId, 10),
"uniquenick": user.UniqueNick,
"lt": loginTicket,
"id": command.OtherValues["id"],
},
})
}

View File

@@ -1,62 +0,0 @@
package gcsp
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"strconv"
"wwfc/common"
"wwfc/database"
)
func getProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string {
strProfileId := command.OtherValues["profileid"]
profileId, _ := strconv.ParseInt(strProfileId, 10, 0)
user := database.GetProfile(pool, ctx, profileId)
_ = common.RandomHexString(32)
return common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "pi",
CommandValue: "",
OtherValues: map[string]string{
"profileid": command.OtherValues["profileid"],
"nick": user.UniqueNick,
"userid": strconv.FormatInt(user.UserId, 10),
"email": user.Email,
"sig": "b126556e5ee62d4da9629dfad0f6b2a8",
"uniquenick": user.UniqueNick,
"firstname": user.FirstName,
"lastname": user.LastName,
"pid": "11",
"lon": "0.000000",
"lat": "0.000000",
"loc": "",
"id": command.OtherValues["id"],
},
})
}
func updateProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
var firstName string
var lastName string
if v, ok := command.OtherValues["firstname"]; ok {
firstName = v
}
if v, ok := command.OtherValues["lastname"]; ok {
lastName = v
}
database.UpdateUser(pool, ctx, firstName, lastName, userId)
}
func createStatus() string {
return common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "bm",
CommandValue: "100",
OtherValues: map[string]string{
"f": "5",
"msg": "|s|0|ss|Offline",
},
})
}

View File

@@ -32,24 +32,37 @@ func generateProof(gpcmChallenge, nasChallenge, authToken, clientChallenge strin
return generateResponse(clientChallenge, nasChallenge, authToken, gpcmChallenge)
}
func login(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand, challenge string) string {
func Login(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand, challenge string) (string, bool) {
if session.LoggedIn {
log.Fatalf("Attempt to login twice")
}
// TODO: Validate login token with one in database
authToken := command.OtherValues["authtoken"]
response := generateResponse(challenge, "0qUekMb4", authToken, command.OtherValues["challenge"])
if response != command.OtherValues["response"] {
log.Fatalf("i hate my life")
// TODO: Return an error
log.Fatalf("response mismatch")
}
proof := generateProof(challenge, "0qUekMb4", command.OtherValues["authtoken"], command.OtherValues["challenge"])
// Perform the login with the database.
// TODO: Check valid result
user, _ := database.LoginUserToGPCM(pool, ctx, authToken)
user, ok := database.LoginUserToGPCM(pool, ctx, authToken)
if !ok {
// TODO: Return an error
log.Fatalf("GPCM login error")
}
session.User = user
loginTicket := strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1)
// TODO: Remove in favour of proper thread safe holding
userId = user.UserId
// Now initiate the session
_ = database.CreateSession(pool, ctx, user.ProfileId, loginTicket)
_ = database.CreateSession(pool, ctx, session.User.ProfileId, loginTicket)
session.LoggedIn = true
session.ModuleName += ":" + strconv.FormatInt(int64(session.User.ProfileId), 10)
session.ModuleName += "/" + common.CalcFriendCodeString(session.User.ProfileId, "RMCJ")
return common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",
@@ -57,11 +70,11 @@ func login(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyComman
OtherValues: map[string]string{
"sesskey": "199714190",
"proof": proof,
"userid": strconv.FormatInt(user.UserId, 10),
"profileid": strconv.FormatInt(user.ProfileId, 10),
"uniquenick": user.UniqueNick,
"userid": strconv.FormatInt(session.User.UserId, 10),
"profileid": strconv.FormatInt(int64(session.User.ProfileId), 10),
"uniquenick": session.User.UniqueNick,
"lt": loginTicket,
"id": command.OtherValues["id"],
},
})
}), true
}

View File

@@ -11,13 +11,19 @@ import (
"net"
"time"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
)
type GameSpySession struct {
User database.User
ModuleName string
LoggedIn bool
}
var (
ctx = context.Background()
pool *pgxpool.Pool
userId int64
ctx = context.Background()
pool *pgxpool.Pool
)
func StartServer() {
@@ -60,6 +66,12 @@ func StartServer() {
// Handles incoming requests.
func handleRequest(conn net.Conn) {
session := GameSpySession{
User: database.User{},
ModuleName: "GPCM",
LoggedIn: false,
}
defer conn.Close()
// Set session ID and challenge
@@ -67,19 +79,17 @@ func handleRequest(conn net.Conn) {
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Notice("GPCM", "Unable to set keepalive:", err.Error())
logging.Notice(session.ModuleName, "Unable to set keepalive:", err.Error())
}
err = conn.(*net.TCPConn).SetKeepAlivePeriod(time.Hour * 1000)
if err != nil {
logging.Notice("GPCM", "Unable to set keepalive:", err.Error())
logging.Notice(session.ModuleName, "Unable to set keepalive:", err.Error())
}
conn.Write([]byte(fmt.Sprintf(`\lc\1\challenge\%s\id\1\final\`, challenge)))
logging.Notice("GPCM", "Connection established from", conn.RemoteAddr().String())
loggedIn := false
logging.Notice(session.ModuleName, "Connection established from", conn.RemoteAddr().String())
// Here we go into the listening loop
for {
@@ -88,32 +98,31 @@ func handleRequest(conn net.Conn) {
if err != nil {
if errors.Is(err, io.EOF) {
// Client closed connection, terminate.
logging.Notice("GPCM", "Client closed connection")
logging.Notice(session.ModuleName, "Client closed connection")
return
}
logging.Notice(session.ModuleName, "Connection lost")
return
}
commands, err := common.ParseGameSpyMessage(string(buffer))
if err != nil {
logging.Notice("GPCM", "Error parsing message:", err.Error())
logging.Notice("GPCM", "Raw data:", string(buffer))
logging.Notice(session.ModuleName, "Error parsing message:", err.Error())
logging.Notice(session.ModuleName, "Raw data:", string(buffer))
return
}
for _, command := range commands {
logging.Notice("GPCM", "Command:", aurora.Yellow(command.Command).String())
logging.Notice(session.ModuleName, "Command:", aurora.Yellow(command.Command).String())
if loggedIn == false {
if session.LoggedIn == false {
if command.Command != "login" {
logging.Notice("GPCM", "Attempt to run command before login!!!")
logging.Notice(session.ModuleName, "Attempt to run command before login!!!")
return
}
payload := login(pool, ctx, command, challenge)
if userId != 0 {
loggedIn = true
}
payload, _ := Login(&session, pool, ctx, command, challenge)
conn.Write([]byte(payload))
}
}
@@ -130,24 +139,24 @@ func handleRequest(conn net.Conn) {
return
case "updatepro":
updateProfile(pool, ctx, command)
UpdateProfile(&session, pool, ctx, command)
break
case "status":
logging.Notice("GPCM", "statstring:", aurora.Cyan(command.OtherValues["statstring"]).String())
logging.Notice(session.ModuleName, "statstring:", aurora.Cyan(command.OtherValues["statstring"]).String())
if command.OtherValues["locstring"] == "" {
logging.Notice("GPCM", "locstring: (empty)")
logging.Notice(session.ModuleName, "locstring: (empty)")
} else {
logging.Notice("GPCM", "locstring:", aurora.Cyan(command.OtherValues["locstring"]).String())
logging.Notice(session.ModuleName, "locstring:", aurora.Cyan(command.OtherValues["locstring"]).String())
}
break
case "addbuddy":
addFriend(pool, ctx, command)
AddFriend(&session, pool, ctx, command)
break
case "delbuddy":
removeFriend(pool, ctx, command)
RemoveFriend(&session, pool, ctx, command)
break
}
}
@@ -159,7 +168,7 @@ func handleRequest(conn net.Conn) {
break
case "getprofile":
payload := getProfile(pool, ctx, command)
payload := GetProfile(&session, pool, ctx, command)
conn.Write([]byte(payload))
break
}

View File

@@ -2,20 +2,22 @@ package gpcm
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"strconv"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
func getProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string {
func GetProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string {
strProfileId := command.OtherValues["profileid"]
profileId, _ := strconv.ParseInt(strProfileId, 10, 0)
profileId, err := strconv.ParseInt(strProfileId, 10, 32)
if err != nil {
panic(err)
}
user := database.GetProfile(pool, ctx, profileId)
user := database.GetProfile(pool, ctx, uint32(profileId))
_ = common.RandomHexString(32)
return common.CreateGameSpyMessage(common.GameSpyCommand{
@@ -39,7 +41,7 @@ func getProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyC
})
}
func updateProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
func UpdateProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
var firstName string
var lastName string
if v, ok := command.OtherValues["firstname"]; ok {
@@ -50,24 +52,22 @@ func updateProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameS
lastName = v
}
database.UpdateUser(pool, ctx, firstName, lastName, userId)
database.UpdateUser(pool, ctx, firstName, lastName, session.User.UserId)
}
func addFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
profileid := command.OtherValues["newprofileid"]
profileid_int, err := strconv.ParseUint(profileid, 10, 32)
func AddFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
strProfileId := command.OtherValues["newprofileid"]
profileId, err := strconv.ParseUint(strProfileId, 10, 32)
if err != nil {
logging.Notice("GPCM", "Error parsing profileid:", err.Error())
return
panic(err)
}
fc := common.CalcFriendCodeString(uint32(profileid_int), "RMCJ")
logging.Notice("GPCM", "Add friend:", aurora.Cyan(profileid).String(), aurora.Cyan(fc).String())
fc := common.CalcFriendCodeString(uint32(profileId), "RMCJ")
logging.Notice(session.ModuleName, "Add friend:", aurora.Cyan(strProfileId).String(), aurora.Cyan(fc).String())
// TODO
}
func removeFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
func RemoveFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
// TODO
}

View File

@@ -2,14 +2,18 @@ package master
import (
"encoding/binary"
"github.com/logrusorgru/aurora/v3"
"net"
"strconv"
"strings"
"wwfc/logging"
)
func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) {
sessionId := binary.BigEndian.Uint32(buffer[1:5])
logging.Notice("AVAILABLE", "Received heartbeat from", addr.String())
moduleName := "AVAILABLE:" + strconv.FormatInt(int64(sessionId), 10)
logging.Notice(moduleName, "Received heartbeat from", aurora.Cyan(addr).String())
values := strings.Split(string(buffer[5:]), "\u0000")
payload := map[string]string{}
@@ -19,12 +23,31 @@ func heartbeat(conn net.PacketConn, addr net.Addr, buffer []byte) {
}
payload[values[i]] = values[i+1]
logging.Notice(moduleName, aurora.Cyan(values[i]).String()+":", aurora.Cyan(values[i+1]).String())
}
publicip, ok := payload["publicip"]
if ok && publicip != "0" {
if !ok || publicip == "0" {
sendChallenge(conn, addr, sessionId)
return
}
sendChallenge(conn, addr, sessionId)
// TODO: Check if the client is registered
statechanged, ok := payload["statechanged"]
if ok {
if statechanged == "1" {
// statechanged is 1 and publicip is not 0
// TODO: This would be a good place to run the server->client message exploit
// for DNS patcher games that require code patches. The status code should be
// set to 5 at this point, which is required.
logging.Notice(moduleName, "Client server update")
// Fall through
}
if statechanged == "2" {
logging.Notice(moduleName, "Client server shutdown")
return
}
}
}

View File

@@ -22,8 +22,23 @@ var (
)
const (
ServerList = iota
ModuleName = "MATCHMAKING"
// Requests sent from the client
ServerListRequest = 0x00
ServerInfoRequest = 0x01
SendMessageRequest = 0x02
KeepaliveReply = 0x03
MapLoopRequest = 0x04
PlayerSearchRequest = 0x05
// Requests sent from the server to the client
PushKeysMessage = 0x01
PushServerMessage = 0x02
KeepaliveMessage = 0x03
DeleteServerMessage = 0x04
MapLoopMessage = 0x05
PlayerSearchMessage = 0x06
)
func StartServer() {
@@ -91,10 +106,30 @@ func handleRequest(conn net.Conn) {
}
switch buffer[2] {
case ServerList:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_LIST").String())
case ServerListRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_LIST_REQUEST").String())
handleServerListRequest(conn, buffer)
break
serverList(conn, buffer)
case ServerInfoRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_INFO_REQUEST").String())
break
case SendMessageRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SEND_MESSAGE_REQUEST").String())
// TODO
break
case KeepaliveReply:
logging.Notice(ModuleName, "Command:", aurora.Yellow("KEEPALIVE_REPLY").String())
break
case MapLoopRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("MAPLOOP_REQUEST").String())
break
case PlayerSearchRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("PLAYER_SEARCH_REQUEST").String())
break
default:

View File

@@ -10,6 +10,55 @@ import (
"wwfc/logging"
)
const (
// Server flags
UnsolicitedUDPFlag = 1 << 0 // 0x01 / 1
PrivateIPFlag = 1 << 1 // 0x02 / 2
ConnectNegotiateFlag = 1 << 2 // 0x04 / 4
ICMPIPFlag = 1 << 3 // 0x08 / 8
NonstandardPortFlag = 1 << 4 // 0x10 / 16
NonstandardPrivatePortFlag = 1 << 5 // 0x20 / 32
HasKeysFlag = 1 << 6 // 0x40 / 64
HasFullRulesFlag = 1 << 7 // 0x80 / 128
// Key Type list
KeyTypeString = 0x00
KeyTypeByte = 0x01
KeyTypeShort = 0x02
// Options for ServerListRequest
NoServerListOption = 1 << 1 // 0x02 / 2
PushUpdatesOption = 1 << 2 // 0x04 / 4
AlternateSourceIPOption = 1 << 3 // 0x08 / 8
NoListCacheOption = 1 << 6 // 0x40 / 64
LimitResultCountOption = 1 << 7 // 0x80 / 128
)
func FindServers(gueryGame string, filter string) ([]map[string]string, error) {
// TODO
// This is a temporary hardcoded server
server := map[string]string{
"localip0": "192.168.1.100",
"localport": "64174",
"natneg": "1",
"gamename": "mariokartwii",
"publicip": "2130706433", // 127.0.0.1
"publicport": "64174",
"numplayers": "0",
"maxplayers": "11",
"dwc_pid": "27",
"dwc_mtype": "2",
"dwc_mver": "90",
"dwc_eval": "1",
"dwc_groupid": "100000027",
"dwc_hoststate": "2",
"dwc_suspend": "1",
}
return []map[string]string{server}, nil
}
func popString(buffer []byte, index int) (string, int) {
str := common.GetString(buffer[index:])
return str, index + len(str) + 1
@@ -23,20 +72,14 @@ func popUint32(buffer []byte, index int) (uint32, int) {
return binary.BigEndian.Uint32(buffer[index:]), index + 4
}
func serverList(conn net.Conn, buffer []byte) {
const (
FlagNoServerList = 1 << 1 // 0x02
FlagAlternateSourceIP = 1 << 3 // 0x08
FlagLimitResultCount = 1 << 7 // 0x80
)
func handleServerListRequest(conn net.Conn, buffer []byte) {
index := 9
queryGame, index := popString(buffer, index)
gameName, index := popString(buffer, index)
challenge, index := popBytes(buffer, index, 8)
filter, index := popString(buffer, index)
fields, index := popString(buffer, index)
flags, index := popUint32(buffer, index)
options, index := popUint32(buffer, index)
logging.Notice(ModuleName, "queryGame:", aurora.Cyan(queryGame).String(), "- gameName:", aurora.Cyan(gameName).String(), "- filter:", aurora.Cyan(filter).String(), "- fields:", aurora.Cyan(fields).String())
@@ -59,7 +102,7 @@ func serverList(conn net.Conn, buffer []byte) {
fieldList = append(fieldList, field)
}
if flags&FlagNoServerList != 0 || len(fieldList) == 0 {
if options&NoServerListOption != 0 || len(fieldList) == 0 {
// The client requests its own public IP and game port
logging.Notice(ModuleName, "Reply without server list", aurora.Cyan(conn.RemoteAddr()).String())
@@ -82,15 +125,131 @@ func serverList(conn net.Conn, buffer []byte) {
output = append(output, byte(len(fieldList)))
for _, field := range fieldList {
output = append(output, 0x00) // Value?
output = append(output, []byte(field)...)
output = append(output, 0x00) // String end
output = append(output, 0x00) // Key type (0 = string, 1 = byte, 2 = short)
output = append(output, []byte(field)...) // String
output = append(output, 0x00) // String terminator
}
output = append(output, 0x00) // Zero length string to end the list
// TODO: Send server list here
servers, err := FindServers(queryGame, filter)
if err != nil {
panic(err)
}
// Server with no flags and -1 tells the client the message has ended
for _, server := range servers {
var flags byte
var flagsBuffer []byte
// Server will always have keys
flags |= HasKeysFlag
var natneg string
var exists bool
if natneg, exists = server["natneg"]; exists && natneg != "0" {
flags |= ConnectNegotiateFlag
}
var publicip string
if publicip, exists = server["publicip"]; !exists {
logging.Notice(ModuleName, "Server exists without public IP")
continue
}
ip, err := strconv.ParseUint(publicip, 10, 32)
if err != nil {
logging.Notice(ModuleName, "Server has invalid public IP value")
}
flagsBuffer = binary.BigEndian.AppendUint32(flagsBuffer, uint32(ip))
var port string
port, exists = server["publicport"]
if !exists {
// Fall back to local port if public port doesn't exist
if port, exists = server["localport"]; !exists {
logging.Notice(ModuleName, "Server exists without port")
continue
}
}
portValue, err := strconv.ParseUint(port, 10, 16)
if err != nil {
logging.Notice(ModuleName, "Server has invalid port value")
continue
}
flags |= NonstandardPortFlag
flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16(portValue))
// Use the first local IP if it exists, this is used to skip natneg if multiple players are on the same network
if localip0, exists := server["localip0"]; exists {
flags |= PrivateIPFlag
// localip is written like "192.168.255.255" for example, so it needs to be parsed
ipSplit := strings.Split(localip0, ".")
if len(ipSplit) != 4 {
logging.Notice(ModuleName, "Server has invalid local IP")
continue
}
err = nil
for _, s := range ipSplit {
val, err := strconv.ParseUint(s, 10, 8)
if err != nil {
break
}
flagsBuffer = append(flagsBuffer, byte(val))
}
if err != nil {
logging.Notice(ModuleName, "Server has invalid local IP value")
continue
}
}
if localport, exists := server["localport"]; exists {
portValue, err = strconv.ParseUint(localport, 10, 16)
if err != nil {
logging.Notice(ModuleName, "Server has invalid local port value")
continue
}
flags |= NonstandardPrivatePortFlag
flagsBuffer = binary.BigEndian.AppendUint16(flagsBuffer, uint16(portValue))
}
// Just a dummy IP? This is taken from dwc_network_server_emulator
// TODO: Check if this is actually needed
flags |= ICMPIPFlag
flagsBuffer = append(flagsBuffer, []byte{0, 0, 0, 0}...)
// Finally, write the server buffer to the output
output = append(output, flags)
output = append(output, flagsBuffer...)
if (flags & HasKeysFlag) == 0 {
// Server does not have keys, so skip them
logging.Notice(ModuleName, "Wrote server without keys")
continue
}
// Add the requested fields
for _, field := range fieldList {
output = append(output, 0xff)
if str, exists := server[field]; exists {
output = append(output, []byte(str)...)
}
// Add null terminator so the string will be empty if the field doesn't exist
output = append(output, 0x00)
}
logging.Notice(ModuleName, "Wrote server with keys")
}
// Server with 0 flags and IP of 0xffffffff terminates the list
output = append(output, []byte{0x00, 0xff, 0xff, 0xff, 0xff}...)
// Write the encrypted reply