Everything up until creating a session

This commit is contained in:
Sketch
2023-09-14 21:40:58 -04:00
committed by GitHub
parent a3108d819d
commit 7a41f8a019
18 changed files with 776 additions and 16 deletions

174
common/encryption.go Normal file
View File

@@ -0,0 +1,174 @@
package common
import (
"time"
)
func EncryptTypeX(key []byte, challenge []byte, data []byte) []byte {
returnData := make([]byte, 20)
returnData = append(returnData, data...)
keyLen := len(key)
challengeLen := len(challenge)
rnd := time.Now().Unix()
for i := 0; i < 20; i++ {
rnd = (rnd * 0x343FD) + 0x269EC3
returnData[i] = byte(rnd ^ int64(key[i%keyLen]) ^ int64(challenge[i%challengeLen]))
}
headerLen := 7
returnData[0] = byte((headerLen - 2) ^ 0xec)
returnData[1] = 0x00
returnData[2] = 0x00
returnData[headerLen-1] = byte((20 - headerLen) ^ 0xea)
header := returnData[:20]
encxkey := make([]byte, 261)
returnData = initEncrypt(encxkey, key, challenge, returnData)
func6e(encxkey, returnData)
return append(header, returnData...)
}
func initEncrypt(encxkey, key, validate, data []byte) []byte {
// TODO: Bounds
headerLen := (data[0] ^ 0xec) + 2
dataStart := (data[headerLen-1] ^ 0xea)
data = enctypexFuncX(encxkey, key, validate, data[headerLen:])
return data[dataStart:]
}
func enctypexFuncX(encxkey, key, challenge, data []byte) []byte {
keyLen := len(key)
for i := 0; i < len(data); i++ {
challenge[(key[i%keyLen]*byte(i))&7] ^= challenge[i&7] ^ data[i]
}
func4(encxkey, challenge, 8)
return data
}
func func4(encxkey, challenge []byte, challengeLen int) {
for i := 0; i < 256; i++ {
encxkey[i] = byte(i)
}
n1 := 0
n2 := 0
t1 := 0
for i := 255; i != -1; i-- {
t1, n1, n2 = func5(encxkey, i, challenge, challengeLen, n1, n2)
t2 := encxkey[i]
encxkey[i] = encxkey[t1]
encxkey[t1] = t2
}
encxkey[256] = encxkey[1]
encxkey[257] = encxkey[3]
encxkey[258] = encxkey[5]
encxkey[259] = encxkey[7]
encxkey[260] = encxkey[n1&0xff]
}
func func5(encxkey []byte, cnt int, id []byte, idLen, n1, n2 int) (int, int, int) {
if cnt == 0 {
return 0, n1, n2
}
mask := 1
doLoop := true
if cnt > 1 {
for doLoop {
mask = (mask << 1) + 1
doLoop = mask < cnt
}
}
i := 0
tmp := 0
doLoop = true
for doLoop {
n1 = int(encxkey[n1&0xff] + id[n2])
n2 += 1
if n2 >= idLen {
n2 = 0
n1 += idLen
}
tmp = n1 & mask
i += 1
if i > 11 {
tmp %= cnt
}
doLoop = tmp > cnt
}
return tmp, n1, n2
}
func func6e(encxkey []byte, data []byte) []byte {
for i := 0; i < len(data); i++ {
data[i] = func7e(encxkey, data[i])
}
return data
}
func func7e(encxkey []byte, d byte) byte {
a := encxkey[256]
b := encxkey[257]
c := encxkey[a]
encxkey[256] = (a + 1) & 0xff
encxkey[257] = (b + c) & 0xff
a = encxkey[260]
b = encxkey[257]
b = encxkey[b]
c = encxkey[a]
encxkey[a] = b
a = encxkey[259]
b = encxkey[257]
a = encxkey[a]
encxkey[b] = a
a = encxkey[256]
b = encxkey[259]
a = encxkey[a]
encxkey[b] = a
a = encxkey[256]
encxkey[a] = c
b = encxkey[258]
a = encxkey[c]
c = encxkey[259]
b = (a + b) & 0xff
encxkey[258] = b
a = b
c = encxkey[c]
b = encxkey[257]
b = encxkey[b]
a = encxkey[a]
c = (b + c) & 0xff
b = encxkey[260]
b = encxkey[b]
c = (b + c) & 0xff
b = encxkey[c]
c = encxkey[256]
c = encxkey[c]
a = (a + c) & 0xff
c = encxkey[b]
b = encxkey[a]
c ^= b ^ d
encxkey[260] = c
encxkey[259] = d
return c
}

