diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5477e569..87ba9197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,15 +58,10 @@ jobs: name: lighthouse-test-results-${{matrix.os.prettyName}} path: ${{github.workspace}}/TestResults-${{matrix.os.prettyName}}.trx - - name: Process Test Results (Control) - if: ${{ matrix.os.prettyName == 'Linux' }} - 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.1 + id: process-trx + if: ${{ always() }} + uses: im-open/process-dotnet-test-results@v2.0.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} create-status-check: false diff --git a/ProjectLighthouse.Tests/DatabaseFact.cs b/ProjectLighthouse.Tests/DatabaseFactAttribute.cs similarity index 61% rename from ProjectLighthouse.Tests/DatabaseFact.cs rename to ProjectLighthouse.Tests/DatabaseFactAttribute.cs index a62ecd5a..3089bdc8 100644 --- a/ProjectLighthouse.Tests/DatabaseFact.cs +++ b/ProjectLighthouse.Tests/DatabaseFactAttribute.cs @@ -4,9 +4,11 @@ using Xunit; namespace LBPUnion.ProjectLighthouse.Tests { - public sealed class DatabaseFact : FactAttribute + public sealed class DatabaseFactAttribute : FactAttribute { - public DatabaseFact() + private static readonly object migrateLock = new(); + + public DatabaseFactAttribute() { ServerSettings.Instance = new ServerSettings(); ServerSettings.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse"; @@ -16,8 +18,11 @@ namespace LBPUnion.ProjectLighthouse.Tests } else { - using Database database = new(); - database.Database.Migrate(); + lock(migrateLock) + { + using Database database = new(); + database.Database.Migrate(); + } } } } diff --git a/ProjectLighthouse.Tests/LighthouseTest.cs b/ProjectLighthouse.Tests/LighthouseTest.cs index 22babd5c..dd96dbf0 100644 --- a/ProjectLighthouse.Tests/LighthouseTest.cs +++ b/ProjectLighthouse.Tests/LighthouseTest.cs @@ -1,7 +1,7 @@ +using System; 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; @@ -25,9 +25,13 @@ namespace LBPUnion.ProjectLighthouse.Tests this.Client = this.Server.CreateClient(); } - - public async Task AuthenticateResponse(int number = 0, bool createUser = true) + public async Task AuthenticateResponse(int number = -1, bool createUser = true) { + if (number == -1) + { + number = new Random().Next(); + } + const string username = "unitTestUser"; if (createUser) { @@ -65,8 +69,8 @@ namespace LBPUnion.ProjectLighthouse.Tests public async Task UploadFileEndpointRequest(string filePath) { - byte[] bytes = Encoding.UTF8.GetBytes(await File.ReadAllTextAsync(filePath)); - string hash = HashHelper.Sha1Hash(bytes); + byte[] bytes = await File.ReadAllBytesAsync(filePath); + string hash = HashHelper.Sha1Hash(bytes).ToLower(); return await this.Client.PostAsync($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", new ByteArrayContent(bytes)); } diff --git a/ProjectLighthouse.Tests/Tests/UploadTests.cs b/ProjectLighthouse.Tests/Tests/UploadTests.cs index fb73be01..8c7f0bd5 100644 --- a/ProjectLighthouse.Tests/Tests/UploadTests.cs +++ b/ProjectLighthouse.Tests/Tests/UploadTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Xunit; @@ -18,6 +19,7 @@ namespace LBPUnion.ProjectLighthouse.Tests public async Task ShouldNotAcceptScript() { HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestScript.ff"); + Assert.False(response.StatusCode == HttpStatusCode.Forbidden); Assert.False(response.IsSuccessStatusCode); } @@ -25,6 +27,7 @@ namespace LBPUnion.ProjectLighthouse.Tests public async Task ShouldNotAcceptFarc() { HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestFarc.farc"); + Assert.False(response.StatusCode == HttpStatusCode.Forbidden); Assert.False(response.IsSuccessStatusCode); } @@ -32,6 +35,7 @@ namespace LBPUnion.ProjectLighthouse.Tests public async Task ShouldNotAcceptGarbage() { HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestGarbage.bin"); + Assert.False(response.StatusCode == HttpStatusCode.Forbidden); Assert.False(response.IsSuccessStatusCode); } @@ -39,6 +43,7 @@ namespace LBPUnion.ProjectLighthouse.Tests public async Task ShouldAcceptTexture() { HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestTexture.tex"); + Assert.False(response.StatusCode == HttpStatusCode.Forbidden); Assert.True(response.IsSuccessStatusCode); } @@ -46,6 +51,7 @@ namespace LBPUnion.ProjectLighthouse.Tests public async Task ShouldAcceptLevel() { HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestLevel.lvl"); + Assert.False(response.StatusCode == HttpStatusCode.Forbidden); Assert.True(response.IsSuccessStatusCode); } } diff --git a/ProjectLighthouse/Controllers/LoginController.cs b/ProjectLighthouse/Controllers/LoginController.cs index a8126a94..02ed9740 100644 --- a/ProjectLighthouse/Controllers/LoginController.cs +++ b/ProjectLighthouse/Controllers/LoginController.cs @@ -43,41 +43,59 @@ namespace LBPUnion.ProjectLighthouse.Controllers } if (loginData == null) return this.BadRequest(); - IPAddress? ipAddress = this.HttpContext.Connection.RemoteIpAddress; - if (ipAddress == null) return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever + IPAddress? remoteIpAddress = this.HttpContext.Connection.RemoteIpAddress; + if (remoteIpAddress == null) return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever - string userLocation = ipAddress.ToString(); + string ipAddress = remoteIpAddress.ToString(); - GameToken? token = await this.database.AuthenticateUser(loginData, userLocation, titleId); - if (token == null) return this.StatusCode(403, ""); + // Get an existing token from the IP & username + GameToken? token = await this.database.GameTokens.Include + (t => t.User) + .FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == loginData.Username && !t.Used); + + if (token == null) // If we cant find an existing token, try to generate a new one + { + token = await this.database.AuthenticateUser(loginData, ipAddress, titleId); + if (token == null) return this.StatusCode(403, ""); // If not, then 403. + } User? user = await this.database.UserFromGameToken(token, true); if (user == null) return this.StatusCode(403, ""); if (ServerSettings.Instance.UseExternalAuth) { - string ipAddressAndName = $"{token.UserLocation}|{user.Username}"; - if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3) + if (ServerSettings.Instance.BlockDeniedUsers) { - this.database.AuthenticationAttempts.RemoveRange - (this.database.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId)); + string ipAddressAndName = $"{token.UserLocation}|{user.Username}"; + if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3) + { + this.database.AuthenticationAttempts.RemoveRange + (this.database.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId)); - DeniedAuthenticationHelper.AddAttempt(ipAddressAndName); + DeniedAuthenticationHelper.AddAttempt(ipAddressAndName); - await this.database.SaveChangesAsync(); - return this.StatusCode(403, ""); + await this.database.SaveChangesAsync(); + return this.StatusCode(403, ""); + } } - AuthenticationAttempt authAttempt = new() + if (this.database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).Select(a => a.IpAddress).Contains(ipAddress)) { - GameToken = token, - GameTokenId = token.TokenId, - Timestamp = TimestampHelper.Timestamp, - IPAddress = userLocation, - Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3 - }; + token.Approved = true; + } + else + { + AuthenticationAttempt authAttempt = new() + { + GameToken = token, + GameTokenId = token.TokenId, + Timestamp = TimestampHelper.Timestamp, + IPAddress = ipAddress, + Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3 + }; - this.database.AuthenticationAttempts.Add(authAttempt); + this.database.AuthenticationAttempts.Add(authAttempt); + } } else { @@ -86,9 +104,18 @@ namespace LBPUnion.ProjectLighthouse.Controllers await this.database.SaveChangesAsync(); - Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance); + if (!token.Approved) return this.StatusCode(403, ""); - // Create a new room on LBP2+/Vita + Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance); + // After this point we are now considering this session as logged in. + + // We just logged in with the token. Mark it as used so someone else doesnt try to use it, + // and so we don't pick the same token up when logging in later. + token.Used = true; + + await this.database.SaveChangesAsync(); + + // Create a new room on LBP2/3/Vita if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user); return this.Ok diff --git a/ProjectLighthouse/Controllers/MessageController.cs b/ProjectLighthouse/Controllers/MessageController.cs index 80851bbe..c16f6921 100644 --- a/ProjectLighthouse/Controllers/MessageController.cs +++ b/ProjectLighthouse/Controllers/MessageController.cs @@ -1,3 +1,4 @@ +#nullable enable using System.IO; using System.Threading.Tasks; using Kettu; @@ -22,25 +23,44 @@ namespace LBPUnion.ProjectLighthouse.Controllers } [HttpGet("eula")] - public IActionResult Eula() => this.Ok(ServerSettings.Instance.EulaText + "\n" + $"{EulaHelper.License}\n"); + public async Task Eula() + { + User? user = await this.database.UserFromGameRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + return this.Ok(ServerSettings.Instance.EulaText + "\n" + $"{EulaHelper.License}\n"); + } [HttpGet("announce")] public async Task Announce() { - User user = await this.database.UserFromGameRequest(this.Request, true); + #if !DEBUG + User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); + #else + (User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request); - if (ServerSettings.Instance.UseExternalAuth) - return this.Ok - ( - "Please stay on this screen.\n" + - $"Before continuing, you must approve this session at {ServerSettings.Instance.ExternalUrl}.\n" + - "Please keep in mind that if the session is denied you may have to wait up to 5-10 minutes to try logging in again.\n" + - "Once approved, you may press X and continue.\n\n" + - ServerSettings.Instance.EulaText - ); + if (userAndToken == null) return this.StatusCode(403, ""); - return this.Ok($"You are now logged in as {user.Username} (id: {user.UserId}).\n\n" + ServerSettings.Instance.EulaText); + // ReSharper disable once PossibleInvalidOperationException + User user = userAndToken.Value.Item1; + GameToken gameToken = userAndToken.Value.Item2; + #endif + + return this.Ok + ( + $"You are now logged in as {user.Username}.\n\n" + + #if DEBUG + "---DEBUG INFO---\n" + + $"user.UserId: {user.UserId}\n" + + $"token.Approved: {gameToken.Approved}\n" + + $"token.Used: {gameToken.Used}\n" + + $"token.UserLocation: {gameToken.UserLocation}\n" + + $"token.GameVersion: {gameToken.GameVersion}\n" + + "---DEBUG INFO---\n\n" + + #endif + ServerSettings.Instance.EulaText + ); } [HttpGet("notification")] @@ -52,7 +72,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers [HttpPost("filter")] public async Task Filter() { - User user = await this.database.UserFromGameRequest(this.Request); + User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); string loggedText = await new StreamReader(this.Request.Body).ReadToEndAsync(); diff --git a/ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs b/ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs new file mode 100644 index 00000000..f801b055 --- /dev/null +++ b/ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs @@ -0,0 +1,67 @@ +#nullable enable +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Controllers.Website.ExternalAuth +{ + [ApiController] + [Route("/authentication")] + public class AutoApprovalController : ControllerBase + { + private readonly Database database; + + public AutoApprovalController(Database database) + { + this.database = database; + } + + [HttpGet("autoApprove/{id:int}")] + public async Task AutoApprove([FromRoute] int id) + { + User? user = this.database.UserFromWebRequest(this.Request); + if (user == null) return this.Redirect("/login"); + + AuthenticationAttempt? authAttempt = await this.database.AuthenticationAttempts.Include + (a => a.GameToken) + .FirstOrDefaultAsync(a => a.AuthenticationAttemptId == id); + + if (authAttempt == null) return this.BadRequest(); + if (authAttempt.GameToken.UserId != user.UserId) return this.Redirect("/login"); + + authAttempt.GameToken.Approved = true; + + UserApprovedIpAddress approvedIpAddress = new() + { + UserId = user.UserId, + User = user, + IpAddress = authAttempt.IPAddress, + }; + + this.database.UserApprovedIpAddresses.Add(approvedIpAddress); + this.database.AuthenticationAttempts.Remove(authAttempt); + + await this.database.SaveChangesAsync(); + + return this.Redirect("/authentication"); + } + + [HttpGet("revokeAutoApproval/{id:int}")] + public async Task RevokeAutoApproval([FromRoute] int id) + { + User? user = this.database.UserFromWebRequest(this.Request); + if (user == null) return this.Redirect("/login"); + + UserApprovedIpAddress? approvedIpAddress = await this.database.UserApprovedIpAddresses.FirstOrDefaultAsync(a => a.UserApprovedIpAddressId == id); + if (approvedIpAddress == null) return this.BadRequest(); + if (approvedIpAddress.UserId != user.UserId) return this.Redirect("/login"); + + this.database.UserApprovedIpAddresses.Remove(approvedIpAddress); + + await this.database.SaveChangesAsync(); + + return this.Redirect("/authentication/autoApprovals"); + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 2af08e30..e28957ba 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -34,6 +34,7 @@ namespace LBPUnion.ProjectLighthouse public DbSet AuthenticationAttempts { get; set; } public DbSet Reviews { get; set; } public DbSet RatedReviews { get; set; } + public DbSet UserApprovedIpAddresses { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); @@ -66,13 +67,13 @@ namespace LBPUnion.ProjectLighthouse #nullable enable public async Task AuthenticateUser(LoginData loginData, string userLocation, string titleId = "") { - // TODO: don't use psn name to authenticate User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username); if (user == null) return null; GameToken gameToken = new() { UserToken = HashHelper.GenerateAuthToken(), + User = user, UserId = user.UserId, UserLocation = userLocation, GameVersion = GameVersionHelper.FromTitleId(titleId), diff --git a/ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs b/ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs new file mode 100644 index 00000000..4f9264df --- /dev/null +++ b/ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs @@ -0,0 +1,50 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20211213195540_AddUserApprovedIpAddresses")] + public partial class AddUserApprovedIpAddresses : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserApprovedIpAddresses", + columns: table => new + { + UserApprovedIpAddressId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + IpAddress = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserApprovedIpAddresses", x => x.UserApprovedIpAddressId); + table.ForeignKey( + name: "FK_UserApprovedIpAddresses_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_UserApprovedIpAddresses_UserId", + table: "UserApprovedIpAddresses", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserApprovedIpAddresses"); + } + } +} diff --git a/ProjectLighthouse/Migrations/20211214005427_AddUsedBoolToGameToken.cs b/ProjectLighthouse/Migrations/20211214005427_AddUsedBoolToGameToken.cs new file mode 100644 index 00000000..359a10ef --- /dev/null +++ b/ProjectLighthouse/Migrations/20211214005427_AddUsedBoolToGameToken.cs @@ -0,0 +1,55 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20211214005427_AddUsedBoolToGameToken")] + public partial class AddUsedBoolToGameToken : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // Incompatible with old tokens + migrationBuilder.Sql("DELETE FROM AuthenticationAttempts"); + migrationBuilder.Sql("DELETE FROM GameTokens"); + + migrationBuilder.AddColumn( + name: "Used", + table: "GameTokens", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateIndex( + name: "IX_GameTokens_UserId", + table: "GameTokens", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_GameTokens_Users_UserId", + table: "GameTokens", + column: "UserId", + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_GameTokens_Users_UserId", + table: "GameTokens"); + + migrationBuilder.DropIndex( + name: "IX_GameTokens_UserId", + table: "GameTokens"); + + migrationBuilder.DropColumn( + name: "Used", + table: "GameTokens"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 4afc7b51..cdb0c96a 100644 --- a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -55,6 +55,9 @@ namespace ProjectLighthouse.Migrations b.Property("GameVersion") .HasColumnType("int"); + b.Property("Used") + .HasColumnType("tinyint(1)"); + b.Property("UserId") .HasColumnType("int"); @@ -66,6 +69,8 @@ namespace ProjectLighthouse.Migrations b.HasKey("TokenId"); + b.HasIndex("UserId"); + b.ToTable("GameTokens"); }); @@ -587,6 +592,25 @@ namespace ProjectLighthouse.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.UserApprovedIpAddress", b => + { + b.Property("UserApprovedIpAddressId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IpAddress") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("UserApprovedIpAddressId"); + + b.HasIndex("UserId"); + + b.ToTable("UserApprovedIpAddresses"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.WebToken", b => { b.Property("TokenId") @@ -615,6 +639,17 @@ namespace ProjectLighthouse.Migrations b.Navigation("GameToken"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.GameToken", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.HeartedProfile", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "HeartedUser") @@ -829,6 +864,17 @@ namespace ProjectLighthouse.Migrations b.Navigation("Location"); }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.UserApprovedIpAddress", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); #pragma warning restore 612, 618 } } diff --git a/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml b/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml index 58521e46..d5a56773 100644 --- a/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml +++ b/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml @@ -4,8 +4,8 @@ @{ Layout = "Layouts/BaseLayout"; + Model.Title = "Authentication"; } -

