Merge branch 'main' into more-slot-categories

This commit is contained in:
jvyden 2021-12-22 17:28:36 -05:00
commit 71fa2c1617
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
26 changed files with 629 additions and 90 deletions

View file

@ -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

View file

@ -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();
}
}
}
}

View file

@ -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<HttpResponseMessage> AuthenticateResponse(int number = 0, bool createUser = true)
public async Task<HttpResponseMessage> 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<HttpResponseMessage> 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));
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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();

View file

@ -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<IActionResult> 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<IActionResult> 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");
}
}
}

View file

@ -34,6 +34,7 @@ namespace LBPUnion.ProjectLighthouse
public DbSet<AuthenticationAttempt> AuthenticationAttempts { get; set; }
public DbSet<Review> Reviews { get; set; }
public DbSet<RatedReview> RatedReviews { get; set; }
public DbSet<UserApprovedIpAddress> 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<GameToken?> 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),

View file

@ -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<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<int>(type: "int", nullable: false),
IpAddress = table.Column<string>(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");
}
}
}

View file

@ -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<bool>(
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");
}
}
}

View file

@ -55,6 +55,9 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("GameVersion")
.HasColumnType("int");
b.Property<bool>("Used")
.HasColumnType("tinyint(1)");
b.Property<int>("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<int>("UserApprovedIpAddressId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("IpAddress")
.HasColumnType("longtext");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("UserApprovedIpAddressId");
b.HasIndex("UserId");
b.ToTable("UserApprovedIpAddresses");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.WebToken", b =>
{
b.Property<int>("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
}
}

View file

@ -4,8 +4,8 @@
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Authentication";
}
<h1>Authentication</h1>
@if (Model.AuthenticationAttempts.Count == 0)
{
@ -14,22 +14,48 @@
else
{
<p>You have @Model.AuthenticationAttempts.Count authentication attempts pending.</p>
<a href="/authentication/denyAll">
<button class="ui small red button">Deny all</button>
</a>
@if (Model.IpAddress != null)
{
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. 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.</p>
}
}
<a href="/authentication/autoApprovals">
<button class="ui small blue button">
<i class="cog icon"></i>
<span>Manage automatically approved IP addresses</span>
</button>
</a>
<a href="/authentication/denyAll">
<button class="ui small red button">
<i class="x icon"></i>
<span>Deny all</span>
</button>
</a>
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
{
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp);
<div class="ui red segment">
<p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
<div>
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny green button">
<i class="check icon"></i>
<span>Automatically approve every time</span>
</button>
</a>
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny green button">Approve</button>
<button class="ui tiny yellow button">
<i class="check icon"></i>
<span>Approve this time</span>
</button>
</a>
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny red button">Deny</button>
<button class="ui tiny red button">
<i class="x icon"></i>
<span>Deny</span>
</button>
</a>
</div>
</div>

View file

@ -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<AuthenticationAttempt> AuthenticationAttempts;
public IPAddress? IpAddress;
public AuthenticationPage(Database database) : base(database)
{}
public async Task<IActionResult> 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)

View file

@ -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)
{
<div class="ui blue segment">
<p>@approvedIpAddress.IpAddress</p>
<a href="/authentication/revokeAutoApproval/@approvedIpAddress.UserApprovedIpAddressId">
<button class="ui red button">
<i class="trash icon"></i>
<span>Revoke</span>
</button>
</a>
</div>
}

View file

@ -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<UserApprovedIpAddress> ApprovedIpAddresses;
public async Task<IActionResult> 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();
}
}
}

View file

@ -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)
{
<p>You are currently logged in as <b>@Model.User.Username</b>.</p>
if (ServerSettings.Instance.UseExternalAuth && Model.AuthenticationAttemptsCount > 0)
{
<p>
<b>You have @Model.AuthenticationAttemptsCount authentication attempts pending. Click <a href="/authentication">here</a> to view them.</b>
</p>
}
}
@if (Model.PlayersOnlineCount == 1)
@ -21,6 +28,7 @@
<a href="/user/@user.UserId">@user.Username</a>
}
}
else if (Model.PlayersOnlineCount == 0)
{
<p>There are no users online. Why not hop on?</p>

View file

@ -16,6 +16,8 @@ namespace LBPUnion.ProjectLighthouse.Pages
public List<User> 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<int> 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();

View file

@ -64,6 +64,17 @@
gtag('config', '@ServerSettings.Instance.GoogleAnalyticsId');
</script>
}
<style>
canvas.photo-subjects {
opacity: 1;
transition: opacity 0.3s;
}
canvas.hide-subjects {
opacity: 0;
}
</style>
</head>
<body>
<div class="pageContainer">

