mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-04-26 02:05:38 -05:00
Store blob hashes, require on login
Blob hashes are now stored in the database under the `hashes` table. They are stored internally as a map of packIDs to a map of versions to a map of regions to the hash. On login, a pack ID, version, and hash are required or the login attempt is rejected. Hashes can be updated using the `/api/hash` endpoint. The schema has been updated accordingly.
This commit is contained in:
parent
406660115d
commit
6a40b5c9ec
86
api/hash.go
Normal file
86
api/hash.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"wwfc/database"
|
||||||
|
"wwfc/logging"
|
||||||
|
|
||||||
|
"github.com/logrusorgru/aurora/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleHash(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var success bool
|
||||||
|
var err string
|
||||||
|
var statusCode int
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
success, err, statusCode = handleHashImpl(r)
|
||||||
|
} else if r.Method == http.MethodOptions {
|
||||||
|
statusCode = http.StatusNoContent
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
} else {
|
||||||
|
err = "Incorrect request. POST only."
|
||||||
|
statusCode = http.StatusMethodNotAllowed
|
||||||
|
w.Header().Set("Allow", "POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
var jsonData []byte
|
||||||
|
|
||||||
|
if statusCode != http.StatusNoContent {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
jsonData, _ = json.Marshal(HashResponse{success, err})
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
w.Write(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HashRequestSpec struct {
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
PackID uint32 `json:"pack_id"`
|
||||||
|
Version uint32 `json:"version"`
|
||||||
|
HashNTSCU string `json:"hash_ntscu"`
|
||||||
|
HashNTSCJ string `json:"hash_ntscj"`
|
||||||
|
HashNTSCK string `json:"hash_ntsck"`
|
||||||
|
HashPAL string `json:"hash_pal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HashResponse struct {
|
||||||
|
Success bool
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHashImpl(r *http.Request) (bool, string, int) {
|
||||||
|
// TODO: Actual authentication rather than a fixed secret
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, "Unable to read request body", http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var req HashRequestSpec
|
||||||
|
err = json.Unmarshal(body, &req)
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error(), http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiSecret == "" || req.Secret != apiSecret {
|
||||||
|
return false, "Invalid API secret in request", http.StatusUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Notice("API", "Hashes Received, PackID:", aurora.Cyan(req.PackID), "Version:", aurora.Cyan(req.Version), "\nNTSCU:", aurora.Cyan(req.HashNTSCU), "\nNTSCJ:", aurora.Cyan(req.HashNTSCJ), "\nNTSCK:", aurora.Cyan(req.HashNTSCK), "\nPAL:", aurora.Cyan(req.HashPAL))
|
||||||
|
|
||||||
|
err = database.UpdateHash(pool, ctx, req.PackID, req.Version, req.HashNTSCU, req.HashNTSCJ, req.HashNTSCK, req.HashPAL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error(), http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "", http.StatusOK
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"wwfc/common"
|
"wwfc/common"
|
||||||
|
"wwfc/database"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +32,11 @@ func StartServer(reload bool) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = database.HashInit(pool, ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Shutdown() {
|
func Shutdown() {
|
||||||
|
|
|
||||||
136
database/hash.go
Normal file
136
database/hash.go
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"wwfc/logging"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
"github.com/logrusorgru/aurora/v3"
|
||||||
|
"github.com/sasha-s/go-deadlock"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetHashes = `SELECT * FROM hashes`
|
||||||
|
InsertHash = `INSERT
|
||||||
|
INTO hashes (pack_id, version, hash_ntscu, hash_ntscj, hash_ntsck, hash_pal)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (pack_id, version)
|
||||||
|
DO UPDATE SET hash_ntscu = $3, hash_ntscj = $4, hash_ntsck = $5, hash_pal = $6`
|
||||||
|
)
|
||||||
|
|
||||||
|
type Region byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
R_NTSCU Region = iota
|
||||||
|
R_NTSCJ
|
||||||
|
R_NTSCK
|
||||||
|
R_PAL
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mutex = deadlock.Mutex{}
|
||||||
|
hashes = map[uint32]map[uint32]map[Region]string{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashInit(pool *pgxpool.Pool, ctx context.Context) error {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
logging.Info("DB", "Populating hashes from the database")
|
||||||
|
|
||||||
|
rows, err := pool.Query(ctx, GetHashes)
|
||||||
|
defer rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var packID uint32
|
||||||
|
var version uint32
|
||||||
|
var hashNTSCU string
|
||||||
|
var hashNTSCJ string
|
||||||
|
var hashNTSCK string
|
||||||
|
var hashPAL string
|
||||||
|
|
||||||
|
err = rows.Scan(&packID, &version, &hashNTSCU, &hashNTSCJ, &hashNTSCK, &hashPAL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
versions, exists := hashes[packID]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
temp := map[uint32]map[Region]string{}
|
||||||
|
hashes[packID] = temp
|
||||||
|
versions = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
regions, exists := versions[packID]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
temp := map[Region]string{}
|
||||||
|
versions[packID] = temp
|
||||||
|
regions = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
regions[R_NTSCU] = hashNTSCU
|
||||||
|
regions[R_NTSCJ] = hashNTSCJ
|
||||||
|
regions[R_NTSCK] = hashNTSCK
|
||||||
|
regions[R_PAL] = hashPAL
|
||||||
|
|
||||||
|
logging.Info("DB", "Populated hashes for PackID:", aurora.Cyan(packID), "Version:", aurora.Cyan(version), "\nNTSCU:", aurora.Cyan(hashNTSCU), "\nNTSCJ:", aurora.Cyan(hashNTSCJ), "\nNTSCK:", aurora.Cyan(hashNTSCK), "\nPAL:", aurora.Cyan(hashPAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateHash(pool *pgxpool.Pool, ctx context.Context, packID uint32, version uint32, hashNTSCU string, hashNTSCJ string, hashNTSCK string, hashPAL string) error {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
versions, exists := hashes[packID]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
temp := map[uint32]map[Region]string{}
|
||||||
|
hashes[packID] = temp
|
||||||
|
versions = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
regions, exists := versions[packID]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
temp := map[Region]string{}
|
||||||
|
versions[packID] = temp
|
||||||
|
regions = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
regions[R_NTSCU] = hashNTSCU
|
||||||
|
regions[R_NTSCJ] = hashNTSCJ
|
||||||
|
regions[R_NTSCK] = hashNTSCK
|
||||||
|
regions[R_PAL] = hashPAL
|
||||||
|
|
||||||
|
_, err := pool.Exec(ctx, InsertHash, packID, version, hashNTSCU, hashNTSCJ, hashNTSCK, hashPAL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("DB", "Failed to update hashes for PackID:", aurora.Cyan(packID), "Version:", aurora.Cyan(version), "error:", err.Error())
|
||||||
|
} else {
|
||||||
|
logging.Info("DB", "Successfully updated hashes for PackID:", aurora.Cyan(packID), "Version:", aurora.Cyan(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateHash(packID uint32, version uint32, region Region, hash string) bool {
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
if versions, exists := hashes[packID]; exists {
|
||||||
|
if regions, exists := versions[version]; exists {
|
||||||
|
if hash_real, exists := regions[region]; exists {
|
||||||
|
return hash_real != "" && hash_real == hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,10 @@ var (
|
||||||
ErrLoginBadPreAuth = MakeGPError(0x010A, "There was an error validating the pre-authentication.", true)
|
ErrLoginBadPreAuth = MakeGPError(0x010A, "There was an error validating the pre-authentication.", true)
|
||||||
ErrLoginLoginTicketInvalid = MakeGPError(0x010B, "The login ticket was unable to be validated.", true)
|
ErrLoginLoginTicketInvalid = MakeGPError(0x010B, "The login ticket was unable to be validated.", true)
|
||||||
ErrLoginLoginTicketExpired = MakeGPError(0x010C, "The login ticket had expired and could not be used.", true)
|
ErrLoginLoginTicketExpired = MakeGPError(0x010C, "The login ticket had expired and could not be used.", true)
|
||||||
|
ErrLoginBadPackID = MakeGPError(0x010D, "The provided Pack ID was invalid.", true)
|
||||||
|
ErrLoginBadPackVersion = MakeGPError(0x010E, "The provided Pack Version was invalid.", true)
|
||||||
|
ErrLoginBadRegion = MakeGPError(0x0110, "The provided Region was invalid for the provided Pack ID.", true)
|
||||||
|
ErrLoginBadHash = MakeGPError(0x0111, "The hash for the provided Pack ID and Version was invalid.", true)
|
||||||
|
|
||||||
// New user errors
|
// New user errors
|
||||||
ErrNewUser = MakeGPError(0x0200, "There was an error creating a new user.", true)
|
ErrNewUser = MakeGPError(0x0200, "There was an error creating a new user.", true)
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,75 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if g.GameName == "mariokartwii" {
|
||||||
|
packIDStr, exists := command.OtherValues["pack_id"]
|
||||||
|
if exists {
|
||||||
|
logging.Info(g.ModuleName, "pack_id:", aurora.Cyan(packIDStr))
|
||||||
|
} else {
|
||||||
|
logging.Error(g.ModuleName, "Missing pack_id")
|
||||||
|
g.replyError(ErrLoginBadPackID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packVerStr, exists := command.OtherValues["pack_ver"]
|
||||||
|
if exists {
|
||||||
|
logging.Info(g.ModuleName, "pack_ver:", aurora.Cyan(packVerStr))
|
||||||
|
} else {
|
||||||
|
logging.Error(g.ModuleName, "Missing pack_ver")
|
||||||
|
g.replyError(ErrLoginBadPackVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packHashStr, exists := command.OtherValues["pack_hash"]
|
||||||
|
if exists {
|
||||||
|
logging.Info(g.ModuleName, "pack_hash:", aurora.Cyan(packHashStr))
|
||||||
|
} else {
|
||||||
|
logging.Error(g.ModuleName, "Missing pack_hash")
|
||||||
|
g.replyError(ErrLoginBadHash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(packHashStr) != 20 {
|
||||||
|
logging.Error(g.ModuleName, "Invalid pack_hash: Mismatched len,", aurora.Cyan(len(packHashStr)))
|
||||||
|
g.replyError(ErrLoginBadHash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packID, err := strconv.ParseUint(packIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(g.ModuleName, "Invalid pack_id:", aurora.Cyan(packID))
|
||||||
|
g.replyError(ErrLoginBadPackID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
packVer, err := strconv.ParseUint(packIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(g.ModuleName, "Invalid pack_ver:", aurora.Cyan(packVer))
|
||||||
|
g.replyError(ErrLoginBadPackID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var region database.Region
|
||||||
|
switch gamecd {
|
||||||
|
case "RMCE":
|
||||||
|
region = database.R_NTSCU
|
||||||
|
case "RMCJ":
|
||||||
|
region = database.R_NTSCJ
|
||||||
|
case "RMCP":
|
||||||
|
region = database.R_PAL
|
||||||
|
default:
|
||||||
|
logging.Error(g.ModuleName, "Invalid gamecd:", aurora.Cyan(gamecd))
|
||||||
|
g.replyError(ErrLoginBadRegion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !database.ValidateHash(uint32(packID), uint32(packVer), region, packHashStr) {
|
||||||
|
logging.Error(g.ModuleName, "Invalid pack_hash: Mismatched len,", aurora.Cyan(len(packHashStr)))
|
||||||
|
g.replyError(ErrLoginBadHash)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g.LoginInfoSet = true
|
g.LoginInfoSet = true
|
||||||
|
|
||||||
expectedUnitCode := common.GetExpectedUnitCode(g.GameName)
|
expectedUnitCode := common.GetExpectedUnitCode(g.GameName)
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,11 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/api/hash" {
|
||||||
|
api.HandleHash(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logging.Info("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr))
|
logging.Info("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr))
|
||||||
replyHTTPError(w, 404, "404 Not Found")
|
replyHTTPError(w, 404, "404 Not Found")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
schema.sql
14
schema.sql
|
|
@ -48,7 +48,7 @@ ALTER TABLE ONLY public.users
|
||||||
ADD IF NOT EXISTS ban_reason_hidden character varying,
|
ADD IF NOT EXISTS ban_reason_hidden character varying,
|
||||||
ADD IF NOT EXISTS ban_moderator character varying,
|
ADD IF NOT EXISTS ban_moderator character varying,
|
||||||
ADD IF NOT EXISTS ban_tos boolean,
|
ADD IF NOT EXISTS ban_tos boolean,
|
||||||
ADD IF NOT EXISTS open_host boolean DEFAULT false;
|
ADD IF NOT EXISTS open_host boolean DEFAULT false;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Change ng_device_id from bigint to bigint[]
|
-- Change ng_device_id from bigint to bigint[]
|
||||||
|
|
@ -135,6 +135,18 @@ ALTER TABLE ONLY public.users
|
||||||
ADD CONSTRAINT users_pkey PRIMARY KEY (profile_id);
|
ADD CONSTRAINT users_pkey PRIMARY KEY (profile_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.hashes (
|
||||||
|
pack_id integer,
|
||||||
|
version integer,
|
||||||
|
hash_ntscu character(40) NOT NULL,
|
||||||
|
hash_ntscj character(40) NOT NULL,
|
||||||
|
hash_ntsck character(40) NOT NULL,
|
||||||
|
hash_pal character(40) NOT NULL,
|
||||||
|
CONSTRAINT one_hash_per_version_per_pack UNIQUE (pack_id, version)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.hashes OWNER TO wiilink;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user