Rename matchmaking to serverbrowser

This commit is contained in:
mkwcat
2023-11-01 07:43:41 -04:00
parent 2fb7d43c31
commit 3eda89331e
4 changed files with 5 additions and 5 deletions

77
serverbrowser/filter.go Normal file
View File

@@ -0,0 +1,77 @@
package serverbrowser
import (
"github.com/logrusorgru/aurora/v3"
"regexp"
"strconv"
"wwfc/logging"
)
// TODO: Even if we don't use it in the end, we could still implement parsing the filter string.
// DWC makes requests in the following two formats:
// Matching: dwc_mver = %d and dwc_pid != %u and maxplayers = %d and numplayers < %d and dwc_mtype = %d and dwc_hoststate = %u and dwc_suspend = %u and (%s)
// ...OR
// Self Lookup: dwc_pid = %u
// Example: dwc_mver = 90 and dwc_pid != 43 and maxplayers = 11 and numplayers < 11 and dwc_mtype = 0 and dwc_hoststate = 0 and dwc_suspend = 0 and (rk = 'vs' and ev >= 4250 and ev <= 5750 and p = 0)
var (
regexSelfLookup = regexp.MustCompile(`^dwc_pid = (\d{1,10})$`)
regexMatchmaking = regexp.MustCompile(`^dwc_mver = -?(\d{1,10}) and dwc_pid != (\d{1,10}) and maxplayers = -?(\d{1,10}) and numplayers < -?(\d{1,10}) and dwc_mtype = -?(\d{1,10}) and dwc_hoststate = (\d{1,10}) and dwc_suspend = (\d{1,10}) and \((.*)\)$`)
)
func FilterServers(servers []map[string]string, queryGame string, filter string) []map[string]string {
if match := regexSelfLookup.FindStringSubmatch(filter); match != nil {
dwc_pid := match[1]
// Search for where the profile ID matches
for _, server := range servers {
if server["dwc_pid"] == dwc_pid {
logging.Info(ModuleName, "Self lookup from", aurora.Cyan(dwc_pid), "ok")
return []map[string]string{server}
}
}
logging.Error(ModuleName, "Could not find server with dwc_pid", aurora.Cyan(dwc_pid))
return []map[string]string{}
}
if match := regexMatchmaking.FindStringSubmatch(filter); match != nil {
dwc_mver := match[1]
dwc_pid := match[2]
maxplayers := match[3]
numplayers, err := strconv.ParseInt(match[4], 10, 32)
if err != nil {
logging.Error(ModuleName, "Invalid numplayers:", aurora.Cyan(match[4]), "from", aurora.Cyan(match))
return []map[string]string{}
}
dwc_mtype := match[5]
dwc_hoststate := match[6]
dwc_suspend := match[7]
// gameFilter := match[8]
filtered := []map[string]string{}
// Find servers that match the requested parameters
// TODO: Handle game specific filters (i.e. Regionals or MKW VR search)
for _, server := range servers {
if server["dwc_mver"] == dwc_mver && server["dwc_pid"] != dwc_pid && server["maxplayers"] == maxplayers && server["dwc_mtype"] == dwc_mtype && server["dwc_hoststate"] == dwc_hoststate && server["dwc_suspend"] == dwc_suspend {
server_numplayers, err := strconv.ParseInt(server["numplayers"], 10, 32)
if err != nil {
logging.Error(ModuleName, "Invalid numplayers:", aurora.Cyan(match[4]))
continue
}
if server_numplayers < numplayers {
filtered = append(filtered, server)
}
}
}
logging.Info(ModuleName, "Matched", aurora.BrightCyan(len(filtered)), "servers")
return filtered
}
logging.Error(ModuleName, "Unable to match filter for", aurora.Cyan(filter))
return []map[string]string{}
}

171
serverbrowser/main.go Normal file
View File

@@ -0,0 +1,171 @@
package serverbrowser
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"io"
"net"
"os"
"time"
"wwfc/common"
"wwfc/logging"
)
var (
ctx = context.Background()
pool *pgxpool.Pool
userId int
)
const (
ModuleName = "SB"
// 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() {
// 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)
}
address := config.Address + ":28910"
l, err := net.Listen("tcp", address)
if err != nil {
panic(err)
}
// Close the listener when the application closes.
defer l.Close()
logging.Notice(ModuleName, "Listening on", address)
for {
// Listen for an incoming connection.
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
os.Exit(1)
}
// Handle connections in a new goroutine.
go handleRequest(conn)
}
}
// Handles incoming requests.
func handleRequest(conn net.Conn) {
defer conn.Close()
err := conn.(*net.TCPConn).SetKeepAlive(true)
if err != nil {
logging.Notice(ModuleName, "Unable to set keepalive", err.Error())
}
err = conn.(*net.TCPConn).SetKeepAlivePeriod(time.Hour * 1000)
if err != nil {
logging.Notice(ModuleName, "Unable to set keepalive", err.Error())
}
logging.Notice(ModuleName, "Connection established from", aurora.BrightCyan(conn.RemoteAddr()))
// Here we go into the listening loop
bufferSize := 0
packetSize := uint16(0)
buffer := []byte{}
for {
// Remove stale data and remake the buffer
buffer = append(buffer[packetSize:], make([]byte, 1024-packetSize)...)
bufferSize -= int(packetSize)
packetSize = 0
// Packets tend to be sent in fragments, so this loop makes sure the packets has been fully received before continuing
for {
if bufferSize > 2 {
packetSize = binary.BigEndian.Uint16(buffer[:2])
if packetSize < 3 || packetSize >= 1024 {
logging.Error(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.Error(ModuleName, "Connection error")
return
}
bufferSize += readSize
}
switch buffer[2] {
case ServerListRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_LIST_REQUEST"))
handleServerListRequest(conn, buffer[:packetSize])
break
case ServerInfoRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SERVER_INFO_REQUEST"))
break
case SendMessageRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("SEND_MESSAGE_REQUEST"))
handleSendMessageRequest(conn, buffer[:packetSize])
break
case KeepaliveReply:
logging.Notice(ModuleName, "Command:", aurora.Yellow("KEEPALIVE_REPLY"))
break
case MapLoopRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("MAPLOOP_REQUEST"))
break
case PlayerSearchRequest:
logging.Notice(ModuleName, "Command:", aurora.Yellow("PLAYER_SEARCH_REQUEST"))
break
default:
logging.Error(ModuleName, "Unknown command:", aurora.Cyan(buffer[2]))
break
}
}
}

