Files
2026-10-16 23:33:13 -07:00

204 lines
6.0 KiB
C#

using Microsoft.AspNetCore.Mvc;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats;
namespace RadicadeServer.Images
{
[Route("images")]
public class ImageAPI : Controller
{
private const string SignatureData =
"IWwe/pZ5vWWqNSkSM/54isgDxlZkdrP0sUrppKCbNktO2yCOTjq746xWiiLsueGuVcAGQqkjeRTimxolHckS/YXSYkEJxtiCXbLlsRia2DyAqtWVkGWsfczzFhp/56U66FVzolTspPCvjScOVlGO7dDIK7sJ+ndcRauWjsQsC6g3e7rUc6uwY099a6gy7sw6xr5BFZQSz8wg+fqyHYD/Sc4nQQVOTFZNNASqbJYhpNhEMXRnafCMuLl8a3mkGwvy3t4q2D/7SM48xrGZjEV47qNx1A91KCe28XVToFh4BzwEUU8nZ0d+KwV79MGarLo1cY8igc8FcoThKcovI4ClOg==";
public const string PatchedKeyId = "KEY:RSA:p1.radicade.internal";
public const string StockKeyId = "KEY:RSA:p1.rec.net";
public static readonly string SignatureString = BuildSignature(PatchedKeyId);
public static readonly string StockSignatureString = BuildSignature(StockKeyId);
public const string KeyIdQueryParameter = "rcsig";
public static readonly string BareRouteKeyId =
IsWellFormedKeyId(Environment.GetEnvironmentVariable("RADICADE_IMAGE_BARE_KEYID") ?? string.Empty)
? Environment.GetEnvironmentVariable("RADICADE_IMAGE_BARE_KEYID") ?? StockKeyId
: StockKeyId;
private static string BuildSignature(string keyId) => $"key-id={keyId}; data={SignatureData}";
[HttpGet("img/{imageName}")]
public async Task<IActionResult> GetImageFile(string imageName, [FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig,
[FromQuery(Name = KeyIdQueryParameter)] string clientKeyId)
{
return await ProcessImage(imageName, width, cropSquare, false, SignatureFor(clientKeyId, "img", PatchedKeyId));
}
[HttpGet("{imageName}")]
public async Task<IActionResult> GetImageFileAlt(string imageName, [FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig,
[FromQuery(Name = KeyIdQueryParameter)] string clientKeyId)
{
return await ProcessImage(imageName, width, cropSquare, false, SignatureFor(clientKeyId, "bare", BareRouteKeyId));
}
[HttpGet("default")]
public async Task<IActionResult> GetDefaultImage([FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig,
[FromQuery(Name = KeyIdQueryParameter)] string clientKeyId)
{
return await ProcessImage(DBConfig.DefaultRoomImage, width, cropSquare, false, SignatureFor(clientKeyId, "default", BareRouteKeyId));
}
public static string SignatureFor(string clientKeyId, string route, string routeDefaultKeyId)
{
var keyId = IsWellFormedKeyId(clientKeyId) ? clientKeyId : routeDefaultKeyId;
return BuildSignature(keyId);
}
public static bool IsWellFormedKeyId(string keyId)
{
const string prefix = "KEY:RSA:p1.";
if (string.IsNullOrEmpty(keyId) || keyId.Length > 96 || !keyId.StartsWith(prefix, StringComparison.Ordinal))
return false;
var tail = keyId.Substring(prefix.Length);
if (tail.Length == 0 || tail.Length > 64)
return false;
foreach (var c in tail)
{
if (!char.IsLetterOrDigit(c) && c != '.' && c != '-' && c != '_')
return false;
}
return true;
}
public static string[] AssetRoots() => new[] { DBConfig.CDNImages, DBConfig.CDNCustom, DBConfig.CDNStorage };
private static readonly string[] ExtensionVariants = { ".png", ".jpg", ".jpeg" };
public static string? ResolveAsset(string imageName) => ResolveAsset(imageName, AssetRoots());
public static string? ResolveAsset(string imageName, IEnumerable<string> roots)
{
var name = Path.GetFileName(imageName);
if (string.IsNullOrWhiteSpace(name) || name == "." || name == "..")
return null;
foreach (var candidate in CandidateNames(name))
{
foreach (var root in roots)
{
var path = Path.Join(root, candidate);
if (WithinRoot(root, path) && System.IO.File.Exists(path))
{
return path;
}
}
}
return null;
}
private static IEnumerable<string> CandidateNames(string name)
{
yield return name;
var existing = Path.GetExtension(name);
foreach (var variant in ExtensionVariants)
{
if (existing.Length == 0)
{
yield return name + variant;
}
else if (!string.Equals(existing, variant, StringComparison.OrdinalIgnoreCase))
{
yield return string.Concat(name.AsSpan(0, name.Length - existing.Length), variant);
}
}
}
private static bool WithinRoot(string root, string path)
{
try
{
var fullRoot = Path.GetFullPath(root) + Path.DirectorySeparatorChar;
return Path.GetFullPath(path).StartsWith(fullRoot, StringComparison.Ordinal);
}
catch (ArgumentException)
{
return false;
}
}
private async Task<IActionResult> ProcessImage(string imageName, int? width, int? cropSquare, bool isFallback = false, string? signature = null)
{
Response.Headers["Content-Signature"] = signature ?? SignatureString;
var fullPath = ResolveAsset(imageName);
if (fullPath == null)
{
if (!isFallback)
{
var fallback = ResolveAsset(DBConfig.DefaultRoomImage);
if (fallback != null)
{
return await ServeImageFile(fallback, width, cropSquare);
}
}
return NotFound();
}
return await ServeImageFile(fullPath, width, cropSquare);
}
private async Task<IActionResult> ServeImageFile(string fullPath, int? width, int? cropSquare)
{
if (!width.HasValue)
{
return PhysicalFile(fullPath, "image/jpeg");
}
Image image;
try
{
image = await Image.LoadAsync(fullPath);
}
catch (Exception)
{
return PhysicalFile(fullPath, "image/jpeg");
}
using (image)
{
if (cropSquare == 1)
{
int size = Math.Min(image.Width, image.Height);
var cropRect = new Rectangle((image.Width - size) / 2, (image.Height - size) / 2, size, size);
image.Mutate(x =>
{
x.Crop(cropRect);
x.Resize(width.Value, width.Value);
});
}
else
{
image.Mutate(x => x.Resize(width.Value, 0));
}
IImageFormat format = await Image.DetectFormatAsync(fullPath);
var ms = new MemoryStream();
await image.SaveAsync(ms, format);
ms.Position = 0;
return File(ms, format.DefaultMimeType);
}
}
}
}