using System.Net; using System.Net.Http.Headers; namespace Nuuru.Server.Services; internal static class RemoteFetchSupport { private static readonly MediaTypeWithQualityHeaderValue[] DocumentAcceptHeaders = [ new("text/html"), new("application/xhtml+xml"), new("application/xml", 0.9), new("*/*", 0.8) ]; private static readonly MediaTypeWithQualityHeaderValue[] ImageAcceptHeaders = [ new("image/avif"), new("image/webp"), new("image/png"), new("image/svg+xml"), new("image/*", 0.8), new("*/*", 0.5) ]; public static void ApplyBrowserHeaders(HttpRequestMessage request, Uri targetUri, bool isImageRequest) { request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0"); request.Headers.TryAddWithoutValidation("Accept-Language", "en-US,en;q=0.9"); if (isImageRequest) { request.Headers.Referrer = new Uri($"{targetUri.Scheme}://{targetUri.Host}/"); foreach (var header in ImageAcceptHeaders) request.Headers.Accept.Add(header); request.Headers.TryAddWithoutValidation("Sec-Fetch-Storage-Access", "none"); request.Headers.TryAddWithoutValidation("Sec-Fetch-Dest", "image"); request.Headers.TryAddWithoutValidation("Sec-Fetch-Mode", "no-cors"); request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "cross-site"); return; } foreach (var header in DocumentAcceptHeaders) request.Headers.Accept.Add(header); request.Headers.TryAddWithoutValidation("Upgrade-Insecure-Requests", "1"); request.Headers.TryAddWithoutValidation("Sec-Fetch-Dest", "document"); request.Headers.TryAddWithoutValidation("Sec-Fetch-Mode", "navigate"); request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "none"); request.Headers.TryAddWithoutValidation("Sec-Fetch-User", "?1"); } public static async Task IsSafeRemoteUriAsync(Uri uri, CancellationToken cancellationToken) { if (!uri.IsAbsoluteUri || uri.Scheme is not ("http" or "https")) return false; if (uri.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) return false; if (IPAddress.TryParse(uri.Host, out var ipAddress)) return !IsPrivateAddress(ipAddress); try { var addresses = await Dns.GetHostAddressesAsync(uri.Host, cancellationToken); return addresses.Length > 0 && addresses.All(address => !IsPrivateAddress(address)); } catch { return false; } } public static async Task ReadBytesUpToAsync(Stream stream, int maxBytes, CancellationToken cancellationToken) { using var buffer = new MemoryStream(); var chunk = new byte[8192]; while (buffer.Length <= maxBytes) { var bytesToRead = Math.Min(chunk.Length, maxBytes + 1 - (int)buffer.Length); if (bytesToRead <= 0) break; var read = await stream.ReadAsync(chunk.AsMemory(0, bytesToRead), cancellationToken); if (read == 0) break; await buffer.WriteAsync(chunk.AsMemory(0, read), cancellationToken); } return buffer.Length > maxBytes ? null : buffer.ToArray(); } private static bool IsPrivateAddress(IPAddress address) { if (IPAddress.IsLoopback(address)) return true; if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { var bytes = address.GetAddressBytes(); return bytes[0] == 10 || (bytes[0] == 172 && bytes[1] is >= 16 and <= 31) || (bytes[0] == 192 && bytes[1] == 168) || bytes[0] == 127 || (bytes[0] == 169 && bytes[1] == 254); } if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) { return address.IsIPv6LinkLocal || address.IsIPv6Multicast || address.IsIPv6SiteLocal || address.Equals(IPAddress.IPv6Loopback) || (address.GetAddressBytes()[0] & 0xFE) == 0xFC; } return true; } }