mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
204 lines
6.0 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|