diff --git a/api/ban.go b/api/ban.go index f158eab..b34266c 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,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 } diff --git a/api/kick.go b/api/kick.go index 09e1c8b..43228cd 100644 --- a/api/kick.go +++ b/api/kick.go @@ -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 } diff --git a/api/unban.go b/api/unban.go index ad258f8..72081d6 100644 --- a/api/unban.go +++ b/api/unban.go @@ -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 } diff --git a/database/login.go b/database/login.go index f0c2418..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,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 diff --git a/database/user.go b/database/user.go index 7fc2a77..c268a9b 100644 --- a/database/user.go +++ b/database/user.go @@ -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 } 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 8a3c712..6d90439 100644 --- a/gpcm/kick.go +++ b/gpcm/kick.go @@ -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, + }) + } +} diff --git a/gpcm/login.go b/gpcm/login.go index 7b931b6..99f29d6 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -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{ diff --git a/gpcm/motd.go b/gpcm/motd.go index b0b6c04..8bf4d2a 100644 --- a/gpcm/motd.go +++ b/gpcm/motd.go @@ -14,3 +14,4 @@ func GetMessageOfTheDay() (string, error) { return string(contents), nil } +