Skip to content

Commit

Permalink
Verify all the things + fix grief reports (#139)
Browse files Browse the repository at this point in the history
This does some code cleanup and sanitizes user inputs a lot more (+ a
fix for grief reports)
  • Loading branch information
jvyden authored Aug 30, 2023
2 parents e3be36e + 1eff44e commit 4015ff4
Show file tree
Hide file tree
Showing 24 changed files with 406 additions and 96 deletions.
4 changes: 1 addition & 3 deletions Refresh.GameServer/Database/GameDatabaseContext.Photos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ public partial class GameDatabaseContext // Photos
{
public void UploadPhoto(SerializedPhoto photo, GameUser publisher)
{
long firstValidTime = new DateTimeOffset(2007, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds();

GamePhoto newPhoto = new()
{
SmallHash = photo.SmallHash,
Expand All @@ -25,7 +23,7 @@ public void UploadPhoto(SerializedPhoto photo, GameUser publisher)
LevelType = photo.Level.Type,
LevelId = photo.Level.LevelId,

TakenAt = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(photo.Timestamp, firstValidTime, this._time.TimestampSeconds)),
TakenAt = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(photo.Timestamp, this._time.EarliestDate, this._time.TimestampSeconds)),
PublishedAt = this._time.Now,
};

Expand Down
8 changes: 7 additions & 1 deletion Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NotEnoughLogs;
using Refresh.GameServer.Database;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Time;
using Refresh.GameServer.Types.Comments;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Lists;
Expand All @@ -16,8 +17,13 @@ namespace Refresh.GameServer.Endpoints.Game;
public class CommentEndpoints : EndpointGroup
{
[GameEndpoint("postUserComment/{username}", ContentType.Xml, Method.Post)]
public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, GameComment body, GameUser user)
public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, GameComment body, GameUser user, IDateTimeProvider timeProvider)
{
if (body.Content.Length > 4096)
{
return BadRequest;
}

GameUser? profile = database.GetUserByUsername(username);
if (profile == null) return NotFound;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Xml.Serialization;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.ApiV3.DataTypes;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Levels.SkillRewards;
using Refresh.GameServer.Types.Matching;
using Refresh.GameServer.Types.Reviews;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Endpoints.Game.DataTypes.Request;

[XmlRoot("slot")]
[XmlType("slot")]
public class GameLevelRequest : IDataConvertableFrom<GameLevelRequest, GameLevel>
{
[XmlElement("id")] public required int LevelId { get; set; }

[XmlElement("name")] public required string Title { get; set; }
[XmlElement("icon")] public required string IconHash { get; set; }
[XmlElement("description")] public required string Description { get; set; }
[XmlElement("location")] public required GameLocation Location { get; set; }

[XmlElement("game")] public required int GameVersion { get; set; }
[XmlElement("rootLevel")] public required string RootResource { get; set; }

[XmlElement("firstPublished")] public required long PublishDate { get; set; } // unix seconds
[XmlElement("lastUpdated")] public required long UpdateDate { get; set; }

[XmlElement("minPlayers")] public required int MinPlayers { get; set; }
[XmlElement("maxPlayers")] public required int MaxPlayers { get; set; }
[XmlElement("enforceMinMaxPlayers")] public required bool EnforceMinMaxPlayers { get; set; }

[XmlElement("sameScreenGame")] public required bool SameScreenGame { get; set; }

[XmlAttribute("type")] public string Type { get; set; } = "user";

[XmlElement("npHandle")] public SerializedUserHandle Handle { get; set; } = null!;

[XmlArray("customRewards")]
[XmlArrayItem("customReward")]
public required List<GameSkillReward> SkillRewards { get; set; }

[XmlElement("resource")] public List<string> XmlResources { get; set; } = new();

public static GameLevelRequest? FromOld(GameLevel? old)
{
if (old == null) return null;

GameLevelRequest request = new()
{
LevelId = old.LevelId,
Title = old.Title,
IconHash = old.IconHash,
Description = old.Description,
Location = old.Location,
GameVersion = old.GameVersion.ToSerializedGame(),
RootResource = old.RootResource,
PublishDate = old.PublishDate,
UpdateDate = old.UpdateDate,
MinPlayers = old.MinPlayers,
MaxPlayers = old.MaxPlayers,
EnforceMinMaxPlayers = old.EnforceMinMaxPlayers,
SameScreenGame = old.SameScreenGame,
SkillRewards = old.SkillRewards.ToList(),
};

return request;
}

public GameLevel ToGameLevel(GameUser publisher) =>
new()
{
LevelId = this.LevelId,
Title = this.Title,
IconHash = this.IconHash,
Description = this.Description,
Location = this.Location,
RootResource = this.RootResource,
PublishDate = this.PublishDate,
UpdateDate = this.UpdateDate,
MinPlayers = this.MinPlayers,
MaxPlayers = this.MaxPlayers,
EnforceMinMaxPlayers = this.EnforceMinMaxPlayers,
SameScreenGame = this.SameScreenGame,
SkillRewards = this.SkillRewards.ToArray(),
Publisher = publisher,
};

public static IEnumerable<GameLevelRequest> FromOldList(IEnumerable<GameLevel> oldList) => oldList.Select(FromOld)!;
}
22 changes: 7 additions & 15 deletions Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,19 @@ namespace Refresh.GameServer.Endpoints.Game.Levels;
public class LeaderboardEndpoints : EndpointGroup
{
[GameEndpoint("play/user/{id}", ContentType.Xml, Method.Post)]
public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, int? id)
public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id)
{
if (id == null) return BadRequest;

GameLevel? level = database.GetLevelById(id.Value);
GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

database.PlayLevel(level, user);
return OK;
}

[GameEndpoint("scoreboard/user/{id}", ContentType.Xml, Method.Post)]
public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseContext database, int? id, SerializedScore body)
public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseContext database, int id, SerializedScore body)
{
if (id == null) return BadRequest;

GameLevel? level = database.GetLevelById(id.Value);
GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

//Validate the score is a non-negative amount
Expand All @@ -41,8 +37,7 @@ public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseC
return BadRequest;
}