36
common/strings.go Normal file
View File

@@ -0,0 +1,36 @@
package common
import (
"bytes"
"math/rand"
"time"
)
var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandomString(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
var hexRunes = []rune("0123456789abcdefabcdef")
func RandomHexString(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(hexRunes))]
}
return string(b)
}
func GetString(buf []byte) string {
nullTerminator := bytes.IndexByte(buf, 0)
return string(buf[:nullTerminator])
}

View File

@@ -2,9 +2,8 @@ package database
import (
"context"
"wwfc/common"
"github.com/jackc/pgx/v4/pgxpool"
"wwfc/common"
)
const (

View File

@@ -3,10 +3,9 @@ package database
import (
"context"
"errors"
"wwfc/common"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"wwfc/common"
)
const (

66
gcsp/login.go Normal file
View File

@@ -0,0 +1,66 @@
package gcsp
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"github.com/jackc/pgx/v4/pgxpool"
"log"
"strconv"
"strings"
"wwfc/common"
"wwfc/database"
)
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.
user := database.LoginUserToGCPM(pool, ctx, authToken)
loginTicket := strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1)
// TODO: REMOVE!!!!!
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.Itoa(user.UserId),
"profileid": strconv.Itoa(user.ProfileId),
"uniquenick": user.UniqueNick,
"lt": loginTicket,
"id": command.OtherValues["id"],
},
})
}

117
gcsp/main.go Normal file
View File

