Add remote image validation for screenshot URLs
This commit is contained in:
@@ -44,6 +44,86 @@ internal static class EndpointHelpers
|
||||
|| path.EndsWith(".gif") || path.EndsWith(".webp") || path.EndsWith(".avif");
|
||||
}
|
||||
|
||||
public static async Task<bool> IsReachableImageAsync(string? url, IHttpClientFactory httpFactory, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return true;
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
|
||||
if (uri.Scheme is not ("http" or "https")) return false;
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(3));
|
||||
|
||||
var client = httpFactory.CreateClient();
|
||||
|
||||
try
|
||||
{
|
||||
using var head = new HttpRequestMessage(HttpMethod.Head, uri);
|
||||
var headResp = await client.SendAsync(head, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
if (headResp.IsSuccessStatusCode)
|
||||
{
|
||||
var ctHeader = headResp.Content.Headers.ContentType?.MediaType;
|
||||
if (!string.IsNullOrWhiteSpace(ctHeader) && ctHeader.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { /* fallback */ }
|
||||
|
||||
try
|
||||
{
|
||||
using var get = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
get.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(0, 1023);
|
||||
var resp = await client.SendAsync(get, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
if (!resp.IsSuccessStatusCode) return false;
|
||||
|
||||
var ctHeader = resp.Content.Headers.ContentType?.MediaType;
|
||||
if (!string.IsNullOrWhiteSpace(ctHeader) && ctHeader.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
await using var stream = await resp.Content.ReadAsStreamAsync(cts.Token);
|
||||
Span<byte> buffer = stackalloc byte[12];
|
||||
var read = await stream.ReadAsync(buffer, cts.Token);
|
||||
var sig = buffer[..read];
|
||||
|
||||
if (IsMagic(sig, "PNG")) return true;
|
||||
if (IsMagic(sig, new byte[] { 0xFF, 0xD8 })) return true; // JPEG
|
||||
if (IsMagic(sig, "GIF8")) return true;
|
||||
if (IsRiffWithTag(sig, "WEBP")) return true;
|
||||
if (ContainsFtyp(sig, "avif")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMagic(ReadOnlySpan<byte> data, string ascii)
|
||||
{
|
||||
var bytes = System.Text.Encoding.ASCII.GetBytes(ascii);
|
||||
return data.StartsWith(bytes);
|
||||
}
|
||||
|
||||
private static bool IsMagic(ReadOnlySpan<byte> data, ReadOnlySpan<byte> magic) => data.StartsWith(magic);
|
||||
|
||||
private static bool IsRiffWithTag(ReadOnlySpan<byte> data, string tag)
|
||||
{
|
||||
if (data.Length < 12) return false;
|
||||
var riff = System.Text.Encoding.ASCII.GetBytes("RIFF");
|
||||
if (!data.StartsWith(riff)) return false;
|
||||
var tagBytes = System.Text.Encoding.ASCII.GetBytes(tag);
|
||||
return data[8..].StartsWith(tagBytes);
|
||||
}
|
||||
|
||||
private static bool ContainsFtyp(ReadOnlySpan<byte> data, string brand)
|
||||
{
|
||||
if (data.Length < 12) return false;
|
||||
var ftyp = System.Text.Encoding.ASCII.GetBytes("ftyp");
|
||||
if (!data[4..].StartsWith(ftyp)) return false;
|
||||
var brandBytes = System.Text.Encoding.ASCII.GetBytes(brand);
|
||||
return data[8..].StartsWith(brandBytes);
|
||||
}
|
||||
|
||||
public static async Task<bool> IsAdmin(HttpContext ctx, AppDbContext db, IConfiguration config)
|
||||
{
|
||||
var player = await GetAuthenticatedPlayer(ctx, db);
|
||||
|
||||
Reference in New Issue
Block a user