wfc-server/serverbrowser/server.go
2023-11-11 11:49:45 -05:00

254 lines
7.6 KiB
Go

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 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())
gameInfo := common.GetGameInfoByName(gameName)
if gameInfo == nil {
// Game doesn't exist in the game list.
return
}
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
// TODO: Key is currently hardcoded to Mario Kart Wii
conn.Write(common.EncryptTypeX([]byte(gameInfo.SecretKey), 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
publicIP, _ := common.IPFormatToString(conn.RemoteAddr().String())
servers := filterServers(qr2.GetSessionServers(), queryGame, filter, publicIP)
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.ParseInt(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(gameInfo.SecretKey), 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:])
}