Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aed71f0
Certificate caching on WinHttpHandler to eliminate extra call to Cust…
liveans Jan 24, 2025
f08a569
Review feedback
liveans Feb 17, 2025
80ee04c
Merge branch 'main' into winhttp_servervalidationcallback_cache_certi…
liveans Feb 17, 2025
63de970
Review feedback
liveans Feb 17, 2025
00640e2
Framework compat + Review Feedback
liveans Feb 17, 2025
4ed25ff
Implement Timer to clear cache
liveans Feb 17, 2025
b15191d
Review feedback
liveans Feb 17, 2025
736944d
Review Feedback
liveans Feb 18, 2025
a8e05d0
Merge branch 'main' into winhttp_servervalidationcallback_cache_certi…
liveans Feb 18, 2025
548e6fd
Review feedback
liveans Feb 20, 2025
c365911
Fix RemoteExecutor issue and change delay to ms
liveans Feb 20, 2025
9e00762
Review feedback
liveans Feb 20, 2025
7f8540b
Apply suggestions from code review
liveans Feb 20, 2025
e0a9524
Merge branch 'main' into winhttp_servervalidationcallback_cache_certi…
liveans Feb 24, 2025
7d2bbbf
Merge branch 'main' into winhttp_servervalidationcallback_cache_certi…
liveans Mar 31, 2025
ae27025
Add ExecutionContext SuppressFlow
liveans Mar 31, 2025
61e7e88
Merge branch 'main' into winhttp_servervalidationcallback_cache_certi…
liveans Apr 4, 2025
adcce2b
Fix alignment issues
liveans Apr 7, 2025
2a6529f
Add offset docs for IPAddress parsing
liveans Apr 7, 2025
930d589
Merge branch 'winhttp_servervalidationcallback_cache_certificate_expe…
liveans Apr 7, 2025
3497e8f
Merge branch 'main' into winhttp_servervalidationcallback_cache_certi…
liveans Apr 7, 2025
12dfec6
Use RawDataMemory for lookup in Modern .NET
liveans Apr 8, 2025
23c2f94
Merge branch 'winhttp_servervalidationcallback_cache_certificate_expe…
liveans Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,14 @@ public struct WINHTTP_ASYNC_RESULT
public uint dwError;
}

[StructLayout(LayoutKind.Sequential)]
public unsafe struct WINHTTP_CONNECTION_INFO
{
public uint cbSize;
public uint __alignment;
public fixed byte LocalAddress[128];
public fixed byte RemoteAddress[128];
}

