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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text;
using XFESystem.Browser;
using XFESystem.Kernel;

namespace XFESystem.HttpProtocol.Tests;

public sealed class NativeHttpProtocolTests
{
    [Fact]
    public void ContentLengthIsAByteLengthAndUtf8ChineseDecodesCorrectly()
    {
        const string html = "<p>中文 42</p>";
        var response = Parse("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " +
            Encoding.UTF8.GetByteCount(html) + "\r\n\r\n" + html);
        Assert.Equal(200, response.StatusCode);
        Assert.Equal(html, NativeHttpProtocol.DecodeBody(response));
    }

    [Fact]
    public void ArbitraryPacketBoundariesDoNotChangeHeadersOrBody()
    {
        const string wire = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Security-Policy: script-src 'none'\r\n\r\nHello";
        for (var packetSize = 1; packetSize <= Encoding.UTF8.GetByteCount(wire); packetSize++)
        {
            var response = Parse(wire, packetSize: packetSize);
            Assert.Equal("Hello", NativeHttpProtocol.DecodeBody(response));
            Assert.Equal("script-src 'none'", response.ContentSecurityPolicy);
        }
    }

    [Fact]
    public void ChunkedBodySupportsExtensionsAndBoundedTrailers()
    {
        var response = Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: Chunked\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n4;Name=Value\r\nWiki\r\n5\r\npedia\r\n0\r\nChecksum: 42\r\n\r\n", packetSize: 1);
        Assert.Equal("Wikipedia", NativeHttpProtocol.DecodeBody(response));
    }

    [Fact]
    public void CloseDelimitedBodyReadsUntilRealEof()
    {
        var response = Parse("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\nClose-delimited", packetSize: 2);
        Assert.Equal("Close-delimited", NativeHttpProtocol.DecodeBody(response));
    }

    [Fact]
    public void InterimResponsesAreBoundedAndDoNotLoseFollowingBytes()
    {
        var response = Parse("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
        Assert.Equal("OK", NativeHttpProtocol.DecodeBody(response));
        Assert.Throws<IOException>(() => Parse(string.Concat(Enumerable.Repeat("HTTP/1.1 100 Continue\r\n\r\n", 5))));
    }

    [Theory]
    [InlineData("Content-Length: 5\r\nTransfer-Encoding: chunked\r\n")]
    [InlineData("Content-Length: 5\r\nContent-Length: 5\r\n")]
    [InlineData("Transfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n")]
    [InlineData(" Content-Length: 5\r\n")]
    [InlineData("Bad Name: Value\r\n")]
    public void AmbiguousDuplicateAndFoldedFramingIsRejected(string headers)
    {
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\n" + headers + "\r\nHello"));
    }

    [Theory]
    [InlineData("Content-Length: -1\r\n")]
    [InlineData("Content-Length: +1\r\n")]
    [InlineData("Content-Length: 1x\r\n")]
    [InlineData("Content-Length: 0x10\r\n")]
    public void InvalidLengthsAreRejected(string headers)
    {
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\n" + headers + "\r\n"));
    }

    [Fact]
    public void OversizedBodiesFailBeforeAnUnboundedAllocation()
    {
        Assert.Throws<BrowserLimitException>(() => Parse("HTTP/1.1 200 OK\r\nContent-Length: 101\r\n\r\n", maximum: 100));
        Assert.Throws<BrowserLimitException>(() => Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n65\r\n", maximum: 100));
        Assert.Throws<BrowserLimitException>(() => Parse("HTTP/1.0 200 OK\r\n\r\n" + new string('X', 101), maximum: 100));
        Assert.Throws<BrowserLimitException>(() => Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\n1234\r\n4\r\n5678\r\n0\r\n\r\n", maximum: 6));
    }

    [Fact]
    public void PrematureEofAndMissingChunkCrLfAreNotReportedAsSuccess()
    {
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHi"));
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWikiX\n0\r\n\r\n"));
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWi"));
    }

    [Fact]
    public void ForbiddenTrailerFieldsAndUnsupportedCodingsAreRejected()
    {
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nContent-Length: 0\r\n\r\n"));
        Assert.Throws<NotSupportedException>(() => Parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip, chunked\r\n\r\n"));
        Assert.Throws<NotSupportedException>(() => Parse("HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: 0\r\n\r\n"));
        Assert.Throws<NotSupportedException>(() => Parse("HTTP/1.1 101 Upgrade\r\n\r\n"));
    }

    [Fact]
    public void HeaderLineTotalAndCountLimitsAreEnforced()
    {
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\nX: " + new string('A', 4_096) + "\r\n\r\n"));
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\n" + string.Concat(Enumerable.Range(0, 101).Select(index => $"X{index}: A\r\n")) + "\r\n"));
        Assert.Throws<IOException>(() => Parse("HTTP/1.1 200 OK\r\n" + string.Concat(Enumerable.Range(0, 10).Select(index => $"X{index}: {new string('A', 2_000)}\r\n")) + "\r\n"));
    }

    [Fact]
    public void RedirectAndNoContentResponsesReturnWithoutReadingAnUnneededBody()
    {
        var redirected = Parse("HTTP/1.1 302 Found\r\nLocation: ../Next\r\nContent-Length: 100\r\n\r\n");
        Assert.Equal(302, redirected.StatusCode);
        Assert.Equal("../Next", redirected.Location);
        Assert.Empty(redirected.Body);
        Assert.Empty(Parse("HTTP/1.1 204 No Content\r\n\r\n").Body);
    }

    [Theory]
    [InlineData("HTTP/2 200 OK\r\n\r\n")]
    [InlineData("HTTP/1.1 20A OK\r\n\r\n")]
    [InlineData("HTTP/1.1 600 Wrong\r\n\r\n")]
    [InlineData("HTTP/1.1 200 OK\n\n")]
    [InlineData("HTTP/1.1 200 OK\r\nX: \u001b[31m\r\n\r\n")]
    public void InvalidStatusAndHeaderControlBytesAreRejected(string wire)
    {
        Assert.Throws<IOException>(() => Parse(wire));
    }

    [Fact]
    public void CharsetSelectionIsExplicitAndUnsupportedCharsetsFail()
    {
        var response = Parse("HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=ascii\r\nContent-Length: 2\r\n\r\nOK");
        Assert.Equal("OK", NativeHttpProtocol.DecodeBody(response));
        Assert.Throws<NotSupportedException>(() => NativeHttpProtocol.DecodeBody(response with { ContentType = "text/plain; charset=gbk" }));
    }

    private static NativeHttpResponse Parse(string wire, int maximum = 10_000, int packetSize = 4_096)
    {
        var bytes = Encoding.UTF8.GetBytes(wire);
        var position = 0;
        return NativeHttpProtocol.ReadResponse((target, offset, count) =>
        {
            var read = Math.Min(Math.Min(count, packetSize), bytes.Length - position);
            Array.Copy(bytes, position, target, offset, read);
            position += read;
            return read;
        }, maximum);
    }
}