Authentication

@if (Model.AuthenticationAttempts.Count == 0) { @@ -14,22 +14,48 @@ else {

You have @Model.AuthenticationAttempts.Count authentication attempts pending.

- - - + @if (Model.IpAddress != null) + { +

This device's IP address is @(Model.IpAddress.ToString()). If this matches with an authentication attempt below, then it's safe to assume the authentication attempt came from the same network as this device.

+ } } + + + + + + + @foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts) { DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp);

A @authAttempt.Platform authentication request was logged at @timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC from the IP address @authAttempt.IPAddress.

diff --git a/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml.cs b/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml.cs index 13a1f5eb..6ca2576a 100644 --- a/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml.cs +++ b/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml.cs @@ -1,5 +1,7 @@ +#nullable enable using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types; @@ -13,12 +15,17 @@ namespace LBPUnion.ProjectLighthouse.Pages.ExternalAuth { public List AuthenticationAttempts; + + public IPAddress? IpAddress; public AuthenticationPage(Database database) : base(database) {} public async Task OnGet() { if (!ServerSettings.Instance.UseExternalAuth) return this.NotFound(); + if (this.User == null) return this.StatusCode(403, ""); + + this.IpAddress = this.HttpContext.Connection.RemoteIpAddress; this.AuthenticationAttempts = this.Database.AuthenticationAttempts.Include (a => a.GameToken) diff --git a/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml new file mode 100644 index 00000000..dde2fa8a --- /dev/null +++ b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml @@ -0,0 +1,22 @@ +@page "/authentication/autoApprovals" +@using LBPUnion.ProjectLighthouse.Types +@model LBPUnion.ProjectLighthouse.Pages.ExternalAuth.ManageUserApprovedIpAddressesPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = "Automatically approved IP addresses"; +} + + +@foreach (UserApprovedIpAddress approvedIpAddress in Model.ApprovedIpAddresses) +{ +
+

