diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ee41701..5477e569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,13 +60,13 @@ jobs: - name: Process Test Results (Control) if: ${{ matrix.os.prettyName == 'Linux' }} - uses: im-open/process-dotnet-test-results@v2.0.0 + uses: im-open/process-dotnet-test-results@v2.0.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Process Test Results if: ${{ matrix.os.prettyName != 'Linux' }} - uses: im-open/process-dotnet-test-results@v2.0.0 + uses: im-open/process-dotnet-test-results@v2.0.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} create-status-check: false diff --git a/.gitignore b/.gitignore index 1f63e982..ff16b3ae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ riderModule.iml /ProjectLighthouse/ProjectLighthouse.csproj.user .vs/ .vscode/ -.editorconfig \ No newline at end of file +.editorconfig +lighthouse.config.json +gitBranch.txt +gitVersion.txt diff --git a/ProjectLighthouse.Tests/DatabaseFact.cs b/ProjectLighthouse.Tests/DatabaseFact.cs index 7fdc1524..a62ecd5a 100644 --- a/ProjectLighthouse.Tests/DatabaseFact.cs +++ b/ProjectLighthouse.Tests/DatabaseFact.cs @@ -8,8 +8,9 @@ namespace LBPUnion.ProjectLighthouse.Tests { public DatabaseFact() { - ServerSettings.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; - if (!ServerSettings.DbConnected) + ServerSettings.Instance = new ServerSettings(); + ServerSettings.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; + if (!ServerStatics.DbConnected) { this.Skip = "Database not available"; } diff --git a/ProjectLighthouse.Tests/LighthouseTest.cs b/ProjectLighthouse.Tests/LighthouseTest.cs index df207d74..3efb3684 100644 --- a/ProjectLighthouse.Tests/LighthouseTest.cs +++ b/ProjectLighthouse.Tests/LighthouseTest.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Helpers; @@ -55,6 +56,14 @@ namespace LBPUnion.ProjectLighthouse.Tests return this.Client.SendAsync(requestMessage); } + public async Task UploadFileEndpointRequest(string filePath) + { + byte[] bytes = Encoding.UTF8.GetBytes(await File.ReadAllTextAsync(filePath)); + string hash = HashHelper.Sha1Hash(bytes); + + return await this.Client.PostAsync($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", new ByteArrayContent(bytes)); + } + public async Task UploadFileRequest(string endpoint, string filePath) => await this.Client.PostAsync(endpoint, new StringContent(await File.ReadAllTextAsync(filePath))); diff --git a/ProjectLighthouse.Tests/Tests/AuthenticationTests.cs b/ProjectLighthouse.Tests/Tests/AuthenticationTests.cs index 893648e0..346b957b 100644 --- a/ProjectLighthouse.Tests/Tests/AuthenticationTests.cs +++ b/ProjectLighthouse.Tests/Tests/AuthenticationTests.cs @@ -28,7 +28,7 @@ namespace LBPUnion.ProjectLighthouse.Tests Assert.True(response.IsSuccessStatusCode); string responseContent = await response.Content.ReadAsStringAsync(); Assert.Contains("MM_AUTH=", responseContent); - Assert.Contains(ServerSettings.ServerName, responseContent); + Assert.Contains(ServerStatics.ServerName, responseContent); } [DatabaseFact] @@ -41,7 +41,7 @@ namespace LBPUnion.ProjectLighthouse.Tests Assert.NotNull(loginResult.LbpEnvVer); Assert.Contains("MM_AUTH=", loginResult.AuthTicket); - Assert.Equal(ServerSettings.ServerName, loginResult.LbpEnvVer); + Assert.Equal(ServerStatics.ServerName, loginResult.LbpEnvVer); } [DatabaseFact] @@ -59,7 +59,7 @@ namespace LBPUnion.ProjectLighthouse.Tests [DatabaseFact] public async Task ShouldReturnForbiddenWhenNotAuthenticated() { - HttpResponseMessage response = await this.Client.GetAsync("/LITTLEBIGPLANETPS3_XML/eula"); + HttpResponseMessage response = await this.Client.GetAsync("/LITTLEBIGPLANETPS3_XML/announce"); Assert.False(response.IsSuccessStatusCode); Assert.True(response.StatusCode == HttpStatusCode.Forbidden); } diff --git a/ProjectLighthouse.Tests/Tests/SlotTests.cs b/ProjectLighthouse.Tests/Tests/SlotTests.cs index 65abac0f..52f4d690 100644 --- a/ProjectLighthouse.Tests/Tests/SlotTests.cs +++ b/ProjectLighthouse.Tests/Tests/SlotTests.cs @@ -17,13 +17,18 @@ namespace LBPUnion.ProjectLighthouse.Tests User userA = await database.CreateUser("unitTestUser0"); User userB = await database.CreateUser("unitTestUser1"); - Location l = new(); + Location l = new() + { + X = 0, + Y = 0, + }; database.Locations.Add(l); await database.SaveChangesAsync(); Slot slotA = new() { Creator = userA, + CreatorId = userA.UserId, Name = "slotA", Location = l, LocationId = l.Id, @@ -33,6 +38,7 @@ namespace LBPUnion.ProjectLighthouse.Tests Slot slotB = new() { Creator = userB, + CreatorId = userB.UserId, Name = "slotB", Location = l, LocationId = l.Id, @@ -49,8 +55,10 @@ namespace LBPUnion.ProjectLighthouse.Tests LoginResult loginResult = await this.Authenticate(); - HttpResponseMessage respMessageA = await this.AuthenticatedRequest("LITTLEBIGPLANETPS3_XML/slots/by?u=unitTestUser0", loginResult.AuthTicket); - HttpResponseMessage respMessageB = await this.AuthenticatedRequest("LITTLEBIGPLANETPS3_XML/slots/by?u=unitTestUser1", loginResult.AuthTicket); + HttpResponseMessage respMessageA = await this.AuthenticatedRequest + ("LITTLEBIGPLANETPS3_XML/slots/by?u=unitTestUser0&pageStart=1&pageSize=1", loginResult.AuthTicket); + HttpResponseMessage respMessageB = await this.AuthenticatedRequest + ("LITTLEBIGPLANETPS3_XML/slots/by?u=unitTestUser1&pageStart=1&pageSize=1", loginResult.AuthTicket); Assert.True(respMessageA.IsSuccessStatusCode); Assert.True(respMessageB.IsSuccessStatusCode); diff --git a/ProjectLighthouse.Tests/Tests/UploadTests.cs b/ProjectLighthouse.Tests/Tests/UploadTests.cs index 41612256..fb73be01 100644 --- a/ProjectLighthouse.Tests/Tests/UploadTests.cs +++ b/ProjectLighthouse.Tests/Tests/UploadTests.cs @@ -17,35 +17,35 @@ namespace LBPUnion.ProjectLighthouse.Tests [Fact] public async Task ShouldNotAcceptScript() { - HttpResponseMessage response = await this.UploadFileRequest("/LITTLEBIGPLANETPS3_XML/upload/scriptTest", "ExampleFiles/TestScript.ff"); + HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestScript.ff"); Assert.False(response.IsSuccessStatusCode); } [Fact] public async Task ShouldNotAcceptFarc() { - HttpResponseMessage response = await this.UploadFileRequest("/LITTLEBIGPLANETPS3_XML/upload/farcTest", "ExampleFiles/TestFarc.farc"); + HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestFarc.farc"); Assert.False(response.IsSuccessStatusCode); } [Fact] public async Task ShouldNotAcceptGarbage() { - HttpResponseMessage response = await this.UploadFileRequest("/LITTLEBIGPLANETPS3_XML/upload/garbageTest", "ExampleFiles/TestGarbage.bin"); + HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestGarbage.bin"); Assert.False(response.IsSuccessStatusCode); } [Fact] public async Task ShouldAcceptTexture() { - HttpResponseMessage response = await this.UploadFileRequest("/LITTLEBIGPLANETPS3_XML/upload/textureTest", "ExampleFiles/TestTexture.tex"); + HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestTexture.tex"); Assert.True(response.IsSuccessStatusCode); } [Fact] public async Task ShouldAcceptLevel() { - HttpResponseMessage response = await this.UploadFileRequest("/LITTLEBIGPLANETPS3_XML/upload/levelTest", "ExampleFiles/TestLevel.lvl"); + HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestLevel.lvl"); Assert.True(response.IsSuccessStatusCode); } } diff --git a/ProjectLighthouse/Controllers/ListController.cs b/ProjectLighthouse/Controllers/ListController.cs index 4e3014f8..f4139dc1 100644 --- a/ProjectLighthouse/Controllers/ListController.cs +++ b/ProjectLighthouse/Controllers/ListController.cs @@ -90,6 +90,19 @@ namespace LBPUnion.ProjectLighthouse.Controllers return this.Ok(); } + [HttpPost("lolcatftw/clear")] + public async Task ClearQueuedLevels() + { + User? user = await this.database.UserFromRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + this.database.QueuedLevels.RemoveRange(this.database.QueuedLevels.Where(q => q.UserId == user.UserId)); + + await this.database.SaveChangesAsync(); + + return this.Ok(); + } + #endregion #region Hearted Levels diff --git a/ProjectLighthouse/Controllers/LoginController.cs b/ProjectLighthouse/Controllers/LoginController.cs index d3e4df74..6bce4863 100644 --- a/ProjectLighthouse/Controllers/LoginController.cs +++ b/ProjectLighthouse/Controllers/LoginController.cs @@ -65,7 +65,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers new LoginResult { AuthTicket = "MM_AUTH=" + token.UserToken, - LbpEnvVer = ServerSettings.ServerName, + LbpEnvVer = ServerStatics.ServerName, }.Serialize() ); } diff --git a/ProjectLighthouse/Controllers/MessageController.cs b/ProjectLighthouse/Controllers/MessageController.cs index 26ec9d3e..beab0d93 100644 --- a/ProjectLighthouse/Controllers/MessageController.cs +++ b/ProjectLighthouse/Controllers/MessageController.cs @@ -4,6 +4,7 @@ using Kettu; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types; +using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Mvc; namespace LBPUnion.ProjectLighthouse.Controllers @@ -21,11 +22,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers } [HttpGet("eula")] - public async Task Eula() - { - User user = await this.database.UserFromRequest(this.Request); - return user == null ? this.StatusCode(403, "") : this.Ok(EulaHelper.PrivateInstanceNoticeOrBlank + "\n" + $"{EulaHelper.License}\n"); - } + public IActionResult Eula() => this.Ok(ServerSettings.Instance.EulaText + "\n" + $"{EulaHelper.License}\n"); [HttpGet("announce")] public async Task Announce() @@ -33,7 +30,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers User user = await this.database.UserFromRequest(this.Request); if (user == null) return this.StatusCode(403, ""); - return this.Ok($"You are now logged in as user {user.Username} (id {user.UserId}).\n\n" + EulaHelper.PrivateInstanceNoticeOrBlank); + return this.Ok($"You are now logged in as user {user.Username} (id {user.UserId}).\n\n" + ServerSettings.Instance.EulaText); } [HttpGet("notification")] diff --git a/ProjectLighthouse/Controllers/ResourcesController.cs b/ProjectLighthouse/Controllers/ResourcesController.cs index 3680bbfd..adb7734b 100644 --- a/ProjectLighthouse/Controllers/ResourcesController.cs +++ b/ProjectLighthouse/Controllers/ResourcesController.cs @@ -51,10 +51,8 @@ namespace LBPUnion.ProjectLighthouse.Controllers // TODO: check if this is a valid hash [HttpPost("upload/{hash}")] - [AllowSynchronousIo] public async Task UploadResource(string hash) { - string assetsDirectory = FileHelper.ResourcePath; string path = FileHelper.GetResourcePath(hash); @@ -70,6 +68,12 @@ namespace LBPUnion.ProjectLighthouse.Controllers return this.UnprocessableEntity(); } + if (HashHelper.Sha1Hash(file.Data) != hash) + { + Logger.Log($"File hash does not match the uploaded file! (hash: {hash}, type: {file.FileType})", LoggerLevelResources.Instance); + return this.Conflict(); + } + Logger.Log($"File is OK! (hash: {hash}, type: {file.FileType})", LoggerLevelResources.Instance); await IOFile.WriteAllBytesAsync(path, file.Data); return this.Ok(); diff --git a/ProjectLighthouse/Controllers/SlotsController.cs b/ProjectLighthouse/Controllers/SlotsController.cs index f7cfe67e..2ded22b4 100644 --- a/ProjectLighthouse/Controllers/SlotsController.cs +++ b/ProjectLighthouse/Controllers/SlotsController.cs @@ -42,7 +42,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers .Include(s => s.Location) .Where(s => s.Creator!.Username == user.Username) .Skip(pageStart - 1) - .Take(Math.Min(pageSize, ServerSettings.EntitledSlots)), + .Take(Math.Min(pageSize, ServerStatics.EntitledSlots)), string.Empty, (current, slot) => current + slot.Serialize() ); @@ -56,7 +56,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers new Dictionary { { - "hint_start", pageStart + Math.Min(pageSize, ServerSettings.EntitledSlots) + "hint_start", pageStart + Math.Min(pageSize, ServerStatics.EntitledSlots) }, { "total", user.UsedSlots diff --git a/ProjectLighthouse/Controllers/StatisticsController.cs b/ProjectLighthouse/Controllers/StatisticsController.cs index 9deb12c4..eb945419 100644 --- a/ProjectLighthouse/Controllers/StatisticsController.cs +++ b/ProjectLighthouse/Controllers/StatisticsController.cs @@ -1,9 +1,7 @@ -using System.Linq; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Serialization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Controllers { @@ -20,18 +18,13 @@ namespace LBPUnion.ProjectLighthouse.Controllers [HttpGet("playersInPodCount")] [HttpGet("totalPlayerCount")] - public async Task TotalPlayerCount() - { - int recentMatches = await this.database.LastMatches.Where(l => TimestampHelper.Timestamp - l.Timestamp < 60).CountAsync(); - - return this.Ok(recentMatches.ToString()); - } + public async Task TotalPlayerCount() => this.Ok((await StatisticsHelper.RecentMatches()).ToString()!); [HttpGet("planetStats")] public async Task PlanetStats() { - int totalSlotCount = await this.database.Slots.CountAsync(); - int mmPicksCount = await this.database.Slots.CountAsync(s => s.TeamPick); + int totalSlotCount = await StatisticsHelper.SlotCount(); + int mmPicksCount = await StatisticsHelper.MMPicksCount(); return this.Ok ( @@ -41,6 +34,6 @@ namespace LBPUnion.ProjectLighthouse.Controllers } [HttpGet("planetStats/totalLevelCount")] - public async Task TotalLevelCount() => this.Ok((await this.database.Slots.CountAsync()).ToString()); + public async Task TotalLevelCount() => this.Ok((await StatisticsHelper.SlotCount()).ToString()); } } \ No newline at end of file diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 0af44ada..2d1bfbeb 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -33,7 +33,7 @@ namespace LBPUnion.ProjectLighthouse public DbSet RatedReviews { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseMySql(ServerSettings.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); + => options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); public async Task CreateUser(string username) { diff --git a/ProjectLighthouse/Helpers/AllowSynchronousIOAttribute.cs b/ProjectLighthouse/Helpers/AllowSynchronousIOAttribute.cs deleted file mode 100644 index 1054f1a7..00000000 --- a/ProjectLighthouse/Helpers/AllowSynchronousIOAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace LBPUnion.ProjectLighthouse.Helpers -{ - // Yoinked from https://stackoverflow.com/a/68530667 - // Thanks to T-moty! - /// - /// Allows synchronous stream operations for this request. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class AllowSynchronousIoAttribute : ActionFilterAttribute - { - public override void OnResultExecuting(ResultExecutingContext context) - { - IHttpBodyControlFeature syncIoFeature = context.HttpContext.Features.Get(); - if (syncIoFeature != null) syncIoFeature.AllowSynchronousIO = true; - } - } -} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/EulaHelper.cs b/ProjectLighthouse/Helpers/EulaHelper.cs index bf59ff2c..03c91a38 100644 --- a/ProjectLighthouse/Helpers/EulaHelper.cs +++ b/ProjectLighthouse/Helpers/EulaHelper.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - namespace LBPUnion.ProjectLighthouse.Helpers { public static class EulaHelper @@ -17,13 +15,5 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see ."; - - public const string PrivateInstanceNotice = @"This server 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."; - - [SuppressMessage("ReSharper", "HeuristicUnreachableCode")] - public const string PrivateInstanceNoticeOrBlank = ShowPrivateInstanceNotice ? PrivateInstanceNotice : ""; - - public const bool ShowPrivateInstanceNotice = false; } } \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/FileHelper.cs b/ProjectLighthouse/Helpers/FileHelper.cs index f519effa..f85b3113 100644 --- a/ProjectLighthouse/Helpers/FileHelper.cs +++ b/ProjectLighthouse/Helpers/FileHelper.cs @@ -78,6 +78,18 @@ namespace LBPUnion.ProjectLighthouse.Helpers public static bool ResourceExists(string hash) => File.Exists(GetResourcePath(hash)); + public static int ResourceSize(string hash) + { + try + { + return (int)new FileInfo(GetResourcePath(hash)).Length; + } + catch + { + return 0; + } + } + public static void EnsureDirectoryCreated(string path) { if (!Directory.Exists(path)) Directory.CreateDirectory(path ?? throw new ArgumentNullException(nameof(path))); diff --git a/ProjectLighthouse/Helpers/GitVersionHelper.cs b/ProjectLighthouse/Helpers/GitVersionHelper.cs new file mode 100644 index 00000000..857728fe --- /dev/null +++ b/ProjectLighthouse/Helpers/GitVersionHelper.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using Kettu; +using LBPUnion.ProjectLighthouse.Logging; + +namespace LBPUnion.ProjectLighthouse.Helpers +{ + public static class GitVersionHelper + { + static GitVersionHelper() + { + try + { + CommitHash = readManifestFile("gitVersion.txt"); + Branch = readManifestFile("gitBranch.txt"); + CanCheckForUpdates = true; + } + catch + { + Logger.Log + ( + "Project Lighthouse was built incorrectly. Please make sure git is available when building. " + + "Because of this, you will not be notified of updates.", + LoggerLevelStartup.Instance + ); + CommitHash = "invalid"; + Branch = "invalid"; + CanCheckForUpdates = false; + } + + if (IsDirty) + { + Logger.Log + ( + "This is a modified version of Project Lighthouse. " + + "Please make sure you are properly disclosing the source code to any users who may be using this instance.", + LoggerLevelStartup.Instance + ); + CanCheckForUpdates = false; + } + } + + private static string readManifestFile(string fileName) + { + using Stream stream = typeof(Program).Assembly.GetManifestResourceStream($"{typeof(Program).Namespace}.{fileName}"); + using StreamReader reader = new(stream ?? throw new Exception("The assembly or manifest resource is null.")); + + return reader.ReadToEnd().Trim(); + } + + public static string CommitHash { get; set; } + public static string Branch { get; set; } + public static bool IsDirty => CommitHash.EndsWith("-dirty"); + public static bool CanCheckForUpdates { get; set; } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/HashHelper.cs b/ProjectLighthouse/Helpers/HashHelper.cs index ffda6e17..838a22a4 100644 --- a/ProjectLighthouse/Helpers/HashHelper.cs +++ b/ProjectLighthouse/Helpers/HashHelper.cs @@ -11,7 +11,7 @@ namespace LBPUnion.ProjectLighthouse.Helpers [SuppressMessage("ReSharper", "UnusedMember.Global")] public static class HashHelper { -// private static readonly SHA1 sha1 = SHA1.Create(); + private static readonly SHA1 sha1 = SHA1.Create(); private static readonly SHA256 sha256 = SHA256.Create(); private static readonly Random random = new(); @@ -67,11 +67,11 @@ namespace LBPUnion.ProjectLighthouse.Helpers public static string Sha256Hash(string str) => Sha256Hash(Encoding.UTF8.GetBytes(str)); - public static string Sha256Hash(byte[] bytes) - { - byte[] hash = sha256.ComputeHash(bytes); - return Encoding.UTF8.GetString(hash, 0, hash.Length); - } + public static string Sha256Hash(byte[] bytes) => BitConverter.ToString(sha256.ComputeHash(bytes)).Replace("-", ""); + + public static string Sha1Hash(string str) => Sha1Hash(Encoding.UTF8.GetBytes(str)); + + public static string Sha1Hash(byte[] bytes) => BitConverter.ToString(sha1.ComputeHash(bytes)).Replace("-", ""); public static string BCryptHash(string str) => BCrypt.Net.BCrypt.HashPassword(str); diff --git a/ProjectLighthouse/Helpers/InfluxHelper.cs b/ProjectLighthouse/Helpers/InfluxHelper.cs new file mode 100644 index 00000000..166429fc --- /dev/null +++ b/ProjectLighthouse/Helpers/InfluxHelper.cs @@ -0,0 +1,50 @@ +using System.Threading; +using System.Threading.Tasks; +using InfluxDB.Client; +using InfluxDB.Client.Writes; +using Kettu; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Settings; + +namespace LBPUnion.ProjectLighthouse.Helpers +{ + public static class InfluxHelper + { + public static readonly InfluxDBClient Client = InfluxDBClientFactory.Create(ServerSettings.Instance.InfluxUrl, ServerSettings.Instance.InfluxToken); + + public static async void Log() + { + using WriteApi writeApi = Client.GetWriteApi(); + PointData point = PointData.Measurement("lighthouse") + .Field("playerCount", await StatisticsHelper.RecentMatches()) + .Field("slotCount", await StatisticsHelper.SlotCount()); + + writeApi.WritePoint(ServerSettings.Instance.InfluxBucket, ServerSettings.Instance.InfluxOrg, point); + + writeApi.Flush(); + } + + public static async Task StartLogging() + { + await Client.ReadyAsync(); + Logger.Log("InfluxDB is now ready.", LoggerLevelInflux.Instance); + Thread t = new + ( + delegate() + { + while (true) + { + #pragma warning disable CS4014 + Log(); + #pragma warning restore CS4014 +// Logger.Log("Logged.", LoggerLevelInflux.Instance); + Thread.Sleep(60000); + } + } + ); + t.IsBackground = true; + t.Name = "InfluxDB Logger"; + t.Start(); + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/StatisticsHelper.cs b/ProjectLighthouse/Helpers/StatisticsHelper.cs new file mode 100644 index 00000000..3a43ed81 --- /dev/null +++ b/ProjectLighthouse/Helpers/StatisticsHelper.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Helpers +{ + public static class StatisticsHelper + { + private static readonly Database database = new(); + + public static async Task RecentMatches() => await database.LastMatches.Where(l => TimestampHelper.Timestamp - l.Timestamp < 300).CountAsync(); + + public static async Task SlotCount() => await database.Slots.CountAsync(); + + public static async Task MMPicksCount() => await database.Slots.CountAsync(s => s.TeamPick); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Logging/InfluxLogger.cs b/ProjectLighthouse/Logging/InfluxLogger.cs new file mode 100644 index 00000000..a4ab28e9 --- /dev/null +++ b/ProjectLighthouse/Logging/InfluxLogger.cs @@ -0,0 +1,27 @@ +using InfluxDB.Client; +using InfluxDB.Client.Writes; +using Kettu; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Types.Settings; + +namespace LBPUnion.ProjectLighthouse.Logging +{ + public class InfluxLogger : LoggerBase + { + + public override void Send(LoggerLine line) + { + string channel = string.IsNullOrEmpty(line.LoggerLevel.Channel) ? "" : $"[{line.LoggerLevel.Channel}] "; + + string level = $"{$"{line.LoggerLevel.Name} {channel}".TrimEnd()}"; + string content = line.LineData; + + using WriteApi writeApi = InfluxHelper.Client.GetWriteApi(); + + PointData point = PointData.Measurement("lighthouseLog").Field("level", level).Field("content", content); + + writeApi.WritePoint(ServerSettings.Instance.InfluxBucket, ServerSettings.Instance.InfluxOrg, point); + } + public override bool AllowMultiple => false; + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Logging/LoggerLevels.cs b/ProjectLighthouse/Logging/LoggerLevels.cs index 2fb28109..9d81a53b 100644 --- a/ProjectLighthouse/Logging/LoggerLevels.cs +++ b/ProjectLighthouse/Logging/LoggerLevels.cs @@ -51,6 +51,18 @@ namespace LBPUnion.ProjectLighthouse.Logging public override string Name => "Photos"; } + public class LoggerLevelConfig : LoggerLevel + { + public static readonly LoggerLevelConfig Instance = new(); + public override string Name => "Config"; + } + + public class LoggerLevelInflux : LoggerLevel + { + public static readonly LoggerLevelInflux Instance = new(); + public override string Name => "Influx"; + } + public class LoggerLevelAspNet : LoggerLevel { diff --git a/ProjectLighthouse/Program.cs b/ProjectLighthouse/Program.cs index d3136b16..695e2470 100644 --- a/ProjectLighthouse/Program.cs +++ b/ProjectLighthouse/Program.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using Kettu; +using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Settings; using Microsoft.AspNetCore.Hosting; @@ -28,8 +29,13 @@ namespace LBPUnion.ProjectLighthouse Logger.AddLogger(new LighthouseFileLogger()); Logger.Log("Welcome to Project Lighthouse!", LoggerLevelStartup.Instance); + Logger.Log($"Running {ServerStatics.ServerName} {GitVersionHelper.CommitHash}@{GitVersionHelper.Branch}", LoggerLevelStartup.Instance); + + // This loads the config, see ServerSettings.cs for more information + Logger.Log("Loaded config file version " + ServerSettings.Instance.ConfigVersion, LoggerLevelStartup.Instance); + Logger.Log("Determining if the database is available...", LoggerLevelStartup.Instance); - bool dbConnected = ServerSettings.DbConnected; + bool dbConnected = ServerStatics.DbConnected; Logger.Log(dbConnected ? "Connected to the database." : "Database unavailable! Exiting.", LoggerLevelStartup.Instance); if (!dbConnected) Environment.Exit(1); @@ -38,6 +44,15 @@ namespace LBPUnion.ProjectLighthouse Logger.Log("Migrating database...", LoggerLevelDatabase.Instance); MigrateDatabase(database); + if (ServerSettings.Instance.InfluxEnabled) + { + Logger.Log("Influx logging is enabled. Starting influx logging...", LoggerLevelStartup.Instance); + #pragma warning disable CS4014 + InfluxHelper.StartLogging(); + #pragma warning restore CS4014 + if (ServerSettings.Instance.InfluxLoggingEnabled) Logger.AddLogger(new InfluxLogger()); + } + stopwatch.Stop(); Logger.Log($"Ready! Startup took {stopwatch.ElapsedMilliseconds}ms. Passing off control to ASP.NET...", LoggerLevelStartup.Instance); diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 51f3117d..b576a35c 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -9,6 +9,7 @@ + @@ -21,7 +22,19 @@ - + + + Always + + + + Always + + + + + + diff --git a/ProjectLighthouse/Types/Levels/Slot.cs b/ProjectLighthouse/Types/Levels/Slot.cs index dd117b12..42353c32 100644 --- a/ProjectLighthouse/Types/Levels/Slot.cs +++ b/ProjectLighthouse/Types/Levels/Slot.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Types.Profiles; using LBPUnion.ProjectLighthouse.Types.Reviews; @@ -206,7 +207,8 @@ namespace LBPUnion.ProjectLighthouse.Types.Levels public string SerializeResources() { - return this.Resources.Aggregate("", (current, resource) => current + LbpSerializer.StringElement("resource", resource)); + return this.Resources.Aggregate("", (current, resource) => current + LbpSerializer.StringElement("resource", resource)) + + LbpSerializer.StringElement("sizeOfResources", this.Resources.Sum(FileHelper.ResourceSize)); } public string Serialize(RatedLevel? yourRatingStats = null, VisitedLevel? yourVisitedStats = null, Review? yourReview = null) diff --git a/ProjectLighthouse/Types/Settings/ServerSettings.cs b/ProjectLighthouse/Types/Settings/ServerSettings.cs index daaa3e95..405e90db 100644 --- a/ProjectLighthouse/Types/Settings/ServerSettings.cs +++ b/ProjectLighthouse/Types/Settings/ServerSettings.cs @@ -1,44 +1,91 @@ -#nullable enable using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; using Kettu; using LBPUnion.ProjectLighthouse.Logging; namespace LBPUnion.ProjectLighthouse.Types.Settings { - public static class ServerSettings + [Serializable] + public class ServerSettings { - /// - /// The maximum amount of slots allowed on users' earth - /// - public const int EntitledSlots = 50; + static ServerSettings() + { + if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own - public const int ListsQuota = 50; + if (File.Exists(ConfigFileName)) + { + string configFile = File.ReadAllText(ConfigFileName); - public const string ServerName = "ProjectLighthouse"; + Instance = JsonSerializer.Deserialize(configFile) ?? throw new ArgumentNullException(nameof(ConfigFileName)); - private static string? dbConnectionString; + if (Instance.ConfigVersion >= CurrentConfigVersion) return; - public static string DbConnectionString { - get { - if (dbConnectionString == null) return dbConnectionString = Environment.GetEnvironmentVariable("LIGHTHOUSE_DB_CONNECTION_STRING") ?? ""; + Logger.Log($"Upgrading config file from version {Instance.ConfigVersion} to version {CurrentConfigVersion}", LoggerLevelConfig.Instance); + Instance.ConfigVersion = CurrentConfigVersion; + configFile = JsonSerializer.Serialize + ( + Instance, + typeof(ServerSettings), + new JsonSerializerOptions + { + WriteIndented = true, + } + ); - return dbConnectionString; + File.WriteAllText(ConfigFileName, configFile); } - set => dbConnectionString = value; - } + else + { + string configFile = JsonSerializer.Serialize + ( + new ServerSettings(), + typeof(ServerSettings), + new JsonSerializerOptions + { + WriteIndented = true, + } + ); - public static bool DbConnected { - get { - try - { - return new Database().Database.CanConnect(); - } - catch(Exception e) - { - Logger.Log(e.ToString(), LoggerLevelDatabase.Instance); - return false; - } + File.WriteAllText(ConfigFileName, configFile); + + Logger.Log + ( + "The configuration file was not found. " + + "A blank configuration file has been created for you at " + + $"{Path.Combine(Environment.CurrentDirectory, ConfigFileName)}", + LoggerLevelConfig.Instance + ); + + Environment.Exit(1); } } + + #region Meta + + [NotNull] + public static ServerSettings Instance; + + public const int CurrentConfigVersion = 4; + + [JsonPropertyName("ConfigVersionDoNotModifyOrYouWillBeSlapped")] + public int ConfigVersion { get; set; } = CurrentConfigVersion; + + public const string ConfigFileName = "lighthouse.config.json"; + + #endregion Meta + + public bool InfluxEnabled { get; set; } + public bool InfluxLoggingEnabled { get; set; } + public string InfluxOrg { get; set; } = "lighthouse"; + public string InfluxBucket { get; set; } = "lighthouse"; + public string InfluxToken { get; set; } = ""; + public string InfluxUrl { get; set; } = "http://localhost:8086"; + + public string EulaText { get; set; } = ""; + + public string DbConnectionString { get; set; } = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Settings/ServerStatics.cs b/ProjectLighthouse/Types/Settings/ServerStatics.cs new file mode 100644 index 00000000..a6b29548 --- /dev/null +++ b/ProjectLighthouse/Types/Settings/ServerStatics.cs @@ -0,0 +1,36 @@ +#nullable enable +using System; +using System.Linq; +using Kettu; +using LBPUnion.ProjectLighthouse.Logging; + +namespace LBPUnion.ProjectLighthouse.Types.Settings +{ + public static class ServerStatics + { + /// + /// The maximum amount of slots allowed on users' earth + /// + public const int EntitledSlots = 50; + + public const int ListsQuota = 50; + + public const string ServerName = "ProjectLighthouse"; + + public static bool DbConnected { + get { + try + { + return new Database().Database.CanConnect(); + } + catch(Exception e) + { + Logger.Log(e.ToString(), LoggerLevelDatabase.Instance); + return false; + } + } + } + + public static bool IsUnitTesting => AppDomain.CurrentDomain.GetAssemblies().Any(assembly => assembly.FullName.StartsWith("xunit")); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/User.cs b/ProjectLighthouse/Types/User.cs index 3e87be8f..9346fc87 100644 --- a/ProjectLighthouse/Types/User.cs +++ b/ProjectLighthouse/Types/User.cs @@ -105,7 +105,7 @@ namespace LBPUnion.ProjectLighthouse.Types LbpSerializer.StringElement("game", this.Game) + this.SerializeSlots(gameVersion == GameVersion.LittleBigPlanetVita) + LbpSerializer.StringElement("lists", this.Lists) + - LbpSerializer.StringElement("lists_quota", ServerSettings.ListsQuota) + // technically not a part of the user but LBP expects it + LbpSerializer.StringElement("lists_quota", ServerStatics.ListsQuota) + // technically not a part of the user but LBP expects it LbpSerializer.StringElement("biography", this.Biography) + LbpSerializer.StringElement("reviewCount", this.Reviews) + LbpSerializer.StringElement("commentCount", this.Comments) + @@ -147,7 +147,7 @@ namespace LBPUnion.ProjectLighthouse.Types /// /// The number of slots remaining on the earth /// - public int FreeSlots => ServerSettings.EntitledSlots - this.UsedSlots; + public int FreeSlots => ServerStatics.EntitledSlots - this.UsedSlots; private static readonly string[] slotTypes = { @@ -177,12 +177,12 @@ namespace LBPUnion.ProjectLighthouse.Types slotTypesLocal = slotTypes; } - slots += LbpSerializer.StringElement("entitledSlots", ServerSettings.EntitledSlots); + slots += LbpSerializer.StringElement("entitledSlots", ServerStatics.EntitledSlots); slots += LbpSerializer.StringElement("freeSlots", this.FreeSlots); foreach (string slotType in slotTypesLocal) { - slots += LbpSerializer.StringElement(slotType + "EntitledSlots", ServerSettings.EntitledSlots); + slots += LbpSerializer.StringElement(slotType + "EntitledSlots", ServerStatics.EntitledSlots); // ReSharper disable once StringLiteralTypo slots += LbpSerializer.StringElement(slotType + slotType == "crossControl" ? "PurchsedSlots" : "PurchasedSlots", 0); slots += LbpSerializer.StringElement(slotType + "FreeSlots", this.FreeSlots); diff --git a/README.md b/README.md index e45b3290..f3ff44b4 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Finally, take a break. Chances are that took a while. ## Contributing Tips -### Database +### Database migrations Some modifications may require updates to the database schema. You can automatically create a migration file by: @@ -72,6 +72,12 @@ Some modifications may require updates to the database schema. You can automatic you're doing. 4. Running `dotnet ef migrations add --project ProjectLighthouse`. +### Running tests + +You can run tests either through your IDE or by running `dotnet tests`. + +Keep in mind while running database tests you need to have `LIGHTHOUSE_DB_CONNECTION_STRING` set. + ## Compatibility across games and platforms | Game | Console (PS3/Vita) | Emulator (RPCS3) | Next-Gen (PS4/PS5) |