160 lines
5.8 KiB
C#
160 lines
5.8 KiB
C#
using DeluxeBackend.Services;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using SkiaSharp;
|
|
using System.Formats.Asn1;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace DeluxeBackend.Controllers
|
|
{
|
|
[Route("Images")]
|
|
[ApiController]
|
|
public class ImagesController : ControllerBase
|
|
{
|
|
|
|
private readonly HttpClient httpClient;
|
|
private readonly string _cachePath;
|
|
|
|
|
|
|
|
public ImagesController(HttpClient httpClient, IWebHostEnvironment env)
|
|
{
|
|
this.httpClient = httpClient;
|
|
_cachePath = Path.Combine(env.ContentRootPath, "image_cache");
|
|
}
|
|
|
|
[HttpGet("{*filePath}")]
|
|
public async Task<IActionResult> GetImage(
|
|
string filePath,
|
|
[FromQuery] int? width = null,
|
|
[FromQuery] int? height = null,
|
|
[FromQuery] string? cropSquare = null,
|
|
[FromQuery] string? sig = null)
|
|
{
|
|
if (string.IsNullOrEmpty(filePath)) return BadRequest();
|
|
|
|
if (sig != null && sig != "p1") return BadRequest();
|
|
|
|
bool crop = cropSquare?.ToLower() switch
|
|
{
|
|
"1" => true,
|
|
"true" => true,
|
|
_ => false
|
|
};
|
|
|
|
string cacheKey = GenerateCacheKey(filePath, width, height, crop);
|
|
string cachedFilePath = Path.Combine(_cachePath, cacheKey + ".png");
|
|
byte[] imageBytes;
|
|
|
|
if (System.IO.File.Exists(cachedFilePath))
|
|
{
|
|
imageBytes = await System.IO.File.ReadAllBytesAsync(cachedFilePath);
|
|
}
|
|
else
|
|
{
|
|
string cdnUrl = $"{CdnService.Url.TrimEnd('/')}/img/{filePath}";
|
|
|
|
try
|
|
{
|
|
var response = await httpClient.GetAsync(cdnUrl);
|
|
if (!response.IsSuccessStatusCode) return NotFound();
|
|
|
|
using var stream = await response.Content.ReadAsStreamAsync();
|
|
using var original = SKBitmap.Decode(stream);
|
|
|
|
if (original == null) return await ServeRawFallback(cdnUrl, sig, filePath);
|
|
|
|
SKBitmap currentBitmap = original;
|
|
|
|
if (crop)
|
|
{
|
|
int size = Math.Min(original.Width, original.Height);
|
|
int x = (original.Width - size) / 2;
|
|
int y = (original.Height - size) / 2;
|
|
|
|
var subset = new SKBitmap(size, size);
|
|
original.ExtractSubset(subset, new SKRectI(x, y, x + size, y + size));
|
|
currentBitmap = subset;
|
|
}
|
|
|
|
if (width.HasValue || height.HasValue)
|
|
{
|
|
int w = width ?? (int)(currentBitmap.Width * ((float)height! / currentBitmap.Height));
|
|
int h = height ?? (int)(currentBitmap.Height * ((float)width! / currentBitmap.Width));
|
|
|
|
var info = new SKImageInfo(w, h);
|
|
var resized = new SKBitmap(info);
|
|
currentBitmap.ScalePixels(resized, SKSamplingOptions.Default);
|
|
|
|
if (currentBitmap != original) currentBitmap.Dispose();
|
|
currentBitmap = resized;
|
|
}
|
|
|
|
using (var image = SKImage.FromBitmap(currentBitmap))
|
|
using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
|
|
{
|
|
imageBytes = data.ToArray();
|
|
}
|
|
|
|
if (currentBitmap != original) currentBitmap.Dispose();
|
|
|
|
if (!Directory.Exists(_cachePath)) Directory.CreateDirectory(_cachePath);
|
|
await System.IO.File.WriteAllBytesAsync(cachedFilePath, imageBytes);
|
|
}
|
|
catch
|
|
{
|
|
return await ServeRawFallback(cdnUrl, sig, filePath);
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(sig))
|
|
{
|
|
SignPayloadAndAppendHeader(imageBytes, sig);
|
|
}
|
|
|
|
return File(imageBytes, "image/png");
|
|
}
|
|
|
|
private async Task<IActionResult> ServeRawFallback(string url, string? sig, string? filename)
|
|
{
|
|
try
|
|
{
|
|
var response = await httpClient.GetAsync(url);
|
|
if (!response.IsSuccessStatusCode) return NotFound();
|
|
|
|
byte[] rawBytes = await response.Content.ReadAsByteArrayAsync();
|
|
|
|
if (!string.IsNullOrEmpty(sig))
|
|
{
|
|
SignPayloadAndAppendHeader(Encoding.UTF8.GetBytes(filename), sig);
|
|
}
|
|
|
|
string contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
|
|
return File(rawBytes, contentType);
|
|
}
|
|
catch
|
|
{
|
|
return StatusCode(500, "Error pipeline processing fallback asset safely.");
|
|
}
|
|
}
|
|
|
|
private void SignPayloadAndAppendHeader(byte[] payload, string sig)
|
|
{
|
|
using var rsa = RSA.Create();
|
|
rsa.ImportParameters(ServerConfig.RsaParams);
|
|
|
|
byte[] signatureBytes = rsa.SignData(payload, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
|
|
string base64Signature = Convert.ToBase64String(signatureBytes);
|
|
|
|
Response.Headers.Append("Content-Signature", $"key-id=KEY:RSA:{sig}.rec.net; data={base64Signature};");
|
|
}
|
|
|
|
private static string GenerateCacheKey(string path, int? w, int? h, bool crop)
|
|
{
|
|
string rawKey = $"{path}_{w}_{h}_{crop}";
|
|
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawKey));
|
|
return Convert.ToHexString(hashBytes);
|
|
}
|
|
}
|
|
} |