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 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 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); } } }