diff --git a/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketDefaults.cs b/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketDefaults.cs index b8e8530cd..1db016036 100644 --- a/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketDefaults.cs +++ b/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketDefaults.cs @@ -3,18 +3,155 @@ namespace Refresh.Core.RateLimits.EndpointRateLimiting; +// TODO add IDs for all API buckets here +// TODO separate buckets for PSP for certain endpoints, since the ones in question are spammed by PSP in certain cases. +// Generally, fetch endpoints should use separate buckets depending on whether they are game/API endpoints, +// while upload/modification/deletion endpoints should share buckets. public static class EndpointBucketDefaults { public static readonly FrozenDictionary Buckets = new Dictionary() { #region Misc - {EndpointBucketId.Default, new(90, 300, 45)}, + {EndpointBucketId.Default, new(90, 300, 45)}, + #endregion + + #region Authentication + {EndpointBucketId.GameLogin, new(300, 10, 300)}, + #endregion + + #region Instance + {EndpointBucketId.GameGetGameConfig, new(240, 30, 180)}, + {EndpointBucketId.GameGetInstanceStats, new(240, 30, 180)}, + {EndpointBucketId.GameGetEula, new(240, 30, 180)}, + {EndpointBucketId.GameGetListOfAnnouncements, new(240, 30, 180)}, + #endregion + + #region Categories + // LBP3 spams if fetching Genre categories fails, so keep a little higher than reasonable + {EndpointBucketId.GameGetListOfCategories, new(240, 40, 180)}, #endregion #region Levels - // game sometimes requests many levels in bursts + {EndpointBucketId.GameGetListOfLevels, new(240, 50, 180)}, + + // Game sometimes requests many levels in bursts. {EndpointBucketId.GameGetSingleLevel, new(240, 200, 180)}, {EndpointBucketId.ApiGetSingleLevel, new(240, 50, 180)}, + + // Should use separate buckets for each publish endpoint so we don't end up allowing /startPublish but blocking /publish + // Also, keeping these buckets separate might avoid confusion by the server owner where they might wonder why + // it takes them less publish attempts to hit the limit than the actual limit they've set. + {EndpointBucketId.GamePrepareLevelPublish, new(600, 20, 360)}, + {EndpointBucketId.GameRealLevelPublish, new(600, 20, 360)}, + + {EndpointBucketId.DeleteLevel, new(300, 20, 180)}, + {EndpointBucketId.HeartLevel, new(300, 30, 180)}, + {EndpointBucketId.QueueLevel, new(300, 50, 180)}, // lbp3 has a hacky feature where you can mass-queue levels from playlists + {EndpointBucketId.TagLevel, new(300, 10, 180)}, + {EndpointBucketId.RateLevel, new(300, 20, 180)}, + #endregion + + #region Level Scores + {EndpointBucketId.GameGetListOfLevelScores, new(300, 40, 180)}, + {EndpointBucketId.GameUploadLevelScore, new(300, 30, 180)}, + + {EndpointBucketId.GamePlayLevel, new(300, 30, 180)}, + #endregion + + #region Reviews + {EndpointBucketId.GameGetListOfReviews, new(300, 40, 180)}, + {EndpointBucketId.GameGetSingleReview, new(300, 40, 180)}, + + {EndpointBucketId.UploadReview, new(300, 12, 180)}, + {EndpointBucketId.RateReview, new(300, 40, 180)}, + {EndpointBucketId.DeleteReview, new(300, 30, 180)}, + #endregion + + #region Comments (both Profile and Level) + {EndpointBucketId.GameGetListOfComments, new(300, 40, 180)}, + {EndpointBucketId.GameGetSingleComment, new(300, 40, 180)}, + + {EndpointBucketId.UploadComment, new(300, 18, 180)}, + {EndpointBucketId.RateComment, new(300, 40, 180)}, + {EndpointBucketId.DeleteComment, new(300, 30, 180)}, + #endregion + + #region Photos + {EndpointBucketId.GameGetListOfPhotos, new(300, 40, 180)}, + {EndpointBucketId.GameGetSinglePhoto, new(300, 40, 180)}, + + {EndpointBucketId.GameUploadPhoto, new(300, 25, 180)}, + {EndpointBucketId.DeletePhoto, new(300, 30, 180)}, + #endregion + + #region Users + {EndpointBucketId.GameGetListOfUsers, new(300, 60, 180)}, + {EndpointBucketId.GameGetSingleUser, new(300, 60, 180)}, + + {EndpointBucketId.UpdateUser, new(300, 20, 180)}, + {EndpointBucketId.GameUploadFriendData, new(240, 6, 180)}, + {EndpointBucketId.GameSyncUserPrivacySettings, new(300, 10, 180)}, + {EndpointBucketId.HeartUser, new(300, 30, 180)}, + #endregion + + #region Assets + // Regular download limits should be high on both game and API because both the game and third party API clients + // (e.g. archive_dl) are likely to download many of these at times depending on what level they're trying to load + // (additionally, adventures will usually have even more dependencies than regular levels!) + {EndpointBucketId.GameUploadAsset, new(300, 150, 180)}, + {EndpointBucketId.GameDownloadAsset, new(240, 500, 120)}, + #endregion + + #region Matching + {EndpointBucketId.GameUpdateRoomOrGetRooms, new(240, 30, 120)}, + #endregion + + #region Playlists + {EndpointBucketId.Lbp1GetListOfPlaylists, new(240, 50, 180)}, + {EndpointBucketId.Lbp1GetPlaylistContents, new(240, 50, 180)}, + + // LBP3 doesn't cache playlists or playlist levels, so it'll request these far more often than LBP1. + // Also, it will spam the levels endpoint for every playlist from the playlist response for a particular user. + // Playlists in general are very messy and buggy in LBP3, they have a property on their playlist response describing its + // preview level icons, but they instead spam these level requests to get the icons instead of using the playlist property. + {EndpointBucketId.Lbp3GetListOfPlaylists, new(240, 50, 180)}, + {EndpointBucketId.Lbp3GetPlaylistContents, new(240, 90, 180)}, + + {EndpointBucketId.CreatePlaylist, new(240, 30, 180)}, + {EndpointBucketId.UpdatePlaylistMetadata, new(240, 30, 180)}, + {EndpointBucketId.UpdatePlaylistContents, new(240, 50, 180)}, + {EndpointBucketId.HeartPlaylist, new(240, 30, 180)}, + {EndpointBucketId.DeletePlaylist, new(240, 30, 180)}, + #endregion + + #region Activity + {EndpointBucketId.GameGetActivityPage, new(240, 50, 180)}, + #endregion + + #region Notifications + {EndpointBucketId.GameGetListOfNotifications, new(240, 20, 180)}, + #endregion + + #region Moderation + {EndpointBucketId.GameUploadGriefReport, new(300, 10, 240)}, + {EndpointBucketId.GameFilterModeratedAssets, new(300, 60, 180)}, + + // this high just because of adventure uploading, + // TODO rate-limit specific chat commands separately (would need condition in endpoint method) + {EndpointBucketId.GameFilterChatMessage, new(60, 900, 30)}, + #endregion + + #region Pins + {EndpointBucketId.GameSyncPinProgress, new(240, 12, 180)}, + #endregion + + #region Challenges + {EndpointBucketId.GameUploadPlayerChallenge, new(240, 8, 180)}, + {EndpointBucketId.GameUploadPlayerChallengeScore, new(240, 16, 180)}, + + {EndpointBucketId.GameGetListOfPlayerChallenges, new(240, 20, 180)}, + {EndpointBucketId.GameGetListOfPlayerChallengeScores, new(240, 50, 180)}, + {EndpointBucketId.GameGetSinglePlayerChallengeScore, new(240, 40, 180)}, #endregion }.ToFrozenDictionary(); } \ No newline at end of file diff --git a/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketId.cs b/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketId.cs index b8970ca92..699522487 100644 --- a/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketId.cs +++ b/Refresh.Core/RateLimits/EndpointRateLimiting/EndpointBucketId.cs @@ -1,16 +1,133 @@ namespace Refresh.Core.RateLimits.EndpointRateLimiting; -// TODO add IDs for all API/game buckets here -// Generally, fetch endpoints should use separate buckets depending on whether they are game/API endpoints, -// while upload/modification/deletion endpoints should share buckets. +// For comments and explanations regarding these buckets, see EndpointBucketDefaults. public enum EndpointBucketId { #region Misc Default, #endregion - + + #region Authentication + GameLogin, + #endregion + + #region Instance + GameGetGameConfig, + GameGetInstanceStats, + + GameGetEula, + GameGetListOfAnnouncements, + #endregion + + #region Categories + GameGetListOfCategories, + #endregion + #region Levels + GameGetListOfLevels, GameGetSingleLevel, ApiGetSingleLevel, + + GamePrepareLevelPublish, + GameRealLevelPublish, + + DeleteLevel, + HeartLevel, + QueueLevel, + TagLevel, + RateLevel, + #endregion + + #region Level Scores + GameGetListOfLevelScores, + GameUploadLevelScore, + + GamePlayLevel, + #endregion + + #region Reviews + GameGetListOfReviews, + GameGetSingleReview, + + UploadReview, + RateReview, + DeleteReview, + #endregion + + #region Comments (both Profile and Level) + GameGetListOfComments, + GameGetSingleComment, + + UploadComment, + RateComment, + DeleteComment, + #endregion + + #region Photos + GameGetListOfPhotos, + GameGetSinglePhoto, + + GameUploadPhoto, + DeletePhoto, + #endregion + + #region Users + GameGetListOfUsers, + GameGetSingleUser, + + UpdateUser, + GameUploadFriendData, + GameSyncUserPrivacySettings, + HeartUser, + #endregion + + #region Assets + GameUploadAsset, + GameDownloadAsset, + #endregion + + #region Matching + GameUpdateRoomOrGetRooms, + #endregion + + #region Playlists + Lbp1GetListOfPlaylists, + Lbp1GetPlaylistContents, + + Lbp3GetListOfPlaylists, + Lbp3GetPlaylistContents, + + CreatePlaylist, + UpdatePlaylistMetadata, + UpdatePlaylistContents, + HeartPlaylist, + DeletePlaylist, + #endregion + + #region Activity + GameGetActivityPage, + #endregion + + #region Notifications + GameGetListOfNotifications, + #endregion + + #region Moderation + GameUploadGriefReport, + GameFilterModeratedAssets, + GameFilterChatMessage, + #endregion + + #region Pins + GameSyncPinProgress, + #endregion + + #region Challenges + GameUploadPlayerChallenge, + GameUploadPlayerChallengeScore, + + GameGetListOfPlayerChallenges, + GameGetListOfPlayerChallengeScores, + GameGetSinglePlayerChallengeScore, #endregion } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Endpoints/ActivityEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ActivityEndpoints.cs index 9fa6f6fe4..685df96b8 100644 --- a/Refresh.Interfaces.Game/Endpoints/ActivityEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ActivityEndpoints.cs @@ -2,14 +2,13 @@ using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Activity; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Authentication; @@ -28,8 +27,7 @@ public class ActivityEndpoints : EndpointGroup [GameEndpoint("stream", ContentType.Xml)] [GameEndpoint("stream", ContentType.Xml, HttpMethods.Post)] - [RateLimitSettings(ActivityPageEndpointLimits.TimeoutDuration, ActivityPageEndpointLimits.GameRequestAmount, - ActivityPageEndpointLimits.BlockDuration, ActivityPageEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetActivityPage)] [NullStatusCode(BadRequest)] [MinimumRole(GameUserRole.Restricted)] public SerializedActivityPage? GetRecentActivity(RequestContext context, GameServerConfig config, GameDatabaseContext database, GameUser? user, @@ -68,8 +66,7 @@ public class ActivityEndpoints : EndpointGroup [GameEndpoint("stream/slot/{type}/{id}", ContentType.Xml)] [NullStatusCode(BadRequest)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ActivityPageEndpointLimits.TimeoutDuration, ActivityPageEndpointLimits.GameRequestAmount, - ActivityPageEndpointLimits.BlockDuration, ActivityPageEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetActivityPage)] public Response GetRecentActivityForLevel(RequestContext context, GameServerConfig config, GameDatabaseContext database, GameUser? user, string type, int id, DataContext dataContext) { @@ -117,8 +114,7 @@ public Response GetRecentActivityForLevel(RequestContext context, GameServerConf [GameEndpoint("stream/user2/{username}", ContentType.Xml)] [NullStatusCode(BadRequest)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ActivityPageEndpointLimits.TimeoutDuration, ActivityPageEndpointLimits.GameRequestAmount, - ActivityPageEndpointLimits.BlockDuration, ActivityPageEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetActivityPage)] public Response GetRecentActivityFromUser(RequestContext context, GameServerConfig config, GameDatabaseContext database, string username, DataContext dataContext) { @@ -174,8 +170,7 @@ public Response GetRecentActivityFromUser(RequestContext context, GameServerConf [GameEndpoint("news", ContentType.Xml)] [Authentication(false)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ActivityPageEndpointLimits.TimeoutDuration, ActivityPageEndpointLimits.GameRequestAmount, - ActivityPageEndpointLimits.BlockDuration, ActivityPageEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetActivityPage)] // basically the same public Response GetNews(RequestContext context, GameDatabaseContext database, IDateTimeProvider time, Token? token) { List items = new(); diff --git a/Refresh.Interfaces.Game/Endpoints/AnnouncementEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/AnnouncementEndpoints.cs index 54bd3563d..69c592af0 100644 --- a/Refresh.Interfaces.Game/Endpoints/AnnouncementEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/AnnouncementEndpoints.cs @@ -3,13 +3,12 @@ using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses.Serialization; using Bunkum.Listener.Protocol; using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Users; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Matching; using Refresh.Database; @@ -79,7 +78,7 @@ private static bool AnnounceGetContest(StringBuilder output, Token token, GameDa [GameEndpoint("announce")] [MinimumRole(GameUserRole.Restricted)] [SuppressMessage("ReSharper", "RedundantAssignment")] - [RateLimitSettings(420, 12, 380, "announcements-game")] + [EndpointRateLimit(EndpointBucketId.GameGetListOfAnnouncements)] public string Announce(RequestContext context, GameServerConfig config, GameUser user, GameDatabaseContext database, Token token, IDateTimeProvider timeProvider) { if (user.Role == GameUserRole.Restricted) @@ -129,8 +128,7 @@ Your account is currently in restricted mode. [GameEndpoint("notification", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(NotificationsEndpointLimits.TimeoutDuration, NotificationsEndpointLimits.GameRequestAmount, - NotificationsEndpointLimits.BlockDuration, NotificationsEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfNotifications)] public string Notification(RequestContext context, GameServerConfig config, Token token, GameDatabaseContext database, MatchService matchService) { // On LBP1 the only regular ticking request is /notification, diff --git a/Refresh.Interfaces.Game/Endpoints/CategoryEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/CategoryEndpoints.cs index eb5260710..92f9fd27e 100644 --- a/Refresh.Interfaces.Game/Endpoints/CategoryEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/CategoryEndpoints.cs @@ -1,11 +1,9 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Listener.Protocol; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Levels; -using Refresh.Core.RateLimits.Users; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Categories; using Refresh.Core.Types.Categories.Levels; using Refresh.Core.Types.Data; @@ -26,7 +24,7 @@ public class CategoryEndpoints : EndpointGroup [GameEndpoint("searches", ContentType.Xml)] [GameEndpoint("genres", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(420, 20, 300, "categories-game")] + [EndpointRateLimit(EndpointBucketId.GameGetListOfCategories)] public SerializedCategoryList GetModernCategories(RequestContext context, CategoryService categoryService, DataContext dataContext, GameServerConfig config) { (int skip, int count) = context.GetPageData(); @@ -58,8 +56,7 @@ public SerializedCategoryList GetModernCategories(RequestContext context, Catego [GameEndpoint("searches/levels/{apiRoute}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public SerializedCategoryResultsList? GetLevelsFromCategory(RequestContext context, CategoryService categories, GameUser user, string apiRoute, DataContext dataContext) { @@ -83,8 +80,7 @@ public SerializedCategoryList GetModernCategories(RequestContext context, Catego [GameEndpoint("searches/users/{apiRoute}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(UserListEndpointLimits.TimeoutDuration, UserListEndpointLimits.RequestAmount, - UserListEndpointLimits.BlockDuration, UserListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfUsers)] public SerializedCategoryResultsList? GetUsersFromCategory(RequestContext context, CategoryService categories, GameUser user, string apiRoute, DataContext dataContext, GameServerConfig config) { diff --git a/Refresh.Interfaces.Game/Endpoints/ChallengeEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ChallengeEndpoints.cs index a488330f4..1f673d7ec 100644 --- a/Refresh.Interfaces.Game/Endpoints/ChallengeEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ChallengeEndpoints.cs @@ -1,13 +1,12 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Constants; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Challenges; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Data; using Refresh.Core.Types.Matching; @@ -30,8 +29,7 @@ public class ChallengeEndpoints : EndpointGroup [GameEndpoint("challenge", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(ChallengeUploadEndpointLimits.TimeoutDuration, ChallengeUploadEndpointLimits.RequestAmount, - ChallengeUploadEndpointLimits.BlockDuration, ChallengeUploadEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameUploadPlayerChallenge)] public Response UploadChallenge(RequestContext context, DataContext dataContext, GameUser user, SerializedChallenge body, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -111,8 +109,7 @@ public Response UploadChallenge(RequestContext context, DataContext dataContext, [GameEndpoint("user/{username}/challenges", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ChallengeListEndpointLimits.TimeoutDuration, ChallengeListEndpointLimits.RequestAmount, - ChallengeListEndpointLimits.BlockDuration, ChallengeListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPlayerChallenges)] public Response GetChallengesByUser(RequestContext context, GameUser user, DataContext dataContext) { GameRoom? room = dataContext.Match.RoomAccessor.GetRoomByUser(user, dataContext.Platform, dataContext.Game); @@ -165,8 +162,7 @@ public Response GetChallengesByUser(RequestContext context, GameUser user, DataC [GameEndpoint("user/{username}/challenges/joined", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(ChallengeListEndpointLimits.TimeoutDuration, ChallengeListEndpointLimits.RequestAmount, - ChallengeListEndpointLimits.BlockDuration, ChallengeListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPlayerChallenges)] public Response GetJoinedChallenges(RequestContext context, GameUser user, DataContext dataContext) { // Ignore username since this is only ever called for the calling user. @@ -187,8 +183,7 @@ public Response GetJoinedChallenges(RequestContext context, GameUser user, DataC /// [GameEndpoint("challenge/{challengeId}/scoreboard", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(ChallengeScoreUploadEndpointLimits.TimeoutDuration, ChallengeScoreUploadEndpointLimits.RequestAmount, - ChallengeScoreUploadEndpointLimits.BlockDuration, ChallengeScoreUploadEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameUploadPlayerChallengeScore)] public Response SubmitChallengeScore(RequestContext context, DataContext dataContext, GameUser user, SerializedChallengeAttempt body, int challengeId, ChallengeGhostRateLimitService ghostService, GameServerConfig config) @@ -242,8 +237,7 @@ public Response SubmitChallengeScore(RequestContext context, DataContext dataCon [GameEndpoint("challenge/{challengeId}/scoreboard/{username}", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(ChallengeSingleScoreEndpointLimits.TimeoutDuration, ChallengeSingleScoreEndpointLimits.RequestAmount, - ChallengeSingleScoreEndpointLimits.BlockDuration, ChallengeSingleScoreEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetSinglePlayerChallengeScore)] public SerializedChallengeScore? GetUsersHighScoreForChallenge(RequestContext context, DataContext dataContext, GameUser user, int challengeId, string username, ChallengeGhostRateLimitService ghostService) { ghostService.RemoveUserFromRateLimit(user.UserId); @@ -263,8 +257,7 @@ public Response SubmitChallengeScore(RequestContext context, DataContext dataCon [GameEndpoint("challenge/{challengeId}/scoreboard", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(ChallengeScoreListEndpointLimits.TimeoutDuration, ChallengeScoreListEndpointLimits.RequestAmount, - ChallengeScoreListEndpointLimits.BlockDuration, ChallengeScoreListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPlayerChallengeScores)] public SerializedChallengeScoreList? GetScoresForChallenge(RequestContext context, DataContext dataContext, GameUser user, int challengeId, ChallengeGhostRateLimitService ghostService) { ghostService.RemoveUserFromRateLimit(user.UserId); @@ -298,8 +291,7 @@ public Response SubmitChallengeScore(RequestContext context, DataContext dataCon [GameEndpoint("challenge/{challengeId}/scoreboard/{username}/contextual", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(ChallengeScoreListEndpointLimits.TimeoutDuration, ChallengeScoreListEndpointLimits.RequestAmount, - ChallengeScoreListEndpointLimits.BlockDuration, ChallengeScoreListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPlayerChallengeScores)] public SerializedChallengeScoreList? GetContextualScoresForChallenge(RequestContext context, DataContext dataContext, GameUser user, int challengeId, ChallengeGhostRateLimitService ghostService) { ghostService.RemoveUserFromRateLimit(user.UserId); diff --git a/Refresh.Interfaces.Game/Endpoints/CommentEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/CommentEndpoints.cs index 9913271e5..0c95662a6 100644 --- a/Refresh.Interfaces.Game/Endpoints/CommentEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/CommentEndpoints.cs @@ -1,6 +1,5 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; @@ -8,8 +7,7 @@ using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Comments; -using Refresh.Core.RateLimits.Relations; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Comments; @@ -24,8 +22,7 @@ public class CommentEndpoints : EndpointGroup { [GameEndpoint("postUserComment/{username}", ContentType.Xml, HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommentUploadEndpointLimits.TimeoutDuration, CommentUploadEndpointLimits.RequestAmount, - CommentUploadEndpointLimits.BlockDuration, CommentUploadEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UploadComment)] public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, SerializedComment body, GameUser user, IDateTimeProvider timeProvider, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -52,8 +49,7 @@ public Response PostProfileComment(RequestContext context, GameDatabaseContext d [GameEndpoint("userComments/{username}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(CommentListEndpointLimits.TimeoutDuration, CommentListEndpointLimits.RequestAmount, - CommentListEndpointLimits.BlockDuration, CommentListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfComments)] public SerializedCommentList? GetProfileComments(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, string username) { GameUser? profile = database.GetUserByUsername(username); @@ -66,6 +62,7 @@ public Response PostProfileComment(RequestContext context, GameDatabaseContext d } [GameEndpoint("deleteUserComment/{username}", HttpMethods.Post)] + [EndpointRateLimit(EndpointBucketId.DeleteComment)] public Response DeleteProfileComment(RequestContext context, GameDatabaseContext database, string username, GameUser user) { if (!int.TryParse(context.QueryString["commentId"], out int commentId)) return BadRequest; @@ -91,8 +88,7 @@ public Response DeleteProfileComment(RequestContext context, GameDatabaseContext [GameEndpoint("postComment/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommentUploadEndpointLimits.TimeoutDuration, CommentUploadEndpointLimits.RequestAmount, - CommentUploadEndpointLimits.BlockDuration, CommentUploadEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UploadComment)] public Response PostLevelComment(RequestContext context, GameDatabaseContext database, string slotType, int id, SerializedComment body, GameUser user, GameServerConfig config) { @@ -119,8 +115,7 @@ public Response PostLevelComment(RequestContext context, GameDatabaseContext dat [GameEndpoint("comments/{slotType}/{id}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(CommentListEndpointLimits.TimeoutDuration, CommentListEndpointLimits.RequestAmount, - CommentListEndpointLimits.BlockDuration, CommentListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfComments)] public SerializedCommentList? GetLevelComments(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, string slotType, int id) { @@ -134,6 +129,7 @@ public Response PostLevelComment(RequestContext context, GameDatabaseContext dat } [GameEndpoint("deleteComment/{slotType}/{id}", HttpMethods.Post)] + [EndpointRateLimit(EndpointBucketId.DeleteComment)] public Response DeleteLevelComment(RequestContext context, GameDatabaseContext database, string slotType, int id, GameUser user) { if (!int.TryParse(context.QueryString["commentId"], out int commentId)) return BadRequest; @@ -158,8 +154,7 @@ public Response DeleteLevelComment(RequestContext context, GameDatabaseContext d } [GameEndpoint("rateUserComment/{content}", HttpMethods.Post)] // profile comments - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.RateComment)] public Response RateProfileComment(RequestContext context, GameDatabaseContext database, GameUser user, string content, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -177,8 +172,7 @@ public Response RateProfileComment(RequestContext context, GameDatabaseContext d } [GameEndpoint("rateComment/{slotType}/{content}", HttpMethods.Post)] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.RateComment)] public Response RateLevelComment(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, string content, GameServerConfig config) { if (user.IsWriteBlocked(config)) diff --git a/Refresh.Interfaces.Game/Endpoints/Handshake/AuthenticationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Handshake/AuthenticationEndpoints.cs index 621caa70c..546230faa 100644 --- a/Refresh.Interfaces.Game/Endpoints/Handshake/AuthenticationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Handshake/AuthenticationEndpoints.cs @@ -2,7 +2,6 @@ using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; @@ -14,6 +13,7 @@ using Refresh.Core; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Matching; using Refresh.Database; @@ -26,7 +26,7 @@ public class AuthenticationEndpoints : EndpointGroup { [GameEndpoint("login", HttpMethods.Post, ContentType.Xml), Authentication(false), AllowDuringMaintenance] [NullStatusCode(Forbidden)] - [RateLimitSettings(300, 10, 300, "auth")] + [EndpointRateLimit(EndpointBucketId.GameLogin)] [MinimumRole(GameUserRole.Restricted)] public object? Authenticate(RequestContext context, GameDatabaseContext database, diff --git a/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs index 417a98832..1360a34b0 100644 --- a/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs @@ -2,7 +2,6 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.Endpoints.Debugging; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Core.Responses.Serialization; using Bunkum.Listener.Protocol; @@ -10,6 +9,7 @@ using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Levels; @@ -21,13 +21,9 @@ namespace Refresh.Interfaces.Game.Endpoints.Handshake; public class MetadataEndpoints : EndpointGroup { - private const int ConfigRequestTimeoutDuration = 480; - private const int ConfigRequestAmount = 40; - private const int ConfigBlockDuration = 420; - private const string ConfigBucket = "game-configs"; - [GameEndpoint("privacySettings", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] + [EndpointRateLimit(EndpointBucketId.GameSyncUserPrivacySettings)] public SerializedPrivacySettings GetPrivacySettings(RequestContext context, GameUser user) { return new SerializedPrivacySettings @@ -39,6 +35,7 @@ public SerializedPrivacySettings GetPrivacySettings(RequestContext context, Game [GameEndpoint("privacySettings", ContentType.Xml, HttpMethods.Post)] [MinimumRole(GameUserRole.Restricted)] + [EndpointRateLimit(EndpointBucketId.GameSyncUserPrivacySettings)] public SerializedPrivacySettings SetPrivacySettings(RequestContext context, SerializedPrivacySettings body, GameDatabaseContext database, GameUser user) { database.SetPrivacySettings(user, body); @@ -47,7 +44,7 @@ public SerializedPrivacySettings SetPrivacySettings(RequestContext context, Seri } [GameEndpoint("npdata", ContentType.Xml, HttpMethods.Post)] - [RateLimitSettings(480, 8, 420, "game-npdata")] + [EndpointRateLimit(EndpointBucketId.GameUploadFriendData)] public Response SetFriendData(RequestContext context, GameUser user, GameDatabaseContext database, SerializedFriendData body, DataContext dataContext, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -76,7 +73,7 @@ private static readonly Lazy NetworkSettingsFile [GameEndpoint("network_settings.nws")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ConfigRequestTimeoutDuration, ConfigRequestAmount, ConfigBlockDuration, ConfigBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public string NetworkSettings(RequestContext context, GameServerConfig config) { string? networkSettings = NetworkSettingsFile.Value; @@ -124,7 +121,7 @@ private static readonly Lazy TelemetryConfigFile [GameEndpoint("telemetry.cfg")] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(Gone)] - [RateLimitSettings(ConfigRequestTimeoutDuration, ConfigRequestAmount, ConfigBlockDuration, ConfigBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public string? TelemetryConfig(RequestContext context) { bool created = TelemetryConfigFile.IsValueCreated; @@ -150,7 +147,7 @@ private static readonly Lazy PromotionsFile [GameEndpoint("promotions")] [NullStatusCode(OK)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ConfigRequestTimeoutDuration, ConfigRequestAmount, ConfigBlockDuration, ConfigBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public string? Promotions(RequestContext context) { bool created = PromotionsFile.IsValueCreated; @@ -165,10 +162,10 @@ private static readonly Lazy PromotionsFile return promotions; } + // TODO apparently this can be used to send a farc and map containing custom pin, dlc pack, and translation data to lbp2/3 [GameEndpoint("farc_hashes")] [MinimumRole(GameUserRole.Restricted)] - //Stubbed to return a 410 Gone, so LBP3 doesn't spam us. - //The game doesn't actually use this information for anything, so we don't allow server owners to replace this. + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public Response FarcHashes(RequestContext context) => Gone; //TODO: In the future this should allow you to have separate files per language since the game sends the language through the `language` query parameter. @@ -183,7 +180,7 @@ private static readonly Lazy DeveloperVideosFile [GameEndpoint("developer_videos")] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(OK)] - [RateLimitSettings(ConfigRequestTimeoutDuration, ConfigRequestAmount, ConfigBlockDuration, ConfigBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public string? DeveloperVideos(RequestContext context) { bool created = DeveloperVideosFile.IsValueCreated; @@ -222,7 +219,7 @@ private static readonly Lazy ChallengeConfigFile [GameEndpoint("ChallengeConfig.xml", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ConfigRequestTimeoutDuration, ConfigRequestAmount, ConfigBlockDuration, ConfigBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public string ChallengeConfig(RequestContext context) { bool created = ChallengeConfigFile.IsValueCreated; @@ -259,7 +256,7 @@ public string ChallengeConfig(RequestContext context) [GameEndpoint("tags")] [GameEndpoint("tags/popular")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ConfigRequestTimeoutDuration, ConfigRequestAmount, ConfigBlockDuration, ConfigBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetGameConfig)] public string Tags(RequestContext context) => TagExtensions.AllTags; // Stub this for now. Nothing will happen if this is unimplemented, and we likely won't use any data sent here for now. diff --git a/Refresh.Interfaces.Game/Endpoints/Handshake/WelcomeEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Handshake/WelcomeEndpoints.cs index 75a61dc88..557263723 100644 --- a/Refresh.Interfaces.Game/Endpoints/Handshake/WelcomeEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Handshake/WelcomeEndpoints.cs @@ -1,9 +1,9 @@ using System.Text; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Database.Models.Users; namespace Refresh.Interfaces.Game.Endpoints.Handshake; @@ -28,7 +28,7 @@ You should have received a copy of the GNU Affero General Public License [GameEndpoint("eula")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(480, 20, 420, "eula")] + [EndpointRateLimit(EndpointBucketId.GameGetEula)] public string License(RequestContext context, GameServerConfig config, ContactInfoConfig contact) { StringBuilder builder = new(); diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs index eb04110d7..1b4e972f0 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs @@ -1,14 +1,12 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Leaderboard; -using Refresh.Core.RateLimits.Relations; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Authentication; @@ -24,16 +22,10 @@ namespace Refresh.Interfaces.Game.Endpoints.Levels; public class LeaderboardEndpoints : EndpointGroup { - private const int RequestTimeoutDuration = 300; - private const int MaxScoreSubmissionAmount = 20; // 10 should be enough normally, but LBP PSP exists... - private const int RequestBlockDuration = 300; - private const string ScoreSubmissionBucketName = "score-submission"; - // LBP1 doesn't send any requests to this endpoint if a user enters an online user level. [GameEndpoint("play/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(PlayLevelEndpointLimits.TimeoutDuration, PlayLevelEndpointLimits.RequestAmount, - PlayLevelEndpointLimits.BlockDuration, PlayLevelEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GamePlayLevel)] public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); @@ -66,8 +58,7 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon [GameEndpoint("scoreboard/{slotType}/{id}", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ScoreListEndpointLimits.TimeoutDuration, ScoreListEndpointLimits.GameRequestAmount, - ScoreListEndpointLimits.BlockDuration, ScoreListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevelScores)] public Response GetUserScores(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, Token token, DataContext dataContext) { @@ -82,8 +73,7 @@ public Response GetUserScores(RequestContext context, GameUser user, GameDatabas [GameEndpoint("scoreboard/friends/{slotType}/{id}", HttpMethods.Post, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ScoreListEndpointLimits.TimeoutDuration, ScoreListEndpointLimits.GameRequestAmount, - ScoreListEndpointLimits.BlockDuration, ScoreListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevelScores)] [NullStatusCode(NotFound)] public SerializedScoreLeaderboardList? GetLevelFriendLeaderboard(RequestContext context, GameUser user, @@ -101,7 +91,7 @@ public Response GetUserScores(RequestContext context, GameUser user, GameDatabas } [GameEndpoint("scoreboard/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] - [RateLimitSettings(RequestTimeoutDuration, MaxScoreSubmissionAmount, RequestBlockDuration, ScoreSubmissionBucketName)] + [EndpointRateLimit(EndpointBucketId.GameUploadLevelScore)] [RequireEmailVerified] public Response SubmitScore(RequestContext context, GameUser user, GameServerConfig config, GameDatabaseContext database, string slotType, int id, SerializedScore body, Token token, @@ -255,8 +245,7 @@ private void AwardScoreboardPins(DatabaseList scores, DataContext [GameEndpoint("topscores/{slotType}/{id}/{type}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(ScoreListEndpointLimits.TimeoutDuration, ScoreListEndpointLimits.GameRequestAmount, - ScoreListEndpointLimits.BlockDuration, ScoreListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevelScores)] public SerializedScoreList? GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, string slotType, int id, int type, DataContext dataContext, GameUser user, IDateTimeProvider dateTimeProvider) { @@ -273,8 +262,7 @@ private void AwardScoreboardPins(DatabaseList scores, DataContext [GameEndpoint("friendscores/{slotType}/{id}/{type}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(ScoreListEndpointLimits.TimeoutDuration, ScoreListEndpointLimits.GameRequestAmount, - ScoreListEndpointLimits.BlockDuration, ScoreListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevelScores)] public SerializedScoreList? GetFriendTopScoresForLevel(RequestContext context, GameDatabaseContext database, string slotType, int id, int type, DataContext dataContext, GameUser user, IDateTimeProvider dateTimeProvider) { diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/LevelEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/LevelEndpoints.cs index 99faaff44..7845c7668 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/LevelEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/LevelEndpoints.cs @@ -1,13 +1,11 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Core.Storage; using Bunkum.Listener.Protocol; using Refresh.Common.Constants; using Refresh.Core.Authentication.Permission; using Refresh.Core.RateLimits.EndpointRateLimiting; -using Refresh.Core.RateLimits.Levels; using Refresh.Core.Services; using Refresh.Core.Types.Categories; using Refresh.Core.Types.Data; @@ -28,8 +26,7 @@ public class LevelEndpoints : EndpointGroup { [GameEndpoint("slots/{route}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public SerializedMinimalLevelList? GetLevels(RequestContext context, GameDatabaseContext database, CategoryService categoryService, @@ -106,8 +103,7 @@ public class LevelEndpoints : EndpointGroup [GameEndpoint("slots/{route}/{username}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public SerializedMinimalLevelList? GetLevelsWithPlayer(RequestContext context, GameDatabaseContext database, CategoryService categories, @@ -127,8 +123,7 @@ public class LevelEndpoints : EndpointGroup // The syntax error in the query params (& instead of ?) makes Bunkum include them as part of the ID route param [GameEndpoint("slots/like/{slotType}/{id}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public Response GetLevelsLikeLevel(RequestContext context, DataContext dataContext, GameUser user, string slotType, string id) { string levelIdStr; @@ -194,8 +189,7 @@ public Response GetLevelsLikeLevel(RequestContext context, DataContext dataConte [GameEndpoint("slotList", ContentType.Xml)] [NullStatusCode(BadRequest)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public SerializedLevelList? GetMultipleLevels(RequestContext context, GameDatabaseContext database, GameUser user, Token token, DataContext dataContext) { @@ -232,8 +226,7 @@ public Response GetLevelsLikeLevel(RequestContext context, DataContext dataConte [GameEndpoint("slots", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public SerializedMinimalLevelList? NewestLevels(RequestContext context, GameDatabaseContext database, CategoryService categories, @@ -248,8 +241,7 @@ public Response GetLevelsLikeLevel(RequestContext context, DataContext dataConte [GameEndpoint("favouriteSlots/{username}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfLevels)] public SerializedMinimalFavouriteLevelList? FavouriteLevels(RequestContext context, GameDatabaseContext database, CategoryService categories, diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs index 21060c13f..8b920d1a6 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs @@ -1,7 +1,6 @@ using System.Net; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; @@ -15,6 +14,7 @@ using Refresh.Core.Importing; using Refresh.Core.Services; using Refresh.Core.Types.Assets.Validation; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models; @@ -30,12 +30,6 @@ namespace Refresh.Interfaces.Game.Endpoints.Levels; public class PublishEndpoints : EndpointGroup { - private const int RequestTimeoutDuration = 900; // 15 minutes - private const int MaxRequestAmount = 15; - private const int RequestBlockDuration = RequestTimeoutDuration; - private const string PublishBucket = "level-publish"; - private const string StartPublishBucket = "level-start-publish"; - /// /// Does basic verification on a level /// @@ -181,7 +175,7 @@ private static bool IsTimedLevelLimitReached(DataContext dataContext, GameUser u [GameEndpoint("startPublish", ContentType.Xml, HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(RequestTimeoutDuration, MaxRequestAmount, RequestBlockDuration, StartPublishBucket)] + [EndpointRateLimit(EndpointBucketId.GamePrepareLevelPublish)] public Response StartPublish(RequestContext context, GameLevelRequest body, DataContext dataContext, @@ -229,7 +223,7 @@ public Response StartPublish(RequestContext context, [GameEndpoint("publish", ContentType.Xml, HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(RequestTimeoutDuration, MaxRequestAmount, RequestBlockDuration, PublishBucket)] + [EndpointRateLimit(EndpointBucketId.GameRealLevelPublish)] public Response PublishLevel(RequestContext context, GameLevelRequest body, DataContext dataContext, @@ -309,6 +303,7 @@ public Response PublishLevel(RequestContext context, } [GameEndpoint("unpublish/{id}", ContentType.Xml, HttpMethods.Post)] + [EndpointRateLimit(EndpointBucketId.DeleteLevel)] public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); diff --git a/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs index 7971b184e..773b9cc4a 100644 --- a/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs @@ -2,13 +2,12 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; using Bunkum.Core.Endpoints.Debugging; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Presence; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Data; using Refresh.Core.Types.Matching; @@ -73,8 +72,7 @@ public static string FixupLocationData(string body) [GameEndpoint("match", HttpMethods.Post, ContentType.Json)] [DebugRequestBody, DebugResponseBody] [RequireEmailVerified] - [RateLimitSettings(GamePresenceEndpointLimits.TimeoutDuration, GamePresenceEndpointLimits.RequestAmount, - GamePresenceEndpointLimits.BlockDuration, GamePresenceEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameUpdateRoomOrGetRooms)] public Response Match( RequestContext context, string body, @@ -109,8 +107,7 @@ public Response Match( // since the "bump" is much less often. This will at the very least make API tools be able to see LBP1 player activity and player counts. // LBP1 doesn't send any requests to /play if a user enters an online user level, so use this endpoint to increment plays for those. [GameEndpoint("enterLevel/{slotType}/{id}", HttpMethods.Post)] - [RateLimitSettings(GamePresenceEndpointLimits.TimeoutDuration, GamePresenceEndpointLimits.RequestAmount, - GamePresenceEndpointLimits.BlockDuration, GamePresenceEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameUpdateRoomOrGetRooms)] public Response EnterLevel(RequestContext context, Token token, MatchService matchService, string slotType, int id, DataContext dataContext) { GameRoom room = matchService.GetOrCreateRoomByPlayer(token.User, token.TokenPlatform, token.TokenGame, NatType.Strict, false); diff --git a/Refresh.Interfaces.Game/Endpoints/ModerationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ModerationEndpoints.cs index d67312b7c..585903b13 100644 --- a/Refresh.Interfaces.Game/Endpoints/ModerationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ModerationEndpoints.cs @@ -1,9 +1,9 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Core.Authentication.Permission; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Commands; using Refresh.Core.Types.Data; @@ -27,6 +27,7 @@ public SerializedModeratedSlotList ModerateSlots(RequestContext context, Seriali } [GameEndpoint("showModerated", HttpMethods.Post, ContentType.Xml)] + [EndpointRateLimit(EndpointBucketId.GameFilterModeratedAssets)] public SerializedModeratedResourceList ModerateResources(RequestContext context, SerializedModeratedResourceList body, DataContext dataContext) { return new SerializedModeratedResourceList @@ -47,7 +48,7 @@ public SerializedModeratedResourceList ModerateResources(RequestContext context, /// The string shown in-game. [GameEndpoint("filter", HttpMethods.Post)] [AllowEmptyBody] - [RateLimitSettings(180, 100, 120, "filter")] + [EndpointRateLimit(EndpointBucketId.GameFilterChatMessage)] public string Filter(RequestContext context, CommandService commandService, string body, GameUser user, Token token, GameDatabaseContext database) { // TODO: Add actual filtering/censoring diff --git a/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs index 81583013f..1a956bec1 100644 --- a/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs @@ -9,8 +9,8 @@ using Refresh.Core.Configuration; using Refresh.Core.Helpers; using Refresh.Core.Importing; -using Refresh.Core.RateLimits.Photos; using Refresh.Core.Services; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Assets.Validation; using Refresh.Core.Types.Data; using Refresh.Database; @@ -27,7 +27,7 @@ public class PhotoEndpoints : EndpointGroup { [GameEndpoint("uploadPhoto", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(300, 30, 240, "upload-photo")] + [EndpointRateLimit(EndpointBucketId.GameUploadPhoto)] public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDatabaseContext database, GameUser user, IDataStore dataStore, AssetImporter importer, DataContext dataContext, AipiService? aipi, GameServerConfig config) @@ -185,23 +185,20 @@ private static Response GetPhotos(RequestContext context, GameDatabaseContext da [GameEndpoint("photos/with", ContentType.Xml)] [Authentication(false)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(PhotoListEndpointLimits.TimeoutDuration, PhotoListEndpointLimits.GameRequestAmount, - PhotoListEndpointLimits.BlockDuration, PhotoListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPhotos)] public Response PhotosWithUser(RequestContext context, GameDatabaseContext database, DataContext dataContext) => GetPhotos(context, database, dataContext, database.GetPhotosWithUser); [GameEndpoint("photos/by", ContentType.Xml)] [Authentication(false)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(PhotoListEndpointLimits.TimeoutDuration, PhotoListEndpointLimits.GameRequestAmount, - PhotoListEndpointLimits.BlockDuration, PhotoListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPhotos)] public Response PhotosByUser(RequestContext context, GameDatabaseContext database, DataContext dataContext) => GetPhotos(context, database, dataContext, database.GetPhotosByUser); [GameEndpoint("photos/{slotType}/{levelId}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(PhotoListEndpointLimits.TimeoutDuration, PhotoListEndpointLimits.GameRequestAmount, - PhotoListEndpointLimits.BlockDuration, PhotoListEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfPhotos)] public SerializedPhotoList? GetPhotosOnLevel(RequestContext context, DataContext dataContext, string slotType, int levelId) { GameLevel? level = dataContext.Database.GetLevelByIdAndType(slotType, levelId); @@ -228,8 +225,7 @@ public Response PhotosByUser(RequestContext context, GameDatabaseContext databas [GameEndpoint("photo/{id}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(SinglePhotoEndpointLimits.TimeoutDuration, SinglePhotoEndpointLimits.RequestAmount, - SinglePhotoEndpointLimits.BlockDuration, SinglePhotoEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetSinglePhoto)] public SerializedPhoto? GetPhotoById(RequestContext context, DataContext dataContext, int id) { GamePhoto? photo = dataContext.Database.GetPhotoById(id); diff --git a/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp1PlaylistEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp1PlaylistEndpoints.cs index 3f3532fb3..87cb58745 100644 --- a/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp1PlaylistEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp1PlaylistEndpoints.cs @@ -1,14 +1,12 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Constants; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Levels; -using Refresh.Core.RateLimits.Playlists; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models; @@ -54,8 +52,7 @@ private string ValidateIconHash(string iconHash, DataContext dataContext) // Creates a playlist, with an optional parent ID [GameEndpoint("createPlaylist", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistCreationEndpointLimits.UploadTimeoutDuration, PlaylistCreationEndpointLimits.MaxCreateAmount, - PlaylistCreationEndpointLimits.UploadBlockDuration, PlaylistCreationEndpointLimits.CreateBucket)] + [EndpointRateLimit(EndpointBucketId.CreatePlaylist)] public Response CreatePlaylist(RequestContext context, DataContext dataContext, GameServerConfig config, SerializedLbp1Playlist body) { GameUser user = dataContext.User!; @@ -128,8 +125,7 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext, [GameEndpoint("playlist/{id}", HttpMethods.Get, ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(LevelListEndpointLimits.TimeoutDuration, LevelListEndpointLimits.RequestAmount, - LevelListEndpointLimits.BlockDuration, LevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.Lbp1GetPlaylistContents)] public SerializedMinimalLevelList? GetPlaylistSlots(RequestContext context, DataContext dataContext, int id) { GamePlaylist? playlist = dataContext.Database.GetPlaylistById(id); @@ -158,8 +154,7 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext, [GameEndpoint("playlistsContainingSlot/{slotType}/{slotId}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(NotFound)] - [RateLimitSettings(PlaylistListEndpointLimits.TimeoutDuration, PlaylistListEndpointLimits.RequestAmount, - PlaylistListEndpointLimits.BlockDuration, PlaylistListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.Lbp1GetListOfPlaylists)] public SerializedMinimalLevelList? PlaylistsContainingSlot(RequestContext context, DataContext dataContext, string slotType, int slotId) { @@ -210,8 +205,7 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext, [GameEndpoint("setPlaylistMetaData/{id}", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistCreationEndpointLimits.UploadTimeoutDuration, PlaylistCreationEndpointLimits.MaxCreateAmount, - PlaylistCreationEndpointLimits.UploadBlockDuration, PlaylistCreationEndpointLimits.CreateBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistMetadata)] public Response UpdatePlaylistMetadata(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, int id, SerializedLbp1Playlist body) { if (user.IsWriteBlocked(config)) @@ -241,6 +235,7 @@ public Response UpdatePlaylistMetadata(RequestContext context, GameServerConfig [GameEndpoint("deletePlaylist/{id}", HttpMethods.Post)] [RequireEmailVerified] + [EndpointRateLimit(EndpointBucketId.DeletePlaylist)] public Response DeletePlaylist(RequestContext context, GameDatabaseContext database, GameUser user, int id) { GamePlaylist? playlist = database.GetPlaylistById(id); @@ -257,8 +252,7 @@ public Response DeletePlaylist(RequestContext context, GameDatabaseContext datab [GameEndpoint("addToPlaylist/{playlistId}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(PlaylistModificationEndpointLimits.TimeoutDuration, PlaylistModificationEndpointLimits.RequestAmount, - PlaylistModificationEndpointLimits.BlockDuration, PlaylistModificationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistContents)] public Response AddSlotToPlaylist(RequestContext context, GameServerConfig config, GameDatabaseContext database, GameUser user, int playlistId) { if (user.IsWriteBlocked(config)) @@ -339,8 +333,7 @@ public Response AddSlotToPlaylist(RequestContext context, GameServerConfig confi [GameEndpoint("removeFromPlaylist/{playlistId}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(PlaylistModificationEndpointLimits.TimeoutDuration, PlaylistModificationEndpointLimits.RequestAmount, - PlaylistModificationEndpointLimits.BlockDuration, PlaylistModificationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistContents)] public Response RemoveSlotFromPlaylist(RequestContext context, GameServerConfig config, GameDatabaseContext database, GameUser user, int playlistId) { @@ -392,8 +385,7 @@ public Response RemoveSlotFromPlaylist(RequestContext context, GameServerConfig [GameEndpoint("moveFromPlaylist/{from}", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistModificationEndpointLimits.TimeoutDuration, PlaylistModificationEndpointLimits.RequestAmount, - PlaylistModificationEndpointLimits.BlockDuration, PlaylistModificationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistContents)] public Response MoveSlotFromPlaylist(RequestContext context, GameServerConfig config, GameDatabaseContext database, GameUser user, int from) { if (user.IsWriteBlocked(config)) diff --git a/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp3PlaylistEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp3PlaylistEndpoints.cs index 31bd16442..e48097729 100644 --- a/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp3PlaylistEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Playlists/Lbp3PlaylistEndpoints.cs @@ -1,12 +1,12 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Constants; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Levels; @@ -15,8 +15,6 @@ using Refresh.Interfaces.Game.Types.Levels; using Refresh.Interfaces.Game.Types.Lists; using Refresh.Interfaces.Game.Types.Playlists; -using Refresh.Core.RateLimits.Playlists; -using Refresh.Core.RateLimits.Relations; using Refresh.Database.Models; namespace Refresh.Interfaces.Game.Endpoints.Playlists; @@ -25,8 +23,7 @@ public class Lbp3PlaylistEndpoints : EndpointGroup { [GameEndpoint("playlists", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistCreationEndpointLimits.UploadTimeoutDuration, PlaylistCreationEndpointLimits.MaxCreateAmount, - PlaylistCreationEndpointLimits.UploadBlockDuration, PlaylistCreationEndpointLimits.CreateBucket)] + [EndpointRateLimit(EndpointBucketId.CreatePlaylist)] public Response CreatePlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, SerializedLbp3Playlist body) { if (user.IsWriteBlocked(config)) @@ -69,8 +66,7 @@ public Response CreatePlaylist(RequestContext context, GameServerConfig config, [GameEndpoint("playlists/{playlistId}", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistCreationEndpointLimits.UploadTimeoutDuration, PlaylistCreationEndpointLimits.MaxCreateAmount, - PlaylistCreationEndpointLimits.UploadBlockDuration, PlaylistCreationEndpointLimits.CreateBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistMetadata)] public Response UpdatePlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, SerializedLbp3Playlist body, int playlistId) { if (user.IsWriteBlocked(config)) @@ -97,6 +93,7 @@ public Response UpdatePlaylist(RequestContext context, GameServerConfig config, [GameEndpoint("playlists/{playlistId}/delete", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] + [EndpointRateLimit(EndpointBucketId.DeletePlaylist)] public Response DeletePlaylist(RequestContext context, DataContext dataContext, GameUser user, int playlistId) { GamePlaylist? playlist = dataContext.Database.GetPlaylistById(playlistId); @@ -114,8 +111,7 @@ public Response DeletePlaylist(RequestContext context, DataContext dataContext, [GameEndpoint("playlists/{playlistId}/slots", HttpMethods.Get, ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(Lbp3PlaylistLevelListEndpointLimits.TimeoutDuration, Lbp3PlaylistLevelListEndpointLimits.RequestAmount, - Lbp3PlaylistLevelListEndpointLimits.BlockDuration, Lbp3PlaylistLevelListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.Lbp3GetPlaylistContents)] public SerializedMinimalLevelList? GetPlaylistLevels(RequestContext context, DataContext dataContext, GameUser user, int playlistId) { GamePlaylist? playlist = dataContext.Database.GetPlaylistById(playlistId); @@ -134,8 +130,7 @@ public Response DeletePlaylist(RequestContext context, DataContext dataContext, [GameEndpoint("playlists/{playlistId}/slots", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistModificationEndpointLimits.TimeoutDuration, PlaylistModificationEndpointLimits.RequestAmount, - PlaylistModificationEndpointLimits.BlockDuration, PlaylistModificationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistContents)] public Response AddLevelsToPlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, SerializedLevelIdList body, GameUser user, int playlistId) { if (user.IsWriteBlocked(config)) @@ -159,8 +154,7 @@ public Response AddLevelsToPlaylist(RequestContext context, GameServerConfig con [GameEndpoint("playlists/{playlistId}/order_slots", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistModificationEndpointLimits.TimeoutDuration, PlaylistModificationEndpointLimits.RequestAmount, - PlaylistModificationEndpointLimits.BlockDuration, PlaylistModificationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistContents)] public Response ReorderPlaylistLevels(RequestContext context, GameServerConfig config, DataContext dataContext, SerializedLevelIdList body, GameUser user, int playlistId) { if (user.IsWriteBlocked(config)) @@ -180,8 +174,7 @@ public Response ReorderPlaylistLevels(RequestContext context, GameServerConfig c [GameEndpoint("playlists/{playlistId}/slots/{levelId}/delete", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(PlaylistModificationEndpointLimits.TimeoutDuration, PlaylistModificationEndpointLimits.RequestAmount, - PlaylistModificationEndpointLimits.BlockDuration, PlaylistModificationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdatePlaylistContents)] public Response RemoveLevelFromPlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, int playlistId, int levelId) { if (user.IsWriteBlocked(config)) @@ -206,8 +199,7 @@ public Response RemoveLevelFromPlaylist(RequestContext context, GameServerConfig [GameEndpoint("user/{username}/playlists", HttpMethods.Get, ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(PlaylistListEndpointLimits.TimeoutDuration, PlaylistListEndpointLimits.Lbp3RequestAmount, - PlaylistListEndpointLimits.BlockDuration, PlaylistListEndpointLimits.Lbp3RequestBucket)] + [EndpointRateLimit(EndpointBucketId.Lbp3GetListOfPlaylists)] public SerializedLbp3PlaylistList? GetPlaylistsByUser(RequestContext context, DataContext dataContext, string username) { GameUser? user = dataContext.Database.GetUserByUsername(username); @@ -225,8 +217,7 @@ public Response RemoveLevelFromPlaylist(RequestContext context, GameServerConfig [GameEndpoint("favouritePlaylists/{username}", HttpMethods.Get, ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(PlaylistListEndpointLimits.TimeoutDuration, PlaylistListEndpointLimits.RequestAmount, - PlaylistListEndpointLimits.BlockDuration, PlaylistListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.Lbp3GetListOfPlaylists)] public SerializedLbp3PlaylistList? GetFavouritedPlaylists(RequestContext context, DataContext dataContext, string username) { GameUser? user = dataContext.Database.GetUserByUsername(username); @@ -244,8 +235,7 @@ public Response RemoveLevelFromPlaylist(RequestContext context, GameServerConfig [GameEndpoint("favourite/playlist/{playlistId}", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.HeartPlaylist)] public Response FavouritePlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, int playlistId) { if (user.IsWriteBlocked(config)) @@ -261,8 +251,7 @@ public Response FavouritePlaylist(RequestContext context, GameServerConfig confi [GameEndpoint("unfavourite/playlist/{playlistId}", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.HeartPlaylist)] public Response UnfavouritePlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, int playlistId) { if (user.IsWriteBlocked(config)) diff --git a/Refresh.Interfaces.Game/Endpoints/PresenceEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/PresenceEndpoints.cs index cd63831fb..60461d93a 100644 --- a/Refresh.Interfaces.Game/Endpoints/PresenceEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/PresenceEndpoints.cs @@ -1,10 +1,10 @@ using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Core.Authentication.Permission; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Database; using Refresh.Database.Models.Authentication; @@ -14,30 +14,25 @@ namespace Refresh.Interfaces.Game.Endpoints; public class PresenceEndpoints : EndpointGroup { - private const int TimeoutDuration = 420; - private const int RequestAmount = 12; - private const int BlockDuration = 300; - private const string RequestBucket = "game-stats"; - [GameEndpoint("playersInPodCount")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(TimeoutDuration, RequestAmount, BlockDuration, RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetInstanceStats)] public int TotalPlayersInPod(RequestContext context, MatchService match) => match.RoomAccessor.GetStatistics().PlayersInPodCount; [GameEndpoint("totalPlayerCount")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(TimeoutDuration, RequestAmount, BlockDuration, RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetInstanceStats)] public int TotalPlayers(RequestContext context, MatchService match) => match.RoomAccessor.GetStatistics().PlayerCount; [GameEndpoint("planetStats/highestSlotId")] [GameEndpoint("planetStats/totalLevelCount")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(TimeoutDuration, RequestAmount, BlockDuration, RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetInstanceStats)] public int GetTotalLevelCount(RequestContext context, GameDatabaseContext database, Token token) => database.GetTotalLevelCount(token.TokenGame); [GameEndpoint("planetStats", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(TimeoutDuration, RequestAmount, BlockDuration, RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetInstanceStats)] public SerializedLevelStatisticsResponse GetLevelStatistics(RequestContext context, GameDatabaseContext database, Token token) => new() { TotalLevels = database.GetTotalLevelCount(token.TokenGame), diff --git a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs index 9fa6ace34..8a1b78fa5 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -1,13 +1,11 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Relations; -using Refresh.Core.RateLimits.Users; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Levels; @@ -21,8 +19,7 @@ public class RelationEndpoints : EndpointGroup { [GameEndpoint("favourite/slot/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.HeartLevel)] public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) { @@ -40,8 +37,7 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab [GameEndpoint("unfavourite/slot/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.HeartLevel)] public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) { @@ -59,8 +55,7 @@ public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext dat [GameEndpoint("favourite/user/{username}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.HeartUser)] public Response FavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, GameServerConfig config, DataContext dataContext) { @@ -78,8 +73,7 @@ public Response FavouriteUser(RequestContext context, GameDatabaseContext databa [GameEndpoint("unfavourite/user/{username}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.HeartUser)] public Response UnfavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, GameServerConfig config, DataContext dataContext) { @@ -98,8 +92,7 @@ public Response UnfavouriteUser(RequestContext context, GameDatabaseContext data [GameEndpoint("favouriteUsers/{username}", ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(UserListEndpointLimits.TimeoutDuration, UserListEndpointLimits.RequestAmount, - UserListEndpointLimits.BlockDuration, UserListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfUsers)] public SerializedFavouriteUserList? GetFavouriteUsers(RequestContext context, GameDatabaseContext database, string username, DataContext dataContext) { @@ -114,8 +107,7 @@ public Response UnfavouriteUser(RequestContext context, GameDatabaseContext data [GameEndpoint("lolcatftw/add/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.QueueLevel)] public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); @@ -127,8 +119,7 @@ public Response QueueLevel(RequestContext context, GameDatabaseContext database, [GameEndpoint("lolcatftw/remove/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.QueueLevel)] public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); @@ -140,8 +131,7 @@ public Response DequeueLevel(RequestContext context, GameDatabaseContext databas [GameEndpoint("lolcatftw/clear", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.QueueLevel)] public Response ClearQueue(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext) { database.ClearQueue(user); @@ -150,8 +140,7 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database, [GameEndpoint("tag/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(LevelTaggingEndpointLimits.TimeoutDuration, LevelTaggingEndpointLimits.RequestAmount, - LevelTaggingEndpointLimits.BlockDuration, LevelTaggingEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.TagLevel)] public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, string slotType, int id, string body, GameServerConfig config) { diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index f05777e14..01180d52e 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -1,7 +1,6 @@ using System.Drawing; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; @@ -9,6 +8,7 @@ using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Database; using Refresh.Database.Models.Authentication; using Refresh.Database.Models.Levels; @@ -25,7 +25,7 @@ public class ReportingEndpoints : EndpointGroup // TODO: LBP1 beta builds upload all related assets after sending the report itself to this endpoint, handle that case [GameEndpoint("grief", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] - [RateLimitSettings(600, 10, 300, "grief-report-upload")] + [EndpointRateLimit(EndpointBucketId.GameUploadGriefReport)] public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body, GameUser user, IDateTimeProvider time, Token token, GameServerConfig config) { diff --git a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs index cc2b2d462..33e75af8b 100644 --- a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Core.Storage; using Bunkum.Listener.Protocol; @@ -12,6 +11,7 @@ using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; using Refresh.Core.Importing; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Data; using Refresh.Database; @@ -30,7 +30,7 @@ public class ResourceEndpoints : EndpointGroup [GameEndpoint("upload/{hash}", HttpMethods.Post)] [RequireEmailVerified] [SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] - [RateLimitSettings(300, 200, 240, "game-asset-upload")] + [EndpointRateLimit(EndpointBucketId.GameUploadAsset)] public Response UploadAsset(RequestContext context, string hash, string? type, byte[] body, IDataStore dataStore, GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider, Token token, DataContext dataContext) @@ -111,7 +111,7 @@ public Response UploadAsset(RequestContext context, string hash, string? type, b [GameEndpoint("r/{hash}")] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(300, 340, 240, "game-asset-download")] + [EndpointRateLimit(EndpointBucketId.GameDownloadAsset)] public Response GetResource(RequestContext context, GameUser user, Token token, string hash, DataContext dataContext, ChallengeGhostRateLimitService ghostService) { if (!CommonPatterns.Sha1Regex().IsMatch(hash)) return BadRequest; @@ -156,7 +156,7 @@ public Response GetResource(RequestContext context, GameUser user, Token token, [GameEndpoint("filterResources", HttpMethods.Post, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(BadRequest)] - [RateLimitSettings(450, 12, 300, "game-filter-resources")] + [EndpointRateLimit(EndpointBucketId.GameFilterModeratedAssets)] public SerializedResourceList? GetAssetsMissingFromStore(RequestContext context, SerializedResourceList body, IDataStore dataStore) { if(body.Items.Any(hash => !CommonPatterns.Sha1Regex().IsMatch(hash))) diff --git a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs index 197e49f07..de425fe58 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs @@ -1,6 +1,5 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; -using Bunkum.Core.RateLimit; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; @@ -8,8 +7,6 @@ using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; -using Refresh.Core.RateLimits.Relations; -using Refresh.Core.RateLimits.Reviews; using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Authentication; @@ -17,6 +14,7 @@ using Refresh.Database.Models.Levels; using Refresh.Database.Models.Users; using Refresh.Interfaces.Game.Types.Reviews; +using Refresh.Core.RateLimits.EndpointRateLimiting; namespace Refresh.Interfaces.Game.Endpoints; @@ -24,9 +22,8 @@ public class ReviewEndpoints : EndpointGroup { [GameEndpoint("dpadrate/{slotType}/{id}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] - public Response SubmitRating(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, + [EndpointRateLimit(EndpointBucketId.RateLevel)] + public Response SubmitLevelDpadRating(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, GameServerConfig config, DataContext dataContext) { if (user.IsWriteBlocked(config)) @@ -54,9 +51,8 @@ public Response SubmitRating(RequestContext context, GameDatabaseContext databas [GameEndpoint("rate/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [AllowEmptyBody] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] - public Response RateUserLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, + [EndpointRateLimit(EndpointBucketId.RateLevel)] + public Response SubmitLevelStarRating(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, DataContext dataContext, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -99,8 +95,7 @@ public Response RateUserLevel(RequestContext context, GameDatabaseContext databa [GameEndpoint("reviewsFor/{slotType}/{id}", ContentType.Xml)] [AllowEmptyBody] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ReviewListEndpointLimits.TimeoutDuration, ReviewListEndpointLimits.RequestAmount, - ReviewListEndpointLimits.BlockDuration, ReviewListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfReviews)] public Response GetReviewsForLevel(RequestContext context, GameDatabaseContext database, string slotType, int id, DataContext dataContext) { @@ -117,8 +112,7 @@ public Response GetReviewsForLevel(RequestContext context, GameDatabaseContext d [GameEndpoint("reviewsBy/{username}", ContentType.Xml)] [AllowEmptyBody] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(ReviewListEndpointLimits.TimeoutDuration, ReviewListEndpointLimits.RequestAmount, - ReviewListEndpointLimits.BlockDuration, ReviewListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfReviews)] public Response GetReviewsByUser(RequestContext context, GameDatabaseContext database, string username, DataContext dataContext) { @@ -138,8 +132,7 @@ public Response GetReviewsByUser(RequestContext context, GameDatabaseContext dat [GameEndpoint("postReview/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(ReviewUploadEndpointLimits.TimeoutDuration, ReviewUploadEndpointLimits.RequestAmount, - ReviewUploadEndpointLimits.BlockDuration, ReviewUploadEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.UploadReview)] public Response PostReviewForLevel(RequestContext context, GameDatabaseContext database, string slotType, @@ -192,8 +185,7 @@ public Response PostReviewForLevel(RequestContext context, [GameEndpoint("rateReview/user/{levelId}/{username}", HttpMethods.Post)] [RequireEmailVerified] - [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, - CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.RateReview)] public Response SubmitReviewRating(RequestContext request, GameDatabaseContext database, GameUser user, int levelId, string username, GameServerConfig config) { diff --git a/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs index 45135decb..1d3f4bda4 100644 --- a/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs @@ -11,7 +11,7 @@ using Refresh.Core.Configuration; using Refresh.Core.Helpers; using Refresh.Core.Importing; -using Refresh.Core.RateLimits.Users; +using Refresh.Core.RateLimits.EndpointRateLimiting; using Refresh.Core.Services; using Refresh.Core.Types.Assets.Validation; using Refresh.Core.Types.Data; @@ -30,16 +30,14 @@ public class UserEndpoints : EndpointGroup { [GameEndpoint("user/{name}", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(SingleUserEndpointLimits.TimeoutDuration, SingleUserEndpointLimits.GameRequestAmount, - SingleUserEndpointLimits.BlockDuration, SingleUserEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetSingleUser)] public GameUserResponse? GetUser(RequestContext context, GameDatabaseContext database, string name, Token token, IDataStore dataStore, DataContext dataContext) => GameUserResponse.FromOld(database.GetUserByUsername(name), dataContext); [GameEndpoint("users", HttpMethods.Get, ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(UserListEndpointLimits.TimeoutDuration, UserListEndpointLimits.RequestAmount, - UserListEndpointLimits.BlockDuration, UserListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfUsers)] public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseContext database, Token token, IDataStore dataStore, DataContext dataContext) { @@ -65,8 +63,7 @@ public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseC [GameEndpoint("myFriends", HttpMethods.Get, ContentType.Xml)] [NullStatusCode(NotFound)] [MinimumRole(GameUserRole.Restricted)] - [RateLimitSettings(UserListEndpointLimits.TimeoutDuration, UserListEndpointLimits.RequestAmount, - UserListEndpointLimits.BlockDuration, UserListEndpointLimits.RequestBucket)] + [EndpointRateLimit(EndpointBucketId.GameGetListOfUsers)] public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext) { @@ -76,8 +73,7 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont [GameEndpoint("updateUser", HttpMethods.Post, ContentType.Xml)] [NullStatusCode(BadRequest)] - [RateLimitSettings(UserModificationEndpointLimits.TimeoutDuration, UserModificationEndpointLimits.GameRequestAmount, - UserModificationEndpointLimits.BlockDuration, UserModificationEndpointLimits.GameRequestBucket)] + [EndpointRateLimit(EndpointBucketId.UpdateUser)] public Response UpdateUser(RequestContext context, DataContext dataContext, GameUser user, string body, GuidCheckerService guidChecker, AssetImporter importer, AipiService? aipi) { @@ -209,15 +205,10 @@ public Response UpdateUser(RequestContext context, DataContext dataContext, Game return OK; } - private const int PinTimeoutDuration = 480; - private const int PinRequestAmount = 8; - private const int PinBlockDuration = 420; - private const string PinBucket = "game-pins"; // Amount could aswell be 1, considering the default intervall in the NWS (5 minutes), but profile pin updating exists... - [GameEndpoint("update_my_pins", HttpMethods.Post, ContentType.Json)] [RequireEmailVerified] [NullStatusCode(BadRequest)] - [RateLimitSettings(PinTimeoutDuration, PinRequestAmount, PinBlockDuration, PinBucket)] + [EndpointRateLimit(EndpointBucketId.GameSyncPinProgress)] public SerializedPins? UpdatePins(RequestContext context, DataContext dataContext, GameUser user, SerializedPins body, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -267,7 +258,7 @@ public Response UpdateUser(RequestContext context, DataContext dataContext, Game [GameEndpoint("get_my_pins", HttpMethods.Get, ContentType.Json)] [MinimumRole(GameUserRole.Restricted)] [NullStatusCode(Unauthorized)] - [RateLimitSettings(PinTimeoutDuration, PinRequestAmount, PinBlockDuration, PinBucket)] + [EndpointRateLimit(EndpointBucketId.GameSyncPinProgress)] public SerializedPins? GetPins(RequestContext context, DataContext dataContext, GameUser user) { return SerializedPins.FromOld