Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 82 additions & 33 deletions Refresh.Interfaces.Game/Endpoints/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
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.Constants;
using Refresh.Core.Authentication.Permission;
using Refresh.Core.Configuration;
using Refresh.Core.Helpers;
using Refresh.Core.Importing;
using Refresh.Core.RateLimits.Users;
using Refresh.Core.Services;
using Refresh.Core.Types.Assets.Validation;
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.Game.Endpoints.DataTypes.Response;
Expand Down Expand Up @@ -73,7 +78,8 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont
[NullStatusCode(BadRequest)]
[RateLimitSettings(UserModificationEndpointLimits.TimeoutDuration, UserModificationEndpointLimits.GameRequestAmount,
UserModificationEndpointLimits.BlockDuration, UserModificationEndpointLimits.GameRequestBucket)]
public string? UpdateUser(RequestContext context, DataContext dataContext, GameUser user, string body, GuidCheckerService guidChecker)
public Response UpdateUser(RequestContext context, DataContext dataContext, GameUser user, string body, GuidCheckerService guidChecker,
AssetImporter importer, AipiService aipi)
{
SerializedUpdateData? data = null;

Expand All @@ -83,7 +89,7 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont
{
XmlSerializer profileSerializer = new(typeof(SerializedUpdateDataProfile));
if (profileSerializer.Deserialize(new StringReader(body)) is not SerializedUpdateDataProfile profileData)
return null;
return BadRequest;

data ??= profileData;
}
Expand All @@ -96,7 +102,7 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont
{
XmlSerializer planetSerializer = new(typeof(SerializedUpdateDataPlanets));
if (planetSerializer.Deserialize(new StringReader(body)) is not SerializedUpdateDataPlanets planetsData)
return null;
return BadRequest;

data ??= planetsData;
}
Expand All @@ -108,47 +114,85 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont
if (data == null)
{
dataContext.Database.AddErrorNotification("Profile update failed", "Your profile failed to update because the data could not be read.", user);
return null;
return BadRequest;
}

if (data.IconHash != null)
{
//If the icon is a GUID
if (data.IconHash.StartsWith('g'))
{
//Parse out the GUID
long guid = long.Parse(data.IconHash.AsSpan()[1..]);

//If its not a valid GUID, block the request
if (data.IconHash.StartsWith('g') && !guidChecker.IsTextureGuid(dataContext.Game, guid))
{
dataContext.Database.AddErrorNotification("Profile update failed", "Your avatar failed to update because the asset was an invalid GUID.", user);
return null;
}
}
else if (data.IconHash.IsBlankHash())
ValidatedAssetResult iconResult = ResourceValidationHelper.ValidateReference(new(data.IconHash, dataContext, importer, aipi)
{
// Force hash to be a specific value if the icon is supposed to be reset/default to a PSN avatar,
// to not allow uncontrolled values which would still count as blank/empty hash (e.g. unlimited whitespaces)
data.IconHash = "0";
}
else if (!dataContext.DataStore.ExistsInStore(data.IconHash))
MustBeTexture = true,
AssetContextTypeStr = "avatar",
}, context.Logger);
data.IconHash = iconResult.NewAssetRef;

if (iconResult.Status != OK)
{
//If the asset does not exist on the server, block the request
dataContext.Database.AddErrorNotification("Profile update failed", "Your avatar failed to update because the asset was missing on the server.", user);
return null;
if (iconResult.ErrorMessage != null) dataContext.Database.AddErrorNotification("Profile update failed", iconResult.ErrorMessage, user);
return iconResult.Status;
}
}

if (data.LevelLocations != null && data.LevelLocations.Count > 0)
AssetValidationParameters faceParams = new(null!, dataContext, importer, aipi)
{
dataContext.Database.UpdateLevelLocations(data.LevelLocations, user);
MayBeBlank = false,
MayBeGuid = false,
MustBeTexture = true,
AssetContextTypeStr = "image",
};

if (data.YayFaceHash != null)
{
faceParams.AssetRef = data.YayFaceHash;
ValidatedAssetResult yayResult = ResourceValidationHelper.ValidateReference(faceParams, context.Logger);
data.YayFaceHash = yayResult.NewAssetRef;

if (yayResult.Status != OK) return yayResult.Status; // no need to notify, these are always updated in the background
}
if (!string.IsNullOrEmpty(data.PlanetsHash) && data.PlanetsHash != "0" /* Empty planets */ && !dataContext.DataStore.ExistsInStore(data.PlanetsHash))

