diff --git a/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs b/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs index b5a8e79e..13f3a999 100644 --- a/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs +++ b/ProjectLighthouse.Servers.API/Controllers/UserEndpoints.cs @@ -59,29 +59,27 @@ public class UserEndpoints : ApiEndpointController [HttpPost("user/inviteToken")] public async Task CreateUserInviteToken() { - if (Configuration.ServerConfiguration.Instance.Authentication.PrivateRegistration || - Configuration.ServerConfiguration.Instance.Authentication.RegistrationEnabled) + if (!Configuration.ServerConfiguration.Instance.Authentication.PrivateRegistration && + !Configuration.ServerConfiguration.Instance.Authentication.RegistrationEnabled) + return this.NotFound(); + + string authHeader = this.Request.Headers["Authorization"]; + if (string.IsNullOrWhiteSpace(authHeader)) return this.NotFound(); + + string authToken = authHeader[(authHeader.IndexOf(' ') + 1)..]; + + APIKey? apiKey = await this.database.APIKeys.FirstOrDefaultAsync(k => k.Key == authToken); + if (apiKey == null) return this.StatusCode(403, null); + + RegistrationToken token = new() { + Created = DateTime.Now, + Token = CryptoHelper.GenerateAuthToken(), + }; - string authHeader = this.Request.Headers["Authorization"]; - if (!string.IsNullOrWhiteSpace(authHeader)) - { - string authToken = authHeader.Substring(authHeader.IndexOf(' ') + 1); + this.database.RegistrationTokens.Add(token); + await this.database.SaveChangesAsync(); - APIKey? apiKey = await this.database.APIKeys.FirstOrDefaultAsync(k => k.Key == authToken); - if (apiKey == null) return this.StatusCode(403, null); - - RegistrationToken token = new(); - token.Created = DateTime.Now; - token.Token = CryptoHelper.GenerateAuthToken(); - - this.database.RegistrationTokens.Add(token); - await this.database.SaveChangesAsync(); - - return Ok(token.Token); - } - - } - return this.NotFound(); + return this.Ok(token.Token); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs index 9ad44412..d633402b 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/ClientConfigurationController.cs @@ -27,7 +27,7 @@ public class ClientConfigurationController : ControllerBase GameToken? token = await this.database.GameTokenFromRequest(this.Request); if (token == null) return this.StatusCode(403, ""); - HostString hostname = this.Request.Host; + string hostname = ServerConfiguration.Instance.GameApiExternalUrl; return this.Ok ( "ProbabilityOfPacketDelay 0.0\nMinPacketDelayFrames 0\nMaxPacketDelayFrames 3\nProbabilityOfPacketDrop 0.0\nEnableFakeConditionsForLoopback true\nNumberOfFramesPredictionAllowedForNonLocalPlayer 1000\nEnablePrediction true\nMinPredictedFrames 0\nMaxPredictedFrames 10\nAllowGameRendCameraSplit true\nFramesBeforeAgressiveCatchup 30\nPredictionPadSides 200\nPredictionPadTop 200\nPredictionPadBottom 200\nShowErrorNumbers true\nAllowModeratedLevels false\nAllowModeratedPoppetItems false\nTIMEOUT_WAIT_FOR_JOIN_RESPONSE_FROM_PREV_PARTY_HOST 50.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_HOST 30.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_MEMBER 45.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN_FRIEND 15.0\nTIMEOUT_WAIT_FOR_CONNECTION_FROM_HOST 30.0\nTIMEOUT_WAIT_FOR_ROOM_ID_TO_JOIN 60.0\nTIMEOUT_WAIT_FOR_GET_NUM_PLAYERS_ONLINE 60.0\nTIMEOUT_WAIT_FOR_SIGNALLING_CONNECTIONS 120.0\nTIMEOUT_WAIT_FOR_PARTY_DATA 60.0\nTIME_TO_WAIT_FOR_LEAVE_MESSAGE_TO_COME_BACK 20.0\nTIME_TO_WAIT_FOR_FOLLOWING_REQUESTS_TO_ARRIVE 30.0\nTIMEOUT_WAIT_FOR_FINISHED_MIGRATING_HOST 30.0\nTIMEOUT_WAIT_FOR_PARTY_LEADER_FINISH_JOINING 45.0\nTIMEOUT_WAIT_FOR_QUICKPLAY_LEVEL 60.0\nTIMEOUT_WAIT_FOR_PLAYERS_TO_JOIN 30.0\nTIMEOUT_WAIT_FOR_DIVE_IN_PLAYERS 240.0\nTIMEOUT_WAIT_FOR_FIND_BEST_ROOM 60.0\nTIMEOUT_DIVE_IN_TOTAL 300.0\nTIMEOUT_WAIT_FOR_SOCKET_CONNECTION 120.0\nTIMEOUT_WAIT_FOR_REQUEST_RESOURCE_MESSAGE 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_GET_RESOURCE_LIST 120.0\nTIMEOUT_WAIT_FOR_CLIENT_TO_LOAD_RESOURCES 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_SAVE_GAME_STATE 30.0\nTIMEOUT_WAIT_FOR_ADD_PLAYERS_TO_TAKE 30.0\nTIMEOUT_WAIT_FOR_UPDATE_FROM_CLIENT 90.0\nTIMEOUT_WAIT_FOR_HOST_TO_GET_RESOURCE_LIST 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_SAVE_GAME_STATE 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_ADD_US 30.0\nTIMEOUT_WAIT_FOR_UPDATE 60.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN 50.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_PRESENCE 60.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_CONNECTION 120.0\nSECONDS_BETWEEN_PINS_AWARDED_UPLOADS 300.0\nEnableKeepAlive true\nAllowVoIPRecordingPlayback true\nOverheatingThresholdDisallowMidgameJoin 0.95\nMaxCatchupFrames 3\nMaxLagBeforeShowLoading 23\nMinLagBeforeHideLoading 30\nLagImprovementInflectionPoint -1.0\nFlickerThreshold 2.0\nClosedDemo2014Version 1\nClosedDemo2014Expired false\nEnablePlayedFilter true\nEnableCommunityDecorations true\nGameStateUpdateRate 10.0\nGameStateUpdateRateWithConsumers 1.0\nDisableDLCPublishCheck false\nEnableDiveIn true\nEnableHackChecks false\nAllowOnlineCreate true\n" + @@ -41,6 +41,10 @@ public class ClientConfigurationController : ControllerBase [Produces("text/xml")] public IActionResult Conf() => this.Ok("false"); + [HttpGet("ChallengeConfig.xml")] + [Produces("text/xml")] + public IActionResult Challenges() => this.Ok(); + [HttpGet("farc_hashes")] public IActionResult FarcHashes() => this.Ok(); @@ -50,7 +54,7 @@ public class ClientConfigurationController : ControllerBase { User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); - + PrivacySettings ps = new() { LevelVisibility = user.LevelVisibility.ToSerializedString(), diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs index bd7c0019..18fd3244 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/LoginController.cs @@ -139,6 +139,7 @@ public class LoginController : ControllerBase { AuthTicket = "MM_AUTH=" + token.UserToken, ServerBrand = VersionHelper.EnvVer, + TitleStorageUrl = ServerConfiguration.Instance.GameApiExternalUrl, }.Serialize() ); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs index f8f91830..1f35667d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs @@ -60,12 +60,12 @@ public class PublishController : ControllerBase Slot? oldSlot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == slot.SlotId); if (oldSlot == null) { - Logger.Warn("Rejecting level reupload, could not find old slot", LogArea.Publish); + Logger.Warn("Rejecting level republish, could not find old slot", LogArea.Publish); return this.NotFound(); } if (oldSlot.CreatorId != user.UserId) { - Logger.Warn("Rejecting level reupload, old slot's creator is not publishing user", LogArea.Publish); + Logger.Warn("Rejecting level republish, old slot's creator is not publishing user", LogArea.Publish); return this.BadRequest(); } } @@ -87,7 +87,7 @@ public class PublishController : ControllerBase /// Endpoint actually used to publish a level /// [HttpPost("publish")] - public async Task Publish() + public async Task Publish([FromQuery] string? game) { (User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request); @@ -178,6 +178,22 @@ public class PublishController : ControllerBase return this.BadRequest(); } + // I hate lbp3 + if (game != null) + { + GameVersion intendedVersion = FromAbbreviation(game); + if (intendedVersion != GameVersion.Unknown && intendedVersion != slotVersion) + { + // Delete the useless rootLevel that lbp3 just uploaded + if(slotVersion == GameVersion.LittleBigPlanet3) + FileHelper.DeleteResource(slot.RootLevel); + + slot.GameVersion = oldSlot.GameVersion; + slot.RootLevel = oldSlot.RootLevel; + slot.ResourceCollection = oldSlot.ResourceCollection; + } + } + oldSlot.Location.X = slot.Location.X; oldSlot.Location.Y = slot.Location.Y; @@ -277,6 +293,19 @@ public class PublishController : ControllerBase return this.Ok(); } + private static GameVersion FromAbbreviation(string abbr) + { + return abbr switch + { + "lbp1" => GameVersion.LittleBigPlanet1, + "lbp2" => GameVersion.LittleBigPlanet2, + "lbp3" => GameVersion.LittleBigPlanet3, + "lbpv" => GameVersion.LittleBigPlanetVita, + "lbppsp" => GameVersion.LittleBigPlanetPSP, + _ => GameVersion.Unknown, + }; + } + private async Task getSlotFromBody() { this.Request.Body.Position = 0; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index 78f1305a..8d737c5d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -214,8 +214,8 @@ public class ScoreController : ControllerBase var rankedScores = this.database.Scores .Where(s => s.SlotId == slotId && s.Type == type) .Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId) - .Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Contains(id))) .AsEnumerable() + .Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Contains(id))) .OrderByDescending(s => s.Points) .ThenBy(s => s.ScoreId) .ToList() diff --git a/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs b/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs new file mode 100644 index 00000000..b41e744c --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs @@ -0,0 +1,129 @@ +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Middlewares; +using Microsoft.Extensions.Primitives; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares; + +public class DigestMiddleware : Middleware +{ + private readonly bool computeDigests; + + public DigestMiddleware(RequestDelegate next, bool computeDigests) : base(next) + { + this.computeDigests = computeDigests; + } + + private static readonly HashSet exemptPathList = new() + { + "/login", + "/eula", + "/announce", + "/status", + "/farc_hashes", + "/t_conf", + "/network_settings.nws", + "/ChallengeConfig.xml", + }; + + public override async Task InvokeAsync(HttpContext context) + { + // Client digest check. + if (!context.Request.Cookies.TryGetValue("MM_AUTH", out string? authCookie) || authCookie == null) + authCookie = string.Empty; + string digestPath = context.Request.Path; + #if !DEBUG + const string url = "/LITTLEBIGPLANETPS3_XML"; + string strippedPath = digestPath.Contains(url) ? digestPath[url.Length..] : ""; + #endif + Stream body = context.Request.Body; + + bool usedAlternateDigestKey = false; + + if (this.computeDigests && digestPath.StartsWith("/LITTLEBIGPLANETPS3_XML")) + { + // The game sets X-Digest-B on a resource upload instead of X-Digest-A + string digestHeaderKey = "X-Digest-A"; + bool excludeBodyFromDigest = false; + if (digestPath.Contains("/upload/")) + { + digestHeaderKey = "X-Digest-B"; + excludeBodyFromDigest = true; + } + + string clientRequestDigest = await CryptoHelper.ComputeDigest + (digestPath, authCookie, body, ServerConfiguration.Instance.DigestKey.PrimaryDigestKey, excludeBodyFromDigest); + + // Check the digest we've just calculated against the digest header if the game set the header. They should match. + if (context.Request.Headers.TryGetValue(digestHeaderKey, out StringValues sentDigest)) + { + if (clientRequestDigest != sentDigest) + { + // If we got here, the normal ServerDigestKey failed to validate. Lets try again with the alternate digest key. + usedAlternateDigestKey = true; + + // Reset the body stream + body.Position = 0; + + clientRequestDigest = await CryptoHelper.ComputeDigest + (digestPath, authCookie, body, ServerConfiguration.Instance.DigestKey.AlternateDigestKey, excludeBodyFromDigest); + if (clientRequestDigest != sentDigest) + { + #if DEBUG + Console.WriteLine("Digest failed"); + Console.WriteLine("digestKey: " + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey); + Console.WriteLine("altDigestKey: " + ServerConfiguration.Instance.DigestKey.AlternateDigestKey); + Console.WriteLine("computed digest: " + clientRequestDigest); + #endif + // We still failed to validate. Abort the request. + context.Response.StatusCode = 403; + return; + } + } + } + #if !DEBUG + // The game doesn't start sending digests until after the announcement so if it's not one of those requests + // and it doesn't include a digest we need to reject the request + else if (!ServerStatics.IsUnitTesting && !exemptPathList.Contains(strippedPath)) + { + context.Response.StatusCode = 403; + return; + } + #endif + + context.Response.Headers.Add("X-Digest-B", clientRequestDigest); + context.Request.Body.Position = 0; + } + + // This does the same as above, but for the response stream. + await using MemoryStream responseBuffer = new(); + Stream oldResponseStream = context.Response.Body; + context.Response.Body = responseBuffer; + + await this.next(context); // Handle the request so we can get the server digest hash + responseBuffer.Position = 0; + + // Compute the server digest hash. + if (this.computeDigests) + { + responseBuffer.Position = 0; + + string digestKey = usedAlternateDigestKey + ? ServerConfiguration.Instance.DigestKey.AlternateDigestKey + : ServerConfiguration.Instance.DigestKey.PrimaryDigestKey; + + // Compute the digest for the response. + string serverDigest = await CryptoHelper.ComputeDigest(context.Request.Path, authCookie, responseBuffer, digestKey); + context.Response.Headers.Add("X-Digest-A", serverDigest); + } + + // Add a content-length header if it isn't present to disable response chunking + if (!context.Response.Headers.ContainsKey("Content-Length")) + context.Response.Headers.Add("Content-Length", responseBuffer.Length.ToString()); + + // Copy the buffered response to the actual response stream. + responseBuffer.Position = 0; + await responseBuffer.CopyToAsync(oldResponseStream); + context.Response.Body = oldResponseStream; + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Middlewares/SetLastContactMiddleware.cs b/ProjectLighthouse.Servers.GameServer/Middlewares/SetLastContactMiddleware.cs new file mode 100644 index 00000000..76c94f35 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Middlewares/SetLastContactMiddleware.cs @@ -0,0 +1,29 @@ +using LBPUnion.ProjectLighthouse.Middlewares; +using LBPUnion.ProjectLighthouse.PlayerData; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares; + +public class SetLastContactMiddleware : MiddlewareDBContext +{ + public SetLastContactMiddleware(RequestDelegate next) : base(next) + { } + + public override async Task InvokeAsync(HttpContext context, Database database) + { + #nullable enable + // Log LastContact for LBP1. This is done on LBP2/3/V on a Match request. + if (context.Request.Path.ToString().StartsWith("/LITTLEBIGPLANETPS3_XML")) + { + // We begin by grabbing a token from the request, if this is a LBPPS3_XML request of course. + GameToken? gameToken = await database.GameTokenFromRequest(context.Request); + + if (gameToken?.GameVersion == GameVersion.LittleBigPlanet1) + // Ignore UserFromGameToken null because user must exist for a token to exist + await LastContactHelper.SetLastContact + (database, (await database.UserFromGameToken(gameToken))!, GameVersion.LittleBigPlanet1, gameToken.Platform); + } + #nullable disable + + await this.next(context); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs index daf0dab2..1ee53179 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -1,11 +1,9 @@ using LBPUnion.ProjectLighthouse.Configuration; -using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Middlewares; -using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.Serialization; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.Primitives; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; @@ -66,134 +64,9 @@ public class GameServerStartup app.UseForwardedHeaders(); app.UseMiddleware(); - - // Digest check - app.Use - ( - async (context, next) => - { - // Client digest check. - if (!context.Request.Cookies.TryGetValue("MM_AUTH", out string? authCookie) || authCookie == null) authCookie = string.Empty; - string digestPath = context.Request.Path; - #if !DEBUG - const string url = "/LITTLEBIGPLANETPS3_XML"; - string strippedPath = digestPath.Contains(url) ? digestPath[url.Length..] : ""; - #endif - Stream body = context.Request.Body; - - bool usedAlternateDigestKey = false; - - if (computeDigests && digestPath.StartsWith("/LITTLEBIGPLANETPS3_XML")) - { - // The game sets X-Digest-B on a resource upload instead of X-Digest-A - string digestHeaderKey = "X-Digest-A"; - bool excludeBodyFromDigest = false; - if (digestPath.Contains("/upload/")) - { - digestHeaderKey = "X-Digest-B"; - excludeBodyFromDigest = true; - } - - string clientRequestDigest = await CryptoHelper.ComputeDigest - (digestPath, authCookie, body, ServerConfiguration.Instance.DigestKey.PrimaryDigestKey, excludeBodyFromDigest); - - // Check the digest we've just calculated against the digest header if the game set the header. They should match. - if (context.Request.Headers.TryGetValue(digestHeaderKey, out StringValues sentDigest)) - { - if (clientRequestDigest != sentDigest) - { - // If we got here, the normal ServerDigestKey failed to validate. Lets try again with the alternate digest key. - usedAlternateDigestKey = true; - - // Reset the body stream - body.Position = 0; - - clientRequestDigest = await CryptoHelper.ComputeDigest - (digestPath, authCookie, body, ServerConfiguration.Instance.DigestKey.AlternateDigestKey, excludeBodyFromDigest); - if (clientRequestDigest != sentDigest) - { - #if DEBUG - Console.WriteLine("Digest failed"); - Console.WriteLine("digestKey: " + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey); - Console.WriteLine("altDigestKey: " + ServerConfiguration.Instance.DigestKey.AlternateDigestKey); - Console.WriteLine("computed digest: " + clientRequestDigest); - #endif - // We still failed to validate. Abort the request. - context.Response.StatusCode = 403; - return; - } - } - } - #if !DEBUG - // The game doesn't start sending digests until after the announcement so if it's not one of those requests - // and it doesn't include a digest we need to reject the request - else if (!ServerStatics.IsUnitTesting && !strippedPath.Equals("/login") && !strippedPath.Equals("/eula") - && !strippedPath.Equals("/announce") && !strippedPath.Equals("/status") && !strippedPath.Equals("/farc_hashes")) - { - context.Response.StatusCode = 403; - return; - } - #endif - - context.Response.Headers.Add("X-Digest-B", clientRequestDigest); - context.Request.Body.Position = 0; - } - - // This does the same as above, but for the response stream. - await using MemoryStream responseBuffer = new(); - Stream oldResponseStream = context.Response.Body; - context.Response.Body = responseBuffer; - - await next(context); // Handle the request so we can get the server digest hash - responseBuffer.Position = 0; - - // Compute the server digest hash. - if (computeDigests) - { - responseBuffer.Position = 0; - - string digestKey = usedAlternateDigestKey - ? ServerConfiguration.Instance.DigestKey.AlternateDigestKey - : ServerConfiguration.Instance.DigestKey.PrimaryDigestKey; - - // Compute the digest for the response. - string serverDigest = await CryptoHelper.ComputeDigest(context.Request.Path, authCookie, responseBuffer, digestKey); - context.Response.Headers.Add("X-Digest-A", serverDigest); - } - - // Add a content-length header if it isn't present to disable response chunking - if(!context.Response.Headers.ContainsKey("Content-Length")) - context.Response.Headers.Add("Content-Length", responseBuffer.Length.ToString()); - - // Copy the buffered response to the actual response stream. - responseBuffer.Position = 0; - await responseBuffer.CopyToAsync(oldResponseStream); - context.Response.Body = oldResponseStream; - } - ); - - app.Use - ( - async (context, next) => - { - #nullable enable - // Log LastContact for LBP1. This is done on LBP2/3/V on a Match request. - if (context.Request.Path.ToString().StartsWith("/LITTLEBIGPLANETPS3_XML")) - { - // We begin by grabbing a token from the request, if this is a LBPPS3_XML request of course. - await using Database database = new(); // Gets nuked at the end of the scope - GameToken? gameToken = await database.GameTokenFromRequest(context.Request); - - if (gameToken != null && gameToken.GameVersion == GameVersion.LittleBigPlanet1) - // Ignore UserFromGameToken null because user must exist for a token to exist - await LastContactHelper.SetLastContact - (database, (await database.UserFromGameToken(gameToken))!, GameVersion.LittleBigPlanet1, gameToken.Platform); - } - #nullable disable - - await next(context); - } - ); + app.UseMiddleware(computeDigests); + app.UseMiddleware(); + app.UseMiddleware(); app.UseRouting(); diff --git a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml index 0a3fa0f6..b187bffb 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml @@ -13,7 +13,8 @@ } else { -

