Rework CI and add game server login tests (#615)

* Make tests run in release mode

* Fix multiple command in ci

* I forgot how yaml works

* Shitty workaround

* grr mondays

* Add an NP ticket builder for unit tests

* Add NP ticket unit testing

* Fix range indexers for finding uid

* Fix LoginTests

* Validate unit test signatures

* Make builder code follow same style conventions

* Remove remnant of hardcoded issuer id
This commit is contained in:
Josh 2023-01-07 00:12:39 -06:00 committed by GitHub
parent 337124690d
commit 20b2ef5700
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 540 additions and 80 deletions

View file

@ -40,20 +40,25 @@ jobs:
dotnet-version: "7.0.x"
- name: Compile
run: dotnet build -c Debug
run: |
dotnet restore
dotnet build -c Release --no-restore
dotnet build -c Release --no-restore --no-dependencies ProjectLighthouse.Tests
dotnet build -c Release --no-restore --no-dependencies ProjectLighthouse.Tests.GameApiTests
dotnet build -c Release --no-restore --no-dependencies ProjectLighthouse.Tests.WebsiteTests
- name: Run tests on ProjectLighthouse.Tests
continue-on-error: true
run: dotnet test --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-Tests.trx" ProjectLighthouse.Tests
run: dotnet test -c Release --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-Tests.trx" ProjectLighthouse.Tests
- name: Run tests on ProjectLighthouse.Tests.GameApiTests
continue-on-error: true
run: dotnet test --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-GameApiTests.trx" ProjectLighthouse.Tests.GameApiTests
run: dotnet test -c Release --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-GameApiTests.trx" ProjectLighthouse.Tests.GameApiTests
- name: Run tests on ProjectLighthouse.Tests.WebsiteTests
if: ${{ matrix.os.webTest }}
continue-on-error: true
run: dotnet test --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-WebsiteTests.trx" ProjectLighthouse.Tests.WebsiteTests
run: dotnet test -c Release --no-build --logger "trx;LogFileName=${{github.workspace}}/TestResults-${{matrix.os.prettyName}}-WebsiteTests.trx" ProjectLighthouse.Tests.WebsiteTests
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always

View file

@ -1,7 +1,6 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup;

View file

@ -0,0 +1,97 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse;
using LBPUnion.ProjectLighthouse.Administration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup;
using LBPUnion.ProjectLighthouse.Tests;
using LBPUnion.ProjectLighthouse.Tickets;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Tests;
public class LoginTests : LighthouseServerTest<GameServerTestStartup>
{
[Fact]
public async Task ShouldLoginWithGoodTicket()
{
string username = await this.CreateRandomUser();
ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]);
byte[] ticketData = new TicketBuilder()
.SetUsername(username)
.SetUserId(userId)
.Build();
HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData));
Assert.True(response.IsSuccessStatusCode);
}
[Fact]
public async Task ShouldNotLoginWithExpiredTicket()
{
string username = await this.CreateRandomUser();
ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]);
byte[] ticketData = new TicketBuilder()
.SetUsername(username)
.SetUserId(userId)
.setExpirationTime((ulong)TimeHelper.TimestampMillis - 1000 * 60)
.Build();
HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData));
Assert.False(response.IsSuccessStatusCode);
Assert.True(response.StatusCode == HttpStatusCode.BadRequest);
}
[Fact]
public async Task ShouldNotLoginWithBadTitleId()
{
string username = await this.CreateRandomUser();
ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]);
byte[] ticketData = new TicketBuilder()
.SetUsername(username)
.SetUserId(userId)
.SetTitleId("UP9000-BLUS30079_00")
.Build();
HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData));
Assert.False(response.IsSuccessStatusCode);
Assert.True(response.StatusCode == HttpStatusCode.BadRequest);
}
[Fact]
public async Task ShouldNotLoginWithBadSignature()
{
string username = await this.CreateRandomUser();
ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]);
byte[] ticketData = new TicketBuilder()
.SetUsername(username)
.SetUserId(userId)
.Build();
ticketData[^21] = 0;
HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData));
Assert.False(response.IsSuccessStatusCode);
Assert.True(response.StatusCode == HttpStatusCode.BadRequest);
}
[Fact]
public async Task ShouldNotLoginIfBanned()
{
string username = await this.CreateRandomUser();
ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]);
await using Database database = new();
User user = await database.Users.FirstAsync(u => u.Username == username);
user.PermissionLevel = PermissionLevel.Banned;
await database.SaveChangesAsync();
byte[] ticketData = new TicketBuilder()
.SetUsername(username)
.SetUserId(userId)
.Build();
HttpResponseMessage response =
await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData));
Assert.False(response.IsSuccessStatusCode);
Assert.True(response.StatusCode == HttpStatusCode.Forbidden);
}
}

