using System.Net;
using System.Text;
namespace XFESystem.Browser;
/// <summary>
/// Explicit HttpClient adapter for a runtime with working sockets, TLS, clock
/// and trust roots. It is never selected as an implicit bare-metal fallback.
/// </summary>
public sealed class HttpWebTransport : IWebTransport, IWebTransportCapabilities, IDisposable
{
public bool IsNative => false;
public bool SupportsHttp => true;
public bool SupportsHttps => true;
private readonly HttpClient client = new(new SocketsHttpHandler
{
AllowAutoRedirect = false,
UseCookies = false,
AutomaticDecompression = DecompressionMethods.All
}) { Timeout = Timeout.InfiniteTimeSpan };
public WebResponse Get(Uri address, BrowserLimits limits)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(limits);
limits.Validate();
BrowserService.ValidateWebAddress(address);
using var cancellation = new CancellationTokenSource(limits.NetworkTimeout);
var target = address;
for (var redirects = 0; ; redirects++)
{
using var request = new HttpRequestMessage(HttpMethod.Get, target);
request.Headers.UserAgent.ParseAdd("XFESystem-Browser/0.3");
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,text/plain,text/markdown");
using var response = client.Send(request, HttpCompletionOption.ResponseHeadersRead, cancellation.Token);
var status = (int)response.StatusCode;
if (status is 301 or 302 or 303 or 307 or 308)
{
if (redirects >= limits.MaximumRedirects)
throw new InvalidOperationException("The redirect limit was reached.");
var location = response.Headers.Location ?? throw new InvalidOperationException("The redirect has no Location header.");
var next = location.IsAbsoluteUri ? location : new Uri(target, location);
BrowserService.ValidateWebAddress(next);
if (target.Scheme == "https" && next.Scheme != "https")
throw new InvalidOperationException("HTTPS cannot redirect to HTTP.");
target = next;
continue;
}
if (response.Content.Headers.ContentLength > (long)limits.MaximumHtmlCharacters * 4)
throw new BrowserLimitException("The HTTP body exceeds its byte limit.");
var charset = response.Content.Headers.ContentType?.CharSet?.Trim('"') ?? "utf-8";
Encoding encoding;
try { encoding = Encoding.GetEncoding(charset); }
catch (ArgumentException) { throw new NotSupportedException("The response charset is unsupported: " + charset); }
using var stream = response.Content.ReadAsStream(cancellation.Token);
using var reader = new StreamReader(stream, encoding, true, 4_096);
var text = new BoundedText(limits.MaximumHtmlCharacters);
var buffer = new char[4_096];
while (true)
{
var read = reader.ReadAsync(buffer.AsMemory(), cancellation.Token).AsTask().GetAwaiter().GetResult();
if (read == 0) break;
text.Append(new string(buffer, 0, read));
}
// HttpClient's default TLS handler validates trust chain, hostname and validity.
// No custom certificate callback is installed by this adapter.
return new(target, status, response.Content.Headers.ContentType?.ToString() ?? string.Empty,
text.ToString(), target.Scheme == "https", response.Headers.TryGetValues("Content-Security-Policy", out var policies)
? string.Join("; ", policies) : null);
}
}
public void Dispose() => client.Dispose();
}
using System.Net;
using System.Text;
namespace XFESystem.Browser;
/// <summary>
/// Explicit HttpClient adapter for a runtime with working sockets, TLS, clock
/// and trust roots. It is never selected as an implicit bare-metal fallback.
/// </summary>
public sealed class HttpWebTransport : IWebTransport, IWebTransportCapabilities, IDisposable
{
public bool IsNative => false;
public bool SupportsHttp => true;
public bool SupportsHttps => true;
private readonly HttpClient client = new(new SocketsHttpHandler
{
AllowAutoRedirect = false,
UseCookies = false,
AutomaticDecompression = DecompressionMethods.All
}) { Timeout = Timeout.InfiniteTimeSpan };
public WebResponse Get(Uri address, BrowserLimits limits)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(limits);
limits.Validate();
BrowserService.ValidateWebAddress(address);
using var cancellation = new CancellationTokenSource(limits.NetworkTimeout);
var target = address;
for (var redirects = 0; ; redirects++)
{
using var request = new HttpRequestMessage(HttpMethod.Get, target);
request.Headers.UserAgent.ParseAdd("XFESystem-Browser/0.3");
request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,text/plain,text/markdown");
using var response = client.Send(request, HttpCompletionOption.ResponseHeadersRead, cancellation.Token);
var status = (int)response.StatusCode;
if (status is 301 or 302 or 303 or 307 or 308)
{
if (redirects >= limits.MaximumRedirects)
throw new InvalidOperationException("The redirect limit was reached.");
var location = response.Headers.Location ?? throw new InvalidOperationException("The redirect has no Location header.");
var next = location.IsAbsoluteUri ? location : new Uri(target, location);
BrowserService.ValidateWebAddress(next);
if (target.Scheme == "https" && next.Scheme != "https")
throw new InvalidOperationException("HTTPS cannot redirect to HTTP.");
target = next;
continue;
}
if (response.Content.Headers.ContentLength > (long)limits.MaximumHtmlCharacters * 4)
throw new BrowserLimitException("The HTTP body exceeds its byte limit.");
var charset = response.Content.Headers.ContentType?.CharSet?.Trim('"') ?? "utf-8";
Encoding encoding;
try { encoding = Encoding.GetEncoding(charset); }
catch (ArgumentException) { throw new NotSupportedException("The response charset is unsupported: " + charset); }
using var stream = response.Content.ReadAsStream(cancellation.Token);
using var reader = new StreamReader(stream, encoding, true, 4_096);
var text = new BoundedText(limits.MaximumHtmlCharacters);
var buffer = new char[4_096];
while (true)
{
var read = reader.ReadAsync(buffer.AsMemory(), cancellation.Token).AsTask().GetAwaiter().GetResult();
if (read == 0) break;
text.Append(new string(buffer, 0, read));
}
// HttpClient's default TLS handler validates trust chain, hostname and validity.
// No custom certificate callback is installed by this adapter.
return new(target, status, response.Content.Headers.ContentType?.ToString() ?? string.Empty,
text.ToString(), target.Scheme == "https", response.Headers.TryGetValues("Content-Security-Policy", out var policies)
? string.Join("; ", policies) : null);
}
}
public void Dispose() => client.Dispose();
}