Base for sending messages between clients

This commit is contained in:
mkwcat 2023-10-24 22:13:01 -04:00
parent b47fe639ae
commit c9b9835364
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
10 changed files with 321 additions and 58 deletions

View File

@ -3,12 +3,11 @@ package database
import (
"context"
"database/sql"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"strconv"
"wwfc/common"
"wwfc/logging"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
const (

View File

@ -3,11 +3,10 @@ package database
import (
"context"
"errors"
"math/rand"
"wwfc/common"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"math/rand"
"wwfc/common"
)
const (
@ -70,13 +69,14 @@ func deleteSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32) {
}
}
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) User {
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
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)
if err != nil {
panic(err)
return User{}, false
}
return user
user.ProfileId = profileId
return user, true
}

View File

@ -9,8 +9,11 @@ import (
"github.com/logrusorgru/aurora/v3"
"io"
"net"
"strconv"
"strings"
"time"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
)
@ -62,17 +65,20 @@ func StartServer() {
func handleRequest(conn net.Conn) {
defer conn.Close()
moduleName := "GCSP"
knownProfileId := uint32(0)
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Notice("GCSP", "Unable to set keepalive:", err.Error())
logging.Notice(moduleName, "Unable to set keepalive:", err.Error())
}
err = conn.(*net.TCPConn).SetKeepAlivePeriod(time.Hour * 1000)
if err != nil {
logging.Notice("GCSP", "Unable to set keepalive:", err.Error())
logging.Notice(moduleName, "Unable to set keepalive:", err.Error())
}
logging.Notice("GCSP", "Connection established from", conn.RemoteAddr().String())
logging.Notice(moduleName, "Connection established from", conn.RemoteAddr().String())
// Here we go into the listening loop
for {
@ -91,17 +97,97 @@ func handleRequest(conn net.Conn) {
}
for _, command := range commands {
logging.Notice("GCSP", "Command:", aurora.Yellow(command.Command).String())
logging.Notice(moduleName, "Command:", aurora.Yellow(command.Command).String())
switch command.Command {
case "ka":
conn.Write([]byte(`\ka\\final\`))
break
case "otherslist":
// This message needs to be sorted so we can't use a regular map
payload := `\otherslist\\oldone\\o\0\uniquenick\7me4ijr5sRMCJ3d9uhvh\final\`
conn.Write([]byte(payload))
strProfileId, ok := command.OtherValues["profileid"]
if !ok {
logging.Notice(moduleName, "Missing profileid in otherslist")
return
}
profileId, err := strconv.ParseUint(strProfileId, 10, 32)
if err != nil {
panic(err)
}
if knownProfileId == 0 {
knownProfileId = uint32(profileId)
moduleName = "GCSP:" + strconv.FormatUint(profileId, 10)
moduleName += "/" + common.CalcFriendCodeString(uint32(profileId), "RMCJ")
} else if uint32(profileId) != knownProfileId {
logging.Notice(moduleName, "WARN: Mismatched profile ID in otherslist:", aurora.Cyan(strProfileId).String())
}
conn.Write([]byte(handleOthersList(moduleName, uint32(profileId), command)))
break
}
}
}
}
func handleOthersList(moduleName string, profileId uint32, command common.GameSpyCommand) string {
empty := `\otherslist\\final\`
_, ok := command.OtherValues["sesskey"]
if !ok {
logging.Notice(moduleName, "Missing sesskey in otherslist")
return empty
}
numopids, ok := command.OtherValues["numopids"]
if !ok {
logging.Notice(moduleName, "Missing numopids in otherslist")
return empty
}
opids, ok := command.OtherValues["opids"]
if !ok {
logging.Notice(moduleName, "Missing opids in otherslist")
return empty
}
_, ok = command.OtherValues["gamename"]
if !ok {
logging.Notice(moduleName, "Missing gamename in otherslist")
return empty
}
numOpidsValue, err := strconv.Atoi(numopids)
if err != nil {
panic(err)
}
opidsSplit := strings.Split(opids, "|")
if len(opidsSplit) != numOpidsValue {
logging.Notice(moduleName, "Mismatch opids length with numopids:", aurora.Cyan(len(opidsSplit)).String(), "!=", aurora.Cyan(numOpidsValue).String())
return empty
}
payload := `\otherslist\`
for _, strOtherId := range opidsSplit {
otherId, err := strconv.ParseUint(strOtherId, 10, 32)
if err != nil {
panic(err)
}
// TODO: Perhaps this could be condensed into one database query
// Also TODO: Check if the players are actually friends
user, ok := database.GetProfile(pool, ctx, uint32(otherId))
if !ok {
logging.Notice(moduleName, "Other ID doesn't exist:", aurora.Cyan(strOtherId).String())
// If the profile doesn't exist then skip adding it
continue
}
payload += `\o\` + strconv.FormatUint(uint64(user.ProfileId), 10)
payload += `\uniquenick\` + user.UniqueNick
}
payload += `\oldone\\final\`
return payload
}

View File

@ -56,6 +56,18 @@ func Login(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, com
}
session.User = user
// Check to see if a session is already open with this profile ID
// TODO: Test this
mutex.Lock()
_, exists := sessions[session.User.ProfileId]
if exists {
mutex.Unlock()
// TODO: Return an error
log.Fatalf("Session with profile ID %u is already open", session.User.ProfileId)
}
sessions[session.User.ProfileId] = session
mutex.Unlock()
loginTicket := strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1)
// Now initiate the session
_ = database.CreateSession(pool, ctx, session.User.ProfileId, loginTicket)

View File

@ -9,6 +9,7 @@ import (
"github.com/logrusorgru/aurora/v3"
"io"
"net"
"sync"
"time"
"wwfc/common"
"wwfc/database"
@ -16,14 +17,19 @@ import (
)
type GameSpySession struct {
Conn net.Conn
User database.User
ModuleName string
LoggedIn bool
Status 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{}
mutex = sync.RWMutex{}
)
func StartServer() {
@ -64,15 +70,27 @@ func StartServer() {
}
}
func closeSession(session *GameSpySession) {
mutex.Lock()
defer mutex.Unlock()
session.Conn.Close()
if session.LoggedIn {
delete(sessions, session.User.ProfileId)
}
}
// Handles incoming requests.
func handleRequest(conn net.Conn) {
session := GameSpySession{
Conn: conn,
User: database.User{},
ModuleName: "GPCM",
LoggedIn: false,
Status: "",
}
defer conn.Close()
defer closeSession(&session)
// Set session ID and challenge
challenge := common.RandomString(10)
@ -118,7 +136,7 @@ func handleRequest(conn net.Conn) {
if session.LoggedIn == false {
if command.Command != "login" {
logging.Notice(session.ModuleName, "Attempt to run command before login!!!")
logging.Notice(session.ModuleName, "Attempt to run command before login!")
return
}
@ -139,24 +157,19 @@ func handleRequest(conn net.Conn) {
return
case "updatepro":
UpdateProfile(&session, pool, ctx, command)
updateProfile(&session, pool, ctx, command)
break
case "status":
logging.Notice(session.ModuleName, "statstring:", aurora.Cyan(command.OtherValues["statstring"]).String())
if command.OtherValues["locstring"] == "" {
logging.Notice(session.ModuleName, "locstring: (empty)")
} else {
logging.Notice(session.ModuleName, "locstring:", aurora.Cyan(command.OtherValues["locstring"]).String())
}
setStatus(&session, pool, ctx, command)
break
case "addbuddy":
AddFriend(&session, pool, ctx, command)
addFriend(&session, pool, ctx, command)
break
case "delbuddy":
RemoveFriend(&session, pool, ctx, command)
removeFriend(&session, pool, ctx, command)
break
}
}
@ -168,7 +181,7 @@ func handleRequest(conn net.Conn) {
break
case "getprofile":
payload := GetProfile(&session, pool, ctx, command)
payload := getProfile(&session, pool, ctx, command)
conn.Write([]byte(payload))
break
}

View File

@ -5,19 +5,23 @@ import (
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"strconv"
"strings"
"wwfc/common"
"wwfc/database"
"wwfc/logging"
)
func GetProfile(session *GameSpySession, 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, err := strconv.ParseInt(strProfileId, 10, 32)
if err != nil {
panic(err)
}
user := database.GetProfile(pool, ctx, uint32(profileId))
user, ok := database.GetProfile(pool, ctx, uint32(profileId))
if !ok {
return `\pi\\final\`
}
_ = common.RandomHexString(32)
return common.CreateGameSpyMessage(common.GameSpyCommand{
@ -28,7 +32,7 @@ func GetProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context
"nick": user.UniqueNick,
"userid": strconv.FormatInt(user.UserId, 10),
"email": user.Email,
"sig": "b126556e5ee62d4da9629dfad0f6b2a8",
"sig": common.RandomHexString(32),
"uniquenick": user.UniqueNick,
"firstname": user.FirstName,
"lastname": user.LastName,
@ -41,7 +45,7 @@ func GetProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context
})
}
func UpdateProfile(session *GameSpySession, 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 {
@ -55,7 +59,62 @@ func UpdateProfile(session *GameSpySession, pool *pgxpool.Pool, ctx context.Cont
database.UpdateUser(pool, ctx, firstName, lastName, session.User.UserId)
}
func AddFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
func setStatus(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
status := command.CommandValue
statstring, ok := command.OtherValues["statstring"]
if !ok {
logging.Notice(session.ModuleName, "Missing statstring")
statstring = ""
} else {
if statstring == "" {
logging.Notice(session.ModuleName, "statstring: (empty)")
} else {
logging.Notice(session.ModuleName, "statstring:", aurora.Cyan(statstring).String())
}
}
locstring, ok := command.OtherValues["locstring"]
if !ok {
logging.Notice(session.ModuleName, "Missing locstring")
locstring = ""
} else {
if locstring == "" {
logging.Notice(session.ModuleName, "locstring: (empty)")
} else {
logging.Notice(session.ModuleName, "locstring:", aurora.Cyan(locstring).String())
}
}
// Get the IP address for the status msg
var rawIP int
for i, s := range strings.Split(strings.Split(session.Conn.RemoteAddr().String(), ":")[0], ".") {
val, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
rawIP |= val << (24 - i*8)
}
// TODO: Check if this handles negative numbers correctly
ip := strconv.FormatInt(int64(int32(rawIP)), 10)
friendStatus := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "bm",
CommandValue: "100",
OtherValues: map[string]string{
"f": strconv.FormatUint(uint64(session.User.ProfileId), 10),
"msg": "|s|" + status + "|ss|" + statstring + "|ls|" + locstring + "|ip|" + ip + "|p|0|qm|0",
},
})
mutex.Lock()
session.Status = friendStatus
mutex.Unlock()
}
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 {
@ -67,17 +126,6 @@ func AddFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context,
// TODO
}
func RemoveFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
func removeFriend(session *GameSpySession, pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
// TODO
}
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

@ -13,8 +13,9 @@ import (
var (
// I would use a sync.Map instead of the map mutex combo, but this performs better.
sessions = map[uint32]*Session{}
mutex = sync.RWMutex{}
sessions = map[uint32]*Session{}
mutex = sync.RWMutex{}
masterConn net.PacketConn
)
const (
@ -41,6 +42,8 @@ func StartServer() {
panic(err)
}
masterConn = conn
// Close the listener when the application closes.
defer conn.Close()
logging.Notice("MASTER", "Listening on", address)

View File

@ -1,7 +1,11 @@
package master
import (
"encoding/binary"
"github.com/logrusorgru/aurora/v3"
"net"
"strconv"
"strings"
"time"
"wwfc/logging"
)
@ -14,6 +18,7 @@ const (
type Session struct {
SessionID uint32
Addr net.Addr
Challenge string
Authenticated bool
LastKeepAlive int64
@ -43,22 +48,23 @@ func setSessionData(sessionId uint32, payload map[string]string) Session {
Data: payload,
}
sessions[sessionId] = &data
return data
}
session.Data = payload
session.LastKeepAlive = time.Now().Unix()
return *session
}
// Get a copy of the list of servers
func GetSessionServers() []map[string]string {
mutex.Lock()
defer mutex.Unlock()
var servers []map[string]string
currentTime := time.Now().Unix()
var servers []map[string]string
mutex.Lock()
defer mutex.Unlock()
for _, session := range sessions {
if !session.Authenticated {
continue
@ -74,3 +80,55 @@ func GetSessionServers() []map[string]string {
return servers
}
func SendClientMessage(destIP string, message []byte) {
var rawIP int
for i, s := range strings.Split(strings.Split(destIP, ":")[0], ".") {
val, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
rawIP |= val << (24 - i*8)
}
// TODO: Check if this handles negative numbers correctly
destIPIntStr := strconv.FormatInt(int64(int32(rawIP)), 10)
currentTime := time.Now().Unix()
mutex.Lock()
// Find the session with the IP
for _, session := range sessions {
if !session.Authenticated {
continue
}
// If the last keep alive was over a minute ago then consider the server unreachable
if session.LastKeepAlive < currentTime-60 {
continue
}
if session.Data["publicip"] == destIPIntStr {
// Found the client, now send the message
payload := createResponseHeader(ClientMessageRequest, session.SessionID)
mutex.Unlock()
payload = append(payload, []byte{0, 0, 0, 0}...)
binary.BigEndian.PutUint32(payload[len(payload)-4:], uint32(time.Now().Unix()))
payload = append(payload, message...)
destIPAddr, err := net.ResolveUDPAddr("udp", destIP)
if err != nil {
panic(err)
}
logging.Notice("MASTER", "Sending message...")
masterConn.WriteTo(payload, destIPAddr)
return
}
}
mutex.Unlock()
logging.Notice("MASTER", "Could not find destination server")
}

View File

@ -3,6 +3,7 @@ package matchmaking
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
@ -95,20 +96,50 @@ func handleRequest(conn net.Conn) {
}
// Here we go into the listening loop
bufferSize := 0
packetSize := uint16(0)
buffer := []byte{}
for {
buffer := make([]byte, 1024)
_, err := bufio.NewReader(conn).Read(buffer)
if err != nil {
if errors.Is(err, io.EOF) {
// Client closed connection, terminate.
// Remove stale data and remake the buffer
buffer = append(buffer[packetSize:], make([]byte, 1024-packetSize)...)
bufferSize -= int(packetSize)
packetSize = 0
for {
if bufferSize > 2 {
packetSize = binary.BigEndian.Uint16(buffer[:2])
if packetSize < 3 {
logging.Notice(ModuleName, "Invalid packet size - terminating")
return
}
if bufferSize >= int(packetSize) {
// Got a full packet, break to continue
break
}
}
readSize, err := bufio.NewReader(conn).Read(buffer[bufferSize:])
if err != nil {
if errors.Is(err, io.EOF) {
logging.Notice(ModuleName, "Connection closed")
return
}
logging.Notice(ModuleName, "Connection error")
return
}
bufferSize += readSize
}
logging.Notice(ModuleName, "packet size:", aurora.Cyan(packetSize).String())
logging.Notice(ModuleName, "buffer size:", aurora.Cyan(bufferSize).String())
switch buffer[2] {
case ServerListRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_LIST_REQUEST").String())
handleServerListRequest(conn, buffer)
handleServerListRequest(conn, buffer[:packetSize])
break
case ServerInfoRequest:
@ -117,7 +148,7 @@ func handleRequest(conn net.Conn) {
case SendMessageRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SEND_MESSAGE_REQUEST").String())
// TODO
handleSendMessageRequest(conn, buffer[:packetSize])
break
case KeepaliveReply:

View File

@ -2,6 +2,7 @@ package matchmaking
import (
"encoding/binary"
"fmt"
"github.com/logrusorgru/aurora/v3"
"net"
"strconv"
@ -238,3 +239,15 @@ func handleServerListRequest(conn net.Conn, buffer []byte) {
// Write the encrypted reply
conn.Write(common.EncryptTypeX([]byte("9r3Rmy"), challenge, output))
}
func handleSendMessageRequest(conn net.Conn, buffer []byte) {
// Read destination IP from buffer
destIP := fmt.Sprintf("%d.%d.%d.%d:%d", buffer[3], buffer[4], buffer[5], buffer[6], binary.BigEndian.Uint16(buffer[7:9]))
logging.Notice(ModuleName, "Send message from", aurora.Cyan(conn.RemoteAddr()).String(), "to", aurora.Cyan(destIP).String())
// TODO: Perform basic packet verification
// TODO SECURITY: Check if the selected IP is actually online, or at least make sure it's not a local IP
master.SendClientMessage(destIP, buffer[9:])
}