View file

@ -10,8 +10,10 @@ public sealed class DatabaseFactAttribute : FactAttribute
public DatabaseFactAttribute()
{
ServerConfiguration.Instance = new ServerConfiguration();
ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
ServerConfiguration.Instance = new ServerConfiguration
{
DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse",
};
if (!ServerStatics.DbConnected) this.Skip = "Database not available";
else
lock(migrateLock)

View file

@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Tickets;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
@ -25,27 +26,38 @@ public class LighthouseServerTest<TStartup> where TStartup : class
this.Server = new TestServer(new WebHostBuilder().UseStartup<TStartup>());
this.Client = this.Server.CreateClient();
}
public async Task<HttpResponseMessage> AuthenticateResponse(int number = -1, bool createUser = true)
public async Task<string> CreateRandomUser(int number = -1, bool createUser = true)
{
if (number == -1) number = new Random().Next();
const string username = "unitTestUser";
if (createUser)
{
await using Database database = new();
if (await database.Users.FirstOrDefaultAsync(u => u.Username == $"{username}{number}") == null)
{
User user = await database.CreateUser($"{username}{number}", CryptoHelper.BCryptHash($"unitTestPassword{number}"));
User user = await database.CreateUser($"{username}{number}",
CryptoHelper.BCryptHash($"unitTestPassword{number}"));
user.LinkedPsnId = (ulong)number;
await database.SaveChangesAsync();
}
}
//TODO: generate actual tickets
string stringContent = $"unitTestTicket{username}{number}";
return $"{username}{number}";
}
public async Task<HttpResponseMessage> AuthenticateResponse(int number = -1, bool createUser = true)
{
string username = await this.CreateRandomUser(number, createUser);
byte[] ticketData = new TicketBuilder()
.SetUsername($"{username}{number}")
.SetUserId((ulong)number)
.Build();
HttpResponseMessage response = await this.Client.PostAsync
($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new StringContent(stringContent));
($"/LITTLEBIGPLANETPS3_XML/login?titleID={GameVersionHelper.LittleBigPlanet2TitleIds[0]}", new ByteArrayContent(ticketData));
return response;
}

View file

