File: StartLineTests.cs
Web Access
Project: src\src\Servers\Kestrel\Core\test\Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj (Microsoft.AspNetCore.Server.Kestrel.Core.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.IO.Pipelines;
using System.Text;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Time.Testing;
using Moq;
 
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
 
public class StartLineTests : IDisposable
{
    private IDuplexPipe Transport { get; }
    private MemoryPool<byte> MemoryPool { get; }
    private Http1Connection Http1Connection { get; }
    private Http1ParsingHandler ParsingHandler { get; }
    private IHttpParser<Http1ParsingHandler> Parser { get; }
 
    [Fact]
    public void InOriginForm()
    {
        var rawTarget = "/path%20with%20spaces?q=123&w=xyzw1";
        var path = "/path with spaces";
        var query = "?q=123&w=xyzw1";
        Http1Connection.Reset();
        // RawTarget, Path, QueryString are null after reset
        Assert.Null(Http1Connection.RawTarget);
        Assert.Null(Http1Connection.Path);
        Assert.Null(Http1Connection.QueryString);
 
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        Assert.NotSame(path, Http1Connection.Path);
        Assert.NotSame(query, Http1Connection.QueryString);
    }
 
    [Fact]
    public void InAuthorityForm()
    {
        var rawTarget = "example.com:1234";
        var path = string.Empty;
        var query = string.Empty;
        Http1Connection.Reset();
        // RawTarget, Path, QueryString are null after reset
        Assert.Null(Http1Connection.RawTarget);
        Assert.Null(Http1Connection.Path);
        Assert.Null(Http1Connection.QueryString);
 
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        // Empty strings, so interned and the same.
        Assert.Same(path, Http1Connection.Path);
        Assert.Same(query, Http1Connection.QueryString);
    }
 
    [Fact]
    public void InAbsoluteForm()
    {
        var rawTarget = "http://localhost/path1?q=123&w=xyzw";
        var path = "/path1";
        var query = "?q=123&w=xyzw";
        Http1Connection.Reset();
        // RawTarget, Path, QueryString are null after reset
        Assert.Null(Http1Connection.RawTarget);
        Assert.Null(Http1Connection.Path);
        Assert.Null(Http1Connection.QueryString);
 
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        Assert.NotSame(path, Http1Connection.Path);
        Assert.NotSame(query, Http1Connection.QueryString);
    }
 
    [Fact]
    public void InAsteriskForm()
    {
        var rawTarget = "*";
        var path = string.Empty;
        var query = string.Empty;
        Http1Connection.Reset();
        // RawTarget, Path, QueryString are null after reset
        Assert.Null(Http1Connection.RawTarget);
        Assert.Null(Http1Connection.Path);
        Assert.Null(Http1Connection.QueryString);
 
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // Asterisk is interned string, so the same.
        Assert.Same(rawTarget, Http1Connection.RawTarget);
        // Empty strings, so interned and the same.
        Assert.Same(path, Http1Connection.Path);
        Assert.Same(query, Http1Connection.QueryString);
    }
 
    [Fact]
    public void DifferentFormsWorkTogether()
    {
        // InOriginForm
        var rawTarget = "/a%20path%20with%20spaces?q=123&w=xyzw12";
        var path = "/a path with spaces";
        var query = "?q=123&w=xyzw12";
        Http1Connection.Reset();
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"POST {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        Assert.NotSame(path, Http1Connection.Path);
        Assert.NotSame(query, Http1Connection.QueryString);
 
        InAuthorityForm();
 
        InOriginForm();
        InAbsoluteForm();
 
        InOriginForm();
        InAsteriskForm();
 
        InAuthorityForm();
        InAsteriskForm();
 
        InAbsoluteForm();
        InAuthorityForm();
 
        InAbsoluteForm();
        InAsteriskForm();
 
        InAbsoluteForm();
        InAuthorityForm();
    }
 
    [Theory]
    [InlineData("/abs/path", "/abs/path", "")]
    [InlineData("/", "/", "")]
    [InlineData("/path", "/path", "")]
    [InlineData("/?q=123&w=xyz", "/", "?q=123&w=xyz")]
    [InlineData("/path?q=123&w=xyz", "/path", "?q=123&w=xyz")]
    [InlineData("/path%20with%20space?q=abc%20123", "/path with space", "?q=abc%20123")]
    public void OriginForms(string rawTarget, string path, string query)
    {
        Http1Connection.Reset();
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        var prevRequestUrl = Http1Connection.RawTarget;
        var prevPath = Http1Connection.Path;
        var prevQuery = Http1Connection.QueryString;
 
        // Identical requests keep same materialized string values
        for (var i = 0; i < 5; i++)
        {
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);
 
            // Parser decodes % encoding in place, so we need to recreate the ROS
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            reader = new SequenceReader<byte>(ros);
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);
 
            // But not the same as the inputs.
 
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            // string.Empty is used for empty strings, so should be the same.
            Assert.True(query.Length == 0 || !ReferenceEquals(query, Http1Connection.QueryString));
 
            // However, materalized strings are reused if generated for previous requests.
 
            Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
            Assert.Same(prevPath, Http1Connection.Path);
            Assert.Same(prevQuery, Http1Connection.QueryString);
 
            prevRequestUrl = Http1Connection.RawTarget;
            prevPath = Http1Connection.Path;
            prevQuery = Http1Connection.QueryString;
        }
 
        // Different OriginForm request changes values
 
        rawTarget = "/path1?q=123&w=xyzw";
        path = "/path1";
        query = "?q=123&w=xyzw";
 
        Http1Connection.Reset();
        ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
        reader = new SequenceReader<byte>(ros);
        Parser.ParseRequestLine(ParsingHandler, ref reader);
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        Assert.NotSame(path, Http1Connection.Path);
        Assert.NotSame(query, Http1Connection.QueryString);
 
        // Not equal previous request.
        Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
        Assert.NotEqual(prevPath, Http1Connection.Path);
        Assert.NotEqual(prevQuery, Http1Connection.QueryString);
 
        DifferentFormsWorkTogether();
    }
 
    [Theory]
    [InlineData("http://localhost/abs/path", "/abs/path", "")]
    [InlineData("https://localhost/abs/path", "/abs/path", "")] // handles mismatch scheme
    [InlineData("https://localhost:22/abs/path", "/abs/path", "")] // handles mismatched ports
    [InlineData("https://differenthost/abs/path", "/abs/path", "")] // handles mismatched hostname
    [InlineData("http://localhost/", "/", "")]
    [InlineData("http://root@contoso.com/path", "/path", "")]
    [InlineData("http://root:password@contoso.com/path", "/path", "")]
    [InlineData("https://localhost/", "/", "")]
    [InlineData("http://localhost", "/", "")]
    [InlineData("http://127.0.0.1/", "/", "")]
    [InlineData("http://[::1]/", "/", "")]
    [InlineData("http://[::1]:8080/", "/", "")]
    [InlineData("http://localhost?q=123&w=xyz", "/", "?q=123&w=xyz")]
    [InlineData("http://localhost/?q=123&w=xyz", "/", "?q=123&w=xyz")]
    [InlineData("http://localhost/path?q=123&w=xyz", "/path", "?q=123&w=xyz")]
    [InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "?q=abc%20123")]
    public void AbsoluteForms(string rawTarget, string path, string query)
    {
        Http1Connection.Reset();
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        var prevRequestUrl = Http1Connection.RawTarget;
        var prevPath = Http1Connection.Path;
        var prevQuery = Http1Connection.QueryString;
 
        // Identical requests keep same materialized string values
        for (var i = 0; i < 5; i++)
        {
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);
 
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
            reader = new SequenceReader<byte>(ros);
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);
 
            // But not the same as the inputs.
 
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            Assert.NotSame(path, Http1Connection.Path);
            // string.Empty is used for empty strings, so should be the same.
            Assert.True(query.Length == 0 || !ReferenceEquals(query, Http1Connection.QueryString));
 
            // However, materalized strings are reused if generated for previous requests.
 
            Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
            Assert.Same(prevPath, Http1Connection.Path);
            Assert.Same(prevQuery, Http1Connection.QueryString);
 
            prevRequestUrl = Http1Connection.RawTarget;
            prevPath = Http1Connection.Path;
            prevQuery = Http1Connection.QueryString;
        }
 
        // Different Absolute Form request changes values
 
        rawTarget = "http://localhost/path1?q=123&w=xyzw";
        path = "/path1";
        query = "?q=123&w=xyzw";
 
        Http1Connection.Reset();
        ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
        reader = new SequenceReader<byte>(ros);
        Parser.ParseRequestLine(ParsingHandler, ref reader);
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        Assert.NotSame(path, Http1Connection.Path);
        Assert.NotSame(query, Http1Connection.QueryString);
 
        // Not equal previous request.
        Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
        Assert.NotEqual(prevPath, Http1Connection.Path);
        Assert.NotEqual(prevQuery, Http1Connection.QueryString);
 
        DifferentFormsWorkTogether();
    }
 
    [Fact]
    public void AsteriskForms()
    {
        var rawTarget = "*";
        var path = string.Empty;
        var query = string.Empty;
 
        Http1Connection.Reset();
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        var prevRequestUrl = Http1Connection.RawTarget;
        var prevPath = Http1Connection.Path;
        var prevQuery = Http1Connection.QueryString;
 
        // Identical requests keep same materialized string values
        for (var i = 0; i < 5; i++)
        {
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);
 
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"OPTIONS {rawTarget} HTTP/1.1\r\n"));
            reader = new SequenceReader<byte>(ros);
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);
 
            // Also same as the inputs (interned strings).
 
            Assert.Same(rawTarget, Http1Connection.RawTarget);
            Assert.Same(path, Http1Connection.Path);
            Assert.Same(query, Http1Connection.QueryString);
 
            // Materalized strings are reused if generated for previous requests.
 
            Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
            Assert.Same(prevPath, Http1Connection.Path);
            Assert.Same(prevQuery, Http1Connection.QueryString);
 
            prevRequestUrl = Http1Connection.RawTarget;
            prevPath = Http1Connection.Path;
            prevQuery = Http1Connection.QueryString;
        }
 
        // Different request changes values (can't be Astrisk Form as all the same)
        rawTarget = "http://localhost/path1?q=123&w=xyzw";
        path = "/path1";
        query = "?q=123&w=xyzw";
 
        Http1Connection.Reset();
        ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"GET {rawTarget} HTTP/1.1\r\n"));
        reader = new SequenceReader<byte>(ros);
        Parser.ParseRequestLine(ParsingHandler, ref reader);
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        Assert.NotSame(path, Http1Connection.Path);
        Assert.NotSame(query, Http1Connection.QueryString);
 
        // Not equal previous request.
        Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
        Assert.NotEqual(prevPath, Http1Connection.Path);
        Assert.NotEqual(prevQuery, Http1Connection.QueryString);
 
        DifferentFormsWorkTogether();
    }
 
    [Theory]
    [InlineData("localhost", "", "")]
    [InlineData("localhost:22", "", "")] // handles mismatched ports
    [InlineData("differenthost", "", "")] // handles mismatched hostname
    [InlineData("different-host", "", "")]
    [InlineData("127.0.0.1", "", "")]
    [InlineData("[::1]", "", "")]
    [InlineData("[::1]:8080", "", "")]
    public void AuthorityForms(string rawTarget, string path, string query)
    {
        Http1Connection.Reset();
        var ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
        var reader = new SequenceReader<byte>(ros);
        Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
        var prevRequestUrl = Http1Connection.RawTarget;
        var prevPath = Http1Connection.Path;
        var prevQuery = Http1Connection.QueryString;
 
        // Identical requests keep same materialized string values
        for (var i = 0; i < 5; i++)
        {
            Http1Connection.Reset();
            // RawTarget, Path, QueryString are null after reset
            Assert.Null(Http1Connection.RawTarget);
            Assert.Null(Http1Connection.Path);
            Assert.Null(Http1Connection.QueryString);
 
            ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
            reader = new SequenceReader<byte>(ros);
            Assert.True(Parser.ParseRequestLine(ParsingHandler, ref reader));
 
            // Equal the inputs.
            Assert.Equal(rawTarget, Http1Connection.RawTarget);
            Assert.Equal(path, Http1Connection.Path);
            Assert.Equal(query, Http1Connection.QueryString);
 
            // RawTarget not the same as the input.
            Assert.NotSame(rawTarget, Http1Connection.RawTarget);
            // Others same as the inputs, empty strings.
            Assert.Same(path, Http1Connection.Path);
            Assert.Same(query, Http1Connection.QueryString);
 
            // However, materalized strings are reused if generated for previous requests.
 
            Assert.Same(prevRequestUrl, Http1Connection.RawTarget);
            Assert.Same(prevPath, Http1Connection.Path);
            Assert.Same(prevQuery, Http1Connection.QueryString);
 
            prevRequestUrl = Http1Connection.RawTarget;
            prevPath = Http1Connection.Path;
            prevQuery = Http1Connection.QueryString;
        }
 
        // Different Authority Form request changes values
        rawTarget = "example.org:2345";
        path = "";
        query = "";
 
        Http1Connection.Reset();
        ros = new ReadOnlySequence<byte>(Encoding.ASCII.GetBytes($"CONNECT {rawTarget} HTTP/1.1\r\n"));
        reader = new SequenceReader<byte>(ros);
        Parser.ParseRequestLine(ParsingHandler, ref reader);
 
        // Equal the inputs.
        Assert.Equal(rawTarget, Http1Connection.RawTarget);
        Assert.Equal(path, Http1Connection.Path);
        Assert.Equal(query, Http1Connection.QueryString);
 
        // But not the same as the inputs.
        Assert.NotSame(rawTarget, Http1Connection.RawTarget);
        // Empty interned strings
        Assert.Same(path, Http1Connection.Path);
        Assert.Same(query, Http1Connection.QueryString);
 
        // Not equal previous request.
        Assert.NotEqual(prevRequestUrl, Http1Connection.RawTarget);
 
        DifferentFormsWorkTogether();
    }
 
    public StartLineTests()
    {
        MemoryPool = PinnedBlockMemoryPoolFactory.Create();
        var options = new PipeOptions(MemoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
        var pair = DuplexPipe.CreateConnectionPair(options, options);
        Transport = pair.Transport;
 
        var timeProvider = new FakeTimeProvider();
        var serviceContext = TestContextFactory.CreateServiceContext(
            serverOptions: new KestrelServerOptions(),
            timeProvider: timeProvider,
            httpParser: new HttpParser<Http1ParsingHandler>());
 
        var connectionContext = TestContextFactory.CreateHttpConnectionContext(
            serviceContext: serviceContext,
            connectionContext: Mock.Of<ConnectionContext>(),
            transport: Transport,
            timeoutControl: new TimeoutControl(timeoutHandler: null, timeProvider),
            memoryPool: MemoryPool,
            connectionFeatures: new FeatureCollection());
 
        Http1Connection = new Http1Connection(connectionContext);
 
        Parser = new HttpParser<Http1ParsingHandler>(showErrorDetails: true);
        ParsingHandler = new Http1ParsingHandler(Http1Connection);
    }
 
    public void Dispose()
    {
        Transport.Input.Complete();
        Transport.Output.Complete();
 
        MemoryPool.Dispose();
    }
}