253
serverbrowser/server.go Normal file
View File

@@ -0,0 +1,253 @@
package serverbrowser
import (
"encoding/binary"
"fmt"
"github.com/logrusorgru/aurora/v3"
"net"
"strconv"
"strings"
"wwfc/common"
"wwfc/logging"
"wwfc/qr2"
)
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
SendFieldsForAllOption = 1 << 0 // 0x01 / 1
NoServerListOption = 1 << 1 // 0x02 / 2
PushUpdatesOption = 1 << 2 // 0x04 / 4
AlternateSourceIPOption = 1 << 3 // 0x08 / 8
SendGroupsOption = 1 << 5 // 0x20 / 32
NoListCacheOption = 1 << 6 // 0x40 / 64
LimitResultCountOption = 1 << 7 // 0x80 / 128
)
func FindServers(queryGame string, filter string) ([]map[string]string, error) {
// TODO: Handle gueryGame, filter
return FilterServers(qr2.GetSessionServers(), queryGame, filter), nil
}
func popString(buffer []byte, index int) (string, int) {
str := common.GetString(buffer[index:])
return str, index + len(str) + 1
}
func popBytes(buffer []byte, index int, size int) ([]byte, int) {
return buffer[index : index+size], index + size
}
func popUint32(buffer []byte, index int) (uint32, int) {
return binary.BigEndian.Uint32(buffer[index:]), index + 4
}
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)
options, index := popUint32(buffer, index)
logging.Info(ModuleName, "queryGame:", aurora.Cyan(queryGame).String(), "- gameName:", aurora.Cyan(gameName).String(), "- filter:", aurora.Cyan(filter).String(), "- fields:", aurora.Cyan(fields).String())
var output []byte
for _, s := range strings.Split(strings.Split(conn.RemoteAddr().String(), ":")[0], ".") {
val, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
output = append(output, byte(val))
}
var fieldList []string
for _, field := range strings.Split(fields, "\\") {
if len(field) == 0 || field == " " {
continue
}
fieldList = append(fieldList, field)
}
if options&NoServerListOption != 0 || len(fieldList) == 0 {
// The client requests its own public IP and game port
logging.Info(ModuleName, "Reply without server list", aurora.Cyan(conn.RemoteAddr()))
// The default game port 6500
output = binary.BigEndian.AppendUint16(output, 6500)
// Write the encrypted reply
conn.Write(common.EncryptTypeX([]byte("9r3Rmy"), challenge, output))
return
}
logging.Info(ModuleName, "Reply with server list", aurora.Cyan(conn.RemoteAddr()))
// The client's port
port, err := strconv.Atoi(strings.Split(conn.RemoteAddr().String(), ":")[1])
if err != nil {
panic(err)
}
output = binary.BigEndian.AppendUint16(output, uint16(port))
output = append(output, byte(len(fieldList)))
for _, field := range fieldList {
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
servers, err := FindServers(queryGame, filter)
if err != nil {
panic(err)
}
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.Error(ModuleName, "Server exists without public IP")
continue
}
ip, err := strconv.ParseUint(publicip, 10, 32)
if err != nil {
logging.Error(ModuleName, "Server has invalid public IP value:", aurora.Cyan(publicip))
}
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.Error(ModuleName, "Server exists without port (publicip =", aurora.Cyan(publicip).String()+")")
continue
}
}
portValue, err := strconv.ParseUint(port, 10, 16)
if err != nil {
logging.Error(ModuleName, "Server has invalid port value:", aurora.Cyan(port))
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.Error(ModuleName, "Server has invalid local IP:", aurora.Cyan(localip0))
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.Error(ModuleName, "Server has invalid local IP value:", aurora.Cyan(localip0))
continue
}
}
if localport, exists := server["localport"]; exists {
portValue, err = strconv.ParseUint(localport, 10, 16)
if err != nil {
logging.Error(ModuleName, "Server has invalid local port value:", aurora.Cyan(localport))
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.Info(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.Info(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
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.BrightCyan(conn.RemoteAddr()), "to", aurora.BrightCyan(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
qr2.SendClientMessage(destIP, buffer[9:])
}