Skip to content

Commit 61e5293

Browse files
authored
Playlist upload rate-limits and icon validation (#986)
Playlist creation requests from both LBP1 and 3 are now rate-limited to discourage spamming empty unedited playlists, which has already happened multiple times since Patchwork 1.3 released, so it might become a problem soon. Update requests are also rate-limited but less tightly. Also, both LBP1 playlist upload (create and update) endpoints will now set the request's icon to blank if it is invalid or missing, and I've finally added a few unit tests for playlists, mostly to test icon validation.
2 parents e5c1204 + 4550e55 commit 61e5293

File tree

4 files changed

+322
-3
lines changed

4 files changed

+322
-3
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Refresh.Core.RateLimits;
2+
3+
/// <summary>
4+
/// Shared rate limit parameters for game and API playlist endpoints
5+
/// </summary>
6+
public static class PlaylistEndpointLimits
7+
{
8+
// rate-limits
9+
public const int UploadTimeoutDuration = 450;
10+
public const int MaxCreateAmount = 8; // should be enough
11+
public const int MaxUpdateAmount = 12;
12+
public const int UploadBlockDuration = 300;
13+
public const string CreateBucket = "playlist-create";
14+
public const string UpdateBucket = "playlist-update";
15+
}

Refresh.Interfaces.Game/Endpoints/Playlists/Lbp1PlaylistEndpoints.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Bunkum.Core;
22
using Bunkum.Core.Endpoints;
3+
using Bunkum.Core.RateLimit;
34
using Bunkum.Core.Responses;
45
using Bunkum.Listener.Protocol;
56
using Bunkum.Protocols.Http;
@@ -11,6 +12,7 @@
1112
using Refresh.Database.Models.Levels;
1213
using Refresh.Database.Models.Playlists;
1314
using Refresh.Database.Models.Users;
15+
using static Refresh.Core.RateLimits.PlaylistEndpointLimits;
1416
using Refresh.Interfaces.Game.Types.Levels;
1517
using Refresh.Interfaces.Game.Types.Lists;
1618
using Refresh.Interfaces.Game.Types.Playlists;
@@ -19,9 +21,38 @@ namespace Refresh.Interfaces.Game.Endpoints.Playlists;
1921

2022
public class Lbp1PlaylistEndpoints : EndpointGroup
2123
{
24+
/// <summary>
25+
/// Validate the playlist icon. If it's blank, an invalid GUID or a remote asset which doesn't exist on the server, reset to blank hash
26+
/// and do not return an error to not upset the game in certain cases (also because the user cannot choose the icon when creating a playlist).
27+
/// </summary>
28+
private string ValidateIconHash(string iconHash, DataContext dataContext)
29+
{
30+
if (iconHash.IsBlankHash())
31+
{
32+
return "0";
33+
}
34+
else if (iconHash.StartsWith('g'))
35+
{
36+
//Parse out the GUID
37+
long guid = long.Parse(iconHash.AsSpan()[1..]);
38+
39+
if (!dataContext.GuidChecker.IsTextureGuid(dataContext.Game, guid))
40+
{
41+
return "0";
42+
}
43+
}
44+
else if (!dataContext.DataStore.ExistsInStore(iconHash))
45+
{
46+
return "0";
47+
}
48+
49+
return iconHash;
50+
}
51+
2252
// Creates a playlist, with an optional parent ID
2353
[GameEndpoint("createPlaylist", HttpMethods.Post, ContentType.Xml)]
2454
[RequireEmailVerified]
55+
[RateLimitSettings(UploadTimeoutDuration, MaxCreateAmount, UploadBlockDuration, CreateBucket)]
2556
public Response CreatePlaylist(RequestContext context, DataContext dataContext, GameServerConfig config, SerializedLbp1Playlist body)
2657
{
2758
GameUser user = dataContext.User!;
@@ -57,6 +88,9 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext,
5788
if (body.Description.Length > UgcLimits.DescriptionLimit)
5889
body.Description = body.Description[..UgcLimits.DescriptionLimit];
5990

91+
// Validate icon
92+
body.Icon = this.ValidateIconHash(body.Icon, dataContext);
93+
6094
// Create the playlist, marking it as the root playlist if the user does not have one set already
6195
GamePlaylist playlist = dataContext.Database.CreatePlaylist(user, body, rootPlaylist == null);
6296

@@ -150,12 +184,13 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext,
150184

151185
[GameEndpoint("setPlaylistMetaData/{id}", HttpMethods.Post, ContentType.Xml)]
152186
[RequireEmailVerified]
153-
public Response UpdatePlaylistMetadata(RequestContext context, GameServerConfig config, GameDatabaseContext database, GameUser user, int id, SerializedLbp1Playlist body)
187+
[RateLimitSettings(UploadTimeoutDuration, MaxUpdateAmount, UploadBlockDuration, UpdateBucket)]
188+
public Response UpdatePlaylistMetadata(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, int id, SerializedLbp1Playlist body)
154189
{
155190
if (user.IsWriteBlocked(config))
156191
return Unauthorized;
157192

158-
GamePlaylist? playlist = database.GetPlaylistById(id);
193+
GamePlaylist? playlist = dataContext.Database.GetPlaylistById(id);
159194
if (playlist == null)
160195
return NotFound;
161196

@@ -170,7 +205,10 @@ public Response UpdatePlaylistMetadata(RequestContext context, GameServerConfig
170205
if (body.Description.Length > UgcLimits.DescriptionLimit)
171206
body.Description = body.Description[..UgcLimits.DescriptionLimit];
172207

173-
database.UpdatePlaylist(playlist, body);
208+
// Validate icon
209+
body.Icon = this.ValidateIconHash(body.Icon, dataContext);
210+
211+
dataContext.Database.UpdatePlaylist(playlist, body);
174212
return OK;
175213
}
176214

Refresh.Interfaces.Game/Endpoints/Playlists/Lbp3PlaylistEndpoints.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Bunkum.Core;
22
using Bunkum.Core.Endpoints;
3+
using Bunkum.Core.RateLimit;
34
using Bunkum.Core.Responses;
45
using Bunkum.Listener.Protocol;
56
using Bunkum.Protocols.Http;
@@ -11,6 +12,7 @@
1112
using Refresh.Database.Models.Levels;
1213
using Refresh.Database.Models.Playlists;
1314
using Refresh.Database.Models.Users;
15+
using static Refresh.Core.RateLimits.PlaylistEndpointLimits;
1416
using Refresh.Interfaces.Game.Types.Levels;
1517
using Refresh.Interfaces.Game.Types.Lists;
1618
using Refresh.Interfaces.Game.Types.Playlists;
@@ -21,6 +23,7 @@ public class Lbp3PlaylistEndpoints : EndpointGroup
2123
{
2224
[GameEndpoint("playlists", HttpMethods.Post, ContentType.Xml)]
2325
[RequireEmailVerified]
26+
[RateLimitSettings(UploadTimeoutDuration, MaxCreateAmount, UploadBlockDuration, CreateBucket)]
2427
public Response CreatePlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, SerializedLbp3Playlist body)
2528
{
2629
if (user.IsWriteBlocked(config))
@@ -48,6 +51,7 @@ public Response CreatePlaylist(RequestContext context, GameServerConfig config,
4851

4952
[GameEndpoint("playlists/{playlistId}", HttpMethods.Post, ContentType.Xml)]
5053
[RequireEmailVerified]
54+
[RateLimitSettings(UploadTimeoutDuration, MaxUpdateAmount, UploadBlockDuration, UpdateBucket)]
5155
public Response UpdatePlaylist(RequestContext context, GameServerConfig config, DataContext dataContext, GameUser user, SerializedLbp3Playlist body, int playlistId)
5256
{
5357
if (user.IsWriteBlocked(config))

0 commit comments

Comments
 (0)