using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; namespace XFESystem.Browser.Tests; public sealed class HttpWebTransportTests { [Fact] public async Task ActualLoopbackHttpRedirectAndHtmlJavaScriptWorkThroughTheExplicitTransport() { using var server = new TestHttpServer((request, _) => request.StartsWith("GET /Start ", StringComparison.Ordinal) ? "HTTP/1.1 302 Found\r\nLocation: /Page\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" : Response("
Before
")); using var transport = new HttpWebTransport(); var page = new BrowserService(transport).Navigate(server.Address + "Start"); Assert.Equal("Loopback", page.Title); Assert.Equal("Network JS 42", page.Text); Assert.EndsWith("/Page", page.Address, StringComparison.Ordinal); await server.WaitForRequests(2); } [Fact] public async Task RedirectLoopsAndOversizedBodiesAreRejected() { using var redirect = new TestHttpServer((_, _) => "HTTP/1.1 302 Found\r\nLocation: /Again\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); using var transport = new HttpWebTransport(); var page = new BrowserService(transport, new BrowserLimits { MaximumRedirects = 1 }).Navigate(redirect.Address); Assert.Contains("redirect limit", page.Text, StringComparison.Ordinal); await redirect.WaitForRequests(2); using var oversized = new TestHttpServer((_, _) => Response(new string('X', 2_000))); var limited = new BrowserService(transport, new BrowserLimits { MaximumHtmlCharacters = 100 }); Assert.Equal("Navigation failed", limited.Navigate(oversized.Address).Title); await oversized.WaitForRequests(1); } [Fact] public async Task HttpCspHeaderPreventsAutomaticInlineScriptExecution() { using var server = new TestHttpServer((_, _) => Response("Original
", "Content-Security-Policy: script-src 'none'\r\n")); using var transport = new HttpWebTransport(); var page = new BrowserService(transport).Navigate(server.Address); Assert.Contains("Original", page.Text, StringComparison.Ordinal); Assert.DoesNotContain("Forbidden", page.Text, StringComparison.Ordinal); Assert.Contains("Automatic scripts blocked", page.Text, StringComparison.Ordinal); await server.WaitForRequests(1); } [Fact] public async Task ActualTlsConnectionRejectsAnUntrustedCertificate() { using var key = RSA.Create(2048); var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var san = new SubjectAlternativeNameBuilder(); san.AddDnsName("localhost"); san.AddIpAddress(IPAddress.Loopback); request.CertificateExtensions.Add(san.Build()); using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddMinutes(5)); var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var serving = Task.Run(async () => { using var client = await listener.AcceptTcpClientAsync(cancellation.Token); await using var tls = new SslStream(client.GetStream()); try { await tls.AuthenticateAsServerAsync(new SslServerAuthenticationOptions { ServerCertificate = certificate, EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 }, cancellation.Token); await tls.WriteAsync(Encoding.UTF8.GetBytes(Response("Must not be trusted
")), cancellation.Token); } catch (Exception exception) when (exception is AuthenticationException or IOException) { } }, cancellation.Token); try { using var transport = new HttpWebTransport(); var page = new BrowserService(transport, new BrowserLimits { NetworkTimeout = TimeSpan.FromSeconds(5) }) .Navigate($"https://127.0.0.1:{port}/"); Assert.Equal("Navigation failed", page.Title); Assert.DoesNotContain("Must not be trusted", page.Text, StringComparison.Ordinal); await serving.WaitAsync(TimeSpan.FromSeconds(8)); } finally { listener.Stop(); } } private static string Response(string html, string headers = "") => "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n" + headers + $"Content-Length: {Encoding.UTF8.GetByteCount(html)}\r\nConnection: close\r\n\r\n" + html; private sealed class TestHttpServer : IDisposable { private readonly TcpListener listener = new(IPAddress.Loopback, 0); private readonly CancellationTokenSource cancellation = new(TimeSpan.FromSeconds(10)); private readonly Task serving; private int requests; public TestHttpServer(Func