mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-09-14 03:25:52 -05:00
Rename matchmaking to serverbrowser
This commit is contained in:
77
serverbrowser/filter.go
Normal file
77
serverbrowser/filter.go
Normal 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
171
serverbrowser/main.go
Normal 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
253
serverbrowser/server.go
Normal 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:])
|
||||
}
|
||||
Reference in New Issue
Block a user