Failed to send email, please try again later

+

Failed to send email, please try again later.

+

If this issue persists please contact an Administrator

} diff --git a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs index 7431acc4..1a9db832 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SendVerificationEmailPage.cshtml.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Collections.Concurrent; using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles.Email; @@ -14,6 +16,9 @@ public class SendVerificationEmailPage : BaseLayout public SendVerificationEmailPage(Database database) : base(database) {} + // (User id, timestamp of last request + 30 seconds) + private static readonly ConcurrentDictionary recentlySentEmail = new(); + public bool Success { get; set; } public async Task OnGet() @@ -35,22 +40,34 @@ public class SendVerificationEmailPage : BaseLayout } #endif - EmailVerificationToken? verifyToken = await this.Database.EmailVerificationTokens.FirstOrDefaultAsync(v => v.UserId == user.UserId); - // If user doesn't have a token or it is expired then regenerate - if (verifyToken == null || DateTime.Now > verifyToken.ExpiresAt) + // Remove expired entries + for (int i = recentlySentEmail.Count - 1; i >= 0; i--) { - verifyToken = new EmailVerificationToken - { - UserId = user.UserId, - User = user, - EmailToken = CryptoHelper.GenerateAuthToken(), - ExpiresAt = DateTime.Now.AddHours(6), - }; - - this.Database.EmailVerificationTokens.Add(verifyToken); - await this.Database.SaveChangesAsync(); + KeyValuePair entry = recentlySentEmail.ElementAt(i); + if (TimeHelper.TimestampMillis > recentlySentEmail[user.UserId]) recentlySentEmail.TryRemove(entry.Key, out _); } + if (recentlySentEmail.ContainsKey(user.UserId) && recentlySentEmail[user.UserId] > TimeHelper.TimestampMillis) + { + this.Success = true; + return this.Page(); + } + + string? existingToken = await this.Database.EmailVerificationTokens.Where(v => v.UserId == user.UserId).Select(v => v.EmailToken).FirstOrDefaultAsync(); + if(existingToken != null) + this.Database.EmailVerificationTokens.RemoveWhere(t => t.EmailToken == existingToken); + + EmailVerificationToken verifyToken = new() + { + UserId = user.UserId, + User = user, + EmailToken = CryptoHelper.GenerateAuthToken(), + ExpiresAt = DateTime.Now.AddHours(6), + }; + + this.Database.EmailVerificationTokens.Add(verifyToken); + await this.Database.SaveChangesAsync(); + string body = "Hello,\n\n" + $"This email is a request to verify this email for your (likely new!) Project Lighthouse account ({user.Username}).\n\n" + $"To verify your account, click the following link: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" + @@ -58,6 +75,9 @@ public class SendVerificationEmailPage : BaseLayout this.Success = SMTPHelper.SendEmail(user.EmailAddress, "Project Lighthouse Email Verification", body); + // Don't send another email for 30 seconds + recentlySentEmail.TryAdd(user.UserId, TimeHelper.TimestampMillis + 30 * 1000); + return this.Page(); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml index 08fec3e4..f6be5501 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotSettingsPage.cshtml @@ -17,6 +17,8 @@ bool isMobile = Request.IsMobile(); int size = isMobile ? 100 : 200; + bool isAdventure = Model.Slot?.IsAdventurePlanet ?? false; + string advenStyleExt = isAdventure ? "-webkit-mask-image: url(/assets/advSlotCardMask.png); -webkit-mask-size: contain; border-radius: 0%;" : ""; }