GameSubmittedScore? score = database.SubmitScore(body, user, level);
if (score == null) return Unauthorized;
GameSubmittedScore score = database.SubmitScore(body, user, level);

IEnumerable<ScoreWithRank>? scores = database.GetRankedScoresAroundScore(score, 5);
Debug.Assert(scores != null);
Expand All @@ -52,12 +47,9 @@ public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseC

[GameEndpoint("topscores/user/{id}/{type}", ContentType.Xml)]
[MinimumRole(GameUserRole.Restricted)]
public SerializedScoreList? GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, int? id, int? type)
public SerializedScoreList? GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, int id, int type)
{
if (id == null) return null;
if (type == null) return null;

GameLevel? level = database.GetLevelById(id.Value);
GameLevel? level = database.GetLevelById(id);
if (level == null) return null;

(int skip, int count) = context.GetPageData();
Expand Down
46 changes: 39 additions & 7 deletions Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
using Bunkum.HttpServer.Endpoints;
using Bunkum.HttpServer.Responses;
using Bunkum.HttpServer.Storage;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.Game.DataTypes.Request;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.UserData;
Expand All @@ -13,10 +15,40 @@ namespace Refresh.GameServer.Endpoints.Game.Levels;

public class PublishEndpoints : EndpointGroup
{
/// <summary>
/// Does basic verification on a level
/// </summary>
/// <param name="body">The level to verify</param>
/// <param name="user">The user that is attempting to upload</param>
/// <param name="logger">A logger instance</param>
/// <returns>Whether or not validation succeeded</returns>
private static bool VerifyLevel(GameLevelRequest body, GameUser user, LoggerContainer<BunkumContext> logger)
{
if (body.Title.Length > 256)
{
return false;
}

if (body.Description.Length > 4096)
{
return false;
}

if (body.MaxPlayers > 4 || body.MinPlayers > 4)
{
return false;
}

return true;
}

[GameEndpoint("startPublish", ContentType.Xml, Method.Post)]
[NullStatusCode(BadRequest)]
public SerializedLevelResources? StartPublish(RequestContext context, GameDatabaseContext database, GameLevelResponse body, IDataStore dataStore)
public SerializedLevelResources? StartPublish(RequestContext context, GameUser user, GameDatabaseContext database, GameLevelRequest body, IDataStore dataStore, LoggerContainer<BunkumContext> logger)
{
//If verifying the request fails, return null
if (!VerifyLevel(body, user, logger)) return null;

List<string> hashes = new();
hashes.AddRange(body.XmlResources);
hashes.Add(body.RootResource);
Expand All @@ -33,8 +65,11 @@ public class PublishEndpoints : EndpointGroup
}

[GameEndpoint("publish", ContentType.Xml, Method.Post)]
public Response PublishLevel(RequestContext context, GameUser user, Token token, GameDatabaseContext database, GameLevelResponse body, IDataStore dataStore)
public Response PublishLevel(RequestContext context, GameUser user, Token token, GameDatabaseContext database, GameLevelRequest body, IDataStore dataStore, LoggerContainer<BunkumContext> logger)
{
//If verifying the request fails, return null
if (!VerifyLevel(body, user, logger)) return BadRequest;

GameLevel level = body.ToGameLevel(user);
level.GameVersion = token.TokenGame;

Expand Down Expand Up @@ -67,12 +102,9 @@ public Response PublishLevel(RequestContext context, GameUser user, Token token,
return new Response(GameLevelResponse.FromOld(level)!, ContentType.Xml);
}

[GameEndpoint("unpublish/{idStr}", ContentType.Xml, Method.Post)]
public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, string idStr)
[GameEndpoint("unpublish/{id}", ContentType.Xml, Method.Post)]
public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id)
{
int.TryParse(idStr, out int id);
if (id == default) return BadRequest;

GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

Expand Down
28 changes: 8 additions & 20 deletions Refresh.GameServer/Endpoints/Game/RelationEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ namespace Refresh.GameServer.Endpoints.Game;

public class RelationEndpoints : EndpointGroup
{
[GameEndpoint("favourite/slot/user/{idStr}", Method.Post)]
public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr)
[GameEndpoint("favourite/slot/user/{id}", Method.Post)]
public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id)
{
int.TryParse(idStr, out int id);
if (id == default) return BadRequest;

GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

Expand All @@ -30,12 +27,9 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab
return Unauthorized;
}

[GameEndpoint("unfavourite/slot/user/{idStr}", Method.Post)]
public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr)
[GameEndpoint("unfavourite/slot/user/{id}", Method.Post)]
public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id)
{
int.TryParse(idStr, out int id);
if (id == default) return BadRequest;

GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

Expand Down Expand Up @@ -84,12 +78,9 @@ public Response UnfavouriteUser(RequestContext context, GameDatabaseContext data
return new SerializedFavouriteUserList(GameUserResponse.FromOldListWithExtraData(users, token.TokenGame).ToList(), users.Count);
}

[GameEndpoint("lolcatftw/add/user/{idStr}", Method.Post)]
public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr)
[GameEndpoint("lolcatftw/add/user/{id}", Method.Post)]
public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id)
{
int.TryParse(idStr, out int id);
if (id == default) return BadRequest;

GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

Expand All @@ -99,12 +90,9 @@ public Response QueueLevel(RequestContext context, GameDatabaseContext database,
return Unauthorized;
}

[GameEndpoint("lolcatftw/remove/user/{idStr}", Method.Post)]
public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr)
[GameEndpoint("lolcatftw/remove/user/{id}", Method.Post)]
public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id)
{
int.TryParse(idStr, out int id);
if (id == default) return BadRequest;

GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

Expand Down
7 changes: 6 additions & 1 deletion Refresh.GameServer/Endpoints/Game/ReportingEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ namespace Refresh.GameServer.Endpoints.Game;
public class ReportingEndpoints : EndpointGroup
{
[GameEndpoint("grief", Method.Post, ContentType.Xml)]
public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body)
public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body)
{
if ((body.LevelId != 0 && database.GetLevelById(body.LevelId) == null) || body.Players.Length > 4 || body.ScreenElements.Player.Length > 4)
{
return BadRequest;
}

database.AddGriefReport(body);

return OK;
Expand Down
5 changes: 4 additions & 1 deletion Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Database;
using Refresh.GameServer.Importing;
using Refresh.GameServer.Time;
using Refresh.GameServer.Types.Assets;
using Refresh.GameServer.Types.Lists;
using Refresh.GameServer.Types.Roles;
Expand All @@ -23,7 +24,7 @@ public class ResourceEndpoints : EndpointGroup
[GameEndpoint("upload/{hash}", Method.Post)]
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
public Response UploadAsset(RequestContext context, string hash, string type, byte[] body, IDataStore dataStore,
GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config)
GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider)
{
if (dataStore.ExistsInStore(hash))
return Conflict;
Expand All @@ -32,6 +33,8 @@ public Response UploadAsset(RequestContext context, string hash, string type, by
if (gameAsset == null)
return BadRequest;

gameAsset.UploadDate = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(gameAsset.UploadDate.ToUnixTimeSeconds(), timeProvider.EarliestDate, timeProvider.TimestampSeconds));

// for example, if asset safety level is Dangerous (2) and maximum is configured as Safe (0), return 401
// if asset safety is Safe (0), and maximum is configured as Safe (0), proceed
if (gameAsset.SafetyLevel > config.MaximumAssetSafetyLevel)
Expand Down
Loading

0 comments on commit 4015ff4

Please sign in to comment.