From bdce41eaa431db74193640e0eaf3907adbdb349f Mon Sep 17 00:00:00 2001 From: mkwcat Date: Sun, 22 Oct 2023 21:22:44 -0400 Subject: [PATCH] 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 --- common/friend_code.go | 4 +- database/login.go | 4 +- database/user.go | 11 +-- gcsp/login.go | 68 --------------- gcsp/profile.go | 62 -------------- gpcm/login.go | 33 +++++--- gpcm/main.go | 61 ++++++++------ gpcm/profile.go | 34 ++++---- master/heartbeat.go | 29 ++++++- matchmaking/main.go | 43 +++++++++- matchmaking/server.go | 187 ++++++++++++++++++++++++++++++++++++++---- 11 files changed, 321 insertions(+), 215 deletions(-) delete mode 100644 gcsp/login.go delete mode 100644 gcsp/profile.go diff --git a/common/friend_code.go b/common/friend_code.go index 71926a3..d0c9ff8 100644 --- a/common/friend_code.go +++ b/common/friend_code.go @@ -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:] } diff --git a/database/login.go b/database/login.go index 0118e70..f4dd389 100644 --- a/database/login.go +++ b/database/login.go @@ -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 diff --git a/database/user.go b/database/user.go index 189aedb..bd3f873 100644 --- a/database/user.go +++ b/database/user.go @@ -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) diff --git a/gcsp/login.go b/gcsp/login.go deleted file mode 100644 index c580d45..0000000 --- a/gcsp/login.go +++ /dev/null @@ -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"], - }, - }) -} diff --git a/gcsp/profile.go b/gcsp/profile.go deleted file mode 100644 index 6198a98..0000000 --- a/gcsp/profile.go +++ /dev/null @@ -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", - }, - }) -} diff --git a/gpcm/login.go b/gpcm/login.go index f990183..fe3547a 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -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 } diff --git a/gpcm/main.go b/gpcm/main.go index 824327b..556b58f 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -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 } diff --git a/gpcm/profile.go b/gpcm/profile.go index 713886e..9d7301a 100644 --- a/gpcm/profile.go +++ b/gpcm/profile.go @@ -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 } diff --git a/master/heartbeat.go b/master/heartbeat.go index 885639c..1ede2ee 100644 --- a/master/heartbeat.go +++ b/master/heartbeat.go @@ -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 + } + } } diff --git a/matchmaking/main.go b/matchmaking/main.go index 94caab9..296c464 100644 --- a/matchmaking/main.go +++ b/matchmaking/main.go @@ -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: diff --git a/matchmaking/server.go b/matchmaking/server.go index f85e5ff..d8ffee1 100644 --- a/matchmaking/server.go +++ b/matchmaking/server.go @@ -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