From cfec099bdd73d292ab5e10537825ce832be5f79c Mon Sep 17 00:00:00 2001 From: ppeb Date: Sat, 24 Aug 2024 20:42:00 -0500 Subject: [PATCH 01/11] Add IP logging to ban, kick, unban --- api/ban.go | 36 +++++++++++++++++++----------------- api/kick.go | 22 +++++++++++++--------- api/unban.go | 21 ++++++++++++--------- database/user.go | 11 +++++++++++ 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/api/ban.go b/api/ban.go index f158eab..3fd177d 100644 --- a/api/ban.go +++ b/api/ban.go @@ -11,7 +11,7 @@ import ( ) func HandleBan(w http.ResponseWriter, r *http.Request) { - errorString := handleBanImpl(w, r) + errorString, ip := handleBanImpl(w, r) if errorString != "" { jsonData, _ := json.Marshal(map[string]string{"error": errorString}) w.Header().Set("Content-Type", "application/json") @@ -19,7 +19,7 @@ func HandleBan(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) w.Write(jsonData) } else { - jsonData, _ := json.Marshal(map[string]string{"success": "true"}) + jsonData, _ := json.Marshal(map[string]string{"success": "true", "ip": ip}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) @@ -27,42 +27,42 @@ func HandleBan(w http.ResponseWriter, r *http.Request) { } } -func handleBanImpl(w http.ResponseWriter, r *http.Request) string { +func handleBanImpl(w http.ResponseWriter, r *http.Request) (string, string) { // TODO: Actual authentication rather than a fixed secret // TODO: Use POST instead of GET u, err := url.Parse(r.URL.String()) if err != nil { - return "Bad request" + return "Bad request", "" } query, err := url.ParseQuery(u.RawQuery) if err != nil { - return "Bad request" + return "Bad request", "" } if apiSecret == "" || query.Get("secret") != apiSecret { - return "Invalid API secret" + return "Invalid API secret", "" } pidStr := query.Get("pid") if pidStr == "" { - return "Missing pid in request" + return "Missing pid in request", "" } pid, err := strconv.ParseUint(pidStr, 10, 32) if err != nil { - return "Invalid pid" + return "Invalid pid", "" } tosStr := query.Get("tos") if tosStr == "" { - return "Missing tos in request" + return "Missing tos in request", "" } tos, err := strconv.ParseBool(tosStr) if err != nil { - return "Invalid tos" + return "Invalid tos", "" } minutes := uint64(0) @@ -70,7 +70,7 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) string { minutesStr := query.Get("minutes") minutes, err = strconv.ParseUint(minutesStr, 10, 32) if err != nil { - return "Invalid minutes" + return "Invalid minutes", "" } } @@ -79,7 +79,7 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) string { hoursStr := query.Get("hours") hours, err = strconv.ParseUint(hoursStr, 10, 32) if err != nil { - return "Invalid hours" + return "Invalid hours", "" } } @@ -88,13 +88,13 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) string { daysStr := query.Get("days") days, err = strconv.ParseUint(daysStr, 10, 32) if err != nil { - return "Invalid days" + return "Invalid days", "" } } reason := query.Get("reason") if "reason" == "" { - return "Missing ban reason" + return "Missing ban reason", "" } // reason_hidden is optional @@ -107,13 +107,13 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) string { minutes = days*24*60 + hours*60 + minutes if minutes == 0 { - return "Missing ban length" + return "Missing ban length", "" } length := time.Duration(minutes) * time.Minute if !database.BanUser(pool, ctx, uint32(pid), tos, length, reason, reasonHidden, moderator) { - return "Failed to ban user" + return "Failed to ban user", "" } if tos { @@ -122,5 +122,7 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) string { gpcm.KickPlayer(uint32(pid), "restricted") } - return "" + ip := database.GetUserIP(pool, ctx, uint32(pid)) + + return "", ip } diff --git a/api/kick.go b/api/kick.go index 09e1c8b..bd9eb47 100644 --- a/api/kick.go +++ b/api/kick.go @@ -5,11 +5,12 @@ import ( "net/http" "net/url" "strconv" + "wwfc/database" "wwfc/gpcm" ) func HandleKick(w http.ResponseWriter, r *http.Request) { - errorString := handleKickImpl(w, r) + errorString, ip := handleKickImpl(w, r) if errorString != "" { jsonData, _ := json.Marshal(map[string]string{"error": errorString}) w.Header().Set("Content-Type", "application/json") @@ -17,7 +18,7 @@ func HandleKick(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) w.Write(jsonData) } else { - jsonData, _ := json.Marshal(map[string]string{"success": "true"}) + jsonData, _ := json.Marshal(map[string]string{"success": "true", "ip": ip}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) @@ -25,34 +26,37 @@ func HandleKick(w http.ResponseWriter, r *http.Request) { } } -func handleKickImpl(w http.ResponseWriter, r *http.Request) string { +func handleKickImpl(w http.ResponseWriter, r *http.Request) (string, string) { // TODO: Actual authentication rather than a fixed secret // TODO: Use POST instead of GET u, err := url.Parse(r.URL.String()) if err != nil { - return "Bad request" + return "Bad request", "" } query, err := url.ParseQuery(u.RawQuery) if err != nil { - return "Bad request" + return "Bad request", "" } if apiSecret == "" || query.Get("secret") != apiSecret { - return "Invalid API secret" + return "Invalid API secret", "" } pidStr := query.Get("pid") if pidStr == "" { - return "Missing pid in request" + return "Missing pid in request", "" } pid, err := strconv.ParseUint(pidStr, 10, 32) if err != nil { - return "Invalid pid" + return "Invalid pid", "" } gpcm.KickPlayer(uint32(pid), "moderator_kick") - return "" + + ip := database.GetUserIP(pool, ctx, uint32(pid)) + + return "", ip } diff --git a/api/unban.go b/api/unban.go index ad258f8..1e9e917 100644 --- a/api/unban.go +++ b/api/unban.go @@ -9,7 +9,7 @@ import ( ) func HandleUnban(w http.ResponseWriter, r *http.Request) { - errorString := handleUnbanImpl(w, r) + errorString, ip := handleUnbanImpl(w, r) if errorString != "" { jsonData, _ := json.Marshal(map[string]string{"error": errorString}) w.Header().Set("Content-Type", "application/json") @@ -17,7 +17,7 @@ func HandleUnban(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) w.Write(jsonData) } else { - jsonData, _ := json.Marshal(map[string]string{"success": "true"}) + jsonData, _ := json.Marshal(map[string]string{"success": "true", "ip": ip}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) @@ -25,34 +25,37 @@ func HandleUnban(w http.ResponseWriter, r *http.Request) { } } -func handleUnbanImpl(w http.ResponseWriter, r *http.Request) string { +func handleUnbanImpl(w http.ResponseWriter, r *http.Request) (string, string) { // TODO: Actual authentication rather than a fixed secret // TODO: Use POST instead of GET u, err := url.Parse(r.URL.String()) if err != nil { - return "Bad request" + return "Bad request", "" } query, err := url.ParseQuery(u.RawQuery) if err != nil { - return "Bad request" + return "Bad request", "" } if apiSecret == "" || query.Get("secret") != apiSecret { - return "Invalid API secret" + return "Invalid API secret", "" } pidStr := query.Get("pid") if pidStr == "" { - return "Missing pid in request" + return "Missing pid in request", "" } pid, err := strconv.ParseUint(pidStr, 10, 32) if err != nil { - return "Invalid pid" + return "Invalid pid", "" } database.UnbanUser(pool, ctx, uint32(pid)) - return "" + + ip := database.GetUserIP(pool, ctx, uint32(pid)) + + return "", ip } diff --git a/database/user.go b/database/user.go index 7fc2a77..691a772 100644 --- a/database/user.go +++ b/database/user.go @@ -16,6 +16,7 @@ const ( 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` + GetUserLastIPAddress = `SELECT last_ip_address FROM users WHERE profile_id = $1` 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` @@ -147,6 +148,16 @@ func UnbanUser(pool *pgxpool.Pool, ctx context.Context, profileId uint32) bool { return err == nil } +func GetUserIP(pool *pgxpool.Pool, ctx context.Context, profileId uint32) string { + var ip string + err := pool.QueryRow(ctx, GetUserLastIPAddress, profileId).Scan(&ip) + if err != nil { + return "Unknown" + } + + return ip +} + func GetMKWFriendInfo(pool *pgxpool.Pool, ctx context.Context, profileId uint32) string { var info string err := pool.QueryRow(ctx, GetMKWFriendInfoQuery, profileId).Scan(&info) From d1dd5816c2428cfd91defd1c6ca45f4de66e2927 Mon Sep 17 00:00:00 2001 From: ppeb Date: Sun, 25 Aug 2024 15:11:07 -0500 Subject: [PATCH 02/11] Rework ban, kick, unban APIs. Add motd API. --- api/ban.go | 142 +++++++++++++++++++++------------------------------ api/kick.go | 74 +++++++++++++++------------ api/main.go | 17 ++++++ api/motd.go | 78 ++++++++++++++++++++++++++++ api/unban.go | 74 +++++++++++++++------------ gpcm/motd.go | 25 +++++++-- nas/main.go | 5 ++ 7 files changed, 262 insertions(+), 153 deletions(-) create mode 100644 api/motd.go diff --git a/api/ban.go b/api/ban.go index 3fd177d..ea5ee2a 100644 --- a/api/ban.go +++ b/api/ban.go @@ -2,8 +2,8 @@ package api import ( "encoding/json" + "io" "net/http" - "net/url" "strconv" "time" "wwfc/database" @@ -11,118 +11,92 @@ import ( ) func HandleBan(w http.ResponseWriter, r *http.Request) { - errorString, ip := 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 jsonData map[string]string + var statusCode int + + switch r.Method { + case http.MethodHead: + statusCode = http.StatusOK + case http.MethodPost: + jsonData, statusCode = handleBanImpl(w, r) + default: + jsonData = mmss("error", "Incorrect request. POST or HEAD only.") + statusCode = http.StatusBadRequest + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if len(jsonData) == 0 { + w.WriteHeader(statusCode) } else { - jsonData, _ := json.Marshal(map[string]string{"success": "true", "ip": ip}) - 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) + json, _ := json.Marshal(jsonData) + w.Header().Set("Content-Length", strconv.Itoa(len(json))) + w.WriteHeader(statusCode) + w.Write(json) } } -func handleBanImpl(w http.ResponseWriter, r *http.Request) (string, string) { +type BanRequestSpec struct { + Secret string + Pid uint32 + Days uint64 + Hours uint64 + Minutes uint64 + Tos bool + Reason string + ReasonHidden string + Moderator string +} + +func handleBanImpl(w http.ResponseWriter, r *http.Request) (map[string]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 mmss("error", "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 mmss("error", err.Error()), http.StatusBadRequest } - if apiSecret == "" || query.Get("secret") != apiSecret { - return "Invalid API secret", "" + if apiSecret == "" || req.Secret != apiSecret { + return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized } - pidStr := query.Get("pid") - if pidStr == "" { - return "Missing pid in request", "" + if req.Pid == 0 { + return mmss("error", "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 mmss("error", "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 mmss("error", "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 mmss("error", "Failed to ban user"), http.StatusInternalServerError } - if tos { - gpcm.KickPlayer(uint32(pid), "banned") + if req.Tos { + gpcm.KickPlayer(req.Pid, "banned") } else { - gpcm.KickPlayer(uint32(pid), "restricted") + gpcm.KickPlayer(req.Pid, "restricted") } - ip := database.GetUserIP(pool, ctx, uint32(pid)) - - return "", ip + ip := database.GetUserIP(pool, ctx, req.Pid) + return mmss("result", "success", "ip", ip), http.StatusOK } diff --git a/api/kick.go b/api/kick.go index bd9eb47..0090c28 100644 --- a/api/kick.go +++ b/api/kick.go @@ -2,61 +2,69 @@ package api import ( "encoding/json" + "io" "net/http" - "net/url" "strconv" "wwfc/database" "wwfc/gpcm" ) func HandleKick(w http.ResponseWriter, r *http.Request) { - errorString, ip := 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 jsonData map[string]string + var statusCode int + + switch r.Method { + case http.MethodHead: + statusCode = http.StatusOK + case http.MethodPost: + jsonData, statusCode = handleKickImpl(w, r) + default: + jsonData = mmss("error", "Incorrect request. POST or HEAD only.") + statusCode = http.StatusBadRequest + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if len(jsonData) == 0 { + w.WriteHeader(statusCode) } else { - jsonData, _ := json.Marshal(map[string]string{"success": "true", "ip": ip}) - 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) + json, _ := json.Marshal(jsonData) + w.Header().Set("Content-Length", strconv.Itoa(len(json))) + w.WriteHeader(statusCode) + w.Write(json) } } -func handleKickImpl(w http.ResponseWriter, r *http.Request) (string, string) { +type KickRequestSpec struct { + Secret string + Pid uint32 +} + +func handleKickImpl(w http.ResponseWriter, r *http.Request) (map[string]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 mmss("error", "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 mmss("error", err.Error()), http.StatusBadRequest } - if apiSecret == "" || query.Get("secret") != apiSecret { - return "Invalid API secret", "" + if apiSecret == "" || req.Secret != apiSecret { + return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized } - pidStr := query.Get("pid") - if pidStr == "" { - return "Missing pid in request", "" + if req.Pid == 0 { + return mmss("error", "pid missing or 0 in request"), http.StatusBadRequest } - pid, err := strconv.ParseUint(pidStr, 10, 32) - if err != nil { - return "Invalid pid", "" - } + gpcm.KickPlayer(req.Pid, "moderator_kick") - gpcm.KickPlayer(uint32(pid), "moderator_kick") - - ip := database.GetUserIP(pool, ctx, uint32(pid)) - - return "", ip + ip := database.GetUserIP(pool, ctx, req.Pid) + return mmss("status", "success", "ip", ip), http.StatusOK } diff --git a/api/main.go b/api/main.go index 33928eb..6f74e26 100644 --- a/api/main.go +++ b/api/main.go @@ -35,3 +35,20 @@ func StartServer(reload bool) { func Shutdown() { } + +// make map string string +func mmss(data ...string) map[string]string { + ret := make(map[string]string) + + l := len(data) + + if l%2 != 0 || l == 0 { + panic("Length of data must be divisible by two") + } + + for i := 0; i < l; i += 2 { + ret[data[i]] = data[i+1] + } + + return ret +} diff --git a/api/motd.go b/api/motd.go new file mode 100644 index 0000000..53e436b --- /dev/null +++ b/api/motd.go @@ -0,0 +1,78 @@ +package api + +import ( + "encoding/json" + "io" + "net/http" + "strconv" + "wwfc/gpcm" +) + +func HandleMotd(w http.ResponseWriter, r *http.Request) { + var jsonData map[string]string + var statusCode int + + switch r.Method { + case http.MethodHead: + statusCode = http.StatusOK + case http.MethodGet: + motd, err := gpcm.GetMessageOfTheDay() + if err != nil { + jsonData = mmss("error", err.Error()) + statusCode = http.StatusInternalServerError + break + } + + jsonData = mmss("motd", motd) + statusCode = http.StatusOK + case http.MethodPost: + jsonData, statusCode = handleMotdImpl(w, r) + default: + jsonData = mmss("error", "Incorrect request. POST, GET, or HEAD only.") + statusCode = http.StatusBadRequest + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if len(jsonData) == 0 { + w.WriteHeader(statusCode) + } else { + json, _ := json.Marshal(jsonData) + w.Header().Set("Content-Length", strconv.Itoa(len(json))) + w.WriteHeader(statusCode) + w.Write(json) + } +} + +type MotdRequestSpec struct { + Secret string + Motd string +} + +func handleMotdImpl(w http.ResponseWriter, r *http.Request) (map[string]string, int) { + // TODO: Actual authentication rather than a fixed secret + + body, err := io.ReadAll(r.Body) + if err != nil { + return mmss("error", "Unable to read request body"), http.StatusBadRequest + } + + var req MotdRequestSpec + err = json.Unmarshal(body, &req) + if err != nil { + return mmss("error", err.Error()), http.StatusBadRequest + } + + if apiSecret == "" || req.Secret != apiSecret { + return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized + } + + err = gpcm.SetMessageOfTheDay(req.Motd) + if err != nil { + return mmss("error", err.Error()), http.StatusInternalServerError + } + + // Don't return empty JSON, this is placeholder for now. + return mmss("result", "success"), http.StatusOK +} diff --git a/api/unban.go b/api/unban.go index 1e9e917..8d27c41 100644 --- a/api/unban.go +++ b/api/unban.go @@ -2,60 +2,70 @@ package api import ( "encoding/json" + "io" "net/http" - "net/url" "strconv" "wwfc/database" ) func HandleUnban(w http.ResponseWriter, r *http.Request) { - errorString, ip := 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 jsonData map[string]string + var statusCode int + + switch r.Method { + case http.MethodHead: + statusCode = http.StatusOK + case http.MethodPost: + jsonData, statusCode = handleUnbanImpl(w, r) + default: + jsonData = mmss("error", "Incorrect request. POST or HEAD only.") + statusCode = http.StatusBadRequest + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if len(jsonData) == 0 { + w.WriteHeader(statusCode) } else { - jsonData, _ := json.Marshal(map[string]string{"success": "true", "ip": ip}) - 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) + json, _ := json.Marshal(jsonData) + w.Header().Set("Content-Length", strconv.Itoa(len(json))) + w.WriteHeader(statusCode) + w.Write(json) } } -func handleUnbanImpl(w http.ResponseWriter, r *http.Request) (string, string) { +type UnbanRequestSpec struct { + Secret string + Pid uint32 +} + +func handleUnbanImpl(w http.ResponseWriter, r *http.Request) (map[string]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 mmss("error", "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 mmss("error", err.Error()), http.StatusBadRequest } - if apiSecret == "" || query.Get("secret") != apiSecret { - return "Invalid API secret", "" + if apiSecret == "" || req.Secret != apiSecret { + return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized } - pidStr := query.Get("pid") - if pidStr == "" { - return "Missing pid in request", "" + if req.Pid == 0 { + return mmss("error", "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 mmss("error", "Failed to unban user"), http.StatusInternalServerError } - database.UnbanUser(pool, ctx, uint32(pid)) - - ip := database.GetUserIP(pool, ctx, uint32(pid)) - - return "", ip + ip := database.GetUserIP(pool, ctx, req.Pid) + return mmss("result", "success", "ip", ip), http.StatusOK } diff --git a/gpcm/motd.go b/gpcm/motd.go index b0b6c04..440f1e3 100644 --- a/gpcm/motd.go +++ b/gpcm/motd.go @@ -1,16 +1,33 @@ package gpcm import ( + "errors" "os" ) var motdFilepath = "./motd.txt" +var motd string = "" func GetMessageOfTheDay() (string, error) { - contents, err := os.ReadFile(motdFilepath) - if err != nil { - return "", err + if motd == "" { + contents, err := os.ReadFile(motdFilepath) + if err != nil { + return "", err + } + + motd = string(contents) } - return string(contents), nil + return motd, nil +} + +func SetMessageOfTheDay(nmotd string) error { + if nmotd == "" { + return errors.New("Motd cannot be empty") + } + + err := os.WriteFile(motdFilepath, []byte(nmotd), 0644) + motd = nmotd + + return err } diff --git a/nas/main.go b/nas/main.go index e16f541..2916c84 100644 --- a/nas/main.go +++ b/nas/main.go @@ -168,6 +168,11 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } + if r.URL.Path == "/api/motd" { + api.HandleMotd(w, r) + return + } + 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") } From 90f38dccca1304c3d918fa3ce540b5163d57f897 Mon Sep 17 00:00:00 2001 From: ppeb Date: Sun, 25 Aug 2024 15:31:23 -0500 Subject: [PATCH 03/11] Move mmss to api/utils.go --- api/main.go | 17 ----------------- api/utils.go | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 api/utils.go diff --git a/api/main.go b/api/main.go index 6f74e26..33928eb 100644 --- a/api/main.go +++ b/api/main.go @@ -35,20 +35,3 @@ func StartServer(reload bool) { func Shutdown() { } - -// make map string string -func mmss(data ...string) map[string]string { - ret := make(map[string]string) - - l := len(data) - - if l%2 != 0 || l == 0 { - panic("Length of data must be divisible by two") - } - - for i := 0; i < l; i += 2 { - ret[data[i]] = data[i+1] - } - - return ret -} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 0000000..64e03f7 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,18 @@ +package api + +// make map string string +func mmss(data ...string) map[string]string { + ret := make(map[string]string) + + l := len(data) + + if l%2 != 0 || l == 0 { + panic("Length of data must be divisible by two") + } + + for i := 0; i < l; i += 2 { + ret[data[i]] = data[i+1] + } + + return ret +} From c0993486363985e123c3a3296c89b6bfca4e6e3a Mon Sep 17 00:00:00 2001 From: ppeb Date: Tue, 27 Aug 2024 22:20:30 -0500 Subject: [PATCH 04/11] Include User object with responses to some APIs --- api/ban.go | 44 +++++++++++++++++++++++++++++--------------- api/kick.go | 38 ++++++++++++++++++++++++++------------ api/motd.go | 39 ++++++++++++++++++++++++--------------- api/unban.go | 40 +++++++++++++++++++++++++++------------- api/utils.go | 21 +++++++-------------- database/user.go | 17 ++++------------- 6 files changed, 117 insertions(+), 82 deletions(-) diff --git a/api/ban.go b/api/ban.go index ea5ee2a..e3deecd 100644 --- a/api/ban.go +++ b/api/ban.go @@ -11,26 +11,32 @@ import ( ) func HandleBan(w http.ResponseWriter, r *http.Request) { - var jsonData map[string]string + var user *database.User + var success bool + var err string var statusCode int switch r.Method { case http.MethodHead: statusCode = http.StatusOK case http.MethodPost: - jsonData, statusCode = handleBanImpl(w, r) + user, success, err, statusCode = handleBanImpl(w, r) default: - jsonData = mmss("error", "Incorrect request. POST or HEAD only.") + err = "Incorrect request. POST or HEAD only." statusCode = http.StatusBadRequest } + if user == nil { + user = &database.User{} + } + w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if len(jsonData) == 0 { + if r.Method == http.MethodHead { w.WriteHeader(statusCode) } else { - json, _ := json.Marshal(jsonData) + json, _ := json.Marshal(UserActionResponse{*user, success, err}) w.Header().Set("Content-Length", strconv.Itoa(len(json))) w.WriteHeader(statusCode) w.Write(json) @@ -49,30 +55,30 @@ type BanRequestSpec struct { Moderator string } -func handleBanImpl(w http.ResponseWriter, r *http.Request) (map[string]string, int) { +func handleBanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return mmss("error", "Unable to read request body"), http.StatusBadRequest + return nil, false, "Unable to read request body", http.StatusBadRequest } var req BanRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return mmss("error", err.Error()), http.StatusBadRequest + return nil, false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized + return nil, false, "Invalid API secret in request", http.StatusUnauthorized } if req.Pid == 0 { - return mmss("error", "pid missing or 0 in request"), http.StatusBadRequest + return nil, false, "pid missing or 0 in request", http.StatusBadRequest } if req.Reason == "" { - return mmss("error", "Missing ban reason in request"), http.StatusBadRequest + return nil, false, "Missing ban reason in request", http.StatusBadRequest } moderator := req.Moderator @@ -82,13 +88,13 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) (map[string]string, i minutes := req.Days*24*60 + req.Hours*60 + req.Minutes if minutes == 0 { - return mmss("error", "Ban length missing or 0"), http.StatusBadRequest + return nil, false, "Ban length missing or 0", http.StatusBadRequest } length := time.Duration(minutes) * time.Minute if !database.BanUser(pool, ctx, req.Pid, req.Tos, length, req.Reason, req.ReasonHidden, moderator) { - return mmss("error", "Failed to ban user"), http.StatusInternalServerError + return nil, false, "Failed to ban user", http.StatusInternalServerError } if req.Tos { @@ -97,6 +103,14 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) (map[string]string, i gpcm.KickPlayer(req.Pid, "restricted") } - ip := database.GetUserIP(pool, ctx, req.Pid) - return mmss("result", "success", "ip", ip), http.StatusOK + var message string + user, success := database.GetProfile(pool, ctx, req.Pid) + + if success { + message = "" + } else { + message = "Unable to query user data from the database" + } + + return &user, success, message, http.StatusOK } diff --git a/api/kick.go b/api/kick.go index 0090c28..84cdf59 100644 --- a/api/kick.go +++ b/api/kick.go @@ -10,26 +10,32 @@ import ( ) func HandleKick(w http.ResponseWriter, r *http.Request) { - var jsonData map[string]string + var user *database.User + var success bool + var err string var statusCode int switch r.Method { case http.MethodHead: statusCode = http.StatusOK case http.MethodPost: - jsonData, statusCode = handleKickImpl(w, r) + user, success, err, statusCode = handleKickImpl(w, r) default: - jsonData = mmss("error", "Incorrect request. POST or HEAD only.") + err = "Incorrect request. POST or HEAD only." statusCode = http.StatusBadRequest } + if user == nil { + user = &database.User{} + } + w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if len(jsonData) == 0 { + if r.Method == http.MethodHead { w.WriteHeader(statusCode) } else { - json, _ := json.Marshal(jsonData) + json, _ := json.Marshal(UserActionResponse{*user, success, err}) w.Header().Set("Content-Length", strconv.Itoa(len(json))) w.WriteHeader(statusCode) w.Write(json) @@ -41,30 +47,38 @@ type KickRequestSpec struct { Pid uint32 } -func handleKickImpl(w http.ResponseWriter, r *http.Request) (map[string]string, int) { +func handleKickImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return mmss("error", "Unable to read request body"), http.StatusBadRequest + return nil, false, "Unable to read request body", http.StatusBadRequest } var req KickRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return mmss("error", err.Error()), http.StatusBadRequest + return nil, false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized + return nil, false, "Invalid API secret in request", http.StatusUnauthorized } if req.Pid == 0 { - return mmss("error", "pid missing or 0 in request"), http.StatusBadRequest + return nil, false, "pid missing or 0 in request", http.StatusBadRequest } gpcm.KickPlayer(req.Pid, "moderator_kick") - ip := database.GetUserIP(pool, ctx, req.Pid) - return mmss("status", "success", "ip", ip), http.StatusOK + var message string + user, success := database.GetProfile(pool, ctx, req.Pid) + + if success { + message = "" + } else { + message = "Unable to query user data from the database" + } + + return &user, success, message, http.StatusOK } diff --git a/api/motd.go b/api/motd.go index 53e436b..8170d3c 100644 --- a/api/motd.go +++ b/api/motd.go @@ -9,36 +9,39 @@ import ( ) func HandleMotd(w http.ResponseWriter, r *http.Request) { - var jsonData map[string]string + var motd string + var success bool + var err string var statusCode int switch r.Method { case http.MethodHead: statusCode = http.StatusOK case http.MethodGet: - motd, err := gpcm.GetMessageOfTheDay() - if err != nil { - jsonData = mmss("error", err.Error()) + _motd, motdErr := gpcm.GetMessageOfTheDay() + if motdErr != nil { + err = motdErr.Error() statusCode = http.StatusInternalServerError break } - jsonData = mmss("motd", motd) + motd = _motd + success = true statusCode = http.StatusOK case http.MethodPost: - jsonData, statusCode = handleMotdImpl(w, r) + success, err, statusCode = handleMotdImpl(w, r) default: - jsonData = mmss("error", "Incorrect request. POST, GET, or HEAD only.") + err = "Incorrect request. POST, GET, or HEAD only." statusCode = http.StatusBadRequest } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if len(jsonData) == 0 { + if r.Method == http.MethodHead { w.WriteHeader(statusCode) } else { - json, _ := json.Marshal(jsonData) + json, _ := json.Marshal(MotdResponse{motd, success, err}) w.Header().Set("Content-Length", strconv.Itoa(len(json))) w.WriteHeader(statusCode) w.Write(json) @@ -50,29 +53,35 @@ type MotdRequestSpec struct { Motd string } -func handleMotdImpl(w http.ResponseWriter, r *http.Request) (map[string]string, int) { +type MotdResponse struct { + Motd string + Success bool + Error string +} + +func handleMotdImpl(w http.ResponseWriter, r *http.Request) (bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return mmss("error", "Unable to read request body"), http.StatusBadRequest + return false, "Unable to read request body", http.StatusBadRequest } var req MotdRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return mmss("error", err.Error()), http.StatusBadRequest + return false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized + return false, "Invalid API secret in request", http.StatusUnauthorized } err = gpcm.SetMessageOfTheDay(req.Motd) if err != nil { - return mmss("error", err.Error()), http.StatusInternalServerError + return false, err.Error(), http.StatusInternalServerError } // Don't return empty JSON, this is placeholder for now. - return mmss("result", "success"), http.StatusOK + return true, "", http.StatusOK } diff --git a/api/unban.go b/api/unban.go index 8d27c41..f91dd25 100644 --- a/api/unban.go +++ b/api/unban.go @@ -9,26 +9,32 @@ import ( ) func HandleUnban(w http.ResponseWriter, r *http.Request) { - var jsonData map[string]string + var user *database.User + var success bool + var err string var statusCode int switch r.Method { case http.MethodHead: statusCode = http.StatusOK case http.MethodPost: - jsonData, statusCode = handleUnbanImpl(w, r) + user, success, err, statusCode = handleUnbanImpl(w, r) default: - jsonData = mmss("error", "Incorrect request. POST or HEAD only.") + err = "Incorrect request. POST or HEAD only." statusCode = http.StatusBadRequest } + if user == nil { + user = &database.User{} + } + w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if len(jsonData) == 0 { + if r.Method == http.MethodHead { w.WriteHeader(statusCode) } else { - json, _ := json.Marshal(jsonData) + json, _ := json.Marshal(UserActionResponse{*user, success, err}) w.Header().Set("Content-Length", strconv.Itoa(len(json))) w.WriteHeader(statusCode) w.Write(json) @@ -40,32 +46,40 @@ type UnbanRequestSpec struct { Pid uint32 } -func handleUnbanImpl(w http.ResponseWriter, r *http.Request) (map[string]string, int) { +func handleUnbanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return mmss("error", "Unable to read request body"), http.StatusBadRequest + return nil, false, "Unable to read request body", http.StatusBadRequest } var req UnbanRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return mmss("error", err.Error()), http.StatusBadRequest + return nil, false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return mmss("error", "Invalid API secret in request"), http.StatusUnauthorized + return nil, false, "Invalid API secret in request", http.StatusUnauthorized } if req.Pid == 0 { - return mmss("error", "pid missing or 0 in request"), http.StatusBadRequest + return nil, false, "pid missing or 0 in request", http.StatusBadRequest } if !database.UnbanUser(pool, ctx, req.Pid) { - return mmss("error", "Failed to unban user"), http.StatusInternalServerError + return nil, false, "Failed to unban user", http.StatusInternalServerError } - ip := database.GetUserIP(pool, ctx, req.Pid) - return mmss("result", "success", "ip", ip), http.StatusOK + var message string + user, success := database.GetProfile(pool, ctx, req.Pid) + + if success { + message = "" + } else { + message = "Unable to query user data from the database" + } + + return &user, success, message, http.StatusOK } diff --git a/api/utils.go b/api/utils.go index 64e03f7..0baa852 100644 --- a/api/utils.go +++ b/api/utils.go @@ -1,18 +1,11 @@ package api -// make map string string -func mmss(data ...string) map[string]string { - ret := make(map[string]string) +import ( + "wwfc/database" +) - l := len(data) - - if l%2 != 0 || l == 0 { - panic("Length of data must be divisible by two") - } - - for i := 0; i < l; i += 2 { - ret[data[i]] = data[i+1] - } - - return ret +type UserActionResponse struct { + User database.User + Success bool + Error string } diff --git a/database/user.go b/database/user.go index 691a772..aba2086 100644 --- a/database/user.go +++ b/database/user.go @@ -15,8 +15,7 @@ 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` - GetUserLastIPAddress = `SELECT last_ip_address 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` 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` @@ -41,6 +40,8 @@ type User struct { Restricted bool RestrictedDeviceId uint32 OpenHost bool + LastInGameSn string + LastIPAddress string } var ( @@ -129,7 +130,7 @@ 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 } @@ -148,16 +149,6 @@ func UnbanUser(pool *pgxpool.Pool, ctx context.Context, profileId uint32) bool { return err == nil } -func GetUserIP(pool *pgxpool.Pool, ctx context.Context, profileId uint32) string { - var ip string - err := pool.QueryRow(ctx, GetUserLastIPAddress, profileId).Scan(&ip) - if err != nil { - return "Unknown" - } - - return ip -} - func GetMKWFriendInfo(pool *pgxpool.Pool, ctx context.Context, profileId uint32) string { var info string err := pool.QueryRow(ctx, GetMKWFriendInfoQuery, profileId).Scan(&info) From 3099499868f2f647980c157a1b910353c3b94a8b Mon Sep 17 00:00:00 2001 From: ppeb Date: Tue, 27 Aug 2024 22:48:05 -0500 Subject: [PATCH 05/11] Rename utils.go to user_action_response.go for clarity --- api/{utils.go => user_action_response.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/{utils.go => user_action_response.go} (100%) diff --git a/api/utils.go b/api/user_action_response.go similarity index 100% rename from api/utils.go rename to api/user_action_response.go From 64e007631f6535cc088429a5751265acdd8b31c6 Mon Sep 17 00:00:00 2001 From: ppeb Date: Mon, 16 Sep 2024 00:32:34 -0500 Subject: [PATCH 06/11] Add /api/clear to drop user from database --- api/clear.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ database/user.go | 14 +++++++++ nas/main.go | 5 ++++ 3 files changed, 94 insertions(+) create mode 100644 api/clear.go diff --git a/api/clear.go b/api/clear.go new file mode 100644 index 0000000..1d4b97a --- /dev/null +++ b/api/clear.go @@ -0,0 +1,75 @@ +package api + +import ( + "encoding/json" + "io" + "net/http" + "strconv" + "wwfc/database" +) + +func HandleClear(w http.ResponseWriter, r *http.Request) { + var user *database.User + var success bool + var err string + var statusCode int + + switch r.Method { + case http.MethodHead: + statusCode = http.StatusOK + case http.MethodPost: + user, success, err, statusCode = handleClearImpl(w, r) + default: + err = "Incorrect request. POST or HEAD only." + statusCode = http.StatusBadRequest + } + + if user == nil { + user = &database.User{} + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if r.Method == http.MethodHead { + w.WriteHeader(statusCode) + } else { + json, _ := json.Marshal(UserActionResponse{*user, success, err}) + w.Header().Set("Content-Length", strconv.Itoa(len(json))) + w.WriteHeader(statusCode) + w.Write(json) + } +} + +type ClearRequestSpec struct { + Secret string + Pid uint32 +} + +func handleClearImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { + // TODO: Actual authentication rather than a fixed secret + + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, false, "Unable to read request body", http.StatusBadRequest + } + + var req ClearRequestSpec + err = json.Unmarshal(body, &req) + if err != nil { + return nil, false, err.Error(), http.StatusBadRequest + } + + if apiSecret == "" || req.Secret != apiSecret { + return nil, false, "Invalid API secret in request", http.StatusUnauthorized + } + + user, success := database.ClearProfile(pool, ctx, req.Pid) + + if !success { + return nil, false, "Unable to query user data from the database", http.StatusInternalServerError + } + + // Don't return empty JSON, this is placeholder for now. + return &user, true, "", http.StatusOK +} diff --git a/database/user.go b/database/user.go index aba2086..4072a60 100644 --- a/database/user.go +++ b/database/user.go @@ -16,6 +16,7 @@ const ( 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, last_ip_address, last_ingamesn FROM users WHERE profile_id = $1` + DeleteUser = `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` @@ -139,6 +140,19 @@ func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User return user, true } +func ClearProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) { + user := User{} + row := pool.QueryRow(ctx, DeleteUser, 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 + } + + user.ProfileId = profileId + return user, true +} + func BanUser(pool *pgxpool.Pool, ctx context.Context, profileId uint32, tos bool, length time.Duration, reason string, reasonHidden string, moderator string) bool { _, err := pool.Exec(ctx, UpdateUserBan, profileId, time.Now(), time.Now().Add(length), reason, reasonHidden, moderator, tos) return err == nil diff --git a/nas/main.go b/nas/main.go index 2916c84..8e9c95c 100644 --- a/nas/main.go +++ b/nas/main.go @@ -173,6 +173,11 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } + if r.URL.Path == "/api/clear" { + api.HandleClear(w, r) + return + } + 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") } From e12944ccab5d90ca30d7cd9770cee682627fd3a1 Mon Sep 17 00:00:00 2001 From: ppeb Date: Mon, 16 Sep 2024 01:17:31 -0500 Subject: [PATCH 07/11] Remove HEAD support, change sql query name --- api/ban.go | 7 ++----- api/clear.go | 9 +++------ api/kick.go | 7 ++----- api/motd.go | 2 -- api/unban.go | 7 ++----- database/user.go | 4 ++-- 6 files changed, 11 insertions(+), 25 deletions(-) diff --git a/api/ban.go b/api/ban.go index e3deecd..125eab8 100644 --- a/api/ban.go +++ b/api/ban.go @@ -16,12 +16,9 @@ func HandleBan(w http.ResponseWriter, r *http.Request) { var err string var statusCode int - switch r.Method { - case http.MethodHead: - statusCode = http.StatusOK - case http.MethodPost: + if r.Method == http.MethodPost { user, success, err, statusCode = handleBanImpl(w, r) - default: + } else { err = "Incorrect request. POST or HEAD only." statusCode = http.StatusBadRequest } diff --git a/api/clear.go b/api/clear.go index 1d4b97a..77d8024 100644 --- a/api/clear.go +++ b/api/clear.go @@ -14,13 +14,10 @@ func HandleClear(w http.ResponseWriter, r *http.Request) { var err string var statusCode int - switch r.Method { - case http.MethodHead: - statusCode = http.StatusOK - case http.MethodPost: + if r.Method == http.MethodPost { user, success, err, statusCode = handleClearImpl(w, r) - default: - err = "Incorrect request. POST or HEAD only." + } else { + err = "Incorrect request. POST only." statusCode = http.StatusBadRequest } diff --git a/api/kick.go b/api/kick.go index 84cdf59..64d1e72 100644 --- a/api/kick.go +++ b/api/kick.go @@ -15,12 +15,9 @@ func HandleKick(w http.ResponseWriter, r *http.Request) { var err string var statusCode int - switch r.Method { - case http.MethodHead: - statusCode = http.StatusOK - case http.MethodPost: + if r.Method == http.MethodPost { user, success, err, statusCode = handleKickImpl(w, r) - default: + } else { err = "Incorrect request. POST or HEAD only." statusCode = http.StatusBadRequest } diff --git a/api/motd.go b/api/motd.go index 8170d3c..025b819 100644 --- a/api/motd.go +++ b/api/motd.go @@ -15,8 +15,6 @@ func HandleMotd(w http.ResponseWriter, r *http.Request) { var statusCode int switch r.Method { - case http.MethodHead: - statusCode = http.StatusOK case http.MethodGet: _motd, motdErr := gpcm.GetMessageOfTheDay() if motdErr != nil { diff --git a/api/unban.go b/api/unban.go index f91dd25..375d5c3 100644 --- a/api/unban.go +++ b/api/unban.go @@ -14,12 +14,9 @@ func HandleUnban(w http.ResponseWriter, r *http.Request) { var err string var statusCode int - switch r.Method { - case http.MethodHead: - statusCode = http.StatusOK - case http.MethodPost: + if r.Method == http.MethodPost { user, success, err, statusCode = handleUnbanImpl(w, r) - default: + } else { err = "Incorrect request. POST or HEAD only." statusCode = http.StatusBadRequest } diff --git a/database/user.go b/database/user.go index 4072a60..9bec502 100644 --- a/database/user.go +++ b/database/user.go @@ -16,7 +16,7 @@ const ( 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, last_ip_address, last_ingamesn FROM users WHERE profile_id = $1` - DeleteUser = `DELETE FROM users WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn` + 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` @@ -142,7 +142,7 @@ func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User func ClearProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) { user := User{} - row := pool.QueryRow(ctx, DeleteUser, profileId) + 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 { From df1c6a83fce428772aa4a3042705f3ce65374e92 Mon Sep 17 00:00:00 2001 From: ppeb Date: Mon, 16 Sep 2024 02:33:43 -0500 Subject: [PATCH 08/11] Send kick and ban reasons to players --- api/ban.go | 15 ++++++++++----- api/kick.go | 16 +++++++++++++++- database/login.go | 1 + database/user.go | 1 + gpcm/kick.go | 15 +++++++++++++++ gpcm/login.go | 11 ++++++++++- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/api/ban.go b/api/ban.go index 125eab8..411fcb9 100644 --- a/api/ban.go +++ b/api/ban.go @@ -94,11 +94,16 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool return nil, false, "Failed to ban user", http.StatusInternalServerError } - if req.Tos { - gpcm.KickPlayer(req.Pid, "banned") - } else { - gpcm.KickPlayer(req.Pid, "restricted") - } + gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCErrorMessage{ + ErrorCode: 22002, + MessageRMC: map[byte]string{ + gpcm.LangEnglish: "" + + "You have been banned from Retro WFC\n" + + "Reason: " + req.Reason + "\n" + + "Error Code: %[1]d\n" + + "Support Info: NG%08[2]x", + }, + }) var message string user, success := database.GetProfile(pool, ctx, req.Pid) diff --git a/api/kick.go b/api/kick.go index 64d1e72..6990e9f 100644 --- a/api/kick.go +++ b/api/kick.go @@ -41,6 +41,7 @@ func HandleKick(w http.ResponseWriter, r *http.Request) { type KickRequestSpec struct { Secret string + Reason string Pid uint32 } @@ -66,7 +67,20 @@ func handleKickImpl(w http.ResponseWriter, r *http.Request) (*database.User, boo return nil, false, "pid missing or 0 in request", http.StatusBadRequest } - gpcm.KickPlayer(req.Pid, "moderator_kick") + if req.Reason == "" { + return nil, false, "Missing kick reason in request", http.StatusBadRequest + } + + gpcm.KickPlayerCustomMessage(req.Pid, "moderator_kick", gpcm.WWFCErrorMessage{ + ErrorCode: 22004, + MessageRMC: map[byte]string{ + gpcm.LangEnglish: "" + + "You have been kicked from\n" + + "Retro WFC by a moderator.\n" + + "Reason: " + req.Reason + "\n" + + "Error Code: %[1]d", + }, + }) var message string user, success := database.GetProfile(pool, ctx, req.Pid) diff --git a/database/login.go b/database/login.go index f0c2418..c152987 100644 --- a/database/login.go +++ b/database/login.go @@ -172,6 +172,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb var bannedDeviceIdList []uint32 timeNow := time.Now() err = pool.QueryRow(ctx, SearchUserBan, user.NgDeviceId, user.ProfileId, ipAddress, *lastIPAddress, timeNow).Scan(&banExists, &banTOS, &bannedDeviceIdList) + if err != nil { if err != pgx.ErrNoRows { return User{}, err diff --git a/database/user.go b/database/user.go index 9bec502..c268a9b 100644 --- a/database/user.go +++ b/database/user.go @@ -40,6 +40,7 @@ type User struct { LastName string Restricted bool RestrictedDeviceId uint32 + BanReason string OpenHost bool LastInGameSn string LastIPAddress string diff --git a/gpcm/kick.go b/gpcm/kick.go index 8a3c712..080380c 100644 --- a/gpcm/kick.go +++ b/gpcm/kick.go @@ -49,3 +49,18 @@ func KickPlayer(profileID uint32, reason string) { kickPlayer(profileID, reason) } + +// Exists because the above function is used in too many places to be updated easily +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, + }) + } +} diff --git a/gpcm/login.go b/gpcm/login.go index 7b931b6..49080ab 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -476,7 +476,16 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string ErrorCode: ErrLogin.ErrorCode, ErrorString: "The profile is banned from the service.", Fatal: true, - WWFCMessage: WWFCMsgProfileBannedTOS, + WWFCMessage: WWFCErrorMessage{ + ErrorCode: 22002, + MessageRMC: map[byte]string{ + LangEnglish: "" + + "You are banned from Retro WFC\n" + + "Reason: " + user.BanReason + "\n" + + "Error Code: %[1]d\n" + + "Support Info: NG%08[2]x", + }, + }, }) } else { g.replyError(GPError{ From ea56689f08b0ea295b7a58b7dc7ead58b0470919 Mon Sep 17 00:00:00 2001 From: ppeb Date: Sun, 2 Mar 2025 23:45:45 -0600 Subject: [PATCH 09/11] Fix missing ban reason dropped during merge --- api/ban.go | 2 +- api/kick.go | 2 +- database/login.go | 9 ++++++--- gpcm/login.go | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/api/ban.go b/api/ban.go index 411fcb9..b6efc0e 100644 --- a/api/ban.go +++ b/api/ban.go @@ -98,7 +98,7 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool ErrorCode: 22002, MessageRMC: map[byte]string{ gpcm.LangEnglish: "" + - "You have been banned from Retro WFC\n" + + "You have been banned from WiiLink WFC\n" + "Reason: " + req.Reason + "\n" + "Error Code: %[1]d\n" + "Support Info: NG%08[2]x", diff --git a/api/kick.go b/api/kick.go index 6990e9f..38c15f1 100644 --- a/api/kick.go +++ b/api/kick.go @@ -76,7 +76,7 @@ func handleKickImpl(w http.ResponseWriter, r *http.Request) (*database.User, boo MessageRMC: map[byte]string{ gpcm.LangEnglish: "" + "You have been kicked from\n" + - "Retro WFC by a moderator.\n" + + "WiiLink WFC by a moderator.\n" + "Reason: " + req.Reason + "\n" + "Error Code: %[1]d", }, diff --git a/database/login.go b/database/login.go index c152987..08f51c4 100644 --- a/database/login.go +++ b/database/login.go @@ -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,10 @@ 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 { @@ -203,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 diff --git a/gpcm/login.go b/gpcm/login.go index 49080ab..4c27307 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -480,7 +480,7 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string ErrorCode: 22002, MessageRMC: map[byte]string{ LangEnglish: "" + - "You are banned from Retro WFC\n" + + "You are banned from WiiLink WFC\n" + "Reason: " + user.BanReason + "\n" + "Error Code: %[1]d\n" + "Support Info: NG%08[2]x", From 3144bd7cf8ac48d5339ef07942e932ad7d4233de Mon Sep 17 00:00:00 2001 From: ppeb Date: Mon, 3 Mar 2025 19:33:41 -0600 Subject: [PATCH 10/11] Revert motd, clear, and user action response --- api/ban.go | 50 +++++++++------------- api/clear.go | 72 ------------------------------- api/kick.go | 47 ++++++++------------ api/motd.go | 85 ------------------------------------- api/unban.go | 46 ++++++++------------ api/user_action_response.go | 11 ----- gpcm/motd.go | 24 ++--------- nas/main.go | 10 ----- 8 files changed, 57 insertions(+), 288 deletions(-) delete mode 100644 api/clear.go delete mode 100644 api/motd.go delete mode 100644 api/user_action_response.go diff --git a/api/ban.go b/api/ban.go index b6efc0e..07f2f47 100644 --- a/api/ban.go +++ b/api/ban.go @@ -11,33 +11,30 @@ import ( ) func HandleBan(w http.ResponseWriter, r *http.Request) { - var user *database.User var success bool var err string var statusCode int if r.Method == http.MethodPost { - user, success, err, statusCode = handleBanImpl(w, r) + success, err, statusCode = handleBanImpl(r) } else { - err = "Incorrect request. POST or HEAD only." + err = "Incorrect request. POST only." statusCode = http.StatusBadRequest } - if user == nil { - user = &database.User{} - } - w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if r.Method == http.MethodHead { - w.WriteHeader(statusCode) + var jsonData []byte + if success { + jsonData, _ = json.Marshal(map[string]string{"success": "true"}) } else { - json, _ := json.Marshal(UserActionResponse{*user, success, err}) - w.Header().Set("Content-Length", strconv.Itoa(len(json))) - w.WriteHeader(statusCode) - w.Write(json) + jsonData, _ = json.Marshal(map[string]string{"error": err}) } + + w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) + w.WriteHeader(statusCode) + w.Write(jsonData) } type BanRequestSpec struct { @@ -52,30 +49,30 @@ type BanRequestSpec struct { Moderator string } -func handleBanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { +func handleBanImpl(r *http.Request) (bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return nil, false, "Unable to read request body", http.StatusBadRequest + return false, "Unable to read request body", http.StatusBadRequest } var req BanRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return nil, false, err.Error(), http.StatusBadRequest + return false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return nil, false, "Invalid API secret in request", http.StatusUnauthorized + return false, "Invalid API secret in request", http.StatusUnauthorized } if req.Pid == 0 { - return nil, false, "pid missing or 0 in request", http.StatusBadRequest + return false, "pid missing or 0 in request", http.StatusBadRequest } if req.Reason == "" { - return nil, false, "Missing ban reason in request", http.StatusBadRequest + return false, "Missing ban reason in request", http.StatusBadRequest } moderator := req.Moderator @@ -85,13 +82,13 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool minutes := req.Days*24*60 + req.Hours*60 + req.Minutes if minutes == 0 { - return nil, false, "Ban length missing or 0", http.StatusBadRequest + return false, "Ban length missing or 0", http.StatusBadRequest } length := time.Duration(minutes) * time.Minute if !database.BanUser(pool, ctx, req.Pid, req.Tos, length, req.Reason, req.ReasonHidden, moderator) { - return nil, false, "Failed to ban user", http.StatusInternalServerError + return false, "Failed to ban user", http.StatusInternalServerError } gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCErrorMessage{ @@ -105,14 +102,5 @@ func handleBanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool }, }) - var message string - user, success := database.GetProfile(pool, ctx, req.Pid) - - if success { - message = "" - } else { - message = "Unable to query user data from the database" - } - - return &user, success, message, http.StatusOK + return true, "", http.StatusOK } diff --git a/api/clear.go b/api/clear.go deleted file mode 100644 index 77d8024..0000000 --- a/api/clear.go +++ /dev/null @@ -1,72 +0,0 @@ -package api - -import ( - "encoding/json" - "io" - "net/http" - "strconv" - "wwfc/database" -) - -func HandleClear(w http.ResponseWriter, r *http.Request) { - var user *database.User - var success bool - var err string - var statusCode int - - if r.Method == http.MethodPost { - user, success, err, statusCode = handleClearImpl(w, r) - } else { - err = "Incorrect request. POST only." - statusCode = http.StatusBadRequest - } - - if user == nil { - user = &database.User{} - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - - if r.Method == http.MethodHead { - w.WriteHeader(statusCode) - } else { - json, _ := json.Marshal(UserActionResponse{*user, success, err}) - w.Header().Set("Content-Length", strconv.Itoa(len(json))) - w.WriteHeader(statusCode) - w.Write(json) - } -} - -type ClearRequestSpec struct { - Secret string - Pid uint32 -} - -func handleClearImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { - // TODO: Actual authentication rather than a fixed secret - - body, err := io.ReadAll(r.Body) - if err != nil { - return nil, false, "Unable to read request body", http.StatusBadRequest - } - - var req ClearRequestSpec - err = json.Unmarshal(body, &req) - if err != nil { - return nil, false, err.Error(), http.StatusBadRequest - } - - if apiSecret == "" || req.Secret != apiSecret { - return nil, false, "Invalid API secret in request", http.StatusUnauthorized - } - - user, success := database.ClearProfile(pool, ctx, req.Pid) - - if !success { - return nil, false, "Unable to query user data from the database", http.StatusInternalServerError - } - - // Don't return empty JSON, this is placeholder for now. - return &user, true, "", http.StatusOK -} diff --git a/api/kick.go b/api/kick.go index 38c15f1..c9b5c51 100644 --- a/api/kick.go +++ b/api/kick.go @@ -5,38 +5,34 @@ import ( "io" "net/http" "strconv" - "wwfc/database" "wwfc/gpcm" ) func HandleKick(w http.ResponseWriter, r *http.Request) { - var user *database.User var success bool var err string var statusCode int if r.Method == http.MethodPost { - user, success, err, statusCode = handleKickImpl(w, r) + success, err, statusCode = handleKickImpl(r) } else { - err = "Incorrect request. POST or HEAD only." + err = "Incorrect request. POST only." statusCode = http.StatusBadRequest } - if user == nil { - user = &database.User{} - } - w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if r.Method == http.MethodHead { - w.WriteHeader(statusCode) + var jsonData []byte + if success { + jsonData, _ = json.Marshal(map[string]string{"success": "true"}) } else { - json, _ := json.Marshal(UserActionResponse{*user, success, err}) - w.Header().Set("Content-Length", strconv.Itoa(len(json))) - w.WriteHeader(statusCode) - w.Write(json) + jsonData, _ = json.Marshal(map[string]string{"error": err}) } + + w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) + w.WriteHeader(statusCode) + w.Write(jsonData) } type KickRequestSpec struct { @@ -45,30 +41,30 @@ type KickRequestSpec struct { Pid uint32 } -func handleKickImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { +func handleKickImpl(r *http.Request) (bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return nil, false, "Unable to read request body", http.StatusBadRequest + return false, "Unable to read request body", http.StatusBadRequest } var req KickRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return nil, false, err.Error(), http.StatusBadRequest + return false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return nil, false, "Invalid API secret in request", http.StatusUnauthorized + return false, "Invalid API secret in request", http.StatusUnauthorized } if req.Pid == 0 { - return nil, false, "pid missing or 0 in request", http.StatusBadRequest + return false, "pid missing or 0 in request", http.StatusBadRequest } if req.Reason == "" { - return nil, false, "Missing kick reason in request", http.StatusBadRequest + return false, "Missing kick reason in request", http.StatusBadRequest } gpcm.KickPlayerCustomMessage(req.Pid, "moderator_kick", gpcm.WWFCErrorMessage{ @@ -82,14 +78,5 @@ func handleKickImpl(w http.ResponseWriter, r *http.Request) (*database.User, boo }, }) - var message string - user, success := database.GetProfile(pool, ctx, req.Pid) - - if success { - message = "" - } else { - message = "Unable to query user data from the database" - } - - return &user, success, message, http.StatusOK + return true, "", http.StatusOK } diff --git a/api/motd.go b/api/motd.go deleted file mode 100644 index 025b819..0000000 --- a/api/motd.go +++ /dev/null @@ -1,85 +0,0 @@ -package api - -import ( - "encoding/json" - "io" - "net/http" - "strconv" - "wwfc/gpcm" -) - -func HandleMotd(w http.ResponseWriter, r *http.Request) { - var motd string - var success bool - var err string - var statusCode int - - switch r.Method { - case http.MethodGet: - _motd, motdErr := gpcm.GetMessageOfTheDay() - if motdErr != nil { - err = motdErr.Error() - statusCode = http.StatusInternalServerError - break - } - - motd = _motd - success = true - statusCode = http.StatusOK - case http.MethodPost: - success, err, statusCode = handleMotdImpl(w, r) - default: - err = "Incorrect request. POST, GET, or HEAD only." - statusCode = http.StatusBadRequest - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - - if r.Method == http.MethodHead { - w.WriteHeader(statusCode) - } else { - json, _ := json.Marshal(MotdResponse{motd, success, err}) - w.Header().Set("Content-Length", strconv.Itoa(len(json))) - w.WriteHeader(statusCode) - w.Write(json) - } -} - -type MotdRequestSpec struct { - Secret string - Motd string -} - -type MotdResponse struct { - Motd string - Success bool - Error string -} - -func handleMotdImpl(w http.ResponseWriter, 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 MotdRequestSpec - 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 - } - - err = gpcm.SetMessageOfTheDay(req.Motd) - if err != nil { - return false, err.Error(), http.StatusInternalServerError - } - - // Don't return empty JSON, this is placeholder for now. - return true, "", http.StatusOK -} diff --git a/api/unban.go b/api/unban.go index 375d5c3..72081d6 100644 --- a/api/unban.go +++ b/api/unban.go @@ -9,33 +9,30 @@ import ( ) func HandleUnban(w http.ResponseWriter, r *http.Request) { - var user *database.User var success bool var err string var statusCode int if r.Method == http.MethodPost { - user, success, err, statusCode = handleUnbanImpl(w, r) + success, err, statusCode = handleUnbanImpl(r) } else { - err = "Incorrect request. POST or HEAD only." + err = "Incorrect request. POST only." statusCode = http.StatusBadRequest } - if user == nil { - user = &database.User{} - } - w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - if r.Method == http.MethodHead { - w.WriteHeader(statusCode) + var jsonData []byte + if success { + jsonData, _ = json.Marshal(map[string]string{"success": "true"}) } else { - json, _ := json.Marshal(UserActionResponse{*user, success, err}) - w.Header().Set("Content-Length", strconv.Itoa(len(json))) - w.WriteHeader(statusCode) - w.Write(json) + jsonData, _ = json.Marshal(map[string]string{"error": err}) } + + w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) + w.WriteHeader(statusCode) + w.Write(jsonData) } type UnbanRequestSpec struct { @@ -43,40 +40,31 @@ type UnbanRequestSpec struct { Pid uint32 } -func handleUnbanImpl(w http.ResponseWriter, r *http.Request) (*database.User, bool, string, int) { +func handleUnbanImpl(r *http.Request) (bool, string, int) { // TODO: Actual authentication rather than a fixed secret body, err := io.ReadAll(r.Body) if err != nil { - return nil, false, "Unable to read request body", http.StatusBadRequest + return false, "Unable to read request body", http.StatusBadRequest } var req UnbanRequestSpec err = json.Unmarshal(body, &req) if err != nil { - return nil, false, err.Error(), http.StatusBadRequest + return false, err.Error(), http.StatusBadRequest } if apiSecret == "" || req.Secret != apiSecret { - return nil, false, "Invalid API secret in request", http.StatusUnauthorized + return false, "Invalid API secret in request", http.StatusUnauthorized } if req.Pid == 0 { - return nil, false, "pid missing or 0 in request", http.StatusBadRequest + return false, "pid missing or 0 in request", http.StatusBadRequest } if !database.UnbanUser(pool, ctx, req.Pid) { - return nil, false, "Failed to unban user", http.StatusInternalServerError + return false, "Failed to unban user", http.StatusInternalServerError } - var message string - user, success := database.GetProfile(pool, ctx, req.Pid) - - if success { - message = "" - } else { - message = "Unable to query user data from the database" - } - - return &user, success, message, http.StatusOK + return true, "", http.StatusOK } diff --git a/api/user_action_response.go b/api/user_action_response.go deleted file mode 100644 index 0baa852..0000000 --- a/api/user_action_response.go +++ /dev/null @@ -1,11 +0,0 @@ -package api - -import ( - "wwfc/database" -) - -type UserActionResponse struct { - User database.User - Success bool - Error string -} diff --git a/gpcm/motd.go b/gpcm/motd.go index 440f1e3..8bf4d2a 100644 --- a/gpcm/motd.go +++ b/gpcm/motd.go @@ -1,33 +1,17 @@ package gpcm import ( - "errors" "os" ) var motdFilepath = "./motd.txt" -var motd string = "" func GetMessageOfTheDay() (string, error) { - if motd == "" { - contents, err := os.ReadFile(motdFilepath) - if err != nil { - return "", err - } - - motd = string(contents) + contents, err := os.ReadFile(motdFilepath) + if err != nil { + return "", err } - return motd, nil + return string(contents), nil } -func SetMessageOfTheDay(nmotd string) error { - if nmotd == "" { - return errors.New("Motd cannot be empty") - } - - err := os.WriteFile(motdFilepath, []byte(nmotd), 0644) - motd = nmotd - - return err -} diff --git a/nas/main.go b/nas/main.go index 8e9c95c..e16f541 100644 --- a/nas/main.go +++ b/nas/main.go @@ -168,16 +168,6 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } - if r.URL.Path == "/api/motd" { - api.HandleMotd(w, r) - return - } - - if r.URL.Path == "/api/clear" { - api.HandleClear(w, r) - return - } - 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") } From 95360023a555ed4b6e762d4f27638c5318c2df68 Mon Sep 17 00:00:00 2001 From: ppeb Date: Mon, 3 Mar 2025 20:23:48 -0600 Subject: [PATCH 11/11] Move custom kick and ban error messages to error.go --- api/ban.go | 11 +---------- api/kick.go | 11 +---------- gpcm/error.go | 25 ++++++++++++++++++++++++- gpcm/kick.go | 2 +- gpcm/login.go | 14 +++----------- 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/api/ban.go b/api/ban.go index 07f2f47..b34266c 100644 --- a/api/ban.go +++ b/api/ban.go @@ -91,16 +91,7 @@ func handleBanImpl(r *http.Request) (bool, string, int) { return false, "Failed to ban user", http.StatusInternalServerError } - gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCErrorMessage{ - ErrorCode: 22002, - MessageRMC: map[byte]string{ - gpcm.LangEnglish: "" + - "You have been banned from WiiLink WFC\n" + - "Reason: " + req.Reason + "\n" + - "Error Code: %[1]d\n" + - "Support Info: NG%08[2]x", - }, - }) + gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCMsgProfileRestrictedCustom) return true, "", http.StatusOK } diff --git a/api/kick.go b/api/kick.go index c9b5c51..43228cd 100644 --- a/api/kick.go +++ b/api/kick.go @@ -67,16 +67,7 @@ func handleKickImpl(r *http.Request) (bool, string, int) { return false, "Missing kick reason in request", http.StatusBadRequest } - gpcm.KickPlayerCustomMessage(req.Pid, "moderator_kick", gpcm.WWFCErrorMessage{ - ErrorCode: 22004, - MessageRMC: map[byte]string{ - gpcm.LangEnglish: "" + - "You have been kicked from\n" + - "WiiLink WFC by a moderator.\n" + - "Reason: " + req.Reason + "\n" + - "Error Code: %[1]d", - }, - }) + gpcm.KickPlayerCustomMessage(req.Pid, req.Reason, gpcm.WWFCMsgKickedCustom) return true, "", http.StatusOK } diff --git a/gpcm/error.go b/gpcm/error.go index 328f31c..3a05ec9 100644 --- a/gpcm/error.go +++ b/gpcm/error.go @@ -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) diff --git a/gpcm/kick.go b/gpcm/kick.go index 080380c..6d90439 100644 --- a/gpcm/kick.go +++ b/gpcm/kick.go @@ -50,7 +50,6 @@ func KickPlayer(profileID uint32, reason string) { kickPlayer(profileID, reason) } -// Exists because the above function is used in too many places to be updated easily func KickPlayerCustomMessage(profileID uint32, reason string, message WWFCErrorMessage) { mutex.Lock() defer mutex.Unlock() @@ -61,6 +60,7 @@ func KickPlayerCustomMessage(profileID uint32, reason string, message WWFCErrorM ErrorString: "The player was kicked from the server. Reason: " + reason, Fatal: true, WWFCMessage: message, + Reason: reason, }) } } diff --git a/gpcm/login.go b/gpcm/login.go index 4c27307..99f29d6 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -474,18 +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: WWFCErrorMessage{ - ErrorCode: 22002, - MessageRMC: map[byte]string{ - LangEnglish: "" + - "You are banned from WiiLink WFC\n" + - "Reason: " + user.BanReason + "\n" + - "Error Code: %[1]d\n" + - "Support Info: NG%08[2]x", - }, - }, + WWFCMessage: WWFCMsgKickedCustom, + Reason: user.BanReason, }) } else { g.replyError(GPError{