From 92c1b127648042320ed7cb5be71b0c2267a2bad9 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 8 Jan 2024 18:23:32 +0800 Subject: [PATCH 1/4] dynamic proxy --- .../Snap.Hutao.Win32/Interop.Libraries.cs | 54 ++ .../Snap.Hutao.Win32/Interop.WinHttp.cs | 624 ++++++++++++++++++ .../Snap.Hutao.Win32/NativeMethods.txt | 13 +- src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs | 9 + .../DependencyInjection.cs | 8 + .../IO/Http/DynamicProxy/DynamicHttpProxy.cs | 81 +++ .../IO/Http/DynamicProxy/FailedProxyCache.cs | 138 ++++ .../Core/IO/Http/DynamicProxy/HttpNoProxy.cs | 15 + .../IO/Http/DynamicProxy/HttpWindowsProxy.cs | 325 +++++++++ .../IO/Http/DynamicProxy/IMultiWebProxy.cs | 16 + .../Core/IO/Http/DynamicProxy/MultiProxy.cs | 268 ++++++++ .../Core/IO/Http/DynamicProxy/SimpleRegex.cs | 73 ++ .../Core/IO/Http/DynamicProxy/UriScheme.cs | 24 + .../Http/DynamicProxy/WinInetProxyHelper.cs | 179 +++++ .../Snap.Hutao/Core/Shell/RegistryMonitor.cs | 146 ++++ 15 files changed, 1972 insertions(+), 1 deletion(-) create mode 100644 src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs create mode 100644 src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs new file mode 100644 index 00000000..32c7a1f4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs @@ -0,0 +1,54 @@ +namespace Snap.Hutao.Win32; + +internal static partial class Interop +{ + internal static partial class Libraries + { + internal const string Activeds = "activeds.dll"; + internal const string Advapi32 = "advapi32.dll"; + internal const string Authz = "authz.dll"; + internal const string BCrypt = "BCrypt.dll"; + internal const string Credui = "credui.dll"; + internal const string Crypt32 = "crypt32.dll"; + internal const string CryptUI = "cryptui.dll"; + internal const string Dnsapi = "dnsapi.dll"; + internal const string Dsrole = "dsrole.dll"; + internal const string Gdi32 = "gdi32.dll"; + internal const string HttpApi = "httpapi.dll"; + internal const string IpHlpApi = "iphlpapi.dll"; + internal const string Kernel32 = "kernel32.dll"; + internal const string Logoncli = "logoncli.dll"; + internal const string Mswsock = "mswsock.dll"; + internal const string NCrypt = "ncrypt.dll"; + internal const string Netapi32 = "netapi32.dll"; + internal const string Netutils = "netutils.dll"; + internal const string NtDll = "ntdll.dll"; + internal const string Odbc32 = "odbc32.dll"; + internal const string Ole32 = "ole32.dll"; + internal const string OleAut32 = "oleaut32.dll"; + internal const string Pdh = "pdh.dll"; + internal const string Secur32 = "secur32.dll"; + internal const string Shell32 = "shell32.dll"; + internal const string SspiCli = "sspicli.dll"; + internal const string User32 = "user32.dll"; + internal const string Version = "version.dll"; + internal const string WebSocket = "websocket.dll"; + internal const string Wevtapi = "wevtapi.dll"; + internal const string WinHttp = "winhttp.dll"; + internal const string WinMM = "winmm.dll"; + internal const string Wkscli = "wkscli.dll"; + internal const string Wldap32 = "wldap32.dll"; + internal const string Ws2_32 = "ws2_32.dll"; + internal const string Wtsapi32 = "wtsapi32.dll"; + internal const string CompressionNative = "System.IO.Compression.Native"; + internal const string GlobalizationNative = "System.Globalization.Native"; + internal const string MsQuic = "msquic.dll"; + internal const string HostPolicy = "hostpolicy"; + internal const string Ucrtbase = "ucrtbase.dll"; + internal const string Xolehlp = "xolehlp.dll"; + internal const string Comdlg32 = "comdlg32.dll"; + internal const string Gdiplus = "gdiplus.dll"; + internal const string Oleaut32 = "oleaut32.dll"; + internal const string Winspool = "winspool.drv"; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs new file mode 100644 index 00000000..84d74504 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs @@ -0,0 +1,624 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Text; + +namespace Snap.Hutao.Win32; + +internal static partial class Interop +{ + internal static partial class WinHttp + { + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + public static partial SafeWinHttpHandle WinHttpOpen( + IntPtr userAgent, + uint accessType, + string? proxyName, + string? proxyBypass, int flags); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpCloseHandle( + IntPtr handle); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + public static partial SafeWinHttpHandle WinHttpConnect( + SafeWinHttpHandle sessionHandle, + string serverName, + ushort serverPort, + uint reserved); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + public static partial SafeWinHttpHandle WinHttpOpenRequest( + SafeWinHttpHandle connectHandle, + string verb, + string objectName, + string? version, + string referrer, + string acceptTypes, + uint flags); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpAddRequestHeaders( + SafeWinHttpHandle requestHandle, +#if NET7_0_OR_GREATER + [MarshalUsing(typeof(SimpleStringBufferMarshaller))] StringBuilder headers, +#else +#pragma warning disable CA1838 // Uses pooled StringBuilder + [In] StringBuilder headers, +#pragma warning restore CA1838 // Uses pooled StringBuilder +#endif + uint headersLength, + uint modifiers); + +#if NET7_0_OR_GREATER + [CustomMarshaller(typeof(StringBuilder), MarshalMode.ManagedToUnmanagedIn, typeof(SimpleStringBufferMarshaller))] + private static unsafe class SimpleStringBufferMarshaller + { + public static void* ConvertToUnmanaged(StringBuilder builder) + { + int length = builder.Length + 1; + void* value = NativeMemory.Alloc(sizeof(char) * (nuint)length); + Span buffer = new(value, length); + buffer.Clear(); + builder.CopyTo(0, buffer, length - 1); + return value; + } + + public static void Free(void* value) => NativeMemory.Free(value); + } +#endif + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpAddRequestHeaders( + SafeWinHttpHandle requestHandle, + string headers, + uint headersLength, + uint modifiers); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpSendRequest( + SafeWinHttpHandle requestHandle, + IntPtr headers, + uint headersLength, + IntPtr optional, + uint optionalLength, + uint totalLength, + IntPtr context); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpReceiveResponse( + SafeWinHttpHandle requestHandle, + IntPtr reserved); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryDataAvailable( + SafeWinHttpHandle requestHandle, + IntPtr parameterIgnoredAndShouldBeNullForAsync); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpReadData( + SafeWinHttpHandle requestHandle, + IntPtr buffer, + uint bufferSize, + IntPtr parameterIgnoredAndShouldBeNullForAsync); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryHeaders( + SafeWinHttpHandle requestHandle, + uint infoLevel, + string name, + IntPtr buffer, + ref uint bufferLength, + ref uint index); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryHeaders( + SafeWinHttpHandle requestHandle, + uint infoLevel, + string name, + ref uint number, + ref uint bufferLength, + IntPtr index); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryOption( + SafeWinHttpHandle handle, + uint option, + ref IntPtr buffer, + ref uint bufferSize); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryOption( + SafeWinHttpHandle handle, + uint option, + IntPtr buffer, + ref uint bufferSize); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryOption( + SafeWinHttpHandle handle, + uint option, + ref uint buffer, + ref uint bufferSize); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpWriteData( + SafeWinHttpHandle requestHandle, + IntPtr buffer, + uint bufferSize, + IntPtr parameterIgnoredAndShouldBeNullForAsync); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpSetOption( + SafeWinHttpHandle handle, + uint option, + ref uint optionData, + uint optionLength = sizeof(uint)); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpSetOption( + SafeWinHttpHandle handle, + uint option, + IntPtr optionData, + uint optionLength); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpSetCredentials( + SafeWinHttpHandle requestHandle, + uint authTargets, + uint authScheme, + string? userName, + string? password, + IntPtr reserved); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpQueryAuthSchemes( + SafeWinHttpHandle requestHandle, + out uint supportedSchemes, + out uint firstScheme, + out uint authTarget); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpSetTimeouts( + SafeWinHttpHandle handle, + int resolveTimeout, + int connectTimeout, + int sendTimeout, + int receiveTimeout); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpGetIEProxyConfigForCurrentUser( + out WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WinHttpGetProxyForUrl( + SafeWinHttpHandle? sessionHandle, + string url, + ref WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions, + out WINHTTP_PROXY_INFO proxyInfo); + + [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + public static partial IntPtr WinHttpSetStatusCallback( + SafeWinHttpHandle handle, + WINHTTP_STATUS_CALLBACK callback, + uint notificationFlags, + IntPtr reserved); + } + internal static partial class WinHttp + { + internal class SafeWinHttpHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeWinHttpHandle? _parentHandle; + + public SafeWinHttpHandle() : base(true) + { + } + + public static void DisposeAndClearHandle(ref SafeWinHttpHandle? safeHandle) + { + if (safeHandle != null) + { + safeHandle.Dispose(); + safeHandle = null; + } + } + + public void SetParentHandle(SafeWinHttpHandle parentHandle) + { + Debug.Assert(_parentHandle == null); + Debug.Assert(parentHandle != null); + Debug.Assert(!parentHandle.IsInvalid); + + bool ignore = false; + parentHandle.DangerousAddRef(ref ignore); + + _parentHandle = parentHandle; + } + + // Important: WinHttp API calls should not happen while another WinHttp call for the same handle did not + // return. During finalization that was not initiated by the Dispose pattern we don't expect any other WinHttp + // calls in progress. + protected override bool ReleaseHandle() + { + if (_parentHandle != null) + { + _parentHandle.DangerousRelease(); + _parentHandle = null; + } + + return Interop.WinHttp.WinHttpCloseHandle(handle); + } + } + } + internal static partial class WinHttp + { + public const uint ERROR_SUCCESS = 0; + public const uint ERROR_FILE_NOT_FOUND = 2; + public const uint ERROR_INVALID_HANDLE = 6; + public const uint ERROR_INVALID_PARAMETER = 87; + public const uint ERROR_INSUFFICIENT_BUFFER = 122; + public const uint ERROR_NOT_FOUND = 1168; + public const uint ERROR_WINHTTP_INVALID_OPTION = 12009; + public const uint ERROR_WINHTTP_LOGIN_FAILURE = 12015; + public const uint ERROR_WINHTTP_OPERATION_CANCELLED = 12017; + public const uint ERROR_WINHTTP_INCORRECT_HANDLE_STATE = 12019; + public const uint ERROR_WINHTTP_CONNECTION_ERROR = 12030; + public const uint ERROR_WINHTTP_RESEND_REQUEST = 12032; + public const uint ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED = 12044; + public const uint ERROR_WINHTTP_HEADER_NOT_FOUND = 12150; + public const uint ERROR_WINHTTP_SECURE_FAILURE = 12175; + public const uint ERROR_WINHTTP_AUTODETECTION_FAILED = 12180; + + public const uint WINHTTP_OPTION_PROXY = 38; + public const uint WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0; + public const uint WINHTTP_ACCESS_TYPE_NO_PROXY = 1; + public const uint WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3; + public const uint WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4; + + public const uint WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001; + public const uint WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002; + public const uint WINHTTP_AUTOPROXY_HOST_KEEPCASE = 0x00000004; + public const uint WINHTTP_AUTOPROXY_HOST_LOWERCASE = 0x00000008; + public const uint WINHTTP_AUTOPROXY_RUN_INPROCESS = 0x00010000; + public const uint WINHTTP_AUTOPROXY_RUN_OUTPROCESS_ONLY = 0x00020000; + public const uint WINHTTP_AUTOPROXY_NO_DIRECTACCESS = 0x00040000; + public const uint WINHTTP_AUTOPROXY_NO_CACHE_CLIENT = 0x00080000; + public const uint WINHTTP_AUTOPROXY_NO_CACHE_SVC = 0x00100000; + public const uint WINHTTP_AUTOPROXY_SORT_RESULTS = 0x00400000; + + public const uint WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001; + public const uint WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002; + + public const string WINHTTP_NO_PROXY_NAME = null; + public const string WINHTTP_NO_PROXY_BYPASS = null; + + public const uint WINHTTP_ADDREQ_FLAG_ADD = 0x20000000; + public const uint WINHTTP_ADDREQ_FLAG_REPLACE = 0x80000000; + + public const string WINHTTP_NO_REFERER = null; + public const string WINHTTP_DEFAULT_ACCEPT_TYPES = null; + + public const ushort INTERNET_DEFAULT_PORT = 0; + public const ushort INTERNET_DEFAULT_HTTP_PORT = 80; + public const ushort INTERNET_DEFAULT_HTTPS_PORT = 443; + + public const uint WINHTTP_FLAG_SECURE = 0x00800000; + public const uint WINHTTP_FLAG_ESCAPE_DISABLE = 0x00000040; + public const uint WINHTTP_FLAG_AUTOMATIC_CHUNKING = 0x00000200; + + public const uint WINHTTP_QUERY_FLAG_NUMBER = 0x20000000; + public const uint WINHTTP_QUERY_VERSION = 18; + public const uint WINHTTP_QUERY_STATUS_CODE = 19; + public const uint WINHTTP_QUERY_STATUS_TEXT = 20; + public const uint WINHTTP_QUERY_RAW_HEADERS = 21; + public const uint WINHTTP_QUERY_RAW_HEADERS_CRLF = 22; + public const uint WINHTTP_QUERY_FLAG_TRAILERS = 0x02000000; + public const uint WINHTTP_QUERY_CONTENT_ENCODING = 29; + public const uint WINHTTP_QUERY_SET_COOKIE = 43; + public const uint WINHTTP_QUERY_CUSTOM = 65535; + public const string WINHTTP_HEADER_NAME_BY_INDEX = null; + public const byte[] WINHTTP_NO_OUTPUT_BUFFER = null; + + public const uint WINHTTP_OPTION_DECOMPRESSION = 118; + public const uint WINHTTP_DECOMPRESSION_FLAG_GZIP = 0x00000001; + public const uint WINHTTP_DECOMPRESSION_FLAG_DEFLATE = 0x00000002; + public const uint WINHTTP_DECOMPRESSION_FLAG_ALL = WINHTTP_DECOMPRESSION_FLAG_GZIP | WINHTTP_DECOMPRESSION_FLAG_DEFLATE; + + public const uint WINHTTP_OPTION_REDIRECT_POLICY = 88; + public const uint WINHTTP_OPTION_REDIRECT_POLICY_NEVER = 0; + public const uint WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP = 1; + public const uint WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS = 2; + public const uint WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS = 89; + + public const uint WINHTTP_OPTION_MAX_CONNS_PER_SERVER = 73; + public const uint WINHTTP_OPTION_MAX_CONNS_PER_1_0_SERVER = 74; + + public const uint WINHTTP_OPTION_DISABLE_FEATURE = 63; + public const uint WINHTTP_DISABLE_COOKIES = 0x00000001; + public const uint WINHTTP_DISABLE_REDIRECTS = 0x00000002; + public const uint WINHTTP_DISABLE_AUTHENTICATION = 0x00000004; + public const uint WINHTTP_DISABLE_KEEP_ALIVE = 0x00000008; + + public const uint WINHTTP_OPTION_ENABLE_FEATURE = 79; + public const uint WINHTTP_ENABLE_SSL_REVOCATION = 0x00000001; + + public const uint WINHTTP_OPTION_CLIENT_CERT_CONTEXT = 47; + public const uint WINHTTP_OPTION_CLIENT_CERT_ISSUER_LIST = 94; + public const uint WINHTTP_OPTION_SERVER_CERT_CONTEXT = 78; + public const uint WINHTTP_OPTION_SECURITY_FLAGS = 31; + public const uint WINHTTP_OPTION_SECURE_PROTOCOLS = 84; + public const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 = 0x00000008; + public const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 = 0x00000020; + public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 = 0x00000080; + public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 = 0x00000200; + public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 = 0x00000800; + public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3 = 0x00002000; + + public const uint SECURITY_FLAG_IGNORE_UNKNOWN_CA = 0x00000100; + public const uint SECURITY_FLAG_IGNORE_CERT_DATE_INVALID = 0x00002000; + public const uint SECURITY_FLAG_IGNORE_CERT_CN_INVALID = 0x00001000; + public const uint SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE = 0x00000200; + + public const uint WINHTTP_OPTION_AUTOLOGON_POLICY = 77; + public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM = 0; // default creds only sent to intranet servers (default) + public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW = 1; // default creds set to all servers + public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH = 2; // default creds never sent + + public const uint WINHTTP_AUTH_SCHEME_BASIC = 0x00000001; + public const uint WINHTTP_AUTH_SCHEME_NTLM = 0x00000002; + public const uint WINHTTP_AUTH_SCHEME_PASSPORT = 0x00000004; + public const uint WINHTTP_AUTH_SCHEME_DIGEST = 0x00000008; + public const uint WINHTTP_AUTH_SCHEME_NEGOTIATE = 0x00000010; + + public const uint WINHTTP_AUTH_TARGET_SERVER = 0x00000000; + public const uint WINHTTP_AUTH_TARGET_PROXY = 0x00000001; + + public const uint WINHTTP_OPTION_USERNAME = 0x1000; + // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Suppression approved. It is property descriptor, not secret value.")] + public const uint WINHTTP_OPTION_PASSWORD = 0x1001; + public const uint WINHTTP_OPTION_PROXY_USERNAME = 0x1002; + // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Suppression approved. It is property descriptor, not secret value.")] + public const uint WINHTTP_OPTION_PROXY_PASSWORD = 0x1003; + + public const uint WINHTTP_OPTION_SERVER_SPN_USED = 106; + public const uint WINHTTP_OPTION_SERVER_CBT = 108; + + public const uint WINHTTP_OPTION_CONNECT_TIMEOUT = 3; + public const uint WINHTTP_OPTION_SEND_TIMEOUT = 5; + public const uint WINHTTP_OPTION_RECEIVE_TIMEOUT = 6; + + public const uint WINHTTP_OPTION_URL = 34; + + public const uint WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE = 91; + public const uint WINHTTP_OPTION_MAX_RESPONSE_DRAIN_SIZE = 92; + public const uint WINHTTP_OPTION_CONNECTION_INFO = 93; + + public const uint WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS = 111; + + public const uint WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT = 161; + public const uint WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL = 133; + public const uint WINHTTP_OPTION_HTTP_PROTOCOL_USED = 134; + public const uint WINHTTP_PROTOCOL_FLAG_HTTP2 = 0x1; + public const uint WINHTTP_HTTP2_PLUS_CLIENT_CERT_FLAG = 0x1; + public const uint WINHTTP_OPTION_DISABLE_STREAM_QUEUE = 139; + + public const uint WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114; + public const uint WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT = 115; + public const uint WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL = 116; + + 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 const uint WINHTTP_OPTION_STREAM_ERROR_CODE = 159; + public const uint WINHTTP_OPTION_REQUIRE_STREAM_END = 160; + + public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE + { + WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE = 0, + WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE = 1, + WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE = 2, + WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE = 3, + WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE = 4 + } + + public const uint WINHTTP_OPTION_CONTEXT_VALUE = 45; + + public const uint WINHTTP_FLAG_ASYNC = 0x10000000; + + public const uint WINHTTP_CALLBACK_STATUS_RESOLVING_NAME = 0x00000001; + public const uint WINHTTP_CALLBACK_STATUS_NAME_RESOLVED = 0x00000002; + public const uint WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER = 0x00000004; + public const uint WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER = 0x00000008; + public const uint WINHTTP_CALLBACK_STATUS_SENDING_REQUEST = 0x00000010; + public const uint WINHTTP_CALLBACK_STATUS_REQUEST_SENT = 0x00000020; + public const uint WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE = 0x00000040; + public const uint WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED = 0x00000080; + public const uint WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION = 0x00000100; + public const uint WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED = 0x00000200; + public const uint WINHTTP_CALLBACK_STATUS_HANDLE_CREATED = 0x00000400; + public const uint WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING = 0x00000800; + public const uint WINHTTP_CALLBACK_STATUS_DETECTING_PROXY = 0x00001000; + public const uint WINHTTP_CALLBACK_STATUS_REDIRECT = 0x00004000; + public const uint WINHTTP_CALLBACK_STATUS_INTERMEDIATE_RESPONSE = 0x00008000; + public const uint WINHTTP_CALLBACK_STATUS_SECURE_FAILURE = 0x00010000; + public const uint WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE = 0x00020000; + public const uint WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE = 0x00040000; + public const uint WINHTTP_CALLBACK_STATUS_READ_COMPLETE = 0x00080000; + public const uint WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE = 0x00100000; + public const uint WINHTTP_CALLBACK_STATUS_REQUEST_ERROR = 0x00200000; + public const uint WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE = 0x00400000; + public const uint WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE = 0x01000000; + public const uint WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE = 0x02000000; + public const uint WINHTTP_CALLBACK_STATUS_SHUTDOWN_COMPLETE = 0x04000000; + + public const uint WINHTTP_CALLBACK_FLAG_SEND_REQUEST = + WINHTTP_CALLBACK_STATUS_SENDING_REQUEST | + WINHTTP_CALLBACK_STATUS_REQUEST_SENT; + public const uint WINHTTP_CALLBACK_FLAG_HANDLES = + WINHTTP_CALLBACK_STATUS_HANDLE_CREATED | + WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING; + public const uint WINHTTP_CALLBACK_FLAG_REDIRECT = WINHTTP_CALLBACK_STATUS_REDIRECT; + public const uint WINHTTP_CALLBACK_FLAG_SECURE_FAILURE = WINHTTP_CALLBACK_STATUS_SECURE_FAILURE; + public const uint WINHTTP_CALLBACK_FLAG_SENDREQUEST_COMPLETE = WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE; + public const uint WINHTTP_CALLBACK_FLAG_HEADERS_AVAILABLE = WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE; + public const uint WINHTTP_CALLBACK_FLAG_DATA_AVAILABLE = WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE; + public const uint WINHTTP_CALLBACK_FLAG_READ_COMPLETE = WINHTTP_CALLBACK_STATUS_READ_COMPLETE; + public const uint WINHTTP_CALLBACK_FLAG_WRITE_COMPLETE = WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE; + public const uint WINHTTP_CALLBACK_FLAG_REQUEST_ERROR = WINHTTP_CALLBACK_STATUS_REQUEST_ERROR; + public const uint WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE = WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE; + public const uint WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS = + WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE | + WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE | + WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE | + WINHTTP_CALLBACK_STATUS_READ_COMPLETE | + WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE | + WINHTTP_CALLBACK_STATUS_REQUEST_ERROR | + WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE; + public const uint WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS = 0xFFFFFFFF; + + public const uint WININET_E_CONNECTION_RESET = 0x80072EFF; + + public const int WINHTTP_INVALID_STATUS_CALLBACK = -1; + public delegate void WINHTTP_STATUS_CALLBACK( + IntPtr handle, + IntPtr context, + uint internetStatus, + IntPtr statusInformation, + uint statusInformationLength); + +#if NET7_0_OR_GREATER + [NativeMarshalling(typeof(Marshaller))] +#endif + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WINHTTP_AUTOPROXY_OPTIONS + { + public uint Flags; + public uint AutoDetectFlags; + [MarshalAs(UnmanagedType.LPWStr)] + public string? AutoConfigUrl; + public IntPtr Reserved1; + public uint Reserved2; + [MarshalAs(UnmanagedType.Bool)] + public bool AutoLoginIfChallenged; +#if NET7_0_OR_GREATER + [CustomMarshaller(typeof(WINHTTP_AUTOPROXY_OPTIONS), MarshalMode.Default, typeof(Marshaller))] + public static class Marshaller + { + public static Native ConvertToUnmanaged(WINHTTP_AUTOPROXY_OPTIONS managed) => new(managed); + + public static WINHTTP_AUTOPROXY_OPTIONS ConvertToManaged(Native native) => native.ToManaged(); + + public static void Free(Native native) => native.FreeNative(); + + public readonly struct Native + { + private readonly uint Flags; + private readonly uint AutoDetectFlags; + private readonly IntPtr AutoConfigUrl; + private readonly IntPtr Reserved1; + private readonly uint Reserved2; + private readonly int AutoLoginIfChallenged; + + public Native(WINHTTP_AUTOPROXY_OPTIONS managed) + { + Flags = managed.Flags; + AutoDetectFlags = managed.AutoDetectFlags; + AutoConfigUrl = managed.AutoConfigUrl is not null ? Marshal.StringToCoTaskMemUni(managed.AutoConfigUrl) : IntPtr.Zero; + Reserved1 = managed.Reserved1; + Reserved2 = managed.Reserved2; + AutoLoginIfChallenged = managed.AutoLoginIfChallenged ? 1 : 0; + } + + public WINHTTP_AUTOPROXY_OPTIONS ToManaged() + { + return new WINHTTP_AUTOPROXY_OPTIONS + { + Flags = Flags, + AutoDetectFlags = AutoDetectFlags, + AutoConfigUrl = AutoConfigUrl != IntPtr.Zero ? Marshal.PtrToStringUni(AutoConfigUrl) : null, + Reserved1 = Reserved1, + Reserved2 = Reserved2, + AutoLoginIfChallenged = AutoLoginIfChallenged != 0 + }; + } + + public void FreeNative() + { + Marshal.FreeCoTaskMem(AutoConfigUrl); + } + } + } +#endif + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG + { + public int AutoDetect; + public IntPtr AutoConfigUrl; + public IntPtr Proxy; + public IntPtr ProxyBypass; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WINHTTP_PROXY_INFO + { + public uint AccessType; + public IntPtr Proxy; + public IntPtr ProxyBypass; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WINHTTP_ASYNC_RESULT + { + public IntPtr dwResult; + 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; + public const uint API_WRITE_DATA = 4; + public const uint API_SEND_REQUEST = 5; + + public enum WINHTTP_WEB_SOCKET_OPERATION + { + WINHTTP_WEB_SOCKET_SEND_OPERATION = 0, + WINHTTP_WEB_SOCKET_RECEIVE_OPERATION = 1, + WINHTTP_WEB_SOCKET_CLOSE_OPERATION = 2, + WINHTTP_WEB_SOCKET_SHUTDOWN_OPERATION = 3 + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt index 30ce0c80..1f93710a 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt +++ b/src/Snap.Hutao/Snap.Hutao.Win32/NativeMethods.txt @@ -1,4 +1,15 @@ -// COMCTL32 +// ADVAPI32 +RegCloseKey +RegOpenKeyExW +RegNotifyChangeKeyValue +REG_NOTIFY_FILTER +HKEY_CLASSES_ROOT +HKEY_CURRENT_USER +HKEY_LOCAL_MACHINE +HKEY_USERS +HKEY_CURRENT_CONFIG + +// COMCTL32 DefSubclassProc RemoveWindowSubclass SetWindowSubclass diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs index a54fa034..2b049d3e 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs +++ b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs @@ -1,6 +1,7 @@ using System; using Windows.Win32.Foundation; using Windows.Win32.System.Com; +using Windows.Win32.System.Registry; namespace Windows.Win32; @@ -14,4 +15,12 @@ internal static partial class PInvoke ppv = (TInterface)o; return hr; } + + internal static unsafe WIN32_ERROR RegOpenKeyEx(HKEY hKey, string lpSubKey, uint ulOptions, REG_SAM_FLAGS samDesired, out HKEY phkResult) + { + HKEY hKeyResult; + WIN32_ERROR __result = PInvoke.RegOpenKeyEx(hKey, lpSubKey, ulOptions, samDesired, &hKeyResult); + phkResult = hKeyResult; + return __result; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs index 4e778855..18286d7d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/DependencyInjection.cs @@ -2,9 +2,11 @@ // Licensed under the MIT license. using CommunityToolkit.Mvvm.Messaging; +using Snap.Hutao.Core.IO.Http.DynamicProxy; using Snap.Hutao.Core.Logging; using Snap.Hutao.Service; using System.Globalization; +using System.Net.Http; using System.Runtime.CompilerServices; using Windows.Globalization; @@ -41,6 +43,7 @@ internal static class DependencyInjection serviceProvider.InitializeConsoleWindow(); serviceProvider.InitializeCulture(); + serviceProvider.InitializedDynamicHttpProxy(); return serviceProvider; } @@ -67,4 +70,9 @@ internal static class DependencyInjection { _ = serviceProvider.GetRequiredService(); } + + private static void InitializedDynamicHttpProxy(this IServiceProvider serviceProvider) + { + HttpClient.DefaultProxy = serviceProvider.GetRequiredService(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs new file mode 100644 index 00000000..967180c3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs @@ -0,0 +1,81 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Shell; +using System.Net; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +[Injection(InjectAs.Singleton)] +internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable +{ + private readonly RegistryMonitor registryMonitor; + + private IWebProxy innerProxy; + + public DynamicHttpProxy() + { + HttpWindowsProxy.TryCreate(out IWebProxy? proxy); + innerProxy = proxy ?? new HttpNoProxy(); + + registryMonitor = RegistryMonitor.Create(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections", OnRegistryChanged); + registryMonitor.Start(); + } + + /// + public ICredentials? Credentials + { + get => InnerProxy.Credentials; + set => InnerProxy.Credentials = value; + } + + private IWebProxy InnerProxy + { + get => innerProxy; + set + { + if (ReferenceEquals(innerProxy, value)) + { + return; + } + + if (innerProxy is IDisposable disposable) + { + disposable.Dispose(); + } + + innerProxy = value; + } + } + + public void UpdateProxy() + { + HttpWindowsProxy.TryCreate(out IWebProxy? proxy); + InnerProxy = proxy ?? new HttpNoProxy(); + } + + public Uri? GetProxy(Uri destination) + { + return InnerProxy.GetProxy(destination); + } + + public bool IsBypassed(Uri host) + { + return InnerProxy.IsBypassed(host); + } + + public void Dispose() + { + if (innerProxy is IDisposable disposable) + { + disposable.Dispose(); + } + + registryMonitor.Dispose(); + } + + private void OnRegistryChanged(object? sender, EventArgs e) + { + UpdateProxy(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs new file mode 100644 index 00000000..f2fdead6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs @@ -0,0 +1,138 @@ +#pragma warning disable +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/FailedProxyCache.cs +/// +internal sealed class FailedProxyCache +{ + /// + /// When returned by , indicates a proxy is immediately usable. + /// + public const long Immediate = 0; + + // If a proxy fails, time out 30 minutes. WinHTTP and Firefox both use this. + private const int FailureTimeoutInMilliseconds = 1000 * 60 * 30; + + // Scan through the failures and flush any that have expired every 5 minutes. + private const int FlushFailuresTimerInMilliseconds = 1000 * 60 * 5; + + // _failedProxies will only be flushed (rare but somewhat expensive) if we have more than this number of proxies in our dictionary. See Cleanup() for details. + private const int LargeProxyConfigBoundary = 8; + + // Value is the Environment.TickCount64 to remove the proxy from the failure list. + private readonly ConcurrentDictionary _failedProxies = new ConcurrentDictionary(); + + // When Environment.TickCount64 >= _nextFlushTicks, cause a flush. + private long _nextFlushTicks = Environment.TickCount64 + FlushFailuresTimerInMilliseconds; + + // This lock can be folded into _nextFlushTicks for space optimization, but + // this class should only have a single instance so would rather have clarity. + private SpinLock _flushLock = new SpinLock(enableThreadOwnerTracking: false); // mutable struct; do not make this readonly + + /// + /// Checks when a proxy will become usable. + /// + /// The of the proxy to check. + /// If the proxy can be used, . Otherwise, the next that it should be used. + public long GetProxyRenewTicks(Uri uri) + { + Cleanup(); + + // If not failed, ready immediately. + if (!_failedProxies.TryGetValue(uri, out long renewTicks)) + { + return Immediate; + } + + // If we haven't reached out renew time, the proxy can't be used. + if (Environment.TickCount64 < renewTicks) + { + return renewTicks; + } + + // Renew time reached, we can remove the proxy from the cache. + if (TryRenewProxy(uri, renewTicks)) + { + return Immediate; + } + + // Another thread updated the cache before we could remove it. + // We can't know if this is a removal or an update, so check again. + return _failedProxies.TryGetValue(uri, out renewTicks) ? renewTicks : Immediate; + } + + /// + /// Sets a proxy as failed, to avoid trying it again for some time. + /// + /// The URI of the proxy. + public void SetProxyFailed(Uri uri) + { + _failedProxies[uri] = Environment.TickCount64 + FailureTimeoutInMilliseconds; + Cleanup(); + } + + /// + /// Renews a proxy prior to its period expiring. Used when all proxies are failed to renew the proxy closest to being renewed. + /// + /// The of the proxy to renew. + /// The current renewal time for the proxy. If the value has changed from this, the proxy will not be renewed. + public bool TryRenewProxy(Uri uri, long renewTicks) => + _failedProxies.TryRemove(new KeyValuePair(uri, renewTicks)); + + /// + /// Cleans up any old proxies that should no longer be marked as failing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Cleanup() + { + if (_failedProxies.Count > LargeProxyConfigBoundary && Environment.TickCount64 >= Interlocked.Read(ref _nextFlushTicks)) + { + CleanupHelper(); + } + } + + /// + /// Cleans up any old proxies that should no longer be marked as failing. + /// + /// + /// I expect this to never be called by in a production system. It is only needed in the case + /// that a system has a very large number of proxies that the PAC script cycles through. It is moderately expensive, + /// so it's only run periodically and is disabled until we exceed failed proxies. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void CleanupHelper() + { + bool lockTaken = false; + try + { + _flushLock.TryEnter(ref lockTaken); + if (!lockTaken) + { + return; + } + + long curTicks = Environment.TickCount64; + + foreach (KeyValuePair kvp in _failedProxies) + { + if (curTicks >= kvp.Value) + { + ((ICollection>)_failedProxies).Remove(kvp); + } + } + } + finally + { + if (lockTaken) + { + Interlocked.Exchange(ref _nextFlushTicks, Environment.TickCount64 + FlushFailuresTimerInMilliseconds); + _flushLock.Exit(false); + } + } + } +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs new file mode 100644 index 00000000..62e624c0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs @@ -0,0 +1,15 @@ +#pragma warning disable +using System.Net; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpNoProxy.cs +/// +internal sealed class HttpNoProxy : IWebProxy +{ + public ICredentials? Credentials { get; set; } + public Uri? GetProxy(Uri destination) => null; + public bool IsBypassed(Uri host) => true; +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs new file mode 100644 index 00000000..a01b27a4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs @@ -0,0 +1,325 @@ +#pragma warning disable +using System.Net; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using static Snap.Hutao.Win32.Interop.WinHttp; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpWindowsProxy.cs +/// +internal sealed class HttpWindowsProxy : IMultiWebProxy, IDisposable +{ + private readonly MultiProxy _insecureProxy; // URI of the http system proxy if set + private readonly MultiProxy _secureProxy; // URI of the https system proxy if set + private readonly FailedProxyCache _failedProxies = new FailedProxyCache(); + private readonly List? _bypass; // list of domains not to proxy + private readonly bool _bypassLocal; // we should bypass domain considered local + private readonly List? _localIp; + private ICredentials? _credentials; + private readonly WinInetProxyHelper _proxyHelper; + private SafeWinHttpHandle? _sessionHandle; + private bool _disposed; + + public static bool TryCreate([NotNullWhen(true)] out IWebProxy? proxy) + { + // This will get basic proxy setting from system using existing + // WinInetProxyHelper functions. If no proxy is enabled, it will return null. + SafeWinHttpHandle? sessionHandle = null; + proxy = null; + + WinInetProxyHelper proxyHelper = new WinInetProxyHelper(); + if (!proxyHelper.ManualSettingsOnly && !proxyHelper.AutoSettingsUsed) + { + return false; + } + + if (proxyHelper.AutoSettingsUsed) + { + sessionHandle = WinHttpOpen( + IntPtr.Zero, + WINHTTP_ACCESS_TYPE_NO_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + (int)WINHTTP_FLAG_ASYNC); + + if (sessionHandle.IsInvalid) + { + // Proxy failures are currently ignored by managed handler. + sessionHandle.Dispose(); + return false; + } + } + + proxy = new HttpWindowsProxy(proxyHelper, sessionHandle); + return true; + } + + private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sessionHandle) + { + _proxyHelper = proxyHelper; + _sessionHandle = sessionHandle; + + if (proxyHelper.ManualSettingsUsed) + { + _secureProxy = MultiProxy.Parse(_failedProxies, proxyHelper.Proxy, true); + _insecureProxy = MultiProxy.Parse(_failedProxies, proxyHelper.Proxy, false); + + if (!string.IsNullOrWhiteSpace(proxyHelper.ProxyBypass)) + { + int idx = 0; + string? tmp; + + // Process bypass list for manual setting. + // Initial list size is best guess based on string length assuming each entry is at least 5 characters on average. + _bypass = new List(proxyHelper.ProxyBypass.Length / 5); + + while (idx < proxyHelper.ProxyBypass.Length) + { + // Strip leading spaces and scheme if any. + while (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] == ' ') { idx += 1; }; + if (string.Compare(proxyHelper.ProxyBypass, idx, "http://", 0, 7, StringComparison.OrdinalIgnoreCase) == 0) + { + idx += 7; + } + else if (string.Compare(proxyHelper.ProxyBypass, idx, "https://", 0, 8, StringComparison.OrdinalIgnoreCase) == 0) + { + idx += 8; + } + + if (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] == '[') + { + // Strip [] from IPv6 so we can use IdnHost laster for matching. + idx += 1; + } + + int start = idx; + while (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] != ' ' && proxyHelper.ProxyBypass[idx] != ';' && proxyHelper.ProxyBypass[idx] != ']') { idx += 1; }; + + if (idx == start) + { + // Empty string. + tmp = null; + } + else if (string.Compare(proxyHelper.ProxyBypass, start, "", 0, 7, StringComparison.OrdinalIgnoreCase) == 0) + { + _bypassLocal = true; + tmp = null; + } + else + { + tmp = proxyHelper.ProxyBypass.Substring(start, idx - start); + } + + // Skip trailing characters if any. + if (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] != ';') + { + // Got stopped at space or ']'. Strip until next ';' or end. + while (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] != ';') { idx += 1; }; + } + if (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] == ';') + { + idx++; + } + if (tmp == null) + { + continue; + } + + _bypass.Add(tmp); + } + if (_bypass.Count == 0) + { + // Bypass string only had garbage we did not parse. + _bypass = null; + } + } + + if (_bypassLocal) + { + _localIp = new List(); + foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + IPInterfaceProperties ipProps = netInterface.GetIPProperties(); + foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses) + { + _localIp.Add(addr.Address); + } + } + } + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + if (_sessionHandle != null && !_sessionHandle.IsInvalid) + { + SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle); + } + } + } + + /// + /// Gets the proxy URI. (IWebProxy interface) + /// + public Uri? GetProxy(Uri uri) + { + GetMultiProxy(uri).ReadNext(out Uri? proxyUri, out _); + return proxyUri; + } + + /// + /// Gets the proxy URIs. + /// + public MultiProxy GetMultiProxy(Uri uri) + { + // We need WinHTTP to detect and/or process a PAC (JavaScript) file. This maps to + // "Automatically detect settings" and/or "Use automatic configuration script" from IE + // settings. But, calling into WinHTTP can be slow especially when it has to call into + // the out-of-process service to discover, load, and run the PAC file. So, we skip + // calling into WinHTTP if there was a recent failure to detect a PAC file on the network. + // This is a common error. The default IE settings on a Windows machine consist of the + // single checkbox for "Automatically detect settings" turned on and most networks + // won't actually discover a PAC file on the network since WPAD protocol isn't configured. + if (_proxyHelper.AutoSettingsUsed && !_proxyHelper.RecentAutoDetectionFailure) + { + WINHTTP_PROXY_INFO proxyInfo = default; + try + { + if (_proxyHelper.GetProxyForUrl(_sessionHandle, uri, out proxyInfo)) + { + // If WinHTTP just specified a Proxy with no ProxyBypass list, then + // we can return the Proxy uri directly. + if (proxyInfo.ProxyBypass == IntPtr.Zero) + { + if (proxyInfo.Proxy != IntPtr.Zero) + { + string proxyStr = Marshal.PtrToStringUni(proxyInfo.Proxy)!; + + return MultiProxy.CreateLazy(_failedProxies, proxyStr, IsSecureUri(uri)); + } + else + { + return MultiProxy.Empty; + } + } + + // A bypass list was also specified. This means that WinHTTP has fallen back to + // using the manual IE settings specified and there is a ProxyBypass list also. + // Since we're not really using the full WinHTTP stack, we need to use HttpSystemProxy + // to do the computation of the final proxy uri merging the information from the Proxy + // and ProxyBypass strings. + } + else + { + return MultiProxy.Empty; + } + } + finally + { + Marshal.FreeHGlobal(proxyInfo.Proxy); + Marshal.FreeHGlobal(proxyInfo.ProxyBypass); + } + } + + // Fallback to manual settings if present. + if (_proxyHelper.ManualSettingsUsed) + { + if (_bypassLocal) + { + IPAddress? address; + + if (uri.IsLoopback) + { + // This is optimization for loopback addresses. + // Unfortunately this does not work for all local addresses. + return MultiProxy.Empty; + } + + // Pre-Check if host may be IP address to avoid parsing. + if (uri.HostNameType == UriHostNameType.IPv6 || uri.HostNameType == UriHostNameType.IPv4) + { + // RFC1123 allows labels to start with number. + // Leading number may or may not be IP address. + // IPv6 [::1] notation. '[' is not valid character in names. + if (IPAddress.TryParse(uri.IdnHost, out address)) + { + // Host is valid IP address. + // Check if it belongs to local system. + foreach (IPAddress a in _localIp!) + { + if (a.Equals(address)) + { + return MultiProxy.Empty; + } + } + } + } + if (uri.HostNameType != UriHostNameType.IPv6 && !uri.IdnHost.Contains('.')) + { + // Not address and does not have a dot. + // Hosts without FQDN are considered local. + return MultiProxy.Empty; + } + } + + // Check if we have other rules for bypass. + if (_bypass != null) + { + foreach (string entry in _bypass) + { + // IdnHost does not have []. + if (SimpleRegex.IsMatchWithStarWildcard(uri.IdnHost, entry)) + { + return MultiProxy.Empty; + } + } + } + + // We did not find match on bypass list. + return IsSecureUri(uri) ? _secureProxy : _insecureProxy; + } + + return MultiProxy.Empty; + } + + private static bool IsSecureUri(Uri uri) + { + return uri.Scheme == UriScheme.Https || uri.Scheme == UriScheme.Wss; + } + + /// + /// Checks if URI is subject to proxy or not. + /// + public bool IsBypassed(Uri uri) + { + // This HttpSystemProxy class is only consumed by SocketsHttpHandler and is not exposed outside of + // SocketsHttpHandler. The current pattern for consumption of IWebProxy is to call IsBypassed first. + // If it returns false, then the caller will call GetProxy. For this proxy implementation, computing + // the return value for IsBypassed is as costly as calling GetProxy. We want to avoid doing extra + // work. So, this proxy implementation for the IsBypassed method can always return false. Then the + // GetProxy method will return non-null for a proxy, or null if no proxy should be used. + return false; + } + + public ICredentials? Credentials + { + get + { + return _credentials; + } + set + { + _credentials = value; + } + } + + // Access function for unit tests. + internal List? BypassList => _bypass; +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs new file mode 100644 index 00000000..f906d055 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs @@ -0,0 +1,16 @@ +#pragma warning disable +using System.Net; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/IMultiWebProxy.cs +/// +internal interface IMultiWebProxy : IWebProxy +{ + /// + /// Gets the proxy URIs. + /// + public MultiProxy GetMultiProxy(Uri uri); +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs new file mode 100644 index 00000000..da75d93b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs @@ -0,0 +1,268 @@ +#pragma warning disable +using System.Diagnostics; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/MultiProxy.cs +/// +internal struct MultiProxy +{ + private readonly FailedProxyCache? _failedProxyCache; + private readonly Uri[]? _uris; + private readonly string? _proxyConfig; + private readonly bool _secure; + private int _currentIndex; + private Uri? _currentUri; + + private MultiProxy(FailedProxyCache? failedProxyCache, Uri[] uris) + { + _failedProxyCache = failedProxyCache; + _uris = uris; + _proxyConfig = null; + _secure = default; + _currentIndex = 0; + _currentUri = null; + } + + private MultiProxy(FailedProxyCache failedProxyCache, string proxyConfig, bool secure) + { + _failedProxyCache = failedProxyCache; + _uris = null; + _proxyConfig = proxyConfig; + _secure = secure; + _currentIndex = 0; + _currentUri = null; + } + + public static MultiProxy Empty => new MultiProxy(null, Array.Empty()); + + /// + /// Parses a WinHTTP proxy config into a MultiProxy instance. + /// + /// The cache of failed proxy requests to employ. + /// The WinHTTP proxy config to parse. + /// If true, return proxies suitable for use with a secure connection. If false, return proxies suitable for an insecure connection. + public static MultiProxy Parse(FailedProxyCache failedProxyCache, string? proxyConfig, bool secure) + { + Debug.Assert(failedProxyCache != null); + + Uri[] uris = Array.Empty(); + + ReadOnlySpan span = proxyConfig; + while (TryParseProxyConfigPart(span, secure, out Uri? uri, out int charactersConsumed)) + { + int idx = uris.Length; + + // Assume that we will typically not have more than 1...3 proxies, so just + // grow by 1. This method is currently only used once per process, so the + // case of an abnormally large config will not be much of a concern anyway. + Array.Resize(ref uris, idx + 1); + uris[idx] = uri; + + span = span.Slice(charactersConsumed); + } + + return new MultiProxy(failedProxyCache, uris); + } + + /// + /// Initializes a MultiProxy instance that lazily parses a given WinHTTP configuration string. + /// + /// The cache of failed proxy requests to employ. + /// The WinHTTP proxy config to parse. + /// If true, return proxies suitable for use with a secure connection. If false, return proxies suitable for an insecure connection. + public static MultiProxy CreateLazy(FailedProxyCache failedProxyCache, string proxyConfig, bool secure) + { + Debug.Assert(failedProxyCache != null); + + return string.IsNullOrEmpty(proxyConfig) == false ? + new MultiProxy(failedProxyCache, proxyConfig, secure) : + MultiProxy.Empty; + } + + /// + /// Reads the next proxy URI from the MultiProxy. + /// + /// The next proxy to use for the request. + /// If true, indicates there are no further proxies to read from the config. + /// If there is a proxy available, true. Otherwise, false. + public bool ReadNext([NotNullWhen(true)] out Uri? uri, out bool isFinalProxy) + { + // Enumerating indicates the previous proxy has failed; mark it as such. + if (_currentUri != null) + { + Debug.Assert(_failedProxyCache != null); + _failedProxyCache.SetProxyFailed(_currentUri); + } + + // If no more proxies to read, return out quickly. + if (!ReadNextHelper(out uri, out isFinalProxy)) + { + _currentUri = null; + return false; + } + + // If this is the first ReadNext() and all proxies are marked as failed, return the proxy that is closest to renewal. + Uri? oldestFailedProxyUri = null; + long oldestFailedProxyTicks = long.MaxValue; + + do + { + Debug.Assert(_failedProxyCache != null); + long renewTicks = _failedProxyCache.GetProxyRenewTicks(uri); + + // Proxy hasn't failed recently, return for use. + if (renewTicks == FailedProxyCache.Immediate) + { + _currentUri = uri; + return true; + } + + if (renewTicks < oldestFailedProxyTicks) + { + oldestFailedProxyUri = uri; + oldestFailedProxyTicks = renewTicks; + } + } + while (ReadNextHelper(out uri, out isFinalProxy)); + + // All the proxies in the config have failed; in this case, return the proxy that is closest to renewal. + if (_currentUri == null) + { + uri = oldestFailedProxyUri; + _currentUri = oldestFailedProxyUri; + + if (oldestFailedProxyUri != null) + { + Debug.Assert(uri != null); + _failedProxyCache.TryRenewProxy(uri, oldestFailedProxyTicks); + return true; + } + } + + return false; + } + + /// + /// Reads the next proxy URI from the MultiProxy, either via parsing a config string or from an array. + /// + private bool ReadNextHelper([NotNullWhen(true)] out Uri? uri, out bool isFinalProxy) + { + Debug.Assert(_uris != null || _proxyConfig != null, $"{nameof(ReadNext)} must not be called on a default-initialized {nameof(MultiProxy)}."); + + if (_uris != null) + { + if (_currentIndex == _uris.Length) + { + uri = default; + isFinalProxy = default; + return false; + } + + uri = _uris[_currentIndex++]; + isFinalProxy = _currentIndex == _uris.Length; + return true; + } + + Debug.Assert(_proxyConfig != null); + if (_currentIndex < _proxyConfig.Length) + { + bool hasProxy = TryParseProxyConfigPart(_proxyConfig.AsSpan(_currentIndex), _secure, out uri!, out int charactersConsumed); + + _currentIndex += charactersConsumed; + Debug.Assert(_currentIndex <= _proxyConfig.Length); + + isFinalProxy = _currentIndex == _proxyConfig.Length; + + return hasProxy; + } + + uri = default; + isFinalProxy = default; + return false; + } + + /// + /// This method is used to parse WinINet Proxy strings, a single proxy at a time. + /// + /// + /// The strings are a semicolon or whitespace separated list, with each entry in the following format: + /// ([<scheme>=][<scheme>"://"]<server>[":"<port>]) + /// + private static bool TryParseProxyConfigPart(ReadOnlySpan proxyString, bool secure, [NotNullWhen(true)] out Uri? uri, out int charactersConsumed) + { + const int SECURE_FLAG = 1; + const int INSECURE_FLAG = 2; + const string ProxyDelimiters = "; \n\r\t"; + + int wantedFlag = secure ? SECURE_FLAG : INSECURE_FLAG; + int originalLength = proxyString.Length; + + while (true) + { + // Skip any delimiters. + int iter = 0; + while (iter < proxyString.Length && ProxyDelimiters.Contains(proxyString[iter])) + { + ++iter; + } + + if (iter == proxyString.Length) + { + break; + } + + proxyString = proxyString.Slice(iter); + + // Determine which scheme this part is for. + // If no schema is defined, use both. + int proxyType = SECURE_FLAG | INSECURE_FLAG; + + if (proxyString.StartsWith("http=")) + { + proxyType = INSECURE_FLAG; + proxyString = proxyString.Slice("http=".Length); + } + else if (proxyString.StartsWith("https=")) + { + proxyType = SECURE_FLAG; + proxyString = proxyString.Slice("https=".Length); + } + + if (proxyString.StartsWith("http://")) + { + proxyType = INSECURE_FLAG; + proxyString = proxyString.Slice("http://".Length); + } + else if (proxyString.StartsWith("https://")) + { + proxyType = SECURE_FLAG; + proxyString = proxyString.Slice("https://".Length); + } + + // Find the next delimiter, or end of string. + iter = proxyString.IndexOfAny(ProxyDelimiters); + if (iter < 0) + { + iter = proxyString.Length; + } + + // Return URI if it's a match to what we want. + if ((proxyType & wantedFlag) != 0 && Uri.TryCreate(string.Concat("http://", proxyString.Slice(0, iter)), UriKind.Absolute, out uri)) + { + charactersConsumed = originalLength - proxyString.Length + iter; + Debug.Assert(charactersConsumed > 0); + + return true; + } + + proxyString = proxyString.Slice(iter); + } + + uri = null; + charactersConsumed = originalLength; + return false; + } +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs new file mode 100644 index 00000000..6d992b2d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs @@ -0,0 +1,73 @@ +#pragma warning disable +using System.Diagnostics; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Common/src/System/Text/SimpleRegex.cs +/// +internal static class SimpleRegex +{ + // Based on wildcmp written by Jack Handy - jakkhandy@hotmail.com + // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing + + /// + /// Perform a match between an input string and a pattern in which the only special character + /// is an asterisk, which can map to zero or more of any character in the input. + /// + /// The input to match. + /// The pattern to match against. + /// true if the input matches the pattern; otherwise, false. + public static bool IsMatchWithStarWildcard(ReadOnlySpan input, ReadOnlySpan pattern) + { + int inputPos = 0, inputPosSaved = -1; + int patternPos = 0, patternPosSaved = -1; + + // Loop through each character in the input. + while (inputPos < input.Length) + { + if (patternPos < pattern.Length && pattern[patternPos] == '*') + { + // If we're currently positioned on a wildcard in the pattern, + // move past it and remember where we are to backtrack to. + inputPosSaved = inputPos; + patternPosSaved = ++patternPos; + } + else if (patternPos < pattern.Length && + (pattern[patternPos] == input[inputPos] || + char.ToUpperInvariant(pattern[patternPos]) == char.ToUpperInvariant(input[inputPos]))) + { + // If the characters in the pattern and the input match, advance both. + inputPos++; + patternPos++; + } + else if (patternPosSaved == -1) + { + // If we're not on a wildcard and the current characters don't match and we don't have + // any wildcard to backtrack to, this is not a match. + return false; + } + else + { + // Otherwise, this is not a wildcard, the characters don't match, but we do have a + // wildcard saved, so backtrack to it and use it to consume the next input character. + inputPos = ++inputPosSaved; + patternPos = patternPosSaved; + } + } + + // We've reached the end of the input. Eat all wildcards immediately after where we are + // in the pattern, as if they're at the end, they'll all just map to nothing (and if it + // turns out there's something after them, eating them won't matter). + while (patternPos < pattern.Length && pattern[patternPos] == '*') + { + patternPos++; + } + + // If we are in fact at the end of the pattern, then we successfully matched. + // If there's anything left, it's not a wildcard, so it doesn't match. + Debug.Assert(patternPos <= pattern.Length); + return patternPos == pattern.Length; + } +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs new file mode 100644 index 00000000..124f898a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs @@ -0,0 +1,24 @@ +#pragma warning disable +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Common/src/System/Net/UriScheme.cs +/// +internal static class UriScheme +{ + public const string File = "file"; + public const string Ftp = "ftp"; + public const string Gopher = "gopher"; + public const string Http = "http"; + public const string Https = "https"; + public const string News = "news"; + public const string NetPipe = "net.pipe"; + public const string NetTcp = "net.tcp"; + public const string Nntp = "nntp"; + public const string Mailto = "mailto"; + public const string Ws = "ws"; + public const string Wss = "wss"; + + public const string SchemeDelimiter = "://"; +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs new file mode 100644 index 00000000..41be5ec1 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs @@ -0,0 +1,179 @@ +#pragma warning disable +using System.Runtime.InteropServices; +using static Snap.Hutao.Win32.Interop.WinHttp; + +namespace Snap.Hutao.Core.IO.Http.DynamicProxy; + +/// +/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Common/src/System/Net/Http/WinInetProxyHelper.cs +/// +internal sealed class WinInetProxyHelper +{ + private const int RecentAutoDetectionInterval = 120_000; // 2 minutes in milliseconds. + private readonly string? _autoConfigUrl, _proxy, _proxyBypass; + private readonly bool _autoDetect; + private readonly bool _useProxy; + private bool _autoDetectionFailed; + private int _lastTimeAutoDetectionFailed; // Environment.TickCount units (milliseconds). + + public WinInetProxyHelper() + { + WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig = default; + + try + { + if (WinHttpGetIEProxyConfigForCurrentUser(out proxyConfig)) + { + _autoConfigUrl = Marshal.PtrToStringUni(proxyConfig.AutoConfigUrl)!; + _autoDetect = proxyConfig.AutoDetect != 0; + _proxy = Marshal.PtrToStringUni(proxyConfig.Proxy)!; + _proxyBypass = Marshal.PtrToStringUni(proxyConfig.ProxyBypass)!; + + _useProxy = true; + } + else + { + // We match behavior of WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY and ignore errors. + int lastError = Marshal.GetLastWin32Error(); + } + } + finally + { + // FreeHGlobal already checks for null pointer before freeing the memory. + Marshal.FreeHGlobal(proxyConfig.AutoConfigUrl); + Marshal.FreeHGlobal(proxyConfig.Proxy); + Marshal.FreeHGlobal(proxyConfig.ProxyBypass); + } + } + + public string? AutoConfigUrl => _autoConfigUrl; + + public bool AutoDetect => _autoDetect; + + public bool AutoSettingsUsed => AutoDetect || !string.IsNullOrEmpty(AutoConfigUrl); + + public bool ManualSettingsUsed => !string.IsNullOrEmpty(Proxy); + + public bool ManualSettingsOnly => !AutoSettingsUsed && ManualSettingsUsed; + + public string? Proxy => _proxy; + + public string? ProxyBypass => _proxyBypass; + + public bool RecentAutoDetectionFailure => + _autoDetectionFailed && + Environment.TickCount - _lastTimeAutoDetectionFailed <= RecentAutoDetectionInterval; + + public bool GetProxyForUrl( + SafeWinHttpHandle? sessionHandle, + Uri uri, + out WINHTTP_PROXY_INFO proxyInfo) + { + proxyInfo.AccessType = WINHTTP_ACCESS_TYPE_NO_PROXY; + proxyInfo.Proxy = IntPtr.Zero; + proxyInfo.ProxyBypass = IntPtr.Zero; + + if (!_useProxy) + { + return false; + } + + bool useProxy = false; + + WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions; + autoProxyOptions.AutoConfigUrl = AutoConfigUrl; + autoProxyOptions.AutoDetectFlags = AutoDetect ? + (WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A) : 0; + autoProxyOptions.AutoLoginIfChallenged = false; + autoProxyOptions.Flags = + (AutoDetect ? WINHTTP_AUTOPROXY_AUTO_DETECT : 0) | + (!string.IsNullOrEmpty(AutoConfigUrl) ? WINHTTP_AUTOPROXY_CONFIG_URL : 0); + autoProxyOptions.Reserved1 = IntPtr.Zero; + autoProxyOptions.Reserved2 = 0; + + // AutoProxy Cache. + // https://docs.microsoft.com/en-us/windows/desktop/WinHttp/autoproxy-cache + // If the out-of-process service is active when WinHttpGetProxyForUrl is called, the cached autoproxy + // URL and script are available to the whole computer. However, if the out-of-process service is used, + // and the fAutoLogonIfChallenged flag in the pAutoProxyOptions structure is true, then the autoproxy + // URL and script are not cached. Therefore, calling WinHttpGetProxyForUrl with the fAutoLogonIfChallenged + // member set to TRUE results in additional overhead operations that may affect performance. + // The following steps can be used to improve performance: + // 1. Call WinHttpGetProxyForUrl with the fAutoLogonIfChallenged parameter set to false. The autoproxy + // URL and script are cached for future calls to WinHttpGetProxyForUrl. + // 2. If Step 1 fails, with ERROR_WINHTTP_LOGIN_FAILURE, then call WinHttpGetProxyForUrl with the + // fAutoLogonIfChallenged member set to TRUE. + // + // We match behavior of WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY and ignore errors. +#pragma warning disable CA1845 // file is shared with a build that lacks string.Concat for spans + // Underlying code does not understand WebSockets so we need to convert it to http or https. + string destination = uri.AbsoluteUri; + if (uri.Scheme == UriScheme.Wss) + { + destination = UriScheme.Https + destination.Substring(UriScheme.Wss.Length); + } + else if (uri.Scheme == UriScheme.Ws) + { + destination = UriScheme.Http + destination.Substring(UriScheme.Ws.Length); + } +#pragma warning restore CA1845 + + var repeat = false; + do + { + _autoDetectionFailed = false; + if (WinHttpGetProxyForUrl( + sessionHandle!, + destination, + ref autoProxyOptions, + out proxyInfo)) + { + useProxy = true; + + break; + } + else + { + var lastError = Marshal.GetLastWin32Error(); + + if (lastError == ERROR_WINHTTP_LOGIN_FAILURE) + { + if (repeat) + { + // We don't retry more than once. + break; + } + else + { + repeat = true; + autoProxyOptions.AutoLoginIfChallenged = true; + } + } + else + { + if (lastError == ERROR_WINHTTP_AUTODETECTION_FAILED) + { + _autoDetectionFailed = true; + _lastTimeAutoDetectionFailed = Environment.TickCount; + } + + break; + } + } + } while (repeat); + + // Fall back to manual settings if available. + if (!useProxy && !string.IsNullOrEmpty(Proxy)) + { + proxyInfo.AccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; + proxyInfo.Proxy = Marshal.StringToHGlobalUni(Proxy); + proxyInfo.ProxyBypass = string.IsNullOrEmpty(ProxyBypass) ? + IntPtr.Zero : Marshal.StringToHGlobalUni(ProxyBypass); + + useProxy = true; + } + + return useProxy; + } +} +#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs b/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs new file mode 100644 index 00000000..3ec03751 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs @@ -0,0 +1,146 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Registry; + +namespace Snap.Hutao.Core.Shell; + +internal sealed partial class RegistryMonitor : IDisposable +{ + private readonly ManualResetEvent eventTerminate = new(false); + private readonly CancellationTokenSource cancellationTokenSource = new(); + + private readonly REG_SAM_FLAGS samFlags = REG_SAM_FLAGS.KEY_QUERY_VALUE | REG_SAM_FLAGS.KEY_NOTIFY | REG_SAM_FLAGS.KEY_READ; + private readonly REG_NOTIFY_FILTER notiftFilters = REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_ATTRIBUTES | + REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_SECURITY; + + private HKEY hKey; + private string subKey = default!; + private bool disposed; + + public RegistryMonitor(string name, EventHandler eventHandler) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + InitRegistryKey(name); + RegChanged += eventHandler; + } + + public event EventHandler? RegChanged; + + public static RegistryMonitor Create(string name, EventHandler eventHandler) + { + return new RegistryMonitor(name, eventHandler); + } + + public void Start() + { + ObjectDisposedException.ThrowIf(disposed, this); + + eventTerminate.Reset(); + MonitorCoreAsync(cancellationTokenSource.Token).SafeForget(); + } + + public void Dispose() + { + eventTerminate.Dispose(); + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + disposed = true; + GC.SuppressFinalize(this); + } + + private void InitRegistryKey(string name) + { + string[] nameParts = name.Split('\\'); + + switch (nameParts[0]) + { + case "HKEY_CLASSES_ROOT": + case "HKCR": + hKey = HKEY.HKEY_CLASSES_ROOT; + break; + + case "HKEY_CURRENT_USER": + case "HKCU": + hKey = HKEY.HKEY_CURRENT_USER; + break; + + case "HKEY_LOCAL_MACHINE": + case "HKLM": + hKey = HKEY.HKEY_LOCAL_MACHINE; + break; + + case "HKEY_USERS": + hKey = HKEY.HKEY_USERS; + break; + + case "HKEY_CURRENT_CONFIG": + hKey = HKEY.HKEY_CURRENT_CONFIG; + break; + + default: + hKey = HKEY.Null; + throw new ArgumentException("The registry hive '" + nameParts[0] + "' is not supported", nameof(name)); + } + + subKey = string.Join("\\", nameParts, 1, nameParts.Length - 1); + } + + private async ValueTask MonitorCoreAsync(CancellationToken token) + { + using (PeriodicTimer timer = new(TimeSpan.FromSeconds(1))) + { + try + { + while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) + { + if (token.IsCancellationRequested) + { + break; + } + + WIN32_ERROR result = PInvoke.RegOpenKeyEx(hKey, subKey, 0, samFlags, out HKEY registryKey); + if (result != WIN32_ERROR.ERROR_SUCCESS) + { + throw new Win32Exception((int)result); + } + + AutoResetEvent eventNotify = new(false); + + try + { + WaitHandle[] waitHandles = [eventNotify, eventTerminate]; + while (!eventTerminate.WaitOne(0, true)) + { + result = PInvoke.RegNotifyChangeKeyValue(registryKey, true, notiftFilters, (HANDLE)eventNotify.SafeWaitHandle.DangerousGetHandle(), true); + if (result != 0) + { + throw new Win32Exception((int)result); + } + + if (WaitHandle.WaitAny(waitHandles) == 0) + { + RegChanged?.Invoke(this, EventArgs.Empty); + } + } + } + finally + { + if (registryKey != IntPtr.Zero) + { + PInvoke.RegCloseKey(registryKey); + } + + eventNotify.Dispose(); + } + } + } + catch (OperationCanceledException) + { + } + } + } +} From 8710150897183a406829716bda3e90a4c72307ae Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 8 Jan 2024 20:19:05 +0800 Subject: [PATCH 2/4] use reflect to reduce code size --- .../Snap.Hutao.Win32/Interop.Libraries.cs | 54 -- .../Snap.Hutao.Win32/Interop.WinHttp.cs | 624 ------------------ .../IO/Http/DynamicProxy/DynamicHttpProxy.cs | 24 +- .../IO/Http/DynamicProxy/FailedProxyCache.cs | 138 ---- .../Core/IO/Http/DynamicProxy/HttpNoProxy.cs | 15 - .../IO/Http/DynamicProxy/HttpWindowsProxy.cs | 325 --------- .../IO/Http/DynamicProxy/IMultiWebProxy.cs | 16 - .../Core/IO/Http/DynamicProxy/MultiProxy.cs | 268 -------- .../Core/IO/Http/DynamicProxy/SimpleRegex.cs | 73 -- .../Core/IO/Http/DynamicProxy/UriScheme.cs | 24 - .../Http/DynamicProxy/WinInetProxyHelper.cs | 179 ----- ...{RegistryMonitor.cs => RegistryWatcher.cs} | 8 +- 12 files changed, 19 insertions(+), 1729 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs rename src/Snap.Hutao/Snap.Hutao/Core/Shell/{RegistryMonitor.cs => RegistryWatcher.cs} (94%) diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs deleted file mode 100644 index 32c7a1f4..00000000 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Interop.Libraries.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Snap.Hutao.Win32; - -internal static partial class Interop -{ - internal static partial class Libraries - { - internal const string Activeds = "activeds.dll"; - internal const string Advapi32 = "advapi32.dll"; - internal const string Authz = "authz.dll"; - internal const string BCrypt = "BCrypt.dll"; - internal const string Credui = "credui.dll"; - internal const string Crypt32 = "crypt32.dll"; - internal const string CryptUI = "cryptui.dll"; - internal const string Dnsapi = "dnsapi.dll"; - internal const string Dsrole = "dsrole.dll"; - internal const string Gdi32 = "gdi32.dll"; - internal const string HttpApi = "httpapi.dll"; - internal const string IpHlpApi = "iphlpapi.dll"; - internal const string Kernel32 = "kernel32.dll"; - internal const string Logoncli = "logoncli.dll"; - internal const string Mswsock = "mswsock.dll"; - internal const string NCrypt = "ncrypt.dll"; - internal const string Netapi32 = "netapi32.dll"; - internal const string Netutils = "netutils.dll"; - internal const string NtDll = "ntdll.dll"; - internal const string Odbc32 = "odbc32.dll"; - internal const string Ole32 = "ole32.dll"; - internal const string OleAut32 = "oleaut32.dll"; - internal const string Pdh = "pdh.dll"; - internal const string Secur32 = "secur32.dll"; - internal const string Shell32 = "shell32.dll"; - internal const string SspiCli = "sspicli.dll"; - internal const string User32 = "user32.dll"; - internal const string Version = "version.dll"; - internal const string WebSocket = "websocket.dll"; - internal const string Wevtapi = "wevtapi.dll"; - internal const string WinHttp = "winhttp.dll"; - internal const string WinMM = "winmm.dll"; - internal const string Wkscli = "wkscli.dll"; - internal const string Wldap32 = "wldap32.dll"; - internal const string Ws2_32 = "ws2_32.dll"; - internal const string Wtsapi32 = "wtsapi32.dll"; - internal const string CompressionNative = "System.IO.Compression.Native"; - internal const string GlobalizationNative = "System.Globalization.Native"; - internal const string MsQuic = "msquic.dll"; - internal const string HostPolicy = "hostpolicy"; - internal const string Ucrtbase = "ucrtbase.dll"; - internal const string Xolehlp = "xolehlp.dll"; - internal const string Comdlg32 = "comdlg32.dll"; - internal const string Gdiplus = "gdiplus.dll"; - internal const string Oleaut32 = "oleaut32.dll"; - internal const string Winspool = "winspool.drv"; - } -} diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs b/src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs deleted file mode 100644 index 84d74504..00000000 --- a/src/Snap.Hutao/Snap.Hutao.Win32/Interop.WinHttp.cs +++ /dev/null @@ -1,624 +0,0 @@ -using Microsoft.Win32.SafeHandles; -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.Marshalling; -using System.Text; - -namespace Snap.Hutao.Win32; - -internal static partial class Interop -{ - internal static partial class WinHttp - { - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - public static partial SafeWinHttpHandle WinHttpOpen( - IntPtr userAgent, - uint accessType, - string? proxyName, - string? proxyBypass, int flags); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpCloseHandle( - IntPtr handle); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - public static partial SafeWinHttpHandle WinHttpConnect( - SafeWinHttpHandle sessionHandle, - string serverName, - ushort serverPort, - uint reserved); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - public static partial SafeWinHttpHandle WinHttpOpenRequest( - SafeWinHttpHandle connectHandle, - string verb, - string objectName, - string? version, - string referrer, - string acceptTypes, - uint flags); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpAddRequestHeaders( - SafeWinHttpHandle requestHandle, -#if NET7_0_OR_GREATER - [MarshalUsing(typeof(SimpleStringBufferMarshaller))] StringBuilder headers, -#else -#pragma warning disable CA1838 // Uses pooled StringBuilder - [In] StringBuilder headers, -#pragma warning restore CA1838 // Uses pooled StringBuilder -#endif - uint headersLength, - uint modifiers); - -#if NET7_0_OR_GREATER - [CustomMarshaller(typeof(StringBuilder), MarshalMode.ManagedToUnmanagedIn, typeof(SimpleStringBufferMarshaller))] - private static unsafe class SimpleStringBufferMarshaller - { - public static void* ConvertToUnmanaged(StringBuilder builder) - { - int length = builder.Length + 1; - void* value = NativeMemory.Alloc(sizeof(char) * (nuint)length); - Span buffer = new(value, length); - buffer.Clear(); - builder.CopyTo(0, buffer, length - 1); - return value; - } - - public static void Free(void* value) => NativeMemory.Free(value); - } -#endif - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpAddRequestHeaders( - SafeWinHttpHandle requestHandle, - string headers, - uint headersLength, - uint modifiers); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpSendRequest( - SafeWinHttpHandle requestHandle, - IntPtr headers, - uint headersLength, - IntPtr optional, - uint optionalLength, - uint totalLength, - IntPtr context); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpReceiveResponse( - SafeWinHttpHandle requestHandle, - IntPtr reserved); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryDataAvailable( - SafeWinHttpHandle requestHandle, - IntPtr parameterIgnoredAndShouldBeNullForAsync); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpReadData( - SafeWinHttpHandle requestHandle, - IntPtr buffer, - uint bufferSize, - IntPtr parameterIgnoredAndShouldBeNullForAsync); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryHeaders( - SafeWinHttpHandle requestHandle, - uint infoLevel, - string name, - IntPtr buffer, - ref uint bufferLength, - ref uint index); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryHeaders( - SafeWinHttpHandle requestHandle, - uint infoLevel, - string name, - ref uint number, - ref uint bufferLength, - IntPtr index); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryOption( - SafeWinHttpHandle handle, - uint option, - ref IntPtr buffer, - ref uint bufferSize); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryOption( - SafeWinHttpHandle handle, - uint option, - IntPtr buffer, - ref uint bufferSize); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryOption( - SafeWinHttpHandle handle, - uint option, - ref uint buffer, - ref uint bufferSize); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpWriteData( - SafeWinHttpHandle requestHandle, - IntPtr buffer, - uint bufferSize, - IntPtr parameterIgnoredAndShouldBeNullForAsync); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpSetOption( - SafeWinHttpHandle handle, - uint option, - ref uint optionData, - uint optionLength = sizeof(uint)); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpSetOption( - SafeWinHttpHandle handle, - uint option, - IntPtr optionData, - uint optionLength); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpSetCredentials( - SafeWinHttpHandle requestHandle, - uint authTargets, - uint authScheme, - string? userName, - string? password, - IntPtr reserved); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpQueryAuthSchemes( - SafeWinHttpHandle requestHandle, - out uint supportedSchemes, - out uint firstScheme, - out uint authTarget); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpSetTimeouts( - SafeWinHttpHandle handle, - int resolveTimeout, - int connectTimeout, - int sendTimeout, - int receiveTimeout); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpGetIEProxyConfigForCurrentUser( - out WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - [return: MarshalAs(UnmanagedType.Bool)] - public static partial bool WinHttpGetProxyForUrl( - SafeWinHttpHandle? sessionHandle, - string url, - ref WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions, - out WINHTTP_PROXY_INFO proxyInfo); - - [LibraryImport(Libraries.WinHttp, SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - public static partial IntPtr WinHttpSetStatusCallback( - SafeWinHttpHandle handle, - WINHTTP_STATUS_CALLBACK callback, - uint notificationFlags, - IntPtr reserved); - } - internal static partial class WinHttp - { - internal class SafeWinHttpHandle : SafeHandleZeroOrMinusOneIsInvalid - { - private SafeWinHttpHandle? _parentHandle; - - public SafeWinHttpHandle() : base(true) - { - } - - public static void DisposeAndClearHandle(ref SafeWinHttpHandle? safeHandle) - { - if (safeHandle != null) - { - safeHandle.Dispose(); - safeHandle = null; - } - } - - public void SetParentHandle(SafeWinHttpHandle parentHandle) - { - Debug.Assert(_parentHandle == null); - Debug.Assert(parentHandle != null); - Debug.Assert(!parentHandle.IsInvalid); - - bool ignore = false; - parentHandle.DangerousAddRef(ref ignore); - - _parentHandle = parentHandle; - } - - // Important: WinHttp API calls should not happen while another WinHttp call for the same handle did not - // return. During finalization that was not initiated by the Dispose pattern we don't expect any other WinHttp - // calls in progress. - protected override bool ReleaseHandle() - { - if (_parentHandle != null) - { - _parentHandle.DangerousRelease(); - _parentHandle = null; - } - - return Interop.WinHttp.WinHttpCloseHandle(handle); - } - } - } - internal static partial class WinHttp - { - public const uint ERROR_SUCCESS = 0; - public const uint ERROR_FILE_NOT_FOUND = 2; - public const uint ERROR_INVALID_HANDLE = 6; - public const uint ERROR_INVALID_PARAMETER = 87; - public const uint ERROR_INSUFFICIENT_BUFFER = 122; - public const uint ERROR_NOT_FOUND = 1168; - public const uint ERROR_WINHTTP_INVALID_OPTION = 12009; - public const uint ERROR_WINHTTP_LOGIN_FAILURE = 12015; - public const uint ERROR_WINHTTP_OPERATION_CANCELLED = 12017; - public const uint ERROR_WINHTTP_INCORRECT_HANDLE_STATE = 12019; - public const uint ERROR_WINHTTP_CONNECTION_ERROR = 12030; - public const uint ERROR_WINHTTP_RESEND_REQUEST = 12032; - public const uint ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED = 12044; - public const uint ERROR_WINHTTP_HEADER_NOT_FOUND = 12150; - public const uint ERROR_WINHTTP_SECURE_FAILURE = 12175; - public const uint ERROR_WINHTTP_AUTODETECTION_FAILED = 12180; - - public const uint WINHTTP_OPTION_PROXY = 38; - public const uint WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0; - public const uint WINHTTP_ACCESS_TYPE_NO_PROXY = 1; - public const uint WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3; - public const uint WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4; - - public const uint WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001; - public const uint WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002; - public const uint WINHTTP_AUTOPROXY_HOST_KEEPCASE = 0x00000004; - public const uint WINHTTP_AUTOPROXY_HOST_LOWERCASE = 0x00000008; - public const uint WINHTTP_AUTOPROXY_RUN_INPROCESS = 0x00010000; - public const uint WINHTTP_AUTOPROXY_RUN_OUTPROCESS_ONLY = 0x00020000; - public const uint WINHTTP_AUTOPROXY_NO_DIRECTACCESS = 0x00040000; - public const uint WINHTTP_AUTOPROXY_NO_CACHE_CLIENT = 0x00080000; - public const uint WINHTTP_AUTOPROXY_NO_CACHE_SVC = 0x00100000; - public const uint WINHTTP_AUTOPROXY_SORT_RESULTS = 0x00400000; - - public const uint WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001; - public const uint WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002; - - public const string WINHTTP_NO_PROXY_NAME = null; - public const string WINHTTP_NO_PROXY_BYPASS = null; - - public const uint WINHTTP_ADDREQ_FLAG_ADD = 0x20000000; - public const uint WINHTTP_ADDREQ_FLAG_REPLACE = 0x80000000; - - public const string WINHTTP_NO_REFERER = null; - public const string WINHTTP_DEFAULT_ACCEPT_TYPES = null; - - public const ushort INTERNET_DEFAULT_PORT = 0; - public const ushort INTERNET_DEFAULT_HTTP_PORT = 80; - public const ushort INTERNET_DEFAULT_HTTPS_PORT = 443; - - public const uint WINHTTP_FLAG_SECURE = 0x00800000; - public const uint WINHTTP_FLAG_ESCAPE_DISABLE = 0x00000040; - public const uint WINHTTP_FLAG_AUTOMATIC_CHUNKING = 0x00000200; - - public const uint WINHTTP_QUERY_FLAG_NUMBER = 0x20000000; - public const uint WINHTTP_QUERY_VERSION = 18; - public const uint WINHTTP_QUERY_STATUS_CODE = 19; - public const uint WINHTTP_QUERY_STATUS_TEXT = 20; - public const uint WINHTTP_QUERY_RAW_HEADERS = 21; - public const uint WINHTTP_QUERY_RAW_HEADERS_CRLF = 22; - public const uint WINHTTP_QUERY_FLAG_TRAILERS = 0x02000000; - public const uint WINHTTP_QUERY_CONTENT_ENCODING = 29; - public const uint WINHTTP_QUERY_SET_COOKIE = 43; - public const uint WINHTTP_QUERY_CUSTOM = 65535; - public const string WINHTTP_HEADER_NAME_BY_INDEX = null; - public const byte[] WINHTTP_NO_OUTPUT_BUFFER = null; - - public const uint WINHTTP_OPTION_DECOMPRESSION = 118; - public const uint WINHTTP_DECOMPRESSION_FLAG_GZIP = 0x00000001; - public const uint WINHTTP_DECOMPRESSION_FLAG_DEFLATE = 0x00000002; - public const uint WINHTTP_DECOMPRESSION_FLAG_ALL = WINHTTP_DECOMPRESSION_FLAG_GZIP | WINHTTP_DECOMPRESSION_FLAG_DEFLATE; - - public const uint WINHTTP_OPTION_REDIRECT_POLICY = 88; - public const uint WINHTTP_OPTION_REDIRECT_POLICY_NEVER = 0; - public const uint WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP = 1; - public const uint WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS = 2; - public const uint WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS = 89; - - public const uint WINHTTP_OPTION_MAX_CONNS_PER_SERVER = 73; - public const uint WINHTTP_OPTION_MAX_CONNS_PER_1_0_SERVER = 74; - - public const uint WINHTTP_OPTION_DISABLE_FEATURE = 63; - public const uint WINHTTP_DISABLE_COOKIES = 0x00000001; - public const uint WINHTTP_DISABLE_REDIRECTS = 0x00000002; - public const uint WINHTTP_DISABLE_AUTHENTICATION = 0x00000004; - public const uint WINHTTP_DISABLE_KEEP_ALIVE = 0x00000008; - - public const uint WINHTTP_OPTION_ENABLE_FEATURE = 79; - public const uint WINHTTP_ENABLE_SSL_REVOCATION = 0x00000001; - - public const uint WINHTTP_OPTION_CLIENT_CERT_CONTEXT = 47; - public const uint WINHTTP_OPTION_CLIENT_CERT_ISSUER_LIST = 94; - public const uint WINHTTP_OPTION_SERVER_CERT_CONTEXT = 78; - public const uint WINHTTP_OPTION_SECURITY_FLAGS = 31; - public const uint WINHTTP_OPTION_SECURE_PROTOCOLS = 84; - public const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 = 0x00000008; - public const uint WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 = 0x00000020; - public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 = 0x00000080; - public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 = 0x00000200; - public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 = 0x00000800; - public const uint WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3 = 0x00002000; - - public const uint SECURITY_FLAG_IGNORE_UNKNOWN_CA = 0x00000100; - public const uint SECURITY_FLAG_IGNORE_CERT_DATE_INVALID = 0x00002000; - public const uint SECURITY_FLAG_IGNORE_CERT_CN_INVALID = 0x00001000; - public const uint SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE = 0x00000200; - - public const uint WINHTTP_OPTION_AUTOLOGON_POLICY = 77; - public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM = 0; // default creds only sent to intranet servers (default) - public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW = 1; // default creds set to all servers - public const uint WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH = 2; // default creds never sent - - public const uint WINHTTP_AUTH_SCHEME_BASIC = 0x00000001; - public const uint WINHTTP_AUTH_SCHEME_NTLM = 0x00000002; - public const uint WINHTTP_AUTH_SCHEME_PASSPORT = 0x00000004; - public const uint WINHTTP_AUTH_SCHEME_DIGEST = 0x00000008; - public const uint WINHTTP_AUTH_SCHEME_NEGOTIATE = 0x00000010; - - public const uint WINHTTP_AUTH_TARGET_SERVER = 0x00000000; - public const uint WINHTTP_AUTH_TARGET_PROXY = 0x00000001; - - public const uint WINHTTP_OPTION_USERNAME = 0x1000; - // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Suppression approved. It is property descriptor, not secret value.")] - public const uint WINHTTP_OPTION_PASSWORD = 0x1001; - public const uint WINHTTP_OPTION_PROXY_USERNAME = 0x1002; - // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Suppression approved. It is property descriptor, not secret value.")] - public const uint WINHTTP_OPTION_PROXY_PASSWORD = 0x1003; - - public const uint WINHTTP_OPTION_SERVER_SPN_USED = 106; - public const uint WINHTTP_OPTION_SERVER_CBT = 108; - - public const uint WINHTTP_OPTION_CONNECT_TIMEOUT = 3; - public const uint WINHTTP_OPTION_SEND_TIMEOUT = 5; - public const uint WINHTTP_OPTION_RECEIVE_TIMEOUT = 6; - - public const uint WINHTTP_OPTION_URL = 34; - - public const uint WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE = 91; - public const uint WINHTTP_OPTION_MAX_RESPONSE_DRAIN_SIZE = 92; - public const uint WINHTTP_OPTION_CONNECTION_INFO = 93; - - public const uint WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS = 111; - - public const uint WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT = 161; - public const uint WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL = 133; - public const uint WINHTTP_OPTION_HTTP_PROTOCOL_USED = 134; - public const uint WINHTTP_PROTOCOL_FLAG_HTTP2 = 0x1; - public const uint WINHTTP_HTTP2_PLUS_CLIENT_CERT_FLAG = 0x1; - public const uint WINHTTP_OPTION_DISABLE_STREAM_QUEUE = 139; - - public const uint WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114; - public const uint WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT = 115; - public const uint WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL = 116; - - 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 const uint WINHTTP_OPTION_STREAM_ERROR_CODE = 159; - public const uint WINHTTP_OPTION_REQUIRE_STREAM_END = 160; - - public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE - { - WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE = 0, - WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE = 1, - WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE = 2, - WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE = 3, - WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE = 4 - } - - public const uint WINHTTP_OPTION_CONTEXT_VALUE = 45; - - public const uint WINHTTP_FLAG_ASYNC = 0x10000000; - - public const uint WINHTTP_CALLBACK_STATUS_RESOLVING_NAME = 0x00000001; - public const uint WINHTTP_CALLBACK_STATUS_NAME_RESOLVED = 0x00000002; - public const uint WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER = 0x00000004; - public const uint WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER = 0x00000008; - public const uint WINHTTP_CALLBACK_STATUS_SENDING_REQUEST = 0x00000010; - public const uint WINHTTP_CALLBACK_STATUS_REQUEST_SENT = 0x00000020; - public const uint WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE = 0x00000040; - public const uint WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED = 0x00000080; - public const uint WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION = 0x00000100; - public const uint WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED = 0x00000200; - public const uint WINHTTP_CALLBACK_STATUS_HANDLE_CREATED = 0x00000400; - public const uint WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING = 0x00000800; - public const uint WINHTTP_CALLBACK_STATUS_DETECTING_PROXY = 0x00001000; - public const uint WINHTTP_CALLBACK_STATUS_REDIRECT = 0x00004000; - public const uint WINHTTP_CALLBACK_STATUS_INTERMEDIATE_RESPONSE = 0x00008000; - public const uint WINHTTP_CALLBACK_STATUS_SECURE_FAILURE = 0x00010000; - public const uint WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE = 0x00020000; - public const uint WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE = 0x00040000; - public const uint WINHTTP_CALLBACK_STATUS_READ_COMPLETE = 0x00080000; - public const uint WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE = 0x00100000; - public const uint WINHTTP_CALLBACK_STATUS_REQUEST_ERROR = 0x00200000; - public const uint WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE = 0x00400000; - public const uint WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE = 0x01000000; - public const uint WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE = 0x02000000; - public const uint WINHTTP_CALLBACK_STATUS_SHUTDOWN_COMPLETE = 0x04000000; - - public const uint WINHTTP_CALLBACK_FLAG_SEND_REQUEST = - WINHTTP_CALLBACK_STATUS_SENDING_REQUEST | - WINHTTP_CALLBACK_STATUS_REQUEST_SENT; - public const uint WINHTTP_CALLBACK_FLAG_HANDLES = - WINHTTP_CALLBACK_STATUS_HANDLE_CREATED | - WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING; - public const uint WINHTTP_CALLBACK_FLAG_REDIRECT = WINHTTP_CALLBACK_STATUS_REDIRECT; - public const uint WINHTTP_CALLBACK_FLAG_SECURE_FAILURE = WINHTTP_CALLBACK_STATUS_SECURE_FAILURE; - public const uint WINHTTP_CALLBACK_FLAG_SENDREQUEST_COMPLETE = WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE; - public const uint WINHTTP_CALLBACK_FLAG_HEADERS_AVAILABLE = WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE; - public const uint WINHTTP_CALLBACK_FLAG_DATA_AVAILABLE = WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE; - public const uint WINHTTP_CALLBACK_FLAG_READ_COMPLETE = WINHTTP_CALLBACK_STATUS_READ_COMPLETE; - public const uint WINHTTP_CALLBACK_FLAG_WRITE_COMPLETE = WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE; - public const uint WINHTTP_CALLBACK_FLAG_REQUEST_ERROR = WINHTTP_CALLBACK_STATUS_REQUEST_ERROR; - public const uint WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE = WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE; - public const uint WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS = - WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE | - WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE | - WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE | - WINHTTP_CALLBACK_STATUS_READ_COMPLETE | - WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE | - WINHTTP_CALLBACK_STATUS_REQUEST_ERROR | - WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE; - public const uint WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS = 0xFFFFFFFF; - - public const uint WININET_E_CONNECTION_RESET = 0x80072EFF; - - public const int WINHTTP_INVALID_STATUS_CALLBACK = -1; - public delegate void WINHTTP_STATUS_CALLBACK( - IntPtr handle, - IntPtr context, - uint internetStatus, - IntPtr statusInformation, - uint statusInformationLength); - -#if NET7_0_OR_GREATER - [NativeMarshalling(typeof(Marshaller))] -#endif - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WINHTTP_AUTOPROXY_OPTIONS - { - public uint Flags; - public uint AutoDetectFlags; - [MarshalAs(UnmanagedType.LPWStr)] - public string? AutoConfigUrl; - public IntPtr Reserved1; - public uint Reserved2; - [MarshalAs(UnmanagedType.Bool)] - public bool AutoLoginIfChallenged; -#if NET7_0_OR_GREATER - [CustomMarshaller(typeof(WINHTTP_AUTOPROXY_OPTIONS), MarshalMode.Default, typeof(Marshaller))] - public static class Marshaller - { - public static Native ConvertToUnmanaged(WINHTTP_AUTOPROXY_OPTIONS managed) => new(managed); - - public static WINHTTP_AUTOPROXY_OPTIONS ConvertToManaged(Native native) => native.ToManaged(); - - public static void Free(Native native) => native.FreeNative(); - - public readonly struct Native - { - private readonly uint Flags; - private readonly uint AutoDetectFlags; - private readonly IntPtr AutoConfigUrl; - private readonly IntPtr Reserved1; - private readonly uint Reserved2; - private readonly int AutoLoginIfChallenged; - - public Native(WINHTTP_AUTOPROXY_OPTIONS managed) - { - Flags = managed.Flags; - AutoDetectFlags = managed.AutoDetectFlags; - AutoConfigUrl = managed.AutoConfigUrl is not null ? Marshal.StringToCoTaskMemUni(managed.AutoConfigUrl) : IntPtr.Zero; - Reserved1 = managed.Reserved1; - Reserved2 = managed.Reserved2; - AutoLoginIfChallenged = managed.AutoLoginIfChallenged ? 1 : 0; - } - - public WINHTTP_AUTOPROXY_OPTIONS ToManaged() - { - return new WINHTTP_AUTOPROXY_OPTIONS - { - Flags = Flags, - AutoDetectFlags = AutoDetectFlags, - AutoConfigUrl = AutoConfigUrl != IntPtr.Zero ? Marshal.PtrToStringUni(AutoConfigUrl) : null, - Reserved1 = Reserved1, - Reserved2 = Reserved2, - AutoLoginIfChallenged = AutoLoginIfChallenged != 0 - }; - } - - public void FreeNative() - { - Marshal.FreeCoTaskMem(AutoConfigUrl); - } - } - } -#endif - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG - { - public int AutoDetect; - public IntPtr AutoConfigUrl; - public IntPtr Proxy; - public IntPtr ProxyBypass; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WINHTTP_PROXY_INFO - { - public uint AccessType; - public IntPtr Proxy; - public IntPtr ProxyBypass; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WINHTTP_ASYNC_RESULT - { - public IntPtr dwResult; - 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; - public const uint API_WRITE_DATA = 4; - public const uint API_SEND_REQUEST = 5; - - public enum WINHTTP_WEB_SOCKET_OPERATION - { - WINHTTP_WEB_SOCKET_SEND_OPERATION = 0, - WINHTTP_WEB_SOCKET_RECEIVE_OPERATION = 1, - WINHTTP_WEB_SOCKET_CLOSE_OPERATION = 2, - WINHTTP_WEB_SOCKET_SHUTDOWN_OPERATION = 3 - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs index 967180c3..808ae2d1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs @@ -3,23 +3,23 @@ using Snap.Hutao.Core.Shell; using System.Net; +using System.Reflection; namespace Snap.Hutao.Core.IO.Http.DynamicProxy; [Injection(InjectAs.Singleton)] internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable { - private readonly RegistryMonitor registryMonitor; + private readonly RegistryWatcher watcher; - private IWebProxy innerProxy; + private IWebProxy innerProxy = default!; public DynamicHttpProxy() { - HttpWindowsProxy.TryCreate(out IWebProxy? proxy); - innerProxy = proxy ?? new HttpNoProxy(); + UpdateProxy(); - registryMonitor = RegistryMonitor.Create(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections", OnRegistryChanged); - registryMonitor.Start(); + watcher = RegistryWatcher.Create(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections", OnRegistryChanged); + watcher.Start(); } /// @@ -50,8 +50,14 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable public void UpdateProxy() { - HttpWindowsProxy.TryCreate(out IWebProxy? proxy); - InnerProxy = proxy ?? new HttpNoProxy(); + Assembly httpNamespace = Assembly.Load("System.Net.Http"); + Type? systemProxyInfoType = httpNamespace.GetType("System.Net.Http.SystemProxyInfo"); + ArgumentNullException.ThrowIfNull(systemProxyInfoType); + MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public); + ArgumentNullException.ThrowIfNull(constructSystemProxyMethod); + IWebProxy? proxy = (IWebProxy?)constructSystemProxyMethod.Invoke(null, null); + ArgumentNullException.ThrowIfNull(proxy); + InnerProxy = proxy; } public Uri? GetProxy(Uri destination) @@ -71,7 +77,7 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable disposable.Dispose(); } - registryMonitor.Dispose(); + watcher.Dispose(); } private void OnRegistryChanged(object? sender, EventArgs e) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs deleted file mode 100644 index f2fdead6..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/FailedProxyCache.cs +++ /dev/null @@ -1,138 +0,0 @@ -#pragma warning disable -using System.Collections.Concurrent; -using System.Runtime.CompilerServices; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/FailedProxyCache.cs -/// -internal sealed class FailedProxyCache -{ - /// - /// When returned by , indicates a proxy is immediately usable. - /// - public const long Immediate = 0; - - // If a proxy fails, time out 30 minutes. WinHTTP and Firefox both use this. - private const int FailureTimeoutInMilliseconds = 1000 * 60 * 30; - - // Scan through the failures and flush any that have expired every 5 minutes. - private const int FlushFailuresTimerInMilliseconds = 1000 * 60 * 5; - - // _failedProxies will only be flushed (rare but somewhat expensive) if we have more than this number of proxies in our dictionary. See Cleanup() for details. - private const int LargeProxyConfigBoundary = 8; - - // Value is the Environment.TickCount64 to remove the proxy from the failure list. - private readonly ConcurrentDictionary _failedProxies = new ConcurrentDictionary(); - - // When Environment.TickCount64 >= _nextFlushTicks, cause a flush. - private long _nextFlushTicks = Environment.TickCount64 + FlushFailuresTimerInMilliseconds; - - // This lock can be folded into _nextFlushTicks for space optimization, but - // this class should only have a single instance so would rather have clarity. - private SpinLock _flushLock = new SpinLock(enableThreadOwnerTracking: false); // mutable struct; do not make this readonly - - /// - /// Checks when a proxy will become usable. - /// - /// The of the proxy to check. - /// If the proxy can be used, . Otherwise, the next that it should be used. - public long GetProxyRenewTicks(Uri uri) - { - Cleanup(); - - // If not failed, ready immediately. - if (!_failedProxies.TryGetValue(uri, out long renewTicks)) - { - return Immediate; - } - - // If we haven't reached out renew time, the proxy can't be used. - if (Environment.TickCount64 < renewTicks) - { - return renewTicks; - } - - // Renew time reached, we can remove the proxy from the cache. - if (TryRenewProxy(uri, renewTicks)) - { - return Immediate; - } - - // Another thread updated the cache before we could remove it. - // We can't know if this is a removal or an update, so check again. - return _failedProxies.TryGetValue(uri, out renewTicks) ? renewTicks : Immediate; - } - - /// - /// Sets a proxy as failed, to avoid trying it again for some time. - /// - /// The URI of the proxy. - public void SetProxyFailed(Uri uri) - { - _failedProxies[uri] = Environment.TickCount64 + FailureTimeoutInMilliseconds; - Cleanup(); - } - - /// - /// Renews a proxy prior to its period expiring. Used when all proxies are failed to renew the proxy closest to being renewed. - /// - /// The of the proxy to renew. - /// The current renewal time for the proxy. If the value has changed from this, the proxy will not be renewed. - public bool TryRenewProxy(Uri uri, long renewTicks) => - _failedProxies.TryRemove(new KeyValuePair(uri, renewTicks)); - - /// - /// Cleans up any old proxies that should no longer be marked as failing. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Cleanup() - { - if (_failedProxies.Count > LargeProxyConfigBoundary && Environment.TickCount64 >= Interlocked.Read(ref _nextFlushTicks)) - { - CleanupHelper(); - } - } - - /// - /// Cleans up any old proxies that should no longer be marked as failing. - /// - /// - /// I expect this to never be called by in a production system. It is only needed in the case - /// that a system has a very large number of proxies that the PAC script cycles through. It is moderately expensive, - /// so it's only run periodically and is disabled until we exceed failed proxies. - /// - [MethodImpl(MethodImplOptions.NoInlining)] - private void CleanupHelper() - { - bool lockTaken = false; - try - { - _flushLock.TryEnter(ref lockTaken); - if (!lockTaken) - { - return; - } - - long curTicks = Environment.TickCount64; - - foreach (KeyValuePair kvp in _failedProxies) - { - if (curTicks >= kvp.Value) - { - ((ICollection>)_failedProxies).Remove(kvp); - } - } - } - finally - { - if (lockTaken) - { - Interlocked.Exchange(ref _nextFlushTicks, Environment.TickCount64 + FlushFailuresTimerInMilliseconds); - _flushLock.Exit(false); - } - } - } -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs deleted file mode 100644 index 62e624c0..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpNoProxy.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable -using System.Net; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpNoProxy.cs -/// -internal sealed class HttpNoProxy : IWebProxy -{ - public ICredentials? Credentials { get; set; } - public Uri? GetProxy(Uri destination) => null; - public bool IsBypassed(Uri host) => true; -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs deleted file mode 100644 index a01b27a4..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/HttpWindowsProxy.cs +++ /dev/null @@ -1,325 +0,0 @@ -#pragma warning disable -using System.Net; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using static Snap.Hutao.Win32.Interop.WinHttp; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpWindowsProxy.cs -/// -internal sealed class HttpWindowsProxy : IMultiWebProxy, IDisposable -{ - private readonly MultiProxy _insecureProxy; // URI of the http system proxy if set - private readonly MultiProxy _secureProxy; // URI of the https system proxy if set - private readonly FailedProxyCache _failedProxies = new FailedProxyCache(); - private readonly List? _bypass; // list of domains not to proxy - private readonly bool _bypassLocal; // we should bypass domain considered local - private readonly List? _localIp; - private ICredentials? _credentials; - private readonly WinInetProxyHelper _proxyHelper; - private SafeWinHttpHandle? _sessionHandle; - private bool _disposed; - - public static bool TryCreate([NotNullWhen(true)] out IWebProxy? proxy) - { - // This will get basic proxy setting from system using existing - // WinInetProxyHelper functions. If no proxy is enabled, it will return null. - SafeWinHttpHandle? sessionHandle = null; - proxy = null; - - WinInetProxyHelper proxyHelper = new WinInetProxyHelper(); - if (!proxyHelper.ManualSettingsOnly && !proxyHelper.AutoSettingsUsed) - { - return false; - } - - if (proxyHelper.AutoSettingsUsed) - { - sessionHandle = WinHttpOpen( - IntPtr.Zero, - WINHTTP_ACCESS_TYPE_NO_PROXY, - WINHTTP_NO_PROXY_NAME, - WINHTTP_NO_PROXY_BYPASS, - (int)WINHTTP_FLAG_ASYNC); - - if (sessionHandle.IsInvalid) - { - // Proxy failures are currently ignored by managed handler. - sessionHandle.Dispose(); - return false; - } - } - - proxy = new HttpWindowsProxy(proxyHelper, sessionHandle); - return true; - } - - private HttpWindowsProxy(WinInetProxyHelper proxyHelper, SafeWinHttpHandle? sessionHandle) - { - _proxyHelper = proxyHelper; - _sessionHandle = sessionHandle; - - if (proxyHelper.ManualSettingsUsed) - { - _secureProxy = MultiProxy.Parse(_failedProxies, proxyHelper.Proxy, true); - _insecureProxy = MultiProxy.Parse(_failedProxies, proxyHelper.Proxy, false); - - if (!string.IsNullOrWhiteSpace(proxyHelper.ProxyBypass)) - { - int idx = 0; - string? tmp; - - // Process bypass list for manual setting. - // Initial list size is best guess based on string length assuming each entry is at least 5 characters on average. - _bypass = new List(proxyHelper.ProxyBypass.Length / 5); - - while (idx < proxyHelper.ProxyBypass.Length) - { - // Strip leading spaces and scheme if any. - while (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] == ' ') { idx += 1; }; - if (string.Compare(proxyHelper.ProxyBypass, idx, "http://", 0, 7, StringComparison.OrdinalIgnoreCase) == 0) - { - idx += 7; - } - else if (string.Compare(proxyHelper.ProxyBypass, idx, "https://", 0, 8, StringComparison.OrdinalIgnoreCase) == 0) - { - idx += 8; - } - - if (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] == '[') - { - // Strip [] from IPv6 so we can use IdnHost laster for matching. - idx += 1; - } - - int start = idx; - while (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] != ' ' && proxyHelper.ProxyBypass[idx] != ';' && proxyHelper.ProxyBypass[idx] != ']') { idx += 1; }; - - if (idx == start) - { - // Empty string. - tmp = null; - } - else if (string.Compare(proxyHelper.ProxyBypass, start, "", 0, 7, StringComparison.OrdinalIgnoreCase) == 0) - { - _bypassLocal = true; - tmp = null; - } - else - { - tmp = proxyHelper.ProxyBypass.Substring(start, idx - start); - } - - // Skip trailing characters if any. - if (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] != ';') - { - // Got stopped at space or ']'. Strip until next ';' or end. - while (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] != ';') { idx += 1; }; - } - if (idx < proxyHelper.ProxyBypass.Length && proxyHelper.ProxyBypass[idx] == ';') - { - idx++; - } - if (tmp == null) - { - continue; - } - - _bypass.Add(tmp); - } - if (_bypass.Count == 0) - { - // Bypass string only had garbage we did not parse. - _bypass = null; - } - } - - if (_bypassLocal) - { - _localIp = new List(); - foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) - { - IPInterfaceProperties ipProps = netInterface.GetIPProperties(); - foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses) - { - _localIp.Add(addr.Address); - } - } - } - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - - if (_sessionHandle != null && !_sessionHandle.IsInvalid) - { - SafeWinHttpHandle.DisposeAndClearHandle(ref _sessionHandle); - } - } - } - - /// - /// Gets the proxy URI. (IWebProxy interface) - /// - public Uri? GetProxy(Uri uri) - { - GetMultiProxy(uri).ReadNext(out Uri? proxyUri, out _); - return proxyUri; - } - - /// - /// Gets the proxy URIs. - /// - public MultiProxy GetMultiProxy(Uri uri) - { - // We need WinHTTP to detect and/or process a PAC (JavaScript) file. This maps to - // "Automatically detect settings" and/or "Use automatic configuration script" from IE - // settings. But, calling into WinHTTP can be slow especially when it has to call into - // the out-of-process service to discover, load, and run the PAC file. So, we skip - // calling into WinHTTP if there was a recent failure to detect a PAC file on the network. - // This is a common error. The default IE settings on a Windows machine consist of the - // single checkbox for "Automatically detect settings" turned on and most networks - // won't actually discover a PAC file on the network since WPAD protocol isn't configured. - if (_proxyHelper.AutoSettingsUsed && !_proxyHelper.RecentAutoDetectionFailure) - { - WINHTTP_PROXY_INFO proxyInfo = default; - try - { - if (_proxyHelper.GetProxyForUrl(_sessionHandle, uri, out proxyInfo)) - { - // If WinHTTP just specified a Proxy with no ProxyBypass list, then - // we can return the Proxy uri directly. - if (proxyInfo.ProxyBypass == IntPtr.Zero) - { - if (proxyInfo.Proxy != IntPtr.Zero) - { - string proxyStr = Marshal.PtrToStringUni(proxyInfo.Proxy)!; - - return MultiProxy.CreateLazy(_failedProxies, proxyStr, IsSecureUri(uri)); - } - else - { - return MultiProxy.Empty; - } - } - - // A bypass list was also specified. This means that WinHTTP has fallen back to - // using the manual IE settings specified and there is a ProxyBypass list also. - // Since we're not really using the full WinHTTP stack, we need to use HttpSystemProxy - // to do the computation of the final proxy uri merging the information from the Proxy - // and ProxyBypass strings. - } - else - { - return MultiProxy.Empty; - } - } - finally - { - Marshal.FreeHGlobal(proxyInfo.Proxy); - Marshal.FreeHGlobal(proxyInfo.ProxyBypass); - } - } - - // Fallback to manual settings if present. - if (_proxyHelper.ManualSettingsUsed) - { - if (_bypassLocal) - { - IPAddress? address; - - if (uri.IsLoopback) - { - // This is optimization for loopback addresses. - // Unfortunately this does not work for all local addresses. - return MultiProxy.Empty; - } - - // Pre-Check if host may be IP address to avoid parsing. - if (uri.HostNameType == UriHostNameType.IPv6 || uri.HostNameType == UriHostNameType.IPv4) - { - // RFC1123 allows labels to start with number. - // Leading number may or may not be IP address. - // IPv6 [::1] notation. '[' is not valid character in names. - if (IPAddress.TryParse(uri.IdnHost, out address)) - { - // Host is valid IP address. - // Check if it belongs to local system. - foreach (IPAddress a in _localIp!) - { - if (a.Equals(address)) - { - return MultiProxy.Empty; - } - } - } - } - if (uri.HostNameType != UriHostNameType.IPv6 && !uri.IdnHost.Contains('.')) - { - // Not address and does not have a dot. - // Hosts without FQDN are considered local. - return MultiProxy.Empty; - } - } - - // Check if we have other rules for bypass. - if (_bypass != null) - { - foreach (string entry in _bypass) - { - // IdnHost does not have []. - if (SimpleRegex.IsMatchWithStarWildcard(uri.IdnHost, entry)) - { - return MultiProxy.Empty; - } - } - } - - // We did not find match on bypass list. - return IsSecureUri(uri) ? _secureProxy : _insecureProxy; - } - - return MultiProxy.Empty; - } - - private static bool IsSecureUri(Uri uri) - { - return uri.Scheme == UriScheme.Https || uri.Scheme == UriScheme.Wss; - } - - /// - /// Checks if URI is subject to proxy or not. - /// - public bool IsBypassed(Uri uri) - { - // This HttpSystemProxy class is only consumed by SocketsHttpHandler and is not exposed outside of - // SocketsHttpHandler. The current pattern for consumption of IWebProxy is to call IsBypassed first. - // If it returns false, then the caller will call GetProxy. For this proxy implementation, computing - // the return value for IsBypassed is as costly as calling GetProxy. We want to avoid doing extra - // work. So, this proxy implementation for the IsBypassed method can always return false. Then the - // GetProxy method will return non-null for a proxy, or null if no proxy should be used. - return false; - } - - public ICredentials? Credentials - { - get - { - return _credentials; - } - set - { - _credentials = value; - } - } - - // Access function for unit tests. - internal List? BypassList => _bypass; -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs deleted file mode 100644 index f906d055..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/IMultiWebProxy.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable -using System.Net; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/IMultiWebProxy.cs -/// -internal interface IMultiWebProxy : IWebProxy -{ - /// - /// Gets the proxy URIs. - /// - public MultiProxy GetMultiProxy(Uri uri); -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs deleted file mode 100644 index da75d93b..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/MultiProxy.cs +++ /dev/null @@ -1,268 +0,0 @@ -#pragma warning disable -using System.Diagnostics; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/MultiProxy.cs -/// -internal struct MultiProxy -{ - private readonly FailedProxyCache? _failedProxyCache; - private readonly Uri[]? _uris; - private readonly string? _proxyConfig; - private readonly bool _secure; - private int _currentIndex; - private Uri? _currentUri; - - private MultiProxy(FailedProxyCache? failedProxyCache, Uri[] uris) - { - _failedProxyCache = failedProxyCache; - _uris = uris; - _proxyConfig = null; - _secure = default; - _currentIndex = 0; - _currentUri = null; - } - - private MultiProxy(FailedProxyCache failedProxyCache, string proxyConfig, bool secure) - { - _failedProxyCache = failedProxyCache; - _uris = null; - _proxyConfig = proxyConfig; - _secure = secure; - _currentIndex = 0; - _currentUri = null; - } - - public static MultiProxy Empty => new MultiProxy(null, Array.Empty()); - - /// - /// Parses a WinHTTP proxy config into a MultiProxy instance. - /// - /// The cache of failed proxy requests to employ. - /// The WinHTTP proxy config to parse. - /// If true, return proxies suitable for use with a secure connection. If false, return proxies suitable for an insecure connection. - public static MultiProxy Parse(FailedProxyCache failedProxyCache, string? proxyConfig, bool secure) - { - Debug.Assert(failedProxyCache != null); - - Uri[] uris = Array.Empty(); - - ReadOnlySpan span = proxyConfig; - while (TryParseProxyConfigPart(span, secure, out Uri? uri, out int charactersConsumed)) - { - int idx = uris.Length; - - // Assume that we will typically not have more than 1...3 proxies, so just - // grow by 1. This method is currently only used once per process, so the - // case of an abnormally large config will not be much of a concern anyway. - Array.Resize(ref uris, idx + 1); - uris[idx] = uri; - - span = span.Slice(charactersConsumed); - } - - return new MultiProxy(failedProxyCache, uris); - } - - /// - /// Initializes a MultiProxy instance that lazily parses a given WinHTTP configuration string. - /// - /// The cache of failed proxy requests to employ. - /// The WinHTTP proxy config to parse. - /// If true, return proxies suitable for use with a secure connection. If false, return proxies suitable for an insecure connection. - public static MultiProxy CreateLazy(FailedProxyCache failedProxyCache, string proxyConfig, bool secure) - { - Debug.Assert(failedProxyCache != null); - - return string.IsNullOrEmpty(proxyConfig) == false ? - new MultiProxy(failedProxyCache, proxyConfig, secure) : - MultiProxy.Empty; - } - - /// - /// Reads the next proxy URI from the MultiProxy. - /// - /// The next proxy to use for the request. - /// If true, indicates there are no further proxies to read from the config. - /// If there is a proxy available, true. Otherwise, false. - public bool ReadNext([NotNullWhen(true)] out Uri? uri, out bool isFinalProxy) - { - // Enumerating indicates the previous proxy has failed; mark it as such. - if (_currentUri != null) - { - Debug.Assert(_failedProxyCache != null); - _failedProxyCache.SetProxyFailed(_currentUri); - } - - // If no more proxies to read, return out quickly. - if (!ReadNextHelper(out uri, out isFinalProxy)) - { - _currentUri = null; - return false; - } - - // If this is the first ReadNext() and all proxies are marked as failed, return the proxy that is closest to renewal. - Uri? oldestFailedProxyUri = null; - long oldestFailedProxyTicks = long.MaxValue; - - do - { - Debug.Assert(_failedProxyCache != null); - long renewTicks = _failedProxyCache.GetProxyRenewTicks(uri); - - // Proxy hasn't failed recently, return for use. - if (renewTicks == FailedProxyCache.Immediate) - { - _currentUri = uri; - return true; - } - - if (renewTicks < oldestFailedProxyTicks) - { - oldestFailedProxyUri = uri; - oldestFailedProxyTicks = renewTicks; - } - } - while (ReadNextHelper(out uri, out isFinalProxy)); - - // All the proxies in the config have failed; in this case, return the proxy that is closest to renewal. - if (_currentUri == null) - { - uri = oldestFailedProxyUri; - _currentUri = oldestFailedProxyUri; - - if (oldestFailedProxyUri != null) - { - Debug.Assert(uri != null); - _failedProxyCache.TryRenewProxy(uri, oldestFailedProxyTicks); - return true; - } - } - - return false; - } - - /// - /// Reads the next proxy URI from the MultiProxy, either via parsing a config string or from an array. - /// - private bool ReadNextHelper([NotNullWhen(true)] out Uri? uri, out bool isFinalProxy) - { - Debug.Assert(_uris != null || _proxyConfig != null, $"{nameof(ReadNext)} must not be called on a default-initialized {nameof(MultiProxy)}."); - - if (_uris != null) - { - if (_currentIndex == _uris.Length) - { - uri = default; - isFinalProxy = default; - return false; - } - - uri = _uris[_currentIndex++]; - isFinalProxy = _currentIndex == _uris.Length; - return true; - } - - Debug.Assert(_proxyConfig != null); - if (_currentIndex < _proxyConfig.Length) - { - bool hasProxy = TryParseProxyConfigPart(_proxyConfig.AsSpan(_currentIndex), _secure, out uri!, out int charactersConsumed); - - _currentIndex += charactersConsumed; - Debug.Assert(_currentIndex <= _proxyConfig.Length); - - isFinalProxy = _currentIndex == _proxyConfig.Length; - - return hasProxy; - } - - uri = default; - isFinalProxy = default; - return false; - } - - /// - /// This method is used to parse WinINet Proxy strings, a single proxy at a time. - /// - /// - /// The strings are a semicolon or whitespace separated list, with each entry in the following format: - /// ([<scheme>=][<scheme>"://"]<server>[":"<port>]) - /// - private static bool TryParseProxyConfigPart(ReadOnlySpan proxyString, bool secure, [NotNullWhen(true)] out Uri? uri, out int charactersConsumed) - { - const int SECURE_FLAG = 1; - const int INSECURE_FLAG = 2; - const string ProxyDelimiters = "; \n\r\t"; - - int wantedFlag = secure ? SECURE_FLAG : INSECURE_FLAG; - int originalLength = proxyString.Length; - - while (true) - { - // Skip any delimiters. - int iter = 0; - while (iter < proxyString.Length && ProxyDelimiters.Contains(proxyString[iter])) - { - ++iter; - } - - if (iter == proxyString.Length) - { - break; - } - - proxyString = proxyString.Slice(iter); - - // Determine which scheme this part is for. - // If no schema is defined, use both. - int proxyType = SECURE_FLAG | INSECURE_FLAG; - - if (proxyString.StartsWith("http=")) - { - proxyType = INSECURE_FLAG; - proxyString = proxyString.Slice("http=".Length); - } - else if (proxyString.StartsWith("https=")) - { - proxyType = SECURE_FLAG; - proxyString = proxyString.Slice("https=".Length); - } - - if (proxyString.StartsWith("http://")) - { - proxyType = INSECURE_FLAG; - proxyString = proxyString.Slice("http://".Length); - } - else if (proxyString.StartsWith("https://")) - { - proxyType = SECURE_FLAG; - proxyString = proxyString.Slice("https://".Length); - } - - // Find the next delimiter, or end of string. - iter = proxyString.IndexOfAny(ProxyDelimiters); - if (iter < 0) - { - iter = proxyString.Length; - } - - // Return URI if it's a match to what we want. - if ((proxyType & wantedFlag) != 0 && Uri.TryCreate(string.Concat("http://", proxyString.Slice(0, iter)), UriKind.Absolute, out uri)) - { - charactersConsumed = originalLength - proxyString.Length + iter; - Debug.Assert(charactersConsumed > 0); - - return true; - } - - proxyString = proxyString.Slice(iter); - } - - uri = null; - charactersConsumed = originalLength; - return false; - } -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs deleted file mode 100644 index 6d992b2d..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/SimpleRegex.cs +++ /dev/null @@ -1,73 +0,0 @@ -#pragma warning disable -using System.Diagnostics; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Common/src/System/Text/SimpleRegex.cs -/// -internal static class SimpleRegex -{ - // Based on wildcmp written by Jack Handy - jakkhandy@hotmail.com - // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing - - /// - /// Perform a match between an input string and a pattern in which the only special character - /// is an asterisk, which can map to zero or more of any character in the input. - /// - /// The input to match. - /// The pattern to match against. - /// true if the input matches the pattern; otherwise, false. - public static bool IsMatchWithStarWildcard(ReadOnlySpan input, ReadOnlySpan pattern) - { - int inputPos = 0, inputPosSaved = -1; - int patternPos = 0, patternPosSaved = -1; - - // Loop through each character in the input. - while (inputPos < input.Length) - { - if (patternPos < pattern.Length && pattern[patternPos] == '*') - { - // If we're currently positioned on a wildcard in the pattern, - // move past it and remember where we are to backtrack to. - inputPosSaved = inputPos; - patternPosSaved = ++patternPos; - } - else if (patternPos < pattern.Length && - (pattern[patternPos] == input[inputPos] || - char.ToUpperInvariant(pattern[patternPos]) == char.ToUpperInvariant(input[inputPos]))) - { - // If the characters in the pattern and the input match, advance both. - inputPos++; - patternPos++; - } - else if (patternPosSaved == -1) - { - // If we're not on a wildcard and the current characters don't match and we don't have - // any wildcard to backtrack to, this is not a match. - return false; - } - else - { - // Otherwise, this is not a wildcard, the characters don't match, but we do have a - // wildcard saved, so backtrack to it and use it to consume the next input character. - inputPos = ++inputPosSaved; - patternPos = patternPosSaved; - } - } - - // We've reached the end of the input. Eat all wildcards immediately after where we are - // in the pattern, as if they're at the end, they'll all just map to nothing (and if it - // turns out there's something after them, eating them won't matter). - while (patternPos < pattern.Length && pattern[patternPos] == '*') - { - patternPos++; - } - - // If we are in fact at the end of the pattern, then we successfully matched. - // If there's anything left, it's not a wildcard, so it doesn't match. - Debug.Assert(patternPos <= pattern.Length); - return patternPos == pattern.Length; - } -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs deleted file mode 100644 index 124f898a..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/UriScheme.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Common/src/System/Net/UriScheme.cs -/// -internal static class UriScheme -{ - public const string File = "file"; - public const string Ftp = "ftp"; - public const string Gopher = "gopher"; - public const string Http = "http"; - public const string Https = "https"; - public const string News = "news"; - public const string NetPipe = "net.pipe"; - public const string NetTcp = "net.tcp"; - public const string Nntp = "nntp"; - public const string Mailto = "mailto"; - public const string Ws = "ws"; - public const string Wss = "wss"; - - public const string SchemeDelimiter = "://"; -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs deleted file mode 100644 index 41be5ec1..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/WinInetProxyHelper.cs +++ /dev/null @@ -1,179 +0,0 @@ -#pragma warning disable -using System.Runtime.InteropServices; -using static Snap.Hutao.Win32.Interop.WinHttp; - -namespace Snap.Hutao.Core.IO.Http.DynamicProxy; - -/// -/// Copied from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Common/src/System/Net/Http/WinInetProxyHelper.cs -/// -internal sealed class WinInetProxyHelper -{ - private const int RecentAutoDetectionInterval = 120_000; // 2 minutes in milliseconds. - private readonly string? _autoConfigUrl, _proxy, _proxyBypass; - private readonly bool _autoDetect; - private readonly bool _useProxy; - private bool _autoDetectionFailed; - private int _lastTimeAutoDetectionFailed; // Environment.TickCount units (milliseconds). - - public WinInetProxyHelper() - { - WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig = default; - - try - { - if (WinHttpGetIEProxyConfigForCurrentUser(out proxyConfig)) - { - _autoConfigUrl = Marshal.PtrToStringUni(proxyConfig.AutoConfigUrl)!; - _autoDetect = proxyConfig.AutoDetect != 0; - _proxy = Marshal.PtrToStringUni(proxyConfig.Proxy)!; - _proxyBypass = Marshal.PtrToStringUni(proxyConfig.ProxyBypass)!; - - _useProxy = true; - } - else - { - // We match behavior of WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY and ignore errors. - int lastError = Marshal.GetLastWin32Error(); - } - } - finally - { - // FreeHGlobal already checks for null pointer before freeing the memory. - Marshal.FreeHGlobal(proxyConfig.AutoConfigUrl); - Marshal.FreeHGlobal(proxyConfig.Proxy); - Marshal.FreeHGlobal(proxyConfig.ProxyBypass); - } - } - - public string? AutoConfigUrl => _autoConfigUrl; - - public bool AutoDetect => _autoDetect; - - public bool AutoSettingsUsed => AutoDetect || !string.IsNullOrEmpty(AutoConfigUrl); - - public bool ManualSettingsUsed => !string.IsNullOrEmpty(Proxy); - - public bool ManualSettingsOnly => !AutoSettingsUsed && ManualSettingsUsed; - - public string? Proxy => _proxy; - - public string? ProxyBypass => _proxyBypass; - - public bool RecentAutoDetectionFailure => - _autoDetectionFailed && - Environment.TickCount - _lastTimeAutoDetectionFailed <= RecentAutoDetectionInterval; - - public bool GetProxyForUrl( - SafeWinHttpHandle? sessionHandle, - Uri uri, - out WINHTTP_PROXY_INFO proxyInfo) - { - proxyInfo.AccessType = WINHTTP_ACCESS_TYPE_NO_PROXY; - proxyInfo.Proxy = IntPtr.Zero; - proxyInfo.ProxyBypass = IntPtr.Zero; - - if (!_useProxy) - { - return false; - } - - bool useProxy = false; - - WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions; - autoProxyOptions.AutoConfigUrl = AutoConfigUrl; - autoProxyOptions.AutoDetectFlags = AutoDetect ? - (WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A) : 0; - autoProxyOptions.AutoLoginIfChallenged = false; - autoProxyOptions.Flags = - (AutoDetect ? WINHTTP_AUTOPROXY_AUTO_DETECT : 0) | - (!string.IsNullOrEmpty(AutoConfigUrl) ? WINHTTP_AUTOPROXY_CONFIG_URL : 0); - autoProxyOptions.Reserved1 = IntPtr.Zero; - autoProxyOptions.Reserved2 = 0; - - // AutoProxy Cache. - // https://docs.microsoft.com/en-us/windows/desktop/WinHttp/autoproxy-cache - // If the out-of-process service is active when WinHttpGetProxyForUrl is called, the cached autoproxy - // URL and script are available to the whole computer. However, if the out-of-process service is used, - // and the fAutoLogonIfChallenged flag in the pAutoProxyOptions structure is true, then the autoproxy - // URL and script are not cached. Therefore, calling WinHttpGetProxyForUrl with the fAutoLogonIfChallenged - // member set to TRUE results in additional overhead operations that may affect performance. - // The following steps can be used to improve performance: - // 1. Call WinHttpGetProxyForUrl with the fAutoLogonIfChallenged parameter set to false. The autoproxy - // URL and script are cached for future calls to WinHttpGetProxyForUrl. - // 2. If Step 1 fails, with ERROR_WINHTTP_LOGIN_FAILURE, then call WinHttpGetProxyForUrl with the - // fAutoLogonIfChallenged member set to TRUE. - // - // We match behavior of WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY and ignore errors. -#pragma warning disable CA1845 // file is shared with a build that lacks string.Concat for spans - // Underlying code does not understand WebSockets so we need to convert it to http or https. - string destination = uri.AbsoluteUri; - if (uri.Scheme == UriScheme.Wss) - { - destination = UriScheme.Https + destination.Substring(UriScheme.Wss.Length); - } - else if (uri.Scheme == UriScheme.Ws) - { - destination = UriScheme.Http + destination.Substring(UriScheme.Ws.Length); - } -#pragma warning restore CA1845 - - var repeat = false; - do - { - _autoDetectionFailed = false; - if (WinHttpGetProxyForUrl( - sessionHandle!, - destination, - ref autoProxyOptions, - out proxyInfo)) - { - useProxy = true; - - break; - } - else - { - var lastError = Marshal.GetLastWin32Error(); - - if (lastError == ERROR_WINHTTP_LOGIN_FAILURE) - { - if (repeat) - { - // We don't retry more than once. - break; - } - else - { - repeat = true; - autoProxyOptions.AutoLoginIfChallenged = true; - } - } - else - { - if (lastError == ERROR_WINHTTP_AUTODETECTION_FAILED) - { - _autoDetectionFailed = true; - _lastTimeAutoDetectionFailed = Environment.TickCount; - } - - break; - } - } - } while (repeat); - - // Fall back to manual settings if available. - if (!useProxy && !string.IsNullOrEmpty(Proxy)) - { - proxyInfo.AccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; - proxyInfo.Proxy = Marshal.StringToHGlobalUni(Proxy); - proxyInfo.ProxyBypass = string.IsNullOrEmpty(ProxyBypass) ? - IntPtr.Zero : Marshal.StringToHGlobalUni(ProxyBypass); - - useProxy = true; - } - - return useProxy; - } -} -#pragma warning restore \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs b/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs rename to src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs index 3ec03751..45632c6d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryMonitor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs @@ -7,7 +7,7 @@ using Windows.Win32.System.Registry; namespace Snap.Hutao.Core.Shell; -internal sealed partial class RegistryMonitor : IDisposable +internal sealed partial class RegistryWatcher : IDisposable { private readonly ManualResetEvent eventTerminate = new(false); private readonly CancellationTokenSource cancellationTokenSource = new(); @@ -20,7 +20,7 @@ internal sealed partial class RegistryMonitor : IDisposable private string subKey = default!; private bool disposed; - public RegistryMonitor(string name, EventHandler eventHandler) + public RegistryWatcher(string name, EventHandler eventHandler) { ArgumentException.ThrowIfNullOrEmpty(name); @@ -30,9 +30,9 @@ internal sealed partial class RegistryMonitor : IDisposable public event EventHandler? RegChanged; - public static RegistryMonitor Create(string name, EventHandler eventHandler) + public static RegistryWatcher Create(string name, EventHandler eventHandler) { - return new RegistryMonitor(name, eventHandler); + return new RegistryWatcher(name, eventHandler); } public void Start() From 5f9b4a7cb28aa8b42055f142a16f0467b2ffb2af Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 8 Jan 2024 21:55:59 +0800 Subject: [PATCH 3/4] refactor RegistryWatcher --- src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs | 8 - .../IO/Http/DynamicProxy/DynamicHttpProxy.cs | 2 +- .../Snap.Hutao/Core/Shell/RegistryWatcher.cs | 197 +++++++++--------- 3 files changed, 103 insertions(+), 104 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs index 2b049d3e..fc570f6e 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs +++ b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs @@ -15,12 +15,4 @@ internal static partial class PInvoke ppv = (TInterface)o; return hr; } - - internal static unsafe WIN32_ERROR RegOpenKeyEx(HKEY hKey, string lpSubKey, uint ulOptions, REG_SAM_FLAGS samDesired, out HKEY phkResult) - { - HKEY hKeyResult; - WIN32_ERROR __result = PInvoke.RegOpenKeyEx(hKey, lpSubKey, ulOptions, samDesired, &hKeyResult); - phkResult = hKeyResult; - return __result; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs index 808ae2d1..65861589 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs @@ -18,7 +18,7 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable { UpdateProxy(); - watcher = RegistryWatcher.Create(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections", OnRegistryChanged); + watcher = new (@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections"); watcher.Start(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs b/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs index 45632c6d..4ad120af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs @@ -1,146 +1,153 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Windows.Win32; +using System.Runtime.InteropServices; using Windows.Win32.Foundation; using Windows.Win32.System.Registry; +using static Windows.Win32.PInvoke; namespace Snap.Hutao.Core.Shell; internal sealed partial class RegistryWatcher : IDisposable { - private readonly ManualResetEvent eventTerminate = new(false); + private const REG_SAM_FLAGS RegSamFlags = + REG_SAM_FLAGS.KEY_QUERY_VALUE | + REG_SAM_FLAGS.KEY_NOTIFY | + REG_SAM_FLAGS.KEY_READ; + + private const REG_NOTIFY_FILTER RegNotifyFilters = + REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_NAME | + REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_ATTRIBUTES | + REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_LAST_SET | + REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_SECURITY; + + private readonly ManualResetEvent disposeEvent = new(false); private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly REG_SAM_FLAGS samFlags = REG_SAM_FLAGS.KEY_QUERY_VALUE | REG_SAM_FLAGS.KEY_NOTIFY | REG_SAM_FLAGS.KEY_READ; - private readonly REG_NOTIFY_FILTER notiftFilters = REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_ATTRIBUTES | - REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_FILTER.REG_NOTIFY_CHANGE_SECURITY; - - private HKEY hKey; - private string subKey = default!; + private readonly HKEY hKey; + private readonly string subKey = default!; + private readonly Action valueChangedCallback; + private readonly object syncRoot = new(); private bool disposed; - public RegistryWatcher(string name, EventHandler eventHandler) + public RegistryWatcher(string keyName, Action valueChangedCallback) { - ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentException.ThrowIfNullOrEmpty(keyName); + string[] pathArray = keyName.Split('\\'); - InitRegistryKey(name); - RegChanged += eventHandler; - } + hKey = pathArray[0] switch + { + nameof(HKEY.HKEY_CLASSES_ROOT) => HKEY.HKEY_CLASSES_ROOT, + nameof(HKEY.HKEY_CURRENT_USER) => HKEY.HKEY_CURRENT_USER, + nameof(HKEY.HKEY_LOCAL_MACHINE) => HKEY.HKEY_LOCAL_MACHINE, + nameof(HKEY.HKEY_USERS) => HKEY.HKEY_USERS, + nameof(HKEY.HKEY_CURRENT_CONFIG) => HKEY.HKEY_CURRENT_CONFIG, + _ => throw new ArgumentException("The registry hive '" + pathArray[0] + "' is not supported", nameof(keyName)), + }; - public event EventHandler? RegChanged; - - public static RegistryWatcher Create(string name, EventHandler eventHandler) - { - return new RegistryWatcher(name, eventHandler); + subKey = string.Join("\\", pathArray[1..]); + this.valueChangedCallback = valueChangedCallback; } public void Start() { ObjectDisposedException.ThrowIf(disposed, this); - - eventTerminate.Reset(); - MonitorCoreAsync(cancellationTokenSource.Token).SafeForget(); + WatchAsync(cancellationTokenSource.Token).SafeForget(); } public void Dispose() { - eventTerminate.Dispose(); - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - disposed = true; - GC.SuppressFinalize(this); - } - - private void InitRegistryKey(string name) - { - string[] nameParts = name.Split('\\'); - - switch (nameParts[0]) + // Standard no-reentrancy pattern + if (disposed) { - case "HKEY_CLASSES_ROOT": - case "HKCR": - hKey = HKEY.HKEY_CLASSES_ROOT; - break; - - case "HKEY_CURRENT_USER": - case "HKCU": - hKey = HKEY.HKEY_CURRENT_USER; - break; - - case "HKEY_LOCAL_MACHINE": - case "HKLM": - hKey = HKEY.HKEY_LOCAL_MACHINE; - break; - - case "HKEY_USERS": - hKey = HKEY.HKEY_USERS; - break; - - case "HKEY_CURRENT_CONFIG": - hKey = HKEY.HKEY_CURRENT_CONFIG; - break; - - default: - hKey = HKEY.Null; - throw new ArgumentException("The registry hive '" + nameParts[0] + "' is not supported", nameof(name)); + return; } - subKey = string.Join("\\", nameParts, 1, nameParts.Length - 1); + lock (syncRoot) + { + if (disposed) + { + return; + } + + // First cancel the outer while loop + cancellationTokenSource.Cancel(); + + // Then signal the inner while loop to exit + disposeEvent.Set(); + + // Wait for both loops to exit + disposeEvent.WaitOne(); + + disposeEvent.Dispose(); + cancellationTokenSource.Dispose(); + + disposed = true; + + GC.SuppressFinalize(this); + } } - private async ValueTask MonitorCoreAsync(CancellationToken token) + [SuppressMessage("", "SH002")] + private static unsafe void UnsafeRegOpenKeyEx(HKEY hKey, string subKey, uint ulOptions, REG_SAM_FLAGS samDesired, out HKEY result) { - using (PeriodicTimer timer = new(TimeSpan.FromSeconds(1))) + fixed (HKEY* resultPtr = &result) { - try + HRESULT hResult = HRESULT_FROM_WIN32(RegOpenKeyEx(hKey, subKey, ulOptions, samDesired, resultPtr)); + Marshal.ThrowExceptionForHR(hResult); + } + } + + [SuppressMessage("", "SH002")] + private static unsafe void UnsafeRegNotifyChangeKeyValue(HKEY hKey, BOOL bWatchSubtree, REG_NOTIFY_FILTER dwNotifyFilter, HANDLE hEvent, BOOL fAsynchronous) + { + HRESULT hRESULT = HRESULT_FROM_WIN32(RegNotifyChangeKeyValue(hKey, bWatchSubtree, dwNotifyFilter, hEvent, fAsynchronous)); + Marshal.ThrowExceptionForHR(hRESULT); + } + + private async ValueTask WatchAsync(CancellationToken token) + { + try + { + while (!token.IsCancellationRequested) { - while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + + UnsafeRegOpenKeyEx(hKey, subKey, 0, RegSamFlags, out HKEY registryKey); + + using (ManualResetEvent notifyEvent = new(false)) { - if (token.IsCancellationRequested) - { - break; - } - - WIN32_ERROR result = PInvoke.RegOpenKeyEx(hKey, subKey, 0, samFlags, out HKEY registryKey); - if (result != WIN32_ERROR.ERROR_SUCCESS) - { - throw new Win32Exception((int)result); - } - - AutoResetEvent eventNotify = new(false); + HANDLE hEvent = (HANDLE)notifyEvent.SafeWaitHandle.DangerousGetHandle(); try { - WaitHandle[] waitHandles = [eventNotify, eventTerminate]; - while (!eventTerminate.WaitOne(0, true)) + // If terminateEvent is signaled, the Dispose method + // has been called and the object is shutting down. + // The outer token has already canceled, so we can + // skip both loops and exit the method. + while (!disposeEvent.WaitOne(0, true)) { - result = PInvoke.RegNotifyChangeKeyValue(registryKey, true, notiftFilters, (HANDLE)eventNotify.SafeWaitHandle.DangerousGetHandle(), true); - if (result != 0) - { - throw new Win32Exception((int)result); - } + UnsafeRegNotifyChangeKeyValue(registryKey, true, RegNotifyFilters, hEvent, true); - if (WaitHandle.WaitAny(waitHandles) == 0) + if (WaitHandle.WaitAny([notifyEvent, disposeEvent]) is 0) { - RegChanged?.Invoke(this, EventArgs.Empty); + valueChangedCallback(); + notifyEvent.Reset(); } } } finally { - if (registryKey != IntPtr.Zero) - { - PInvoke.RegCloseKey(registryKey); - } - - eventNotify.Dispose(); + RegCloseKey(registryKey); } } } - catch (OperationCanceledException) - { - } + + // Before exiting, signal the Dispose method. + disposeEvent.Reset(); + } + catch (OperationCanceledException) + { } } -} +} \ No newline at end of file From f1d9787e4593d0c6bd063fc194ff0f29cd576a63 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 8 Jan 2024 22:18:07 +0800 Subject: [PATCH 4/4] fix method call --- src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs | 1 - .../IO/Http/DynamicProxy/DynamicHttpProxy.cs | 46 +++++++++---------- .../Registry}/RegistryWatcher.cs | 10 ++-- 3 files changed, 29 insertions(+), 28 deletions(-) rename src/Snap.Hutao/Snap.Hutao/{Core/Shell => Win32/Registry}/RegistryWatcher.cs (96%) diff --git a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs index fc570f6e..a54fa034 100644 --- a/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs +++ b/src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs @@ -1,7 +1,6 @@ using System; using Windows.Win32.Foundation; using Windows.Win32.System.Com; -using Windows.Win32.System.Registry; namespace Windows.Win32; diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs index 65861589..3485479c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/Http/DynamicProxy/DynamicHttpProxy.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core.Shell; +using Snap.Hutao.Win32.Registry; using System.Net; using System.Reflection; @@ -10,15 +10,29 @@ namespace Snap.Hutao.Core.IO.Http.DynamicProxy; [Injection(InjectAs.Singleton)] internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable { + private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections"; + + private static readonly MethodInfo ConstructSystemProxyMethod; + private readonly RegistryWatcher watcher; private IWebProxy innerProxy = default!; + static DynamicHttpProxy() + { + Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo"); + ArgumentNullException.ThrowIfNull(systemProxyInfoType); + + MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public); + ArgumentNullException.ThrowIfNull(constructSystemProxyMethod); + ConstructSystemProxyMethod = constructSystemProxyMethod; + } + public DynamicHttpProxy() { UpdateProxy(); - watcher = new (@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections"); + watcher = new(ProxySettingPath, UpdateProxy); watcher.Start(); } @@ -32,6 +46,8 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable private IWebProxy InnerProxy { get => innerProxy; + + [MemberNotNull(nameof(innerProxy))] set { if (ReferenceEquals(innerProxy, value)) @@ -39,24 +55,17 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable return; } - if (innerProxy is IDisposable disposable) - { - disposable.Dispose(); - } - + (innerProxy as IDisposable)?.Dispose(); innerProxy = value; } } + [MemberNotNull(nameof(innerProxy))] public void UpdateProxy() { - Assembly httpNamespace = Assembly.Load("System.Net.Http"); - Type? systemProxyInfoType = httpNamespace.GetType("System.Net.Http.SystemProxyInfo"); - ArgumentNullException.ThrowIfNull(systemProxyInfoType); - MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public); - ArgumentNullException.ThrowIfNull(constructSystemProxyMethod); - IWebProxy? proxy = (IWebProxy?)constructSystemProxyMethod.Invoke(null, null); + IWebProxy? proxy = ConstructSystemProxyMethod.Invoke(default, default) as IWebProxy; ArgumentNullException.ThrowIfNull(proxy); + InnerProxy = proxy; } @@ -72,16 +81,7 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable public void Dispose() { - if (innerProxy is IDisposable disposable) - { - disposable.Dispose(); - } - + (innerProxy as IDisposable)?.Dispose(); watcher.Dispose(); } - - private void OnRegistryChanged(object? sender, EventArgs e) - { - UpdateProxy(); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs b/src/Snap.Hutao/Snap.Hutao/Win32/Registry/RegistryWatcher.cs similarity index 96% rename from src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs rename to src/Snap.Hutao/Snap.Hutao/Win32/Registry/RegistryWatcher.cs index 4ad120af..43fa8020 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Shell/RegistryWatcher.cs +++ b/src/Snap.Hutao/Snap.Hutao/Win32/Registry/RegistryWatcher.cs @@ -6,7 +6,7 @@ using Windows.Win32.Foundation; using Windows.Win32.System.Registry; using static Windows.Win32.PInvoke; -namespace Snap.Hutao.Core.Shell; +namespace Snap.Hutao.Win32.Registry; internal sealed partial class RegistryWatcher : IDisposable { @@ -32,7 +32,6 @@ internal sealed partial class RegistryWatcher : IDisposable public RegistryWatcher(string keyName, Action valueChangedCallback) { - ArgumentException.ThrowIfNullOrEmpty(keyName); string[] pathArray = keyName.Split('\\'); hKey = pathArray[0] switch @@ -143,8 +142,11 @@ internal sealed partial class RegistryWatcher : IDisposable } } - // Before exiting, signal the Dispose method. - disposeEvent.Reset(); + if (!disposed) + { + // Before exiting, signal the Dispose method. + disposeEvent.Reset(); + } } catch (OperationCanceledException) {