diff --git a/.idea/.idea.ProjectLighthouse/.idea/.name b/.idea/.idea.ProjectLighthouse/.idea/.name new file mode 100644 index 00000000..cbe8482b --- /dev/null +++ b/.idea/.idea.ProjectLighthouse/.idea/.name @@ -0,0 +1 @@ +ProjectLighthouse \ No newline at end of file diff --git a/ProjectLighthouse/Controllers/ClientConfigurationController.cs b/ProjectLighthouse/Controllers/ClientConfigurationController.cs index 7e5aefce..94010d42 100644 --- a/ProjectLighthouse/Controllers/ClientConfigurationController.cs +++ b/ProjectLighthouse/Controllers/ClientConfigurationController.cs @@ -2,36 +2,44 @@ using System.Diagnostics.CodeAnalysis; using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Mvc; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/")] [Produces("text/plain")] - public class ClientConfigurationController : ControllerBase { + public class ClientConfigurationController : ControllerBase + { [HttpGet("network_settings.nws")] [SuppressMessage("ReSharper", "StringLiteralTypo")] - public IActionResult NetworkSettings() { - 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 true\nAllowModeratedPoppetItems true\nShowLevelBoos true\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 120.0\nTIMEOUT_WAIT_FOR_FIND_BEST_ROOM 30.0\nTIMEOUT_DIVE_IN_TOTAL 1000000.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\nCDNHostName localhost\nTelemetryServer localhost\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"); + public IActionResult NetworkSettings() + { + 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 true\nAllowModeratedPoppetItems true\nShowLevelBoos true\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 120.0\nTIMEOUT_WAIT_FOR_FIND_BEST_ROOM 30.0\nTIMEOUT_DIVE_IN_TOTAL 1000000.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\nCDNHostName localhost\nTelemetryServer localhost\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"); } [HttpGet("t_conf")] [Produces("text/json")] - public IActionResult Conf() { + public IActionResult Conf() + { return this.Ok("[{\"StatusCode\":200}]"); } [HttpGet("farc_hashes")] - public IActionResult FarcHashes() { + public IActionResult FarcHashes() + { return this.Ok(); } [HttpGet("privacySettings")] [Produces("text/xml")] - public IActionResult PrivacySettings() { - PrivacySettings ps = new() { + public IActionResult PrivacySettings() + { + PrivacySettings ps = new() + { LevelVisibility = "all", ProfileVisibility = "all", }; - + return this.Ok(ps.Serialize()); } } diff --git a/ProjectLighthouse/Controllers/LoginController.cs b/ProjectLighthouse/Controllers/LoginController.cs index edf494d0..491c1bfb 100644 --- a/ProjectLighthouse/Controllers/LoginController.cs +++ b/ProjectLighthouse/Controllers/LoginController.cs @@ -5,34 +5,41 @@ using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Mvc; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/login")] [Produces("text/xml")] - public class LoginController : ControllerBase { + public class LoginController : ControllerBase + { private readonly Database database; - public LoginController(Database database) { + public LoginController(Database database) + { this.database = database; } - + [HttpPost] - public async Task Login() { + public async Task Login() + { string body = await new StreamReader(this.Request.Body).ReadToEndAsync(); LoginData loginData; - try { + try + { loginData = LoginData.CreateFromString(body); } - catch { + catch + { return this.BadRequest(); } Token? token = await this.database.AuthenticateUser(loginData); - if(token == null) return this.StatusCode(403, ""); + if (token == null) return this.StatusCode(403, ""); - return this.Ok(new LoginResult { + return this.Ok(new LoginResult + { AuthTicket = "MM_AUTH=" + token.UserToken, LbpEnvVer = ServerSettings.ServerName, }.Serialize()); diff --git a/ProjectLighthouse/Controllers/MatchController.cs b/ProjectLighthouse/Controllers/MatchController.cs index 6d34b60e..e4ccb354 100644 --- a/ProjectLighthouse/Controllers/MatchController.cs +++ b/ProjectLighthouse/Controllers/MatchController.cs @@ -11,57 +11,70 @@ using LBPUnion.ProjectLighthouse.Types.Profiles; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/")] [Produces("text/xml")] - public class MatchController : ControllerBase { + public class MatchController : ControllerBase + { private readonly Database database; - public MatchController(Database database) { + + public MatchController(Database database) + { this.database = database; } [HttpPost("match")] [Produces("text/json")] - public async Task Match() { + public async Task Match() + { User? user = await this.database.UserFromRequest(this.Request); - if(user == null) return this.StatusCode(403, ""); + if (user == null) return this.StatusCode(403, ""); #region Parse match data + // Example POST /match: [UpdateMyPlayerData,["Player":"FireGamer9872"]] - + string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); - if(bodyString.Contains("FindBestRoom")) { - return this.Ok("[{\"StatusCode\":200},{\"Players\":[{\"PlayerId\":\"literally1984\",\"matching_res\":0},{\"PlayerId\":\"jvyden\",\"matching_res\":1}],\"Slots\":[[5,0]],\"RoomState\":\"E_ROOM_IN_POD\",\"HostMood\":\"E_MOOD_EVERYONE\",\"LevelCompletionEstimate\":0,\"PassedNoJoinPoint\":0,\"MoveConnected\":false,\"Location\":[\"127.0.0.1\"],\"BuildVersion\":289,\"Language\":1,\"FirstSeenTimestamp\":1427331263756,\"LastSeenTimestamp\":1635112546000,\"GameId\":1,\"NatType\":2,\"Friends\":[],\"Blocked\":[],\"RecentlyLeft\":[],\"FailedJoin\":[]}]"); + if (bodyString.Contains("FindBestRoom")) + { + return this.Ok( + "[{\"StatusCode\":200},{\"Players\":[{\"PlayerId\":\"literally1984\",\"matching_res\":0},{\"PlayerId\":\"jvyden\",\"matching_res\":1}],\"Slots\":[[5,0]],\"RoomState\":\"E_ROOM_IN_POD\",\"HostMood\":\"E_MOOD_EVERYONE\",\"LevelCompletionEstimate\":0,\"PassedNoJoinPoint\":0,\"MoveConnected\":false,\"Location\":[\"127.0.0.1\"],\"BuildVersion\":289,\"Language\":1,\"FirstSeenTimestamp\":1427331263756,\"LastSeenTimestamp\":1635112546000,\"GameId\":1,\"NatType\":2,\"Friends\":[],\"Blocked\":[],\"RecentlyLeft\":[],\"FailedJoin\":[]}]"); } - - if(bodyString[0] != '[') return this.BadRequest(); + + if (bodyString[0] != '[') return this.BadRequest(); IMatchData? matchData; - try { + try + { matchData = MatchHelper.Deserialize(bodyString); } - catch(Exception e) { + catch (Exception e) + { Logger.Log("Exception while parsing MatchData: " + e); Logger.Log("Data: " + bodyString); return this.BadRequest(); } - if(matchData == null) return this.BadRequest(); - + if (matchData == null) return this.BadRequest(); + #endregion #region Update LastMatch + LastMatch? lastMatch = await this.database.LastMatches .Where(l => l.UserId == user.UserId).FirstOrDefaultAsync(); // below makes it not look like trash // ReSharper disable once ConvertIfStatementToNullCoalescingExpression - if(lastMatch == null) { - lastMatch = new LastMatch { + if (lastMatch == null) + { + lastMatch = new LastMatch + { UserId = user.UserId, }; this.database.LastMatches.Add(lastMatch); @@ -70,14 +83,16 @@ namespace LBPUnion.ProjectLighthouse.Controllers { lastMatch.Timestamp = TimestampHelper.Timestamp; await this.database.SaveChangesAsync(); + #endregion - + return this.Ok("[{\"StatusCode\":200}]"); } [HttpGet("playersInPodCount")] [HttpGet("totalPlayerCount")] - public async Task TotalPlayerCount() { + public async Task TotalPlayerCount() + { int recentMatches = await this.database.LastMatches .Where(l => TimestampHelper.Timestamp - l.Timestamp < 60) .CountAsync(); diff --git a/ProjectLighthouse/Controllers/MessageController.cs b/ProjectLighthouse/Controllers/MessageController.cs index ff6b8112..eba2834a 100644 --- a/ProjectLighthouse/Controllers/MessageController.cs +++ b/ProjectLighthouse/Controllers/MessageController.cs @@ -3,38 +3,48 @@ using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/")] [Produces("text/plain")] - public class MessageController : ControllerBase { + public class MessageController : ControllerBase + { private readonly Database database; - public MessageController(Database database) { + + public MessageController(Database database) + { this.database = database; } [HttpGet("eula")] - public async Task Eula() { + public async Task Eula() + { User user = await this.database.UserFromRequest(this.Request); - return user == null ? this.StatusCode(403, "") : - this.Ok($"You are now logged in as user {user.Username} (id {user.UserId}).\n" + - "This is a private testing instance. Please do not make anything public for now, and keep in mind security isn't as tight as a full release would."); + return user == null + ? this.Ok("You aren't logged in, but you're connected to a private LBP server.") + : this.Ok($"You are now logged in as user {user.Username} (id {user.UserId}).\n" + + "This is a private testing instance. Please do not make anything public for now, and keep in mind security isn't as tight as a full release would."); } [HttpGet("announce")] - public IActionResult Announce() { + public IActionResult Announce() + { return this.Ok(""); } [HttpGet("notification")] - public IActionResult Notification() { + public IActionResult Notification() + { return this.Ok(); } + /// /// Filters chat messages sent by a user. /// [HttpPost("filter")] - public async Task Filter() { + public async Task Filter() + { return this.Ok(await new StreamReader(this.Request.Body).ReadToEndAsync()); } } diff --git a/ProjectLighthouse/Controllers/NewsController.cs b/ProjectLighthouse/Controllers/NewsController.cs index 5fa6e8f3..10a29254 100644 --- a/ProjectLighthouse/Controllers/NewsController.cs +++ b/ProjectLighthouse/Controllers/NewsController.cs @@ -1,18 +1,30 @@ +using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.News; using Microsoft.AspNetCore.Mvc; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/news")] [Produces("text/xml")] - public class NewsController : ControllerBase { + public class NewsController : ControllerBase + { + [HttpGet("/developer_videos")] + public async Task DeveloperVideos() + { + return Ok(); + } + [HttpGet] - public IActionResult Get() { - string newsEntry = LbpSerializer.StringElement("item", new NewsEntry { + public IActionResult Get() + { + string newsEntry = LbpSerializer.StringElement("item", new NewsEntry + { Category = "no_category", Summary = "test summary", - Image = new NewsImage { + Image = new NewsImage + { Hash = "4947269c5f7061b27225611ee58a9a91a8031bbe", Alignment = "right", }, @@ -21,7 +33,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers { Text = "Test Text", Date = 1348755214000, }.Serialize()); - + return this.Ok(LbpSerializer.StringElement("news", newsEntry)); } } diff --git a/ProjectLighthouse/Controllers/ResourcesController.cs b/ProjectLighthouse/Controllers/ResourcesController.cs index 7b80c591..c7726935 100644 --- a/ProjectLighthouse/Controllers/ResourcesController.cs +++ b/ProjectLighthouse/Controllers/ResourcesController.cs @@ -10,60 +10,68 @@ using LBPUnion.ProjectLighthouse.Types.Files; using Microsoft.AspNetCore.Mvc; using IOFile = System.IO.File; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/")] [Produces("text/xml")] - public class ResourcesController : ControllerBase { + public class ResourcesController : ControllerBase + { [HttpPost("showModerated")] - public IActionResult ShowModerated() { + public IActionResult ShowModerated() + { return this.Ok(LbpSerializer.BlankElement("resources")); } [HttpPost("filterResources")] [HttpPost("showNotUploaded")] - public async Task FilterResources() { - string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); - - XmlSerializer serializer = new(typeof(ResourceList)); - ResourceList resourceList = (ResourceList)serializer.Deserialize(new StringReader(bodyString)); + public async Task FilterResources() + { + string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); - if(resourceList == null) return this.BadRequest(); + XmlSerializer serializer = new(typeof(ResourceList)); + ResourceList resourceList = (ResourceList) serializer.Deserialize(new StringReader(bodyString)); + + if (resourceList == null) return this.BadRequest(); string resources = resourceList.Resources .Where(s => !FileHelper.ResourceExists(s)) - .Aggregate("", (current, hash) => + .Aggregate("", (current, hash) => current + LbpSerializer.StringElement("resource", hash)); return this.Ok(LbpSerializer.StringElement("resources", resources)); } [HttpGet("r/{hash}")] - public IActionResult GetResource(string hash) { + public IActionResult GetResource(string hash) + { string path = FileHelper.GetResourcePath(hash); - if(FileHelper.ResourceExists(hash)) { + if (FileHelper.ResourceExists(hash)) + { return this.File(IOFile.OpenRead(path), "application/octet-stream"); } + return this.NotFound(); } // TODO: check if this is a valid hash [HttpPost("upload/{hash}")] [AllowSynchronousIo] - public async Task UploadResource(string hash) { + public async Task UploadResource(string hash) + { string assetsDirectory = FileHelper.ResourcePath; string path = FileHelper.GetResourcePath(hash); - + FileHelper.EnsureDirectoryCreated(assetsDirectory); - if(FileHelper.ResourceExists(hash)) this.Ok(); // no reason to fail if it's already uploaded + if (FileHelper.ResourceExists(hash)) this.Ok(); // no reason to fail if it's already uploaded Logger.Log($"Processing resource upload (hash: {hash})"); LbpFile file = new(await BinaryHelper.ReadFromPipeReader(Request.BodyReader)); - if(!FileHelper.IsFileSafe(file)) return this.UnprocessableEntity(); - + if (!FileHelper.IsFileSafe(file)) return this.UnprocessableEntity(); + await IOFile.WriteAllBytesAsync(path, file.Data); return this.Ok(); } diff --git a/ProjectLighthouse/Controllers/UserController.cs b/ProjectLighthouse/Controllers/UserController.cs index 1c69c039..6fd0bac8 100644 --- a/ProjectLighthouse/Controllers/UserController.cs +++ b/ProjectLighthouse/Controllers/UserController.cs @@ -8,26 +8,37 @@ using LBPUnion.ProjectLighthouse.Types.Profiles; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse.Controllers { +namespace LBPUnion.ProjectLighthouse.Controllers +{ [ApiController] [Route("LITTLEBIGPLANETPS3_XML/")] [Produces("text/xml")] - public class UserController : ControllerBase { + public class UserController : ControllerBase + { private readonly Database database; - public UserController(Database database) { + + public UserController(Database database) + { this.database = database; } [HttpGet("user/{username}")] - public async Task GetUser(string username) { + public async Task GetUser(string username) + { User user = await this.database.Users .Include(u => u.Location) .FirstOrDefaultAsync(u => u.Username == username); - if(user == null) return this.NotFound(); + if (user == null) return this.NotFound(); return this.Ok(user.Serialize()); } + [HttpGet("user/{username}/playlists")] + public async Task GetUserPlaylists(string username) + { + return this.Ok(); + } + // [HttpPost("user/{username}")] // public async Task CreateUser(string username) { // await new Database().CreateUser(username); @@ -35,12 +46,14 @@ namespace LBPUnion.ProjectLighthouse.Controllers { // } [HttpPost("updateUser")] - public async Task UpdateUser() { + public async Task UpdateUser() + { User user = await this.database.UserFromRequest(this.Request); - if(user == null) return this.StatusCode(403, ""); + if (user == null) return this.StatusCode(403, ""); - XmlReaderSettings settings = new() { + XmlReaderSettings settings = new() + { Async = true, // this is apparently not default }; @@ -62,39 +75,57 @@ namespace LBPUnion.ProjectLighthouse.Controllers { // // // if you find a way to make it not stupid feel free to replace this - using(XmlReader reader = XmlReader.Create(this.Request.Body, settings)) { - List path = new(); // you can think of this as a file path in the XML, like -> -> - while(await reader.ReadAsync()) { + using (XmlReader reader = XmlReader.Create(this.Request.Body, settings)) + { + List + path = new(); // you can think of this as a file path in the XML, like -> -> + while (await reader.ReadAsync()) + { // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch(reader.NodeType) { + switch (reader.NodeType) + { case XmlNodeType.Element: path.Add(reader.Name); break; case XmlNodeType.Text: - switch(path[1]) { - case "biography": { + switch (path[1]) + { + case "biography": + { user.Biography = await reader.GetValueAsync(); break; } - case "location": { - locationChanged = true; // if we're here then we're probably about to change the location. + case "location": + { + locationChanged = + true; // if we're here then we're probably about to change the location. // ReSharper disable once ConvertIfStatementToSwitchStatement - if(path[2] == "x") { - user.Location.X = Convert.ToInt32(await reader.GetValueAsync()); // GetValue only returns a string, i guess we just hope its a number lol - } else if(path[2] == "y") { + if (path[2] == "x") + { + user.Location.X = + Convert.ToInt32( + await reader + .GetValueAsync()); // GetValue only returns a string, i guess we just hope its a number lol + } + else if (path[2] == "y") + { user.Location.Y = Convert.ToInt32(await reader.GetValueAsync()); } + break; } - case "icon": { + case "icon": + { user.IconHash = await reader.GetValueAsync(); break; } - case "planets": { + case "planets": + { user.PlanetHash = await reader.GetValueAsync(); break; } } + break; case XmlNodeType.EndElement: path.RemoveAt(path.Count - 1); @@ -102,19 +133,23 @@ namespace LBPUnion.ProjectLighthouse.Controllers { } } } - + // the way location on a user card works is stupid and will not save with the way below as-is, so we do the following: - if(locationChanged) { // only modify the database if we modify here - Location l = await this.database.Locations.Where(l => l.Id == user.LocationId).FirstOrDefaultAsync(); // find the location in the database again + if (locationChanged) + { + // only modify the database if we modify here + Location l = await this.database.Locations.Where(l => l.Id == user.LocationId) + .FirstOrDefaultAsync(); // find the location in the database again // set the location in the database to the one we modified above l.X = user.Location.X; l.Y = user.Location.Y; - + // now both are in sync, and will update in the database. } - - if(this.database.ChangeTracker.HasChanges()) await this.database.SaveChangesAsync(); // save the user to the database if we changed anything + + if (this.database.ChangeTracker.HasChanges()) + await this.database.SaveChangesAsync(); // save the user to the database if we changed anything return this.Ok(); } } diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 4dd23287..2bb4714c 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -8,8 +8,10 @@ using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -namespace LBPUnion.ProjectLighthouse { - public class Database : DbContext { +namespace LBPUnion.ProjectLighthouse +{ + public class Database : DbContext + { public DbSet Users { get; set; } public DbSet Locations { get; set; } public DbSet Slots { get; set; } @@ -26,16 +28,18 @@ namespace LBPUnion.ProjectLighthouse { MySqlServerVersion.LatestSupportedServerVersion ); - public async Task CreateUser(string username) { + public async Task CreateUser(string username) + { User user; - if((user = await this.Users.Where(u => u.Username == username).FirstOrDefaultAsync()) != null) + if ((user = await this.Users.Where(u => u.Username == username).FirstOrDefaultAsync()) != null) return user; Location l = new(); // store to get id after submitting this.Locations.Add(l); // add to table await this.SaveChangesAsync(); // saving to the database returns the id and sets it on this entity - user = new User { + user = new User + { Username = username, LocationId = l.Id, Biography = username + " hasn't introduced themselves yet.", @@ -46,14 +50,16 @@ namespace LBPUnion.ProjectLighthouse { return user; } - - #nullable enable - public async Task AuthenticateUser(LoginData loginData) { + +#nullable enable + public async Task AuthenticateUser(LoginData loginData) + { // TODO: don't use psn name to authenticate - User user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username) + User user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username) ?? await this.CreateUser(loginData.Username); - Token token = new() { + Token token = new() + { UserToken = HashHelper.GenerateAuthToken(), UserId = user.UserId, }; @@ -64,21 +70,24 @@ namespace LBPUnion.ProjectLighthouse { return token; } - public async Task UserFromAuthToken(string authToken) { + public async Task UserFromAuthToken(string authToken) + { Token? token = await this.Tokens.FirstOrDefaultAsync(t => t.UserToken == authToken); - if(token == null) return null; + if (token == null) return null; return await this.Users .Include(u => u.Location) .FirstOrDefaultAsync(u => u.UserId == token.UserId); } - public async Task UserFromRequest(HttpRequest request) { - if(!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth) || mmAuth == null) { + public async Task UserFromRequest(HttpRequest request) + { + if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth) || mmAuth == null) + { return null; } - + return await this.UserFromAuthToken(mmAuth); } - #nullable disable +#nullable disable } } \ No newline at end of file diff --git a/ProjectLighthouse/DigestUtils.cs b/ProjectLighthouse/DigestUtils.cs new file mode 100644 index 00000000..392054d2 --- /dev/null +++ b/ProjectLighthouse/DigestUtils.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace LBPUnion.ProjectLighthouse +{ + public static class DigestUtils + { + public static async Task ComputeDigest(string path, string authCookie, Stream body, + string digestKey) + { + var memoryStream = new MemoryStream(); + + var pathBytes = Encoding.UTF8.GetBytes(path); + var cookieBytes = string.IsNullOrEmpty(authCookie) + ? Array.Empty() + : Encoding.UTF8.GetBytes(authCookie); + var keyBytes = Encoding.UTF8.GetBytes(digestKey); + + await body.CopyToAsync(memoryStream); + + var bodyBytes = memoryStream.ToArray(); + + using var sha1 = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); + sha1.AppendData(bodyBytes); + if (cookieBytes.Length > 0) + sha1.AppendData(cookieBytes); + sha1.AppendData(pathBytes); + sha1.AppendData(keyBytes); + + var digestBytes = sha1.GetHashAndReset(); + var digestString = Convert.ToHexString(digestBytes).ToLower(); + + return digestString; + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Logging/LighthouseFileLogger.cs b/ProjectLighthouse/Logging/LighthouseFileLogger.cs index 98cb4d60..1305986f 100644 --- a/ProjectLighthouse/Logging/LighthouseFileLogger.cs +++ b/ProjectLighthouse/Logging/LighthouseFileLogger.cs @@ -7,7 +7,9 @@ namespace LBPUnion.ProjectLighthouse.Logging { public class LighthouseFileLogger : LoggerBase { private static readonly string logsDirectory = Path.Combine(Environment.CurrentDirectory, "logs"); - public override void Send(LoggerLine line) { + public override void Send(LoggerLine line) + { + return; FileHelper.EnsureDirectoryCreated(logsDirectory); File.AppendAllText(Path.Combine(logsDirectory, line.LoggerLevel + ".log"), line.LineData + "\n"); diff --git a/ProjectLighthouse/Program.cs b/ProjectLighthouse/Program.cs index 8b2a6bf2..c9a49d9f 100644 --- a/ProjectLighthouse/Program.cs +++ b/ProjectLighthouse/Program.cs @@ -10,51 +10,59 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace LBPUnion.ProjectLighthouse { - public static class Program { - public static void Main(string[] args) { +namespace LBPUnion.ProjectLighthouse +{ + public static class Program + { + public static void Main(string[] args) + { // Log startup time Stopwatch startupStopwatch = new(); startupStopwatch.Start(); - + // Setup logging - + Logger.StartLogging(); LoggerLine.LogFormat = "[{0}] {1}"; Logger.AddLogger(new ConsoleLogger()); Logger.AddLogger(new LighthouseFileLogger()); - + Logger.Log("Welcome to Project Lighthouse!", LoggerLevelStartup.Instance); Logger.Log("Determining if the database is available...", LoggerLevelStartup.Instance); bool dbConnected = ServerSettings.DbConnected; - Logger.Log(dbConnected ? "Connected to the database." : "Database unavailable! Exiting.", LoggerLevelStartup.Instance); + Logger.Log(dbConnected ? "Connected to the database." : "Database unavailable! Exiting.", + LoggerLevelStartup.Instance); - if(dbConnected) { + if (dbConnected) + { Stopwatch migrationStopwatch = new(); migrationStopwatch.Start(); - + Logger.Log("Migrating database...", LoggerLevelDatabase.Instance); using Database database = new(); database.Database.Migrate(); - + migrationStopwatch.Stop(); Logger.Log($"Migration took {migrationStopwatch.ElapsedMilliseconds}ms.", LoggerLevelDatabase.Instance); - } else Environment.Exit(1); - + } + else Environment.Exit(1); + startupStopwatch.Stop(); - Logger.Log($"Ready! Startup took {startupStopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LoggerLevelStartup.Instance); + Logger.Log( + $"Ready! Startup took {startupStopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", + LoggerLevelStartup.Instance); CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); - }) - .ConfigureLogging(logging => { + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .ConfigureLogging(logging => + { logging.ClearProviders(); - logging.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + logging.Services.TryAddEnumerable(ServiceDescriptor + .Singleton()); }); } } \ No newline at end of file diff --git a/ProjectLighthouse/Startup.cs b/ProjectLighthouse/Startup.cs index d83ff302..ec15f68f 100644 --- a/ProjectLighthouse/Startup.cs +++ b/ProjectLighthouse/Startup.cs @@ -1,5 +1,8 @@ +using System; using System.Diagnostics; using System.IO; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; using Kettu; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Serialization; @@ -9,48 +12,115 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; -namespace LBPUnion.ProjectLighthouse { - public class Startup { - public Startup(IConfiguration configuration) { +namespace LBPUnion.ProjectLighthouse +{ + public class Startup + { + public Startup(IConfiguration configuration) + { this.Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) { + public void ConfigureServices(IServiceCollection services) + { services.AddControllers(); services.AddMvc(options => options.OutputFormatters.Add(new XmlOutputFormatter())); - + services.AddDbContext(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - if(env.IsDevelopment()) { + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + var computeDigests = true; + var serverDigestKey = Environment.GetEnvironmentVariable("SERVER_DIGEST_KEY"); + if (string.IsNullOrWhiteSpace(serverDigestKey)) + { + Logger.Log( + "SERVER_DIGEST_KEY environment variable wasn't set, so server digest headers won't be set. This will break LBP 1 and LBP 3." + ); + computeDigests = false; + } + + if (env.IsDevelopment()) + { app.UseDeveloperExceptionPage(); } // Logs every request and the response to it // Example: "200, 13ms: GET /LITTLEBIGPLANETPS3_XML/news" // Example: "404, 127ms: GET /asdasd?query=osucookiezi727ppbluezenithtopplayhdhr" - app.Use(async (context, next) => { + app.Use(async (context, next) => + { Stopwatch requestStopwatch = new(); requestStopwatch.Start(); - + context.Request.EnableBuffering(); // Allows us to reset the position of Request.Body for later logging + + // Client digest check. + var authCookie = null as string; + if (!context.Request.Cookies.TryGetValue("MM_AUTH", out authCookie)) + authCookie = string.Empty; + if (context.Request.Headers.TryGetValue("X-Digest-A", out var clientDigest)) + { + var digestPath = context.Request.Path; + var body = context.Request.Body; + + var digest = await DigestUtils.ComputeDigest(digestPath, authCookie, body, serverDigestKey); + + if (digest != clientDigest) + { + Logger.Log($"Client digest {clientDigest} does not match server digest {digest}."); + context.Abort(); + return; + } + else + { + context.Response.Headers.Add("X-Digest-B", digest); + context.Request.Body.Position = 0; + } + } + + // This does the same as above, but for the response stream. + using var responseBuffer = new MemoryStream(); + var oldResponseStream = context.Response.Body; + context.Response.Body = responseBuffer; + await next(); // Handle the request so we can get the status code from it + + // Compute the server digest hash. + if (computeDigests && context.Request.Headers.TryGetValue("X-Digest-A", out var a)) + { + responseBuffer.Position = 0; + + // Compute the digest for the response. + var serverDigest = await DigestUtils.ComputeDigest(context.Request.Path, authCookie, + responseBuffer, serverDigestKey); + context.Response.Headers.Add("X-Digest-A", serverDigest); + } + + // Copy the buffered response to the actual respose stream. + responseBuffer.Position = 0; + await responseBuffer.CopyToAsync(oldResponseStream); + + context.Response.Body = oldResponseStream; + requestStopwatch.Stop(); - + Logger.Log( $"{context.Response.StatusCode}, {requestStopwatch.ElapsedMilliseconds}ms: {context.Request.Method} {context.Request.Path}{context.Request.QueryString}", LoggerLevelHttp.Instance ); - - if(context.Request.Method == "POST") { + + if (context.Request.Method == "POST") + { context.Request.Body.Position = 0; Logger.Log(await new StreamReader(context.Request.Body).ReadToEndAsync(), LoggerLevelHttp.Instance); } @@ -58,9 +128,7 @@ namespace LBPUnion.ProjectLighthouse { app.UseRouting(); - app.UseEndpoints(endpoints => { - endpoints.MapControllers(); - }); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } } \ No newline at end of file