Skip to content

Commit a28e8da

Browse files
committed
Block duplicate photo uploads
1 parent dbba2fb commit a28e8da

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

Refresh.Database/GameDatabaseContext.Photos.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ public GamePhotoSubject AddSubjectForPhoto(GamePhoto photo, int playerId, string
135135
return subject;
136136
}
137137

138+
public GamePhoto? GetPhotoByAnyHash(string smallHash, string mediumHash, string largeHash, string planHash)
139+
=> this.GamePhotosIncluded.FirstOrDefault(p =>
140+
p.SmallAssetHash == smallHash ||
141+
p.MediumAssetHash == mediumHash ||
142+
p.LargeAssetHash == largeHash ||
143+
p.PlanHash == planHash);
144+
138145
public void RemovePhoto(GamePhoto photo)
139146
{
140147
foreach (GameUser subjectUser in this.GetUsersInPhoto(photo).ToArray())

Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa
4343
if (body.PhotoSubjects.Count > 4)
4444
{
4545
context.Logger.LogWarning(BunkumCategory.UserContent, $"Too many subjects in photo, rejecting photo upload. Uploader: {user.UserId}");
46+
database.AddErrorNotification("Photo upload failed", "The photo had more than 4 players", user);
4647
return BadRequest;
4748
}
4849

@@ -52,6 +53,14 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa
5253
return Unauthorized;
5354
}
5455

56+
GamePhoto? existingPhoto = database.GetPhotoByAnyHash(body.SmallHash, body.MediumHash, body.LargeHash, body.PlanHash);
57+
if (existingPhoto != null)
58+
{
59+
// TODO: show photo names in these error notifications once we start to deserialize and store plan data
60+
database.AddErrorNotification("Photo upload failed", "The photo already exists on the server", user);
61+
return BadRequest;
62+
}
63+
5564
List<string> hashes = [body.LargeHash, body.MediumHash, body.SmallHash];
5665
foreach (string hash in hashes.Distinct())
5766
{

RefreshTests.GameServer/Tests/Photos/PhotoEndpointsTests.cs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Refresh.Database.Models.Photos;
88
using Refresh.Interfaces.Game.Types.Lists;
99
using Refresh.Database.Helpers;
10+
using System.Security.Cryptography;
1011

1112
namespace RefreshTests.GameServer.Tests.Photos;
1213

@@ -318,7 +319,7 @@ public void CantDeleteInvalidPhoto()
318319
Assert.That(message.StatusCode, Is.EqualTo(NotFound));
319320
}
320321

