This commit is contained in:
Lapis
2026-06-04 20:26:43 -04:00
parent 01277dfcbe
commit 98fdf6cd24
3 changed files with 116 additions and 40 deletions
+1
View File
@@ -6,3 +6,4 @@ riderModule.iml
/.idea /.idea
/CannedNet.API/Migrations /CannedNet.API/Migrations
CannedNet.sln.DotSettings.user CannedNet.sln.DotSettings.user
CannedNet/Data/PrivateKey.pem
@@ -1,14 +1,26 @@
using System.Net.Mime; using System.Net.Mime;
using CannedNet.Services.Infrastructure; using CannedNet.Services.Infrastructure;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
namespace CannedNet.Services.Controllers; namespace CannedNet.Services.Controllers;
public class ImageController public class ImageController
{ {
private static readonly HttpClient HttpClient = new(); private static readonly byte[] PlaceholderJpeg;
static ImageController()
{
Signatures.Init();
using var image = new Image<Rgba32>(64, 64);
image.Mutate(x => x.BackgroundColor(Color.FromRgb(32, 32, 32)));
using var ms = new MemoryStream();
image.SaveAsJpeg(ms, new JpegEncoder { Quality = 50 });
PlaceholderJpeg = ms.ToArray();
}
public WebApplicationBuilder Initialize(string[]? args = null) public WebApplicationBuilder Initialize(string[]? args = null)
=> ServiceExtensions.CreateRecNetBuilder(args); => ServiceExtensions.CreateRecNetBuilder(args);
@@ -16,67 +28,84 @@ public class ImageController
public void MapEndpoints(WebApplication app) public void MapEndpoints(WebApplication app)
{ {
var imagesDir = Path.Combine("Images"); var imagesDir = Path.Combine("Images");
Directory.CreateDirectory(imagesDir); Directory.CreateDirectory(imagesDir);
app.MapGet("/{imageName}", async ( app.MapGet("/{imageName}", async (HttpContext context, string imageName) =>
HttpContext context,
string imageName
) =>
{ {
byte[] imageBytes;
var filePath = Path.Combine(imagesDir, imageName); var filePath = Path.Combine(imagesDir, imageName);
byte[] imageBytes;
string contentType;
if (File.Exists(filePath)) if (File.Exists(filePath))
{ {
imageBytes = await File.ReadAllBytesAsync(filePath); imageBytes = await File.ReadAllBytesAsync(filePath);
var ext = Path.GetExtension(imageName).ToLowerInvariant();
contentType = ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".webp" => "image/webp",
".bmp" => "image/bmp",
_ => "image/png"
};
} }
else else
{ {
var response = await HttpClient.GetAsync( imageBytes = PlaceholderJpeg;
$"https://cdn.rec.net/img/{imageName}" contentType = "image/jpeg";
);
if (!response.IsSuccessStatusCode)
return Results.NotFound();
imageBytes = await response.Content.ReadAsByteArrayAsync();
} }
int? width = null; var cropSquare = context.Request.Query["cropSquare"].FirstOrDefault();
int? height = null; var widthStr = context.Request.Query["width"].FirstOrDefault();
var heightStr = context.Request.Query["height"].FirstOrDefault();
if (int.TryParse(context.Request.Query["width"], out var w)) if (string.IsNullOrEmpty(widthStr) && string.IsNullOrEmpty(heightStr) && string.IsNullOrEmpty(cropSquare))
width = w;
if (int.TryParse(context.Request.Query["height"], out var h))
height = h;
if (width == null && height == null)
{ {
return Results.File(imageBytes, MediaTypeNames.Image.Png); SignImageResponse(context, ref imageBytes);
return Results.File(imageBytes, contentType);
} }
using var image = Image.Load(imageBytes); using var image = Image.Load(imageBytes);
image.Mutate(x => x.Resize(new ResizeOptions var resizeWidth = 0;
var resizeHeight = 0;
if (!string.IsNullOrEmpty(cropSquare) && cropSquare != "0" && cropSquare != "false")
{ {
Size = new Size( var size = Math.Min(image.Width, image.Height);
width ?? 0, var x = (image.Width - size) / 2;
height ?? 0 var y = (image.Height - size) / 2;
), image.Mutate(img => img.Crop(new Rectangle(x, y, size, size)));
Mode = ResizeMode.Max }
}));
await using var output = new MemoryStream(); if (int.TryParse(widthStr, out var w))
resizeWidth = w;
await image.SaveAsync(output, new PngEncoder()); if (int.TryParse(heightStr, out var h))
resizeHeight = h;
return Results.File( if (resizeWidth > 0 || resizeHeight > 0)
output.ToArray(), {
MediaTypeNames.Image.Png image.Mutate(x => x.Resize(resizeWidth, resizeHeight));
); }
using var output = new MemoryStream();
await image.SaveAsJpegAsync(output, new JpegEncoder { Quality = 85 });
imageBytes = output.ToArray();
SignImageResponse(context, ref imageBytes);
return Results.File(imageBytes, "image/jpeg");
}); });
} }
private static void SignImageResponse(HttpContext context, ref byte[] imageBytes)
{
if (context.Request.Query["sig"] != "p1") return;
var signature = Signatures.Sign(imageBytes);
if (signature != null)
{
context.Response.Headers["Content-Signature"] = $"key-id=KEY:RSA:p1.rec.net; data={signature}";
}
}
} }
+46
View File
@@ -0,0 +1,46 @@
using System.Security.Cryptography;
using System.Text;
namespace CannedNet.Services;
public static class Signatures
{
private static RSA? _rsa;
private static bool _initialized;
public static void Init()
{
if (_initialized) return;
_initialized = true;
try
{
var pemPath = Path.Combine("Data", "PrivateKey.pem");
if (!File.Exists(pemPath))
{
Console.WriteLine("sigs - creating new sig key");
Directory.CreateDirectory("Data");
using var newKey = RSA.Create(2048);
var pem = newKey.ExportPkcs8PrivateKeyPem();
File.WriteAllText(pemPath, pem);
Console.WriteLine("sigs - key saved to " + pemPath);
}
var pemContent = File.ReadAllText(pemPath);
_rsa = RSA.Create();
_rsa.ImportFromPem(pemContent.ToCharArray());
Console.WriteLine("sigs - key loaded");
}
catch (Exception ex)
{
Console.WriteLine($"sigs - failed to load key: {ex.Message}");
}
}
public static string? Sign(byte[] data)
{
if (_rsa == null) return null;
var signatureBytes = _rsa.SignData(data, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signatureBytes);
}
}