XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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 ActualNosniffHeaderAndBrowserIdentityReachTheTransportBoundary()
    {
        string requestText = "";
        using var server = new TestHttpServer((request, _) => { requestText = request; return Response("actual", "X-Content-Type-Options: NoSniff\r\n"); });
        using var transport = new HttpWebTransport();
        Assert.True(transport.Get(new(server.Address), new()).NoSniff);
        Assert.Contains("User-Agent: " + BrowserIdentity.UserAgent, requestText, StringComparison.Ordinal);
        var page = new BrowserService();
        try { page.Navigate("about:Start"); Assert.Equal(BrowserIdentity.UserAgent, page.ExecuteJavaScript("navigator.userAgent")); }
        finally { page.Release(); }
        await server.WaitForRequests(1);
    }
    [Fact]
    public async Task ScriptHeadersReachTheWireButForbiddenFieldsAndCrossOriginRedirectsDoNot()
    {
        string captured = ""; bool escaped = false;
        using var other = new TestHttpServer((_, _) => { escaped = true; return Response("foreign"); });
        using var server = new TestHttpServer((request, _) =>
        {
            captured = request;
            return request.StartsWith("GET /Redirect ", StringComparison.Ordinal)
                ? "HTTP/1.1 302 Found\r\nLocation: " + other.Address + "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" : Response("actual");
        });
        using var transport = new HttpWebTransport();
        transport.Get(new(server.Address), new(), new Dictionary<string, string> { ["X-Autosuggest-ContentWidth"] = "800", ["Content-Type"] = "application/json", ["Cookie"] = "forged", ["Host"] = "forged.test" });
        Assert.Contains("X-Autosuggest-ContentWidth: 800", captured, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("Content-Type: application/json", captured, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("forged", captured, StringComparison.Ordinal);
        Assert.Throws<NotSupportedException>(() => transport.Get(new(server.Address + "Redirect"), new(), new Dictionary<string, string> { ["Authorization"] = "test-only" }));
        await server.WaitForRequests(2); Assert.False(escaped);
        Assert.Throws<ArgumentException>(() => BrowserRequestHeaders.Copy(new Dictionary<string, string> { ["X-Test"] = "a\r\nInjected: value" }));
    }
    [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("<title>Loopback</title><p id='Value'>Before</p><script>document.getElementById('Value').textContent = 'Network JS ' + (6 * 7);</script>"));
        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("<p id='Value'>Original</p><script>document.getElementById('Value').textContent='Forbidden'</script>",
            "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("<p>Must not be trusted</p>")), 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<string, int, string> response)
        {
            listener.Start();
            Address = $"http://127.0.0.1:{((IPEndPoint)listener.LocalEndpoint).Port}/";
            serving = Task.Run(async () =>
            {
                while (!cancellation.IsCancellationRequested)
                {
                    try
                    {
                        using var client = await listener.AcceptTcpClientAsync(cancellation.Token);
                        await using var stream = client.GetStream();
                        var header = new StringBuilder();
                        var one = new byte[1];
                        while (header.Length < 8_192 && !header.ToString().EndsWith("\r\n\r\n", StringComparison.Ordinal))
                        {
                            if (await stream.ReadAsync(one, cancellation.Token) == 0) break;
                            header.Append((char)one[0]);
                        }
                        var count = Interlocked.Increment(ref requests);
                        var body = Encoding.UTF8.GetBytes(response(header.ToString(), count));
                        await stream.WriteAsync(body, cancellation.Token);
                    }
                    catch (Exception exception) when (exception is OperationCanceledException or SocketException or IOException) { break; }
                    // Stop() may dispose the Windows accept socket after the
                    // loop condition but before AcceptAsync registers its wait.
                    catch (ObjectDisposedException) when (cancellation.IsCancellationRequested) { break; }
                }
            }, cancellation.Token);
        }

        public string Address { get; }

        public async Task WaitForRequests(int count)
        {
            var deadline = DateTime.UtcNow.AddSeconds(5);
            while (Volatile.Read(ref requests) < count && DateTime.UtcNow < deadline)
                await Task.Delay(10);
            Assert.Equal(count, Volatile.Read(ref requests));
        }

        public void Dispose()
        {
            cancellation.Cancel();
            listener.Stop();
            try { serving.GetAwaiter().GetResult(); } catch (OperationCanceledException) { }
            cancellation.Dispose();
        }
    }
}