/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs
641 строка
26 KB
Brennan
Update HttpSys shutdown logic (#65217)
05 фев 2026, 04:44
Не верифицирован
05 фев 2026, 04:44
22155f6
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Text; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.HttpSys; public class RequestTests : LoggedTest { [ConditionalFact] public async Task Request_SimpleGet_ExpectedFieldsSet() { string root; using (Utilities.CreateHttpServerReturnRoot("/basepath", out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); // Request Keys Assert.Equal("GET", requestInfo.Method); Assert.Equal(Stream.Null, requestInfo.Body); Assert.NotNull(requestInfo.Headers); Assert.Equal("http", requestInfo.Scheme); Assert.Equal("/basepath", requestInfo.PathBase); Assert.Equal("/SomePath", requestInfo.Path); Assert.Equal("?SomeQuery", requestInfo.QueryString); Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget); Assert.Equal("HTTP/1.1", requestInfo.Protocol); Assert.False(httpContext.Request.CanHaveBody()); var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>(); Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString()); Assert.NotEqual(0, connectionInfo.RemotePort); Assert.Equal("::1", connectionInfo.LocalIpAddress.ToString()); Assert.NotEqual(0, connectionInfo.LocalPort); Assert.NotNull(connectionInfo.ConnectionId); // Trace identifier var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>(); Assert.NotNull(requestIdentifierFeature); Assert.NotNull(requestIdentifierFeature.TraceIdentifier); // Note: Response keys are validated in the ResponseTests return Task.CompletedTask; }, options => { }, LoggerFactory)) { string response = await SendRequestAsync(root + "/basepath/SomePath?SomeQuery"); Assert.Equal(string.Empty, response); } } [ConditionalFact] public async Task Request_FieldsCanBeSet_Set() { string root; using (Utilities.CreateHttpServerReturnRoot("/basepath", out root, httpContext => { try { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); // Request Keys requestInfo.Method = "TEST"; Assert.Equal("TEST", requestInfo.Method); requestInfo.Body = new MemoryStream(); Assert.IsType<MemoryStream>(requestInfo.Body); var customHeaders = new HeaderCollection(); requestInfo.Headers = customHeaders; Assert.Same(customHeaders, requestInfo.Headers); requestInfo.Scheme = "abcd"; Assert.Equal("abcd", requestInfo.Scheme); requestInfo.PathBase = "/customized/Base"; Assert.Equal("/customized/Base", requestInfo.PathBase); requestInfo.Path = "/customized/Path"; Assert.Equal("/customized/Path", requestInfo.Path); requestInfo.QueryString = "?customizedQuery"; Assert.Equal("?customizedQuery", requestInfo.QueryString); requestInfo.RawTarget = "/customized/raw?Target"; Assert.Equal("/customized/raw?Target", requestInfo.RawTarget); requestInfo.Protocol = "Custom/2.0"; Assert.Equal("Custom/2.0", requestInfo.Protocol); var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>(); connectionInfo.RemoteIpAddress = IPAddress.Broadcast; Assert.Equal(IPAddress.Broadcast, connectionInfo.RemoteIpAddress); connectionInfo.RemotePort = 12345; Assert.Equal(12345, connectionInfo.RemotePort); connectionInfo.LocalIpAddress = IPAddress.Any; Assert.Equal(IPAddress.Any, connectionInfo.LocalIpAddress); connectionInfo.LocalPort = 54321; Assert.Equal(54321, connectionInfo.LocalPort); connectionInfo.ConnectionId = "CustomId"; Assert.Equal("CustomId", connectionInfo.ConnectionId); // Trace identifier var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>(); Assert.NotNull(requestIdentifierFeature); requestIdentifierFeature.TraceIdentifier = "customTrace"; Assert.Equal("customTrace", requestIdentifierFeature.TraceIdentifier); // Note: Response keys are validated in the ResponseTests } catch (Exception ex) { byte[] body = Encoding.ASCII.GetBytes(ex.ToString()); httpContext.Response.Body.Write(body, 0, body.Length); } return Task.FromResult(0); }, options => { }, LoggerFactory)) { string response = await SendRequestAsync(root + "/basepath/SomePath?SomeQuery"); Assert.Equal(string.Empty, response); } } [ConditionalFact] public async Task Request_FieldsCanBeSetToNull_Set() { string root; using (Utilities.CreateHttpServerReturnRoot("/basepath", out root, httpContext => { try { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); // Request Keys requestInfo.Method = null; Assert.Null(requestInfo.Method); requestInfo.Body = null; Assert.Null(requestInfo.Body); requestInfo.Headers = null; Assert.Null(requestInfo.Headers); requestInfo.Scheme = null; Assert.Null(requestInfo.Scheme); requestInfo.PathBase = null; Assert.Null(requestInfo.PathBase); requestInfo.Path = null; Assert.Null(requestInfo.Path); requestInfo.QueryString = null; Assert.Null(requestInfo.QueryString); requestInfo.RawTarget = null; Assert.Null(requestInfo.RawTarget); requestInfo.Protocol = null; Assert.Null(requestInfo.Protocol); var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>(); connectionInfo.RemoteIpAddress = null; Assert.Null(connectionInfo.RemoteIpAddress); connectionInfo.RemotePort = -1; Assert.Equal(-1, connectionInfo.RemotePort); connectionInfo.LocalIpAddress = null; Assert.Null(connectionInfo.LocalIpAddress); connectionInfo.LocalPort = -1; Assert.Equal(-1, connectionInfo.LocalPort); connectionInfo.ConnectionId = null; Assert.Null(connectionInfo.ConnectionId); // Trace identifier var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>(); Assert.NotNull(requestIdentifierFeature); requestIdentifierFeature.TraceIdentifier = null; Assert.Null(requestIdentifierFeature.TraceIdentifier); // Note: Response keys are validated in the ResponseTests } catch (Exception ex) { byte[] body = Encoding.ASCII.GetBytes(ex.ToString()); httpContext.Response.Body.Write(body, 0, body.Length); } return Task.FromResult(0); }, options => { }, LoggerFactory)) { string response = await SendRequestAsync(root + "/basepath/SomePath?SomeQuery"); Assert.Equal(string.Empty, response); } } [ConditionalTheory] [InlineData("/", "/", "", "/")] [InlineData("/basepath/", "/basepath", "/basepath", "")] [InlineData("/basepath/", "/basepath/", "/basepath", "/")] [InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")] [InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")] [InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")] [InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")] [InlineData("/base", "///base/path1/path2", "///base", "/path1/path2")] public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath) { string root; using (Utilities.CreateHttpServerReturnRoot(pathBase, out root, httpContext => { try { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>(); var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>(); // Request Keys Assert.Equal("http", requestInfo.Scheme); Assert.Equal(expectedPath, requestInfo.Path); Assert.Equal(expectedPathBase, requestInfo.PathBase); Assert.Equal(string.Empty, requestInfo.QueryString); Assert.Equal(requestPath, requestInfo.RawTarget); // Trace identifier Assert.NotNull(requestIdentifierFeature); Assert.NotNull(requestIdentifierFeature.TraceIdentifier); } catch (Exception ex) { byte[] body = Encoding.ASCII.GetBytes(ex.ToString()); httpContext.Response.Body.Write(body, 0, body.Length); } return Task.FromResult(0); }, options => { }, LoggerFactory)) { string response = await SendRequestAsync(root + requestPath); Assert.Equal(string.Empty, response); } } [ConditionalFact] public async Task Request_DoubleEscapingAllowed() { string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); Assert.Equal("/%2F", requestInfo.Path); Assert.Equal("/%252F", requestInfo.RawTarget); return Task.FromResult(0); }, options => { }, LoggerFactory)) { var response = await SendSocketRequestAsync(root, "/%252F"); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal("200", responseStatusCode); } } [ConditionalFact] public async Task Request_FullUriInRequestLine_ParsesPath() { using (var server = Utilities.CreateHttpServerReturnRoot("/", out var root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); Assert.Equal("/", requestInfo.Path); Assert.Equal("", requestInfo.PathBase); return Task.CompletedTask; }, options => { }, LoggerFactory)) { // Send a HTTP request with the request line: // GET http://localhost:5001 HTTP/1.1 var response = await SendSocketRequestAsync(root, root); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal(StatusCodes.Status200OK.ToString(CultureInfo.InvariantCulture), responseStatusCode); } } [ConditionalFact] public async Task Request_FullUriInRequestLineWithSlashesInQuery_BlockedByHttpSys() { using (var server = Utilities.CreateHttpServerReturnRoot("/", out var root, httpContext => { return Task.CompletedTask; }, options => { }, LoggerFactory)) { // Send a HTTP request with the request line: // GET http://localhost:5001?query=value/1/2 HTTP/1.1 // Should return a 400 as it is a client error var response = await SendSocketRequestAsync(root, root + "?query=value/1/2"); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal(StatusCodes.Status400BadRequest.ToString(CultureInfo.InvariantCulture), responseStatusCode); } } [ConditionalTheory] // The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2" [InlineData("/", "", "/")] [InlineData("/random", "", "/random")] [InlineData("/11", "/11", "")] [InlineData("/11/", "/11", "/")] [InlineData("/11/random", "/11", "/random")] [InlineData("/2", "/2", "")] [InlineData("/2/", "/2", "/")] [InlineData("/2/random", "/2", "/random")] [InlineData("/2/3", "/2/3", "")] [InlineData("/2/3/", "/2/3", "/")] [InlineData("/2/3/random", "/2/3", "/random")] public async Task Request_MultiplePrefixes(string requestPath, string expectedPathBase, string expectedPath) { string root; using (CreateServer(out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>(); try { Assert.Equal(expectedPath, requestInfo.Path); Assert.Equal(expectedPathBase, requestInfo.PathBase); Assert.Equal(requestPath, requestInfo.RawTarget); // Trace identifier Assert.NotNull(requestIdentifierFeature); Assert.NotNull(requestIdentifierFeature.TraceIdentifier); } catch (Exception ex) { byte[] body = Encoding.ASCII.GetBytes(ex.ToString()); httpContext.Response.Body.Write(body, 0, body.Length); } return Task.FromResult(0); })) { string response = await SendRequestAsync(root + requestPath); Assert.Equal(string.Empty, response); } } [ConditionalFact] public async Task Request_UrlUnescaping() { // Must start with '/' var stringBuilder = new StringBuilder("/"); for (var i = 32; i < 127; i++) { stringBuilder.Append("%"); stringBuilder.Append(i.ToString("X2", CultureInfo.InvariantCulture)); } var rawPath = stringBuilder.ToString(); string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); Assert.Equal(rawPath, requestInfo.RawTarget); // '/' %2F is an exception, un-escaping it would change the structure of the path Assert.Equal("/ !\"#$%&'()*+,-.%2F0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", requestInfo.Path); return Task.FromResult(0); }, options => { }, LoggerFactory)) { var response = await SendSocketRequestAsync(root, rawPath); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal("200", responseStatusCode); } } [Fact] public async Task Latin1UrlIsRejected() { string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { Assert.Fail("Request should not reach here"); return Task.FromResult(0); }, options => { }, LoggerFactory)) { var uri = new Uri(root); StringBuilder builder = new StringBuilder(); builder.AppendLine(FormattableString.Invariant($"GET /a HTTP/1.1")); builder.AppendLine("Connection: close"); builder.Append("HOST: "); builder.AppendLine(uri.Authority); builder.AppendLine(); byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); // Replace the 'a' in the path with a Latin1 value request[5] = 0xe1; using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)) { socket.Connect(uri.Host, uri.Port); socket.Send(request); var response = new byte[12]; await Task.Run(() => socket.Receive(response)); var statusCode = Encoding.UTF8.GetString(response).Substring(9); Assert.Equal("400", statusCode); } } var errorLogs = TestSink.Writes.Where(w => w.LogLevel >= LogLevel.Error).Select(w => w.Exception); Assert.False(errorLogs.Any(), string.Join(" ", errorLogs)); } [ConditionalFact] public async Task Request_WithDoubleSlashes_LeftAlone() { var rawPath = "//a/b//c"; string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); Assert.Equal(rawPath, requestInfo.RawTarget); Assert.Equal(rawPath, requestInfo.Path); return Task.FromResult(0); }, options => { }, LoggerFactory)) { var response = await SendSocketRequestAsync(root, rawPath); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal("200", responseStatusCode); } } [ConditionalTheory] [InlineData("/", "/a/b/../c", "", "/a/c")] [InlineData("/", "/a/b/./c", "", "/a/b/c")] [InlineData("/a", "/a/./c", "/a", "/c")] [InlineData("/a", "/a/d/../b/c", "/a", "/b/c")] [InlineData("/a/b", "/a/d/../b/c", "/a/b", "/c")] // Http.Sys uses the cooked URL when routing. [InlineData("/a/b", "/a/./b/c", "/a/b", "/c")] // Http.Sys uses the cooked URL when routing. public async Task Request_WithNavigation_Removed(string basePath, string input, string expectedPathBase, string expectedPath) { string root; using (var server = Utilities.CreateHttpServerReturnRoot(basePath, out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); Assert.Equal(input, requestInfo.RawTarget); Assert.Equal(expectedPathBase, requestInfo.PathBase); Assert.Equal(expectedPath, requestInfo.Path); return Task.FromResult(0); }, options => { }, LoggerFactory)) { var response = await SendSocketRequestAsync(root, input); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal("200", responseStatusCode); } } [ConditionalTheory] [InlineData("/a/b/%2E%2E/c", "/a/c")] [InlineData("/a/b/%2E/c", "/a/b/c")] public async Task Request_WithEscapedNavigation_Removed(string input, string expected) { string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { var requestInfo = httpContext.Features.Get<IHttpRequestFeature>(); Assert.Equal(input, requestInfo.RawTarget); Assert.Equal(expected, requestInfo.Path); return Task.FromResult(0); }, options => { }, LoggerFactory)) { var response = await SendSocketRequestAsync(root, input); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.Equal("200", responseStatusCode); } } [ConditionalFact] public async Task Request_ControlCharacters_400() { string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { throw new NotImplementedException(); }, options => { }, LoggerFactory)) { for (var i = 0; i < 32; i++) { if (i == 9 || i == 10) { continue; // \t and \r are allowed by Http.Sys. } var response = await SendSocketRequestAsync(root, "/" + (char)i); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.True(string.Equals("400", responseStatusCode), i.ToString("X2", CultureInfo.InvariantCulture)); } } } [ConditionalFact] public async Task Request_EscapedControlCharacters_400() { string root; using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext => { throw new NotImplementedException(); }, options => { }, LoggerFactory)) { for (var i = 0; i < 32; i++) { var response = await SendSocketRequestAsync(root, "/%" + i.ToString("X2", CultureInfo.InvariantCulture)); var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 " Assert.True(string.Equals("400", responseStatusCode), i.ToString("X2", CultureInfo.InvariantCulture)); } } } [ConditionalFact] public async Task RequestAborted_AfterAccessingProperty_Notified() { var registered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var result = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var server = Utilities.CreateHttpServerReturnRoot("/", out var address, async httpContext => { var ct = httpContext.RequestAborted; if (!ct.CanBeCanceled || ct.IsCancellationRequested) { result.SetException(new Exception("The CT isn't valid.")); return; } ct.Register(() => result.SetResult()); registered.SetResult(); // Don't exit until it fires or else it could be disposed. await result.Task.DefaultTimeout(); }, options => { }, LoggerFactory); // Send a request and then abort. var uri = new Uri(address); StringBuilder builder = new StringBuilder(); builder.AppendLine("POST / HTTP/1.1"); builder.AppendLine("Connection: close"); builder.AppendLine("Content-Length: 10"); builder.Append("HOST: "); builder.AppendLine(uri.Authority); builder.AppendLine(); byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(uri.Host, uri.Port); socket.Send(request); // Wait for the token to be setup before aborting. await registered.Task.DefaultTimeout(); socket.Close(); await result.Task.DefaultTimeout(); } [ConditionalFact] public async Task RequestAbortedDurringRead_BeforeAccessingProperty_TokenAlreadyCanceled() { var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var result = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); using var server = Utilities.CreateHttpServerReturnRoot("/", out var address, async httpContext => { await requestAborted.Task.DefaultTimeout(); try { await httpContext.Request.Body.ReadAsync(new byte[10]).DefaultTimeout(); result.SetException(new Exception("This should have aborted")); return; } catch (IOException) { } result.SetResult(httpContext.RequestAborted.IsCancellationRequested); }, options => { }, LoggerFactory); // Send a request and then abort. var uri = new Uri(address); StringBuilder builder = new StringBuilder(); builder.AppendLine("POST / HTTP/1.1"); builder.AppendLine("Connection: close"); builder.AppendLine("Content-Length: 10"); builder.Append("HOST: "); builder.AppendLine(uri.Authority); builder.AppendLine(); byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(uri.Host, uri.Port); socket.Send(request); socket.Close(); requestAborted.SetResult(); var wasCancelled = await result.Task; Assert.True(wasCancelled); } private IServer CreateServer(out string root, RequestDelegate app) { // TODO: We're just doing this to get a dynamic port. This can be removed later when we add support for hot-adding prefixes. var dynamicServer = Utilities.CreateHttpServerReturnRoot("/", out root, app, // Set MaxAccepts to 1 as we're throwing this server away immediately, so we can reduce the amount of work being done options => { options.MaxAccepts = 1; }, LoggerFactory); dynamicServer.Dispose(); var rootUri = new Uri(root); var server = Utilities.CreatePump(LoggerFactory); foreach (string path in new[] { "/", "/11", "/2/3", "/2", "/11/2" }) { server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(rootUri.Scheme, rootUri.Host, rootUri.Port, path)); } server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait(); return server; } private async Task<string> SendRequestAsync(string uri) { using (HttpClient client = new HttpClient()) { return await client.GetStringAsync(uri); } } private async Task<string> SendSocketRequestAsync(string address, string path) { var uri = new Uri(address); StringBuilder builder = new StringBuilder(); builder.AppendLine("GET " + path + " HTTP/1.1"); builder.AppendLine("Connection: close"); builder.Append("HOST: "); builder.AppendLine(uri.Authority); builder.AppendLine(); byte[] request = Encoding.ASCII.GetBytes(builder.ToString()); using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)) { socket.Connect(uri.Host, uri.Port); socket.Send(request); var response = new byte[12]; await Task.Run(() => socket.Receive(response)); return Encoding.ASCII.GetString(response); } } }