@ -46,33 +46,65 @@ public class NPTicket
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
private byte[] ticketSignature { get; set; } = Array.Empty<byte>();
private byte[] ticketSignatureIdentifier { get; set; } = Array.Empty<byte>();
public GameVersion GameVersion { get; set; }
private static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
private static readonly ECDomainParameters secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
private static readonly ECDomainParameters secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
public static readonly ECDomainParameters Secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
public static readonly ECDomainParameters Secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
private static readonly ECPoint rpcnPublic = secp224K1.Curve.CreatePoint(
private static readonly ECPoint rpcnPublic = Secp224K1.Curve.CreatePoint(
new BigInteger("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16),
new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 16));
private static readonly ECPoint psnPublic = secp192R1.Curve.CreatePoint(
private static readonly ECPoint psnPublic = Secp192R1.Curve.CreatePoint(
new BigInteger("39c62d061d4ee35c5f3f7531de0af3cf918346526edac727", 16),
new BigInteger("a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86", 16));
private ECDomainParameters getCurveParams() => this.IsRpcn() ? secp224K1 : secp192R1;
private static readonly ECPoint unitTestPublic = Secp192R1.Curve.CreatePoint(
new BigInteger("b6f3374bde4ec23a25e1508889e7d7e71870ba74daf8654f", 16),
new BigInteger("738de93dad0fffb5642045439afaaf8c6fda319a72d2a584", 16));
private ECPoint getPublicKey() => this.IsRpcn() ? rpcnPublic : psnPublic;
internal class SignatureParams
{
public string HashAlgo { get; set; }
public ECPoint PublicKey { get; set; }
public ECDomainParameters CurveParams { get; set; }
public SignatureParams(string hashAlgo, ECPoint pubKey, ECDomainParameters curve)
{
this.HashAlgo = hashAlgo;
this.PublicKey = pubKey;
this.CurveParams = curve;
}
}
private readonly Dictionary<string, SignatureParams> signatureParamsMap = new()
{
//psn
{ "719F1D4A", new SignatureParams("SHA-1", psnPublic, Secp192R1) },
//rpcn
{ "5250434E", new SignatureParams("SHA-224", rpcnPublic, Secp224K1) },
//unit test
{ "54455354", new SignatureParams("SHA-1", unitTestPublic, Secp192R1) },
};
private bool ValidateSignature()
{
ECPublicKeyParameters pubKey = new(this.getPublicKey(), this.getCurveParams());
string algo = this.IsRpcn() ? "SHA-224" : "SHA-1";
string identifierHex = Convert.ToHexString(this.ticketSignatureIdentifier);
if (!this.signatureParamsMap.ContainsKey(identifierHex))
{
Logger.Warn($"Unknown signature identifier in ticket: {identifierHex}, platform={this.Platform}", LogArea.Login);
return false;
}
ISigner signer = SignerUtilities.GetSigner($"{algo}withECDSA");
SignatureParams sigParams = this.signatureParamsMap[identifierHex];
ECPublicKeyParameters pubKey = new(sigParams.PublicKey, sigParams.CurveParams);
ISigner signer = SignerUtilities.GetSigner($"{sigParams.HashAlgo}withECDSA");
signer.Init(false, pubKey);
signer.BlockUpdate(this.ticketBody);
@ -80,18 +112,6 @@ public class NPTicket
return signer.VerifySignature(this.ticketSignature);
}
private bool IsRpcn() => this.IssuerId == 0x33333333;
private static readonly Dictionary<Platform, byte[]> identifierByPlatform = new()
{
{
Platform.RPCS3, "RPCN"u8.ToArray()
},
{
Platform.PS3, new byte[]{ 0x71, 0x9F, 0x1D, 0x4A, }
},
};
// Sometimes psn signatures have one or two extra empty bytes
// This is slow but it's better than carelessly chopping 0's
private static byte[] ParseSignature(byte[] signature)
@ -136,14 +156,7 @@ public class NPTicket
reader.ReadSectionHeader(); // footer header
byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier
Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3;
byte[] platformIdent = identifierByPlatform[platform];
if (!ticketIdent.SequenceEqual(platformIdent))
{
Logger.Warn(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ticketIdent)} == {Convert.ToHexString(platformIdent)}", LogArea.Login);
return false;
}
npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary();
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
return true;
@ -175,14 +188,7 @@ public class NPTicket
reader.ReadSectionHeader(); // footer header
byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier
Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3;
byte[] platformIdent = identifierByPlatform[platform];
if (!ticketIdent.SequenceEqual(platformIdent))
{
Logger.Warn(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ticketIdent)} == {Convert.ToHexString(platformIdent)}", LogArea.Login);
return false;
}
npTicket.ticketSignatureIdentifier = reader.ReadTicketBinary();
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
return true;
@ -216,9 +222,14 @@ public class NPTicket
if (!parsedSuccessfully) return false;
npTicket.ticketBody = npTicket.IsRpcn()
? data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray()
: data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray();
npTicket.ticketBody = Convert.ToHexString(npTicket.ticketSignatureIdentifier) switch
{
// rpcn
"5250434E" => data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray(),
// psn and unit test
"719F1D4A" or "54455354" => data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray(),
_ => throw new ArgumentOutOfRangeException(nameof(npTicket)),
};
return true;
}
@ -229,28 +240,6 @@ public class NPTicket
public static NPTicket? CreateFromBytes(byte[] data)
{
NPTicket npTicket = new();
#if DEBUG
if (data[0] == 'u' && ServerStatics.IsUnitTesting)
{
string dataStr = Encoding.UTF8.GetString(data);
if (dataStr.StartsWith("unitTestTicket"))
{
npTicket = new NPTicket
{
IssuerId = 0,
ticketVersion = new Version(0, 0),
Platform = Platform.UnitTest,
GameVersion = GameVersion.LittleBigPlanet2,
ExpireDate = 0,
IssuedDate = 0,
Username = dataStr["unitTestTicket".Length..],
UserId = ulong.Parse(dataStr["unitTestTicketunitTestUser".Length..]),
};
return npTicket;
}
}
#endif
try
{
using MemoryStream ms = new(data);
@ -263,14 +252,14 @@ public class NPTicket
return null;
}
if ((long)npTicket.IssuedDate > TimeHelper.TimestampMillis)
if (npTicket.IssuedDate > (ulong)TimeHelper.TimestampMillis)
{
Logger.Warn($"Ticket isn't valid yet from {npTicket.Username}", LogArea.Login);
Logger.Warn($"Ticket isn't valid yet from {npTicket.Username} ({npTicket.IssuedDate} > {(ulong)TimeHelper.TimestampMillis})", LogArea.Login);
return null;
}
if (TimeHelper.TimestampMillis > (long)npTicket.ExpireDate)
if ((ulong)TimeHelper.TimestampMillis > npTicket.ExpireDate)
{
Logger.Warn($"Ticket has expired from {npTicket.Username}", LogArea.Login);
Logger.Warn($"Ticket has expired from {npTicket.Username} ({(ulong)TimeHelper.TimestampMillis} > {npTicket.ExpireDate}", LogArea.Login);
return null;
}
@ -299,12 +288,13 @@ public class NPTicket
{
0x100 => Platform.PS3,
0x33333333 => Platform.RPCS3,
0x74657374 => Platform.UnitTest,
_ => Platform.Unknown,
};
if (npTicket.Platform == Platform.PS3 && npTicket.GameVersion == GameVersion.LittleBigPlanetVita) npTicket.Platform = Platform.Vita;
if (npTicket.Platform == Platform.Unknown)
if (npTicket.Platform == Platform.Unknown || (npTicket.Platform == Platform.UnitTest && !ServerStatics.IsUnitTesting))
{
Logger.Warn($"Could not determine platform from IssuerId {npTicket.IssuerId} decimal", LogArea.Login);
return null;

View file

@ -0,0 +1,160 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Tickets.Types;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
namespace LBPUnion.ProjectLighthouse.Tickets;
// generates v2.1 tickets
public class TicketBuilder
{
private static uint _idDispenser;
private string Username { get; set; } = "test";
private string TitleId { get; set; } = "UP9000-BCUS98245_00";
private ulong IssueTime { get; set; } = (ulong)TimeHelper.TimestampMillis;
private ulong ExpirationTime { get; set; } = (ulong)TimeHelper.TimestampMillis + 15 * 60 * 1000;
private uint IssuerId { get; set; } = 0x74657374;
private ulong UserId { get; set; }
private string Country { get; set; } = "br";
private string Domain { get; set; } = "un";
private uint Status { get; set; }
private static readonly BigInteger privateKey = new("0edab5a79f0fff1cafd80849f620363ca15bcb89197f153d", 16);
public byte[] Build()
{
uint ticketId = _idDispenser++;
byte[] serialBytes = new byte[0x14];
BinaryPrimitives.WriteUInt32BigEndian(serialBytes, ticketId);
byte[] onlineId = new byte[0x20];
Encoding.ASCII.GetBytes(this.Username).CopyTo(onlineId, 0);
byte[] country = new byte[0x4];
Encoding.ASCII.GetBytes(this.Country).CopyTo(country, 0);
byte[] domain = new byte[0x4];
Encoding.ASCII.GetBytes(this.Domain).CopyTo(domain, 0);
byte[] titleId = new byte[0x18];
Encoding.ASCII.GetBytes(this.TitleId).CopyTo(titleId, 0);
List<TicketData> userData = new()
{
new BinaryData(serialBytes),
new UInt32Data(this.IssuerId),
new TimestampData(this.IssueTime),
new TimestampData(this.ExpirationTime),
new UInt64Data(this.UserId),
new StringData(onlineId),
new BinaryData(country),
new BinaryData(domain),
new BinaryData(titleId),
new UInt32Data(this.Status),
new EmptyData(),
new EmptyData(),
};
TicketData userBlob = new BlobData(0, userData);
List<TicketData> footer = new()
{
new BinaryData("TEST"u8.ToArray()),
new BinaryData(new byte[0x38]),
};
TicketData footerBlob = new BlobData(2, footer);
MemoryStream ms = new();
BinaryWriter writer = new(ms);
writer.Write(new byte[]{0x21, 0x01, 0x00, 0x00,});
int ticketLen = userBlob.Len() + footerBlob.Len() + 8;
byte[] lenAsBytes = new byte[4];
BinaryPrimitives.WriteUInt32BigEndian(lenAsBytes, (uint)ticketLen);
writer.Write(lenAsBytes);
userBlob.Write(writer);
footerBlob.Write(writer);
byte[] data = ms.ToArray();
// sign all data besides the signature
byte[] ticketData = data.AsSpan()[..(data.Length-0x38)].ToArray();
byte[] signature = SignData(ticketData);
if (signature.Length < 0x38)
{
Array.Resize(ref signature, 0x38);
}
ticketData = ticketData.Concat(signature).ToArray();
return ticketData;
}
private static byte[] SignData(byte[] data)
{
ECPrivateKeyParameters key = new(privateKey, NPTicket.Secp192R1);
ISigner signer = SignerUtilities.GetSigner("SHA-1withECDSA");
signer.Init(true, key);
signer.BlockUpdate(data);
return signer.GenerateSignature();
}
public TicketBuilder SetUsername(string username)
{
this.Username = username;
return this;
}
public TicketBuilder SetTitleId(string titleId)
{
this.TitleId = titleId;
return this;
}
public TicketBuilder setExpirationTime(ulong expirationTime)
{
this.ExpirationTime = expirationTime;
return this;
}
public TicketBuilder SetIssueTime(ulong issueTime)
{
this.IssueTime = issueTime;
return this;
}
public TicketBuilder SetIssuerId(ushort issuerId)
{
this.IssuerId = issuerId;
return this;
}
public TicketBuilder SetUserId(ulong userId)
{
this.UserId = userId;
return this;
}
public TicketBuilder SetCountry(string country)
{
this.Country = country;
return this;
}
public TicketBuilder SetDomain(string domain)
{
this.Domain = domain;
return this;
}
public TicketBuilder SetStatus(uint status)
{
this.Status = status;
return this;
}
}

View file

@ -0,0 +1,23 @@
using System.IO;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class BinaryData : TicketData
{
private readonly byte[] val;
public BinaryData(byte[] val)
{
this.val = val;
}
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
writer.Write(this.val);
}
public override short Id() => 8;
public override short Len() => (short)this.val.Length;
}

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class BlobData : TicketData
{
private readonly byte id;
private readonly List<TicketData> data;
public BlobData(byte id, List<TicketData> data)
{
this.id = id;
this.data = data;
}
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
foreach (TicketData d in this.data)
{
d.Write(writer);
}
}
public override short Id() => (short)(0x3000 | this.id);
public override short Len() => (short)this.data.Sum(d => d.Len()+4);
}