321-
[Test]
322+
[Test]
322323
public void CantDeleteOthersPhoto()
323324
{
324325
using TestContext context = this.GetServer();
@@ -381,4 +382,130 @@ public void CantDeleteOthersPhoto()
381382
response = message.Content.ReadAsXML<SerializedPhotoList>();
382383
Assert.That(response.Items, Has.Count.EqualTo(1));
383384
}
385+
386+
[Test]
387+
public void CannotUploadPhotoIfDuplicateImage()
388+
{
389+
using TestContext context = this.GetServer();
390+
GameUser user = context.CreateUser();
391+
using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
392+
393+
//Upload our """photo"""
394+
HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result;
395+
Assert.That(message.StatusCode, Is.EqualTo(OK));
396+
397+
//Upload our """plan"""
398+
ReadOnlySpan<byte> planData = "PLNbum"u8;
399+
string planHash = HexHelper.BytesToHexString(SHA1.HashData(planData));
400+
message = client.PostAsync("/lbp/upload/" + planHash, new ByteArrayContent(planData.ToArray())).Result;
401+
402+
//Upload another """plan"""
403+
ReadOnlySpan<byte> anotherPlanData = "PLNble"u8;
404+
string anotherPlanHash = HexHelper.BytesToHexString(SHA1.HashData(anotherPlanData));
405+
message = client.PostAsync("/lbp/upload/" + anotherPlanHash, new ByteArrayContent(anotherPlanData.ToArray())).Result;
406+
407+
SerializedPhoto photo1 = new()
408+
{
409+
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
410+
AuthorName = user.Username,
411+
SmallHash = TEST_ASSET_HASH,
412+
MediumHash = TEST_ASSET_HASH,
413+
LargeHash = TEST_ASSET_HASH,
414+
PlanHash = planHash,
415+
Level = new SerializedPhotoLevel
416+
{
417+
LevelId = 0,
418+
Title = "",
419+
Type = "pod",
420+
}
421+
};
422+
423+
//Upload photo once
424+
message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo1.AsXML())).Result;
425+
Assert.That(message.StatusCode, Is.EqualTo(OK));
426+
427+
//Upload photo again (different plan hash)
428+
SerializedPhoto photo2 = new()
429+
{
430+
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
431+
AuthorName = user.Username,
432+
SmallHash = TEST_ASSET_HASH,
433+
MediumHash = TEST_ASSET_HASH,
434+
LargeHash = TEST_ASSET_HASH,
435+
PlanHash = anotherPlanHash,
436+
Level = new SerializedPhotoLevel
437+
{
438+
LevelId = 0,
439+
Title = "",
440+
Type = "pod",
441+
}
442+
};
443+
message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo2.AsXML())).Result;
444+
Assert.That(message.StatusCode, Is.EqualTo(BadRequest));
445+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(1));
446+
}
447+
448+
[Test]
449+
public void CannotUploadPhotoIfDuplicatePlan()
450+
{
451+
using TestContext context = this.GetServer();
452+
GameUser user = context.CreateUser();
453+
using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, user);
454+
455+
//Upload our """photo"""
456+
HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result;
457+
Assert.That(message.StatusCode, Is.EqualTo(OK));
458+
459+
//Upload another """photo"""
460+
ReadOnlySpan<byte> anotherTextureData = "TEX r"u8;
461+
string anotherTextureHash = HexHelper.BytesToHexString(SHA1.HashData(anotherTextureData));
462+
message = client.PostAsync("/lbp/upload/" + anotherTextureHash, new ByteArrayContent(anotherTextureData.ToArray())).Result;
463+
Assert.That(message.StatusCode, Is.EqualTo(OK));
464+
465+
//Upload our """plan"""
466+
ReadOnlySpan<byte> planData = "PLNb"u8;
467+
string planHash = HexHelper.BytesToHexString(SHA1.HashData(planData));
468+
message = client.PostAsync("/lbp/upload/" + planHash, new ByteArrayContent(planData.ToArray())).Result;
469+
Assert.That(message.StatusCode, Is.EqualTo(OK));
470+
471+
SerializedPhoto photo1 = new()
472+
{
473+
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
474+
AuthorName = user.Username,
475+
SmallHash = TEST_ASSET_HASH,
476+
MediumHash = TEST_ASSET_HASH,
477+
LargeHash = TEST_ASSET_HASH,
478+
PlanHash = planHash,
479+
Level = new SerializedPhotoLevel
480+
{
481+
LevelId = 0,
482+
Title = "",
483+
Type = "pod",
484+
}
485+
};
486+
487+
//Upload photo once
488+
message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo1.AsXML())).Result;
489+
Assert.That(message.StatusCode, Is.EqualTo(OK));
490+
491+
//Upload photo again (different image hashes)
492+
SerializedPhoto photo2 = new()
493+
{
494+
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
495+
AuthorName = user.Username,
496+
SmallHash = anotherTextureHash,
497+
MediumHash = anotherTextureHash,
498+
LargeHash = anotherTextureHash,
499+
PlanHash = planHash,
500+
Level = new SerializedPhotoLevel
501+
{
502+
LevelId = 0,
503+
Title = "",
504+
Type = "pod",
505+
}
506+
};
507+
message = client.PostAsync($"/lbp/uploadPhoto", new StringContent(photo2.AsXML())).Result;
508+
Assert.That(message.StatusCode, Is.EqualTo(BadRequest));
509+
Assert.That(context.Database.GetNotificationCountByUser(user), Is.EqualTo(1));
510+
}
384511
}

0 commit comments

Comments
 (0)