if (data.MehFaceHash != null)
{
dataContext.Database.AddErrorNotification("Profile update failed", "Your planets failed to update because the asset was missing on the server.", user);
return null;
faceParams.AssetRef = data.MehFaceHash;
ValidatedAssetResult mehResult = ResourceValidationHelper.ValidateReference(faceParams, context.Logger);
data.MehFaceHash = mehResult.NewAssetRef;

if (mehResult.Status != OK) return mehResult.Status;
}

if (data.BooFaceHash != null)
{
faceParams.AssetRef = data.BooFaceHash;
ValidatedAssetResult booResult = ResourceValidationHelper.ValidateReference(faceParams, context.Logger);
data.BooFaceHash = booResult.NewAssetRef;

if (booResult.Status != OK) return booResult.Status;
}

if (data.PlanetsHash != null)
{
// Some LBP2 alpha builds like to insert newlines here
data.PlanetsHash = data.PlanetsHash.Replace("\n", "");

ValidatedAssetResult planetResult = ResourceValidationHelper.ValidateReference(new(data.PlanetsHash, dataContext, importer)
{
// blank = reset planets, but GUIDs should never happen
MayBeGuid = false,
AssetContextTypeStr = "planet asset",
OnNewAssetRefCallback = delegate(string newRef) { data.PlanetsHash = newRef; }
}, context.Logger);
data.PlanetsHash = planetResult.NewAssetRef;

if (planetResult.Status != OK)
{
if (planetResult.ErrorMessage != null) dataContext.Database.AddErrorNotification("Planet update failed", planetResult.ErrorMessage, user);
return planetResult.Status;
}
// TODO also read contents and ensure the asset actually contains an earth and a moon
else if (planetResult.AssetInfo != null && planetResult.AssetInfo.AssetType != GameAssetType.Level)
{
if (planetResult.ErrorMessage != null) dataContext.Database.AddErrorNotification("Planet update failed", "The asset was badly formatted.", user);
return BadRequest;
}
}

// Trim description
Expand All @@ -157,8 +201,13 @@ public SerializedFriendsList GetFriends(RequestContext context, GameDatabaseCont
data.Description = data.Description[..UgcLimits.DescriptionLimit];
}

if (data.LevelLocations != null && data.LevelLocations.Count > 0)
{
dataContext.Database.UpdateLevelLocations(data.LevelLocations, user);
}

dataContext.Database.UpdateUserData(user, data, dataContext.Game);
return string.Empty;
return OK;
}

private const int PinTimeoutDuration = 480;
Expand Down
168 changes: 168 additions & 0 deletions RefreshTests.GameServer/Tests/Planets/PlanetUploadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System.Security.Cryptography;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.Game.Types.UserData;
using RefreshTests.GameServer.Extensions;

namespace RefreshTests.GameServer.Tests.Planets;

public class PlanetUploadTests : GameServerTest
{
[Test]
public void RejectPlanetsIfGuid()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = "g34567",
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(BadRequest));
}

[Test]
public void AllowBlankPlanets()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = "0",
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(OK));
}

[Test]
public void AllowHashedPlanets()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

ReadOnlySpan<byte> data = "LVLb"u8;
string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower();
context.GetDataStore().WriteToStore(hash, data);

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = hash,
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(OK));
}

[Test]
public void AllowHashedPlanetsWithNewline()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

ReadOnlySpan<byte> data = "LVLb"u8;
string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower();
context.GetDataStore().WriteToStore(hash, data);

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = hash + "\n",
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(OK));
}

[Test]
public void RejectPlanetsIfWrongResourceType()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

ReadOnlySpan<byte> data = "PLNb"u8;
string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower();
context.GetDataStore().WriteToStore(hash, data);

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = hash,
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(BadRequest));
}

[Test]
public void RejectPlanetsIfNotUploaded()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

ReadOnlySpan<byte> data = "LVLb"u8;
string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower();
// Don't upload the asset

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = hash,
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(NotFound));
}

[Test]
public void RejectPlanetsIfDisallowed()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

ReadOnlySpan<byte> data = "LVLb"u8;
string hash = BitConverter.ToString(SHA1.HashData(data)).Replace("-", "").ToLower();
context.GetDataStore().WriteToStore(hash, data);
context.Database.DisallowAsset(hash, GameAssetType.Level, "garbage music");

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = hash,
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(Unauthorized));
}

[Test]
public void RejectPlanetsIfInvalidHash()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet3, TokenPlatform.PS3, user);

SerializedUpdateDataProfile request = new()
{
PlanetsHash = "adserdtfgzhgj",
};

HttpResponseMessage message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result;
Assert.That(message.StatusCode, Is.EqualTo(BadRequest));
}
}
Loading
Loading