[StructLayout(LayoutKind.Sequential)]
public struct tcp_keepalive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1644,7 +1644,8 @@ private void SetStatusCallback(
Interop.WinHttp.WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS |
Interop.WinHttp.WINHTTP_CALLBACK_FLAG_HANDLES |
Interop.WinHttp.WINHTTP_CALLBACK_FLAG_REDIRECT |
Interop.WinHttp.WINHTTP_CALLBACK_FLAG_SEND_REQUEST;
Interop.WinHttp.WINHTTP_CALLBACK_FLAG_SEND_REQUEST |
Interop.WinHttp.WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER;

IntPtr oldCallback = Interop.WinHttp.WinHttpSetStatusCallback(
requestHandle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;

Expand All @@ -20,6 +23,8 @@ internal static class WinHttpRequestCallback
public static Interop.WinHttp.WINHTTP_STATUS_CALLBACK StaticCallbackDelegate =
new Interop.WinHttp.WINHTTP_STATUS_CALLBACK(WinHttpCallback);

private static ConcurrentDictionary<IPAddress, string> s_cachedCertificates = new();

public static void WinHttpCallback(
IntPtr handle,
IntPtr context,
Expand Down Expand Up @@ -56,6 +61,11 @@ private static void RequestCallback(
{
switch (internetStatus)
{
case Interop.WinHttp.WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER:
IPAddress connectedToIPAddress = IPAddress.Parse(Marshal.PtrToStringUni(statusInformation)!);
OnRequestConnectedToServer(connectedToIPAddress);
return;

case Interop.WinHttp.WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
OnRequestHandleClosing(state);
return;
Expand Down Expand Up @@ -121,6 +131,18 @@ private static void RequestCallback(
}
}

private static void OnRequestConnectedToServer(IPAddress connectedIPAddress)
{
if (s_cachedCertificates.TryRemove(connectedIPAddress, out _))
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"Removed cached certificate for {connectedIPAddress}");
}
else
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"No cached certificate for {connectedIPAddress} to remove");
}
}

private static void OnRequestHandleClosing(WinHttpRequestState state)
{
Debug.Assert(state != null, "OnRequestSendRequestComplete: state is null");
Expand Down Expand Up @@ -279,6 +301,48 @@ private static void OnRequestSendingRequest(WinHttpRequestState state)
var serverCertificate = new X509Certificate2(certHandle);
Interop.Crypt32.CertFreeCertificateContext(certHandle);

IPAddress? ipAddress = null;
unsafe
{
Interop.WinHttp.WINHTTP_CONNECTION_INFO connectionInfo;
Interop.WinHttp.WINHTTP_CONNECTION_INFO* pConnectionInfo = &connectionInfo;
uint infoSize = (uint)sizeof(Interop.WinHttp.WINHTTP_CONNECTION_INFO);
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(state, $"sizeof(WINHTTP_CONNECTION_INFO)={infoSize}");
if (Interop.WinHttp.WinHttpQueryOption(
state.RequestHandle,
Interop.WinHttp.WINHTTP_OPTION_CONNECTION_INFO,
(IntPtr)pConnectionInfo,
ref infoSize))
{
ReadOnlySpan<byte> RemoteAddressSpan = new ReadOnlySpan<byte>(connectionInfo.RemoteAddress, 128);
AddressFamily addressFamily = (AddressFamily)BitConverter.ToInt16(RemoteAddressSpan.ToArray(), 0);
ipAddress = addressFamily switch
{
AddressFamily.InterNetwork => new IPAddress(BinaryPrimitives.ReadUInt32LittleEndian(RemoteAddressSpan.Slice(4))),
AddressFamily.InterNetworkV6 => new IPAddress(RemoteAddressSpan.Slice(8, 16).ToArray()),
_ => null
};
if (ipAddress != null && NetEventSource.Log.IsEnabled()) NetEventSource.Info(state, $"ipAddress: {ipAddress}");

}
else
{
int lastError = Marshal.GetLastWin32Error();
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(state, $"Error getting WINHTTP_OPTION_CONNECTION_INFO, {lastError}");
}
}

if (ipAddress is not null && s_cachedCertificates.TryGetValue(ipAddress, out string? thumbprint) && thumbprint == serverCertificate.Thumbprint)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(state, $"Skipping certificate validation. ipAddress: {ipAddress}, Thumbprint: {serverCertificate.Thumbprint}");
serverCertificate.Dispose();
return;
}
else
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(state, $"Certificate validation is required! IPAddress = {ipAddress}, Thumbprint: {serverCertificate.Thumbprint}");
}

X509Chain? chain = null;
SslPolicyErrors sslPolicyErrors;
bool result = false;
Expand All @@ -298,6 +362,10 @@ private static void OnRequestSendingRequest(WinHttpRequestState state)
serverCertificate,
chain,
sslPolicyErrors);
if (result && ipAddress is not null)
{
s_cachedCertificates[ipAddress] = serverCertificate.Thumbprint;
}
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using TestUtilities;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -46,6 +46,66 @@ public void SendAsync_SimpleGet_Success()
}
}

[OuterLoop]
[Fact]
public async Task SendAsync_ServerCertificateValidationCallback_CalledOnce()
{
using TestEventListener listener = new TestEventListener(_output, TestEventListener.NetworkingEvents);
int callbackCount = 0;
var handler = new WinHttpHandler()
{
ServerCertificateValidationCallback = (m, cert, chain, err) =>
{
Interlocked.Increment(ref callbackCount);
return true;
}
};
using (var client = new HttpClient(handler))
{
for (int i = 0; i < 5; i++)
{
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, Configuration.Http.SecureRemoteEchoServer)
{
Version = HttpVersion.Version11
});
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
_ = await response.Content.ReadAsStringAsync();
}
Assert.Equal(1, callbackCount);
}
}

[OuterLoop]
[Fact]
public async Task SendAsync_ServerCertificateValidationCallbackHttp2_CalledOnce()
{
using TestEventListener testEventListener = new TestEventListener(_output, TestEventListener.NetworkingEvents);
int callbackCount = 0;


var handler = new WinHttpHandler()
{
ServerCertificateValidationCallback = (m, cert, chain, err) =>
{
Interlocked.Increment(ref callbackCount);
return true;
}
};
using (var client = new HttpClient(handler))
{
for (int i = 0; i < 5; i++)
{
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, System.Net.Test.Common.Configuration.Http.Http2RemoteEchoServer)
{
Version = HttpVersion20.Value
});
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
_ = await response.Content.ReadAsStringAsync();
}
Assert.Equal(1, callbackCount);
}
}

[OuterLoop]
[Theory]
[InlineData(CookieUsePolicy.UseInternalCookieStoreOnly, "cookieName1", "cookieValue1")]
Expand Down
Loading