@@ -0,0 +1,117 @@
package gcsp
import (
"bufio"
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"io"
"log"
"net"
"os"
"time"
"wwfc/common"
"wwfc/logging"
)
var (
ctx = context.Background()
pool *pgxpool.Pool
userId int
)
func checkError(err error) {
if err != nil {
log.Fatalf("GCSP server has encountered a fatal error! Reason: %v\n", err)
}
}
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)
checkError(err)
pool, err = pgxpool.ConnectConfig(ctx, dbConf)
checkError(err)
l, err := net.Listen("tcp", "127.0.0.1:29901")
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
// Close the listener when the application closes.
defer l.Close()
fmt.Println("Listening on " + "127.0.0.1:29901")
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 {
fmt.Printf("Unable to set keepalive - %s", err)
}
err = conn.(*net.TCPConn).SetKeepAlivePeriod(time.Hour * 1000)
if err != nil {
fmt.Printf("Unable to set keepalive - %s", err)
}
// log.Printf("%s: Connection established from %s. Sending challenge.", aurora.Green("[NOTICE]"), aurora.Yellow(conn.RemoteAddr()))
// conn.Write([]byte(fmt.Sprintf(`\lc\1\challenge\%s\id\1\final\`, challenge)))
// Here we go into the listening loop
for {
buffer := make([]byte, 1024)
_, err := bufio.NewReader(conn).Read(buffer)
if err != nil {
if errors.Is(err, io.EOF) {
// Client closed connection, terminate.
return
}
}
commands, err := common.ParseGameSpyMessage(string(buffer))
if err != nil {
log.Fatal(err)
}
for _, command := range commands {
logging.Notice("GCSP", "Message received. Command:", aurora.Yellow(command.Command).String())
switch command.Command {
case "ka":
conn.Write([]byte(`\ka\\final\`))
break
case "otherslist":
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "otherslist",
CommandValue: "",
OtherValues: map[string]string{
"o": "0",
"uniquenick": "7me4ijr5sRMCJ3d9uhvh",
"oldone": "",
},
})
conn.Write([]byte(payload))
break
}
}
}
}

62
gcsp/profile.go Normal file
View File

@@ -0,0 +1,62 @@
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.Atoi(strProfileId)
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.Itoa(user.UserId),
"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",
},
})
}

View File

@@ -5,13 +5,12 @@ import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"github.com/jackc/pgx/v4/pgxpool"
"log"
"strconv"
"strings"
"wwfc/common"
"wwfc/database"
"github.com/jackc/pgx/v4/pgxpool"
)
func generateResponse(gpcmChallenge, nasChallenge, authToken, clientChallenge string) string {
@@ -46,7 +45,8 @@ func login(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyComman
// Perform the login with the database.
user := database.LoginUserToGCPM(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)

View File

@@ -5,15 +5,14 @@ import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"io"
"log"
"net"
"os"
"time"
"wwfc/common"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
var (
@@ -121,7 +120,6 @@ func handleRequest(conn net.Conn) {
break
case "getprofile":
payload := getProfile(pool, ctx, command)
fmt.Println(payload)
conn.Write([]byte(payload))
break
}

16
logging/log.go Normal file
View File

@@ -0,0 +1,16 @@
package logging
import (
"github.com/logrusorgru/aurora/v3"
"log"
)
func Notice(module string, arguments ...string) {
var finalStr string
for _, argument := range arguments {
finalStr += argument
finalStr += " "
}
log.Printf("[%s]: %s", aurora.Green(module), finalStr)
}

View File

@@ -2,15 +2,17 @@ package main
import (
"sync"
"wwfc/gcsp"
"wwfc/gpcm"
"wwfc/gpsp"
"wwfc/master"
"wwfc/matchmaking"
"wwfc/nas"
)
func main() {
wg := &sync.WaitGroup{}
actions := []func(){nas.StartServer, gpcm.StartServer, gpsp.StartServer}
wg.Add(3)
actions := []func(){nas.StartServer, gpcm.StartServer, master.StartServer, gcsp.StartServer, matchmaking.StartServer}
wg.Add(4)
for _, action := range actions {
go func(ac func()) {
defer wg.Done()

5
master/challenge.go Normal file
View File

@@ -0,0 +1,5 @@
package master
func challenge() {
}

57
master/heartbeat.go Normal file
View File

@@ -0,0 +1,57 @@
package master
import (
"encoding/binary"
"fmt"
"net"
"strconv"
"strings"
"wwfc/common"
"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())
values := strings.Split(string(buffer[5:]), "\u0000")
payload := map[string]string{}
for i := 0; i < len(values); i += 2 {
if values[i] == "" {
break
}
payload[values[i]] = values[i+1]
}
// Generate challenge and send to server
var hexIP string
for _, i := range strings.Split(payload["localip0"], ".") {
val, err := strconv.ParseUint(i, 10, 64)
if err != nil {
panic(err)
}
hexIP += fmt.Sprintf("%02X", val)
}
port, err := strconv.ParseUint(payload["localport"], 10, 64)
if err != nil {
panic(err)
}
hexPort := fmt.Sprintf("%04X", port)
challenge := common.RandomString(6) + "00" + hexIP + hexPort
mutex.Lock()
session := sessions[sessionId]
session.Challenge = challenge
mutex.Unlock()
response := []byte{0xfe, 0xfd, 0x01}
response = binary.BigEndian.AppendUint32(response, sessionId)
response = append(response, []byte(challenge)...)
response = append(response, 0)
conn.WriteTo(response, addr)
}

57
master/main.go Normal file
View File

@@ -0,0 +1,57 @@
package master
import (
"encoding/binary"
"log"
"net"
"sync"
)
var (
// I would use a sync.Map instead of the map mutex combo, but this performs better.
sessions = map[uint32]*Session{}
mutex = sync.RWMutex{}
)
func StartServer() {
conn, err := net.ListenPacket("udp", ":27900")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
for {
buf := make([]byte, 1024)
_, addr, err := conn.ReadFrom(buf)
if err != nil {
continue
}
go handleConnection(conn, addr, buf)
}
}
func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) {
if buffer[0] != 9 {
addSession(addr, buffer)
}
switch buffer[0] {
case 1:
// Challenge
sessionId := binary.BigEndian.Uint32(buffer[1:5])
response := []byte{0xfe, 0xfd, 0x0a}
response = binary.BigEndian.AppendUint32(response, sessionId)
conn.WriteTo(response, addr)
break
case 3:
heartbeat(conn, addr, buffer)
break
case 9:
conn.WriteTo([]byte{0xfe, 0xfd, 0x09, 0x00, 0x00, 0x00, 0x00}, addr)
break
default:
return
}
}

29
master/session.go Normal file
View File

@@ -0,0 +1,29 @@
package master
import (
"encoding/binary"
"net"
)
type Session struct {
SessionID uint32
Challenge string
SecretKey string
IsConnected bool
}
func addSession(addr net.Addr, buffer []byte) {
sessionId := binary.BigEndian.Uint32(buffer[1:5])
mutex.Lock()
if _, ok := sessions[sessionId]; !ok {
sessions[sessionId] = &Session{
SessionID: sessionId,
Challenge: "",
// TODO: This is hardcoded for Mario Kart Wii
SecretKey: "9r3Rmy",
IsConnected: true,
}
}
mutex.Unlock()
}

102
matchmaking/main.go Normal file
View File

@@ -0,0 +1,102 @@
package matchmaking
import (
"bufio"
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"io"
"log"
"net"
"os"
"time"
"wwfc/common"
"wwfc/logging"
)
var (
ctx = context.Background()
pool *pgxpool.Pool
userId int
)
const (
ServerList = iota
ModuleName = "MATCHMAKING"
)
func checkError(err error) {
if err != nil {
log.Fatalf("GCSP server has encountered a fatal error! Reason: %v\n", err)
}
}
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)
checkError(err)
pool, err = pgxpool.ConnectConfig(ctx, dbConf)
checkError(err)
l, err := net.Listen("tcp", "127.0.0.1:28910")
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
// Close the listener when the application closes.
defer l.Close()
fmt.Println("Listening on " + "127.0.0.1:28910")
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 {
fmt.Printf("Unable to set keepalive - %s", err)
}
err = conn.(*net.TCPConn).SetKeepAlivePeriod(time.Hour * 1000)
if err != nil {
fmt.Printf("Unable to set keepalive - %s", err)
}
// log.Printf("%s: Connection established from %s. Sending challenge.", aurora.Green("[NOTICE]"), aurora.Yellow(conn.RemoteAddr()))
// conn.Write([]byte(fmt.Sprintf(`\lc\1\challenge\%s\id\1\final\`, challenge)))
// Here we go into the listening loop
for {
buffer := make([]byte, 1024)
_, err := bufio.NewReader(conn).Read(buffer)
if err != nil {
if errors.Is(err, io.EOF) {
// Client closed connection, terminate.
return
}
}
logging.Notice(ModuleName, "Help me please")
switch buffer[2] {
case ServerList:
serverList(conn, buffer)
break
}
}
}

43
matchmaking/server.go Normal file
View File

@@ -0,0 +1,43 @@
package matchmaking
import (
"encoding/binary"
"net"
"strings"
"wwfc/common"
"wwfc/logging"
)
func serverList(conn net.Conn, buffer []byte) {
logging.Notice(ModuleName, "Received server list command")
// TODO: Make a custom decoder for this? Go's binary decoder does not support strings as they are not a fixed width.
//listVersion := buffer[3]
//encodingVersion := buffer[4]
//gameVersion := binary.BigEndian.Uint32(buffer[5:])
index := 9
queryGame := common.GetString(buffer[index:])
index += len(queryGame) + 1
gameName := common.GetString(buffer[index:])
index += len(gameName) + 1
challenge := buffer[index : index+8]
index += 8
filter := common.GetString(buffer[index:])
index += len(filter) + 1
fields := common.GetString(buffer[index:])
index += len(fields) + 1
options := binary.BigEndian.Uint32(buffer[index:])
index += 4
logging.Notice(ModuleName, "Values", queryGame, gameName, string(challenge), string(options))
// TODO: Find a game if possible, but there is nobody to do that with yet!
output := []byte(strings.Replace(conn.RemoteAddr().String(), ".", "", -1))
output = binary.BigEndian.AppendUint16(output, 6500)
// encrypted := common.EncryptTypeX([]byte("9r3Rmy"), challenge, output)
conn.Write(output)
}

View File

@@ -2,7 +2,6 @@ package nas
import (
"encoding/base64"
"fmt"
"github.com/logrusorgru/aurora/v3"
"log"
"net/http"
@@ -60,7 +59,6 @@ func (route *Route) Handle() http.Handler {
path := strings.Replace(r.URL.Path, "/", "", -1)
actionName, _ := base64.StdEncoding.DecodeString(strings.Replace(r.PostForm.Get("action"), "*", "=", -1))
fmt.Println(string(actionName))
var action Action
for _, _action := range route.Actions {
if path == _action.ServiceType && string(actionName) == _action.ActionName {