|
1 | 1 | // Licensed to the .NET Foundation under one or more agreements.
|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 |
|
| 4 | +using System.Collections.Concurrent; |
4 | 5 | using System.Collections.Generic;
|
5 | 6 | using System.Diagnostics;
|
| 7 | +using System.Diagnostics.CodeAnalysis; |
6 | 8 | using System.IO;
|
7 | 9 | using System.Net.Http.Headers;
|
8 | 10 | using System.Net.Security;
|
@@ -41,11 +43,14 @@ public class WinHttpHandler : HttpMessageHandler
|
41 | 43 | internal static readonly Version HttpVersion20 = new Version(2, 0);
|
42 | 44 | internal static readonly Version HttpVersion30 = new Version(3, 0);
|
43 | 45 | internal static readonly Version HttpVersionUnknown = new Version(0, 0);
|
| 46 | + internal static bool CertificateCachingAppContextSwitchEnabled { get; } = AppContext.TryGetSwitch("System.Net.Http.UseWinHttpCertificateCaching", out bool enabled) && enabled; |
44 | 47 | private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue);
|
45 | 48 |
|
46 | 49 | private static readonly StringWithQualityHeaderValue s_gzipHeaderValue = new StringWithQualityHeaderValue("gzip");
|
47 | 50 | private static readonly StringWithQualityHeaderValue s_deflateHeaderValue = new StringWithQualityHeaderValue("deflate");
|
48 | 51 | private static readonly Lazy<bool> s_supportsTls13 = new Lazy<bool>(CheckTls13Support);
|
| 52 | + private static readonly TimeSpan s_cleanCachedCertificateTimeout = TimeSpan.FromMilliseconds((int?)AppDomain.CurrentDomain.GetData("System.Net.Http.WinHttpCertificateCachingCleanupTimerInterval") ?? 60_000); |
| 53 | + private static readonly long s_staleTimeout = (long)(s_cleanCachedCertificateTimeout.TotalSeconds * Stopwatch.Frequency); |
49 | 54 |
|
50 | 55 | [ThreadStatic]
|
51 | 56 | private static StringBuilder? t_requestHeadersBuilder;
|
@@ -93,9 +98,44 @@ private Func<
|
93 | 98 | private volatile bool _disposed;
|
94 | 99 | private SafeWinHttpHandle? _sessionHandle;
|
95 | 100 | private readonly WinHttpAuthHelper _authHelper = new WinHttpAuthHelper();
|
| 101 | + private readonly Timer? _certificateCleanupTimer; |
| 102 | + private bool _isTimerRunning; |
| 103 | + private readonly ConcurrentDictionary<CachedCertificateKey, CachedCertificateValue> _cachedCertificates = new(); |
96 | 104 |
|
97 | 105 | public WinHttpHandler()
|
98 | 106 | {
|
| 107 | + if (CertificateCachingAppContextSwitchEnabled) |
| 108 | + { |
| 109 | + WeakReference<WinHttpHandler> thisRef = new(this); |
| 110 | + bool restoreFlow = false; |
| 111 | + try |
| 112 | + { |
| 113 | + if (!ExecutionContext.IsFlowSuppressed()) |
| 114 | + { |
| 115 | + ExecutionContext.SuppressFlow(); |
| 116 | + restoreFlow = true; |
| 117 | + } |
| 118 | + |
| 119 | + _certificateCleanupTimer = new Timer( |
| 120 | + static s => |
| 121 | + { |
| 122 | + if (((WeakReference<WinHttpHandler>)s!).TryGetTarget(out WinHttpHandler? thisRef)) |
| 123 | + { |
| 124 | + thisRef.ClearStaleCertificates(); |
| 125 | + } |
| 126 | + }, |
| 127 | + thisRef, |
| 128 | + Timeout.Infinite, |
| 129 | + Timeout.Infinite); |
| 130 | + } |
| 131 | + finally |
| 132 | + { |
| 133 | + if (restoreFlow) |
| 134 | + { |
| 135 | + ExecutionContext.RestoreFlow(); |
| 136 | + } |
| 137 | + } |
| 138 | + } |
99 | 139 | }
|
100 | 140 |
|
101 | 141 | #region Properties
|
@@ -543,9 +583,12 @@ protected override void Dispose(bool disposing)
|
543 | 583 | {
|
544 | 584 | _disposed = true;
|
545 | 585 |
|
546 |
| - if (disposing && _sessionHandle != null) |
| 586 | + if (disposing) |
547 | 587 | {
|
548 |
| - SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle); |
| 588 | + if (_sessionHandle is not null) { |
| 589 | + SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle); |
| 590 | + } |
| 591 | + _certificateCleanupTimer?.Dispose(); |
549 | 592 | }
|
550 | 593 | }
|
551 | 594 |
|
@@ -1644,7 +1687,8 @@ private void SetStatusCallback(
|
1644 | 1687 | Interop.WinHttp.WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS |
|
1645 | 1688 | Interop.WinHttp.WINHTTP_CALLBACK_FLAG_HANDLES |
|
1646 | 1689 | Interop.WinHttp.WINHTTP_CALLBACK_FLAG_REDIRECT |
|
1647 |
| - Interop.WinHttp.WINHTTP_CALLBACK_FLAG_SEND_REQUEST; |
| 1690 | + Interop.WinHttp.WINHTTP_CALLBACK_FLAG_SEND_REQUEST | |
| 1691 | + Interop.WinHttp.WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER; |
1648 | 1692 |
|
1649 | 1693 | IntPtr oldCallback = Interop.WinHttp.WinHttpSetStatusCallback(
|
1650 | 1694 | requestHandle,
|
@@ -1730,5 +1774,90 @@ private RendezvousAwaitable<int> InternalReceiveResponseHeadersAsync(WinHttpRequ
|
1730 | 1774 |
|
1731 | 1775 | return state.LifecycleAwaitable;
|
1732 | 1776 | }
|
| 1777 | + |
| 1778 | + internal bool GetCertificateFromCache(CachedCertificateKey key, [NotNullWhen(true)] out byte[]? rawCertificateBytes) |
| 1779 | + { |
| 1780 | + if (_cachedCertificates.TryGetValue(key, out CachedCertificateValue? cachedValue)) |
| 1781 | + { |
| 1782 | + cachedValue.LastUsedTime = Stopwatch.GetTimestamp(); |
| 1783 | + rawCertificateBytes = cachedValue.RawCertificateData; |
| 1784 | + return true; |
| 1785 | + } |
| 1786 | + |
| 1787 | + rawCertificateBytes = null; |
| 1788 | + return false; |
| 1789 | + } |
| 1790 | + |
| 1791 | + internal void AddCertificateToCache(CachedCertificateKey key, byte[] rawCertificateData) |
| 1792 | + { |
| 1793 | + if (_cachedCertificates.TryAdd(key, new CachedCertificateValue(rawCertificateData, Stopwatch.GetTimestamp()))) |
| 1794 | + { |
| 1795 | + EnsureCleanupTimerRunning(); |
| 1796 | + } |
| 1797 | + } |
| 1798 | + |
| 1799 | + internal bool TryRemoveCertificateFromCache(CachedCertificateKey key) |
| 1800 | + { |
| 1801 | + bool result = _cachedCertificates.TryRemove(key, out _); |
| 1802 | + if (result) |
| 1803 | + { |
| 1804 | + StopCleanupTimerIfEmpty(); |
| 1805 | + } |
| 1806 | + return result; |
| 1807 | + } |
| 1808 | + |
| 1809 | + private void ChangeCleanerTimer(TimeSpan timeout) |
| 1810 | + { |
| 1811 | + Debug.Assert(Monitor.IsEntered(_lockObject)); |
| 1812 | + Debug.Assert(_certificateCleanupTimer != null); |
| 1813 | + if (_certificateCleanupTimer!.Change(timeout, Timeout.InfiniteTimeSpan)) |
| 1814 | + { |
| 1815 | + _isTimerRunning = timeout != Timeout.InfiniteTimeSpan; |
| 1816 | + } |
| 1817 | + } |
| 1818 | + |
| 1819 | + private void ClearStaleCertificates() |
| 1820 | + { |
| 1821 | + foreach (KeyValuePair<CachedCertificateKey, CachedCertificateValue> kvPair in _cachedCertificates) |
| 1822 | + { |
| 1823 | + if (IsStale(kvPair.Value.LastUsedTime)) |
| 1824 | + { |
| 1825 | + _cachedCertificates.TryRemove(kvPair.Key, out _); |
| 1826 | + } |
| 1827 | + } |
| 1828 | + |
| 1829 | + lock (_lockObject) |
| 1830 | + { |
| 1831 | + ChangeCleanerTimer(_cachedCertificates.IsEmpty ? Timeout.InfiniteTimeSpan : s_cleanCachedCertificateTimeout); |
| 1832 | + } |
| 1833 | + |
| 1834 | + static bool IsStale(long lastUsedTime) |
| 1835 | + { |
| 1836 | + long now = Stopwatch.GetTimestamp(); |
| 1837 | + return (now - lastUsedTime) > s_staleTimeout; |
| 1838 | + } |
| 1839 | + } |
| 1840 | + |
| 1841 | + private void EnsureCleanupTimerRunning() |
| 1842 | + { |
| 1843 | + lock (_lockObject) |
| 1844 | + { |
| 1845 | + if (!_cachedCertificates.IsEmpty && !_isTimerRunning) |
| 1846 | + { |
| 1847 | + ChangeCleanerTimer(s_cleanCachedCertificateTimeout); |
| 1848 | + } |
| 1849 | + } |
| 1850 | + } |
| 1851 | + |
| 1852 | + private void StopCleanupTimerIfEmpty() |
| 1853 | + { |
| 1854 | + lock (_lockObject) |
| 1855 | + { |
| 1856 | + if (_cachedCertificates.IsEmpty && _isTimerRunning) |
| 1857 | + { |
| 1858 | + ChangeCleanerTimer(Timeout.InfiniteTimeSpan); |
| 1859 | + } |
| 1860 | + } |
| 1861 | + } |
1733 | 1862 | }
|
1734 | 1863 | }
|
0 commit comments