@approvedIpAddress.IpAddress

+ + + +
+} \ No newline at end of file diff --git a/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs new file mode 100644 index 00000000..00dfa3be --- /dev/null +++ b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Pages.ExternalAuth +{ + public class ManageUserApprovedIpAddressesPage : BaseLayout + { + public ManageUserApprovedIpAddressesPage(Database database) : base(database) + {} + + public List ApprovedIpAddresses; + + public async Task OnGet() + { + User? user = this.Database.UserFromWebRequest(this.Request); + if (user == null) return this.Redirect("/login"); + + this.ApprovedIpAddresses = await this.Database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).ToListAsync(); + + return this.Page(); + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Pages/LandingPage.cshtml b/ProjectLighthouse/Pages/LandingPage.cshtml index fc67aaae..3a977b88 100644 --- a/ProjectLighthouse/Pages/LandingPage.cshtml +++ b/ProjectLighthouse/Pages/LandingPage.cshtml @@ -1,5 +1,6 @@ @page "/" @using LBPUnion.ProjectLighthouse.Types +@using LBPUnion.ProjectLighthouse.Types.Settings @model LBPUnion.ProjectLighthouse.Pages.LandingPage @{ @@ -11,6 +12,12 @@ @if (Model.User != null) {

You are currently logged in as @Model.User.Username.

+ if (ServerSettings.Instance.UseExternalAuth && Model.AuthenticationAttemptsCount > 0) + { +

+ You have @Model.AuthenticationAttemptsCount authentication attempts pending. Click here to view them. +

+ } } @if (Model.PlayersOnlineCount == 1) @@ -21,6 +28,7 @@ @user.Username } } + else if (Model.PlayersOnlineCount == 0) {

There are no users online. Why not hop on?

diff --git a/ProjectLighthouse/Pages/LandingPage.cshtml.cs b/ProjectLighthouse/Pages/LandingPage.cshtml.cs index 23289463..de398d10 100644 --- a/ProjectLighthouse/Pages/LandingPage.cshtml.cs +++ b/ProjectLighthouse/Pages/LandingPage.cshtml.cs @@ -16,6 +16,8 @@ namespace LBPUnion.ProjectLighthouse.Pages public List PlayersOnline; public int PlayersOnlineCount; + + public int AuthenticationAttemptsCount; public LandingPage(Database database) : base(database) {} @@ -27,6 +29,13 @@ namespace LBPUnion.ProjectLighthouse.Pages this.PlayersOnlineCount = await StatisticsHelper.RecentMatches(); + if (user != null) + { + this.AuthenticationAttemptsCount = await this.Database.AuthenticationAttempts.Include + (a => a.GameToken) + .CountAsync(a => a.GameToken.UserId == user.UserId); + } + List userIds = await this.Database.LastContacts.Where(l => TimestampHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync(); this.PlayersOnline = await this.Database.Users.Where(u => userIds.Contains(u.UserId)).ToListAsync(); diff --git a/ProjectLighthouse/Pages/Layouts/BaseLayout.cshtml b/ProjectLighthouse/Pages/Layouts/BaseLayout.cshtml index 2295cea3..d4e14ade 100644 --- a/ProjectLighthouse/Pages/Layouts/BaseLayout.cshtml +++ b/ProjectLighthouse/Pages/Layouts/BaseLayout.cshtml @@ -64,6 +64,17 @@ gtag('config', '@ServerSettings.Instance.GoogleAnalyticsId'); } + +
diff --git a/ProjectLighthouse/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse/Pages/Partials/PhotoPartial.cshtml index 33a8fbc1..2b422c5a 100644 --- a/ProjectLighthouse/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse/Pages/Partials/PhotoPartial.cshtml @@ -1,7 +1,14 @@ @using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.Types.Photo - + +
+ + + +

@@ -16,7 +23,104 @@

Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"):

-@foreach (PhotoSubject subject in Model.Subjects) -{ - @subject.User.Username -} \ No newline at end of file +
+ @foreach (PhotoSubject subject in Model.Subjects) + { + @subject.User.Username + } +
+ +@{ + PhotoSubject[] subjects = Model.Subjects.ToArray(); + foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username; +} + + \ No newline at end of file diff --git a/ProjectLighthouse/Pages/UserPage.cshtml b/ProjectLighthouse/Pages/UserPage.cshtml index 128a7883..2ad6916e 100644 --- a/ProjectLighthouse/Pages/UserPage.cshtml +++ b/ProjectLighthouse/Pages/UserPage.cshtml @@ -1,4 +1,6 @@ @page "/user/{userId:int}" +@using System.IO +@using System.Web @using LBPUnion.ProjectLighthouse.Types @using LBPUnion.ProjectLighthouse.Types.Profiles @using LBPUnion.ProjectLighthouse.Types.Settings @@ -74,7 +76,7 @@ @foreach (Photo photo in Model.Photos) {
- @await Html.PartialAsync("Partials/PhotoPartial", photo); + @await Html.PartialAsync("Partials/PhotoPartial", photo)
}
@@ -92,9 +94,12 @@ @foreach (Comment comment in Model.Comments!) { DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000); + StringWriter messageWriter = new StringWriter(); + HttpUtility.HtmlDecode(comment.Message, messageWriter); + String decodedMessage = messageWriter.ToString();
@comment.Poster.Username: - @comment.Message + @decodedMessage

@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC

diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index 0fef627e..ffc2f979 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -33,10 +33,6 @@ - - <_ContentIncludedByDefault Remove="Pages\Admin\Index.cshtml"/> - - diff --git a/ProjectLighthouse/Types/GameToken.cs b/ProjectLighthouse/Types/GameToken.cs index 4fdca27d..74ea937d 100644 --- a/ProjectLighthouse/Types/GameToken.cs +++ b/ProjectLighthouse/Types/GameToken.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace LBPUnion.ProjectLighthouse.Types { @@ -10,12 +11,19 @@ namespace LBPUnion.ProjectLighthouse.Types public int UserId { get; set; } + [ForeignKey(nameof(UserId))] + public User User { get; set; } + public string UserToken { get; set; } public string UserLocation { get; set; } public GameVersion GameVersion { get; set; } - public bool Approved { get; set; } = false; + // Set by /authentication webpage + public bool Approved { get; set; } + + // Set to true on login + public bool Used { get; set; } } } \ No newline at end of file diff --git a/ProjectLighthouse/Types/PhotoSubject.cs b/ProjectLighthouse/Types/PhotoSubject.cs index 99be0fb7..05144f33 100644 --- a/ProjectLighthouse/Types/PhotoSubject.cs +++ b/ProjectLighthouse/Types/PhotoSubject.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Serialization; @@ -20,6 +21,7 @@ namespace LBPUnion.ProjectLighthouse.Types [XmlIgnore] [ForeignKey(nameof(UserId))] + [JsonIgnore] public User User { get; set; } [NotMapped] diff --git a/ProjectLighthouse/Types/Settings/ServerSettings.cs b/ProjectLighthouse/Types/Settings/ServerSettings.cs index ef1943e3..bdbb7676 100644 --- a/ProjectLighthouse/Types/Settings/ServerSettings.cs +++ b/ProjectLighthouse/Types/Settings/ServerSettings.cs @@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings public class ServerSettings { - public const int CurrentConfigVersion = 12; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! + public const int CurrentConfigVersion = 13; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! static ServerSettings() { if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own @@ -97,6 +97,8 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings public string GoogleAnalyticsId { get; set; } = ""; + public bool BlockDeniedUsers = true; + #region Meta [NotNull] diff --git a/ProjectLighthouse/Types/UserApprovedIpAddress.cs b/ProjectLighthouse/Types/UserApprovedIpAddress.cs new file mode 100644 index 00000000..06c84183 --- /dev/null +++ b/ProjectLighthouse/Types/UserApprovedIpAddress.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LBPUnion.ProjectLighthouse.Types +{ + public class UserApprovedIpAddress + { + [Key] + public int UserApprovedIpAddressId { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + public string IpAddress { get; set; } + } +} \ No newline at end of file diff --git a/README.md b/README.md index e68ac31e..9ece56f8 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,18 @@ This project is the main server component that LittleBigPlanet games connect to. ## WARNING! -This is beta software, and thus is not ready for public use yet. We're not responsible if someone connects and hacks -your entire machine and deletes all your files. +This is **beta software**, and thus is **not stable**. -That said, feel free to develop privately! +We're not responsible if someone hacks your machine and wipes your database. + +Make frequent backups, and be sure to report any vulnerabilities. ## Building This will be written when we're out of beta. Consider this your barrier to entry ;). +It is recommended to build with Release if you plan to use Lighthouse in a production environment. + ## Running Lighthouse requires a MySQL database at this time. For Linux users running docker, one can be set up using @@ -31,18 +34,21 @@ Once you've gotten MySQL running you can run Lighthouse. It will take care of th PS3 is difficult to set up, so I will be going over how to set up RPCS3 instead. A guide will be coming for PS3 closer to release. You can also follow this guide if you want to learn how to modify your EBOOT. -*Note: This requires a modified copy of RPCS3. You can find a working -patch [here](https://gist.github.com/jvyden/0d9619f7dd3dbc49f7583486bdacad75).* +There are also community-provided guides in [the official LBP Union Discord](https://www.lbpunion.com/discord), which +you can follow at your own discretion. -Start by getting a copy of LittleBigPlanet 2 installed. It can be digital (NPUA80662) or disc (BCUS98245). I won't get -into how because if you got this far you should already know what you're doing. For those that don't, -the [RPCS3 Quickstart Guide](https://rpcs3.net/quickstart) should cover it. +*Note: This requires a modified copy of RPCS3. You can find a working +version [on our GitHub](https://github.com/LBPUnion/rpcs3).* + +Start by getting a copy of LittleBigPlanet 2 installed. It can be digital (NPUA80662) or disc (BCUS98245). For those +that don't, the [RPCS3 Quickstart Guide](https://rpcs3.net/quickstart) should cover it. Next, download [UnionPatcher](https://github.com/LBPUnion/UnionPatcher/). Binaries can be found by reading the README.md file. You should have everything you need now, so open up RPCS3 and go to Utilities -> Decrypt PS3 Binaries. Point this -to `rpcs3/dev_hdd0/game/(title id)/USRDIR/EBOOT.BIN`. +to `rpcs3/dev_hdd0/game/(title id)/USRDIR/EBOOT.BIN`. You can grab your title id by right clicking the game in RPCS3 and +clicking Copy Info -> Copy Serial. This should give you a file named `EBOOT.elf` in the same folder. Next, fire up UnionPatcher (making sure to select the correct project to start, e.g. on Mac launch `UnionPatcher.Gui.MacOS`.) @@ -68,25 +74,35 @@ Some modifications may require updates to the database schema. You can automatic 1. Making sure the tools are installed. You can do this by running `dotnet tool restore`. 2. Making sure `LIGHTHOUSE_DB_CONNECTION_STRING` is set correctly. See the `Running` section for more details. -3. Making your changes to the database. I won't cover this since if you're making database changes you should know what - you're doing. +3. Modifying the database schema via the C# portion of the code. Do not modify the actual SQL database. 4. Running `dotnet ef migrations add --project ProjectLighthouse`. +This process will create a migration file from the changes made in the C# code. + +The new migrations will automatically be applied upon starting Lighthouse. + ### 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. +### Continuous Integration (CI) Tips + +- You can skip CI runs for a commit if you specify `[skip ci]` at the beginning of the commit name. This is useful for + formatting changes, etc. +- When creating your first pull request, CI will not run initially. A team member will have to approve you for use of + running CI on a pull request. This is because of GitHub policy. + ## Compatibility across games and platforms -| Game | Console (PS3/Vita/PSP) | Emulator (RPCS3/Vita3k/PPSSPP) | Next-Gen (PS4/PS5) | -|----------|---------------------------------------|----------------------------------------------------------|------------------------| -| LBP1 | Compatible | Incompatible, crashes on entering pod computer | N/A | -| LBP2 | Compatible | Compatible with patched RPCS3 | N/A | -| LBP3 | Somewhat compatible, frequent crashes | Somewhat compatible with patched RPCS3, frequent crashes | Incompatible | -| LBP Vita | Compatible | Incompatible, marked as "bootable" on Vita3k | N/A | -| LBP PSP | Potentially compatible | Incompatible, PSN not supported on PPSSPP | Potentially Compatible | +| Game | Console (PS3/Vita/PSP) | Emulator (RPCS3/Vita3k/PPSSPP) | Next-Gen (PS4/PS5/Vita) | +|----------|---------------------------------------|----------------------------------------------------------|-------------------------| +| LBP1 | Compatible | Incompatible, crashes on entering pod computer | N/A | +| LBP2 | Compatible | Compatible with patched RPCS3 | N/A | +| LBP3 | Somewhat compatible, frequent crashes | Somewhat compatible with patched RPCS3, frequent crashes | Incompatible | +| LBP Vita | Compatible | Incompatible, marked as "bootable" on Vita3k | N/A | +| LBP PSP | Potentially compatible | Incompatible, PSN not supported on PPSSPP | Potentially Compatible | While LBP Vita and LBP PSP can be supported, they are not properly seperated from the mainline games at this time. We recommend you run seperate instances for these games to avoid problems.