forked from LittleBigRefresh/Refresh
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResourceEndpoints.cs
More file actions
161 lines (135 loc) · 7.21 KB
/
Copy pathResourceEndpoints.cs
File metadata and controls
161 lines (135 loc) · 7.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
using System.Diagnostics;
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;
using Bunkum.Protocols.Http;
using Refresh.Common.Time;
using Refresh.Common.Verification;
using Refresh.Core.Authentication.Permission;
using Refresh.Core.Configuration;
using Refresh.Core.Importing;
using Refresh.Core.Services;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.Game.Types.Lists;
namespace Refresh.Interfaces.Game.Endpoints;
public class ResourceEndpoints : EndpointGroup
{
//NOTE: type does nothing here, but it's sent by LBP so we have to accept it
[GameEndpoint("upload/{hash}/{type}", HttpMethods.Post)]
[GameEndpoint("upload/{hash}", HttpMethods.Post)]
[RequireEmailVerified]
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
[RateLimitSettings(300, 200, 240, "game-asset-upload")]
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)
{
if (user.IsWriteBlocked(config))
return Unauthorized;
if (!CommonPatterns.Sha1Regex().IsMatch(hash)) return BadRequest;
bool isPSP = context.IsPSP();
string assetPath = hash;
if (isPSP)
assetPath = $"psp/{hash}";
if (dataStore.ExistsInStore(assetPath))
return Conflict;
RolePermissions rolePerms = user.GetRolePermissionsForUser(config);
if (body.Length + user.FilesizeQuotaUsage > rolePerms.UserFilesizeQuota)
{
context.Logger.LogWarning(BunkumCategory.UserContent, "User {0} has hit the filesize quota ({1} bytes), rejecting.", user.Username, rolePerms.UserFilesizeQuota);
return RequestEntityTooLarge;
}
if (body.Length > 1_048_576 * 2)
{
context.Logger.LogWarning(BunkumCategory.UserContent, "{0} is above 2MB ({1} bytes), rejecting.", hash, body.Length);
return RequestEntityTooLarge;
}
GameAsset? gameAsset = importer.ReadAndVerifyAsset(hash, body, token.TokenPlatform, database);
if (gameAsset == null)
return BadRequest;
gameAsset.UploadDate = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(gameAsset.UploadDate.ToUnixTimeSeconds(), timeProvider.EarliestDate, timeProvider.TimestampSeconds));
AssetFlags blockedAssetFlags = rolePerms.BlockedAssetFlags.ToAssetFlags();
// Don't block any assets uploaded from PSP, else block any unwanted assets,
// For example, if the "blocked asset flags" has the "Media" bit set, and so does the asset,
// then that bit will be set after the AND operation, and we know to block it.
if ((gameAsset.AssetFlags & blockedAssetFlags) != 0 && !isPSP)
{
context.Logger.LogWarning(BunkumCategory.UserContent, $"{gameAsset.AssetType} {hash} by {user} is above configured safety limit " +
$"({gameAsset.AssetFlags} is blocked by {blockedAssetFlags})");
return Unauthorized;
}
if (isPSP && gameAsset.AssetFlags.HasFlag(AssetFlags.Media) && blockedAssetFlags.HasFlag(AssetFlags.Media))
{
context.Logger.LogWarning(BunkumCategory.UserContent, $"{gameAsset.AssetType} {hash} by {user} cannot be uploaded because media is disabled");
return Unauthorized;
}
if (!dataStore.WriteToStore(assetPath, body))
return InternalServerError;
gameAsset.OriginalUploader = user;
database.AddAssetToDatabase(gameAsset);
dataContext.Cache.CacheAsset(gameAsset.AssetHash, gameAsset);
database.IncrementUserFilesizeQuota(user, body.Length);
GameLevel? level = database.GetLevelByRootResource(hash);
// If there is a level with this root resource, update the modded status of the level
// This is catching the case where the level resource was uploaded after the slot was published.
if (level != null) database.UpdateLevelModdedStatus(level);
context.Logger.LogInfo(BunkumCategory.UserContent, $"{user} uploaded a {gameAsset.AssetType} ({body.Length / 1024f:F1} KB)");
return OK;
}
[GameEndpoint("r/{hash}")]
[MinimumRole(GameUserRole.Restricted)]
[RateLimitSettings(300, 340, 240, "game-asset-download")]
public Response GetResource(RequestContext context, GameUser user, Token token, string hash, DataContext dataContext, ChallengeGhostRateLimitService ghostService)
{
if (!CommonPatterns.Sha1Regex().IsMatch(hash)) return BadRequest;
//If the request comes from a PSP client,
if (context.IsPSP())
{
//Point the hash into the `psp` folder
hash = $"psp/{hash}";
}
if (!dataContext.DataStore.ExistsInStore(hash))
return NotFound;
// Part of a workaround to prevent LBP Hub from breaking challenge ghost replay.
// See ChallengeGhostRateLimitService's summary for more information.
if (token.TokenGame == TokenGame.BetaBuild && dataContext.Cache.GetAssetInfo(hash, dataContext.Database)?.AssetType == GameAssetType.ChallengeGhost)
{
if (ghostService.IsUserRateLimited(user.UserId))
{
// Return OK but with no content here, else Hub will try downloading this asset a second time before giving up,
// which makes the game freeze longer and might screw with this rate limit.
context.Logger.LogDebug(BunkumCategory.UserContent, $"Returning OK without ChallengeGhost content due to dedicated rate-limit");
return OK;
}
else
{
// Continue with request normally, but also add user to rate limit for requests in the near future
ghostService.AddUserToRateLimit(user.UserId);
}
}
if (!dataContext.DataStore.TryGetDataFromStore(hash, out byte[]? data))
return InternalServerError;
Debug.Assert(data != null);
return new Response(data, ContentType.BinaryData);
}
[GameEndpoint("showNotUploaded", HttpMethods.Post, ContentType.Xml)]
[GameEndpoint("filterResources", HttpMethods.Post, ContentType.Xml)]
[MinimumRole(GameUserRole.Restricted)]
[NullStatusCode(BadRequest)]
[RateLimitSettings(450, 12, 300, "game-filter-resources")]
public SerializedResourceList? GetAssetsMissingFromStore(RequestContext context, SerializedResourceList body, IDataStore dataStore)
{
if(body.Items.Any(hash => !CommonPatterns.Sha1Regex().IsMatch(hash)))
return null;
return new SerializedResourceList(body.Items.Where(r => !dataStore.ExistsInStore(context.IsPSP() ? $"psp/{r}" : r)));
}
}