diff --git a/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs b/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs index 0ab94178f5d5c..aac3f8091a913 100644 --- a/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs +++ b/src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs @@ -164,6 +164,8 @@ internal partial class WinHttp public const uint WINHTTP_OPTION_WEB_SOCKET_RECEIVE_BUFFER_SIZE = 122; public const uint WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE = 123; + public const uint WINHTTP_OPTION_TCP_KEEPALIVE = 152; + public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE { WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE = 0, @@ -276,6 +278,15 @@ public struct WINHTTP_ASYNC_RESULT public uint dwError; } + + [StructLayout(LayoutKind.Sequential)] + public struct tcp_keepalive + { + public uint onoff; + public uint keepalivetime; + public uint keepaliveinterval; + } + public const uint API_RECEIVE_RESPONSE = 1; public const uint API_QUERY_DATA_AVAILABLE = 2; public const uint API_READ_DATA = 3; diff --git a/src/libraries/System.Net.Http.WinHttpHandler/ref/System.Net.Http.WinHttpHandler.cs b/src/libraries/System.Net.Http.WinHttpHandler/ref/System.Net.Http.WinHttpHandler.cs index 7339effba4e25..12c86997fb1e0 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/ref/System.Net.Http.WinHttpHandler.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/ref/System.Net.Http.WinHttpHandler.cs @@ -44,6 +44,9 @@ public WinHttpHandler() { } public System.Func? ServerCertificateValidationCallback { get { throw null; } set { } } public System.Net.ICredentials? ServerCredentials { get { throw null; } set { } } public System.Security.Authentication.SslProtocols SslProtocols { get { throw null; } set { } } + public bool TcpKeepAliveEnabled { get { throw null; } set { } } + public System.TimeSpan TcpKeepAliveTime { get { throw null; } set { } } + public System.TimeSpan TcpKeepAliveInterval { get { throw null; } set { } } public System.Net.Http.WindowsProxyUsePolicy WindowsProxyUsePolicy { get { throw null; } set { } } protected override void Dispose(bool disposing) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { throw null; } diff --git a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs index b8d859e615b84..1525bc936e01d 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs @@ -76,6 +76,13 @@ private Func< private TimeSpan _sendTimeout = TimeSpan.FromSeconds(30); private TimeSpan _receiveHeadersTimeout = TimeSpan.FromSeconds(30); private TimeSpan _receiveDataTimeout = TimeSpan.FromSeconds(30); + + // Using OS defaults for "Keep-alive timeout" and "keep-alive interval" + // as documented in https://docs.microsoft.com/en-us/windows/win32/winsock/sio-keepalive-vals#remarks + private TimeSpan _tcpKeepAliveTime = TimeSpan.FromHours(2); + private TimeSpan _tcpKeepAliveInterval = TimeSpan.FromSeconds(1); + private bool _tcpKeepAliveEnabled; + private int _maxResponseHeadersLength = HttpHandlerDefaults.DefaultMaxResponseHeadersLength; private int _maxResponseDrainSize = 64 * 1024; private IDictionary _properties; // Only create dictionary when required. @@ -188,6 +195,7 @@ public SslProtocols SslProtocols } } + public Func< HttpRequestMessage, X509Certificate2, @@ -369,16 +377,13 @@ public TimeSpan SendTimeout set { - if (value != Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value > s_maxTimeout)) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - + CheckTimeSpanPropertyValue(value); CheckDisposedOrStarted(); _sendTimeout = value; } } + public TimeSpan ReceiveHeadersTimeout { get @@ -388,11 +393,7 @@ public TimeSpan ReceiveHeadersTimeout set { - if (value != Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value > s_maxTimeout)) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - + CheckTimeSpanPropertyValue(value); CheckDisposedOrStarted(); _receiveHeadersTimeout = value; } @@ -407,16 +408,74 @@ public TimeSpan ReceiveDataTimeout set { - if (value != Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value > s_maxTimeout)) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - + CheckTimeSpanPropertyValue(value); CheckDisposedOrStarted(); _receiveDataTimeout = value; } } + /// + /// Gets or sets a value indicating whether TCP keep-alive is enabled. + /// + /// + /// If enabled, the values of and will be forwarded + /// to set WINHTTP_OPTION_TCP_KEEPALIVE, enabling and configuring TCP keep-alive for the backing TCP socket. + /// + public bool TcpKeepAliveEnabled + { + get + { + return _tcpKeepAliveEnabled; + } + set + { + CheckDisposedOrStarted(); + _tcpKeepAliveEnabled = value; + } + } + + /// + /// Gets or sets the TCP keep-alive timeout. + /// + /// + /// Has no effect if is . + /// The default value of this property is 2 hours. + /// + public TimeSpan TcpKeepAliveTime + { + get + { + return _tcpKeepAliveTime; + } + set + { + CheckTimeSpanPropertyValue(value); + CheckDisposedOrStarted(); + _tcpKeepAliveTime = value; + } + } + + /// + /// Gets or sets the TCP keep-alive interval. + /// + /// + /// Has no effect if is . + /// The default value of this property is 1 second. + /// + public TimeSpan TcpKeepAliveInterval + { + get + { + return _tcpKeepAliveInterval; + } + set + { + CheckTimeSpanPropertyValue(value); + CheckDisposedOrStarted(); + _tcpKeepAliveInterval = value; + } + } + public int MaxResponseHeadersLength { get @@ -936,6 +995,28 @@ private void SetSessionHandleOptions(SafeWinHttpHandle sessionHandle) SetSessionHandleTlsOptions(sessionHandle); SetSessionHandleTimeoutOptions(sessionHandle); SetDisableHttp2StreamQueue(sessionHandle); + SetTcpKeepalive(sessionHandle); + } + + private unsafe void SetTcpKeepalive(SafeWinHttpHandle sessionHandle) + { + if (_tcpKeepAliveEnabled) + { + var tcpKeepalive = new Interop.WinHttp.tcp_keepalive + { + onoff = 1, + + // Timeout.InfiniteTimeSpan will be converted to uint.MaxValue milliseconds (~ 50 days) + keepaliveinterval = (uint)_tcpKeepAliveInterval.TotalMilliseconds, + keepalivetime = (uint)_tcpKeepAliveTime.TotalMilliseconds + }; + + SetWinHttpOption( + sessionHandle, + Interop.WinHttp.WINHTTP_OPTION_TCP_KEEPALIVE, + (IntPtr)(&tcpKeepalive), + (uint)sizeof(Interop.WinHttp.tcp_keepalive)); + } } private void SetSessionHandleConnectionOptions(SafeWinHttpHandle sessionHandle) @@ -1363,6 +1444,14 @@ private void CheckDisposedOrStarted() } } + private static void CheckTimeSpanPropertyValue(TimeSpan timeSpan) + { + if (timeSpan != Timeout.InfiniteTimeSpan && (timeSpan <= TimeSpan.Zero || timeSpan > s_maxTimeout)) + { + throw new ArgumentOutOfRangeException("value"); + } + } + private void SetStatusCallback( SafeWinHttpHandle requestHandle, Interop.WinHttp.WINHTTP_STATUS_CALLBACK callback) diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/WinHttpHandlerTest.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/WinHttpHandlerTest.cs index 25c6afbc0f5f3..76efea2d3bb54 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/WinHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/WinHttpHandlerTest.cs @@ -168,7 +168,6 @@ public async Task GetAsync_SetCookieContainerMultipleCookies_CookiesSent() Assert.Equal("POST", responseContent.Method); Assert.Equal(payload, responseContent.BodyContent); Assert.Equal(cookies.ToDictionary(c => c.Name, c => c.Value), responseContent.Cookies); - }; } @@ -218,6 +217,28 @@ public async Task SendAsync_MultipleHttp2ConnectionsEnabled_CreateAdditionalConn } } + [OuterLoop("Uses external service")] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows10Version2004OrGreater))] + public async Task SendAsync_UseTcpKeepAliveOptions() + { + using var handler = new WinHttpHandler() + { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = TimeSpan.FromSeconds(1), + TcpKeepAliveInterval = TimeSpan.FromMilliseconds(500) + }; + + using var client = new HttpClient(handler); + + var response = client.GetAsync(System.Net.Test.Common.Configuration.Http.RemoteEchoServer).Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + _output.WriteLine(responseContent); + + // Uncomment this to observe an exchange of "TCP Keep-Alive" and "TCP Keep-Alive ACK" packets: + // await Task.Delay(5000); + } + private async Task VerifyResponse(Task task, string payloadText) { Assert.True(task.IsCompleted); diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/APICallHistory.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/APICallHistory.cs index 37f5d8c68d463..961cb2ffbad54 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/APICallHistory.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/APICallHistory.cs @@ -70,6 +70,8 @@ public static ProxyInfo RequestProxySettings public static List WinHttpOptionClientCertContext { get { return winHttpOptionClientCertContextList; } } + public static (uint OnOff, uint KeepAliveTime, uint KeepAliveInterval)? WinHttpOptionTcpKeepAlive { get; set; } + public static void Reset() { sessionProxySettings.AccessType = null; @@ -93,6 +95,7 @@ public static void Reset() WinHttpOptionRedirectPolicy = null; WinHttpOptionSendTimeout = null; WinHttpOptionReceiveTimeout = null; + WinHttpOptionTcpKeepAlive = null; winHttpOptionClientCertContextList.Clear(); } diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs index 24e4b717859c2..a92c0124d9e5c 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/FakeInterop.cs @@ -537,7 +537,7 @@ public static bool WinHttpSetOption( return true; } - public static bool WinHttpSetOption( + public unsafe static bool WinHttpSetOption( SafeWinHttpHandle handle, uint option, IntPtr optionData, @@ -556,6 +556,11 @@ public static bool WinHttpSetOption( { APICallHistory.WinHttpOptionClientCertContext.Add(optionData); } + else if (option == Interop.WinHttp.WINHTTP_OPTION_TCP_KEEPALIVE) + { + Interop.WinHttp.tcp_keepalive* ptr = (Interop.WinHttp.tcp_keepalive*)optionData; + APICallHistory.WinHttpOptionTcpKeepAlive = (ptr->onoff, ptr->keepalivetime, ptr->keepaliveinterval); + } return true; } diff --git a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/WinHttpHandlerTest.cs b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/WinHttpHandlerTest.cs index 32e87d56af648..0d7e4622d98d5 100644 --- a/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/WinHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http.WinHttpHandler/tests/UnitTests/WinHttpHandlerTest.cs @@ -55,14 +55,112 @@ public void Ctor_ExpectedDefaultPropertyValues() Assert.Null(handler.DefaultProxyCredentials); Assert.Null(handler.Proxy); Assert.Equal(int.MaxValue, handler.MaxConnectionsPerServer); + Assert.Equal(TimeSpan.FromSeconds(30), handler.SendTimeout); Assert.Equal(TimeSpan.FromSeconds(30), handler.ReceiveHeadersTimeout); Assert.Equal(TimeSpan.FromSeconds(30), handler.ReceiveDataTimeout); + + Assert.False(handler.TcpKeepAliveEnabled); + Assert.Equal(TimeSpan.FromHours(2), handler.TcpKeepAliveTime); + Assert.Equal(TimeSpan.FromSeconds(1), handler.TcpKeepAliveInterval); + Assert.Equal(64, handler.MaxResponseHeadersLength); Assert.Equal(64 * 1024, handler.MaxResponseDrainSize); Assert.NotNull(handler.Properties); } + [Fact] + public void SetInvalidTimeouts_ThrowsArgumentOutOfRangeException() + { + TimeSpan[] invalidIntervals = + { + TimeSpan.FromSeconds(-1), + TimeSpan.FromSeconds(0), + TimeSpan.FromSeconds(int.MaxValue) + }; + + var setters = new Action[] + { + (h, t) => h.SendTimeout = t, + (h, t) => h.ReceiveHeadersTimeout = t, + (h, t) => h.ReceiveDataTimeout = t, + (h, t) => h.TcpKeepAliveInterval = t, + (h, t) => h.TcpKeepAliveTime = t, + }; + + using var handler = new WinHttpHandler(); + + foreach (Action setter in setters) + { + foreach (TimeSpan invalid in invalidIntervals) + { + Assert.Throws(() => setter(handler, invalid)); + } + } + } + + [Fact] + public void TcpKeepAliveOptions_Roundtrip() + { + using var handler = new WinHttpHandler() + { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = TimeSpan.FromMinutes(42), + TcpKeepAliveInterval = TimeSpan.FromSeconds(13) + }; + + Assert.True(handler.TcpKeepAliveEnabled); + Assert.Equal(TimeSpan.FromMinutes(42), handler.TcpKeepAliveTime); + Assert.Equal(TimeSpan.FromSeconds(13), handler.TcpKeepAliveInterval); + } + + [Fact] + public void TcpKeepalive_WhenDisabled_DoesntSetOptions() + { + using var handler = new WinHttpHandler(); + + SendRequestHelper.Send( + handler, + () => handler.TcpKeepAliveEnabled = false ); + Assert.Null(APICallHistory.WinHttpOptionTcpKeepAlive); + } + + [Fact] + public void TcpKeepalive_WhenEnabled_ForwardsCorrectNativeOptions() + { + using var handler = new WinHttpHandler(); + + SendRequestHelper.Send(handler, () => { + handler.TcpKeepAliveEnabled = true; + handler.TcpKeepAliveTime = TimeSpan.FromMinutes(13); + handler.TcpKeepAliveInterval = TimeSpan.FromSeconds(42); + }); + + (uint onOff, uint keepAliveTime, uint keepAliveInterval) = APICallHistory.WinHttpOptionTcpKeepAlive.Value; + + Assert.True(onOff != 0); + Assert.Equal(13_000u * 60u, keepAliveTime); + Assert.Equal(42_000u, keepAliveInterval); + } + + [Fact] + public void TcpKeepalive_InfiniteTimeSpan_TranslatesToUInt32MaxValue() + { + using var handler = new WinHttpHandler(); + + SendRequestHelper.Send(handler, () => { + handler.TcpKeepAliveEnabled = true; + handler.TcpKeepAliveTime = Timeout.InfiniteTimeSpan; + handler.TcpKeepAliveInterval = Timeout.InfiniteTimeSpan; + }); + + (uint onOff, uint keepAliveTime, uint keepAliveInterval) = APICallHistory.WinHttpOptionTcpKeepAlive.Value; + + Assert.True(onOff != 0); + Assert.Equal(uint.MaxValue, keepAliveTime); + Assert.Equal(uint.MaxValue, keepAliveInterval); + } + [Fact] public void AutomaticRedirection_SetFalseAndGet_ValueIsFalse() {