View file

@ -1,7 +1,14 @@
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Types.Photo
<img src="/gameAssets/@Model.LargeHash" style="width: 100%; height: auto; border-radius: .28571429rem;">
<div style="position: relative">
<canvas class="hide-subjects" id="canvas-subjects-@Model.PhotoId" width="1920" height="1080"
style="position: absolute; transform: rotate(180deg)">
</canvas>
<img id="game-image-@Model.PhotoId" src="/gameAssets/@Model.LargeHash"
style="width: 100%; height: auto; border-radius: .28571429rem;">
</div>
<br>
<p>
@ -16,7 +23,104 @@
<p>
<b>Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"):</b>
</p>
@foreach (PhotoSubject subject in Model.Subjects)
{
<a href="/user/@subject.UserId">@subject.User.Username</a>
}
<div id="hover-subjects-@Model.PhotoId">
@foreach (PhotoSubject subject in Model.Subjects)
{
<a href="/user/@subject.UserId">@subject.User.Username</a>
}
</div>
@{
PhotoSubject[] subjects = Model.Subjects.ToArray();
foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username;
}
<script>
// render the page first so that image heights have been calculated
window.addEventListener("load", function () {
const canvas = document.getElementById("canvas-subjects-@Model.PhotoId");
const hoverer = document.getElementById("hover-subjects-@Model.PhotoId");
const image = document.getElementById("game-image-@Model.PhotoId");
hoverer.addEventListener('mouseenter', function () {
canvas.className = "photo-subjects";
});
hoverer.addEventListener('mouseleave', function () {
canvas.className = "photo-subjects hide-subjects";
});
const context = canvas.getContext('2d');
const subjects = @Html.Raw(Json.Serialize(subjects.ToArray()));
canvas.width = image.offsetWidth;
canvas.height = image.clientHeight; // space for names to hang off
const w = canvas.width;
const h = canvas.height;
// halfwidth, halfheight
const hw = w / 2;
const hh = h / 2;
const colours = ["#96dd3c", "#ceb424", "#cc0a1d", "#c800cc"];
subjects.forEach((s, si) => {
const colour = colours[si % 4];
// Bounding box
const bounds = s.bounds.split(",").map(parseFloat);
const [x1, y1, x2, y2] = bounds.map(n => Math.min(Math.max(n, -1), 1));
const bx = hw - (x2 * hw);
const by = hh - (y2 * hh);
const bw = (x2 - x1) * hw;
const bh = (y2 - y1) * hh;
context.beginPath();
context.lineWidth = 3;
context.strokeStyle = colour;
context.rect(bx, by, bw, bh);
context.stroke();
// Move into relative coordinates from bounding box
context.translate(bx, by);
// Username label
context.font = "16px Lato";
context.fillStyle = colour;
// Text width/height for the label background
const tw = context.measureText(s.username).width;
const th = 24;
// Check if the label will flow off the bottom of the frame
const overflowBottom = (bounds[3] * hh) > (hh - 24);
// Check if the label will flow off the left of the frame
const overflowLeft = (bounds[2] * hw - tw) < (-hw);
// Set alignment
context.textAlign = overflowLeft ? "start" : "end";
// Text x / y
const lx = overflowLeft ? -bw + 6 : -6;
const ly = overflowBottom ? -bh - 6 : 16;
// Label background x / y
const lbx = overflowLeft ? bw - tw - 12 : -2;
const lby = overflowBottom ? bh : -24;
// Draw background
context.fillRect(lbx, lby, tw + 16, th);
// Draw text, rotated back upright (canvas draws rotated 180deg)
context.fillStyle = "white";
context.rotate(Math.PI);
context.fillText(s.username, lx, ly);
// reset transform
context.setTransform(1, 0, 0, 1, 0, 0);
})
}, false);
</script>

View file

@ -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)
{
<div class="eight wide column">
@await Html.PartialAsync("Partials/PhotoPartial", photo);
@await Html.PartialAsync("Partials/PhotoPartial", photo)
</div>
}
</div>
@ -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();
<div>
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
<span>@comment.Message</span>
<span>@decodedMessage</span>
<p>
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
</p>

View file

@ -33,10 +33,6 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Pages\Admin\Index.cshtml"/>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="git describe --long --always --dirty --exclude=\* --abbrev=8 &gt; &quot;$(ProjectDir)/gitVersion.txt&quot;"/>
<Exec Command="git branch --show-current &gt; &quot;$(ProjectDir)/gitBranch.txt&quot;"/>

View file

@ -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; }
}
}

View file

@ -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]

View file

@ -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]

View file

@ -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; }
}
}

View file

@ -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 <NameOfMigrationInPascalCase> --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.