View file

@ -0,0 +1,15 @@
using System.IO;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class EmptyData : TicketData
{
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
}
public override short Id() => 0;
public override short Len() => 0;
}

View file

@ -0,0 +1,29 @@
using System.IO;
using System.Text;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class StringData : TicketData
{
private readonly byte[] val;
public StringData(string val)
{
this.val = Encoding.ASCII.GetBytes(val);
}
public StringData(byte[] val)
{
this.val = val;
}
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
writer.Write(this.val);
}
public override short Id() => 4;
public override short Len() => (short)this.val.Length;
}

View file

@ -0,0 +1,21 @@
using System.Buffers.Binary;
using System.IO;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public abstract class TicketData
{
public void WriteHeader(BinaryWriter writer)
{
byte[] id = new byte[2];
byte[] len = new byte[2];
BinaryPrimitives.WriteUInt16BigEndian(id, (ushort)this.Id());
BinaryPrimitives.WriteUInt16BigEndian(len, (ushort)this.Len());
writer.Write(id);
writer.Write(len);
}
public abstract void Write(BinaryWriter writer);
public abstract short Id();
public abstract short Len();
}

View file

@ -0,0 +1,26 @@
using System.Buffers.Binary;
using System.IO;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class TimestampData : TicketData
{
private readonly ulong val;
public TimestampData(ulong val)
{
this.val = val;
}
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
byte[] data = new byte[8];
BinaryPrimitives.WriteUInt64BigEndian(data, this.val);
writer.Write(data);
}
public override short Id() => 7;
public override short Len() => 8;
}

View file

@ -0,0 +1,26 @@
using System.Buffers.Binary;
using System.IO;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class UInt32Data : TicketData
{
private readonly uint val;
public UInt32Data(uint val)
{
this.val = val;
}
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
byte[] data = new byte[4];
BinaryPrimitives.WriteUInt32BigEndian(data, this.val);
writer.Write(data);
}
public override short Id() => 1;
public override short Len() => 4;
}

View file

@ -0,0 +1,26 @@
using System.Buffers.Binary;
using System.IO;
namespace LBPUnion.ProjectLighthouse.Tickets.Types;
public class UInt64Data : TicketData
{
private readonly ulong val;
public UInt64Data(ulong val)
{
this.val = val;
}
public override void Write(BinaryWriter writer)
{
this.WriteHeader(writer);
byte[] data = new byte[8];
BinaryPrimitives.WriteUInt64BigEndian(data, this.val);
writer.Write(data);
}
public override short Id() => 2;
public override short Len() => 8;
}