diff --git a/README.md b/README.md index 5c24935..112df40 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ using (WmiConnection con = new WmiConnection()) } ``` -Query all partitions for a remote machine with credentials: +Query all partitions for a remote machine with credentials (UPN format recommended): ```C# var opt = new WmiConnectionOptions() { EnablePackageEncryption = true }; -var cred = new NetworkCredential("USERNAME", "PASSWORD", "DOMAIN"); +var cred = new NetworkCredential("USER@DOMAIN", "PASSWORD"); using (WmiConnection con = new WmiConnection(@"\\MACHINENAME\root\cimv2", cred, opt)) { diff --git a/WmiLight.Native/WmiLight.Native.rc b/WmiLight.Native/WmiLight.Native.rc index b38f96e..c18d168 100644 --- a/WmiLight.Native/WmiLight.Native.rc +++ b/WmiLight.Native/WmiLight.Native.rc @@ -43,8 +43,8 @@ END // VS_VERSION_INFO VERSIONINFO - FILEVERSION 7,0,0,0 - PRODUCTVERSION 7,0,0,0 + FILEVERSION 7,1,0,0 + PRODUCTVERSION 7,1,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -70,12 +70,12 @@ BEGIN VALUE "FileDescription", "The native part of the WmiLight lib." #endif - VALUE "FileVersion", "7.0.0.0" + VALUE "FileVersion", "7.1.0.0" VALUE "InternalName", "WmiLight.Native" VALUE "LegalCopyright", "Copyright 2026 Martin Kuschnik" VALUE "OriginalFilename", "WmiLight.Native.dll" VALUE "ProductName", "WmiLight" - VALUE "ProductVersion", "7.0.0.0" + VALUE "ProductVersion", "7.1.0.0" END END BLOCK "VarFileInfo" diff --git a/WmiLight.Native/WmiLightNative.cpp b/WmiLight.Native/WmiLightNative.cpp index 5d313e7..5a025a7 100644 --- a/WmiLight.Native/WmiLightNative.cpp +++ b/WmiLight.Native/WmiLightNative.cpp @@ -87,19 +87,18 @@ extern "C" { // only need to export C interface if ); } - __declspec(dllexport) HRESULT _stdcall SetProxy( IUnknown* pIUnknown, wchar_t* username, wchar_t* password, - wchar_t* authority, + wchar_t* domain, ImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel) { if (pIUnknown == nullptr) return E_POINTER; - if (username == nullptr && password == nullptr && authority == nullptr) + if (username == nullptr && password == nullptr && domain == nullptr) { return CoSetProxyBlanket( pIUnknown, @@ -116,9 +115,9 @@ extern "C" { // only need to export C interface if authInfo.User = (unsigned short*)username; authInfo.UserLength = username == nullptr ? 0 : static_cast(wcslen(username)); - - authInfo.Domain = (unsigned short*)authority; - authInfo.DomainLength = authority == nullptr ? 0 : static_cast(wcslen(authority)); + + authInfo.Domain = (unsigned short*)domain; + authInfo.DomainLength = domain == nullptr ? 0 : static_cast(wcslen(domain)); authInfo.Password = (unsigned short*)password; authInfo.PasswordLength = password == nullptr ? 0 : static_cast(wcslen(password)); @@ -129,7 +128,7 @@ extern "C" { // only need to export C interface if pIUnknown, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, - nullptr, + COLE_DEFAULT_PRINCIPAL, authenticationLevel, impersonationLevel, &authInfo, diff --git a/WmiLight/Exceptions/InvalidMethodException.cs b/WmiLight/Exceptions/InvalidMethodException.cs index 837e062..bc73e9a 100644 --- a/WmiLight/Exceptions/InvalidMethodException.cs +++ b/WmiLight/Exceptions/InvalidMethodException.cs @@ -27,6 +27,11 @@ internal InvalidMethodException(string method, WbemStatus wbemStatus) #endregion + #region Description + /// + /// Gets the name of the invalid Method. + /// + #endregion public string Method { get; } } } diff --git a/WmiLight/Exceptions/LocalCredentialsException.cs b/WmiLight/Exceptions/LocalCredentialsException.cs index 8b23c81..db8f1a5 100644 --- a/WmiLight/Exceptions/LocalCredentialsException.cs +++ b/WmiLight/Exceptions/LocalCredentialsException.cs @@ -2,7 +2,7 @@ { #region Description /// - /// The Exception for the case that the Username, password, or authority are specified for a local . + /// The Exception for the case that the Username, password, or domain are specified for a local . /// #endregion public class LocalCredentialsException : WmiException diff --git a/WmiLight/Exceptions/TransportFailureException.cs b/WmiLight/Exceptions/TransportFailureException.cs index 1cfa9b7..9b57d82 100644 --- a/WmiLight/Exceptions/TransportFailureException.cs +++ b/WmiLight/Exceptions/TransportFailureException.cs @@ -2,7 +2,7 @@ { #region Description /// - /// The Exception for the case that the Username, password, or authority are specified for a local . + /// The Exception that is thrown when the WMI service returns WBEM_E_TRANSPORT_FAILURE. /// #endregion public class TransportFailureException : WmiException diff --git a/WmiLight/Internal/HResult.cs b/WmiLight/Internal/HResult.cs index 3e51168..3fd544a 100644 --- a/WmiLight/Internal/HResult.cs +++ b/WmiLight/Internal/HResult.cs @@ -190,7 +190,7 @@ public static implicit operator Exception(HResult hr) return new TransportFailureException(new HResultInfo(hr, "The remote procedure call (RPC) link between the current process and WMI failed.", WbemStatus.WBEM_E_TRANSPORT_FAILURE.ToString())); case (int)WbemStatus.WBEM_E_LOCAL_CREDENTIALS: - return new LocalCredentialsException(new HResultInfo(hr, "Username, password, or authority can only used on a remote connection.", WbemStatus.WBEM_E_LOCAL_CREDENTIALS.ToString())); + return new LocalCredentialsException(new HResultInfo(hr, "Username, password, or domain can only used on a remote connection.", WbemStatus.WBEM_E_LOCAL_CREDENTIALS.ToString())); case (int)WbemStatus.WBEM_E_FAILED: return new WmiException(new HResultInfo(hr, "An unspecified error occurred.", WbemStatus.WBEM_E_FAILED.ToString())); diff --git a/WmiLight/Internal/NativeMethods.cs b/WmiLight/Internal/NativeMethods.cs index aeb51b9..2286d89 100644 --- a/WmiLight/Internal/NativeMethods.cs +++ b/WmiLight/Internal/NativeMethods.cs @@ -86,7 +86,7 @@ public static extern HResult SetProxy( [MarshalAs(UnmanagedType.LPWStr)] string password, [MarshalAs(UnmanagedType.LPWStr)] - string authority, + string domain, ImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel); @@ -197,7 +197,7 @@ public static extern HResult SetProxy( [MarshalAs(UnmanagedType.LPWStr)] string password, [MarshalAs(UnmanagedType.LPWStr)] - string authority, + string domain, ImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel); @@ -314,14 +314,14 @@ public static HResult SetProxy( IntPtr pIUnknown, string username, string password, - string authority, + string domain, ImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel) { if (Environment.Is64BitProcess) - return x64.SetProxy(pIUnknown, username, password, authority, impersonationLevel, authenticationLevel); + return x64.SetProxy(pIUnknown, username, password, domain, impersonationLevel, authenticationLevel); else - return x86.SetProxy(pIUnknown, username, password, authority, impersonationLevel, authenticationLevel); + return x86.SetProxy(pIUnknown, username, password, domain, impersonationLevel, authenticationLevel); } public static HResult ExecQuery( IntPtr pWbemServices, diff --git a/WmiLight/Internal/NativeMethods.netcore.cs b/WmiLight/Internal/NativeMethods.netcore.cs index e4c7fd8..121bbe1 100644 --- a/WmiLight/Internal/NativeMethods.netcore.cs +++ b/WmiLight/Internal/NativeMethods.netcore.cs @@ -88,7 +88,7 @@ public static partial HResult SetProxy( [MarshalAs(UnmanagedType.LPWStr)] string password, [MarshalAs(UnmanagedType.LPWStr)] - string authority, + string domain, ImpersonationLevel impersonationLevel, AuthenticationLevel authenticationLevel); diff --git a/WmiLight/Internal/NormalizedCredential.cs b/WmiLight/Internal/NormalizedCredential.cs new file mode 100644 index 0000000..4be0cac --- /dev/null +++ b/WmiLight/Internal/NormalizedCredential.cs @@ -0,0 +1,67 @@ +namespace WmiLight +{ + using System; + using System.Diagnostics; + using System.Net; + + internal class NormalizedCredential + { + internal NormalizedCredential(NetworkCredential networkCredential) + { + if (networkCredential.UserName != null) + { + string[] usernameParts = networkCredential.UserName.Split('\\'); + + if (usernameParts.Length == 2) + { + this.UserNameWithoutDomain = usernameParts[1]; + this.UserNameWithDomain = networkCredential.UserName; + + if (string.IsNullOrEmpty(networkCredential.Domain)) + this.Domain = usernameParts[0]; + else + this.Domain = networkCredential.Domain; + } + else + { + usernameParts = networkCredential.UserName.Split('@'); + + if (usernameParts.Length == 2) + { + this.UserNameWithoutDomain = usernameParts[0]; + this.UserNameWithDomain = networkCredential.UserName; + + if (string.IsNullOrEmpty(networkCredential.Domain) || string.Equals(networkCredential.Domain, usernameParts[1], StringComparison.OrdinalIgnoreCase)) + this.Domain = usernameParts[1]; + else + this.Domain = networkCredential.Domain; + + } + else + { + this.UserNameWithoutDomain = networkCredential.UserName; + this.UserNameWithDomain = string.IsNullOrEmpty(networkCredential.Domain) ? networkCredential.UserName : $"{networkCredential.Domain}\\{networkCredential.UserName}"; + this.Domain = networkCredential.Domain; + } + } + } + else + { + this.UserNameWithoutDomain = null; + this.UserNameWithDomain = null; + this.Domain = networkCredential.Domain; + } + + this.Password = networkCredential.Password; + } + + internal string UserNameWithoutDomain { get; } + + internal string UserNameWithDomain { get; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal string Password { get; } + + internal string Domain { get; } + } +} diff --git a/WmiLight/Wbem/Enumerations/WbemStatus.cs b/WmiLight/Wbem/Enumerations/WbemStatus.cs index 4572275..59a5f6c 100644 --- a/WmiLight/Wbem/Enumerations/WbemStatus.cs +++ b/WmiLight/Wbem/Enumerations/WbemStatus.cs @@ -160,7 +160,7 @@ internal enum WbemStatus : int #region Description /// - /// The user specified a username, password, or authority on a + /// The user specified a username, password, or domain on a /// local connection. The user must use an empty user name and password and rely on /// default security. /// diff --git a/WmiLight/Wbem/WbemServices.cs b/WmiLight/Wbem/WbemServices.cs index 3354d54..fa66abf 100644 --- a/WmiLight/Wbem/WbemServices.cs +++ b/WmiLight/Wbem/WbemServices.cs @@ -26,12 +26,12 @@ internal void SetProxy(ImpersonationLevel impersonate, AuthenticationLevel authL throw (Exception)hResult; } - internal void SetProxy(string userName, string password, string authority, ImpersonationLevel impersonate, AuthenticationLevel authLevel) + internal void SetProxy(string userName, string password, string domain, ImpersonationLevel impersonate, AuthenticationLevel authLevel) { if (this.Disposed) throw new ObjectDisposedException(nameof(WbemServices)); - HResult hResult = NativeMethods.SetProxy(this, userName, password, authority, impersonate, authLevel); + HResult hResult = NativeMethods.SetProxy(this, userName, password, domain, impersonate, authLevel); if (hResult.Failed) throw (Exception)hResult; @@ -61,7 +61,7 @@ internal WbemClassObjectEnumerator ExecQuery(string query, WbemClassObjectEnumer return new WbemClassObjectEnumerator(pEnumerator); } - internal WbemClassObjectEnumerator ExecQuery(string query, WbemClassObjectEnumeratorBehaviorOption behaviorOption, IntPtr ctx, string userName, string password, string authority, AuthenticationLevel authLevel, ImpersonationLevel impersonate) + internal WbemClassObjectEnumerator ExecQuery(string query, WbemClassObjectEnumeratorBehaviorOption behaviorOption, IntPtr ctx, string userName, string password, string domain, AuthenticationLevel authLevel, ImpersonationLevel impersonate) { if (this.Disposed) throw new ObjectDisposedException(nameof(WbemServices)); @@ -82,7 +82,7 @@ internal WbemClassObjectEnumerator ExecQuery(string query, WbemClassObjectEnumer } } - hResult = NativeMethods.SetProxy(pEnumerator, userName, password, authority, impersonate, authLevel); + hResult = NativeMethods.SetProxy(pEnumerator, userName, password, domain, impersonate, authLevel); if (hResult.Failed) throw (Exception)hResult; diff --git a/WmiLight/WmiConnection.cs b/WmiLight/WmiConnection.cs index 8ec3655..5a49bd5 100644 --- a/WmiLight/WmiConnection.cs +++ b/WmiLight/WmiConnection.cs @@ -41,7 +41,7 @@ public partial class WmiConnection : IDisposable /// #endregion [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly NetworkCredential credential; + private readonly NormalizedCredential credential; #region Description /// @@ -163,7 +163,9 @@ public WmiConnection(string path) public WmiConnection(string path, WmiConnectionOptions options) { this.path = path; - this.options = options; + + if (options != null) + this.options = options; } #region Description @@ -182,7 +184,9 @@ public WmiConnection(string path, WmiConnectionOptions options) public WmiConnection(string path, NetworkCredential credential) { this.path = path; - this.credential = credential; + + if (credential != null) + this.credential = new NormalizedCredential(credential); } #region Description @@ -202,8 +206,12 @@ public WmiConnection(string path, NetworkCredential credential) public WmiConnection(string path, NetworkCredential credential, WmiConnectionOptions options) { this.path = path; - this.credential = credential; - this.options = options; + + if (credential != null) + this.credential = new NormalizedCredential(credential); + + if (options != null) + this.options = options; } #endregion @@ -229,34 +237,6 @@ public bool IsConnected } } - #region Description - /// - /// Gets the Authority for a remote connection. - /// - #endregion - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string Authority - { - get - { - if (this.credential != null && !string.IsNullOrWhiteSpace(this.credential.Domain)) - { - if (this.options.AuthenticationProtocol == AuthenticationProtocol.Kerberos) - { - return string.Format("Kerberos: {0}", this.credential.Domain); - } - else - { - return string.Format("NTLMDOMAIN: {0}", this.credential.Domain); - } - } - else - { - return null; - } - } - } - #region Description /// /// Gets a value indicating whether the configuration is remote. @@ -301,8 +281,20 @@ public void Open() { try { - this.wbemServices = locator.ConnectServer(this.path, this.credential.UserName, this.credential.Password, null, WbemConnectOption.None, this.Authority, this.context); - this.wbemServices.SetProxy(this.credential.UserName, this.credential.Password, this.Authority, ImpersonationLevel.Impersonate, authLevel); + // Authority is always null. Therefore, the operating system negotiates with COM to determine whether NTLM or Kerberos authentication is used. + // + // Until version 7.0.0, I attempted to let the user decide whether NTLM or Kerberos should be used. + // When NTLM was chosen, the Authority was set to "NTLMDomain:" to enforce NTLM usage. This worked well for NTLM. + // Unfortunately, with "Kerberos:", it always resulted in the following DCOM error, regardless of how the domain was configured. + // + // DCOM was unable to communicate with the computer COMPUTER.DOMAIN using any of the configured protocols; + // requested by PID 1dd8 (EXE PATH), while activating CLSID {8BC3F05E-D86B-11D0-A075-00C04FB68820}. + // + // To minimize support requests and frustration for the end user, I now rely on Windows to negotiate the correct protocol automatically. + + this.wbemServices = locator.ConnectServer(this.path, this.credential.UserNameWithDomain, this.credential.Password, null, WbemConnectOption.None, null, this.context); + + this.wbemServices.SetProxy(this.credential.UserNameWithoutDomain, this.credential.Password, this.credential.Domain, ImpersonationLevel.Impersonate, authLevel); } catch (LocalCredentialsException) { @@ -612,6 +604,9 @@ internal WmiEventSubscription ExecNotificationQueryAsync(string query, Action @@ -8,20 +10,21 @@ public class WmiConnectionOptions { #region Properties - + #region Description /// /// Gets or sets the mode of authentication. /// #endregion + [Obsolete("Manual selection of AuthenticationProtocol is no longer supported. The operating system now automatically determines whether NTLM or Kerberos authentication is used.")] public AuthenticationProtocol AuthenticationProtocol { get; set; } - + #region Description /// /// Gets or sets a value indicating whether the packages will be encrypted. /// #endregion - public bool EnablePackageEncryption { get; set; } + public bool EnablePackageEncryption { get; set; } #endregion } diff --git a/WmiLight/WmiLight.csproj b/WmiLight/WmiLight.csproj index 9e012b5..c99c40c 100644 --- a/WmiLight/WmiLight.csproj +++ b/WmiLight/WmiLight.csproj @@ -23,7 +23,7 @@ - 7.0.0 + 7.1.0 WmiLight Martin Kuschnik Martin Kuschnik @@ -42,7 +42,7 @@ <_NeedToDownloadMicrosoftNetSdkCompilersToolsetPackage>true - 7.0.0 + 7.1.0 True