Merge pull request #72 from Retro-Rewind-Team/api

Update APIs
This commit is contained in:
Palapeli 2025-03-04 10:07:08 -05:00 committed by GitHub
commit e5a6aedb34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 209 additions and 150 deletions

View File

@ -2,8 +2,8 @@ package api
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"time"
"wwfc/database"
@ -11,116 +11,87 @@ import (
)
func HandleBan(w http.ResponseWriter, r *http.Request) {
errorString := handleBanImpl(w, r)
if errorString != "" {
jsonData, _ := json.Marshal(map[string]string{"error": errorString})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
var success bool
var err string
var statusCode int
if r.Method == http.MethodPost {
success, err, statusCode = handleBanImpl(r)
} else {
jsonData, _ := json.Marshal(map[string]string{"success": "true"})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
err = "Incorrect request. POST only."
statusCode = http.StatusBadRequest
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
var jsonData []byte
if success {
jsonData, _ = json.Marshal(map[string]string{"success": "true"})
} else {
jsonData, _ = json.Marshal(map[string]string{"error": err})
}
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.WriteHeader(statusCode)
w.Write(jsonData)
}
func handleBanImpl(w http.ResponseWriter, r *http.Request) string {
type BanRequestSpec struct {
Secret string
Pid uint32
Days uint64
Hours uint64
Minutes uint64
Tos bool
Reason string
ReasonHidden string
Moderator string
}
func handleBanImpl(r *http.Request) (bool, string, int) {
// TODO: Actual authentication rather than a fixed secret
// TODO: Use POST instead of GET
u, err := url.Parse(r.URL.String())
body, err := io.ReadAll(r.Body)
if err != nil {
return "Bad request"
return false, "Unable to read request body", http.StatusBadRequest
}
query, err := url.ParseQuery(u.RawQuery)
var req BanRequestSpec
err = json.Unmarshal(body, &req)
if err != nil {
return "Bad request"
return false, err.Error(), http.StatusBadRequest
}
if apiSecret == "" || query.Get("secret") != apiSecret {
return "Invalid API secret"
if apiSecret == "" || req.Secret != apiSecret {
return false, "Invalid API secret in request", http.StatusUnauthorized
}
pidStr := query.Get("pid")
if pidStr == "" {
return "Missing pid in request"
if req.Pid == 0 {
return false, "pid missing or 0 in request", http.StatusBadRequest
}
pid, err := strconv.ParseUint(pidStr, 10, 32)
if err != nil {
return "Invalid pid"
if req.Reason == "" {
return false, "Missing ban reason in request", http.StatusBadRequest
}
tosStr := query.Get("tos")
if tosStr == "" {
return "Missing tos in request"
}
tos, err := strconv.ParseBool(tosStr)
if err != nil {
return "Invalid tos"
}
minutes := uint64(0)
if query.Get("minutes") != "" {
minutesStr := query.Get("minutes")
minutes, err = strconv.ParseUint(minutesStr, 10, 32)
if err != nil {
return "Invalid minutes"
}
}
hours := uint64(0)
if query.Get("hours") != "" {
hoursStr := query.Get("hours")
hours, err = strconv.ParseUint(hoursStr, 10, 32)
if err != nil {
return "Invalid hours"
}
}
days := uint64(0)
if query.Get("days") != "" {
daysStr := query.Get("days")
days, err = strconv.ParseUint(daysStr, 10, 32)
if err != nil {
return "Invalid days"
}
}
reason := query.Get("reason")
if "reason" == "" {
return "Missing ban reason"
}
// reason_hidden is optional
reasonHidden := query.Get("reason_hidden")
moderator := query.Get("moderator")
if "moderator" == "" {
moderator := req.Moderator
if moderator == "" {
moderator = "admin"
}
minutes = days*24*60 + hours*60 + minutes
minutes := req.Days*24*60 + req.Hours*60 + req.Minutes
if minutes == 0 {
return "Missing ban length"
return false, "Ban length missing or 0", http.StatusBadRequest
}
length := time.Duration(minutes) * time.Minute
if !database.BanUser(pool, ctx, uint32(pid), tos, length, reason, reasonHidden, moderator) {
return "Failed to ban user"
if !database.BanUser(pool, ctx, req.Pid, req.Tos, length, req.Reason, req.ReasonHidden, moderator) {
return false, "Failed to ban user", http.StatusInternalServerError
}
if tos {
gpcm.KickPlayer(uint32(pid), "banned")
} else {
gpcm.KickPlayer(uint32(pid), "restricted")
}
gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCMsgProfileRestrictedCustom)
return ""
return true, "", http.StatusOK
}

View File

@ -2,57 +2,72 @@ package api
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"wwfc/gpcm"
)
func HandleKick(w http.ResponseWriter, r *http.Request) {
errorString := handleKickImpl(w, r)
if errorString != "" {
jsonData, _ := json.Marshal(map[string]string{"error": errorString})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
var success bool
var err string
var statusCode int
if r.Method == http.MethodPost {
success, err, statusCode = handleKickImpl(r)
} else {
jsonData, _ := json.Marshal(map[string]string{"success": "true"})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
err = "Incorrect request. POST only."
statusCode = http.StatusBadRequest
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
var jsonData []byte
if success {
jsonData, _ = json.Marshal(map[string]string{"success": "true"})
} else {
jsonData, _ = json.Marshal(map[string]string{"error": err})
}
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.WriteHeader(statusCode)
w.Write(jsonData)
}
func handleKickImpl(w http.ResponseWriter, r *http.Request) string {
type KickRequestSpec struct {
Secret string
Reason string
Pid uint32
}
func handleKickImpl(r *http.Request) (bool, string, int) {
// TODO: Actual authentication rather than a fixed secret
// TODO: Use POST instead of GET
u, err := url.Parse(r.URL.String())
body, err := io.ReadAll(r.Body)
if err != nil {
return "Bad request"
return false, "Unable to read request body", http.StatusBadRequest
}
query, err := url.ParseQuery(u.RawQuery)
var req KickRequestSpec
err = json.Unmarshal(body, &req)
if err != nil {
return "Bad request"
return false, err.Error(), http.StatusBadRequest
}
if apiSecret == "" || query.Get("secret") != apiSecret {
return "Invalid API secret"
if apiSecret == "" || req.Secret != apiSecret {
return false, "Invalid API secret in request", http.StatusUnauthorized
}
pidStr := query.Get("pid")
if pidStr == "" {
return "Missing pid in request"
if req.Pid == 0 {
return false, "pid missing or 0 in request", http.StatusBadRequest
}
pid, err := strconv.ParseUint(pidStr, 10, 32)
if err != nil {
return "Invalid pid"
if req.Reason == "" {
return false, "Missing kick reason in request", http.StatusBadRequest
}
gpcm.KickPlayer(uint32(pid), "moderator_kick")
return ""
gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCMsgKickedCustom)
return true, "", http.StatusOK
}

View File

@ -2,57 +2,69 @@ package api
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"wwfc/database"
)
func HandleUnban(w http.ResponseWriter, r *http.Request) {
errorString := handleUnbanImpl(w, r)
if errorString != "" {
jsonData, _ := json.Marshal(map[string]string{"error": errorString})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
var success bool
var err string
var statusCode int
if r.Method == http.MethodPost {
success, err, statusCode = handleUnbanImpl(r)
} else {
jsonData, _ := json.Marshal(map[string]string{"success": "true"})
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
err = "Incorrect request. POST only."
statusCode = http.StatusBadRequest
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
var jsonData []byte
if success {
jsonData, _ = json.Marshal(map[string]string{"success": "true"})
} else {
jsonData, _ = json.Marshal(map[string]string{"error": err})
}
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.WriteHeader(statusCode)
w.Write(jsonData)
}
func handleUnbanImpl(w http.ResponseWriter, r *http.Request) string {
type UnbanRequestSpec struct {
Secret string
Pid uint32
}
func handleUnbanImpl(r *http.Request) (bool, string, int) {
// TODO: Actual authentication rather than a fixed secret
// TODO: Use POST instead of GET
u, err := url.Parse(r.URL.String())
body, err := io.ReadAll(r.Body)
if err != nil {
return "Bad request"
return false, "Unable to read request body", http.StatusBadRequest
}
query, err := url.ParseQuery(u.RawQuery)
var req UnbanRequestSpec
err = json.Unmarshal(body, &req)
if err != nil {
return "Bad request"
return false, err.Error(), http.StatusBadRequest
}
if apiSecret == "" || query.Get("secret") != apiSecret {
return "Invalid API secret"
if apiSecret == "" || req.Secret != apiSecret {
return false, "Invalid API secret in request", http.StatusUnauthorized
}
pidStr := query.Get("pid")
if pidStr == "" {
return "Missing pid in request"
if req.Pid == 0 {
return false, "pid missing or 0 in request", http.StatusBadRequest
}
pid, err := strconv.ParseUint(pidStr, 10, 32)
if err != nil {
return "Invalid pid"
if !database.UnbanUser(pool, ctx, req.Pid) {
return false, "Failed to unban user", http.StatusInternalServerError
}
database.UnbanUser(pool, ctx, uint32(pid))
return ""
return true, "", http.StatusOK
}

View File

@ -26,7 +26,7 @@ const (
ON ng_device_id && array[dt.device_id]
) SELECT array_agg(DISTINCT device_id) FROM device_tree
)
SELECT has_ban, ban_tos, ng_device_id
SELECT has_ban, ban_tos, ng_device_id, ban_reason
FROM users
WHERE has_ban = true
AND (profile_id = $2
@ -170,8 +170,11 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
var banExists bool
var banTOS bool
var bannedDeviceIdList []uint32
var banReason string
timeNow := time.Now()
err = pool.QueryRow(ctx, SearchUserBan, user.NgDeviceId, user.ProfileId, ipAddress, *lastIPAddress, timeNow).Scan(&banExists, &banTOS, &bannedDeviceIdList)
err = pool.QueryRow(ctx, SearchUserBan, user.NgDeviceId, user.ProfileId, ipAddress, *lastIPAddress, timeNow).Scan(&banExists, &banTOS, &bannedDeviceIdList, &banReason)
if err != nil {
if err != pgx.ErrNoRows {
return User{}, err
@ -202,12 +205,13 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
if banTOS {
logging.Warn("DATABASE", "Profile", aurora.Cyan(user.ProfileId), "is banned")
return User{RestrictedDeviceId: bannedDeviceId}, ErrProfileBannedTOS
return User{RestrictedDeviceId: bannedDeviceId, BanReason: banReason}, ErrProfileBannedTOS
}
logging.Warn("DATABASE", "Profile", aurora.Cyan(user.ProfileId), "is restricted")
user.Restricted = true
user.RestrictedDeviceId = bannedDeviceId
user.BanReason = banReason
}
return user, nil

View File

@ -15,7 +15,8 @@ const (
UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END, open_host = CASE WHEN $7 THEN $6 ELSE open_host END WHERE profile_id = $1`
UpdateUserProfileID = `UPDATE users SET profile_id = $3 WHERE user_id = $1 AND gsbrcd = $2`
UpdateUserNGDeviceID = `UPDATE users SET ng_device_id = $2 WHERE profile_id = $1`
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host FROM users WHERE profile_id = $1`
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn FROM users WHERE profile_id = $1`
ClearProfileQuery = `DELETE FROM users WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn`
DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)`
IsProfileIDInUse = `SELECT EXISTS(SELECT 1 FROM users WHERE profile_id = $1)`
DeleteUserSession = `DELETE FROM sessions WHERE profile_id = $1`
@ -39,7 +40,10 @@ type User struct {
LastName string
Restricted bool
RestrictedDeviceId uint32
BanReason string
OpenHost bool
LastInGameSn string
LastIPAddress string
}
var (
@ -128,7 +132,20 @@ func (user *User) UpdateProfile(pool *pgxpool.Pool, ctx context.Context, data ma
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
user := User{}
row := pool.QueryRow(ctx, GetUser, profileId)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn)
if err != nil {
return User{}, false
}
user.ProfileId = profileId
return user, true
}
func ClearProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
user := User{}
row := pool.QueryRow(ctx, ClearProfileQuery, profileId)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn)
if err != nil {
return User{}, false
}

View File

@ -35,6 +35,7 @@ type GPError struct {
ErrorString string
Fatal bool
WWFCMessage WWFCErrorMessage
Reason string
}
func MakeGPError(errorCode int, errorString string, fatal bool) GPError {
@ -247,6 +248,17 @@ var (
},
}
WWFCMsgProfileRestrictedCustom = WWFCErrorMessage{
ErrorCode: 22002,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been banned from WiiLink WFC\n" +
"Error Code: %[1]d\n" +
"Support Info: NG%08[2]x\n" +
"Reason: %s",
},
}
WWFCMsgKickedGeneric = WWFCErrorMessage{
ErrorCode: 22004,
MessageRMC: map[byte]string{
@ -281,6 +293,17 @@ var (
},
}
WWFCMsgKickedCustom = WWFCErrorMessage{
ErrorCode: 22002,
MessageRMC: map[byte]string{
LangEnglish: "" +
"You have been kicked from WiiLink WFC\n" +
"Error Code: %[1]d\n" +
"Support Info: NG%08[2]x\n" +
"Reason: %s",
},
}
WWFCMsgConsoleMismatch = WWFCErrorMessage{
ErrorCode: 22005,
MessageRMC: map[byte]string{
@ -405,7 +428,7 @@ func (err GPError) GetMessageTranslate(gameName string, region byte, lang byte,
errMsg = err.WWFCMessage.MessageRMC[LangEnglish]
}
errMsg = fmt.Sprintf(errMsg, err.WWFCMessage.ErrorCode, ngid)
errMsg = fmt.Sprintf(errMsg, err.WWFCMessage.ErrorCode, ngid, err.Reason)
errMsgUTF16 := utf16.Encode([]rune(errMsg))
errMsgByteArray := common.UTF16ToByteArray(errMsgUTF16)

View File

@ -49,3 +49,18 @@ func KickPlayer(profileID uint32, reason string) {
kickPlayer(profileID, reason)
}
func KickPlayerCustomMessage(profileID uint32, reason string, message WWFCErrorMessage) {
mutex.Lock()
defer mutex.Unlock()
if session, exists := sessions[profileID]; exists {
session.replyError(GPError{
ErrorCode: ErrConnectionClosed.ErrorCode,
ErrorString: "The player was kicked from the server. Reason: " + reason,
Fatal: true,
WWFCMessage: message,
Reason: reason,
})
}
}

View File

@ -474,9 +474,10 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string
} else if err == database.ErrProfileBannedTOS {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The profile is banned from the service.",
ErrorString: "The profile is banned from the service. Reason: " + user.BanReason,
Fatal: true,
WWFCMessage: WWFCMsgProfileBannedTOS,
WWFCMessage: WWFCMsgKickedCustom,
Reason: user.BanReason,
})
} else {
g.replyError(GPError{

View File

@ -14,3 +14,4 @@ func GetMessageOfTheDay() (string, error) {
return string(contents), nil
}