mirror of
https://github.com/CannedNet/CannedNet.git
synced 2026-10-27 16:09:03 -07:00
sigs
This commit is contained in:
@@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user