mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
128 lines
6.0 KiB
C#
128 lines
6.0 KiB
C#
using System.Text;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
public class ImageSignatureRequestTests
|
|
{
|
|
private static byte[] Metadata(params string[] literals)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
using var writer = new BinaryWriter(stream);
|
|
writer.Write(0xFAB11BAFu);
|
|
writer.Write(27);
|
|
writer.Write(24);
|
|
writer.Write(literals.Length * 8);
|
|
writer.Write(24 + literals.Length * 8);
|
|
writer.Write(literals.Sum(s => Encoding.UTF8.GetByteCount(s)));
|
|
int index = 0;
|
|
foreach (var literal in literals)
|
|
{
|
|
int size = Encoding.UTF8.GetByteCount(literal);
|
|
writer.Write(size);
|
|
writer.Write(index);
|
|
index += size;
|
|
}
|
|
foreach (var literal in literals) writer.Write(Encoding.UTF8.GetBytes(literal));
|
|
return stream.ToArray();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(".rec.net")]
|
|
[InlineData(".radicade.internal")]
|
|
public void ClientMetadataControlsHeaderEvenOnWrongRoute(string tail)
|
|
{
|
|
var key = ImageSignatureRequest.DetectKeyId(Metadata("https://img.rec.net", tail));
|
|
Assert.Equal("KEY:RSA:p1" + tail, key);
|
|
var url = NameserverRedirect.Rewrite("https://img.rec.net/photo?width=64&sig=p1", "https://private.example");
|
|
url = ImageSignatureRequest.AddKeyId(url, "https://private.example", key);
|
|
Assert.Contains("width=64&sig=p1&rcsig=" + Uri.EscapeDataString(key), url);
|
|
var header = RadicadeServer.Images.ImageAPI.SignatureFor(key, "bare", RadicadeServer.Images.ImageAPI.StockKeyId);
|
|
Assert.StartsWith("key-id=" + key + "; data=", header);
|
|
Assert.Equal(url, ImageSignatureRequest.AddKeyId(url, "https://private.example", key));
|
|
}
|
|
|
|
// Rec Room's own metadata does not use the plain v27 header: the four literal-table
|
|
// fields are shuffled into other 32-bit slots, so the reader discovers the layout.
|
|
// This writes the same shape (the 20230831 build puts table offset at 0x74, table
|
|
// size at 0x24, data offset at 0xA0, data size at 0x3C) at 0x100-aligned offsets.
|
|
private static byte[] ShuffledMetadata(params string[] literals)
|
|
{
|
|
// Real metadata files carry tens of thousands of literals; the reader refuses
|
|
// tables that are too small to be one, so the fixture is padded past that floor.
|
|
var padded = literals.Concat(Enumerable.Range(0, 40).Select(i => $"filler literal {i}")).ToArray();
|
|
int tableOffset = 0x100;
|
|
int tableSize = padded.Length * 8;
|
|
int dataOffset = tableOffset + tableSize;
|
|
int dataSize = padded.Sum(s => Encoding.UTF8.GetByteCount(s));
|
|
var file = new byte[dataOffset + dataSize];
|
|
BitConverter.GetBytes(0x2EBE0u).CopyTo(file, 0);
|
|
BitConverter.GetBytes(tableSize).CopyTo(file, 0x24);
|
|
BitConverter.GetBytes(dataSize).CopyTo(file, 0x3C);
|
|
BitConverter.GetBytes(tableOffset).CopyTo(file, 0x74);
|
|
BitConverter.GetBytes(dataOffset).CopyTo(file, 0xA0);
|
|
int index = 0;
|
|
for (int i = 0; i < padded.Length; i++)
|
|
{
|
|
int size = Encoding.UTF8.GetByteCount(padded[i]);
|
|
BitConverter.GetBytes(size).CopyTo(file, tableOffset + 8 * i);
|
|
BitConverter.GetBytes(index).CopyTo(file, tableOffset + 8 * i + 4);
|
|
Encoding.UTF8.GetBytes(padded[i]).CopyTo(file, dataOffset + index);
|
|
index += size;
|
|
}
|
|
return file;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(".rec.net")]
|
|
[InlineData(".radicade.internal")]
|
|
public void ShuffledHeaderMetadataIsDiscoveredStructurally(string tail)
|
|
{
|
|
var metadata = ShuffledMetadata("https://img.rec.net", tail, "another literal");
|
|
Assert.Equal("KEY:RSA:p1" + tail, ImageSignatureRequest.DetectKeyIdFromGameMetadata(metadata));
|
|
Assert.Equal("KEY:RSA:p1" + tail, ImageSignatureRequest.DetectKeyIdFromGameMetadata(metadata));
|
|
// A plain v27 file keeps working through the same entry point.
|
|
Assert.Equal("KEY:RSA:p1" + tail, ImageSignatureRequest.DetectKeyIdFromGameMetadata(Metadata(tail)));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShuffledHeaderAmbiguityAndGarbageStayUndecided()
|
|
{
|
|
Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata(
|
|
ShuffledMetadata(".rec.net", ".radicade.internal", "x")));
|
|
Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata(
|
|
ShuffledMetadata("https://img.rec.net", "no key tail here")));
|
|
Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata(new byte[512]));
|
|
var random = new byte[4096];
|
|
new Random(7).NextBytes(random);
|
|
Assert.Null(ImageSignatureRequest.DetectKeyIdFromGameMetadata(random));
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidOrAmbiguousMetadataDoesNotGuess()
|
|
{
|
|
Assert.Null(ImageSignatureRequest.DetectKeyId(new byte[2]));
|
|
Assert.Null(ImageSignatureRequest.DetectKeyId(Metadata(".rec.net", ".radicade.internal")));
|
|
Assert.Null(ImageSignatureRequest.DetectKeyId(Metadata("https://img.rec.net")));
|
|
var invalid = Metadata(".rec.net");
|
|
Array.Copy(BitConverter.GetBytes(int.MaxValue), 0, invalid, 12, 4);
|
|
Assert.Null(ImageSignatureRequest.DetectKeyId(invalid));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://other.example/images/photo")]
|
|
[InlineData("https://private.example.evil/images/photo")]
|
|
[InlineData("https://private.example/api/photo")]
|
|
[InlineData("http://private.example/images/photo")]
|
|
public void OnlyConfiguredServerImagesReceiveHint(string url)
|
|
=> Assert.Equal(url, ImageSignatureRequest.AddKeyId(url, "https://private.example", "KEY:RSA:p1.rec.net"));
|
|
|
|
[Fact]
|
|
public void PatchedRouteAndServerBasePathKeepParametersAndReplaceStaleHint()
|
|
{
|
|
var url = ImageSignatureRequest.AddKeyId("https://private.example/base/images/img/photo?rcsig=old&cropSquare=1#f",
|
|
"https://private.example/base/", "KEY:RSA:p1.radicade.internal");
|
|
Assert.Equal("https://private.example/base/images/img/photo?cropSquare=1&rcsig=KEY%3ARSA%3Ap1.radicade.internal#f", url);
|
|
}
|
|
}
|