mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-16 14:42:28 +00:00
Implement PSN ticket reading
This commit is contained in:
parent
ef84bf1d50
commit
7081b725a8
14 changed files with 266 additions and 68 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,6 +28,7 @@ gitVersion.txt
|
|||
gitRemotes.txt
|
||||
gitUnpushed.txt
|
||||
logs/*
|
||||
npTicket*
|
||||
|
||||
# MSBuild stuff
|
||||
bin/
|
||||
|
|
|
@ -37,7 +37,8 @@ public class LighthouseServerTest
|
|||
await database.CreateUser($"{username}{number}", HashHelper.BCryptHash($"unitTestPassword{number}"));
|
||||
}
|
||||
|
||||
string stringContent = $"{LoginData.UsernamePrefix}{username}{number}{(char)0x00}";
|
||||
//TODO: generate actual tickets
|
||||
string stringContent = $"unitTestTicket{username}{number}";
|
||||
|
||||
HttpResponseMessage response = await this.Client.PostAsync
|
||||
($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent));
|
||||
|
|
|
@ -8,8 +8,10 @@ using LBPUnion.ProjectLighthouse.Helpers;
|
|||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Types;
|
||||
using LBPUnion.ProjectLighthouse.Types.Settings;
|
||||
using LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IOFile = System.IO.File;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Controllers;
|
||||
|
||||
|
@ -26,25 +28,29 @@ public class LoginController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Login([FromQuery] string? titleId)
|
||||
public async Task<IActionResult> Login()
|
||||
{
|
||||
titleId ??= "";
|
||||
MemoryStream ms = new();
|
||||
await this.Request.Body.CopyToAsync(ms);
|
||||
byte[] loginData = ms.ToArray();
|
||||
|
||||
string body = await new StreamReader(this.Request.Body).ReadToEndAsync();
|
||||
#if DEBUG
|
||||
await IOFile.WriteAllBytesAsync($"npTicket-{TimestampHelper.TimestampMillis}.txt", loginData);
|
||||
#endif
|
||||
|
||||
LoginData? loginData;
|
||||
NPTicket? npTicket;
|
||||
try
|
||||
{
|
||||
loginData = LoginData.CreateFromString(body);
|
||||
npTicket = NPTicket.CreateFromBytes(loginData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
loginData = null;
|
||||
npTicket = null;
|
||||
}
|
||||
|
||||
if (loginData == null)
|
||||
if (npTicket == null)
|
||||
{
|
||||
Logger.Log("loginData was null, rejecting login", LoggerLevelLogin.Instance);
|
||||
Logger.Log("npTicket was null, rejecting login", LoggerLevelLogin.Instance);
|
||||
return this.BadRequest();
|
||||
}
|
||||
|
||||
|
@ -60,11 +66,11 @@ public class LoginController : ControllerBase
|
|||
// 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);
|
||||
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.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);
|
||||
token = await this.database.AuthenticateUser(npTicket, ipAddress);
|
||||
if (token == null)
|
||||
{
|
||||
Logger.Log("unable to find/generate a token, rejecting login", LoggerLevelLogin.Instance);
|
||||
|
@ -129,7 +135,7 @@ public class LoginController : ControllerBase
|
|||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance);
|
||||
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client", 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,
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kettu;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Types;
|
||||
using LBPUnion.ProjectLighthouse.Types.Categories;
|
||||
using LBPUnion.ProjectLighthouse.Types.Levels;
|
||||
using LBPUnion.ProjectLighthouse.Types.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.Types.Reviews;
|
||||
using LBPUnion.ProjectLighthouse.Types.Settings;
|
||||
using LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -67,9 +66,9 @@ public class Database : DbContext
|
|||
}
|
||||
|
||||
#nullable enable
|
||||
public async Task<GameToken?> AuthenticateUser(LoginData loginData, string userLocation, string titleId = "")
|
||||
public async Task<GameToken?> AuthenticateUser(NPTicket npTicket, string userLocation)
|
||||
{
|
||||
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username);
|
||||
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username);
|
||||
if (user == null) return null;
|
||||
|
||||
GameToken gameToken = new()
|
||||
|
@ -78,15 +77,9 @@ public class Database : DbContext
|
|||
User = user,
|
||||
UserId = user.UserId,
|
||||
UserLocation = userLocation,
|
||||
GameVersion = GameVersionHelper.FromTitleId(titleId),
|
||||
GameVersion = npTicket.GameVersion,
|
||||
};
|
||||
|
||||
if (gameToken.GameVersion == GameVersion.Unknown)
|
||||
{
|
||||
Logger.Log($"Unknown GameVersion for TitleId {titleId}", LoggerLevelLogin.Instance);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.GameTokens.Add(gameToken);
|
||||
await this.SaveChangesAsync();
|
||||
|
||||
|
|
|
@ -23,6 +23,10 @@ public static class BinaryReaderExtensions
|
|||
|
||||
public static int ReadInt32BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(int)).Reverse(), 0);
|
||||
|
||||
public static ulong ReadUInt64BE(this BinaryReader binRdr) => BitConverter.ToUInt32(binRdr.ReadBytesRequired(sizeof(ulong)).Reverse(), 0);
|
||||
|
||||
public static long ReadInt64BE(this BinaryReader binRdr) => BitConverter.ToInt32(binRdr.ReadBytesRequired(sizeof(long)).Reverse(), 0);
|
||||
|
||||
public static byte[] ReadBytesRequired(this BinaryReader binRdr, int byteCount)
|
||||
{
|
||||
byte[] result = binRdr.ReadBytes(byteCount);
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Types;
|
||||
|
||||
/// <summary>
|
||||
/// The data sent from POST /LOGIN.
|
||||
/// </summary>
|
||||
public class LoginData
|
||||
{
|
||||
|
||||
public static readonly string UsernamePrefix = Encoding.ASCII.GetString
|
||||
(
|
||||
new byte[]
|
||||
{
|
||||
0x04, 0x00, 0x20,
|
||||
}
|
||||
);
|
||||
|
||||
public string Username { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a X-I-5 Ticket into `LoginData`.
|
||||
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
|
||||
/// </summary>
|
||||
public static LoginData? CreateFromString(string str)
|
||||
{
|
||||
str = str.Replace("\b", ""); // Remove backspace characters
|
||||
|
||||
using MemoryStream ms = new(Encoding.ASCII.GetBytes(str));
|
||||
using BinaryReader reader = new(ms);
|
||||
|
||||
if (!str.Contains(UsernamePrefix)) return null;
|
||||
|
||||
LoginData loginData = new();
|
||||
|
||||
reader.BaseStream.Position = str.IndexOf(UsernamePrefix, StringComparison.Ordinal) + UsernamePrefix.Length;
|
||||
loginData.Username = BinaryHelper.ReadString(reader).Replace("\0", string.Empty);
|
||||
|
||||
return loginData;
|
||||
}
|
||||
}
|
|
@ -5,4 +5,7 @@ public enum Platform
|
|||
PS3 = 0,
|
||||
RPCS3 = 1,
|
||||
Vita = 2,
|
||||
PSP = 3,
|
||||
UnitTest = 4,
|
||||
Unknown = -1,
|
||||
}
|
7
ProjectLighthouse/Types/Tickets/DataHeader.cs
Normal file
7
ProjectLighthouse/Types/Tickets/DataHeader.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
|
||||
public struct DataHeader
|
||||
{
|
||||
public DataType Type;
|
||||
public ushort Length;
|
||||
}
|
11
ProjectLighthouse/Types/Tickets/DataType.cs
Normal file
11
ProjectLighthouse/Types/Tickets/DataType.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
|
||||
public enum DataType : byte
|
||||
{
|
||||
Empty = 0x00,
|
||||
UInt32 = 0x01,
|
||||
UInt64 = 0x02,
|
||||
String = 0x04,
|
||||
Timestamp = 0x07,
|
||||
Binary = 0x08,
|
||||
}
|
125
ProjectLighthouse/Types/Tickets/NPTicket.cs
Normal file
125
ProjectLighthouse/Types/Tickets/NPTicket.cs
Normal file
|
@ -0,0 +1,125 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Kettu;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Types.Settings;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
|
||||
/// <summary>
|
||||
/// A PSN ticket, typically sent from PS3/RPCN
|
||||
/// </summary>
|
||||
public class NPTicket
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
private Version ticketVersion { get; set; }
|
||||
|
||||
public Platform Platform { get; set; }
|
||||
|
||||
public uint IssuerId { get; set; }
|
||||
public ulong IssuedDate { get; set; }
|
||||
public ulong ExpireDate { get; set; }
|
||||
|
||||
public GameVersion GameVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// https://www.psdevwiki.com/ps3/X-I-5-Ticket
|
||||
/// </summary>
|
||||
public static NPTicket? CreateFromBytes(byte[] data)
|
||||
{
|
||||
#if DEBUG
|
||||
if (data[0] == 'u' && ServerStatics.IsUnitTesting)
|
||||
{
|
||||
string dataStr = Encoding.UTF8.GetString(data);
|
||||
if (dataStr.StartsWith("unitTestTicket"))
|
||||
{
|
||||
NPTicket npTicket = new()
|
||||
{
|
||||
IssuerId = 0,
|
||||
ticketVersion = new Version(0, 0),
|
||||
Platform = Platform.UnitTest,
|
||||
GameVersion = GameVersion.LittleBigPlanet2,
|
||||
ExpireDate = 0,
|
||||
IssuedDate = 0,
|
||||
};
|
||||
|
||||
npTicket.Username = dataStr.Substring(14);
|
||||
|
||||
return npTicket;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
try
|
||||
{
|
||||
NPTicket npTicket = new();
|
||||
using MemoryStream ms = new(data);
|
||||
using TicketReader reader = new(ms);
|
||||
|
||||
npTicket.ticketVersion = reader.ReadTicketVersion();
|
||||
|
||||
reader.ReadBytes(4); // Skip header
|
||||
|
||||
reader.ReadUInt16BE(); // Ticket length, we don't care about this
|
||||
|
||||
if (npTicket.ticketVersion != "2.1") throw new NotImplementedException();
|
||||
|
||||
#if DEBUG
|
||||
SectionHeader bodyHeader = reader.ReadSectionHeader();
|
||||
Logger.Log($"bodyHeader.Type is {bodyHeader.Type}", LoggerLevelLogin.Instance);
|
||||
#else
|
||||
reader.ReadSectionHeader();
|
||||
#endif
|
||||
|
||||
reader.ReadTicketString(); // "Serial id", but its apparently not what we're looking for
|
||||
|
||||
npTicket.IssuerId = reader.ReadTicketUInt32();
|
||||
npTicket.IssuedDate = reader.ReadTicketUInt64();
|
||||
npTicket.ExpireDate = reader.ReadTicketUInt64();
|
||||
|
||||
reader.ReadTicketUInt64(); // PSN User id, we don't care about this
|
||||
|
||||
npTicket.Username = reader.ReadTicketString();
|
||||
|
||||
reader.ReadTicketString(); // Country
|
||||
reader.ReadTicketString(); // Domain
|
||||
|
||||
// Title ID, kinda..
|
||||
// Data: "UP9000-BCUS98245_00
|
||||
string titleId = reader.ReadTicketString();
|
||||
titleId = titleId.Substring(7); // Trim UP9000-
|
||||
titleId = titleId.Substring(0, titleId.Length - 3); // Trim _00 at the end
|
||||
|
||||
#if DEBUG
|
||||
Logger.Log($"titleId is {titleId}", LoggerLevelLogin.Instance);
|
||||
#endif
|
||||
|
||||
npTicket.GameVersion = GameVersionHelper.FromTitleId(titleId); // Finally, convert it to GameVersion
|
||||
|
||||
// Production PSN Issuer ID: 0x100
|
||||
// RPCN Issuer ID: 0x33333333
|
||||
npTicket.Platform = npTicket.IssuerId switch
|
||||
{
|
||||
0x100 => Platform.PS3,
|
||||
0x33333333 => Platform.RPCS3,
|
||||
_ => Platform.Unknown,
|
||||
};
|
||||
|
||||
if (npTicket.Platform == Platform.Unknown)
|
||||
{
|
||||
Logger.Log($"", LoggerLevelLogin.Instance);
|
||||
return null;
|
||||
}
|
||||
|
||||
return npTicket;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
7
ProjectLighthouse/Types/Tickets/SectionHeader.cs
Normal file
7
ProjectLighthouse/Types/Tickets/SectionHeader.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
|
||||
public struct SectionHeader
|
||||
{
|
||||
public SectionType Type;
|
||||
public ushort Length;
|
||||
}
|
7
ProjectLighthouse/Types/Tickets/SectionType.cs
Normal file
7
ProjectLighthouse/Types/Tickets/SectionType.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
|
||||
public enum SectionType : byte
|
||||
{
|
||||
Body = 0x00,
|
||||
Footer = 0x02,
|
||||
}
|
61
ProjectLighthouse/Types/Tickets/TicketReader.cs
Normal file
61
ProjectLighthouse/Types/Tickets/TicketReader.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using LBPUnion.ProjectLighthouse.Helpers.Extensions;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Types.Tickets;
|
||||
|
||||
public class TicketReader : BinaryReader
|
||||
{
|
||||
public TicketReader([NotNull] Stream input) : base(input)
|
||||
{}
|
||||
|
||||
public Version ReadTicketVersion() => new(this.ReadByte() >> 4, this.ReadByte());
|
||||
|
||||
public SectionHeader ReadSectionHeader()
|
||||
{
|
||||
this.ReadByte();
|
||||
|
||||
SectionHeader sectionHeader = new();
|
||||
sectionHeader.Type = (SectionType)this.ReadByte();
|
||||
sectionHeader.Length = this.ReadUInt16BE();
|
||||
|
||||
return sectionHeader;
|
||||
}
|
||||
|
||||
public DataHeader ReadDataHeader()
|
||||
{
|
||||
DataHeader dataHeader = new();
|
||||
dataHeader.Type = (DataType)this.ReadUInt16BE();
|
||||
dataHeader.Length = this.ReadUInt16BE();
|
||||
|
||||
return dataHeader;
|
||||
}
|
||||
|
||||
public byte[] ReadTicketBinary()
|
||||
{
|
||||
DataHeader dataHeader = this.ReadDataHeader();
|
||||
Debug.Assert(dataHeader.Type == DataType.Binary || dataHeader.Type == DataType.String);
|
||||
|
||||
return this.ReadBytes(dataHeader.Length);
|
||||
}
|
||||
|
||||
public string ReadTicketString() => Encoding.UTF8.GetString(this.ReadTicketBinary()).TrimEnd('\0');
|
||||
|
||||
public uint ReadTicketUInt32()
|
||||
{
|
||||
DataHeader dataHeader = this.ReadDataHeader();
|
||||
Debug.Assert(dataHeader.Type == DataType.UInt32);
|
||||
|
||||
return this.ReadUInt32BE();
|
||||
}
|
||||
|
||||
public ulong ReadTicketUInt64()
|
||||
{
|
||||
DataHeader dataHeader = this.ReadDataHeader();
|
||||
Debug.Assert(dataHeader.Type == DataType.UInt64 || dataHeader.Type == DataType.Timestamp);
|
||||
|
||||
return this.ReadUInt64BE();
|
||||
}
|
||||
}
|
17
ProjectLighthouse/Types/Version.cs
Normal file
17
ProjectLighthouse/Types/Version.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace LBPUnion.ProjectLighthouse.Types;
|
||||
|
||||
public class Version
|
||||
{
|
||||
public int Major { get; set; }
|
||||
public int Minor { get; set; }
|
||||
|
||||
public Version(int major, int minor)
|
||||
{
|
||||
this.Major = major;
|
||||
this.Minor = minor;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{this.Major}.{this.Minor}";
|
||||
|
||||
public static implicit operator string(Version v) => v.ToString();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue