diff --git a/src/libraries/System.Net.Requests/src/Resources/Strings.resx b/src/libraries/System.Net.Requests/src/Resources/Strings.resx index b33f2a02440356..4c0a7a45c146ff 100644 --- a/src/libraries/System.Net.Requests/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Requests/src/Resources/Strings.resx @@ -264,4 +264,7 @@ The ServicePointManager does not support proxies with the {0} scheme. + + Reached the maximum number of BindIPEndPointDelegate retries. + diff --git a/src/libraries/System.Net.Requests/src/System.Net.Requests.csproj b/src/libraries/System.Net.Requests/src/System.Net.Requests.csproj index 397622b4806a03..b53b7272ea4171 100644 --- a/src/libraries/System.Net.Requests/src/System.Net.Requests.csproj +++ b/src/libraries/System.Net.Requests/src/System.Net.Requests.csproj @@ -110,6 +110,7 @@ + diff --git a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs index 2a54bbb4d8d50d..44c60115913654 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Net; using System.Net.Cache; using System.Net.Http; using System.Net.Http.Headers; @@ -41,6 +42,7 @@ public class HttpWebRequest : WebRequest, ISerializable private Task? _sendRequestTask; private static int _defaultMaxResponseHeadersLength = HttpHandlerDefaults.DefaultMaxResponseHeadersLength; + private static int _defaultMaximumErrorResponseLength = -1; private int _beginGetRequestStreamCalled; private int _beginGetResponseCalled; @@ -420,11 +422,7 @@ public string? Referer /// /// Sets the media type header /// - public string? MediaType - { - get; - set; - } + public string? MediaType { get; set; } /// /// @@ -677,14 +675,22 @@ public static int DefaultMaximumResponseHeadersLength } set { + ArgumentOutOfRangeException.ThrowIfLessThan(value, 0); _defaultMaxResponseHeadersLength = value; } } - // NOP public static int DefaultMaximumErrorResponseLength { - get; set; + get + { + return _defaultMaximumErrorResponseLength; + } + set + { + ArgumentOutOfRangeException.ThrowIfLessThan(value, -1); + _defaultMaximumErrorResponseLength = value; + } } private static RequestCachePolicy? _defaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); @@ -806,10 +812,12 @@ public Version ProtocolVersion if (value.Equals(HttpVersion.Version11)) { IsVersionHttp10 = false; + ServicePoint.ProtocolVersion = HttpVersion.Version11; } else if (value.Equals(HttpVersion.Version10)) { IsVersionHttp10 = true; + ServicePoint.ProtocolVersion = HttpVersion.Version10; } else { @@ -1621,6 +1629,13 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http handler.UseCookies = false; } + if (parameters.ServicePoint is { } servicePoint) + { + handler.MaxConnectionsPerServer = servicePoint.ConnectionLimit; + handler.PooledConnectionIdleTimeout = TimeSpan.FromMilliseconds(servicePoint.MaxIdleTime); + handler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(servicePoint.ConnectionLeaseTimeout); + } + Debug.Assert(handler.UseProxy); // Default of handler.UseProxy is true. Debug.Assert(handler.Proxy == null); // Default of handler.Proxy is null. @@ -1638,7 +1653,7 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http { handler.UseProxy = false; } - else if (!object.ReferenceEquals(parameters.Proxy, WebRequest.GetSystemWebProxy())) + else if (!ReferenceEquals(parameters.Proxy, GetSystemWebProxy())) { handler.Proxy = parameters.Proxy; } @@ -1659,10 +1674,20 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http handler.SslOptions.EnabledSslProtocols = (SslProtocols)parameters.SslProtocols; handler.SslOptions.CertificateRevocationCheckMode = parameters.CheckCertificateRevocationList ? X509RevocationMode.Online : X509RevocationMode.NoCheck; RemoteCertificateValidationCallback? rcvc = parameters.ServerCertificateValidationCallback; - if (rcvc != null) + handler.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => { - handler.SslOptions.RemoteCertificateValidationCallback = (message, cert, chain, errors) => rcvc(request!, cert, chain, errors); - } + if (parameters.ServicePoint is { } servicePoint) + { + servicePoint.Certificate = cert; + } + + if (rcvc is not null) + { + return rcvc(request!, cert, chain, errors); + } + + return errors == SslPolicyErrors.None; + }; // Set up a ConnectCallback so that we can control Socket-specific settings, like ReadWriteTimeout => socket.Send/ReceiveTimeout. handler.ConnectCallback = async (context, cancellationToken) => @@ -1671,6 +1696,10 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http try { + IPAddress[] addresses = parameters.Async ? + await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, cancellationToken).ConfigureAwait(false) : + Dns.GetHostAddresses(context.DnsEndPoint.Host); + if (parameters.ServicePoint is { } servicePoint) { if (servicePoint.ReceiveBufferSize != -1) @@ -1684,19 +1713,58 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, keepAlive.Time); socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, keepAlive.Interval); } + + BindHelper(servicePoint, ref addresses, socket, context.DnsEndPoint.Port); + static void BindHelper(ServicePoint servicePoint, ref IPAddress[] addresses, Socket socket, int port) + { + if (servicePoint.BindIPEndPointDelegate is null) + { + return; + } + + const int MaxRetries = 100; + foreach (IPAddress address in addresses) + { + int retryCount = 0; + for (; retryCount < MaxRetries; retryCount++) + { + IPEndPoint? endPoint = servicePoint.BindIPEndPointDelegate(servicePoint, new IPEndPoint(address, port), retryCount); + if (endPoint is null) // Get other address to try + { + break; + } + + try + { + socket.Bind(endPoint); + addresses = [address]; + return; // Bind successful, exit loops. + } + catch + { + continue; + } + } + + if (retryCount >= MaxRetries) + { + throw new OverflowException(SR.net_maximumbindretries); + } + } + } } - socket.NoDelay = true; + socket.NoDelay = !(parameters.ServicePoint?.UseNagleAlgorithm) ?? true; if (parameters.Async) { - await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false); + await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, cancellationToken).ConfigureAwait(false); } else { using (cancellationToken.UnsafeRegister(s => ((Socket)s!).Dispose(), socket)) { - socket.Connect(context.DnsEndPoint); + socket.Connect(addresses, context.DnsEndPoint.Port); } // Throw in case cancellation caused the socket to be disposed after the Connect completed diff --git a/src/libraries/System.Net.Requests/src/System/Net/HttpWebResponse.cs b/src/libraries/System.Net.Requests/src/System/Net/HttpWebResponse.cs index f7fae7869b1e7f..7b0e9b90681fc9 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/HttpWebResponse.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/HttpWebResponse.cs @@ -8,6 +8,8 @@ using System.Net.Http; using System.Runtime.Serialization; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Net { @@ -337,7 +339,14 @@ public override Stream GetResponseStream() CheckDisposed(); if (_httpResponseMessage.Content != null) { - return _httpResponseMessage.Content.ReadAsStream(); + Stream contentStream = _httpResponseMessage.Content.ReadAsStream(); + int maxErrorResponseLength = HttpWebRequest.DefaultMaximumErrorResponseLength; + if (maxErrorResponseLength < 0 || StatusCode < HttpStatusCode.BadRequest) + { + return contentStream; + } + + return new TruncatedReadStream(contentStream, maxErrorResponseLength); } return Stream.Null; @@ -371,5 +380,56 @@ private void CheckDisposed() } private static string GetHeaderValueAsString(IEnumerable values) => string.Join(", ", values); + + internal sealed class TruncatedReadStream(Stream innerStream, int maxSize) : Stream + { + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + { + return Read(new Span(buffer, offset, count)); + } + + public override int Read(Span buffer) + { + int readBytes = innerStream.Read(buffer.Slice(0, Math.Min(buffer.Length, maxSize))); + maxSize -= readBytes; + return readBytes; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int readBytes = await innerStream.ReadAsync(buffer.Slice(0, Math.Min(buffer.Length, maxSize)), cancellationToken) + .ConfigureAwait(false); + maxSize -= readBytes; + return readBytes; + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override ValueTask DisposeAsync() => innerStream.DisposeAsync(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + innerStream.Dispose(); + } + } + } } } diff --git a/src/libraries/System.Net.Requests/src/System/Net/ServicePoint/ServicePointManager.cs b/src/libraries/System.Net.Requests/src/System/Net/ServicePoint/ServicePointManager.cs index a0cf9dcece157f..bbf20b3e808b62 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/ServicePoint/ServicePointManager.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/ServicePoint/ServicePointManager.cs @@ -78,7 +78,7 @@ public static int MaxServicePointIdleTime } } - public static bool UseNagleAlgorithm { get; set; } = true; + public static bool UseNagleAlgorithm { get; set; } public static bool Expect100Continue { get; set; } = true; @@ -156,7 +156,8 @@ public static ServicePoint FindServicePoint(Uri address, IWebProxy? proxy) IdleSince = DateTime.Now, Expect100Continue = Expect100Continue, UseNagleAlgorithm = UseNagleAlgorithm, - KeepAlive = KeepAlive + KeepAlive = KeepAlive, + MaxIdleTime = MaxServicePointIdleTime }; s_servicePointTable[tableKey] = new WeakReference(sp); diff --git a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs index 45563ccc3dd0ed..73c66872a7e56a 100644 --- a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs @@ -258,7 +258,7 @@ public void Ctor_VerifyDefaults_Success(Uri remoteServer) Assert.Equal(64, HttpWebRequest.DefaultMaximumResponseHeadersLength); Assert.NotNull(HttpWebRequest.DefaultCachePolicy); Assert.Equal(RequestCacheLevel.BypassCache, HttpWebRequest.DefaultCachePolicy.Level); - Assert.Equal(0, HttpWebRequest.DefaultMaximumErrorResponseLength); + Assert.Equal(-1, HttpWebRequest.DefaultMaximumErrorResponseLength); Assert.NotNull(request.Proxy); Assert.Equal(remoteServer, request.RequestUri); Assert.True(request.SupportsCookieContainer); @@ -2089,7 +2089,7 @@ await LoopbackServer.CreateClientAndServerAsync( request.ContinueTimeout = 30000; Stream requestStream = await request.GetRequestStreamAsync(); requestStream.Write("aaaa\r\n\r\n"u8); - await request.GetResponseAsync(); + await GetResponseAsync(request); }, async (server) => { @@ -2118,7 +2118,7 @@ await LoopbackServer.CreateClientAndServerAsync( request.ContinueTimeout = continueTimeout; Stream requestStream = await request.GetRequestStreamAsync(); requestStream.Write("aaaa\r\n\r\n"u8); - await request.GetResponseAsync(); + await GetResponseAsync(request); }, async (server) => { @@ -2144,7 +2144,7 @@ await LoopbackServer.CreateClientAndServerAsync( HttpWebRequest request = WebRequest.CreateHttp(uri); request.Method = "POST"; request.ServicePoint.Expect100Continue = expect100Continue; - await request.GetResponseAsync(); + await GetResponseAsync(request); }, async (server) => { @@ -2167,6 +2167,122 @@ await server.AcceptConnectionAsync( ); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void SendHttpRequest_WhenDefaultMaximumErrorResponseLengthSet_Success() + { + RemoteExecutor.Invoke(async (async) => + { + TaskCompletionSource tcs = new TaskCompletionSource(); + await LoopbackServer.CreateClientAndServerAsync( + async (uri) => + { + HttpWebRequest request = WebRequest.CreateHttp(uri); + HttpWebRequest.DefaultMaximumErrorResponseLength = 5; + var exception = + await Assert.ThrowsAsync(() => bool.Parse(async) ? request.GetResponseAsync() : Task.Run(() => request.GetResponse())); + tcs.SetResult(); + Assert.NotNull(exception.Response); + using (var responseStream = exception.Response.GetResponseStream()) + { + var buffer = new byte[10]; + int readLen = responseStream.Read(buffer, 0, buffer.Length); + Assert.Equal(5, readLen); + Assert.Equal(new string('a', 5), Encoding.UTF8.GetString(buffer[0..readLen])); + Assert.Equal(0, responseStream.Read(buffer)); + } + }, + async (server) => + { + await server.AcceptConnectionAsync( + async connection => + { + await connection.SendResponseAsync(statusCode: HttpStatusCode.BadRequest, content: new string('a', 10)); + await tcs.Task; + }); + }); + }, IsAsync.ToString()).Dispose(); + } + + [Fact] + public void HttpWebRequest_SetProtocolVersion_Success() + { + HttpWebRequest request = WebRequest.CreateHttp(Configuration.Http.RemoteEchoServer); + + request.ProtocolVersion = HttpVersion.Version10; + Assert.Equal(HttpVersion.Version10, request.ServicePoint.ProtocolVersion); + + request.ProtocolVersion = HttpVersion.Version11; + Assert.Equal(HttpVersion.Version11, request.ServicePoint.ProtocolVersion); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void SendHttpRequest_BindIPEndPoint_Success() + { + RemoteExecutor.Invoke(async (async) => + { + TaskCompletionSource tcs = new TaskCompletionSource(); + await LoopbackServer.CreateClientAndServerAsync( + async (uri) => + { + HttpWebRequest request = WebRequest.CreateHttp(uri); + request.ServicePoint.BindIPEndPointDelegate = (_, _, _) => new IPEndPoint(IPAddress.Loopback, 27277); + var responseTask = bool.Parse(async) ? request.GetResponseAsync() : Task.Run(() => request.GetResponse()); + using (var response = (HttpWebResponse)await responseTask) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + tcs.SetResult(); + }, + async (server) => + { + await server.AcceptConnectionAsync( + async connection => + { + var ipEp = (IPEndPoint)connection.Socket.RemoteEndPoint; + Assert.Equal(27277, ipEp.Port); + await connection.SendResponseAsync(); + await tcs.Task; + }); + }); + }, IsAsync.ToString()).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void SendHttpRequest_BindIPEndPoint_Throws() + { + RemoteExecutor.Invoke(async (async) => + { + Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + ValueTask? clientSocket = null; + CancellationTokenSource cts = new CancellationTokenSource(); + if (PlatformDetection.IsLinux) + { + socket.Listen(); + clientSocket = socket.AcceptAsync(cts.Token); + } + + try + { + // URI shouldn't matter because it should throw exception before connection open. + HttpWebRequest request = WebRequest.CreateHttp(Configuration.Http.RemoteEchoServer); + request.ServicePoint.BindIPEndPointDelegate = (_, _, _) => (IPEndPoint)socket.LocalEndPoint!; + var exception = await Assert.ThrowsAsync(() => + bool.Parse(async) ? request.GetResponseAsync() : Task.Run(() => request.GetResponse())); + Assert.IsType(exception.InnerException?.InnerException); + } + finally + { + if (clientSocket is not null) + { + await cts.CancelAsync(); + } + socket.Dispose(); + cts.Dispose(); + } + }, IsAsync.ToString()).Dispose(); + } + private void RequestStreamCallback(IAsyncResult asynchronousResult) { RequestState state = (RequestState)asynchronousResult.AsyncState; diff --git a/src/libraries/System.Net.Requests/tests/ServicePointTests/ServicePointManagerTest.cs b/src/libraries/System.Net.Requests/tests/ServicePointTests/ServicePointManagerTest.cs index c1230598a8d4e5..ec64068ed456fc 100644 --- a/src/libraries/System.Net.Requests/tests/ServicePointTests/ServicePointManagerTest.cs +++ b/src/libraries/System.Net.Requests/tests/ServicePointTests/ServicePointManagerTest.cs @@ -181,7 +181,7 @@ public static void ServerCertificateValidationCallback_Roundtrips() [Fact] public static void UseNagleAlgorithm_Roundtrips() { - Assert.True(ServicePointManager.UseNagleAlgorithm); + Assert.False(ServicePointManager.UseNagleAlgorithm); try { ServicePointManager.UseNagleAlgorithm = false; @@ -325,7 +325,7 @@ public static void FindServicePoint_ReturnedServicePointMatchesExpectedValues() Assert.Equal(new Version(1, 1), sp.ProtocolVersion); Assert.Equal(-1, sp.ReceiveBufferSize); Assert.True(sp.SupportsPipelining, "SupportsPipelining"); - Assert.True(sp.UseNagleAlgorithm, "UseNagleAlgorithm"); + Assert.False(sp.UseNagleAlgorithm, "UseNagleAlgorithm"); }).Dispose(); }