| | 1 | | using System; |
| | 2 | | using System.Collections.Concurrent; |
| | 3 | | using System.Collections.Generic; |
| | 4 | | using System.DirectoryServices.ActiveDirectory; |
| | 5 | | using System.DirectoryServices.Protocols; |
| | 6 | | using System.Linq; |
| | 7 | | using System.Net; |
| | 8 | | using System.Runtime.CompilerServices; |
| | 9 | | using System.Threading; |
| | 10 | | using System.Threading.Tasks; |
| | 11 | | using Microsoft.Extensions.Logging; |
| | 12 | | using SharpHoundCommonLib.Enums; |
| | 13 | | using SharpHoundCommonLib.Exceptions; |
| | 14 | | using SharpHoundCommonLib.LDAPQueries; |
| | 15 | | using SharpHoundCommonLib.Processors; |
| | 16 | | using SharpHoundRPC.NetAPINative; |
| | 17 | |
|
| | 18 | | namespace SharpHoundCommonLib { |
| | 19 | | internal class LdapConnectionPool : IDisposable{ |
| | 20 | | private readonly ConcurrentBag<LdapConnectionWrapper> _connections; |
| | 21 | | private readonly ConcurrentBag<LdapConnectionWrapper> _globalCatalogConnection; |
| | 22 | | private readonly SemaphoreSlim _semaphore; |
| | 23 | | private readonly string _identifier; |
| | 24 | | private readonly string _poolIdentifier; |
| | 25 | | private readonly LdapConfig _ldapConfig; |
| | 26 | | private readonly ILogger _log; |
| | 27 | | private readonly PortScanner _portScanner; |
| | 28 | | private readonly NativeMethods _nativeMethods; |
| 0 | 29 | | private static readonly TimeSpan MinBackoffDelay = TimeSpan.FromSeconds(2); |
| 0 | 30 | | private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(20); |
| | 31 | | private const int BackoffDelayMultiplier = 2; |
| | 32 | | private const int MaxRetries = 3; |
| 0 | 33 | | private static readonly ConcurrentDictionary<string, NetAPIStructs.DomainControllerInfo?> DCInfoCache = new(); |
| | 34 | |
|
| 2 | 35 | | public LdapConnectionPool(string identifier, string poolIdentifier, LdapConfig config, PortScanner scanner = nul |
| 1 | 36 | | _connections = new ConcurrentBag<LdapConnectionWrapper>(); |
| 1 | 37 | | _globalCatalogConnection = new ConcurrentBag<LdapConnectionWrapper>(); |
| | 38 | | //TODO: Re-enable this once we track down the semaphore deadlock |
| | 39 | | // if (config.MaxConcurrentQueries > 0) { |
| | 40 | | // _semaphore = new SemaphoreSlim(config.MaxConcurrentQueries, config.MaxConcurrentQueries); |
| | 41 | | // } else { |
| | 42 | | // //If MaxConcurrentQueries is 0, we'll just disable the semaphore entirely |
| | 43 | | // _semaphore = null; |
| | 44 | | // } |
| | 45 | |
|
| 1 | 46 | | _identifier = identifier; |
| 1 | 47 | | _poolIdentifier = poolIdentifier; |
| 1 | 48 | | _ldapConfig = config; |
| 1 | 49 | | _log = log ?? Logging.LogProvider.CreateLogger("LdapConnectionPool"); |
| 1 | 50 | | _portScanner = scanner ?? new PortScanner(); |
| 1 | 51 | | _nativeMethods = nativeMethods ?? new NativeMethods(); |
| 1 | 52 | | } |
| | 53 | |
|
| 1 | 54 | | private async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetLdapConnection(bo |
| 2 | 55 | | if (globalCatalog) { |
| 1 | 56 | | return await GetGlobalCatalogConnectionAsync(); |
| | 57 | | } |
| 0 | 58 | | return await GetConnectionAsync(); |
| 1 | 59 | | } |
| | 60 | |
|
| | 61 | | public async IAsyncEnumerable<LdapResult<IDirectoryObject>> Query(LdapQueryParameters queryParameters, |
| 1 | 62 | | [EnumeratorCancellation] CancellationToken cancellationToken = new()) { |
| 1 | 63 | | var setupResult = await SetupLdapQuery(queryParameters); |
| | 64 | |
|
| 2 | 65 | | if (!setupResult.Success) { |
| 1 | 66 | | _log.LogInformation("Query - Failure during query setup: {Reason}\n{Info}", setupResult.Message, |
| 1 | 67 | | queryParameters.GetQueryInfo()); |
| 1 | 68 | | yield break; |
| | 69 | | } |
| | 70 | |
|
| 0 | 71 | | var searchRequest = setupResult.SearchRequest; |
| 0 | 72 | | var connectionWrapper = setupResult.ConnectionWrapper; |
| | 73 | |
|
| 0 | 74 | | if (cancellationToken.IsCancellationRequested) { |
| 0 | 75 | | ReleaseConnection(connectionWrapper); |
| 0 | 76 | | yield break; |
| | 77 | | } |
| | 78 | |
|
| 0 | 79 | | var queryRetryCount = 0; |
| 0 | 80 | | var busyRetryCount = 0; |
| 0 | 81 | | LdapResult<IDirectoryObject> tempResult = null; |
| 0 | 82 | | var querySuccess = false; |
| 0 | 83 | | SearchResponse response = null; |
| 0 | 84 | | while (!cancellationToken.IsCancellationRequested) { |
| | 85 | | //Grab our semaphore here to take one of our query slots |
| 0 | 86 | | if (_semaphore != null){ |
| 0 | 87 | | _log.LogTrace("Query entering semaphore with {Count} remaining for query {Info}", _semaphore.Current |
| 0 | 88 | | await _semaphore.WaitAsync(cancellationToken); |
| 0 | 89 | | _log.LogTrace("Query entered semaphore with {Count} remaining for query {Info}", _semaphore.CurrentC |
| 0 | 90 | | } |
| 0 | 91 | | try { |
| 0 | 92 | | _log.LogTrace("Sending ldap request - {Info}", queryParameters.GetQueryInfo()); |
| 0 | 93 | | response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); |
| | 94 | |
|
| 0 | 95 | | if (response != null) { |
| 0 | 96 | | querySuccess = true; |
| 0 | 97 | | } else if (queryRetryCount == MaxRetries) { |
| 0 | 98 | | tempResult = |
| 0 | 99 | | LdapResult<IDirectoryObject>.Fail($"Failed to get a response after {MaxRetries} attempts", |
| 0 | 100 | | queryParameters); |
| 0 | 101 | | } else { |
| 0 | 102 | | queryRetryCount++; |
| 0 | 103 | | } |
| 0 | 104 | | } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown && |
| 0 | 105 | | queryRetryCount < MaxRetries) { |
| | 106 | | /* |
| | 107 | | * A ServerDown exception indicates that our connection is no longer valid for one of many reasons. |
| | 108 | | * We'll want to release our connection back to the pool, but dispose it. We need a new connection, |
| | 109 | | * and because this is not a paged query, we can get this connection from anywhere. |
| | 110 | | * |
| | 111 | | * We use queryRetryCount here to prevent an infinite retry loop from occurring |
| | 112 | | * |
| | 113 | | * Release our connection in a faulted state since the connection is defunct. Attempt to get a new c |
| | 114 | | * since non-paged queries do not require same server connections |
| | 115 | | */ |
| 0 | 116 | | queryRetryCount++; |
| 0 | 117 | | _log.LogDebug("Query - Attempting to recover from ServerDown for query {Info} (Attempt {Count})", qu |
| 0 | 118 | | ReleaseConnection(connectionWrapper, true); |
| | 119 | |
|
| 0 | 120 | | for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { |
| 0 | 121 | | var backoffDelay = GetNextBackoff(retryCount); |
| 0 | 122 | | await Task.Delay(backoffDelay, cancellationToken); |
| 0 | 123 | | var (success, newConnectionWrapper, _) = |
| 0 | 124 | | await GetLdapConnection(queryParameters.GlobalCatalog); |
| 0 | 125 | | if (success) { |
| 0 | 126 | | _log.LogDebug( |
| 0 | 127 | | "Query - Recovered from ServerDown successfully, connection made to {NewServer}", |
| 0 | 128 | | newConnectionWrapper.GetServer()); |
| 0 | 129 | | connectionWrapper = newConnectionWrapper; |
| 0 | 130 | | break; |
| | 131 | | } |
| | 132 | |
|
| | 133 | | //If we hit our max retries for making a new connection, set tempResult so we can yield it after |
| 0 | 134 | | if (retryCount == MaxRetries - 1) { |
| 0 | 135 | | _log.LogError("Query - Failed to get a new connection after ServerDown.\n{Info}", |
| 0 | 136 | | queryParameters.GetQueryInfo()); |
| 0 | 137 | | tempResult = |
| 0 | 138 | | LdapResult<IDirectoryObject>.Fail( |
| 0 | 139 | | "Query - Failed to get a new connection after ServerDown.", queryParameters); |
| 0 | 140 | | } |
| 0 | 141 | | } |
| 0 | 142 | | } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { |
| | 143 | | /* |
| | 144 | | * If we get a busy error, we want to do an exponential backoff, but maintain the current connection |
| | 145 | | * The expectation is that given enough time, the server should stop being busy and service our quer |
| | 146 | | */ |
| 0 | 147 | | busyRetryCount++; |
| 0 | 148 | | _log.LogDebug("Query - Executing busy backoff for query {Info} (Attempt {Count})", queryParameters.G |
| 0 | 149 | | var backoffDelay = GetNextBackoff(busyRetryCount); |
| 0 | 150 | | await Task.Delay(backoffDelay, cancellationToken); |
| 0 | 151 | | } catch (LdapException le) { |
| | 152 | | /* |
| | 153 | | * This is our fallback catch. If our retry counts have been exhausted this will trigger and break u |
| | 154 | | */ |
| 0 | 155 | | tempResult = LdapResult<IDirectoryObject>.Fail( |
| 0 | 156 | | $"Query - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessa |
| 0 | 157 | | queryParameters); |
| 0 | 158 | | } catch (Exception e) { |
| | 159 | | /* |
| | 160 | | * Generic exception handling for unforeseen circumstances |
| | 161 | | */ |
| 0 | 162 | | tempResult = |
| 0 | 163 | | LdapResult<IDirectoryObject>.Fail($"Query - Caught unrecoverable exception: {e.Message}", |
| 0 | 164 | | queryParameters); |
| 0 | 165 | | } finally { |
| | 166 | | // Always release our semaphore to prevent deadlocks |
| 0 | 167 | | if (_semaphore != null) { |
| 0 | 168 | | _log.LogTrace("Query releasing semaphore with {Count} remaining for query {Info}", _semaphore.Cu |
| 0 | 169 | | _semaphore.Release(); |
| 0 | 170 | | _log.LogTrace("Query released semaphore with {Count} remaining for query {Info}", _semaphore.Cur |
| 0 | 171 | | } |
| 0 | 172 | | } |
| | 173 | |
|
| | 174 | | //If we have a tempResult set it means we hit an error we couldn't recover from, so yield that result an |
| 0 | 175 | | if (tempResult != null) { |
| 0 | 176 | | if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) { |
| 0 | 177 | | ReleaseConnection(connectionWrapper, true); |
| 0 | 178 | | } else { |
| 0 | 179 | | ReleaseConnection(connectionWrapper); |
| 0 | 180 | | } |
| | 181 | |
|
| 0 | 182 | | yield return tempResult; |
| 0 | 183 | | yield break; |
| | 184 | | } |
| | 185 | |
|
| | 186 | | //If we've successfully made our query, break out of the while loop |
| 0 | 187 | | if (querySuccess) { |
| 0 | 188 | | break; |
| | 189 | | } |
| 0 | 190 | | } |
| | 191 | |
|
| 0 | 192 | | ReleaseConnection(connectionWrapper); |
| 0 | 193 | | foreach (SearchResultEntry entry in response.Entries) { |
| 0 | 194 | | yield return LdapResult<IDirectoryObject>.Ok(new SearchResultEntryWrapper(entry)); |
| 0 | 195 | | } |
| 1 | 196 | | } |
| | 197 | |
|
| | 198 | | public async IAsyncEnumerable<LdapResult<IDirectoryObject>> PagedQuery(LdapQueryParameters queryParameters, |
| 0 | 199 | | [EnumeratorCancellation] CancellationToken cancellationToken = new()) { |
| 0 | 200 | | var setupResult = await SetupLdapQuery(queryParameters); |
| | 201 | |
|
| 0 | 202 | | if (!setupResult.Success) { |
| 0 | 203 | | _log.LogInformation("PagedQuery - Failure during query setup: {Reason}\n{Info}", setupResult.Message, |
| 0 | 204 | | queryParameters.GetQueryInfo()); |
| 0 | 205 | | yield break; |
| | 206 | | } |
| | 207 | |
|
| 0 | 208 | | var searchRequest = setupResult.SearchRequest; |
| 0 | 209 | | var connectionWrapper = setupResult.ConnectionWrapper; |
| 0 | 210 | | var serverName = setupResult.Server; |
| | 211 | |
|
| 0 | 212 | | if (serverName == null) { |
| 0 | 213 | | _log.LogWarning("PagedQuery - Failed to get a server name for connection, retry not possible"); |
| 0 | 214 | | } |
| | 215 | |
|
| 0 | 216 | | var pageControl = new PageResultRequestControl(500); |
| 0 | 217 | | searchRequest.Controls.Add(pageControl); |
| | 218 | |
|
| 0 | 219 | | PageResultResponseControl pageResponse = null; |
| 0 | 220 | | var busyRetryCount = 0; |
| 0 | 221 | | var queryRetryCount = 0; |
| 0 | 222 | | LdapResult<IDirectoryObject> tempResult = null; |
| | 223 | |
|
| 0 | 224 | | while (!cancellationToken.IsCancellationRequested) { |
| 0 | 225 | | if (_semaphore != null){ |
| 0 | 226 | | _log.LogTrace("PagedQuery entering semaphore with {Count} remaining for query {Info}", _semaphore.Cu |
| 0 | 227 | | await _semaphore.WaitAsync(cancellationToken); |
| 0 | 228 | | _log.LogTrace("PagedQuery entered semaphore with {Count} remaining for query {Info}", _semaphore.Cur |
| 0 | 229 | | } |
| 0 | 230 | | SearchResponse response = null; |
| 0 | 231 | | try { |
| 0 | 232 | | _log.LogTrace("Sending paged ldap request - {Info}", queryParameters.GetQueryInfo()); |
| 0 | 233 | | response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); |
| 0 | 234 | | if (response != null) { |
| 0 | 235 | | pageResponse = (PageResultResponseControl)response.Controls |
| 0 | 236 | | .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault(); |
| 0 | 237 | | queryRetryCount = 0; |
| 0 | 238 | | } else if (queryRetryCount == MaxRetries) { |
| 0 | 239 | | tempResult = LdapResult<IDirectoryObject>.Fail( |
| 0 | 240 | | $"PagedQuery - Failed to get a response after {MaxRetries} attempts", |
| 0 | 241 | | queryParameters); |
| 0 | 242 | | } else { |
| 0 | 243 | | queryRetryCount++; |
| 0 | 244 | | } |
| 0 | 245 | | } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown) { |
| | 246 | | /* |
| | 247 | | * A ServerDown exception indicates that our connection is no longer valid for one of many reasons. |
| | 248 | | * We'll want to release our connection back to the pool, but dispose it. We need a new connection, |
| | 249 | | * and because this is not a paged query, we can get this connection from anywhere. |
| | 250 | | * |
| | 251 | | * We use queryRetryCount here to prevent an infinite retry loop from occurring |
| | 252 | | * |
| | 253 | | * Release our connection in a faulted state since the connection is defunct. |
| | 254 | | * Paged queries require a connection to be made to the same server which we started the paged query |
| | 255 | | */ |
| 0 | 256 | | if (serverName == null) { |
| 0 | 257 | | _log.LogError( |
| 0 | 258 | | "PagedQuery - Received server down exception without a known servername. Unable to generate |
| 0 | 259 | | queryParameters.GetQueryInfo()); |
| 0 | 260 | | ReleaseConnection(connectionWrapper, true); |
| 0 | 261 | | yield break; |
| | 262 | | } |
| | 263 | |
|
| 0 | 264 | | _log.LogDebug("PagedQuery - Attempting to recover from ServerDown for query {Info} (Attempt {Count}) |
| | 265 | |
|
| 0 | 266 | | ReleaseConnection(connectionWrapper, true); |
| 0 | 267 | | for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { |
| 0 | 268 | | var backoffDelay = GetNextBackoff(retryCount); |
| 0 | 269 | | await Task.Delay(backoffDelay, cancellationToken); |
| 0 | 270 | | var (success, ldapConnectionWrapperNew, _) = |
| 0 | 271 | | GetConnectionForSpecificServerAsync(serverName, queryParameters.GlobalCatalog); |
| | 272 | |
|
| 0 | 273 | | if (success) { |
| 0 | 274 | | _log.LogDebug("PagedQuery - Recovered from ServerDown successfully"); |
| 0 | 275 | | connectionWrapper = ldapConnectionWrapperNew; |
| 0 | 276 | | break; |
| | 277 | | } |
| | 278 | |
|
| 0 | 279 | | if (retryCount == MaxRetries - 1) { |
| 0 | 280 | | _log.LogError("PagedQuery - Failed to get a new connection after ServerDown.\n{Info}", |
| 0 | 281 | | queryParameters.GetQueryInfo()); |
| 0 | 282 | | tempResult = |
| 0 | 283 | | LdapResult<IDirectoryObject>.Fail("Failed to get a new connection after serverdown", |
| 0 | 284 | | queryParameters, le.ErrorCode); |
| 0 | 285 | | } |
| 0 | 286 | | } |
| 0 | 287 | | } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { |
| | 288 | | /* |
| | 289 | | * If we get a busy error, we want to do an exponential backoff, but maintain the current connection |
| | 290 | | * The expectation is that given enough time, the server should stop being busy and service our quer |
| | 291 | | */ |
| 0 | 292 | | busyRetryCount++; |
| 0 | 293 | | _log.LogDebug("PagedQuery - Executing busy backoff for query {Info} (Attempt {Count})", queryParamet |
| 0 | 294 | | var backoffDelay = GetNextBackoff(busyRetryCount); |
| 0 | 295 | | await Task.Delay(backoffDelay, cancellationToken); |
| 0 | 296 | | } catch (LdapException le) { |
| 0 | 297 | | tempResult = LdapResult<IDirectoryObject>.Fail( |
| 0 | 298 | | $"PagedQuery - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerError |
| 0 | 299 | | queryParameters, le.ErrorCode); |
| 0 | 300 | | } catch (Exception e) { |
| 0 | 301 | | tempResult = |
| 0 | 302 | | LdapResult<IDirectoryObject>.Fail($"PagedQuery - Caught unrecoverable exception: {e.Message}", |
| 0 | 303 | | queryParameters); |
| 0 | 304 | | } finally { |
| 0 | 305 | | if (_semaphore != null) { |
| 0 | 306 | | _log.LogTrace("PagedQuery releasing semaphore with {Count} remaining for query {Info}", _semapho |
| 0 | 307 | | _semaphore.Release(); |
| 0 | 308 | | _log.LogTrace("PagedQuery released semaphore with {Count} remaining for query {Info}", _semaphor |
| 0 | 309 | | } |
| 0 | 310 | | } |
| | 311 | |
|
| 0 | 312 | | if (tempResult != null) { |
| 0 | 313 | | if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) { |
| 0 | 314 | | ReleaseConnection(connectionWrapper, true); |
| 0 | 315 | | } else { |
| 0 | 316 | | ReleaseConnection(connectionWrapper); |
| 0 | 317 | | } |
| | 318 | |
|
| 0 | 319 | | yield return tempResult; |
| 0 | 320 | | yield break; |
| | 321 | | } |
| | 322 | |
|
| 0 | 323 | | if (cancellationToken.IsCancellationRequested) { |
| 0 | 324 | | ReleaseConnection(connectionWrapper); |
| 0 | 325 | | yield break; |
| | 326 | | } |
| | 327 | |
|
| | 328 | | //I'm not sure why this happens sometimes, but if we try the request again, it works sometimes, other ti |
| 0 | 329 | | if (response == null || pageResponse == null) { |
| 0 | 330 | | continue; |
| | 331 | | } |
| | 332 | |
|
| 0 | 333 | | foreach (SearchResultEntry entry in response.Entries) { |
| 0 | 334 | | if (cancellationToken.IsCancellationRequested) { |
| 0 | 335 | | ReleaseConnection(connectionWrapper); |
| 0 | 336 | | yield break; |
| | 337 | | } |
| | 338 | |
|
| 0 | 339 | | yield return LdapResult<IDirectoryObject>.Ok(new SearchResultEntryWrapper(entry)); |
| 0 | 340 | | } |
| | 341 | |
|
| 0 | 342 | | if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 || |
| 0 | 343 | | cancellationToken.IsCancellationRequested) { |
| 0 | 344 | | ReleaseConnection(connectionWrapper); |
| 0 | 345 | | yield break; |
| | 346 | | } |
| | 347 | |
|
| 0 | 348 | | pageControl.Cookie = pageResponse.Cookie; |
| 0 | 349 | | } |
| 0 | 350 | | } |
| | 351 | |
|
| 1 | 352 | | private async Task<LdapQuerySetupResult> SetupLdapQuery(LdapQueryParameters queryParameters) { |
| 1 | 353 | | var result = new LdapQuerySetupResult(); |
| 1 | 354 | | var (success, connectionWrapper, message) = |
| 1 | 355 | | await GetLdapConnection(queryParameters.GlobalCatalog); |
| 2 | 356 | | if (!success) { |
| 1 | 357 | | result.Success = false; |
| 1 | 358 | | result.Message = $"Unable to create a connection: {message}"; |
| 1 | 359 | | return result; |
| | 360 | | } |
| | 361 | |
|
| | 362 | | //This should never happen as far as I know, so just checking for safety |
| 0 | 363 | | if (connectionWrapper.Connection == null) { |
| 0 | 364 | | result.Success = false; |
| 0 | 365 | | result.Message = "Connection object is null"; |
| 0 | 366 | | return result; |
| | 367 | | } |
| | 368 | |
|
| 0 | 369 | | if (!CreateSearchRequest(queryParameters, connectionWrapper, out var searchRequest)) { |
| 0 | 370 | | result.Success = false; |
| 0 | 371 | | result.Message = "Failed to create search request"; |
| 0 | 372 | | ReleaseConnection(connectionWrapper); |
| 0 | 373 | | return result; |
| | 374 | | } |
| | 375 | |
|
| 0 | 376 | | result.Server = connectionWrapper.GetServer(); |
| 0 | 377 | | result.Success = true; |
| 0 | 378 | | result.SearchRequest = searchRequest; |
| 0 | 379 | | result.ConnectionWrapper = connectionWrapper; |
| 0 | 380 | | return result; |
| 1 | 381 | | } |
| | 382 | |
|
| | 383 | | public async IAsyncEnumerable<Result<string>> RangedRetrieval(string distinguishedName, |
| 0 | 384 | | string attributeName, [EnumeratorCancellation] CancellationToken cancellationToken = new()) { |
| 0 | 385 | | var domain = Helpers.DistinguishedNameToDomain(distinguishedName); |
| | 386 | |
|
| 0 | 387 | | var connectionResult = await GetConnectionAsync(); |
| 0 | 388 | | if (!connectionResult.Success) { |
| 0 | 389 | | yield return Result<string>.Fail(connectionResult.Message); |
| 0 | 390 | | yield break; |
| | 391 | | } |
| | 392 | |
|
| 0 | 393 | | var index = 0; |
| 0 | 394 | | var step = 0; |
| | 395 | |
|
| | 396 | | //Start by using * as our upper index, which will automatically give us the range size |
| 0 | 397 | | var currentRange = $"{attributeName};range={index}-*"; |
| 0 | 398 | | var complete = false; |
| | 399 | |
|
| 0 | 400 | | var queryParameters = new LdapQueryParameters { |
| 0 | 401 | | DomainName = domain, |
| 0 | 402 | | LDAPFilter = $"{attributeName}=*", |
| 0 | 403 | | Attributes = new[] { currentRange }, |
| 0 | 404 | | SearchScope = SearchScope.Base, |
| 0 | 405 | | SearchBase = distinguishedName |
| 0 | 406 | | }; |
| 0 | 407 | | var connectionWrapper = connectionResult.ConnectionWrapper; |
| | 408 | |
|
| 0 | 409 | | if (!CreateSearchRequest(queryParameters, connectionWrapper, out var searchRequest)) { |
| 0 | 410 | | ReleaseConnection(connectionWrapper); |
| 0 | 411 | | yield return Result<string>.Fail("Failed to create search request"); |
| 0 | 412 | | yield break; |
| | 413 | | } |
| | 414 | |
|
| 0 | 415 | | var queryRetryCount = 0; |
| 0 | 416 | | var busyRetryCount = 0; |
| | 417 | |
|
| 0 | 418 | | LdapResult<string> tempResult = null; |
| | 419 | |
|
| 0 | 420 | | while (!cancellationToken.IsCancellationRequested) { |
| 0 | 421 | | SearchResponse response = null; |
| 0 | 422 | | if (_semaphore != null){ |
| 0 | 423 | | _log.LogTrace("RangedRetrieval entering semaphore with {Count} remaining for query {Info}", _semapho |
| 0 | 424 | | await _semaphore.WaitAsync(cancellationToken); |
| 0 | 425 | | _log.LogTrace("RangedRetrieval entered semaphore with {Count} remaining for query {Info}", _semaphor |
| 0 | 426 | | } |
| 0 | 427 | | try { |
| 0 | 428 | | response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); |
| 0 | 429 | | } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { |
| 0 | 430 | | busyRetryCount++; |
| 0 | 431 | | _log.LogDebug("RangedRetrieval - Executing busy backoff for query {Info} (Attempt {Count})", queryPa |
| 0 | 432 | | var backoffDelay = GetNextBackoff(busyRetryCount); |
| 0 | 433 | | await Task.Delay(backoffDelay, cancellationToken); |
| 0 | 434 | | } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown && |
| 0 | 435 | | queryRetryCount < MaxRetries) { |
| 0 | 436 | | queryRetryCount++; |
| 0 | 437 | | _log.LogDebug("RangedRetrieval - Attempting to recover from ServerDown for query {Info} (Attempt {Co |
| 0 | 438 | | ReleaseConnection(connectionWrapper, true); |
| 0 | 439 | | for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { |
| 0 | 440 | | var backoffDelay = GetNextBackoff(retryCount); |
| 0 | 441 | | await Task.Delay(backoffDelay, cancellationToken); |
| 0 | 442 | | var (success, newConnectionWrapper, message) = |
| 0 | 443 | | await GetLdapConnection(false); |
| 0 | 444 | | if (success) { |
| 0 | 445 | | _log.LogDebug( |
| 0 | 446 | | "RangedRetrieval - Recovered from ServerDown successfully, connection made to {NewServer |
| 0 | 447 | | newConnectionWrapper.GetServer()); |
| 0 | 448 | | connectionWrapper = newConnectionWrapper; |
| 0 | 449 | | break; |
| | 450 | | } |
| | 451 | |
|
| | 452 | | //If we hit our max retries for making a new connection, set tempResult so we can yield it after |
| 0 | 453 | | if (retryCount == MaxRetries - 1) { |
| 0 | 454 | | _log.LogError( |
| 0 | 455 | | "RangedRetrieval - Failed to get a new connection after ServerDown for path {Path}", |
| 0 | 456 | | distinguishedName); |
| 0 | 457 | | tempResult = |
| 0 | 458 | | LdapResult<string>.Fail( |
| 0 | 459 | | "RangedRetrieval - Failed to get a new connection after ServerDown.", |
| 0 | 460 | | queryParameters, le.ErrorCode); |
| 0 | 461 | | } |
| 0 | 462 | | } |
| 0 | 463 | | } catch (LdapException le) { |
| 0 | 464 | | tempResult = LdapResult<string>.Fail( |
| 0 | 465 | | $"Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (Er |
| 0 | 466 | | queryParameters, le.ErrorCode); |
| 0 | 467 | | } catch (Exception e) { |
| 0 | 468 | | tempResult = |
| 0 | 469 | | LdapResult<string>.Fail($"Caught unrecoverable exception: {e.Message}", queryParameters); |
| 0 | 470 | | } finally { |
| 0 | 471 | | if (_semaphore != null) { |
| 0 | 472 | | _log.LogTrace("RangedRetrieval releasing semaphore with {Count} remaining for query {Info}", _se |
| 0 | 473 | | _semaphore.Release(); |
| 0 | 474 | | _log.LogTrace("RangedRetrieval released semaphore with {Count} remaining for query {Info}", _sem |
| 0 | 475 | | } |
| 0 | 476 | | } |
| | 477 | |
|
| | 478 | | //If we have a tempResult set it means we hit an error we couldn't recover from, so yield that result an |
| | 479 | | //We handle connection release in the relevant exception blocks |
| 0 | 480 | | if (tempResult != null) { |
| 0 | 481 | | if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) { |
| 0 | 482 | | ReleaseConnection(connectionWrapper, true); |
| 0 | 483 | | } else { |
| 0 | 484 | | ReleaseConnection(connectionWrapper); |
| 0 | 485 | | } |
| | 486 | |
|
| 0 | 487 | | yield return tempResult; |
| 0 | 488 | | yield break; |
| | 489 | | } |
| | 490 | |
|
| 0 | 491 | | if (response?.Entries.Count == 1) { |
| 0 | 492 | | var entry = response.Entries[0]; |
| | 493 | | //We dont know the name of our attribute, but there should only be one, so we're safe to just use a |
| 0 | 494 | | foreach (string attr in entry.Attributes.AttributeNames) { |
| 0 | 495 | | currentRange = attr; |
| 0 | 496 | | complete = currentRange.IndexOf("*", 0, StringComparison.OrdinalIgnoreCase) > 0; |
| 0 | 497 | | step = entry.Attributes[currentRange].Count; |
| 0 | 498 | | } |
| | 499 | |
|
| | 500 | | //Release our connection before we iterate |
| 0 | 501 | | if (complete) { |
| 0 | 502 | | ReleaseConnection(connectionWrapper); |
| 0 | 503 | | } |
| | 504 | |
|
| 0 | 505 | | foreach (string dn in entry.Attributes[currentRange].GetValues(typeof(string))) { |
| 0 | 506 | | yield return Result<string>.Ok(dn); |
| 0 | 507 | | index++; |
| 0 | 508 | | } |
| | 509 | |
|
| 0 | 510 | | if (complete) { |
| 0 | 511 | | yield break; |
| | 512 | | } |
| | 513 | |
|
| 0 | 514 | | currentRange = $"{attributeName};range={index}-{index + step}"; |
| 0 | 515 | | searchRequest.Attributes.Clear(); |
| 0 | 516 | | searchRequest.Attributes.Add(currentRange); |
| 0 | 517 | | } else { |
| | 518 | | //I dont know what can cause a RR to have multiple entries, but its nothing good. Break out |
| 0 | 519 | | ReleaseConnection(connectionWrapper); |
| 0 | 520 | | yield break; |
| | 521 | | } |
| 0 | 522 | | } |
| | 523 | |
|
| 0 | 524 | | ReleaseConnection(connectionWrapper); |
| 0 | 525 | | } |
| | 526 | |
|
| 0 | 527 | | private static TimeSpan GetNextBackoff(int retryCount) { |
| 0 | 528 | | return TimeSpan.FromSeconds(Math.Min( |
| 0 | 529 | | MinBackoffDelay.TotalSeconds * Math.Pow(BackoffDelayMultiplier, retryCount), |
| 0 | 530 | | MaxBackoffDelay.TotalSeconds)); |
| 0 | 531 | | } |
| | 532 | |
|
| | 533 | | private bool CreateSearchRequest(LdapQueryParameters queryParameters, |
| 0 | 534 | | LdapConnectionWrapper connectionWrapper, out SearchRequest searchRequest) { |
| | 535 | | string basePath; |
| 0 | 536 | | if (!string.IsNullOrWhiteSpace(queryParameters.SearchBase)) { |
| 0 | 537 | | basePath = queryParameters.SearchBase; |
| 0 | 538 | | } else if (!connectionWrapper.GetSearchBase(queryParameters.NamingContext, out basePath)) { |
| | 539 | | string tempPath; |
| 0 | 540 | | if (CallDsGetDcName(queryParameters.DomainName, out var info) && info != null) { |
| 0 | 541 | | tempPath = Helpers.DomainNameToDistinguishedName(info.Value.DomainName); |
| 0 | 542 | | connectionWrapper.SaveContext(queryParameters.NamingContext, basePath); |
| 0 | 543 | | } else if (LdapUtils.GetDomain(queryParameters.DomainName,_ldapConfig, out var domainObject)) { |
| 0 | 544 | | tempPath = Helpers.DomainNameToDistinguishedName(domainObject.Name); |
| 0 | 545 | | } else { |
| 0 | 546 | | searchRequest = null; |
| 0 | 547 | | return false; |
| | 548 | | } |
| | 549 | |
|
| 0 | 550 | | basePath = queryParameters.NamingContext switch { |
| 0 | 551 | | NamingContext.Configuration => $"CN=Configuration,{tempPath}", |
| 0 | 552 | | NamingContext.Schema => $"CN=Schema,CN=Configuration,{tempPath}", |
| 0 | 553 | | NamingContext.Default => tempPath, |
| 0 | 554 | | _ => throw new ArgumentOutOfRangeException() |
| 0 | 555 | | }; |
| | 556 | |
|
| 0 | 557 | | connectionWrapper.SaveContext(queryParameters.NamingContext, basePath); |
| 0 | 558 | | } |
| | 559 | |
|
| 0 | 560 | | if (string.IsNullOrWhiteSpace(queryParameters.SearchBase) && !string.IsNullOrWhiteSpace(queryParameters.Rela |
| 0 | 561 | | basePath = $"{queryParameters.RelativeSearchBase},{basePath}"; |
| 0 | 562 | | } |
| | 563 | |
|
| 0 | 564 | | searchRequest = new SearchRequest(basePath, queryParameters.LDAPFilter, queryParameters.SearchScope, |
| 0 | 565 | | queryParameters.Attributes); |
| 0 | 566 | | searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); |
| 0 | 567 | | if (queryParameters.IncludeDeleted) { |
| 0 | 568 | | searchRequest.Controls.Add(new ShowDeletedControl()); |
| 0 | 569 | | } |
| | 570 | |
|
| 0 | 571 | | if (queryParameters.IncludeSecurityDescriptor) { |
| 0 | 572 | | searchRequest.Controls.Add(new SecurityDescriptorFlagControl { |
| 0 | 573 | | SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner |
| 0 | 574 | | }); |
| 0 | 575 | | } |
| | 576 | |
|
| 0 | 577 | | return true; |
| 0 | 578 | | } |
| | 579 | |
|
| 0 | 580 | | private bool CallDsGetDcName(string domainName, out NetAPIStructs.DomainControllerInfo? info) { |
| 0 | 581 | | if (DCInfoCache.TryGetValue(domainName.ToUpper().Trim(), out info)) return info != null; |
| | 582 | |
|
| 0 | 583 | | var apiResult = _nativeMethods.CallDsGetDcName(null, domainName, |
| 0 | 584 | | (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY | |
| 0 | 585 | | NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME | |
| 0 | 586 | | NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED)); |
| | 587 | |
|
| 0 | 588 | | if (apiResult.IsFailed) { |
| 0 | 589 | | DCInfoCache.TryAdd(domainName.ToUpper().Trim(), null); |
| 0 | 590 | | return false; |
| | 591 | | } |
| | 592 | |
|
| 0 | 593 | | info = apiResult.Value; |
| 0 | 594 | | return true; |
| 0 | 595 | | } |
| | 596 | |
|
| 0 | 597 | | public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetConnectionAsync() |
| 0 | 598 | | if (!_connections.TryTake(out var connectionWrapper)) { |
| 0 | 599 | | var (success, connection, message) = await CreateNewConnection(); |
| 0 | 600 | | if (!success) { |
| 0 | 601 | | return (false, null, message); |
| | 602 | | } |
| | 603 | |
|
| 0 | 604 | | connectionWrapper = connection; |
| 0 | 605 | | } |
| | 606 | |
|
| 0 | 607 | | return (true, connectionWrapper, null); |
| 0 | 608 | | } |
| | 609 | |
|
| | 610 | | public (bool Success, LdapConnectionWrapper connectionWrapper, string Message) |
| 0 | 611 | | GetConnectionForSpecificServerAsync(string server, bool globalCatalog) { |
| 0 | 612 | | return CreateNewConnectionForServer(server, globalCatalog); |
| 0 | 613 | | } |
| | 614 | |
|
| 1 | 615 | | public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetGlobalCatalogConne |
| 2 | 616 | | if (!_globalCatalogConnection.TryTake(out var connectionWrapper)) { |
| 1 | 617 | | var (success, connection, message) = await CreateNewConnection(true); |
| 2 | 618 | | if (!success) { |
| | 619 | | //If we didn't get a connection, immediately release the semaphore so we don't have hanging ones |
| 1 | 620 | | return (false, null, message); |
| | 621 | | } |
| | 622 | |
|
| 0 | 623 | | connectionWrapper = connection; |
| 0 | 624 | | } |
| | 625 | |
|
| 0 | 626 | | return (true, connectionWrapper, null); |
| 1 | 627 | | } |
| | 628 | |
|
| 0 | 629 | | public void ReleaseConnection(LdapConnectionWrapper connectionWrapper, bool connectionFaulted = false) { |
| 0 | 630 | | if (!connectionFaulted) { |
| 0 | 631 | | if (connectionWrapper.GlobalCatalog) { |
| 0 | 632 | | _globalCatalogConnection.Add(connectionWrapper); |
| 0 | 633 | | } |
| 0 | 634 | | else { |
| 0 | 635 | | _connections.Add(connectionWrapper); |
| 0 | 636 | | } |
| 0 | 637 | | } |
| 0 | 638 | | else { |
| 0 | 639 | | connectionWrapper.Connection.Dispose(); |
| 0 | 640 | | } |
| 0 | 641 | | } |
| | 642 | |
|
| 0 | 643 | | public void Dispose() { |
| 0 | 644 | | while (_connections.TryTake(out var wrapper)) { |
| 0 | 645 | | wrapper.Connection.Dispose(); |
| 0 | 646 | | } |
| 0 | 647 | | } |
| | 648 | |
|
| 1 | 649 | | private async Task<(bool Success, LdapConnectionWrapper Connection, string Message)> CreateNewConnection(bool gl |
| 1 | 650 | | try { |
| 1 | 651 | | if (!string.IsNullOrWhiteSpace(_ldapConfig.Server)) { |
| 0 | 652 | | return CreateNewConnectionForServer(_ldapConfig.Server, globalCatalog); |
| | 653 | | } |
| | 654 | |
|
| 1 | 655 | | if (CreateLdapConnection(_identifier.ToUpper().Trim(), globalCatalog, out var connectionWrapper)) { |
| 0 | 656 | | _log.LogDebug("Successfully created ldap connection for domain: {Domain} using strategy 1. SSL: {SSl |
| 0 | 657 | | return (true, connectionWrapper, ""); |
| | 658 | | } |
| | 659 | |
|
| | 660 | | string tempDomainName; |
| | 661 | |
|
| 1 | 662 | | var dsGetDcNameResult = _nativeMethods.CallDsGetDcName(null, _identifier, |
| 1 | 663 | | (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY | |
| 1 | 664 | | NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME | |
| 1 | 665 | | NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED)); |
| 1 | 666 | | if (dsGetDcNameResult.IsSuccess) { |
| 0 | 667 | | tempDomainName = dsGetDcNameResult.Value.DomainName; |
| | 668 | |
|
| 0 | 669 | | if (!tempDomainName.Equals(_identifier, StringComparison.OrdinalIgnoreCase) && |
| 0 | 670 | | CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) { |
| 0 | 671 | | _log.LogDebug( |
| 0 | 672 | | "Successfully created ldap connection for domain: {Domain} using strategy 2 with name {NewNa |
| 0 | 673 | | _identifier, tempDomainName); |
| 0 | 674 | | return (true, connectionWrapper, ""); |
| | 675 | | } |
| | 676 | |
|
| 0 | 677 | | var server = dsGetDcNameResult.Value.DomainControllerName.TrimStart('\\'); |
| | 678 | |
|
| 0 | 679 | | var result = |
| 0 | 680 | | await CreateLDAPConnectionWithPortCheck(server, globalCatalog); |
| 0 | 681 | | if (result.success) { |
| 0 | 682 | | _log.LogDebug( |
| 0 | 683 | | "Successfully created ldap connection for domain: {Domain} using strategy 3 to server {Serve |
| 0 | 684 | | _identifier, server); |
| 0 | 685 | | return (true, result.connection, ""); |
| | 686 | | } |
| 0 | 687 | | } |
| | 688 | |
|
| 2 | 689 | | if (!LdapUtils.GetDomain(_identifier, _ldapConfig, out var domainObject) || domainObject.Name == null) { |
| | 690 | | //If we don't get a result here, we effectively have no other ways to resolve this domain, so we'll |
| 1 | 691 | | _log.LogDebug( |
| 1 | 692 | | "Could not get domain object from GetDomain, unable to create ldap connection for domain {Domain |
| 1 | 693 | | _identifier); |
| 1 | 694 | | return (false, null, "Unable to get domain object for further strategies"); |
| | 695 | | } |
| 0 | 696 | | tempDomainName = domainObject.Name.ToUpper().Trim(); |
| | 697 | |
|
| 0 | 698 | | if (!tempDomainName.Equals(_identifier, StringComparison.OrdinalIgnoreCase) && |
| 0 | 699 | | CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) { |
| 0 | 700 | | _log.LogDebug( |
| 0 | 701 | | "Successfully created ldap connection for domain: {Domain} using strategy 4 with name {NewName}" |
| 0 | 702 | | _identifier, tempDomainName); |
| 0 | 703 | | return (true, connectionWrapper, ""); |
| | 704 | | } |
| | 705 | |
|
| 0 | 706 | | var primaryDomainController = domainObject.PdcRoleOwner.Name; |
| 0 | 707 | | var portConnectionResult = |
| 0 | 708 | | await CreateLDAPConnectionWithPortCheck(primaryDomainController, globalCatalog); |
| 0 | 709 | | if (portConnectionResult.success) { |
| 0 | 710 | | _log.LogDebug( |
| 0 | 711 | | "Successfully created ldap connection for domain: {Domain} using strategy 5 with to pdc {Server} |
| 0 | 712 | | _identifier, primaryDomainController); |
| 0 | 713 | | return (true, portConnectionResult.connection, ""); |
| | 714 | | } |
| | 715 | |
|
| 0 | 716 | | foreach (DomainController dc in domainObject.DomainControllers) { |
| 0 | 717 | | portConnectionResult = |
| 0 | 718 | | await CreateLDAPConnectionWithPortCheck(dc.Name, globalCatalog); |
| 0 | 719 | | if (portConnectionResult.success) { |
| 0 | 720 | | _log.LogDebug( |
| 0 | 721 | | "Successfully created ldap connection for domain: {Domain} using strategy 6 with to pdc {Ser |
| 0 | 722 | | _identifier, primaryDomainController); |
| 0 | 723 | | return (true, portConnectionResult.connection, ""); |
| | 724 | | } |
| 0 | 725 | | } |
| 0 | 726 | | } catch (Exception e) { |
| 0 | 727 | | _log.LogInformation(e, "We will not be able to connect to domain {Domain} by any strategy, leaving it.", |
| 0 | 728 | | } |
| | 729 | |
|
| 0 | 730 | | return (false, null, "All attempted connections failed"); |
| 1 | 731 | | } |
| | 732 | |
|
| 0 | 733 | | private (bool Success, LdapConnectionWrapper Connection, string Message ) CreateNewConnectionForServer(string id |
| 0 | 734 | | if (CreateLdapConnection(identifier, globalCatalog, out var serverConnection)) { |
| 0 | 735 | | return (true, serverConnection, ""); |
| | 736 | | } |
| | 737 | |
|
| 0 | 738 | | return (false, null, $"Failed to create ldap connection for {identifier}"); |
| 0 | 739 | | } |
| | 740 | |
|
| | 741 | | private bool CreateLdapConnection(string target, bool globalCatalog, |
| 1 | 742 | | out LdapConnectionWrapper connection) { |
| 1 | 743 | | var baseConnection = CreateBaseConnection(target, true, globalCatalog); |
| 1 | 744 | | if (TestLdapConnection(baseConnection, out var result)) { |
| 0 | 745 | | connection = new LdapConnectionWrapper(baseConnection, result.SearchResultEntry, globalCatalog, _poolIde |
| 0 | 746 | | return true; |
| | 747 | | } |
| | 748 | |
|
| 1 | 749 | | try { |
| 1 | 750 | | baseConnection.Dispose(); |
| 1 | 751 | | } |
| 0 | 752 | | catch { |
| | 753 | | //this is just in case |
| 0 | 754 | | } |
| | 755 | |
|
| 1 | 756 | | if (_ldapConfig.ForceSSL) { |
| 0 | 757 | | connection = null; |
| 0 | 758 | | return false; |
| | 759 | | } |
| | 760 | |
|
| 1 | 761 | | baseConnection = CreateBaseConnection(target, false, globalCatalog); |
| 1 | 762 | | if (TestLdapConnection(baseConnection, out result)) { |
| 0 | 763 | | connection = new LdapConnectionWrapper(baseConnection, result.SearchResultEntry, globalCatalog, _poolIde |
| 0 | 764 | | return true; |
| | 765 | | } |
| | 766 | |
|
| 1 | 767 | | try { |
| 1 | 768 | | baseConnection.Dispose(); |
| 1 | 769 | | } |
| 0 | 770 | | catch { |
| | 771 | | //this is just in case |
| 0 | 772 | | } |
| | 773 | |
|
| 1 | 774 | | connection = null; |
| 1 | 775 | | return false; |
| 1 | 776 | | } |
| | 777 | |
|
| | 778 | | private LdapConnection CreateBaseConnection(string directoryIdentifier, bool ssl, |
| 2 | 779 | | bool globalCatalog) { |
| 2 | 780 | | _log.LogDebug("Creating connection for identifier {Identifier}", directoryIdentifier); |
| 2 | 781 | | var port = globalCatalog ? _ldapConfig.GetGCPort(ssl) : _ldapConfig.GetPort(ssl); |
| 2 | 782 | | var identifier = new LdapDirectoryIdentifier(directoryIdentifier, port, false, false); |
| 2 | 783 | | var connection = new LdapConnection(identifier) { Timeout = new TimeSpan(0, 0, 5, 0) }; |
| | 784 | |
|
| | 785 | | //These options are important! |
| 2 | 786 | | connection.SessionOptions.ProtocolVersion = 3; |
| | 787 | | //Referral chasing does not work with paged searches |
| 2 | 788 | | connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None; |
| 3 | 789 | | if (ssl) connection.SessionOptions.SecureSocketLayer = true; |
| | 790 | |
|
| 3 | 791 | | if (_ldapConfig.DisableSigning || ssl) { |
| 1 | 792 | | connection.SessionOptions.Signing = false; |
| 1 | 793 | | connection.SessionOptions.Sealing = false; |
| 1 | 794 | | } |
| 1 | 795 | | else { |
| 1 | 796 | | connection.SessionOptions.Signing = true; |
| 1 | 797 | | connection.SessionOptions.Sealing = true; |
| 1 | 798 | | } |
| | 799 | |
|
| 2 | 800 | | if (_ldapConfig.DisableCertVerification) |
| 0 | 801 | | connection.SessionOptions.VerifyServerCertificate = (_, _) => true; |
| | 802 | |
|
| 2 | 803 | | if (_ldapConfig.Username != null) { |
| 0 | 804 | | var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password); |
| 0 | 805 | | connection.Credential = cred; |
| 0 | 806 | | } |
| | 807 | |
|
| 2 | 808 | | connection.AuthType = _ldapConfig.AuthType; |
| | 809 | |
|
| 2 | 810 | | return connection; |
| 2 | 811 | | } |
| | 812 | |
|
| | 813 | | /// <summary> |
| | 814 | | /// Tests whether an LDAP connection is working |
| | 815 | | /// </summary> |
| | 816 | | /// <param name="connection">The ldap connection object to test</param> |
| | 817 | | /// <param name="testResult">The results fo the connection test</param> |
| | 818 | | /// <returns>True if connection was successful, false otherwise</returns> |
| | 819 | | /// <exception cref="LdapAuthenticationException">Something is wrong with the supplied credentials</exception> |
| | 820 | | /// <exception cref="NoLdapDataException"> |
| | 821 | | /// A connection "succeeded" but no data was returned. This can be related to |
| | 822 | | /// kerberos auth across trusts or just simply lack of permissions |
| | 823 | | /// </exception> |
| 2 | 824 | | private bool TestLdapConnection(LdapConnection connection, out LdapConnectionTestResult testResult) { |
| 2 | 825 | | testResult = new LdapConnectionTestResult(); |
| 2 | 826 | | try { |
| | 827 | | //Attempt an initial bind. If this fails, likely auth is invalid, or its not a valid target |
| 2 | 828 | | connection.Bind(); |
| 0 | 829 | | } |
| 4 | 830 | | catch (LdapException e) { |
| | 831 | | //TODO: Maybe look at this and find a better way? |
| 2 | 832 | | if (e.ErrorCode is (int)LdapErrorCodes.InvalidCredentials or (int)ResultCode.InappropriateAuthentication |
| 0 | 833 | | connection.Dispose(); |
| 0 | 834 | | throw new LdapAuthenticationException(e); |
| | 835 | | } |
| | 836 | |
|
| 2 | 837 | | testResult.Message = e.Message; |
| 2 | 838 | | testResult.ErrorCode = e.ErrorCode; |
| 2 | 839 | | return false; |
| | 840 | | } |
| 0 | 841 | | catch (Exception e) { |
| 0 | 842 | | testResult.Message = e.Message; |
| 0 | 843 | | return false; |
| | 844 | | } |
| | 845 | |
|
| | 846 | | SearchResponse response; |
| 0 | 847 | | try { |
| | 848 | | //Do an initial search request to get the rootDSE |
| | 849 | | //This ldap filter is equivalent to (objectclass=*) |
| 0 | 850 | | var searchRequest = CreateSearchRequest("", new LdapFilter().AddAllObjects().GetFilter(), |
| 0 | 851 | | SearchScope.Base, null); |
| | 852 | |
|
| 0 | 853 | | response = (SearchResponse)connection.SendRequest(searchRequest); |
| 0 | 854 | | } |
| 0 | 855 | | catch (LdapException e) { |
| | 856 | | /* |
| | 857 | | * If we can't send the initial search request, its unlikely any other search requests will work so we w |
| | 858 | | */ |
| 0 | 859 | | testResult.Message = e.Message; |
| 0 | 860 | | testResult.ErrorCode = e.ErrorCode; |
| 0 | 861 | | return false; |
| | 862 | | } |
| | 863 | |
|
| 0 | 864 | | if (response?.Entries == null || response.Entries.Count == 0) { |
| | 865 | | /* |
| | 866 | | * This can happen for one of two reasons, either we dont have permission to query AD or we're authentic |
| | 867 | | * across external trusts with kerberos authentication without Forest Search Order properly configured. |
| | 868 | | * Either way, this connection isn't useful for us because we're not going to get data, so return false |
| | 869 | | */ |
| | 870 | |
|
| 0 | 871 | | connection.Dispose(); |
| 0 | 872 | | throw new NoLdapDataException(); |
| | 873 | | } |
| | 874 | |
|
| 0 | 875 | | testResult.SearchResultEntry = new SearchResultEntryWrapper(response.Entries[0]); |
| 0 | 876 | | testResult.Message = ""; |
| 0 | 877 | | return true; |
| 2 | 878 | | } |
| | 879 | |
|
| | 880 | | private class LdapConnectionTestResult { |
| 2 | 881 | | public string Message { get; set; } |
| 0 | 882 | | public IDirectoryObject SearchResultEntry { get; set; } |
| 2 | 883 | | public int ErrorCode { get; set; } |
| | 884 | | } |
| | 885 | |
|
| | 886 | | private async Task<(bool success, LdapConnectionWrapper connection)> CreateLDAPConnectionWithPortCheck( |
| 0 | 887 | | string target, bool globalCatalog) { |
| 0 | 888 | | if (globalCatalog) { |
| 0 | 889 | | if (await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(true)) || (!_ldapConfig.ForceSSL && |
| 0 | 890 | | await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(false)))) |
| 0 | 891 | | return (CreateLdapConnection(target, true, out var connection), connection); |
| 0 | 892 | | } |
| 0 | 893 | | else { |
| 0 | 894 | | if (await _portScanner.CheckPort(target, _ldapConfig.GetPort(true)) || (!_ldapConfig.ForceSSL && |
| 0 | 895 | | await _portScanner.CheckPort(target, _ldapConfig.GetPort(false)))) |
| 0 | 896 | | return (CreateLdapConnection(target, true, out var connection), connection); |
| 0 | 897 | | } |
| | 898 | |
|
| 0 | 899 | | return (false, null); |
| 0 | 900 | | } |
| | 901 | |
|
| | 902 | | private SearchRequest CreateSearchRequest(string distinguishedName, string ldapFilter, |
| | 903 | | SearchScope searchScope, |
| 0 | 904 | | string[] attributes) { |
| 0 | 905 | | var searchRequest = new SearchRequest(distinguishedName, ldapFilter, |
| 0 | 906 | | searchScope, attributes); |
| 0 | 907 | | searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); |
| 0 | 908 | | return searchRequest; |
| 0 | 909 | | } |
| | 910 | | } |
| | 911 | | } |